Martino Ferrari 9d9e538a90 fix(logic): subscribe signals referenced by array action node params
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>
2026-06-24 13:50:26 +02:00
2026-06-24 01:39:15 +02:00
2026-06-24 01:39:15 +02:00
2026-04-25 22:56:09 +02:00
2026-06-23 17:59:40 +02:00
2026-06-23 17:59:40 +02:00
2026-04-30 23:01:01 +02:00
2026-06-24 01:39:15 +02:00
2026-06-21 23:50:03 +02:00
2026-04-24 15:09:14 +02:00
2026-04-25 22:56:09 +02:00

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 the epics build 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

S
Description
uOPI: light, fast, web based OPI
Readme 15 MiB
Languages
Go 47.8%
TypeScript 43.8%
CSS 6.9%
C 0.5%
Shell 0.4%
Other 0.6%