API reference#
Everything a client program touches. Quaternions on this API are wxyz.
Client#
- class frankatwin.remote_client.FrankaTwinClient(cfg, *, connect_timeout_s=3.0, verbose=False)[source]#
- Parameters:
cfg (RobotConfig)
connect_timeout_s (float)
verbose (bool)
- set_ee_target(target_pos, target_quat)[source]#
Publish a new EE setpoint (wxyz). Returns the daemon’s
state_headright after the write – the target is in force from tickhead + 1(seefrankatwin.ring_log); 0 from a daemon that predates ring logs.
- get_state(*, fresh=False)[source]#
Return the most recent cached state frame.
If fresh=True, force a synchronous round-trip to the daemon instead of using the SUB-stream cache. Use sparingly; the cache is sufficient for policy loops.
- Parameters:
fresh (bool)
- Return type:
RobotState | None
- log_start(path=None, *, poll_hz=None)[source]#
Start a 1 kHz ring log (
frankatwin.ring_log).From now until
log_stopthe daemon copies every 1 kHz state frame out of the shm ring and stamps eachset_ee_targetwith the tick it took effect.pathis wherelog_stop/log_savewrite the CSV on this machine; its directory is created now, so that a path that cannot be written fails before the run and not after it. Without apath, read the rows withlog_fetch. Returns{"path", "seq_start", "poll_hz", "log_id"}.
- log_stop(*, save=True)[source]#
Stop the ring log and return the summary (
num_frames,seq_first/seq_last,num_targets,gaps,dropped_frames,resets, …).With
saveand apathfromlog_startthe rows are fetched and written right away (log_save;path/targets_pathin the summary).save=Falsereturns as soon as the daemon has merged the session – calllog_savewhen there is time for the transfer.
- log_fetch(table='merged', *, chunk_rows=10000)[source]#
The last finished ring log’s rows as a float64 array, pulled from the daemon
chunk_rowsat a time.tableis"merged"(one row per tick,ring_log.MERGED_COLUMNS) or"targets"(one row per setpoint,ring_log.TARGETS_COLUMNS). The daemon keeps the log until the nextlog_start.
- log_save(path=None)[source]#
Fetch the last finished ring log and write it on this machine: the merged CSV at
path(default:log_start’s) and<stem>_targets.csvnext to it. Returns{"path", "targets_path"}.The daemon keeps the log until the next
log_start, so a save that failed can be repeated, from a new client too.
- move_to_pose(target_pos, target_quat, q_max_speed=None)[source]#
q_max_speed is approximate here – see LocalController.move_to_pose.
NUC-side controller#
- class frankatwin.local_controller.LocalController(cfg, *, autostart=True, verbose=False)[source]#
Owns the shm segment and the C++ controller subprocess.
Typical usage:
cfg = load_config() with LocalController(cfg) as robot: robot.set_gains(kp_pos=200, kp_ori=20) robot.set_ee_target(np.array([0.5, 0.0, 0.4]), np.array([1.0, 0.0, 0.0, 0.0])) state = robot.get_state()
- Parameters:
cfg (RobotConfig)
autostart (bool)
verbose (bool)
- is_controller_alive()[source]#
True iff the osc_shm subprocess exists and has not exited.
- Return type:
- ensure_running()[source]#
Restart osc_shm if it has died unexpectedly.
Returns True if a restart was performed, False if the controller was already alive. Logs the dead process’ exit code + captured stdout/stderr before relaunching so the crash reason (libfranka reflex, RT overrun, etc.) is not lost. Intended to be driven by the daemon watchdog; callers must serialize this against move_to*/lifecycle ops (osc_shm needs the sole FCI session).
- Return type:
- set_ee_target(target_pos, target_quat)[source]#
Publish a new EE setpoint. wxyz quaternion.
Returns the
state_headread right after the write: the newest 1 kHz frame at that moment. osc_shm snapshots the command before it publishes a tick’s frame, so this target is in force from framehead + 1at the latest (frankatwin.ring_log). An active ring log records it.
- set_gains(kp_pos=None, kp_ori=None, kd_pos=None, kd_ori=None, error_delta_pos=None)[source]#
Update controller gains. Any None argument keeps the current value.
- get_state(k=None)[source]#
Return the latest RobotState (or None if no frame yet).
If k is provided, returns a list of up to k most recent frames in chronological order.
- Parameters:
k (int | None)
- Return type:
RobotState | None
- log_start(path=None, *, poll_hz=None)[source]#
Start a 1 kHz ring log (
frankatwin.ring_log.RunRecorder).A daemon thread copies every state frame out of the shm ring from now on, and every
set_ee_targetuntillog_stopis stamped with the tick it took effect.pathis written onlog_stop, on this machine. Without one the session stays in memory, to be read withlog_fetchor written withlog_save– what the daemon does, so that the files end up on the PC that drove the run and not here. One session at a time; starting one drops the last finished log.
- log_stop(*, save=True)[source]#
Stop the ring log and return the summary.
The merged rows and the setpoint rows stay in memory until the next
log_start(log_fetch,log_save). Withsaveand apathfromlog_startthe two CSVs are written here as well.
- log_fetch(table='merged', offset=0, count=None)[source]#
Rows
[offset, offset + count)of the last finished ring log (a view).tableis"merged"(one row per tick,ring_log.MERGED_COLUMNS) or"targets"(one row per setpoint,ring_log.TARGETS_COLUMNS);count=Noneruns to the end.
- log_save(path=None)[source]#
Write the last finished ring log on this machine: the merged CSV at
path(default:log_start’s) and<stem>_targets.csvnext to it. Returns{"path", "targets_path"}.
- move_to_q(q_target, q_max_speed=None)[source]#
Blocking joint-space reset using libfranka MotionGenerator.
Mutually exclusive with osc_shm: this method stops the controller, runs move_to, then restarts the controller.
- move_to_pose(target_pos, target_quat, q_max_speed=None)[source]#
Blocking task-space reset using libfranka CartesianPose motion type.
target_quat is wxyz. No external IK; libfranka solves internally.
q_max_speed is APPROXIMATE here: because the IK lives inside libfranka, move_to estimates the joint speed from the Jacobian at the start pose alone. It degrades over large reorientations and near singularities. See src/move_to.cpp.
Configuration#
Load and validate frankatwin runtime config.
- Config resolution order (first hit wins):
Explicit path= argument to load_config(path=…) / –config.
$FRANKATWIN_CONFIG.
<repo>/config/robot.yaml – the checkout this package was installed from (pip install -e .).
Relative paths.build_dir is resolved against the repo root.
- frankatwin.config.resolve_config_path(path=None)[source]#
Pick the config file per the module docstring (existence is checked by load_config).
- class frankatwin.config.NetworkConfig(nuc_host: 'str', cmd_port: 'int', state_port: 'int', state_cache: 'int' = 256)[source]#
- class frankatwin.config.ControlConfig(frequency_hz: 'int', kp_pos: 'float', kp_ori: 'float', kd_pos: 'Optional[float]', kd_ori: 'Optional[float]', error_delta_pos: 'float')[source]#
- class frankatwin.config.ResetConfig(q_max_speed: 'float' = 0.5)[source]#
- Parameters:
q_max_speed (float)
- class frankatwin.config.GripperConfig(enabled=True, move_speed=0.1, grasp_speed=0.1, grasp_force=70.0, grasp_width=-0.01, epsilon_inner=0.08, epsilon_outer=0.08, max_width=0.08)[source]#
Franka Hand defaults for the gripper_* commands (libfranka Gripper).
Closing is a libfranka grasp: the fingers drive towards grasp_width and squeeze with grasp_force once they stall on the object, so the OBJECT sets the resting width and grasp_force sets how hard it is held. The default grasp_width = -0.01 (past full closure) means “close as far as you can and hold”. epsilon_inner/outer is the band around grasp_width inside which libfranka reports is_grasped; 0.08 (the whole stroke) counts every stall as a grasp, matching the deoxys stack this replaces.
- class frankatwin.config.LoadConfig(mass=0.0, com=<factory>, inertia=<factory>)[source]#
End-effector payload (e.g. a mounted camera) for gravity compensation.
mass <= 0 means “no extra load” -> osc_shm leaves the Desk-configured load untouched (calls no setLoad). com is the flange->load COM vector [m]; inertia is the 3x3 about the COM, row-major [kg m^2].
- class frankatwin.config.CollisionConfig(torque_threshold=100.0, cartesian_threshold=100.0)[source]#
libfranka collision-reflex thresholds passed to setCollisionBehavior.
Each scalar fills ALL entries of the torque (7) / Cartesian (6) lower & upper, nominal & acceleration arrays. The task-impedance controller’s own push is bounded by kp*error_delta (~25 N / ~9 Nm at kp_pos=500/kp_ori=30), so thresholds below that range trip “cartesian_reflex” on insertion contact. Defaults match the deoxys stack (100), which never reflexed during insertion.
- class frankatwin.config.RobotConfig(network, robot, control, paths, reset=<factory>, gripper=<factory>, load=<factory>, collision=<factory>, source_path=None)[source]#
Top-level config bundle. Pass-through for clients/daemon/local.
- Parameters:
network (NetworkConfig)
robot (RobotSpec)
control (ControlConfig)
paths (PathsConfig)
reset (ResetConfig)
gripper (GripperConfig)
load (LoadConfig)
collision (CollisionConfig)
source_path (Path | None)
Quaternions#
Minimal quaternion helpers in the API’s wxyz convention (numpy only).
Everything on the wire – RobotState.ee_quat, set_ee_target, move_to_pose – is wxyz. Sidecars / CSVs / IsaacLab are xyzw; convert at the boundary.
- frankatwin.quat.from_rotvec_wxyz(rotvec)[source]#
Axis-angle vector (rad) -> unit quaternion; exact for small angles.
Excitation references#
- frankatwin.excitation.build_multiband_trajectory(t_s, x_anchor, q_anchor_xyzw, amp_x=0.06, amp_y=0.06, amp_z=0.06, amp_yaw=0.25, amp_roll=0.0, amp_ramp_s=2.0, *, amp_rx=0.2, amp_ry=0.15, ramp_down_s=2.0, profile='heldout')[source]#
Build the multi-band excitation trajectory on an arbitrary time grid.
- Returns:
x_des ((N, 3) Cartesian target positions, in the same frame as
x_anchor.)dx_des ((N, 3) target Cartesian velocities (analytic derivative).)
quat_des ((N, 4) target orientation quaternions, **xyzw* order, unit-norm.*)
yaw ((N,) yaw angle (about world-z) applied to the anchor.)
roll ((N,) roll angle (about EE local-z) applied to the anchor.)
tilt ((N, 2) world-frame axis-angle components (rx, ry) applied to the anchor.)
The defaults are
MULTIBAND_PROFILES["heldout"](0.06 m on x / y / z,yaw 0.25 rad, tilt 0.20 / 0.15 rad, roll off, 2 s fade-in and fade-out).
profilepicks the tone table (multiband_tones) and nothing else.Setting the four rotation amplitudes to 0 gives the position-only variant
(orientation held at
q_anchor_xyzw).
- Parameters:
- Return type:
- frankatwin.excitation.build_chirp_trajectory(t_s, x_anchor, q_anchor_xyzw, *, f0=0.1, f1=0.7, amp_x=0.1, amp_y=0.1, amp_z=0.15, amp_rx=0.5, amp_ry=0.25, amp_rz=0.5, ramp_up_s=2.0, ramp_down_s=3.0, amp_taper_exp=0.0)[source]#
Build a 6-DOF chirp reference trajectory around the given anchor pose.
amp_taper_expscales every amplitude by (f0 / f_inst)^p along the sweep (0 = constant amplitude, the low band; 0.5, the high band; 1 = constant reference velocity; 2 = constant reference acceleration – seeCHIRP_BANDS).- Returns:
(x_des, dx_des, quat_des_xyzw, rot_offsets)–x_des(T, 3) Cartesian target positions;dx_des(T, 3) analytic target velocities;quat_des_xyzw(T, 4) target orientation (xyzw), the anchor pre-rotated by the world-frame axis-angle offset;rot_offsets(T, 3) that axis-angle offset (for diagnostics and the sidecar).- Parameters:
- Return type:
Shared memory#
POSIX shared-memory layout for frankatwin.
This module is a faithful numpy.dtype mirror of src/shm_layout.h. The C++ side static-asserts the field offsets; this side enforces them via a unit test (tests/test_shm_layout.py) using ctypes to cross-check. Keep both in sync.
Concurrency model: - Command (Python writer, C++ reader): seqlock with 64-bit seq. Even = stable. - State (C++ writer, Python reader): SPSC ring buffer indexed by state_head.
All atomic operations are performed with explicit memory barriers (numpy.atomic-like) via _atomic_* helpers below; numpy itself does not provide atomics, so we rely on the GIL + write ordering of single-uint64 stores being atomic on x86_64 (they are when 8-byte aligned, which our layout guarantees).
- class frankatwin.shm_layout.ShmView(raw, header, command, states)[source]#
Read/write views over an open POSIX shared-memory segment.
Lifetime: caller owns the underlying shared_memory.SharedMemory object (see SharedMemoryAccess). All numpy views are zero-copy aliases of the same buffer.
- Parameters:
raw (memoryview)
header (ndarray)
command (ndarray)
states (ndarray)
- write_command(target_pos, target_quat, kp_pos, kp_ori, kd_pos, kd_ori, error_delta_pos, enabled)[source]#
Atomically publish a new command. Uses the seqlock protocol.
Caller must ensure target_pos has shape (3,) and target_quat has shape (4,) in wxyz convention with unit norm.
RAII wrapper over multiprocessing.shared_memory.SharedMemory.
On Linux this maps to /dev/shm/<name> (the leading ‘/’ in the C-style POSIX shm name is stripped by SharedMemory; pass either /frankatwin_osc or frankatwin_osc and we normalize).