DECTmo is a DECT NR+ video and movement stack for the Freenove 4WD car.
Current production scope:
Video:
ESP32-P4-EYE camera -> HW H.264 encoder -> SPI -> nRF9151 TX
-> DECT NR+ carrier 1669 -> nRF9151 RX -> UART H.264 -> PC/Raspberry Pi viewer
Movement:
nRF9161 TX DK buttons -> DECT NR+ carrier 1668 -> nRF9161 RX
-> UART1 921600 -> Raspberry Pi Pico -> Freenove motors
The Raspberry Pi 5 USB-camera/JPEG UART video path remains as a fallback. The active video path is ESP32-P4-EYE H.264 over SPI because it gives much lower latency and better DECT bandwidth use.
Video TX: nRF9151 DK serial 1051219070, COM21/COM20
Video RX: nRF9151 DK serial 1051202270, COM9/COM8
Movement TX: nRF9161 DK serial 1050997258, COM17/COM18
Movement RX: nRF9161 DK serial 1050926057, COM5/COM6
ESP32 camera: ESP32-P4-EYE, normally COM19 when USB connected
Manual .hex images for this scope are stored locally in:
firmware_hex/2026-05-26/
These .hex files are ignored by Git. See firmware_hex/README.md for
checksums and manual reflashing commands.
Camera sensor: OV2710 through MIPI CSI
Camera capture: 1280x720
Encoded stream: 1008x576 H.264
Frame rate target: 10 FPS
Bitrate target: 450 kbps
GOP: 10
QP range: 30..44
ESP32 SPI packet: 64 bytes
ESP32 SPI payload: 54 bytes
SPI clock: 8 MHz
DECT video carrier: 1669
DECT MCS: 4
DECT TX power field: 13
Viewer UART baud: 921600
The nRF receiver reassembles H.264 NAL units, waits for SPS/PPS/IDR sync, and drops incomplete NALs after sequence gaps so corrupted frames do not reach the decoder.
ESP32-P4-EYE to video TX nRF9151:
ESP32 GPIO6 SCLK -> nRF9151 P0.16
ESP32 GPIO8 MOSI -> nRF9151 P0.17
ESP32 GPIO10 MISO -> nRF9151 P0.19
ESP32 GPIO7 CS -> nRF9151 P0.07
ESP32 GPIO34 READY -> nRF9151 P0.06
ESP32 GND -> nRF9151 GND
ESP32-P4-EYE camera control pins used by firmware:
GPIO11 camera XCLK
GPIO12 camera power enable
GPIO13 camera SCCB/I2C SCL
GPIO14 camera SCCB/I2C SDA
Use common GND. Keep SPI wires short.
When all three video devices are on the laptop, use reset-sync:
python experimental/h264_dect/tools/h264_usb_receiver.py COM9 --baud 921600 --reset-nrf 1051219070 --reset-esp COM19 --stdout | ffplay -fflags nobuffer -flags low_delay -framedrop -probesize 32 -analyzeduration 0 -f h264 -If COM9 shows no stream, try COM8; only one CDC port carries the H.264
output.
When the ESP32-P4-EYE and video TX nRF9151 are externally powered, plug only the video RX nRF9151 into the PC and run:
python experimental/h264_dect/tools/h264_usb_receiver.py COM9 --baud 921600 --stdout | ffplay -fflags nobuffer -flags low_delay -framedrop -probesize 32 -analyzeduration 0 -f h264 -Validate a received stream:
python experimental/h264_dect/tools/h264_usb_receiver.py COM9 --baud 921600 --frames 180 --output dect_check.h264 --report-every 60
ffprobe -v error -f h264 -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 dect_check.h264
ffmpeg -v error -f h264 -i dect_check.h264 -f null -Expected:
codec_name=h264
width=1008
height=576
No ffmpeg output means no decode errors.
Movement uses two nRF9161 DK boards:
Movement TX 1050997258: DK buttons -> DECT NR+ carrier 1668
Movement RX 1050926057: DECT NR+ carrier 1668 -> UART1 -> Pico UART0 GP1
Button mapping:
BTN4 -> forward
BTN3 -> right
BTN1 -> backward
BTN2 -> left
RX emits Pico motor bridge commands:
direction 1 -> F 34 250
direction 2 -> WHEELS 34 0 0 -34 250
direction 3 -> B 34 250
direction 4 -> WHEELS -34 0 0 34 250
Direct RX nRF9161 to Pico wiring:
nRF9161 DK UART1 TX / P0.29 / Arduino D1 -> Pico GP1 / UART0 RX
nRF9161 DK GND -> Pico GND
Pico GP0 / UART0 TX -> nRF9161 DK UART1 RX / P0.28 / Arduino D0 (optional)
The Pico firmware in control/pico_micropython_bridge/main.py listens on USB
serial and UART0 GP1 at 921600 baud. Bench DECT button reception is verified;
final car/Pico motor test is the next hardware check.
Open an nRF Connect SDK terminal or set the toolchain environment first:
$tool='C:\ncs\toolchains\936afb6332'
$env:PATH="$tool\opt\bin;$tool\opt\bin\Scripts;$tool\usr\bin;$tool\nrfutil\bin;$env:PATH"
$env:ZEPHYR_TOOLCHAIN_VARIANT='zephyr'
$env:ZEPHYR_SDK_INSTALL_DIR="$tool\opt\zephyr-sdk"Video TX nRF9151:
C:\ncs\toolchains\936afb6332\opt\bin\python.exe -m west -z C:\ncs\v3.3.0\zephyr build -b nrf9151dk/nrf9151/ns -d experimental/h264_dect/nrf9151_dect_video/build_tx -p always --extra-conf prj_tx.conf experimental/h264_dect/nrf9151_dect_video
nrfutil device program --firmware experimental/h264_dect/nrf9151_dect_video/build_tx/merged.hex --serial-number 1051219070 --family nrf91 --options chip_erase_mode=ERASE_RANGES_TOUCHED_BY_FIRMWARE,verify=VERIFY_NONE,reset=RESET_SYSTEMVideo RX nRF9151:
C:\ncs\toolchains\936afb6332\opt\bin\python.exe -m west -z C:\ncs\v3.3.0\zephyr build -b nrf9151dk/nrf9151/ns -d experimental/h264_dect/nrf9151_dect_video/build_rx_9151 -p always --extra-conf prj_rx.conf experimental/h264_dect/nrf9151_dect_video
nrfutil device program --firmware experimental/h264_dect/nrf9151_dect_video/build_rx_9151/merged.hex --serial-number 1051202270 --family nrf91 --options chip_erase_mode=ERASE_RANGES_TOUCHED_BY_FIRMWARE,verify=VERIFY_NONE,reset=RESET_SYSTEMMovement TX nRF9161:
C:\ncs\toolchains\936afb6332\opt\bin\python.exe -m west -z C:\ncs\v3.3.0\zephyr build -b nrf9161dk/nrf9161/ns -d experimental/h264_dect/nrf91_movement_dect/build_tx_9161 -p always --extra-conf prj_tx.conf experimental/h264_dect/nrf91_movement_dect
nrfutil device program --firmware experimental/h264_dect/nrf91_movement_dect/build_tx_9161/merged.hex --serial-number 1050997258 --family nrf91 --options chip_erase_mode=ERASE_RANGES_TOUCHED_BY_FIRMWARE,verify=VERIFY_NONE,reset=RESET_SYSTEMMovement RX nRF9161:
C:\ncs\toolchains\936afb6332\opt\bin\python.exe -m west -z C:\ncs\v3.3.0\zephyr build -b nrf9161dk/nrf9161/ns -d experimental/h264_dect/nrf91_movement_dect/build_rx_9161 -p always --extra-conf prj_rx.conf experimental/h264_dect/nrf91_movement_dect
nrfutil device program --firmware experimental/h264_dect/nrf91_movement_dect/build_rx_9161/merged.hex --serial-number 1050926057 --family nrf91 --options chip_erase_mode=ERASE_RANGES_TOUCHED_BY_FIRMWARE,verify=VERIFY_NONE,reset=RESET_SYSTEMESP32-P4-EYE:
. C:\Espressif\tools\Microsoft.v6.0.1.PowerShell_profile.ps1
idf.py -C experimental/h264_dect/esp32_p4_h264_spi build
idf.py -C experimental/h264_dect/esp32_p4_h264_spi -p COM19 flashexperimental/h264_dect/esp32_p4_h264_spi/
ESP32-P4-EYE MIPI camera capture, H.264 encoding, SPI slave transport.
experimental/h264_dect/nrf9151_dect_video/
nRF91 DECT video TX/RX firmware. TX reads ESP32 SPI packets. RX writes H.264 records to UART.
experimental/h264_dect/nrf91_movement_dect/
nRF91 DECT movement TX/RX firmware. TX reads DK buttons. RX writes Pico movement commands.
experimental/h264_dect/tools/h264_usb_receiver.py
PC/Raspberry Pi receiver tool. Reads UART H.264 records and pipes Annex-B to ffplay.
control/pico_micropython_bridge/main.py
Pico motor bridge firmware.
control/pi5_controller/
Legacy Raspberry Pi movement runtime, JPEG video sender/receiver, and local web control.
Fallback USB camera sender:
python3 control/pi5_controller/video_sender.py --video-uart /dev/ttyACM3 --quality 12 --fps 4 --verboseFallback browser receiver:
python control/pi5_controller/video_receiver_web.py --uart COM14 --dtr --rtsOpen:
http://127.0.0.1:5001
python -m compileall control tests
python -m unittest discover -s tests- Lift car before movement tests.
- Video and movement must stay on different carriers: video
1669, movement1668. - Motor power must come from the car power system, not Raspberry Pi USB.
- Do not run legacy
web_control.pyandmovement_runtime.pytogether; both need Pico serial. - Keep
AGENTS.mdlocal only. It is operator context, not product source.