Skip to the build guide
ElsewhereMachines

01 / 09 · THE PARTS

Make a sonar panel.

Press a real button. Send a ping. Let the story tell you what’s beyond the windows.

Build it with Otherwhere OS. We’ll go from loose parts to a complete scenario, with the configuration beside the thing it controls.

  • 01 Raspberry Pi with a 40-pin header, power supply, and storage
  • 02 Two momentary buttons: action and push-to-talk
  • 03 An LED, a 1 kΩ resistor, breadboard, and jumper wires
  • 04 A supported microphone and powered speaker

This guide uses the reference board, orion.console@3. The wiring is shared by both adventures. Changing the physical layout means defining your own board.

Just here for the software? Start with the local exercise Wire it

02 / 09 · WIRE IT

One connection at a time.

Disconnect the Pi’s power before wiring. Choose a part to see its connection and the matching board configuration.

components:
  primary_button:
    type: orion.momentary_button@1
    config:
      pin: 16
      active_low: true
      debounce_ms: 40
board.yaml · components excerpt

GPIO signals are 3.3 V. Keep 5 V away from these connections, and keep the resistor in series with the LED. Check your Pi’s header orientation ↗

Speaker, microphone, and power

Use a supported powered speaker or amplifier and microphone. The board expects ALSA devices named speaker and microphone; these aliases must match your Pi’s audio setup. The GPIO header does not drive the speaker directly.

Use the supported Pi image and power supply for your build. Device and audio setup →

Name it

03 / 09 · NAME IT

Give the parts useful names.

The board owns the pins. The scenario asks for a button, a light, a speaker, and a way to talk. That is what lets the same wiring become a different machine.

capabilities:
  primary_action: primary_button
  push_to_talk: talk_button
  status_indicator: alert_light
  main_audio: speaker
  microphone: microphone
board.yaml / scenario.yaml · root excerpts
BCM 16primary_buttonprimary_action

A button has no permanent sound or story role. Adding another supported button usually means adding a component and binding. New hardware primitives may need a driver.

The story

04 / 09 · THE STORY

Leave something out there.

Write who is aboard and how they respond. Start with an unknown contact. The captain gets to investigate; a button press does not decide what they find.

characters:
  orion:
    name: Navigator Orion
    personality: Observant, patient, and quietly amused by unexpected discoveries.
    prompt: You navigate a homemade submarine on an imagined ocean expedition. Help
      its captain listen, investigate, and name what they discover. Begin with
      an unexplained sound beyond the windows. Let the captain decide whether to
      follow it, wait, or ask a question. A whale is one possible encounter, not
      a guaranteed result of a button press.
    voice: orion_voice
    initial_presence: submarine
scenario.yaml · root excerpts

Start from the complete download, then change the character, rules, and starting facts together. Give your version its own scenario identity. Published versions are immutable.

A press

05 / 09 · A PRESS

Make the first press count.

The button turns the lamp on and starts a named ping. While it is playing, ready is false, so another press cannot start a second one.

- id: send_ping
  on: primary_action.pressed
  when:
    - { path: state.ready, op: eq, value: true }
  actions:
    - state.set: { path: ready, value: false }
    - light.set: { target: status_indicator, value: true }
    - sound.play: { asset: sonar_ping, key: ping }
    - timer.start: { timer: ping_timeout, delay_ms: 8000 }
scenario.yaml · device.maps.sonar.routes / root assets / device declarations

The sounds are included in the download: verified prototype cues from the existing inventory. An asset alias chooses audio; it does not generate it.

Use your own sound

Choose or create a WAV, measure its SHA-256, byte count, and PCM duration, and add it to assets/catalog.json. Its file path is relative to that catalog. Point the scenario’s asset alias to that hash, then rerun the local exercise. The helper checks the actual bytes.

A response

06 / 09 · A RESPONSE

Let completion move the story.

When the ping actually finishes, turn the lamp off, reopen the button, and ask the story for an observation. The response uses the current story and the captain’s earlier choices.

- id: hear_contact
  on: sound.completed
  when:
    - { path: event.key, op: eq, value: ping }
  actions:
    - light.set: { target: status_indicator, value: false }
    - timer.cancel: { timer: ping_timeout }
    - state.set: { path: ready, value: true }
    - server.emit: { event: contact_requested }
scenario.yaml · device.maps.sonar.routes / root inputs

The whale is one illustrated possibility. The real director receives an observation request; discoveries become durable facts through accepted story effects.

What if the story is already speaking?

A non-record input can be accepted as busy_recorded during an active turn. It is recorded without queuing a surprise story action for later. Device observations remain diagnostics; only declared story inputs take this path.

Recovery

07 / 09 · RECOVERY

Give it a way back.

If the ping never completes, an eight-second timeout stops the sound and reopens the button. Talking or reconnecting also clears temporary state. A missing completion never becomes a discovery.

- id: ping_timed_out
  on: timer.ping_timeout
  actions:
    - sound.stop: { target: main_audio }
    - light.set: { target: status_indicator, value: false }
    - state.set: { path: ready, value: true }
scenario.yaml · playable-map routes / device.state_sync

Local cues yield to narration and capture. Keep the completion, timeout, reconnect, and talk routes together when you adapt the example.

Try it locally

08 / 09 · TRY IT LOCALLY

Run the actual sequence.

Download the whole build: board, complete scenario, audio, catalog, and interaction trace. Extract the builds folder into your supplied pi-exp checkout.

Download the complete build 903 KB

Use Bun 1.4.0, Python 3.12, and uv 0.12.0. The prototype runtime comes with the supplied checkout; this download contains the build files. Run these commands from the checkout root.

bun --no-env-file tools/install-typescript-dependencies.ts
uv sync --project apps/device-agent --frozen --all-groups
terminal · trace tab shows the opening inputs

The exercise builds the package, verifies the audio, and runs the real reactor with fake hardware. Read each step’s commands, local state, and server intents. Completing a fake sound is explicit; advancing time only drives timers.

Your Pi

09 / 09 · YOUR PI

Put it on your build.

With your Pi enrolled against the matching board, upload the audio, publish the scenario, and build its package. Otherwhere OS downloads and verifies the package before activating it.

This prototype uses the supplied installation and enrollment workflow, plus a configured server and authorized admin CLI. Set up and enroll your Pi → Keep credentials in your local runtime file.

bun --env-file=.env apps/admin-cli/src/main.ts asset upload builds/sonar/assets/audio/sonar_ping.wav --media-type audio/wav --kind PACKAGE_ASSET --json
terminal · connected application commands

Use the actual package UUID returned by the build for PACKAGE_ID, and your enrolled Pi’s UUID for DEVICE_ID. Confirm orion.console@3 is published and matches that device. If you changed the layout or story, publish the new board/scenario identity and use those references instead.

How do I know it is running?

Inspect the package’s exact board, scenario, assets, and hash. After selecting it, inspect the device again and wait for desired and active package IDs to agree. An existing story session can retain its current scenario; preserve it or end it deliberately before switching.

Then try the real button, lamp, speaker, and push-to-talk. The local exercise does not prove physical wiring, microphone capture, server effects, or a heard story response. Package and device reference →

Same parts. Another story.Send a rescue signal