Custom Firmware¶
Custom firmware is OPTIONAL
Custom firmware is OPTIONAL for basic OpenNova setup. Stock Novabot firmware works with BLE provisioning (direct IP). Custom firmware adds: mDNS discovery, SSH access, camera streaming.
Feasibility Assessment¶
Can we build custom firmware for the Novabot charger and mower without the original source code? The answer depends on the device.
Charger (ESP32-S3) --- Fully Feasible¶
| Aspect | Status | Details |
|---|---|---|
| Decompilation | Complete | Ghidra: 7405 functions, 296K lines of C |
| Architecture | Simple | MQTT <-> LoRa bridge, 3 FreeRTOS tasks |
| Framework | Known | ESP-IDF v4.4.2 (open source) |
| Protocol | Documented | All MQTT commands, LoRa packets, BLE provisioning mapped |
| Hardware | Identified | ESP32-S3, GD25Q64 flash, EBYTE LoRa, UM960 RTK |
| Binary patching | Working | MQTT host replacement tool available |
| Rebuild from scratch | Feasible | Clean-room ESP-IDF project possible |
The charger is architecturally simple: it receives JSON commands via MQTT, translates them to binary LoRa packets, and vice versa. With 7405 decompiled functions, every code path is visible. A complete rewrite in ESP-IDF is realistic.
Mower (Horizon X3 / Linux) --- Modifiable, Not Rebuildable¶
| Aspect | Status | Details |
|---|---|---|
| OS | Full Linux | Ubuntu/Debian ARM64, ROS 2 Galactic |
| Firmware format | Debian package | .deb with 7570 files |
| Core binaries | Compiled C++ | ~40 ELF binaries, 239 shared libraries |
| Scripts | Editable | 575 shell, 298 Python, 136 YAML configs |
| AI models | Binary | 2 DNN models (8.1MB + 3.6MB), not editable |
| Rebuild from scratch | Not feasible | Requires ROS 2 source, Horizon BPU SDK, camera drivers |
| Modify and repackage | Fully feasible | Unpack .deb, edit scripts/configs, repack, OTA flash |
The mower firmware is a Debian package. While the compiled ROS 2 nodes cannot be rebuilt without source code, the scripts, configs, and system settings are fully editable. This covers the most important modifications:
- SSH server installation
- Server URL configuration (mDNS discovery + fallback cascade)
- Camera MJPEG streaming
- LED control bridge
- WiFi AP fallback
- Extended commands (reboot, system info, PIN verify)
- STM32 MCU firmware patching (PIN lock bypass)
- daemon_node fix for mqtt_node startup
Current Firmware Version¶
v6.0.2-custom-16 (installed 8 March 2026 via SSH)
This is the latest custom firmware running on the mower. It includes all features listed below.
Custom-16 Feature Summary¶
| Feature | Description |
|---|---|
| SSH server | openssh-server installed at first boot |
| Server URL config | set_server_urls.sh runs at every boot |
| mDNS discovery | Firmware queries opennovabot.local at boot via raw mDNS |
| Fallback cascade | mDNS (8s) -> last-known IP -> FALLBACK_HOST -> skip |
| Atomic config writes | json_config.json with factory backup + pre-boot validation |
| Camera stream | Python ROS 2 node on port 8000 (MJPEG) |
| LED bridge | Python MQTT->ROS bridge for LED/headlight control |
| WiFi AP fallback | Creates AP OpenNova if home WiFi fails after 90s |
| daemon_node fix | Prevents watchdog from killing custom scripts; starts mqtt_node |
| Extended commands | Python ROS 2 node: reboot, camera snapshot, system info, PIN verify |
| STM32 MCU v3.6.6 | PIN lock bypass + verify response fix for ROS 2 action compat |
Mower .deb Firmware Composition¶
Total files: 7570
By type:
+-- Shell scripts: 575 (editable)
+-- Python scripts: 298 (editable)
+-- YAML configs: 136 (editable)
+-- JSON configs: 45 (editable)
+-- Launch files: 38 (editable)
+-- ELF binaries: ~40 (compiled C++)
+-- Shared libraries: 239 (.so files)
+-- AI models: 2 (Horizon BPU format)
+-- Camera calibration: 12 (JSON/txt)
+-- Other: ~6185 (ROS packages, headers, cmake, etc.)
Key insight: Over 1000 files are directly editable text. The OTA system (dpkg -x) simply extracts the .deb content, making modification straightforward.
What Can Be Modified¶
Without Source Code (script/config level)¶
| Modification | Method | Files |
|---|---|---|
| Install SSH server | Add apt install openssh-server to start_service.sh |
scripts/start_service.sh |
| Change server URLs | Override /userdata/lfi/http_address.txt at boot |
scripts/set_server_urls.sh |
| Change MQTT host | Python json merge into json_config.json at boot |
scripts/set_server_urls.sh |
| mDNS server discovery | Raw mDNS query for opennovabot.local |
scripts/set_server_urls.sh |
| Enable ROS 2 network | Remove ROS_LOCALHOST_ONLY=1 |
scripts/run_novabot.sh |
| Camera streaming | Python ROS 2 MJPEG node on port 8000 | scripts/camera_stream.py |
| LED control | Python MQTT->ROS bridge | scripts/led_bridge.py |
| Extended commands | Python MQTT listener for reboot, info, PIN verify | scripts/extended_commands.py |
| WiFi AP fallback | hostapd hotspot if STA fails | scripts/wifi_ap_fallback.sh |
| WiFi watchdog | Continuous monitoring + auto-recovery | scripts/wifi_watchdog.sh |
| Config validation | Pre-boot json_config.json integrity check |
scripts/validate_config.sh |
| daemon_node | Watchdog that spawns and monitors mqtt_node | run_novabot.sh injection |
| Perception tuning | Adjust thresholds, modes | perception_conf/*.yaml |
| Navigation params | Tune planners, costmap | Nav2 YAML configs |
| STM32 MCU firmware | Replace .bin in MCU_BIN directory |
MCU_BIN/*.bin |
Requires Source Code (binary level)¶
| Modification | Reason |
|---|---|
| MQTT protocol changes | Hardcoded in mqtt_node (6.3MB ELF) |
| New ROS 2 service types | Requires message compilation |
| Camera driver modifications | Compiled camera_307_cap binary |
| Motor control changes | Compiled chassis_control binary |
| AI model replacement | Requires Horizon BPU toolchain |
Build Process¶
Custom Mower Firmware Builder¶
A build script (research/build_custom_firmware.sh) automates the process of creating modified mower firmware:
# Basic usage --- auto-detect newest .deb, use defaults
./research/build_custom_firmware.sh
# Specify server and version
./research/build_custom_firmware.sh \
--server 192.168.1.50 \
--http-port 3000 \
--ssh-password novabot \
--version custom-16
# Bundle the server inside the firmware (runs on the mower itself)
./research/build_custom_firmware.sh \
--include-server \
--bundle-node \
--bundle-node-ip 192.168.0.244 \
--version custom-16-server
Build Script Options¶
| Option | Default | Description |
|---|---|---|
--input |
auto-detect | Source .deb firmware file |
--server |
novabot.local |
Local server hostname/IP (fallback host) |
--http-port |
(none) | HTTP API port (omit for reverse proxy on 80) |
--mqtt-host |
same as --server |
MQTT broker hostname |
--mqtt-port |
1883 |
MQTT broker port |
--ssh-password |
novabot |
Root SSH password |
--ssh-port |
22 |
SSH listen port |
--remote-ros2 |
off | Remove ROS_LOCALHOST_ONLY=1 restriction |
--include-server |
off | Bundle server + dashboard in firmware |
--bundle-node |
off | Also bundle Node.js + node_modules (offline install) |
--bundle-node-ip |
(auto) | Mower IP to copy node_modules from |
--server-port |
3000 |
Dashboard port (with --include-server) |
--version |
custom-1 |
Version suffix appended to base version |
What the Builder Does (step by step)¶
| Step | Description |
|---|---|
| 1. SSH install | Adds openssh-server + hostapd installation to start_service.sh |
| 2. URL patches | Creates set_server_urls.sh with mDNS discovery + fallback cascade |
| 3. Config validation | Creates validate_config.sh for pre-boot json_config.json integrity |
| 4. Camera stream | Copies camera_stream.py (MJPEG on port 8000) |
| 5. LED bridge | Copies led_bridge.py (MQTT->ROS /led_set) |
| 6. WiFi AP fallback | Creates wifi_ap_fallback.sh + wifi_watchdog.sh |
| 7. daemon_node fix | Injects ros2 run daemon_process daemon_node into run_novabot.sh |
| 8. Extended commands | Copies extended_commands.py + pin_verify_ros2.py |
| 9. STM32 MCU patch | Copies v3.6.6 PIN unlock binary to MCU_BIN directory |
| 10. Server bundle | (optional) Bundles server + dashboard + Node.js |
| 11. Version update | Updates novabot_api.yaml + Readme.txt + package_verify.json |
| 12. Build .deb | Repackages modified firmware into .deb + generates metadata JSON |
Generated Files¶
| File | Description |
|---|---|
research/firmware/mower_firmware_v6.0.2-custom-16.deb |
Modified .deb package (~35MB) |
research/firmware/mower_firmware_v6.0.2-custom-16.json |
Metadata (version, md5, filename) |
research/firmware/ota_flash_command.json |
Ready-to-use OTA MQTT command |
macOS Compatibility
The build script works on macOS without dpkg-deb by using ar + tar directly. All sed calls use macOS-compatible sed -i '' syntax. COPYFILE_DISABLE=1 prevents macOS ._* resource fork files in the tar.
The general approach for building custom mower firmware:
1. Extract original .deb:
ar x mower_firmware_v6.0.2.deb
tar -xf data.tar.xz
2. Modify scripts, configs, add new files
3. Repackage:
tar -cJf data.tar.xz .
ar rcs custom_firmware.deb debian-binary control.tar.xz data.tar.xz
The .deb uses flat directory structure (not root/novabot/), and the OTA system extracts it via dpkg -x to /root/novabot.new/.
Server Discovery (mDNS + Fallback Cascade)¶
The custom firmware uses a multi-step approach to find the local server at every boot:
flowchart TD
A[Boot / WiFi connected] --> B[Wait for WiFi up to 30s]
B --> C{mDNS query opennovabot.local}
C -->|Found within 8s| D[Use discovered IP]
C -->|Timeout| E{Last-known IP file exists?}
E -->|Yes| F[Use last-known IP]
E -->|No| G{FALLBACK_HOST set?}
G -->|Yes| H[Use fallback host]
G -->|No| I[Skip -- keep existing config]
D --> J[Write http_address.txt + json_config.json]
F --> J
H --> J
The mDNS discovery is implemented as a raw Python socket query (no external dependencies). It queries for opennovabot.local, which is advertised by the bootstrap wizard running on the host machine.
Critical: http_address.txt format
The firmware prepends http:// when building URLs. The file must contain ONLY host:port (e.g. 192.168.0.177:3000), with NO http:// prefix and NO trailing newline. The build script uses printf "%s" instead of echo for this reason.
Critical: json_config.json handling
NEVER overwrite the entire file. It contains BLE-provisioned data (WiFi, LoRa, SN) that would be lost. The build script uses a Python JSON merge that updates ONLY the mqtt section, with atomic writes (.tmp -> verify -> os.replace), factory backup (.factory, created once), and rolling backup (.bak).
OTA Flash Process¶
Custom firmware is installed via the standard OTA mechanism --- no physical access needed:
sequenceDiagram
participant Server as Local Server
participant MQTT as MQTT Broker
participant Mower
participant HTTP as HTTP File Server
Note over Server: Host custom .deb on HTTP server
Server->>MQTT: Publish ota_upgrade_cmd to<br/>Dart/Send_mqtt/<SN>
MQTT->>Mower: ota_upgrade_cmd<br/>{cmd: "upgrade", type: "full", content: "app"}
Note over Mower: Wait for CHARGING state
Mower->>HTTP: Download custom .deb (http:// only!)
Note over Mower: Verify MD5 checksum
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 -> start_service.sh<br/>(installs SSH) -> verify -> reboot
Note over Mower: SSH available on port 22
Prerequisites¶
- Mower must be charging --- OTA download only starts when
battery_state == "CHARGING" - HTTP file server --- host the .deb file on a server reachable by the mower
- MQTT access --- ability to publish to
Dart/Send_mqtt/<SN> - Correct MD5 --- the mower verifies the checksum before installing
OTA Command Payload¶
CRITICAL --- exact OTA payload (NEVER modify)
The OTA command must match this exact structure. Any deviation causes the mower's mqtt_node to silently ignore it.
{
"ota_upgrade_cmd": {
"cmd": "upgrade",
"type": "full",
"content": "app",
"url": "http://your-server:3000/api/dashboard/firmware/download/mower_firmware_v6.0.2-custom-16.deb",
"version": "v6.0.2-custom-16",
"md5": "abcdef1234567890abcdef1234567890"
}
}
Required fields:
| Field | Value | Why |
|---|---|---|
cmd |
"upgrade" |
Required --- mqtt_node ignores the command without it |
type |
"full" |
Required --- "increment" downloads nothing |
content |
"app" |
Required --- must be a string, NOT an object. mqtt_node ignores without it |
url |
http://... |
Must be http://, NOT https:// --- mower has no TLS support for OTA |
version |
version string | Used for display and version comparison |
md5 |
MD5 hash | Mower verifies checksum before installing |
NO tz field!
NEVER include a tz field in the OTA command. The mower's mqtt_node reads the tz value, writes it to a timezone file, and then overwrites type with "increment", which causes the download to fail. The broker-level fix in broker.ts strips tz from app-originated commands, but dashboard-triggered OTA must also omit it.
Send via the dashboard OTA trigger endpoint or mosquitto_pub:
# Via dashboard (recommended)
curl -X POST http://your-server:3000/api/dashboard/ota/trigger/LFIN2230700238 \
-H 'Content-Type: application/json' \
-d '{"version_id": 1}'
# Via mosquitto_pub (manual)
mosquitto_pub -h localhost -t "Dart/Send_mqtt/LFIN2230700238" \
-m '{"ota_upgrade_cmd":{"cmd":"upgrade","type":"full","content":"app","url":"http://192.168.0.177:3000/api/dashboard/firmware/download/mower_firmware_v6.0.2-custom-16.deb","version":"v6.0.2-custom-16","md5":"..."}}'
Safety & Rollback¶
The mower has a built-in rollback mechanism:
- Before installing, current firmware is backed up to
/root/novabot.bak/ - After extraction, the installer checks if
run_novabot.shexists and is non-empty - If the check fails, the installer automatically restores from backup
- User data (maps, CSV files, charging station config) is preserved across updates
OTA boot loop
If upgrade.txt=1 and /root/novabot.new exists, run_ota.sh copies firmware at every boot. Fix via SSH: echo 0 > /userdata/ota/upgrade.txt && rm -rf /root/novabot.new
run_ota.sh does NOT preserve json_config.json
WiFi and LoRa configuration is lost after OTA. The charger restores LoRa params via its LoRa link. WiFi must be re-provisioned via BLE if the factory backup does not exist.
Always test modifications incrementally
Start with minimal changes (just SSH) before adding more modifications. The rollback mechanism protects against broken firmware, but it is better to be cautious.
Extended Commands¶
The extended_commands.py Python service runs alongside mqtt_node and handles commands that are not built into the stock firmware.
MQTT Interface¶
| Direction | Topic |
|---|---|
| Commands | novabot/extended/<SN> |
| Responses | novabot/extended_response/<SN> |
Unencrypted
Extended commands use their own topic namespace and are NOT AES-encrypted (unlike Dart/Send_mqtt/<SN>). This is a separate channel from mqtt_node.
Available Commands¶
| Command | Payload | Description |
|---|---|---|
set_robot_reboot |
{} |
System reboot with 3s delay |
get_system_info |
{} |
CPU temp, uptime, disk, memory, ROS nodes |
save_camera_image |
{} |
Camera snapshot (delegated to camera_stream.py) |
verify_pin |
{"code": "1234"} |
PIN verify via pin_verify_ros2.py subprocess |
PIN Verify Workaround (pin_verify_ros2.py)¶
The stock mqtt_node has a broken C++ action client for ChassisPinCodeSet --- it times out after 21 seconds because it never finds the action server. A Python ROS 2 action client finds it in under 1 second.
pin_verify_ros2.py is a standalone ROS 2 script called by extended_commands.py via subprocess. It sends a type=2 (verify) goal to chassis_control_node, which forwards it to the STM32 MCU.
Startup: extended_commands.py is launched by run_novabot.sh with a 12-second delay after boot, giving ROS 2 nodes time to initialize.
STM32 MCU Firmware (v3.6.6)¶
The STM32F407 on the chassis PCB handles the touchscreen display, motor control, sensors, PIN lock, and LoRa communication.
Hardware¶
| Property | Value |
|---|---|
| Microcontroller | STM32F407 on chassis PCB |
| Connection | USB serial /dev/ttyACM0 to ARM SoC (Horizon X3) |
| Original firmware | v3.6.0, 444,144 bytes |
| Patched firmware | v3.6.6, same size |
| Location on mower | /root/novabot/install/chassis_control/share/chassis_control/MCU_BIN/ |
Flashing Mechanism¶
chassis_control_node reads the firmware version from the filename (not the binary contents). The naming pattern is v{major}_{minor}_{patch} and the comparison value is major * 10000 + minor * 100 + patch.
To install a new MCU firmware version:
- Place the
.binfile in theMCU_BIN/directory with the correct version in the filename - Remove old versions to avoid conflicts
- Reboot the mower completely (not just ROS nodes)
chassis_control_nodedetects the version difference and auto-flashes via IAP at boot
Both filename AND binary version must match
The MCU reports its version after flashing via serial. If the binary's internal version bytes (at offset 0x47638) don't match the filename, things will get confused.
PIN Lock Bypass (v3.6.6 patch)¶
The custom STM32 firmware adds remote PIN verification (type=2) to the stock command handler.
Root cause chain for error_status=151 (PIN locked):
STM32 check_pin_lock() → sets error_byte (0x20000774) = 0x02
→ CMD 0x20 serial report includes 0x02
→ chassis_control_node reads error_no_pin_code flag
→ mqtt_node action client timeout (21s, broken C++ client)
→ error_status=151 persists in MQTT status reports
v3.6.6 fixes (cumulative):
| Version | Fix |
|---|---|
| v3.6.3 | Clear error_flag_byte + incident_flag_byte after verify |
| v3.6.4 | Clear error_byte at 0x20000774 (the actual CMD 0x20 data source) |
| v3.6.5 | Set lock_state = 0xFF --- permanently skips check_pin_lock() state machine until reboot |
| v3.6.6 | Return status=0 (was 2) --- chassis_control_node only accepts 0 as success |
Build tool: research/firmware/STM32/patch_pin_unlock.py
cd research/firmware/STM32/
python3 patch_pin_unlock.py
# Produces: novabot_stm32f407_v3_6_6_NewMotor25082301_pin_unlock.bin
The build script automatically copies this binary into the firmware's MCU_BIN/ directory.
After successful PIN verify via type=2:
- Screen switches to home (0x0C)
- All error flags are cleared (display, incident, CMD 0x20 error byte)
lock_stateset to0xFF---check_pin_lock()permanently skipped until reboot- Response returns
status=0--- compatible with ROS 2ChassisPinCodeSetaction
Charger Firmware Modification¶
Binary Patching (v0.3.6 / v0.4.0)¶
Since the charger firmware is a single binary image (not a package), modification requires binary patching:
A patch tool handles:
- ESP32-S3 image header parsing and segment identification
- String replacement in DROM (read-only data) segments
- String relocation when replacement is longer than original
- Code reference updates (literal pool pointers)
- SHA256 hash recalculation
# Patch MQTT host
node research/patch_firmware.js --mqtt-host my.server.nl
# Analyze strings without patching
node research/patch_firmware.js --analyze
For NVS-stored MQTT settings, use BLE:
Available patched binaries:
| Version | Changes | File |
|---|---|---|
| v0.3.6 | MQTT -> local server | research/firmware/charger_v0.3.6_patched.bin |
| v0.4.0 | MQTT -> local server | research/firmware/charger_v0.4.0_patched.bin |
The charger binary contains hardcoded MQTT server hostnames. These can be patched using binary search-and-replace with SHA256 hash recalculation. The ESP32-S3 OTA system validates the image header checksum, so the hash must be updated after patching.
For runtime MQTT host changes, the charger reads its primary MQTT address from NVS (mqtt_data), which can be set via BLE provisioning.
Full Rewrite (ESP-IDF)¶
A complete charger firmware rewrite is feasible:
| Component | Source | Effort |
|---|---|---|
| MQTT client | ESP-IDF esp_mqtt |
Low (well documented) |
| LoRa driver | EBYTE E32/E22 UART protocol | Medium (fully reverse-engineered) |
| BLE provisioning | ESP-IDF esp_ble_gatts |
Medium (GATT service 0x1234) |
| WiFi management | ESP-IDF esp_wifi |
Low (standard API) |
| GPS/RTK | NMEA parser for UM960 | Low (standard protocol) |
| NVS storage | ESP-IDF nvs_flash |
Low (standard API) |
| OTA | ESP-IDF esp_ota_ops |
Low (standard API) |
| Command dispatch | cJSON + switch table | Medium (all commands documented) |
| Status reporting | cJSON + timer | Low (format fully known) |
Estimated complexity: ~3000-5000 lines of C, primarily mapping between JSON and LoRa binary packets. The complete LoRa protocol, MQTT command set, BLE provisioning flow, and NVS storage format are all documented.
MQTT Host Configuration¶
Understanding how each device gets its MQTT server address is crucial for custom firmware:
| Device | Primary Source | Fallback | Needs Binary Patching? |
|---|---|---|---|
| Charger | NVS mqtt_data (set via BLE) |
Hardcoded in binary | Only for fallback IP |
| Mower | /userdata/lfi/json_config.json (set at boot) |
mDNS -> last-known IP -> fallback host | No --- set_server_urls.sh handles it |
For the mower, the MQTT host is set at every boot by set_server_urls.sh, which uses a discovery cascade: mDNS query for opennovabot.local (8s timeout) -> last-known IP from file -> hardcoded fallback host -> skip (keep existing config).
For the charger, the primary MQTT host is stored in NVS (set via BLE provisioning). The hardcoded fallback IP in the binary is only used when DNS resolution fails completely.
HTTP Server URL Configuration¶
The mower's mqtt_node reads the HTTP server URL from /userdata/lfi/http_address.txt. This is used for:
- Map ZIP uploads (
uploadEquipmentMap) - Track uploads (
uploadEquipmentTrack) - Work record saving (
saveCutGrassRecord) - Plan queries (
queryPlanFromMachine)
Custom firmware overrides this file at every boot to ensure it always points to the local server, even if the mower's firmware tries to reset it.
File format
The firmware prepends http:// when building URLs. Write ONLY host:port to this file (e.g. 192.168.0.177:3000), with NO http:// prefix and NO trailing newline. Use printf "%s" not echo.
Distribution Model¶
Decided March 2026
The server runs in a Docker container on Mac/NAS/RPi, NOT on the mower. Reasons: CPU load, battery drain, heat, brick risk, no dashboard when mower is offline.
Architecture¶
flowchart LR
subgraph Host["Host Machine (Mac/NAS/RPi)"]
Docker["Docker Container<br/>server + dashboard<br/>+ Aedes MQTT broker"]
Bootstrap["Bootstrap Wizard<br/>(native, not Docker)<br/>+ mDNS opennovabot.local"]
end
subgraph Mower["Mower (custom firmware)"]
Firmware["v6.0.2-custom-16<br/>mDNS discovery<br/>set_server_urls.sh"]
end
subgraph App["Novabot App"]
Flutter["Flutter v2.3.8+<br/>DNS redirect via<br/>app.lfibot.com"]
end
Firmware -->|mDNS query| Bootstrap
Firmware -->|MQTT + HTTP| Docker
App -->|HTTPS + MQTT| Docker
Key Decisions¶
| Decision | Rationale |
|---|---|
| No Novabot binaries distributed | Legal risk. Only patch tools shipped; user downloads firmware from cloud |
| Charger patching | Binary patching for MQTT URLs (patch_firmware.js) + BLE NVS set (ble_set_mqtt.js) |
| Bootstrap wizard | bootstrap/ --- standalone tool for initial firmware flash + mDNS advertising of opennovabot.local |
| mDNS in bootstrap, not Docker | Docker bridge networking blocks multicast on macOS. Bootstrap runs native on host |
Novabot Cloud Unreliable
The Novabot cloud has been experiencing frequent outages since March 2026. Firmware downloads from the cloud may not be available during outages.
WiFi AP Fallback¶
If the mower cannot connect to its home WiFi network within 90 seconds of boot, it creates a WiFi hotspot for emergency access:
| Setting | Value |
|---|---|
| SSID | OpenNova |
| Password | novabot123 |
| IP | 192.168.4.1 |
| DHCP range | 192.168.4.10 -- 192.168.4.50 |
A separate WiFi watchdog (wifi_watchdog.sh) runs continuously and:
- Checks WiFi connectivity every 30 seconds
- After 2 minutes without WiFi, checks if
json_config.jsonhas awifisection - If missing, restores from backup/factory and restarts
mqtt_node - If WiFi still fails after 5 minutes, starts the AP fallback as a safety net
Recovery¶
Mower Firmware¶
- Automatic rollback: If
run_novabot.shis missing or empty after OTA extraction, the installer restores from/root/novabot.bak/ - SSH access:
ssh root@<mower-ip>(password:novabot) - WiFi AP fallback: Connect to
OpenNovahotspot if home WiFi is unreachable - Ethernet: Static IP
192.168.1.10/24oneth0is always configured
STM32 MCU¶
- Original firmware backup:
MCU_BIN/novabot_stm32f407_v3_6_0_NewMotor25082301.bin.bakon the mower - Restore: Copy the
.bakfile back with the original filename and reboot - MCU stuck in IAP mode: Display may be blank, but the ARM SoC still boots with WiFi (SSH accessible)
Config Recovery¶
json_config.json has three layers of protection:
- Factory backup (
.factory) --- created once, NEVER overwritten - Rolling backup (
.bak) --- updated at every successful modification - Pre-boot validation (
validate_config.sh) --- checks forsnsection beforemqtt_nodestarts
Recovery cascade: .json -> .bak -> .factory