diff --git a/ESPWebDAV.cpp b/ESPWebDAV.cpp index 3205680..63666d9 100644 --- a/ESPWebDAV.cpp +++ b/ESPWebDAV.cpp @@ -24,7 +24,18 @@ bool ESPWebDAV::init(int chipSelectPin, SPISettings spiSettings, int serverPort) return sd.begin(chipSelectPin, spiSettings); } +// ------------------------ +bool ESPWebDAV::initSD(int chipSelectPin, SPISettings spiSettings) { + // initialize the SD card + return sd.begin(chipSelectPin, spiSettings); +} +// ------------------------ +bool ESPWebDAV::startServer() { +// ------------------------ + // start the wifi server + server->begin(); +} // ------------------------ void ESPWebDAV::handleNotFound() { @@ -555,3 +566,4 @@ void ESPWebDAV::handleDelete(ResourceType resource) { send("200 OK", NULL, ""); } +ESPWebDAV dav; diff --git a/ESPWebDAV.h b/ESPWebDAV.h index d64835a..89e06a3 100644 --- a/ESPWebDAV.h +++ b/ESPWebDAV.h @@ -1,12 +1,15 @@ #include #include -// debugging -// #define DBG_PRINT(...) { Serial.print(__VA_ARGS__); } -// #define DBG_PRINTLN(...) { Serial.println(__VA_ARGS__); } -// production -#define DBG_PRINT(...) { } -#define DBG_PRINTLN(...) { } +#define DEBUG + +#ifdef DEBUG + #define DBG_PRINT(...) { Serial.print(__VA_ARGS__); } + #define DBG_PRINTLN(...) { Serial.println(__VA_ARGS__); } +#else + #define DBG_PRINT(...) {} + #define DBG_PRINTLN(...) {} +#endif // constants for WebServer #define CONTENT_LENGTH_UNKNOWN ((size_t) -1) @@ -20,6 +23,8 @@ enum DepthType { DEPTH_NONE, DEPTH_CHILD, DEPTH_ALL }; class ESPWebDAV { public: bool init(int chipSelectPin, SPISettings spiSettings, int serverPort); + bool initSD(int chipSelectPin, SPISettings spiSettings); + bool startServer(); bool isClientWaiting(); void handleClient(String blank = ""); void rejectClient(String rejectMessage); @@ -76,7 +81,4 @@ class ESPWebDAV { int _contentLength; }; - - - - +extern ESPWebDAV dav; diff --git a/ESPWebDAV.ino b/ESPWebDAV.ino new file mode 100644 index 0000000..885de86 --- /dev/null +++ b/ESPWebDAV.ino @@ -0,0 +1,96 @@ +// Using the WebDAV server with Rigidbot 3D printer. +// Printer controller is a variation of Rambo running Marlin firmware + +#include "serial.h" +#include "parser.h" +#include "config.h" +#include "network.h" +#include "gcode.h" +#include "sdControl.h" + +// LED is connected to GPIO2 on this board +#define INIT_LED {pinMode(2, OUTPUT);} +#define LED_ON {digitalWrite(2, LOW);} +#define LED_OFF {digitalWrite(2, HIGH);} + +// ------------------------ +void setup() { + SERIAL_INIT(115200); + INIT_LED; + blink(); + + sdcontrol.setup(); + + // ----- WIFI ------- + if(config.load() == 1) { // Connected before + if(!network.start()) { + SERIAL_ECHOLN("Connect fail, please check your INI file or set the wifi config and connect again"); + SERIAL_ECHOLN("- M50: Set the wifi ssid , 'M50 ssid-name'"); + SERIAL_ECHOLN("- M51: Set the wifi password , 'M51 password'"); + SERIAL_ECHOLN("- M52: Start to connect the wifi"); + SERIAL_ECHOLN("- M53: Check the connection status"); + } + } + else { + SERIAL_ECHOLN("Welcome to FYSETC: www.fysetc.com"); + SERIAL_ECHOLN("Please set the wifi config first"); + SERIAL_ECHOLN("- M50: Set the wifi ssid , 'M50 ssid-name'"); + SERIAL_ECHOLN("- M51: Set the wifi password , 'M51 password'"); + SERIAL_ECHOLN("- M52: Start to connect the wifi"); + SERIAL_ECHOLN("- M53: Check the connection status"); + } +} + +// ------------------------ +void loop() { + // handle the request + network.handle(); + + // Handle gcode + gcode.Handle(); + + // blink + statusBlink(); +} + +// ------------------------ +void blink() { +// ------------------------ + LED_ON; + delay(100); + LED_OFF; + delay(400); +} + +// ------------------------ +void errorBlink() { +// ------------------------ + for(int i = 0; i < 100; i++) { + LED_ON; + delay(50); + LED_OFF; + delay(50); + } +} + +void statusBlink() { + static unsigned long time = 0; + if(millis() > time + 1000 ) { + if(network.isConnecting()) { + LED_OFF; + } + else if(network.isConnected()) { + LED_ON; + delay(50); + LED_OFF; + } + else { + LED_ON; + } + time = millis(); + } + + // SPI bus not ready + //if(millis() < spiBlockoutTime) + // blink(); +} diff --git a/README.md b/README.md index 537e338..b9d2d0d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # WebDAV Server and a 3D Printer + This project is a WiFi WebDAV server using ESP8266 SoC. It maintains the filesystem on an SD card. Supports the basic WebDav operations - *PROPFIND*, *GET*, *PUT*, *DELETE*, *MKCOL*, *MOVE* etc. @@ -11,31 +12,93 @@ I am using this setup as a networked drive for 3D Printer running Marlin. Follow GCode can be directly uploaded from the slicer (Cura) to this remote drive, thereby simplifying the workflow. - ![Printer Hookup Diagram](PrinterHookup2.jpg) ## Dependencies: + 1. [ESP8266 Arduino Core version 2.4](https://github.com/esp8266/Arduino) 2. [SdFat library](https://github.com/greiman/SdFat) - ## Use: -Compile and upload the program to an ESP8266 module. ESP12-E was used for development and testing. -Connect the SPI bus lines to SD card. -ESP Module|SD Card ----|--- -GPIO13|MOSI -GPIO12|MISO -GPIO14|SCK -GPIO4|CS -GPIO5|CS Sense +### Compile and upload + +#### Compile + +If you don't want to update the firmware. You don't need to do this. Compile and upload the program to an ESP8266 module. + +- Open the project + + Download this project and open it with [arduino](https://www.arduino.cc/) software. + +- Add board manager link + + Add boards manager link: `https://arduino.esp8266.com/stable/package_esp8266com_index.json` to File->Preferences board manager, Documentation: https://arduino-esp8266.readthedocs.io/en/2.7.1/ + +- Select board + + Select Tools->boards->Generic ESP8285 Module. + +- Click the Arduino compile button + +#### Upload + +1. Pulg in the USB cable to your computer +2. Diag the switch on the module to `USB2UART` +3. Press and hold the module FLSH +4. Connect the USB cable to the module +5. Release the module FLSH button +6. Click the Arduino upload button + +### Config + +First you can see our video [here](https://www.youtube.com/watch?v=YAFAK-jPcOs). You have two ways to config the module. + +*note: The card should be formatted for Fat16 or Fat32* -The card should be formatted for Fat16 or Fat32 +#### Option 1: INI file -To access the drive from Windows, type ```\\esp_hostname_or_ip\DavWWWRoot``` at the Run prompt, or use Map Network Drive menu in Windows Explorer. +You can edit the example ```SETUP.INI``` file in ```ini``` folder, change the SSID and PASSWORD value. And then copy ```SETUP.INI``` file to your root SD card. Then insert it to the module. + +1. Turn the module option button to ```USB2UART``` +2. Open a COM software in your computer +3. Connect the module to your computer with USB cable +4. Open the software COM port + +you can see the module IP and other information. + +*note: if you miss the serial output, you can click the ```RST``` button in the module.* + +#### Option 2 : Command + +Insert your sdcard to the module. + +1. Turn the module option button to ```USB2UART``` +2. Open a COM software in your computer +3. Connect the module to your computer with USB cable +4. Open the software COM port + +And use the following command to connect the network or check the network status + + M50: Set the wifi ssid , 'M50 ssid-name' + M51: Set the wifi password , 'M51 password' + M52: Start to connect the wifi + M53: Check the connection status + +### Access + +#### windows + +To access the drive from Windows, type ```\\ip\DavWWWRoot``` at the Run prompt, this will show in serial output as our [video](https://www.youtube.com/watch?v=YAFAK-jPcOs) shows. + +Or use Map Network Drive menu in Windows Explorer. + +#### MAC + +Just need to use ```http://192.168.0.x``` in access network drive option ## References + Marlin Firmware - [http://marlinfw.org/](http://marlinfw.org/) Cura Slicer - [https://ultimaker.com/en/products/ultimaker-cura-software](https://ultimaker.com/en/products/ultimaker-cura-software) @@ -43,6 +106,3 @@ Cura Slicer - [https://ultimaker.com/en/products/ultimaker-cura-software](https: 3D Printer LCD and SD Card Interface - [http://reprap.org/wiki/RepRapDiscount_Full_Graphic_Smart_Controller](http://reprap.org/wiki/RepRapDiscount_Full_Graphic_Smart_Controller) LCD Schematics - [http://reprap.org/mediawiki/images/7/79/LCD_connect_SCHDOC.pdf](http://reprap.org/mediawiki/images/7/79/LCD_connect_SCHDOC.pdf) - - - diff --git a/config.cpp b/config.cpp new file mode 100644 index 0000000..24abee8 --- /dev/null +++ b/config.cpp @@ -0,0 +1,191 @@ +#include +#include +#include +#include +#include "pins.h" +#include "config.h" +#include "serial.h" +#include "sdControl.h" + +int Config::loadSD() { + SdFat sdfat; + + SERIAL_ECHOLN("Going to load config from INI file"); + + if(!sdcontrol.canWeTakeBus()) { + SERIAL_ECHOLN("Marlin is controling the bus"); + return -1; + } + sdcontrol.takeBusControl(); + + if(!sdfat.begin(SD_CS, SPI_FULL_SPEED)) { + SERIAL_ECHOLN("Initial SD failed"); + sdcontrol.relinquishBusControl(); + return -2; + } + + File file = sdfat.open("SETUP.INI", FILE_READ); + if (!file) { + SERIAL_ECHOLN("Open INI file failed"); + sdcontrol.relinquishBusControl(); + return -3; + } + + // Get SSID and PASSWORD from file + int rst = 0,step = 0; + String buffer,sKEY,sValue; + while (file.available()) { // check for EOF + buffer = file.readStringUntil('\n'); + if(buffer.length() == 0) continue; // Empty line + buffer.replace("\r", ""); // Delete all CR + int iS = buffer.indexOf('='); // Get the seperator + if(iS < 0) continue; // Bad line + sKEY = buffer.substring(0,iS); + sValue = buffer.substring(iS+1); + if(sKEY == "SSID") { + SERIAL_ECHOLN("INI file : SSID found"); + if(sValue.length()>0) { + memset(data.ssid,'\0',WIFI_SSID_LEN); + sValue.toCharArray(data.ssid,WIFI_SSID_LEN); + step++; + } + else { + rst = -4; + goto FAIL; + } + } + else if(sKEY == "PASSWORD") { + SERIAL_ECHOLN("INI file : PASSWORD found"); + if(sValue.length()>0) { + memset(data.psw,'\0',WIFI_PASSWD_LEN); + sValue.toCharArray(data.psw,WIFI_PASSWD_LEN); + step++; + } + else { + rst = -5; + goto FAIL; + } + } + else continue; // Bad line + } + if(step != 2) { // We miss ssid or password + //memset(data,) // TODO: do we need to empty the data? + SERIAL_ECHOLN("Please check your SSDI or PASSWORD in ini file"); + rst = -6; + goto FAIL; + } + + FAIL: + file.close(); + sdcontrol.relinquishBusControl(); + return rst; +} + +unsigned char Config::load() { + // Try to get the config from ini file + if(0 == loadSD()) + { + return 1; // Return as connected before + } + + SERIAL_ECHOLN("Going to load config from EEPROM"); + + EEPROM.begin(EEPROM_SIZE); + uint8_t *p = (uint8_t*)(&data); + for (int i = 0; i < sizeof(data); i++) + { + *(p + i) = EEPROM.read(i); + } + EEPROM.commit(); + + if(data.flag) { + SERIAL_ECHOLN("Going to use the old config to connect the network"); + } + SERIAL_ECHOLN("We didn't connect the network before"); + return data.flag; +} + +char* Config::ssid() { + return data.ssid; +} + +void Config::ssid(char* ssid) { + if(ssid == NULL) return; + strncpy(data.ssid,ssid,WIFI_SSID_LEN); +} + +char* Config::password() { + return data.psw; +} + +void Config::password(char* password) { + if(password == NULL) return; + strncpy(data.psw,password,WIFI_PASSWD_LEN); +} + +void Config::save(const char*ssid,const char*password) { + if(ssid ==NULL || password==NULL) + return; + + EEPROM.begin(EEPROM_SIZE); + data.flag = 1; + strncpy(data.ssid, ssid, WIFI_SSID_LEN); + strncpy(data.psw, password, WIFI_PASSWD_LEN); + uint8_t *p = (uint8_t*)(&data); + for (int i = 0; i < sizeof(data); i++) + { + EEPROM.write(i, *(p + i)); + } + EEPROM.commit(); +} + +void Config::save() { + if(data.ssid == NULL || data.psw == NULL) + return; + + EEPROM.begin(EEPROM_SIZE); + data.flag = 1; + uint8_t *p = (uint8_t*)(&data); + for (int i = 0; i < sizeof(data); i++) + { + EEPROM.write(i, *(p + i)); + } + EEPROM.commit(); +} + +// Save to ip address to sdcard +int Config::save_ip(const char *ip) { + SdFat sdfat; + + SERIAL_ECHOLN("Going to save config to ip.gcode file"); + + if(!sdcontrol.canWeTakeBus()) { + SERIAL_ECHOLN("Marlin is controling the bus"); + return -1; + } + sdcontrol.takeBusControl(); + + if(!sdfat.begin(SD_CS, SPI_FULL_SPEED)) { + SERIAL_ECHOLN("Initial SD failed"); + sdcontrol.relinquishBusControl(); + return -2; + } + + // Remove the old file + sdfat.remove("ip.gcode"); + + File file = sdfat.open("ip.gcode", FILE_WRITE); + if (!file) { + SERIAL_ECHOLN("Open ip file failed"); + sdcontrol.relinquishBusControl(); + return -3; + } + + // Get SSID and PASSWORD from file + char buf[21] = "M117 "; + strncat(buf,ip,15); + file.write(buf, 21); + file.close(); +} + +Config config; diff --git a/config.h b/config.h new file mode 100644 index 0000000..0101ad7 --- /dev/null +++ b/config.h @@ -0,0 +1,37 @@ +#ifndef _CONFIG_H_ +#define _CONFIG_H_ + +#include +#include + +#define WIFI_SSID_LEN 32 +#define WIFI_PASSWD_LEN 64 + +#define EEPROM_SIZE 512 + +typedef struct config_type +{ + unsigned char flag; // Was saved before? + char ssid[32]; + char psw[64]; +}CONFIG_TYPE; + +class Config { +public: + int loadSD(); + unsigned char load(); + char* ssid(); + void ssid(char* ssid); + char* password(); + void password(char* password); + void save(const char*ssid,const char*password); + void save(); + int save_ip(const char *ip); + +protected: + CONFIG_TYPE data; +}; + +extern Config config; + +#endif diff --git a/gcode.cpp b/gcode.cpp new file mode 100644 index 0000000..77faffb --- /dev/null +++ b/gcode.cpp @@ -0,0 +1,189 @@ +#include "gcode.h" +#include "config.h" +#include "parser.h" +#include "network.h" +#include "serial.h" +#include + +Gcode gcode; + +void Gcode::Handle() { + // Get the serial input + if (commands_in_queue < BUFSIZE) get_serial_commands(); + + if (commands_in_queue) { + process_next_command(); + + // The queue may be reset by a command handler or by code invoked by idle() within a handler + if (commands_in_queue) { + --commands_in_queue; + if (++cmd_queue_index_r >= BUFSIZE) cmd_queue_index_r = 0; + } + } +} + +/** + * Once a new command is in the ring buffer, call this to commit it + */ +void Gcode::_commit_command(bool say_ok) { + send_ok[cmd_queue_index_w] = say_ok; + if (++cmd_queue_index_w >= BUFSIZE) cmd_queue_index_w = 0; + commands_in_queue++; +} + +/** + * Copy a command from RAM into the main command buffer. + * Return true if the command was successfully added. + * Return false for a full buffer, or if the 'command' is a comment. + */ +bool Gcode::_enqueuecommand(const char* cmd, bool say_ok) { + if (*cmd == ';' || commands_in_queue >= BUFSIZE) return false; + strcpy(command_queue[cmd_queue_index_w], cmd); + _commit_command(say_ok); + return true; +} + +/** + * Get all commands waiting on the serial port and queue them. + * Exit when the buffer is full or when no more characters are + * left on the serial port. + */ +void Gcode::get_serial_commands() { + static char serial_line_buffer[MAX_CMD_SIZE]; + static bool serial_comment_mode = false; + + /** + * Loop while serial characters are incoming and the queue is not full + */ + int c; + while (commands_in_queue < BUFSIZE && (c = Serial.read()) >= 0) { + + char serial_char = c; + + /** + * If the character ends the line + */ + if (serial_char == '\n' || serial_char == '\r') { + + serial_comment_mode = false; // end of line == end of comment + + // Skip empty lines and comments + if (!serial_count) { continue; } + + serial_line_buffer[serial_count] = 0; // Terminate string + serial_count = 0; // Reset buffer + + char* command = serial_line_buffer; + + while (*command == ' ') command++; // Skip leading spaces + + // Add the command to the queue + _enqueuecommand(serial_line_buffer, true); + } + else if (serial_count >= MAX_CMD_SIZE - 1) { + // Keep fetching, but ignore normal characters beyond the max length + // The command will be injected when EOL is reached + } + else if (serial_char == '\\') { // Handle escapes + if ((c = MYSERIAL0.read()) >= 0 && !serial_comment_mode) // if we have one more character, copy it over + serial_line_buffer[serial_count++] = (char)c; + // otherwise do nothing + } + else { // it's not a newline, carriage return or escape char + if (serial_char == ';') serial_comment_mode = true; + if (!serial_comment_mode) serial_line_buffer[serial_count++] = serial_char; + } + } // queue has space, serial has data +} + +/** + * M50: Set the Wifi ssid + */ +void Gcode::gcode_M50() { + for (char *fn = parser.string_arg; *fn; ++fn); + config.ssid(parser.string_arg); + SERIAL_ECHO("ssid:"); + SERIAL_ECHOLN(config.ssid()); +} + +/** + * M50: Set the Wifi password + */ +void Gcode::gcode_M51() { + for (char *fn = parser.string_arg; *fn; ++fn) if (*fn == ' ') *fn = '\0'; + config.password(parser.string_arg); + SERIAL_ECHO("password:"); + SERIAL_ECHOLN(config.password()); +} + +/** + * M52: Connect the wifi + */ +void Gcode::gcode_M52() { + if(!network.start()) { + SERIAL_ECHOLN("Connect fail, please check your INI file or set the wifi config and connect again"); + SERIAL_ECHOLN("- M50: Set the wifi ssid , 'M50 ssid-name'"); + SERIAL_ECHOLN("- M51: Set the wifi password , 'M51 password'"); + SERIAL_ECHOLN("- M52: Start to connect the wifi"); + SERIAL_ECHOLN("- M53: Check the connection status"); + } +} + +/** + * M53: Check wifi status + */ +void Gcode::gcode_M53() { + if(WiFi.status() != WL_CONNECTED) { + SERIAL_ECHOLN("Wifi not connected"); + SERIAL_ECHOLN("- M50: Set the wifi ssid , 'M50 ssid-name'"); + SERIAL_ECHOLN("- M51: Set the wifi password , 'M51 password'"); + SERIAL_ECHOLN("- M52: Start to connect the wifi"); + SERIAL_ECHOLN("- M53: Check the connection status"); + } + else { + SERIAL_ECHOLN(""); + SERIAL_ECHO("Connected to "); SERIAL_ECHOLN(WiFi.SSID()); + SERIAL_ECHO("IP address: "); SERIAL_ECHOLN(WiFi.localIP()); + SERIAL_ECHO("RSSI: "); SERIAL_ECHOLN(WiFi.RSSI()); + SERIAL_ECHO("Mode: "); SERIAL_ECHOLN(WiFi.getPhyMode()); + SERIAL_ECHO("Asscess to SD at the Run prompt : \\\\"); SERIAL_ECHO(WiFi.localIP());SERIAL_ECHOLN("\\DavWWWRoot"); + } +} + +/** + * Process the parsed command and dispatch it to its handler + */ +void Gcode::process_parsed_command() { + //SERIAL_ECHOLNPAIR("command_letter:", parser.command_letter); + //SERIAL_ECHOLNPAIR("codenum:", parser.codenum); + + // Handle a known G, M, or T + switch (parser.command_letter) { + case 'G': switch (parser.codenum) { + default: parser.unknown_command_error(); + } + break; + + case 'M': switch (parser.codenum) { + case 50: gcode_M50(); break; + case 51: gcode_M51(); break; + case 52: gcode_M52(); break; + case 53: gcode_M53(); break; + default: parser.unknown_command_error(); + } + break; + + default: parser.unknown_command_error(); + } + + SERIAL_ECHOLN("ok"); +} + +void Gcode::process_next_command() { + char * const current_command = command_queue[cmd_queue_index_r]; + current_command_sd_pos = command_sd_pos[cmd_queue_index_r]; + + // Parse the next command in the queue + parser.parse(current_command); + process_parsed_command(); +} diff --git a/gcode.h b/gcode.h new file mode 100644 index 0000000..dcf04a6 --- /dev/null +++ b/gcode.h @@ -0,0 +1,53 @@ +#ifndef _GCODE_H_ +#define _GCODE_H_ + +#define MAX_CMD_SIZE 96 +#define BUFSIZE 4 + +/** + * GCode + * + * - Handle gcode + */ +class Gcode { +public: + void Handle(); + +private: + void _commit_command(bool say_ok); + bool _enqueuecommand(const char* cmd, bool say_ok=false); + void get_serial_commands(); + void gcode_M50(); + void gcode_M51(); + void gcode_M52(); + void gcode_M53(); + void process_parsed_command(); + void process_next_command(); + + /** + * GCode Command Queue + * A simple ring buffer of BUFSIZE command strings. + * + * Commands are copied into this buffer by the command injectors + * (immediate, serial, sd card) and they are processed sequentially by + * the main loop. The process_next_command function parses the next + * command and hands off execution to individual handler functions. + */ + unsigned char commands_in_queue = 0, // Count of commands in the queue + cmd_queue_index_r = 0, // Ring buffer read (out) position + cmd_queue_index_w = 0; // Ring buffer write (in) position + + unsigned long command_sd_pos[BUFSIZE]; + volatile unsigned long current_command_sd_pos; + + char command_queue[BUFSIZE][MAX_CMD_SIZE]; + + bool send_ok[BUFSIZE]; + + // Number of characters read in the current line of serial input + int serial_count; // = 0; +}; + +extern Gcode gcode; + +#endif diff --git a/ini/SETUP.INI b/ini/SETUP.INI new file mode 100644 index 0000000..3a48cd8 --- /dev/null +++ b/ini/SETUP.INI @@ -0,0 +1,2 @@ +SSID=xxxx +PASSWORD=xxxx \ No newline at end of file diff --git a/macros.h b/macros.h new file mode 100644 index 0000000..1d7d6bb --- /dev/null +++ b/macros.h @@ -0,0 +1,11 @@ +typedef uint32_t millis_t; + +#define WITHIN(V,L,H) ((V) >= (L) && (V) <= (H)) +#define NUMERIC(a) WITHIN(a, '0', '9') +#define DECIMAL(a) (NUMERIC(a) || a == '.') +#define NUMERIC_SIGNED(a) (NUMERIC(a) || (a) == '-' || (a) == '+') +#define DECIMAL_SIGNED(a) (DECIMAL(a) || (a) == '-' || (a) == '+') + +#define constrain(value, arg_min, arg_max) ((value) < (arg_min) ? (arg_min) :((value) > (arg_max) ? (arg_max) : (value))) + +#define FORCE_INLINE __attribute__((always_inline)) inline diff --git a/network.cpp b/network.cpp new file mode 100644 index 0000000..6db1083 --- /dev/null +++ b/network.cpp @@ -0,0 +1,123 @@ +#include "network.h" +#include "serial.h" +#include "config.h" +#include "pins.h" +#include "ESP8266WiFi.h" +#include "ESPWebDAV.h" +#include "sdControl.h" + +String IpAddress2String(const IPAddress& ipAddress) +{ + return String(ipAddress[0]) + String(".") +\ + String(ipAddress[1]) + String(".") +\ + String(ipAddress[2]) + String(".") +\ + String(ipAddress[3]) ; +} + +bool Network::start() { + wifiConnected = false; + wifiConnecting = true; + + // Set hostname first + WiFi.hostname(HOSTNAME); + // Reduce startup surge current + WiFi.setAutoConnect(false); + WiFi.mode(WIFI_STA); + WiFi.setPhyMode(WIFI_PHY_MODE_11N); + WiFi.begin(config.ssid(), config.password()); + + // Wait for connection + unsigned int timeout = 0; + while(WiFi.status() != WL_CONNECTED) { + //blink(); + SERIAL_ECHO("."); + timeout++; + if(timeout++ > WIFI_CONNECT_TIMEOUT/100) { + SERIAL_ECHOLN(""); + wifiConnecting = false; + return false; + } + else + delay(100); + } + + SERIAL_ECHOLN(""); + SERIAL_ECHO("Connected to "); SERIAL_ECHOLN(config.ssid()); + SERIAL_ECHO("IP address: "); SERIAL_ECHOLN(WiFi.localIP()); + SERIAL_ECHO("RSSI: "); SERIAL_ECHOLN(WiFi.RSSI()); + SERIAL_ECHO("Mode: "); SERIAL_ECHOLN(WiFi.getPhyMode()); + SERIAL_ECHO("Asscess to SD at the Run prompt : \\\\"); SERIAL_ECHO(WiFi.localIP());SERIAL_ECHOLN("\\DavWWWRoot"); + + wifiConnected = true; + + config.save(); + String sIp = IpAddress2String(WiFi.localIP()); + config.save_ip(sIp.c_str()); + + SERIAL_ECHOLN("Going to start DAV server"); + if(startDAVServer() < 0) return false; + wifiConnecting = false; + + return true; +} + +int Network::startDAVServer() { + if(!sdcontrol.canWeTakeBus()) { + return -1; + } + sdcontrol.takeBusControl(); + + // start the SD DAV server + if(!dav.init(SD_CS, SPI_FULL_SPEED, SERVER_PORT)) { + DBG_PRINT("ERROR: "); DBG_PRINTLN("Failed to initialize SD Card"); + // indicate error on LED + //errorBlink(); + initFailed = true; + } + else { + //blink(); + } + + sdcontrol.relinquishBusControl(); + DBG_PRINTLN("FYSETC WebDAV server started"); + return 0; +} + +bool Network::isConnected() { + return wifiConnected; +} + +bool Network::isConnecting() { + return wifiConnecting; +} + +// a client is waiting and FS is ready and other SPI master is not using the bus +bool Network::ready() { + if(!isConnected()) return false; + + // do it only if there is a need to read FS + if(!dav.isClientWaiting()) return false; + + if(initFailed) { + dav.rejectClient("Failed to initialize SD Card"); + return false; + } + + // has other master been using the bus in last few seconds + if(!sdcontrol.canWeTakeBus()) { + dav.rejectClient("Marlin is reading from SD card"); + return false; + } + + return true; +} + +void Network::handle() { + if(network.ready()) { + sdcontrol.takeBusControl(); + dav.handleClient(); + sdcontrol.relinquishBusControl(); + } +} + +Network network; diff --git a/network.h b/network.h new file mode 100644 index 0000000..1757897 --- /dev/null +++ b/network.h @@ -0,0 +1,27 @@ +#ifndef _NETWORK_H_ +#define _NETWORK_H_ + +#define HOSTNAME "FYSETC" +#define SERVER_PORT 80 + +#define WIFI_CONNECT_TIMEOUT 30000UL + +class Network { +public: + Network() { initFailed = false;wifiConnecting = true;} + bool start(); + int startDAVServer(); + bool isConnected(); + bool isConnecting(); + void handle(); + bool ready(); + +private: + bool wifiConnected; + bool wifiConnecting; + bool initFailed; +}; + +extern Network network; + +#endif diff --git a/parser.cpp b/parser.cpp new file mode 100644 index 0000000..e17ca6e --- /dev/null +++ b/parser.cpp @@ -0,0 +1,200 @@ +/** + * Base on Malrin https://github.com/MarlinFirmware/Marlin + * parser.cpp - Parser for a GCode line, providing a parameter interface. + */ +#include "serial.h" +#include "parser.h" + +// Must be declared for allocation and to satisfy the linker +// Zero values need no initialization. +char *GCodeParser::command_ptr, + *GCodeParser::string_arg, + *GCodeParser::value_ptr; +char GCodeParser::command_letter; +int GCodeParser::codenum; + +char *GCodeParser::command_args; // start of parameters + +// Create a global instance of the GCode parser singleton +GCodeParser parser; + +/** + * Clear all code-seen (and value pointers) + * + * Since each param is set/cleared on seen codes, + * this may be optimized by commenting out ZERO(param) + */ +void GCodeParser::reset() { + string_arg = NULL; // No whole line argument + command_letter = '?'; // No command letter + codenum = 0; // No command code +} + +// Populate all fields by parsing a single line of GCode +// 58 bytes of SRAM are used to speed up seen/value +void GCodeParser::parse(char *p) { + + reset(); // No codes to report + + // Skip spaces + while (*p == ' ') ++p; + + // Skip N[-0-9] if included in the command line + if (*p == 'N' && NUMERIC_SIGNED(p[1])) { + p += 2; // skip N[-0-9] + while (NUMERIC(*p)) ++p; // skip [0-9]* + while (*p == ' ') ++p; // skip [ ]* + } + + // *p now points to the current command, which should be G, M, or T + command_ptr = p; + + // Get the command letter, which must be G, M, or T + const char letter = *p++; + + // Nullify asterisk and trailing whitespace + char *starpos = strchr(p, '*'); + if (starpos) { + --starpos; // * + while (*starpos == ' ') --starpos; // spaces... + starpos[1] = '\0'; + } + + // Bail if the letter is not G, M, or T + switch (letter) { case 'G': case 'M': case 'T': break; default: return; } + + // Skip spaces to get the numeric part + while (*p == ' ') p++; + + // Bail if there's no command code number + if (!NUMERIC(*p)) return; + + // Save the command letter at this point + // A '?' signifies an unknown command + command_letter = letter; + + // Get the code number - integer digits only + codenum = 0; + do { + codenum *= 10, codenum += *p++ - '0'; + } while (NUMERIC(*p)); + + // Skip all spaces to get to the first argument, or nul + while (*p == ' ') p++; + + // The command parameters (if any) start here, for sure! + + // Only use string_arg for these M codes + if (letter == 'M') switch (codenum) { case 50: case 51: case 23: case 28: case 30: case 117: case 118: case 928: string_arg = p; return; default: break; } + + #if defined(DEBUG_GCODE_PARSER) + const bool debug = codenum == 800; + #endif + + /** + * Find all parameters, set flags and pointers for fast parsing + * + * Most codes ignore 'string_arg', but those that want a string will get the right pointer. + * The following loop assigns the first "parameter" having no numeric value to 'string_arg'. + * This allows M0/M1 with expire time to work: "M0 S5 You Win!" + * For 'M118' you must use 'E1' and 'A1' rather than just 'E' or 'A' + */ + string_arg = NULL; + while (const char code = *p++) { // Get the next parameter. A NUL ends the loop + + // Special handling for M32 [P] !/path/to/file.g# + // The path must be the last parameter + if (code == '!' && letter == 'M' && codenum == 32) { + string_arg = p; // Name starts after '!' + char * const lb = strchr(p, '#'); // Already seen '#' as SD char (to pause buffering) + if (lb) *lb = '\0'; // Safe to mark the end of the filename + return; + } + + // Arguments MUST be uppercase for fast GCode parsing + #define PARAM_TEST true + + if (PARAM_TEST) { + + while (*p == ' ') p++; // Skip spaces between parameters & values + + const bool has_num = valid_float(p); + + #if defined(DEBUG_GCODE_PARSER) + if (debug) { + SERIAL_ECHOPAIR("Got letter ", code); + SERIAL_ECHOPAIR(" at index ", (int)(p - command_ptr - 1)); + if (has_num) SERIAL_ECHOPGM(" (has_num)"); + } + #endif + + if (!has_num && !string_arg) { // No value? First time, keep as string_arg + string_arg = p - 1; + #if defined(DEBUG_GCODE_PARSER) + if (debug) SERIAL_ECHOPAIR(" string_arg: ", hex_address((void*)string_arg)); // DEBUG + #endif + } + + #if defined(DEBUG_GCODE_PARSER) + if (debug) SERIAL_EOL(); + #endif + } + else if (!string_arg) { // Not A-Z? First time, keep as the string_arg + string_arg = p - 1; + #if defined(DEBUG_GCODE_PARSER) + if (debug) SERIAL_ECHOPAIR(" string_arg: ", hex_address((void*)string_arg)); // DEBUG + #endif + } + + if (!WITHIN(*p, 'A', 'Z')) { // Another parameter right away? + while (*p && DECIMAL_SIGNED(*p)) p++; // Skip over the value section of a parameter + while (*p == ' ') p++; // Skip over all spaces + } + } +} + +void GCodeParser::unknown_command_error() { + SERIAL_ECHO_START(); + SERIAL_ECHOPAIR("Unknown:", command_ptr); + SERIAL_CHAR('"'); + SERIAL_EOL(); +} + +#if defined(DEBUG_GCODE_PARSER) + + void GCodeParser::debug() { + SERIAL_ECHOPAIR("Command: ", command_ptr); + SERIAL_ECHOPAIR(" (", command_letter); + SERIAL_ECHO(codenum); + SERIAL_ECHOLNPGM(")"); + SERIAL_ECHOPAIR(" args: \"", command_args); + SERIAL_CHAR('"'); + if (string_arg) { + SERIAL_ECHOPGM(" string: \""); + SERIAL_ECHO(string_arg); + SERIAL_CHAR('"'); + } + SERIAL_ECHOPGM("\n\n"); + for (char c = 'A'; c <= 'Z'; ++c) { + if (seen(c)) { + SERIAL_ECHOPAIR("Code '", c); SERIAL_ECHOPGM("':"); + if (has_value()) { + SERIAL_ECHOPAIR("\n float: ", value_float()); + SERIAL_ECHOPAIR("\n long: ", value_long()); + SERIAL_ECHOPAIR("\n ulong: ", value_ulong()); + SERIAL_ECHOPAIR("\n millis: ", value_millis()); + SERIAL_ECHOPAIR("\n sec-ms: ", value_millis_from_seconds()); + SERIAL_ECHOPAIR("\n int: ", value_int()); + SERIAL_ECHOPAIR("\n ushort: ", value_ushort()); + SERIAL_ECHOPAIR("\n byte: ", (int)value_byte()); + SERIAL_ECHOPAIR("\n bool: ", (int)value_bool()); + SERIAL_ECHOPAIR("\n linear: ", value_linear_units()); + } + else + SERIAL_ECHOPGM(" (no value)"); + SERIAL_ECHOPGM("\n\n"); + } + } + } + +#endif // DEBUG_GCODE_PARSER diff --git a/parser.h b/parser.h new file mode 100644 index 0000000..1f3f99d --- /dev/null +++ b/parser.h @@ -0,0 +1,141 @@ +/** + * Base on Malrin https://github.com/MarlinFirmware/Marlin + * parser.h - Parser for a GCode line, providing a parameter interface. + * Codes like M149 control the way the GCode parser behaves, + * so settings for these codes are located in this class. + */ + +#ifndef _PARSER_H_ +#define _PARSER_H_ + +//#include "enum.h" +#include +#include +#include "macros.h" + +#define strtof strtod + +/** + * GCode parser + * + * - Parse a single gcode line for its letter, code, subcode, and parameters + * - Provide accessors for parameters: + * - Parameter exists + * - Parameter has value + * - Parameter value in different units and types + */ +class GCodeParser { + +private: + static char *value_ptr; // Set by seen, used to fetch the value + static char *command_args; // Args start here, for slow scan + +public: + + // Command line state + static char *command_ptr, // The command, so it can be echoed + *string_arg; // string of command line + + static char command_letter; // G, M, or T + static int codenum; // 123 + + #if defined(DEBUG_GCODE_PARSER) + static void debug(); + #endif + + GCodeParser() { + } + + // Reset is done before parsing + static void reset(); + + #define LETTER_BIT(N) ((N) - 'A') + + FORCE_INLINE static bool valid_signless(const char * const p) { + return NUMERIC(p[0]) || (p[0] == '.' && NUMERIC(p[1])); // .?[0-9] + } + + FORCE_INLINE static bool valid_float(const char * const p) { + return valid_signless(p) || ((p[0] == '-' || p[0] == '+') && valid_signless(&p[1])); // [-+]?.?[0-9] + } + + + // Code is found in the string. If not found, value_ptr is unchanged. + // This allows "if (seen('A')||seen('B'))" to use the last-found value. + static bool seen(const char c) { + char *p = strchr(command_args, c); + const bool b = !!p; + if (b) value_ptr = valid_float(&p[1]) ? &p[1] : (char*)NULL; + return b; + } + + static bool seen_any() { return *command_args == '\0'; } + + #define SEEN_TEST(L) !!strchr(command_args, L) + + // Seen any axis parameter + static bool seen_axis() { + return SEEN_TEST('X') || SEEN_TEST('Y') || SEEN_TEST('Z') || SEEN_TEST('E'); + } + + // Populate all fields by parsing a single line of GCode + // This uses 54 bytes of SRAM to speed up seen/value + static void parse(char * p); + + // The code value pointer was set + FORCE_INLINE static bool has_value() { return value_ptr != NULL; } + + // Seen a parameter with a value + inline static bool seenval(const char c) { return seen(c) && has_value(); } + + // Float removes 'E' to prevent scientific notation interpretation + inline static float value_float() { + if (value_ptr) { + char *e = value_ptr; + for (;;) { + const char c = *e; + if (c == '\0' || c == ' ') break; + if (c == 'E' || c == 'e') { + *e = '\0'; + const float ret = strtof(value_ptr, NULL); + *e = c; + return ret; + } + ++e; + } + return strtof(value_ptr, NULL); + } + return 0; + } + + // Code value as a long or ulong + inline static int32_t value_long() { return value_ptr ? strtol(value_ptr, NULL, 10) : 0L; } + inline static uint32_t value_ulong() { return value_ptr ? strtoul(value_ptr, NULL, 10) : 0UL; } + + // Code value for use as time + FORCE_INLINE static millis_t value_millis() { return value_ulong(); } + FORCE_INLINE static millis_t value_millis_from_seconds() { return value_float() * 1000UL; } + + // Reduce to fewer bits + FORCE_INLINE static int16_t value_int() { return (int16_t)value_long(); } + FORCE_INLINE static uint16_t value_ushort() { return (uint16_t)value_long(); } + inline static uint8_t value_byte() { return (uint8_t)constrain(value_long(), 0, 255); } + + // Bool is true with no value or non-zero + inline static bool value_bool() { return !has_value() || !!value_byte(); } + + void unknown_command_error(); + + // Provide simple value accessors with default option + FORCE_INLINE static float floatval(const char c, const float dval=0.0) { return seenval(c) ? value_float() : dval; } + FORCE_INLINE static bool boolval(const char c, const bool dval=false) { return seenval(c) ? value_bool() : (seen(c) ? true : dval); } + FORCE_INLINE static uint8_t byteval(const char c, const uint8_t dval=0) { return seenval(c) ? value_byte() : dval; } + FORCE_INLINE static int16_t intval(const char c, const int16_t dval=0) { return seenval(c) ? value_int() : dval; } + FORCE_INLINE static uint16_t ushortval(const char c, const uint16_t dval=0) { return seenval(c) ? value_ushort() : dval; } + FORCE_INLINE static int32_t longval(const char c, const int32_t dval=0) { return seenval(c) ? value_long() : dval; } + FORCE_INLINE static uint32_t ulongval(const char c, const uint32_t dval=0) { return seenval(c) ? value_ulong() : dval; } +}; + +extern GCodeParser parser; + +#endif // _PARSER_H_ diff --git a/pins.h b/pins.h new file mode 100644 index 0000000..46997b3 --- /dev/null +++ b/pins.h @@ -0,0 +1,10 @@ +#ifndef _SD_H_ +#define _SD_H_ + +#define SD_CS 4 +#define MISO_PIN 12 +#define MOSI_PIN 13 +#define SCLK_PIN 14 +#define CS_SENSE 5 + +#endif \ No newline at end of file diff --git a/sdControl.cpp b/sdControl.cpp new file mode 100644 index 0000000..c061a9b --- /dev/null +++ b/sdControl.cpp @@ -0,0 +1,48 @@ +#include +#include "sdControl.h" +#include "pins.h" + +volatile long SDControl::_spiBlockoutTime = 0; +bool SDControl::_weTookBus = false; + +void SDControl::setup() { + // ----- GPIO ------- + // Detect when other master uses SPI bus + pinMode(CS_SENSE, INPUT); + attachInterrupt(CS_SENSE, []() { + if(!_weTookBus) + _spiBlockoutTime = millis() + SPI_BLOCKOUT_PERIOD; + }, FALLING); + + // wait for other master to assert SPI bus first + delay(SPI_BLOCKOUT_PERIOD); +} + +// ------------------------ +void SDControl::takeBusControl() { +// ------------------------ + _weTookBus = true; + //LED_ON; + pinMode(MISO_PIN, SPECIAL); + pinMode(MOSI_PIN, SPECIAL); + pinMode(SCLK_PIN, SPECIAL); + pinMode(SD_CS, OUTPUT); +} + +// ------------------------ +void SDControl::relinquishBusControl() { +// ------------------------ + pinMode(MISO_PIN, INPUT); + pinMode(MOSI_PIN, INPUT); + pinMode(SCLK_PIN, INPUT); + pinMode(SD_CS, INPUT); + //LED_OFF; + _weTookBus = false; +} + +bool SDControl::canWeTakeBus() { + if(millis() < _spiBlockoutTime) { + return false; + } + return true; +} diff --git a/sdControl.h b/sdControl.h new file mode 100644 index 0000000..3e02416 --- /dev/null +++ b/sdControl.h @@ -0,0 +1,21 @@ +#ifndef _SD_CONTROL_H_ +#define _SD_CONTROL_H_ + +#define SPI_BLOCKOUT_PERIOD 20000UL + +class SDControl { +public: + SDControl() { } + static void setup(); + static void takeBusControl(); + static void relinquishBusControl(); + static bool canWeTakeBus(); + +private: + static volatile long _spiBlockoutTime; + static bool _weTookBus; +}; + +extern SDControl sdcontrol; + +#endif diff --git a/serial.cpp b/serial.cpp new file mode 100644 index 0000000..791e8fa --- /dev/null +++ b/serial.cpp @@ -0,0 +1,36 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (C) 2016 MarlinFirmware [https://github.com/MarlinFirmware/Marlin] + * + * Based on Sprinter and grbl. + * Copyright (C) 2011 Camiel Gubbels / Erik van der Zalm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "serial.h" + +const char errormagic[] PROGMEM = "Error:"; +const char echomagic[] PROGMEM = "echo:"; + +void serial_echopair_PGM(const char* s_P, const char *v) { serialprintPGM(s_P); SERIAL_ECHO(v); } +void serial_echopair_PGM(const char* s_P, char v) { serialprintPGM(s_P); SERIAL_CHAR(v); } +void serial_echopair_PGM(const char* s_P, int v) { serialprintPGM(s_P); SERIAL_ECHO(v); } +void serial_echopair_PGM(const char* s_P, long v) { serialprintPGM(s_P); SERIAL_ECHO(v); } +void serial_echopair_PGM(const char* s_P, float v) { serialprintPGM(s_P); SERIAL_ECHO(v); } +void serial_echopair_PGM(const char* s_P, double v) { serialprintPGM(s_P); SERIAL_ECHO(v); } +void serial_echopair_PGM(const char* s_P, unsigned long v) { serialprintPGM(s_P); SERIAL_ECHO(v); } + +void serial_spaces(uint8_t count) { while (count--) SERIAL_CHAR(' '); } diff --git a/serial.h b/serial.h new file mode 100644 index 0000000..4c276a2 --- /dev/null +++ b/serial.h @@ -0,0 +1,100 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (C) 2016 MarlinFirmware [https://github.com/MarlinFirmware/Marlin] + * + * Based on Sprinter and grbl. + * Copyright (C) 2011 Camiel Gubbels / Erik van der Zalm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef __SERIAL_H__ +#define __SERIAL_H__ + +#include +#include "macros.h" + +#define MYSERIAL0 Serial + +#define SERIAL_INIT(...) { Serial.begin(__VA_ARGS__); } + +extern const char echomagic[] PROGMEM; +extern const char errormagic[] PROGMEM; + +#define SERIAL_CHAR(x) ((void)MYSERIAL0.write(x)) +#define SERIAL_EOL() SERIAL_CHAR('\n') + +#define SERIAL_PRINT(x,b) MYSERIAL0.print(x,b) +#define SERIAL_PRINTLN(x,b) MYSERIAL0.println(x,b) +#define SERIAL_PRINTF(args...) MYSERIAL0.printf(args) + +#define SERIAL_FLUSH() MYSERIAL0.flush() + +#define SERIAL_PROTOCOLCHAR(x) SERIAL_CHAR(x) +#define SERIAL_PROTOCOL(x) MYSERIAL0.print(x) +#define SERIAL_PROTOCOL_F(x,y) MYSERIAL0.print(x,y) +#define SERIAL_PROTOCOLPGM(x) serialprintPGM(PSTR(x)) +#define SERIAL_PROTOCOLLN(x) do{ MYSERIAL0.print(x); SERIAL_EOL(); }while(0) +#define SERIAL_PROTOCOLLNPGM(x) serialprintPGM(PSTR(x "\n")) +#define SERIAL_PROTOCOLPAIR(name, value) serial_echopair_PGM(PSTR(name),(value)) +#define SERIAL_PROTOCOLLNPAIR(name, value) do{ SERIAL_PROTOCOLPAIR(name, value); SERIAL_EOL(); }while(0) + +#define SERIAL_ECHO_START() serialprintPGM(echomagic) +#define SERIAL_ECHO(x) SERIAL_PROTOCOL(x) +#define SERIAL_ECHOPGM(x) SERIAL_PROTOCOLPGM(x) +#define SERIAL_ECHOLN(x) SERIAL_PROTOCOLLN(x) +#define SERIAL_ECHOLNPGM(x) SERIAL_PROTOCOLLNPGM(x) +#define SERIAL_ECHOPAIR(pre,value) SERIAL_PROTOCOLPAIR(pre, value) +#define SERIAL_ECHOLNPAIR(pre,value) SERIAL_PROTOCOLLNPAIR(pre, value) +#define SERIAL_ECHO_F(x,y) SERIAL_PROTOCOL_F(x,y) + +#define SERIAL_ERROR_START() serialprintPGM(errormagic) +#define SERIAL_ERROR(x) SERIAL_PROTOCOL(x) +#define SERIAL_ERRORPGM(x) SERIAL_PROTOCOLPGM(x) +#define SERIAL_ERRORLN(x) SERIAL_PROTOCOLLN(x) +#define SERIAL_ERRORLNPGM(x) SERIAL_PROTOCOLLNPGM(x) + +// These macros compensate for float imprecision +#define SERIAL_PROTOCOLPAIR_F(pre, value) SERIAL_PROTOCOLPAIR(pre, FIXFLOAT(value)) +#define SERIAL_PROTOCOLLNPAIR_F(pre, value) SERIAL_PROTOCOLLNPAIR(pre, FIXFLOAT(value)) +#define SERIAL_ECHOPAIR_F(pre,value) SERIAL_ECHOPAIR(pre, FIXFLOAT(value)) +#define SERIAL_ECHOLNPAIR_F(pre, value) SERIAL_ECHOLNPAIR(pre, FIXFLOAT(value)) + +// +// Functions for serial printing from PROGMEM. (Saves loads of SRAM.) +// +FORCE_INLINE void serialprintPGM(const char* str) { + while (char ch = pgm_read_byte(str++)) SERIAL_CHAR(ch); +} + +void serial_echopair_PGM(const char* s_P, const char *v); +void serial_echopair_PGM(const char* s_P, char v); +void serial_echopair_PGM(const char* s_P, int v); +void serial_echopair_PGM(const char* s_P, long v); +void serial_echopair_PGM(const char* s_P, float v); +void serial_echopair_PGM(const char* s_P, double v); +void serial_echopair_PGM(const char* s_P, unsigned int v); +void serial_echopair_PGM(const char* s_P, unsigned long v); +FORCE_INLINE void serial_echopair_PGM(const char* s_P, uint8_t v) { serial_echopair_PGM(s_P, (int)v); } +FORCE_INLINE void serial_echopair_PGM(const char* s_P, uint16_t v) { serial_echopair_PGM(s_P, (int)v); } +FORCE_INLINE void serial_echopair_PGM(const char* s_P, bool v) { serial_echopair_PGM(s_P, (int)v); } +FORCE_INLINE void serial_echopair_PGM(const char* s_P, void *v) { serial_echopair_PGM(s_P, (unsigned long)v); } + +void serial_spaces(uint8_t count); +#define SERIAL_ECHO_SP(C) serial_spaces(C) +#define SERIAL_ERROR_SP(C) serial_spaces(C) +#define SERIAL_PROTOCOL_SP(C) serial_spaces(C) + +#endif // __SERIAL_H__