subscribeRefs() walked node kinds to build the signal subscription set but omitted the new action.array.push/set/remove nodes. Signal or local-var references in their expr/index params were never subscribed, so the live cache held no value and they resolved to NaN at run time (action.accumulate was handled, its modern equivalent action.array.push was not). Add the missing cases, including the comma-split index list for array.set. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
uopi
A web-based HMI (Human-Machine Interface) for monitoring and controlling EPICS-based scientific and industrial control systems.
uopi runs as a single portable binary with no runtime dependencies. Point any browser at it — including over an SSH tunnel — and you get a full drag-and-drop HMI builder and live data viewer.
Features
| Feature | Details |
|---|---|
| Live data | EPICS Channel Access (CA) / PVAccess and user-defined synthetic signals |
| HMI editor | Drag-and-drop widgets, resize, align/distribute, undo/redo, saved as XML |
| Widget library | Text view/label, gauge, LED, multi-LED, bar, set-point control, button, image, link, plots |
| Plot types | Time-series (uPlot), FFT, waterfall, histogram, bar chart, logic analyser |
| Plot panels | Dedicated chart panels with a recursive split layout (tmux/IDE-style) where plots fill the viewport |
| Panel logic | In-editor node-graph flow editor (triggers → actions, dialogs) that runs client-side in view mode |
| Control logic | Server-side always-on flow graphs with cron/alarm triggers and Lua blocks |
| Configuration manager | Versioned configuration sets (typed signal schemas, grouped) and instances (values) with validation, array/CSV editing, apply-to-signals, and JSON import/export |
| Version history & diff | Git-style history for panels, synthetic signals, control logic and config sets/instances: view, fork, promote any revision, with unified / side-by-side diff |
| Local variables | Panel-scoped state variables for set-points, toggles and counters used by logic |
| Historical data | EPICS Archive Appliance integration via REST; time range picker in UI |
| Synthetic signals | Compose, filter, and transform signals via a wizard or visual node-graph editor (gain, offset, moving average, lowpass, highpass, derivative, integral, clamp, formula); panel/user/global visibility scopes |
| Access control | Identity via trusted proxy header; per-user global level (write/read-only/no-access); per-panel ownership + sharing; nested folders; optional logic-editor allowlist |
| Multi-client | Subscriptions are multiplexed — N clients share one upstream connection per signal |
| Signal discovery | Filter/search, CSV import, manual add, Channel Finder proxy |
| Observability | Prometheus-format metrics at /metrics |
| Single binary | Frontend assets embedded at build time — no web server needed |
Quick Start
Prerequisites
- Go 1.22+ — the only build dependency (the frontend is bundled by a pure-Go esbuild wrapper)
- EPICS Base (
libca) — only required for theepicsbuild tag; the stub and synthetic data sources work without it
Build
# Full build — bundles the frontend then compiles the binary
make all
The binary is written to dist/uopi.
Run
./dist/uopi # uses default config (stub data source only)
./dist/uopi --config uopi.toml # explicit config file
Then open http://localhost:8080 in your browser.
SSH tunnel example:
ssh -L 8080:localhost:8080 user@controlhost
# then open http://localhost:8080 locally
Configuration
Create uopi.toml (all fields are optional — shown values are defaults):
[server]
listen = ":8080"
storage_dir = "./interfaces" # where interface XML, ACL and logic data are stored
# Identity & access control (all optional)
trusted_user_header = "" # HTTP header carrying the proxy-authenticated user
default_user = "" # identity used when the header is absent (LAN/dev)
# logic_editors = [] # restrict who may edit panel/control logic (users or groups)
# [[server.blacklist]] # downgrade a user: level = "readonly" or "noaccess"
# user = "guest"
# level = "readonly"
# [[groups]] # named user sets, referenced by panel sharing
# name = "operators"
# members = ["alice", "bob"]
[datasource.epics]
enabled = false # requires build with -tags epics
ca_addr_list = "" # overrides EPICS_CA_ADDR_LIST if non-empty
archive_url = "" # EPICS Archive Appliance base URL, e.g. http://archiver:17665
channel_finder_url = "" # EPICS Channel Finder base URL (optional)
[datasource.synthetic]
enabled = true
All settings can be overridden with environment variables using the UOPI_ prefix and double underscores for nesting:
UOPI_SERVER_LISTEN=":9090" UOPI_DATASOURCE_EPICS_ENABLED=true ./dist/uopi
Building with EPICS Support
The EPICS data source uses CGo bindings to libca. First set up the EPICS environment variables, then:
export EPICS_BASE=/path/to/epics/base
export CGO_CFLAGS="-I$EPICS_BASE/include -I$EPICS_BASE/include/os/Linux"
export CGO_LDFLAGS="-L$EPICS_BASE/lib/linux-x86_64 -lca -lCom"
make backend-epics
Then enable it in uopi.toml:
[datasource.epics]
enabled = true
API
The REST API is available under /api/v1. Key endpoints:
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/datasources |
List registered data sources |
GET |
/api/v1/signals?ds=<name> |
List signals for a data source |
GET |
/api/v1/signals/search?q=<query> |
Search signals across all sources |
GET |
/api/v1/channel-finder?q=<query> |
Proxy to EPICS Channel Finder |
GET/POST |
/api/v1/interfaces |
List or create HMI interfaces |
GET/PUT/DELETE |
/api/v1/interfaces/{id} |
Read, update, or delete an interface |
POST |
/api/v1/interfaces/{id}/clone |
Duplicate an interface |
POST |
/api/v1/interfaces/reorder |
Reorder panels / move them between folders |
GET/PUT |
/api/v1/interfaces/{id}/acl |
Read or set a panel's sharing rules |
GET/POST |
/api/v1/folders |
List or create panel folders |
PUT/DELETE |
/api/v1/folders/{id} |
Rename/reparent or delete a folder |
GET/POST/DELETE |
/api/v1/synthetic |
Manage synthetic signal definitions |
GET/POST |
/api/v1/controllogic |
List or create server-side control-logic graphs |
GET/PUT/DELETE |
/api/v1/controllogic/{id} |
Read, update, or delete a control-logic graph |
GET/POST |
/api/v1/config/sets |
List or create configuration sets (schemas) |
GET/PUT/DELETE |
/api/v1/config/sets/{id} |
Read, update, or delete a config set |
GET/POST |
/api/v1/config/instances |
List or create configuration instances (values) |
GET/PUT/DELETE |
/api/v1/config/instances/{id} |
Read, update, or delete a config instance |
POST |
/api/v1/config/instances/{id}/apply |
Write an instance's values to their target signals |
GET |
/api/v1/config/{sets|instances}/diff |
Structural diff between two revisions |
GET |
/api/v1/{interfaces|synthetic|controllogic|config/sets|config/instances}/{id}/versions |
List revisions; …/{version} fetches one |
POST |
…/{id}/versions/{v}/promote |
Promote a revision to current |
POST |
…/{id}/versions/{v}/fork |
Fork a revision into a new document |
GET |
/api/v1/me |
Caller identity, access level, groups, logic-edit permission |
GET |
/api/v1/usergroups |
List configured users and groups (for sharing) |
GET |
/metrics |
Prometheus-format server metrics |
GET |
/healthz |
Health check |
WebSocket live data is at ws://<host>/ws. See docs/TECHNICAL_SPEC.md for the message protocol.
Development
# Build frontend only (output → web/dist/)
make frontend
# Build backend only (frontend must already be built)
make backend
# Run the server directly without a binary (uses stub data source)
go run ./cmd/uopi
# Run all tests
make test
# Run tests with race detector
make race
# Run benchmarks
make bench
# Static analysis
make lint
# Clean build artefacts
make clean
Observability
The /metrics endpoint exposes Prometheus-format counters and gauges:
uopi_uptime_seconds — seconds since start
uopi_ws_connections — current open WebSocket connections
uopi_signal_subscriptions — current unique upstream signal subscriptions
uopi_ws_messages_in_total — total messages received from clients
uopi_ws_messages_out_total — total messages sent to clients
uopi_write_ops_total — total signal write operations
uopi_history_requests_total — total historical data requests
Scrape with any Prometheus-compatible tool or view directly in the browser.
Docs
| Document | Description |
|---|---|
docs/FUNCTIONAL_SPEC.md |
User-facing feature specification |
docs/TECHNICAL_SPEC.md |
Architecture, WebSocket protocol, API design |
docs/WORK_PLAN.md |
Phased development plan with milestones |
Release
Releases are built with goreleaser:
goreleaser release --clean
See .goreleaser.yaml. Note that release binaries are built without EPICS support (CGO_ENABLED=0) for maximum portability. Build from source with -tags epics for CA connectivity.
License
TBD