forked from adafruit/Adafruit-Tweet-Receipt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGutenbird.ino
563 lines (494 loc) · 22.3 KB
/
Gutenbird.ino
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
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
/*
Gutenbird demo sketch: monitors one or more Twitter accounts for changes,
displaying updates on attached thermal printer.
Written by Adafruit Industries, distributed under BSD License.
MUST BE CONFIGURED FOR TWITTER 1.1 API BEFORE USE. See notes below.
REQUIRES ARDUINO IDE 1.0 OR LATER -- Back-porting is not likely to occur,
as the code is deeply dependent on the Stream class, etc.
Requires Adafruit fork of Peter Knight's Cryptosuite library for Arduino:
https://github.com/adafruit/Cryptosuite
Required hardware includes an Ethernet-connected Arduino board such as the
Arduino Ethernet or other Arduino-compatible board with an Arduino Ethernet
Shield, plus an Adafruit Mini Thermal Receipt printer and all related power
supplies and cabling.
Resources:
http://www.adafruit.com/products/418 Arduino Ethernet
http://www.adafruit.com/products/284 FTDI Friend
http://www.adafruit.com/products/201 Arduino Uno
http://www.adafruit.com/products/201 Ethernet Shield
http://www.adafruit.com/products/597 Mini Thermal Receipt Printer
http://www.adafruit.com/products/600 Printer starter pack
Uses Twitter 1.1 API. This REQUIRES a Twitter account and some account
configuration. Start at dev.twitter.com, sign in with your Twitter
credentials, select "My Applications" from the avatar drop-down menu at the
top right, then "Create a new application." Provide a name, description,
placeholder URL and complete the captcha, after which you'll be provided a
"consumer key" and "consumer secret" for your app. Select "Create access
token" to also generate an "access token" and "access token secret."
ALL FOUR STRINGS must be copied to the correct positions in the globals below,
and configure the search string to your liking. DO NOT SHARE your keys or
secrets! If you put code on Github or other public repository, replace them
with dummy strings.
Copyright (c) 2013 Adafruit Industries.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <Dns.h>
#include <sha1.h>
#include <Adafruit_Thermal.h>
#include <SoftwareSerial.h>
// Similar to F(), but for PROGMEM string pointers rather than literals
#define F2(progmem_ptr) (const __FlashStringHelper *)progmem_ptr
// Configurable globals. Edit to your needs. -------------------------------
const char PROGMEM
// Twitter application credentials -- see notes above -- DO NOT SHARE.
consumer_key[] = "PUT_YOUR_CONSUMER_KEY_HERE",
access_token[] = "PUT_YOUR_ACCESS_TOKEN_HERE",
signingKey[] = "PUT_YOUR_CONSUMER_SECRET_HERE" // Consumer secret
"&" "PUT_YOUR_ACCESS_TOKEN_SECRET_HERE", // Access token secret
// The ampersand is intentional -- do not delete!
// queryString can be any valid Twitter API search string, including
// boolean operators. See http://dev.twitter.com/docs/using-search
// for options and syntax. Funny characters do NOT need to be URL
// encoded here -- the code takes care of that.
queryString[] = "from:Adafruit",
// Other globals. You probably won't need to change these. -----------------
endpoint[] = "/1.1/search/tweets.json",
agent[] = "Gutenbird v1.0";
const char
host[] = "api.twitter.com";
const int
led_pin = 3, // To status LED (hardware PWM pin)
// Pin 4 is skipped -- this is the Card Select line for Arduino Ethernet!
printer_RX_Pin = 5, // Printer connection: green wire
printer_TX_Pin = 6, // Printer connection: yellow wire
printer_Ground = 7; // Printer connection: black wire
const unsigned long
pollingInterval = 60L * 1000L, // Note: Twitter server will allow 150/hr max
searchesPerDay = 86400000L / pollingInterval,
connectTimeout = 15L * 1000L, // Max time to wait for server connection
responseTimeout = 15L * 1000L; // Max time to wait for data from server
Adafruit_Thermal
printer(printer_RX_Pin, printer_TX_Pin);
byte
maxTweets = 1, // One tweet on first run; avoid runaway output
sleepPos = 0, // Current "sleep throb" table position
resultsDepth, // Used in JSON parsing
// Ethernet MAC address is found on sticker on Ethernet shield or board:
mac[] = { 0x90, 0xA2, 0xDA, 0x00, 0x76, 0x09 };
IPAddress
ip(192,168,0,118); // Fallback address -- code will try DHCP first
char
lastId[21], // 18446744073709551615\0 (64-bit maxint as string)
timeStamp[32], // WWW, DD MMM YYYY HH:MM:SS +XXXX\0
fromUser[16], // Max username length (15) + \0
msgText[141], // Max tweet length (140) + \0
name[12], // Temp space for name:value parsing
value[141]; // Temp space for name:value parsing
int
searchCount = 0;
unsigned long
currentTime = 0L;
EthernetClient
client;
PROGMEM byte
sleepTab[] = { // "Sleep throb" brightness table (reverse for second half)
0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 2, 3, 4, 5, 6, 8, 10, 13,
15, 19, 22, 26, 31, 36, 41, 47, 54, 61,
68, 76, 84, 92, 101, 110, 120, 129, 139, 148,
158, 167, 177, 186, 194, 203, 211, 218, 225, 232,
237, 242, 246, 250, 252, 254, 255 };
// --------------------------------------------------------------------------
void setup() {
// Set up LED "sleep throb" ASAP, using Timer1 interrupt:
TCCR1A = _BV(WGM11); // Mode 14 (fast PWM), 64:1 prescale, OC1A off
TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS11) | _BV(CS10);
ICR1 = 8333; // ~30 Hz between sleep throb updates
TIMSK1 |= _BV(TOIE1); // Enable Timer1 interrupt
sei(); // Enable global interrupts
randomSeed(analogRead(0));
Serial.begin(57600);
pinMode(printer_Ground, OUTPUT);
digitalWrite(printer_Ground, LOW); // Just a reference ground, not power
printer.begin();
printer.sleep();
// Initialize Ethernet connection. Request dynamic
// IP address, fall back on fixed IP if that fails:
Serial.print(F("Initializing Ethernet..."));
if(Ethernet.begin(mac)) {
Serial.print(F("OK\r\n"));
} else {
Serial.print(F("\r\nno DHCP response, using static IP address."));
Ethernet.begin(mac, ip);
}
// Get initial time from time server (make a few tries if needed)
for(uint8_t i=0; (i<5) && !(currentTime = getTime()); delay(15000L), i++);
// Clear all string data
strcpy_P(lastId, PSTR("1"));
timeStamp[0] = fromUser[0] = msgText[0] = name[0] = value[0] = 0;
}
// Search occurs in loop. ---------------------------------------------------
void loop() {
uint8_t *in, out, i;
char nonce[9], // 8 random digits + NUL
searchTime[11], // 32-bit int + NUL
b64[29];
unsigned long startTime, t;
static const char PROGMEM b64chars[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
startTime = millis();
// Disable Timer1 interrupt during network access, else there's trouble.
// Just show LED at steady 100% while working. :T
TIMSK1 &= ~_BV(TOIE1);
analogWrite(led_pin, 255);
// Initialize unique values for query
sprintf(nonce, "%04x%04x", random() ^ currentTime, startTime ^ currentTime);
sprintf(searchTime, "%ld", currentTime);
// Some debugging/testing/status stuff
Serial.print(F(" Current time: "));
Serial.println(currentTime);
Serial.print(F(" Last ID: "));
Serial.println(lastId);
Sha1.initHmac_P((uint8_t *)signingKey, sizeof(signingKey) - 1);
// A dirty hack makes the Oauth song and dance MUCH simpler within the
// Arduino's limited RAM and CPU. A proper general-purpose implementation
// would be expected to URL-encode keys and values (from the complete list
// of GET parameters and authentication values), sort by encoded key,
// concatenate and URL-encode the combined result. Sorting is avoided
// because every query this program performs has exactly the same set of
// parameters, so we've pre-sorted the list as it appears here. Keys
// (and many values) are pre-encoded because they never change; many are
// passed through verbatim because the format is known to not require
// encoding. Most reside in PROGMEM, not RAM. This is bending a LOT of
// rules of Good and Proper Authentication and would land you an 'F' in
// Comp Sci class, but it handles the required task and is VERY compact.
Sha1.print(F("GET&http%3A%2F%2F"));
Sha1.print(host);
urlEncode(Sha1, endpoint, true, false);
Sha1.print(F("&count%3D"));
Sha1.print(maxTweets);
Sha1.print(F("%26include_entities%3D0%26oauth_consumer_key%3D"));
Sha1.print(F2(consumer_key));
Sha1.print(F("%26oauth_nonce%3D"));
Sha1.print(nonce);
Sha1.print(F("%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D"));
Sha1.print(searchTime);
Sha1.print(F("%26oauth_token%3D"));
Sha1.print(F2(access_token));
Sha1.print(F("%26oauth_version%3D1.0%26q%3D"));
urlEncode(Sha1, queryString, true, true);
Sha1.print(F("%26since_id%3D"));
Sha1.print(lastId);
// base64-encode SHA-1 hash output. This is NOT a general-purpose base64
// encoder! It's stripped down for the fixed-length hash -- always 20
// bytes input, always 27 chars output + '='.
for(in = Sha1.resultHmac(), out=0; ; in += 3) { // octets to sextets
b64[out++] = in[0] >> 2;
b64[out++] = ((in[0] & 0x03) << 4) | (in[1] >> 4);
if(out >= 26) break;
b64[out++] = ((in[1] & 0x0f) << 2) | (in[2] >> 6);
b64[out++] = in[2] & 0x3f;
}
b64[out] = (in[1] & 0x0f) << 2;
// Remap sextets to base64 ASCII chars
for(i=0; i<=out; i++) b64[i] = pgm_read_byte(&b64chars[b64[i]]);
b64[i++] = '=';
b64[i++] = 0;
Serial.print(F("Connecting to server..."));
t = millis();
while((client.connect(host, 80) == false) &&
((millis() - t) < connectTimeout));
if(client.connected()) { // Success!
Serial.print(F("OK\r\nIssuing HTTP request..."));
// Unlike the hash prep, parameters in the HTTP request don't require
// sorting, but are still somewhat ordered by function: GET parameters
// (search values), HTTP headers and Oauth credentials.
client.print(F("GET "));
client.print(F2(endpoint));
client.print(F("?count="));
client.print(maxTweets);
client.print(F("&since_id="));
client.print(lastId);
client.print(F("&include_entities=0&q="));
urlEncode(client, queryString, true, false);
client.print(F(" HTTP/1.1\r\nHost: "));
client.print(host);
client.print(F("\r\nUser-Agent: "));
client.print(F2(agent));
client.print(F("\r\nConnection: close\r\n"
"Content-Type: application/x-www-form-urlencoded;charset=UTF-8\r\n"
"Authorization: Oauth oauth_consumer_key=\""));
client.print(F2(consumer_key));
client.print(F("\", oauth_nonce=\""));
client.print(nonce);
client.print(F("\", oauth_signature=\""));
urlEncode(client, b64, false, false);
client.print(F("\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\""));
client.print(searchTime);
client.print(F("\", oauth_token=\""));
client.print(F2(access_token));
client.print(F("\", oauth_version=\"1.0\"\r\n\r\n"));
Serial.print(F("OK\r\nAwaiting results (if any)..."));
t = millis();
while((!client.available()) && ((millis() - t) < responseTimeout));
if(client.available()) { // Response received?
// Could add HTTP response header parsing here (400, etc.)
if(client.find("\r\n\r\n")) { // Skip HTTP response header
Serial.print(F("OK\r\nProcessing results...\r\n"));
resultsDepth = 0;
jsonParse(0, 0);
} else Serial.print(F("response not recognized.\r\n"));
} else Serial.print(F("connection timed out.\r\n"));
Serial.print(F("Done.\r\n"));
client.stop();
} else { // Couldn't contact server
Serial.print(F("failed\r\n"));
}
// Update time in seconds. Once per day, re-sync with time server
currentTime += pollingInterval / 1000L;
if((++searchCount >= searchesPerDay) && (t = getTime())) {
currentTime = t;
searchCount = 0;
}
// Sometimes network access & printing occurrs so quickly, the steady-on
// LED wouldn't even be apparent, instead resembling a discontinuity in
// the otherwise smooth sleep throb. Keep it on at least 4 seconds.
while((millis() - startTime) < 4000UL);
// Pause between queries, factoring in time already spent on network
// access, parsing, printing and LED pause above.
if((millis() - startTime) < pollingInterval) {
Serial.print(F("Pausing..."));
sleepPos = sizeof(sleepTab); // Resume following brightest position
TIMSK1 |= _BV(TOIE1); // Re-enable Timer1 interrupt for sleep throb
while((millis() - startTime) < pollingInterval);
Serial.print(F("done\r\n"));
}
}
// Helper functions. --------------------------------------------------------
boolean jsonParse(int depth, byte endChar) {
int c, i;
boolean readName = true;
for(;;) {
while(isspace(c = timedRead())); // Scan past whitespace
if(c < 0) return false; // Timeout
if(c == endChar) return true; // EOD
if(c == '{') { // Object follows
if(!jsonParse(depth + 1, '}')) return false;
if(!depth) return true; // End of file
if(depth == resultsDepth) { // End of object in results list
// Output one tweet to printer
printer.wake();
printer.inverseOn();
printer.write(' ');
printer.print(fromUser);
for(i=strlen(fromUser); i<31; i++) printer.write(' ');
printer.inverseOff();
printer.underlineOn();
printer.print(timeStamp);
for(i=strlen(timeStamp); i<32; i++) printer.write(' ');
printer.underlineOff();
printer.println(msgText);
printer.feed(3);
printer.sleep();
// Dump to serial console as well
Serial.print(F(" User: "));
Serial.println(fromUser);
Serial.print(F(" Text: "));
Serial.println(msgText);
Serial.print(F(" Time: "));
Serial.println(timeStamp);
// Clear strings for next object
timeStamp[0] = fromUser[0] = msgText[0] = 0;
maxTweets = 5; // After first, subsequent queries allow more tweets
}
} else if(c == '[') { // Array follows
if((!resultsDepth) && (!strcasecmp(name, "statuses")))
resultsDepth = depth + 1;
if(!jsonParse(depth + 1,']')) return false;
} else if((c == '"') || (c == '\'')) { // String follows
if(readName) { // Name-reading mode
if(!readString(name, sizeof(name)-1, c)) return false;
} else { // Value-reading mode
if(!readString(value, sizeof(value)-1, c)) return false;
// Process name and value strings:
if (!strcasecmp(name, "max_id_str")) {
strncpy(lastId, value, sizeof(lastId)-1);
} else if(!strcasecmp(name, "created_at")) {
// Use created_at value for tweet, not user
if(depth == (resultsDepth + 1)) {
strncpy(timeStamp, value, sizeof(timeStamp)-1);
}
} else if(!strcasecmp(name, "screen_name")) {
strncpy(fromUser, value, sizeof(fromUser)-1);
} else if(!strcasecmp(name, "text")) {
strncpy(msgText, value, sizeof(msgText)-1);
} else if((!strcasecmp(name, "id_str")) &&
(strcasecmp(value, lastId) > 0) &&
(depth == (resultsDepth + 1))) {
strncpy(lastId, value, sizeof(lastId)-1);
}
}
} else if(c == ':') { // Separator between name:value
readName = false; // Now in value-reading mode
value[0] = 0; // Clear existing value data
} else if(c == ',') {
// Separator between name:value pairs.
readName = true; // Now in name-reading mode
name[0] = 0; // Clear existing name data
} // Else true/false/null or a number follows. These values aren't
// used or expected by this program, so just ignore...either a comma
// or endChar will come along eventually, these are handled above.
}
}
// Read string from client stream into destination buffer, up to a maximum
// requested length. Buffer should be at least 1 byte larger than this to
// accommodate NUL terminator. Opening quote is assumed already read,
// closing quote will be discarded, and stream will be positioned
// immediately following the closing quote (regardless whether max length
// is reached -- excess chars are discarded). Returns true on success
// (including zero-length string), false on timeout/read error.
boolean readString(char *dest, int maxLen, char quote) {
int c, len = 0;
while((c = timedRead()) != quote) { // Read until closing quote
if(c == '\\') { // Escaped char follows
c = timedRead(); // Read it
// Certain escaped values are for cursor control --
// there might be more suitable printer codes for each.
if (c == 'b') c = '\b'; // Backspace
else if(c == 'f') c = '\f'; // Form feed
else if(c == 'n') c = '\n'; // Newline
else if(c == 'r') c = '\r'; // Carriage return
else if(c == 't') c = '\t'; // Tab
else if(c == 'u') c = unidecode(4);
else if(c == 'U') c = unidecode(8);
// else c is unaltered -- an escaped char such as \ or "
} // else c is a normal unescaped char
if(c < 0) return false; // Timeout
// In order to properly position the client stream at the end of
// the string, characters are read to the end quote, even if the max
// string length is reached...the extra chars are simply discarded.
if(len < maxLen) dest[len++] = c;
}
dest[len] = 0;
return true; // Success (even if empty string)
}
// Read a given number of hexadecimal characters from client stream,
// representing a Unicode symbol. Return -1 on error, else return nearest
// equivalent glyph in printer's charset. (See notes below -- for now,
// always returns '-' or -1.)
int unidecode(byte len) {
int c, v, result = 0;
while(len--) {
if((c = timedRead()) < 0) return -1; // Stream timeout
if ((c >= '0') && (c <= '9')) v = c - '0';
else if((c >= 'A') && (c <= 'F')) v = 10 + c - 'A';
else if((c >= 'a') && (c <= 'f')) v = 10 + c - 'a';
else return '-'; // garbage
result = (result << 4) | v;
}
// To do: some Unicode symbols may have equivalents in the printer's
// native character set. Remap any such result values to corresponding
// printer codes. Until then, all Unicode symbols are returned as '-'.
// (This function still serves an interim purpose in skipping a given
// number of hex chars while watching for timeouts or malformed input.)
return '-';
}
// Read from client stream with a 5 second timeout. Although an
// essentially identical method already exists in the Stream() class,
// it's declared private there...so this is a local copy.
int timedRead(void) {
unsigned long start = millis();
while((!client.available()) && ((millis() - start) < 5000L));
return client.read(); // -1 on timeout
}
// URL-encoding output function for Print class.
// Input from RAM or PROGMEM (flash). Double-encoding is a weird special
// case for Oauth (encoded strings get encoded a second time).
void urlEncode(
Print &p, // EthernetClient, Sha1, etc.
const char *src, // String to be encoded
boolean progmem, // If true, string is in PROGMEM (else RAM)
boolean x2) // If true, "double encode" parenthesis
{
static const char PROGMEM hexChar[] = "0123456789ABCDEF";
uint8_t c;
while((c = (progmem ? pgm_read_byte(src) : *src))) {
if(((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) ||
((c >= '0') && (c <= '9')) || strchr_P(PSTR("-_.~"), c)) {
p.write(c);
} else {
if(x2) p.print("%25");
else p.write('%');
p.write(pgm_read_byte(&hexChar[c >> 4]));
p.write(pgm_read_byte(&hexChar[c & 15]));
}
src++;
}
}
// Minimalist time server query; adapted from Arduino UdpNTPClient tutorial.
unsigned long getTime(void) {
EthernetUDP udp;
DNSClient dns;
IPAddress addr;
byte buf[48];
unsigned long t = 0L;
Serial.print(F("Polling time server..."));
udp.begin(8888);
dns.begin(Ethernet.dnsServerIP());
// Get a time server address from NTP pool
if(dns.getHostByName("pool.ntp.org", addr)) {
static const char PROGMEM
timeReqA[] = { 227, 0, 6, 236 },
timeReqB[] = { 49, 78, 49, 52 };
// Assemble and issue request packet
memset(buf, 0, sizeof(buf));
memcpy_P( buf , timeReqA, sizeof(timeReqA));
memcpy_P(&buf[12], timeReqB, sizeof(timeReqB));
udp.beginPacket(addr, 123);
udp.write(buf, sizeof(buf));
udp.endPacket();
delay(1000); // Allow time for response
if(udp.parsePacket()) {
// Read result, convert to UNIX time format
udp.read(buf, sizeof(buf));
t = (((unsigned long)buf[40] << 24) |
((unsigned long)buf[41] << 16) |
((unsigned long)buf[42] << 8) |
(unsigned long)buf[43]) - 2208988800UL;
Serial.print(F("OK\r\n"));
}
}
udp.stop();
if(!t) Serial.print(F("error\r\n"));
return t;
}
// Timer1 interrupt handler for sleep throb
ISR(TIMER1_OVF_vect, ISR_NOBLOCK) {
// Sine table contains only first half...reflect for second half...
analogWrite(led_pin, pgm_read_byte(&sleepTab[
(sleepPos >= sizeof(sleepTab)) ?
(sizeof(sleepTab) * 2 - 1 - sleepPos) : sleepPos]));
if(++sleepPos >= (sizeof(sleepTab) * 2)) sleepPos = 0; // Roll over
TIFR1 |= TOV1; // Clear Timer1 interrupt flag
}