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

2319 lines
56 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>Rust on Robots: Hands-on Embedded Rust on STM32</title>
<!-- Minified reveal.js assets, preloaded before the deck starts. -->
<link as="style" href="https://cdn.jsdelivr.net/npm/reveal.js@5/dist/reveal.min.css" rel="preload"/>
<link as="style" href="https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/highlight/monokai.css" rel="preload"/>
<link as="style" href="https://cdn.jsdelivr.net/npm/reveal.js@5/dist/print/pdf.css" rel="preload"/>
<link as="script" href="https://cdn.jsdelivr.net/npm/reveal.js@5/dist/reveal.min.js" rel="preload"/>
<link as="script" href="https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/markdown/markdown.min.js" rel="preload"/>
<link as="script" href="https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/highlight/highlight.min.js" rel="preload"/>
<link as="script" href="https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/notes/notes.min.js" rel="preload"/>
<link href="https://cdn.jsdelivr.net/npm/reveal.js@5/dist/reveal.min.css" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/highlight/monokai.css" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/reveal.js@5/dist/print/pdf.css" media="print" rel="stylesheet" id="reveal-print-pdf-css"/>
<style>
:root {
--corpuls-red: #df0015;
--corpuls-red-dark: #a40012;
--ink: #101214;
--muted: #4f555b;
--paper: #eef0f2;
--surface: rgba(255,255,255,.86);
--surface-solid: #ffffff;
--surface-soft: #f8f9fa;
--line: #d4d8dd;
--code-bg: #17191d;
--code-fg: #f5f6f7;
--safe-top: 96px;
--safe-bottom: 84px;
--safe-x: 118px;
--safe-right: 118px;
--content-width: 1340px;
--content-wide: 1420px;
}
html, body {
width: 100%;
height: 100%;
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at 12% 82%, rgba(223,0,21,0.115), transparent 30%),
radial-gradient(circle at 86% 14%, rgba(255,255,255,0.92), transparent 34%),
linear-gradient(135deg, #fbfbfc 0%, #eef0f2 43%, #e4e7ea 100%);
z-index: 0;
}
body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image: linear-gradient(90deg, rgba(16,18,20,.035) 1px, transparent 1px), linear-gradient(180deg, rgba(16,18,20,.03) 1px, transparent 1px);
background-size: 64px 64px;
opacity: .28;
z-index: 0;
}
.corpuls-logo {
position: fixed;
right: 0;
top: 0;
width: 700px;
height: auto;
z-index: 20;
pointer-events: none;
}
.deck-footer {
position: fixed;
left: 62px;
bottom: 36px;
z-index: 20;
font-size: 19px;
letter-spacing: .13em;
text-transform: uppercase;
color: var(--muted);
pointer-events: none;
}
.deck-footer::before {
content: "";
display: inline-block;
width: 62px;
height: 6px;
margin-right: 17px;
background: var(--corpuls-red);
vertical-align: middle;
border-radius: 999px;
box-shadow: 0 9px 18px rgba(223,0,21,.25);
}
.reveal {
color: var(--ink);
font-size: 56px;
}
.reveal .slides {
text-align: center;
}
.reveal .slides section {
box-sizing: border-box;
height: 100%;
min-height: 100%;
padding: var(--safe-top) var(--safe-x) var(--safe-bottom) var(--safe-x);
overflow: hidden;
}
.reveal .slides section.present {
display: flex !important;
flex-direction: column;
justify-content: center;
align-items: center;
}
.reveal .slides section > * {
max-width: var(--content-width);
width: 100%;
margin-left: auto;
margin-right: auto;
}
.reveal .slides section.code-slide > *,
.reveal .slides section.architecture > *,
.reveal .slides section.ultra-dense > *,
.reveal .slides section.dense > * {
max-width: var(--content-wide);
}
.reveal .slides section::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: min(1500px, calc(100% - 236px));
height: min(835px, calc(100% - 190px));
transform: translate(-50%, -50%);
border-radius: 28px;
border: 1px solid rgba(255,255,255,.68);
background: linear-gradient(140deg, rgba(255,255,255,.36), rgba(255,255,255,.10));
box-shadow: 0 24px 70px rgba(25,31,38,.08);
z-index: -1;
}
.reveal .slides section.cover::after,
.reveal .slides section.section-title::after {
background: linear-gradient(130deg, rgba(255,255,255,.70), rgba(255,255,255,.12));
border-color: rgba(255,255,255,.80);
}
.reveal h1,
.reveal h2,
.reveal h3 {
color: var(--ink);
font-weight: 820;
letter-spacing: -0.027em;
line-height: 1.05;
text-transform: none;
text-align: center;
text-wrap: balance;
}
.reveal h1 {
font-size: 1.22em;
margin: 0 0 .44em 0;
padding-right: 0;
}
.reveal h1::after {
content: "";
display: block;
width: 108px;
height: 7px;
margin: 20px auto 0 auto;
border-radius: 999px;
background: var(--corpuls-red);
box-shadow: 0 10px 22px rgba(223,0,21,.24);
}
.reveal h2 {
font-size: 1.06em;
color: var(--muted);
font-weight: 720;
margin: -.15em 0 .74em 0;
padding-right: 0;
}
.reveal h3 {
font-size: 1.04em;
margin: .55em 0 .35em 0;
color: var(--corpuls-red-dark);
}
.reveal p,
.reveal li {
font-size: 1.02em;
line-height: 1.20;
color: #202124;
}
.reveal p {
margin: .38em auto;
text-align: center;
text-wrap: pretty;
}
.reveal strong {
color: var(--corpuls-red-dark);
font-weight: 820;
}
.reveal em {
color: var(--corpuls-red-dark);
font-style: normal;
font-weight: 760;
}
.reveal ul,
.reveal ol {
margin: .34em auto .46em auto;
padding-left: 1.18em;
text-align: left;
max-width: 1120px;
width: fit-content;
}
.reveal li {
margin: .18em 0;
padding-left: .04em;
}
.reveal li::marker {
color: var(--corpuls-red);
font-weight: 820;
}
.reveal code:not(pre code) {
color: var(--corpuls-red-dark);
background: rgba(223,0,21,.075);
padding: .05em .22em;
border-radius: .28em;
font-size: .91em;
white-space: nowrap;
}
.reveal pre {
width: min(1160px, 88%);
margin: 20px auto 8px auto;
border-radius: 22px;
overflow: hidden;
box-shadow: 0 20px 48px rgba(20,25,30,.20);
border: 1px solid rgba(255,255,255,.55);
background: var(--code-bg);
text-align: left;
}
.reveal pre code {
max-height: 665px;
padding: 24px 30px;
font-size: .60em;
line-height: 1.18;
background: var(--code-bg);
color: var(--code-fg);
}
.reveal .code-slide pre code,
.reveal .architecture pre code.language-text,
.reveal .stacked-steps pre code.language-text {
font-size: .58em;
line-height: 1.17;
max-height: 650px;
}
.reveal .dense p,
.reveal .dense li {
font-size: .92em;
line-height: 1.18;
}
.reveal .dense pre code {
font-size: .52em;
line-height: 1.14;
max-height: 630px;
}
.reveal .ultra-dense p,
.reveal .ultra-dense li {
font-size: .80em;
line-height: 1.14;
}
.reveal .ultra-dense pre code {
font-size: .47em;
line-height: 1.10;
max-height: 620px;
}
.reveal blockquote {
border-left: 9px solid var(--corpuls-red);
background: var(--surface);
box-shadow: 0 14px 34px rgba(20,25,30,.10);
padding: 24px 34px;
margin: 0 auto;
text-align: left;
border-radius: 18px;
}
.reveal .cover h1 {
font-size: 1.56em;
margin-bottom: .30em;
padding-right: 0;
}
.reveal .cover h2 {
font-size: 1.12em;
max-width: 980px;
color: var(--muted);
}
.reveal .cover p {
margin-top: 1.4em;
font-size: .92em;
color: var(--muted);
}
.reveal .cover::before,
.reveal .section-title::before {
content: "";
position: absolute;
left: calc(50% - 760px);
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 360px;
border-radius: 999px;
background: var(--corpuls-red);
box-shadow: 0 22px 48px rgba(223,0,21,.30);
}
.reveal .cover > *,
.reveal .section-title > * {
padding-left: 0;
}
.reveal .section-title h1 {
font-size: 1.42em;
color: var(--ink);
}
.reveal .exercise h1::before {
content: "Workshop step";
display: block;
width: max-content;
max-width: 100%;
margin: 0 auto 16px auto;
padding: 9px 17px;
border: 1px solid rgba(223,0,21,.28);
border-left: 7px solid var(--corpuls-red);
border-radius: 999px;
color: var(--corpuls-red-dark);
background: rgba(255,255,255,.82);
font-size: 18px;
letter-spacing: .09em;
text-transform: uppercase;
font-weight: 820;
}
.reveal .exercise h1::after { margin-top: 18px; }
.reveal .exercise ol,
.reveal .exercise ul,
.reveal .two-column ul,
.reveal .two-column ol {
columns: 2;
column-gap: 88px;
max-width: 1200px;
width: auto;
}
.reveal .exercise li,
.reveal .two-column li {
break-inside: avoid;
margin-bottom: .18em;
}
.reveal .closing h1 {
color: var(--corpuls-red-dark);
}
.reveal table {
border-collapse: collapse;
font-size: .86em;
width: 100%;
background: var(--surface-solid);
border-radius: 18px;
overflow: hidden;
box-shadow: 0 12px 30px rgba(20,25,30,.10);
}
.reveal th,
.reveal td {
padding: 14px 18px;
border-bottom: 1px solid var(--line);
}
.reveal th {
color: var(--corpuls-red-dark);
}
.reveal .progress {
color: var(--corpuls-red);
height: 6px;
}
.reveal .controls {
color: var(--corpuls-red);
right: 44px;
bottom: 62px;
}
.reveal .slide-number {
color: var(--muted);
background: transparent;
right: 54px;
bottom: 32px;
font-size: 19px;
}
.reveal .hljs {
background: var(--code-bg);
}
.reveal .flow-chain {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
flex-wrap: wrap;
max-width: 1220px;
margin: 24px auto 16px auto;
}
.reveal .flow-chain span {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 66px;
padding: 12px 22px;
border-radius: 20px;
background: rgba(255,255,255,.86);
border: 1px solid rgba(212,216,221,.90);
box-shadow: 0 14px 30px rgba(20,25,30,.08);
font-weight: 760;
font-size: .75em;
color: var(--ink);
}
.reveal .flow-chain span::after {
content: "→";
margin-left: 18px;
color: var(--corpuls-red);
font-weight: 900;
}
.reveal .flow-chain span:last-child::after { content: ""; margin-left: 0; }
.reveal .chapter-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 22px;
max-width: 1260px;
margin: 18px auto 0 auto;
text-align: left;
font-size: .60em;
}
.reveal .chapter-grid div {
display: grid;
grid-template-columns: 210px 1fr;
gap: 18px;
align-items: baseline;
padding: 13px 16px;
border-radius: 15px;
background: rgba(255,255,255,.82);
border: 1px solid rgba(212,216,221,.9);
box-shadow: 0 10px 22px rgba(20,25,30,.06);
}
.reveal .chapter-grid code {
color: var(--corpuls-red-dark);
background: rgba(223,0,21,.08);
padding: 3px 7px;
border-radius: 8px;
white-space: nowrap;
font-size: .95em;
}
.reveal .fragment:not(.visible) {
opacity: .18;
visibility: visible;
}
.reveal .fragment.visible {
opacity: 1;
}
.reveal .slides section.present > * {
transform: translateX(0) !important;
}
@media (max-width: 900px) {
:root { --safe-top: 72px; --safe-bottom: 72px; --safe-x: 30px; --safe-right: 30px; }
.corpuls-logo { width: 312px; right: 0px; top: 0px; }
.deck-footer { left: 20px; bottom: 16px; font-size: 10px; }
.reveal { font-size: 34px; }
.reveal .slides section::after { width: calc(100% - 44px); height: calc(100% - 120px); }
.reveal .cover::before, .reveal .section-title::before { left: 20px; height: 180px; }
.reveal .exercise ol, .reveal .exercise ul, .reveal .two-column ul, .reveal .two-column ol { columns: 1; }
}
.reveal section.two-cards {
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.reveal .split-cards {
width: min(1500px, 88vw);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3.2rem;
align-items: stretch;
justify-content: center;
margin: 2.2rem auto 0;
}
.reveal .slide-card {
background: rgba(255, 255, 255, 0.82);
border: 2px solid rgba(180, 180, 180, 0.45);
border-top: 0.42rem solid var(--accent-red);
border-radius: 1.2rem;
padding: 1.6rem 2rem;
box-shadow: 0 1rem 2.4rem rgba(0, 0, 0, 0.08);
text-align: left;
}
.reveal .slide-card h3 {
margin: 0 0 1rem;
font-size: 1.18em;
font-weight: 800;
color: var(--accent-red);
text-align: center;
}
.reveal .slide-card ul {
margin: 0;
padding-left: 1.25em;
columns: unset;
column-count: unset;
}
.reveal .slide-card li {
margin: 0.42em 0;
break-inside: avoid;
page-break-inside: avoid;
-webkit-column-break-inside: avoid;
}
.reveal section.image-slide {
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.reveal .slide-image {
display: block;
width: auto;
height: auto;
max-width: min(1200px, 70vw);
max-height: 720px;
object-fit: contain;
margin: 1.6rem auto 0;
border-radius: 1.1rem;
box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.14);
border: 1px solid rgba(0, 0, 0, 0.08);
}
body.print-pdf-mode {
width: 1920px !important;
height: 1080px !important;
background: var(--paper) !important;
}
body.print-pdf-mode .reveal .slides section.image-slide img.slide-image,
body.print-pdf-mode .reveal .slides section.image-slide img.slide-image.r-stretch {
display: block !important;
position: static !important;
visibility: visible !important;
opacity: 1 !important;
width: auto !important;
height: auto !important;
max-width: 1180px !important;
max-height: 660px !important;
object-fit: contain !important;
margin: 28px auto 0 auto !important;
transform: none !important;
}
body.print-pdf-mode .corpuls-logo,
body.print-pdf-mode .deck-footer {
position: fixed !important;
z-index: 9999 !important;
}
@media print {
@page {
size: 16in 9in;
margin: 0;
}
html,
body {
margin: 0 !important;
padding: 0 !important;
background: var(--paper) !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
body::before,
body::after,
.reveal .slides section::after,
.corpuls-logo,
.deck-footer {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.reveal .slide-image,
.reveal .slide-image.r-stretch {
display: block !important;
position: static !important;
width: auto !important;
height: auto !important;
max-width: 1180px !important;
max-height: 660px !important;
object-fit: contain !important;
margin: 28px auto 0 auto !important;
visibility: visible !important;
opacity: 1 !important;
}
.reveal .fragment:not(.visible) {
opacity: 1 !important;
visibility: visible !important;
}
.reveal .progress,
.reveal .controls {
display: none !important;
}
}
</style>
</head>
<body>
<img alt="corpuls logo" class="corpuls-logo" src="docs/corpuls_logo_long.png" />
<div class="deck-footer">Rust on Robots - Workshop - by WieErWill </div>
<div class="reveal">
<div class="slides">
<section class="cover" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Rust on Robots
## Hands-on Embedded Rust on STM32
by WieErWill
</textarea>
</section>
<section class="code-slide content dense" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Today is about robot firmware architecture
We use Rust to understand how robot software is structured:
<div class="flow-chain">
<span>hardware signal</span>
<span>software type</span>
<span>robot event</span>
<span>robot state</span>
<span>decision</span>
<span>actuator command</span>
</div>
</textarea>
</section>
<section class="content image-slide" data-markdown>
<textarea data-template>
# Corpuls
<img
class="slide-image r-stretch"
src="docs/Stage-madeingermany3.jpg"
alt="Corpuls Devices"
/>
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# We will not build a full robot in two hours
We do not have:
- full chassis
- motors
- motor drivers
- camera module
- ultrasonic hardware
- playing field
</textarea>
</section>
<section class="content two-column" data-auto-animate="" data-markdown="">
<textarea data-template="">
# We will build a robot framework
We do have:
- STM32 Blue Pill
- ST-Link programmer
- RGB LED
- 5-way button
→ The STM32 board is our small, reduced robot
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# The workshop target
By the end, we want this model:
<div class="flow-chain">
<span>button / sensor input</span>
<span>RobotEvent</span>
<span>RobotMode</span>
<span>MotionCommand</span>
<span>LED status or wheel\-speed model</span>
</div>
Different hardware. Same architecture. Easy Adaptable.
</textarea>
</section>
<section class="content" data-markdown="">
<textarea data-template="">
# Room calibration
1. Who has written Rust before?
2. Who has programmed a microcontroller before?
3. Who has built, programmed or broken a robot before?
</textarea>
</section>
<section class="content" data-markdown="">
<textarea data-template="">
# Get the Repo
Code along and try out yourself
`git.wieerwill.dev/wieerwill/rust-stm32`
1. Clone it `git clone ...`
2. Install RustUp or Nix shell
3. Try it out (more in Chapter 0 later)
</textarea>
</section>
<section class="content image-slide" data-markdown>
<textarea data-template>
# My RoboCup Junior Soccer journey
<img
class="slide-image r-stretch"
src="docs/JuniorCup.jpg"
alt="Junior RoboCup"
/>
</textarea>
</section>
<section class="code-slide content dense" data-auto-animate="" data-markdown="">
<textarea data-template="">
# A soccer robot starts simple
1. turn on
2. show status
3. wait for start
4. find the ball
5. drive toward the ball
6. avoid problems
7. recover and continue
</textarea>
</section>
<section class="code-slide content" data-auto-animate="" data-markdown="">
<textarea data-template="">
# A (soccer) robot is a loop with consequences
<div class="flow-chain">
<span>observe</span>
<span>interpret</span>
<span>decide</span>
<span>move</span>
<span>observe again</span>
</div>
The hard part is keeping this loop understandable when reality becomes messy
</textarea>
</section>
<section class="content balanced two-cards" data-markdown>
<textarea data-template>
# Robot software touches the real world
<div class="split-cards">
<div class="slide-card fragment">
<h3>Messy inputs</h3>
<ul>
<li>camera frame</li>
<li>ball position</li>
<li>line / field detection</li>
<li>ultrasonic distance</li>
<li>start button</li>
<li>battery state</li>
<li>motor feedback</li>
</ul>
</div>
<div class="slide-card fragment">
<h3>Physical outputs</h3>
<ul>
<li>wheel speeds</li>
<li>LED status</li>
<li>logs</li>
<li>stop behaviour</li>
<li>recovery behaviour</li>
</ul>
</div>
</div>
</textarea>
</section>
<section class="code-slide content" data-auto-animate="" data-markdown="">
<textarea data-template="">
# The board is our reduced robot
<div class="flow-chain">
<div><span>RGB LED -> actuator / status output</span></div>
<div><span>5-way button -> input device / fake sensor rig</span></div>
<div><span>STM32 loop -> robot control loop</span></div>
<div><span>Rust types -> robot domain model</span></div>
</div>
Small hardware. Real architecture.
</textarea>
</section>
<section class="architecture code-slide content dense" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter map
<div style="font-size:smaller;">
1. <code>00_check</code> toolchain check
2. <code>01_rust_basics</code>values, functions, mutation
3. <code>02_ownership</code>ownership and borrowing
4. <code>03_types</code>domain types, enums
5. <code>04_first_firmware</code>minimal STM32 firmware
6. <code>05_rgb_output</code>LED as actuator
7. <code>06_button_events</code>button as sensor
8. <code>07_events</code>raw input to robot event
9. <code>08_state_machine</code>robot behaviour
10. <code>09_sensor_pipeline</code>camera and ultrasonic model
11. <code>10_motor_commands</code>motion intent to wheel speeds
12. <code>11_kinematics</code>geometry and inverse kinematics
13. <code>12_embassy</code>async Embassy loop and timers
</div>
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 0: Toolchain
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 0: Toolchain check
Open: `src/bin/00_check.rs`
Run: `run-00`
Expected result: `prints the toolchain check`
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# Why Rust for this?
- Who owns this peripheral?
- Who may mutate robot state?
- What happens when a sensor fails?
- Which commands are valid?
- Which states should be impossible?
- Where does hardware end and behaviour begin?
Rust makes many of those boundaries easily visible
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# Rust is strict in useful places
- memory safety without a garbage collector
- explicit ownership
- controlled mutation
- strong enums and pattern matching
- `Option` and `Result`
- small `no_std` programs for microcontrollers
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# But Rust does not fix everything
- wrong wiring
- noisy sensors
- weak batteries
- bad control logic
- missing calibration
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 01: Values
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 01: Values are explicit
```rust
let speed = 42;
let mut target_speed = 0;
target_speed = speed;
```
Default: values are immutable.
Mutation must be requested with `mut`.
For robot code, that is a good default
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Robot translation: Mutation should have a home
Bad: `everything can change everything`
Better:
<div class="flow-chain">
<span>sensor reading</span>
<span>event</span>
<span>state transition</span>
<span>command</span>
</div>
The robot state changes in one obvious place.
</textarea>
</section>
<section class="code-slide exercise" data-markdown="">
<textarea data-template="">
# Chapter 01 exercise
Change behaviour without changing structure
Open: `src/bin/01_rust_basics.rs`
Tasks:
1. Change a constant.
2. Change a function return value.
3. Add a new status value.
4. Run the program.
Checkpoint: `run-01`
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 02: Ownership
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 02: Ownership means responsibility
```rust
let command = String::from("forward");
let next_command = command;
// command is no longer usable here
```
One value has one owner.
When ownership moves, responsibility moves.
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Robot translation: Peripherals are resources
A GPIO pin is not just an integer.
It represents access to real hardware.
<div class="flow-chain">
<span>LED pin owner</span>
<span>configure output</span>
<span>set high / low</span>
</div>
Ownership makes accidental sharing harder.
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Borrowing means temporary access
```rust
fn print_command(command: &amp;str) {
println!("command = {command}");
}
let command = String::from("forward");
print_command(&amp;command);
// command is still usable here
```
A borrow can inspect without taking ownership
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Mutable borrowing: One writer at a time
```rust
fn stop(command: &amp;mut MotionCommand) {
command.forward = 0.0;
command.strafe = 0.0;
command.rotate = 0.0;
}
```
Rust does not like hidden shared mutation.
For robot code, that is useful.
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Robot translation: Who may change the robot state?
A robot state should not be mutated from everywhere.
<div class="flow-chain">
<span>sensor module -> creates observations</span>
<span>event module -> creates RobotEvent</span>
<span>state machine -> changes RobotMode</span>
<span>command module -> creates MotionCommand</span>
<span>hardware module -> applies output</span>
</div>
Each layer has a job.
</textarea>
</section>
<section class="code-slide exercise" data-markdown="">
<textarea data-template="">
# Chapter 02 exercise: Fix the ownership error
Open: `src/bin/02_ownership.rs`
Tasks:
1. Run it.
2. Read the compiler error.
3. Fix it with a borrow.
4. Add a mutable command update.
Checkpoint: `run-02`
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 03: Types
</textarea>
</section>
<section class="code-slide content two-column" data-markdown="">
<textarea data-template="">
# Chapter 03: Numbers are not enough
```rust
let value = 42;
```
What is it?
- centimetres?
- percent?
- PWM duty cycle?
- motor speed?
- battery voltage?
- camera pixel coordinate?
Robot code needs meaning, not just numbers
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Types are safety rails
A number alone does not tell us what it means.
Types carry meaning.
```rust
struct DistanceCm(u16);
struct MotorPower(f32);
struct BallXPixel(u16);
struct BatteryMillivolts(u16);
```
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Enums model robot reality better than booleans
```rust
enum BallObservation {
NotSeen,
Left,
Center,
Right,
TooClose,
}
```
This is clearer than:
```rust
ball_seen: bool
ball_left: bool
ball_right: bool
```
Because impossible combinations disappear.
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Failure is part of the API
A sensor may fail.
The type should admit that.
```rust
enum SensorError {
Timeout,
OutOfRange,
NotCalibrated,
}
fn read_distance() -> Result<DistanceCm, SensorError> {
todo!()
}
```
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Robot translation: Sensor data is evidence
A sensor reading is not behaviour.
<div class="flow-chain">
<span>raw echo time</span>
<span>distance</span>
<span>classification</span>
<span>RobotEvent</span>
<span>state transition</span>
</div>
The robot should not directly react to raw numbers!
</textarea>
</section>
<section class="code-slide exercise" data-markdown="">
<textarea data-template="">
# Chapter 03 exercise: Replace loose values with domain types
Open: `src/bin/03_types.rs`
Tasks:
1. Replace a raw distance number with `DistanceCm`.
2. Replace booleans with `BallObservation`.
3. Return `Result` from a fake sensor function.
4. Match on the result.
Checkpoint: `run-03`
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 04: Embedded Rust
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# Chapter 04: First embedded Rust program
On the STM32 we usually do not have:
- an operating system
- heap by default
- files
- a normal terminal
- `std`
We write `no_std` firmware!
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Bare metal means we own the loop
<div class="flow-chain">
<span>configure hardware</span>
<span>enter loop</span>
<span>read input</span>
<span>update state</span>
<span>write output</span>
<span>repeat</span>
</div>
That loop is the beginning of a robot controller.
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# Firmware is not an app with a main window
On a microcontroller:
- startup code prepares the chip
- firmware configures peripherals
- the loop runs forever
- timing is explicit
- debugging is limited
- output may be one LED
</textarea>
</section>
<section class="code-slide exercise" data-markdown="">
<textarea data-template="">
# First Embedded Firmware
Open: `src/bin/04_first_firmware.rs`
Build: `build-04`
Flash: `run-04`
Expected result: The board boots and reaches the firmware loop.
</textarea>
</section>
<section class="content" data-markdown="">
<textarea data-template="">
# Hardware silence is a valid embedded state
If nothing happens, debug in layers:
1. Is the board powered?
2. Is the ST-Link visible?
3. Is the chip target correct?
4. Did the firmware flash?
5. Is the pin correct?
6. Is the LED orientation correct?
Do not randomly change five things at once!
</textarea>
</section>
<section class="content" data-markdown="">
<textarea data-template="">
# Embedded debugging is layered
1. host build works?
2. target build works?
3. probe visible?
4. flash succeeds?
5. firmware boots?
6. pin toggles?
7. external wiring works?
Debug from the outside in.
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 05: Output
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 05: Output - LED as actuator
An LED is a tiny actuator.
It gives us visible robot state.
<div class="flow-chain">
<div><span>Off</span> <span>idle</span></div>
<div><span>Blue blink</span> <span>searching</span></div>
<div><span>Green</span> <span>driving</span></div>
<div><span>Red blink</span> <span>obstacle / error</span></div>
</div>
The LED becomes the robot dashboard.
</textarea>
</section>
<section class="content" data-markdown="">
<textarea data-template="">
# Robot translation: Status output matters
A robot needs visible state because:
- it may not have a screen
- logs may be unavailable
- behaviour may look ambiguous
- debugging happens on the floor/in the field
- humans need to know if it is safe
A status LED is not (just) decoration
</textarea>
</section>
<section class="code-slide dense exercise" data-markdown="">
<textarea data-template="">
# Chapter 05 exercise: Status output
Open: `src/bin/05_rgb_output.rs`
Tasks:
1. Make the LED blink.
2. Change the colour.
3. Add a status pattern.
4. Use one pattern for `Idle`.
5. Use one pattern for `Searching`.
Checkpoint: `run-05`
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 06: Input
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 06: Input - button as sensor
The 5-way button becomes our fake sensor rig
<div class="flow-chain">
<div><span>Center</span><span>start / stop</span></div>
<div><span>Left</span><span>ball left</span></div>
<div><span>Right</span><span>ball right</span></div>
<div><span>Up</span><span>ball centered</span></div>
<div><span>Down</span><span>obstacle detected</span></div>
</div>
This lets us simulate camera or ultrasonic input.
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Raw input is not robot meaning
Button press:
```text
raw electrical state
```
Robot event:
```text
BallLeft
ObstacleDetected
StartStop
```
We separate hardware reading from robot meaning.
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# Robot translation: Perception is a boundary
The controller should not care whether `BallLeft` came from:
- a camera
- a test file
- a simulator
- a button
- a future ML model
It only needs the event.
</textarea>
</section>
<section class="code-slide exercise" data-markdown="">
<textarea data-template="">
# Chapter 06 exercise: Button to event
Open: `src/bin/06_button_events.rs`
Tasks:
1. Read the 5-way button.
2. Convert direction to `RobotEvent`.
3. Show the event with LED status or logging.
4. Add a default `NoEvent` case.
Checkpoint: `run-06`
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 07: Events
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Chapter 07: Events
```rust
enum RobotEvent {
NoEvent,
StartStop,
BallLeft,
BallRight,
BallCentered,
BallLost,
ObstacleDetected,
Timeout,
}
```
Events are interpreted facts.
They are not raw hardware.
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Event priority matters
```text
New Events: BallCentered + ObstacleDetected
```
What should win?
Probably not: `drive forward`
Some events are safety-critical.
</textarea>
</section>
<section class="code-slide exercise" data-markdown="">
<textarea data-template="">
# Chapter 07 exercise: Add event priority
Open: `src/bin/07_events.rs`
Tasks:
1. Convert button direction to `RobotEvent`.
2. Add a function `prioritize_event`.
3. Ensure `ObstacleDetected` wins.
4. Keep `NoEvent` harmless.
Checkpoint: `test-07`
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 08: State Machine
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 08: Robot state machine
The robot needs memory.
```rust
enum RobotMode {
Idle,
SearchBall,
AlignToBall,
DriveToBall,
AvoidObstacle,
Error,
}
```
It must know what it is currently trying to do.
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# State is not the same as event
Event: `BallLeft`
State: `AlignToBall`
Command: `RotateLeft`
Keep them separate.
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# State produces command
```rust
enum MotionCommand {
Stop,
Forward,
RotateLeft,
RotateRight,
StrafeLeft,
StrafeRight,
Avoid,
}
```
The robot should think in intent first.
</textarea>
</section>
<section class="code-slide content dense ultra-dense" data-markdown="">
<textarea data-template="">
# Decision 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),
(mode, _) → (mode, MotionCommand::Stop),
}
}
```
One place. One transition table. Less mystery.
</textarea>
</section>
<section class="code-slide dense exercise" data-markdown="">
<textarea data-template="">
# Chapter 08 exercise: Make the robot behave
Open: `src/bin/08_state_machine.rs`
Tasks:
1. Add `BallLost -> SearchBall`.
2. Add `StartStop -> Idle`.
3. Ensure `ObstacleDetected` always wins.
4. Map `RobotMode` to LED status.
5. Add one new mode or command.
Checkpoint: `run-08`
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# We can test robot behaviour without a robot
```rust
#[test]
fn obstacle_wins_over_ball() {
let (mode, command) = decide(
RobotMode::DriveToBall,
RobotEvent::ObstacleDetected,
);
assert_eq!(mode, RobotMode::AvoidObstacle);
assert_eq!(command, MotionCommand::Stop);
}
```
Robot logic can be tested on the laptop.
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 09: more Sensors
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 09: Camera model
Real camera pipeline:
<div class="flow-chain">
<span>camera frame</span>
<span>color / blob / ML detection</span>
<span>BallObservation</span>
<span>RobotEvent</span>
</div>
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Camera model -> Workshop model
```rust
enum BallObservation {
NotSeen,
Left,
Center,
Right,
TooClose,
}
```
The controller should not care *how* the ball was detected.
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# Camera data is not clean
A camera may give:
- no ball
- multiple candidates
- wrong colour
- motion blur
- partial visibility
- bad lighting
- stale frame
So convert pixels into a small domain model before logic processing.
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Chapter 09: Ultrasonic model
```rust
struct RawEchoMicros(u32);
struct DistanceCm(u16);
enum ObstacleObservation {
Clear,
Warning,
TooClose,
}
```
Pipeline:
<div class="flow-chain">
<span>raw echo</span>
<span>distance</span>
<span>classification</span>
<span>event</span>
</div>
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Sensor fusion starts small
For a soccer robot with vision and Ultrasonic:
<div class="flow-chain">
<div><span>BallCentered + Clear</span><span>DriveToBall</span></div>
<div><span>BallCentered + TooClose</span><span>Stop or AvoidObstacle</span></div>
<div><span>BallLost + Clear</span><span>SearchBall</span></div>
<div><span>BallLeft + Warning</span><span>Align carefully</span></div>
</div>
Multiple inputs must become one decision.
</textarea>
</section>
<section class="code-slide dense exercise" data-markdown="">
<textarea data-template="">
# Chapter 09 exercise: Sensor pipeline
Open: `src/bin/09_sensor_pipeline.rs`
1. Convert camera to `BallObservation`.
2. Convert distance to `ObstacleObservation`.
3. Observations to Event.
4. Feed event into `decide()`.
5. Add test for obstacle priority.
Checkpoint: `test-09`
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 10: Motion
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 10: Motion commands
The robot does not think in PWM first.
It thinks in motion intent:
```rust
struct MotionVector {
forward: f32,
strafe: f32,
rotate: f32,
}
```
Then a lower layer translates intent into wheel speeds.
</textarea>
</section>
<section class="code-slide content dense ultra-dense" data-markdown="">
<textarea data-template="">
# From symbolic command to vector
```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,
},
...
_ → MotionVector {
forward: 0.0,
strafe: 0.0,
rotate: 0.0,
},
}
}
```
Intent becomes a numeric movement request.
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Four-wheel omni drive: Command to wheels
```rust
struct WheelSpeeds {
front_left: f32,
front_right: f32,
rear_left: f32,
rear_right: f32,
}
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,
}
}
```
The formula is simple. The real robot is not.
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# Real motors add boring but important details
A real motor layer needs:
- speed normalization
- motor orientation calibration
- PWM generation
- motor driver limits
- battery compensation
- emergency stop behaviour
- testing on the floor
Rust helps keep the layers honest.
</textarea>
</section>
<section class="code-slide dense exercise" data-markdown="">
<textarea data-template="">
# Chapter 10 ex: command to wheel speeds
Open: `src/bin/10_motor_commands.rs`
1. Convert `MotionCommand` to `MotionVector`.
2. Convert `MotionVector` to `WheelSpeeds`.
3. Normalize speeds.
4. Add a test for `Stop`.
5. Add a test for `RotateLeft`.
Checkpoint: `test-10`
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 11: Kinematics
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 11: Kinematics enters when geometry matters
For a mobile base:
<div class="flow-chain">
<span>robot motion intent</span>
<span>wheel speeds</span>
</div>
For a robot arm:
<div class="flow-chain">
<span>target position</span>
<span>joint angles</span>
<span>motor commands</span>
</div>
That second step is inverse kinematics.
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Forward kinematics asks: Where did we end up?
<div class="flow-chain">
<span>joint angles</span>
<span>robot geometry</span>
<span>tool position</span>
</div>
Example:
```text
base angle + shoulder angle + elbow angle
→ x / y / z position
```
Good for checking and simulation.
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Inverse kinematics asks: How do we get there?
<div class="flow-chain">
<span>target position</span>
<span>robot geometry</span>
<span>joint angles</span>
</div>
Example:
```rust
struct ArmTarget {
x_mm: f32,
y_mm: f32,
z_mm: f32,
}
struct JointAngles {
base_deg: f32,
shoulder_deg: f32,
elbow_deg: f32,
}
```
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Invalid movement should fail before hardware moves
```rust
enum KinematicsError {
TargetOutOfReach,
JointLimitExceeded,
CollisionRisk,
}
fn inverse_kinematics(
target: ArmTarget,
) -> Result<JointAngles, KinematicsError> {
todo!()
}
```
The safest motor command is the one you never send.
</textarea>
</section>
<section class="code-slide exercise" data-markdown="">
<textarea data-template="">
# Chapter 11 exercise: Reject invalid motion
Open: `src/bin/11_kinematics.rs`
Tasks:
1. Run the tests.
2. Change one reachable target.
3. Add one rejected target.
4. Keep unsafe motion out of the command layer.
Checkpoint: `test-11`
</textarea>
</section>
<section class="section-title" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 12: Embassy
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Chapter 12: Why Embassy?
So far, we wrote the loop ourselves:
<div class="flow-chain">
<span>configure hardware</span>
<span>loop forever</span>
<span>check time</span>
<span>read input</span>
<span>update state</span>
<span>write output</span>
</div>
But it becomes noisy when several things must happen at different rates.
</textarea>
</section>
<section class="content two-column" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 12: Robots rarely do only one thing
* blink status LED every 500 ms
* read buttons or sensors often
* print diagnostics occasionally
* update behaviour state
* send motor commands regularly
* wait without blocking everything else
A single hand-written loop can do this. It just gets ugly.
</textarea>
</section>
<section class="content two-column" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Chapter 12: established embedded crates
We do not need to build every firmware primitive ourselves.
Embassy gives us reusable embedded building blocks:
* async executor
* timers & tickers
* task spawning
* embedded-focused scheduling model
The setup stays explicit.
The waiting and task structure get cleaner.
</textarea>
</section>
<section class="content" data-markdown="">
<textarea data-template="">
# Chapter 12: What Embassy is
Embassy is an embedded Rust framework built around async Rust.
It provides an async executor designed for embedded systems.
Important embedded properties:
* no heap required
* tasks are statically allocated
* async/await works on microcontrollers
* timers can express waiting without busy-loop plumbing
</textarea>
</section>
<section class="code-slide content dense ultra-dense" data-markdown="">
<textarea data-template="">
# Chapter 12: From one loop to tasks
Manual loop model:
<div class="flow-chain">
<span>main loop</span>
<span>do everything</span>
<span>manually track time</span>
<span>repeat</span>
</div>
Embassy model:
<div class="flow-chain">
<span>init hardware</span>
<span>spawn tasks</span>
<span>await timers</span>
<span>executor resumes work</span>
</div>
The firmware still owns the hardware.
The structure is cleaner.
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Chapter 12: Timer-driven waiting
Instead of manual delay plumbing:
```rust
loop {
do_work();
busy_wait();
}
```
Embassy lets us express waiting directly:
```rs
loop {
do_work();
Timer::after(Duration::from_millis(500)).await;
}
```
The code says what it means:
`do work, then wait`
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 12: Periodic work with Ticker
For periodic work:
```rs
let mut ticker = Ticker::every(Duration::from_millis(500));
loop {
ticker.next().await;
hprintln!("heartbeat");
}
```
useful for:
* periodic sensor sampling
* regular status updates
* control-loop prototypes
</textarea>
</section>
<section class="code-slide content" data-markdown="">
<textarea data-template="">
# Chapter 12: Async task shape
```rs
#[embassy_executor::task]
async fn heartbeat_task() {
let mut ticker = Ticker::every(Duration::from_millis(500));
loop {
ticker.next().await;
hprintln!("heartbeat");
}
}
```
The task looks sequential.
The executor handles waiting and resuming.
</textarea>
</section>
<section class="code-slide content dense" data-markdown="">
<textarea data-template="">
# Chapter 12: Main task stays explicit
```rs
#[embassy_executor::main]
async fn main(spawner: Spawner) {
spawner.spawn(heartbeat_task()).unwrap();
loop {
hprintln!("main task alive");
Timer::after(Duration::from_secs(2)).await;
}
}
```
The structure is still visible:
<div class="flow-chain">
<span>start executor</span>
<span>spawn heartbeat</span>
<span>main task keeps running</span>
</div>
</textarea>
</section>
<section class="code-slide exercise" data-markdown="">
<textarea data-template="">
# Chapter 12: Embassy example
Open: `src/bin/12_embassy.rs`
Tasks:
1. Change the heartbeat text.
2. Change the timer period.
3. Add a second async task.
Run: `run-12`
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# Chapter 12: What Embassy does not solve
Embassy does not remove:
* hardware setup
* ownership rules
* bad wiring
* wrong pin mapping
* sensor noise
* control logic bugs
* the need for testing on real hardware
It helps structure waiting and concurrent tasks.
It does not make the robot correct by itself.
</textarea>
</section>
<section class="architecture code-slide dense" data-auto-animate="" data-markdown="">
<textarea data-template="">
# The full architecture we built toward
```text
camera / ultrasonic / button (Sensors)
|
raw reading
|
domain observation
|
RobotEvent
|
RobotMode
|
MotionCommand
|
MotionVector
|
WheelSpeeds
|
motor driver (Output)
```
</textarea>
</section>
<section class="architecture code-slide" data-auto-animate="" data-markdown="">
<textarea data-template="">
# Hardware and software work together in layers
```text
Hardware layer
GPIO, timers, PWM, ADC, buses
Driver layer
LED, button, motor driver, sensor
Domain layer
BallObservation, ObstacleObservation
Behaviour layer
RobotMode, state machine
Control layer
MotionCommand, wheel speeds
```
Do not mix this into one loop!
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# What Rust helped with
- explicit ownership of resources
- visible mutation
- clear domain types
- enums for valid states
- `Result` for sensor failure
- testable robot logic
- separation between hardware and behaviour
- small embedded programs without `std`
Rust did not remove the need for calibration, physics or debugging!
</textarea>
</section>
<section class="content two-column" data-markdown="">
<textarea data-template="">
# What robotics taught us
- Raw input is not meaning
- Meaning is not behaviour
- Behaviour is not motor output
- Motor output is not guaranteed motion
- Real systems need feedback
- Safety should win over ambition
The robot is a loop, not a script
</textarea>
</section>
<section class="closing" data-markdown="">
<textarea data-template="">
# Where to continue
0. Get yourself a microcontroller
1. Add real ultrasonic hardware
2. Add motor driver output
3. Add a camera module or external vision process
4. Add logs and host-side tests
5. Move repeated tasks to Embassy async
6. Put the board onto a small chassis
7. Test on the floor
Build one layer at a time
</textarea>
</section>
<section class="closing" data-markdown="">
<textarea data-template="">
# Useful references
- The Rust Book: ownership and borrowing
- The Embedded Rust Book: `no_std` and bare-metal concepts
- probe-rs: flashing and embedded debugging tooling
- RoboCupJunior Soccer rules and description
</textarea>
</section>
<section class="closing" data-markdown="">
<textarea data-template="">
# Questions?
more at wieerwill.dev & corpuls.world
<img
class="slide-image r-stretch"
src="docs/corpuls_heart.jpg"
alt="Corpuls Heart"
/>
</textarea>
</section>
<section class="closing" data-markdown="">
<textarea data-template="">
# Feedback?
more at wieerwill.dev & corpuls.world
<img
class="slide-image r-stretch"
src="docs/rust-on-robots-hands-on-embedded-rust-on-stm32_jeutter_1154375_feedback-code.png"
alt="WAD Feedback Form"
/>
</textarea>
</section>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5/dist/reveal.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/markdown/markdown.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/highlight/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/notes/notes.min.js"></script>
<script>
const searchParams = new URLSearchParams(window.location.search);
const isPrintPdf = searchParams.has('print-pdf');
const deckDirectoryUrl = (() => {
const url = new URL(window.location.href);
url.hash = '';
url.search = '';
if (url.pathname.endsWith('/presentation.html/')) {
url.pathname = url.pathname.replace(/\/presentation\.html\/$/, '/');
} else {
url.pathname = url.pathname.replace(/\/presentation\.html$/, '/');
}
return url;
})();
const resolveDeckAsset = path => new URL(path.replace(/^\.\//, ''), deckDirectoryUrl).href;
const normalizeLocalImageSources = root => {
const scope = root || document;
scope.querySelectorAll('img[src^="docs/"], img[src^="./docs/"]').forEach(img => {
const originalSrc = img.getAttribute('src');
if (!originalSrc) return;
img.src = resolveDeckAsset(originalSrc);
});
};
normalizeLocalImageSources(document);
if (isPrintPdf) {
document.body.classList.add('print-pdf-mode');
}
Reveal.on('ready', () => {
normalizeLocalImageSources(document);
document.querySelectorAll('.reveal .slides section').forEach(section => {
const listItems = section.querySelectorAll('ul li, ol li');
listItems.forEach((li, i) => {
if (!li.classList.contains('no-fragment')) {
li.classList.add('fragment');
li.setAttribute('data-fragment-index', String(i));
}
});
const flowItems = section.querySelectorAll('.flow-chain span, .chapter-grid div');
flowItems.forEach((el, i) => {
el.classList.add('fragment');
el.setAttribute('data-fragment-index', String(i));
});
});
Reveal.sync();
});
Reveal.initialize({
hash: true,
history: true,
controls: true,
controlsTutorial: true,
progress: true,
slideNumber: 'c/t',
center: true,
width: 1920,
height: 1080,
margin: isPrintPdf ? 0 : 0.02,
minScale: 0.2,
maxScale: 1.6,
pdfMaxPagesPerSlide: 1,
pdfSeparateFragments: false,
transition: 'slide',
transitionSpeed: 'fast',
backgroundTransition: 'fade',
autoAnimate: true,
autoAnimateEasing: 'ease-out',
autoAnimateDuration: 0.55,
markdown: {
smartypants: true
},
plugins: [ RevealMarkdown, RevealHighlight, RevealNotes ]
}).then(() => {
normalizeLocalImageSources(document);
if (isPrintPdf) {
Promise.all(
Array.from(document.images).map(img => {
if (img.complete) return Promise.resolve();
return new Promise(resolve => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
})
).then(() => {
document.querySelectorAll('img.slide-image').forEach(img => {
img.removeAttribute('loading');
img.removeAttribute('decoding');
img.classList.remove('r-stretch');
});
Reveal.layout();
window.dispatchEvent(new Event('resize'));
});
}
});
</script>
</body>
</html>