2319 lines
56 KiB
HTML
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: &str) {
|
|
println!("command = {command}");
|
|
}
|
|
|
|
let command = String::from("forward");
|
|
print_command(&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: &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>
|