Skip to content

Latest commit

 

History

History
773 lines (626 loc) · 33 KB

File metadata and controls

773 lines (626 loc) · 33 KB

Installation

This guide starts with a blank controller and Raspberry Pi and ends with a frame that returns after a reboot. Follow the sections in order. The first panel test uses an included plate, so you can prove the hardware before setting up Codex or bird observations.

Inky Bird Frame runtime architecture showing observation and generation services; the controller; the Raspberry Pi display node and an optional trusted browser application reading the approved catalog over a private network; and the Inky Impression panel

Every application connection is client-initiated. The display node connects directly to the controller. A browser connects directly only from a trusted HTTP origin; otherwise, its application proxy or TLS terminator connects to the controller. The controller's configured port (8793 in the supplied example configuration) must remain on a trusted network and must not be forwarded from the public internet.

Choose the two computers

The controller and display node have different jobs. A capable Linux Raspberry Pi can run both, but the recommended framed build puts a small Pi behind the display and runs the controller elsewhere. The controller can also run on an existing Docker host; follow the Docker controller guide instead of either native controller section.

Role Recommended Supported Notes
Controller Existing Apple silicon Mac or Ubuntu Server 24.04 LTS computer macOS with launchd; 64-bit Ubuntu 24.04 with systemd; 64-bit Raspberry Pi OS Bookworm or later with systemd A Raspberry Pi 4 with 4GB is the smallest recommended dedicated controller. Docker installation is documented separately.
Display Raspberry Pi Zero 2 W with pre-soldered 40-pin header and Raspberry Pi OS Lite 64-bit Raspberry Pi OS Bookworm or later on a 40-pin Raspberry Pi Supports the 7.3-inch PIM773 at 800x480 and 13.3-inch PIM774 at 1600x1200.

The setup command detects launchd or systemd, but detection does not mean every operating system has been tested. Other systems may work. Contributions that document and test them are welcome.

Pimoroni lists PIM773 and PIM774 as compatible with every 40-pin Raspberry Pi, including Zero variants. A Zero without a header requires soldering. The Zero 2 W has a 64-bit processor and built-in 2.4 GHz Wi-Fi. See the Inky Impression product page and Raspberry Pi Zero 2 W specifications. The application reads the panel geometry from Pimoroni's EEPROM; no display model or resolution setting belongs in config.toml.

The supported topology remains one active display node per controller. Multiple nodes can read the same catalog, but the controller's fetch and success heartbeats describe the display role as a whole rather than identifying each physical panel independently.

Before you begin

You need:

  • the framed-display parts in the hardware guide;
  • one controller from the support table;
  • a computer with a microSD reader;
  • a GitHub connection to clone this public repository;
  • a ChatGPT plan that includes Codex, or an OpenAI API key with separate API billing;
  • coordinates, or a supported postal-code lookup, for location-based discovery; and
  • administrative access on both computers.

The controller requires Python 3.11 or newer, git, rsync, uv, and Codex CLI. The display requires git, rsync, and Pimoroni's Python environment.

Network requirements

From To Purpose
Controller Internet HTTPS (TCP 443) Codex, internet-backed observation services and geocoder, licensed references, and configured research sources
Controller BirdNET-Go HTTP(S) endpoint Read-only summaries from an explicitly configured self-hosted station
Display node Configured controller TCP port (8793 in the supplied example) Health and catalog reads, image downloads, and narrow display-health reports
Trusted browser application Same-origin proxy, HTTPS endpoint, or controller HTTP port for an HTTP application Optional read-only catalog and image downloads; an exact allowed origin is required when the request is cross-origin
Application proxy or TLS terminator Configured controller TCP port (8793 in the supplied example) Server-side catalog and image requests; keep this hop on the trusted network
Setup computer Display node SSH (TCP 22) Installation, updates, and troubleshooting

The two computers do not have to share a subnet. They must be routable to each other, and Wi-Fi client isolation must not block the display from reaching the controller. Give the controller a stable DNS name or DHCP reservation because the display stores its URL. The display itself may use ordinary DHCP because it initiates every application connection.

Do not expose the configured controller port to the public internet. The built-in server is an unauthenticated LAN service. It serves the active catalog and accepts only narrow display-health reports; it does not provide user authentication. Use a VPN or an authenticated TLS reverse proxy if traffic must cross an untrusted network.

The example configuration sets controller.bind_host = "0.0.0.0", which listens on every interface of the controller host. On a multi-homed controller, set bind_host to the specific LAN address the display uses so the service is not offered on other networks.

Browser applications

Inky does not include a standalone browser application. This section is for an operator-supplied application that consumes Inky's read-only catalog API or frames its narrow featured-plate embed. Both permissions are disabled by default. To let a trusted web application fetch the active catalog and its images directly, list its exact origin under [controller]:

[controller]
cors_allowed_origins = ["https://frame.example.test"]

An origin contains only the scheme, host, and optional port. Inky rejects wildcards, credentials, paths, queries, and fragments. Use an ASCII hostname or the canonical form of an IP address. Internationalized and punycode hostnames are not supported. A matching origin can read GET /v1/catalog and the active catalog's portrait and display PNGs under GET /v1/assets/. Other catalog files, inactive plates, health, the featured-plate embed, and display telemetry do not receive cross-origin access. Requests from other origins receive the normal response without an Access-Control-Allow-Origin header.

Framing is a separate permission. List each trusted parent origin explicitly:

[controller]
embed_allowed_origins = ["https://home.example.test"]

The controller adds those origins to the embed's Content Security Policy frame-ancestors directive. CSP does not reliably match IP-address sources, so framing origins must use DNS hostnames. Underscores and trailing dots are also rejected. Unlisted sites cannot frame the embed. Keeping CORS and framing separate means an existing catalog reader does not silently gain permission to present controller content inside its pages.

The browser catalog includes active species identity, approval time, image paths and checksums, and any available observation count and latest-detection time. It does not include provider names, raw observations, locations, credentials, or private provider state. Browser reads never refresh the physical display's health signals. The server also verifies an active image's catalog checksum before it sends the file.

This setting does not add authentication, TLS, or public-internet safety. Keep the controller private. Prefer a same-origin application proxy, which needs no CORS permission and can authenticate the browser before making a server-side request to Inky. Direct public-HTTPS-to-local-network behavior varies by browser: the browser may block mixed content, ask for local-network permission, or allow only specific local address forms. A VPN does not make those browser rules consistent. Terminate HTTPS for the controller or use the same-origin proxy when you need a portable path.

Direct cross-origin access through cookie, session, client-certificate, or HTTP Authorization proxies is not compatible with this interface. Inky does not permit credentialed CORS requests and does not implement CORS or Private Network Access preflight. Standard catalog GET requests use no custom request headers. The service logs each request locally with the source IP address, request path and query, and HTTP status. It does not log request headers or send browser analytics to an external service. Normal systemd journal or macOS log retention applies.

Homepage dashboard

Homepage can show Inky with its stock service widgets; no Homepage fork, custom JavaScript, or Inky-specific plugin is required. Its customapi widget fetches through the Homepage server, while its iframe widget loads in the browser.

For a compact status card, add one customapi widget:

- Birds:
    - Inky Bird Frame:
        icon: mdi-bird
        href: https://github.com/veteranbv/inky-bird-frame
        description: Observation-driven field-journal plates
        siteMonitor: http://inky-controller:8793/v1/catalog
        widget:
          type: customapi
          url: http://inky-controller:8793/health
          refreshInterval: 300000
          mappings:
            - field: active_species
              label: Active
              format: number
            - field: approved_species
              label: Plates
              format: number
            - field: version
              label: Version
              format: text

For the featured-plate card, use Homepage's multiple-widget form and add the iframe after the same status widget:

- Birds:
    - Inky Bird Frame:
        icon: mdi-bird
        href: https://github.com/veteranbv/inky-bird-frame
        description: Observation-driven field-journal plates
        siteMonitor: http://inky-controller:8793/v1/catalog
        widgets:
          - type: customapi
            url: http://inky-controller:8793/health
            refreshInterval: 300000
            mappings:
              - field: active_species
                label: Active
                format: number
              - field: approved_species
                label: Plates
                format: number
              - field: version
                label: Version
                format: text
          - type: iframe
            src: https://inky.example.test/v1/embed/featured-plate
            classes: h-60
            allowScrolling: no
            referrerPolicy: no-referrer

Replace the names and URLs with addresses reachable from the Homepage server and browser. siteMonitor uses /v1/catalog, not /health, because the catalog route fails closed when active catalog state is unavailable. Inky answers Homepage's HEAD readiness probe without transferring the catalog. The stats request is server-side and needs no CORS permission. Polling every five minutes avoids Homepage's much noisier default while keeping the counts useful.

The optional image needs a browser-reachable HTTPS route for the controller. A true same-origin proxy beneath the Homepage origin works with the default frame-ancestors 'self' policy. For a separate controller origin, as shown above, add the Homepage origin to embed_allowed_origins. Make sure a reverse proxy forwards both /v1/embed/featured-plate and /v1/assets/, and does not replace the controller's CSP with a conflicting frame-denial header. The embed uses a relative asset URL so those routes can share a direct origin or the same proxy path prefix. Keep the route private; use authentication or a VPN before traffic crosses an untrusted boundary. Allowing the origin does not secure the controller. The iframe refreshes every 15 minutes and displays the upright portrait, never the rotated hardware image. The embed refreshes itself, so do not add Homepage's refreshInterval option. Homepage 2.1.2 accumulates iframe timers when that option is enabled. That release also drops its documented iframe name setting before rendering, so the example omits an ineffective setting. This is an upstream accessibility limitation: Homepage 2.1.2 renders the stock iframe without an accessible frame name. The embedded document itself still has a descriptive title, and its image has alternative text.

Whenever any active entry has a valid detection time, the featured plate comes from that set: newest detection wins, with approval time and taxon ID breaking ties. Only when no active entry has a detection time does newest approval become the fallback. BirdWeather, BirdNET-Go, and Bird Buddy are currently the only sources that contribute those times, so this is not a universal latest-observation view and does not claim to match the plate currently on the frame. The embed exposes only the selected approved bird identity and image. A valid empty catalog shows a quiet empty state; invalid or unavailable catalog state returns 503.

1. Install the controller

Choose the macOS or Linux path below. Keep the source checkout. Native installations use an explicit GitHub release tag; future updates fetch the selected tag and run the same setup command again. Replace vX.Y.Z in the examples with the exact release you intend to install.

macOS controller

Install prerequisites with Homebrew:

brew install git rsync uv
brew install --cask codex

OpenAI also publishes npm packages and direct release binaries. See the official Codex CLI installation options.

Clone and prepare the project:

git clone https://github.com/veteranbv/inky-bird-frame.git
cd inky-bird-frame
INKY_VERSION=vX.Y.Z
git checkout --detach "$INKY_VERSION"
uv sync --extra controller --locked
mkdir -p "$HOME/Library/Application Support/Inky Bird Frame"
cp config.example.toml \
  "$HOME/Library/Application Support/Inky Bird Frame/config.toml"
chmod 600 "$HOME/Library/Application Support/Inky Bird Frame/config.toml"

Edit the private file:

open -t "$HOME/Library/Application Support/Inky Bird Frame/config.toml"

At minimum, set:

  • one discovery location from zip_code, postal_code plus country_code, or latitude plus longitude when iNaturalist or eBird is enabled;
  • controller.codex_path to the output of command -v codex;
  • optionally, controller.codex_model to a model you have validated for source-backed profile research, image generation, and review (omit it to use Codex's configured default, and run one foreground generation after changing the pin);
  • display_node.controller_url to a name or address the Pi can reach, such as http://bird-controller.local:8793; and
  • any paths that should live outside the configuration directory.

Sign in with the account that will run the LaunchAgents:

codex login
codex login status

OpenAI recommends ChatGPT sign-in to use Codex through a Plus, Pro, Business, Edu, or Enterprise plan. API-key login is supported but billed separately. See Codex authentication.

Validate, preview, install, and diagnose:

CONFIG="$HOME/Library/Application Support/Inky Bird Frame/config.toml"
uv run inky-bird-frame config validate --config "$CONFIG"
uv run inky-bird-frame setup controller --config "$CONFIG"
uv run inky-bird-frame setup controller --config "$CONFIG" --yes
uv run inky-bird-frame doctor controller --config "$CONFIG"

The preview lists changes and performs no installation. --yes copies a managed runtime to ~/Services/inky-bird-frame, installs its locked controller environment, creates LaunchAgents, loads them, and verifies them with launchctl.

LaunchAgents begin when this macOS user logs in. They survive application updates and restart after logout/login or reboot. For unattended recovery, the Mac must also power on after an outage and reach that user session. Newer supported desktop Macs expose System Settings > Energy > Start up when power is connected; Apple documents the hardware and OS requirements in Turn on a Mac without pressing its power button.

Ubuntu or Raspberry Pi OS controller

The commands below are for Debian-family systemd hosts. Start with a 64-bit Ubuntu Server 24.04 LTS or Raspberry Pi OS Bookworm-or-later installation.

sudo apt update
sudo apt install -y git rsync npm pipx
pipx ensurepath
source "$HOME/.profile"
pipx install uv
mkdir -p "$HOME/.local"
npm config set prefix "$HOME/.local"
npm install -g @openai/codex
export PATH="$HOME/.local/bin:$PATH"

Add export PATH="$HOME/.local/bin:$PATH" to the account's shell profile if it is not already present. The installer stores the absolute Codex path in service configuration, so later service starts do not depend on the interactive PATH.

Clone and prepare the project:

git clone https://github.com/veteranbv/inky-bird-frame.git
cd inky-bird-frame
INKY_VERSION=vX.Y.Z
git checkout --detach "$INKY_VERSION"
uv sync --extra controller --locked
mkdir -p "$HOME/.config/inky-bird-frame"
cp config.example.toml "$HOME/.config/inky-bird-frame/config.toml"
chmod 600 "$HOME/.config/inky-bird-frame/config.toml"
nano "$HOME/.config/inky-bird-frame/config.toml"

Set the same minimum fields listed in the macOS section. On a headless controller, use OpenAI's device-code flow:

codex login --device-auth
codex login status

Device-code login must be permitted in the ChatGPT account or workspace. The official headless login guide documents that requirement and fallback methods.

Validate, preview, install, and diagnose:

CONFIG="$HOME/.config/inky-bird-frame/config.toml"
uv run inky-bird-frame config validate --config "$CONFIG"
uv run inky-bird-frame setup controller --config "$CONFIG"
uv run inky-bird-frame setup controller --config "$CONFIG" --yes
uv run inky-bird-frame doctor controller --config "$CONFIG"

The installer creates a persistent inky-bird-frame-controller.service plus refresh, generation, and optional notification/publication timers. It enables them for boot and verifies their state through systemd. Setup performs one successful refresh before installing the units, then each timer schedules its first run relative to activation and continues at the configured interval. Installer progress and any sudo prompt remain visible. See the official systemd.timer manual.

Docker controller

Use the Docker controller guide on an AMD64 or ARM64 Docker host. The normal path pulls the published GHCR image; it does not build from source. After the controller health check passes, continue at Prepare the display Pi.

Verify controller data

Run a read-only discovery, then refresh the active catalog:

IBF="$HOME/Services/inky-bird-frame/.venv/bin/inky-bird-frame"
"$IBF" discover --config "$CONFIG"
"$IBF" refresh --config "$CONFIG"
curl --fail --silent "http://127.0.0.1:8793/health"

The health response must have "ok": true. version identifies the running application. approved_species is the reusable catalog size. active_species is the approved subset currently observed in the configured window and radius or retained in the private collection. It may initially be zero in a region whose birds have not been generated yet. The approved count comes from the last built catalog index; the active count comes from current active-catalog state. The endpoint does not rebuild or scan the catalog, so it stays fast as the catalog grows. Run "$IBF" --version to identify the command-line installation directly.

An existing controller can explicitly retain every currently approved plate after upgrading by previewing and applying collection import-approved; see the operations guide.

2. Prepare the display Pi

Flash Raspberry Pi OS

Use Raspberry Pi Imager to write Raspberry Pi OS Lite (64-bit) to the microSD card. In Imager's OS customization:

  1. choose a unique hostname;
  2. create a non-default username and password;
  3. configure the wall location's Wi-Fi country, SSID, and password;
  4. enable SSH with public-key authentication when possible; and
  5. set the time zone.

SSH is disabled by default unless it is enabled during imaging or later on the Pi. Raspberry Pi documents these settings in Remote access and recommends Raspberry Pi OS for most Pi use in Operating systems.

Boot the Pi, find its hostname or DHCP lease, and connect:

ssh YOUR_USER@YOUR_PI_HOSTNAME.local

Confirm the network before attaching application behavior:

hostnamectl
ip address show
ip route

Attach PIM773 or PIM774

Shut down and disconnect power before handling the boards:

sudo poweroff

Raspberry Pi Zero 2 W mounted to the rear of PIM774

Conceptual assembly sequence, not a pinout or dimensional drawing. Follow the Pimoroni hardware documentation supplied with the panel.

Hold the Inky board by its edges. Align the Pi's complete 40-pin header with the Inky connector, press it straight into place, and secure the Pi with the included standoffs. Do not press on the glass panel. Pimoroni ships both supported displays assembled with the required mounting hardware and documents that no soldering is needed when the Pi already has a 40-pin header.

Reconnect power and SSH back in.

Install the Pimoroni environment

Use Pimoroni's supported installer rather than modifying the system Python:

sudo apt update
sudo apt install -y git rsync
git clone https://github.com/pimoroni/inky.git "$HOME/inky"
cd "$HOME/inky"
./install.sh

The installer creates ~/.virtualenvs/pimoroni and configures the required Pi interfaces. Pimoroni recommends Raspberry Pi OS Bookworm or later and documents the manual SPI, I2C, and dtoverlay=spi0-0cs requirements in the Inky library installation guide. Reboot if the installer asks.

Prove the panel without Codex

Clone this project and install it into the same Pimoroni environment:

git clone https://github.com/veteranbv/inky-bird-frame.git
cd inky-bird-frame
INKY_VERSION=vX.Y.Z
git checkout --detach "$INKY_VERSION"
"$HOME/.virtualenvs/pimoroni/bin/python" -m pip install -e '.[inky]'
"$HOME/.virtualenvs/pimoroni/bin/inky-bird-frame" display-image \
  catalog/species/12942-eastern-bluebird/display.png

The included Eastern Bluebird should appear in portrait orientation. PIM774 uses the canonical image unchanged. PIM773 automatically contains the complete plate on its wider 800x480 aspect ratio with paper-colored margins, which appear above and below the plate when the panel is portrait-mounted. It does not crop or stretch the illustration or notes. A refresh normally takes tens of seconds and may take longer when cold. Do not continue until this test succeeds; it isolates the Pi, Python environment, 40-pin connection, and display from every controller dependency.

3. Connect the display to the controller

Create a private display configuration. Do not copy the controller's private file because it may contain credentials and a private discovery location.

cd "$HOME/inky-bird-frame"
mkdir -p "$HOME/.config/inky-bird-frame"
cp config.example.toml "$HOME/.config/inky-bird-frame/config.toml"
chmod 600 "$HOME/.config/inky-bird-frame/config.toml"
nano "$HOME/.config/inky-bird-frame/config.toml"

Set display_node.controller_url to the stable controller URL and choose display_node.rotation_mode. Set schedule.rotation_minutes to the desired cadence. The display process uses only [display_node] and display schedule values; leave other example values non-sensitive and disabled.

From the Pi, verify the exact controller URL before installation:

curl --fail --silent "http://YOUR_CONTROLLER:8793/health"

Then validate, preview, install, and diagnose:

CONFIG="$HOME/.config/inky-bird-frame/config.toml"
INKY="$HOME/.virtualenvs/pimoroni/bin/inky-bird-frame"
"$INKY" config validate --config "$CONFIG"
"$INKY" setup display --config "$CONFIG" --source-dir "$PWD" \
  --venv "$HOME/.virtualenvs/pimoroni"
"$INKY" setup display --config "$CONFIG" --source-dir "$PWD" \
  --venv "$HOME/.virtualenvs/pimoroni" --yes
"$INKY" doctor display --config "$CONFIG"

Setup installs and enables a systemd timer but does not force an immediate panel update. The timer takes over after display_startup_delay_seconds. Start the first live rotation explicitly when the controller is ready:

"$INKY" display-cycle --config "$CONFIG" --force

If the controller reports active_species: 0, run a generation cycle on the controller first:

# Run these on the controller with its private configuration.
"$HOME/Services/inky-bird-frame/.venv/bin/inky-bird-frame" generate \
  --config /path/to/controller/config.toml
"$HOME/Services/inky-bird-frame/.venv/bin/inky-bird-frame" refresh \
  --config /path/to/controller/config.toml

Use the controller's configuration path for those two controller commands, not the Pi's display configuration.

4. Verify automatic recovery

Do not call installation complete until both doctor commands report "ready": true.

On a systemd controller:

systemctl is-enabled inky-bird-frame-controller.service
systemctl is-active inky-bird-frame-controller.service
systemctl list-timers 'inky-bird-frame-*'

On the display:

systemctl is-enabled inky-bird-frame-display.timer
systemctl is-active inky-bird-frame-display.timer
systemctl list-timers inky-bird-frame-display.timer

On macOS:

launchctl print "gui/$(id -u)/com.inky-bird-frame.serve"
launchctl print "gui/$(id -u)/com.inky-bird-frame.refresh"
launchctl print "gui/$(id -u)/com.inky-bird-frame.generate"

The Pi and Linux controller services return automatically after a reboot. The macOS LaunchAgents return when their owning user logs in. The e-paper panel keeps its last image without power, so a controller or network outage does not blank the frame.

Configuration map

Section Used by Purpose
[discovery] Controller Providers, credentials, location, radius, observation window, and species cap
[controller] Controller Persistent paths, Codex executable, HTTP bind, and generation bounds
[research] Controller Bounded fallback research policy and approved domains
[notifications] Controller Optional Apprise destinations, events, retries, and noise controls
[public_catalog] Controller Optional owner-only publication of reusable approved plates
[display_node] Display Controller URL, local state path, and selection policy
[schedule] Both Refresh, generation, publication, notification, and rotation intervals

config.example.toml contains comments, supported values, and recommended defaults. Configuration is TOML everywhere. Keep real files outside the Git checkout, mode 0600, and out of backups or support bundles that are shared publicly.

Update

Read the release notes and take a quiesced backup using the backup and restore guide. Choose the exact target release, update each source checkout to that tag, review the changes, and rerun the same setup command. Setup is idempotent and replaces managed application code and native service definitions without replacing the private configuration. Replace vX.Y.Z below with that target tag.

INKY_VERSION=vX.Y.Z
git fetch --tags --prune
git checkout --detach "$INKY_VERSION"
uv sync --extra controller --locked       # controller checkout
uv run inky-bird-frame setup controller --config "$CONFIG" --yes
uv run inky-bird-frame doctor controller --config "$CONFIG"

On the display:

INKY_VERSION=vX.Y.Z
git fetch --tags --prune
git checkout --detach "$INKY_VERSION"
"$INKY" setup display --config "$CONFIG" --source-dir "$PWD" \
  --venv "$HOME/.virtualenvs/pimoroni" --yes
"$INKY" doctor display --config "$CONFIG"

--source-dir "$PWD" is required in the display update path because setup points the Pimoroni environment at the managed runtime. The explicit source directory ensures each update installs the checkout that was just selected rather than the previous managed copy.

When the controller and display are updated separately, follow the release notes for that upgrade. Ordinarily, update and validate the controller first. When the release notes require a display-first update for a new catalog schema, update display nodes before the controller. The active catalog wire format is schema_version: 1, and a display node refuses a catalog with a newer schema version: the cycle fails closed and the panel keeps its last image until the display is updated. Private collection membership does not change this wire schema.

Current display nodes report a parsed catalog fetch through POST /v1/display-fetch and a verified panel update through POST /v1/display-success. The reports contain only {"schema_version": 1}. If an older controller rejects either POST, the display ignores the telemetry failure and continues rotating plates. Controller and display releases before v0.7.0 used state-changing GET telemetry that is no longer accepted. A direct split upgrade from an older release cannot preserve heartbeat reporting: the old role uses GET while the new role uses POST. Update the controller first, then update the display promptly, and expect a temporary telemetry gap. If uninterrupted heartbeat monitoring is required, update both roles to v0.7.0 first, verify POST telemetry, and then continue to the newer release.

Docker controller updates pull a published image and recreate the services. See the Docker update instructions for version pinning and rollback.

Uninstall

From the source checkout, run the uninstall script that matches the role and platform. Each script removes the installed services and timers but preserves private configuration, controller state, catalogs, and generated art:

./deploy/uninstall-controller.sh           # macOS controller
./deploy/uninstall-controller-systemd.sh   # Linux controller
./deploy/uninstall-display-local.sh        # display node

The equivalent manual removal follows. On a systemd controller:

for unit in \
  inky-bird-frame-controller.service \
  inky-bird-frame-refresh.timer \
  inky-bird-frame-generate.timer \
  inky-bird-frame-catalog-publish.timer \
  inky-bird-frame-notifications.timer; do
  sudo systemctl disable --now "$unit" 2>/dev/null || true
done
sudo rm -f /etc/systemd/system/inky-bird-frame-{controller,refresh,generate,catalog-publish,notifications}.{service,timer}
sudo systemctl daemon-reload

Missing optional units are harmless. On the display:

sudo systemctl disable --now inky-bird-frame-display.timer
sudo rm -f /etc/systemd/system/inky-bird-frame-display.{service,timer}
sudo systemctl daemon-reload

On macOS, unload each installed label and remove its matching plist from ~/Library/LaunchAgents:

for label in serve refresh generate catalog-publish notifications; do
  launchctl bootout "gui/$(id -u)/com.inky-bird-frame.$label" 2>/dev/null || true
  rm -f "$HOME/Library/LaunchAgents/com.inky-bird-frame.$label.plist"
done

After service removal, delete ~/Services/inky-bird-frame only if its managed runtime is no longer needed. Delete the private support/configuration directory only after saving any catalog or state you want to keep.

Troubleshooting

Start with the role's doctor command. Its JSON output gives every check a stable ID, status, summary, and remediation. Continue with the focused runbook in troubleshooting.md.

Authoritative references