/* * controller.cpp - g2core controller and top level parser * This file is part of the g2core project * * Copyright (c) 2010 - 2019 Alden S. Hart, Jr. * Copyright (c) 2013 - 2019 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. */ #include "g2core.h" // #1 #include "config.h" // #2 #include "controller.h" #include "json_parser.h" #include "text_parser.h" #include "gcode.h" #include "canonical_machine.h" #include "plan_arc.h" #include "planner.h" #include "stepper.h" #include "temperature.h" #include "encoder.h" #include "hardware.h" #include "gpio.h" #include "report.h" #include "help.h" #include "util.h" #include "xio.h" #include "settings.h" #include "persistence.h" #include "safety_manager.h" #include "MotatePower.h" #if MARLIN_COMPAT_ENABLED == true #include "marlin_compatibility.h" #endif /**************************************************************************************** **** STRUCTURE ALLOCATIONS ************************************************************* ****************************************************************************************/ controller_t cs; // controller state structure /**************************************************************************************** **** STATICS AND LOCALS **************************************************************** ****************************************************************************************/ static void _controller_HSM(void); static stat_t _led_indicator(void); // twiddle the LED indicator static stat_t _safety_handler(void); // new (replaces _interlock_estop_handler) static stat_t _limit_switch_handler(void); // revised for new GPIO code static void _init_assertions(void); static stat_t _test_assertions(void); static stat_t _test_system_assertions(void); static stat_t _sync_to_planner(void); static stat_t _sync_to_tx_buffer(void); static stat_t _dispatch_command(void); static stat_t _dispatch_control(void); static void _dispatch_kernel(const devflags_t flags); static stat_t _controller_state(void); // manage controller state transitions static Motate::OutputPin safe_pin; gpioDigitalInputHandler _limit_input_handler { [](const bool state, const inputEdgeFlag edge, const uint8_t triggering_pin_number) { if (edge != INPUT_EDGE_LEADING) { return GPIO_NOT_HANDLED; } cm->limit_requested = triggering_pin_number; return GPIO_NOT_HANDLED; // allow others to see this notice }, 5, // priority nullptr // next - nullptr to start with }; /*********************************************************************************** **** CODE ************************************************************************* ***********************************************************************************/ /* * controller_init() - controller init */ void controller_init() { // preserve settable parameters that may have already been set up commMode comm_mode = cs.comm_mode; memset(&cs, 0, sizeof(controller_t)); // clear all values, job_id's, pointers and status _init_assertions(); cs.comm_mode = comm_mode; // restore parameters cs.fw_build = G2CORE_FIRMWARE_BUILD; // set up identification cs.fw_version = G2CORE_FIRMWARE_VERSION; cs.controller_state = CONTROLLER_STARTUP; // ready to run startup lines if (xio_connected()) { cs.controller_state = CONTROLLER_CONNECTED; } // IndicatorLed.setFrequency(100000); din_handlers[INPUT_ACTION_LIMIT].registerHandler(&_limit_input_handler); safety_manager->init(); } void controller_request_enquiry() { sprintf(cs.out_buf, "{\"ack\":true}\n"); xio_writeline(cs.out_buf); } /* * controller_run() - MAIN LOOP - top-level controller * * The order of the dispatched tasks is very important. * Tasks are ordered by increasing dependency (blocking hierarchy). * Tasks that are dependent on completion of lower-level tasks must be * later in the list than the task(s) they are dependent upon. * * Tasks must be written as continuations as they will be called repeatedly, * and are called even if they are not currently active. * * The DISPATCH macro calls the function and returns to the controller parent * if not finished (STAT_EAGAIN), preventing later routines from running * (they remain blocked). Any other condition - OK or ERR - drops through * and runs the next routine in the list. * * A routine that had no action (i.e. is OFF or idle) should return STAT_NOOP */ void controller_run() { while (true) { _controller_HSM(); } } #define DISPATCH(func) if (func == STAT_EAGAIN) return; static void _controller_HSM() { //----- Interrupt Service Routines are the highest priority controller functions ----// // See hardware.h for a list of ISRs and their priorities. // //----- kernel level ISR handlers ----(flags are set in ISRs)------------------------// // Order is important, and line breaks indicate dependency groups DISPATCH(hardware_periodic()); // give the hardware a chance to do stuff DISPATCH(_led_indicator()); // blink LEDs at the current rate DISPATCH(_safety_handler()); // invoke shutdown DISPATCH(temperature_callback()); // makes sure temperatures are under control DISPATCH(_limit_switch_handler()); // invoke limit switch DISPATCH(_controller_state()); // controller state management DISPATCH(_test_system_assertions()); // system integrity assertions DISPATCH(_dispatch_control()); // read any control messages prior to executing cycles //----- planner hierarchy for gcode and cycles ---------------------------------------// DISPATCH(st_motor_power_callback()); // stepper motor power sequencing DISPATCH(sr_status_report_callback()); // conditionally send status report DISPATCH(qr_queue_report_callback()); // conditionally send queue report // these 3 must be in this exact order: DISPATCH(mp_planner_callback()); // motion planner DISPATCH(cm_operation_runner_callback()); // operation action runner DISPATCH(cm_arc_callback(cm)); // arc generation runs as a cycle above lines DISPATCH(cm_homing_cycle_callback()); // homing cycle operation (G28.2) DISPATCH(cm_probing_cycle_callback()); // probing cycle operation (G38.2) DISPATCH(cm_jogging_cycle_callback()); // jog cycle operation DISPATCH(cm_deferred_write_callback()); // persist G10 changes when not in machining cycle DISPATCH(cm_feedhold_command_blocker()); // blocks new Gcode from arriving while in feedhold #if MARLIN_COMPAT_ENABLED == true DISPATCH(marlin_callback()); // handle Marlin stuff - may return EAGAIN, must be after planner_callback! #endif DISPATCH(write_persistent_values_callback()); //----- command readers and parsers --------------------------------------------------// DISPATCH(_sync_to_planner()); // ensure there is at least one free buffer in planning queue DISPATCH(_sync_to_tx_buffer()); // sync with TX buffer (pseudo-blocking) DISPATCH(_dispatch_command()); // MUST BE LAST - read and execute next command } /**************************************************************************************** * command dispatchers * _dispatch_control - entry point for control-only dispatches * _dispatch_command - entry point for control and data dispatches * _dispatch_kernel - core dispatch routines * * Reads next command line and dispatches to relevant parser or action * * Note: The dispatchers must only read and process a single line from the * RX queue before returning control to the main loop. */ static stat_t _dispatch_control() { if (cs.controller_state == CONTROLLER_READY) { devflags_t flags = DEV_IS_CTRL; if ((cs.bufp = xio_readline(flags, cs.linelen)) != NULL) { _dispatch_kernel(flags); } } return (STAT_OK); } static stat_t _dispatch_command() { if (cs.controller_state == CONTROLLER_READY) { devflags_t flags = DEV_IS_BOTH | DEV_IS_MUTED; // expressly state we'll handle muted devices if ((!mp_planner_is_full(mp)) && (cs.bufp = xio_readline(flags, cs.linelen)) != NULL) { _dispatch_kernel(flags); } } return (STAT_OK); } static void _dispatch_kernel(const devflags_t flags) { stat_t status; if (flags & DEV_IS_MUTED) { status = STAT_INPUT_FROM_MUTED_CHANNEL_ERROR; nv_reset_nv_list(); // get a fresh nvObj list nv_add_string((const char *)"msg", "lines from muted devices are ignored"); nv_print_list(status, TEXT_NO_PRINT, JSON_RESPONSE_TO_MUTED_FORMAT); // It's possible to let some stuff through, but that's not happening yet. return; } #if MARLIN_COMPAT_ENABLED == true // marlin_handle_fake_stk500 returns true if it responded to a stk500v2 message if (marlin_handle_fake_stk500(cs.bufp)) { js.json_mode = MARLIN_COMM_MODE; sr.status_report_verbosity = SR_OFF; qr.queue_report_verbosity = QR_OFF; return; } #endif while ((*cs.bufp == SPC) || (*cs.bufp == TAB)) { // position past any leading whitespace cs.bufp++; } strncpy(cs.saved_buf, cs.bufp, SAVED_BUFFER_LEN-1); // save input buffer for reporting if (*cs.bufp == NUL) { // blank line - just a CR or the 2nd termination in a CRLF if (js.json_mode == TEXT_MODE) { text_response(STAT_OK, cs.saved_buf); return; } } // trap single character commands if (*cs.bufp == '!') { cm_request_feedhold(FEEDHOLD_TYPE_ACTIONS, FEEDHOLD_EXIT_CYCLE); } else if (*cs.bufp == '~') { cm_request_cycle_start(); } else if (*cs.bufp == '%') { cm_request_queue_flush(); xio_flush_to_command(); } else if (*cs.bufp == EOT) { cm_request_job_kill(); xio_flush_to_command(); } else if (*cs.bufp == ENQ) { controller_request_enquiry(); } else if (*cs.bufp == CAN) { hw_hard_reset(); } // reset immediately else if (*cs.bufp == '{') { // process as JSON mode if (cs.comm_mode == AUTO_MODE) { js.json_mode = JSON_MODE; // switch to JSON mode } cs.comm_request_mode = JSON_MODE; // mode of this command json_parser(cs.bufp); } #ifdef __TEXT_MODE else if (strchr("$?Hh", *cs.bufp) != NULL) { // process as text mode if (cs.comm_mode == AUTO_MODE) { js.json_mode = TEXT_MODE; } // switch to text mode cs.comm_request_mode = TEXT_MODE; // mode of this command status = text_parser(cs.bufp); if (js.json_mode == TEXT_MODE) { // needed in case mode was changed by $EJ=1 text_response(status, cs.saved_buf); } } else if (js.json_mode == TEXT_MODE) { // anything else is interpreted as Gcode cs.comm_request_mode = TEXT_MODE; // mode of this command text_response(gcode_parser(cs.bufp), cs.saved_buf); } #endif #if MARLIN_COMPAT_ENABLED == true else if (js.json_mode == MARLIN_COMM_MODE) { // handle marlin-specific protocol gcode cs.comm_request_mode = MARLIN_COMM_MODE; // mode of this command marlin_response(gcode_parser(cs.bufp), cs.saved_buf); } #endif else { // anything else is interpreted as Gcode cs.comm_request_mode = JSON_MODE; // mode of this command // this optimization bypasses the standard JSON parser and does what it needs directly nvObj_t *nv = nv_reset_nv_list(); // get a fresh nvObj list strcpy(nv->token, "gc"); // label is as a Gcode block (do not get an index - not necessary) nv_copy_string(nv, cs.bufp); // copy the Gcode line nv->valuetype = TYPE_STRING; status = gcode_parser(cs.bufp); #if MARLIN_COMPAT_ENABLED == true if (js.json_mode == MARLIN_COMM_MODE) { // in case a marlin-specific M-code was found cs.comm_request_mode = MARLIN_COMM_MODE; // mode of this command // We are switching to marlin_comm_mode, kill status reports and queue reports sr.status_report_verbosity = SR_OFF; qr.queue_report_verbosity = QR_OFF; marlin_response(status, cs.saved_buf); return; } #endif nv_print_list(status, TEXT_NO_PRINT, JSON_RESPONSE_FORMAT); sr_request_status_report(SR_REQUEST_TIMED); // generate incremental status report to show any changes } } /**** Local Functions ******************************************************************/ /* * _reset_comms_mode() - reset the communications mode (and other effected settings) after connection or disconnection */ void _reset_comms_mode() { // reset the communications mode cs.comm_mode = COMM_MODE; js.json_mode = (COMM_MODE < AUTO_MODE) ? COMM_MODE : JSON_MODE; #if MARLIN_COMPAT_ENABLED == true mst.marlin_flavor = false; #endif sr.status_report_verbosity = STATUS_REPORT_VERBOSITY; qr.queue_report_verbosity = QUEUE_REPORT_VERBOSITY; } /* CONTROLLER STATE MANAGEMENT * _controller_state() - manage controller connection, startup, and other state changes */ Motate::Timeout _connection_timeout; static stat_t _controller_state() { if (cs.controller_state == CONTROLLER_CONNECTED) { // first time through after reset cs.controller_state = CONTROLLER_STARTUP; // This is here just to put a small delay in before the startup message. #if MARLIN_COMPAT_ENABLED == true // For Marlin compatibility, we need this to be long enough for the UI to say something and reveal // if it's a Marlin-compatible UI. if (xio_connected()) { // xio_connected will only return true for USB and other non-permanent connections _connection_timeout.set(2000); } else { _connection_timeout.set(10); } #else _connection_timeout.set(1); #endif } // first time through after reset if ((cs.controller_state == CONTROLLER_STARTUP) && (!_connection_timeout.isSet() || _connection_timeout.isPast())) { if (MARLIN_COMM_MODE != js.json_mode) { // MARLIN_COMM_MODE is always defined, just not always used _reset_comms_mode(); } cs.controller_state = CONTROLLER_READY; rpt_print_system_ready_message(); } return (STAT_OK); } /* * controller_set_connected(bool) - hook for xio to tell the controller that we * have/don't have a connection. */ void controller_set_connected(bool is_connected) { // turn off reports while no-one's listening or we determine what dialect they speak sr.status_report_verbosity = SR_OFF; qr.queue_report_verbosity = QR_OFF; if (is_connected) { cs.controller_state = CONTROLLER_CONNECTED; // we JUST connected } else { // we just disconnected from the last device, we'll expect a banner again _reset_comms_mode(); cs.controller_state = CONTROLLER_NOT_CONNECTED; } } /* * controller_set_muted(bool) - hook for xio to tell the controller that we * have/don't have one or more muted devices. */ void controller_set_muted(bool is_muted) { // TODO: care about text mode if (is_muted) { // one channel just got muted. const bool only_to_muted = true; xio_writeline("{\"muted\":true}\n", only_to_muted); } else { // we're assuming anything that can be muted speaks g2core-dialect JSON _reset_comms_mode(); // something was just unmuted (usually because USB disconnected), tell it xio_writeline("{\"muted\":false}\n"); } } /* * controller_parse_control() - return true if command is a control (versus data) * Note: parsing for control is somewhat naiive. This will need to get better */ bool controller_parse_control(char *p) { if (strchr("{$?!~%Hh", *p) != NULL) { // a match indicates control line return (true); } return (false); } /* * _led_indicator() - blink an LED to show it we are normal, alarmed, or shut down */ static stat_t _led_indicator() { uint32_t blink_rate; if (cm_get_machine_state() == MACHINE_ALARM) { blink_rate = LED_ALARM_BLINK_RATE; } else if (cm_get_machine_state() == MACHINE_SHUTDOWN) { blink_rate = LED_SHUTDOWN_BLINK_RATE; } else if (cm_get_machine_state() == MACHINE_PANIC) { blink_rate = LED_PANIC_BLINK_RATE; } else { blink_rate = LED_NORMAL_BLINK_RATE; } if (blink_rate != cs.led_blink_rate) { cs.led_blink_rate = blink_rate; cs.led_timer = 0; } if (SysTickTimer.getValue() > cs.led_timer) { cs.led_timer = SysTickTimer.getValue() + cs.led_blink_rate; IndicatorLed.toggle(); } return (STAT_OK); } /* * _sync_to_tx_buffer() - return eagain if TX queue is backed up * _sync_to_planner() - return eagain if planner is not ready for a new command */ static stat_t _sync_to_tx_buffer() { return (STAT_OK); } static stat_t _sync_to_planner() { if (mp_planner_is_full(mp)) { // allow up to N planner buffers for this line return (STAT_EAGAIN); } return (STAT_OK); } /**************************************************************************************** * ALARM STATE HANDLERS * * _safety_handler() - handle safety stuff like shutdown and interlock * _limit_switch_handler() - shut down system if limit switch fired * * Some handlers return EAGAIN causing the control loop to never advance beyond that point. * * _interlock_handler() reacts the follwing ways: * - safety_interlock_requested == INPUT_EDGE_NONE is normal operation (no interlock) * - safety_interlock_requested == INPUT_EDGE_LEADING is interlock onset * - safety_interlock_requested == INPUT_EDGE_TRAILING is interlock offset */ static stat_t _safety_handler(void) { safety_manager->periodic_handler(); return(STAT_OK); } static stat_t _limit_switch_handler(void) { auto machine_state = cm_get_machine_state(); if ((machine_state != MACHINE_ALARM) && (machine_state != MACHINE_PANIC) && (machine_state != MACHINE_SHUTDOWN)) { safe_pin.toggle(); } if ((cm->limit_enable == true) && (cm->limit_requested != 0)) { char msg[10]; sprintf(msg, "input %d", (int)cm->limit_requested); cm->limit_requested = false; // clear limit request used here ^ cm_alarm(STAT_LIMIT_SWITCH_HIT, msg); } return (STAT_OK); } /**************************************************************************************** * _init_assertions() - initialize controller memory integrity assertions * _test_assertions() - check controller memory integrity assertions * _test_system_assertions() - check assertions for entire system */ static void _init_assertions() { cs.magic_start = MAGICNUM; cs.magic_end = MAGICNUM; } static stat_t _test_assertions() { if ((cs.magic_start != MAGICNUM) || (cs.magic_end != MAGICNUM)) { return(cm_panic(STAT_CONTROLLER_ASSERTION_FAILURE, "controller_test_assertions()")); } return (STAT_OK); } stat_t _test_system_assertions() { // these functions will panic if an assertion fails _test_assertions(); // controller assertions (local) config_test_assertions(); canonical_machine_test_assertions(&cm1); canonical_machine_test_assertions(&cm2); planner_assert(&mp1); planner_assert(&mp2); stepper_test_assertions(); encoder_test_assertions(); xio_test_assertions(); return (STAT_OK); }