Statistics
| Branch: | Revision:

imlightsw / PiBox / imlightsw.ino @ 0d84cfd8

History | View | Annotate | Download (31.6 KB)

1
/*******************************************************************************
2
 * PiBox IoT: ESP8266 controller
3
 *
4
 * imlightsw.ino:  Provide RESTful interface to control this device.
5
 *
6
 * License: see LICENSE file
7
 ******************************************************************************
8
 * Notes:
9
 * Being a C file (and not Java), please adhere to the 80 character line
10
 * length standard in this file.
11
 ******************************************************************************/
12

    
13
/* Core libraries */
14
#include <stdio.h>
15
#include <string.h>
16
#include <FS.h>
17
// #include <EEPROM.h>
18
#include <ESP8266WiFi.h>
19
#include <WiFiClient.h>
20
#include <ESP8266WebServer.h>
21
#include <ESP8266HTTPClient.h>
22
#include <WiFiManager.h>
23
#include <WiFiUdp.h>
24

    
25
/* Third party libraries */
26
#include <AES.h>
27
#include <Timer.h>
28
#include <aJSON.h>
29

    
30
/*
31
 * If defined, use the serial output instead of the Blue LED pin.
32
 */
33
#ifdef USE_SERIAL
34
#define PRINTLN(a)      Serial.println(a);
35
#define PRINT(a)        Serial.print(a);
36
#define SERIAL_BEGIN(a) Serial.begin(a);
37
#define SETBLUELED(a)
38
#else
39
#define PRINTLN(a)
40
#define PRINT(a)
41
#define SERIAL_BEGIN(a)
42
#define SETBLUELED(a)   setBlueLed(a)
43
#endif
44

    
45
/*
46
 * Blue LED pin id.
47
 */
48
#define BLUE_LED_PIN    1
49

    
50
/*
51
 * Blink Patterns
52
 */
53
#define BP_CONNECTED            blinkPattern(4,1000)
54
#define BP_CONFIG_MODE          blinkPattern(2,500)
55
#define BP_CONNECT_MODE         blinkPattern(5,125)
56
#define BP_PAIR1_MODE           blinkPattern(2,150)
57
#define BP_PAIR2_MODE           blinkPattern(2,750)
58
#define BP_NOT_REGISTERED       blinkPattern(1,1500)
59

    
60
/*
61
 * Filename for saving credentials.
62
 */
63
char *registration_path     = "/registration.txt";
64

    
65
/*
66
 * Config Mode AP SSID
67
 */
68
String apname = "imiot";
69

    
70
/*
71
 * In the case were setup fails, we don't want to do anything in our main loop.
72
 */
73
boolean resetMode = false;
74

    
75
/*
76
 * AP Config mode disabled by default.
77
 * If Config button is ON at boot, we go into AP config mode.
78
 * After config is complete, button must be set OFF and device power cycled.
79
 * If Config button is OFF at boot, but turned on after then we go into PAIR mode (LED solid).
80
 * After PAIRING (LED blinks) the button must be turned off for normal operation.
81
 */
82
boolean configMode = false;
83

    
84
/*
85
 * Multicast setup
86
 * This is the address/port we use to send a multicast message asking
87
 * for monitor's to respond.  The first response we get is the one we
88
 * try to pair with.
89
 */
90
#define MULTICAST_PORT          13911
91
IPAddress ipMulti( 234, 1, 42, 3 );
92

    
93
/*
94
 * Incrememented with each multicast message we send, to identify
95
 * async responses to them.
96
 */
97
static unsigned long multicastID = 0;
98

    
99
/*
100
 * Timer object
101
 * To functions are "scheduled" to run periodically.
102
 * One is a registration function.  This runs only while we aren't paired.
103
 * The other is the server, which handles inbound REST API requests.
104
 */
105
Timer t;
106
int registerID = -1;
107
int serverID   = -1;
108

    
109
/*
110
 * Registration and encryption
111
 * The uuid is provided by the monitor as part of the pairing process.
112
 * The uuid identifies this device to the monitor and is used, in part, as the key.
113
 * The key is used to encrypt/decrypt messages to/from the monitor.
114
 */
115
String uuid           = "";
116
String key            = "";
117
String monitorAddress = "";
118

    
119
/*
120
 * Object used to handle encryption/decryption.
121
 */
122
AES aes;
123

    
124
/*
125
 * Device type and version
126
 * These aren't really used yet but may be in the future as part of
127
 * the pairing process.
128
 */
129
#define MT_IRONMAN      7
130
#define MA_PAIR_IOT     1
131
static char msgType     = MT_IRONMAN;
132
static char msgAction   = MA_PAIR_IOT;
133

    
134
/*
135
 * This is the web server that accepts inbound messages.
136
 * HTTPS is too taxing on an ESP-01 so we encrypt the body of messages instead.
137
 */
138
ESP8266WebServer server(80);
139

    
140
/*
141
 * Handle auto-connection
142
 * Globally scoped so API can reset, if necessary.
143
 */
144
WiFiManager wifiManager;
145

    
146
/* 
147
 * NOTE: WiFi object is always available because we included ESP8266WiFi.h 
148
 * See https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/station-class.html
149
 */
150

    
151
/*
152
 * Operational Flags
153
 */
154
char flags = 0;
155
#define FL_REGISTERED       0x01   // If this flag is set then we're paired already.
156
#define FL_IN_REGISTRATION  0x02   // If this flag is set then we're currently trying to register.
157
#define FL_SPIFFS_OK        0x04   // If this flag is set then SPIFFS is available.
158

    
159
/*
160
 * Function Prototypes
161
 */
162
int     handleDevicePOST();
163
int     handleDeviceGET();
164
int     handleRegistration();
165
int     handleNotFound();
166
int     paired();
167
void    saveFile( char *path, String data );
168
String  readFile( char *path );
169
void    blinkPattern(int count, int interval);
170

    
171
/*
172
 * ========================================================================
173
 * Device Specific Functions and variables
174
 * ========================================================================
175
 */
176

    
177
/*
178
 * Device state
179
 * Off: 0
180
 * On:  1
181
 */
182
int state = 0;
183

    
184
/*
185
 * ========================================================================
186
 * Name:   pairEnabled
187
 * Prototype:  int pairEnabled()( void )
188
 *
189
 * Description:
190
 * Checks GPIO pins to see if the pair button is enabled.
191
 *
192
 * Returns:
193
 * 1 if the the button is enabled or 0 (zero) if it is not.
194
 * ========================================================================
195
 */
196
int pairEnabled()
197
{
198
    boolean pairMode = false;
199

    
200
    /* Set pin modes */
201
    pinMode(0, OUTPUT);
202
    pinMode(2, INPUT);
203

    
204
    /* Set GPIO0 output low */
205
    // PRINTLN("PAIR Mode: Set GPIO0 low.");
206
    digitalWrite(0, LOW);
207

    
208
    /* check GPIO2 input to see if push button pressed connecting it to GPIO0 */
209
    // PRINTLN("PairMode: Checking GPIO2 state.");
210
    pairMode = (digitalRead(2) == LOW);
211

    
212
    /* Reset GPIO0 HIGH to turn off the relay */
213
    // PRINTLN("PairMode: Resetting GPIO0.");
214
    digitalWrite(0, HIGH);
215

    
216
    if ( pairMode )
217
        return(1);
218
    else
219
        return(0);
220
}
221

    
222
/*
223
 * ========================================================================
224
 * Name:   setupURIHandlers
225
 * Prototype:  void setupURIHandlers()( void )
226
 *
227
 * Description:
228
 * Sets the callbacks for specific URI patterns.
229
 * ========================================================================
230
 */
231
void setupURIHandlers()
232
{
233
    server.on("/device",   HTTP_POST, handleDevicePOST);
234
    server.on("/device",   HTTP_GET,  handleDeviceGET);
235
    server.on("/register", HTTP_POST, handleRegistration);
236
    server.onNotFound(handleNotFound);
237
}
238

    
239
/*
240
 * ========================================================================
241
 * Name:   handleDevicePOST
242
 * Prototype:  int handleDevicePOST()( void )
243
 *
244
 * Description:
245
 * A POST is used to change device state.  The body of the message is AES
246
 * encoded in the following format:
247
 *    UUID+JSON
248
 * where state is OFF or ON (as text strings).
249
 * ========================================================================
250
 */
251
int handleDevicePOST()
252
{
253
    String          body;
254
    char            *json_string;
255
    char            keyChar[sizeof(key)+1];
256
    byte            *keyPtr;
257
    byte            *bodyPtr;
258
    aJsonObject*    root;
259
    aJsonObject*    device;
260

    
261
    /*
262
     * Validate the request.
263
     */
264
    if ( ! paired() )
265
    {
266
        server.send(401, "text/plain", "Failed");
267
        return(1);
268
    }
269

    
270
    /*
271
     * Extract body of message.
272
     */
273
    if (server.hasArg("plain")== false)
274
    {
275
        server.send(400, "text/plain", "Body not received");
276
        return(1);
277
    }
278
    body = server.arg("plain");
279

    
280
    // Convert key to byte *keyPtr
281
    key.toCharArray(keyChar, sizeof(key));
282
    keyPtr = (unsigned byte *)keyChar;
283

    
284
    // Convert body to byte *bodyPtr
285
    char bodyChar[body.length()+1];
286
    body.toCharArray(bodyChar, body.length());
287
    bodyPtr = (unsigned byte *)bodyChar;
288

    
289
    /*
290
     * Decrypt the message, extracting the JSON.
291
     */
292
    byte json[body.length()];
293
    char *ptr = (char *)json;
294
    aes.do_aes_decrypt(bodyPtr, body.length(), (byte *)ptr, (byte *)keyChar, 128);
295

    
296
    /*
297
     * Pull the UUID and make sure it matches.
298
     */
299
    if ( strncmp((char *)uuid.c_str(), (char *)ptr, uuid.length()) != 0 )
300
    {
301
        server.send(409, "text/plain", "Failed");
302
        return(1);
303
    }
304

    
305
    /*
306
     * Move past the UUIDtogetthe real JSON request.
307
     */
308
    ptr += uuid.length();
309

    
310
    /*
311
     * Parse the JSON.
312
     */
313
    root = aJson.parse( (char *)ptr );
314
    device = aJson.getObjectItem(root, "device");
315

    
316
    /*
317
     * Process request.
318
     */
319
    // field = aJson.getObjectItem(root, "fieldName");
320
    // PRINTLN(field->valuestring);
321

    
322
    // Ack the request.
323
    server.send(200, "text/plain", "Ok");
324

    
325
    // Cleanup
326
    aJson.deleteItem(root);
327
}
328

    
329
/*
330
 * ========================================================================
331
 * Name:   handleDeviceGET
332
 * Prototype:  int handleDeviceGET()( void )
333
 *
334
 * Description:
335
 * A GET is used to retrieve device state.  The body of the message is AES
336
 * encoded in the following format:
337
 *    UUID+JSON
338
 * where state is OFF or ON (as text strings).
339
 *
340
 * Notes:
341
 * See https://github.com/spaniakos/AES/blob/master/examples_Rpi/aes.cpp
342
 * For random number of iv: https://www.tutorialspoint.com/arduino/arduino_random_numbers.htm
343
 * Random seed should come from TX or RX in operational mode, but won't matter otherwise.
344
 * ========================================================================
345
 */
346
int handleDeviceGET()
347
{
348
    String          message;
349
    String          ptr;
350
    aJsonObject     *root,*fmt;
351
    char            *json_String;
352
    char            *uuidChar;
353
    char            keyChar[sizeof(key)+1];
354
    byte            *keyPtr;
355

    
356
    /*
357
     * Reject requests when we're not paired.
358
     */
359
    if ( ! paired() )
360
    {
361
        server.send(401, "text/plain", "Failed");
362
        return(1);
363
    }
364

    
365
    // Convert key to byte *keyPtr
366
    key.toCharArray(keyChar, sizeof(key));
367
    keyPtr = (unsigned byte *)keyChar;
368

    
369
    // Build our JSON response
370
    root = aJson.createObject();
371
    fmt = aJson.createObject();
372
    aJson.addItemToObject(root, "device", fmt);
373
    aJson.addNumberToObject(fmt,"state",  state);
374
    json_String = aJson.print(root);
375
    message = String(json_String);
376
    aJson.deleteItem(root);
377

    
378
    // Build outbound string.
379
    ptr = uuid + message;
380

    
381
    // Retrieve byte array from our constructed message.
382
    byte bytes[ptr.length()+1];
383
    memset(bytes, 0, ptr.length()+1);
384
    ptr.getBytes(bytes, ptr.length());
385

    
386
    // Need properly sized buffers for output from AES encryption
387
    byte plain_p[ptr.length() + (N_BLOCK - (ptr.length() % 16)) - 1];
388
    byte cipher[sizeof(plain_p)];
389

    
390
    // Encrypt the message.
391
    aes.do_aes_encrypt((byte *)bytes, (int)ptr.length(),
392
            (byte *)cipher, (byte *)keyChar, 128);
393

    
394
    // Encode message as string
395

    
396
    /* Build the JSON response packet.  This is the encrypted version of our message. */
397
    // TBD
398
    // root = aJson.createObject();
399
    // aJson.addStringToObject(root, "iv", fmt);
400
    // aJson.addStringToObject(root, "message", bytes);
401

    
402
    // Send response
403
    byte message_str[ptr.length()+1];
404
    memset(message_str, 0, ptr.length()+1);
405
    ptr.getBytes( message_str, ptr.length() );
406
    PRINTLN((char *)message_str);
407
    server.send(200, "text/plain", (char *)message_str);
408

    
409
    // Cleanup
410
    aJson.deleteItem(root);
411
}
412

    
413
/*
414
 * ========================================================================
415
 * Name:   handleRegistration
416
 * Prototype:  int handleRegistration()( void )
417
 *
418
 * Description:
419
 * A UUID is passed from the monitor in response to a multicast 
420
 * registration request.  This is the server's way of saying
421
 * "You are UUID".
422
 *
423
 * Since this is our first contact we have to pass this in the open.
424
 * Because of that we make it simple and just pass the UUID in
425
 * the URI, as such:  /register/<uuid>.
426
 * 
427
 * If the server responds with HTTP 200 then we're registered.
428
 *
429
 * Notes:
430
 * Insecure?  Sure.  But it requires someone listening precisely for two
431
 * specific, non-repeated messages to know exactly how to hack this device.
432
 * The uuid is never passed in clear text on-the-wire again.
433
 *
434
 * And yes, I thought about HTTPS, but ESP8266's (specifically ESP-01's)
435
 * really can't do those.  AES encrypted and encoded JSON is good enough.
436
 *
437
 * Imporant:
438
 * Once registered if you move your monitor (re: server) IP then you
439
 * have to re-pair!
440
 * ========================================================================
441
 */
442
int handleRegistration()
443
{
444
    String message   = "";
445
    String path      = server.uri();
446
    int imIdx        = -1;
447
    int registerIdx  = -1;
448
    int uuidIdx      = -1;
449
    HTTPClient http;
450

    
451
    /*
452
     * Do we already have a UUID?  If so, ignore this request.
453
     * But don't tell the requester why.  They should know already.
454
     */
455
    if ( flags & FL_REGISTERED )
456
    {
457
        message = "UUID already set; registration request will be quietly ignored.";
458
        PRINTLN(message);
459
        server.send(200, "text/plain", "Okay");
460
        return(0);
461
    }
462

    
463
    /* Get URI path separator indices */
464
    imIdx = path.indexOf("/", 1);
465
    if ( imIdx != -1 )
466
        registerIdx = path.indexOf("/", imIdx+1);
467
    if ( registerIdx != -1 )
468
        uuidIdx = path.indexOf("/", registerIdx+1);
469
    if ( uuidIdx == -1 )
470
    {
471
        message = "Missing required UUID component: uri = ";
472
        message += server.uri();
473
        PRINTLN(message);
474
        server.send(400, "text/plain", "Failed");
475
        return(1);
476
    }
477

    
478
    /* Parse the UUID that will be used until the next pairing. */
479
    uuid = path.substring(uuidIdx+1);
480
    if ( uuid.length() < 16 )
481
    {
482
        message = "UUID length is too short (<16 characters)";
483
        PRINTLN(message);
484
        server.send(406, "text/plain", "Failed");
485
        return(1);
486
    }
487

    
488
    /* Gen AES key from UUID (like Jarvis) */
489
    key = uuid.substring(0,16);
490

    
491
    /* Save the monitor's IP address */
492
    monitorAddress = server.client().remoteIP().toString();
493

    
494
    /* Ack the request. */
495
    server.send(200, "text/plain", "Ok");
496

    
497
    /* 
498
     * Now complete the registration by calling the monitor's registration REST API: POST /pair/iot/register/UUID
499
     * Use ESP8266HTTPClient to contact the monitor:
500
     *      See https://techtutorialsx.com/2016/07/21/esp8266-post-requests/
501
     */
502

    
503
    String url = "http://" + monitorAddress + "/pair/iot/register/" + uuid;
504
    PRINTLN("Registering with url = " + url);
505
    http.begin(url);
506
    http.addHeader("Content-Type", "text/plain");
507
    int httpCode = http.POST("{}");
508
    PRINTLN(httpCode);
509
    if ( httpCode != 200 )
510
    {
511
        message = "Failed registration: code = " + httpCode;
512
        PRINTLN(message);
513
    }
514
    else
515
    {
516
        PRINTLN("Registration OK.");
517

    
518
        /* Save registration data. */
519
        String registration_info = String(uuid + ":" + key + ":" + monitorAddress + "\n");
520
        saveFile(registration_path, registration_info);
521

    
522
        /* Mark us a registered */
523
        flags |= FL_REGISTERED;
524

    
525
        /* Stop registration multicast */
526
        if ( registerID != -1 )
527
        {
528
            t.stop(registerID);
529
            registerID = -1;
530
        }
531
    }
532
}
533

    
534
/*
535
 * ========================================================================
536
 * Support Functions
537
 * ========================================================================
538
 */
539

    
540
/*
541
 * ========================================================================
542
 * Name:   serialNotify
543
 * Prototype:  void serialNotify()( void )
544
 *
545
 * Description:
546
 * Just display a message to the serial console.  This is a timed event.
547
 * ========================================================================
548
 */
549
void serialNotify()
550
{
551
    PRINTLN("In Operational Mode.");
552

    
553
    /*
554
     * Let user know if we're not registered yet.
555
     */
556
    if ( !(flags & FL_REGISTERED) )
557
    {
558
        BP_NOT_REGISTERED;
559
    }
560

    
561
}
562

    
563
/*
564
 * ========================================================================
565
 * Name:   paired
566
 * Prototype:  int paired()( void )
567
 *
568
 * Description:
569
 * Check if this device is already paired.
570
 *
571
 * Returns:
572
 * 0 if we are not paired or the request comes from the wrong monitor IP.
573
 * 1 if we are paired and the request comes from the saved monitor IP.
574
 * ========================================================================
575
 */
576
int paired()
577
{
578
    /*
579
     * Are we registered?  That's the basic requirement.
580
     */
581
    if ( !(flags & FL_REGISTERED) )
582
    {
583
        return(0);
584
    }
585

    
586
    /*
587
     * Does the request come from the server we expect it to?
588
     */
589
    String clientIP = server.client().remoteIP().toString();;
590
    if ( !clientIP.equals(monitorAddress) )
591
    {
592
        return(0);
593
    }
594

    
595
    /* Looks good. */
596
    return(1);
597
}
598

    
599
/*
600
 * ========================================================================
601
 * Name:   webLoop
602
 * Prototype:  void webLoop()( void )
603
 *
604
 * Description:
605
 * Check the web server for client connections.
606
 * ========================================================================
607
 */
608
void webLoop()
609
{
610
    server.handleClient();
611
}
612

    
613
/*
614
 * ========================================================================
615
 * Name:   deviceRegister
616
 * Prototype:  void deviceRegister()( void )
617
 *
618
 * Description:
619
 * Send registration request via multicast until registration completes.
620
 *
621
 * Notes:
622
 * Multicast request is Ack'd by server.  We use that Ack to identify
623
 * the server IP so we can contact it via http to complete registration.
624
 * ========================================================================
625
 */
626
void deviceRegister()
627
{
628
    String message;
629

    
630
    /* Skip if Wifi is not running. */
631
    if (WiFi.status() != WL_CONNECTED)
632
        return;
633

    
634
    /*
635
     * Send multicast request.  The outbound message has the following format.
636
     * Byte 0:      Message type  (MT_IRONMAN)
637
     * Byte 1:      Action type (MA_PAIR_IOT)
638
     * Byte 3-4:    Unused
639
     * Byte 5-8:    Integer size of payload (0, but payload size is 4 bytes long)
640
     * If the server accepts those, it responds with an UUID via the web interface.
641
     */
642

    
643
    WiFiUDP Udp;
644
    Udp.beginPacketMulticast(ipMulti, MULTICAST_PORT, WiFi.localIP());
645
    Udp.write(msgType);
646
    Udp.write(msgAction);
647
    char buffer = 0;
648
    for(int i=0; i<6; i++)
649
    {
650
        Udp.write(buffer);
651
    }
652
    Udp.endPacket();
653

    
654
    message  = "Sent multicast announcement: \n";
655
    message += multicastID;
656
    multicastID++;
657
    PRINTLN(message);
658
}
659

    
660
/*
661
 * ========================================================================
662
 * Command Handlers
663
 * ========================================================================
664
 */
665

    
666
/*
667
 * ========================================================================
668
 * Name:   reset
669
 * Prototype:  int reset( void )
670
 *
671
 * Description:
672
 * Reset WiFi connection, allowing reconfiguration.
673
 *
674
 * Notes:
675
 * Never returns because it calls ESP.reset().
676
 * ========================================================================
677
 */
678
void reset()
679
{
680
    wifiManager.resetSettings();
681
    flags          = 0;
682
    uuid           = "";
683
    key            = "";
684
    monitorAddress = "";
685
    delay(3000);
686
}
687

    
688
/*
689
 * ========================================================================
690
 * Name:   handleNotFound
691
 * Prototype:  int handleNotFound( void )
692
 *
693
 * Description:
694
 * Handle invalid requests.  This sends a text reply, which may not be
695
 * suitable for the remote caller.
696
 * ========================================================================
697
 */
698
int handleNotFound()
699
{
700
    /*
701
     * Requests when we're not paired are rejected.
702
     */
703
    if ( ! paired() )
704
    {
705
        server.send(401, "text/plain", "Failed");
706
        return(1);
707
    }
708

    
709
    /*
710
     * Validate the request.  That's more important than telling them it's
711
     * a bad request.
712
     */
713
    String message = "Invalid command\n\n";
714
    message += "URI: ";
715
    message += server.uri();
716
    message += "\nMethod: ";
717
    message += (server.method() == HTTP_GET)?"GET":"POST";
718
    message += "\nArguments: ";
719
    message += server.args();
720
    message += "\n";
721
    for (uint8_t i=0; i<server.args(); i++)
722
    {
723
        message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
724
    }
725
    server.send(400, "text/plain", message);
726
}
727

    
728
/*
729
 * ========================================================================
730
 * Name:   configModeCallback
731
 * Prototype:  void configModeCallback( void )
732
 *
733
 * Description:
734
 * Print status info if connection is lost.
735
 * ========================================================================
736
 */
737
void configModeCallback(WiFiManager *myWiFiManager)
738
{
739
    PRINTLN("Failed connection to last AP.");
740
    PRINTLN("Entered config mode.");
741
    PRINTLN(WiFi.softAPIP());
742
    PRINTLN(myWiFiManager->getConfigPortalSSID());
743
}
744

    
745
/*
746
 * ========================================================================
747
 * Pin Handlers
748
 * ========================================================================
749
 */
750

    
751
/*
752
 * ========================================================================
753
 * Name:   setBlueLed
754
 * Prototype:  void setBlueLed( boolean enabled )
755
 *
756
 * Description:
757
 * Set the state of the blue LED.  This only works if USE_SERIAL is
758
 * not defined.
759
 *
760
 * Notes:
761
 * LED is lit when the pin is set LOW.
762
 * ========================================================================
763
 */
764
void setBlueLed(boolean enabled)
765
{
766
    pinMode(BLUE_LED_PIN, OUTPUT);
767
    if ( enabled )
768
    {
769
        digitalWrite(1, LOW);
770
    }
771
    else
772
    {
773
        digitalWrite(1, HIGH);
774
    }
775
}
776

    
777
/*
778
 * ========================================================================
779
 * Name:   relayOn
780
 * Prototype:  void relayOn( void )
781
 *
782
 * Description:
783
 * Turns the relay on, enabling power to flow to the controlled device.
784
 * ========================================================================
785
 */
786
void relayOn(void)
787
{
788
    PRINTLN("Turning relay on.");
789
    pinMode(2, INPUT);
790
    pinMode(0, OUTPUT);
791
    digitalWrite(0, LOW);
792
}
793

    
794
/*
795
 * ========================================================================
796
 * Name:   relayOff
797
 * Prototype:  void relayOff( void )
798
 *
799
 * Description:
800
 * Turns the relay off, disabling power to the controlled device.
801
 * ========================================================================
802
 */
803
void relayOff(void)
804
{
805
    PRINTLN("Turning relay off.");
806
    pinMode(0, INPUT);
807
    pinMode(2, OUTPUT);
808
    digitalWrite(2, LOW);
809
}
810

    
811
/*
812
 * ========================================================================
813
 * Name:   blinkPattern
814
 * Prototype:  void blinkPattern( int count, int interval )
815
 *
816
 * Description:
817
 * Blinks the BlueLED in a specified pattern.
818
 * ========================================================================
819
 */
820
void blinkPattern(int count, int interval)
821
{
822
    /* Blink patterns repeat "count" times, with "interval" milliseconds between patterns. */
823
    for(int i=0; i<count; i++)
824
    {
825
        SETBLUELED(true); delay(interval); SETBLUELED(false); delay(interval);
826
        PRINT(".");
827
    }
828
}
829

    
830
/*
831
 * ========================================================================
832
 * File Management
833
 * ========================================================================
834
 */
835

    
836
/*
837
 * ========================================================================
838
 * Name:   saveFile
839
 * Prototype:  void saveFile( char *path, String data )
840
 *
841
 * Description:
842
 * Saves data to the specified path.  The contents of the file associated 
843
 * with path will be replaced by the new data.
844
 * ========================================================================
845
 */
846
void saveFile( char *path, String data )
847
{
848
    String message;
849
    if ( ! (flags & FL_SPIFFS_OK) )
850
    {
851
        message = "SPIFFS not mounted.  Can't save to " + String(path);
852
        PRINTLN(message);
853
    }
854

    
855
    File f = SPIFFS.open(path, "w");
856
    if ( !f ) 
857
    {
858
        message = "Failed to open " + String(path) + " for writing.";
859
        PRINTLN(message);
860
    }
861
    else
862
    {
863
        f.println(data.c_str());
864
        f.close();
865
        message = "Saved data to " + String(path);
866
        PRINTLN(message);
867
    } 
868
}
869

    
870
/*
871
 * ========================================================================
872
 * Name:   readFile
873
 * Prototype:  String readFile( char *path )
874
 *
875
 * Description:
876
 * Reads the file from the specified path and returns it as a single string,
877
 * including newlines.
878
 * ========================================================================
879
 */
880
String readFile( char *path )
881
{
882
    String message;
883
    if ( ! (flags & FL_SPIFFS_OK) )
884
    {
885
        message = "SPIFFS not mounted.  Can't read from " + String(path);
886
        PRINTLN(message);
887
    }
888

    
889
    String data = String("");
890
    File f = SPIFFS.open(path, "r");
891
    if ( f ) 
892
    {
893
        PRINTLN("Reading from SPIFFS");
894
        while(f.available()) 
895
        {
896
            //Lets read line by line from the file
897
            data.concat( f.readStringUntil('\n') );
898
            data.concat( String('\n') );
899
        }
900
        f.close();
901
        message = "Read from " + String(path) + ": " + data;
902
        PRINTLN(message);
903
    }
904
    else
905
    {
906
        message = "Failed to open " + String(path) + " for reading.";
907
        PRINTLN(message);
908
    }
909
    return(data);
910
}
911

    
912
/*
913
 * ========================================================================
914
 * Setup
915
 * ========================================================================
916
 */
917

    
918
/*
919
 * ========================================================================
920
 * Name:   setup
921
 * Prototype:  void setup( void )
922
 *
923
 * Description:
924
 * Initialization function; required by Arduino enviroment.
925
 * This function handles setup of WiFi connections and configures
926
 * URI handlers for the web server.
927
 * ========================================================================
928
 */
929
void setup(void)
930
{
931
    String message;
932

    
933
    /*
934
     * Serial port output seems to work better at 9600.
935
     */
936
    SERIAL_BEGIN(115200);
937
    PRINTLN("");
938
    PRINTLN("Waiting on SDK.");
939
#ifdef USE_SERIAL
940
#ifdef DEBUG_CORE
941
    Serial.setDebugOutput(true);
942
#endif
943
#endif
944
    delay(3500);
945

    
946
    /*
947
     * Initial setup for using WifiManager.
948
     */
949
    wifiManager.setDebugOutput(true);
950
    wifiManager.setConnectTimeout(30);
951

    
952
    /*
953
     * Setup SPIFFS for saving data.
954
     */
955
    if ( !SPIFFS.begin() )
956
    {
957
        PRINTLN("Can't mount SPIFFS.");
958
    }
959
    else
960
    {
961
        PRINTLN("Mounted SPIFFS.");
962
        flags |= FL_SPIFFS_OK;
963
    }
964

    
965
    /*
966
     * Set callback to be notified when connection to last configured
967
     * remote AP fails.
968
     */
969
    wifiManager.setAPCallback(configModeCallback);
970

    
971
#ifdef RESET
972
    /*
973
     * Use this to reset the board to it's default state.
974
     * Set RESET=1 on the GNU Make command line to use this.
975
     * Then after the board resets once, run the build without RESET=1
976
     * build/upload again.
977
     */
978
    PRINTLN("Resetting WiFiManager.");
979
    reset();
980
    PRINTLN("Done. Now reflash the board.");
981
    resetMode = true;
982
    return;
983

    
984
#endif // RESET
985

    
986
    /*
987
     * Method to use WITH Config mode button.
988
     */
989

    
990
    /* Set GPIO0 output low */
991
    PRINTLN("Set GPIO0 low.");
992
    pinMode(0, OUTPUT);
993
    digitalWrite(0, LOW);
994

    
995
    /*
996
     * Check GPIO2 input to see if push button is pressed (connecting it to GPIO0)
997
     * placing us in ConfigMode.
998
     */
999
    PRINTLN("Checking boot state.");
1000
    configMode = (digitalRead(2) == LOW);
1001
    if (configMode)
1002
    {
1003
        PRINTLN("In Config Mode.");
1004
        BP_CONFIG_MODE;
1005
        delay(1000);
1006
        SETBLUELED(true);
1007

    
1008
        /*
1009
         * Start AP and serve up web page configuration.
1010
         */
1011
        while ( 1 )
1012
        {
1013
            wifiManager.startConfigPortal((char *)apname.c_str());
1014
            if (WiFi.status() == WL_CONNECTED)
1015
            {
1016
                break;
1017
            }
1018
            message = "Failed connection to ";
1019
            message += WiFi.SSID();
1020
            message += "; trying again.";
1021
            PRINTLN(message);
1022
        }
1023
        BP_CONNECTED;
1024
        PRINTLN("");
1025
        PRINT("Connected to ");
1026
        PRINTLN(WiFi.SSID());
1027
        PRINT("IP address: ");
1028
        PRINTLN(WiFi.localIP());
1029

    
1030
        return;
1031
    }
1032
    else
1033
    {
1034
        /*
1035
         * In operational mode we just try to connect to the last configured AP.
1036
         * We don't use WiFiManager here because we don't want to drop back into the 
1037
         * auto-configure AP on the device.  If connection fails with saved credentials
1038
         * then we simply punt in this mode.
1039
         */
1040
        PRINTLN("In Operational Mode.");
1041
        WiFi.begin();
1042
        while ( 1 )
1043
        {
1044
            if (WiFi.status() == WL_CONNECTED)
1045
            {
1046
                break;
1047
            }
1048
            message = "Failed connection to ";
1049
            message += WiFi.SSID();
1050
            message += "; trying again.";
1051
            PRINTLN(message);
1052
            BP_CONNECT_MODE;
1053
            delay(1500);
1054
        }
1055
        BP_CONNECTED;
1056
        PRINTLN("imiot setup:");
1057
        PRINT("Connected to: "); PRINTLN(WiFi.SSID());
1058
        PRINT("IP address  : "); PRINTLN(WiFi.localIP());
1059
    }
1060

    
1061
    /* Reset GPIO0 HIGH to turn off the relay */
1062
    PRINTLN("Resetting GPIO0.");
1063
    digitalWrite(0, HIGH);
1064

    
1065
    /*
1066
     * Setup web server URI handlers: this is done in a function to make
1067
     * it easy to find where device specific code will go.
1068
     */
1069
    setupURIHandlers();
1070

    
1071
    /* Notify user of status, if they're watching the serial console. */
1072
    server.begin();
1073
    PRINTLN("HTTP server started");
1074

    
1075
    /* Set schedule for checking timed events. */
1076
    PRINTLN("Scheduling webLoop.");
1077
    serverID = t.every(25, webLoop);
1078

    
1079
    /* Load registration info, if any. */
1080
    if ( SPIFFS.exists(registration_path) )
1081
    {
1082
        // split into: uuid, key and monitorAddress
1083
        String registration = readFile(registration_path);
1084
        char *reg_copy = strdup(registration.c_str());
1085

    
1086
        char *ptr = strtok(reg_copy, ":");
1087
        uuid = String(strdup(ptr));
1088

    
1089
        ptr = strtok(NULL, ":");
1090
        key = String(strdup(ptr));
1091

    
1092
        ptr = strtok(NULL, ":");
1093
        monitorAddress = String(strdup(ptr));
1094

    
1095
        free(reg_copy);
1096
    }
1097
    else
1098
    {
1099
        PRINTLN("Device registration not found.");
1100
    }
1101
}
1102

    
1103
/*
1104
 * ========================================================================
1105
 * Name:   loop
1106
 * Prototype:  void loop( void )
1107
 *
1108
 * Description:
1109
 * Main loop function; required by Arduino enviroment.
1110
 * This function uses the Timer API to spin, caling handlers at specified
1111
 * intervals.
1112
 * ========================================================================
1113
 */
1114
void loop(void)
1115
{
1116
    /* Do nothing here if we're in RESET mode. */
1117
    if ( resetMode )
1118
    {
1119
        return;
1120
    }
1121

    
1122
    /*
1123
     * We don't do anything until user has reset from Config Mode.
1124
     * That requires a power cycle.
1125
     */
1126
    if ( configMode )
1127
    {
1128
        BP_CONNECTED;
1129
        delay(3000);
1130
        return;
1131
    }
1132

    
1133
    /*
1134
     * If we're not registered AND PAIR mode button is set, go into pairing mode.
1135
     */
1136
    if ( pairEnabled() )
1137
    {
1138
        /*
1139
         * We should notify existing monitor that we're going away here.
1140
         * TBD: requires new REST API.
1141
         */
1142

    
1143
        /* Assume we're no longer registered. */
1144
        flags &= !FL_REGISTERED;
1145
        SPIFFS.remove(registration_path);
1146

    
1147
        do
1148
        {
1149
            /*
1150
             * Disable notification that we're in operational mode.
1151
             */
1152
            if ( serverID != -1 )
1153
            {
1154
                t.stop(serverID);
1155
                serverID = -1;
1156
            }
1157
    
1158
            /* Schedule multicast query for every 3 seconds. */
1159
            if ( registerID == -1 )
1160
            {
1161
                PRINTLN("Attempting pair mode.");
1162
                PRINTLN("Scheduling deviceRegister.");
1163
                registerID = t.every(3000, deviceRegister);
1164
            }
1165
    
1166
            /* Show blink pattern in Pair Mode */
1167
            BP_PAIR1_MODE;
1168
            BP_PAIR2_MODE;
1169
            t.update();
1170
    
1171
        } while ( !(flags & FL_REGISTERED) && pairEnabled() );
1172
    }
1173
    else
1174
    {
1175
        /*
1176
         * Operational Mode.
1177
         */
1178

    
1179
        /*
1180
         * If we return from pair mode we may need to disable deviceRegister() calls.
1181
         */
1182
        if ( registerID != -1 )
1183
        {
1184
            t.stop(registerID);
1185
            registerID = -1;
1186
        }
1187

    
1188
        /*
1189
         * If not started, setup notification that we're in operational mode.
1190
         */
1191
        if ( serverID == -1 )
1192
        {
1193
            PRINTLN("Scheduling Serial Notification.");
1194
            serverID = t.every(3000, serialNotify);
1195
        }
1196

    
1197
        /* Check for timer events */
1198
        t.update();
1199
    }
1200
}