Skip to content

Commit 392ff4e

Browse files
committed
Merge branch 'docs/demo-recording'
2 parents e75c773 + 01ca00b commit 392ff4e

6 files changed

Lines changed: 206 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Versioning follows [Semantic Versioning](https://semver.org/).
99

1010
### Added
1111

12+
- A recorded demo in the README: one run showing the install, a PASS with its
13+
exit code, the `--trace` wire output including the OBC's own command frame, a
14+
thermal module closing a heater loop, and a deliberate failure exiting 1. The
15+
session is scripted rather than hand-recorded, so it can be reproduced when
16+
the CLI's output changes — see [`docs/demo/`](docs/demo/).
1217
- A documented, tested path to writing your own subsystem module:
1318
[`docs/writing-a-module.md`](docs/writing-a-module.md). It covers the
1419
module/FSM contract, the config dataclass that doubles as the

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ breakdown.
5454

5555
## Quickstart
5656

57+
![Demo of a full run](https://raw.githubusercontent.com/glocker/CubeSatTestbed/main/docs/images/demo.gif)
58+
59+
One run end to end: install, a PASS with its exit code, the wire trace proving
60+
assertions read frames off the bus, a thermal module closing a heater loop, and
61+
a deliberate failure exiting 1. Recorded from `main` — the published 1.0.0
62+
predates `init`, `--example` and `--trace`. The cast is scripted and
63+
re-recordable: see [`docs/demo/`](docs/demo/).
64+
5765
```sh
5866
pip install cubesat-testbed
5967
cubesat-testbed run --example default

docs/demo/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Demo recording
2+
3+
The GIF embedded in the top-level `README.md` is not hand-recorded. It is
4+
rendered from a scripted session so it can be reproduced whenever the CLI's
5+
output changes, instead of quietly drifting out of date the way a one-off
6+
screen capture does.
7+
8+
- `demo.sh` — the content of the recording: the commands, the pacing, and the
9+
short prompt it prints instead of whoever's real shell prompt. Run it
10+
directly to iterate on the script without recording anything.
11+
- `record.sh` — wraps `demo.sh` in `asciinema`, renders the resulting cast to
12+
`demo.gif` with `agg`, and installs both tools into `~/.local/bin` if they
13+
are missing. Neither needs root.
14+
15+
```sh
16+
docs/demo/record.sh
17+
cp docs/demo/demo.gif docs/images/demo.gif
18+
```
19+
20+
`asciinema` needs a real TTY, so this cannot run from CI or a non-interactive
21+
session. Size the terminal to roughly 100x30 first: `asciinema` 2.x takes the
22+
frame size from the terminal it runs in (3.x is passed `--cols`/`--rows`).
23+
24+
## What the recording has to show
25+
26+
Two things are the point of the demo and should survive any edit:
27+
28+
- **The wire trace.** The strongest claim this project makes is that an
29+
assertion only ever observes telemetry by decoding a frame that crossed the
30+
bus. `--trace` is what makes that visible, including the OBC's own
31+
low-battery command as bytes.
32+
- **A deliberate failure with a non-zero exit code.** A demo where everything
33+
passes says nothing about whether the tool would catch a regression. The
34+
script breaks the injected battery level so the FDIR rule never trips, and
35+
the assertion fails with the observed value — which is exactly what CI sees.
36+
37+
## Installing from git
38+
39+
`demo.sh` installs from `git+https://github.com/glocker/CubeSatTestbed@main`
40+
rather than from PyPI, because the published 1.0.0 predates `init`,
41+
`--example` and `--trace`; a demo of those commands cannot be recorded against
42+
it. Once a release carrying them is on PyPI, set `INSTALL_SPEC` at the top of
43+
`demo.sh` back to plain `cubesat-testbed` and re-record.

docs/demo/demo.sh

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env bash
2+
# Scripted CubeSatTestbed demo -- the *content* of the recording.
3+
#
4+
# Run it through record.sh, which wraps it in asciinema and renders the GIF.
5+
# Running it directly also works and is the fastest way to iterate on the
6+
# script without recording anything.
7+
#
8+
# Deliberately not `set -e`: the last step is *supposed* to fail, and its
9+
# non-zero exit code is the point of the demo.
10+
set -u
11+
12+
# PyPI still serves 1.0.0, which predates `init`, `--example` and `--trace`.
13+
# Until 1.1.0 is published the demo has to install from git; after that,
14+
# change this to just `cubesat-testbed` and re-record.
15+
INSTALL_SPEC="${INSTALL_SPEC:-git+https://github.com/glocker/CubeSatTestbed@main}"
16+
17+
TYPE_DELAY="${TYPE_DELAY:-0.03}" # seconds per typed character
18+
PAUSE="${PAUSE:-1.2}" # pause after a command's output
19+
20+
# A short prompt of our own: the recording must not show whatever host and
21+
# username happen to be running it.
22+
PROMPT_COLOR=$'\033[38;5;114m'
23+
COMMENT_COLOR=$'\033[38;5;245m'
24+
RESET=$'\033[0m'
25+
26+
say() {
27+
printf '%s# %s%s\n' "$COMMENT_COLOR" "$*" "$RESET"
28+
sleep 0.9
29+
}
30+
31+
type_cmd() {
32+
printf '%s$%s ' "$PROMPT_COLOR" "$RESET"
33+
local i
34+
for ((i = 0; i < ${#1}; i++)); do
35+
printf '%s' "${1:i:1}"
36+
sleep "$TYPE_DELAY"
37+
done
38+
printf '\n'
39+
sleep 0.35
40+
}
41+
42+
run() {
43+
type_cmd "$1"
44+
eval "$1"
45+
sleep "$PAUSE"
46+
}
47+
48+
WORKDIR="$(mktemp -d)"
49+
cd "$WORKDIR"
50+
51+
say "A CubeSat subsystem testbed: deterministic scenarios over a real CSP/CAN bus."
52+
say "Install it into a clean virtualenv."
53+
run "python3 -m venv .venv && . .venv/bin/activate && pip install -q $INSTALL_SPEC"
54+
run "cubesat-testbed init --list"
55+
56+
say "Copy a packaged example out and run it: setup, scenario, PASS, exit code."
57+
run "cubesat-testbed init demo && cd demo"
58+
run "cubesat-testbed run -c setup.toml -s scenario.yaml; echo exit=\$?"
59+
60+
say "Assertions never read Python state -- they decode frames off the bus."
61+
say "--trace prints every frame that actually crossed it."
62+
run "cubesat-testbed run --example default --trace 2>&1 | head -8"
63+
64+
say "And the OBC's low-battery decision is on that same bus, as real bytes:"
65+
run "cubesat-testbed run --example default --trace 2>&1 | grep command | head -2"
66+
67+
say "Subsystem behaviour lives in modules. Here a thermal node and an OBC"
68+
say "rule close a heater control loop entirely over that same bus."
69+
run "cubesat-testbed run --example thermal-heater"
70+
71+
say "Now break it on purpose: inject a battery level that never trips the rule."
72+
say "The FDIR logic then never sheds the payload -- and the test must catch that."
73+
run "sed -i s/value:\\ 25/value:\\ 95/ scenario.yaml"
74+
run "cubesat-testbed run -c setup.toml -s scenario.yaml; echo exit=\$?"
75+
76+
say "FAIL, with the observed value, and exit code 1. That is what CI sees."
77+
sleep 2

docs/demo/record.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env bash
2+
# Record demo.sh as an asciinema cast and render it to an animated GIF.
3+
#
4+
# Installs both tools into ~/.local/bin without root: asciinema is pure
5+
# Python, agg is a single static binary. Run this from a real terminal --
6+
# asciinema needs a TTY, which is the one thing a non-interactive session
7+
# cannot give it.
8+
set -euo pipefail
9+
10+
AGG_VERSION="${AGG_VERSION:-v1.9.0}"
11+
12+
# agg ships one binary per architecture and there is no universal build, so
13+
# pick by uname rather than assuming x86_64 -- guessing wrong fails late, at
14+
# render time, with a bare "Exec format error" after the cast is recorded.
15+
if [ -z "${AGG_TARGET:-}" ]; then
16+
case "$(uname -m)" in
17+
x86_64 | amd64) AGG_TARGET="x86_64-unknown-linux-musl" ;;
18+
aarch64 | arm64) AGG_TARGET="aarch64-unknown-linux-gnu" ;;
19+
armv7l | armv6l) AGG_TARGET="arm-unknown-linux-gnueabihf" ;;
20+
*)
21+
echo "no agg build for $(uname -m); set AGG_TARGET manually" >&2
22+
exit 1
23+
;;
24+
esac
25+
fi
26+
27+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28+
OUT_DIR="${OUT_DIR:-$HERE}"
29+
CAST="$OUT_DIR/demo.cast"
30+
GIF="$OUT_DIR/demo.gif"
31+
32+
BIN_DIR="$HOME/.local/bin"
33+
mkdir -p "$BIN_DIR" "$OUT_DIR"
34+
export PATH="$BIN_DIR:$PATH"
35+
36+
if ! command -v asciinema >/dev/null 2>&1; then
37+
echo "installing asciinema..."
38+
if command -v uv >/dev/null 2>&1; then
39+
uv tool install asciinema
40+
else
41+
python3 -m pip install --user asciinema
42+
fi
43+
fi
44+
45+
if ! command -v agg >/dev/null 2>&1 || ! agg --version >/dev/null 2>&1; then
46+
echo "installing agg $AGG_VERSION for $AGG_TARGET..."
47+
curl -fsSL -o "$BIN_DIR/agg" \
48+
"https://github.com/asciinema/agg/releases/download/$AGG_VERSION/agg-$AGG_TARGET"
49+
chmod +x "$BIN_DIR/agg"
50+
# Fail here, not after a recording, if the binary still does not run.
51+
agg --version >/dev/null
52+
fi
53+
54+
rm -f "$CAST" "$GIF"
55+
56+
# --cols/--rows exist in asciinema 3.x but not 2.x, where the pty simply
57+
# inherits the terminal. Probe instead of pinning a version: on 2.x, size
58+
# your terminal to about 100x30 before recording.
59+
REC_ARGS=(--idle-time-limit 1 --command "bash $HERE/demo.sh")
60+
if asciinema rec --help 2>&1 | grep -q -- '--cols'; then
61+
REC_ARGS+=(--cols 100 --rows 30)
62+
fi
63+
64+
echo "recording -> $CAST"
65+
asciinema rec "${REC_ARGS[@]}" "$CAST"
66+
67+
echo "rendering -> $GIF"
68+
agg --font-size 16 --speed 1.3 --theme asciinema "$CAST" "$GIF"
69+
70+
ls -lh "$GIF"
71+
echo
72+
echo "Preview it, then copy into the repo:"
73+
echo " cp $GIF ~/CubesatTestbed/docs/images/demo.gif"

docs/images/demo.gif

1.64 MB
Loading

0 commit comments

Comments
 (0)