/* * 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