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:
- Keep the example focused on exactly one concept.
- Include a working baseline.
- Include one or more clearly marked
TODOsections for participants. - Print or show enough feedback that participants know whether it works.
- Keep robotics terminology consistent across chapters.
- Reuse domain names where possible:
RobotEvent,RobotMode,MotionCommand,BallObservation,ObstacleObservation. - Prefer explicit and readable code over clever abstractions.
- Avoid advanced lifetime-heavy Rust unless it is absolutely necessary.
- Keep all host-side examples testable without STM32 hardware.
- 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:
- Run the example.
- Confirm that the output appears.
- Change one printed line.
- 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_checkworks.- 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_speedwith a function. - Print both values.
- Show one intentionally mutable variable, such as
status_messageortarget_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:
- Run the example.
- Change the search speed.
- Add a second function, for example
select_status_name. - Change one immutable variable and observe the compiler error.
- 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:
- Create a command.
- Print it by borrowing it.
- Modify it through a mutable borrow.
- 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:
- Run the baseline.
- Uncomment the move example and read the compiler error.
- Revert it.
- Add a function
rotate_left(command: &mut MotionCommand). - Call it before
stop. - 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
MotionCommandCopy; the move error is useful. - If the example gets too frustrating, a later comment may explain when
Copywould 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
Resultfor sensor failurematchfor 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:
- Convert a raw echo value into
DistanceCm. - Classify distance as clear, warning or too close.
- Represent ball position with
BallObservation. - 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:
- Run the example.
- Change the fake echo value.
- Observe how the classification changes.
- Add a new
BallObservationvariant, for exampleFarAway. - Add or update one
matcharm. - 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, notunwrap, so error handling is visible.
Acceptance criteria
- Baseline compiles and prints at least one valid distance classification.
- Baseline demonstrates one
Errpath. - 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_stdmeans 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:
- Start on the STM32.
- Initialize the minimal required runtime/HAL setup.
- 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:
- Build the firmware.
- Flash it to the STM32.
- Confirm that flashing succeeds.
- 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:
- Flash the baseline.
- Confirm the LED behaviour.
- Change the blink timing.
- Add a second or third status pattern.
- 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:
- Read the 5-way button.
- Convert the pressed direction to
RobotEvent. - Show feedback through LED or logs.
- Keep a neutral
NoEventstate 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:
- Flash the baseline.
- Press each button direction.
- Observe LED/log feedback.
- Change the mapping, for example swap left and right.
- Add a new event such as
BallLostif 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.
NoEventis 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:
- Run the tests.
- Add a test where
BallCenteredandObstacleDetectedhappen together. - Ensure
ObstacleDetectedwins. - Add
Timeoutas another high-priority event. - Decide whether
StartStopshould beatObstacleDetectedor 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
ObstacleDetectedwins over ball events.NoEventdoes 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:
- Read button input.
- Convert it to
RobotEvent. - Update
RobotModethrough adecideortransitionfunction. - Produce a
MotionCommand. - 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:
- Flash the baseline.
- Press
Centerto leaveIdle. - Use
Left,Right, andUpto simulate ball observations. - Use
Downto simulate an obstacle. - Add or fix one transition.
- 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:
- A fake camera input.
- A fake ultrasonic input.
- Conversion into
BallObservationandObstacleObservation. - Conversion into one or more
RobotEventvalues. - Event priority.
- A final call into
decideor 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:
- Run the tests.
- Change fake ball positions.
- Change distance thresholds.
- Add a warning behaviour for
ObstacleObservation::Warning. - Add a test where the ball is centered but the obstacle is too close.
- 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:
- Convert
MotionCommandtoMotionVector. - Convert
MotionVectortoWheelSpeeds. - Normalize wheel speeds to
-1.0..=1.0. - 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:
- Run the tests.
- Add tests for
Stop,Forward, andRotateLeft. - Change the speed for
Forward. - Add a new command such as
KickApproachorSlowAlign. - 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
f32acceptable for the workshop model. - Tests involving floats should use small tolerance helpers instead of direct equality where needed.
Acceptance criteria
MotionCommandmaps toMotionVector.MotionVectormaps toWheelSpeeds.- 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:
- Run the tests.
- Change the target position.
- Trigger
TargetOutOfReach. - Add a
JointLimitExceededrule. - 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:
- Run host sanity check.
- Learn explicit values and mutation.
- Learn ownership and borrowing through commands.
- Learn domain types and sensor errors.
- Flash first firmware.
- Control LED as robot status output.
- Read button as fake robot sensor.
- Add event priority.
- Build the robot state machine.
- Model camera and ultrasonic processing.
- Convert decisions to wheel speeds.
- Close with kinematics and safe movement APIs.
- 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:
- Initialize Embassy for the Blue Pill.
- Spawn one periodic async task.
- Print a heartbeat from that task.
- Print a second message from the main task.
- Use
TimerorTickerinstead 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:
- Run the baseline.
- Change the print text.
- Change the timer period.
- Add a second async task.
- 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
TimerorTickerfor 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.