A scripted workbench for VESC motor controllers: read, write and verify configuration, drive LispBM, and diagnose problems — all over a phone's Bluetooth bridge, with no USB and no opening the enclosure.
Built while getting a LaCroix Nazaré (FOCBOX Unity) onto firmware 7 and keeping a discontinued DAVEGA X display working. That shim ships here as the worked example, but the tooling is the point.
Maintained by Isaac Rowntree. MIT. Contributions welcome — see CONTRIBUTING.md.
You are in the right place if you have hit one of these:
- "supported vesc firmware versions 5.x to 6.x - press any button to restart" — a DAVEGA X stuck in a boot loop after a VESC firmware 7 update. The shim fixes it without downgrading. DAVEGA is discontinued, so no display-side update is coming.
- You want to script VESC config but have no USB access — the ESC is sealed in a deck or enclosure, and VESC Tool's CLI is serial-only. This drives it over the phone app's Wireless Bridge to Computer (TCP) instead: how that works.
- You want VESC settings in version control — read to XML, diff, apply, verify, repeat. Both motor sides of a dual ESC.
- You are writing LispBM for a VESC and want to run it in the real interpreter before uploading it to something with wheels on it.
- Your throttle stopped working after a LispBM script ran —
uart-startpermanently writesapp_to_use = APP_NONE.make upload-lispguards against it; why. - You want to know what the remote is actually sending —
make ppm-watchtells "the remote is not transmitting" apart from "the decoder is not running", which look identical in the GUI.
Tested on a FOCBOX Unity (dual motor, internal CAN) on a LaCroix Nazaré, firmware 7.00. The tooling is hardware-agnostic; the profile is the only board-specific part.
VESC Tool's CLI is serial-only: --vescPort calls connectSerial(), and it
rejects an IP and never completes the handshake over a socat PTY. But
--loadQml runs arbitrary QML inside the application, with the VescIf
singleton in scope — and VescIf.connectTcp() is invokable. Point that at the
phone app's TCP bridge and the whole configuration API is scriptable:
your laptop --TCP--> phone (VESC Tool app) --BLE--> ESC
make pull # read both motor sides' configs to XML
make apply # write them back, then verify by reading them againdocs/connecting.md is the how — a copy-pasteable
connect.qml that works without the rest of this repo, the VescIf API surface
worth knowing, and the connection failure modes.
Important
setMcconf(true) is required — with check=false the ESC silently ignores the
write. And wait for the firmware string before reading anything:
isPortConnected() goes true seconds before the ESC has sent its parameters,
and reads in that window return defaults that look exactly like a wiped
controller. Same after any reboot.
make upload-lisp LISP=path/to/script.lisp # upload and run
make lisp-stats # heap, CPU, globals
make lisp-stop / lisp-erase # stop or remove
make test-lisp # run it in the real interpreterThere is an integration harness too: lisp/tests/test_reader.lisp drives the
real UART reader through a fake serial line that can split a frame header
across reads, so framing and resync are tested rather than assumed.
Scripts are tested by running them in the upstream LispBM REPL (Docker),
not by transcribing them into another language. lisp/minify.py strips
them before upload, because upload happens in 384-byte chunks with a 1-second
per-chunk timeout and size genuinely matters.
Globals double as a telemetry channel: lispGetStats returns them, so a script
can report internal counters without needing print output to work.
make davega-debug SECS=60 # health check with a verdict
make faults # stored fault history + live values, both sides
make ppm-watch # live remote readout, prints only on change
make ppm-cal # guided calibration: neutral / full throttle / full brakeppm-watch distinguishes "the remote is not transmitting" from "the decoder is
not running" — both look identical in a GUI.
make motors-off / motors-on # app-disable-output, no config write
make reboot # stop LispBM and restart the app layer
make lisp-erase # back to stock behaviour
make check # bridge up? desktop VESC Tool closed?motors-off lets you work on a live board with the display running and the
motors inert, without touching configuration.
A DAVEGA X on VESC 7 refuses to start:
supported vesc firmware versions 5.x to 6.x - press any button to restart
The telemetry protocol did not change — lisp/tests/protocol-diff.sh proves it
against upstream on every CI run:
| Surface | 6.00 | 7.x | Same? |
|---|---|---|---|
COMM_GET_VALUES payload |
25 fields | 25 fields | ✅ byte-identical |
COMM_FW_VERSION structure |
— | — | ✅ identical |
FW_VERSION_MAJOR/MINOR |
6/00 | 7/01 | ❌ the only difference |
So the display is not failing to parse anything. It is refusing to talk, on two
bytes. The shim proxies every command to the firmware's own decoder via
cmds-proc and rewrites only those two, recomputing the CRC — 27 lines of Lisp
for the rewrite itself, 178 for the whole shim including the UART reader and
its debug counters.
See docs/davega-shim.md. For the display itself — its
hardware, its /config.json settings, and what is still downloadable from the
vendor — see docs/davega-x.md.
The shim keeps the stock display working. If you would rather replace it, the
DAVEGA X is an ESP32 running MicroPython that runs a user start.py before
its own app — so a new dashboard is one file, and holding UP at boot puts the
stock one back.
Ten themes, each with its own layout rather than its own palette: an analogue tachometer, hexagonal shards, hairline arcs, one enormous numeral, concentric rings, a shift-light rail, a power-flow meter. Pick one from the display's own menu, or:
make davega-install # push the dashboard, as precompiled bytecode
make davega-theme THEME=nazare # choose a layout
make davega-display MODE=day # or nightThose pictures are not mockups. They are the real screen code run through a host-side stand-in for the panel, saved as the pixels it produced — which is also how the tests work:
make gui-test # 642 checks, no hardware
make test-device # the whole dashboard in real MicroPython, in docker
make mockups # every screen x every theme x day/night, to HTMLThe panel has fill_rectangle, pixel and writeblock and no line, circle or
polygon, and on the device a draw call costs 2.9 ms whatever its size. So an
arc drawn a rectangle per column costs 481 ms — four times the budget for a
whole frame — and the curves are instead composed into a buffer and pushed a
band at a time, about 6 ms for 240×20 pixels. See davega/README.md
for the design, and docs/theme-layouts.md for what the
hardware will and will not do.
Three things live here, split by where the code runs:
vesc/ scripts that drive VESC Tool from your laptop
qml/ the VescIf scripts behind `make pull`, `apply`, `ppm-watch`, ...
profiles/ connection details per known-good board
lisp/ code that runs ON the motor controller
src/ the shim, the flight recorder, and their pieces
tests/ run in the real upstream LispBM interpreter, in Docker
model/ a Python model of the shim's protocol, and its tests
davega/ code that runs ON the display (and the tools that build it)
gui/ ships verbatim to /gui - the same package name on both sides
start.py ships to /start.py; the escape hatch
harness/ a host-side stand-in for the panel, so tests need no hardware
tests/ 642 checks, and device/ runs the lot in real MicroPython
tools/ WebREPL client, mockup and screenshot generators
docs/ is the writing: findings,
connecting, the display,
what the panel will draw,
this board — where every default here came from —
and keeping the firmware alive, which records what
the vendor still serves, what it hashes to, and what may be passed on.
- VESC firmware 6.06+ for
cmds-proc(developed on 7.00) - VESC Tool desktop — never opened as a GUI, it is just the Qt runtime the
scripts need. macOS and Linux paths are both detected; override with
VESC= - VESC Tool on a phone, connected to the ESC over Bluetooth
- Docker, for the Lisp tests
- Both devices on the same network, without client isolation (most guest Wi-Fi has it — use a phone hotspot instead)
Full walkthrough, including a standalone connect.qml you can use without this
repo: docs/connecting.md.
1. On the phone — connect to the ESC over BLE as normal, then Start page → Wireless Bridge to Computer (TCP) → Activate Bridge. Note the phone's IP; the bridge listens on port 65102 and accepts one client at a time, so close desktop VESC Tool.
2. On the laptop — put the address in vesc/profiles/local.mk (gitignored):
HOST ?= 192.168.1.100 # phone running the bridge
PORT ?= 65102
CANID ?= 124 # second motor thread, if you have one3. Check before you do anything else:
make check # bridge reachable? desktop VESC Tool closed?
make probe # connect, report firmware and LispBM stats
make help # everything elsemake check tells the two failure modes apart in a second, instead of leaving
you to interpret a two-minute timeout.
Every target takes PROFILE=:
make pull PROFILE=vesc/profiles/local.mk
make davega-debug PROFILE=vesc/profiles/nazare-unity.mkvesc/profiles/ holds one file per known-good setup — connection details and the
second motor's CAN id. Deliberately not motor tuning: current limits and
gearing are specific to your hardware and copying someone else's is how motors
and packs get damaged. Run the detection wizard.
| Config read/write/verify over BLE | ✅ |
| LispBM upload, test, diagnose | ✅ |
| DAVEGA version gate bypass | ✅ telemetry live on a Unity, FW 7.00 |
COMM_FORWARD_CAN to a second motor |
✅ verified on hardware — findings |
| Traction control | ✅ confirmed in motion, tc_max_diff 6000 on grass |
| Hardware tested | FOCBOX Unity only |
| DAVEGA X WebREPL scripts | ✅ run on the display — settings read and written, gearing corrected — docs |
| Replacement dashboard | ✅ installed and running on the display, ten layouts, bytecode — design |
| Dashboard tested in the real interpreter | ✅ MicroPython 1.14 in Docker, end to end — how |
| Tested in motion | ✅ ridden, and it holds up |
This writes motor controller configuration and can enable or disable motor
output. Wheels off the ground for anything involving detection, app config or
app-disable-output. Verify the throttle before riding.
Connection problems — bridge unreachable, reads coming back as defaults — are in docs/connecting.md.
Board behaviour:
| Symptom | Cause | Fix |
|---|---|---|
Throttle dead after running Lisp that calls uart-start |
app_to_use flashed to APP_NONE |
make apply-appconf — and make upload-lisp now prevents it |
| PPM settings will not stick | ctrl_type = 0 rejects the sub-config |
make apply-appconf primes it automatically |
| A write "succeeds" but nothing changes | setMcconf(false) |
Always setMcconf(true) |
did you forget to upload the code |
lispWriteCode does not land code |
make upload-lisp (uses CodeLoader.lispUploadFromPath) |
| A Lisp context dies silently | (var t ...) shadows LispBM's t, which never resolves from the environment |
Rename the variable |
The reasoning behind each, and the firmware source it comes from: docs/findings.md. Still-open problems: docs/known-issues.md.
VESC and LispBM by Benjamin Vedder. DAVEGA by Jan Pomikalek. This project is not affiliated with either.
