/* * 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