/*
* xio.cpp - extended IO functions
* This file is part of the g2core project
*
* Copyright (c) 2013 - 2018 Alden S. Hart Jr.
* Copyright (c) 2013 - 2018 Robert Giseburt
*
* This file ("the software") is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2 as published by the
* Free Software Foundation. You should have received a copy of the GNU General Public
* License, version 2 along with the software. If not, see .
*
* As a special exception, you may use this file as part of a software library without
* restriction. Specifically, if other files instantiate templates or use macros or
* inline functions from this file, or you compile this file and link it with other
* files to produce an executable, this file does not by itself cause the resulting
* executable to be covered by the GNU General Public License. This exception does not
* however invalidate any other reasons why the executable file might be covered by the
* GNU General Public License.
*
* THE SOFTWARE IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY
* WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
* SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* XIO acts as an entry point into lower level IO routines - mostly serial IO. It supports
* the USB, SPI and file IO sub-systems, as well as providing low level character functions
* used by stdio (printf()).
*/
#include "g2core.h"
#include "config.h"
#include "hardware.h"
#include "xio.h"
#include "report.h"
#include "controller.h"
#include "util.h"
#include "settings.h"
#include "board_xio.h"
#include "MotateBuffer.h"
using Motate::RXBuffer;
using Motate::TXBuffer;
#include "MotateUtilities.h" // for HOT_DATA and HOT_FUNC
#ifdef __TEXT_MODE
#include "text_parser.h"
#endif
// defines for assertions
/**** HIGH LEVEL EXPLANATION OF XIO ****
*
* The XIO subsystem serves three purposes:
* 1) Handle the connection states of various IO channels (USB for now)
* 2) Marshall reads/writes/etc from the rest of the system to/from the managed channels
* 3) Maintain buffers for line-based reading on devices.
*
* We have three object types: xioDeviceWrapperBase, xioDeviceWrapper, and xio_t.
*
* xioDeviceWrapperBase -- is an abstract base class object that manages and provides access to:
* *) the line read buffer and state
* *) the state machine for a single device
* *) pure-virtual functions for read/write/flush (to override later)
* *) a readline implementation that is device agnostic
*
* xioDeviceWrapper -- is a concrete template-specialized child of xioDeviceWrapperBase:
* *) Wraps any "device" that supports readchar(), flushRead(), and write(const uint8_t *buffer, int16_t len)
* *) Calls the device's setConnectionCallback() on construction, and contains the connection state machine
* *) Calls into the xio singleton for multi-device checks. (This mildly complicates the order that we define
* these structures, since they depend on each other.)
* *) Calls controller_set_connected() to inform the higher system when the first device has connected and
* the last device has disconnected.
*
* xio_t -- the class used by the xio singleton
* *) Contains the array of xioDeviceWrapperBase pointers.
* *) Handles system-wide readline(), write(), and flushRead()
* *) Handles making cross-device checks and changes for the state machine.
*
***************************************/
/**** Structures ****/
// We need a buffer to hold single character commands, like !~%, ^x, etc.
// We also want it to have a NULL character, so we make it two characters.
char single_char_buffer[2] = " ";
// Checks against arbitrary flags variable (passed in)
// Prefer to use the object is*() methods over these.
bool checkForCtrl(devflags_t flags_to_check) { return flags_to_check & DEV_IS_CTRL; }
bool checkForCtrlOnly(devflags_t flags_to_check) { return (flags_to_check & (DEV_IS_CTRL|DEV_IS_DATA)) == DEV_IS_CTRL; }
bool checkForData(devflags_t flags_to_check) { return flags_to_check & DEV_IS_DATA; }
bool checkForNotActive(devflags_t flags_to_check) { return !(flags_to_check & DEV_IS_ACTIVE); }
bool checkForCtrlAndData(devflags_t flags_to_check) { return (flags_to_check & (DEV_IS_CTRL|DEV_IS_DATA)) == (DEV_IS_CTRL|DEV_IS_DATA); }
bool checkForCtrlAndPrimary(devflags_t flags_to_check) { return (flags_to_check & (DEV_IS_CTRL|DEV_IS_PRIMARY)) == (DEV_IS_CTRL|DEV_IS_PRIMARY); }
struct xioDeviceWrapperBase { // C++ base class for device primitives
// connection and device management
uint8_t caps; // bitfield for capabilities flags (these are persistent)
devflags_t flags; // bitfield for device state flags (these are not)
devflags_t next_flags; // bitfield for next-state transitions
// Checks against class flags variable:
// bool canRead() { return caps & DEV_CAN_READ; }
// bool canWrite() { return caps & DEV_CAN_WRITE; }
// bool canBeCtrl() { return caps & DEV_CAN_BE_CTRL; }
// bool canBeData() { return caps & DEV_CAN_BE_DATA; }
bool isCtrl() { return flags & DEV_IS_CTRL; } // called externally: DeviceWrappers[i]->isCtrl()
bool isData() { return flags & DEV_IS_DATA; } // subclasses can call directly (no pointer): isCtrl()
bool isPrimary() { return flags & DEV_IS_PRIMARY; }
bool isAlwaysDataAndCtrl() { return caps & DEV_IS_ALWAYS_BOTH; }
bool isMuteAsSecondary() { return caps & DEV_IS_MUTE_SECONDARY; }
bool isConnected() { return flags & DEV_IS_CONNECTED; }
bool isNotConnected() { return !(flags & DEV_IS_CONNECTED); }
bool isReady() { return flags & DEV_IS_READY; }
bool isActive() { return flags & DEV_IS_ACTIVE; }
bool isMuted() { return flags & DEV_IS_MUTED; }
// Combination checks
bool isCtrlAndActive() { return ((flags & (DEV_IS_CTRL|DEV_IS_ACTIVE)) == (DEV_IS_CTRL|DEV_IS_ACTIVE)); }
bool isDataAndActive() { return ((flags & (DEV_IS_DATA|DEV_IS_ACTIVE)) == (DEV_IS_DATA|DEV_IS_ACTIVE)); }
bool isNotCtrlOnly() { return ((flags & (DEV_IS_CTRL|DEV_IS_DATA)) != (DEV_IS_CTRL)); }
// Manipulation functions
void setData() { flags |= DEV_IS_DATA; }
void clearData() { flags &= ~DEV_IS_DATA; }
void setActive() { flags |= DEV_IS_ACTIVE; }
void clearActive() { flags &= DEV_IS_ACTIVE; }
void setPrimary() { flags |= DEV_IS_PRIMARY; }
void clearPrimary() { flags &= ~DEV_IS_PRIMARY; }
void setAsConnectedAndReady() { flags |= ( DEV_IS_CONNECTED | DEV_IS_READY); };
void setAsPrimaryActiveDualRole() {
if (isAlwaysDataAndCtrl() || isMuteAsSecondary()) {
// In both cases, it cannot be a PRIMARY
// Also, we remove a MUTED flag
flags = (flags & ~DEV_IS_MUTED) | (DEV_IS_CTRL | DEV_IS_DATA | DEV_IS_ACTIVE);
} else {
flags |= (DEV_IS_CTRL | DEV_IS_DATA | DEV_IS_PRIMARY | DEV_IS_ACTIVE);
}
};
void setAsActiveData() { flags |= ( DEV_IS_DATA | DEV_IS_ACTIVE); };
void setAsMuted() { flags = (flags & ~(DEV_IS_PRIMARY | DEV_IS_DATA | DEV_IS_CTRL)) | DEV_IS_MUTED; };
void clearFlags() { flags = DEV_FLAGS_CLEAR; }
xioDeviceWrapperBase(uint8_t _caps) : caps(_caps),
flags((_caps & DEV_IS_ALWAYS_BOTH) ? (DEV_IS_CTRL | DEV_IS_DATA) : DEV_FLAGS_CLEAR),
next_flags(DEV_FLAGS_CLEAR)
{
};
// Don't use pure virtuals! They massively slow down the calls.
// But these MUST be overridden!
virtual int16_t readchar() { return -1; };
virtual void flush() {};
virtual void flushRead() {}; // This should call _flushLine() before flushing the device.
virtual bool flushToCommand() { return false; };
virtual int16_t write(const char *buffer, int16_t len) { return -1; };
virtual char *readline(devflags_t limit_flags, uint16_t &size) { return nullptr; };
virtual void flushDevice() {};
#if MARLIN_COMPAT_ENABLED == true
virtual void exitFakeBootloaderMode() {};
#endif
};
// Here we create the xio_t class, which has convenience methods to handle cross-device actions as a whole.
struct xio_t {
uint16_t magic_start;
xioDeviceWrapperBase* DeviceWrappers[DEV_MAX];
const uint8_t _dev_count;
template
xio_t(ds... args) : magic_start(MAGICNUM), DeviceWrappers {args...}, _dev_count(sizeof...(args)), magic_end(MAGICNUM) {
};
// ##### Connection management functions
bool connected() {
for (int8_t i = 0; i < _dev_count; ++i) {
if(DeviceWrappers[i]->isConnected()) {
return true;
}
}
return false;
};
bool othersConnected(xioDeviceWrapperBase* except) {
for (int8_t i = 0; i < _dev_count; ++i) {
if((DeviceWrappers[i] != except) && (!DeviceWrappers[i]->isAlwaysDataAndCtrl()) && DeviceWrappers[i]->isConnected()) {
return true;
}
}
return false;
};
void removeDataFromPrimary() {
// Why is this first pass here? -RG
for (int8_t i = 0; i < _dev_count; ++i) {
if (DeviceWrappers[i]->isDataAndActive()) {
return;
}
}
for (int8_t i = 0; i < _dev_count; ++i) {
if (DeviceWrappers[i]->isPrimary()) {
DeviceWrappers[i]->clearData();
}
}
};
bool checkMutedSecondaryChannels() {
bool muted_something = false;
for (int8_t i = 0; i < _dev_count; ++i) {
if (DeviceWrappers[i]->isMuteAsSecondary()) {
DeviceWrappers[i]->setAsMuted();
muted_something = true;
}
}
return muted_something;
}
bool deactivateAndUnmuteChannels() {
bool unmuted_something = false;
for(int8_t i = 0; i < _dev_count; ++i) {
if (DeviceWrappers[i]->isMuted()) {
unmuted_something = true;
DeviceWrappers[i]->setAsPrimaryActiveDualRole(); // NOTE: muted secondary devices won't be set PRIMARY
} else {
DeviceWrappers[i]->clearActive();
}
}
return unmuted_something;
};
// ##### Cross-Device read/write/etc. functions
/*
* write() - write a block to a device
*
* There are a few issues with this function that I don't know how to resolve right now:
* 1) If a device fails to write the data, or all the data, then it's ignored
* 2) Only the amount written by the *last* device to match (CTRL|ACTIVE) is returned.
*
* In the current environment, these are not foreseen to cause trouble since these
* are blocking writes and we expect to only really be writing to one device.
*/
size_t write(const char *buffer, size_t size, bool only_to_muted)
{
size_t total_written = -1;
for (int8_t i = 0; i < _dev_count; ++i) {
bool ok_channel = false;
if (!only_to_muted) {
ok_channel = DeviceWrappers[i]->isCtrlAndActive();
} else {
ok_channel = DeviceWrappers[i]->isMuted();
}
if (ok_channel) {
const char *buf = buffer;
int16_t to_write = size;
while (to_write > 0) {
size_t written = DeviceWrappers[i]->write(buf, to_write);
buf += written;
to_write -= written;
total_written += written;
}
}
}
return total_written;
}
/*
* writeline() - write a complete line to the controldevice
*
* The input buffer must be NUL terminated
*/
int16_t writeline(const char *buffer, bool only_to_muted)
{
int16_t len = strlen(buffer);
return write(buffer, len, only_to_muted);
};
/*
* flush() - flush all readable devices' write buffers
*/
void flush()
{
for (int8_t i = 0; i < _dev_count; ++i) {
DeviceWrappers[i]->flush();
}
}
/*
* flushRead() - flush all readable devices' read buffers
*/
void flushRead()
{
for (int8_t i = 0; i < _dev_count; ++i) {
DeviceWrappers[i]->flushRead();
}
}
/*
* flushToCommand() - flush all readable devices' read buffers up to the last returned command
*
* Note that only one device will flush.
*
*/
void flushToCommand()
{
for (int8_t i = 0; i < _dev_count; ++i) {
DeviceWrappers[i]->flushToCommand();
}
}
/*
* readline() - read a complete line from a device
*
* Reads a line of text from the next active device that has one ready. With some exceptions.
* Accepts CR or LF as line terminator. Replaces CR or LF with NUL in the returned string.
*
* This function iterates over all active control and data devices, including reading from
* multiple control devices. It will also manage multiple data devices, but only one data
* device may be active at a time.
*
* ARGS:
*
* flags - Bitfield containing the type of channel(s) to read. Looks at DEV_IS_CTRL and
* DEV_IS_DATA bits in the device flag field. 'Flags' is loaded with the flags of
* the channel that was read on return, or 0 (DEV_FLAGS_CLEAR) if no line was returned.
*
* size - Returns the size of the completed buffer, including the NUL termination character.
* Lines may be returned truncated to the length of the serial input buffer if the text
* from the physical device is longer than the read buffer for the device. The size value
* provided as a calling argument is ignored (size doesn't matter).
*
* char * Returns a pointer to the buffer containing the line, or NULL (*0) if no text
*/
char *readline(devflags_t &flags, uint16_t &size)
{
char *ret_buffer;
devflags_t limit_flags = flags; // Store it so it can't get mangled
// Always check control-capable devices FIRST
for (uint8_t dev=0; dev < _dev_count; dev++) {
if (!DeviceWrappers[dev]->isActive()) {
continue;
}
// If this channel is a DATA only, skip it this pass
if (!DeviceWrappers[dev]->isCtrl()) {
continue;
}
ret_buffer = DeviceWrappers[dev]->readline(DEV_IS_CTRL, size);
if (size > 0) {
flags = DeviceWrappers[dev]->flags;
return ret_buffer;
}
}
// We only do this second pass if this is not a CTRL-only read
if (!checkForCtrlOnly(limit_flags)) {
for (uint8_t dev=0; dev < _dev_count; dev++) {
if (!DeviceWrappers[dev]->isActive())
continue;
ret_buffer = DeviceWrappers[dev]->readline(limit_flags, size);
if (size > 0) {
flags = DeviceWrappers[dev]->flags;
return ret_buffer;
}
}
}
size = 0;
flags = 0;
return (NULL);
};
void flushDevice(devflags_t &flags)
{
for (uint8_t dev=0; dev < _dev_count; dev++) {
if(!DeviceWrappers[dev]->isActive())
continue;
if(!(DeviceWrappers[dev]->flags & flags))
continue;
DeviceWrappers[dev]->flushRead();
}
};
#if MARLIN_COMPAT_ENABLED == true
void exitFakeBootloaderMode() {
for (int8_t i = 0; i < _dev_count; ++i) {
DeviceWrappers[i]->exitFakeBootloaderMode();
}
};
#endif
uint16_t magic_end;
};
// Declare (but don't define) the xio singleton object now, define it later
// Why? xioDeviceWrapper uses it, but we need to define it to contain xioDeviceWrapper objects.
extern xio_t xio;
// Use a templated subclass so we don't have to create a new subclass for every type of Device.
// All this requires is the readByte() function to exist in type Device.
// Note that we expect Device to be a pointer type!
// See here for a discussion of what this means if you are not familiar with C++
// https://github.com/synthetos/g2/wiki/Dual-Endpoint-USB-Internals#c-classes-virtual-functions-and-inheritance
// LineRXBuffer takes the Motate RXBuffer (which handles "transfers", usually DMA),
// and adds G2 line-reading semantics to it.
template
struct LineRXBuffer : RXBuffer<_size, owner_type, char> {
typedef RXBuffer<_size, owner_type, char> parent_type;
// Let's help the compiler clarify what we mean by a few things:
// (This is because of the templating, it needs a little extra clarification.)
using parent_type::_data;
using parent_type::_getWriteOffset;
using parent_type::_last_known_write_offset;
using parent_type::_read_offset;
using parent_type::isEmpty;
using parent_type::_restartTransfer;
using parent_type::_canBeRead;
// START OF LineRXBuffer PROPER
static_assert(((_header_count-1)&_header_count)==0, "_header_count must be 2^N");
char _line_buffer[_line_buffer_size+1]; // hold exactly one line to return
uint32_t _line_end_guard = 0xBEEF;
// General term usage:
// * "index" indicates it's in to _headers array
// * "offset" means it's a character in the _data array
uint16_t _scan_offset; // offset into data of the last character scanned
uint16_t _line_start_offset; // offset into first character of the line, or the first char to ignore (too-long lines)
uint16_t _last_line_length; // used for ensuring lines aren't too long
bool _ignore_until_next_line; // if we get a too-long-line, we ignore the rest by setting this flag
bool _at_start_of_line; // true if the last character scanned was the end of a line
bool _last_control_was_feedhold; // true if the last single-character we found was a feedhold, meaning a feedhold was requested
uint16_t _lines_found; // count of complete non-control lines that were found during scanning.
volatile uint16_t _last_scan_offset; // DIAGNOSTIC
bool _last_returned_a_control = false;
#if MARLIN_COMPAT_ENABLED == true
enum class STK500V2_State {
Done, // not in the faked stk500v2 bootloader
Timeout, // timeout period, waiting for a start character
Start, // waiting for 0x1B
Sequence, // waiting for sequence byte
Length_0, // waiting for MSB of length
Length_1, // waiting for LSB of length
Header_End,// waiting for 0x0E
Data, // waiting for more data
Checksum // waiting for checksum byte
};
STK500V2_State _stk_parser_state;
uint16_t _stk_packet_data_length;
Motate::Timeout _stk_timeout;
void startFakeBootloaderMode() {
_stk_parser_state = STK500V2_State::Timeout;
_stk_timeout.set(2000); // two seconds
}
void exitFakeBootloaderMode() {
_stk_parser_state = STK500V2_State::Done;
}
#endif
LineRXBuffer(owner_type owner) : parent_type{owner} {};
void init() {
parent_type::init();
_at_start_of_line = true;
};
struct SkipSections {
struct SkipSection {
uint16_t start_offset; // the offset of the first character to skip
uint16_t end_offset; // the offset of the next character to read after skipping
};
static constexpr uint16_t _section_count = 16;
SkipSection _sections[_section_count];
uint8_t read_section_idx; // index of the first skip section to skip
uint8_t write_section_idx; // index of the next skip section to populate
bool isFull() {
return ((write_section_idx+1)&(_section_count-1)) == read_section_idx;
};
bool isEmpty() {
return (write_section_idx == read_section_idx);
};
void addSkip(uint16_t start_offset, uint16_t end_offset) {
if (!isEmpty()) {
uint8_t last_write_section_idx = write_section_idx;
if (write_section_idx == 0) {
last_write_section_idx = _section_count-1;
} else {
last_write_section_idx--;
}
if (_sections[last_write_section_idx].end_offset == start_offset) {
_sections[last_write_section_idx].end_offset = end_offset;
return;
}
}
_sections[write_section_idx].start_offset = start_offset;
_sections[write_section_idx].end_offset = end_offset;
write_section_idx = ((write_section_idx+1)&(_section_count-1));
};
void popSkip() {
_sections[read_section_idx].start_offset = 0;
_sections[read_section_idx].end_offset = 0;
read_section_idx = ((read_section_idx+1)&(_section_count-1));
};
bool skip(volatile uint16_t &from) {
if (!isEmpty()) {
SkipSection &next_skip = _sections[read_section_idx];
if (next_skip.start_offset == from) {
from = next_skip.end_offset;
popSkip();
return true;
}
}
return false;
};
// const SkipSection& next_skip() {
// return _sections[read_section_idx];
// }
};
SkipSections _skip_sections;
uint16_t _getNextScanOffset() {
return ((_scan_offset + 1) & (_size-1));
}
bool _isMoreToScan() {
return _canBeRead(_scan_offset);
};
/*
* _scanBuffer()
*
* Make a pass through the RX DMA buffer to locate any control lines, and count lines.
* Single character controls, like !, ~, %, and ^x are also considered control "lines"
*
* _scanBuffer() is called at the beginning of readline, and is effectively the first
* "phase" of readline.
*
* This function is designed to be able to exit from almost any point, and
* come back in and resume where it left off. This allows it to scan to the
* end of the buffer then exit. When the function is called next it picks up
* where it left off - i.e. avoiding rescanning the entire buffer multiple times.
*
* _scanBuffer() returns true if it finds a control line.
* The control line starts at the character at _line_start_offset and includes
* the characters up to _scan_offset-1. If there are multiple line-ending chars
* ("\r\n" for example) _scan_offset will point to the *first* one.
*
* With ASCII art (where "." means "invalid data" or "don't care"):
*
* Example 1 of _scanBuffer() == true:
* _data = "G0X10\n{jvm:5}\n{xvm:1200}\nG0Y10\nG1Z......"
* ^ ^
* | |
* _line_start_offset |
* _scan_offset
*
* Example 2 of _scanBuffer() == true:
* _data = "G0X10\n.........{xvm:1200}\nG0Y10\nG1Z......"
* ^ ^
* | |
* _line_start_offset |
* _scan_offset
*
* Example 2 of _scanBuffer() == true:
* _data = "G0X10\n!......"
* ^^
* ||
* _line_start_offset|
* _scan_offset
*
* For _scanBuffer() == false, IGNORE _line_start_offset and _scan_offset!!!
* Only use _read_offset, and use _lines_found>0 to determine if _data contains a line to return.
* Also note that _read_offset needs to be moved once the data is copied to _line_buffer!
*/
/* Explanation of cases and how we handle it.
*
* Our first task in this loop is two-fold (done at the same time):
* A) Scan the RX DMA buffer for the next complete line, then classify the line.
* B) Scan for a single-character command (!~% ^D, etc), then classify as a control line.
*
* If we find a line that classifies as "control" then we return true and stop scanning.
*
* We also have a constraint that we may run out of characters at any time. This is OK,
* and enough state is kept that we can enter the function at any point with new
* characters added to the RX DMA buffer and get the same results.
*
* Another constraint is that lines MAY have single character commands embedded in them.
* In this case we need to un-embed them. Since we may not have the end of the line yet,
* we need to move the command to the beginning of the line.
*
* Note that _at_start_of_line means that we *just* parsed a character that is *at* the end of the line.
* So, for a \r\n sequence, _at_start_of_line will go true of the \r, and we'll see the \n and it'll stay
* true, then the first non \r or \n char will set it to false, and *then* start the next line.
*/
bool _scanBuffer() {
_last_scan_offset = _scan_offset;
while (_isMoreToScan()) {
bool ends_line = false;
bool is_control = false;
char c = _data[_scan_offset];
#if MARLIN_COMPAT_ENABLED == true
// it's possible something will try to talk stk500v2 to us.
// See https://github.com/synthetos/g2/wiki/Marlin-Compatibility#stk500v2
if ((_stk_parser_state == STK500V2_State::Done) && (c == 0)) {
debug_trap("scan ran into NULL (Marlin-mode)");
flush(); // consider the connection and all data trashed
return false;
}
if (_stk_parser_state >= STK500V2_State::Timeout) {
if (_stk_parser_state == STK500V2_State::Timeout) {
if (_stk_timeout.isPast()) {
_stk_parser_state = STK500V2_State::Done;
// start over, outside of stk500v2 mode
continue;
}
// if we got something before the timeout, then we're in stk500v2 mode
// we'll look at what we got and maybe exit anyway
_stk_parser_state = STK500V2_State::Start;
}
if (_stk_parser_state == STK500V2_State::Start) {
if (c == 0x1B) {
_stk_parser_state = STK500V2_State::Sequence;
// this is the start of this "line" and we can "read" (skip) everything up to here.
_read_offset = _scan_offset;
_line_start_offset = _scan_offset;
}
else if ((c == '{') || (c == 'N') || (c == '\n') || (c == '\r') || (c == 'G') || (c == 'M')) {
_stk_parser_state = STK500V2_State::Done; // jump out of bootloader mode
_read_offset = _scan_offset;
continue;
}
} else if (_stk_parser_state == STK500V2_State::Sequence) {
_stk_parser_state = STK500V2_State::Length_0; // we ignore the sequence
} else if (_stk_parser_state == STK500V2_State::Length_0) {
_stk_packet_data_length = c << 8;
_stk_parser_state = STK500V2_State::Length_1;
} else if (_stk_parser_state == STK500V2_State::Length_1) {
_stk_packet_data_length |= c;
_stk_parser_state = STK500V2_State::Header_End;
} else if (_stk_parser_state == STK500V2_State::Header_End) {
if (c == 0x0E) {
_stk_parser_state = STK500V2_State::Data;
} else { // end-of-header marker was corrupt, start over
_stk_packet_data_length = 0;
_read_offset = _scan_offset;
_stk_parser_state = STK500V2_State::Start;
}
} else if (_stk_parser_state == STK500V2_State::Data) {
if (--_stk_packet_data_length == 0) { // we don't read the data here, just return it
_stk_parser_state = STK500V2_State::Checksum;
}
} else if (_stk_parser_state == STK500V2_State::Checksum) {
// We do NOT check the checksum, since if it's corrupt, we'd need to reply, and we can't reply here.
// At this point, we at least have a complete packet we will use the "control" return mechanism to
// handle this since controls don't have to be \r\n-terminated
is_control = true;
ends_line = true;
_stk_parser_state = STK500V2_State::Start; // this line is complete, reset the state engine
}
}
else
#else // not MARLIN_COMPAT_ENABLED
if (c == 0) {
debug_trap("_scanBuffer() scan ran into NULL");
flush(); // consider the connection and all data trashed
return false;
}
#endif // MARLIN_COMPAT_ENABLED
// Look for line endings
if (c == '\r' || c == '\n') {
if (_ignore_until_next_line) {
// we finally ended the line we were ignoring
// add a skip section to jump over the overage
_skip_sections.addSkip(_line_start_offset, _scan_offset);
// move the start of the next skip section to after this skip
_line_start_offset = _scan_offset;
// we DON'T want to end it normally (by counting a line)
_at_start_of_line = true;
_ignore_until_next_line = false;
_last_line_length = 0;
}
else if (!_at_start_of_line) { // We only mark ends_line for the first end-line char, and if
ends_line = true; // _at_start_of_line is already true, this is not the first.
}
}
// prevent going further if we are ignoring
else if (_ignore_until_next_line)
{
// don't do anything
}
// Classify the line if it's a single character
else if (_at_start_of_line &&
((c == '!') || // feedhold
(c == '~') || // cycle start
(c == ENQ) || // request ENQ/ack
(c == CHAR_RESET) || // ^X - reset (aka cancel, terminate)
(c == CHAR_ALARM) || // ^D - request job kill (end of transmission)
(c == '%' && (_last_control_was_feedhold || cm_has_hold())) // flush (only in feedhold or part of control header)
))
{
_last_control_was_feedhold = (c == '!');
_line_start_offset = _scan_offset;
// single-character control
is_control = true;
ends_line = true;
}
else {
if (_at_start_of_line) {
// This is the first character at the beginning of the line.
_line_start_offset = _scan_offset;
_last_line_length = 0;
}
_at_start_of_line = false;
_last_control_was_feedhold = false;
}
// bump the _scan_offset
_scan_offset = _getNextScanOffset();
_last_line_length++;
if (ends_line) {
// _scan_offset is now one past the end of the line,
// which means it is at the start of a new line
_at_start_of_line = true;
// Here we classify the line.
// If is_control is already true, it's an already classified
// single-character command.
if (!is_control) {
// TODO --- Call a function to do this
if (_data[_line_start_offset] == '{') {
is_control = true;
}
// TODO ---
}
if (is_control) { // we found a control
// Quick check for single-character with a \n after it
while (_isMoreToScan() &&
((_data[_scan_offset] == '\n') ||
(_data[_scan_offset] == '\r'))
)
{
_scan_offset = _getNextScanOffset();
}
return true;
} else { // we did find one more line, though.
_lines_found++;
}
} // if ends_line
else if (_last_line_length == (_line_buffer_size - 1)) {
// force an end-of-line, splitting this line into two lines
_ignore_until_next_line = true;
_line_start_offset = _scan_offset;
_lines_found++;
}
} //while (_isMoreToScan())
// special edge case: we ran out of items to scan (buffer full?), but we're ignoring because a line was too long
// example: we get a line that it multiple-times the length of the buffer
// so we'll dump skip sections to the readline will move the read pointer forward
if (_ignore_until_next_line && (_line_start_offset != _scan_offset)) {
// add a skip section to jump over the overage
_skip_sections.addSkip(_line_start_offset, _scan_offset);
// move the start of the next skip section to after this skip
_line_start_offset = _scan_offset;
}
return false; // no control was found
};
/*
* readline()
*
* This is the ONLY external interface in this class
*
* Exit condition when a control is found: _line_start_offset and _scan_offset should be the same.
* If the control was the first char of the buffer it also moves the _data_offset, marking it as read
*/
char *readline(bool control_only, uint16_t &line_size) {
// This is tricky: if we don't have room for more skip_sections, then we
// can't scan any more for controls. So we don't scan, amd hope some lines are read.
bool found_control = _skip_sections.isFull() ? false : _scanBuffer();
_restartTransfer();
_last_returned_a_control = found_control;
char *dst_ptr = _line_buffer;
line_size = 0;
if (found_control) {
// Optimization: if the control was found at the beginning of _data, we note that now
// and update the _read_offset when we update _line_start_offset
bool ctrl_is_at_beginning_of_data = (_line_start_offset == _read_offset);
if (!ctrl_is_at_beginning_of_data) {
_skip_sections.addSkip(_line_start_offset, _scan_offset);
}
// When we get here, _line_start_offset points to either:
// A) A single-character command, OR
// B) A full line.
// Either way, _scan_offset is one past the end, so we don't care which.
if (_data[_line_start_offset] == 0) {
debug_trap("readline() read ran into NULL");
}
// scan past any leftover CR or LF from the previous line
while ((_data[_line_start_offset] == '\n') || (_data[_line_start_offset] == '\r')) {
_line_start_offset = (_line_start_offset+1)&(_size-1);
if (_scan_offset == _line_start_offset) {
debug_trap("readline() read ran into scan (1)");
}
}
// note that if it's marked as a control, it's guaranteed to fit in the line buffer
while (_scan_offset != _line_start_offset) {
// copy the charater to _line_buffer
char c = _data[_line_start_offset];
*dst_ptr = c;
// update the line_size
line_size++;
// update read/write positions
dst_ptr++;
_line_start_offset = (_line_start_offset+1)&(_size-1);
}
// null-terminate the string
*dst_ptr = 0;
if (ctrl_is_at_beginning_of_data) {
_read_offset = _scan_offset;
}
return _line_buffer;
} // end if (found_control)
if (control_only) {
line_size = 0;
return nullptr;
}
// skip sections will always start at the beginning of a line
// handle this, even with no line, in case we're ignoring a huge too-long line
_skip_sections.skip(_read_offset);
if (_lines_found == 0) {
// nothing to return
line_size = 0;
return nullptr;
}
// By the time we get here, we know we have at least one line in _data.
if (_data[_read_offset] == 0) {
debug_trap("readline() read ran into NULL");
}
// scan past any leftover CR or LF from the previous line
char c = _data[_read_offset];
while ((c == '\n') || (c == '\r')) {
_read_offset = (_read_offset+1)&(_size-1);
if (_scan_offset == _read_offset) {
debug_trap("readline() read ran into scan (2)");
}
// this also counts as the beginning of a line
_skip_sections.skip(_read_offset);
c = _data[_read_offset];
}
while (line_size < (_line_buffer_size - 1)) {
_read_offset = (_read_offset+1)&(_size-1);
if ( c == '\r' ||
c == '\n'
) {
break;
}
line_size++;
*dst_ptr = c;
// update read/write positions
dst_ptr++;
c = _data[_read_offset];
}
if (line_size == (_line_buffer_size - 1)) {
// add a line-ending
*dst_ptr++ = '\n';
}
--_lines_found;
_restartTransfer();
// null-terminate the string
*dst_ptr = 0;
return _line_buffer;
}; // readline
// this is called from flushRead()
void flush() {
parent_type::flush();
_scan_offset = _read_offset;
// This is similar to the % "queue flush" handling above, except we flush
// the scan to the to the read (which was just set tot he write by the parent),
// not the other way around.
// record that we have 0 lines (of data) in the buffer
_lines_found = 0;
// and clear out any skip sections we have
while (!_skip_sections.isEmpty()) {
_skip_sections.popSkip();
}
}; // flush
bool flushToCommand() {
if (!_last_returned_a_control) {
return false;
}
// Things that must be managed here:
// * _read_offset -- we're skipping data
// * _lines_found -- we shouldn't have any lines "left"
// * _skip_sections -- there's nothing to skip, we just did
// Things that won't be changed (further):
// * _scan_offset -- we're not changing past where it's scanned
// * _line_start_offset -- we've already adjusted it
// * _at_start_of_line -- should always be true when we're here
// Note that we DO NOT call parent::flush() here. That will toss data
// we haven't scanned yet, beyond where we got the command we want to
// flush to.
// move the read buffer up to where we ended scanning
_read_offset = _scan_offset;
// record that we have 0 lines (of data) in the buffer
_lines_found = 0;
// and clear out any skip sections we have
while (!_skip_sections.isEmpty()) {
_skip_sections.popSkip();
}
_last_returned_a_control = false;
return true;
}; // flush
}; // LineRXBuffer
/* xioDeviceWrapper
* Implements a xioDeviceWrapperBase around a Device. The Device must implement:
* For RXBuffer:
* const base_type* getRXTransferPosition()
* void setRXTransferDoneCallback(std::function &&callback)
* bool startRXTransfer(char *&buffer, uint16_t length)
* For TXBuffer:
* const base_type* getTXTransferPosition()
* void setTXTransferDoneCallback(std::function &&callback)
* bool startTXTransfer(char *&buffer, uint16_t length)
* For xioDeviceWrapper:
* void setConnectionCallback(std::function &&callback)
*/
template
struct xioDeviceWrapper : xioDeviceWrapperBase { // describes a device for reading and writing
Device _dev;
// TODO - make _buffer_size, _header_count, and _line_buffer_size configurable
LineRXBuffer<1024, Device> _rx_buffer;
TXBuffer<1024, Device> _tx_buffer;
xioDeviceWrapper(Device dev, uint8_t _caps) : xioDeviceWrapperBase(_caps), _dev{dev}, _rx_buffer{_dev}, _tx_buffer{_dev}
{
// _dev->setDataAvailableCallback([&](const size_t &length) {
//
// });
};
void init() {
_dev->setConnectionCallback([&](bool connected) { // lambda function
connectedStateChanged(connected);
});
_rx_buffer.init();
_tx_buffer.init();
};
void flush() final {
_tx_buffer.flush();
return _dev->flush();
}
void flushRead() final {
// Flush out any partially or wholly read lines being stored:
_rx_buffer.flush();
_flushLine();
return _dev->flushRead();
}
void _flushLine() {
// TODO: Call to flush the RX buffer line structures
};
bool flushToCommand() final {
return _rx_buffer.flushToCommand();
}
virtual int16_t write(const char *buffer, int16_t len) final {
if (!isConnected()) {
return -1;
}
return _tx_buffer.write(buffer, len);
}
virtual char *readline(devflags_t limit_flags, uint16_t &size) final {
if ((limit_flags & flags) && isConnected()) {
return _rx_buffer.readline(!(limit_flags & DEV_IS_DATA), size);
}
size = 0;
return NULL;
};
void connectedStateChanged(bool connected) {
if (connected) {
if (isNotConnected()) {
// USB0 or UART has just connected
// If one of the devices isAlwaysDataAndCtrl():
// We treat *it* as if it's the only device connected.
// We treat *the other devices* as if it's NOT connected.
// Case 1: This is the first channel to connect -
// set it as CTRL+DATA+PRIMARY channel
// mark all isMutedAsSecondary() as MUTED, and call controller_set_muted(true) if needed
// Case 2: This is the second (or later) channel to connect -
// Case 2a: This device is !isMuteAsSecondary()
// set it as DATA channel, remove DATA flag from PRIMARY channel
// mark all isMutedAsSecondary() as MUTED, and call controller_set_muted(true) if needed
// ... inactive channels are counted as closed
// Case 2b: This devices isMuteAsSecondary(), and needs to be "muted."
// set it as a MUTED channel, call controller_set_connected(true)
// then controller_set_muted(true)
flush(); // toss anything that has been written so far.
setAsConnectedAndReady();
if (isAlwaysDataAndCtrl()) { // Case 1 (ignoring others)
setActive();
controller_set_connected(true);
// Case 2b (not ignoring others)
if (isMuteAsSecondary() && xio.othersConnected(this)) {
controller_set_muted(true); // something was muted
}
return;
}
if (!xio.othersConnected(this)) {
// Case 1
setAsPrimaryActiveDualRole();
// report that there is now have a connection (only for the first one)
controller_set_connected(true);
// make sure secondary channels (that don't show up in isConnected) are muted
if (xio.checkMutedSecondaryChannels()) {
controller_set_muted(true); // something was muted
}
#if MARLIN_COMPAT_ENABLED == true
// start the "fake bootloader" to signal the Host that Marlin (mode) is operating
_rx_buffer.startFakeBootloaderMode();
#endif
}
else if (isMuteAsSecondary()) { // Case 2b
setAsMuted();
controller_set_connected(true); // it DID just just get connected
controller_set_muted(true); // but it muted it too
}
else { // Case 2a
xio.removeDataFromPrimary();
if (xio.checkMutedSecondaryChannels()) {
controller_set_muted(true); // something was muted
}
setAsActiveData();
}
} // flags & DEV_IS_DISCONNECTED
} else { // disconnected
if (isConnected()) {
//USB0 has just disconnected
//Case 1: This channel disconnected while it was a ctrl+data channel (and no other channels are open) -
// finalize this channel, unmute muted channels
//Case 2: This channel disconnected while it was a primary ctrl channel (and other channels are open) -
// finalize this channel, unmute muted channels, deactivate other channels
//Case 3: This channel disconnected while it was a non-primary ctrl channel (and other channels are open) -
// finalize this channel, leave other channels alone
//Case 4: This channel disconnected while it was a data channel (and other channels are open, including a primary)
// finalize this channel, set primary channel as a CTRL+DATA channel if this was the last data channel
//Case 5a: This channel disconnected while it was inactive
//Case 5b: This channel disconnected when it's always present
// don't need to do anything!
//... inactive channels are counted as closed
devflags_t oldflags = flags;
clearFlags();
flush();
flushRead();
if (checkForNotActive(oldflags) || isAlwaysDataAndCtrl()) {
// Case 5a, 5b
} else if (checkForCtrlAndData(oldflags) || !xio.othersConnected(this)) {
// Case 1
if (xio.deactivateAndUnmuteChannels()) {
controller_set_muted(false); // something was unmuted
} else {
controller_set_connected(false);
}
} else if (checkForCtrlAndPrimary(oldflags)) {
// Case 2
if (xio.deactivateAndUnmuteChannels()) {
controller_set_muted(false); // something was unmuted
}
} else if (checkForCtrl(oldflags)) {
// Case 3
} else if (checkForData(oldflags)) {
// Case 4
xio.removeDataFromPrimary();
}
} // flags & DEV_IS_CONNECTED
}
};
#if MARLIN_COMPAT_ENABLED == true
void exitFakeBootloaderMode() override {
_rx_buffer.exitFakeBootloaderMode();
};
#endif
};
// Specialization for xio_flash_file -- we don't need most of the structure around a Device for xio_flash_file
template
struct xioFlashFileDeviceWrapper : xioDeviceWrapperBase { // describes a device for reading and writing
xio_flash_file *_current_file = nullptr;
char _line_buffer[_line_buffer_size]; // hold exactly one line to return -- flash files are read-only, so we copy it
xioFlashFileDeviceWrapper() : xioDeviceWrapperBase(DEV_CAN_READ | DEV_IS_ALWAYS_BOTH)
{
};
bool sendFile(xio_flash_file &new_file) {
if (nullptr != _current_file) {
return false; // we're still sending a file
}
_current_file = &new_file;
_current_file->reset();
setActive();
return true;
}
void init() {
};
void flush() final {
// nothing to do
}
void flushRead() final {
// to flush the file, just forget about it
// next time it's used it'll get reset
_current_file = nullptr;
cs.responses_suppressed = false;
}
bool flushToCommand() final {
// the end of the file is the next "command"
_current_file = nullptr;
cs.responses_suppressed = false;
return false;
}
int16_t write(const char *buffer, int16_t len) final {
return -1;
}
char *readline(devflags_t limit_flags, uint16_t &line_size) final {
if (nullptr == _current_file) {
line_size = 0;
return nullptr;
}
const char *from = _current_file->readline(!(limit_flags & DEV_IS_DATA), line_size);
if ((nullptr == from) && (_current_file->isDone())) {
// all done sending this file, "close" it
_current_file = nullptr;
cs.responses_suppressed = false;
clearActive();
return nullptr;
}
char *dst_ptr = _line_buffer;
uint16_t count = std::min(line_size, uint16_t(_line_buffer_size - 2));
while (count--) {
*dst_ptr++ = *from++;
}
// null-terminate the string
*dst_ptr = 0;
cs.responses_suppressed = true;
return _line_buffer;
};
};
xioFlashFileDeviceWrapper<> flashFileWrapper {};
// ALLOCATIONS
// Declare a device wrapper class for SerialUSB and SerialUSB1
#if XIO_HAS_USB == 1
xioDeviceWrapper serialUSB0Wrapper HOT_DATA {
&SerialUSB,
(DEV_CAN_READ | DEV_CAN_WRITE | DEV_CAN_BE_CTRL | DEV_CAN_BE_DATA)
};
#if USB_SERIAL_PORTS_EXPOSED == 2
xioDeviceWrapper serialUSB1Wrapper HOT_DATA {
&SerialUSB1,
(DEV_CAN_READ | DEV_CAN_WRITE | DEV_CAN_BE_CTRL | DEV_CAN_BE_DATA)
};
#endif
#endif // XIO_HAS_USB
#if XIO_HAS_UART==1
#if defined(XIO_UART_MUTES_WHEN_USB_CONNECTED) && (XIO_UART_MUTES_WHEN_USB_CONNECTED==1)
constexpr devflags_t _serial0ExtraFlags = DEV_IS_ALWAYS_BOTH | DEV_IS_MUTE_SECONDARY;
#else
constexpr devflags_t _serial0ExtraFlags = DEV_IS_ALWAYS_BOTH;
#endif
xioDeviceWrapper serial0Wrapper HOT_DATA {
&Serial,
(DEV_CAN_READ | DEV_CAN_WRITE | _serial0ExtraFlags)
};
#endif // XIO_HAS_UART
// Define the xio singleton (and initialize it to hold our two deviceWrappers)
//xio_t xio = { &serialUSB0Wrapper, &serialUSB1Wrapper };
xio_t xio = {
&flashFileWrapper,
#if XIO_HAS_USB == 1
&serialUSB0Wrapper,
#if USB_SERIAL_PORTS_EXPOSED == 2
&serialUSB1Wrapper,
#endif
#endif // XIO_HAS_USB
#if XIO_HAS_UART == 1
&serial0Wrapper
#endif
};
/**** CODE ****/
/*
* xio_init()
*
* A lambda function closure is provided for trapping connection state changes from USB devices.
* The function is installed as a callback from the lower USB layers. It is called only on edges
* (connect/disconnect transitions). 'Connected' is true if the USB channel has just connected,
* false if it has just disconnected. It is only called on an edge when it changes - so you
* shouldn't see two back-to-back connected=true calls with the same callback.
*
* See here for some good info on lambda functions in C++
* http://www.cprogramming.com/c++11/c++11-lambda-closures.html
*/
void xio_init()
{
board_xio_init();
#if XIO_HAS_USB == 1
serialUSB0Wrapper.init();
#if USB_SERIAL_PORTS_EXPOSED == 2
serialUSB1Wrapper.init();
#endif
#endif
#if XIO_HAS_UART == 1
serial0Wrapper.init();
#endif
}
stat_t xio_test_assertions()
{
if ((BAD_MAGIC(xio.magic_start)) || (BAD_MAGIC(xio.magic_end))) {
return(cm_panic(STAT_XIO_ASSERTION_FAILURE, "xio_test_assertions()"));
}
return (STAT_OK);
}
/*
* write() - write a buffer to a device
*/
size_t xio_write(const char *buffer, size_t size, bool only_to_muted /*= false*/)
{
return xio.write(buffer, size, only_to_muted);
}
/*
* xio_readline() - read a complete line from a device
* xio_writeline() - write a complete line to control device
*
* Defers to xio.readline(), etc.
*/
char *xio_readline(devflags_t &flags, uint16_t &size)
{
return xio.readline(flags, size);
}
int16_t xio_writeline(const char *buffer, bool only_to_muted /*= false*/)
{
return xio.writeline(buffer, only_to_muted);
}
/*
* write() - return true of the device is currently "connected" (there's a fair bit of interpretation)
*/
bool xio_connected()
{
return xio.connected();
}
/*
* xio_send_file() - send the contents of a xio_flash_file - returns false if there's already one sending
*/
bool xio_send_file(xio_flash_file &file) {
return flashFileWrapper.sendFile(file);
}
/*
* xio_flush_to_command() - clear the last read channel up until the command that was read
*/
void xio_flush_to_command() {
return xio.flushToCommand();
}
/*
* xio_flush_device() - flush reads on all devices except those that are active or have flags provided
*/
void xio_flush_device(devflags_t &flags)
{
xio.flushDevice(flags);
}
#if MARLIN_COMPAT_ENABLED == true
/*
* xio_end_fake_bootloader() - end the fake bootloader mode
*/
void xio_exit_fake_bootloader() {
return xio.exitFakeBootloaderMode();
}
#endif
/***********************************************************************************
* newlib-nano support functions
* Here we wire printf to xio
***********************************************************************************/
int _write( int file, char *ptr, int len )
{
return xio_write(ptr, len);
}
/***********************************************************************************
* CONFIGURATION AND INTERFACE FUNCTIONS
* Functions to get and set variables from the cfgArray table
***********************************************************************************/
/*
* xio_set_spi() = 0=disable, 1=enable
*/
//stat_t xio_set_spi(nvObj_t *nv)
//{
// xio.spi_state = (uint8_t)nv->value;
//
// if (fp_EQ(nv->value, SPI_ENABLE)) {
// spi_miso_pin.setMode(kOutput);
// spi_mosi_pin.setMode(kOutput);
// spi_sck_pin.setMode(kOutput);
//
// } else if (fp_EQ(nv->value, SPI_DISABLE)) {
// spi_miso_pin.setMode(kInput);
// spi_mosi_pin.setMode(kInput);
// spi_sck_pin.setMode(kInput);
// }
// return (STAT_OK);
//}
/***********************************************************************************
* TEXT MODE SUPPORT
* Functions to print variables from the cfgArray table
***********************************************************************************/
#ifdef __TEXT_MODE
static const char fmt_spi[] = "[spi] SPI state%20d [0=disabled,1=enabled]\n";
void xio_print_spi(nvObj_t *nv) { text_print(nv, fmt_spi);} // TYPE_INT
#endif // __TEXT_MODE