220 lines
8.2 KiB
C++
220 lines
8.2 KiB
C++
/**
|
|
* Marlin 3D Printer Firmware
|
|
* Copyright (c) 2020 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
|
|
*
|
|
* Based on Sprinter and grbl.
|
|
* Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* MeatPack G-code Compression
|
|
*
|
|
* Algorithm & Implementation: Scott Mudge - mail@scottmudge.com
|
|
* Date: Dec. 2020
|
|
*
|
|
* Character Frequencies from ~30 MB of comment-stripped gcode:
|
|
* '1' -> 4451136 '4' -> 1353273 '\n' -> 1087683 '-' -> 90242
|
|
* '0' -> 4253577 '9' -> 1352147 'G' -> 1075806 'Z' -> 34109
|
|
* ' ' -> 3053297 '3' -> 1262929 'X' -> 975742 'M' -> 11879
|
|
* '.' -> 3035310 '5' -> 1189871 'E' -> 965275 'S' -> 9910
|
|
* '2' -> 1523296 '6' -> 1127900 'Y' -> 965274
|
|
* '8' -> 1366812 '7' -> 1112908 'F' -> 99416
|
|
*
|
|
* When space is omitted the letter 'E' is used in its place
|
|
*/
|
|
|
|
#include "../inc/MarlinConfig.h"
|
|
|
|
#if HAS_MEATPACK
|
|
|
|
#include "meatpack.h"
|
|
|
|
#define MeatPack_ProtocolVersion "PV01"
|
|
//#define MP_DEBUG
|
|
|
|
#define DEBUG_OUT ENABLED(MP_DEBUG)
|
|
#include "../core/debug_out.h"
|
|
|
|
// The 15 most-common characters used in G-code, ~90-95% of all G-code uses these characters
|
|
// Stored in SRAM for performance.
|
|
uint8_t meatPackLookupTable[16] = {
|
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
'.', ' ', '\n', 'G', 'X',
|
|
'\0' // Unused. 0b1111 indicates a literal character
|
|
};
|
|
|
|
#if ENABLED(MP_DEBUG)
|
|
uint8_t chars_decoded = 0; // Log the first 64 bytes after each reset
|
|
#endif
|
|
|
|
void MeatPack::reset_state() {
|
|
state = 0;
|
|
cmd_is_next = false;
|
|
second_char = 0;
|
|
cmd_count = full_char_count = char_out_count = 0;
|
|
TERN_(MP_DEBUG, chars_decoded = 0);
|
|
}
|
|
|
|
/**
|
|
* Unpack one or two characters from a packed byte into a buffer.
|
|
* Return flags indicating whether any literal bytes follow.
|
|
*/
|
|
uint8_t MeatPack::unpack_chars(const uint8_t pk, uint8_t* __restrict const chars_out) {
|
|
uint8_t out = 0;
|
|
|
|
// If lower nybble is 1111, the higher nybble is unused, and next char is full.
|
|
if ((pk & kFirstNotPacked) == kFirstNotPacked)
|
|
out = kFirstCharIsLiteral;
|
|
else {
|
|
const uint8_t chr = pk & 0x0F;
|
|
chars_out[0] = meatPackLookupTable[chr]; // Set the first char
|
|
}
|
|
|
|
// Check if upper nybble is 1111... if so, we don't need the second char.
|
|
if ((pk & kSecondNotPacked) == kSecondNotPacked)
|
|
out |= kSecondCharIsLiteral;
|
|
else {
|
|
const uint8_t chr = (pk >> 4) & 0x0F;
|
|
chars_out[1] = meatPackLookupTable[chr]; // Set the second char
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Interpret a single (non-command) character
|
|
* according to the current MeatPack state.
|
|
*/
|
|
void MeatPack::handle_rx_char_inner(const uint8_t c) {
|
|
if (TEST(state, MPConfig_Bit_Active)) { // Is MeatPack active?
|
|
if (!full_char_count) { // No literal characters to fetch?
|
|
uint8_t buf[2] = { 0, 0 };
|
|
const uint8_t res = unpack_chars(c, buf); // Decode the byte into one or two characters.
|
|
if (res & kFirstCharIsLiteral) { // The 1st character couldn't be packed.
|
|
++full_char_count; // So the next stream byte is a full character.
|
|
if (res & kSecondCharIsLiteral) ++full_char_count; // The 2nd character couldn't be packed. Another stream byte is a full character.
|
|
else second_char = buf[1]; // Retain the unpacked second character.
|
|
}
|
|
else {
|
|
handle_output_char(buf[0]); // Send the unpacked first character out.
|
|
if (buf[0] != '\n') { // After a newline the next char won't be set
|
|
if (res & kSecondCharIsLiteral) ++full_char_count; // The 2nd character couldn't be packed. The next stream byte is a full character.
|
|
else handle_output_char(buf[1]); // Send the unpacked second character out.
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
handle_output_char(c); // Pass through the character that couldn't be packed...
|
|
if (second_char) {
|
|
handle_output_char(second_char); // ...and send an unpacked 2nd character, if set.
|
|
second_char = 0;
|
|
}
|
|
--full_char_count; // One literal character was consumed
|
|
}
|
|
}
|
|
else // Packing not enabled, just copy character to output
|
|
handle_output_char(c);
|
|
}
|
|
|
|
/**
|
|
* Buffer a single output character which will be picked up in
|
|
* GCodeQueue::get_serial_commands via calls to get_result_char
|
|
*/
|
|
void MeatPack::handle_output_char(const uint8_t c) {
|
|
char_out_buf[char_out_count++] = c;
|
|
|
|
#if ENABLED(MP_DEBUG)
|
|
if (chars_decoded < 1024) {
|
|
++chars_decoded;
|
|
DEBUG_ECHOLNPAIR("RB: ", AS_CHAR(c));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Process a MeatPack command byte to update the state.
|
|
* Report the new state to serial.
|
|
*/
|
|
void MeatPack::handle_command(const MeatPack_Command c) {
|
|
switch (c) {
|
|
case MPCommand_QueryConfig: break;
|
|
case MPCommand_EnablePacking: SBI(state, MPConfig_Bit_Active); DEBUG_ECHOLNPGM("[MPDBG] ENA REC"); break;
|
|
case MPCommand_DisablePacking: CBI(state, MPConfig_Bit_Active); DEBUG_ECHOLNPGM("[MPDBG] DIS REC"); break;
|
|
case MPCommand_ResetAll: reset_state(); DEBUG_ECHOLNPGM("[MPDBG] RESET REC"); break;
|
|
case MPCommand_EnableNoSpaces:
|
|
SBI(state, MPConfig_Bit_NoSpaces);
|
|
meatPackLookupTable[kSpaceCharIdx] = kSpaceCharReplace; DEBUG_ECHOLNPGM("[MPDBG] ENA NSP"); break;
|
|
case MPCommand_DisableNoSpaces:
|
|
CBI(state, MPConfig_Bit_NoSpaces);
|
|
meatPackLookupTable[kSpaceCharIdx] = ' '; DEBUG_ECHOLNPGM("[MPDBG] DIS NSP"); break;
|
|
default: DEBUG_ECHOLNPGM("[MPDBG] UNK CMD REC");
|
|
}
|
|
report_state();
|
|
}
|
|
|
|
void MeatPack::report_state() {
|
|
// 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("[MP] ");
|
|
SERIAL_ECHOPGM(MeatPack_ProtocolVersion " ");
|
|
serialprint_onoff(TEST(state, MPConfig_Bit_Active));
|
|
SERIAL_ECHOPGM_P(TEST(state, MPConfig_Bit_NoSpaces) ? PSTR(" NSP\n") : PSTR(" ESP\n"));
|
|
}
|
|
|
|
/**
|
|
* Interpret a single character received from serial
|
|
* according to the current meatpack state.
|
|
*/
|
|
void MeatPack::handle_rx_char(const uint8_t c, const serial_index_t serial_ind) {
|
|
if (c == kCommandByte) { // A command (0xFF) byte?
|
|
if (cmd_count) { // In fact, two in a row?
|
|
cmd_is_next = true; // Then a MeatPack command follows
|
|
cmd_count = 0;
|
|
}
|
|
else
|
|
++cmd_count; // cmd_count = 1 // One command byte received so far...
|
|
return;
|
|
}
|
|
|
|
if (cmd_is_next) { // Were two command bytes received?
|
|
PORT_REDIRECT(SERIAL_PORTMASK(serial_ind));
|
|
handle_command((MeatPack_Command)c); // Then the byte is a MeatPack command
|
|
cmd_is_next = false;
|
|
return;
|
|
}
|
|
|
|
if (cmd_count) { // Only a single 0xFF was received
|
|
handle_rx_char_inner(kCommandByte); // A single 0xFF is passed on literally so it can be interpreted as kFirstNotPacked|kSecondNotPacked
|
|
cmd_count = 0;
|
|
}
|
|
|
|
handle_rx_char_inner(c); // Other characters are passed on for MeatPack decoding
|
|
}
|
|
|
|
uint8_t MeatPack::get_result_char(char * const __restrict out) {
|
|
uint8_t res = 0;
|
|
if (char_out_count) {
|
|
res = char_out_count;
|
|
char_out_count = 0;
|
|
for (uint8_t i = 0; i < res; ++i)
|
|
out[i] = (char)char_out_buf[i];
|
|
}
|
|
return res;
|
|
}
|
|
|
|
#endif // HAS_MEATPACK
|