diff --git a/README.md b/README.md
index fd6f49d..94fbc6c 100644
--- a/README.md
+++ b/README.md
@@ -45,8 +45,7 @@ Tip: Click a section title to expand/collapse.
3. After writing the image, eject the card, insert it into your mower’s Pi or xCore, and turn it on.
-4. Your Pi will boot multiple times.
- ***Sometimes, after the first boot, it may fail to reboot*** (the Pi/xCore’s green LED doesn’t flicker anymore and remains off for >10 seconds). If that happens, a power cycle will get it back on track.
+4. Your Pi will boot multiple times. Be patient and wait till the green activity LED becomes silent for ≥ 10 seconds.
5. ***Optional: Comitup hotspot (if you skipped step 2 "Raspberry Pi Imager configuration")***
If you didn't enter your Wifi settings when asked for the custom settings during Pi Imager (see step 2), or if you accidentally entered the wrong Wifi settings:
@@ -61,7 +60,7 @@ If you didn't enter your Wifi settings when asked for the custom settings during
4. The hotspot will disappear and the mower should connect to your WiFi.
-6. Two minutes after the second reboot, try pinging your mower via `ping openmower` (or the hostname you entered during Pi Imager). If the host can't be found, check your router for the mower's IP address.
+6. Once the green activity LED becomes silent, try pinging your mower via `ping openmower` (or the hostname you entered during Pi Imager). If the host can't be found, check your router for the mower's IP address.
7. ***Optional:***
1. If you didn't configure a custom password during step 2 (Raspberry Pi Imager configuration), login via SSH and change your password now via `passwd`.
@@ -75,10 +74,9 @@ If you didn't enter your Wifi settings when asked for the custom settings during
Manage OpenMower stack (GUI + CLI)
[Dockge](https://dockge.kuma.pet/) (a container manager GUI) and [ttyd](https://tsl0922.github.io/ttyd/) (a web terminal)
-are bundled with the OpenMowerOS image and are unpacked and installed during the final second‑boot step.
-This may take about 2 minutes.
+are bundled together with the OpenMowerOS image, get unpacked and installed during the final boot step.
-Please wait while the Pi/xCore’s green LED is flickering or steadily on.
+Please wait till Pi/xCore’s green LED becomes silent for ≥ 10 seconds.
The WebTerminal is available as a lightweight alternative to SSH for running the same commands.
It can be reached via `http://openmower:7681` (adjust if you changed the hostname).
@@ -99,18 +97,12 @@ For each relevant GUI action, a CLI alternative is available via a powerful `ope

3. Start the stack (including the initial pull)
- - CLI: If you configured your .env file via `openmower configure env` then the stack is pulled and started automatically.
- If not:
-
- ```bash
- openmower pull
- openmower start
- ```
+ - CLI: If you configured your .env file via `openmower configure env` then the stack is pulled and started automatically.
- GUI:

4. Check status and open the OpenMower web app
- - CLI: `openmower status` should list three service names (open_mower_ros, Mosquitto and OpenMowerApp), all with status 'up'. If so, open a browser and visit `http://openmower:8080` (or your configured hostname).
+ - CLI: `openmower status` should list three services (open_mower_ros, Mosquitto and OpenMowerApp), all with status 'up'. If so, open a browser and visit `http://openmower:8080` (or your configured hostname).
- GUI:

diff --git a/TESTED_FEATURES.md b/TESTED_FEATURES.md
index 89d2d1a..c6ccd21 100644
--- a/TESTED_FEATURES.md
+++ b/TESTED_FEATURES.md
@@ -2,30 +2,33 @@
Legend: ✅ Pass · ❌ Fail · 🟡 Todo · 🔁 Retry · 🧪 Manual-only
-| Feature | Expected | HW‑V1
Pi4 | HW‑V2
CM4 | HW‑V2
CM5 |
-| ---------------------------------------- | ----------------------------------------- | :----------: | :----------: | :----------: |
-| Auto-reboot after initial boot | yes, but seem to be a bug | ✅ | ❌🧪 | 🟡 |
-| Debian release `lsb_release -a` | Debian GNU/Linux 13 (trixie) | ✅ | ✅ | 🟡 |
-| OpenMowerOS release `cat /etc/rpi-issue` | OpenMowerOS v2.x YYYY-MM-DD | ✅ | ✅ | 🟡 |
-| Hostname (default) `hostname` | openmower | 🟡 | ✅ | 🟡 |
-| Hostname (non- default) `hostname` | | ✅ | ✅ | 🟡 |
-| Default user/password | openmower/openmower | 🟡 | ✅ | 🟡 |
-| SSH enabled | SSH active on first boot | ✅ | ✅ | 🟡 |
-| SSH public key | Password less SSH login via SSH-key | 🟡 | ✅ | 🟡 |
-| Imager Wi‑Fi | Preseeded Wi‑Fi connects on first boot | ✅ | ✅ | 🟡 |
-| Imager openmower pass | Applied when configured | ✅ | ✅ | 🟡 |
-| No known Wi‑Fi | Comitup AP appears (default SSID pattern) | 🟡 | ✅ | 🟡 |
-| AP portal | Able to configure Wi‑Fi, then joins WLAN | 🟡 | ✅ | 🟡 |
-| Internal LAN | xCore is getting an IPv4 | -- | ✅ | 🟡 |
-| Home LAN | eth0 IPv4 by your networks DHCP | 🟡 | ✅ | 🟡 |
-| SSH | Reachable after network is up | ✅ | ✅ | 🟡 |
-| WebTerminal (ttyd) | Reachable at port 7681 | ✅ | ✅ | 🟡 |
-| Dockge | Reachable at port 5001 | ✅ | ✅ | 🟡 |
-| ESC access | Ports get exposed via `openmower ...` cmd | ✅ | 🟡 | 🟡 |
-| GNSS access | Port get exposed via `openmower ...` cmd | ✅ | 🟡 | 🟡 |
-| Container shell (prefix) | `openmower shell` has docker prefix | ✅ | ✅ | 🟡 |
+| Feature | Expected | HW‑V1
Pi4 | HW‑V2
CM4 | HW‑V2
CM5 |
+| ---------------------------------------- | --------------------------------------------- | :----------: | :----------: | :----------: |
+| Auto-reboot after initial boot | yes | ✅ | ✅ | ✅ |
+| Debian release `lsb_release -a` | Debian GNU/Linux 13 (trixie) | ✅ | ✅ | ✅ |
+| OpenMowerOS release `cat /etc/rpi-issue` | OpenMowerOS v2.x YYYY-MM-DD | ✅ | ✅ | ✅ |
+| Hostname (default) `hostname` | openmower | 🟡 | ✅ | ✅ |
+| Hostname (non-default) `hostname` | | ✅ | ✅ | ✅ |
+| Default user/password | openmower/openmower | 🟡 | ✅ | ✅ |
+| SSH enabled | SSH active on first boot | ✅ | ✅ | ✅ |
+| SSH public key | Password less SSH login via SSH-key | 🟡 | ✅ | ✅ |
+| Imager Wi‑Fi | Preseeded Wi‑Fi connects on first boot | ✅ | ✅ | ✅ |
+| Imager openmower pass | Applied when configured | ✅ | ✅ | ✅ |
+| No known Wi‑Fi | Comitup AP appears (default SSID pattern) | 🟡 | ✅ | ✅ |
+| Comitup captive portal | Able to configure Wi‑Fi, then joins WLAN [^1] | 🟡 | ✅ | ✅ |
+| Internal LAN | xCore is getting an IPv4 | 🟡 | ✅ | ✅ |
+| Home LAN | eth0 IPv4 by your networks DHCP | 🟡 | ✅ | ✅ |
+| SSH | Reachable after network is up | ✅ | ✅ | ✅ |
+| WebTerminal (ttyd) | Reachable at port 7681 | ✅ | ✅ | ✅ |
+| Dockge | Reachable at port 5001 | ✅ | ✅ | ✅ |
+| ESC access | Ports get exposed via `openmower ...` cmd | ✅ | ✅ | 🟡 |
+| GNSS access | Port get exposed via `openmower ...` cmd | ✅ | 🟡 | 🟡 |
+| OpenOCD remote debugging | Remote debugging via `openmower openocd` | 🟡 | ✅ | ✅ |
+| Container shell (prefix) | `openmower shell` has docker prefix | ✅ | ✅ | ✅ |
## Notes
- Update cells as you validate on each hardware combo.
- If Imager Wi‑Fi is set, Comitup should not spawn AP; if not, AP should appear.
+
+[^1]: You probably need to power-cycle your mower after entering your home WiFi credentials
\ No newline at end of file
diff --git a/ext/pi-gen b/ext/pi-gen
index 7cf36c9..7dadcf1 160000
--- a/ext/pi-gen
+++ b/ext/pi-gen
@@ -1 +1 @@
-Subproject commit 7cf36c964ba99ff1c75bfcbac4ff53145e377802
+Subproject commit 7dadcf1fc5ce1648ab09409ab978831690c9a955
diff --git a/pi-gen.config b/pi-gen.config
index 61eb4cb..6b4695c 100644
--- a/pi-gen.config
+++ b/pi-gen.config
@@ -33,3 +33,5 @@ STAGE_LIST="/pi-gen/stage0 /pi-gen/stage1 /pi-gen/stage2 /stage-openmower"
# Compression of exported image
DEPLOY_COMPRESSION=zip
COMPRESSION_LEVEL=6
+
+ENABLE_CLOUD_INIT=0
diff --git a/stage-openmower/15-pi-config/files/boot/firmware/config.addendum b/stage-openmower/15-pi-config/files/boot/firmware/config.addendum
index 4d5f59c..bd1cd10 100644
--- a/stage-openmower/15-pi-config/files/boot/firmware/config.addendum
+++ b/stage-openmower/15-pi-config/files/boot/firmware/config.addendum
@@ -56,8 +56,8 @@ dtparam=ant1
# External- U.FL/IPEX antenna connector:
# dtparam=ant2
-# Fan control (on CM4 it's GPIO18)
-dtoverlay=pwm-gpio-fan,fan_gpio=18
+# Fan control (GPIO fan) — enable and set start temperature (in millidegrees C)
+dtoverlay=pwm-gpio-fan,fan_gpio=18,temp=70000
[cm5]
# CM5 antenna selection — choose Wi‑Fi antenna path
@@ -66,6 +66,5 @@ dtparam=ant1
# External- U.FL/IPEX antenna connector:
# dtparam=ant2
-# Fan control (on CM5 it's GPIO18)
-dtoverlay=pwm-gpio-fan,fan_gpio=18
-
+# Fan control (GPIO fan) — enable and set start temperature (in millidegrees C)
+dtoverlay=pwm-gpio-fan,fan_gpio=18,temp=70000
diff --git a/stage-openmower/20-comitup/00-run-chroot.sh b/stage-openmower/20-comitup/00-run-chroot.sh
index 9c8e507..45b39b0 100755
--- a/stage-openmower/20-comitup/00-run-chroot.sh
+++ b/stage-openmower/20-comitup/00-run-chroot.sh
@@ -5,11 +5,15 @@ apt-get update
apt-get install -y --no-install-recommends comitup network-manager wpasupplicant rfkill
systemctl enable comitup.service
-systemctl enable comitup-web.service
systemctl enable comitup-nm-wifi-ensure.service
systemctl mask NetworkManager-wait-online.service
+systemctl mask wpa-supplicant.service
if ! grep -q '^ap_name:' /etc/comitup.conf 2>/dev/null; then
printf '\n# Default OpenMower AP name for provisioning\nap_name: OpenMower-\n' >> /etc/comitup.conf
fi
+
+# Enable external_callback to manage dnsmasq
+sed -i 's/^#\s*external_callback:.*/external_callback: \/usr\/local\/bin\/comitup-callback/' /etc/comitup.conf
+chown -R root:root /usr/local/bin/comitup-callback
diff --git a/stage-openmower/20-comitup/00-run.sh b/stage-openmower/20-comitup/00-run.sh
index a60642e..0e3d167 100755
--- a/stage-openmower/20-comitup/00-run.sh
+++ b/stage-openmower/20-comitup/00-run.sh
@@ -7,3 +7,4 @@ STAGE_DIR="$(dirname "$0")"
# Install files
install -m 0644 -D "$STAGE_DIR/files/etc/NetworkManager/conf.d/10-comitup.conf" "$ROOTFS_DIR/etc/NetworkManager/conf.d/10-comitup.conf"
install -m 0644 -D "$STAGE_DIR/files/etc/systemd/system/comitup-nm-wifi-ensure.service" "$ROOTFS_DIR/etc/systemd/system/comitup-nm-wifi-ensure.service"
+install -m 0755 -D "$STAGE_DIR/files/usr/local/bin/comitup-callback" "$ROOTFS_DIR/usr/local/bin/comitup-callback"
diff --git a/stage-openmower/20-comitup/files/usr/local/bin/comitup-callback b/stage-openmower/20-comitup/files/usr/local/bin/comitup-callback
new file mode 100644
index 0000000..e632790
--- /dev/null
+++ b/stage-openmower/20-comitup/files/usr/local/bin/comitup-callback
@@ -0,0 +1,32 @@
+#!/bin/bash
+# Callback script for comitup to manage dnsmasq
+# Argument: $1 = state (HOTSPOT, CONNECTING, CONNECTED)
+
+LOG="/var/log/comitup-callback.log"
+DNSMASQ_SERVICE="dnsmasq.service"
+
+log() {
+ echo "$(date): $*" >> "$LOG"
+}
+
+case "$1" in
+ HOTSPOT)
+ log "Entering HOTSPOT mode - stopping dnsmasq"
+ systemctl stop "$DNSMASQ_SERVICE" 2>/dev/null
+ pkill -9 dnsmasq 2>/dev/null
+ # Ensure no dnsmasq is listening on port 67
+ ss -ulpn | grep :67 && log "WARNING: Port 67 still occupied"
+ ;;
+ CONNECTING)
+ log "Entering CONNECTING mode - dnsmasq remains stopped"
+ ;;
+ CONNECTED)
+ log "Entering CONNECTED mode - restarting dnsmasq"
+ systemctl start "$DNSMASQ_SERVICE" 2>/dev/null
+ ;;
+ *)
+ log "Unknown state: $1"
+ ;;
+esac
+
+exit 0
diff --git a/stage-openmower/25-lan/files/etc/dnsmasq.d/10-openmower.conf b/stage-openmower/25-lan/files/etc/dnsmasq.d/10-openmower.conf
index 9dfc4e2..c853f0b 100644
--- a/stage-openmower/25-lan/files/etc/dnsmasq.d/10-openmower.conf
+++ b/stage-openmower/25-lan/files/etc/dnsmasq.d/10-openmower.conf
@@ -2,6 +2,12 @@
# specified interfaces (and the loopback) give the name of the
# interface (eg eth0) here.
# Repeat the line for more than one interface.
+#
+# AH20260105:
+# bind-interfaces together with interface does not work as expected.
+# dnsmasq always bind to 0.0.0.0:67. No 2nd dnsmasq on other interface bindable!
+# See workaround in /usr/local/bin/comitup-callback
+#bind-interfaces
#interface=eth0
# Or you can specify which interface _not_ to listen on
#except-interface=
diff --git a/stage-openmower/40-openmower/00-run-chroot.sh b/stage-openmower/40-openmower/00-run-chroot.sh
index 920bcc9..882cd8f 100755
--- a/stage-openmower/40-openmower/00-run-chroot.sh
+++ b/stage-openmower/40-openmower/00-run-chroot.sh
@@ -14,6 +14,12 @@ chown -R 1000:1000 /home/openmower/ros
mkdir -p /home/openmower/params
chown -R 1000:1000 /home/openmower/params
+# Create dir and symlink for easy access to "latest" ros log
+mkdir -p /data
+chown -R 1000:1000 /data
+ln -s /home/openmower/ros /data/ros
+
+
export DEBIAN_FRONTEND=noninteractive
# Minimal deps for fetching & unpacking
diff --git a/stage-openmower/40-openmower/files/home/openmower/params/mower_params.yaml b/stage-openmower/40-openmower/files/home/openmower/params/mower_params.yaml
index 7e7554b..21221c9 100644
--- a/stage-openmower/40-openmower/files/home/openmower/params/mower_params.yaml
+++ b/stage-openmower/40-openmower/files/home/openmower/params/mower_params.yaml
@@ -30,7 +30,7 @@ mower_logic:
outline_overlap_count: 1
mow_angle_offset: 0
mow_angle_offset_is_absolute: false
- mow_angle_increment: 110
+ mow_angle_increment: 0
gps_wait_time: 5.0
gps_timeout: 5.0
rain_mode: 0
@@ -38,7 +38,7 @@ mower_logic:
rain_check_seconds: 20
undock_distance: 1.0
undock_angled_distance: 2.0
- undock_angle: 45.0
+ undock_angle: 0.0
undock_fixed_angle: false
undock_use_curve: true
# Once you validated the functionality of your emergency sensors,
diff --git a/stage-openmower/45-openocd/00-run-chroot.sh b/stage-openmower/45-openocd/00-run-chroot.sh
new file mode 100755
index 0000000..e29ef56
--- /dev/null
+++ b/stage-openmower/45-openocd/00-run-chroot.sh
@@ -0,0 +1,23 @@
+#!/bin/bash -e
+
+chown -R 1000:1000 /home/openmower/.config
+
+export DEBIAN_FRONTEND=noninteractive
+
+# Install dependencies
+apt-get update
+apt-get install -y --no-install-recommends autoconf automake build-essential curl libftdi-dev libtool libusb-1.0-0-dev git pkg-config rpi.gpio-common texinfo libgpiod-dev libjim-dev
+
+# Create directory for OpenOCD
+mkdir -p /opt
+cd /opt
+
+# Clone OpenOCD repository
+git clone https://github.com/raspberrypi/openocd.git --recursive
+
+# Bootstrap, configure, compile, and install OpenOCD
+cd openocd
+./bootstrap
+./configure --enable-ftdi --enable-sysfsgpio --enable-bcm2835gpio --enable-linuxgpiod
+make -j$(nproc)
+make install
\ No newline at end of file
diff --git a/stage-openmower/45-openocd/00-run.sh b/stage-openmower/45-openocd/00-run.sh
new file mode 100755
index 0000000..7ebe620
--- /dev/null
+++ b/stage-openmower/45-openocd/00-run.sh
@@ -0,0 +1,10 @@
+#!/bin/bash -e
+
+STAGE_DIR="$(dirname "$0")"
+
+# Install files
+install -m 0755 -d "$ROOTFS_DIR/home/openmower/.config/openmower-cli"
+install -m 0664 -D "$STAGE_DIR/files/xcore.cfg" "$ROOTFS_DIR/home/openmower/.config/openmower-cli/xcore.cfg"
+
+install -o "root" -m 0755 -d "$ROOTFS_DIR/root/.config/openmower-cli"
+install -o "root" -m 0664 -D "$STAGE_DIR/files/xcore.cfg" "$ROOTFS_DIR/root/.config/openmower-cli/xcore.cfg"
diff --git a/stage-openmower/45-openocd/files/xcore.cfg b/stage-openmower/45-openocd/files/xcore.cfg
new file mode 100644
index 0000000..f0e4343
--- /dev/null
+++ b/stage-openmower/45-openocd/files/xcore.cfg
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# Config for Raspberry Pi 5 used as a bitbang adapter.
+# https://www.raspberrypi.com/documentation/computers/raspberry-pi.html
+
+# Raspberry Pi 5 is not compatible with bcm2835gpio native GPIO driver.
+# The linuxgpiod driver without configurable adapter speed runs at approximately
+# 800 kHz (SWD writes) and 360 kHz (SWD reads)
+
+adapter driver linuxgpiod
+
+proc read_file { name } {
+ if {[catch {open $name r} fd]} {
+ return ""
+ }
+ set result [read $fd]
+ close $fd
+ return $result
+}
+
+set pcie_aspm [read_file /sys/module/pcie_aspm/parameters/policy]
+if {![string match {*\[performance\]*} $pcie_aspm]} {
+ echo "Warn : Switch PCIe power saving off or the first couple of pulses gets clocked as fast as 20 MHz"
+ echo "Warn : Issue 'echo performance | sudo tee /sys/module/pcie_aspm/parameters/policy'"
+}
+
+set _GPIO_CHIP 0
+
+# Each of the SWD lines need a gpio number set: swclk swdio
+# OpenMower pins
+adapter gpio swclk -chip $_GPIO_CHIP 27
+adapter gpio swdio -chip $_GPIO_CHIP 22
+
+transport select swd
diff --git a/stage-openmower/50-extras/00-packages-nr b/stage-openmower/50-extras/00-packages-nr
index fabfb2c..bf56d48 100644
--- a/stage-openmower/50-extras/00-packages-nr
+++ b/stage-openmower/50-extras/00-packages-nr
@@ -1,6 +1,5 @@
# Install a few helpful packages
mc
-openocd
wavemon
aptitude
socat