1/*
2    Small example sketch demonstrating how to perform OTA via HTTP/S
3    utilizing a MKRGSM 1400 and the storage on the integrated
4    SARA U-201 GSM module.
5
6    Please, be careful because no verification is done on the
7    received OTA file, apart size verification of the transmitted
8    bytes using the HTTP Content-Length header.
9
10    For production-grade OTA procedure you might want to implement
11    a content verification procedure using a CRC calculation
12    or an hash (eg. MD5 or SHA256) comparison.
13
14    Circuit:
15    * MKR GSM 1400 board
16    * Antenna
17    * SIM card with a data plan
18
19    Steps to update a sketch:
20
21    1) Create a new sketch or update an existing one to be updated over-the-air.
22       The sketch needs to contain also the code below for future OTAs.
23       The sketch must include the SSU library via
24       #include <SSU.h>
25
26    2) In the IDE select: Sketch -> Export compiled Binary.
27
28    3) Open the location of the sketch (Sketch -> Show Sketch Folder) and compress it
29       with a lzss tool
30       (eg. https://github.com/arduino-libraries/ArduinoIoTCloud/blob/master/extras/tools/lzss.py).
31
32    4) Upload the .lzss file to your HTTP/S server.
33
34    5) Upload this sketch after configuring the server, port and filename variables.
35
36    The sketch will download the OTA file, store it into the U-201 storage, and
37    will reset the board to trigger the SSU update procedure.
38
39
40    created 25 June 2020
41    by Giampaolo Mancini
42*/
43
44#include <MKRGSM.h>
45
46// This includes triggers the firmware update procedure
47// in the bootloader after reset.
48#include <SSU.h>
49
50// Do not change! SSU will look for these files!
51constexpr char UPDATE_FILE_NAME[] = "UPDATE.BIN.LZSS";
52static constexpr char CHECK_FILE_NAME[] = "UPDATE.OK";
53
54#include "arduino_secrets.h"
55const char PINNUMBER[] = SECRET_PINNUMBER;
56// APN data
57const char GPRS_APN[] = SECRET_GPRS_APN;
58const char GPRS_LOGIN[] = SECRET_GPRS_LOGIN;
59const char GPRS_PASSWORD[] = SECRET_GPRS_PASSWORD;
60
61// Change to GSMClient for non-SSL/TLS connection.
62// Not recommended.
63GSMSSLClient client;
64GPRS gprs;
65GSM gsmAccess;
66
67GSMFileUtils fileUtils;
68
69bool isHeaderComplete = false;
70String httpHeader;
71
72bool isDownloadComplete = false;
73unsigned int fileSize = 0;
74unsigned int totalWritten = 0;
75
76constexpr char server[] = "example.org";
77constexpr int port = 443;
78
79// Name of the new firmware file to be updated.
80constexpr char filename[] = "update.lzss";
81
82
83void setup()
84{
85    unsigned long timeout = millis();
86
87    Serial.begin(9600);
88    while (!Serial && millis() - timeout < 5000)
89        ;
90
91    Serial.println("Starting OTA Update via HTTP and Arduino SSU.");
92    Serial.println();
93
94    bool connected = false;
95
96    Serial.print("Connecting to cellular network... ");
97    while (!connected) {
98        if ((gsmAccess.begin(PINNUMBER) == GSM_READY) && (gprs.attachGPRS(GPRS_APN, GPRS_LOGIN, GPRS_PASSWORD) == GPRS_READY)) {
99            connected = true;
100        } else {
101            Serial.println("Not connected");
102            delay(1000);
103        }
104    }
105
106    Serial.println("Connected.");
107    Serial.println();
108
109    // Modem has already been initialized in the sketch:
110    // begin FileUtils without MODEM initialization.
111    fileUtils.begin(false);
112
113    Serial.print("Connecting to ");
114    Serial.print(server);
115    Serial.print(":");
116    Serial.print(port);
117    Serial.print("... ");
118    if (client.connect(server, port)) {
119        Serial.println("Connected.");
120        Serial.print("Downloading ");
121        Serial.println(filename);
122        Serial.print("... ");
123        // Make the HTTP request:
124        client.print("GET /");
125        client.print(filename);
126        client.println(" HTTP/1.1");
127        client.print("Host: ");
128        client.println(server);
129        client.println("Connection: close");
130        client.println();
131    } else {
132        Serial.println("Connection failed");
133    }
134}
135
136void loop()
137{
138    while (client.available()) {
139        // Skip the HTTP header
140        if (!isHeaderComplete) {
141            const char c = client.read();
142            httpHeader += c;
143            if (httpHeader.endsWith("\r\n\r\n")) {
144                isHeaderComplete = true;
145
146                // Get the size of the OTA file from the
147                // HTTP Content-Length header.
148                fileSize = getContentLength();
149
150                Serial.println();
151                Serial.print("HTTP header complete. ");
152                Serial.print("OTA file size is ");
153                Serial.print(fileSize);
154                Serial.println(" bytes.");
155                if (fileSize == 0) {
156                    Serial.println("Unable to get OTA file size.");
157                    while (true)
158                        ;
159                }
160            }
161        } else {
162            // Read the OTA file in len-bytes blocks to preserve RAM.
163            constexpr size_t len { 512 };
164            char buf[len] { 0 };
165
166            // Read len bytes from HTTP client...
167            uint32_t read = client.readBytes(buf, len);
168            // and append them to the update file.
169            uint32_t written = fileUtils.appendFile(UPDATE_FILE_NAME, buf, read);
170
171            if (written != read) {
172                Serial.println("Error while saving data.");
173                while (true)
174                    ;
175            }
176
177            // Update the received byte counter
178            totalWritten += written;
179
180            // Check for full file received and stored
181            isDownloadComplete = totalWritten == fileSize;
182
183            Serial.print("Received: ");
184            Serial.print(totalWritten);
185            Serial.print("/");
186            Serial.println(fileSize);
187        }
188    }
189    if (isDownloadComplete) {
190        Serial.println();
191        Serial.println("Download complete.");
192        Serial.println("Enabling checkpoint.");
193        Serial.println();
194
195        // Create the checkpoint file: will be removed by SSU
196        // after successful update.
197        auto status = fileUtils.downloadFile(CHECK_FILE_NAME, { 0 }, 1);
198        if (status != 1) {
199            Serial.println("Unable to create checkpoint file.");
200            while (true)
201                ;
202        }
203
204        Serial.println("Resetting MCU in order to trigger SSU...");
205        Serial.println();
206        delay(500);
207        NVIC_SystemReset();
208    }
209}
210
211int getContentLength()
212{
213    const String contentLengthHeader = "Content-Length:";
214    const auto contentLengthHeaderLen = contentLengthHeader.length();
215
216    auto indexContentLengthStart = httpHeader.indexOf(contentLengthHeader);
217    if (indexContentLengthStart < 0) {
218        Serial.println("Unable to find Content-Length header (Start)");
219        return 0;
220    }
221    auto indexContentLengthStop = httpHeader.indexOf("\r\n", indexContentLengthStart);
222    if (indexContentLengthStart < 0) {
223        Serial.println("Unable to find Content-Length header (Stop)");
224        return 0;
225    }
226    auto contentLength = httpHeader.substring(indexContentLengthStart + contentLengthHeaderLen + 1, indexContentLengthStop);
227
228    contentLength.trim();
229    return contentLength.toInt();
230}
231