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
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
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
#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;
}
}
}