All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
v0.2.0 (2026-09-01)
Breaking Changes
- Requires
bb ~> 0.31(was~> 0.22). Actuator command delivery now goes throughBB.Actuator.handle_command/2(bb 0.23+): the framework subscribes each actuator to its command topic, enforces the armed check, and gates payload types before the driver sees them. Code that called the actuators' oldhandle_cast({:command, ...})/handle_infoclauses directly must use the command pipeline (BB.Actuator.set_position/4,BB.call(robot, name, {:command, msg})). BB.Actuator.set_position/4is synchronous under bb 0.30+: it returns:okor{:error, reason}, and the Gripper, LinearTrack, and Cartesian actuators now reply with the error when the controller cannot deliver a frame.- Cartesian moves have a first-class payload:
BB.Ufactory.Message.Command.CartesianMove(6-DOF pose + optional speed/acceleration; integer fields accepted), delivered through the gated pipeline. The raw{:move_cartesian, pose}cast is deprecated: it logs a warning and is dropped when the robot is not armed (it previously moved a disarmed robot — the firmware stays motion-ready underdisarm_action: :hold). use BB.Ufactory.Robots.XArm6takes options (:host,:loop_hz,:simulation,:controller,:cartesian,:gripper,:linear_track) instead of the documented-but-never-compilingcontrollers-override pattern; a:cartesianactuator is included by default. Accessory actuators mount on fixed joints (bb's DSL allows actuators only under joints — the old link-level examples never compiled either).- The controller's shared ETS joint rows are now 5-tuples
(
{joint, current_position, current_torque, set_position, set_velocity}). {:send_and_recv, frame}defaults to a 500 ms timeout (was 5 s — it blocks the controller GenServer mid-stream) and can return{:error, :desync}on an unrecoverable stream.Sensor.ForceTorque's inertpoll_interval_msoption is removed (wrench data is push-based from report frames; the option was ignored).Protocol.cmd_move_joints/4raises for more than 7 angles instead of silently dropping extras;cmd_set_mode/2rejects modes outside0..7.Actuator.Jointrefuses a topology transmission at init ({:stop, {:unsupported_transmission, name}}): bb hands the driver motor-space values but the firmware receives joint angles, so a non-identity transmission would silently skew every command.
Bug Fixes
Protocol.cmd_stop/1now sends SET_STATE 4 (stop) instead of SET_STATE 0 (motion state). State 0 is the firmware's motion-ready ("sport") state, so the:stopdisarm action — including the fresh-connection safety disarm — was commanding the arm into motion state rather than terminating motion and clearing queued commands. Firmware-tier simulator tests now pin the semantics (a byte-level test cannot), andRegisters.set_state/0's doc table is corrected too.- Fixed an ETS lost-update race between report ingestion and joint
commands: both sides did read-whole-row-then-insert on the same public
rows, so a report frame interleaving with a command could silently drop a
fresh target — or resurrect the pre-Stop target after a brake latch. Each
side now writes only its own columns via
:ets.update_element/3. send_and_recvmatches replies by register: while armed, 100 Hz move-acks are in flight that the pre-send drain cannot clear, and the first-frame-wins behavior could hand an RS485 caller a move-ack (refusing a healthy linear-track Stop as:position_unknown). Foreign frames are skipped and split frames reassembled until the deadline.- The 100 Hz loop no longer streams motion on stale feedback: with the
report socket down it kept commanding joints to frozen last-known angles
indefinitely. Ticks are skipped when the last report frame is older than
report_stale_ms(new option, default 250 ms), and afterfeedback_loss_fatal_ms(default 3 s) of blindness with motion pending the controller stops and reports toBB.Safety. - Sensors no longer crash the whole robot supervision tree when the
controller is unreachable at init (mock simulation, or a crash-restart
race on hardware) —
ForceTorqueandCollisiondegrade to a warning. - Gripper clamp raised to the SDK's true fully-open value (850 pulses, was 840 — fully-open commands stopped ~0.9 mm short).
parse_linear_track_position/1requires Modbus function 0x03, so RS485 exception replies no longer decode as garbage positions; u16 speed fields clamp at 0xFFFF instead of bit-wrapping to a crawl.{:register_arm_frames, ...}replies{:error, reason}(not:ok) when the armed-time send fails; the controller ETS handle is always the table name (a cached tid raised after a controller crash-restart);Simulator.command/3reassembles split responses instead of leaking{:more}throughreachable?/3.mix bb_ufactory.sim start <unknown-model>prints the usage error again instead of raisingArgumentError.
Features
Actuator.JointacceptsCommand.StopandCommand.Hold, braking the joint by latching its current reported position as the 100 Hz loop target. It also declarescapabilities: [:position_feedback, :effort_feedback], so joints it drives no longer warn about missing position sensors (the controller publishesJointStatefrom every report frame).Command.Positionvelocity hints are honored: the loop dispatches each batch at the slowest pending hint (clamped to the model maximum), with acceleration scaling to match; brake latches clear the hint so stopping always happens at full speed.Actuator.LinearTrackacceptsCommand.Stop: it reads the carriage's current position over RS485 (one round-trip — the read doubles as the BeginMotion estimate) and re-targets it, refusing the stop with a logged warning when the read fails rather than commanding an assumed position. Stop/Hold refusals are logged everywhere, since fire-and-forget transports discard the error reply.- The 100 Hz control loop runs on
BB.Loop(bb 0.26+): ticks are scheduled against absolute monotonic deadlines, so the loop no longer drifts by per-tick processing time, and overruns are reported via[:bb, :loop, :tick]telemetry instead of accumulating silently. - The out-of-band safety disarm now waits briefly for the firmware's response and logs when the stop went unconfirmed.
Improvements
.formatter.exsimports the Spark DSL locals from bb's exported formatter config instead of a hand-maintained copy.- Added
reachto the dev/test toolchain:.reach.exsmachine-checks the wire-layer purity invariant (Protocol/Report/Registers/Model must not depend on runtime components, message structs, or sockets), andmix checknow runsmix reach.check --arch --smells --strict. - Test suite hardening: byte-exact RS485 frame pins (host/device/function/
register bytes — self-referential assertions could not catch a swapped
bus),
stream_dataproperties for fp32 round-trips and frame parsing across arbitrary TCP split points, firmware-tier stop-semantics tests, and an interleaving stress test for the ETS race. CI compiles test files with--warnings-as-errors(a tag-excluded simulator test calling a removed API previously surfaced only as a warning) and refuses to publish a version without a CHANGELOG entry. - CI runs on Elixir 1.20.4 (was 1.20.2); the library now requires Elixir ~> 1.20.
- Docs corrected end to end: quick-start and accessory examples compile
(verified by test), disarm behaviour and F/T data flow described
truthfully, gripper units fixed, tutorials standardized on the
synchronous
BB.ActuatorAPI.
v0.1.0 (2026-07-19)
Initial release.
Features
BB.Ufactory.Controller— manages the command (502) and real-time report (30003) sockets, the 100 Hz batched joint-motion loop, heartbeat and error-code polling, the arm/disarm safety lifecycle, and hardware configuration (TCP offset/payload, reduced mode, workspace fence).- Actuators: per-joint position (
Actuator.Joint), Cartesian moves (Actuator.Cartesian), Gripper G2 (Actuator.Gripper), and linear track (Actuator.LinearTrack, with stroke clamping). - Sensors: force-torque wrench forwarding (
Sensor.ForceTorque) and firmware collision events (Sensor.Collision). BB.Ufactory.Protocol— pure encode/decode for the full wire protocol, including firmware kinematics and workspace queries (cmd_get_fk,cmd_get_ik,cmd_tcp_limit_check,cmd_joint_limit_check).- Per-model configuration for xArm5/6/7, Lite6, and UF850 with joint-limit tables verified against the official URDFs and against UFACTORY's firmware limit enforcement in simulation.
- Simulator testing support for library consumers:
mix bb_ufactory.sim(container lifecycle),BB.Ufactory.SimulatorCase(ExUnit case template),BB.Ufactory.Simulator(protocol-level helpers that also work against physical arms, including thereachable?/3workspace probe), and the Testing against the UFACTORY simulator tutorial. - Safety hardening throughout: command-socket failures stop the controller
and report to
BB.Safety, malformed report streams force a reconnect, motion ticks never substitute defaults for unknown joint positions, and accessory enable frames are sequenced after the controller's arm sequence.
CI validates every commit against UFACTORY's firmware simulator for all five supported arm models.