x20ctl 0.2.0
Deadzones and response curves, decoded in 0.1.0 but unreachable, are now
editable and confirmed accepted by the hardware. Full detail in
CHANGELOG.md;
protocol notes in
docs/01-protocol.md.
Settings travel over a Bluetooth LE link the pad exposes separately from however
you play, so Bluetooth is needed to change anything even on a cable or the
2.4GHz receiver. Playing is unaffected, and the input tester works without it.
Added
- Stick and trigger deadzones and response curves. Each of the four channels
carries an inner deadzone, an outer deadzone, and two response-curve control
points. The app gives them a page where both points are dragged directly, and
x20 curvesets exact values from the command line. - Writing confirmed against hardware, on both records. A stick deadzone was
written from 8 to 10 and a trigger deadzone from 4 to 6, each read back
changed with the opposite channel untouched, and each restored byte for byte.
This is stated precisely because 0.1.0 could not establish it: the changekey
proof wrote a value already present, which demonstrates the framing is
accepted but never that a write alters anything, since an ignored write and an
accepted one leave identical records behind. tools/verify_curve_write.py, the procedure above, kept so it can be
repeated against another controller rather than taken on trust.- A version number reported by
x20 --version, byx20 statusalongside
the controller's own firmware version, in the app header and sidebar, and in
the executable's file properties. All read one value in the package, and
packaging reads it back out of the same place.
Changed
- Edits save themselves, on a short debounce, so recording, typing, clearing
and vibration changes reach disk without being told to. The Save button
remains and reports whether anything is pending. - The only interruption left is destructive. Clearing a slot that held a
macro asks first and offers to restore it, because a macro already written to
the controller cannot be read back off it. Every other edit is silent. - Battery is re-read every 20 seconds rather than once at connection, so the
gauge and the charging flag describe the controller instead of whatever was
true when the app started. A poll returning nothing leaves the previous
reading alone; a poll that fails reports the link as gone rather than leaving
a stale reading on screen. - The controller is named, mapped by vendor and product id with a fallback
to the reported name, so the header reads EasySMX X20 rather thanXpert2. - Save files are anchored to the user, not the executable, in
%APPDATA%\x20ctl\profiles. Replacingx20ctl.exewith a newer download
leaves them untouched. A test names the three paths that would break this,
because a one-file build unpacks to a temporary directory deleted on exit, and
anything stored beside the executable would appear to work and then lose
everything.
Fixed
- The header and the footer disagreed. A reconnect set the header to
"Connecting…" and left the footer reading "connected", because only one of the
two was updated. - A controller that was off or out of range printed a library traceback on
the command line. It now explains the state and what to do about it, as the
app already did. - A help string was broken by an editing slip, leaving its module unable to
import. The test suite reported it and it was committed regardless; recorded
because the process failure matters more than the typo. - The screenshot tool raced the app's auto-connect, so a controller
answering mid-render replaced the staged values and the published images
depended on whether the pad was switched on. It also still hadXpert2
written into it after the app stopped using that name, so the README showed a
name the app would never display.
Known limits
- The curve drawn between the two control points is this application's own
interpolation. The points are read and written exactly, but no available
source documents how the firmware joins them, so the drawn line is a faithful
picture of where the points sit rather than a claim about the hardware's
arithmetic. - Whether settings survive a power cycle is unverified. Vibration set to
zero read back at a different value after the controller was powered down
overnight, matching neither the written value nor the factory default. Until
that is investigated, treat any written curve as possibly not persisting
across a power cycle. - A capability read precedes every write, as it does in the vendor's own
application, so nothing here establishes whether that handshake is required.
Not available on the X20
The pad publishes a capability descriptor stating what it will accept, and an
X20 reports zero for these. They exist in the hardware but are driven by button
combinations on the pad and are not reachable through this protocol. Another
controller on the same chip may report them as available, and the library gates
on the descriptor, so it will simply work.
- RGB lighting
- Turbo
- Gyro
Deliberately out of scope
- Firmware flashing. That runs through the mass storage bootloader, which is
the only path that can destroy the controller. Use the manufacturer's updater.
x20 statusreports the installed version so you can tell when you are behind.
Install
Download x20ctl.exe below; it needs no Python. From source, pip install -e ".[gui]" puts x20ctl and x20 on your PATH.
Verifying the download
SHA-256 d5f3fb1842df7089cdaa3b6260714770ef85815f7ac0cad7a725aa3b82d8efb6
size 51,916,577 bytes
Get-FileHash .\x20ctl.exe -Algorithm SHA256If that doesn't match, don't run it. You can also scan the file on VirusTotal,
or skip the binary and run from source.
The executable is a PyInstaller one-file build, which bundles a Python
interpreter and unpacks itself to a temporary directory at startup. Several
antivirus engines flag that shape on its own, and this build is unsigned
because a code-signing certificate costs money the project doesn't have, so a
generic heuristic warning is likely and isn't evidence of much on its own. A
detection naming specific behaviour would be, and is worth reporting. The
program has no network code at all: there is no requests, urllib, http or
socket import anywhere in the package. It speaks BLE to the controller and
reads XInput, and has no path to the controller's bootloader. See
SECURITY.md.