Add UART OTA upload with A/B partition support.

Firmware buffers 200-byte chunks into 4 KiB blocks for esp_ota_write; goTool
uploads with per-block ACK flow control and larger UART buffers to avoid stalls.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-19 00:39:59 +02:00
co-authored by Cursor
parent 1ad527119d
commit 59ca269407
20 changed files with 806 additions and 101 deletions
+1
View File
@@ -0,0 +1 @@
OtaPayload.data max_size:200
+43 -31
View File
@@ -38,6 +38,8 @@ typedef struct _alox_EchoPayload {
typedef struct _alox_VersionResponse {
uint32_t version;
pb_callback_t git_hash;
/* * Active OTA app partition label, e.g. "ota_0" or "ota_1". */
pb_callback_t running_partition;
} alox_VersionResponse;
typedef struct _alox_ClientInfo {
@@ -92,23 +94,30 @@ typedef struct _alox_EspNowUnicastTestResponse {
uint32_t seq;
} alox_EspNowUnicastTestResponse;
/* Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS). */
typedef struct _alox_OtaStartPayload {
uint32_t total_size;
uint32_t block_size;
} alox_OtaStartPayload;
/* Host → device: firmware chunk (up to 200 bytes); device buffers 4 KiB before flash write. */
typedef PB_BYTES_ARRAY_T(200) alox_OtaPayload_data_t;
typedef struct _alox_OtaPayload {
uint32_t block_id;
uint32_t chunk_id;
pb_callback_t data;
uint32_t seq;
alox_OtaPayload_data_t data;
} alox_OtaPayload;
/* Host → device: no more payload; device flushes buffer and finalizes OTA. */
typedef struct _alox_OtaEndPayload {
uint32_t status;
char dummy_field;
} alox_OtaEndPayload;
/* Device → host status (also used as ACK after each 4 KiB written).
status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed */
typedef struct _alox_OtaStatusPayload {
uint32_t status;
uint32_t bytes_written;
uint32_t target_slot;
uint32_t error;
} alox_OtaStatusPayload;
typedef struct _alox_UartMessage {
@@ -163,7 +172,7 @@ extern "C" {
#define alox_UartMessage_init_default {_alox_MessageType_MIN, 0, {alox_Ack_init_default}}
#define alox_Ack_init_default {0}
#define alox_EchoPayload_init_default {{{NULL}, NULL}}
#define alox_VersionResponse_init_default {0, {{NULL}, NULL}}
#define alox_VersionResponse_init_default {0, {{NULL}, NULL}, {{NULL}, NULL}}
#define alox_ClientInfo_init_default {0, 0, 0, {{NULL}, NULL}, 0, 0, 0}
#define alox_ClientInfoResponse_init_default {{{NULL}, NULL}}
#define alox_ClientInput_init_default {0, 0, 0, 0}
@@ -172,14 +181,14 @@ extern "C" {
#define alox_AccelDeadzoneResponse_init_default {0, 0, 0, 0}
#define alox_EspNowUnicastTestRequest_init_default {0, 0}
#define alox_EspNowUnicastTestResponse_init_default {0, 0}
#define alox_OtaStartPayload_init_default {0, 0}
#define alox_OtaPayload_init_default {0, 0, {{NULL}, NULL}}
#define alox_OtaStartPayload_init_default {0}
#define alox_OtaPayload_init_default {0, {0, {0}}}
#define alox_OtaEndPayload_init_default {0}
#define alox_OtaStatusPayload_init_default {0}
#define alox_OtaStatusPayload_init_default {0, 0, 0, 0}
#define alox_UartMessage_init_zero {_alox_MessageType_MIN, 0, {alox_Ack_init_zero}}
#define alox_Ack_init_zero {0}
#define alox_EchoPayload_init_zero {{{NULL}, NULL}}
#define alox_VersionResponse_init_zero {0, {{NULL}, NULL}}
#define alox_VersionResponse_init_zero {0, {{NULL}, NULL}, {{NULL}, NULL}}
#define alox_ClientInfo_init_zero {0, 0, 0, {{NULL}, NULL}, 0, 0, 0}
#define alox_ClientInfoResponse_init_zero {{{NULL}, NULL}}
#define alox_ClientInput_init_zero {0, 0, 0, 0}
@@ -188,15 +197,16 @@ extern "C" {
#define alox_AccelDeadzoneResponse_init_zero {0, 0, 0, 0}
#define alox_EspNowUnicastTestRequest_init_zero {0, 0}
#define alox_EspNowUnicastTestResponse_init_zero {0, 0}
#define alox_OtaStartPayload_init_zero {0, 0}
#define alox_OtaPayload_init_zero {0, 0, {{NULL}, NULL}}
#define alox_OtaStartPayload_init_zero {0}
#define alox_OtaPayload_init_zero {0, {0, {0}}}
#define alox_OtaEndPayload_init_zero {0}
#define alox_OtaStatusPayload_init_zero {0}
#define alox_OtaStatusPayload_init_zero {0, 0, 0, 0}
/* Field tags (for use in manual encoding/decoding) */
#define alox_EchoPayload_data_tag 1
#define alox_VersionResponse_version_tag 1
#define alox_VersionResponse_git_hash_tag 2
#define alox_VersionResponse_running_partition_tag 3
#define alox_ClientInfo_id_tag 1
#define alox_ClientInfo_available_tag 2
#define alox_ClientInfo_used_tag 3
@@ -223,12 +233,12 @@ extern "C" {
#define alox_EspNowUnicastTestResponse_success_tag 1
#define alox_EspNowUnicastTestResponse_seq_tag 2
#define alox_OtaStartPayload_total_size_tag 1
#define alox_OtaStartPayload_block_size_tag 2
#define alox_OtaPayload_block_id_tag 1
#define alox_OtaPayload_chunk_id_tag 2
#define alox_OtaPayload_data_tag 3
#define alox_OtaEndPayload_status_tag 1
#define alox_OtaPayload_seq_tag 1
#define alox_OtaPayload_data_tag 2
#define alox_OtaStatusPayload_status_tag 1
#define alox_OtaStatusPayload_bytes_written_tag 2
#define alox_OtaStatusPayload_target_slot_tag 3
#define alox_OtaStatusPayload_error_tag 4
#define alox_UartMessage_type_tag 1
#define alox_UartMessage_ack_payload_tag 2
#define alox_UartMessage_echo_payload_tag 3
@@ -288,7 +298,8 @@ X(a, CALLBACK, SINGULAR, BYTES, data, 1)
#define alox_VersionResponse_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, version, 1) \
X(a, CALLBACK, SINGULAR, STRING, git_hash, 2)
X(a, CALLBACK, SINGULAR, STRING, git_hash, 2) \
X(a, CALLBACK, SINGULAR, STRING, running_partition, 3)
#define alox_VersionResponse_CALLBACK pb_default_field_callback
#define alox_VersionResponse_DEFAULT NULL
@@ -352,25 +363,26 @@ X(a, STATIC, SINGULAR, UINT32, seq, 2)
#define alox_EspNowUnicastTestResponse_DEFAULT NULL
#define alox_OtaStartPayload_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, total_size, 1) \
X(a, STATIC, SINGULAR, UINT32, block_size, 2)
X(a, STATIC, SINGULAR, UINT32, total_size, 1)
#define alox_OtaStartPayload_CALLBACK NULL
#define alox_OtaStartPayload_DEFAULT NULL
#define alox_OtaPayload_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, block_id, 1) \
X(a, STATIC, SINGULAR, UINT32, chunk_id, 2) \
X(a, CALLBACK, SINGULAR, BYTES, data, 3)
#define alox_OtaPayload_CALLBACK pb_default_field_callback
X(a, STATIC, SINGULAR, UINT32, seq, 1) \
X(a, STATIC, SINGULAR, BYTES, data, 2)
#define alox_OtaPayload_CALLBACK NULL
#define alox_OtaPayload_DEFAULT NULL
#define alox_OtaEndPayload_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, status, 1)
#define alox_OtaEndPayload_CALLBACK NULL
#define alox_OtaEndPayload_DEFAULT NULL
#define alox_OtaStatusPayload_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, status, 1)
X(a, STATIC, SINGULAR, UINT32, status, 1) \
X(a, STATIC, SINGULAR, UINT32, bytes_written, 2) \
X(a, STATIC, SINGULAR, UINT32, target_slot, 3) \
X(a, STATIC, SINGULAR, UINT32, error, 4)
#define alox_OtaStatusPayload_CALLBACK NULL
#define alox_OtaStatusPayload_DEFAULT NULL
@@ -417,16 +429,16 @@ extern const pb_msgdesc_t alox_OtaStatusPayload_msg;
/* alox_ClientInfoResponse_size depends on runtime parameters */
/* alox_ClientInputResponse_size depends on runtime parameters */
/* alox_OtaPayload_size depends on runtime parameters */
#define ALOX_MAIN_PROTO_UART_MESSAGES_PB_H_MAX_SIZE alox_ClientInput_size
#define ALOX_MAIN_PROTO_UART_MESSAGES_PB_H_MAX_SIZE alox_OtaStatusPayload_size
#define alox_AccelDeadzoneRequest_size 16
#define alox_AccelDeadzoneResponse_size 20
#define alox_Ack_size 0
#define alox_ClientInput_size 22
#define alox_EspNowUnicastTestRequest_size 12
#define alox_EspNowUnicastTestResponse_size 8
#define alox_OtaEndPayload_size 6
#define alox_OtaStartPayload_size 12
#define alox_OtaStatusPayload_size 6
#define alox_OtaEndPayload_size 0
#define alox_OtaStartPayload_size 6
#define alox_OtaStatusPayload_size 24
#ifdef __cplusplus
} /* extern "C" */
+17 -9
View File
@@ -1,5 +1,7 @@
syntax = "proto3";
import "nanopb.proto";
package alox;
enum MessageType {
@@ -46,6 +48,8 @@ message EchoPayload {
message VersionResponse {
uint32 version = 1;
string git_hash = 2;
/** Active OTA app partition label, e.g. "ota_0" or "ota_1". */
string running_partition = 3;
}
message ClientInfo {
@@ -100,21 +104,25 @@ message EspNowUnicastTestResponse {
uint32 seq = 2;
}
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
message OtaStartPayload {
uint32 total_size = 1;
uint32 block_size = 2;
}
// Host → device: firmware chunk (up to 200 bytes); device buffers 4 KiB before flash write.
message OtaPayload {
uint32 block_id = 1;
uint32 chunk_id = 2;
bytes data = 3;
uint32 seq = 1;
bytes data = 2 [(nanopb).max_size = 200];
}
message OtaEndPayload {
uint32 status = 1;
}
// Host → device: no more payload; device flushes buffer and finalizes OTA.
message OtaEndPayload {}
message OtaStatusPayload {
uint32 status = 1;
// Device → host status (also used as ACK after each 4 KiB written).
// status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed
message OtaStatusPayload {
uint32 status = 1;
uint32 bytes_written = 2;
uint32 target_slot = 3;
uint32 error = 4;
}