AlteriomPainlessMesh v2.1.1
Easy ESP32/ESP8266 mesh networking with Alteriom IoT extensions
Loading...
Searching...
No Matches
ota.hpp
Go to the documentation of this file.
1#ifndef _PAINLESS_MESH_PLUGIN_OTA_HPP_
2#define _PAINLESS_MESH_PLUGIN_OTA_HPP_
3
5
9
10#if !defined(USE_FS_SPIFFS) && !defined(USE_FS_LITTLEFS)
11#if defined(ESP32) || defined(ESP8266)
12#ifdef ESP32
13#define USE_FS_SPIFFS
14#else
15#define USE_FS_LITTLEFS
16#endif
17#endif
18#endif
19
20#ifdef ESP32
21#include <Update.h>
22#endif
23
24#if defined(USE_FS_SPIFFS)
25#include <SPIFFS.h>
26#elif defined(USE_FS_LITTLEFS)
27#include <LittleFS.h>
28#endif
29
30namespace painlessmesh {
31namespace plugin {
32
56namespace ota {
57
61enum class OTA_OP_CODES {
62 ANNOUNCE = 10, // Announce a new update
63 DATA_REQUEST = 11, // Request data from host
64 DATA = 12, // Inbound data to nodes
65};
66
76class Announce : public BroadcastPackage {
77 public:
80
92
97 bool forced = false;
98
105 bool broadcasted = false;
106
113 bool compressed = false;
114
115 size_t noPart = 0;
116
118
119 Announce(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
120 md5 = jsonObj["md5"].as<TSTRING>();
121 hardware = jsonObj["hardware"].as<TSTRING>();
122 role = jsonObj["role"].as<TSTRING>();
123#if ARDUINOJSON_VERSION_MAJOR < 7
124 if (jsonObj.containsKey("forced")) forced = jsonObj["forced"];
125 if (jsonObj.containsKey("broadcasted"))
126 broadcasted = jsonObj["broadcasted"];
127 if (jsonObj.containsKey("compressed"))
128 compressed = jsonObj["compressed"];
129#else
130 if (jsonObj["forced"].is<bool>()) forced = jsonObj["forced"];
131 if (jsonObj["broadcasted"].is<bool>()) broadcasted = jsonObj["broadcasted"];
132 if (jsonObj["compressed"].is<bool>()) compressed = jsonObj["compressed"];
133#endif
134 noPart = jsonObj["noPart"];
135 }
136
137 JsonObject addTo(JsonObject&& jsonObj) const {
138 jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
139 jsonObj["md5"] = md5;
140 jsonObj["hardware"] = hardware;
141 jsonObj["role"] = role;
142 if (forced) jsonObj["forced"] = forced;
143 jsonObj["noPart"] = noPart;
144 jsonObj["broadcasted"] = broadcasted;
145 if (compressed) jsonObj["compressed"] = compressed;
146 return jsonObj;
147 }
148
149#if ARDUINOJSON_VERSION_MAJOR < 7
150 size_t jsonObjectSize() const {
151 return JSON_OBJECT_SIZE(noJsonFields + 5) +
152 round(1.1 * (md5.length() + hardware.length() + role.length()));
153 }
154#endif
155
156 protected:
158 this->routing = routing;
159 }
160};
161
162class Data;
163
171class DataRequest : public Announce {
172 public:
173 size_t partNo = 0;
174 uint32_t dest = 0;
175
176 DataRequest() : Announce(11, router::SINGLE) {}
177
178 DataRequest(JsonObject jsonObj) : Announce(jsonObj) {
179 dest = jsonObj["dest"];
180 partNo = jsonObj["partNo"];
181 }
182
183 JsonObject addTo(JsonObject&& jsonObj) const {
184 jsonObj = Announce::addTo(std::move(jsonObj));
185 jsonObj["dest"] = dest;
186 jsonObj["partNo"] = partNo;
187 return jsonObj;
188 }
189
190 static DataRequest replyTo(const Announce& ann, uint32_t from,
191 size_t partNo) {
192 DataRequest req;
193 req.dest = ann.from;
194 req.md5 = ann.md5;
195 req.hardware = ann.hardware;
196 req.role = ann.role;
197 req.forced = ann.forced;
198 req.noPart = ann.noPart;
199 req.partNo = partNo;
200 req.from = from;
201 req.broadcasted = ann.broadcasted;
202 req.compressed = ann.compressed;
203 return req;
204 }
205
206 static DataRequest replyTo(const Data& d, size_t partNo);
207
208#if ARDUINOJSON_VERSION_MAJOR < 7
209 size_t jsonObjectSize() const {
210 return JSON_OBJECT_SIZE(noJsonFields + 5 + 2) +
211 round(1.1 * (md5.length() + hardware.length() + role.length()));
212 }
213#endif
214
215 protected:
216 DataRequest(int type) : Announce(type, router::SINGLE) {}
217};
218
223class Data : public DataRequest {
224 public:
226
227 Data() : DataRequest(12) {}
228
229 Data(JsonObject jsonObj) : DataRequest(jsonObj) {
230 data = jsonObj["data"].as<TSTRING>();
231 }
232
233 JsonObject addTo(JsonObject&& jsonObj) const {
234 jsonObj = DataRequest::addTo(std::move(jsonObj));
235 jsonObj["data"] = data;
236 return jsonObj;
237 }
238
239 static Data replyTo(const DataRequest& req, TSTRING data, size_t partNo) {
240 Data d;
241 d.from = req.dest;
242 d.dest = req.from;
243 d.md5 = req.md5;
244 d.hardware = req.hardware;
245 d.role = req.role;
246 d.forced = req.forced;
247 d.noPart = req.noPart;
248 d.partNo = partNo;
249 d.data = data;
250 d.broadcasted = req.broadcasted;
251 d.compressed = req.compressed;
252 // Phase 2: Set routing to BROADCAST for true broadcast mode
253 if (req.broadcasted) {
255 }
256 return d;
257 }
258
259#if ARDUINOJSON_VERSION_MAJOR < 7
260 size_t jsonObjectSize() const {
261 return JSON_OBJECT_SIZE(noJsonFields + 5 + 2 + 1) +
262 round(1.1 * (md5.length() + hardware.length() + role.length() +
263 data.length()));
264 }
265#endif
266};
267
268inline DataRequest DataRequest::replyTo(const Data& d, size_t partNo) {
269 DataRequest req;
270 req.from = d.dest;
271 req.dest = d.from;
272 req.md5 = d.md5;
273 req.hardware = d.hardware;
274 req.role = d.role;
275 req.forced = d.forced;
276 req.noPart = d.noPart;
277 req.partNo = partNo;
278 req.broadcasted = d.broadcasted;
279 req.compressed = d.compressed;
280 return req;
281}
282
291 public:
293#ifdef ESP32
294 TSTRING hardware = "ESP32";
295#else
296 TSTRING hardware = "ESP8266";
297#endif
299 size_t noPart = 0;
300 size_t partNo = 0;
301 bool broadcasted = false;
302 bool compressed = false;
303 TSTRING ota_fn = "/ota_fw.json";
304
305 State() {}
306
307 State(JsonObject jsonObj) {
308 md5 = jsonObj["md5"].as<TSTRING>();
309 hardware = jsonObj["hardware"].as<TSTRING>();
310 role = jsonObj["role"].as<TSTRING>();
311 broadcasted = jsonObj["broadcasted"].as<bool>();
312 compressed = jsonObj["compressed"].as<bool>();
313 }
314
315 State(const Announce& ann) {
316 md5 = ann.md5;
317 hardware = ann.hardware;
318 role = ann.role;
319 noPart = ann.noPart;
322 }
323
324 JsonObject addTo(JsonObject&& jsonObj) const {
325 jsonObj["role"] = role;
326 jsonObj["md5"] = md5;
327 jsonObj["hardware"] = hardware;
328 jsonObj["broadcasted"] = broadcasted;
329 jsonObj["compressed"] = compressed;
330 return jsonObj;
331 }
332
333#if ARDUINOJSON_VERSION_MAJOR < 7
334 size_t jsonObjectSize() const {
335 return JSON_OBJECT_SIZE(3) +
336 round(1.1 * (md5.length() + hardware.length() + role.length()));
337 }
338#endif
339
340 std::shared_ptr<Task> task;
341};
342
343typedef std::function<size_t(painlessmesh::plugin::ota::DataRequest,
344 char* buffer)>
346
347template <class T>
348void addSendPackageCallback(Scheduler& scheduler,
351 size_t otaPartSize) {
352 using namespace logger;
353#if defined(ESP32) || defined(ESP8266)
354 mesh.onPackage(
356 [&mesh, callback, otaPartSize](painlessmesh::protocol::Variant& variant) {
357 auto pkg = variant.to<painlessmesh::plugin::ota::DataRequest>();
358 char buffer[otaPartSize + 1];
359 memset(buffer, 0, otaPartSize + 1);
360 auto size = callback(pkg, buffer);
361 // Handle zero size
362 if (!size) {
363 // No data is available by the user app.
364
365 // todo - doubtful, shall we return true or false. What is the purpose
366 // of this return value.
367 return true;
368 }
369 // Encode data as base64 so there are no null characters and can be
370 // shown in plaintext
371 auto b64Data =
372 painlessmesh::base64::encode((unsigned char*)buffer, size);
373 auto reply =
374 painlessmesh::plugin::ota::Data::replyTo(pkg, b64Data, pkg.partNo);
375
376 // Phase 2: Routing is now handled in replyTo() based on broadcasted flag
377 mesh.sendPackage(&reply);
378 if (pkg.broadcasted) {
379 Log(DEBUG, "OTA: Broadcasting chunk %d/%d\n", pkg.partNo, pkg.noPart);
380 }
381 return true;
382 });
383
384#endif
385}
386
387template <class T>
389 Scheduler& scheduler, plugin::PackageHandler<T>& mesh, TSTRING role = "",
390 std::function<void(int, int)> progress_cb = NULL) {
391 using namespace logger;
392#if defined(ESP32) || defined(ESP8266)
393 auto currentFW = std::make_shared<State>();
394 currentFW->role = role;
395 auto updateFW = std::make_shared<State>();
396 updateFW->role = role;
397#ifdef USE_FS_SPIFFS
398 SPIFFS.begin(true); // Start the SPI Flash Files System
399 if (SPIFFS.exists(currentFW->ota_fn)) {
400 auto file = SPIFFS.open(currentFW->ota_fn, "r");
401#else
402 LittleFS.begin(); // Start the SPI Flash Files System
403 if (LittleFS.exists(currentFW->ota_fn)) {
404 auto file = LittleFS.open(currentFW->ota_fn, "r");
405#endif
406 TSTRING msg = "";
407 while (file.available()) {
408 msg += (char)file.read();
409 }
410 protocol::Variant var(msg);
411 auto fw = var.to<State>();
412 if (fw.role == role && fw.hardware == currentFW->hardware) {
413 Log(DEBUG, "MD5 found %s\n", fw.md5.c_str());
414 currentFW->md5 = fw.md5;
415 }
416 }
417
418 mesh.onPackage((int)OTA_OP_CODES::ANNOUNCE, [currentFW, updateFW, &mesh,
419 &scheduler](
420 protocol::Variant& variant) {
421 // convert variant to Announce
422 auto pkg = variant.to<Announce>();
423 // Check if we want the update
424 if (currentFW->role == pkg.role && currentFW->hardware == pkg.hardware) {
425 if ((currentFW->md5 == pkg.md5 && !pkg.forced) ||
426 updateFW->md5 == pkg.md5) {
427 // Either already have it, or already updating to it
428 return false;
429 } else {
430 updateFW->md5 = pkg.md5;
431 updateFW->broadcasted = pkg.broadcasted;
432 updateFW->compressed = pkg.compressed;
433 // If we are not in broadcasted mode, or we are the root node, begin
434 // requesting data
435 if (!pkg.broadcasted || mesh.isRoot()) {
436 auto request =
437 DataRequest::replyTo(pkg, mesh.getNodeId(), updateFW->partNo);
438 // enable the request task
439 updateFW->task =
440 mesh.addTask(scheduler, 30 * TASK_SECOND, 10,
441 [request, &mesh]() { mesh.sendPackage(&request); });
442 updateFW->task->setOnDisable([updateFW]() {
443 Log(ERROR, "OTA: Did not receive the requested data.\n");
444 updateFW->md5 = "";
445 });
446 }
447 }
448 }
449 return false;
450 });
451
452 // mesh.onPackage(11, [currentFW](protocol::Variant &variant) {
453 // Log(ERROR, "Data request should not be send to this node\n");
454 // return false;
455 // });
456
457 mesh.onPackage((int)OTA_OP_CODES::DATA, [currentFW, updateFW, progress_cb,
458 &mesh, &scheduler](
459 protocol::Variant& variant) {
460 auto pkg = variant.to<Data>();
461 // Check whether it is a new part, of correct md5 role etc etc
462 if (updateFW->md5 == pkg.md5 && updateFW->role == pkg.role &&
463 updateFW->hardware == pkg.hardware) {
464 // Have we have received the next chunk of firmware in sequence?
465 if (updateFW->partNo == pkg.partNo) {
466 if (progress_cb != NULL) {
467 progress_cb(pkg.partNo, pkg.noPart);
468 }
469 if (pkg.partNo == 0) {
470#ifdef ESP32
471 uint32_t maxSketchSpace = UPDATE_SIZE_UNKNOWN;
472#else
473 uint32_t maxSketchSpace =
474 (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
475#endif
476 Log(DEBUG, "Sketch size %d\n", maxSketchSpace);
477 if (Update.isRunning()) {
478 Update.end(false);
479 }
480 if (!Update.begin(maxSketchSpace)) { // start with max available size
481 Log(DEBUG, "handleOTA(): OTA start failed!");
482 Update.printError(Serial);
483 Update.end();
484 } else {
485 Update.setMD5(pkg.md5.c_str());
486 }
487 }
488
489 // write data
490 auto b64Data = base64::decode(pkg.data);
491 if (Update.write((uint8_t*)b64Data.c_str(), b64Data.length()) !=
492 b64Data.length()) {
493 Log(ERROR, "handleOTA(): OTA write failed!");
494 Update.printError(Serial);
495 Update.end();
496 updateFW->md5 = "";
497 updateFW->partNo = 0;
498 return false;
499 }
500
501 // If last part then write ota_fn and reboot
502 if (pkg.partNo == pkg.noPart - 1) {
503 // check md5, reboot
504 if (Update.end(true)) { // true to set the size to the
505 // current progress
506#ifdef USE_FS_SPIFFS
507 auto file = SPIFFS.open(updateFW->ota_fn, "w");
508 if (!file) {
509 Log(ERROR,
510 "handleOTA(): Unable to write md5 of new update to the "
511 "SPIFFS "
512 "file. This will result in endless update loops for OTA\n");
513 }
514#else
515 auto file = LittleFS.open(updateFW->ota_fn, "w");
516 if (!file) {
517 Log(ERROR, "handleOTA(): Unable to write md5 of new binary to the LittleFS file. This will result in endless update loops for OTA\n");
518 }
519#endif
520
521 String msg;
522 protocol::Variant var(updateFW.get());
523 var.printTo(msg);
524 file.print(msg);
525 file.close();
526
527 Log(DEBUG, "handleOTA(): OTA Success! %s, %s\n", msg.c_str(),
528 updateFW->role.c_str());
529 // Delay restart by 2 seconds to allow mesh activity to finish
530 mesh.addTask(scheduler, 2 * TASK_SECOND, TASK_ONCE,
531 []() { ESP.restart(); })
532 ->enableDelayed();
533 } else {
534 Log(DEBUG, "handleOTA(): OTA failed!\n");
535 Update.printError(Serial);
536 updateFW->md5 = "";
537 updateFW->partNo = 0;
538 }
539 if (updateFW->task != NULL) {
540 updateFW->task->setOnDisable(NULL);
541 updateFW->task->disable();
542 }
543 } else {
544 // else request more
545 ++updateFW->partNo;
546 if (updateFW->task != NULL) {
547 auto request = DataRequest::replyTo(pkg, updateFW->partNo);
548 updateFW->task->setCallback(
549 [request, &mesh]() { mesh.sendPackage(&request); });
550 // updateFW->task->disable();
551 updateFW->task->restart();
552 }
553 }
554 } else {
555 // We are out-of-sequence, resume with non-broadcasted update
556 if (updateFW->broadcasted && pkg.broadcasted) {
557 Log(DEBUG, "Out of sequence packet! We may have missed a packet?");
558 // Drop of out broadcasted mode
559 updateFW->broadcasted = false;
560 auto request = DataRequest::replyTo(pkg, updateFW->partNo);
561 request.broadcasted = false;
562 updateFW->task =
563 mesh.addTask(scheduler, 30 * TASK_SECOND, 10,
564 [request, &mesh]() { mesh.sendPackage(&request); });
565 updateFW->task->setOnDisable([updateFW]() {
566 Log(ERROR, "OTA: Did not receive the requested data.\n");
567 updateFW->md5 = "";
568 });
569 }
570 }
571 }
572 return false;
573 });
574
575#endif
576}
577
578} // namespace ota
579} // namespace plugin
580} // namespace painlessmesh
581
582#endif
uint32_t getNodeId() const
Definition layout.hpp:108
bool isRoot() const
Definition layout.hpp:113
JsonObject addTo(JsonObject &&jsonObj) const
Definition plugin.hpp:84
int noJsonFields
Definition plugin.hpp:74
router::Type routing
Definition plugin.hpp:72
int type
Definition plugin.hpp:73
uint32_t from
Definition plugin.hpp:71
Definition plugin.hpp:178
bool sendPackage(const protocol::PackageInterface *pkg)
Definition plugin.hpp:223
std::shared_ptr< Task > addTask(Scheduler &scheduler, unsigned long aInterval, long aIterations, std::function< void()> aCallback)
Definition plugin.hpp:255
void onPackage(int type, std::function< bool(protocol::Variant &)> function)
Definition plugin.hpp:241
Announce(int type, router::Type routing)
Definition ota.hpp:157
size_t jsonObjectSize() const
Definition ota.hpp:150
JsonObject addTo(JsonObject &&jsonObj) const
Definition ota.hpp:137
TSTRING md5
Definition ota.hpp:78
Announce(JsonObject jsonObj)
Definition ota.hpp:119
size_t noPart
Definition ota.hpp:115
bool compressed
Definition ota.hpp:113
Announce()
Definition ota.hpp:117
bool forced
Definition ota.hpp:97
TSTRING role
The type of node the firmware is meant for.
Definition ota.hpp:91
TSTRING hardware
Definition ota.hpp:79
bool broadcasted
Definition ota.hpp:105
DataRequest(int type)
Definition ota.hpp:216
uint32_t dest
Definition ota.hpp:174
size_t jsonObjectSize() const
Definition ota.hpp:209
static DataRequest replyTo(const Announce &ann, uint32_t from, size_t partNo)
Definition ota.hpp:190
DataRequest()
Definition ota.hpp:176
DataRequest(JsonObject jsonObj)
Definition ota.hpp:178
size_t partNo
Definition ota.hpp:173
JsonObject addTo(JsonObject &&jsonObj) const
Definition ota.hpp:183
Definition ota.hpp:223
Data()
Definition ota.hpp:227
Data(JsonObject jsonObj)
Definition ota.hpp:229
static Data replyTo(const DataRequest &req, TSTRING data, size_t partNo)
Definition ota.hpp:239
size_t jsonObjectSize() const
Definition ota.hpp:260
TSTRING data
Definition ota.hpp:225
JsonObject addTo(JsonObject &&jsonObj) const
Definition ota.hpp:233
Definition ota.hpp:290
State()
Definition ota.hpp:305
State(JsonObject jsonObj)
Definition ota.hpp:307
TSTRING role
Definition ota.hpp:298
size_t noPart
Definition ota.hpp:299
bool broadcasted
Definition ota.hpp:301
TSTRING ota_fn
Definition ota.hpp:303
bool compressed
Definition ota.hpp:302
TSTRING hardware
Definition ota.hpp:296
TSTRING md5
Definition ota.hpp:292
size_t jsonObjectSize() const
Definition ota.hpp:334
std::shared_ptr< Task > task
Definition ota.hpp:340
size_t partNo
Definition ota.hpp:300
JsonObject addTo(JsonObject &&jsonObj) const
Definition ota.hpp:324
State(const Announce &ann)
Definition ota.hpp:315
Definition protocol.hpp:481
T to()
Definition protocol.hpp:743
String TSTRING
Definition configuration.hpp:132
TSTRING encode(unsigned char const *bytes_to_encode, unsigned int in_len)
Definition base64.hpp:20
const TSTRING decode(const void *data, const size_t &len)
Definition base64.hpp:72
OTA_OP_CODES
Definition ota.hpp:61
void addSendPackageCallback(Scheduler &scheduler, plugin::PackageHandler< T > &mesh, otaDataPacketCallbackType_t callback, size_t otaPartSize)
Definition ota.hpp:348
void addReceivePackageCallback(Scheduler &scheduler, plugin::PackageHandler< T > &mesh, TSTRING role="", std::function< void(int, int)> progress_cb=NULL)
Definition ota.hpp:388
std::function< size_t(painlessmesh::plugin::ota::DataRequest, char *buffer)> otaDataPacketCallbackType_t
Definition ota.hpp:345
Type
Definition protocol.hpp:30
@ BROADCAST
Definition protocol.hpp:30
Definition wifi.hpp:34
painlessmesh::logger::LogClass Log
Definition wifi.cpp:2