Skip to content

GOAT O500 research notes ​

These notes document my investigation into local control of the GOAT O500. They record observations, test results, and open questions, including where the evidence doesn't yet support a conclusion.

The O500 uses a newer platform than the vacuum models supported by Bumper. Do not redirect the mower's DNS or block its Internet access until a local replacement has been tested.

Safe capture workflow ​

Capture at the router, filtering only the mower address. For a UniFi gateway where the mower is on <GATEWAY_IFACE>:

sh
tcpdump -npi <GATEWAY_IFACE> -s 0 -U -w <CAPTURE_PATH>/o500.pcap 'host <MOWER_IP>'

Run the capture through one reproducible action at a time: boot, status refresh, start, stop, and video open/close. PCAPs may contain device identifiers and tokens, so keep them out of version control.

Use the included passive inventory tool:

sh
python tools/o500_pcap_inventory.py <CAPTURE_PATH>/o500.pcap --device-ip <MOWER_IP> --json

It never contacts the robot or a cloud endpoint. It only extracts cleartext metadata from PCAP frames: DNS names, TLS SNI/ALPN, TCP/UDP flow summaries, and STUN/ICE flows.

Repository tooling map ​

The notes explain what each experiment showed and its limitations. The scripts in tools/ handle specific collection and diagnostic tasks:

Primary direction (device analysis):

  • o500_pcap_inventory.py: passive PCAP inventory (DNS/TLS/STUN/flow metadata)
  • tls_probe.py: isolated TLS handshake outcome probe for authorized trust tests
  • jmq_metadata_probe.py: TLS plus redacted MQTT framing metadata for JMQ tests
  • goat_serial_bootstrap.sh: reviewed payload for the owner-installed UART root shell

Dedicated secondary scope (Android checks):

  • o500_app_http_metadata_observer.py: app request method/host/path metadata
  • o500_app_control_schema_observer.py: control request JSON key paths + types
  • o500_app_control_selector_observer.py: control action selectors only
  • o500_app_control_metadata_observer.py: header names + selected protocol enums
  • o500_app_mqtt_observer.py: MQTT client method signatures
  • o500_app_mqtt_metadata_observer.py: redacted MQTT topic metadata

See the tools and setup guide for usage examples and the recommended experiment order.

tools/tls_probe.py is a separate, authorized test listener that records TCP and TLS handshake outcomes. Use it only on an isolated test network with a narrowly scoped router redirect.

Observed O500 services ​

The initial O500 captures show these distinct services:

FunctionHostname patternObserved transport
Command/statusjmq-ngiot-<region>.dc.robotww.ecouser.netlong-lived TLS/443, no ALPN
Device APIapi-ngiot.dc-<region>.robotww.ecouser.netTLS/443, offers HTTP/2 and HTTP/1.1
Camera/videoAWS Kinesis Video and STUN endpointsTLS and STUN/ICE

The existing Bumper MQTT/XMPP implementation cannot service these connections. JMQ is documented by Bumper as MQTT for app v2+, and the O500's no-ALPN TLS handshake is consistent with MQTT-over-TLS. The initial implementation target is therefore a separately tested JMQ authentication/topic adapter built on the existing MQTT broker, rather than a new message transport. Video should remain a separate milestone because it depends on Kinesis/WebRTC signaling and ICE, not the normal mower command channel.

App control API ​

An authorized, metadata-only observation of ECOVACS HOME 3.14.0 showed that phone control actions are sent to:

text
POST https://api-ngiot.dc-eu.ww.ecouser.net/api/iot/endpoint/control

The cloud then forwards the action through the mower's JMQ connection. This means a complete local replacement needs both an app-facing HTTPS control API and the mower-facing JMQ broker; replacing JMQ alone does not make the stock app local.

The observed request envelope has these field paths (values, headers, query strings, and response bodies were deliberately not collected):

text
header.channel, header.m, header.pri, header.reqid, header.ts,
header.tzc, header.tzm, header.ver
body.data.act, body.data.content.type

Other control-related requests used body.data.type, optionally with mid and aid. The next protocol milestone is to obtain an authorized test fixture or capture that reveals the semantic values and signed/authenticated request requirements without exposing account credentials.

For an authorized Start/Stop test, the narrow command selectors were:

text
body.data.act: start | stop
body.data.content.type: auto

Only these selector values were collected; the rest of content, all header values, and all identifiers remain unknown. A future local API adapter should therefore model this as start/stop in auto mode, while rejecting unknown fields until the full non-sensitive command schema can be established.

The same capture established these protocol constants:

text
header.m       = request
header.channel = Android
header.ver     = 0.0.22

No HTTP header names were present either when the request was created or at OkHttp's final call-server interceptor. This rules out ordinary HTTP-header bearer authentication for this request, but does not reveal whether an identifier or signature is carried in the excluded body.data fields.

TLS trust result ​

An authorized on-gateway probe redirected only the O500's JMQ pool traffic to a local TLS server with a self-signed certificate for the expected JMQ hostname. The O500 completed TCP and then rejected the certificate with a TLS bad_certificate alert before application data was exchanged. This proves that DNS/NAT redirection alone is not sufficient. It does not by itself prove certificate pinning: the device may instead use a normal but private CA trust store.

The live JMQ service presents a *.ecouser.net certificate issued by the private ECOVACS CA; it also requests a robot client certificate chained to ECOVACS Cloud Account Root. A local Bumper broker can choose not to verify a robot client certificate, but it still needs a server certificate that the O500 accepts. That prerequisite was met through the authorized writable mqCa path described below; successful TLS and metadata-only MQTT observations supersede this initial assessment.

The current ECOVACS HOME Android application ships the same self-signed ECOVACS CA (SHA-256 fingerprint <REDACTED_CERT_FINGERPRINT>, valid 2021--2121) in assets/ca.pem. This confirms that the private CA is an intentional shared trust anchor; it does not provide a signing key or make it possible to mint an accepted local certificate.

On-device CA confirmation ​

The verified system has a separate writable userdata mount at both /data and /var (/dev/mmcblk0p16 in both cases). On the live owner-accessible device, /var/certs/master.pem is a 1,288-byte self-signed X.509 certificate with subject/issuer ECOVACS CA and validity 2021--2121. It is a certificate, not a private key. The same directory contains the standard ca-certificates.crt bundle.

/etc/conf/hydra.json explicitly sets mqCa to /data/certs/master.pem. This ties the JMQ/MQTT client trust directly to the Ecovacs CA file on writable userdata. The tested local route temporarily replaced this active file with a bundle containing the owner test CA first and the stock CA after it. Preserve and checksum a byte-for-byte backup, validate the combined PEM in advance, redirect only the JMQ hostname in an isolated VLAN, and restore the backup immediately after the test.

The live process is hydra, launched as /usr/bin/hydra -c /data/tiot/hydra/conf/hydra.json. Its root-owned active configuration contains mqCa but no MQTT client-certificate or private-key path. Client identity is therefore either supplied elsewhere, embedded in Hydra, or held by a separate service; locate it before assuming mutual TLS can be reproduced.

Confirmed local JMQ TLS interception path ​

The exact mower JMQ SNI is jmq-ngiot-eu.dc.robotww.ecouser.net. A temporary, source-restricted UDM redirect of the mower's TCP/443 traffic to a local metadata-only TLS probe established that Hydra's mqCa loader effectively uses the first PEM certificate: an Ecovacs-CA-first combined file rejected a test server with x509: certificate signed by unknown authority, while the same two certificates with the short-lived owner test CA first completed TLS 1.3 successfully for the JMQ SNI. The probe did not request a client certificate, so the absence of a peer certificate in that test does not determine the robot's normal mTLS behavior.

The router rule was removed immediately after the handshake and the original master.pem was restored byte-for-byte. Hydra was restarted and subsequently reported MCStatus: OnConnect to its normal cloud endpoint. This proves a reversible local TLS interception route without modifying the verified root. The local test ended at TLS; it did not establish an MQTT CONNECT or command.

tools/jmq_metadata_probe.py records SNI, TLS metadata, and redacted MQTT CONNECT, SUBSCRIBE, PUBLISH-length, and keepalive metadata. It never logs credentials, complete topics, or payloads. The first owner-authorized capture using that harness completed successfully: Hydra negotiated MQTT 3.1.1 over TLS 1.3 with a clean session and 60-second keepalive, then sent four QoS-1 subscriptions. The 52-byte client identifier, username/password values, full topic segments, and payloads were withheld; the harness recorded only lengths, hashes, and redacted topic shape. This begins protocol work but does not establish a working local command or status loop.

An independent public TLS 1.3 handshake to the live JMQ endpoint confirms its CertificateRequest advertises ECOVACS Cloud Account Root as the acceptable client-CA distinguished name. That root is not included in the server certificate chain. Obtain its public certificate from authorized firmware/app assets before requesting client-certificate validation in a local harness; do not attempt to extract the mower's private client key.

July 2026 local-CA recovery checkpoint ​

An initial attempt to append the test CA failed because the stock PEM has no final newline. OpenSSL rejected the concatenated file before any router redirect. The original was preserved as /data/certs/master.pem.o500-local-test.orig and restored through U-Boot UMS; its SHA-256 matched the original (2884c25f...506007a2). A normal boot restarted the owner-installed bootstrap and restored GOAT# root access. No router redirect was installed during this failed attempt, and no cloud traffic was intercepted.

For a future trust-bundle test, construct the replacement with an explicit newline between PEM certificates, validate it before replacement, and avoid set -e in commands sent to the long-lived interactive maintenance shell.

Stock-app control schema and current limitation ​

Using the owner-authorized rooted Android phone and value-free Frida schema observers, the control endpoint's observed body.data fields were name, schedAct, sid, subsets, and using; values were not recorded. Later accepted start attempts emitted no matching OkHttp request to the known control route and no ECOVACS HTTP route metadata. Treat the app's current session as offline or using a different transport; do not infer its current endpoint from the earlier schema.

An owner-authorized focused TCP scan of the mower found only TCP/53 exposed, fingerprinted as Unbound DNS. It answers recursive UDP and TCP public-name queries; CHAOS version.bind and id.server queries time out. This is not evidence of a remotely exploitable bug. If untrusted LAN clients can reach the mower, restrict DNS access to the trusted management VLAN or disable the listener if unnecessary. No exploit payloads were sent.

Provisioning interface ​

The GOAT setup web bundle in the Android app contains a temporary access-point endpoint, http://192.168.0.1:8888/req.do, with the operations GetDevInfo and SetApConfig. This is useful for studying initial Wi-Fi provisioning, but the O500 does not expose that port on its normal LAN address. Treat it as a separate, setup-only interface rather than as an operational local API.

G1 Baize service UART ​

An authorized physical inspection of the GOAT G1 Baize found a valid, receive-only UART diagnostic stream at 115200 baud, 8 data bits, no parity, and one stop bit. It is a controller/boot diagnostic port rather than a Linux login console. A power-cycle produced:

text
g1-baize boot version=0.0.2
OTA status:11111111 file_size=0 crc_r=51eef14b crc_c=51eef14b
step:1
jump_sucess
Model: G1 Baize
Software Version: V0.3.12

This validates the mower TX pad and provides a hardware-assisted firmware research route. The corresponding controller RX pad remains unconfirmed. Do not send serial input or assert break/DTR/RTS on that controller port until its electrical role and command protocol have been established.

Horizon X5 main-console UART ​

On the same external ten-pin service connector, the confirmed main-console mapping is: pin 2 = 3.3 V UART TX, pin 3 = GND, and pin 4 = 3.3 V UART RX. Pin 1 is a 3.3 V supply rail. Pin 7 is the separate G1 Baize controller TX diagnostic output; pin 10 measured as a stable 1.8 V rail. Pins 8 and 9 remain unassigned. Keep the USB-UART VCC, DTR, and RTS leads disconnected.

At 115200 8N1, a receive-only capture exposed the main system console, including:

text
Horizon X5 ASIC BootROM
U-Boot 2022.10 (GOAT_release_X5)
Linux 6.1 on Horizon Robotics X5 lawn mower board
Ubuntu 22.04 LTS, systemd
console=ttyS0,115200n8

Pin 4 was initially tested through a 4.7 kOhm series resistor and did not interrupt U-Boot. Retesting it at 3.3 V without that resistor successfully stopped autoboot and produced the Hobot> U-Boot prompt. Thus the resistor attenuated the input enough to make the earlier negative result misleading. Pins 5, 6, and 9 still did not interrupt U-Boot under the one-character, series-resistor test and should not be treated as console RX.

The boot log also identifies application services such as ROS, eco_main_tools, Wi-Fi Manager, Qconnectmanager, Camera Provider, and an ADB init service. serial-getty@ttyS0 is started, but no login prompt was emitted on the initial receive-only capture. Although pin 4 can interrupt U-Boot, carriage returns sent after normal boot initially produced no response. Later inspection explained that the stock getty wrapper exits on ttyS0 unless a marker exists; a separate autostart bootstrap subsequently provided a working root shell. The boot chain reports verified boot and a dm-verity-protected read-only root filesystem; /data is mounted separately.

The same boot capture shows a hardware USB gadget being activated before the ADB init service starts. This establishes that the firmware can expose ADB over some USB device path, but does not identify the externally accessible female USB-A receptacle as that path. Do not connect that receptacle directly to a PC with an A-to-A cable: if it is a USB host port, that can create a VBUS power conflict. Establish its role first with a USB power meter or a non-invasive port inspection.

The external USB-A receptacle was subsequently attached to a Linux host and enumerated as 18d1:4e26, manufacturer hobot, product xj3. Its sole USB interface identifies itself as ADB Interface (class ff, subclass 42, protocol 01) with bulk IN and OUT endpoints. It is therefore a live ADB gadget path, despite the non-standard USB-A device receptacle. The host saw it as self-powered; no VBUS was measured at the mower receptacle before it was connected. adb devices -l reports serial 0123456789ABCD as unauthorized. No ADB shell or filesystem command has been sent. Standard ADB key approval requires a device-side confirmation path that has not been found.

This is the relevant console for trust-store and local-control research. A normal-Linux root shell was later established using the userdata autostart hook below. At the latest checkpoint, host-to-mower UART input had regressed and could stop U-Boot autoboot but could not reliably execute commands; see "Current hand-off state" before attempting maintenance.

Raw UART echo test (2026-09-27) ​

With U-Boot already at Hobot>, the FTDI adapter (/dev/ttyUSB1) was opened using a small PySerial raw-I/O tool at 115200 8N1, with RTS/CTS and XON/XOFF disabled and no local terminal echo. The tool logged transmitted bytes separately from bytes read back from the adapter. Each test byte appeared on the adapter RX after being sent:

text
TX 68             RX 68             # h
TX 65             RX 65             # e
TX 6C             RX 6C             # l
TX 70             RX 70             # p
TX 0D             RX 0D             # CR
TX 0A             RX 0A             # LF
TX 0D 0A          RX 0D 0A          # CR LF
TX 68 65 6C 70 0D RX 68 65 6C 70 0D # help + CR

No U-Boot command output followed the combined help + CR test. This capture proves only that matching bytes were read from the adapter RX; it did not establish whether they came from the X5 TX, an adapter/board loopback, or another electrical path. Command execution and echo source remain unresolved. The timestamped session log was kept outside the repository at /tmp/o500-uart.log.

UART echo-source verification plan ​

Use a 3.3-V-compatible logic analyzer or oscilloscope, with common ground, to observe the signal at the physical X5 pin 2 (TX) and pin 4 (RX) independently. Keep the mower and adapter supply rails separate. First disconnect the UART adapter from the mower and test an intentional adapter TX-to-RX loopback with a distinctive byte pattern. This establishes how host-side loopback appears; remove the jumper before reconnecting the mower. Then leave the adapter TX / buffer output physically disconnected from mower pin 4 while monitoring mower pin 2 through adapter RX. Sending the same pattern from the host must not make that pattern appear on RX; if it does, find a host-side loopback or wiring short before interpreting any device capture.

With the loopback excluded, connect mower pin 2 to adapter RX and capture both physical pins while sending a short, distinctive pattern to pin 4. A waveform on pin 4 confirms electrical delivery to the X5 input; the same decoded pattern newly appearing on the independently probed pin 2 confirms that the X5 TX output emitted it. It still does not prove the command interpreter accepted the byte. For the Linux shell test, suppress Linux TTY echo before starting Bash (the staged goat_serial_bootstrap_diag.sh does this) and use a visible command response such as id. Do not install that diagnostic variant until /data has a verified backup and tested rollback path. A software-only RX log from the host cannot, by itself, distinguish these electrical sources.

Normal-boot shell input test (2026-09-27) ​

A subsequent capture began before a normal power-on and recorded the complete BootROM, U-Boot, verified-boot, and Linux startup sequence. The userdata autostart service started, and the installed bootstrap emitted GOAT#. Bash also printed:

text
bash: cannot set terminal process group (...): Inappropriate ioctl for device
bash: no job control in this shell
GOAT#

The raw tool then sent id followed separately by CR, LF, and CRLF. Each byte sequence was returned on RX, but no id output or subsequent prompt appeared. The bootstrap runs stty -F /dev/ttyS0 115200 sane; sane enables canonical mode and TTY echo, with CR-to-LF input mapping. Thus the returned characters may be Linux TTY line-discipline echo and do not prove that Bash consumed the line. The Bash job-control warning confirms it has no controlling terminal; that disables job control, although it does not by itself explain why a simple command produced no output.

tools/goat_serial_bootstrap_diag.sh is a separate, not-installed diagnostic candidate. It disables TTY echo and Bash line editing so command output can be distinguished from echoed input. Do not install it until /data has a verified backup and a tested rollback path. The normal-boot capture remains outside the repository at /tmp/o500-uart-normal-boot.log.

A second normal boot was captured with the same raw monitor on 2026-09-27. The boot completed, the bootstrap explicitly stopped Serial Getty on ttyS0, and Bash again emitted GOAT# with the no-job-control warning. After that prompt, id + CR and then an empty CR each returned their exact bytes on RX; neither produced command output nor a fresh prompt. This confirms the failure is reproducible after boot completion and that the stock serial-getty is not the competing reader in this run. It still does not identify whether the kernel TTY input queue is reaching Bash. The second capture is /tmp/o500-uart-access-check.log outside the repository.

Bounded U-Boot prompt test (2026-09-27) ​

With Minicom closed and the raw PySerial monitor open on /dev/ttyUSB1, a single harmless invalid command, ZZZZZZZZ followed by CR, was transmitted while the operator reported the console at Hobot>. Adapter RX showed the same bytes, but U-Boot produced no Unknown command response. Sending LF separately also produced only matching bytes on adapter RX and no prompt or command output. This bounded test did not establish that U-Boot parsed the input line; the echo source remains unverified. The log is /tmp/o500-hobot-prompt-test.log outside the repository.

A paced read-only test followed with version, sending one byte every 100 ms and then CR. The raw monitor logged every TX byte and the same byte on RX, including 0D, but U-Boot emitted no version banner or other response after several seconds. The 100-ms pacing therefore did not restore command execution. The source of the matching RX bytes remains unverified. The log is /tmp/o500-paced-version.log outside the repository.

Line-editing probe and disconnect controls (2026-09-27) ​

The operator reported that disconnecting robot RX removed the echo, and that disconnecting robot TX separately also removed it. The operator also reported no connection between RX and TX on the robot side. These controls support dependence on both robot-side data connections; they do not locate the echo source inside that path. No oscilloscope or logic-analyzer measurement was available.

With the robot reconnected and reported at Hobot>, Minicom closed, and the raw monitor holding the port exclusively, two line-editing probes were sent. Each inserted Z and, 250 ms later, sent an erase key. Neither included CR/LF:

text
Probe         TX bytes    RX bytes
Z + Backspace 5A 08       5A 08
Z + Delete    5A 7F       5A 7F

No erase sequence such as 08 20 08, additional output, or new prompt was observed. This is consistent with a byte-mirroring path and provides no evidence of command-line editing; it does not establish a hardware bridge or exclude vendor-specific console behavior. The target input-buffer state is unknown, so the test does not establish that either inserted Z was removed. The host monitor was closed without sending a line terminator. The capture is /tmp/o500-line-edit-probe.log outside version control. RX timestamps in this tool are assigned when queued data is displayed, not at electrical arrival; the 250-ms intervals must not be interpreted as device response latency.

U-Boot and storage observations ​

The main console exposes U-Boot 2022.10-gf7d6ca3f, built 2025-05-27 as jenkins-GOAT_release_X5_3-13. The normal boot command is:

text
bootcmd=run ab_select_cmd;run avb_boot;
ab_select_cmd=eco_ab_select bootslot ...; setenv slot_suffix _${bootslot}
avb_boot=hb_avb_helper mmc 0 ${slot_suffix}

eco_ab_select records a boot attempt in the Android A/B metadata. Do not run it repeatedly for inspection because it can consume attempts. The observed normal slot was a. The U-Boot avb is_unlocked result is not reliable on this platform because it also prints read_is_device_unlocked not supported yet.

The 14.6 GiB eMMC uses GPT with separate vbmeta_a/b, boot_a/b, and system_a/b partitions, plus userdata as partition 16. In U-Boot partition numbers are parsed as hexadecimal: use mmc 0:10 for GPT partition 16, not mmc 0:16 (which means partition 22). The userdata filesystem is mounted as /data by Linux and contains vendor application data; it has no /misc/adb/adb_keys path.

/data is not the dm-verity-protected root filesystem. The normal boot log shows the verified root as EXT4-fs (dm-0) mounted read-only, followed later by a separate mount of the GPT userdata partition at /data and a systemd-growfs operation for it. U-Boot can also list that partition directly as ext4. It is therefore persistent writable storage, although normal Unix permissions and application-level controls may still apply.

boot_a is a U-Boot FIT image. iminfo reported the normal lawn-mower configuration boardid-0x2000 and a separate boardid-0x010202x5-evb-v2-recovery configuration containing a ramdisk. The latter was booted once with bootm 0x90000000#boardid-0x010202 after loading boot_a into RAM. It exposed no UART output, USB/ADB device, or normal LAN address during the observation window. This was a non-persistent runtime experiment; power-cycle to return to the normal slot. Do not assume the recovery configuration is a usable shell without further evidence.

btype bootargs generates a normal command line containing console=ttyS0,115200n8, hobotboot.slot_suffix=_a, and hobotboot.medium=MMC. A one-time, RAM-only experiment replaced updatebootargs with a wrapper that appended init=/bin/sh, then invoked hb_avb_helper for the known _a slot without calling eco_ab_select. It caused no observed persistent change, but did not produce a usable maintenance shell: the UART echoed id and Ctrl-C but returned neither command output nor a prompt. Treat this as inconclusive; do not regard it as root access. Power-cycle to restore the default U-Boot environment and normal boot.

Confirmed writable /data hooks ​

Read-only inspection of system_a explains the apparent but unusable Linux serial getty. The stock serial-getty@.service invokes /usr/bin/serial_tty.sh, whose effective policy is:

sh
if [[ $1 != "ttyS0" || -f /data/serial_to_be_run ]]; then
    /sbin/agetty ... "$1" "$2"
fi

Therefore serial-getty@ttyS0 starts and immediately exits by design unless the writable marker /data/serial_to_be_run is present. This exactly matches the boot log's repeated start/stop events and explains why no standard login prompt was emitted. Creating that marker is a persistent security-affecting change and does not by itself establish that a usable account/password is available. It was not used for the confirmed root shell.

The system's autostart.service runs /usr/bin/autostart.sh after ROS. That script launches every /data/autostart/*.sh with the argument start in the background. Since the systemd service does not set User=, these scripts run with the service manager's default root identity. This is the strongest confirmed persistent local-extension mechanism: an owner-controlled script in the writable userdata partition can run at each normal boot without modifying the dm-verity-protected system partition. The owner-approved 10-serial-root.sh bootstrap described below uses this hook. Treat it as persistent root access and remove it through UMS when reliable maintenance access is available if it is no longer wanted.

ADB is intentionally enabled by /etc/.usb_config (usb_adb_en). The service chain is adb.service -> /usr/bin/adb.sh -> /usr/bin/adbd.sh -> /etc/init.d/S92adb -> /etc/init.d/usb-gadget.sh start adb. The USB script builds the 18d1:4e26 hobot/xj3 configfs gadget observed on the host. The inspected wrappers contain no daemon argument or authorized-key override; the daemon's own key search path remains to be established before choosing an ADB-key approach.

Current hand-off state ​

The root shell, persistent bootstrap, and local-CA result below are confirmed historical results. At the last observation, the mower had returned to normal boot after testing. Its USB gadget enumerated as ADB device 0123456789ABCD, still unauthorized; verified system partitions were not modified. The confirmed connector mapping is pin 2 = 3.3-V TX, pin 3 = GND, pin 4 = RX.

Later raw-I/O testing observed matching bytes on adapter RX for individual characters and CR/LF, but the source was not isolated; a combined help + CR still produced no command output. Earlier tests also showed a complete version command on adapter RX without it executing. The normal-boot shell prompt appeared, but id produced only adapter RX bytes and no output; stty sane in the bootstrap likely accounts for Linux-side echo. No arbitrary input flooding was attempted. The installed serial-root bootstrap therefore cannot currently be re-entered or removed over the exposed UART, and UMS cannot currently be started by the host. The observed USB path is ADB only and remains unauthorized; it is not a Fastboot or DFU recovery route. Instrument or repair the pin-4 RX path before further device-side changes.

This U-Boot build has ext4write, which can create a file on an ext4 partition, including userdata (mmc 0:10). It can create persistent files without changing the verified system, but does not provide a comparably safe deletion workflow. Do not use it for further changes until a rollback path is defined and tested.

The owner-approved installation of /data/autostart/10-serial-root.sh first failed to produce a console response. The revised payload stops the competing serial-getty@ttyS0 service, sets the UART, and launches interactive Bash. It was installed through UMS controller 1 and verified after normal boot: the console displayed GOAT#, id returned uid=0(root), and serial-getty@ttyS0.service was inactive. The verified root remains read-only as /dev/dm-0; userdata is /dev/mmcblk0p16 mounted at /data. The script remains installed at /data/autostart/10-serial-root.sh, mode 0700, and opens no network service. Its payload is preserved as tools/goat_serial_bootstrap.sh.

When command input worked, U-Boot UMS controller 1 exported userdata with ums 1 mmc 0:10. Controller 0 did not enumerate; controller 1 exposed a 10.5-GiB ext4 userdata disk as /dev/sda. A read-only mount confirmed the partition layout. To remove the bootstrap when maintenance access is restored, run UMS controller 1, mount the exported userdata disk, remove only /data/autostart/10-serial-root.sh, sync, unmount cleanly, then stop UMS and boot normally. Do not create /data/serial_to_be_run; the bootstrap does not need it.

The single recovery boot and temporary init=/bin/sh test were runtime-only and cleared by power-cycling. The TLS test also restored the original CA byte-for-byte and removed the temporary router redirect. The verified system partitions were not modified.

Useful read-only U-Boot commands for continuation are:

text
version
printenv bootcmd ab_select_cmd avb_boot
mmc list
part list mmc 0
part size mmc 0 boot_a bootimagesize
part start mmc 0 boot_a bootimageblk
mmc read ${kernel_addr} ${bootimageblk} ${bootimagesize}
iminfo ${kernel_addr}

Avoid saveenv, all mmc write/mmc erase/avb write_part operations, DFU flashing, and the update_* environment commands. The captured current boot log is /tmp/goat-g1-reconnect-boot.log while this workspace remains available.

JMQ test binding ​

The O500 uses TLS/443 for both JMQ and the NG-IoT API, but they have different hostnames. Use distinct local or virtual IPs in a test VLAN so they can be routed to different listeners without a port collision. The Bumper server repository supports a dedicated MQTT bind for this purpose; run this example there, not from this research-only repository:

sh
BUMPER_LISTEN=<LOCAL_API_LISTEN_IP> BUMPER_MQTT_LISTEN=<LOCAL_MQTT_LISTEN_IP> BUMPER_MQTT_PORT=443 \
  python -m bumper

This only prepares the listener. The certificate trust path and redacted MQTT framing have been demonstrated. Use a source-restricted redirect on an isolated VLAN and keep a tested rollback to the original CA and normal routing.

Next research milestones ​

  1. Restore reliable UART RX or another owner-authorized maintenance path before further device-side changes.
  2. Implement a metadata-minimizing JMQ command/status adapter using the observed TLS and framing behavior.
  3. Obtain semantic command and status mappings without retaining credentials or application payloads.
  4. Add minimal NG-IoT API responses needed for device registration and state.
  5. Test local operation on an isolated VLAN before blocking cloud access.