Add fast binary file transfer to SD card option (#12249)

This commit is contained in:
Chris Pepper
2018-10-31 00:44:12 +00:00
committed by Scott Lahteine
parent 72d8adfd1e
commit 66d44c72c3
65 changed files with 524 additions and 21 deletions

View File

@ -258,14 +258,15 @@ void gcode_line_error(PGM_P err, uint8_t port) {
}
static bool serial_data_available() {
return (MYSERIAL0.available() ? true :
return false
|| MYSERIAL0.available()
#if NUM_SERIAL > 1
MYSERIAL1.available() ? true :
|| MYSERIAL1.available()
#endif
false);
;
}
static int read_serial(const int index) {
static int read_serial(const uint8_t index) {
switch (index) {
case 0: return MYSERIAL0.read();
#if NUM_SERIAL > 1
@ -275,6 +276,263 @@ static int read_serial(const int index) {
}
}
#if ENABLED(FAST_FILE_TRANSFER)
#if ENABLED(SDSUPPORT)
#define CARD_CHAR_P(C) SERIAL_CHAR_P(card.transfer_port, C)
#define CARD_ECHO_P(V) SERIAL_ECHO_P(card.transfer_port, V)
#define CARD_ECHOLN_P(V) SERIAL_ECHOLN_P(card.transfer_port, V)
#endif
static bool serial_data_available(const uint8_t index) {
switch (index) {
case 0: return MYSERIAL0.available();
#if NUM_SERIAL > 1
case 1: return MYSERIAL1.available();
#endif
default: return false;
}
}
class BinaryStream {
public:
enum class StreamState : uint8_t {
STREAM_RESET,
PACKET_RESET,
STREAM_HEADER,
PACKET_HEADER,
PACKET_DATA,
PACKET_VALIDATE,
PACKET_RESEND,
PACKET_FLUSHRX,
PACKET_TIMEOUT,
STREAM_COMPLETE,
STREAM_FAILED,
};
#pragma pack(push, 1)
struct StreamHeader {
uint16_t token;
uint32_t filesize;
};
union {
uint8_t stream_header_bytes[sizeof(StreamHeader)];
StreamHeader stream_header;
};
struct Packet {
struct Header {
uint32_t id;
uint16_t size, checksum;
};
union {
uint8_t header_bytes[sizeof(Header)];
Header header;
};
uint32_t bytes_received;
uint16_t checksum;
millis_t timeout;
} packet{};
#pragma pack(pop)
void packet_reset() {
packet.header.id = 0;
packet.header.size = 0;
packet.header.checksum = 0;
packet.bytes_received = 0;
packet.checksum = 0x53A2;
packet.timeout = millis() + STREAM_MAX_WAIT;
}
void stream_reset() {
packets_received = 0;
bytes_received = 0;
packet_retries = 0;
buffer_next_index = 0;
stream_header.token = 0;
stream_header.filesize = 0;
}
uint32_t checksum(uint32_t seed, uint8_t value) {
return ((seed ^ value) ^ (seed << 8)) & 0xFFFF;
}
// read the next byte from the data stream keeping track of
// whether the stream times out from data starvation
// takes the data variable by reference in order to return status
bool stream_read(uint8_t& data) {
if (ELAPSED(millis(), packet.timeout)) {
stream_state = StreamState::PACKET_TIMEOUT;
return false;
}
if (!serial_data_available(card.transfer_port)) return false;
data = read_serial(card.transfer_port);
packet.timeout = millis() + STREAM_MAX_WAIT;
return true;
}
template<const size_t buffer_size>
void receive(char (&buffer)[buffer_size]) {
uint8_t data = 0;
millis_t tranfer_timeout = millis() + RX_TIMESLICE;
while (PENDING(millis(), tranfer_timeout)) {
switch (stream_state) {
case StreamState::STREAM_RESET:
stream_reset();
case StreamState::PACKET_RESET:
packet_reset();
stream_state = StreamState::PACKET_HEADER;
break;
case StreamState::STREAM_HEADER: // we could also transfer the filename in this packet, rather than handling it in the gcode
for (size_t i = 0; i < sizeof(stream_header); ++i) {
stream_header_bytes[i] = buffer[i];
}
if (stream_header.token == 0x1234) {
stream_state = StreamState::PACKET_RESET;
bytes_received = 0;
time_stream_start = millis();
CARD_ECHO_P("echo: Datastream initialized (");
CARD_ECHO_P(stream_header.filesize);
CARD_ECHOLN_P("Bytes expected)");
CARD_ECHO_P("so"); // confirm active stream and the maximum block size supported
CARD_CHAR_P(static_cast<uint8_t>(buffer_size & 0xFF));
CARD_CHAR_P(static_cast<uint8_t>((buffer_size >> 8) & 0xFF));
CARD_CHAR_P('\n');
}
else {
CARD_ECHOLN_P("echo: Datastream initialization error (invalid token)");
stream_state = StreamState::STREAM_FAILED;
}
buffer_next_index = 0;
break;
case StreamState::PACKET_HEADER:
if (!stream_read(data)) break;
packet.header_bytes[packet.bytes_received++] = data;
if (packet.bytes_received == sizeof(Packet::Header)) {
if (packet.header.id == packets_received) {
buffer_next_index = 0;
packet.bytes_received = 0;
stream_state = StreamState::PACKET_DATA;
}
else {
CARD_ECHO_P("echo: Datastream packet out of order");
stream_state = StreamState::PACKET_FLUSHRX;
}
}
break;
case StreamState::PACKET_DATA:
if (!stream_read(data)) break;
if (buffer_next_index < buffer_size) {
buffer[buffer_next_index] = data;
}
else {
CARD_ECHO_P("echo: Datastream packet data buffer overrun");
stream_state = StreamState::STREAM_FAILED;
break;
}
packet.checksum = checksum(packet.checksum, data);
packet.bytes_received ++;
buffer_next_index ++;
if (packet.bytes_received == packet.header.size) {
stream_state = StreamState::PACKET_VALIDATE;
}
break;
case StreamState::PACKET_VALIDATE:
if (packet.header.checksum == packet.checksum) {
packet_retries = 0;
packets_received ++;
bytes_received += packet.header.size;
if (packet.header.id == 0) { // id 0 is always the stream descriptor
stream_state = StreamState::STREAM_HEADER; // defer packet confirmation to STREAM_HEADER state
}
else {
if (bytes_received < stream_header.filesize) {
stream_state = StreamState::PACKET_RESET; // reset and receive next packet
CARD_ECHOLN_P("ok"); // transmit confirm packet received and valid token
}
else {
stream_state = StreamState::STREAM_COMPLETE; // no more data required
}
if (card.write(buffer, buffer_next_index) < 0) {
stream_state = StreamState::STREAM_FAILED;
CARD_ECHO_P("echo: IO ERROR");
break;
};
}
}
else {
CARD_ECHO_P("echo: Block(");
CARD_ECHO_P(packet.header.id);
CARD_ECHOLN_P(") Corrupt");
stream_state = StreamState::PACKET_FLUSHRX;
}
break;
case StreamState::PACKET_RESEND:
if (packet_retries < MAX_RETRIES) {
packet_retries ++;
stream_state = StreamState::PACKET_RESET;
CARD_ECHO_P("echo: Resend request ");
CARD_ECHOLN_P(packet_retries);
CARD_ECHOLN_P("rs"); // transmit resend packet token
}
else {
stream_state = StreamState::STREAM_FAILED;
}
break;
case StreamState::PACKET_FLUSHRX:
if (ELAPSED(millis(), packet.timeout)) {
stream_state = StreamState::PACKET_RESEND;
break;
}
if (!serial_data_available(card.transfer_port)) break;
read_serial(card.transfer_port); // throw away data
packet.timeout = millis() + STREAM_MAX_WAIT;
break;
case StreamState::PACKET_TIMEOUT:
CARD_ECHOLN_P("echo: Datastream timeout");
stream_state = StreamState::PACKET_RESEND;
break;
case StreamState::STREAM_COMPLETE:
stream_state = StreamState::STREAM_RESET;
card.binary_mode = false;
card.closefile();
CARD_ECHO_P("echo: ");
CARD_ECHO_P(card.filename);
CARD_ECHO_P(" transfer completed @ ");
CARD_ECHO_P(((bytes_received / (millis() - time_stream_start) * 1000) / 1024 ));
CARD_ECHOLN_P("KiB/s");
CARD_ECHOLN_P("sc"); // transmit stream complete token
return;
case StreamState::STREAM_FAILED:
stream_state = StreamState::STREAM_RESET;
card.binary_mode = false;
card.closefile();
card.removeFile(card.filename);
CARD_ECHOLN_P("echo: File transfer failed");
CARD_ECHOLN_P("sf"); // transmit stream failed token
return;
}
}
}
static const uint16_t STREAM_MAX_WAIT = 500, RX_TIMESLICE = 20, MAX_RETRIES = 3;
uint8_t packet_retries;
uint16_t buffer_next_index;
uint32_t packets_received, bytes_received;
millis_t time_stream_start;
StreamState stream_state = StreamState::STREAM_RESET;
} binaryStream{};
#endif // FAST_FILE_TRANSFER
/**
* Get all commands waiting on the serial port and queue them.
* Exit when the buffer is full or when no more characters are
@ -288,6 +546,18 @@ inline void get_serial_commands() {
#endif
;
#if ENABLED(FAST_FILE_TRANSFER)
if (card.saving && card.binary_mode) {
/**
* For binary stream file transfer, use serial_line_buffer as the working
* receive buffer (which limits the packet size to MAX_CMD_SIZE).
* The receive buffer also limits the packet size for reliable transmission.
*/
binaryStream.receive(serial_line_buffer[card.transfer_port]);
return;
}
#endif
// If the command buffer is empty for too long,
// send "wait" to indicate Marlin is still waiting.
#if NO_TIMEOUTS > 0