Ported: Exciting Printer

Hi All,

I have been working on finishing the code to enable the Spark to run the Exciting.io Printer project (http://exciting.io/printer/)

Whilst there has been a few previous threads, I have found that the final code solution was not published :frowning:

As such I have uploaded my current project to GitHub and also attached the main file to this thread, so users can simply download and get going :smiley:

My aim is to also make this code friendly for the Choosatron users, allowing them to utilise their existing equipment with out making any hardware changes

I would love if anyone has any suggestions on improving the code, or is able to port the back end server to something like PHP/NodeJS which I am more familiar with so I can enhance the backend capabilities

GitHub Repo

#define USE_SPARK_RGB                             // Use the onbaord RGB LED, comment this out to use extenal LEDs
#define SPARK_MODE MANUAL                         // Use MANUAL mode to increase data download speed

#define DEBUG                                     // When debug is enabled, log a bunch of stuff to the hardware Serial, if disabled some events will use Spark.publish
#define DEBUG_BAUD 115200                         // Serial baud rate for debugging logs
#define DEBUG_DELAY 5                             // Delay system by x seconds or wait for serial connection, useful to allow time to attach to serial port for debugging

#define PRINTER_CONNECT_VIA_IP                    // Connect via IP instead of DNS Lookup of host, host must still be set however
#define PRINTER_SERVER_HOST "printer.exciting.io" // Print server hostname
#define PRINTER_SERVER_IP 178,79,132,137          // Print server IP (only required if PRINTER_CONNECT_VIA_IP defined)

#define PRINTER_SERVER_PORT 80                    // Printer Server Port (eg. 80)
#define PRINTER_POLL_DELAY 30                     // Time in x seconds between polling server for next print job
#define PRINTER_REQUEST_TIMEOUT 300               // Time in x seconds before request for print job is aborted
#define PRINTER_BAUD 19200                        // Printer baud rate
#define PRINTER_KEEP_PRINTS                       // Uncomment to archive all print jobs -- EXPERIMENTAL
#define PRINTER_CACHE_NAME_FORMAT "%03d.job"      // Adjust to set the name of the cache files
//#define PRINTER_MAX_SIZE 20000                  // If defined, sets max content size to prevent timeouts

// The printerType controls the format of the data sent from the server
// If you're using a completely different kind of printer, change this
// to correspond to your printer's PrintProcessor implementation in the
// server.
//
// If you want to control the darkness of your printouts, append a dot and
// a number, e.g. A2-raw.240 (up to a maximum of 255).
//
// If you want to flip the vertical orientation of your printouts, append
// a number and then .flipped, e.g. A2-raw.240.flipped
#define PRINTER_TYPE "A2-raw"




//#define USE_ADAFRUIT_THERMAL                    // Uncomment to use the Adafruit libary instead -- NOT RECOMMENDED / UNTESTED

/********************************


  Do NOT modify anything below, for normal operations simply adjust the above defines as required



*********************************/














SYSTEM_MODE(SPARK_MODE);
#include "sd-card-library.h"
#ifdef USE_ADAFRUIT_THERMAL
  #include "Adafruit_Thermal.h"
  Adafruit_Thermal printer;
  #define PRINTER_WRITE(b) printer.println(b);
  #define PRINTER_INIT() Serial1.begin(printer_baud); printer.begin(&Serial1);
#else
  #define PRINTER_WRITE(b) Serial1.write(b)
  #define PRINTER_INIT() Serial1.begin(printer_baud);
#endif


const char printerType[] = PRINTER_TYPE;

const char host[] = PRINTER_SERVER_HOST; // the host of the backend server

#ifdef PRINTER_CONNECT_VIA_IP
IPAddress host_ip(PRINTER_SERVER_IP);
#define PRINTER_SERVER host_ip
#else
#define PRINTER_SERVER host
#endif

const unsigned int port = PRINTER_SERVER_PORT;

const unsigned long pollingDelay = PRINTER_POLL_DELAY * 1000; // delay between polling requests (milliseconds)
const unsigned long serialDelay = DEBUG_DELAY * 1000;
const unsigned long requestTimeout = PRINTER_REQUEST_TIMEOUT * 1000;

const int pingTries = 20;

const int serialBaud = 9600;

// Printer Connections
//const int printer_TX_Pin = RX; // this is the yellow wire
//const int printer_RX_Pin = TX; // this is the green wire
const int printer_baud = PRINTER_BAUD;

// Buttons -- Not in use but for reference of the Choosatron
const uint8_t button1 =  D1;
const uint8_t button2 =  D2;
const uint8_t button3 =  D3;
const uint8_t button4 =  D4;

// LED Pins
#ifdef USE_SPARK_RGB
  #define LED_ON true
  #define LED_OFF false
#else
  const uint8_t errorLED = D5;       // the red LED
  const uint8_t downloadLED = D6;    // the amber LED
  const uint8_t readyLED = D7;       // the green LED
  #define LED_ON HIGH
  #define LED_OFF LOW
#endif

#define LED_ERROR 2
#define LED_DOWNLOAD 0
#define LED_READY 1


// SD Card
const uint8_t chipSelect = A2;
const uint8_t mosiPin = A5;
const uint8_t misoPin = A4;
const uint8_t clockPin = A3;
// SD Card debugging
#ifdef DEBUG
  Sd2Card card;
  SdVolume volume;
  SdFile root;
#endif

boolean downloadWaiting = false;
unsigned long content_length = 0;
boolean statusOk = false;
char cacheFilename[10];


// -- Everything below here can be left alone

const char sketchVersion[] = "1.0.6";
// -- Debugging

#ifdef DEBUG
  void debugTimeAndSeparator() {
    Serial.print(millis()); Serial.print(": ");
  }
  void debug(const char *a) {
    debugTimeAndSeparator(); Serial.println(a);
  }
  #define debug2(a, b) debugTimeAndSeparator(); Serial.print(a); Serial.println(b);
#else
  #define debug(a) Spark.publish(a);
  #define debug2(a, b) Spark.publish(a, b);
#endif


// -- Initialize the printer ID

const byte idAddress = 0;
char printerId[17]; // the unique ID for this printer.
//char printerId[] = "4r2f5i4u1l3s3s8d\0";

inline void initPrinterID() {
  #ifdef DEBUG
    debug("Initializing Printer ID");
  #endif
  if ((EEPROM.read(idAddress) == 255) || (EEPROM.read(idAddress+1) == 255)) {
    debug("Generating new ID");
    randomSeed(analogRead(0) * analogRead(5));
    for(int i = 0; i < 16; i += 2) {
      printerId[i] = random(48, 57); // 0-9
      printerId[i+1] = random(97, 122); // a-z
      EEPROM.write(idAddress + i, printerId[i]);
      EEPROM.write(idAddress + i+1, printerId[i+1]);
    }
  } else {
    for(int i = 0; i < 16; i++) {
      printerId[i] = (char)EEPROM.read(idAddress + i);
    }
  }
  printerId[16] = '\0';
  debug2("Printer ID: ", printerId);
}


// -- Initialize the LEDs

void initDiagnosticLEDs() {
  #ifdef DEBUG
    debug("Initializing LEDs");
  #endif
#ifndef USE_SPARK_RGB
  pinMode(errorLED, OUTPUT);
  pinMode(downloadLED, OUTPUT);
  pinMode(readyLED, OUTPUT);
#endif

  statusLED(LED_ERROR, LED_ON);
  delay(500);
  statusLED(LED_ERROR, LED_OFF);

  statusLED(LED_DOWNLOAD, LED_ON);
  delay(500);
  statusLED(LED_DOWNLOAD, LED_OFF);

  statusLED(LED_READY, LED_ON);
  delay(500);
  statusLED(LED_READY, LED_OFF);
}

// -- Initialize the printer connection
void initPrinter() {
  #ifdef DEBUG
    debug("Initializing printer");
  #endif
  PRINTER_INIT();
}


// -- Initialize the SD card

inline void initSD() {
  #ifdef DEBUG
    debug("Initializing SD card");

    // we'll use the initialization code from the utility libraries
    // since we're just testing if the card is working!
    if (!card.init(SPI_HALF_SPEED, chipSelect)) {
      debug("initialization failed. Things to check:");
      debug("* is a card is inserted?");
      debug("* Is your wiring correct?");
      debug("* did you change the chipSelect pin to match your shield or module?");
      return;
    } else {
     debug("Wiring is correct and a card is present.");
    }

    // print the type of card
    Serial.print("\nCard type: ");
    switch(card.type()) {
      case SD_CARD_TYPE_SD1:
        Serial.println("SD1");
        break;
      case SD_CARD_TYPE_SD2:
        Serial.println("SD2");
        break;
      case SD_CARD_TYPE_SDHC:
        Serial.println("SDHC");
        break;
      default:
        Serial.println("Unknown");
    }

    // Now we will try to open the 'volume'/'partition' - it should be FAT16 or FAT32
    if (!volume.init(card)) {
      debug("Could not find FAT16/FAT32 partition.\nMake sure you've formatted the card");
      return;
    }


    // print the type and size of the first FAT-type volume
    uint32_t volumesize;
    Serial.print("\nVolume type is FAT");
    Serial.println(volume.fatType(), DEC);
    //Serial.println();

    volumesize = volume.blocksPerCluster();    // clusters are collections of blocks
    volumesize *= volume.clusterCount();       // we'll have a lot of clusters
    volumesize *= 512;                            // SD card blocks are always 512 bytes
    //Serial.print("Volume size (bytes): ");
    //Serial.println(volumesize);
    //Serial.print("Volume size (Kbytes): ");
    volumesize /= 1024;
    //Serial.println(volumesize);
    volumesize /= 1024;
    debug2("Volume size (Mbytes): ", volumesize);

    //Serial.println(volumesize);


    //Serial.println("\nFiles found on the card (name, date and size in bytes): ");
    //root.openRoot(volume);

    // list all files in the card with date and size
    //root.ls(LS_R | LS_DATE | LS_SIZE);

  #endif

  // Initialize HARDWARE SPI with user defined chipSelect
  if (!SD.begin(chipSelect)) {
    // SD Card failure.
    terminalError(2);
    #ifdef DEBUG
      debug("Initialization failed!");
    #endif
    return;
  }
}


// -- Initialize the Ethernet connection & DHCP

TCPClient client;
inline void initNetwork() {
#ifdef DEBUG
  debug("Initialization network...");
#ifdef SPARK_MODE
  WiFi.connect();
  delay(5000);
#endif
  debug2("IP: ", WiFi.localIP());
  debug2("NM: ", WiFi.subnetMask());
  debug2("GW: ", WiFi.gatewayIP());
#endif
}

// // -- Setup; runs once on boot.

void setup(){
#ifdef DEBUG
  Serial.begin(DEBUG_BAUD);
  while (!Serial.available() || millis() < serialDelay) {Spark.process();}
#endif
  initDiagnosticLEDs();
  initPrinterID();
  initSD();
  initPrinter();
  initNetwork();
}

// Set status LEDs
void statusLED(uint8_t status, bool state) {
#ifdef USE_SPARK_RGB
  if (state == LED_OFF) {
    RGB.control(false);
  } else {
    RGB.control(true);
    RGB.brightness(255);
    switch(status) {
      case LED_ERROR:
        RGB.color(255, 0, 0); //RED
      break;
      case LED_DOWNLOAD:
        RGB.color(0, 255, 0); //YELLOW
      break;
      case LED_READY:
        RGB.color(0, 0, 255); //BLUE
      break;
    }
  }
#else
  switch(status) {
    case LED_ERROR:
      digitalWrite(errorLED, state);
    break;
    case LED_DOWNLOAD:
      digitalWrite(downloadLED, state);
    break;
    case LED_READY:
      digitalWrite(readyLED, state);
    break;
  }
#endif
}



void generateFilename() {
  int n = 0;
  File temp;
  int size = 0;
  //char filename[10];
  snprintf(cacheFilename, sizeof(cacheFilename), PRINTER_CACHE_NAME_FORMAT, n); // includes a three-digit sequence number in the file name
#ifdef PRINTER_KEEP_PRINTS
  while(SD.exists(cacheFilename)) {
    temp = SD.open(cacheFilename, FILE_READ);
    size = temp.size();
    temp.close();
    if (size == 0) {
      SD.remove(cacheFilename);
    } else {
      n++;
      snprintf(cacheFilename, sizeof(cacheFilename), PRINTER_CACHE_NAME_FORMAT, n);
    }
  }
#else
  if (SD.exists(cacheFilename)) {
    if (!SD.remove(cacheFilename)) {
      // Failed to clear cache.
      statusLED(LED_ERROR, LED_ON);
      terminalError(4);
    }
  }
#endif

}


// -- Check for new data and download if found

void checkForDownload() {
  unsigned long length = 0;
  content_length = 0;
  statusOk = false;

#ifdef DEBUG
  unsigned long start = millis();
#endif


  //cacheFilename =
  generateFilename();
  File cache = SD.open(cacheFilename, FILE_WRITE);

  debug2("Attempting to connect to ", PRINTER_SERVER);
  if (client.connect(PRINTER_SERVER, port)) {
    debug2("Connected to ", PRINTER_SERVER);
    statusLED(LED_READY, LED_ON);
    client.print("GET "); client.print("/printer/"); client.print(printerId); client.println(" HTTP/1.0");
    client.print("Host: "); client.print(host); client.print(":"); client.println(port);
    client.flush();
    client.print("Accept: application/vnd.exciting.printer."); client.println(printerType);
    client.print("X-Printer-Version: "); client.println(sketchVersion);
    client.println();
    boolean parsingHeader = true;
    float downloadPerc;
    unsigned long connectTime = millis() + requestTimeout;
    while(client.connected()) {
      while(client.available()) {
        if (parsingHeader) {
          client.find((char*)"HTTP/1.1 ");
          char statusCode[] = "xxx";
          client.readBytes(statusCode, 3);
          statusOk = (strcmp(statusCode, "200") == 0);
          client.find((char*)"Content-Length: ");
          char c;
          while (isdigit(c = client.read())) {
            content_length = content_length*10 + (c - '0');
          }
          if (content_length == 0) {
            debug("Disconnecting due to nothing to download");
            client.stop();
            cache.close();
            SD.remove(cacheFilename);
            return;
          }
#ifdef PRINTER_MAX_SIZE
          else if (content_length > PRINTER_MAX_SIZE) {
            debug("Disconnecting due to content exceeding limit");
            client.stop();
            cache.close();
            SD.remove(cacheFilename);
            return;
          }
#endif
          debug2("Content length: ", content_length);
          client.find((char*)"\n\r\n"); // the first \r may already have been read above
          parsingHeader = false;
        } else {
          cache.write(client.read());
          length++;
          if (length == content_length) {
            debug("Disconnecting due to completion");
            client.stop();
          }
          //char c = client.read();
          //Serial.print(c);
        }
      }
      if (connectTime < millis()) {
        debug("Disconnecting due to timeout");
        client.stop();
        cache.close();
        SD.remove(cacheFilename);
        return;
      }
    }

    debug("Server disconnected");
    statusLED(LED_DOWNLOAD, LED_OFF);

    // Close the connection, and flush any unwritten bytes to the cache.
    client.stop();

    cache.seek(0);

    if (statusOk) {
      if ((content_length == length) && (content_length == cache.size())) {
        if (content_length > 0) {
          debug2("Successfully downloaded print job ", cacheFilename)
          downloadWaiting = true;
          statusLED(LED_READY, LED_ON);
        }
      } else {
        debug2("Failure, content length: ", content_length);
        if (content_length != length) debug2("length: ", length);
        if (content_length != cache.size()) debug2("cache: ", cache.size());
        statusLED(LED_ERROR, LED_ON);
      }
    } else {
      debug("Response code != 200");
      recoverableError();
    }
  } else {
    debug("Couldn't connect");
    recoverableError();
  }

  cache.close();




#ifdef DEBUG
  unsigned long duration = millis() - start;
  debug2("Bytes: ", length);
  debug2("Duration: ", duration);
#endif
}

void flashErrorLEDs(unsigned int times, unsigned int pause) {
  while (times--) {
    statusLED(LED_ERROR, LED_ON);
    //delay(pause);
    statusLED(LED_ERROR, LED_OFF);
    //delay(pause);
  }
}

inline void recoverableError() {
  flashErrorLEDs(5, 100);
}

inline void terminalError(unsigned int times) {
  flashErrorLEDs(times, 500);
  statusLED(LED_ERROR, LED_ON);
  debug("Terminal Error - SYSTEM HALT");
  while(true) {SPARK_WLAN_Loop();} // no point in carrying on, so do nothing forevermore:
}

// // -- Print send any data from the cache to the printer

void printFromDownload() {
  File cache = SD.open(cacheFilename);
  byte b;
  debug2("Printing job : ", cacheFilename);
  int file_size = cache.size();
  while (file_size--) {
    b = (byte)cache.read();
    PRINTER_WRITE(b);
  }
  debug2("Finished printing job : ", cacheFilename);
  cache.close();
  downloadWaiting = false;
  statusLED(LED_READY, LED_OFF);
}


// // -- Check for new data, if any new data then print
unsigned long nextDownloadCheck = 0;
void loop() {
  serialEvent();

  if (nextDownloadCheck < millis()) {
    checkForDownload();
    if (downloadWaiting) {
      printFromDownload();
    }
    nextDownloadCheck = millis() + pollingDelay;
    debug2("Next Download: ", nextDownloadCheck);
  }

  //unsigned long pollingDelaySeconds = pollingDelay / 1000;
  //debug2("Next Poll: ", pollingDelaySeconds);
  //delay(pollingDelay);
}

String inputString = "";
void serialEvent() {
  while (Serial.available()) {
    // get the new byte:
    char inChar = (char)Serial.read();
    //Serial.print(inChar);
    // if the incoming character is a newline, set a flag
    // so the main loop can do something about it:
    if (inChar == '\n' || inChar == '\r') {
    //inputString.toUpperCase();


      Serial.println(inputString);
      if (inputString == "PRINT CACHE") {
        Serial.println("Printing cache...");
        int n = 0;
        File temp;
        int size = 0;
        char filename[10];
        snprintf(filename, sizeof(filename), PRINTER_CACHE_NAME_FORMAT, n); // includes a three-digit sequence number in the file name
        while(SD.exists(filename)) {
          printFromDownload();
          n++;
          snprintf(filename, sizeof(filename), PRINTER_CACHE_NAME_FORMAT, n);
        }
      } else if (inputString == "CLEAR CACHE") {
        Serial.println("Clearing cache...");
        int n = 0;
        File temp;
        int size = 0;
        char filename[10];
        snprintf(filename, sizeof(filename), PRINTER_CACHE_NAME_FORMAT, n); // includes a three-digit sequence number in the file name
        while(SD.exists(filename)) {
          SD.remove(filename);
          Serial.print("Deleting cached job : ");
          Serial.println(filename);
          n++;
          snprintf(filename, sizeof(filename), PRINTER_CACHE_NAME_FORMAT, n);
        }
      } else if (inputString == "CHECK") {
        Serial.println("Checking for downloads...");
        checkForDownload();
        nextDownloadCheck = millis() + pollingDelay;
        debug2("Next Download: ", nextDownloadCheck);
      } else if (inputString == "SPARK") {
        Spark.process();
      } else if (inputString == "DFU") {
        debug("Entering DFU Mode");
        System.bootloader();
      } else {
        Serial.println("Unknown Command!");
      }

      // clear the string:
      inputString = "";



    } else {
      // add it to the inputString:
      inputString += inChar;
    }
  }
}
1 Like

@jerrytron might be a good person to comment about this project :wink:

Updated the source code on this page, looks to be working much better now :slight_smile:

Anyone have a Thermal printer they are willing to test my code out on?