read the gamepad from dora-joy and publish twist and command
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.
This commit is contained in:
+86
-17
@@ -5,11 +5,12 @@
|
||||
# dataflow against it without running the node, so CI needs neither a gamepad
|
||||
# nor a robot.
|
||||
#
|
||||
# `jog`, `request`, `response`, `motion_state` and the two `ui_` ports are
|
||||
# `joy`, `jog`, `request`, `response`, `motion_state` and the two `ui_` ports are
|
||||
# deliberately **untyped**. dora treats a port carrying `request_id` as
|
||||
# pattern-correlated and skips type checks on it, and several Arrow schemas
|
||||
# legitimately flow over each — one per service, one per console message.
|
||||
# Annotating them would claim a contract neither side keeps.
|
||||
# pattern-correlated and skips type checks on it, several Arrow schemas
|
||||
# legitimately flow over each — one per service, one per console message — and
|
||||
# dora 1.0 ships no standard joystick type. Annotating them would claim a
|
||||
# contract neither side keeps.
|
||||
|
||||
apiVersion: 1
|
||||
name: dora-teleop
|
||||
@@ -34,6 +35,24 @@ inputs:
|
||||
ms. Slower than about 100 ms and the arm node's own velocity expiry starts
|
||||
firing between ticks, which an operator feels as the arm stuttering.
|
||||
type: std/core/v1/UInt64
|
||||
joy:
|
||||
required: false
|
||||
description: >-
|
||||
One gamepad reading from `dora-joy`, as the Arrow struct
|
||||
`{axes: list<float32>, buttons: list<bool>, present: bool}`. Raw SDL — the
|
||||
deadzone, the response curve and the base-frame mapping all happen here.
|
||||
Wire it with `queue_size: 1`: `dora-joy` ticks faster than this node does,
|
||||
and a backlog of old readings is the last thing an arm should be driven
|
||||
from.
|
||||
|
||||
**`present: false` stops the arm at once**, without waiting out any timer,
|
||||
because an unplugged pad is not a centred pad. So does the stream going
|
||||
quiet for longer than `JOY_TTL_S` — `dora-joy` publishes on every tick
|
||||
whether or not the pad moved, so silence means that node died, and a dead
|
||||
pad node is indistinguishable from an unplugged pad as far as the arm is
|
||||
concerned.
|
||||
|
||||
Leave it unwired for a browser-only console. The node says so and starts.
|
||||
ui_command:
|
||||
required: false
|
||||
description: >-
|
||||
@@ -79,23 +98,63 @@ inputs:
|
||||
type: std/core/v1/Float32
|
||||
|
||||
outputs:
|
||||
twist:
|
||||
description: >-
|
||||
**The standard motion port.** A velocity, as
|
||||
`{linear: [x, y, z], angular: [x, y, z], frame}` in **SI** — metres per
|
||||
second and radians per second — in the frame named by `frame`, currently
|
||||
always `base`.
|
||||
|
||||
Re-sent every tick while a source is driving, and followed by exactly one
|
||||
all-zero Twist when it stops — once, not repeatedly, because a stream of
|
||||
zeroes keeps a consumer believing a jog is live. There is no answer to a
|
||||
Twist, ever: it expires on the consumer's own TTL and the sender keeps
|
||||
sending.
|
||||
|
||||
Both control sources are normalised to a fraction of full speed before
|
||||
anything is scaled, so a gamepad and the web console asking for the same
|
||||
deflection produce byte-identical Twists.
|
||||
command:
|
||||
description: >-
|
||||
**The standard discrete port.** One thing to do, as `{action, value}`.
|
||||
`action` is robot-agnostic — `gripper_open`, `gripper_close`, `stop`,
|
||||
`home`, `clear`, `freedrive` — so a consumer that swaps a vacuum for a
|
||||
two-finger gripper needs no change here. `value` carries the argument
|
||||
where a verb has one (`freedrive` uses 1.0 and 0.0) and is null otherwise.
|
||||
jog:
|
||||
description: >-
|
||||
A base-frame velocity for the arm, as `{velocity: [vx, vy, vz, wx, wy, wz]}`
|
||||
in mm/s and deg/s. Re-sent every tick while a source is driving, and
|
||||
followed by exactly one all-zero vector when it stops — once, not
|
||||
repeatedly, because a stream of zeroes would keep the arm node believing a
|
||||
jog is live.
|
||||
**Deprecated; use `twist`.** The same motion in `dora-ulite6`'s units:
|
||||
`{velocity: [vx, vy, vz, wx, wy, wz]}` in mm/s and deg/s, base frame.
|
||||
Published alongside `twist` while that node learns the standard one.
|
||||
|
||||
Wire `twist` **or** `jog`, never both — they carry the same intent, and a
|
||||
consumer reading both would be driven twice.
|
||||
request:
|
||||
description: >-
|
||||
A service call on the arm — `stop`, `grasp`, `release`, `clear`,
|
||||
`freedrive`, `home`, `move_to`, `get_state` — with `request_id` in the
|
||||
metadata.
|
||||
**Deprecated; use `command`.** The same verbs under the service names
|
||||
`dora-ulite6` uses — `stop`, `grasp`, `release`, `clear`, `freedrive`,
|
||||
`home` — with `request_id` in the metadata. `move_to` and `get_state` have
|
||||
no standard equivalent yet and go only here.
|
||||
ui_state:
|
||||
description: >-
|
||||
The console's telemetry snapshot, as a JSON string for the WebSocket
|
||||
bridge to broadcast. Also carries error toasts. Every key in it is read by
|
||||
name in the console's `app.js`.
|
||||
name in the console's page, so the two change together.
|
||||
|
||||
# Reserved, not implemented. A joint-space robot — a lerobot SO-101, say — takes
|
||||
# joint targets rather than a Cartesian velocity, and no amount of scaling turns
|
||||
# one into the other: it needs inverse kinematics, which is another node's job.
|
||||
# When such a consumer exists, this is the port it wires, mirroring ROS's
|
||||
# control_msgs/JointJog beside geometry_msgs/Twist:
|
||||
#
|
||||
# joint_jog:
|
||||
# description: >-
|
||||
# A joint-space velocity, as `{names: [str], velocities: [float]}`, the
|
||||
# joints named rather than positional so a consumer cannot mistake the
|
||||
# order.
|
||||
#
|
||||
# It is written down rather than built because a port nothing wires is worse
|
||||
# than no port: it reads as supported and has never once run.
|
||||
|
||||
env:
|
||||
SPEED_MM_S:
|
||||
@@ -124,10 +183,6 @@ env:
|
||||
How much of the response curve is cubic, from 0 (linear) to 1. Fine
|
||||
positioning happens near centre and a linear stick gives it almost no
|
||||
resolution there. Defaults to 0.4.
|
||||
JOYSTICK_INDEX:
|
||||
type: int
|
||||
default: 0
|
||||
description: Which pad to open, when more than one is plugged in. Defaults to 0.
|
||||
JOG_TTL_S:
|
||||
type: float
|
||||
default: 0.4
|
||||
@@ -135,6 +190,14 @@ env:
|
||||
How long the owning source may go quiet before the arm is stopped. Keep it
|
||||
**below** the arm node's VELOCITY_COMMAND_TTL_S (0.5 s by default), which
|
||||
is the controller-side backstop behind this one. Defaults to 0.4.
|
||||
JOY_TTL_S:
|
||||
type: float
|
||||
default: 0.2
|
||||
description: >-
|
||||
How long the `joy` stream may go quiet before the pad reads as gone and the
|
||||
arm is stopped. Must be **below** JOG_TTL_S. `dora-joy` publishes on every
|
||||
tick, so this is really "how many missed readings to tolerate" — at a 20 ms
|
||||
joy tick, the default is ten. Defaults to 0.2.
|
||||
GAMEPAD_IDLE_RELEASE_S:
|
||||
type: float
|
||||
default: 2.0
|
||||
@@ -156,6 +219,10 @@ example: |
|
||||
path: dora-teleop
|
||||
inputs:
|
||||
tick: dora/timer/millis/50
|
||||
joy:
|
||||
source: joy/joy
|
||||
queue_size: 1
|
||||
queue_policy: drop_oldest
|
||||
ui_command: ws-transceiver/data_out
|
||||
clients: ws-transceiver/clients
|
||||
response: ulite6/response
|
||||
@@ -164,6 +231,8 @@ example: |
|
||||
tcp_pose: ulite6/tcp_pose
|
||||
joint_pose: ulite6/joint_pose
|
||||
outputs:
|
||||
- twist
|
||||
- command
|
||||
- jog
|
||||
- request
|
||||
- ui_state
|
||||
|
||||
Reference in New Issue
Block a user