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

1749 lines
39 KiB
Markdown

# 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:
```text
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:
```text
src/bin/
```
The intended file list is:
```text
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.
```text
edit code -> build -> run -> observe result
```
For embedded work this later becomes:
```text
edit code -> build -> flash -> observe hardware
```
## Code behaviour
The host-side program should print a short confirmation message:
```text
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
```text
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.
```text
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:
```text
current speed: 0
selected target speed: 40
status: searching
```
## Suggested code shape
The code should include:
```rust
fn select_target_speed(searching: bool) -> i32 {
if searching {
40
} else {
0
}
}
```
Then in `main`:
```rust
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
```text
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:
```text
Who is responsible for this value or resource?
```
Borrowing answers:
```text
Who may temporarily inspect or modify it?
```
## Code behaviour
The program should define a simple robot command type:
```rust
#[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:
```rust
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:
```rust
// 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
```text
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:
```text
42
```
Better:
```text
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
```rust
#[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
```rust
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
```text
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:
```text
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
```text
build-04
```
## Suggested run
```text
run-04
```
## Rustup fallback
```text
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:
```text
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:
```text
Off -> Idle
Blue blink -> SearchBall
Green -> DriveToBall
Yellow -> AlignToBall
Red blink -> AvoidObstacle or Error
```
The baseline should at least implement two states:
```text
Idle
SearchBall
```
## Suggested types
```rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum RobotMode {
Idle,
SearchBall,
}
```
A helper function should map mode to LED output:
```rust
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
```text
run-05
```
## Rustup fallback
```text
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:
```text
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.
```text
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
```rust
#[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
```rust
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
```text
run-06
```
## Rustup fallback
```text
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.
```text
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:
```rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum RobotEvent {
NoEvent,
StartStop,
BallLeft,
BallRight,
BallCentered,
BallLost,
ObstacleDetected,
Timeout,
}
```
## Suggested function
```rust
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
```text
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
```rust
#[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
```rust
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:
```text
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
```text
run-08
```
## Rustup fallback
```text
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:
```text
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:
```text
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
```rust
#[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:
```rust
#[derive(Debug, Clone, Copy, PartialEq)]
struct FakeBallPixelX(Option<u16>);
```
Then classify it:
```rust
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
```rust
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
```text
cargo test --bin 09_sensor_pipeline
```
Optional run output:
```text
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:
```text
RotateLeft
Forward
Stop
```
But a four-wheel omni-style robot needs four wheel speeds:
```text
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
```rust
#[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
```rust
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:
```rust
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
```rust
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
```text
cargo test --bin 10_motor_commands
```
Optional run output:
```text
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:
```text
motion intent -> wheel speeds
```
For a robot arm:
```text
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
```rust
#[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
```rust
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
```text
cargo test --bin 11_kinematics
```
Optional run output:
```text
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:
```text
RobotEvent
RobotMode
MotionCommand
MotionVector
WheelSpeeds
BallObservation
ObstacleObservation
DistanceCm
SensorError
```
## Shared architecture
The whole workshop should repeatedly reinforce this structure:
```text
raw hardware input
domain observation
robot event
state machine
motion command
hardware output
```
## Host-side vs embedded examples
Host-side examples:
```text
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:
```text
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:
```text
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:
```text
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
```rust
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
```text
run-12
```
## Rustup fallback
```text
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.