diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f4854434..d45f79e85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,7 @@ set(FW_SOURCES lcd.cpp Marlin_main.cpp MarlinSerial.cpp + meatpack.cpp menu.cpp mesh_bed_calibration.cpp mesh_bed_leveling.cpp @@ -177,6 +178,7 @@ set(FW_SOURCES spi.c SpoolJoin.cpp stepper.cpp + strtod.c swi2c.c Tcodes.cpp temperature.cpp diff --git a/Firmware/Configuration.h b/Firmware/Configuration.h index 721ace4ae..f308413cd 100644 --- a/Firmware/Configuration.h +++ b/Firmware/Configuration.h @@ -94,6 +94,9 @@ extern const char _sPrinterMmuName[] PROGMEM; // This determines the communication speed of the printer #define BAUDRATE 115200 +// Enable g-code compression (see https://github.com/scottmudge/OctoPrint-MeatPack) +#define ENABLE_MEATPACK + // This enables the serial port associated to the Bluetooth interface //#define BTENABLED // Enable BT interface on AT90USB devices diff --git a/Firmware/cmdqueue.cpp b/Firmware/cmdqueue.cpp index 76962dffc..32f07275e 100755 --- a/Firmware/cmdqueue.cpp +++ b/Firmware/cmdqueue.cpp @@ -4,6 +4,7 @@ #include "cardreader.h" #include "ultralcd.h" #include "Prusa_farm.h" +#include "meatpack.h" // Reserve BUFSIZE lines of length MAX_CMD_SIZE plus CMDBUFFER_RESERVE_FRONT. char cmdbuffer[BUFSIZE * (MAX_CMD_SIZE + 1) + CMDBUFFER_RESERVE_FRONT]; @@ -365,7 +366,20 @@ void get_command() // start of serial line processing loop while (((MYSERIAL.available() > 0 && !saved_printing) || (MYSERIAL.available() > 0 && isPrintPaused)) && !cmdqueue_serial_disabled) { //is print is saved (crash detection or filament detection), dont process data from serial line +#ifdef ENABLE_MEATPACK + // MeatPack Changes + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + const int rec = MYSERIAL.read(); + if (rec < 0) continue; + + mp_handle_rx_char((uint8_t)rec); + char c_res[2] = {0, 0}; + const uint8_t char_count = mp_get_result_char(c_res); + // Note -- Paired bracket in preproc switch below + for (uint8_t i = 0; i < char_count; ++i) { char serial_char = c_res[i]; +#else char serial_char = MYSERIAL.read(); +#endif serialTimeoutTimer.start(); @@ -526,6 +540,9 @@ void get_command() if(serial_char == ';') comment_mode = true; if(!comment_mode) cmdbuffer[bufindw+CMDHDRSIZE+serial_count++] = serial_char; } + #ifdef ENABLE_MEATPACK + } + #endif } // end of serial line processing loop if (serial_count > 0 && serialTimeoutTimer.expired(farm_mode ? 800 : 2000)) { diff --git a/Firmware/cmdqueue.h b/Firmware/cmdqueue.h index 2d16b8fb1..bf512e09f 100644 --- a/Firmware/cmdqueue.h +++ b/Firmware/cmdqueue.h @@ -71,10 +71,19 @@ extern void repeatcommand_front(); extern void get_command(); extern uint16_t cmdqueue_calc_sd_length(); + +#if defined(__cplusplus) +extern "C" { +#endif + extern double strtod_noE(const char* nptr, char** endptr); +#if defined(__cplusplus) +} +#endif + // Return True if a character was found static inline bool code_seen(char code) { return (strchr_pointer = strchr(CMDBUFFER_CURRENT_STRING, code)) != NULL; } static inline bool code_seen_P(const char *code_PROGMEM) { return (strchr_pointer = strstr_P(CMDBUFFER_CURRENT_STRING, code_PROGMEM)) != NULL; } -static inline float code_value() { return strtod(strchr_pointer+1, NULL);} +static inline float code_value() { return strtod_noE(strchr_pointer+1, NULL);} static inline long code_value_long() { return strtol(strchr_pointer+1, NULL, 10); } static inline int16_t code_value_short() { return int16_t(strtol(strchr_pointer+1, NULL, 10)); }; static inline uint8_t code_value_uint8() { return uint8_t(strtol(strchr_pointer+1, NULL, 10)); }; diff --git a/Firmware/meatpack.cpp b/Firmware/meatpack.cpp new file mode 100644 index 000000000..b68a2ca71 --- /dev/null +++ b/Firmware/meatpack.cpp @@ -0,0 +1,379 @@ +/* +* 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 diff --git a/Firmware/meatpack.h b/Firmware/meatpack.h new file mode 100644 index 000000000..4c949abf2 --- /dev/null +++ b/Firmware/meatpack.h @@ -0,0 +1,70 @@ +/* +* MeatPack G-Code Compression +* +* Algorithm & Implementation: Scott Mudge - mail@scottmudge.com +* Date: Dec. 2020 +* +* Specifically optimized for 3D printing G-Code, this is a zero-cost data compression method +* which packs ~180-190% more data into the same amount of bytes going to the CNC controller. +* As a majority of G-Code can be represented by a restricted alphabet, I performed histogram +* analysis on a wide variety of 3D printing gcode samples, and found ~93% of all gcode could +* be represented by the same 15-character alphabet. +* +* This allowed me to design a system of packing 2 8-bit characters into a single byte, assuming +* they fall within this limited 15-character alphabet. Using a 4-bit lookup table, these 8-bit +* characters can be represented by a 4-bit index. +* +* Combined with some logic to allow commingling of full-width characters outside of this 15- +* character alphabet (at the cost of an extra 8-bits per full-width character), and by stripping +* out unnecessary comments, the end result is gcode which is roughly half the original size. +* +* Why did I do this? I noticed micro-stuttering and other data-bottleneck issues while printing +* objects with high curvature, especially at high speeds. There is also the issue of the limited +* baud rate provided by Prusa's Atmega2560-based boards, over the USB serial connection. So soft- +* ware like OctoPrint would also suffer this same micro-stuttering and poor print quality issue. +* +*/ +#include +#include "Configuration.h" + +#ifndef MEATPACK_H_ +#define MEATPACK_H_ + +#ifdef ENABLE_MEATPACK + +#define MeatPack_SecondNotPacked 0b11110000 +#define MeatPack_FirstNotPacked 0b00001111 + +// These are commands sent to MeatPack to control its behavior. +// They are sent by first sending 2x MeatPack_CommandByte (0xFF) in sequence, +// followed by one of the command bytes below. +// Provided that 0xFF is an exceedingly rare character that is virtually never +// present in g-code naturally, it is safe to assume 2 in sequence should never +// happen naturally, and so it is used as a signal here. +// +// 0xFF *IS* used in "packed" g-code (used to denote that the next 2 characters are +// full-width), however 2 in a row will never occur, as the next 2 bytes will always +// some non-0xFF character. +enum MeatPack_Command { + MPCommand_None = 0U, + // MPCommand_TogglePacking = 253U, -- Unused, byte 253 can be re-used later. + MPCommand_EnablePacking = 251U, + MPCommand_DisablePacking = 250U, + MPCommand_ResetAll = 249U, + MPCommand_QueryConfig = 248U, + MPCommand_EnableNoSpaces = 247U, + MPCommand_DisableNoSpaces = 246U +}; + +// Pass in a character rx'd by SD card or serial. Automatically parses command/ctrl sequences, +// and will control state internally. +extern void mp_handle_rx_char(const uint8_t c); + +// After passing in rx'd char using above method, call this to get characters out. Can return +// from 0 to 2 characters at once. +// @param out [in] Output pointer for unpacked/processed data. +// @return Number of characters returned. Range from 0 to 2. +extern uint8_t mp_get_result_char(char* const __restrict out); +#endif + +#endif // MEATPACK_H_ diff --git a/Firmware/strtod.c b/Firmware/strtod.c new file mode 100644 index 000000000..b039f9dbd --- /dev/null +++ b/Firmware/strtod.c @@ -0,0 +1,178 @@ +// Note -- This is a modified stdtod() method, to prevent the catching of uppercase "E", used in 3D printing g-code. + + +#if !defined(__AVR_TINY__) + +#include +#include +#include +#include +#include /* INFINITY, NAN */ +#include + +/* Only GCC 4.2 calls the library function to convert an unsigned long + to float. Other GCC-es (including 4.3) use a signed long to float + conversion along with a large inline code to correct the result. */ +extern double __floatunsisf(unsigned long); + +PROGMEM static const float pwr_p10[6] = { + 1e+1, 1e+2, 1e+4, 1e+8, 1e+16, 1e+32 +}; +PROGMEM static const float pwr_m10[6] = { + 1e-1, 1e-2, 1e-4, 1e-8, 1e-16, 1e-32 +}; + +/* PSTR() is not used to save 1 byte per string: '\0' at the tail. */ +PROGMEM static const char pstr_inf[] = { 'I','N','F' }; +PROGMEM static const char pstr_inity[] = { 'I','N','I','T','Y' }; +PROGMEM static const char pstr_nan[] = { 'N','A','N' }; + + +double strtod_noE(const char* nptr, char** endptr) +{ + union { + unsigned long u32; + float flt; + } x; + unsigned char c; + int exp; + + unsigned char flag; +#define FL_MINUS 0x01 /* number is negative */ +#define FL_ANY 0x02 /* any digit was readed */ +#define FL_OVFL 0x04 /* overflow was */ +#define FL_DOT 0x08 /* decimal '.' was */ +#define FL_MEXP 0x10 /* exponent 'e' is neg. */ + + if (endptr) + *endptr = (char*)nptr; + + do { + c = *nptr++; + } while (isspace(c)); + + flag = 0; + if (c == '-') { + flag = FL_MINUS; + c = *nptr++; + } + else if (c == '+') { + c = *nptr++; + } + + if (!strncasecmp_P(nptr - 1, pstr_inf, 3)) { + nptr += 2; + if (!strncasecmp_P(nptr, pstr_inity, 5)) + nptr += 5; + if (endptr) + *endptr = (char*)nptr; + return flag & FL_MINUS ? -INFINITY : +INFINITY; + } + + /* NAN() construction is not realised. + Length would be 3 characters only. */ + if (!strncasecmp_P(nptr - 1, pstr_nan, 3)) { + if (endptr) + *endptr = (char*)nptr + 2; + return NAN; + } + + x.u32 = 0; + exp = 0; + while (1) { + + c -= '0'; + + if (c <= 9) { + flag |= FL_ANY; + if (flag & FL_OVFL) { + if (!(flag & FL_DOT)) + exp += 1; + } + else { + if (flag & FL_DOT) + exp -= 1; + /* x.u32 = x.u32 * 10 + c */ + x.u32 = (((x.u32 << 2) + x.u32) << 1) + c; + if (x.u32 >= (ULONG_MAX - 9) / 10) + flag |= FL_OVFL; + } + + } + else if (c == (('.' - '0') & 0xff) && !(flag & FL_DOT)) { + flag |= FL_DOT; + } + else { + break; + } + c = *nptr++; + } + + // Check for exponent "E", but disable capital E + if (c == (('e' - '0') & 0xff) /*|| c == (('E' - '0') & 0xff)*/) + { + int i; + c = *nptr++; + i = 2; + if (c == '-') { + flag |= FL_MEXP; + c = *nptr++; + } + else if (c == '+') { + c = *nptr++; + } + else { + i = 1; + } + c -= '0'; + if (c > 9) { + nptr -= i; + } + else { + i = 0; + do { + if (i < 3200) + i = (((i << 2) + i) << 1) + c; /* i = 10*i + c */ + c = *nptr++ - '0'; + } while (c <= 9); + if (flag & FL_MEXP) + i = -i; + exp += i; + } + } + + if ((flag & FL_ANY) && endptr) + *endptr = (char*)nptr - 1; + + x.flt = __floatunsisf(x.u32); /* manually */ + if ((flag & FL_MINUS) && (flag & FL_ANY)) + x.flt = -x.flt; + + if (x.flt != 0) { + int pwr; + if (exp < 0) { + nptr = (void*)(pwr_m10 + 5); + exp = -exp; + } + else { + nptr = (void*)(pwr_p10 + 5); + } + for (pwr = 32; pwr; pwr >>= 1) { + for (; exp >= pwr; exp -= pwr) { + union { + unsigned long u32; + float flt; + } y; + y.u32 = pgm_read_dword((float*)nptr); + x.flt *= y.flt; + } + nptr -= sizeof(float); + } + if (!isfinite(x.flt) || x.flt == 0) + errno = ERANGE; + } + + return x.flt; +} + +#endif