Prusa-Firmware/Firmware/meatpack.cpp

380 lines
10 KiB
C++

/*
* MeatPack G-Code Compression
*
* Algorithm & Implementation: Scott Mudge - mail@scottmudge.com
* Date: Dec. 2020
*/
#include "meatpack.h"
#ifdef ENABLE_MEATPACK
#include "language.h"
#include "Marlin.h"
//#define MP_DEBUG
// Utility definitions
#define MeatPack_CommandByte 0b11111111
#define MeatPack_NextPackedFirst 0b00000001
#define MeatPack_NextPackedSecond 0b00000010
#define MeatPack_SpaceCharIdx 11U
#define MeatPack_SpaceCharReplace 'E'
#define MeatPack_ProtocolVersion "PV01"
/*
Character Frequencies from ~30 MB of comment-stripped gcode:
'1' -> 4451136
'0' -> 4253577
' ' -> 3053297
'.' -> 3035310
'2' -> 1523296
'8' -> 1366812
'4' -> 1353273
'9' -> 1352147
'3' -> 1262929
'5' -> 1189871
'6' -> 1127900
'7' -> 1112908
'\n' -> 1087683
'G' -> 1075806
'X' -> 975742
'E' -> 965275
'Y' -> 965274
'F' -> 99416
'-' -> 90242
'Z' -> 34109
'M' -> 11879
'S' -> 9910
If spaces are omitted, we add 'E'
*/
// Note:
// I've tried both a switch/case method and a lookup table. The disassembly is exactly the same after compilation, byte-to-byte.
// Thus, performance is identical.
#define USE_LOOKUP_TABLE
// State variables
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum MeatPack_ConfigStateFlags {
MPConfig_None = 0,
MPConfig_Active = (1 << 0),
MPConfig_NoSpaces = (1 << 1)
};
uint8_t mp_config = MPConfig_None; // Configuration state
uint8_t mp_cmd_active = 0; // Is a command is pending
uint8_t mp_char_buf = 0; // Buffers a character if dealing with out-of-sequence pairs
uint8_t mp_cmd_count = 0; // Counts how many command bytes are received (need 2)
uint8_t mp_full_char_queue = 0; // Counts how many full-width characters are to be received
uint8_t mp_char_out_buf[2]; // Output buffer for caching up to 2 characters
uint8_t mp_char_out_count = 0; // Stores number of characters to be read out.
#ifdef USE_LOOKUP_TABLE
// The 15 most-common characters used in G-code, ~90-95% of all g-code uses these characters
// NOT storing this with PROGMEM, given how frequently this table will be accessed.
uint8_t MeatPackLookupTbl[16] = {
'0', // 0000
'1', // 0001
'2', // 0010
'3', // 0011
'4', // 0100
'5', // 0101
'6', // 0110
'7', // 0111
'8', // 1000
'9', // 1001
'.', // 1010
' ', // 1011
'\n', // 1100
'G', // 1101
'X', // 1110
'\0' // never used, 0b1111 is used to indicate next 8-bits is a full character
};
#else
inline uint8_t get_char(const uint8_t in) {
switch (in) {
case 0b0000:
return '0';
break;
case 0b0001:
return '1';
break;
case 0b0010:
return '2';
break;
case 0b0011:
return '3';
break;
case 0b0100:
return '4';
break;
case 0b0101:
return '5';
break;
case 0b0110:
return '6';
break;
case 0b0111:
return '7';
break;
case 0b1000:
return '8';
break;
case 0b1001:
return '9';
break;
case 0b1010:
return '.';
break;
case 0b1011:
return (mp_config & MPConfig_NoSpaces) ? MeatPack_SpaceCharReplace : ' ';
break;
case 0b1100:
return '\n';
break;
case 0b1101:
return 'G';
break;
case 0b1110:
return 'X';
break;
}
return 0;
}
#endif
// #DEBUGGING
#ifdef MP_DEBUG
uint32_t mp_chars_decoded = 0;
#endif
void FORCE_INLINE mp_handle_output_char(const uint8_t c) {
mp_char_out_buf[mp_char_out_count++] = c;
#ifdef MP_DEBUG
if (mp_chars_decoded < 4096) {
++mp_chars_decoded;
SERIAL_ECHOPGM("RB: ");
MYSERIAL.print((char)c);
SERIAL_ECHOLNPGM("");
}
#endif
}
// Storing
// packed = ((low & 0xF) << 4) | (high & 0xF);
// Unpacking
// low = (packed >> 4) & 0xF;
// high = (packed & 0xF);
//==========================================================================
uint8_t FORCE_INLINE mp_unpack_chars(const uint8_t pk, uint8_t* __restrict const chars_out) {
uint8_t out = 0;
#ifdef USE_LOOKUP_TABLE
// If lower 4 bytes is 0b1111, the higher 4 are unused, and next char is full.
if ((pk & MeatPack_FirstNotPacked) == MeatPack_FirstNotPacked) out |= MeatPack_NextPackedFirst;
else chars_out[0] = MeatPackLookupTbl[(pk & 0xF)]; // Assign lower char
// Check if upper 4 bytes is 0b1111... if so, we don't need the second char.
if ((pk & MeatPack_SecondNotPacked) == MeatPack_SecondNotPacked) out |= MeatPack_NextPackedSecond;
else chars_out[1] = MeatPackLookupTbl[((pk >> 4) & 0xf)]; // Assign upper char
#else
// If lower 4 bytes is 0b1111, the higher 4 are unused, and next char is full.
if ((pk & MeatPack_FirstNotPacked) == MeatPack_FirstNotPacked) out |= MeatPack_NextPackedFirst;
else chars_out[0] = get_char(pk & 0xF); // Assign lower char
// Check if upper 4 bytes is 0b1111... if so, we don't need the second char.
if ((pk & MeatPack_SecondNotPacked) == MeatPack_SecondNotPacked) out |= MeatPack_NextPackedSecond;
else chars_out[1] = get_char((pk >> 4) & 0xf); // Assign upper char
#endif
return out;
}
//==============================================================================
void FORCE_INLINE mp_reset_state() {
mp_char_out_count = 0;
mp_cmd_active = MPCommand_None;
mp_config = MPConfig_None;
mp_char_buf = 0;
mp_cmd_count = 0;
mp_cmd_active = 0;
mp_full_char_queue = 0;
#ifdef MP_DEBUG
mp_chars_decoded = 0;
SERIAL_ECHOLNPGM("MP Reset");
#endif
}
//==========================================================================
void FORCE_INLINE mp_handle_rx_char_inner(const uint8_t c) {
// Packing enabled, handle character and re-arrange them appropriately.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if (mp_config & MPConfig_Active) {
if (mp_full_char_queue > 0) {
mp_handle_output_char(c);
if (mp_char_buf > 0) {
mp_handle_output_char(mp_char_buf);
mp_char_buf = 0;
}
--mp_full_char_queue;
}
else {
uint8_t buf[2] = { 0,0 };
const uint8_t res = mp_unpack_chars(c, buf);
if (res & MeatPack_NextPackedFirst) {
++mp_full_char_queue;
if (res & MeatPack_NextPackedSecond) ++mp_full_char_queue;
else mp_char_buf = buf[1];
}
else {
mp_handle_output_char(buf[0]);
if (buf[0] != '\n') {
if (res & MeatPack_NextPackedSecond) ++mp_full_char_queue;
else mp_handle_output_char(buf[1]);
}
}
}
}
else // Packing not enabled, just copy character to output
mp_handle_output_char(c);
}
//==========================================================================
void FORCE_INLINE mp_echo_config_state() {
SERIAL_ECHOPGM(" [MP] "); // Add space at idx 0 just in case first character is dropped due to timing/sync issues.
// NOTE: if any configuration vars are added below, the outgoing sync text for host plugin
// should not contain the "PV' substring, as this is used to indicate protocol version
SERIAL_ECHOPGM(MeatPack_ProtocolVersion);
// Echo current state
if (mp_config & MPConfig_Active)
SERIAL_ECHOPGM(" ON");
else
SERIAL_ECHOPGM(" OFF");
if (mp_config & MPConfig_NoSpaces)
SERIAL_ECHOPGM(" NSP"); // [N]o [SP]aces
else
SERIAL_ECHOPGM(" ESP"); // [E]nabled [SP]aces
SERIAL_ECHOLNPGM("");
// Validate config vars
#ifdef USE_LOOKUP_TABLE
if (mp_config & MPConfig_NoSpaces)
MeatPackLookupTbl[MeatPack_SpaceCharIdx] = (uint8_t)(MeatPack_SpaceCharReplace);
else
MeatPackLookupTbl[MeatPack_SpaceCharIdx] = ' ';
#endif
}
//==========================================================================
void FORCE_INLINE mp_handle_cmd(const MeatPack_Command c) {
switch (c) {
case MPCommand_EnablePacking: {
mp_config |= MPConfig_Active;
#ifdef MP_DEBUG
SERIAL_ECHOLNPGM("[MPDBG] ENABL REC");
#endif
} break;
case MPCommand_DisablePacking: {
mp_config &= ~(MPConfig_Active);
#ifdef MP_DEBUG
SERIAL_ECHOLNPGM("[MPDBG] DISBL REC");
#endif
} break;
case MPCommand_ResetAll: {
mp_reset_state();
#ifdef MP_DEBUG
SERIAL_ECHOLNPGM("[MPDBG] RESET REC");
#endif
} break;
case MPCommand_EnableNoSpaces: {
mp_config |= MPConfig_NoSpaces;
#ifdef MP_DEBUG
SERIAL_ECHOLNPGM("[MPDBG] ENABL NSP");
#endif
} break;
case MPCommand_DisableNoSpaces: {
mp_config &= ~(MPConfig_NoSpaces);
#ifdef MP_DEBUG
SERIAL_ECHOLNPGM("[MPDBG] DISBL NSP");
#endif
} break;
default: {
#ifdef MP_DEBUG
SERIAL_ECHOLN("[MPDBG] UNK CMD REC");
#endif
}
case MPCommand_QueryConfig:
break;
}
mp_echo_config_state();
}
//==========================================================================
void mp_handle_rx_char(const uint8_t c) {
// Check for commit complete
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if (c == (uint8_t)(MeatPack_CommandByte)) {
if (mp_cmd_count > 0) {
mp_cmd_active = 1;
mp_cmd_count = 0;
}
else
++mp_cmd_count;
return;
}
if (mp_cmd_active > 0) {
mp_handle_cmd((MeatPack_Command)c);
mp_cmd_active = 0;
return;
}
if (mp_cmd_count > 0) {
mp_handle_rx_char_inner((uint8_t)(MeatPack_CommandByte));
mp_cmd_count = 0;
}
mp_handle_rx_char_inner(c);
}
//==========================================================================
uint8_t mp_get_result_char(char* const __restrict out) {
if (mp_char_out_count > 0) {
const uint8_t res = mp_char_out_count;
for (uint8_t i = 0; i < mp_char_out_count; ++i)
out[i] = (char)mp_char_out_buf[i];
mp_char_out_count = 0;
return res;
}
return 0;
}
//==============================================================================
void mp_trigger_cmd(const MeatPack_Command cmd)
{
mp_handle_cmd(cmd);
}
#endif