Skip to content

Repository files navigation

FabMotion — Simulated Wafer-Handler Control Loop

FabMotion is a software simulation of a Cartesian gantry wafer handler: path planning, jerk-limited trajectory generation, a fixed-period Rust control loop, and a simulated motor-controller interface (PD-servo plant + encoder/current/limit-switch feedback), with fault injection and a supervisory state machine.

What this is not: FabMotion does not talk to real motors, encoders, or field buses (EtherCAT/CANopen/etc.), and it makes no hard real-time guarantee. It runs as an ordinary process on stock (non-PREEMPT_RT) Linux, where the OS scheduler can and does preempt it. Every timing number below is a real measurement from this repo's own test hardware, not a spec claim — see Measured results.

Architecture

Vision/localization   (Python + OpenCV, synthetic images -> pixel->mm target)
        |
Path planner           (Rust: grid-based A* around virtual obstacles)
        |
Trajectory generator    (Rust: jerk-limited real-time stepper, or closed-form trapezoidal)
        |
Rust Linux control loop (fixed-period, monotonic-clock scheduled, jitter/WCET measured)
        |
Simulated motor controllers  (PD-servo plant per axis: position/velocity cmd, enable/disable)
        |
Encoder/current/limit-switch feedback  (noisy, delayable, dropout-able — all fault-injectable)

Supervisory states: Homing -> Idle -> Running -> Fault -> Recovery -> Idle.

Repository layout

src/
  main.rs            orchestrates a full wafer-transfer demo scenario
  motor.rs           simulated motor-controller interface (plant + feedback + faults)
  trajectory.rs       TrapezoidalProfile (closed-form) + JerkLimitedStepper (real-time)
  planner.rs          A* grid planner with line-of-sight path simplification
  state_machine.rs    Homing/Idle/Running/Fault/Recovery
  fault_injection.rs  JSON-scheduled fault scenarios
  control_loop.rs     fixed-period loop + jitter/deadline-miss/WCET measurement
  logging.rs          CSV per-cycle telemetry + JSON run summary
tests/
  integration_tests.rs  runs the actual compiled binary end-to-end
vision/
  detect_wafer.py     OpenCV fiducial detection on synthetic images (Python, stretch goal)
analysis/
  analyze_logs.py      turns CSV logs into jitter/tracking-error/position plots
faults/
  scenario.json        the default fault-injection schedule used by `cargo run`
.github/workflows/ci.yml   build, test, clippy, vision smoke test

Running it

cargo build --release
cargo test --release                 # 15 unit tests + 2 integration tests

# Full demo: homes 3 axes, then 3 round trips between a load and a process
# station, routing around a virtual obstacle, with 4 scheduled faults
# (encoder dropout, overcurrent, comm delay, stale feedback).
./target/release/fabmotion
#   --duration-s <f64>     default 45
#   --period-us <u64>      default 1000 (1kHz)
#   --faults <path>        default faults/scenario.json
#   --no-faults            disable fault injection
#   --log-csv <path>       default logs/run.csv
#   --summary-json <path>  default logs/summary.json
#   --seed <u64>           RNG seed for encoder noise

python3 analysis/analyze_logs.py --csv logs/run.csv --summary logs/summary.json
python3 vision/detect_wafer.py --seed 1

Simulated motor-controller interface

Each of the 3 axes (X, Y, Z) is a simulated motor-controller interface: a PD-servo driving a point-mass plant, with force/current saturated at a configurable rated current (so a controller error can't produce unphysical instantaneous torque), plus:

  • Position and velocity command inputs
  • Noisy encoder position feedback (independent of the "true" simulated position)
  • A current/force estimate derived from the saturated servo effort
  • Enable/disable state (faults force-disable the axis)
  • Limit switches evaluated against true position
  • Fault codes: Overcurrent, EncoderDropout, StaleFeedback, LimitSwitchMin/Max, CommDelayTimeout, PositionError

Fault injection (faults/scenario.json, or --faults <path>) schedules any of: encoder dropout, stale feedback, overcurrent, limit-switch-stuck, and communication delay, each with a start time and duration. See fault_injection.rs for the schema (tagged JSON, one object per fault).

Trajectory generation

  • TrapezoidalProfile: exact closed-form trapezoidal velocity profile (instantaneous acceleration changes).
  • JerkLimitedStepper: the generator actually used by the demo scenario — a real-time, per-control-cycle generator using a critically-damped, amax-saturated acceleration command (bang-bang-like far from target, smoothly damped near it), then jerk-limited by ramping acceleration at a bounded rate. This is evaluated incrementally each cycle, the way an embedded motion controller would do it, rather than solved in closed form.

Path planning

planner.rs runs 8-connected A* on a 5mm grid around rectangular virtual obstacles, then applies line-of-sight string-pulling to collapse the grid staircase into the minimal set of straight-line waypoints that still clears every obstacle. (An earlier version skipped this step and used the raw grid path — the gantry braked to near-zero speed at every single 5mm grid step and never reached cruise velocity. Worth knowing if you modify the planner.)

Measured results

From a representative 45s run (cargo run --release, default scenario, 1kHz loop, on the CI/dev container's shared vCPU — not a dedicated or PREEMPT_RT machine):

Metric Value
Median loop jitter ~9 us
p99 loop jitter ~190 us
Worst-case observed jitter ~21 ms
Worst-case observed cycle exec time ~4.4 ms
Deadline miss rate ~0.5%
Max tracking error (commanded vs. simulated-true position) ~3.6 mm
Avg settling time per point-to-point move ~1–2 s
Fault detection latency 0 cycles (this model detects synchronously within the cycle the fault manifests)
Recovery duration ~0.1 s (configurable dwell)

Re-run python3 analysis/analyze_logs.py after any cargo run to regenerate analysis/*.png and analysis/report.md with your own numbers — jitter and miss rate are workload- and hardware-dependent, which is the point: they're measured, not assumed.

Example output (from logs/sample/, a 6s run committed in this repo):

jitter histogram position trace tracking error

Why the worst-case jitter (~21ms) is so much larger than the median (~9us): this is an ordinary process on a shared, non-realtime kernel. Rare scheduler preemptions, page faults, or CI-runner noisy-neighbor effects show up as occasional multi-millisecond stalls. A production system needing bounded worst-case latency would need PREEMPT_RT (or a genuine RTOS) and CPU isolation — neither of which this repo claims to provide.

Vision stretch goal

vision/detect_wafer.py generates a synthetic top-down image of a wafer station with a colored fiducial marker, detects it via OpenCV (HSV color threshold + contour centroid), and converts the pixel centroid into a simulated robot target via a fixed pixel-to-mm affine calibration — a stand-in for "camera looking down at the gantry's calibrated coordinate frame." It is not connected to a live camera, and it is intentionally decoupled from the Rust control loop (it writes vision/target.json, which nothing currently auto-consumes — wiring that in is a natural follow-up).

example vision detection

What's simplified / not implemented

  • Multi-axis coordinated motion is per-axis independent, not a true jointly-time-scaled multi-axis blend.
  • Recovery resumes by re-planning from wherever the axes actually settled, rather than resuming the exact interrupted trajectory.
  • The vision pipeline runs on synthetic, not live, camera images.
  • No hard real-time guarantees, ever — see the disclaimer at the top.

License

MIT — see LICENSE.

About

Rust/Linux motion-control simulator for a three-axis wafer handler with A* planning, a measured 1 kHz loop, fault injection, and telemetry analysis.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages