Skip to content

feat: support the XF headtracker on an AUX serial port - #7896

Open
MrScothh wants to merge 2 commits into
EdgeTX:mainfrom
MrScothh:feat/xf-headtracker
Open

MrScothh wants to merge 2 commits into
EdgeTX:mainfrom
MrScothh:feat/xf-headtracker

Conversation

@MrScothh

@MrScothh MrScothh commented Oct 10, 2026 •

Copy link
Copy Markdown

Summary of changes:

This adds the XF (Nanjing Xianfei) headtracker as an input on an AUX serial port. Its roll, pitch, yaw, mode and sensitivity become five mix sources, htR, htP, htY, htM and htS, so the headtracker no longer has to come in through the trainer port or a Lua script, and the trainer port stays free for a real trainer link.

Protocol. The headtracker's receiver has a UART on its expansion port, 115200 8N1. CwHeadTracker offers two outputs for it, and the radio takes either, told apart by the header.

"Wireless PPM", 25 frames a second:

Bytes Content
0-1 E7 9B
2-17 8 PPM channels, uint16 little-endian, in 1/24 us
18-19 CRC16-CCITT (poly 0x1021, init 0) over bytes 0-17, big-endian

"C Series Gimbal", about 50 frames a second on my unit. It is the frame the XF C-20T and the Walksnail/Caddx GM gimbals take, the one ArduPilot's CADDX mount driver and INAV's serial gimbal send:

Bytes Content
0-1 A5 5A
2 mode (bits 0-2), sensitivity (bits 3-7, signed)
3-7 4 reserved bits, then roll, pitch and yaw as 12-bit two's complement angles, -2048..2047 = -180..180 deg
8-9 CRC16-CCITT over bytes 0-7, big-endian

Neither is documented by XF. I worked them out from captures of my unit and checked them against what CwHeadTracker shows; the C Series layout matches ArduPilot's driver. In PPM, channels 4-8 are roll, pitch, yaw, mode and sensitivity in CwHeadTracker's default mapping, and 1000-2000 us maps to -1024..1024. C Series comes out on the same scale: ±180 deg is ±1024, which is what the PPM output gives with CwHeadTracker's default ratio, and mode and sensitivity are scaled as PPM carries them on CH7 and CH8 (mode 2 is 2000 us and sensitivity 15 is 1968.75 us, so (mode - 1) x 1024 and sensitivity x 64). Captures of the same unit in the two outputs give the same values.

Radio

  • New UART mode "XF Headtracker". It is RX only and not offered on VCP.
  • The parser runs on a 10 ms timer, as SpaceMouse does, and uses the existing crc16(CRC_1021, ...).
  • A frame starts at either header. After a bad CRC the parser steps one byte and looks again, so a header-like pair inside the data (E7 9B is 1663 us, and A5 5A can turn up in the angles) cannot keep it out of step, even with C Series frames arriving too close together for the pause between them to help. It also restarts after the pause between PPM frames.
  • With no valid frame for 500 ms the five sources return 0, so a lost link centres a gimbal instead of freezing it off centre.
  • Lua gets the source names htr, htp, hty, htm and hts. The sources are also in the SRC_STICK filter, next to tilt and SpaceMouse.
  • Unlike SpaceMouse, head movement does not reset the inactivity timer, since it is not stick activity.

Scope. I kept to the SpaceMouse pattern: build option XF_HEADTRACKER, OFF by default, in the Horus target, with the sources under PCBHORUS. On a TX16S image, measured against main:

Build Flash RAM
Option off (strings and tables only) +416 B 0
Option on +1144 B +96 B

As suggested on Discord, it's meant as an option that users pick in Buddy's CloudBuild rather than part of the default builds; once this is in, I'll open the matching targets.json entry in EdgeTX/cloudbuild. I'm happy to turn it on by default, or to make it available to more targets, if you prefer.

Source numbering. As with any new source, the five new ones shift the numbers of every later source by 5 on PCBHORUS radios. Models are not affected, since the YAML stores sources by name. On H7 radios with 99 telemetry sensors, the 10-bit srcRaw fields could already not hold the last telemetry sources: on a TX16S MK3 they run up to 584 against a limit of 511. With this change, that limit is reached 5 source numbers (under two sensors) earlier.

Companion. It knows the new AUX mode and the five sources, with YAML tags XFHT_ROLL, XFHT_PITCH, XFHT_YAW, XFHT_MODE and XFHT_SENS matching the firmware. The sources are offered when AUX1 or AUX2 is set to the headtracker.

Generated files

  • YAML parsers regenerated with tools/generate-yaml.sh (15 targets).
  • sim_string_list.h regenerated with m4.
  • tools/cfn_sorter.sh leaves cfn_sort.cpp unchanged.
  • The fonts are not affected: the new strings are ASCII only.

Testing

  • Radio unit tests, in tests/xf_headtracker.cpp:
    • a PPM frame and a C Series frame recorded from the same headtracker, all five sources;
    • a bad CRC in each format;
    • switching from one format to the other;
    • resync after garbage bytes;
    • joining the stream at a header-like PPM channel value;
    • joining the stream at header-like C Series angle bytes with frames 10 ms apart, where only the CRC resync can find step;
    • the 500 ms timeout.
  • The source names are checked in tests/sources.cpp.
  • tools/commit-tests.sh passes:
    • tx16s with -DXF_HEADTRACKER=ON: 248 tests;
    • default options: tx16s (240), tx16smk3 (238) and x9dp2019 (183).
  • Companion: tests/xf_headtracker_yaml_test.cpp covers the YAML round trip, the names and the availability; tests-companion passes all 44 tests with Qt 6.9.3.
  • On my TX16S MK2, with the receiver on AUX2 and 2.12.4 carrying the first version of this code backported: htR, htP and htY follow the head in Wireless PPM. The C Series path and the two new sources are checked against frames captured from the unit, not yet on the radio.

Roll, pitch and yaw from the XF headtracker's receiver (expansion port in
"Wireless PPM" mode) become mix sources htR/htP/htY, with Companion support.
Build option XF_HEADTRACKER, off by default, as SPACEMOUSE.
@rotorman

Copy link
Copy Markdown
Member

@kilrah As you worked on a similar project for ExpressLRS (https://github.com/kilrah/allxf-headtracker-elrs), would you be able to look/review this PR? Especially from the XF protocol perspective? Thx!

@kilrah

kilrah commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

I guess I hadn't even noticed there were multiple UART modes, I just went with what mine was set to i.e. "C Series Gimbal".
That sends only the 3 channels and the mode/sensitivity values but at 100Hz.

If it's to be a dedicated thing for a head tracker I'd think that's better to use since it's 4x faster, but it's worth discussing if there's interest in an 8-channel input that could be more generic.

Guess I have to add a note about mode in my README now though :D

The expansion port now takes either output CwHeadTracker offers for it,
told apart by the header: "Wireless PPM" (E7 9B, 25 Hz) or "C Series
Gimbal" (A5 5A, about 50 Hz on my unit). Both also give the mode and the
sensitivity, as two more sources htM and htS, on the same scale: angles at
+-180 deg full range, mode and sensitivity as PPM carries them on CH7 and
CH8. After a bad CRC the parser steps one byte and looks again, since C
Series frames come too close together for the pause to resync.
@MrScothh

Copy link
Copy Markdown
Author

@kilrah thanks for looking at it. I've added the C Series Gimbal frame too (0eb13b9): the port now takes either output and tells them apart by the header, and both give mode and sensitivity as well, as htM and htS. The two come out on the same scale: angles at ±180° full range, which is what the PPM output gives with CwHeadTracker's default ratio and the same mapping your bridge uses, and mode and sensitivity as PPM carries them on CH7 and CH8.

On the rate: on the XF one I measured about 51 Hz in C Series mode (a capture of 1018 frames) and 25 Hz in Wireless PPM, so 2x here rather than 4x. Maybe the Caddx one is faster.

I'd still keep PPM next to it, as it's the more generic of the two and works with a wider range of setups, custom ones included. I don't use the headtracker with an XF gimbal but with a cheap Chinese two-servo one, and it works very well: a gimbal without spending much, and a simple way to get the channels to the receiver and the FC, and from there to the gimbal.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants