diff --git a/SUMMARY.md b/SUMMARY.md index 9a09d6f..7eb7c6e 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -26,6 +26,7 @@ * [ssl/MbedTLSClient](ltapi/class_mbed_t_l_s_client.md) * [IPv6Address](ltapi/classarduino_1_1_i_pv6_address.md) * [LwIPRxBuffer](ltapi/class_lw_i_p_rx_buffer.md) + * [Update](ltapi/class_update.md) * [WebServer](ltapi/class_web_server.md) * [WiFiMulti](ltapi/class_wi_fi_multi.md) * [Third party libraries](docs/libs-3rd-party.md) diff --git a/arduino/libretuya/libraries/Update/Update.cpp b/arduino/libretuya/libraries/Update/Update.cpp new file mode 100644 index 0000000..e53e434 --- /dev/null +++ b/arduino/libretuya/libraries/Update/Update.cpp @@ -0,0 +1,199 @@ +/* Copyright (c) Kuba SzczodrzyƄski 2022-05-29. */ + +#include "Update.h" + +UpdateClass::UpdateClass() : ctx(NULL), info(NULL), buf(NULL) { + cleanup(); +} + +/** + * @brief Initialize the update process. + * + * @param size total UF2 file size + * @param command must be U_FLASH + * @return false if parameters are invalid or update is running, true otherwise + */ +bool UpdateClass::begin(size_t size, int command, int unused2, uint8_t unused3, const char *unused4) { + if (ctx) + return false; + cleanup(); + + ctx = uf2_ctx_init(LT.otaGetTarget(), FAMILY); + info = uf2_info_init(); + + if (!size) + return errorArd(UPDATE_ERROR_SIZE); + + if (command != U_FLASH) + return errorArd(UPDATE_ERROR_BAD_ARGUMENT); + + bytesTotal = size; + return true; +} + +/** + * @brief Finalize the update process. Check for errors and update completion, then activate the new firmware image. + * + * @param evenIfRemaining no idea + * @return false in case of errors or no update running, true otherwise + */ +bool UpdateClass::end(bool evenIfRemaining) { + if (hasError() || !ctx) + // false if not running + return false; + + if (!isFinished() && !evenIfRemaining) { + // abort if not finished + return errorArd(UPDATE_ERROR_ABORT); + } + // TODO what is evenIfRemaining for? + if (!LT.otaSwitch()) + // try to activate the second OTA + return errorArd(UPDATE_ERROR_ACTIVATE); + + cleanup(); + return true; +} + +/** + * @brief Write a chunk of data to the buffer or flash memory. + * + * It's advised to write in 512-byte chunks (or its multiples). + * + * @param data + * @param len + * @return size_t + */ +size_t UpdateClass::write(uint8_t *data, size_t len) { + size_t written = 0; + if (hasError() || !ctx) + // 0 if not running + return 0; + + /* while (buf == bufPos && len >= UF2_BLOCK_SIZE) { + // buffer empty and entire block is in data + if (!tryWriteData(data, UF2_BLOCK_SIZE)) { + // returns 0 if data contains an invalid block + return written; + } + data += UF2_BLOCK_SIZE; + len -= UF2_BLOCK_SIZE; + written += UF2_BLOCK_SIZE; + } */ + + // write until buffer space is available + uint16_t toWrite; + while (len && (toWrite = min(len, bufLeft()))) { + tryWriteData(data, toWrite); + if (hasError()) + // return on errors + return written; + data += toWrite; + len -= toWrite; + written += toWrite; + } + return written; +} + +size_t UpdateClass::writeStream(Stream &data) { + size_t written = 0; + if (hasError() || !ctx) + // 0 if not running + return 0; + + uint32_t lastData = millis(); + // loop until the update is complete + while (remaining()) { + // check stream availability + int available = data.available(); + if (available <= 0) { + if (millis() - lastData > UPDATE_TIMEOUT_MS) { + // waited for data too long; abort with error + errorArd(UPDATE_ERROR_STREAM); + return written; + } + continue; + } + // available > 0 + lastData = millis(); + + // read data to fit in the remaining buffer space + bufAlloc(); + uint16_t read = data.readBytes(bufPos, bufLeft()); + bufPos += read; + written += read; + tryWriteData(); + if (hasError()) + // return on errors + return written; + } +} + +/** + * @brief Try to use the buffer as a block to write. In case of UF2 errors, + * error codes are set, the update is aborted and 0 is returned + * + * @param data received data to copy to buffer or NULL if already in buffer + * @param len received data length - must be at most bufLeft() + * @return size_t "used" data size - 0 or 512 + */ +size_t UpdateClass::tryWriteData(uint8_t *data, size_t len) { + uf2_block_t *block = NULL; + + if (len == UF2_BLOCK_SIZE) { + // data has a complete block + block = (uf2_block_t *)data; + } else if (data && len) { + // data has a part of a block, copy it to buffer + bufAlloc(); + memcpy(bufPos, data, len); + bufPos += len; + } + + if (!block && bufSize() == UF2_BLOCK_SIZE) { + // use buffer as block (only if not found above) + block = (uf2_block_t *)buf; + } + + // a complete block has been found + if (block) { + if (errorUf2(uf2_check_block(ctx, block))) + // block is invalid + return 0; + + if (errUf2 == UF2_ERR_IGNORE) + // treat ignored blocks as valid + return UF2_BLOCK_SIZE; + + if (!bytesWritten) { + // parse header block to allow retrieving firmware info + if (errorUf2(uf2_parse_header(ctx, block, info))) + // header is invalid + return 0; + + if (bytesTotal == UPDATE_SIZE_UNKNOWN) { + // set total update size from block count info + bytesTotal = block->block_count * UF2_BLOCK_SIZE; + } else if (bytesTotal != block->block_count * UF2_BLOCK_SIZE) { + // given update size does not match the block count + return errorArd(UPDATE_ERROR_SIZE); + } + } else { + // write data blocks normally + if (errorUf2(uf2_write(ctx, block))) + // block writing failed + return 0; + } + + // increment total writing progress + bytesWritten += UF2_BLOCK_SIZE; + // call progress callback + if (callback) + callback(bytesWritten, bytesTotal); + return UF2_BLOCK_SIZE; + } + + return 0; +} + +UpdateClass Update; diff --git a/arduino/libretuya/libraries/Update/Update.h b/arduino/libretuya/libraries/Update/Update.h new file mode 100644 index 0000000..dbc6ada --- /dev/null +++ b/arduino/libretuya/libraries/Update/Update.h @@ -0,0 +1,150 @@ +#pragma once + +#include +#include + +#include "uf2ota/uf2ota.h" + +// No Error +#define UPDATE_ERROR_OK (0) +// Flash Write Failed +#define UPDATE_ERROR_WRITE (1) +// Flash Erase Failed +#define UPDATE_ERROR_ERASE (2) +// Flash Read Failed +#define UPDATE_ERROR_READ (3) +// Not Enough Space +#define UPDATE_ERROR_SPACE (4) +// Bad Size Given +#define UPDATE_ERROR_SIZE (5) +// Stream Read Timeout +#define UPDATE_ERROR_STREAM (6) +// MD5 Check Failed +#define UPDATE_ERROR_MD5 (7) +// Wrong Magic Byte +#define UPDATE_ERROR_MAGIC_BYTE (8) +// Could Not Activate The Firmware +#define UPDATE_ERROR_ACTIVATE (9) +// Partition Could Not be Found +#define UPDATE_ERROR_NO_PARTITION (10) +// Bad Argument +#define UPDATE_ERROR_BAD_ARGUMENT (11) +// Aborted +#define UPDATE_ERROR_ABORT (12) + +#define UPDATE_SIZE_UNKNOWN 0xFFFFFFFF + +#define U_FLASH 0 +#define U_SPIFFS 100 +#define U_AUTH 200 + +#define ENCRYPTED_BLOCK_SIZE 16 + +#define UPDATE_TIMEOUT_MS 30 * 1000 + +class UpdateClass { + public: + typedef std::function THandlerFunction_Progress; + + public: /* Update.cpp */ + UpdateClass(); + bool begin( + size_t size = UPDATE_SIZE_UNKNOWN, + int command = U_FLASH, + int unused2 = -1, + uint8_t unused3 = LOW, + const char *unused4 = NULL // this is for SPIFFS + ); + bool end(bool evenIfRemaining = false); + size_t write(uint8_t *data, size_t len); + size_t writeStream(Stream &data); + bool canRollBack(); + bool rollBack(); + // bool setMD5(const char *expected_md5); + + private: /* Update.cpp */ + size_t tryWriteData(uint8_t *data = NULL, size_t len = 0); + + public: /* UpdateUtil.cpp */ + UpdateClass &onProgress(THandlerFunction_Progress callback); + void abort(); + void printError(Print &out); + const char *errorString(); + const char *getFirmwareName(); + const char *getFirmwareVersion(); + const char *getLibreTuyaVersion(); + const char *getBoardName(); + + private: /* UpdateUtil.cpp */ + void cleanup(); + bool errorUf2(uf2_err_t err); + bool errorArd(uint8_t err); + void bufAlloc(); + uint16_t bufLeft(); + uint16_t bufSize(); + + private: + // uf2ota context + uf2_ota_t *ctx; + uf2_info_t *info; + // block buffer + uint8_t *buf; + uint8_t *bufPos; + // update progress - multiplies of 512 bytes + uint32_t bytesWritten; + uint32_t bytesTotal; + // errors + uf2_err_t errUf2; + uint8_t errArd; + // progress callback + THandlerFunction_Progress callback; + // String _target_md5; + // MD5Builder _md5; + + public: + String md5String(void) { + // return _md5.toString(); + } + + void md5(uint8_t *result) { + // return _md5.getBytes(result); + } + + uint8_t getError() { + return errArd; + } + + uf2_err_t getUF2Error() { + return errUf2; + } + + void clearError() { + errorUf2(UF2_ERR_OK); + } + + bool hasError() { + return errArd != UPDATE_ERROR_OK; + } + + bool isRunning() { + return ctx != NULL; + } + + bool isFinished() { + return bytesWritten == bytesTotal; + } + + size_t size() { + return bytesTotal; + } + + size_t progress() { + return bytesWritten; + } + + size_t remaining() { + return bytesTotal - bytesWritten; + } +}; + +extern UpdateClass Update; diff --git a/arduino/libretuya/libraries/Update/UpdateUtil.cpp b/arduino/libretuya/libraries/Update/UpdateUtil.cpp new file mode 100644 index 0000000..e5f1c0e --- /dev/null +++ b/arduino/libretuya/libraries/Update/UpdateUtil.cpp @@ -0,0 +1,162 @@ +/* Copyright (c) Kuba SzczodrzyƄski 2022-05-30. */ + +#include "Update.h" + +static const uint8_t errorMap[] = { + UPDATE_ERROR_OK, /* UF2_ERR_OK - no error */ + UPDATE_ERROR_OK, /* UF2_ERR_IGNORE - block should be ignored */ + UPDATE_ERROR_MAGIC_BYTE, /* UF2_ERR_MAGIC - wrong magic numbers */ + UPDATE_ERROR_BAD_ARGUMENT, /* UF2_ERR_FAMILY - family ID mismatched */ + UPDATE_ERROR_BAD_ARGUMENT, /* UF2_ERR_NOT_HEADER - block is not a header */ + UPDATE_ERROR_BAD_ARGUMENT, /* UF2_ERR_OTA_VER - unknown/invalid OTA format version */ + UPDATE_ERROR_MAGIC_BYTE, /* UF2_ERR_OTA_WRONG - no data for current OTA index */ + UPDATE_ERROR_NO_PARTITION, /* UF2_ERR_PART_404 - no partition with that name */ + UPDATE_ERROR_BAD_ARGUMENT, /* UF2_ERR_PART_ONE - only one partition tag in a block */ + UPDATE_ERROR_BAD_ARGUMENT, /* UF2_ERR_PART_UNSET - attempted to write without target partition */ + UPDATE_ERROR_BAD_ARGUMENT, /* UF2_ERR_DATA_TOO_LONG - data too long - tags won't fit */ + UPDATE_ERROR_BAD_ARGUMENT, /* UF2_ERR_SEQ_MISMATCH - sequence number mismatched */ + UPDATE_ERROR_ERASE, /* UF2_ERR_ERASE_FAILED - erasing flash failed */ + UPDATE_ERROR_WRITE, /* UF2_ERR_WRITE_FAILED - writing to flash failed */ + UPDATE_ERROR_WRITE /* UF2_ERR_WRITE_LENGTH - wrote fewer data than requested */ +}; + +static char errorStr[14]; + +/** + * @brief Set the callback invoked after writing data to flash. + */ +UpdateClass &UpdateClass::onProgress(THandlerFunction_Progress callback) { + this->callback = callback; + return *this; +} + +void UpdateClass::cleanup() { + free(ctx); // NULL in constructor + ctx = NULL; + uf2_info_free(info); // NULL in constructor + info = NULL; + free(buf); // NULL in constructor + buf = bufPos = NULL; + + bytesWritten = 0; + bytesTotal = 0; + errUf2 = UF2_ERR_OK; + errArd = UPDATE_ERROR_OK; +} + +/** + * @brief Check for UF2 errors. Set errArd and errUf2 in case of errors. + * Ignored blocks are not reported as errors. + * Abort the update. + * Use like: "if (errorUf2(...)) return false;" + * @return true if err is not OK, false otherwise + */ +bool UpdateClass::errorUf2(uf2_err_t err) { + if (err <= UF2_ERR_IGNORE) + return false; + cleanup(); + errUf2 = err; + errArd = errorMap[err]; + return true; +} + +/** + * @brief Set errUf2 and errArd according to given Arduino error code. + * Abort the update. + * Use like: "return errorArd(...);" + * @return false - always + */ +bool UpdateClass::errorArd(uint8_t err) { + cleanup(); + errUf2 = UF2_ERR_OK; + errArd = err; + return false; +} + +/** + * @brief Abort the update with UPDATE_ERROR_ABORT reason. + */ +void UpdateClass::abort() { + errorArd(UPDATE_ERROR_ABORT); +} + +void UpdateClass::bufAlloc() { + if (!buf) + buf = bufPos = (uint8_t *)malloc(UF2_BLOCK_SIZE); +} + +uint16_t UpdateClass::bufLeft() { + return buf + UF2_BLOCK_SIZE - bufPos; +} + +uint16_t UpdateClass::bufSize() { + return bufPos - buf; +} + +/** + * @brief Print string error info to the stream. + */ +void UpdateClass::printError(Print &out) { + out.println(errorString()); +} + +/** + * @brief Get string representation of the error in format + * "ard=..,uf2=..". Returns "" if no error. + */ +const char *UpdateClass::errorString() { + if (!errArd) + return ""; + sprintf(errorStr, "ard=%u,uf2=%u", errArd, errUf2); + return errorStr; +} + +/** + * @brief Get firmware name from UF2 info. + */ +const char *UpdateClass::getFirmwareName() { + if (info) + return info->fw_name; + return NULL; +} + +/** + * @brief Get firmware version from UF2 info. + */ +const char *UpdateClass::getFirmwareVersion() { + if (info) + return info->fw_version; + return NULL; +} + +/** + * @brief Get LibreTuya version from UF2 info. + */ +const char *UpdateClass::getLibreTuyaVersion() { + if (info) + return info->lt_version; + return NULL; +} + +/** + * @brief Get target board name from UF2 info. + */ +const char *UpdateClass::getBoardName() { + if (info) + return info->board; + return NULL; +} + +/** + * @brief See LT.otaCanRollback() for more info. + */ +bool UpdateClass::canRollBack() { + return LT.otaCanRollback(); +} + +/** + * @brief See LT.otaRollback() for more info. + */ +bool UpdateClass::rollBack() { + return LT.otaRollback(); +}