Machining the Taskmaster: Arduino Thermal Printer!

How to machine an aluminum housing on the Tormach 770!

Thermal printer + Aluminum Housing + Arduino Code = TASKMASTER!

Digital reminders are easy to generate AND easy to ignore so, we’re bringing them back into the physical world. We didn’t want to use solid billet so we glued 5 aluminum chunks together using Locktite H800 and machined it into one! This project is a great example of using additive and subtractive machining to get the part you want!

Instructional Video:

Materials:

AdaFruit HUZZAH

Mini Thermal Receipt Printer

MemoBird Printer

Epoxy Sealant

Cutting Tools:

Lakeshore Carbide Chamfer Tool

TTS Diamond Drag Engraver

1" Modular Shear Hog

Downloads:

Fusion 360 File

Project Document

Relevant Sites:

Asana Project Manager

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
// *********************************************************
// **** Include headers for all required libraries here ****
// *********************************************************
#include <Adafruit_Thermal.h>
#include <ESP8266WiFi.h>
#include <ArduinoJson.h>
#include <SoftwareSerial.h>

// *********************************************************
// **** Define data classes here ****
// *********************************************************
/* Used to hold the couple bits of HTTP response header info we want and the body of the response */
class HTTP_Response
{
public:
String hdr_status = "";
bool is_chunked = true;
long body_len = 0;
String body = "";
};

// *************************************************************
// **** Put #defines, constants, static strings, etc here ****
// *************************************************************
/* A couple macros to make adding multilevel debug statements easier */
#define DEBUG_LEVEL_1(x) {if (DEBUG_LEVEL > 0) {Serial.print(x);}}
#define DEBUG_LEVEL_2(x) {if (DEBUG_LEVEL > 1) {Serial.print(x);}}

/* Set this to 0 for no debug messages, 1 for basic debug messages, and 2 for all debug messages */
const int DEBUG_LEVEL = 2;

/* Your wifi SSID and password */
const char* WIFI_SSID = "replacewithyourssid";
const char* WIFI_PASS = "replacewithyourpassword";

/* Your Adafruit IO username, AIO key, and feed name */
const char* AIO_USERNAME = "replacewithusername";
const char* AIO_KEY = "replacewithyourAIOkey";
const char* AIO_FEED1 = "smwthermal";
/* you can add more feeds here and pass them into the data functions to manage multiple feeds */

/* Adafruit IO host and SSL port. Should not have to change these */
const char* AIO_HOST = "io.adafruit.com";
const int AIO_SSL_PORT = 443;

/* Adafruit IO SSL Certificate fingerprint, taken from Adafruit IO library. Used to make sure the TLS/SSL connection is using the proper Adafruit certificate (a security thing)*/
const char* AIO_SSL_FINGERPRINT = "AD 4B 64 B3 67 40 B5 FC 0E 51 9B BD 25 E9 7F 88 B6 2A A3 5B";

/* Adafruit IO API root path */
const char* AIO_API_ROOT_PATH = "/api/v2/";

/* Pin definitions for software serial port that drives the thermal printer */
#define RX_PIN 4
#define TX_PIN 5

// *********************************************************
// **** Declare global variables here ****
// *********************************************************
SoftwareSerial printer_port(RX_PIN, TX_PIN);
Adafruit_Thermal printer(&printer_port);
WiFiClientSecure aio_client;

// **************************************************************************************
// **** Arduino setup() function - put one time initialization and setup code here ****
// **************************************************************************************
void setup()
{
// Huzzah 8266 board has a general purpose red LED attached to Digital pin 0 so set that pin as an output
pinMode(0,OUTPUT);

// Open the USB serial port which will be used to print debug and status messages to the Arduino IDE serial monitor
Serial.begin(115200);

// Wait for the serial port initialization to complete
while(!Serial);

// Open the serial port that talks to the thermal printer
printer_port.begin(19200); //JWS: this was 19200

// Wait for the serial port initialization to complete
while(!printer_port);

// Initialize the printer
printer.begin();

// Connect to WiFi
DEBUG_LEVEL_1("Connecting to WiFi ");
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
DEBUG_LEVEL_1(".");
}

DEBUG_LEVEL_1("\nWiFi connected\nIP address: " + WiFi.localIP().toString() + "\n");
}

// *************************************************************
// **** Arduino loop() function - put main loop code here ****
// *************************************************************
void loop()
{
// Query the Adafruit IO feed defined at the top of the file, restricting the response to just the id and value fields
HTTP_Response resp;
digitalWrite(0,0);
GetLastData(AIO_FEED1, &resp);
digitalWrite(0,1);

// Create the ArduinoJson objects needed to parse the response
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(resp.body);

// If the parsing did not succeed, print out an error message to the serial port
// If it did succeed, process the results
if (root.success() == false)
{
DEBUG_LEVEL_1("Error parsing response body as JSON\n");
DEBUG_LEVEL_2("Body:\n" + resp.body + "\n");
}
else
{
// Extract the id and value fields into separate variables
String id = root["id"];
String value = root["value"];

DEBUG_LEVEL_1(String("id:") + id + "\n");
DEBUG_LEVEL_1(String("value:\n") + value + "\n");

// If we actually got a message, do something with it
if (id.length() > 0 && value.length() > 0)
{
// Print the body of the response to the thermal printer
PrintToThermalPrinter( value, 'S', 'L', false, false );

printer.feed();
printer.feed();

// Delete the message from Adafruit IO
DeleteData( AIO_FEED1, id, &resp);

}
}

// Check for messages every 5 seconds
delay(5000);

/* other main loop code here .... */
}

// *************************************************************
// **** Gets the first message in a given feed ****
// *************************************************************
void GetFirstData( String aio_feed, HTTP_Response* resp )
{
DEBUG_LEVEL_1("Getting first message in feed " + aio_feed + "\n");
SendQueryAndGetResponse("GET", aio_feed, "first", "id,value", resp);
}

// *************************************************************
// **** Gets the last message in a given feed ****
// *************************************************************
void GetLastData( String aio_feed, HTTP_Response* resp )
{
DEBUG_LEVEL_1("Getting last message in feed " + aio_feed + "\n");
SendQueryAndGetResponse("GET", aio_feed, "last", "id,value", resp);
}

// *************************************************************
// **** Gets all messages in a given feed ****
// *************************************************************
void GetAllData( String aio_feed, HTTP_Response* resp )
{
DEBUG_LEVEL_1("Getting all messages in feed " + aio_feed + "\n");
SendQueryAndGetResponse("GET", aio_feed, "", "id,value", resp);
}

// *************************************************************
// **** Deletes a message with a given ID in a given feed ****
// *************************************************************
void DeleteData( String aio_feed, String message_id, HTTP_Response* resp )
{
DEBUG_LEVEL_1("Deleting message in feed " + aio_feed + " with id=" + message_id + "\n");
SendQueryAndGetResponse("DELETE", aio_feed, message_id, "", resp);

// Check the response and if there was an error deleting the message, print an error to the serial port (dependent on DEBUG_LEVEL)
if (resp->hdr_status.equalsIgnoreCase("200 OK") == false)
{
DEBUG_LEVEL_1("Error deleting message with id=" + message_id + "\nStatus=" + resp->hdr_status + "\n");
}
}

// **************************************************************************************
// **** Makes a connection to the Adafruit IO web service, transmits the given ****
// **** request, receives the response, and parses out the header info we want ****
// **** and response body. Debug/logging messages are dumped out the serial port ****
// **** as the function works through all the steps. ****
// **************************************************************************************
bool SendQueryAndGetResponse(String query_type, String aio_feed, String message_id, String include_fields, HTTP_Response* resp)
{
// Create a secure connection to the Adafruit IO site
DEBUG_LEVEL_1(String("Connecting to https://") + AIO_HOST + "\n");

if (aio_client.connect(AIO_HOST, AIO_SSL_PORT) == true)
{
DEBUG_LEVEL_1("Connected\n");
}
else
{
DEBUG_LEVEL_1("Connection failed\n");
return false;
}

// Verify the fingerprint of the SSL certificate being used by the Adafruit site as a bit of a
// security measure to make sure we weren't somehow redirected elsewhere
/* JWS commenting this out:
if (aio_client.verify(AIO_SSL_FINGERPRINT, AIO_HOST) == false)
{
DEBUG_LEVEL_1("SSL certificate doesn't match expected certificate\n");
return false;
}
*/


// Build up the URL to query the proper Adafruit IO feed for the proper user and using the given data retrieval function (ie. first, next, previous, last)
String url = String(AIO_API_ROOT_PATH) + AIO_USERNAME + "/feeds/" + aio_feed + "/data";

// If we were given a message id, tack it on
if (message_id.length() > 0)
{
url += "/" + message_id;
}

// If we were given a list of specific fields to limit the response to, tack those onto the URL
if (include_fields.length() > 0)
{
url += "?include=" + include_fields;
}

// Build up the full HTTP GET request with the above URL and the required headers
String get_req = query_type + " " + url + " HTTP/1.1\r\n" +
"Host: " + AIO_HOST + "\r\n" +
"User-Agent: ESP8266\r\n" +
"X-AIO-KEY: " + AIO_KEY + "\r\n" +
"Content-Type: application/json\r\n" +
"Connection: close\r\n" +
"\r\n";

// Print the GET request to the serial monitor if needed and transmit it to Adafruit IO
// Putting the debug print first is by design because printing to the serial port takes some
// finite amount of time and while it might not matter, it's probably best to drop into the
// response parsing code immediately after sending the request out and not risk missing incoming
// characters due to being tied up printing to the serial port.
DEBUG_LEVEL_1("Sending request\n");
DEBUG_LEVEL_2(get_req);
aio_client.print(get_req);
DEBUG_LEVEL_1("Waiting for response\n");

// The "Connection: close" header in the GET request tells the Adafruit IO server to close it's connection with us
// when it's finished returning the response. That lets us receive data in a loop until the connection is closed.

// First response data we encounter is the response header which is processed differently than the body
bool getting_headers = true;
while (aio_client.connected())
{
// If there is some data available, process it
while (aio_client.available())
{
String line;
char c;

if (getting_headers == true)
{
// Headers are divided into CRLF delimited lines. Read data until a newline character is encountered then process the line
// We pull data from the aio_client stream one character at a time instead of using readStringUntil('\n') because that function
// doesn't put the terminator character in the string and also this way we can dump *exactly* what comes in to the serial port
// for debugging purposes.
line = "";
c = '\0';
while (c != '\n')
{
c = aio_client.read();
DEBUG_LEVEL_2(c);
line += c;
}

// If we run into an empty line, we've reached the end of the header section of the response
if (line == "\r\n")
{
getting_headers = false;
DEBUG_LEVEL_2("Done Headers\n");
DEBUG_LEVEL_2("Raw Response:\n");
}
else
{
// Parse the current header line. If it's interesting to us, the relevant HTTP_Response structure field will be set
if (ParseHeaderLine(line, resp) == false)
{
return false;
}
}
}
else // if not doing the header, process the response body
{
// The body of the response may be a simple series of characters with no special processing required or it may be
// "chunked" into distinct segments, each of which has a leading indicator of the chunk size (in hex)
if (resp->is_chunked == true)
{
// First time through here we will have just finished the header section and be sitting at the first chunk size.
// We don't actually care much about the chunk size though. We'll just skip the chunk size rows and concatenate the
// body content rows until there's nothing left to process. Since the rows come in size/content pairs, if we
// pull two rows from the stream here, every time we get back here we should be properly aligned to pull the next two.

// Skip chunk size line
c = '\0';
while (c != '\n')
{
c = aio_client.read();
DEBUG_LEVEL_2(c);
}

// Read body content line and append to response body
line = "";
c = '\0';
while (c != '\n')
{
c = aio_client.read();
DEBUG_LEVEL_2(c);
line += c;
}

// When we get a chunked body, the above will leave the CRLF characters tacked onto the end of 'line' which we don't want
// so strip them off before appending 'line' to the response body
line.remove(line.length() - 2);
resp->body += line;
}
else
{
// For non-chunked data, just append all available characters to the response body
for (int i = 0; i < aio_client.available(); i++)
{
resp->body += aio_client.read();
}
}
} // process response body
} // while (aio_client.available())
} // while (aio_client.connected())

// We should have the entire response body now so set the body_len field in the HTTP_Response structure
resp->body_len = resp->body.length();
DEBUG_LEVEL_2("Processed response:\n");
DEBUG_LEVEL_2(resp->body + "\n");
DEBUG_LEVEL_1("Response received\n");
}

// *************************************************************
// **** Parses one line of the header section of an HTTP ****
// **** response, extracting a couple bits of information ****
// **** and populating the relevant fields of resp ****
// *************************************************************
bool ParseHeaderLine(String line, HTTP_Response* resp)
{
// Headers could be upper, lower, or mixed case, so convert to all upper case for string compare operations
line.toUpperCase();

// Check for a Transfer-Encoding header. We need this mostly to determine if the body is split into chunks or not.
if (line.indexOf("TRANSFER-ENCODING") > -1)
{
if (line.indexOf("CHUNKED") > -1)
{
resp->is_chunked = true;
}
else if (line.indexOf("IDENTITY") > -1)
{
resp->is_chunked = false;
}
else
{
// We don't support any other encodings (other would generally be some sort of compression)
return false;
}
}

// Check for a Content-Length header. If present, it specifies the length of the body but also implies no chunking
if (line.indexOf("CONTENT_LENGTH") > -1)
{
resp->is_chunked = false;
resp->body_len = line.substring(line.indexOf(":") + 1).toInt();
}

// Check for a Status header
if (line.indexOf("STATUS") > -1)
{
resp->hdr_status = line.substring(line.indexOf(":") + 1);
resp->hdr_status.trim();
}

return true;
}

// *************************************************************
// **** Prints the given string to the thermal printer ****
// *************************************************************
void PrintToThermalPrinter( String msg, char text_size, char justification, bool bold, bool underline )
{
DEBUG_LEVEL_1("Printing message to thermal printer\n");

// Check paper status before trying to print
if (printer.hasPaper() == false)
{
DEBUG_LEVEL_1("Printer has no paper!\n");
return;
}

printer.justify(justification);
printer.setSize(text_size);

if (bold == true)
{
printer.boldOn();
}
else
{
printer.boldOff();
}

if (underline == true)
{
printer.underlineOn();
}
else
{
printer.underlineOff();
}

printer.println("TASK/ASIGNEE/DUE DATE");
printer.print(msg);
printer.println();
printer.println();
printer.println("_____________________");

}

Related Videos & Resources:

Arduino Automation! Stepper-Driven Automatic Cutting Saw
Arduino Stepper iPad Height Machine!
Arduino Vibe Bowl Screw Feeder