c18d50cb91
Background The current implementation of Update() uses the spi_flash_* api to write and read from flash. These functions ignore the partition->encrypted flag and always write raw data to flash even if the partition is marked as encrypted. Changes in this PR Update() now uses the esp_partition_* api. Wrapper functions for esp_partition_* added to ESP.cpp. This was done to maintain a consistent approach to the way the spi_flash_* functions were used. I note though that not all of the esp-idf functions are used are wrapped, for example esp_ota_get_next_update_partition() so it may be that these should not be added? The current implementation of Update() changes the first (magic) byte of firmware to 0xFF on write, and then when the firmware is completely written changes it back to ESP_IMAGE_HEADER_MAGIC. This works without erasing the sector because flash bits can be changed from 1->0 (but not 0->1). If the flash is encrypted then the actual data written to flash will not be all ones, so this approach will not work. In addition, encrypted flash must be written in 16 byte blocks. So, instead of changing the first byte the changed code stashes the first 16 bytes, and starts writing at the 17th byte, leaving the first 16 bytes as 0xFF. Then, in _enablePartition() the stashed bytes can be successfully written. Benefits Whilst it's not possible to use encrypted flash directly from either the Arduino IDE or PIO it's reasonably straightforward to compile and flash a bootloader with the necessary support from a simple esp-idf project and then use ArduinoOTA for subsequent updates. This PR enables the use of this workflow until such time as encrypted flash is supported, and is a first (small) step toward adding support. Regardless of the above, the esp_partition_* api is recommended over the api_flash_* api. Application code should mostly use these esp_partition_* API functions instead of lower level spi_flash_* API functions. Partition table API functions do bounds checking and calculate correct offsets in flash, based on data stored in a partition table.
374 lines
9.4 KiB
C++
374 lines
9.4 KiB
C++
#include "Update.h"
|
|
#include "Arduino.h"
|
|
#include "esp_spi_flash.h"
|
|
#include "esp_ota_ops.h"
|
|
#include "esp_image_format.h"
|
|
|
|
static const char * _err2str(uint8_t _error){
|
|
if(_error == UPDATE_ERROR_OK){
|
|
return ("No Error");
|
|
} else if(_error == UPDATE_ERROR_WRITE){
|
|
return ("Flash Write Failed");
|
|
} else if(_error == UPDATE_ERROR_ERASE){
|
|
return ("Flash Erase Failed");
|
|
} else if(_error == UPDATE_ERROR_READ){
|
|
return ("Flash Read Failed");
|
|
} else if(_error == UPDATE_ERROR_SPACE){
|
|
return ("Not Enough Space");
|
|
} else if(_error == UPDATE_ERROR_SIZE){
|
|
return ("Bad Size Given");
|
|
} else if(_error == UPDATE_ERROR_STREAM){
|
|
return ("Stream Read Timeout");
|
|
} else if(_error == UPDATE_ERROR_MD5){
|
|
return ("MD5 Check Failed");
|
|
} else if(_error == UPDATE_ERROR_MAGIC_BYTE){
|
|
return ("Wrong Magic Byte");
|
|
} else if(_error == UPDATE_ERROR_ACTIVATE){
|
|
return ("Could Not Activate The Firmware");
|
|
} else if(_error == UPDATE_ERROR_NO_PARTITION){
|
|
return ("Partition Could Not be Found");
|
|
} else if(_error == UPDATE_ERROR_BAD_ARGUMENT){
|
|
return ("Bad Argument");
|
|
} else if(_error == UPDATE_ERROR_ABORT){
|
|
return ("Aborted");
|
|
}
|
|
return ("UNKNOWN");
|
|
}
|
|
|
|
static bool _partitionIsBootable(const esp_partition_t* partition){
|
|
uint8_t buf[ENCRYPTED_BLOCK_SIZE];
|
|
if(!partition){
|
|
return false;
|
|
}
|
|
if(!ESP.partitionRead(partition, 0, (uint32_t*)buf, ENCRYPTED_BLOCK_SIZE)) {
|
|
return false;
|
|
}
|
|
|
|
if(buf[0] != ESP_IMAGE_HEADER_MAGIC) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool UpdateClass::_enablePartition(const esp_partition_t* partition){
|
|
if(!partition){
|
|
return false;
|
|
}
|
|
return ESP.partitionWrite(partition, 0, (uint32_t*) _skipBuffer, ENCRYPTED_BLOCK_SIZE);
|
|
}
|
|
|
|
UpdateClass::UpdateClass()
|
|
: _error(0)
|
|
, _buffer(0)
|
|
, _bufferLen(0)
|
|
, _size(0)
|
|
, _progress_callback(NULL)
|
|
, _progress(0)
|
|
, _command(U_FLASH)
|
|
, _partition(NULL)
|
|
{
|
|
}
|
|
|
|
UpdateClass& UpdateClass::onProgress(THandlerFunction_Progress fn) {
|
|
_progress_callback = fn;
|
|
return *this;
|
|
}
|
|
|
|
void UpdateClass::_reset() {
|
|
if (_buffer)
|
|
delete[] _buffer;
|
|
_buffer = 0;
|
|
_bufferLen = 0;
|
|
_progress = 0;
|
|
_size = 0;
|
|
_command = U_FLASH;
|
|
|
|
if(_ledPin != -1) {
|
|
digitalWrite(_ledPin, !_ledOn); // off
|
|
}
|
|
}
|
|
|
|
bool UpdateClass::canRollBack(){
|
|
if(_buffer){ //Update is running
|
|
return false;
|
|
}
|
|
const esp_partition_t* partition = esp_ota_get_next_update_partition(NULL);
|
|
return _partitionIsBootable(partition);
|
|
}
|
|
|
|
bool UpdateClass::rollBack(){
|
|
if(_buffer){ //Update is running
|
|
return false;
|
|
}
|
|
const esp_partition_t* partition = esp_ota_get_next_update_partition(NULL);
|
|
return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition);
|
|
}
|
|
|
|
bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn) {
|
|
if(_size > 0){
|
|
log_w("already running");
|
|
return false;
|
|
}
|
|
|
|
_ledPin = ledPin;
|
|
_ledOn = !!ledOn; // 0(LOW) or 1(HIGH)
|
|
|
|
_reset();
|
|
_error = 0;
|
|
|
|
if(size == 0) {
|
|
_error = UPDATE_ERROR_SIZE;
|
|
return false;
|
|
}
|
|
|
|
if (command == U_FLASH) {
|
|
_partition = esp_ota_get_next_update_partition(NULL);
|
|
if(!_partition){
|
|
_error = UPDATE_ERROR_NO_PARTITION;
|
|
return false;
|
|
}
|
|
log_d("OTA Partition: %s", _partition->label);
|
|
}
|
|
else if (command == U_SPIFFS) {
|
|
_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL);
|
|
if(!_partition){
|
|
_error = UPDATE_ERROR_NO_PARTITION;
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
_error = UPDATE_ERROR_BAD_ARGUMENT;
|
|
log_e("bad command %u", command);
|
|
return false;
|
|
}
|
|
|
|
if(size == UPDATE_SIZE_UNKNOWN){
|
|
size = _partition->size;
|
|
} else if(size > _partition->size){
|
|
_error = UPDATE_ERROR_SIZE;
|
|
log_e("too large %u > %u", size, _partition->size);
|
|
return false;
|
|
}
|
|
|
|
//initialize
|
|
_buffer = (uint8_t*)malloc(SPI_FLASH_SEC_SIZE);
|
|
if(!_buffer){
|
|
log_e("malloc failed");
|
|
return false;
|
|
}
|
|
_size = size;
|
|
_command = command;
|
|
_md5.begin();
|
|
return true;
|
|
}
|
|
|
|
void UpdateClass::_abort(uint8_t err){
|
|
_reset();
|
|
_error = err;
|
|
}
|
|
|
|
void UpdateClass::abort(){
|
|
_abort(UPDATE_ERROR_ABORT);
|
|
}
|
|
|
|
bool UpdateClass::_writeBuffer(){
|
|
//first bytes of new firmware
|
|
uint8_t skip = 0;
|
|
if(!_progress && _command == U_FLASH){
|
|
//check magic
|
|
if(_buffer[0] != ESP_IMAGE_HEADER_MAGIC){
|
|
_abort(UPDATE_ERROR_MAGIC_BYTE);
|
|
return false;
|
|
}
|
|
|
|
//Stash the first 16 bytes of data and set the offset so they are
|
|
//not written at this point so that partially written firmware
|
|
//will not be bootable
|
|
skip = ENCRYPTED_BLOCK_SIZE;
|
|
_skipBuffer = (uint8_t*)malloc(skip);
|
|
if(!_skipBuffer){
|
|
log_e("malloc failed");
|
|
return false;
|
|
}
|
|
memcpy(_skipBuffer, _buffer, skip);
|
|
}
|
|
if (!_progress && _progress_callback) {
|
|
_progress_callback(0, _size);
|
|
}
|
|
if(!ESP.partitionEraseRange(_partition, _progress, SPI_FLASH_SEC_SIZE)){
|
|
_abort(UPDATE_ERROR_ERASE);
|
|
return false;
|
|
}
|
|
if (!ESP.partitionWrite(_partition, _progress + skip, (uint32_t*)_buffer + skip/sizeof(uint32_t), _bufferLen - skip)) {
|
|
_abort(UPDATE_ERROR_WRITE);
|
|
return false;
|
|
}
|
|
//restore magic or md5 will fail
|
|
if(!_progress && _command == U_FLASH){
|
|
_buffer[0] = ESP_IMAGE_HEADER_MAGIC;
|
|
}
|
|
_md5.add(_buffer, _bufferLen);
|
|
_progress += _bufferLen;
|
|
_bufferLen = 0;
|
|
if (_progress_callback) {
|
|
_progress_callback(_progress, _size);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool UpdateClass::_verifyHeader(uint8_t data) {
|
|
if(_command == U_FLASH) {
|
|
if(data != ESP_IMAGE_HEADER_MAGIC) {
|
|
_abort(UPDATE_ERROR_MAGIC_BYTE);
|
|
return false;
|
|
}
|
|
return true;
|
|
} else if(_command == U_SPIFFS) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UpdateClass::_verifyEnd() {
|
|
if(_command == U_FLASH) {
|
|
if(!_enablePartition(_partition) || !_partitionIsBootable(_partition)) {
|
|
_abort(UPDATE_ERROR_READ);
|
|
return false;
|
|
}
|
|
|
|
if(esp_ota_set_boot_partition(_partition)){
|
|
_abort(UPDATE_ERROR_ACTIVATE);
|
|
return false;
|
|
}
|
|
_reset();
|
|
return true;
|
|
} else if(_command == U_SPIFFS) {
|
|
_reset();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UpdateClass::setMD5(const char * expected_md5){
|
|
if(strlen(expected_md5) != 32)
|
|
{
|
|
return false;
|
|
}
|
|
_target_md5 = expected_md5;
|
|
return true;
|
|
}
|
|
|
|
bool UpdateClass::end(bool evenIfRemaining){
|
|
if(hasError() || _size == 0){
|
|
return false;
|
|
}
|
|
|
|
if(!isFinished() && !evenIfRemaining){
|
|
log_e("premature end: res:%u, pos:%u/%u\n", getError(), progress(), _size);
|
|
_abort(UPDATE_ERROR_ABORT);
|
|
return false;
|
|
}
|
|
|
|
if(evenIfRemaining) {
|
|
if(_bufferLen > 0) {
|
|
_writeBuffer();
|
|
}
|
|
_size = progress();
|
|
}
|
|
|
|
_md5.calculate();
|
|
if(_target_md5.length()) {
|
|
if(_target_md5 != _md5.toString()){
|
|
_abort(UPDATE_ERROR_MD5);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return _verifyEnd();
|
|
}
|
|
|
|
size_t UpdateClass::write(uint8_t *data, size_t len) {
|
|
if(hasError() || !isRunning()){
|
|
return 0;
|
|
}
|
|
|
|
if(len > remaining()){
|
|
_abort(UPDATE_ERROR_SPACE);
|
|
return 0;
|
|
}
|
|
|
|
size_t left = len;
|
|
|
|
while((_bufferLen + left) > SPI_FLASH_SEC_SIZE) {
|
|
size_t toBuff = SPI_FLASH_SEC_SIZE - _bufferLen;
|
|
memcpy(_buffer + _bufferLen, data + (len - left), toBuff);
|
|
_bufferLen += toBuff;
|
|
if(!_writeBuffer()){
|
|
return len - left;
|
|
}
|
|
left -= toBuff;
|
|
}
|
|
memcpy(_buffer + _bufferLen, data + (len - left), left);
|
|
_bufferLen += left;
|
|
if(_bufferLen == remaining()){
|
|
if(!_writeBuffer()){
|
|
return len - left;
|
|
}
|
|
}
|
|
return len;
|
|
}
|
|
|
|
size_t UpdateClass::writeStream(Stream &data) {
|
|
size_t written = 0;
|
|
size_t toRead = 0;
|
|
if(hasError() || !isRunning())
|
|
return 0;
|
|
|
|
if(!_verifyHeader(data.peek())) {
|
|
_reset();
|
|
return 0;
|
|
}
|
|
|
|
if(_ledPin != -1) {
|
|
pinMode(_ledPin, OUTPUT);
|
|
}
|
|
|
|
while(remaining()) {
|
|
if(_ledPin != -1) {
|
|
digitalWrite(_ledPin, _ledOn); // Switch LED on
|
|
}
|
|
size_t bytesToRead = SPI_FLASH_SEC_SIZE - _bufferLen;
|
|
if(bytesToRead > remaining()) {
|
|
bytesToRead = remaining();
|
|
}
|
|
|
|
toRead = data.readBytes(_buffer + _bufferLen, bytesToRead);
|
|
if(toRead == 0) { //Timeout
|
|
delay(100);
|
|
toRead = data.readBytes(_buffer + _bufferLen, bytesToRead);
|
|
if(toRead == 0) { //Timeout
|
|
_abort(UPDATE_ERROR_STREAM);
|
|
return written;
|
|
}
|
|
}
|
|
if(_ledPin != -1) {
|
|
digitalWrite(_ledPin, !_ledOn); // Switch LED off
|
|
}
|
|
_bufferLen += toRead;
|
|
if((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer())
|
|
return written;
|
|
written += toRead;
|
|
}
|
|
return written;
|
|
}
|
|
|
|
void UpdateClass::printError(Print &out){
|
|
out.println(_err2str(_error));
|
|
}
|
|
|
|
const char * UpdateClass::errorString(){
|
|
return _err2str(_error);
|
|
}
|
|
|
|
UpdateClass Update;
|