Files
firmware/docs/notes/00-overview.md
Oleksandr ShyshatskyiandClaude Opus 5 28eb20f3da Package numpy, because klippy does not start without it
extras/stepper_resonance_tester.py opens with a bare `import numpy as np`;
klippy.py:122 walks EVERY config section through a load_object whose
importlib.import_module is not wrapped in except ImportError; printer.base.cfg
includes FlashForge's printer.vibration.cfg, which declares
[stepper_resonance_tester]. Since klippy moved to our own CPython 3.13 that has
not been a printer missing resonance testing. It has been a klippy that never
reaches ready.

So this is not a scientific extra. anvil-klipper depends on anvil-python-numpy
the way it depends on cffi, except that the others mean "klippy imports this"
and this one means "klippy starts". It also gives back SHAPER_CALIBRATE and
TEST_RESONANCES, dead since the same switch, and the VFA calibration behind
STEPPER_RESONANCE_FACTORY_CALIBRATE that docs/notes/44-vfa-calibration.md
traces end to end.

WHAT WAS NOT OBVIOUS, in the order it bit.

numpy 2.x builds with meson-python, and meson has never heard of
_PYTHON_SYSCONFIGDATA_NAME -- the trick every other python-* recipe here leans
on. pkg_mesoncross writes a real cross file instead, naming the $PKG_XW
wrappers rather than the raw toolchain: the wrappers carry -EL -mnan=2008, and
a cross file that reaches past them builds a tree that passes every gate which
only reads symbols and is refused at exec() on the printer. pkg_pywheel grew a
PKG_PY_PIP_ARGS branch to pass the file as --config-settings, which is the one
thing PEP 517 does standardise.

The cross file's `python =` is the BUILD python, and that is not a mistake:
meson introspects an interpreter by running it and asking sysconfig, and
pkg_pytarget has already made that sysconfig answer for mipsel. The old trick
does reach meson, just not by its own mechanism.

numpy's meson.build hard-errors below gcc 8.4 and ours is 7.2. The gate is the
SciPy Toolchain Roadmap's support statement, not a requirement of the code:
this toolchain compiles if constexpr, structured bindings, std::optional,
std::string_view, and pocketfft_hdronly.h -- 3635 lines of the heaviest
template C++ in the tree, and the half np.fft.rfft needs. Probed before the
floor was lowered, and lowered rather than removed, behind a guard that fails
loudly if upstream moves the check. 19 extension modules, every one at e_flags
0x70001405/0x70001407, 14MB shipped after pruning tests/, f2py/, stubs and
static libs.

meson-python is self-hosting, so pkg_buildpython runs meson and ninja just to
install the backend -- hence ninja-build in the image and PYPKG_HOST_LIST in
dependency order rather than alphabetical. And pip puts meson and cython in
work/.py-host/bin, which nothing had ever looked in: without that on PATH the
backend fails to install with `meson executable "meson" not found` while pip
list shows it right there.

THE GATE THAT WOULD HAVE CAUGHT ALL OF IT is qa/replica/test_klippy_extras_import.py:
walk the real /usr/data/config include graph, map every section to its extra,
import each on the printer's own interpreter. It needs no MCU, because config
load happens before klippy:mcu_identify. Nothing asked that question before,
which is how two comments in this repo came to assert the module guarded its
own import. test_numpy.py asks for numeric answers rather than a module object
-- linalg.lstsq specifically, because -Dallow-noblas selects the bundled
lapack_lite and _r2_sinusoidal_fit calls it every calibration round. And
test_install.py's include check read only ff-*.cfg, so FlashForge's four --
printer.vibration.cfg among them -- were never asked about at all.

ff-toolchange.cfg wraps the two search commands in _FF_SHAPER_PREP, as
SHAPER_CALIBRATE already was. STEPPER_RESONANCE_FACTORY_CALIBRATE is
deliberately NOT wrapped: it is a gcode_macro, not a registered command, and
same-named sections merge under RawConfigParser(strict=False) -- a wrapper
would not wrap it, it would BE it, with rename_existing pointing at itself. It
needs none, since its body is two calls to a command that is wrapped. No
SAVE_CONFIG in any macro either; it restarts klippy and stays the operator's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8
2026-08-31 13:42:57 +03:00

4.8 KiB
Raw Permalink Blame History

firmwareExe analysis — overview

Analyzed binary: firmwareExe (20.9 MB, ELF 32-bit MIPS32r2, Ingenic XBurst2 SoC, Linux 3.10.14, not stripped — full C++ symbols). Machine identifies itself as "Creator 5 Pro", cloud backend api.voxelshare.com.

The single most important finding

The printer already runs Klipper. firmwareExe is NOT the motion firmware — it is the LVGL touchscreen UI + orchestration daemon. It:

  • starts Klipper with /usr/prog/klipper/start.sh &
  • talks to Klipper's native API socket at /tmp/uds (stock webhooks JSON: gcode/script, objects/query, objects/subscribe, …)
  • implements everything "smart" (toolchange orchestration, print lifecycle, filament runout auto-swap, calibration wizards, cloud/camera/LAN APIs) app-side, by sending G-code strings.

The Klipper fork itself ships no START/END/PAUSE/RESUME/CANCEL macros and no toolchange motion code — a print started from Mainsail/Moonraker gets none of that behaviour unless something re-provides it. That something is this repo: ff_toolchange.py + the config/macros.

System architecture

firmwareExe (touchscreen app)
 ├─ LVGL UI on /dev/fb0 + touch /dev/input/event2
 ├─ KlipperAPI ──/tmp/uds──► Klipper fork (/usr/prog/klipper)
 │                            ├─ mcu           main board: X/Y/Z, bed
 │                            ├─ eboard        /dev/ttyS5 carriage extruder driver
 │                            ├─ eheaterboard  /dev/ttyS4 4 hotend heaters + 24V
 │                            └─ levelboard    /dev/ttyS7 fixed under-bed cylinder
 ├─ DryingService ──/dev/ttyS3──► filament drying box (own MCU, not Klipper)
 ├─ OrcaServer / MQTT cloud / camera
 └─ /usr/data/firmwareRes/config/*.json — per-unit calibration + state

Printer facts

  • 4-tool toolchanger: docked heads T0–T3 at X≈297 (250/280 are the staging and approach waypoints, not the docks), picked up by the X carriage with a lock motor. temperature_sensor motor_value exists (printer.motor.cfg) but the app only ever logged it -- no grab/release decision reads it; see 25-app-vs-klipper-ownership.md.
  • Bed ~250×250, 10×10 bed mesh from the carriage eddy ([probe], eboard:PG0). Nozzle XY/Z offset calibration uses a different sensor: the fixed inductive cylinder under the bed ([e_stop X/Y/Z] on levelboard:PD0).
  • 4 filament channels, each a direct-drive extruder on its own head — there is no feed hub; manual_stepper gear_stepper is the tool LOCK motor (see 47-filament-load-recovered.md). Presence sensors fd_ex0..3, motion sensors fm_ex0..3.
  • Chamber heater + 4 fans + LED, front/top door switches.

Method

The binary is not stripped, so targeted questions are answered by reading symbols and disassembling straight from the file (capstone), rather than a full Ghidra project — Ghidra's auto-analysis wrongly marked the logging helpers noreturn and truncated ~916 functions (29k bogus CALL_RETURN overrides), so its decompile of exactly the interesting CommMgr methods was misleading until repaired. Addresses cited in these notes are verified against the live binary.

Notes map

Files ending -recovered are reconstructions of the stock app's behaviour; the matching -port note says what this repo actually shipped.

  • 10-hardware.md — MCUs, serial ports, Klipper objects the UI expects
  • 20-klipper-fork.md — fork delta: custom commands, the Tn interception
  • 25-app-vs-klipper-ownership.md — which side owns which behaviour, and what is ported
  • 30-toolchange.md — dock/grab/release sequences, sensors, mounted-tool state
  • 40-offsets.md — where per-unit numbers live; per-tool offsets; the absolute print Z offset
  • 44-vfa-calibration.md — VFA (motor torque-ripple) compensation: the stock chain, what applies it, and the numpy gap that stopped klippy booting
  • 45-tool-offset-calibration.md — the Klipper port: storage layout, commands, guards
  • 46-offset-calibration-recovered.md — the app's calibration sequence it was ported from
  • 47-filament-load-recovered.md / 48-filament-load-port.md — load/unload/purge
  • 49-runout-recovered.md — runout and clog handling (ported: ff-runout.cfg)
  • 50-print-lifecycle.md — start/pause/resume/cancel/complete, address-verified
  • 50a-nozzle-clean-recovered.md / 50b-nozzle-clean-port.md — the pre-print nozzle clean
  • 51-pa-calibration-recovered.md — automatic pressure advance: the sweep, and how to port it
  • 60-background.md — filament system, calibration flows, config keys (background context)
  • 70-error-codes.md — full E-code table (behavioral spec)
  • 80-s6-migration.md — the prefix root and s6 supervision plan
  • 85-packaging.md — apk vs opkg, why opkg won, and the .ipk proof of concept