ai-passport

    Reusable MoonBit SDK for the FoloToy AI Passport wearable: 120x160 RGB565 graphics, semantic input, four-voice music engine, battery, and backend driver contracts.

    moonbit
    game
    embedded
    graphics
    audio
    music
    folotoy
    Download zip
    Author
    Version
    0.0.3
    License
    Apache-2.0
    Last updated
    5 days ago
    Downloads
    231

    #ai-passport.mbt

    Reusable MoonBit SDK for the FoloToy AI Passport wearable.

    Mooncakes module: colmugx/ai-passport.

    The SDK defines portable MoonBit application contracts. Host and device backends must implement the same semantics through thin platform adapters. Native, JS, and wasm (web host backend, see below) are supported and tested targets.

    #Repository scope

    This repository is only the reusable SDK. Forest Walk was removed from this repository and must be recovered from git history into the separate ai-passport-template repository. That template is also the place for a starter application, browser preview, device integration, and flashing/provisioning tooling; their migration and release validation are still pending.

    No raylib, ESP-IDF, BSP, or browser APIs appear in SDK MoonBit code or its public API. The web host's browser code lives entirely in the hosts/web/*.js assets (see "Web host" below).

    #Packages

    PackageContents
    corePoint, Size, Rect, Color (RGB565 conversion), LOGICAL_WIDTH = 120, LOGICAL_HEIGHT = 160
    graphicsCanvas drawing (clear, pixel, line, rect, fill_rect, sprite, bitmap text), SpriteSheet, text metrics, read-only FrameView
    inputSemantic Button (Up / Down / Ok), ButtonEvent (Press / Click / DoubleClick / LongPress), edge-detecting InputState
    musicMeter (6/8), Tempo (dotted-quarter BPM), Pitch / Note / Track / Song (max four tracks), Sequencer, TickClock
    audio16 kHz PCM16 mono Synth with four monophonic voices, five waveforms (Pulse12, Pulse25, Pulse50, Triangle, Noise), and a sample-accurate Player that owns the music transport (loop, pause/resume, beat sync)
    batteryBatterySource trait and caching Battery; readings are Int? so unavailable values are explicit
    driverBackend-facing Clock and DisplaySink contracts, plus test fixtures (ZeroClock, SinkProbe)
    hostabiInternal, experimental wasm host-boundary package: ABI v0 constants, the closure-injected HostBridge (DisplaySink / PcmSink / BatterySource / Clock adapters), wasm-gated passport.* externs, and inline-WAT u16 store/load helpers

    #Display model

    The logical screen is fixed at 120×160 pixels for v0.1. Applications draw with logical coordinates; the backend scales/presents however the hardware requires. Colors are authored as RGB and quantized to RGB565 by Color::to_rgb565() — the same quantized colors a preview and the device LCD show.

    Canvas stores one UInt16 RGB565 value per pixel. Canvas::frame_view() creates a read-only view sharing that storage; it does not copy a full frame. FrameView::width(), height(), and copy_rgb565_row(y~, out~ : FixedArray[Int]) -> Int let a backend read rows. The copy returns the number of pixels written: zero for an invalid row and a prefix count when out is too short. A view is valid only until its canvas is next mutated, so a display sink must consume it synchronously or copy the rows it needs. Graphics public APIs do not expose strip rendering or display-controller specifics.

    #Input model

    Buttons are semantic values — Up, Down, Ok — never GPIO or ADC channels. InputState turns raw press/release feeds into pressed, just_pressed, and just_released edges per frame via advance().

    #Music and audio

    • 6/8 meter with dotted-quarter BPM tempo (default 76). Song time is measured in ticks, with four ticks per eighth note by default and twelve ticks per dotted-quarter beat.
    • Song::new accepts at most four tracks (raising SongError::TooManyVoices for more) and stores an immutable snapshot: the caller's authoring arrays are deep-copied, so mutating them afterwards cannot change a constructed song. Read access: meter(), tempo(), ticks_per_eighth(), track_count().
    • Sequencer walks a song with deterministic looping (arrival-based: the first step fires the tick-0 note starts); length() is the loop length in song ticks. TickClock converts elapsed samples into song ticks with exact integer accumulation (no drift), and ticks_to_samples_exact measures from the current clock phase to a future tick boundary.
    • Synth renders 16 kHz signed PCM16 mono, mixes up to four monophonic voices, and clamps to [-32768, 32767]. Instrument articulation defines envelope, volume, and optional vibrato; the waveform defines oscillator shape. Integer envelope ramps reach their targets at the configured sample duration, including release from the level where it begins.
    • Player owns the authoritative sample clock and transport. It fires tick-0 notes before the first sample, processes later starts at exact sample boundaries, and uses a preallocated event buffer through Sequencer::step_into. Note gates end on their musical tick boundary despite fractional clock carry. beat() counts elapsed dotted-quarter beats across loops, including one-tick loops, and pause freezes that count.

    #Battery

    BatterySource::percent and millivolts return Int? to represent unavailable readings. Battery caches readings behind an explicit refresh(); construction performs no source I/O. Battery::fixture(percent~) supplies a test value.

    #Driver contracts (for backend authors)

    A platform backend implements the relevant pub(open) traits:

    • Clockmonotonic_ms() and sleep_ms() for frame pacing.
    • DisplaySinkpresent(frame~ : @graphics.FrameView) receives a synchronous, read-only view of the finished RGB565 canvas.
    • PcmSink (in audio) — write(samples~ : FixedArray[Int]) receives signed PCM16 mono sample blocks.
    • BatterySource (in battery) — percent() and millivolts() return optional readings.

    Backend glue stays thin and replaceable; all reusable logic is pure MoonBit in the packages above.

    #Web host (wasm backend)

    The SDK ships an application-agnostic web host backend for compiled MoonBit wasm apps: hosts/web/passport-host.js with hosts/web/pcm-worklet.js and hosts/web/index.html implements the internal, experimental ABI v0 contract specified in docs/WEB_HOST.md. It is a host backend, not an application preview or template — it holds no application state, and all browser code lives in the hosts/web/*.js assets, not in SDK MoonBit packages. src/hostabi adapts the SDK contracts to the raw wasm boundary, and src/fixture is the smallest main package that proves the boundary end-to-end with deterministic pixels and PCM. ABI v0 is internal and not a frozen public SDK API; the bundle contract is <bundle>/app.wasm plus <bundle>/assets/ (hosts/web/README.md).

    Verify the boundary with the fixture and integration suite, from the repository root:

    moon build --target wasm --release # fixture app.wasm, release profile moon build --target wasm # fixture app.wasm, debug profile (the suite pins both) node hosts/web/tools/gen-test-pcm.mjs # once; creates the committed PCM asset node hosts/web/tools/make-bundle.mjs # assembles _build/passport-bundle node hosts/web/test/run-tests.mjs # nine suites; exits 2 if the fixture artifacts are missing

    CI runs the same gate in a dedicated wasm-host job (.github/workflows/ci.yml): moon check and moon test with --target wasm, both fixture build profiles, and the full integration suite — including the real-browser suite through pinned playwright chromium — with no skip flags.

    #Development

    Run moon check --target native --output-json and moon test --target native --output-json, then the same checks with --target js. Run moon info to regenerate public interfaces and moon fmt to format MoonBit files. Review generated interface changes before release.