This repository houses the Viam module for Mirka robotic sanding hardware.
Drives a Mirka AIROS 550CV orbital sander through its motor drive cabinet over Modbus RTU. Reports spindle speed, tool and drive temperatures, and decoded alarm flags, and accepts start, stop and speed commands.
The drive's ON/OFF state is a hardware line (DI1), so this model controls RUN/STOP and the speed setpoint only.
The following attribute template can be used to configure this model:
{
"serial_path": <string>,
"baud_rate": <uint>,
"parity": <string>,
"slave_address": <uint8>,
"default_rpm": <uint16>,
"timeout_ms": <uint>
}The following attributes are available for this model:
| Name | Type | Inclusion | Description | Default |
|---|---|---|---|---|
serial_path |
string | Required | Serial device for the drive cabinet, e.g. /dev/ttyUSB0. |
|
baud_rate |
uint | Optional | Modbus RTU baud rate. | 19200 |
parity |
string | Optional | Serial parity, one of E, N, O. |
E |
slave_address |
uint8 | Optional | Modbus unit ID of the drive cabinet. | 86 |
default_rpm |
uint16 | Optional | Speed used by start when no rpm is given. Must be within 4000–10000. |
8000 |
timeout_ms |
uint | Optional | Modbus request timeout. | 1000 |
Speed is limited to 4000–10000 RPM. start and set_speed reject values
outside that range.
{
"serial_path": "/dev/ttyUSB0",
"default_rpm": 8000
}Every command is a map with a command key naming the operation, plus that
command's parameters alongside it:
{ "command": "<name>", ... }Starts the spindle. Uses default_rpm unless rpm is supplied.
{ "command": "start", "rpm": 6000 }Response:
{ "running": true, "rpm": 6000 }Stops the spindle.
{ "command": "stop" }Response:
{ "running": false }Writes the speed setpoint without changing the run state. rpm is required.
{ "command": "set_speed", "rpm": 7500 }Response:
{ "rpm": 7500 }Returns the same payload as GetStatus.
{ "command": "status" }Response:
{
"running": true,
"speed_setpoint_rpm": 8000,
"average_speed_rpm": 7980,
"tool_temp_c": 41,
"drive_temp_c": 38,
"operation_state": "RUN",
"alarm_status": 0,
"alarm_flags": []
}operation_state may name more than one state at once, e.g. OFF+STOP for an
idle drive. Possible states are RUN, STOP, ON, OFF,
TOOL_CHANGE_START, TOOL_CHANGE_END, WRITE_PROTECTION_DISABLE and
WRITE_PROTECTION_ENABLE.
alarm_flags decodes alarm_status into names: tool_overheated,
motor_drive_overheated, over_current, under_voltage, over_voltage,
self_test_running, rpm_drop, high_current, tool_change_in_progress,
tool_wiring_fault, factory_reset_mode, write_protection_disabled.
Closing the component issues a best-effort STOP, so a crash or reconfigure does not leave the spindle spinning.
A gantry. Moves the Mirka AutoChanger remover knife anywhere along its 25 mm travel, and publishes a kinematic model so motion planning sees the blade where it actually is. The knife is a Festo EGSS-BS slide driven over IO-Link through an IFM AL1342 master, which the module reaches with ifm's IoT Core JSON API over HTTP.
Page references below are to Manual - Mirka AutoChanger, downloadable from Mirka's technical documents.
A gantry rather than a generic component because a frame system link carries one geometry and this needs five — body, blade, and the three boxes of the head that travels with the blade. Only a kinematic model holds a geometry per link.
Motion goes through the gantry methods; DoCommand carries only what they
cannot express.
This model does not sequence the disc-removal cycle — that interleaves knife positions with robot waypoints, so it belongs to an orchestration service holding both.
Homing is deliberately not a command. See Commissioning.
body static geometry 195 x 250 x 450
knife_joint prismatic, +X, 0-25 mm
blade rides the joint geometry 2 x 180 x 260
head rides the blade geometry 20 x 180 x 60
head-left rides the head geometry 20 x 87 x 45, turned 40° about X
head-right rides head-left geometry 20 x 87 x 45, turned −40° about X
The head is the bumper whose corner grooves fold the disc edge. Its underside is a V: a flat
40 mm wide at z=−68.4, then slopes falling 40° to grooved corners near
z=−119: width and angle from a photo of lab-sander-1, depth touched off with
the pad, since the bevelled underside sits lower than its front edge. head is the bar above the
flat and the two turned boxes lie on the slopes, so the model is no larger than
the real bumper inside the V. The boxes overhang the real bumper's outer edges,
never the V. The three head links are chained only because a kinematic model
is one chain; none moves relative to the others.
Origin is the sliding plate's front face, centred in Y, at the top face the
body and blade share. +X is the direction the knife extends, +Y across the
plate, +Z up, so every geometry hangs below z=0. Units are millimetres.
Each axis is anchored to something that can be physically touched, which is what makes the mounting frame calibratable by touching off with the arm.
| Method | Behaviour |
|---|---|
Kinematics |
the model above |
CurrentInputs |
the knife's current offset along its travel, in mm |
GoToInputs |
moves to any offset on the axis |
Position |
the same offset, in mm |
MoveToPosition |
moves to any offset on the axis; speeds are ignored |
Lengths |
[25] |
IsMoving |
the drive's motion bit |
Stop |
halts motion |
Home |
refuses — see Commissioning |
The axis is continuous over 0 to 25 mm. Offsets off it are refused rather
than snapped, so a caller asking for 40 mm is told, not silently landed at the
end stop.
The three named knife positions are the drive's command vocabulary, not its reachable set. The ends route to the mechanical end stops; everything between is reached by writing the intermediate target. Arrival at an end stop is decided by its process-data bit, anywhere else by comparing the position register against the target — every intermediate point reports the same bit.
A background poller owns the connection and refreshes a snapshot every 100 ms; every read method returns a field of it and performs no I/O. The frame system resolves this component once per node it evaluates, so serving each caller from the device meant dozens of simultaneous requests for one question.
Position comes from the drive's actual-position register (ISDU 0x0120), not
the process-data bits, which assert only at the three taught points.
The poller reads process data every cycle (~25 ms, a cyclic value the master already holds) and the position register only when a move settles, because an acyclic read costs ~860 ms. Nothing consumes the knife's position mid-stroke — the arm is stationary whenever it is near the changer.
The following attribute template can be used to configure this model:
{
"address": <string>,
"port": <int>,
"timeout_ms": <uint>
}The following attributes are available for this model:
| Name | Type | Inclusion | Description | Default |
|---|---|---|---|---|
address |
string | Required | AL1342 IoT interface, host or host:port. Not the fieldbus interface — see Networking. |
|
port |
int | Optional | IO-Link port the remover occupies, 1–8. | 1 |
timeout_ms |
uint | Optional | How long a commanded move may take to reach its position. | 10000 |
timeout_ms bounds physical completion, not the network: a jam answers every
HTTP request perfectly while never reaching its target, so the request timeout
(a constant, 3 s) cannot catch it.
There is no geometry configuration. The envelope is a property of the hardware
and lives in autochanger/model.json.
The manual (p.49) names three positions and the drive has a command for each.
Not the reachable set — the axis is continuous. GetStatus reports the drive's
own state names, because intermediate asserts for every point between the end
stops and cannot say which.
| Manual | Drive reports | Offset | Description |
|---|---|---|---|
| 2 | in |
0 |
Knife flush against the sliding plate. |
| 1 | intermediate |
tuned | Extended just enough to admit an abrasive. See Tuning the grip position. |
| 3 | out |
25 |
Fully out, releasing the disc. |
{
"address": "192.168.50.10"
}The component's api is rdk:component:gantry:
{
"name": "remover",
"api": "rdk:component:gantry",
"model": "viam:mirka:autochanger-remover",
"attributes": { "address": "192.168.50.10" },
"frame": {
"parent": "world",
"translation": { "x": 0, "y": 0, "z": 0 },
"orientation": { "type": "ov_degrees", "value": { "x": 0, "y": 0, "z": 1, "th": 0 } }
}
}Acknowledges a latched drive fault. The drive drops ready and refuses every
command until acknowledged. Edge triggered, so this asserts then clears the bit.
{ "command": "quit_error" }Response:
{ "acknowledged": true }Transport and drive state, for telling whether a fault is the link, the master or the drive. Reads the device live.
{ "command": "diagnostics" }Response (abridged):
{
"module_age": "1m33s",
"live_read_ok": true,
"position": "intermediate",
"position_counts": 2175,
"position_mm": 19.99,
"last_offset_mm": 19.99,
"transport": {
"requests": 665,
"errors": 0,
"by_class": { "ok": 665 },
"by_adr": {
"pdin/getdata": { "count": 639, "mean_rtt_ms": 6.9 },
"iolreadacyclic": { "count": 16, "mean_rtt_ms": 861.7 }
},
"max_in_flight": 2,
"max_wait_ms": 1060.9,
"first_ok_after": "2ms"
}
}by_class counts failures by cause: conn_reset and conn_refused implicate
the master, canceled only means a caller stopped listening. position_mm is a
live read, last_offset_mm what the poller last published — they diverge only if
the poller has stopped tracking.
State is served through GetStatus. There is no status DoCommand for this
model.
{
"position": "intermediate",
"moving": false,
"ready": true,
"raw": 24
}| Field | Description |
|---|---|
position |
in, out, intermediate, or unknown. |
moving |
The drive reports itself in motion. |
ready |
The drive is ready to accept commands. |
raw |
The process-data word, for diagnosis. |
position is decoded from the drive, never inferred from what was last
commanded, so it is correct immediately after a restart. unknown means no
position bit is set — in motion, or parked where none of the three applies. The
reported geometry is exact either way; it comes from the position register.
Configure the component's frame with parent, translation and orientation,
and without its optional geometry field.
That field replaces the whole kinematic envelope with one static box, silently discarding the three this model publishes.
The model is expressed in the component's own frame, so mounting lives entirely
in frame.
Five geometries reach the planner, one per link:
| Link | Description |
|---|---|
body |
Frame, motor and sliding plate. Static. |
blade |
Rides the joint; tracks the current position. |
head |
The bumper above its V, carried by the blade. |
head-left, head-right |
The bumper either side of its V. |
Separate so a caller can permit the collisions the removal process requires — the sander presses against the sliding plate — without permitting contact with the blade.
Required once per unit, fresh from the box. Deliberately not a module command: the precondition is physical and Viam cannot enforce it.
- Remove the sliding plate from the Remover (AutoChanger manual p.49).
- Clear the knife's full travel path in both directions.
- Send the reference command once:
curl -s -X POST http://<address> -H 'Content-Type: application/json' \
-d '{"code":"request","cid":1,"adr":"/iolinkmaster/port[1]/iolinkdevice/iolwriteacyclic","data":{"index":2,"subindex":0,"value":"CE"}}'The value is CE. CD is Restore Factory Settings.
- Watch until the drive settles; a two-pass stroke detection is normal:
curl -s -X POST http://<address> -H 'Content-Type: application/json' \
-d '{"code":"request","cid":1,"adr":"/iolinkmaster/port[1]/iolinkdevice/pdin/getdata"}'- Refit the sliding plate.
Homing with the plate fitted teaches a zero short by the plate's thickness. The symptom appears much later, as discs failing to release (manual p.55).
Homing also overwrites the intermediate position parameter with the full stroke. The module rewrites it before every intermediate move, so re-homing is safe.
The grip position is where the knife is open just enough to admit an abrasive. It is a process parameter, not a hardware fact — it depends on the disc, so the caller holds it, not this module.
Tune it with MoveToPosition until a disc slides in with slight clearance and no
force, approaching from flush each time. Roughly 0.7 mm suits a standard
abrasive.
To measure the slot instead, measure from the plate's front face to the knife's rear face — the surface the abrasive bears against. Measuring to the front of the blade includes its thickness and gives roughly twice the real gap. That is a gap: subtract 0.42 mm, the clearance at the drive's zero, for the joint offset.
- The air nozzle is not installed. The pneumatic kit blows the released disc clear at position 3. Without it a removal may peel a disc without clearing it.
- The body is wider than the blade. 250 mm against 180, so the body sets the approach clearance.
- The model puts the blade 0.42 mm too far in. The geometry places its rear face on the plate at joint zero, where the drive's zero leaves that much clearance. Constant across the stroke, and understates the blade's reach.
failsafeiolinkis unresolved. The master's fail-safe output setting for this port reads0and that enum's meaning is unestablished. It decides what the outputs do when the controller disconnects.
The AL1342 has two independent network interfaces, with different MAC addresses and separate IP configuration:
| Interface | Serves | Ships as |
|---|---|---|
| IoT | HTTP / IoT Core JSON — what this module uses | DHCP, falling back to link-local |
| Fieldbus | Modbus TCP | static, 192.168.1.250 |
They are not bridged and sit on different physical ports. address must be the
IoT interface.
Give it a static address. A link-local address is self-assigned and not stable across conflicts or resets, so a machine config naming one works until the day it silently does not.
The host NIC facing it needs a persistent address too — ip addr add does not
survive a reboot, and viam-server would then fail to reach the master with only a
connect timeout to explain it. Configure it with no gateway, or a
point-to-point industrial link will displace the default route on the interface
carrying general traffic.
A generic service that runs the Mirka AutoChanger disc-removal cycle end to
end: stops the spindle, plans the arm to the remover avoiding the piece, and
alternates arm waypoints with knife moves in the manual's order. remove ends
with the arm in free space at wp5, clear of the changer; it does not return the
arm anywhere, and the caller owns the return trip. It drives the
viam:mirka:autochanger-remover gantry and the viam:mirka:airos-550cv
spindle as dependencies and does not modify either.
{
"remover": "remover",
"arm": "arm",
"mirka": "airos",
"tool_frame": "compliance",
"obstacle_visions": ["snapshot_mesh_vision_service"],
"input_range_override": { "arm": { "2": { "min": -4.0, "max": 0.0 } } },
"grip_offset_mm": 0.7
}Every field is something the service cannot derive on its own:
| Name | Type | Inclusion | Why it can't be derived |
|---|---|---|---|
remover |
string | Required | Which gantry resource is the AutoChanger remover. |
arm |
string | Required | Which arm resource to plan and move. |
mirka |
string | Required | Which generic resource is the spindle to stop in preflight. |
tool_frame |
string | Required | Which frame carries the pad geometry; there is no convention to guess it from. |
obstacle_visions |
list of string | Optional | Vision services whose objects are obstacles, such as the pass mesh. Without any, the cycle plans against the frame system alone. |
input_range_override |
object | Optional | Joint limits that tighten the arm model's, as in the sanding config. |
grip_offset_mm |
float64 | Required | The one disc-dependent number the manual leaves to the integrator; a process parameter, not a hardware fact. Must be strictly between 0 and 25. |
contact_frames |
list of string | Optional | End-of-arm frames besides tool_frame that may touch the remover in contact steps. The press into the sprung plate is modelled as the pad sinking into a rigid body, so any geometry less than the 10 mm press depth behind the pad face reads as a collision. Which frames those are depends on how the cell models its end effector. Must not list tool_frame. |
contact_speed_degs_per_sec |
float | Optional | Joint speed for the contact steps, every move that touches the bumper and wp2 to wp5, in place of the arm's configured speed, so they stay slow whatever the arm is set to. The free moves keep the arm's own speed. Default 10. |
contact_acceleration_degs_per_sec_per_sec |
float | Optional | Joint acceleration for the contact steps, as above. Default 20. |
| Call | Behaviour |
|---|---|
{"command": "remove"} |
Preflight, which plans every arm step before anything moves and refuses (not_started) if any cannot be planned, then run the stored paths. Blocks. One at a time — a second call while running is refused. A failure is reported, not latched: the next call starts afresh with preflight. Refused while a step cycle is paused; continue it with step or abandon it with reset. Context cancel aborts the cycle and stops both actuators; no recovery is attempted. |
{"command": "step"} |
Runs one step, then pauses: the first step is preflight, the next is fold-l-below (the pad below the bumper's left corner), then the rest in the manual's order: the removal cycle lists every step. Resuming is refused unless every arm joint is still within 0.01 rad of where the last arm step left it; the refusal moves nothing and the cycle stays paused. A failure ends the cycle as in remove; the next step starts afresh with preflight. |
{"command": "reset"} |
Abandons a paused step cycle. Moves nothing and reads nothing. Refused while a cycle is running, and an error when nothing is paused. |
{ "command": "remove" }Response:
{ "removed": true }{ "command": "step" }Response, while steps remain, then after the last one:
{ "completed": "fold-l-below", "next": "fold-l-press" }{ "completed": "regrip", "removed": true }A paused cycle lives in memory: a module restart or a reconfigure drops it, so check the cell by eye before the next call.
{ "command": "reset" }Response:
{ "state": "idle" }Owned by the caller, not checked beyond what preflight can see:
- The arm is not moving. Preflight checks this, and refuses (
not_started) whiletool_frameor anycontact_framesgeometry touches the remover: the planner would otherwise drive a pad left in contact out along an unconstrained path. - Any
obstacle_visionsanswer: a vision that errors, rather than returning nothing, fails preflight. - The remover's frame is calibrated (touched off against the plate) — every waypoint is relative to it.
tool_framefollows the pad contract the waypoints are built on: its origin is the centre of the pad face, its +Z is the outward pad normal, and its one geometry (a box or capsule, the pad) sits behind the face along −Z. Preflight checks this in the tool frame and refuses when the geometry is rotated more than 1°, off-centre in X or Y by more than 1 mm, or its +Z face is more than 1 mm offz = 0— a tool frame 30 mm above the face would otherwise press 40 mm at wp2.input_range_override, if the sanding config sets one, matches it.
Status reports state (idle, running, paused, failed), and while
running or failed, step and step_index. A paused status reports
completed and next. A failed status adds error and report, plus
knife_offset_mm, drive_ready, and arm_at_wp5 where the
hardware could be read — each is omitted rather than guessed if its read fails.
arm_at_wp5 is never reported for a not_started failure, since the arm never
left free space.
A failure is reported until the next remove, which starts afresh with
preflight. Nothing latches: a caller that loops remove must stop on an error
itself. A pad left in contact is refused by preflight; a disc left in the knife
or on the pad is not detectable, so check the cell before calling remove
after a failure. A rebuild of the service also closes the old instance, which cancels a
running remove, stops both actuators, and returns only once the cycle has ended.
What each failure leaves in the cell:
| Step | Failure leaves | Disc | Reported as |
|---|---|---|---|
| preflight, including planning every arm step | nothing moved, except knife possibly moved to flush and spindle stopped | on pad | not_started plus reason |
fold-l-below |
arm in free space | on pad | arm_displaced |
fold-l-press, fold-l-stroke |
pad against the bumper | on pad, edge may be bent | in_contact |
| knife grip | arm clear of the bumper, knife between flush and grip | on pad | arm_displaced |
| wp1 transit | arm in free space | on pad | arm_displaced |
| wp2 press, wp3 lift | pad against plate or knife partly under disc | on pad, edge may be lifted | in_contact |
| knife clamp | pad at wp3, knife between grip and flush | edge maybe pinched | in_contact |
| wp4 tilt, wp5 back | pad peeling away, disc clamped | partly or fully off, held by knife | in_contact |
| knife release, knife grip | arm clear at wp5 | in knife or dropped, unknown | knife_holding |
Operator recovery sequence: send the arm home, then quit_error on the
remover driver if it has latched a drive fault, then resume the pass. The service never sends quit_error itself and never
auto-recovers — a fault latched for a reason, and clearing and retrying blind
is how a jam becomes a bent blade.
The step sequence, how each waypoint is derived from the remover model, the planning constraints, and the tuning constants are in removersvc/README.md.