Files
rust-stm32/examples.md
2026-07-03 12:59:18 +02:00

39 KiB

Chapter Examples

This file describes the workshop examples in implementation detail.

The goal is not to create isolated Rust toy snippets. Each example is one step in a build-along journey toward the software core of a RoboCup Junior Soccer-style robot:

hardware input
  ↓
robot event
  ↓
robot state
  ↓
decision
  ↓
motion command
  ↓
actuator output

The examples must be small, independently runnable, and easy to modify during the workshop. Participants should be able to run one chapter, change one thing, break it, fix it, and then move on.

The examples should be implemented as separate binaries under:

src/bin/

The intended file list is:

00_check.rs
01_rust_basics.rs
02_ownership.rs
03_types.rs
04_first_firmware.rs
05_rgb_output.rs
06_button_events.rs
07_events.rs
08_state_machine.rs
09_sensor_pipeline.rs
10_motor_commands.rs
11_kinematics.rs
12_embassy.rs

Host examples run with cargo run or cargo test on the participant laptop. Embedded examples build on the STM32 Blue Pill with the workshop aliases, for example build-04 and run-04. Rustup-only users can use direct cargo build --target thumbv7m-none-eabi --bin ... commands and probe-rs run.

General implementation principles

Each example should follow these rules:

  1. Keep the example focused on exactly one concept.
  2. Include a working baseline.
  3. Include one or more clearly marked TODO sections for participants.
  4. Print or show enough feedback that participants know whether it works.
  5. Keep robotics terminology consistent across chapters.
  6. Reuse domain names where possible: RobotEvent, RobotMode, MotionCommand, BallObservation, ObstacleObservation.
  7. Prefer explicit and readable code over clever abstractions.
  8. Avoid advanced lifetime-heavy Rust unless it is absolutely necessary.
  9. Keep all host-side examples testable without STM32 hardware.
  10. Keep all embedded examples small enough to flash and debug quickly.

The workshop should feel like building the same robot controller step by step, not like jumping between unrelated exercises.


00_check.rs — Toolchain and sanity check

Purpose

This example verifies that the participant environment is usable before the real workshop content starts.

It answers:

  • Can Rust compile a host-side binary?
  • Can the repository be opened and built?
  • Can the participant run a simple command?
  • Optionally: can the STM32 target be built?

This example should not teach a new Rust concept. It is a confidence and troubleshooting checkpoint.

Conceptual role in the robot journey

Before a robot can do anything useful, the development loop must work.

edit code -> build -> run -> observe result

For embedded work this later becomes:

edit code -> build -> flash -> observe hardware

Code behaviour

The host-side program should print a short confirmation message:

Rust on Robots toolchain check
Host program runs correctly
Next step: build the embedded firmware

If useful, also print the selected workshop target/chip as a constant string.

Participant tasks

Participants should:

  1. Run the example.
  2. Confirm that the output appears.
  3. Change one printed line.
  4. Run it again.

Suggested command

cargo run --bin 00_check

Expected result

The program compiles and prints the configured sanity-check message.

Implementation notes for the code bot

  • Keep this example host-side and std-based.
  • No STM32 code here.
  • No complicated dependencies.
  • Use simple println! output.
  • This file should be nearly impossible to fail except for toolchain setup problems.

Acceptance criteria

  • cargo run --bin 00_check works.
  • The output is obvious and friendly.
  • Participants can change one string and rerun it.

01_rust_basics.rs — Values, functions and visible mutation

Purpose

This example introduces only the Rust basics needed for the rest of the workshop:

  • values
  • immutable-by-default variables
  • mut
  • functions
  • simple structs or enums only if helpful

It should not become a full Rust introduction.

Conceptual role in the robot journey

Robot software should not mutate everything from everywhere.

This example introduces the idea that values are stable by default and mutation must be intentional.

sensor reading -> event -> state transition -> command

That model only works if state changes are controlled.

Code behaviour

The program should model a tiny status calculation, not generic arithmetic.

Example behaviour:

  • Start with a fixed current_speed.
  • Compute a target_speed with a function.
  • Print both values.
  • Show one intentionally mutable variable, such as status_message or target_speed.

Possible output:

current speed: 0
selected target speed: 40
status: searching

Suggested code shape

The code should include:

fn select_target_speed(searching: bool) -> i32 {
    if searching {
        40
    } else {
        0
    }
}

Then in main:

let current_speed = 0;
let mut target_speed = select_target_speed(true);

target_speed = target_speed + 5;

The participant should see where mutation happens.

Participant tasks

Participants should:

  1. Run the example.
  2. Change the search speed.
  3. Add a second function, for example select_status_name.
  4. Change one immutable variable and observe the compiler error.
  5. Decide whether the variable should actually be mutable.

Suggested command

cargo run --bin 01_rust_basics

Expected result

The participant sees how Rust separates stable values from mutable values.

Implementation notes for the code bot

  • Keep the code host-side.
  • Use println! for feedback.
  • Include one commented-out line that would fail because it tries to mutate an immutable value.
  • Avoid ownership examples here; that belongs to the next chapter.

Acceptance criteria

  • The example compiles in the baseline state.
  • Participants can modify a constant or function return value.
  • There is one clear optional compiler-error experiment about mutation.

02_ownership.rs — Ownership and borrowing as resource responsibility

Purpose

This example introduces ownership and borrowing through robot commands.

It should teach:

  • moving a value
  • borrowing a value with &T
  • mutably borrowing a value with &mut T
  • why this matters for hardware resources and robot state

Conceptual role in the robot journey

A robot controller should not have five different parts silently changing the same command or peripheral.

Ownership answers:

Who is responsible for this value or resource?

Borrowing answers:

Who may temporarily inspect or modify it?

Code behaviour

The program should define a simple robot command type:

#[derive(Debug)]
struct MotionCommand {
    forward: f32,
    strafe: f32,
    rotate: f32,
}

The program should then:

  1. Create a command.
  2. Print it by borrowing it.
  3. Modify it through a mutable borrow.
  4. Print it again.

Suggested code shape

Include functions like:

fn print_command(command: &MotionCommand) {
    println!("command = {command:?}");
}

fn stop(command: &mut MotionCommand) {
    command.forward = 0.0;
    command.strafe = 0.0;
    command.rotate = 0.0;
}

Also include a commented-out move example:

// let next_command = command;
// print_command(&command); // This would no longer compile after the move.

Participant tasks

Participants should:

  1. Run the baseline.
  2. Uncomment the move example and read the compiler error.
  3. Revert it.
  4. Add a function rotate_left(command: &mut MotionCommand).
  5. Call it before stop.
  6. Print the command after each step.

Suggested command

cargo run --bin 02_ownership

Expected result

Participants should understand ownership as responsibility, not as abstract syntax.

Implementation notes for the code bot

  • Keep this host-side.
  • Use #[derive(Debug, Clone, Copy)] only if needed, but avoid hiding ownership too early.
  • For the first version, do not make MotionCommand Copy; the move error is useful.
  • If the example gets too frustrating, a later comment may explain when Copy would be acceptable.

Acceptance criteria

  • Baseline compiles and prints command changes.
  • There is a safe mutable-borrow update function.
  • There is one optional ownership error experiment.

03_types.rs — Domain types, enums and Result

Purpose

This example shows how Rust types can model robot concepts better than raw numbers and booleans.

It should teach:

  • newtype structs for units
  • enums for meaningful alternatives
  • Result for sensor failure
  • match for explicit handling

Conceptual role in the robot journey

Robots often fail because raw values are treated as meaning.

Bad:

42

Better:

DistanceCm(42)
BallObservation::Left
SensorError::Timeout

Code behaviour

The program should simulate reading a fake ultrasonic sensor and a fake camera result.

It should:

  1. Convert a raw echo value into DistanceCm.
  2. Classify distance as clear, warning or too close.
  3. Represent ball position with BallObservation.
  4. Print the resulting interpretation.

Suggested types

#[derive(Debug, Clone, Copy, PartialEq)]
struct RawEchoMicros(u32);

#[derive(Debug, Clone, Copy, PartialEq)]
struct DistanceCm(u16);

#[derive(Debug, Clone, Copy, PartialEq)]
enum SensorError {
    Timeout,
    OutOfRange,
    NotCalibrated,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum ObstacleObservation {
    Clear,
    Warning,
    TooClose,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum BallObservation {
    NotSeen,
    Left,
    Center,
    Right,
    TooClose,
}

Suggested functions

fn echo_to_distance(raw: RawEchoMicros) -> Result<DistanceCm, SensorError> {
    if raw.0 == 0 {
        return Err(SensorError::Timeout);
    }

    let cm = raw.0 / 58;

    if cm > 400 {
        Err(SensorError::OutOfRange)
    } else {
        Ok(DistanceCm(cm as u16))
    }
}

fn classify_obstacle(distance: DistanceCm) -> ObstacleObservation {
    match distance.0 {
        0..=15 => ObstacleObservation::TooClose,
        16..=35 => ObstacleObservation::Warning,
        _ => ObstacleObservation::Clear,
    }
}

Participant tasks

Participants should:

  1. Run the example.
  2. Change the fake echo value.
  3. Observe how the classification changes.
  4. Add a new BallObservation variant, for example FarAway.
  5. Add or update one match arm.
  6. Trigger a fake sensor error and handle it.

Suggested command

cargo run --bin 03_types

Expected result

Participants should see that type design is robot design.

Implementation notes for the code bot

  • Keep this host-side.
  • Add at least two sample raw echo values.
  • Print both successful and failing cases.
  • Use match, not unwrap, so error handling is visible.

Acceptance criteria

  • Baseline compiles and prints at least one valid distance classification.
  • Baseline demonstrates one Err path.
  • Participants can change thresholds and variants easily.

04_first_firmware.rs — First STM32 firmware

Purpose

This is the first embedded example. It introduces the structure of a minimal STM32 firmware program.

It should teach:

  • embedded firmware is not a desktop application
  • no_std means no normal standard library
  • the program initializes hardware and then stays in a loop
  • flashing is part of the development loop

Conceptual role in the robot journey

Before a robot can read sensors or drive motors, the firmware must boot reliably.

This chapter establishes the embedded loop:

init hardware -> loop forever -> observe behaviour

Code behaviour

The program should:

  1. Start on the STM32.
  2. Initialize the minimal required runtime/HAL setup.
  3. Enter an endless loop and keep the setup minimal.

There does not need to be visible LED output yet. That comes in the next chapter.

Participant tasks

Participants should:

  1. Build the firmware.
  2. Flash it to the STM32.
  3. Confirm that flashing succeeds.
  4. Confirm that the firmware reaches the loop.

Suggested command

build-04

Suggested run

run-04

Rustup fallback

cargo build --target thumbv7m-none-eabi --bin 04_first_firmware
probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/04_first_firmware

Expected result

The binary builds for the embedded target and can be flashed to the board.

Implementation notes for the code bot

  • This file is embedded-target code.
  • Use the HAL/runtime style already selected for the repo.
  • Keep initialization minimal.
  • Do not introduce LED or button logic here.
  • Keep the loop empty or insert a cortex_m::asm::nop().

Acceptance criteria

  • The firmware builds for STM32.
  • The firmware flashes successfully with the selected command.
  • The code clearly shows the initialization and endless loop structure.

05_rgb_output.rs — RGB LED as actuator and status output

Purpose

This example introduces output control.

The RGB LED is treated as a real actuator and status display, not as a toy.

It should teach:

  • configure GPIO as output
  • set pins high/low
  • use timing/delay
  • represent robot state visibly

Conceptual role in the robot journey

A robot needs visible status.

Without a screen or logs, a status LED can show:

Idle
Searching
Driving
Obstacle
Error

This is useful during real floor debugging.

Code behaviour

The firmware should blink or switch the RGB LED according to a status pattern.

Recommended mapping:

Off          -> Idle
Blue blink   -> SearchBall
Green        -> DriveToBall
Yellow       -> AlignToBall
Red blink    -> AvoidObstacle or Error

The baseline should at least implement two states:

Idle
SearchBall

Suggested types

#[derive(Debug, Clone, Copy, PartialEq)]
enum RobotMode {
    Idle,
    SearchBall,
}

A helper function should map mode to LED output:

fn show_status(mode: RobotMode /* plus LED pins */) {
    // Set RGB pins according to mode.
}

The exact signature depends on the HAL pin types.

Participant tasks

Participants should:

  1. Flash the baseline.
  2. Confirm the LED behaviour.
  3. Change the blink timing.
  4. Add a second or third status pattern.
  5. Change the mode in code and observe the LED.

Suggested command

run-05

Rustup fallback

cargo build --target thumbv7m-none-eabi --bin 05_rgb_output
probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/05_rgb_output

Expected result

Participants can see that firmware controls physical output.

Implementation notes for the code bot

  • This file is embedded-target code.
  • The actual RGB LED pin mapping must match the workshop hardware.
  • Define constants or comments for pin mapping.
  • Keep active-high vs active-low behaviour explicit.
  • Avoid hiding all pin operations behind too much abstraction; participants should still see what is happening.

Acceptance criteria

  • The firmware flashes.
  • The LED visibly changes state.
  • Participants can change timing or status mapping in one obvious place.

06_button_events.rs — Button input as fake sensor rig

Purpose

This example introduces input.

The 5-way button is used as a proxy for real robot perception:

Center -> start / stop
Left   -> ball left
Right  -> ball right
Up     -> ball centered
Down   -> obstacle detected

It should teach:

  • configure GPIO as input
  • read button state
  • map raw hardware state to a domain event
  • separate hardware input from robot meaning

Conceptual role in the robot journey

In the real robot, BallLeft may come from a camera pipeline.

In the workshop, it comes from a button.

The controller should not care.

button direction -> RobotEvent
camera result    -> RobotEvent
simulator input   -> RobotEvent

Same interface, different source.

Code behaviour

The firmware should:

  1. Read the 5-way button.
  2. Convert the pressed direction to RobotEvent.
  3. Show feedback through LED or logs.
  4. Keep a neutral NoEvent state when nothing is pressed.

Suggested types

#[derive(Debug, Clone, Copy, PartialEq)]
enum ButtonDirection {
    None,
    Center,
    Up,
    Down,
    Left,
    Right,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum RobotEvent {
    NoEvent,
    StartStop,
    BallLeft,
    BallRight,
    BallCentered,
    ObstacleDetected,
}

Suggested function

fn direction_to_event(direction: ButtonDirection) -> RobotEvent {
    match direction {
        ButtonDirection::None => RobotEvent::NoEvent,
        ButtonDirection::Center => RobotEvent::StartStop,
        ButtonDirection::Left => RobotEvent::BallLeft,
        ButtonDirection::Right => RobotEvent::BallRight,
        ButtonDirection::Up => RobotEvent::BallCentered,
        ButtonDirection::Down => RobotEvent::ObstacleDetected,
    }
}

Participant tasks

Participants should:

  1. Flash the baseline.
  2. Press each button direction.
  3. Observe LED/log feedback.
  4. Change the mapping, for example swap left and right.
  5. Add a new event such as BallLost if the hardware interaction allows it.

Suggested command

run-06

Rustup fallback

cargo build --target thumbv7m-none-eabi --bin 06_button_events
probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/06_button_events

Expected result

Participants understand that raw input must be interpreted before robot logic uses it.

Implementation notes for the code bot

  • This file is embedded-target code.
  • The button pin mapping must match the workshop hardware.
  • If the button hardware is analog or multiplexed, keep the reading logic isolated in one function.
  • If debounce is implemented, keep it simple and visible.
  • If debounce is not implemented, mention in a comment that mechanical inputs may bounce.

Acceptance criteria

  • Each button direction maps to a visible or logged event.
  • NoEvent is handled explicitly.
  • The mapping function is easy to modify.

07_events.rs — Event priority and safety-first interpretation

Purpose

This example moves from single input events to event selection and priority.

It should teach:

  • not all events are equally important
  • safety-relevant events should win
  • event processing can be tested without hardware

Conceptual role in the robot journey

A soccer robot may see the ball and detect an obstacle at the same time.

The robot should not blindly drive forward because the ball is centered.

BallCentered + ObstacleDetected -> Stop / AvoidObstacle

Code behaviour

The host-side program or test should combine possible observations and choose one dominant RobotEvent.

The baseline should include:

  • ball event
  • obstacle event
  • no event
  • priority logic

Suggested types

Use the existing RobotEvent and add if needed:

#[derive(Debug, Clone, Copy, PartialEq)]
enum RobotEvent {
    NoEvent,
    StartStop,
    BallLeft,
    BallRight,
    BallCentered,
    BallLost,
    ObstacleDetected,
    Timeout,
}

Suggested function

fn prioritize_event(events: &[RobotEvent]) -> RobotEvent {
    if events.contains(&RobotEvent::ObstacleDetected) {
        return RobotEvent::ObstacleDetected;
    }

    if events.contains(&RobotEvent::StartStop) {
        return RobotEvent::StartStop;
    }

    for event in events {
        if *event != RobotEvent::NoEvent {
            return *event;
        }
    }

    RobotEvent::NoEvent
}

Participant tasks

Participants should:

  1. Run the tests.
  2. Add a test where BallCentered and ObstacleDetected happen together.
  3. Ensure ObstacleDetected wins.
  4. Add Timeout as another high-priority event.
  5. Decide whether StartStop should beat ObstacleDetected or not.

Suggested command

cargo test --bin 07_events

Expected result

Participants understand that event handling is a design decision, not just syntax.

Implementation notes for the code bot

  • Keep this host-side.
  • Add several unit tests.
  • Do not require STM32 hardware.
  • Keep priorities simple and explicit.
  • Avoid overengineering with priority queues or traits.

Acceptance criteria

  • ObstacleDetected wins over ball events.
  • NoEvent does not trigger behaviour.
  • The priority rules are covered by tests.

08_state_machine.rs — Robot mode and behaviour

Purpose

This is the main robotics software example.

It combines:

  • robot events
  • robot modes
  • motion commands
  • transition logic
  • LED status output on STM32

It should teach that robot behaviour should be explicit and inspectable.

Conceptual role in the robot journey

A soccer robot needs memory.

It must know whether it is:

  • idle
  • searching for the ball
  • aligning to the ball
  • driving to the ball
  • avoiding an obstacle
  • in an error state

This is a state machine.

Code behaviour

The firmware should:

  1. Read button input.
  2. Convert it to RobotEvent.
  3. Update RobotMode through a decide or transition function.
  4. Produce a MotionCommand.
  5. Show the current mode with the RGB LED.

The baseline does not need motors. The LED is the visible actuator.

Suggested types

#[derive(Debug, Clone, Copy, PartialEq)]
enum RobotMode {
    Idle,
    SearchBall,
    AlignToBall,
    DriveToBall,
    AvoidObstacle,
    Error,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum MotionCommand {
    Stop,
    Forward,
    RotateLeft,
    RotateRight,
    StrafeLeft,
    StrafeRight,
    Avoid,
}

Suggested transition function

fn decide(
    mode: RobotMode,
    event: RobotEvent,
) -> (RobotMode, MotionCommand) {
    match (mode, event) {
        (_, RobotEvent::ObstacleDetected) => {
            (RobotMode::AvoidObstacle, MotionCommand::Stop)
        }

        (RobotMode::Idle, RobotEvent::StartStop) => {
            (RobotMode::SearchBall, MotionCommand::RotateLeft)
        }

        (RobotMode::SearchBall, RobotEvent::BallLeft) => {
            (RobotMode::AlignToBall, MotionCommand::RotateLeft)
        }

        (RobotMode::SearchBall, RobotEvent::BallRight) => {
            (RobotMode::AlignToBall, MotionCommand::RotateRight)
        }

        (RobotMode::SearchBall, RobotEvent::BallCentered) => {
            (RobotMode::DriveToBall, MotionCommand::Forward)
        }

        (RobotMode::DriveToBall, RobotEvent::BallLost) => {
            (RobotMode::SearchBall, MotionCommand::RotateLeft)
        }

        (_, RobotEvent::StartStop) => {
            (RobotMode::Idle, MotionCommand::Stop)
        }

        (mode, _) => {
            (mode, MotionCommand::Stop)
        }
    }
}

LED status mapping

Recommended mapping:

Idle           -> Off or slow blue
SearchBall     -> blue blink
AlignToBall    -> yellow
DriveToBall    -> green
AvoidObstacle  -> red blink
Error          -> fast red blink

Participant tasks

Participants should:

  1. Flash the baseline.
  2. Press Center to leave Idle.
  3. Use Left, Right, and Up to simulate ball observations.
  4. Use Down to simulate an obstacle.
  5. Add or fix one transition.
  6. Add one unit test for transition logic if the code is split into testable pure functions.

Suggested command

run-08

Rustup fallback

cargo build --target thumbv7m-none-eabi --bin 08_state_machine
probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/08_state_machine

Optional host-side tests:

cargo test --bin 08_state_machine

Expected result

Participants see that button input can drive a simplified robot behaviour loop.

Implementation notes for the code bot

  • This is embedded-target code, but the transition logic should be pure and testable.
  • Keep hardware reading and state transition separate.
  • If unit tests are awkward in the embedded binary, move shared logic into a library module later. For now, make the pure functions copyable or testable in a host-side companion if needed.
  • Do not introduce motors yet.

Acceptance criteria

  • Button input changes robot mode.
  • Robot mode changes LED status.
  • Obstacle handling has priority.
  • The transition function is visible and easy to edit.

09_sensor_pipeline.rs — Camera and ultrasonic sensor model

Purpose

This example models real robot perception without requiring actual camera or ultrasonic hardware in the workshop.

It should teach:

  • camera frames are not decisions
  • ultrasonic echo values are not decisions
  • raw data must become observations
  • observations become robot events
  • multiple observations can be fused into one behaviour-relevant event

Conceptual role in the robot journey

The real RoboCup-style robot might have:

camera frame -> ball detection -> BallObservation
ultrasonic echo -> distance -> ObstacleObservation

The controller should only consume domain observations or events, not raw pixels or echo timings.

Code behaviour

The host-side program should simulate:

  1. A fake camera input.
  2. A fake ultrasonic input.
  3. Conversion into BallObservation and ObstacleObservation.
  4. Conversion into one or more RobotEvent values.
  5. Event priority.
  6. A final call into decide or a simplified decision function.

Suggested types

#[derive(Debug, Clone, Copy, PartialEq)]
enum BallObservation {
    NotSeen,
    Left,
    Center,
    Right,
    TooClose,
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct RawEchoMicros(u32);

#[derive(Debug, Clone, Copy, PartialEq)]
struct DistanceCm(u16);

#[derive(Debug, Clone, Copy, PartialEq)]
enum ObstacleObservation {
    Clear,
    Warning,
    TooClose,
}

Suggested fake camera model

Use a simple fake input type:

#[derive(Debug, Clone, Copy, PartialEq)]
struct FakeBallPixelX(Option<u16>);

Then classify it:

fn classify_ball(pixel_x: FakeBallPixelX) -> BallObservation {
    match pixel_x.0 {
        None => BallObservation::NotSeen,
        Some(0..=200) => BallObservation::Left,
        Some(201..=440) => BallObservation::Center,
        Some(_) => BallObservation::Right,
    }
}

The numbers do not need to be realistic; they only show the principle.

Suggested ultrasonic model

Reuse echo_to_distance and classify_obstacle from chapter 03 or reimplement them locally for clarity.

Suggested fusion function

fn observations_to_event(
    ball: BallObservation,
    obstacle: ObstacleObservation,
) -> RobotEvent {
    match obstacle {
        ObstacleObservation::TooClose => RobotEvent::ObstacleDetected,
        _ => match ball {
            BallObservation::NotSeen => RobotEvent::BallLost,
            BallObservation::Left => RobotEvent::BallLeft,
            BallObservation::Center => RobotEvent::BallCentered,
            BallObservation::Right => RobotEvent::BallRight,
            BallObservation::TooClose => RobotEvent::BallCentered,
        },
    }
}

Participant tasks

Participants should:

  1. Run the tests.
  2. Change fake ball positions.
  3. Change distance thresholds.
  4. Add a warning behaviour for ObstacleObservation::Warning.
  5. Add a test where the ball is centered but the obstacle is too close.
  6. Ensure the resulting event is ObstacleDetected.

Suggested command

cargo test --bin 09_sensor_pipeline

Optional run output:

cargo run --bin 09_sensor_pipeline

Expected result

Participants understand how a real camera or ultrasonic sensor can later be swapped in without rewriting the robot behaviour layer.

Implementation notes for the code bot

  • Keep this host-side.
  • Include unit tests.
  • Use deterministic fake inputs.
  • Avoid image processing libraries.
  • Avoid hardware dependencies.
  • This chapter is about the interface between perception and behaviour.

Acceptance criteria

  • Fake camera values become BallObservation.
  • Fake ultrasonic values become ObstacleObservation.
  • Observation fusion produces RobotEvent.
  • Obstacle priority is tested.

10_motor_commands.rs — Motion intent to wheel speeds

Purpose

This example models the transition from robot decision to motor-level command.

It should teach:

  • robot behaviour should not directly produce PWM
  • symbolic commands can become motion vectors
  • motion vectors can be mixed into wheel speeds
  • real motor control needs normalization and calibration

Conceptual role in the robot journey

The state machine may decide:

RotateLeft
Forward
Stop

But a four-wheel omni-style robot needs four wheel speeds:

front_left
front_right
rear_left
rear_right

This example builds that translation.

Code behaviour

The host-side program should:

  1. Convert MotionCommand to MotionVector.
  2. Convert MotionVector to WheelSpeeds.
  3. Normalize wheel speeds to -1.0..=1.0.
  4. Test basic command mappings.

Suggested types

#[derive(Debug, Clone, Copy, PartialEq)]
enum MotionCommand {
    Stop,
    Forward,
    RotateLeft,
    RotateRight,
    StrafeLeft,
    StrafeRight,
    Avoid,
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct MotionVector {
    forward: f32,
    strafe: f32,
    rotate: f32,
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct WheelSpeeds {
    front_left: f32,
    front_right: f32,
    rear_left: f32,
    rear_right: f32,
}

Suggested command mapping

fn command_to_vector(command: MotionCommand) -> MotionVector {
    match command {
        MotionCommand::Stop => MotionVector { forward: 0.0, strafe: 0.0, rotate: 0.0 },
        MotionCommand::Forward => MotionVector { forward: 0.6, strafe: 0.0, rotate: 0.0 },
        MotionCommand::RotateLeft => MotionVector { forward: 0.0, strafe: 0.0, rotate: -0.4 },
        MotionCommand::RotateRight => MotionVector { forward: 0.0, strafe: 0.0, rotate: 0.4 },
        MotionCommand::StrafeLeft => MotionVector { forward: 0.0, strafe: -0.5, rotate: 0.0 },
        MotionCommand::StrafeRight => MotionVector { forward: 0.0, strafe: 0.5, rotate: 0.0 },
        MotionCommand::Avoid => MotionVector { forward: -0.3, strafe: 0.0, rotate: 0.3 },
    }
}

Suggested four-wheel mixer

Use a simple omni/mecanum-style mixer:

fn mix(cmd: MotionVector) -> WheelSpeeds {
    WheelSpeeds {
        front_left:  cmd.forward + cmd.strafe + cmd.rotate,
        front_right: cmd.forward - cmd.strafe - cmd.rotate,
        rear_left:   cmd.forward - cmd.strafe + cmd.rotate,
        rear_right:  cmd.forward + cmd.strafe - cmd.rotate,
    }
}

Important: document in comments that signs and wheel layout depend on the actual robot.

Suggested normalization

fn normalize(speeds: WheelSpeeds) -> WheelSpeeds {
    let max = speeds.front_left
        .abs()
        .max(speeds.front_right.abs())
        .max(speeds.rear_left.abs())
        .max(speeds.rear_right.abs());

    if max <= 1.0 {
        speeds
    } else {
        WheelSpeeds {
            front_left: speeds.front_left / max,
            front_right: speeds.front_right / max,
            rear_left: speeds.rear_left / max,
            rear_right: speeds.rear_right / max,
        }
    }
}

Participant tasks

Participants should:

  1. Run the tests.
  2. Add tests for Stop, Forward, and RotateLeft.
  3. Change the speed for Forward.
  4. Add a new command such as KickApproach or SlowAlign.
  5. Confirm that normalization keeps all wheel speeds within range.

Suggested command

cargo test --bin 10_motor_commands

Optional run output:

cargo run --bin 10_motor_commands

Expected result

Participants understand the separation between robot intent and actuator-level command.

Implementation notes for the code bot

  • Keep this host-side.
  • Do not require motor hardware.
  • Include comments about calibration and real wheel orientation.
  • Keep f32 acceptable for the workshop model.
  • Tests involving floats should use small tolerance helpers instead of direct equality where needed.

Acceptance criteria

  • MotionCommand maps to MotionVector.
  • MotionVector maps to WheelSpeeds.
  • Speeds can be normalized.
  • At least three unit tests exist.

11_kinematics.rs — Geometry, forward kinematics and inverse kinematics

Purpose

This final example gives a conceptual and code-level outlook into kinematics.

It should not become a full robotics math lecture.

It should teach:

  • geometry enters when movement depends on robot shape
  • forward kinematics asks where the robot/tool ends up
  • inverse kinematics asks which commands are needed to reach a target
  • invalid movement should fail before hardware moves

Conceptual role in the robot journey

For the soccer base:

motion intent -> wheel speeds

For a robot arm:

target position -> joint angles -> motor commands

Both are translations from desired behaviour into actuator commands.

Code behaviour

The host-side program should model a very small 2D or simplified 3-axis arm calculation.

Keep it simple enough for workshop participants to read.

Recommended approach:

  • Define target position.
  • Define joint angles.
  • Define possible kinematics errors.
  • Implement a simplified reachability check.
  • Optionally return placeholder angles for reachable targets.

The purpose is not physically perfect kinematics. The purpose is modelling safe movement.

Suggested types

#[derive(Debug, Clone, Copy, PartialEq)]
struct Millimeters(f32);

#[derive(Debug, Clone, Copy, PartialEq)]
struct Degrees(f32);

#[derive(Debug, Clone, Copy, PartialEq)]
struct ArmTarget {
    x: Millimeters,
    y: Millimeters,
    z: Millimeters,
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct JointAngles {
    base: Degrees,
    shoulder: Degrees,
    elbow: Degrees,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum KinematicsError {
    TargetOutOfReach,
    JointLimitExceeded,
    CollisionRisk,
}

Suggested inverse kinematics shape

fn inverse_kinematics(target: ArmTarget) -> Result<JointAngles, KinematicsError> {
    let horizontal_distance = (target.x.0 * target.x.0 + target.y.0 * target.y.0).sqrt();
    let distance = (horizontal_distance * horizontal_distance + target.z.0 * target.z.0).sqrt();

    const MAX_REACH_MM: f32 = 250.0;

    if distance > MAX_REACH_MM {
        return Err(KinematicsError::TargetOutOfReach);
    }

    Ok(JointAngles {
        base: Degrees(0.0),
        shoulder: Degrees(45.0),
        elbow: Degrees(45.0),
    })
}

This is intentionally simplified. It teaches the API shape and safety boundary.

Participant tasks

Participants should:

  1. Run the tests.
  2. Change the target position.
  3. Trigger TargetOutOfReach.
  4. Add a JointLimitExceeded rule.
  5. Add one test for reachable and one test for unreachable targets.

Suggested command

cargo test --bin 11_kinematics

Optional run output:

cargo run --bin 11_kinematics

Expected result

Participants understand that robot geometry should be encoded as explicit logic with explicit failure modes.

Implementation notes for the code bot

  • Keep this host-side.
  • Avoid heavy math and external crates.
  • Use simplified geometry and be honest in comments.
  • The key concept is Result<JointAngles, KinematicsError>.
  • Do not imply that placeholder angles are a complete inverse kinematics solution.

Acceptance criteria

  • Reachable targets return Ok(JointAngles).
  • Unreachable targets return Err(KinematicsError::TargetOutOfReach).
  • At least two tests exist.
  • The example closes the workshop story by connecting safe typed APIs to physical robot movement.

Cross-chapter consistency

The implementation should keep names and mental models consistent across examples.

Shared vocabulary

Use these names consistently:

RobotEvent
RobotMode
MotionCommand
MotionVector
WheelSpeeds
BallObservation
ObstacleObservation
DistanceCm
SensorError

Shared architecture

The whole workshop should repeatedly reinforce this structure:

raw hardware input
  ↓
domain observation
  ↓
robot event
  ↓
state machine
  ↓
motion command
  ↓
hardware output

Host-side vs embedded examples

Host-side examples:

00_check.rs
01_rust_basics.rs
02_ownership.rs
03_types.rs
07_events.rs
09_sensor_pipeline.rs
10_motor_commands.rs
11_kinematics.rs

Embedded examples:

04_first_firmware.rs
05_rgb_output.rs
06_button_events.rs
08_state_machine.rs
12_embassy.rs

This split is intentional. If hardware setup fails for a participant, they can still learn and run most of the robot logic locally.

Suggested workshop flow

Participants should experience the examples in this order:

  1. Run host sanity check.
  2. Learn explicit values and mutation.
  3. Learn ownership and borrowing through commands.
  4. Learn domain types and sensor errors.
  5. Flash first firmware.
  6. Control LED as robot status output.
  7. Read button as fake robot sensor.
  8. Add event priority.
  9. Build the robot state machine.
  10. Model camera and ultrasonic processing.
  11. Convert decisions to wheel speeds.
  12. Close with kinematics and safe movement APIs.
  13. Show Embassy async timers, tasks, and printing.

Final learning outcome

At the end of the examples, participants should be able to explain this sentence:

Rust helps us build robot firmware as explicit transformations from physical signals into typed observations, events, states, commands and actuator outputs.

12_embassy.rs — Async Embassy loop, timers, and printing

Purpose

This example shows why a small async framework is useful on embedded Rust.

It should teach:

  • one manual loop can become multiple async tasks
  • timers replace ad-hoc delay code
  • printing can happen while other work keeps running
  • setup stays explicit, but the scheduling gets simpler

Conceptual role in the robot journey

The workshop has mostly shown hand-written loops.

This chapter shows the next step:

init hardware -> spawn tasks -> await timers -> print progress

Code behaviour

The firmware should:

  1. Initialize Embassy for the Blue Pill.
  2. Spawn one periodic async task.
  3. Print a heartbeat from that task.
  4. Print a second message from the main task.
  5. Use Timer or Ticker instead of a manual busy loop delay.

Suggested types and tools

use embassy_executor::Spawner;
use embassy_time::{Duration, Ticker, Timer};
use cortex_m_semihosting::hprintln;

Participant tasks

Participants should:

  1. Run the baseline.
  2. Change the print text.
  3. Change the timer period.
  4. Add a second async task.
  5. Compare this flow with the hand-written loop from chapter 04.

Suggested command

run-12

Rustup fallback

cargo build --target thumbv7m-none-eabi --bin 12_embassy
probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/12_embassy

Expected result

Participants see that Embassy can handle waiting and concurrent work without manual loop plumbing.

Implementation notes for the code bot

  • Keep the example short and readable.
  • Use Timer or Ticker for waiting.
  • Use a small print message for visible progress.
  • Avoid adding hardware drivers unless they help the lesson.
  • Keep the example focused on the async model, not on full application logic.

Acceptance criteria

  • The firmware builds for the Blue Pill.
  • At least one async task runs.
  • Timer-driven waiting is visible in the code.
  • The output shows that the firmware stays alive while work continues.