This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A collection of independent C tools that drive or configure camera motors
(pan/tilt, zoom, focus, iris) on IP-camera SoCs - Xiongmai, HiSilicon, and
Ingenic T31. There is no shared library and no top-level build. Each directory
is self-contained, and api/ is a design document for a future daemon that
would unify them. Most tools are single-file hardware utilities; pelcodtui/
is a multi-file ncurses application with host-runnable tests.
Most tools require real motor hardware and have no tests. pelcodtui tests its
protocol, profiles, state storage, and UART output on a pseudo terminal. The
automated check is .github/workflows/gcc-compat.yml, a required status
check (GCC Gate) on master. Know precisely what it does and does not cover:
- It cross-compiles seven tools on GCC 12 and GCC 14, then builds and tests
pelcodtuinatively.an41908ais excluded, because it needs the proprietary Hi3516CV500 MPP SDK. Changes underan41908a/get no CI coverage and must be built by hand. A green gate says nothing about them. - It is a compiler gate, not a toolchain gate. It uses Debian's glibc cross-compilers (
arm-linux-gnueabihf,mipsel-linux-gnu), not the musl OpenIPC or vendor toolchains these tools actually ship against. That is deliberate: the defects it exists to catch — implicit declarations, format bugs, int conversions — are language-level and libc-independent, whereas the real toolchains are 100 MB–3.7 GB downloads and the vendor ones are GCC 4.4–6.3, too old to catch what a modern compiler rejects. The gap it leaves is a musl-only build break (a header difference between musl and glibc) passing CI and failing on the real toolchain. - It proves the tree still compiles and runs
pelcodtuihost tests. It does not produce a deployable binary, and it says nothing about whether a motor moves.
Because every tool talks to different vendor hardware, the tools cannot be built or run on the development host — they are cross-compiled for ARM and executed on a camera over SSH.
There is no top-level Makefile. Build each tool in its directory. The legacy
hardware tools are cross-compiled; pelcodtui also builds and tests natively.
cd pelcodtui && make testDirectories with a Makefile — xm-kmotor/, xm-uart/, an41908a/, i2c-motor/sigmastar-ssc338q/:
cd xm-kmotor && make # builds ALL variants (see gotcha below)
cd xm-kmotor && make xm-kmotor-openipc # single variant — prefer this
cd i2c-motor/sigmastar-ssc338q && make CC=arm-openipc-linux-musleabi-gcc
make cleanThe -orig / -openipc suffix selects the toolchain via a target-specific CC:
| Target suffix | CC the Makefile invokes |
Used by |
|---|---|---|
-openipc |
arm-openipc-linux-musleabi-gcc |
xm-kmotor, xm-uart |
-orig |
arm-xm-linux-gcc |
xm-kmotor (Xiongmai vendor) |
-orig |
arm-hisiv510-linux-gcc |
xm-uart (HiSilicon vendor) |
-orig |
arm-himix200-linux-gcc |
an41908a (HiSilicon vendor) |
Build gotcha (xm-kmotor/): the pattern rule (xm-%: main.o) shares a single main.o across variants. GNU make inherits the target-specific CC into prerequisites, so plain make compiles main.o once with the -orig toolchain and then links that same object into the -openipc binary. make -n in xm-kmotor/ shows it plainly. Build one variant at a time with make clean in between. xm-uart/ compiles main.c per variant and doesn't have this problem; it also has a native xm-uart-motors-host target for running under uart-bridge.
camhi-motor/, i2c-motor/, and ingenic-motor/ have no Makefile — compile the single source directly with the right cross-compiler for that SoC (see the arch warning below).
Nothing here is packaged for a normal distro; both toolchain families come from OpenIPC GitHub releases.
OpenIPC Buildroot SDKs (provides the -openipc targets) — https://github.com/OpenIPC/firmware/releases/tag/toolchain, one toolchain.<vendor>-<family>.tgz per SoC family, ~100 MB each, GCC 13.3.0 from Buildroot 2024.02.10:
curl -LO https://github.com/OpenIPC/firmware/releases/download/toolchain/toolchain.xiongmai-xm530.tgz
tar xzf toolchain.xiongmai-xm530.tgz
cd arm-openipc-linux-musleabi_sdk-buildroot && ./relocate-sdk.sh # required — paths are baked in
export PATH="$PWD/bin:$PATH" # gives arm-openipc-linux-musleabi-gccrelocate-sdk.sh is not optional; the SDK ships with absolute paths from the build machine. There is also a toolchain-asan release with sanitizer-enabled variants. To build a toolchain from scratch instead, OpenIPC/firmware does it via make <board>_defconfig && make toolchain (see general/toolchain.mk, which sets BR2_TOOLCHAIN_BUILDROOT_VENDOR="openipc").
Watch the architecture — not every target is ARM. ingenic-motor targets the Ingenic T31, which is MIPS little-endian. toolchain.ingenic-t31.tgz unpacks to mipsel-openipc-linux-musl_sdk-buildroot and its compiler is mipsel-openipc-linux-musl-gcc, not an arm-…-musleabi- prefix:
mipsel-openipc-linux-musl-gcc -Wall -o ingenic-motor ingenic-motor/main.c # compiles cleanHiSilicon vendor toolchains (provides -orig for xm-uart and an41908a) — https://github.com/OpenIPC/toolchains, release v1. Each .tgz wraps an inner .tar.bz2. arm-himix200-linux and arm-hisiv610-linux exceed GitHub's asset limit and are split into .part00/.part01/… — cat them back together first.
gh release download v1 -R OpenIPC/toolchains -p 'arm-hisiv510-linux.tgz'
tar xzf arm-hisiv510-linux.tgz && tar xjf arm-hisiv510-linux/arm-hisiv510-linux.tar.bz2Name mismatch to expect: the real binaries use the full ABI prefix — arm-hisiv510-linux-uclibcgnueabi-gcc — but xm-uart/Makefile calls arm-hisiv510-linux-gcc. The short alias only exists if you run the bundled .install script, which hardcodes /opt/hisi-linux/x86-arm. Just symlink the short name onto the real one and put it on PATH; the build then succeeds unmodified and yields a uClibc-linked ARM EABI5 binary.
arm-xm-linux-gcc (the Xiongmai vendor compiler for xm-kmotor's -orig target) is in neither repo — build the -openipc variant unless you have that toolchain from a firmware dump.
an41908a/ also needs the proprietary HiSilicon MPP SDK (Hi3516CV500_SDK_V2.0.2.1) for hi_type.h/mpi_isp.h and the -lmpi -lisp … libraries; set MPP_DIR in its Makefile. That SDK is not in either public repo, so this is the one tool you cannot build from public sources alone. Its Makefile also ends with a sudo cp to /mnt/noc/… — a leftover from one developer's deploy setup; remove or adapt it.
These sources predate modern compilers and are only fully warning-clean on the vendor-era ones, so expect noise (unused variables, signedness mismatches) rather than treating it as a regression you introduced. Two issues that were real errors have been fixed — i2c-motor/main.c was calling ioctl() without <sys/ioctl.h>, which GCC 14+ rejects outright as an implicit declaration, and xm-uart/main.c had printf("Usage : %s\n") with no matching argument. Both now build clean under GCC 14. If you add code here, check it against a current GCC as well as the vendor toolchain, since the vendor compilers (GCC 4.4–6.3) silently accept things newer ones reject.
Understanding which pattern a tool uses explains most of its code:
-
ioctlagainst a vendor kernel module (xm-kmotor,camhi-motor,ingenic-motor). The module creates/dev/motor; the tool is a thin CLI overioctl. The kernel module ships with vendor firmware and must beinsmod'd with GPIO-pin and max-step parameters before the tool works — see each directory's Readme for the exactinsmod/modprobeline, since pin maps are per-camera-model. -
Direct bus / register access from user space (
i2c-motor,i2c-motor/sigmastar-ssc338q,an41908a,zenointel-sd2n4g). No custom kernel module — the tool drives the motor-driver IC itself over I2C SMBus (/dev/i2c-2, MS32006 at addr0x10) or SPI (/dev/spidev2.0, AN41908A) plus sysfs GPIO.i2c-motoralso needsdevmempinmux writes first (documented in its Readme).i2c-motor/sigmastar-ssc338qmaps/dev/memautonomously to un-gate the motor power rail via RIU register0x1F223618(Bank0x111BOffset0x06).zenointel-sd2n4ghas no driver IC at all — it maps/dev/memand bit-bangs a 4-wire stepper head straight on the Goke GK7205V510 PL061 GPIO banks (0x120B0000 + bank*0x1000), replayinggpioStep.ko's half-step phase table; open-loop, no home sensor. -
Serial protocol over UART (
xm-uart). Speaks the XM Pelco-D variant (C5 addr c1 c2 d1 d2 ck 5C) to a Xiongmai zoom/focus lens board on/dev/ttyAMA0at 115200 8N1; interactive keyboard REPL. System getty must be disabled on that UART.xm-uart/PROTOCOL.mdis the measured spec, backed by captures inxm-uart/captures/. Read it before changing any byte: the board accepts only syncC5, ignores checksum/address/trailer, and has no inter-byte timeout, so a partial frame silently eats the next command. The direction and focus bits follow what the stock firmware sends, not generic Pelco-D naming; on this board cmd20x80focuses nearer, measured on video (the reverse of Pelco-D's names).
This is the single most important thing to know before touching the ioctl tools. All three open /dev/motor, but nothing else is shared:
xm-kmotor— encoded ioctl numbers (0x80184D02status,0x80184D03maxsteps,0x40184D01command) overint[]arrays. The command lives ins[0]as a direction bitmask: 1=left, 2=right, 4=up, 8=down, so diagonals are OR'd (6=right-up, 9=left-down); 16=scan, 17=goto, 18=stop, 19=steps, 20=set-position.camhi-motor— different encoded numbers (0xC0046D01/0xC0046D02), ands[0]is a plain enum: 0=stop, 1=zoom−, 2=zoom+, 3=focus+, 4=focus−.ingenic-motor— plain small ioctl numbers (0x1–0x7) over C structs (motor_message,motors_steps,motor_reset_data). Note thatx_max_steps/y_max_stepsinmotor_messageare not in the stock T31 kernel module — they are an OpenIPC extension, so-S/-ioutput depends on a patched module. Itsopen("/dev/motor", 0)also relies on the T31 driver ignoring the open mode.
Do not copy an ioctl constant or command value from one tool into another.
All tools share the shape -d <direction-char> -s <speed> [-x N] [-y N], but the letters mean different things depending on whether the tool moves a camera head or a lens:
- PTZ tools (
xm-kmotor,ingenic-motor,zenointel-sd2n4g):u/d/l/r= up/down/left/right,s= stop,h= set position,g= steps,t/f= goto/scan (xm-kmotor),c/b= cruise/go-back (ingenic-motor). - Lens tools (
camhi-motor,i2c-motor,i2c-motor/sigmastar-ssc338q):u/d= zoom in/out,l/r= focus −/+,i= init (i2c-motor / sigmastar),s= stop.i2c-motor/sigmastar-ssc338qimplements simultaneous parabolic parfocal zoom+focus tracking;-ssets speed (1..100 or PPS),-psets explicit PPS,-n/-xset step counts. Moves are synchronous, so-d sreturns immediately. Relative moves are gated on calibration established by homing (homeor-d i). It also accepts Web UIptz.cgiformatmotor <profile> <h> <v>.
Speed ranges also differ per driver and are silently clamped: 10 (xm-kmotor), 100 (camhi/i2c), 900 (ingenic-motor, the kernel module's practical ceiling), 16383 PPS (sigmastar-ssc338q).
xm-kmotor, ingenic-motor, and i2c-motor/sigmastar-ssc338q expose -j (position/status) and -i (adds max steps) which print hand-rolled JSON to stdout — these are consumed directly by a web UI's ptzclient.cgi, so changing key names or adding stray output to stdout is a breaking change. xm-kmotor's -j/-i emit duplicate "unknown" keys for undecoded status fields.
The only tool with real algorithmic content. It drives the AN41908A lens driver over SPI: register writes are latched by pulsing GPIO VD_FZ (24) for focus/zoom and VD_IS (19) for iris, with RSTB (27) reset on startup. Command sequences (turn_on, send_focus_cmd, init_lens, init_iris) are raw register/value byte triples reverse-engineered from vendor firmware — treat the magic bytes as opaque.
On top of that it implements contrast-based autofocus in a background pthread (AF_proc): it pulls focus-value statistics from the HiSilicon ISP via HI_MPI_ISP_GetFocusStatistics, blends horizontal/vertical metrics into a weighted score using the AFWeight window map, then hill-climbs — probe a direction, follow increasing contrast, reverse and halve the step on each miss until the step reaches zero. This is why the Makefile links the whole MPP library set. The papers backing this approach are linked in the root README.md.
Python (uv run …), not cross-compiled and not covered by the GCC gate; its
tests are cd uart-bridge && uv run pytest (pty pairs, no hardware). It runs on
a lab host wired camera-UART ↔ host ↔ PTZ-board-UART and records JSONL captures.
Ports may be pyserial URLs (socket://…); bridge --tee and xm-uart -l together
replay one camera's lens traffic onto a second camera's board, and TWIN.md is the
procedure for comparing two boards that way.
codec.py holds what is known about the three frame types on that link: the
camera's scrambled A5 <counter^0x25> … stream (20 frames/s, the same
family as xm-uart's init[], never answered by the board), the XM Pelco-D
C5 … 5C commands the board does answer, and its EF 01 type len payload
replies (zoom reports "X1.6 "). An idle capture has no PTZ→camera bytes;
that is normal, not a dead link. Keep codec.key() consistent with what
diff should treat as meaningful.
api/README.md proposes a motors-daemon that loads per-hardware "driver wrapper" .so plugins and exposes a unified PTZ/AF/IRCut API over a UNIX socket. None of it exists yet, and the open questions it raises (one wrapper at a time vs. several; whether protocol logic belongs in the kernel module or the wrapper) are unresolved. Read it before designing anything cross-cutting, but do not treat it as describing current code.