Daoa Mini - Yi Jing for Cardputer ADV
Multi-purpose View the repo
4 stars · 1 forks
- Maintainer
- chatelp
- Capabilities
- wifi
- Chip families
- esp32-s3
Runs on these boards
Unverified
- CardputerUnverified
Flash Cardputer
Unverified esp-atlas asserts this board↔firmware link at the shown trust tier — it does not guarantee your specific unit or the current firmware version. Flashing can erase keys/config and can brick the device. At your own risk.
README
English · Français
Daoa Mini
A line-by-line Yi Jing casting, on a pocket computer. M5Stack Cardputer ADV · ESP32-S3 · 240 × 135 display · fully offline.
The place is listening.
Daoa Mini is a small promotional object for the Daoa mobile app (iPhone / Android): you cast six lines by the traditional three-coin method, the device composes the figure, its complementary figure, and gives you the text — in French or English. No network, no account, no AI. The randomness comes from the ESP32's hardware generator.
Its distinctive feature: before every casting, the device listens to the place.
The place is listening
A passive WiFi scan — never connected, radio switched off the moment it ends, no SSID or BSSID collected, nothing stored or transmitted — becomes a field of particles: one per network in range, its position derived from the channel, its brightness from the signal strength. They drift, breathe, emit concentric ripples, and flare when a line is cast.
That field weights the casting, in a bounded and documented way:
| Reading | Derived from | Effect | |---|---|---| | density | network count + mean energy | leans yin or yang, ± 5 % at most | | agitation | channel spread + signal variance | makes changing lines likelier, + 4 % at most |
The casting stays real: the hardware TRNG remains the only source of
randomness, and the user never chooses anything. The feature is on by
default, switchable off in the settings (s), and fully inspectable
with the w key:
Networks, channels, dBm, density, agitation, β, γ, and the resulting probabilities of the four line values. Transparency is the condition of the rule — see docs/03 (French).
The screens
| | | |:--:|:--:| | | | | Home — the lettermark, the breathing prompt | Reading the place — ~1.4 s: the scan time becomes the ritual | | | | | Casting — six lines bottom-up, cinnabar marks on changing ones | Result — glyph, name, gloss, mini-figure, two pages | | | | | Reading — portrait, Judgment, Image, the six lines' meanings | Structure — the two trigrams, derived from the lines | | | | | Settings — place reading, sounds, volume; persisted to NVS | Promo — QR code to the full app |
Getting around
| Key | Action |
|---|---|
| space | begin, cast a line, continue |
| ← → | first figure ↔ complementary figure |
| ↓ | full text of the figure; then pages; then structure |
| ↑ | go back through the pages |
| l | French ↔ English |
| h | built-in help (7 sections) |
| s | settings (place reading, sounds, volume) |
| w | technical view of the field |
| esc | back |
Sounds are soft-decaying PCM sinusoids, never a square buzzer: the six lines walk up a pentatonic scale (A5 → A6), a fifth above marks the changing lines, and a descending fifth resolves when the result appears.
Why that scale sits two octaves higher than it "should"The scale was first written where it reads naturally on paper — A3 to A4, 220 to 440 Hz. On the device, the casting was completely silent while the interface ticks were perfectly audible.
Nothing wrong with the code: a 1 W micro speaker in a pocket-sized enclosure has its resonance around 1 kHz and rolls off sharply below ~400 Hz. The notes were being played, faithfully, into a band the hardware cannot reproduce. Transposing the whole scale up two octaves — musical relationships untouched, register moved into the speaker's efficient band — fixed it. Worth remembering before designing any audio for this class of device.
The mechanics
Six lines, cast bottom to top. Each is a 6, 7, 8 or 9:
| Value | Nature | Line | Changing | |:--:|---|---|:--:| | 6 | old yin | broken, marked with a cross | → yang | | 7 | young yang | solid | — | | 8 | young yin | broken | — | | 9 | old yang | solid, marked with a circle | → yin |
Traditional three-coin probabilities: 1/8, 3/8, 3/8, 1/8 — a deliberate departure from Daoa v1, which drew uniformly. Changing lines become rare events again, and therefore readable.
The six lines reduced to yin/yang give 6 bits → a King Wen number through a lookup table (Javary/Faure ordering). Transforming the changing lines yields the complementary figure: the one the situation tends toward.
The detail that nearly caught us out — why we don't bias the coinsThe natural way to weight the casting was to bias the coins themselves: P(heads) = 0.5 + ε. It is mathematically useless.
In the three-coin method a line is yang when the number of heads is odd (7 = one head, 9 = three heads). And the parity of a binomial draw is remarkably insensitive to bias:
$$P(\text{odd}) = \frac{1 - (1-2p)^3}{2}$$
Substituting $p = 0.5 + \varepsilon$, everything cancels down to third order: $P(\text{yang}) = 0.5 + 4\varepsilon^3$. With a 5 % coin bias the real effect would be 0.05 % — a thousand times too small to matter.
So the implemented construction acts at the line level, as two independent Bernoulli trials, strictly equivalent to the tradition when unweighted:
P(yang) = 0.5 + β β = (density − 0.5) × 0.1 ∈ [−0.05, +0.05]
P(changing) = 0.25 + γ γ = agitation × 0.04 ∈ [0, 0.04]
At β = γ = 0 you get exactly 1/8, 3/8, 3/8, 1/8 back. Thresholds are compared against 10-bit fields of the TRNG (512/1024 and 256/1024: exact, no modulo bias). One test asserts the neutrality, another that the effect stays bounded at the caps.
The hardware
| | | |---|---| | MCU | ESP32-S3FN8 (StampS3A), dual-core Xtensa LX7 @ 240 MHz | | Memory | 8 MB flash, 512 KB SRAM, no PSRAM | | Display | ST7789V2, 1.14″, 240 × 135 — about 25 × 14 mm at ~245 ppi | | Keyboard | 56 keys, TCA8418 I²C controller | | Audio | ES8311 codec + NS4150B + 1 W speaker | | Also | BMI270 IMU, IR, microSD, 1750 mAh battery |
Firmware footprint: 3.47 MB of flash (41 % of 8 MB) and 50.9 KB of RAM (15.5 %). The full-screen framebuffer is a 16-bit, 64.8 KB sprite in SRAM: everything is drawn off-screen and pushed in one go — no flicker.
Build, simulate, flash
brew install platformio sdl2
# Pure-logic tests (20 cases, no hardware needed)
pio test -e test-native
# macOS simulator: SDL window, 240 × 135 at ×3
pio run -e sim && ./.pio/build/sim/program
# Firmware, with the Cardputer plugged in over USB
pio run -e cardputer-adv -t upload
The distributable image is firmware.factory.bin (bootloader + partition
table + app, written at offset 0) — not firmware.bin, which is the
application alone.
The two generator scripts locate their private sources through
$DAOA_V1_PATH and $DAOA_IOS_PATH; no machine path is written into the
repository. Without them the scripts stop with a clear message, and the
generated headers they produce are not versioned — see below.
Three PlatformIO environments: the firmware (Arduino-ESP32 3.x through pioarduino), the SDL simulator, and the native tests.
The simulator
All rendering goes through the lgfx:: API — never through the hardware —
so the same code draws on the device and on the Mac. That is what makes
visual iteration bearable: no 30-second flash cycle to nudge something by
2 px. It also produces deterministic captures:
./.pio/build/sim/program --shot <dir> # 17 screens, fixed randomness
./.pio/build/sim/program --frames <dir> # 60 frames of the living field
./.pio/build/sim/program --seed <n> # replay one specific casting
./.pio/build/sim/program --fonttest <dir> # glyph coverage sheet
Every image in this README comes out of it.
Architecture
src/
core/ pure logic — C++17, zero Arduino/M5 dependency, natively tested
casting the casting state machine, King Wen table
ambient place reading → bounded weights
ambient/ hardware boundary: real WiFi scan / simulated scan
sound/ PCM synthesis of the interface sounds
ui/ design-system tokens, LovyanGFX components, screens
content/ generated corpus (not versioned — see below)
sim/ SDL entry point — captures, frames, seeds
main.cpp device entry point — TCA8418 keyboard mapping
Randomness is injected (uint32_t (*)()): esp_random() on the
device, a deterministic PRNG in tests and in the simulator. Same for the
WiFi scan and for persistence — core/ only ever sees interfaces, which
is what makes it fully testable on a desktop machine.
The design system
The interface follows a dedicated micro design system, where every
dimension is a constexpr in src/ui/daoa_tokens.h
and every component a render function in
src/ui/components.cpp:
- 135 isn't divisible by 4 — so there is no vertical grid, but three fixed bands instead: header 18, content 99, footer 18.
- A 228 px column = 38 characters at 6 px advance; three type sizes (12 / 16 / 24), leading = height + 2.
- Yin/yang lines in three sizes, thickness/width ratio held at 1:12.
- Five colours, not one more. Paper for what speaks of the casting, stone for what speaks of the interface, cinnabar reserved for changing-line marks, terracotta reserved for the brand.
- No shadows, no borders, no cards: the bands read through emptiness. At 245 ppi on black, a 1 px rule draws more attention than the text it separates.
The font expedition
M5GFX's DejaVu fonts are pure ASCII: no accents at all, as discovered while rendering « Créatif ». After measuring in the simulator, body text moved to efontJA (which covers accented Latin) and the glyphs to efontCN 24 bold — except 遯 (33), 夬 (43) and 姤 (44), missing from the CN font, which fall back to efontJA. A dedicated VLW font for the exact 64 glyphs is on the roadmap.
What this repository does not contain
The repository is public and holds the engine — not the Daoa brand or content. Excluded, and purged from history:
- design deliverables and brand assets;
- the generated headers that encode brand or content: the bilingual corpus of the 64 figures, and the bitmap masks.
src/core/kingwen_table.h stays public: it is mathematical heritage, not
Daoa content.
An accepted consequence: a fresh public clone will not build as-is.
The scripts scripts/extract_content.py and scripts/convert_assets.py
regenerate those headers from private sources. The code, the architecture
and the mechanics remain entirely readable — which is what's worth
anything to someone passing by.
Documentation
The project documentation is in French.
| | | |---|---| | CLAUDE.md | project doctrine, guardrails, technical decisions | | docs/00 | initial analysis: hardware, libraries, tooling | | docs/01 | log of ratified decisions | | docs/02 | tree, build environments, content pipeline | | docs/03 | casting specification — source of truth | | docs/04 | screens, visual direction, fonts, animation | | docs/05 | phases and current state |
Licence
The code in this repository is released under the MIT licence — take it, learn from it, build on it.
What the licence does not cover, because it is not in the repository: the Daoa brand (lettermark, seal, visual identity) and the Yi Jing corpus written for Daoa. Those stay proprietary, and are generated locally from private sources.
About the texts
The texts for the 64 figures — portrait, Judgment, Image, and the meaning of the six lines — are Daoa's own wording, written in faithfulness to public-domain translations: James Legge (1882) for English, Charles de Harlez (1889) for French. Nothing is copied word for word from a protected translation.
The Yi Jing is presented here as a text for reflection, not as an oracle. The device describes; it does not predict.
Read the full README on GitHub
source github.com/chatelp/daoa-mini-cardputeradv