An Xbox One S pad over Bluetooth numbers its controls differently from the
indices this node shipped with: the right stick is on axes 2 and 3, the
triggers on 4 and 5, and the buttons carry gaps at 2 and 5. Against the old
layout an untouched pad reported a resting trigger as full right-stick
deflection, which asked for 30 deg/s of pitch while lying on the desk, held
the lease for ever so GAMEPAD_IDLE_RELEASE_S never fired, and moved a real
arm into a controller error. X toggled freedrive and LB was the E-STOP, so
the resulting latch could not be released from the pad at all.
Layout now carries the swept indices. Each pad is also measured the first
time it is seen at rest: a stick must rest near centre and a trigger near
the bottom of its travel, and a pad that does not fit reads as absent and is
named in the log rather than driving an arm from a numbering nobody checked.
The measured rest becomes each axis's zero, so a worn stick's tilt is
subtracted rather than deadzoned away and a trigger that reads 0 until it
first moves stops reading as half pressed.
Rest is never re-measured while the pad is in use, because doing that under
a held stick would record the deflection as centre and the arm would move
when the operator let go. The probe drives from the same calibration, so
what it prints is still what the arm would get, and it shows the mismatch.
Tests name their controls through axes_for and buttons_for instead of
numbering them; the positional lists are why a wrong index passed a green
suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pad moves to dora-joy, so this node keeps no hardware dependency and the
pad becomes reusable. It arrives on `joy` as a plain reading; `JoyInput.read`
treats silence past JOY_TTL_S as absence, because that node dying looks like
silence rather than like an unplugged pad and the arm cannot tell them apart.
Both control sources are now normalised to a [-1, 1] six-vector before anything
is scaled, and the scaling happens in one function. The web path used to pass a
velocity straight through without going near mapping, which is why the console
had to speak the arm's units. It no longer does.
`twist` (SI) and `command` (robot-agnostic verbs) are the standard ports. `jog`
and `request` still carry the same intent in dora-ulite6's units and service
names so the two repositories can move independently; LEGACY_REQUEST is the
only place that knows what that node calls a verb. Wire one or the other, never
both. `joint_jog` is specified in the manifest and deliberately not built.
Schemas are declared rather than inferred: pyarrow reads a null `value` as its
own type, so an inferred `command` would change its port's type between one
verb and the next.
Three bugs found while moving this code, each with a test:
- `Sample.active` compared raw axes against a threshold, but SDL rests a
trigger at -1.0, so every connected pad read as in use for ever: the pad took
the lease on plug-in and GAMEPAD_IDLE_RELEASE_S never fired.
- The e-stop latch was never consulted when deciding a velocity, so Back
stopped the arm for one tick and the next stick nudge drove it again. Nothing
on the pad could clear it either. It is now checked above the lease, and X
releases it and clears the controller's error, as the console's RESET does.
- Buttons were read before the arbiter granted the lease, so the first press
after the pad had been idle was swallowed: not owned yet when read, and no
longer a rising edge by the next tick.
The axis and button indices in mapping.py are SDL's documented layout rather
than an observed one, and until now the only way to find a pad that numbers
things differently was to watch a real arm move the wrong way.
`dora-teleop-probe` prints the raw axes, the buttons it sees and the velocity
that would be sent, live, with no dataflow and no arm — it cannot move anything.
It goes through the same Gamepad and velocity() the node runs, so what it prints
is what the arm would get; that is the whole reason it is worth having, and the
reason it must not grow its own copy of the mapping.
Every axis is listed, including ones the layout does not use, because an axis
marked unused that moves when a stick is pushed is exactly the finding this is
for. The centred bar makes a worn stick that does not return to zero obvious.