285 lines
14 KiB
Markdown
285 lines
14 KiB
Markdown
# 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.
|
||
|
||
- [x] Initialise Go module (`go mod init github.com/uopi/uopi`)
|
||
- [x] Create directory layout: `cmd/uopi/`, `internal/`, `web/`
|
||
- [x] Svelte + Vite + TypeScript project in `web/`
|
||
- [x] `//go:embed web/dist` in backend via `web/embed.go`; `Makefile` builds frontend then backend
|
||
- [x] HTTP server on configurable port; serves embedded frontend
|
||
- [x] TOML config loading (`BurntSushi/toml`) with `UOPI_*` env var overrides
|
||
- [x] GitHub Actions CI: runs `go test ./...` and `npm run check` on push
|
||
- [x] `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.
|
||
|
||
- [x] Define `DataSource` interface (`internal/datasource/iface.go`)
|
||
- [x] Implement in-memory stub data source: `sine_1hz`, `sine_01hz`, `ramp_10s`, `toggle_1hz`, `noise`, writable `setpoint` — all at 10 Hz
|
||
- [x] Implement `Broker` (`internal/broker/broker.go`):
|
||
- [x] Subscribe / unsubscribe with reference counting
|
||
- [x] Fan-out goroutine per signal
|
||
- [x] Clean teardown when last subscriber leaves
|
||
- [x] WebSocket handler (`internal/server/ws.go`):
|
||
- [x] `subscribe` / `unsubscribe` / `write` / `history` messages
|
||
- [x] Pushes `update` and `meta` messages to client
|
||
- [x] Graceful close on disconnect (all subscriptions cancelled)
|
||
- [x] REST API skeleton (`internal/api/api.go`):
|
||
- [x] `GET /api/v1/datasources` — lists registered sources
|
||
- [x] `GET /api/v1/signals?ds=` — lists signals for a source
|
||
- [x] Interface endpoints return 501 stubs (storage in Phase 6)
|
||
- [x] 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.
|
||
|
||
- [x] CGo wrapper for EPICS `libca` (`internal/datasource/epics/`):
|
||
- [x] Context init with `ca_enable_preemptive_callback` (no polling loop)
|
||
- [x] `ca_create_channel` with connection callback shim
|
||
- [x] `ca_add_event` monitor with value callback (`DBR_TIME_*` types)
|
||
- [x] `ca_put` for writes (float64, int64, string, bool)
|
||
- [x] `ca_get` for DBR_CTRL metadata (units, limits, enum strings)
|
||
- [x] Map CA DBR types to internal `DataType` enum; CA severity → Quality
|
||
- [x] Implement `DataSource` interface for EPICS (`epics.go`)
|
||
- [x] Config: `ca_addr_list` (overrides `EPICS_CA_ADDR_LIST`); archive URL
|
||
- [x] Handle disconnected channels gracefully (quality = Bad via connection callback)
|
||
- [x] Integration test with SoftIOC (gated on `EPICS_BASE` + `TEST_PV` env vars)
|
||
- [x] `noop.go` (`//go:build !epics`) — package compiles without EPICS; `epics.Available()` returns false
|
||
- [x] `archive.go` — Archive Appliance JSON HTTP client for `History()`
|
||
- [x] `make backend-epics` and `make test-epics` targets added to Makefile
|
||
- [x] 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.
|
||
|
||
- [x] App shell (`App.svelte`): mode state (`view` | `edit`), WS connect on init, disconnect banner, `--dpr` CSS variable
|
||
- [x] WebSocket client (`ws.ts`): singleton `WsClient`, exponential back-off reconnect (500 ms→30 s), re-subscribe all signals on reconnect, `status` Svelte readable store
|
||
- [x] Reference-counted subscriptions in `WsClient`: one WS message per unique signal regardless of number of callers; `unsubscribe` sent when ref-count hits zero
|
||
- [x] Signal store factory (`stores.ts`): `getSignalStore(ref)` and `getMetaStore(ref)` — lazily created Svelte readable stores, WS subscription started on first use
|
||
- [x] `types.ts`: `SignalRef`, `SignalValue`, `SignalMeta`, `Widget`, `Interface`
|
||
- [x] `xml.ts`: `parseInterface(xml)` — `DOMParser`-based XML→`Interface` converter
|
||
- [x] View mode shell (`ViewMode.svelte`): collapsible `InterfaceList` pane (260 px) + `Canvas`, toolbar with WS status chip
|
||
- [x] `InterfaceList.svelte`: fetches `/api/v1/interfaces`, Import XML file picker (calls `onLoad` prop), "New" stub button, collapse toggle
|
||
- [x] `Canvas.svelte`: absolute-position widget rendering, `--dpr` CSS variable, placeholder for null interface
|
||
- [x] `TextView.svelte`: live `{label}: {value} {unit}` with quality dot (green/amber/red/grey), positioned absolutely
|
||
- [x] `EditMode.svelte`: stub ("Edit mode — coming in Phase 6")
|
||
- [x] `vite.config.ts`: `/ws` WebSocket proxy added for dev server
|
||
- [x] `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
|
||
- [x] 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
|
||
- [x] 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 | 1–2 days | ✅ Complete |
|
||
| 1 | 3–5 days | ✅ Complete |
|
||
| 2 | 1–2 weeks | ✅ Complete |
|
||
| 3 | 1–2 weeks | — |
|
||
| 4 | 3–5 days | ✅ Complete |
|
||
| 5 | 2–3 weeks | — |
|
||
| 6 | 2–3 weeks | — |
|
||
| 7 | 1–2 weeks | — |
|
||
| 8 | 1 week | — |
|
||
| 9 | 1 week | — |
|
||
| 10 | 1–2 weeks | — |
|
||
| **Total** | **~14–20 weeks** | |
|