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:
parent
1ad527119d
commit
59ca269407
3
Makefile
3
Makefile
@ -19,7 +19,8 @@ default:
|
|||||||
@echo "Set PORT=$(PORT) (current) for goTool targets."
|
@echo "Set PORT=$(PORT) (current) for goTool targets."
|
||||||
|
|
||||||
proto_generate_uart:
|
proto_generate_uart:
|
||||||
python libs/nanopb/generator/nanopb_generator.py main/proto/uart_messages.proto
|
cd main/proto && python ../../libs/nanopb/generator/nanopb_generator.py \
|
||||||
|
-I ../../libs/nanopb/generator/proto uart_messages.proto
|
||||||
|
|
||||||
proto_generate_espnow:
|
proto_generate_espnow:
|
||||||
python libs/nanopb/generator/nanopb_generator.py main/proto/esp_now_messages.proto
|
python libs/nanopb/generator/nanopb_generator.py main/proto/esp_now_messages.proto
|
||||||
|
|||||||
@ -27,6 +27,7 @@ go run . -port /dev/ttyUSB0 clients
|
|||||||
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
|
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
|
||||||
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
|
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
|
||||||
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
|
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
|
||||||
|
| `ota` | 16–19 | UART firmware upload to inactive OTA slot (200 B chunks, 4 KiB flash blocks) |
|
||||||
|
|
||||||
`clients` requires slaves to have responded to master discover broadcasts first.
|
`clients` requires slaves to have responded to master discover broadcasts first.
|
||||||
|
|
||||||
@ -69,6 +70,12 @@ The dashboard can configure nodes using the same UART commands as the CLI:
|
|||||||
|
|
||||||
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test`.
|
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Waits for **ready** after start (~30 s erase), sends 200-byte `OTA_PAYLOAD` frames, reads **block_ack** every 4 KiB, then `OTA_END` and **success**.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
|
go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
|
||||||
```
|
```
|
||||||
|
|||||||
204
goTool/cmd_ota.go
Normal file
204
goTool/cmd_ota.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
uartframe "powerpod/gotool/uart"
|
||||||
|
"powerpod/gotool/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
otaHostChunkSize = 200
|
||||||
|
otaFlashBlockSize = 4096
|
||||||
|
otaPrepareTimeout = 120 * time.Second
|
||||||
|
otaDefaultTimeout = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
otaStPreparing = 1
|
||||||
|
otaStReady = 2
|
||||||
|
otaStBlockAck = 3
|
||||||
|
otaStSuccess = 4
|
||||||
|
otaStFailed = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
func runOTA(sp *serialPort, args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return fmt.Errorf("usage: ota <firmware.bin>")
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return fmt.Errorf("empty firmware file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sp.port.SetReadTimeout(readTimeout)
|
||||||
|
|
||||||
|
sp.mu.Lock()
|
||||||
|
defer sp.mu.Unlock()
|
||||||
|
|
||||||
|
fmt.Printf("OTA start: %d bytes firmware\n", len(data))
|
||||||
|
if err := writeUartMessageLocked(sp, &pb.UartMessage{
|
||||||
|
Type: pb.MessageType_OTA_START,
|
||||||
|
Payload: &pb.UartMessage_OtaStart{
|
||||||
|
OtaStart: &pb.OtaStartPayload{TotalSize: uint32(len(data))},
|
||||||
|
},
|
||||||
|
}, "OTA_START"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := waitOtaStatusLocked(sp, otaStReady, otaPrepareTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sp.port.SetReadTimeout(otaDefaultTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var seq uint32
|
||||||
|
blockNum := 0
|
||||||
|
for offset := 0; offset < len(data); {
|
||||||
|
bytesInBlock := 0
|
||||||
|
for bytesInBlock < otaFlashBlockSize && offset < len(data) {
|
||||||
|
n := otaHostChunkSize
|
||||||
|
room := otaFlashBlockSize - bytesInBlock
|
||||||
|
if n > room {
|
||||||
|
n = room
|
||||||
|
}
|
||||||
|
if offset+n > len(data) {
|
||||||
|
n = len(data) - offset
|
||||||
|
}
|
||||||
|
chunk := data[offset : offset+n]
|
||||||
|
|
||||||
|
if err := writeUartMessageLocked(sp, &pb.UartMessage{
|
||||||
|
Type: pb.MessageType_OTA_PAYLOAD,
|
||||||
|
Payload: &pb.UartMessage_OtaPayload{
|
||||||
|
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
|
||||||
|
},
|
||||||
|
}, "OTA_PAYLOAD"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
seq++
|
||||||
|
offset += n
|
||||||
|
bytesInBlock += n
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytesInBlock == otaFlashBlockSize {
|
||||||
|
blockNum++
|
||||||
|
st, err := waitOtaStatusLocked(sp, otaStBlockAck, otaDefaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf(" block %d ack (%d bytes in flash, %d%%)\n",
|
||||||
|
blockNum, st.GetBytesWritten(), offset*100/len(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeUartMessageLocked(sp, &pb.UartMessage{
|
||||||
|
Type: pb.MessageType_OTA_END,
|
||||||
|
Payload: &pb.UartMessage_OtaEnd{
|
||||||
|
OtaEnd: &pb.OtaEndPayload{},
|
||||||
|
},
|
||||||
|
}, "OTA_END"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
st, err := readOtaStatusLocked(sp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if st.GetStatus() != otaStSuccess {
|
||||||
|
return fmt.Errorf("OTA failed: status=%d error=%d written=%d",
|
||||||
|
st.GetStatus(), st.GetError(), st.GetBytesWritten())
|
||||||
|
}
|
||||||
|
fmt.Printf("OTA success: %d bytes written (slot %d) — reboot to boot new image\n",
|
||||||
|
st.GetBytesWritten(), st.GetTargetSlot())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeUartMessageLocked(sp *serialPort, msg *pb.UartMessage, cmdName string) error {
|
||||||
|
frame, err := encodeUartMessage(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !sp.quiet {
|
||||||
|
log.Printf("sending %s (%d frame bytes)", cmdName, len(frame))
|
||||||
|
}
|
||||||
|
_, err = sp.port.Write(frame)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeUartMessage(msg *pb.UartMessage) ([]byte, error) {
|
||||||
|
body, err := proto.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payload := append([]byte{byte(msg.Type)}, body...)
|
||||||
|
return uartframe.EncodeFrame(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUartPayload(payload []byte) (*pb.UartMessage, error) {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty response")
|
||||||
|
}
|
||||||
|
var msg pb.UartMessage
|
||||||
|
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
msg.Type = pb.MessageType(payload[0])
|
||||||
|
return &msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitOtaStatusLocked(sp *serialPort, want uint32, timeout time.Duration) (*pb.OtaStatusPayload, error) {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for {
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return nil, fmt.Errorf("timeout waiting for OTA status %d", want)
|
||||||
|
}
|
||||||
|
if err := sp.port.SetReadTimeout(time.Until(deadline)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
st, err := readOtaStatusLocked(sp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch st.GetStatus() {
|
||||||
|
case want:
|
||||||
|
if want == otaStReady {
|
||||||
|
fmt.Printf("OTA ready: inactive slot %d\n", st.GetTargetSlot())
|
||||||
|
}
|
||||||
|
return st, nil
|
||||||
|
case otaStPreparing:
|
||||||
|
fmt.Printf("OTA preparing partition (erase may take ~30s)…\n")
|
||||||
|
case otaStFailed:
|
||||||
|
return nil, fmt.Errorf("OTA failed (error=%d)", st.GetError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOtaStatusLocked(sp *serialPort) (*pb.OtaStatusPayload, error) {
|
||||||
|
payload, err := uartframe.ReadFrame(sp.port, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response: %w", err)
|
||||||
|
}
|
||||||
|
msg, err := decodeUartPayload(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if msg.GetType() != pb.MessageType_OTA_STATUS {
|
||||||
|
return nil, fmt.Errorf("unexpected response type %v", msg.GetType())
|
||||||
|
}
|
||||||
|
st := msg.GetOtaStatus()
|
||||||
|
if st == nil {
|
||||||
|
return nil, fmt.Errorf("missing ota_status")
|
||||||
|
}
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
@ -18,7 +18,8 @@ func usage() {
|
|||||||
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n")
|
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n")
|
||||||
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n")
|
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n")
|
||||||
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n")
|
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n")
|
||||||
fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n\n")
|
fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions)\n\n")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ func main() {
|
|||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
runErr = runServe(*portName, *baud, flag.Args()[1:])
|
runErr = runServe(*portName, *baud, flag.Args()[1:])
|
||||||
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "unicast-test", "unicast_test":
|
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "unicast-test", "unicast_test", "ota":
|
||||||
if *portName == "" {
|
if *portName == "" {
|
||||||
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
|
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
|
||||||
usage()
|
usage()
|
||||||
@ -65,6 +66,8 @@ func main() {
|
|||||||
runErr = runDeadzone(sp, flag.Args()[1:])
|
runErr = runDeadzone(sp, flag.Args()[1:])
|
||||||
case "unicast-test", "unicast_test":
|
case "unicast-test", "unicast_test":
|
||||||
runErr = runUnicastTest(sp, flag.Args()[1:])
|
runErr = runUnicastTest(sp, flag.Args()[1:])
|
||||||
|
case "ota":
|
||||||
|
runErr = runOTA(sp, flag.Args()[1:])
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
||||||
|
|||||||
@ -447,11 +447,13 @@ func (x *EchoPayload) GetData() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VersionResponse struct {
|
type VersionResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
|
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"`
|
GitHash string `protobuf:"bytes,2,opt,name=git_hash,json=gitHash,proto3" json:"git_hash,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// * Active OTA app partition label, e.g. "ota_0" or "ota_1".
|
||||||
sizeCache protoimpl.SizeCache
|
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() {
|
func (x *VersionResponse) Reset() {
|
||||||
@ -498,6 +500,13 @@ func (x *VersionResponse) GetGitHash() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *VersionResponse) GetRunningPartition() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.RunningPartition
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type ClientInfo struct {
|
type ClientInfo struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
@ -989,10 +998,10 @@ func (x *EspNowUnicastTestResponse) GetSeq() uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
|
||||||
type OtaStartPayload struct {
|
type OtaStartPayload struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
TotalSize uint32 `protobuf:"varint,1,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -1034,18 +1043,11 @@ func (x *OtaStartPayload) GetTotalSize() uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *OtaStartPayload) GetBlockSize() uint32 {
|
// Host → device: firmware chunk (up to 200 bytes); device buffers 4 KiB before flash write.
|
||||||
if x != nil {
|
|
||||||
return x.BlockSize
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type OtaPayload struct {
|
type OtaPayload struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
BlockId uint32 `protobuf:"varint,1,opt,name=block_id,json=blockId,proto3" json:"block_id,omitempty"`
|
Seq uint32 `protobuf:"varint,1,opt,name=seq,proto3" json:"seq,omitempty"`
|
||||||
ChunkId uint32 `protobuf:"varint,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"`
|
Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
|
||||||
Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -1080,16 +1082,9 @@ func (*OtaPayload) Descriptor() ([]byte, []int) {
|
|||||||
return file_uart_messages_proto_rawDescGZIP(), []int{13}
|
return file_uart_messages_proto_rawDescGZIP(), []int{13}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *OtaPayload) GetBlockId() uint32 {
|
func (x *OtaPayload) GetSeq() uint32 {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.BlockId
|
return x.Seq
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *OtaPayload) GetChunkId() uint32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.ChunkId
|
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -1101,9 +1096,9 @@ func (x *OtaPayload) GetData() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host → device: no more payload; device flushes buffer and finalizes OTA.
|
||||||
type OtaEndPayload struct {
|
type OtaEndPayload struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -1138,16 +1133,14 @@ func (*OtaEndPayload) Descriptor() ([]byte, []int) {
|
|||||||
return file_uart_messages_proto_rawDescGZIP(), []int{14}
|
return file_uart_messages_proto_rawDescGZIP(), []int{14}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *OtaEndPayload) GetStatus() uint32 {
|
// Device → host status (also used as ACK after each 4 KiB written).
|
||||||
if x != nil {
|
// status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed
|
||||||
return x.Status
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type OtaStatusPayload struct {
|
type OtaStatusPayload struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -1189,6 +1182,27 @@ func (x *OtaStatusPayload) GetStatus() uint32 {
|
|||||||
return 0
|
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
|
var File_uart_messages_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_uart_messages_proto_rawDesc = "" +
|
const file_uart_messages_proto_rawDesc = "" +
|
||||||
@ -1216,10 +1230,11 @@ const file_uart_messages_proto_rawDesc = "" +
|
|||||||
"\apayload\"\x05\n" +
|
"\apayload\"\x05\n" +
|
||||||
"\x03Ack\"!\n" +
|
"\x03Ack\"!\n" +
|
||||||
"\vEchoPayload\x12\x12\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" +
|
"\x0fVersionResponse\x12\x18\n" +
|
||||||
"\aversion\x18\x01 \x01(\rR\aversion\x12\x19\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" +
|
"\n" +
|
||||||
"ClientInfo\x12\x0e\n" +
|
"ClientInfo\x12\x0e\n" +
|
||||||
"\x02id\x18\x01 \x01(\rR\x02id\x12\x1c\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" +
|
"\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" +
|
||||||
"\x19EspNowUnicastTestResponse\x12\x18\n" +
|
"\x19EspNowUnicastTestResponse\x12\x18\n" +
|
||||||
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x10\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" +
|
"\x0fOtaStartPayload\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"total_size\x18\x01 \x01(\rR\ttotalSize\x12\x1d\n" +
|
"total_size\x18\x01 \x01(\rR\ttotalSize\"2\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"block_size\x18\x02 \x01(\rR\tblockSize\"V\n" +
|
"OtaPayload\x12\x10\n" +
|
||||||
"\n" +
|
"\x03seq\x18\x01 \x01(\rR\x03seq\x12\x12\n" +
|
||||||
"OtaPayload\x12\x19\n" +
|
"\x04data\x18\x02 \x01(\fR\x04data\"\x0f\n" +
|
||||||
"\bblock_id\x18\x01 \x01(\rR\ablockId\x12\x19\n" +
|
"\rOtaEndPayload\"\x86\x01\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" +
|
|
||||||
"\x10OtaStatusPayload\x12\x16\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" +
|
"\vMessageType\x12\v\n" +
|
||||||
"\aUNKNOWN\x10\x00\x12\a\n" +
|
"\aUNKNOWN\x10\x00\x12\a\n" +
|
||||||
"\x03ACK\x10\x01\x12\b\n" +
|
"\x03ACK\x10\x01\x12\b\n" +
|
||||||
|
|||||||
@ -19,6 +19,8 @@ idf_component_register(
|
|||||||
"cmd_client_info.c"
|
"cmd_client_info.c"
|
||||||
"cmd_accel_deadzone.c"
|
"cmd_accel_deadzone.c"
|
||||||
"cmd_espnow_unicast_test.c"
|
"cmd_espnow_unicast_test.c"
|
||||||
|
"cmd_ota.c"
|
||||||
|
"ota_uart.c"
|
||||||
"client_registry.c"
|
"client_registry.c"
|
||||||
"esp_now_comm.c"
|
"esp_now_comm.c"
|
||||||
"esp_now_proto.c"
|
"esp_now_proto.c"
|
||||||
|
|||||||
@ -184,7 +184,11 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 =
|
|||||||
| 4 | `CLIENT_INFO` | Implemented (`cmd_client_info.c`) — slave list from registry |
|
| 4 | `CLIENT_INFO` | Implemented (`cmd_client_info.c`) — slave list from registry |
|
||||||
| 5 | `CLIENT_INPUT` | Planned |
|
| 5 | `CLIENT_INPUT` | Planned |
|
||||||
| 6 | `ACCEL_DEADZONE` | Implemented (`cmd_accel_deadzone.c`) — get/set accel filter LSB |
|
| 6 | `ACCEL_DEADZONE` | Implemented (`cmd_accel_deadzone.c`) — get/set accel filter LSB |
|
||||||
| 16–20 | OTA / ESP-NOW OTA | Planned |
|
| 16 | `OTA_START` | Implemented (`cmd_ota.c`) — begin UART OTA on inactive slot |
|
||||||
|
| 17 | `OTA_PAYLOAD` | Implemented — up to 200 B per frame; device buffers 4 KiB |
|
||||||
|
| 18 | `OTA_END` | Implemented — flush, `esp_ota_end`, set boot partition |
|
||||||
|
| 19 | `OTA_STATUS` | Device → host (prepare/ready/block ACK/success/failed) |
|
||||||
|
| 20 | `OTA_START_ESPNOW` | Planned |
|
||||||
|
|
||||||
Regenerate C code:
|
Regenerate C code:
|
||||||
|
|
||||||
@ -208,9 +212,32 @@ Build embeds `POWERPOD_GIT_HASH` via `git rev-parse` in `main/CMakeLists.txt`.
|
|||||||
- `type = VERSION`
|
- `type = VERSION`
|
||||||
- `version_response.version` — `POWERPOD_FW_VERSION`
|
- `version_response.version` — `POWERPOD_FW_VERSION`
|
||||||
- `version_response.git_hash` — build git hash string
|
- `version_response.git_hash` — build git hash string
|
||||||
|
- `version_response.running_partition` — active OTA label (`ota_0` / `ota_1`)
|
||||||
|
|
||||||
Encoding: `uart_send_uart_message()` in `uart_proto.c`.
|
Encoding: `uart_send_uart_message()` in `uart_proto.c`.
|
||||||
|
|
||||||
|
At boot, firmware logs the running partition and OTA slot index (A/B).
|
||||||
|
|
||||||
|
### UART OTA (A/B)
|
||||||
|
|
||||||
|
Master only. Inactive app partition is selected with `esp_ota_get_next_update_partition()`; `esp_ota_begin` erases it (can take ~30 s — host should wait).
|
||||||
|
|
||||||
|
| Step | Host → device | Device → host |
|
||||||
|
|------|----------------|---------------|
|
||||||
|
| 1 | `OTA_START` + `total_size` | `OTA_STATUS` preparing, then **ready** (+ `target_slot` 0/1) |
|
||||||
|
| 2 | `OTA_PAYLOAD` chunks (**≤200 B**, `seq` optional) | `OTA_STATUS` **block_ack** only after each **4096 B** written to flash |
|
||||||
|
| 3 | `OTA_END` | `OTA_STATUS` **success** or **failed** (+ `bytes_written`) |
|
||||||
|
|
||||||
|
Implementation: `ota_uart.c` (4 KiB buffer, `esp_ota_write`), `cmd_ota.c`.
|
||||||
|
|
||||||
|
Host upload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
`OtaStatusPayload.status`: `1` preparing, `2` ready, `3` block_ack, `4` success, `5` failed.
|
||||||
|
|
||||||
### ACCEL_DEADZONE command
|
### ACCEL_DEADZONE command
|
||||||
|
|
||||||
Sets the **software** deadzone used by `bosch456.c` when logging accel (see [BMA456 accelerometer](#bma456-accelerometer-bosch456c)). Default **100** LSB.
|
Sets the **software** deadzone used by `bosch456.c` when logging accel (see [BMA456 accelerometer](#bma456-accelerometer-bosch456c)). Default **100** LSB.
|
||||||
|
|||||||
@ -12,4 +12,6 @@ typedef struct {
|
|||||||
char running_partition[APP_RUNNING_PARTITION_LABEL_MAX];
|
char running_partition[APP_RUNNING_PARTITION_LABEL_MAX];
|
||||||
} app_config_t;
|
} app_config_t;
|
||||||
|
|
||||||
|
const app_config_t *app_config_get(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -47,7 +47,7 @@ static const char *message_type_name(uint16_t id) {
|
|||||||
|
|
||||||
void init_cmdHandler(QueueHandle_t queue) {
|
void init_cmdHandler(QueueHandle_t queue) {
|
||||||
cmd_queue = queue;
|
cmd_queue = queue;
|
||||||
if (xTaskCreate(vCmdDispatcherTask, "cmd_dispatch", 4096, NULL, 5, NULL) !=
|
if (xTaskCreate(vCmdDispatcherTask, "cmd_dispatch", 8192, NULL, 5, NULL) !=
|
||||||
pdPASS) {
|
pdPASS) {
|
||||||
ESP_LOGE(TAG, "failed to create cmd_dispatch task");
|
ESP_LOGE(TAG, "failed to create cmd_dispatch task");
|
||||||
}
|
}
|
||||||
|
|||||||
163
main/cmd_ota.c
Normal file
163
main/cmd_ota.c
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
#include "cmd_ota.h"
|
||||||
|
#include "ota_uart.h"
|
||||||
|
#include "uart_cmd.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/idf_additions.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = "[OTA_CMD]";
|
||||||
|
|
||||||
|
#define OTA_PREPARE_STACK 8192
|
||||||
|
#define OTA_PREPARE_PRIO 5
|
||||||
|
|
||||||
|
static void send_ota_status(ota_uart_status_t status, uint32_t err_code) {
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS,
|
||||||
|
alox_UartMessage_ota_status_tag);
|
||||||
|
response.payload.ota_status.status = (uint32_t)status;
|
||||||
|
response.payload.ota_status.bytes_written = ota_uart_bytes_written();
|
||||||
|
int slot = ota_uart_target_slot();
|
||||||
|
response.payload.ota_status.target_slot =
|
||||||
|
slot >= 0 ? (uint32_t)slot : 0;
|
||||||
|
response.payload.ota_status.error = err_code;
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ota_prepare_task(void *param) {
|
||||||
|
uint32_t total_size = (uint32_t)(uintptr_t)param;
|
||||||
|
|
||||||
|
send_ota_status(OTA_UART_ST_PREPARING, 0);
|
||||||
|
|
||||||
|
int slot = ota_uart_prepare(total_size);
|
||||||
|
if (slot < 0) {
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 1);
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS,
|
||||||
|
alox_UartMessage_ota_status_tag);
|
||||||
|
response.payload.ota_status.status = (uint32_t)OTA_UART_ST_READY;
|
||||||
|
response.payload.ota_status.bytes_written = 0;
|
||||||
|
response.payload.ota_status.target_slot = (uint32_t)slot;
|
||||||
|
response.payload.ota_status.error = 0;
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_ota_start(const uint8_t *data, size_t len) {
|
||||||
|
alox_UartMessage uart_msg;
|
||||||
|
alox_OtaStartPayload req = alox_OtaStartPayload_init_zero;
|
||||||
|
|
||||||
|
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alox_OtaStartPayload *req_ptr =
|
||||||
|
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_start_tag, ota_start);
|
||||||
|
if (req_ptr != NULL) {
|
||||||
|
req = *req_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.total_size == 0) {
|
||||||
|
ESP_LOGW(TAG, "OTA_START: total_size required");
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ota_uart_is_active()) {
|
||||||
|
ESP_LOGW(TAG, "OTA_START while session active");
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 4);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xTaskCreate(ota_prepare_task, "ota_prepare", OTA_PREPARE_STACK,
|
||||||
|
(void *)(uintptr_t)req.total_size, OTA_PREPARE_PRIO,
|
||||||
|
NULL) != pdPASS) {
|
||||||
|
ESP_LOGE(TAG, "failed to create ota_prepare task");
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_ota_payload(const uint8_t *data, size_t len) {
|
||||||
|
alox_UartMessage uart_msg;
|
||||||
|
|
||||||
|
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "OTA_PAYLOAD decode failed");
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alox_OtaPayload *req_ptr =
|
||||||
|
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_payload_tag, ota_payload);
|
||||||
|
if (req_ptr == NULL) {
|
||||||
|
ESP_LOGW(TAG, "OTA_PAYLOAD: missing ota_payload (which=%u)",
|
||||||
|
(unsigned)uart_msg.which_payload);
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 11);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req_ptr->data.size == 0) {
|
||||||
|
ESP_LOGW(TAG, "OTA_PAYLOAD: empty data (seq=%lu)",
|
||||||
|
(unsigned long)req_ptr->seq);
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 11);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ota_uart_is_active()) {
|
||||||
|
ESP_LOGW(TAG, "OTA_PAYLOAD without active session (seq=%lu)",
|
||||||
|
(unsigned long)req_ptr->seq);
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 12);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ota_feed_result_t r =
|
||||||
|
ota_uart_feed(req_ptr->data.bytes, req_ptr->data.size);
|
||||||
|
if (r == OTA_FEED_ERROR) {
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 13);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r == OTA_FEED_BLOCK_WRITTEN) {
|
||||||
|
ESP_LOGI(TAG, "OTA block ack (%lu bytes in flash)",
|
||||||
|
(unsigned long)ota_uart_bytes_written());
|
||||||
|
send_ota_status(OTA_UART_ST_BLOCK_ACK, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_ota_end(const uint8_t *data, size_t len) {
|
||||||
|
(void)data;
|
||||||
|
(void)len;
|
||||||
|
|
||||||
|
if (!ota_uart_is_active()) {
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, 20);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t written = ota_uart_bytes_written();
|
||||||
|
int slot = ota_uart_target_slot();
|
||||||
|
bool success = false;
|
||||||
|
esp_err_t err = ota_uart_finish(&success);
|
||||||
|
if (err != ESP_OK || !success) {
|
||||||
|
send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS,
|
||||||
|
alox_UartMessage_ota_status_tag);
|
||||||
|
response.payload.ota_status.status = (uint32_t)OTA_UART_ST_SUCCESS;
|
||||||
|
response.payload.ota_status.bytes_written = written;
|
||||||
|
response.payload.ota_status.target_slot = slot >= 0 ? (uint32_t)slot : 0;
|
||||||
|
response.payload.ota_status.error = 0;
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_ota_register(void) {
|
||||||
|
uart_cmd_register(alox_MessageType_OTA_START, handle_ota_start);
|
||||||
|
uart_cmd_register(alox_MessageType_OTA_PAYLOAD, handle_ota_payload);
|
||||||
|
uart_cmd_register(alox_MessageType_OTA_END, handle_ota_end);
|
||||||
|
}
|
||||||
6
main/cmd_ota.h
Normal file
6
main/cmd_ota.h
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#ifndef CMD_OTA_H
|
||||||
|
#define CMD_OTA_H
|
||||||
|
|
||||||
|
void cmd_ota_register(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -1,4 +1,5 @@
|
|||||||
#include "cmd_version.h"
|
#include "cmd_version.h"
|
||||||
|
#include "app_config.h"
|
||||||
#include "uart_cmd.h"
|
#include "uart_cmd.h"
|
||||||
|
|
||||||
#ifndef POWERPOD_FW_VERSION
|
#ifndef POWERPOD_FW_VERSION
|
||||||
@ -21,6 +22,13 @@ static void handle_version(const uint8_t *data, size_t len) {
|
|||||||
response.payload.version_response.version = POWERPOD_FW_VERSION;
|
response.payload.version_response.version = POWERPOD_FW_VERSION;
|
||||||
response.payload.version_response.git_hash.funcs.encode = uart_cmd_encode_string;
|
response.payload.version_response.git_hash.funcs.encode = uart_cmd_encode_string;
|
||||||
response.payload.version_response.git_hash.arg = (void *)POWERPOD_GIT_HASH;
|
response.payload.version_response.git_hash.arg = (void *)POWERPOD_GIT_HASH;
|
||||||
|
const app_config_t *cfg = app_config_get();
|
||||||
|
if (cfg != NULL && cfg->running_partition[0] != '\0') {
|
||||||
|
response.payload.version_response.running_partition.funcs.encode =
|
||||||
|
uart_cmd_encode_string;
|
||||||
|
response.payload.version_response.running_partition.arg =
|
||||||
|
(void *)cfg->running_partition;
|
||||||
|
}
|
||||||
uart_cmd_send(&response, TAG);
|
uart_cmd_send(&response, TAG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
184
main/ota_uart.c
Normal file
184
main/ota_uart.c
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
#include "ota_uart.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = "[OTA_UART]";
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool active;
|
||||||
|
esp_ota_handle_t handle;
|
||||||
|
const esp_partition_t *update_partition;
|
||||||
|
uint32_t total_size;
|
||||||
|
uint32_t received;
|
||||||
|
uint32_t written;
|
||||||
|
int target_slot;
|
||||||
|
uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE];
|
||||||
|
size_t block_len;
|
||||||
|
} ota_uart_state_t;
|
||||||
|
|
||||||
|
static ota_uart_state_t s_ota;
|
||||||
|
|
||||||
|
static int partition_slot(const esp_partition_t *part) {
|
||||||
|
if (part == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (part->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (part->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ota_uart_is_active(void) { return s_ota.active; }
|
||||||
|
|
||||||
|
int ota_uart_target_slot(void) {
|
||||||
|
return s_ota.active ? s_ota.target_slot : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ota_uart_abort(void) {
|
||||||
|
if (!s_ota.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_ota_abort(s_ota.handle);
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t flush_block(void) {
|
||||||
|
if (s_ota.block_len == 0) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
esp_err_t err =
|
||||||
|
esp_ota_write(s_ota.handle, s_ota.block_buf, s_ota.block_len);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "esp_ota_write %u bytes failed: %s", (unsigned)s_ota.block_len,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
s_ota.written += (uint32_t)s_ota.block_len;
|
||||||
|
s_ota.block_len = 0;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ota_uart_prepare(uint32_t total_size) {
|
||||||
|
if (s_ota.active) {
|
||||||
|
ESP_LOGW(TAG, "OTA already active");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_partition_t *running = esp_ota_get_running_partition();
|
||||||
|
const esp_partition_t *update_partition =
|
||||||
|
esp_ota_get_next_update_partition(NULL);
|
||||||
|
if (update_partition == NULL) {
|
||||||
|
ESP_LOGE(TAG, "no OTA update partition");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "running=%s, update=%s, image_size=%lu",
|
||||||
|
running != NULL ? running->label : "?",
|
||||||
|
update_partition->label, (unsigned long)total_size);
|
||||||
|
|
||||||
|
if (total_size > 0 && total_size > update_partition->size) {
|
||||||
|
ESP_LOGE(TAG, "image too large (%lu > %lu)", (unsigned long)total_size,
|
||||||
|
(unsigned long)update_partition->size);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_ota_handle_t handle;
|
||||||
|
esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
s_ota.active = true;
|
||||||
|
s_ota.handle = handle;
|
||||||
|
s_ota.update_partition = update_partition;
|
||||||
|
s_ota.total_size = total_size;
|
||||||
|
s_ota.target_slot = partition_slot(update_partition);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "OTA prepared, target slot %d (%s) — send 4 KiB chunks",
|
||||||
|
s_ota.target_slot, update_partition->label);
|
||||||
|
return s_ota.target_slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len) {
|
||||||
|
if (!s_ota.active || data == NULL || len == 0) {
|
||||||
|
return OTA_FEED_ERROR;
|
||||||
|
}
|
||||||
|
if (len > OTA_UART_HOST_CHUNK_SIZE) {
|
||||||
|
ESP_LOGW(TAG, "chunk %u > %u, truncating", (unsigned)len,
|
||||||
|
OTA_UART_HOST_CHUNK_SIZE);
|
||||||
|
len = OTA_UART_HOST_CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool block_written = false;
|
||||||
|
size_t offset = 0;
|
||||||
|
while (offset < len) {
|
||||||
|
size_t space = OTA_UART_FLASH_BLOCK_SIZE - s_ota.block_len;
|
||||||
|
size_t n = len - offset;
|
||||||
|
if (n > space) {
|
||||||
|
n = space;
|
||||||
|
}
|
||||||
|
memcpy(s_ota.block_buf + s_ota.block_len, data + offset, n);
|
||||||
|
s_ota.block_len += n;
|
||||||
|
s_ota.received += (uint32_t)n;
|
||||||
|
offset += n;
|
||||||
|
|
||||||
|
if (s_ota.block_len < OTA_UART_FLASH_BLOCK_SIZE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flush_block() != ESP_OK) {
|
||||||
|
ota_uart_abort();
|
||||||
|
return OTA_FEED_ERROR;
|
||||||
|
}
|
||||||
|
block_written = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return block_written ? OTA_FEED_BLOCK_WRITTEN : OTA_FEED_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t ota_uart_bytes_written(void) { return s_ota.written; }
|
||||||
|
|
||||||
|
esp_err_t ota_uart_finish(bool *success_out) {
|
||||||
|
if (success_out != NULL) {
|
||||||
|
*success_out = false;
|
||||||
|
}
|
||||||
|
if (!s_ota.active) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = flush_block();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ota_uart_abort();
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_ota_end(s_ota.handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err));
|
||||||
|
ota_uart_abort();
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_ota_set_boot_partition(s_ota.update_partition);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err));
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "OTA complete: %lu bytes written to %s (slot %d), reboot to run",
|
||||||
|
(unsigned long)s_ota.written, s_ota.update_partition->label,
|
||||||
|
s_ota.target_slot);
|
||||||
|
|
||||||
|
if (success_out != NULL) {
|
||||||
|
*success_out = true;
|
||||||
|
}
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
45
main/ota_uart.h
Normal file
45
main/ota_uart.h
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#ifndef OTA_UART_H
|
||||||
|
#define OTA_UART_H
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define OTA_UART_HOST_CHUNK_SIZE 200u
|
||||||
|
#define OTA_UART_FLASH_BLOCK_SIZE 4096u
|
||||||
|
|
||||||
|
/** OtaStatusPayload.status values (device → host). */
|
||||||
|
typedef enum {
|
||||||
|
OTA_UART_ST_PREPARING = 1,
|
||||||
|
OTA_UART_ST_READY = 2,
|
||||||
|
OTA_UART_ST_BLOCK_ACK = 3,
|
||||||
|
OTA_UART_ST_SUCCESS = 4,
|
||||||
|
OTA_UART_ST_FAILED = 5,
|
||||||
|
} ota_uart_status_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
OTA_FEED_OK = 0,
|
||||||
|
OTA_FEED_BLOCK_WRITTEN,
|
||||||
|
OTA_FEED_ERROR,
|
||||||
|
} ota_feed_result_t;
|
||||||
|
|
||||||
|
bool ota_uart_is_active(void);
|
||||||
|
|
||||||
|
/** 0/1 while session active, else -1. */
|
||||||
|
int ota_uart_target_slot(void);
|
||||||
|
|
||||||
|
/** Begin OTA on the inactive app partition (esp_ota_begin). Returns target slot 0/1. */
|
||||||
|
int ota_uart_prepare(uint32_t total_size);
|
||||||
|
|
||||||
|
void ota_uart_abort(void);
|
||||||
|
|
||||||
|
/** Append up to 200 bytes; flushes 4 KiB blocks to flash when full. */
|
||||||
|
ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
uint32_t ota_uart_bytes_written(void);
|
||||||
|
|
||||||
|
/** Flush remainder, esp_ota_end, set boot partition on success. */
|
||||||
|
esp_err_t ota_uart_finish(bool *success_out);
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -4,6 +4,7 @@
|
|||||||
#include "cmd_espnow_unicast_test.h"
|
#include "cmd_espnow_unicast_test.h"
|
||||||
#include "cmd_client_info.h"
|
#include "cmd_client_info.h"
|
||||||
#include "cmd_version.h"
|
#include "cmd_version.h"
|
||||||
|
#include "cmd_ota.h"
|
||||||
#include "esp_now_comm.h"
|
#include "esp_now_comm.h"
|
||||||
#include "powerpod.h"
|
#include "powerpod.h"
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
@ -50,6 +51,8 @@ static i2c_master_dev_handle_t io_expander;
|
|||||||
|
|
||||||
static app_config_t app_config;
|
static app_config_t app_config;
|
||||||
|
|
||||||
|
const app_config_t *app_config_get(void) { return &app_config; }
|
||||||
|
|
||||||
static QueueHandle_t cmd_queue;
|
static QueueHandle_t cmd_queue;
|
||||||
|
|
||||||
uint8_t reverse_high_nibble_lut(uint8_t n) {
|
uint8_t reverse_high_nibble_lut(uint8_t n) {
|
||||||
@ -123,16 +126,29 @@ void app_main(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const esp_partition_t *running = esp_ota_get_running_partition();
|
const esp_partition_t *running = esp_ota_get_running_partition();
|
||||||
|
int ota_slot = -1;
|
||||||
|
if (running != NULL) {
|
||||||
|
if (running->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_0) {
|
||||||
|
ota_slot = 0;
|
||||||
|
} else if (running->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_1) {
|
||||||
|
ota_slot = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app_config.master = (master != 0);
|
app_config.master = (master != 0);
|
||||||
app_config.network = network;
|
app_config.network = network;
|
||||||
memcpy(app_config.running_partition, running->label,
|
if (running != NULL) {
|
||||||
sizeof(app_config.running_partition));
|
memcpy(app_config.running_partition, running->label,
|
||||||
|
sizeof(app_config.running_partition));
|
||||||
|
} else {
|
||||||
|
app_config.running_partition[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "RUNNING CONFIG:");
|
ESP_LOGI(TAG, "RUNNING CONFIG:");
|
||||||
ESP_LOGI(TAG, "Master: %d", app_config.master);
|
ESP_LOGI(TAG, "Master: %d", app_config.master);
|
||||||
ESP_LOGI(TAG, "Network: %d", app_config.network);
|
ESP_LOGI(TAG, "Network: %d", app_config.network);
|
||||||
ESP_LOGI(TAG, "Running Partition: %s", app_config.running_partition);
|
ESP_LOGI(TAG, "Running Partition: %s (OTA slot %d)",
|
||||||
|
app_config.running_partition, ota_slot);
|
||||||
|
|
||||||
err = esp_now_comm_init(&app_config);
|
err = esp_now_comm_init(&app_config);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@ -144,13 +160,14 @@ void app_main(void) {
|
|||||||
board_input_init();
|
board_input_init();
|
||||||
|
|
||||||
if (app_config.master) {
|
if (app_config.master) {
|
||||||
cmd_queue = xQueueCreate(10, sizeof(generic_msg_t));
|
cmd_queue = xQueueCreate(64, sizeof(generic_msg_t));
|
||||||
init_cmdHandler(cmd_queue);
|
init_cmdHandler(cmd_queue);
|
||||||
init_uart(cmd_queue);
|
init_uart(cmd_queue);
|
||||||
cmd_version_register();
|
cmd_version_register();
|
||||||
cmd_client_info_register();
|
cmd_client_info_register();
|
||||||
cmd_accel_deadzone_register();
|
cmd_accel_deadzone_register();
|
||||||
cmd_espnow_unicast_test_register();
|
cmd_espnow_unicast_test_register();
|
||||||
|
cmd_ota_register();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t current_digit = 10;
|
uint8_t current_digit = 10;
|
||||||
|
|||||||
1
main/proto/uart_messages.options
Normal file
1
main/proto/uart_messages.options
Normal file
@ -0,0 +1 @@
|
|||||||
|
OtaPayload.data max_size:200
|
||||||
@ -38,6 +38,8 @@ typedef struct _alox_EchoPayload {
|
|||||||
typedef struct _alox_VersionResponse {
|
typedef struct _alox_VersionResponse {
|
||||||
uint32_t version;
|
uint32_t version;
|
||||||
pb_callback_t git_hash;
|
pb_callback_t git_hash;
|
||||||
|
/* * Active OTA app partition label, e.g. "ota_0" or "ota_1". */
|
||||||
|
pb_callback_t running_partition;
|
||||||
} alox_VersionResponse;
|
} alox_VersionResponse;
|
||||||
|
|
||||||
typedef struct _alox_ClientInfo {
|
typedef struct _alox_ClientInfo {
|
||||||
@ -92,23 +94,30 @@ typedef struct _alox_EspNowUnicastTestResponse {
|
|||||||
uint32_t seq;
|
uint32_t seq;
|
||||||
} alox_EspNowUnicastTestResponse;
|
} alox_EspNowUnicastTestResponse;
|
||||||
|
|
||||||
|
/* Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS). */
|
||||||
typedef struct _alox_OtaStartPayload {
|
typedef struct _alox_OtaStartPayload {
|
||||||
uint32_t total_size;
|
uint32_t total_size;
|
||||||
uint32_t block_size;
|
|
||||||
} alox_OtaStartPayload;
|
} 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 {
|
typedef struct _alox_OtaPayload {
|
||||||
uint32_t block_id;
|
uint32_t seq;
|
||||||
uint32_t chunk_id;
|
alox_OtaPayload_data_t data;
|
||||||
pb_callback_t data;
|
|
||||||
} alox_OtaPayload;
|
} alox_OtaPayload;
|
||||||
|
|
||||||
|
/* Host → device: no more payload; device flushes buffer and finalizes OTA. */
|
||||||
typedef struct _alox_OtaEndPayload {
|
typedef struct _alox_OtaEndPayload {
|
||||||
uint32_t status;
|
char dummy_field;
|
||||||
} alox_OtaEndPayload;
|
} 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 {
|
typedef struct _alox_OtaStatusPayload {
|
||||||
uint32_t status;
|
uint32_t status;
|
||||||
|
uint32_t bytes_written;
|
||||||
|
uint32_t target_slot;
|
||||||
|
uint32_t error;
|
||||||
} alox_OtaStatusPayload;
|
} alox_OtaStatusPayload;
|
||||||
|
|
||||||
typedef struct _alox_UartMessage {
|
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_UartMessage_init_default {_alox_MessageType_MIN, 0, {alox_Ack_init_default}}
|
||||||
#define alox_Ack_init_default {0}
|
#define alox_Ack_init_default {0}
|
||||||
#define alox_EchoPayload_init_default {{{NULL}, NULL}}
|
#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_ClientInfo_init_default {0, 0, 0, {{NULL}, NULL}, 0, 0, 0}
|
||||||
#define alox_ClientInfoResponse_init_default {{{NULL}, NULL}}
|
#define alox_ClientInfoResponse_init_default {{{NULL}, NULL}}
|
||||||
#define alox_ClientInput_init_default {0, 0, 0, 0}
|
#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_AccelDeadzoneResponse_init_default {0, 0, 0, 0}
|
||||||
#define alox_EspNowUnicastTestRequest_init_default {0, 0}
|
#define alox_EspNowUnicastTestRequest_init_default {0, 0}
|
||||||
#define alox_EspNowUnicastTestResponse_init_default {0, 0}
|
#define alox_EspNowUnicastTestResponse_init_default {0, 0}
|
||||||
#define alox_OtaStartPayload_init_default {0, 0}
|
#define alox_OtaStartPayload_init_default {0}
|
||||||
#define alox_OtaPayload_init_default {0, 0, {{NULL}, NULL}}
|
#define alox_OtaPayload_init_default {0, {0, {0}}}
|
||||||
#define alox_OtaEndPayload_init_default {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_UartMessage_init_zero {_alox_MessageType_MIN, 0, {alox_Ack_init_zero}}
|
||||||
#define alox_Ack_init_zero {0}
|
#define alox_Ack_init_zero {0}
|
||||||
#define alox_EchoPayload_init_zero {{{NULL}, NULL}}
|
#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_ClientInfo_init_zero {0, 0, 0, {{NULL}, NULL}, 0, 0, 0}
|
||||||
#define alox_ClientInfoResponse_init_zero {{{NULL}, NULL}}
|
#define alox_ClientInfoResponse_init_zero {{{NULL}, NULL}}
|
||||||
#define alox_ClientInput_init_zero {0, 0, 0, 0}
|
#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_AccelDeadzoneResponse_init_zero {0, 0, 0, 0}
|
||||||
#define alox_EspNowUnicastTestRequest_init_zero {0, 0}
|
#define alox_EspNowUnicastTestRequest_init_zero {0, 0}
|
||||||
#define alox_EspNowUnicastTestResponse_init_zero {0, 0}
|
#define alox_EspNowUnicastTestResponse_init_zero {0, 0}
|
||||||
#define alox_OtaStartPayload_init_zero {0, 0}
|
#define alox_OtaStartPayload_init_zero {0}
|
||||||
#define alox_OtaPayload_init_zero {0, 0, {{NULL}, NULL}}
|
#define alox_OtaPayload_init_zero {0, {0, {0}}}
|
||||||
#define alox_OtaEndPayload_init_zero {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) */
|
/* Field tags (for use in manual encoding/decoding) */
|
||||||
#define alox_EchoPayload_data_tag 1
|
#define alox_EchoPayload_data_tag 1
|
||||||
#define alox_VersionResponse_version_tag 1
|
#define alox_VersionResponse_version_tag 1
|
||||||
#define alox_VersionResponse_git_hash_tag 2
|
#define alox_VersionResponse_git_hash_tag 2
|
||||||
|
#define alox_VersionResponse_running_partition_tag 3
|
||||||
#define alox_ClientInfo_id_tag 1
|
#define alox_ClientInfo_id_tag 1
|
||||||
#define alox_ClientInfo_available_tag 2
|
#define alox_ClientInfo_available_tag 2
|
||||||
#define alox_ClientInfo_used_tag 3
|
#define alox_ClientInfo_used_tag 3
|
||||||
@ -223,12 +233,12 @@ extern "C" {
|
|||||||
#define alox_EspNowUnicastTestResponse_success_tag 1
|
#define alox_EspNowUnicastTestResponse_success_tag 1
|
||||||
#define alox_EspNowUnicastTestResponse_seq_tag 2
|
#define alox_EspNowUnicastTestResponse_seq_tag 2
|
||||||
#define alox_OtaStartPayload_total_size_tag 1
|
#define alox_OtaStartPayload_total_size_tag 1
|
||||||
#define alox_OtaStartPayload_block_size_tag 2
|
#define alox_OtaPayload_seq_tag 1
|
||||||
#define alox_OtaPayload_block_id_tag 1
|
#define alox_OtaPayload_data_tag 2
|
||||||
#define alox_OtaPayload_chunk_id_tag 2
|
|
||||||
#define alox_OtaPayload_data_tag 3
|
|
||||||
#define alox_OtaEndPayload_status_tag 1
|
|
||||||
#define alox_OtaStatusPayload_status_tag 1
|
#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_type_tag 1
|
||||||
#define alox_UartMessage_ack_payload_tag 2
|
#define alox_UartMessage_ack_payload_tag 2
|
||||||
#define alox_UartMessage_echo_payload_tag 3
|
#define alox_UartMessage_echo_payload_tag 3
|
||||||
@ -288,7 +298,8 @@ X(a, CALLBACK, SINGULAR, BYTES, data, 1)
|
|||||||
|
|
||||||
#define alox_VersionResponse_FIELDLIST(X, a) \
|
#define alox_VersionResponse_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, version, 1) \
|
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_CALLBACK pb_default_field_callback
|
||||||
#define alox_VersionResponse_DEFAULT NULL
|
#define alox_VersionResponse_DEFAULT NULL
|
||||||
|
|
||||||
@ -352,25 +363,26 @@ X(a, STATIC, SINGULAR, UINT32, seq, 2)
|
|||||||
#define alox_EspNowUnicastTestResponse_DEFAULT NULL
|
#define alox_EspNowUnicastTestResponse_DEFAULT NULL
|
||||||
|
|
||||||
#define alox_OtaStartPayload_FIELDLIST(X, a) \
|
#define alox_OtaStartPayload_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, total_size, 1) \
|
X(a, STATIC, SINGULAR, UINT32, total_size, 1)
|
||||||
X(a, STATIC, SINGULAR, UINT32, block_size, 2)
|
|
||||||
#define alox_OtaStartPayload_CALLBACK NULL
|
#define alox_OtaStartPayload_CALLBACK NULL
|
||||||
#define alox_OtaStartPayload_DEFAULT NULL
|
#define alox_OtaStartPayload_DEFAULT NULL
|
||||||
|
|
||||||
#define alox_OtaPayload_FIELDLIST(X, a) \
|
#define alox_OtaPayload_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, block_id, 1) \
|
X(a, STATIC, SINGULAR, UINT32, seq, 1) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, chunk_id, 2) \
|
X(a, STATIC, SINGULAR, BYTES, data, 2)
|
||||||
X(a, CALLBACK, SINGULAR, BYTES, data, 3)
|
#define alox_OtaPayload_CALLBACK NULL
|
||||||
#define alox_OtaPayload_CALLBACK pb_default_field_callback
|
|
||||||
#define alox_OtaPayload_DEFAULT NULL
|
#define alox_OtaPayload_DEFAULT NULL
|
||||||
|
|
||||||
#define alox_OtaEndPayload_FIELDLIST(X, a) \
|
#define alox_OtaEndPayload_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, status, 1)
|
|
||||||
#define alox_OtaEndPayload_CALLBACK NULL
|
#define alox_OtaEndPayload_CALLBACK NULL
|
||||||
#define alox_OtaEndPayload_DEFAULT NULL
|
#define alox_OtaEndPayload_DEFAULT NULL
|
||||||
|
|
||||||
#define alox_OtaStatusPayload_FIELDLIST(X, a) \
|
#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_CALLBACK NULL
|
||||||
#define alox_OtaStatusPayload_DEFAULT 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_ClientInfoResponse_size depends on runtime parameters */
|
||||||
/* alox_ClientInputResponse_size depends on runtime parameters */
|
/* alox_ClientInputResponse_size depends on runtime parameters */
|
||||||
/* alox_OtaPayload_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_AccelDeadzoneRequest_size 16
|
||||||
#define alox_AccelDeadzoneResponse_size 20
|
#define alox_AccelDeadzoneResponse_size 20
|
||||||
#define alox_Ack_size 0
|
#define alox_Ack_size 0
|
||||||
#define alox_ClientInput_size 22
|
#define alox_ClientInput_size 22
|
||||||
#define alox_EspNowUnicastTestRequest_size 12
|
#define alox_EspNowUnicastTestRequest_size 12
|
||||||
#define alox_EspNowUnicastTestResponse_size 8
|
#define alox_EspNowUnicastTestResponse_size 8
|
||||||
#define alox_OtaEndPayload_size 6
|
#define alox_OtaEndPayload_size 0
|
||||||
#define alox_OtaStartPayload_size 12
|
#define alox_OtaStartPayload_size 6
|
||||||
#define alox_OtaStatusPayload_size 6
|
#define alox_OtaStatusPayload_size 24
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
} /* extern "C" */
|
} /* extern "C" */
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
|
import "nanopb.proto";
|
||||||
|
|
||||||
package alox;
|
package alox;
|
||||||
|
|
||||||
enum MessageType {
|
enum MessageType {
|
||||||
@ -46,6 +48,8 @@ message EchoPayload {
|
|||||||
message VersionResponse {
|
message VersionResponse {
|
||||||
uint32 version = 1;
|
uint32 version = 1;
|
||||||
string git_hash = 2;
|
string git_hash = 2;
|
||||||
|
/** Active OTA app partition label, e.g. "ota_0" or "ota_1". */
|
||||||
|
string running_partition = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ClientInfo {
|
message ClientInfo {
|
||||||
@ -100,21 +104,25 @@ message EspNowUnicastTestResponse {
|
|||||||
uint32 seq = 2;
|
uint32 seq = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
|
||||||
message OtaStartPayload {
|
message OtaStartPayload {
|
||||||
uint32 total_size = 1;
|
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 {
|
message OtaPayload {
|
||||||
uint32 block_id = 1;
|
uint32 seq = 1;
|
||||||
uint32 chunk_id = 2;
|
bytes data = 2 [(nanopb).max_size = 200];
|
||||||
bytes data = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message OtaEndPayload {
|
// Host → device: no more payload; device flushes buffer and finalizes OTA.
|
||||||
uint32 status = 1;
|
message 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
|
||||||
message OtaStatusPayload {
|
message OtaStatusPayload {
|
||||||
uint32 status = 1;
|
uint32 status = 1;
|
||||||
|
uint32 bytes_written = 2;
|
||||||
|
uint32 target_slot = 3;
|
||||||
|
uint32 error = 4;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,9 +32,9 @@ static bool uart_enqueue_packet(const uart_packet_t *packet) {
|
|||||||
memcpy(msg.payload, &packet->payload[1], msg.len);
|
memcpy(msg.payload, &packet->payload[1], msg.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xQueueSend(uart_cmd_queue, &msg, 0) != pdPASS) {
|
if (xQueueSend(uart_cmd_queue, &msg, pdMS_TO_TICKS(500)) != pdPASS) {
|
||||||
free(msg.payload);
|
free(msg.payload);
|
||||||
ESP_LOGW(TAG, "command queue full");
|
ESP_LOGW(TAG, "command queue full (cmd 0x%02x)", (unsigned)msg.msg_id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -52,7 +52,7 @@ void init_uart(QueueHandle_t cmd_queue) {
|
|||||||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
err = uart_driver_install(UART_NUM, UART_BUF_SIZE * 2, 0, 0, NULL, 0);
|
err = uart_driver_install(UART_NUM, UART_BUF_SIZE * 2, UART_BUF_SIZE, 0, NULL, 0);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
||||||
return;
|
return;
|
||||||
@ -69,7 +69,7 @@ void init_uart(QueueHandle_t cmd_queue) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xTaskCreate(uart_read_task, "uart_rx", 4096, NULL, 1, NULL) != pdPASS) {
|
if (xTaskCreate(uart_read_task, "uart_rx", 4096, NULL, 5, NULL) != pdPASS) {
|
||||||
ESP_LOGE(TAG, "failed to create uart_read_task");
|
ESP_LOGE(TAG, "failed to create uart_read_task");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,10 +12,10 @@
|
|||||||
#define UART_TXD_PIN 3
|
#define UART_TXD_PIN 3
|
||||||
#define UART_RXD_PIN 2
|
#define UART_RXD_PIN 2
|
||||||
|
|
||||||
#define UART_BUF_SIZE 256
|
#define UART_BUF_SIZE 2048
|
||||||
#define START_MARKER 0xAA
|
#define START_MARKER 0xAA
|
||||||
#define STOP_MARKER 0xCC
|
#define STOP_MARKER 0xCC
|
||||||
#define MAX_BUF_SIZE 256
|
#define MAX_BUF_SIZE 252
|
||||||
#define MAX_PAYLOAD_SIZE \
|
#define MAX_PAYLOAD_SIZE \
|
||||||
MAX_BUF_SIZE - 4 // Buffer overhead, Start, Len, CRC, End
|
MAX_BUF_SIZE - 4 // Buffer overhead, Start, Len, CRC, End
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user