Skip to content

x20ctl 0.2.0

Choose a tag to compare

@AmjadAAYD AmjadAAYD released this 10 Aug 20:37
· 70 commits to main since this release

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 curve sets 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, by x20 status alongside
    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 than Xpert2.
  • Save files are anchored to the user, not the executable, in
    %APPDATA%\x20ctl\profiles. Replacing x20ctl.exe with 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 had Xpert2
    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 status reports 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 SHA256

If 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.