Nocturne Custom Firmware

repository·main·Indexed 18 days ago

https://github.com/usenocturne/nocturne

Custom firmware for the Spotify Car Thing featuring a Rust-based daemon (nocturned), a React/Vite web-based UI, and mobile companion support. Built using Yocto Project with the meta-nocturne layer on top of meta-superbird, it includes an A/B OTA update stack via SWUpdate, a specialized 'bandaid' partition for hot-swappable components, and both production and development image variants.

Tokens
53K
Snippets
162
Records
227
Agent score
63%

What's inside Nocturne

  1. Understand Nocturne OTA mechanisms

    main

    Nocturne uses A/B partitioning with libswupdate for Over-the-Air updates. If a new slot fails to boot three times, the bootloader automatically rolls back.

    There are four types of updates driven by the companion app:

    1. image: Writes a full .swu to the inactive root partition.
    2. daemon: Atomically rotates the nocturned binary on the bandaid bind-mount and restarts the service.
    3. builtin-webapp: Swaps the SPA bundle on the bandaid bind-mount and restarts the service.
    4. bandaid: A combined update for both the daemon and the SPA bundle.

    Delta OTAs: Uses zchunk to ship only changed chunks via HTTP range requests over the USB link.

  2. Understand the meta-nocturne layer composition

    main

    The meta-nocturne layer is the application layer for Nocturne OS. It sits on top of meta-superbird (the BSP) and transforms a bare BSP into a complete Nocturne device by providing the daemon, SPA bundle, wake-word models, fonts, MFi glue, and OTA wrappers.

    Dependencies:

    • core (Poky)
    • superbird (BSP, which includes meta-meson and meta-browser/meta-chromium)

    Compatibility:

    • LAYERSERIES_COMPAT_nocturne = "scarthgap wrynose"
  3. Understand the Bandaid and Floor Sync mechanism

    main

    The bandaid partition holds nocturned.current and the SPA bundle. An opt-overlay systemd unit bind-mounts /var/lib/bandaid/nocturne at /opt/nocturne. This allows the daemon to execute code from the bandaid independently of the rootfs.

    Floor Sync Logic: When a full SWU update occurs, the rootfs is updated but the bandaid remains stale. The nocturne-floor-sync.service handles this:

    1. It compares the baked rootfs floor version against /var/lib/bandaid/nocturne/.floor-version using SemVer + +build identifiers.
    2. If the rootfs is newer, it stages and validates the daemon and UI.
    3. It promotes them atomically and updates the stamp.
    4. If promotion fails, it restores the previous daemon and leaves the stamp unchanged to allow a retry on next boot.

    Hot Updates: Daemon-only or webapp-only updates bypass the rootfs and rewrite the bandaid directly via the companion app's applyUpdate { kind: daemon | builtin-webapp }.

  4. Interact with a booted Nocturne device via USB

    main

    When connected via USB-CDC-NCM, the device is accessible via mDNS as nocturne.local. Use the recipes in image/Justfile to interact with it:

    • just -f image/Justfile ssh: Open an interactive shell.
    • just -f image/Justfile console start: Start the UART agent on /dev/ttyUSB0.
    • just -f image/Justfile reboot-to-maskrom: Drop the device into maskrom mode for reflashing.
    • just -f image/Justfile flash: Perform a full image flash via flashthing-cli.
    • just -f image/Justfile ota: Push a delta OTA to the booted device.
    just -f image/Justfile ssh
  5. Verify the OTA stack in the image

    main

    To verify that the OTA stack is correctly included in your image, do not run these commands on the host directly. Enter the kas/bitbake shell first, then inspect the IMAGE_INSTALL variable.

    just shell
    bitbake -e nocturne-prod-image | grep ^IMAGE_INSTALL

    Expected Output: IMAGE_INSTALL must include:

    • swupdate
    • swupdate-client
    • swupdate-tools
    • nocturne-keys
    • nocturne-state-dirs (via packagegroup-nocturne-core)
  6. Migrate iAP2 Control Session Messages (CSMs) to generated code

    main

    When adding or updating iAP2 CSMs, follow this migration path to move from hand-written modules to generated structs:

    1. Add or update a Csm entry in CSM_INVENTORY with the message id, direction, and param list.
    2. Run the codegen command: just codegen (or cargo run -p nocturne-codegen --bin codegen).
    3. Inspect the output in crates/iap2/src/csm/generated.rs.
    4. Update call sites to use crate::csm::generated::<Type> only after verifying that the generated parameters match the hand-written type byte-for-byte.

    Note: Leave framework CSM message-list behavior (specifically IdentificationInformation params 6/7) in the hand-written modules until Tier 1.3 is reached.

    just codegen
    # OR
    cargo run -p nocturne-codegen --bin codegen
  7. Build the Nocturne Device UI

    main

    The device UI is a React 19 + Vite kiosk app. Use the following commands:

    • just ui-dev: Starts a Vite dev server (expects a daemon on ws://localhost:5000).
    • just ui-build: Creates a static bundle (used by the image build).
    • just ui-lint: Runs linting on the UI code.
    just ui-dev
  8. Release the Nocturne OS image

    main

    Releasing involves generating signed full SWU and delta SWU artifacts. The preferred method is an end-to-end command run from the monorepo root.

    Key Concepts:

    • Build ID: A 14-digit UTC timestamp (YYYYMMDDhhmmss). This ID is embedded in the image and OTA payloads. Set NOCTURNE_BUILD_ID explicitly to reproduce a specific build.
    • Versioning: The version core must match DISTRO_VERSION.
    • Variants: prod and dev releases are both production-signed; the variant determines which built image is published.

    Release Commands:

    1. End-to-end (Preferred): Builds and publishes using nocturne-local (working tree) or nocturne (committed).
    2. Publish-only: Used if artifacts are already built. Requires NOCTURNE_RELEASE_VERSION and NOCTURNE_DELTA_FROM_VERSIONS.
    3. Component Updates: For hot-patching the daemon, UI, or bandaid without a full image flash.
    # Preferred 4.1.0 end-to-end command (run from monorepo root)
    VERSION_CORE=4.1.0
    BUILD_ID=$(date -u +%Y%m%d%H%M%S)
    NOCTURNE_BUILD_ID="$BUILD_ID" \
      just release-image "$VERSION_CORE" /secure/nocturne.pem \
      "${VERSION_CORE}+${BUILD_ID}" prod nocturne-local
    
    # Component (hot) updates
    just package-daemon ../target/aarch64-unknown-linux-gnu/release/nocturned build/ota-components/daemon
    just package-ui ../packages/ui/dist build/ota-components/builtinWebapp
    just package-bandaid ../target/aarch64-unknown-linux-gnu/release/nocturned ../packages/ui/dist build/ota-components/bandaid
    
    # Publish a component
    just publish-component bandaid 4.2.0+20260725192800 build/ota-components/bandaid 4.1.0+20260718120000 stable
  9. Build the Nocturne OS image

    main

    Use just build to compile the Yocto image inside a kas container. The build process downloads upstream layers and crates on the first run. Subsequent builds are faster by reusing build/sstate-cache/ and ccache/.

    Important: Production builds require a valid private key for signing. For local testing without signing, use the NOCTURNE_SWUPDATE_SIGNING_MODE=development-unsigned environment variable.

    Build Outputs: Outputs are located in build/tmp/deploy/images/superbird/:

    • nocturne-prod-image-superbird-flashthing.zip: ext4 read-only rootfs, chromium kiosk.
    • nocturne-dev-image-superbird-flashthing.zip: squashfs-lz4 rootfs, weston desktop + VNC + tools-debug.
    • nocturne-update-{prod,dev}-superbird.swu: Full A/B OTA payloads.
    • nocturne-update-{prod,dev}-delta-superbird.swu: zchunk delta OTA payloads.
    • bandaid.ext4: nocturned + nocturne-ui floor for the bandaid partition.
    # Standard build
    just build
    
    # If using podman instead of docker
    KAS_CONTAINER_ENGINE=podman just build
    
    # Build unsigned artifacts for local testing
    NOCTURNE_SWUPDATE_SIGNING_MODE=development-unsigned just build
  10. Iterate on the daemon or UI during image builds

    main

    If you are making changes to nocturned (the daemon) or nocturne-ui within the monorepo, you can build the image using your local working tree instead of committed code.

    Run just build nocturne-local. This mounts the monorepo root at /monorepo inside the kas container, allowing the build to use your uncommitted changes. The container handles all compilation (Cargo for the daemon, bun-native for the UI).

    Warning: The container's bun install will write node_modules/ into your local tree. Do not run a host-side bun install simultaneously with a local image build.

    just build nocturne-local