Skip to content

Flow: OTA Firmware Update

Overview

The OTA update system supports both app-initiated updates (user checks for updates) and remote push updates (support sends firmware directly via MQTT). The mower has a production-grade OTA pipeline with resume-capable downloads, MD5 verification, atomic replacement, and automatic rollback.

App-Initiated Update Flow

sequenceDiagram
    participant App
    participant Server as Local Server
    participant MQTT as MQTT Broker
    participant Mower
    participant Charger
    participant Cloud as Alibaba OSS

    rect rgb(240, 248, 255)
        Note over App,Cloud: Phase 1: Version Check
        App->>MQTT: ota_version_info
        MQTT->>Mower: ota_version_info
        Mower-->>MQTT: ota_version_info_respond
        MQTT-->>App: {mower: "v6.0.2", charger: "v0.4.0", mcu: "v3.6.6"}

        App->>Server: GET checkOtaNewVersion?version=v6.0.2&equipmentType=LFIN2
        Server-->>App: {upgradeFlag: 1, version, downloadUrl, md5}
    end

    rect rgb(255, 248, 240)
        Note over App,Cloud: Phase 2: Mower OTA
        Note over App: "Are you sure to upgrade?<br/>Expected to take 20-30 minutes"

        App->>MQTT: ota_upgrade_cmd {cmd, type, content, url, version, md5}
        Note over MQTT: Broker removes tz field,<br/>forces type:"full", re-encrypts
        MQTT->>Mower: ota_upgrade_cmd

        Note over Mower: mqtt_node forwards to<br/>ota_client_node via ROS 2<br/>/ota_upgrade_srv service

        Mower-->>MQTT: ota_upgrade_cmd_respond

        Note over Mower: Wait for CHARGING state

        Mower->>Cloud: HTTPS GET firmware .deb (libcurl, resume-capable)

        loop Download progress
            Mower->>MQTT: ota_upgrade_state {progress: 45, state: "downloading"}
            MQTT->>App: ota_upgrade_state
        end

        Note over Mower: MD5 verification
        Note over Mower: dpkg -x → /root/novabot.new/
        Note over Mower: Write "1" to upgrade.txt
        Note over Mower: reboot -f

        Note over Mower: run_ota.sh:<br/>backup → deploy → restore maps → verify → reboot
        Mower->>MQTT: ota_upgrade_state {state: "completed"}
    end

    rect rgb(240, 255, 240)
        Note over App,Cloud: Phase 3: Charger OTA (via mower)
        Note over App: "The charging station can also<br/>be upgraded. Would you like to proceed?"

        Mower->>Charger: TCP/HTTP push firmware to 192.168.4.1
        Charger->>Charger: esp_https_ota → write to inactive OTA partition
        Charger->>Charger: Switch boot partition + reboot
    end

Remote Push (Support-Initiated)

Support can push firmware to a specific device without user interaction via two mechanisms:

sequenceDiagram
    participant Support
    participant CloudBroker as Cloud MQTT Broker
    participant Mower

    rect rgb(255, 240, 240)
        Note over Support,Mower: Method 1: Direct MQTT Push
        Support->>CloudBroker: Publish to Dart/Send_mqtt/<SN>
        CloudBroker->>Mower: ota_upgrade_cmd {type, content}
        Note over Mower: Same flow as app-initiated
    end

    rect rgb(240, 240, 255)
        Note over Support,Mower: Method 2: Cloud Database Override
        Note over Support: Set per-SN firmware version in cloud DB
        Note over Mower: Next time app checks checkOtaNewVersion<br/>→ gets new version → sends ota_upgrade_cmd
    end

No authentication on OTA commands

Any MQTT message on Dart/Send_mqtt/<SN> with ota_upgrade_cmd triggers a firmware download. There is no verification of the sender's identity.


ota_upgrade_cmd JSON Format

CRITICAL — Exact payload format (proven working March 2026)

The payload below is the only format that works. Any deviation (wrong field names, missing fields, extra fields like tz) will cause the update to fail silently.

Full firmware upgrade (CORRECT format)
{
  "ota_upgrade_cmd": {
    "cmd": "upgrade",
    "type": "full",
    "content": "app",
    "url": "http://<server>:<port>/api/dashboard/ota/firmware/<filename>.deb",
    "version": "v6.0.2-custom-16",
    "md5": "<md5-checksum>"
  }
}

Required Fields

Field Value Required Notes
cmd "upgrade" Yes Without this, mqtt_node ignores the command entirely
type "full" Yes "increment" does NOT trigger download
content "app" Yes Must be the string "app", NOT an object. Without this, mqtt_node ignores the command
url "http://..." Yes Must be http:// — mower does NOT support HTTPS for OTA downloads
version version string Yes Version of the firmware being installed
md5 MD5 hash Yes Mower verifies checksum before installing

NO tz field — Broker-level fix required

The Novabot app always includes tz:"Europe/Amsterdam" in ota_upgrade_cmd. When mqtt_node sees a tz field, it:

  1. Writes the timezone to /userdata/ota/novabot_timezone.txt
  2. Overwrites type with "increment" — breaking the full update!

The local server's MQTT broker (authorizePublish in broker.ts) intercepts app→mower messages, removes the tz field, forces type:"full", and re-encrypts. This broker fix must NEVER be removed.

Upgrade Types

Type Description Notes
full Full firmware replacement (.deb package) Only type that works for custom firmware
increment Incremental app update mqtt_node sets this when tz field present — causes silent failure
file_update Individual file updates (.zip with manifest) Requires check.json manifest inside ZIP

| system | Full system upgrade via apt | Runs sudo apt full-upgrade && reboot -f |

system upgrade type

The system type runs sudo apt-get update && sudo apt full-upgrade && sleep 2 && reboot -f. If the mower's apt sources were pointed to a malicious repository, this would allow arbitrary code execution.


Mower OTA Architecture

The mower has two ROS 2 nodes that handle OTA:

graph LR
    MQTT[MQTT Broker] -->|ota_upgrade_cmd| MN[mqtt_node<br/>6.3MB]
    MN -->|/ota_upgrade_srv<br/>ROS 2 service| OC[ota_client_node<br/>5.8MB]
    OC -->|libcurl HTTPS| OSS[Alibaba OSS]
    OC -->|/ota/upgrade_status<br/>ROS 2 topic| MN
    MN -->|ota_upgrade_state| MQTT
    OC -->|Check| CS[/pipe_charge_status<br/>Charging required]

    style MN fill:#f9f,stroke:#333
    style OC fill:#9ff,stroke:#333

ROS 2 Service: OtaUpgradeSys

# Request (mqtt_node → ota_client)
string ota_cmd          # Full JSON string from MQTT command

---

# Response (ota_client → mqtt_node)
bool state              # true = accepted, false = error
string state_out        # Status message

OTA Client Launch Parameters

upgrade_srv_name: /ota_upgrade_srv
ota_state_pub: /ota/upgrade_status
charging_state_sub: /pipe_charge_status
ota_data_dir_root: /userdata/ota
download_max_time: 86400          # 24 hours max download time
upgrade_app_subs_maxt: 600        # 10 minutes max per submodule
upgrade_file_enable: True

Full Firmware Installation Flow

Phase 1: Download (ota_client_node)

1. Receive ota_upgrade_cmd via /ota_upgrade_srv ROS 2 service
2. Parse JSON: extract type, content.upgradeApp.{version, downloadUrl, md5}
3. Wait for mower to be in CHARGING state
   └─ If not charging: pause download, wait, retry every 60s
4. Download .deb to /userdata/ota/upgrade_pkg/
   └─ libcurl with HTTPS, resume-capable, max 24h timeout
5. Verify MD5 checksum
   └─ If mismatch: delete package, retry download
6. Extract: dpkg -x <package.deb> /root/novabot.new/
7. Verify extracted files via /root/novabot.new/package_verify.json
8. Copy charger firmware to /userdata/ota/charging_station_pkg/
9. Start submodule upgrades (charger OTA, max 10 min)
10. Copy run_ota.sh to /userdata/ota/run_ota.sh
11. Write "1" to /userdata/ota/upgrade.txt
12. reboot -f

Phase 2: Install (run_ota.sh at boot)

1. Check /root/novabot.new exists AND /userdata/ota/upgrade.txt == "1"
2. Backup: mv /root/novabot → /root/novabot.bak
3. Deploy: cp -rfp /root/novabot.new → /root/novabot
4. Restore user data:
   └─ Maps, charging_station config, CSV files from backup
5. Run start_service.sh (install system libraries, services)
6. Optionally replace WiFi driver (bcm/)
7. Verify: /root/novabot/scripts/run_novabot.sh exists and non-empty
   └─ If missing: ROLLBACK (cp /root/novabot.bak → /root/novabot)
8. Write "0" to /userdata/ota/upgrade.txt
9. Clean up: rm -rf /root/novabot.new
10. Final reboot

File Update Flow (type: "file_update")

For updating individual files without full firmware replacement:

1. Download .zip package to /userdata/ota/download/
2. Verify MD5
3. Extract to /userdata/ota/download/unzip/
4. Parse check.json manifest:
   {
     "upgradeFiles": [
       {"filePath": "source/in/zip", "destPath": "/target/path", "checkWay": "md5"}
     ]
   }
5. For each file: verify, then replace at destPath
6. Report status to cloud

Charger OTA

Via Mower (Relay)

The mower pushes charger firmware to the charger via two methods:

Method Target Protocol
TCP socket 192.168.4.1 Raw TCP, firmware in chunks
HTTP POST http://192.168.4.1/setotadata HTTP multipart

Firmware source: /userdata/ota/charging_station_pkg/lfi-charging-station_lora.bin

Direct (Charger Own OTA)

The charger can also handle ota_upgrade_cmd directly:

  1. Receives command via MQTT (handled locally, no LoRa relay)
  2. Downloads firmware via esp_https_ota() (ESP-IDF library)
  3. Writes to inactive OTA partition
  4. Updates otadata to switch boot partition
  5. Reboots
graph LR
    OTAData[otadata<br/>ota_seq = 7] -->|"(7-1) % 2 = 0"| OTA0[ota_0: v0.3.6<br/>ACTIVE]
    OTAData -.-> OTA1[ota_1: v0.4.0<br/>inactive]

    style OTA0 fill:#9f9,stroke:#333
    style OTA1 fill:#ddd,stroke:#333

OTA Download URLs

Device URL Pattern Example
Charger https://novabot-oss.oss-accelerate.aliyuncs.com/novabot-file/lfi-charging-station_lora-{timestamp}.bin ...-1709264279437.bin
Mower https://novabot-oss.oss-us-east-1.aliyuncs.com/novabot-file/lfimvp-{YYYYMMDD}{ver}-{timestamp}.deb ...-20240915571-1726376551929.deb

URL contains unpredictable timestamp

The download URL includes a millisecond timestamp, making it impossible to guess. The URL must be obtained from the cloud OTA API or the ota_upgrade_cmd message.

Known Firmware Versions

Device Version Size Notes
Charger v0.3.6 1.4 MB ESP32-S3, ESP-IDF v4.4.2, plain JSON MQTT
Charger v0.4.0 1.4 MB Adds AES-128-CBC MQTT encryption + cJSON_IsNull command validation
Mower v5.7.1 35 MB Debian/ROS 2, Horizon X3
Mower v6.0.2 ~35 MB Latest known stock firmware
Mower v6.0.2-custom-16 ~35 MB Custom: SSH, mDNS, camera stream, extended commands, PIN fix
MCU v3.5.8 444 KB STM32F407 motor controller (stock)
MCU v3.6.6 444 KB STM32F407 custom: PIN lock bypass + verify status=0 (ROS2 compat)

File System Paths

Path Description
/userdata/ota/upgrade.txt Flag: "0"=no update, "1"=pending
/userdata/ota/upgrade_pkg/ Downloaded .deb packages
/userdata/ota/download/ Downloaded .zip packages (file_update)
/userdata/ota/run_ota.sh OTA startup script (updated during OTA)
/userdata/ota/novabot_timezone.txt Timezone from OTA command
/userdata/ota/charging_station_pkg/ Charger firmware for relay
/userdata/ota/ota_client.log OTA operation log
/userdata/lfi/system_version.txt Current system version
/root/novabot/ Active firmware installation
/root/novabot.new/ Extracted new firmware (pre-reboot)
/root/novabot.bak/ Backup of previous firmware (rollback)

OTA Client Version History

Version Date Developer Changes
Pre-V0.0.1 2022 Li Qiang Initial: download, extract, replace
V0.0.1 2023/04 Cai Tao Weak network download, ROS service API, breakpoint resume
V0.0.2 2023/05 Cai Tao Charge-state pause/resume, progress reporting
V0.0.3 2023/10 Cai Tao MD5 verification, file size verification
V0.0.4 2024/02 Cai Tao File update feature (file_update type)

Firmware Patching (Local Server)

A patch tool (research/patch_firmware.js) allows modifying hardcoded MQTT hostnames/IPs in the charger firmware binary so the charger connects to the local server without DNS rewrites.

Patched Firmware Available

Version File MD5 Changes
v0.3.6 research/firmware/charger_v0.3.6_patched.bin fb7427789bf0e164ed00ef9ea8f9dbf0 MQTT → novabot.example.com
v0.4.0 research/firmware/charger_v0.4.0_patched.bin 538f01c8412a7d9936d1de9c298f8918 MQTT → novabot.example.com

Patch Tool Usage

node research/patch_firmware.js                          # Patch with defaults
node research/patch_firmware.js --analyze                # Analyze only
node research/patch_firmware.js --mqtt-host 192.168.1.50 # Short IP (in-place)
node research/patch_firmware.js --mqtt-host my.server.nl # Long hostname (relocation)

The tool handles:

  • ESP32-S3 image header parsing
  • String relocation to unused DROM space when replacement is longer
  • Code reference updates (literal pool pointers)
  • SHA256 hash recalculation

OTA Flash via MQTT

To flash a patched firmware to a charger:

ota_upgrade_cmd
{
  "ota_upgrade_cmd": {
    "type": "full",
    "content": {
      "upgradeApp": {
        "version": "v0.3.6-local",
        "downloadUrl": "http://<server>:8080/charger_v0.3.6_patched.bin",
        "md5": "fb7427789bf0e164ed00ef9ea8f9dbf0"
      }
    }
  }
}

v0.4.0 encrypted charger

For a charger running v0.4.0, commands must be AES-128-CBC encrypted and use null values: {"ota_version_info": null}. Use the raw-tcp endpoint: POST /api/dashboard/raw-tcp/:sn

Important notes

  • esp_https_ota() may require HTTPS — set up an HTTPS server if HTTP fails
  • NVS config (set via BLE) is NOT affected by OTA — production MQTT host also needs BLE re-provisioning
  • On OTA failure: charger automatically boots from the other OTA partition
  • Recovery via UART: press b to manually switch OTA partition
  • Always test on a spare board first!

Security Considerations

Issue Risk Details
No authentication on MQTT OTA Critical Any message on Dart/Send_mqtt/<SN> triggers update
No code signing High Only MD5 integrity check, no digital signatures
Download URL not validated High Can point to any server
system upgrade type Critical Runs apt full-upgrade — malicious apt repo = RCE
Charger OTA via HTTP Medium HTTP POST to 192.168.4.1, no TLS
No rollback verification Low Rollback only checks if run_novabot.sh exists