Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tune a running controller

Live tuning changes how an existing Cartesian control session tracks its targets, without restarting the node. Use it to compare tracking, damping and motion limits. Ordinary motion does not require the panel or any live parameter changes: start with the shipped configuration.

The motion client sends goals and owns the session. The tuning panel sends parameter updates and receives accepted settings and robot state. The node validates both inputs; franka-rs runs the local 1 kHz torque loop on the Pi or realtime PC.
Motion goals and tuning are separate inputs. Both reach the same local controller. Open full-size diagram.

The motion client still owns the arm and keeps the session alive. The panel changes controller parameters; it does not acquire the arm, start motion or replace the motion client. Live tuning currently requires a running Cartesian impedance session. An idle arm or a joint-target session returns not_ready when asked to change parameters.

Open the optional panel

Start the node and your Cartesian client using Serve arms over Zenoh. The panel is the franka-tuning-panel command of the Python client, so pip install franka-node-client of the node’s version installs it (from a checkout, python -m pip install ./crates/franka-node/python; From Python covers the client itself). Then:

franka-tuning-panel --connect tcp/127.0.0.1:7447 --no-multicast \
  --presets ./tuning-presets.json

This example runs the bridge on the same machine as a node listening on port 7447. Open http://127.0.0.1:8765/ and select the arm.

The panel is a pure consumer, so it opens a Zenoh client session (--mode client, the default): it dials out and is never dialled, which is what works through a tunnel, and it still scouts by multicast, so a node on the same LAN needs no --connect. A client will not start with nothing to reach — start the node first, or pass --mode peer to hold a session open while you wait. --listen needs --mode peer; --zenoh-config wins over all of these flags. --timeout is the Zenoh query timeout in seconds, 1.0 by default; raise it on a slow link, where a query would otherwise give up before the node answers.

For a Pi-hosted bridge, tunnel it from the laptop:

ssh -N -L 8765:127.0.0.1:8765 <pi-user>@<pi-address>

Then open the same local URL on the laptop; the panel stays on loopback and only the tunnel reaches it. That is the default, and the one to keep while a robot runs unattended; to serve the panel to the network instead, see below. Access to the node’s Zenoh bus also permits parameter changes: tuning requests identify their sender but do not require the motion lease holder.

--presets names the panel’s preset file, by default ~/.local/state/franka-tuning/presets.json. Presets never change the node’s TOML. Each saved preset holds the owners’ values, a note, and the metrics snapshot at the moment of saving, stamped with the effective_hz it was measured at — snapshots taken at different effective rates are not comparable.

To explore the interface without a robot, run these in two terminals from the repository root instead; the mock is a test tool that stays in the repository. The mock is an owner, so it listens as a peer; the panel dials it as a client:

python crates/franka-node/python/tests/panel_tests/mock_owner.py --listen tcp/127.0.0.1:17447 --no-multicast
franka-tuning-panel --connect tcp/127.0.0.1:17447 --no-multicast \
  --presets ./tuning-presets.json

The mock’s teleoperation controls demonstrate an additional parameter owner; they do not mean that a teleoperation service is included in franka-node.

Reach it from another machine

A tunnel is not the only way. Exposure takes both flags: a --host beyond loopback, and --expose-to-network to say you mean it. Without the flag that --host is refused; with it, on the default --host 127.0.0.1, nothing changes. Together they serve the panel to the network it binds, so a laptop or a tablet opens it like any other service on the bench:

franka-tuning-panel --host 0.0.0.0 --expose-to-network --connect tcp/127.0.0.1:7447

There is no authentication. An exposed panel lets anyone who can reach that port — every machine, and every person, on that network — change any live parameter of a moving robot: its springs, its damping, its motion budgets. Serve it on a network you control, while you are at the bench, and stop it afterwards. The startup line names the URLs it has opened and who can reach them:

WARNING tuning-bridge: the panel is at http://203.0.113.5:8765/, reachable by anyone on that
network: it has no authentication, so any of them can set every live parameter of every arm on
the bus, while it moves

Exposed, the panel serves a request whose Host header is an IP address, alongside the loopback names, so http://203.0.113.5:8765/ works from another machine whatever the address is. A Host that is a DNS name is still refused — including one that merely contains an address, such as 203.0.113.5.evil.example — and that refusal is what keeps the panel’s one browser-side defence: any page on the internet can point its own name at this port (DNS rebinding), and the browser would then count it same-origin with the panel and let it drive the arm; a rebound name is never an IP literal. Bound to loopback, the loopback names stay the only ones served, whatever page asks.

To reach the panel by name, name it — exactly, and repeat the flag for more than one:

franka-tuning-panel --host 0.0.0.0 --expose-to-network --allowed-host <bench-host>.local

--allowed-host matches the whole name, never a suffix, so allowing <bench-host>.local does not allow <bench-host>.local.evil.example. It needs the exposed bind above: on a loopback bind the panel exits with an error instead of serving that name. It is only as trustworthy as whoever can answer for the name: .local names are answered by mDNS, which anyone on the network can claim, and a page served from a name the panel accepts is same-origin with the panel. Prefer the address.

What the Origin check does, and does not, protect: a browser that posts a change must send an Origin equal to the address in its own URL bar, so a page served from anywhere else cannot write even when it aims straight at the panel’s address. A script sends no Origin at all, and nothing here stops it — one curl from any machine on that network retunes a moving arm. The panel is also plain HTTP, so an exposed session is readable and injectable by anyone on the path. Exposure is a decision about who is on that network, not a permission the panel checks.

What the controls change

The panel gets ranges, units, defaults and confirmation thresholds from the running node’s schema, generated from the same LiveTuning::BOUNDS table that checks updates. There is no separate panel limits table. The library-level contract — the fields, what is refused and what is clamped, and how each value crosses into the loop — is in Tune the law while it runs.

FieldMeaning
joint_stiffness[7]Joint springs in the impedance law.
joint_damping[7]Joint damping; the node enforces a floor tied to each joint spring.
cartesian_stiffnessScales the Cartesian stiffness preset and its associated damping.
ik_dampingDamping of the inverse-kinematics solve near poorly conditioned poses.
ik_nullspace_gainPosture correction in the redundant degree of freedom; zero disables it.
velocity_feedforward_gainWeight of the goal velocity in the damping term; zero disables feedforward.
velocity_feedforward_cutoffFilter cutoff for that velocity; the schema marks the filter-off value.
budget[3]Translation velocity, acceleration and jerk norm limits.
rotation_budget[3]Rotation velocity, acceleration and jerk norm limits.

Change a small group of related settings, apply, and compare the same motion before and after. Read the accepted values: the node can clamp a request or raise damping when a spring is increased. Invalid requests change nothing. Crossing a budget’s confirmation threshold, or raising velocity_feedforward_gain above zero, requires an explicit confirmation; already being above it does not ask again. The wrist joints’ damping (joints 5 to 7) is capped lower than the others’, at 40 instead of 60 Nm·s/rad, to keep the velocity barrier stable on their lighter inertia.

How an accepted value reaches the arm depends on the field. The gains — the joint springs and dampers, the Cartesian stiffness, the two IK fields and the feedforward gain — slew in over 0.3 s. A budget’s velocity and acceleration rise at once but ramp down. Only the two jerks and the feedforward filter cutoff step immediately. So most changes show in the motion over the cycles after they are applied. This removes the need to design those transitions in the client; it does not make every combination or every robot pose feasible.

A lowered budget never steps the command, but lowering its acceleration or jerk while the arm moves lengthens the stop (toward v²/2a, plus the jerk’s ramp), so a near goal is overshot and returned to. For example, at the default 0.3 m/s (0.17 m/s per axis), an acceleration dragged from 0.5 to 0.1 m/s² with the goal 3 cm ahead overshoots it by about 20 cm; faster motion or a lower acceleration overshoots further. Lower the velocity first, apply it, then lower the acceleration or jerk.

The displayed params are accepted targets, not a measurement of the gains applied on each realtime cycle. slewing estimates the fraction of the latest change still to go: near 1 just after acceptance, approaching 0. It is calculated from elapsed time, not fed back from the controller; overlapping changes and missed cycles limit its accuracy.

Why feedforward reads zero

Velocity feedforward is off by default (velocity_feedforward = false); with it on, real arms vibrated. The boolean takes precedence at session start, so the live weight is 0 whatever velocity_feedforward_gain says. Setting velocity_feedforward_gain above zero live, which needs a confirmation, enables its contribution for that session; lowering velocity_feedforward_cutoff filters the goal velocity it feeds forward, and is the knob to try against vibration (no value has been validated on hardware). To enable it in future sessions, set velocity_feedforward = true and the desired gain in TOML, then restart the node to load the configuration.

What is saved

Live changes last only for the current session. On session end, values revert to the loaded TOML baseline, origin becomes null, and dirty becomes false. The next session starts from that baseline. A node restart also restores the configuration and changes boot_id.

The version counts accepted updates during a node run: session end does not reset it; node restart does. dirty compares accepted targets with the baseline, not the controller’s instantaneous gains. Panel presets are separate JSON files. They do not edit TOML, and there is no params/save endpoint. Put a chosen setup into TOML explicitly after reviewing it.

Read the diagnostics with their limits

Panel jitter, lag and tracking measurements are computed from published robot state at about 100 Hz, not from the 1 kHz loop, and they are not a complete trace of it. A node publishing faster is sampled 1 in N by the panel, which says so at startup and in the live header (metrics from 1 in 10 of a 1000 Hz stream); the metrics snapshot and every saved preset carry effective_hz. The numbers therefore mean the same thing at any state_hz: lag and the windows behind it are measured against the rate the panel really sees, so a state_hz that 1 in N cannot bring to exactly 100 (250 Hz gives 125 Hz) still times correctly. What does ride with that rate is the jitter filter’s 3 Hz corner (3.75 Hz at 125 Hz) and how a vibration above half the effective rate folds into the band, so compare jitter figures between snapshots at the same effective_hz. state_hz = 100 is the setting to run: above it the panel drops the extra messages before decoding them, but they still cross the Zenoh callback, which on a Pi is real CPU. The node does not yet publish leash-alteration counts or cap-active fractions. Joint inertia hints are unavailable, and the headroom display also needs a compatible teleoperation owner’s limits. A missing metric is not evidence that a limit was never reached.

Larger Cartesian budgets can still reach joint-side limits depending on pose, payload and trajectory; there is no universal translation speed that guarantees success. Robot reflexes, collision settings, session deviation guards and the client’s own target pacing remain in effect. Those guards are not loosened by tuning a budget.

For client implementations and precise message semantics, see the node parameters protocol.

Status

Live tuning is asserted against the simulator, down to the torque the loop produced, and the panel is tested against a mock owner. Neither has been exercised on an arm yet, and no velocity-feedforward setting reachable from here has been validated on hardware.