cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
ota.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
10
11/******************************************************************
12 * 1. Included files
13 ******************************************************************/
14
15#include "ota.h"
16
17#include <stdio.h>
18#include <string.h>
19
20#include "freertos/FreeRTOS.h"
21#include "freertos/task.h"
22#include "freertos/semphr.h"
23
24/* Before esp_ota_ops.h, and it has to stay there — same reason provision.cpp
25 * includes it first: CW_Utils.h drags in Arduino's IPAddress.h, whose
26 * `extern const IPAddress INADDR_NONE;` stops parsing once lwIP's headers have
27 * turned INADDR_NONE into a macro, and the esp_ota headers reach lwIP. */
28#include "CW_Utils.h"
29
30#include "esp_app_desc.h"
31#include "esp_log.h"
32#include "esp_ota_ops.h"
33#include "esp_system.h"
34
35#include "ota_version.h"
36
37static const char *const TAG = "ota";
38
39/******************************************************************
40 * 2. Constants
41 ******************************************************************/
42
43/* Below this an "image" is a truncated download or somebody poking at the
44 * endpoint, and not worth erasing a 2 MB partition over. */
45#define OTA_MIN_IMAGE (256U * 1024U)
46
47/******************************************************************
48 * 3. Module state
49 ******************************************************************/
50
51static char s_running_ver[OTA_VERSION_MAX + 1] = "";
52
53/* An image that has been received, verified and written to the idle slot but
54 * NOT made bootable. Written by the HTTP task, read and resolved by the UI task
55 * once the operator has accepted it on the panel — two tasks and a value that
56 * decides which firmware signs the next transaction, so it takes a lock. */
57static SemaphoreHandle_t s_lock = NULL;
58static bool s_staged = false;
59static bool s_staged_older = false;
60static char s_staged_ver[OTA_VERSION_MAX + 1] = "";
61
62/* The upload in flight. Only one at a time — a second POST is refused rather than
63 * interleaved into the same partition. */
64static volatile bool s_receiving = false;
65static esp_ota_handle_t s_handle = 0;
66static const esp_partition_t *s_dst = NULL;
67
69static bool lock_ready(void)
70{
71 if (s_lock == NULL) { s_lock = xSemaphoreCreateMutex(); }
72 return s_lock != NULL;
73}
74
75/******************************************************************
76 * 4. Receiving an image
77 ******************************************************************/
78
87static bool refuse(const char **err, const char *msg)
88{
89 *err = msg;
90 ESP_LOGW(TAG, "upload refused: %s", msg);
91 return false;
92}
93
94bool ota_begin(size_t len, const char **err)
95{
96 static const char *ignored = "";
97 if (err == NULL) { err = &ignored; }
98
99 if (!lock_ready()) {
100 return refuse(err, "The terminal is out of memory.");
101 }
102 if (s_receiving) {
103 return refuse(err, "Another upload is in progress.");
104 }
105
106 bool staged = false;
107 if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
108 staged = s_staged;
109 (void)xSemaphoreGive(s_lock);
110 }
111 if (staged) {
112 return refuse(err, "An update is already waiting to be accepted on the "
113 "terminal screen. Accept or discard it there first.");
114 }
115
116 s_dst = esp_ota_get_next_update_partition(NULL);
117 if (s_dst == NULL) {
118 /* Single-app partition table: this unit predates OTA support and cannot
119 * be updated over the air at all. Say which, or it reads as a bug. */
120 ESP_LOGE(TAG, "no OTA slot - unit needs a serial reflash first");
121 return refuse(err, "This terminal has no second firmware slot. It has to "
122 "be reflashed over USB once before it can take "
123 "updates.");
124 }
125 if (len < OTA_MIN_IMAGE) {
126 return refuse(err, "That file is too small to be firmware.");
127 }
128 if (len > s_dst->size) {
129 return refuse(err, "That file is larger than the firmware slot.");
130 }
131
132 /* Passing the real length erases only the pages that will be written. */
133 const esp_err_t rc = esp_ota_begin(s_dst, len, &s_handle);
134 if (rc != ESP_OK) {
135 ESP_LOGE(TAG, "esp_ota_begin: %s", esp_err_to_name(rc));
136 return refuse(err, "The terminal could not prepare its firmware slot.");
137 }
138
139 s_receiving = true;
140 ESP_LOGI(TAG, "receiving %u bytes into '%s'",
141 static_cast<unsigned>(len), s_dst->label);
142 return true;
143}
144
145bool ota_write(const void *buf, size_t n)
146{
147 if (!s_receiving) { return false; }
148 const esp_err_t rc = esp_ota_write(s_handle, buf, n);
149 if (rc != ESP_OK) {
150 ESP_LOGE(TAG, "esp_ota_write: %s", esp_err_to_name(rc));
151 return false;
152 }
153 return true;
154}
155
156void ota_abort(void)
157{
158 if (!s_receiving) { return; }
159 (void)esp_ota_abort(s_handle);
160 s_handle = 0;
161 s_receiving = false;
162 ESP_LOGW(TAG, "upload aborted - nothing installed");
163}
164
165bool ota_receiving(void) { return s_receiving; }
166
167bool ota_end(char *ver_out, size_t ver_n, const char **err)
168{
169 static const char *ignored = "";
170 if (err == NULL) { err = &ignored; }
171
172 if (!s_receiving) {
173 *err = "No upload was in progress.";
174 return false;
175 }
176
177 const esp_err_t rc = esp_ota_end(s_handle);
178 s_handle = 0;
179 s_receiving = false;
180 if (rc != ESP_OK) {
181 ESP_LOGE(TAG, "image rejected: %s", esp_err_to_name(rc));
182 *err = (rc == ESP_ERR_OTA_VALIDATE_FAILED)
183 ? "The terminal rejected that image: it is not valid firmware, or "
184 "it is not signed with the key this terminal trusts."
185 : "The terminal could not store that image.";
186 return false;
187 }
188
189 /* Read the version out of the image that was just verified, not out of
190 * anything the browser said about it. */
191 esp_app_desc_t desc;
192 memset(&desc, 0, sizeof(desc));
193 char ver[OTA_VERSION_MAX + 1] = "?";
194 if ((s_dst != NULL) &&
195 (esp_ota_get_partition_description(s_dst, &desc) == ESP_OK)) {
196 (void)snprintf(ver, sizeof(ver), "%.*s",
197 static_cast<int>(sizeof(desc.version)), desc.version);
198 }
199
200 if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(1000)) != pdTRUE) {
201 *err = "The terminal is busy.";
202 return false;
203 }
204 s_staged = true;
206 (void)snprintf(s_staged_ver, sizeof(s_staged_ver), "%s", ver);
207 (void)xSemaphoreGive(s_lock);
208
209 ESP_LOGW(TAG, "staged %s in '%s' - awaiting on-screen accept", ver,
210 (s_dst != NULL) ? s_dst->label : "?");
211 if ((ver_out != NULL) && (ver_n > 0U)) {
212 (void)snprintf(ver_out, ver_n, "%s", ver);
213 }
214 return true;
215}
216
217/******************************************************************
218 * 5. Slot handling
219 ******************************************************************/
220
222{
223 const esp_partition_t *idle = esp_ota_get_next_update_partition(NULL);
224 if (idle == NULL) { return false; }
225
226 esp_ota_img_states_t st = ESP_OTA_IMG_UNDEFINED;
227 if (esp_ota_get_state_partition(idle, &st) != ESP_OK) { return false; }
228 return (st == ESP_OTA_IMG_ABORTED) || (st == ESP_OTA_IMG_INVALID);
229}
230
232{
233 /* Before the early returns below, because this is the one call that happens
234 * once per boot with bring-up behind it — and a terminal that reverted to its
235 * old firmware overnight is exactly the thing whose log line nobody has. */
237 ESP_LOGE(TAG, "the last update did NOT stick: the new image booted and "
238 "never confirmed itself, so this terminal rolled back to %s. "
239 "Install it again and leave it alone until the reader is up.",
241 }
242
243 const esp_partition_t *run = esp_ota_get_running_partition();
244 if (run == NULL) { return false; }
245
246 esp_ota_img_states_t st = ESP_OTA_IMG_UNDEFINED;
247 if (esp_ota_get_state_partition(run, &st) != ESP_OK) { return false; }
248 if (st != ESP_OTA_IMG_PENDING_VERIFY) { return false; } /* not a fresh update */
249
250 if (esp_ota_mark_app_valid_cancel_rollback() == ESP_OK) {
251 ESP_LOGW(TAG, "update to %s confirmed - rollback cancelled",
253 } else {
254 /* The next reset goes back to the old slot. Loud, because the terminal
255 * works right now and will silently be a different version tomorrow. */
256 ESP_LOGE(TAG, "could not confirm this image - it WILL roll back");
257 }
258 /* Fresh either way: the image did boot. Whether it also managed to cancel its
259 * rollback changes nothing for the operator standing in front of it. */
260 return true;
261}
262
263const char *ota_running_version(void)
264{
265 if (s_running_ver[0] == '\0') {
266 const esp_app_desc_t *d = esp_app_get_description();
267 /* esp_app_desc_t::version is a fixed 32-byte field with no promise of a
268 * terminator. Bound the copy. */
269 (void)snprintf(s_running_ver, sizeof(s_running_ver), "%.*s",
270 static_cast<int>(sizeof(d->version)), d->version);
271 }
272 return s_running_ver;
273}
274
275bool ota_staged(char *version, size_t version_n, bool *older)
276{
277 if (s_lock == NULL) { return false; }
278 if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(100)) != pdTRUE) { return false; }
279
280 const bool staged = s_staged;
281 if (staged) {
282 if ((version != NULL) && (version_n > 0U)) {
283 (void)snprintf(version, version_n, "%s", s_staged_ver);
284 }
285 if (older != NULL) { *older = s_staged_older; }
286 }
287 (void)xSemaphoreGive(s_lock);
288 return staged;
289}
290
291bool ota_commit(bool install)
292{
293 if (s_lock == NULL) { return false; }
294 if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(100)) != pdTRUE) { return false; }
295
296 const bool staged = s_staged;
297 s_staged = false;
298 char ver[OTA_VERSION_MAX + 1];
299 (void)snprintf(ver, sizeof(ver), "%s", s_staged_ver);
300 s_staged_ver[0] = '\0';
301 (void)xSemaphoreGive(s_lock);
302
303 if (!staged || !install) { return false; }
304
305 /* The point of no return, and the only line in this file that changes what
306 * the terminal boots. Everything before it was reversible. */
307 const esp_partition_t *dst = esp_ota_get_next_update_partition(NULL);
308 const esp_err_t rc = esp_ota_set_boot_partition(dst);
309 if (rc != ESP_OK) {
310 ESP_LOGE(TAG, "esp_ota_set_boot_partition: %s", esp_err_to_name(rc));
311 return false;
312 }
313
314 ESP_LOGW(TAG, "installing %s from '%s' - rebooting", ver,
315 (dst != NULL) ? dst->label : "?");
316 /* Let the log drain and the panel finish its last frame. */
317 vTaskDelay(pdMS_TO_TICKS(500));
318 esp_restart();
319 return true; /* not reached */
320}
static const char *const TAG
Definition eth_rpc.cpp:33
void ota_abort(void)
Give up on an upload in progress. Nothing is installed. Safe always.
Definition ota.cpp:156
const char * ota_running_version(void)
The running firmware's version, from the image header.
Definition ota.cpp:263
bool ota_end(char *ver_out, size_t ver_n, const char **err)
Close and verify the received image, then stage it for the panel.
Definition ota.cpp:167
bool ota_mark_valid(void)
Confirm the running image, cancelling the rollback armed by the bootloader.
Definition ota.cpp:231
bool ota_receiving(void)
Whether an upload is in flight, so a second can be refused.
Definition ota.cpp:165
static char s_staged_ver[OTA_VERSION_MAX+1]
Definition ota.cpp:60
static bool s_staged
Definition ota.cpp:58
static esp_ota_handle_t s_handle
Definition ota.cpp:65
static SemaphoreHandle_t s_lock
Definition ota.cpp:57
static bool s_staged_older
Definition ota.cpp:59
static const esp_partition_t * s_dst
Definition ota.cpp:66
static bool lock_ready(void)
Create the staging lock on first use.
Definition ota.cpp:69
static bool refuse(const char **err, const char *msg)
Refuse an upload, saying so to the browser AND to the log.
Definition ota.cpp:87
#define OTA_MIN_IMAGE
Definition ota.cpp:45
bool ota_staged(char *version, size_t version_n, bool *older)
Fetch the version of an image that has been received and verified but not yet installed.
Definition ota.cpp:275
static volatile bool s_receiving
Definition ota.cpp:64
bool ota_last_update_failed(void)
Whether the last update was installed and then thrown away.
Definition ota.cpp:221
bool ota_write(const void *buf, size_t n)
Append n bytes to the open slot.
Definition ota.cpp:145
bool ota_commit(bool install)
Resolve a staged image.
Definition ota.cpp:291
static char s_running_ver[OTA_VERSION_MAX+1]
Definition ota.cpp:51
bool ota_begin(size_t len, const char **err)
Open the idle slot for an image of len bytes.
Definition ota.cpp:94
Firmware slot handling: receive an image into the idle slot, verify it, and install it only once some...
Dotted version comparison, for deciding whether an update goes forwards or backwards.
#define OTA_VERSION_MAX
Longest version string looked at, matching esp_app_desc_t::version.
Definition ota_version.h:35
static int ota_version_cmp(const char *a, const char *b)
Order two version strings.
Definition ota_version.h:90