Skip to content

Latest commit

 

History

History
98 lines (71 loc) · 6.29 KB

File metadata and controls

98 lines (71 loc) · 6.29 KB

Cyc — Architecture

A small, single-user Electron desktop app that turns Philips Hue bulbs into a theatrical cue board. This document describes what the code actually does. For the original aspirational design (which is not what got built), see git history.

Process model

Standard Electron split:

  • Main process (src/main/) — Node.js. Owns the SQLite database, the Hue bridge client, electron-store persistence, and IPC handlers. No UI.
  • Renderer process (src/renderer/) — Chromium. Vanilla HTML/CSS/JS. Talks to main only via the preload bridge.
  • Preload (src/shared/preload.js) — runs in an isolated context with access to both Node and the renderer's window. Uses contextBridge to expose a narrow window.electronAPI surface to the renderer. Renderer never gets direct Node access.
┌─────────────────────────────────────┐
│ Renderer (Chromium)                 │
│   index.html · styles.css · app.js  │
│                  ↕ window.electronAPI
├─────────────────────────────────────┤  preload.js (contextBridge)
│                  ↕ ipcRenderer.invoke
├─────────────────────────────────────┤
│ Main (Node)                         │
│   index.js                          │
│     ↳ Database (sqlite3)            │
│     ↳ HueService (node-hue-api)     │
│     ↳ Session store (electron-store)│
│     ↳ Bridge config (electron-store)│
└─────────────────────────────────────┘
            ↕ HTTPS / mDNS / N-UPnP
       ┌─────────────┐
       │ Hue Bridge  │
       └─────────────┘
            ↕ Zigbee
        💡 💡 💡 💡

Data model (SQLite)

Three tables, all in %APPDATA%/cyc-app/cyc.db:

shows         (id, name, created_date, modified_date)
cues          (id, show_id, cue_number REAL, name, fade_time REAL)
cue_lights    (id, cue_id, light_id, is_on, brightness, color)
  • cue_number is a REAL so reordering can use midpoint floats (drop between 2 and 3 → 2.5) without renumbering everyone else.
  • cue_lights.is_on distinguishes "this cue turns light X off" from "this cue ignores light X." Lights NOT in cue_lights for a given cue are left untouched when the cue fires — the bridge keeps their prior state.
  • Schema migrations run on every launch via runMigrations(), idempotent (swallows "duplicate column" errors).

Two electron-store JSON files alongside it:

  • cyc-hue.json{ bridgeIp, appKey } from the pairing handshake
  • cyc-session.json{ showId, cueIndex, savedAt } written after every GO/BACK/Jump for crash recovery

Hue integration

src/main/hue-service.js wraps node-hue-api. Key choices:

  • Standard CLIP API, not Entertainment API. The bridge accepts a transitiontime (in deciseconds) on a single setLightState call and interpolates the fade locally — no DTLS, no UDP streaming, no Entertainment Area, no per-area light limit. Adequate for theatrical cueing; saves enormous implementation complexity.
  • Discovery uses both N-UPnP (Philips' cloud service) and mDNS (local), de-duped by IP. Either path alone is sometimes flaky; together they're reliable.
  • Pairing uses createInsecureLocal().connect() then users.createUser() — requires the physical link button to have been pressed within ~30s.
  • Per-cue fire maps cue_lights rows to a parallel array of setLightState calls with the cue's fade time as transitiontime. Failures per-light are caught individually so one unreachable bulb doesn't break the cue.
  • Heartbeat — renderer pings getConfiguration() every 10s when healthy, every 5s when disconnected. On failure, the main process clears its client reference and the renderer shows the disconnect banner.
  • Color: the editor uses standard <input type="color"> (#rrggbb hex). HueService converts hex→Hue's RGB internally (which the library then converts to xy), and Hue's HSB→hex for "Capture from bridge."

Renderer state

A single mutable state object in app.js. No framework, no reactive store. Render functions read from state and rewrite the relevant DOM subtree (renderCueList, renderEditorLights, updateConnectionBadge, etc.).

Cue editor is a working copy: state.editorLights is a Map<lightId, EditorRow> populated when the modal opens. Edits mutate the map directly; Save persists via updateCueLights. Cancel discards by closing the modal without writing.

SortableJS is initialized fresh on each renderCueList call (prior instance destroyed first) since innerHTML = '' blows away its DOM hooks.

IPC surface

All exposed via window.electronAPI in the renderer:

Namespace Methods
(root) getShows, createShow, getCues, createCue, updateCue, deleteCue, getCueLights, getCueLightCounts, updateCueLights, exportShow, importShow
hue status, ping, reconnect, discover, pair, unpair, getLights, getGroups, applyCue, blackout
session get, save, clear

Every handler is ipcMain.handle (promise-based). Errors propagate to the renderer where they're caught and shown via showError().

Build / packaging

electron-builder produces an NSIS installer (Cyc Setup x.y.z.exe) under dist/. Per-user install, no admin needed. sqlite3 is rebuilt against Electron's V8 ABI via the postinstall script (electron-builder install-app-deps).

No code signing. No auto-updater (manual reinstall to update). For personal use, fine.

What this architecture deliberately is NOT

  • Not a SaaS. No backend, no accounts, no cloud. Everything is local.
  • Not a microservice. One process, one DB file, one bridge connection.
  • Not React/Redux/MUI. Vanilla DOM. The cue list and editor are simple enough that a framework would add more code than it removes.
  • Not multi-user. One operator, one machine, one show at a time.
  • Not protocol-agnostic. Hue-only. Adding DMX or other protocols would mean abstracting HueService behind an interface — a half-day's work, not a redesign.