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
+61 -46
View File
@@ -447,11 +447,13 @@ func (x *EchoPayload) GetData() []byte {
}
type VersionResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
GitHash string `protobuf:"bytes,2,opt,name=git_hash,json=gitHash,proto3" json:"git_hash,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
GitHash string `protobuf:"bytes,2,opt,name=git_hash,json=gitHash,proto3" json:"git_hash,omitempty"`
// * Active OTA app partition label, e.g. "ota_0" or "ota_1".
RunningPartition string `protobuf:"bytes,3,opt,name=running_partition,json=runningPartition,proto3" json:"running_partition,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *VersionResponse) Reset() {
@@ -498,6 +500,13 @@ func (x *VersionResponse) GetGitHash() string {
return ""
}
func (x *VersionResponse) GetRunningPartition() string {
if x != nil {
return x.RunningPartition
}
return ""
}
type ClientInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -989,10 +998,10 @@ func (x *EspNowUnicastTestResponse) GetSeq() uint32 {
return 0
}
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
type OtaStartPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
TotalSize uint32 `protobuf:"varint,1,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"`
BlockSize uint32 `protobuf:"varint,2,opt,name=block_size,json=blockSize,proto3" json:"block_size,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1034,18 +1043,11 @@ func (x *OtaStartPayload) GetTotalSize() uint32 {
return 0
}
func (x *OtaStartPayload) GetBlockSize() uint32 {
if x != nil {
return x.BlockSize
}
return 0
}
// Host → device: firmware chunk (up to 200 bytes); device buffers 4 KiB before flash write.
type OtaPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
BlockId uint32 `protobuf:"varint,1,opt,name=block_id,json=blockId,proto3" json:"block_id,omitempty"`
ChunkId uint32 `protobuf:"varint,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"`
Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
Seq uint32 `protobuf:"varint,1,opt,name=seq,proto3" json:"seq,omitempty"`
Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1080,16 +1082,9 @@ func (*OtaPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{13}
}
func (x *OtaPayload) GetBlockId() uint32 {
func (x *OtaPayload) GetSeq() uint32 {
if x != nil {
return x.BlockId
}
return 0
}
func (x *OtaPayload) GetChunkId() uint32 {
if x != nil {
return x.ChunkId
return x.Seq
}
return 0
}
@@ -1101,9 +1096,9 @@ func (x *OtaPayload) GetData() []byte {
return nil
}
// Host → device: no more payload; device flushes buffer and finalizes OTA.
type OtaEndPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1138,16 +1133,14 @@ func (*OtaEndPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{14}
}
func (x *OtaEndPayload) GetStatus() uint32 {
if x != nil {
return x.Status
}
return 0
}
// Device → host status (also used as ACK after each 4 KiB written).
// status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed
type OtaStatusPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"`
BytesWritten uint32 `protobuf:"varint,2,opt,name=bytes_written,json=bytesWritten,proto3" json:"bytes_written,omitempty"`
TargetSlot uint32 `protobuf:"varint,3,opt,name=target_slot,json=targetSlot,proto3" json:"target_slot,omitempty"`
Error uint32 `protobuf:"varint,4,opt,name=error,proto3" json:"error,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1189,6 +1182,27 @@ func (x *OtaStatusPayload) GetStatus() uint32 {
return 0
}
func (x *OtaStatusPayload) GetBytesWritten() uint32 {
if x != nil {
return x.BytesWritten
}
return 0
}
func (x *OtaStatusPayload) GetTargetSlot() uint32 {
if x != nil {
return x.TargetSlot
}
return 0
}
func (x *OtaStatusPayload) GetError() uint32 {
if x != nil {
return x.Error
}
return 0
}
var File_uart_messages_proto protoreflect.FileDescriptor
const file_uart_messages_proto_rawDesc = "" +
@@ -1216,10 +1230,11 @@ const file_uart_messages_proto_rawDesc = "" +
"\apayload\"\x05\n" +
"\x03Ack\"!\n" +
"\vEchoPayload\x12\x12\n" +
"\x04data\x18\x01 \x01(\fR\x04data\"F\n" +
"\x04data\x18\x01 \x01(\fR\x04data\"s\n" +
"\x0fVersionResponse\x12\x18\n" +
"\aversion\x18\x01 \x01(\rR\aversion\x12\x19\n" +
"\bgit_hash\x18\x02 \x01(\tR\agitHash\"\xc3\x01\n" +
"\bgit_hash\x18\x02 \x01(\tR\agitHash\x12+\n" +
"\x11running_partition\x18\x03 \x01(\tR\x10runningPartition\"\xc3\x01\n" +
"\n" +
"ClientInfo\x12\x0e\n" +
"\x02id\x18\x01 \x01(\rR\x02id\x12\x1c\n" +
@@ -1254,21 +1269,21 @@ const file_uart_messages_proto_rawDesc = "" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" +
"\x19EspNowUnicastTestResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x10\n" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"O\n" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"0\n" +
"\x0fOtaStartPayload\x12\x1d\n" +
"\n" +
"total_size\x18\x01 \x01(\rR\ttotalSize\x12\x1d\n" +
"total_size\x18\x01 \x01(\rR\ttotalSize\"2\n" +
"\n" +
"block_size\x18\x02 \x01(\rR\tblockSize\"V\n" +
"\n" +
"OtaPayload\x12\x19\n" +
"\bblock_id\x18\x01 \x01(\rR\ablockId\x12\x19\n" +
"\bchunk_id\x18\x02 \x01(\rR\achunkId\x12\x12\n" +
"\x04data\x18\x03 \x01(\fR\x04data\"'\n" +
"\rOtaEndPayload\x12\x16\n" +
"\x06status\x18\x01 \x01(\rR\x06status\"*\n" +
"OtaPayload\x12\x10\n" +
"\x03seq\x18\x01 \x01(\rR\x03seq\x12\x12\n" +
"\x04data\x18\x02 \x01(\fR\x04data\"\x0f\n" +
"\rOtaEndPayload\"\x86\x01\n" +
"\x10OtaStatusPayload\x12\x16\n" +
"\x06status\x18\x01 \x01(\rR\x06status*\xdd\x01\n" +
"\x06status\x18\x01 \x01(\rR\x06status\x12#\n" +
"\rbytes_written\x18\x02 \x01(\rR\fbytesWritten\x12\x1f\n" +
"\vtarget_slot\x18\x03 \x01(\rR\n" +
"targetSlot\x12\x14\n" +
"\x05error\x18\x04 \x01(\rR\x05error*\xdd\x01\n" +
"\vMessageType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\a\n" +
"\x03ACK\x10\x01\x12\b\n" +