Skip to content

Commit f2223dd

Browse files
committed
feat(modem): Added test that performs OTA to exercise modem layers
1 parent 82c2cf8 commit f2223dd

File tree

13 files changed

+1034
-1
lines changed

13 files changed

+1034
-1
lines changed

components/esp_modem/include/vfs_resource/vfs_create.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
*/
3232
struct esp_modem_vfs_uart_creator {
3333
const char *dev_name; /*!< VFS device name, e.g. /dev/uart/n */
34-
const struct esp_modem_uart_term_config uart; /*!< UART driver init struct */
34+
struct esp_modem_uart_term_config uart; /*!< UART driver init struct */
3535
};
3636

3737
/**
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# The following lines of boilerplate have to be in your project's CMakeLists
2+
# in this exact order for cmake to work correctly
3+
cmake_minimum_required(VERSION 3.8)
4+
5+
set(EXTRA_COMPONENT_DIRS "../.." "../../examples/modem_tcp_client/components")
6+
7+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
8+
project(ota_test)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Target test running OTA update
2+
3+
## Overview
4+
5+
The aim of this test is to exercise the most commonly failing scenario, running OTA over PPPoS with https.
6+
7+
This project opens a data session, runs basic mqtt operations and initiates OTA update.
8+
It supports the following test configurations:
9+
* Using a real modem device (default config)
10+
* Using VFS device (only to exercise VFS DTE)
11+
* Using network-only DCE (connecting directly to PPP server) -- needs some configuration
12+
13+
### Configuring the PPP server
14+
15+
You need to run these applications on your host machine:
16+
* PPP server
17+
```bash
18+
sudo pppd /dev/ttyUSB1 115200 192.168.11.1:192.168.11.2 ms-dns 8.8.8.8 modem local noauth debug nocrtscts nodetach +ipv6
19+
```
20+
* MQTT broker: Running mosquitto in the default config is enough, configuring the broker's URL to the local PPP address: `config.broker.address.uri = "mqtt://192.168.11.1";`
21+
* HTTP server: Need to support HTTP/1.1 (to support ranges). You can use the script `http_server.py` and configure the OTA endpoint as `"https://192.168.11.1:1234/esp32.bin"`
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
idf_component_register(SRCS manual_ota.cpp transport_batch_tls.cpp
2+
INCLUDE_DIRS "."
3+
PRIV_REQUIRES extra_tcp_transports esp_http_client app_update)
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Unlicense OR CC0-1.0
5+
*/
6+
#include "manual_ota.hpp"
7+
#include "esp_log.h"
8+
#include "esp_ota_ops.h"
9+
#include "esp_app_format.h"
10+
#include "esp_http_client.h"
11+
#include "esp_partition.h"
12+
#include "esp_transport_tcp.h"
13+
#include "transport_batch_tls.hpp"
14+
15+
static const char *TAG = "manual_ota";
16+
17+
bool manual_ota::begin()
18+
{
19+
if (status != state::UNDEF) {
20+
ESP_LOGE(TAG, "Invalid state");
21+
return false;
22+
}
23+
status = state::INIT;
24+
esp_transport_handle_t tcp = esp_transport_tcp_init();
25+
ssl_ = esp_transport_batch_tls_init(tcp, max_buffer_size_);
26+
27+
esp_http_client_config_t config = { };
28+
config.skip_cert_common_name_check = true;
29+
config.url = uri_;
30+
config.transport = ssl_;
31+
const esp_partition_t *configured = esp_ota_get_boot_partition();
32+
const esp_partition_t *running = esp_ota_get_running_partition();
33+
34+
if (configured != running) {
35+
ESP_LOGE(TAG, "Configured OTA boot partition at offset 0x%08" PRIx32 ", but running from offset 0x%08" PRIx32, configured->address, running->address);
36+
return false;
37+
}
38+
39+
http_ = esp_http_client_init(&config);
40+
if (http_ == nullptr) {
41+
ESP_LOGE(TAG, "Failed to initialise HTTP connection");
42+
return false;
43+
}
44+
esp_http_client_set_method(http_, HTTP_METHOD_HEAD);
45+
esp_err_t err = esp_http_client_perform(http_);
46+
if (err == ESP_OK) {
47+
int http_status = esp_http_client_get_status_code(http_);
48+
if (http_status != HttpStatus_Ok) {
49+
ESP_LOGE(TAG, "Received incorrect http status %d", http_status);
50+
return false;
51+
}
52+
} else {
53+
ESP_LOGE(TAG, "ESP HTTP client perform failed: %d", err);
54+
return false;
55+
}
56+
image_length_ = esp_http_client_get_content_length(http_);
57+
ESP_LOGI(TAG, "image_length = %lld", image_length_);
58+
esp_http_client_close(http_);
59+
60+
if (image_length_ > size_) {
61+
char *header_val = nullptr;
62+
asprintf(&header_val, "bytes=0-%d", max_buffer_size_ - 1);
63+
if (header_val == nullptr) {
64+
ESP_LOGE(TAG, "Failed to allocate memory for HTTP header");
65+
return false;
66+
}
67+
esp_http_client_set_header(http_, "Range", header_val);
68+
free(header_val);
69+
}
70+
esp_http_client_set_method(http_, HTTP_METHOD_GET);
71+
72+
partition_ = esp_ota_get_next_update_partition(nullptr);
73+
if (partition_ == nullptr) {
74+
ESP_LOGE(TAG, "Invalid update partition");
75+
return false;
76+
}
77+
ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%" PRIx32, partition_->subtype, partition_->address);
78+
79+
file_length_ = 0;
80+
reconnect_attempts_ = 0;
81+
buffer_.resize(max_buffer_size_);
82+
status = state::IMAGE_CHECK;
83+
return true;
84+
}
85+
86+
bool manual_ota::perform()
87+
{
88+
if (status != state::IMAGE_CHECK && status != state::START) {
89+
ESP_LOGE(TAG, "Invalid state");
90+
return false;
91+
}
92+
esp_err_t err = esp_http_client_open(http_, 0);
93+
if (err != ESP_OK) {
94+
if (image_length_ == file_length_) {
95+
status = state::END;
96+
return false;
97+
}
98+
99+
esp_http_client_close(http_);
100+
ESP_LOGI(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
101+
if (reconnect_attempts_++ < max_reconnect_attempts_) {
102+
if (prepare_reconnect()) {
103+
return true; // will retry in the next iteration
104+
}
105+
}
106+
return fail_cleanup();
107+
}
108+
esp_http_client_fetch_headers(http_);
109+
110+
int batch_len = esp_transport_batch_tls_pre_read(ssl_, max_buffer_size_, timeout_ * 1000);
111+
if (batch_len < 0) {
112+
ESP_LOGE(TAG, "Error: Failed to pre-read plain text data!");
113+
return fail_cleanup();
114+
}
115+
116+
int data_read = esp_http_client_read(http_, buffer_.data(), batch_len);
117+
118+
if (data_read < 0) {
119+
ESP_LOGE(TAG, "Error: SSL data read error");
120+
return fail_cleanup();
121+
} else if (data_read > 0) {
122+
esp_http_client_close(http_);
123+
124+
if (status == state::IMAGE_CHECK) {
125+
esp_app_desc_t new_app_info;
126+
if (data_read > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) {
127+
// check current version with downloading
128+
memcpy(&new_app_info, &buffer_[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t));
129+
ESP_LOGI(TAG, "New firmware version: %s", new_app_info.version);
130+
131+
esp_app_desc_t running_app_info;
132+
const esp_partition_t *running = esp_ota_get_running_partition();
133+
if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) {
134+
ESP_LOGI(TAG, "Running firmware version: %s", running_app_info.version);
135+
}
136+
137+
const esp_partition_t *last_invalid_app = esp_ota_get_last_invalid_partition();
138+
esp_app_desc_t invalid_app_info;
139+
if (esp_ota_get_partition_description(last_invalid_app, &invalid_app_info) == ESP_OK) {
140+
ESP_LOGI(TAG, "Last invalid firmware version: %s", invalid_app_info.version);
141+
}
142+
143+
// check current version with last invalid partition
144+
if (last_invalid_app != NULL) {
145+
if (memcmp(invalid_app_info.version, new_app_info.version, sizeof(new_app_info.version)) == 0) {
146+
ESP_LOGW(TAG, "New version is the same as invalid version.");
147+
ESP_LOGW(TAG, "Previously, there was an attempt to launch the firmware with %s version, but it failed.", invalid_app_info.version);
148+
ESP_LOGW(TAG, "The firmware has been rolled back to the previous version.");
149+
return fail_cleanup();
150+
}
151+
}
152+
153+
status = state::START;
154+
err = esp_ota_begin(partition_, OTA_WITH_SEQUENTIAL_WRITES, &update_handle_);
155+
if (err != ESP_OK) {
156+
ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
157+
esp_ota_abort(update_handle_);
158+
return fail_cleanup();
159+
}
160+
ESP_LOGI(TAG, "esp_ota_begin succeeded");
161+
} else {
162+
ESP_LOGE(TAG, "Received chunk doesn't contain app descriptor");
163+
esp_ota_abort(update_handle_);
164+
return fail_cleanup();
165+
}
166+
}
167+
err = esp_ota_write(update_handle_, (const void *)buffer_.data(), data_read);
168+
if (err != ESP_OK) {
169+
esp_ota_abort(update_handle_);
170+
return fail_cleanup();
171+
}
172+
file_length_ += data_read;
173+
ESP_LOGI(TAG, "Written image length %d", file_length_);
174+
175+
if (image_length_ == file_length_) {
176+
status = state::END;
177+
return false;
178+
}
179+
180+
if (!prepare_reconnect()) {
181+
esp_ota_abort(update_handle_);
182+
return fail_cleanup();
183+
}
184+
185+
} else if (data_read == 0) {
186+
if (file_length_ == 0) {
187+
// Try to handle possible HTTP redirections
188+
int status_code = esp_http_client_get_status_code(http_);
189+
ESP_LOGW(TAG, "Status code: %d", status_code);
190+
err = esp_http_client_set_redirection(http_);
191+
if (err != ESP_OK) {
192+
ESP_LOGE(TAG, "URL redirection Failed");
193+
esp_ota_abort(update_handle_);
194+
return fail_cleanup();
195+
}
196+
197+
err = esp_http_client_open(http_, 0);
198+
if (err != ESP_OK) {
199+
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
200+
return fail_cleanup();
201+
}
202+
esp_http_client_fetch_headers(http_);
203+
}
204+
}
205+
206+
return true;
207+
}
208+
209+
bool manual_ota::prepare_reconnect()
210+
{
211+
esp_http_client_set_method(http_, HTTP_METHOD_GET);
212+
char *header_val = nullptr;
213+
if ((image_length_ - file_length_) > max_buffer_size_) {
214+
asprintf(&header_val, "bytes=%d-%d", file_length_, (file_length_ + max_buffer_size_ - 1));
215+
} else {
216+
asprintf(&header_val, "bytes=%d-", file_length_);
217+
}
218+
if (header_val == nullptr) {
219+
ESP_LOGE(TAG, "Failed to allocate memory for HTTP header");
220+
return false;
221+
}
222+
esp_http_client_set_header(http_, "Range", header_val);
223+
free(header_val);
224+
return true;
225+
}
226+
227+
bool manual_ota::fail_cleanup()
228+
{
229+
esp_http_client_close(http_);
230+
esp_http_client_cleanup(http_);
231+
status = state::FAIL;
232+
return false;
233+
}
234+
235+
bool manual_ota::end()
236+
{
237+
if (status == state::END) {
238+
if (!esp_http_client_is_complete_data_received(http_)) {
239+
ESP_LOGE(TAG, "Error in receiving complete file");
240+
return fail_cleanup();
241+
}
242+
esp_err_t err = esp_ota_end(update_handle_);
243+
if (err != ESP_OK) {
244+
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
245+
ESP_LOGE(TAG, "Image validation failed, image is corrupted");
246+
} else {
247+
ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err));
248+
}
249+
return fail_cleanup();
250+
}
251+
err = esp_ota_set_boot_partition(partition_);
252+
if (err != ESP_OK) {
253+
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err));
254+
return fail_cleanup();
255+
}
256+
return true;
257+
}
258+
return false;
259+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Unlicense OR CC0-1.0
5+
*/
6+
#pragma once
7+
#include <vector>
8+
#include "esp_http_client.h"
9+
#include "esp_partition.h"
10+
#include "esp_transport_tcp.h"
11+
#include "esp_ota_ops.h"
12+
13+
class manual_ota {
14+
public:
15+
enum class state {
16+
UNDEF,
17+
INIT,
18+
IMAGE_CHECK,
19+
START,
20+
END,
21+
FAIL,
22+
};
23+
size_t size_{32};
24+
int timeout_{2};
25+
26+
/**
27+
* @brief Construct a new manual ota object
28+
*
29+
* @param uri URI of the binary image
30+
*/
31+
explicit manual_ota(const char *uri): uri_(uri) {}
32+
33+
/**
34+
* @brief Start the manual OTA process
35+
*
36+
* @return true if started successfully
37+
*/
38+
bool begin();
39+
40+
/**
41+
* @brief Performs one read-write OTA iteration
42+
*
43+
* @return true if the process is in progress
44+
* @return false if the process finished, call end() to get OTA result
45+
*/
46+
bool perform();
47+
48+
/**
49+
* @brief Finishes an OTA update
50+
*
51+
* @return true if the OTA update completed successfully
52+
*/
53+
bool end();
54+
55+
private:
56+
const char *uri_{};
57+
esp_http_client_handle_t http_;
58+
int64_t image_length_;
59+
size_t file_length_;
60+
const size_t max_buffer_size_{size_ * 1024};
61+
const esp_partition_t *partition_{nullptr};
62+
state status{state::UNDEF};
63+
std::vector<char> buffer_{};
64+
int reconnect_attempts_;
65+
const int max_reconnect_attempts_{3};
66+
esp_transport_handle_t ssl_;
67+
esp_ota_handle_t update_handle_{0};
68+
69+
bool prepare_reconnect();
70+
bool fail_cleanup();
71+
};

0 commit comments

Comments
 (0)