Files
uopi/docs/WORK_PLAN.md
T
2026-04-24 15:46:04 +02:00

14 KiB
Raw Blame History

Work Plan — uopi

Phases Overview

Phase Name Focus Milestone
0 Scaffold Repo structure, build toolchain, CI Empty binary serves embedded index page
1 Core Backend Broker, WebSocket protocol, REST skeleton Live signal fan-out working end-to-end
2 EPICS Data Source CA connect, monitor, metadata, write Real EPICS PVs visible in browser console
3 Synthetic Data Source DSP engine, Lua sandbox, signal composition Synthetic signals computed and streamed
4 Frontend Foundation Svelte skeleton, WS client, signal stores, view mode shell Subscribed values rendered in browser
5 View Mode Widgets All widget types rendered in view mode Full interactive HMI panel
6 Edit Mode Konva canvas, drag-and-drop, resize, properties pane Can build and save a panel
7 Edit Mode — Advanced Multi-select, align/distribute, undo/redo Complete editor UX
8 Historical Data Archive integration, time navigation UI Replay past data in widgets
9 Signal Discovery Signal tree, CSV import, EPICS Channel Finder Usable without manual PV entry
10 Hardening Docs, packaging, integration tests, performance Production-ready binary

Phase 0 — Scaffold

Goal: working build pipeline, empty binary that serves a placeholder page.

  • Initialise Go module (go mod init github.com/uopi/uopi)
  • Create directory layout: cmd/uopi/, internal/, web/
  • Svelte + Vite + TypeScript project in web/
  • //go:embed web/dist in backend via web/embed.go; Makefile builds frontend then backend
  • HTTP server on configurable port; serves embedded frontend
  • TOML config loading (BurntSushi/toml) with UOPI_* env var overrides
  • GitHub Actions CI: runs go test ./... and npm run check on push
  • README.md with quick-start instructions

Done when: ./dist/uopi starts and serves an "under construction" page.

Notes:

  • Embed lives in web/embed.go (not cmd/uopi/) because //go:embed paths cannot use ..
  • CI runs commands directly (not via make test) but is functionally equivalent

Phase 1 — Core Backend

Goal: signal broker and WebSocket protocol working with a stub data source.

  • Define DataSource interface (internal/datasource/iface.go)
  • Implement in-memory stub data source: sine_1hz, sine_01hz, ramp_10s, toggle_1hz, noise, writable setpoint — all at 10 Hz
  • Implement Broker (internal/broker/broker.go):
    • Subscribe / unsubscribe with reference counting
    • Fan-out goroutine per signal
    • Clean teardown when last subscriber leaves
  • WebSocket handler (internal/server/ws.go):
    • subscribe / unsubscribe / write / history messages
    • Pushes update and meta messages to client
    • Graceful close on disconnect (all subscriptions cancelled)
  • REST API skeleton (internal/api/api.go):
    • GET /api/v1/datasources — lists registered sources
    • GET /api/v1/signals?ds= — lists signals for a source
    • Interface endpoints return 501 stubs (storage in Phase 6)
  • Unit tests: fan-out, teardown on last-unsub, unknown DS/signal, multi-signal (5 tests, all passing)

Done when: a wscat client can subscribe to the stub source and receive timed updates.

Notes:

  • WebSocket signal refs use {"ds": "stub", "name": "sine_1hz"} objects (not "stub:sine_1hz" strings) to avoid ambiguity with EPICS PV names containing colons
  • broker.ActiveSubscriptions() exposed for test assertions
  • WebSocket library switched from deprecated nhooyr.io/websocket to maintained github.com/coder/websocket (identical API)

Phase 2 — EPICS Data Source

Goal: real EPICS PVs readable and writable through the broker.

  • CGo wrapper for EPICS libca (internal/datasource/epics/):
    • Context init with ca_enable_preemptive_callback (no polling loop)
    • ca_create_channel with connection callback shim
    • ca_add_event monitor with value callback (DBR_TIME_* types)
    • ca_put for writes (float64, int64, string, bool)
    • ca_get for DBR_CTRL metadata (units, limits, enum strings)
  • Map CA DBR types to internal DataType enum; CA severity → Quality
  • Implement DataSource interface for EPICS (epics.go)
  • Config: ca_addr_list (overrides EPICS_CA_ADDR_LIST); archive URL
  • Handle disconnected channels gracefully (quality = Bad via connection callback)
  • Integration test with SoftIOC (gated on EPICS_BASE + TEST_PV env vars)
  • noop.go (//go:build !epics) — package compiles without EPICS; epics.Available() returns false
  • archive.go — Archive Appliance JSON HTTP client for History()
  • make backend-epics and make test-epics targets added to Makefile
  • CI comment explains EPICS build requirements; no EPICS step in CI runners

Done when: a PV subscription round-trips from IOC → broker → WebSocket client with correct timestamp and units. (verified by design; integration test requires live IOC)

Notes:

  • Uses //go:build epics on all CGo files; clean build without the tag: go build ./... passes
  • Handle table (Go map, mutex-protected) maps C-side uintptr_t back to Go channels; callbacks are minimal (no alloc)
  • epics.Available() checked in main.go before registration so EPICS-less builds silently skip

Phase 3 — Synthetic Data Source

Goal: users can define computed signals from existing signals.

  • Define synthetic signal definition schema (JSON)
  • Implement DAG evaluator: nodes re-evaluated on upstream change
  • Built-in node types (internal/dsp/):
    • Arithmetic: gain, offset, add, subtract, multiply, divide
    • Statistics: moving average (by count, by time), RMS
    • Filters: IIR lowpass/highpass/bandpass (via gonum or biquad)
    • Calculus: finite-difference derivative, cumulative integral
    • FFT / inverse FFT (gonum/dft)
    • Threshold / clamp
    • Custom expression (simple formula parser)
  • Lua node: sandboxed gopher-lua state per signal; inputs as globals
  • Load synthetic definitions from synthetic.json at startup
  • REST endpoint to CRUD synthetic signal definitions (persists to JSON file)
  • Unit tests for each DSP node type

Done when: a synthetic moving-average of an EPICS PV is visible in the WebSocket stream.


Phase 4 — Frontend Foundation

Goal: Svelte app connects to WebSocket and reactively displays values.

  • App shell (App.svelte): mode state (view | edit), WS connect on init, disconnect banner, --dpr CSS variable
  • WebSocket client (ws.ts): singleton WsClient, exponential back-off reconnect (500 ms→30 s), re-subscribe all signals on reconnect, status Svelte readable store
  • Reference-counted subscriptions in WsClient: one WS message per unique signal regardless of number of callers; unsubscribe sent when ref-count hits zero
  • Signal store factory (stores.ts): getSignalStore(ref) and getMetaStore(ref) — lazily created Svelte readable stores, WS subscription started on first use
  • types.ts: SignalRef, SignalValue, SignalMeta, Widget, Interface
  • xml.ts: parseInterface(xml)DOMParser-based XML→Interface converter
  • View mode shell (ViewMode.svelte): collapsible InterfaceList pane (260 px) + Canvas, toolbar with WS status chip
  • InterfaceList.svelte: fetches /api/v1/interfaces, Import XML file picker (calls onLoad prop), "New" stub button, collapse toggle
  • Canvas.svelte: absolute-position widget rendering, --dpr CSS variable, placeholder for null interface
  • TextView.svelte: live {label}: {value} {unit} with quality dot (green/amber/red/grey), positioned absolutely
  • EditMode.svelte: stub ("Edit mode — coming in Phase 6")
  • vite.config.ts: /ws WebSocket proxy added for dev server
  • npm run build and npm run check: 0 errors, 0 warnings (95 files checked)

Done when: loading a manually crafted XML interface displays live PV values.

Notes:

  • Uses Svelte 5 runes ($state, $props, $derived) throughout, matching existing code style
  • Routing is a simple mode state variable in App.svelte (no SvelteKit, no router library)
  • Unknown widget types in Canvas render as grey dashed placeholder boxes (forward-compatible)

Phase 5 — View Mode Widgets

Goal: all widget types rendered and interactive in view mode.

  • Text view widget
  • Gauge widget (SVG arc, configurable range/thresholds)
  • Vertical / horizontal bar widget
  • LED widget (condition evaluator, configurable colours)
  • Multi-LED widget (bitset, per-bit labels)
  • Set-value widget (input + Set button; sends write over WS)
  • Button widget (sends fixed value on click)
  • Plot widget:
    • Time-series (uPlot, streaming buffer of N points)
    • FFT, waterfall, histogram, bar chart, logic analyser (ECharts)
    • Multi-signal support; legend
  • Text label (static)
  • Image widget (base64 or server URL)
  • Link widget (navigates to another interface)
  • Right-click context menu: signal info dialog, copy name, export CSV
  • Svelte component tests for each widget type (mock store)

Done when: a complete HMI panel with multiple widget types runs smoothly at 60 fps.


Phase 6 — Edit Mode (Core)

Goal: users can build and save an interface from scratch.

  • Konva stage setup in edit mode; devicePixelRatio scaling
  • Widget renderer adapter: same widget components rendered on Konva layer
  • Signal tree pane: fetch signal list, tree display, filter/search
  • Drag signal from tree → drop on canvas → widget type picker
  • Place widget at drop coordinates with default size
  • Single selection: bounding box + resize handles via Konva Transformer
  • Move widget by dragging body
  • Delete widget (× button or Del key)
  • Properties pane: common options (label, position, size)
  • Per-widget property editors (range, colour, plot type, etc.)
  • Save interface to server (POST/PUT XML)
  • Load interface from server (GET XML, populate canvas)
  • Export / import local XML file

Done when: an engineer can create a panel with 5+ widgets, save it, reload it, and see live data.


Phase 7 — Edit Mode (Advanced)

Goal: complete editing UX matching the functional spec.

  • Multi-select: Ctrl+click toggle; rubber-band area select
  • Group move: drag any selected widget to move all
  • Group delete: Del key on selection
  • Align toolbar: left / center-H / right / top / center-V / bottom
  • Distribute toolbar: evenly by center / by gap (H and V)
  • Undo / redo: command pattern, Ctrl+Z / Ctrl+Shift+Z
  • Snap-to-grid (optional, toggle in toolbar)
  • Add text label tool
  • Add image tool (upload to server or embed base64)
  • Add link tool
  • Collapsible signal tree pane and properties pane (toggle buttons)
  • Right-click on interface in list: Edit / Clone / Delete

Done when: the editor feels complete and the undo stack works reliably.


Phase 8 — Historical Data

Goal: users can navigate to past timestamps using archive data.

  • EPICS Archive Appliance HTTP API client (internal/datasource/epics/archive.go)
  • Broker History() dispatch: route to correct data source's History() impl
  • WebSocket history request / response handling (implemented in Phase 1)
  • Frontend: time range picker in view mode toolbar
  • "Live" button: flushes history state, re-subscribes to live updates
  • Plot widget: switch between streaming and historical range mode
  • Point-value widgets: show value at selected timestamp

Done when: a plot can display 24 hours of archived data with a time slider.


Phase 9 — Signal Discovery

Goal: users can find signals without knowing their names in advance.

  • EPICS: attempt Channel Finder or cainfo enumeration; fall back to manual entry
  • Signal tree: lazy-load children on expand
  • Manual add custom PV (EPICS): text input in signal tree
  • New synthetic signal wizard: name, inputs, node graph UI
  • CSV import: parse NAME, DataSource, DS_PARAMETERS; add to tree

Done when: a new user can find and subscribe to a PV without prior knowledge.


Phase 10 — Hardening

Goal: production-quality binary, docs, performance validation.

  • End-to-end integration tests (real SoftIOC, headless browser via Playwright)
  • Benchmark: 20 clients × 100 signals; verify < 5 ms fan-out latency
  • Benchmark: frontend at 10 Hz update rate; verify 60 fps
  • Static binary validation: run on RHEL 7 (Docker image centos:7)
  • Security: Lua sandbox audit; XML XXE protection; write-permission guard
  • Graceful shutdown: drain WS connections, flush pending writes
  • Structured logging (log/slog) — done from Phase 0
  • /metrics endpoint (Prometheus format) for server monitoring
  • Complete README.md and operator docs
  • Release: goreleaser config for Linux amd64 + arm64 binaries

Done when: binary passes integration tests on CentOS 7 container with a SoftIOC.


Estimated Effort

These are rough single-developer estimates. Parallel work across backend and frontend where possible will reduce calendar time.

Phase Effort Status
0 12 days Complete
1 35 days Complete
2 12 weeks Complete
3 12 weeks
4 35 days Complete
5 23 weeks
6 23 weeks
7 12 weeks
8 1 week
9 1 week
10 12 weeks
Total ~1420 weeks