176 lines
7.2 KiB
C++
176 lines
7.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
|
|
*
|
|
* 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.
|
|
*
|
|
*/
|
|
#pragma once
|
|
|
|
#include <stdint.h>
|
|
#include "../core/serial_hook.h"
|
|
|
|
/**
|
|
* 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 : uint8_t {
|
|
MPCommand_None = 0,
|
|
MPCommand_EnablePacking = 0xFB,
|
|
MPCommand_DisablePacking = 0xFA,
|
|
MPCommand_ResetAll = 0xF9,
|
|
MPCommand_QueryConfig = 0xF8,
|
|
MPCommand_EnableNoSpaces = 0xF7,
|
|
MPCommand_DisableNoSpaces = 0xF6
|
|
};
|
|
|
|
enum MeatPack_ConfigStateBits : uint8_t {
|
|
MPConfig_Bit_Active = 0,
|
|
MPConfig_Bit_NoSpaces = 1
|
|
};
|
|
|
|
class MeatPack {
|
|
|
|
// Utility definitions
|
|
static const uint8_t kCommandByte = 0b11111111,
|
|
kFirstNotPacked = 0b00001111,
|
|
kSecondNotPacked = 0b11110000,
|
|
kFirstCharIsLiteral = 0b00000001,
|
|
kSecondCharIsLiteral = 0b00000010;
|
|
|
|
static const uint8_t kSpaceCharIdx = 11;
|
|
static const char kSpaceCharReplace = 'E';
|
|
|
|
bool cmd_is_next; // A command is pending
|
|
uint8_t state; // Configuration state
|
|
uint8_t second_char; // Buffers a character if dealing with out-of-sequence pairs
|
|
uint8_t cmd_count, // Counter of command bytes received (need 2)
|
|
full_char_count, // Counter for full-width characters to be received
|
|
char_out_count; // Stores number of characters to be read out.
|
|
uint8_t char_out_buf[2]; // Output buffer for caching up to 2 characters
|
|
|
|
public:
|
|
// Pass in a character rx'd by SD card or serial. Automatically parses command/ctrl sequences,
|
|
// and will control state internally.
|
|
void handle_rx_char(const uint8_t c, const serial_index_t serial_ind);
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
uint8_t get_result_char(char * const __restrict out);
|
|
|
|
void reset_state();
|
|
void report_state();
|
|
uint8_t unpack_chars(const uint8_t pk, uint8_t* __restrict const chars_out);
|
|
void handle_command(const MeatPack_Command c);
|
|
void handle_output_char(const uint8_t c);
|
|
void handle_rx_char_inner(const uint8_t c);
|
|
|
|
MeatPack() : cmd_is_next(false), state(0), second_char(0), cmd_count(0), full_char_count(0), char_out_count(0) {}
|
|
};
|
|
|
|
// Implement the MeatPack serial class so it's transparent to rest of the code
|
|
template <typename SerialT>
|
|
struct MeatpackSerial : public SerialBase <MeatpackSerial < SerialT >> {
|
|
typedef SerialBase< MeatpackSerial<SerialT> > BaseClassT;
|
|
|
|
SerialT & out;
|
|
MeatPack meatpack;
|
|
|
|
char serialBuffer[2];
|
|
uint8_t charCount;
|
|
uint8_t readIndex;
|
|
|
|
NO_INLINE size_t write(uint8_t c) { return out.write(c); }
|
|
void flush() { out.flush(); }
|
|
void begin(long br) { out.begin(br); readIndex = 0; }
|
|
void end() { out.end(); }
|
|
|
|
void msgDone() { out.msgDone(); }
|
|
// Existing instances implement Arduino's operator bool, so use that if it's available
|
|
bool connected() { return Private::HasMember_connected<SerialT>::value ? CALL_IF_EXISTS(bool, &out, connected) : (bool)out; }
|
|
void flushTX() { CALL_IF_EXISTS(void, &out, flushTX); }
|
|
SerialFeature features(serial_index_t index) const { return SerialFeature::MeatPack | CALL_IF_EXISTS(SerialFeature, &out, features, index); }
|
|
|
|
|
|
int available(serial_index_t index) {
|
|
if (charCount) return charCount; // The buffer still has data
|
|
if (out.available(index) <= 0) return 0; // No data to read
|
|
|
|
// Don't read in read method, instead do it here, so we can make progress in the read method
|
|
const int r = out.read(index);
|
|
if (r == -1) return 0; // This is an error from the underlying serial code
|
|
meatpack.handle_rx_char((uint8_t)r, index);
|
|
charCount = meatpack.get_result_char(serialBuffer);
|
|
readIndex = 0;
|
|
|
|
return charCount;
|
|
}
|
|
|
|
int readImpl(const serial_index_t index) {
|
|
// Not enough char to make progress?
|
|
if (charCount == 0 && available(index) == 0) return -1;
|
|
|
|
charCount--;
|
|
return serialBuffer[readIndex++];
|
|
}
|
|
|
|
int read(serial_index_t index) { return readImpl(index); }
|
|
int available() { return available(0); }
|
|
int read() { return readImpl(0); }
|
|
|
|
MeatpackSerial(const bool e, SerialT & out) : BaseClassT(e), out(out) {}
|
|
};
|