674 lines
19 KiB
Go
674 lines
19 KiB
Go
//go:build epics
|
|
|
|
// Package epics provides an EPICS Channel Access data source for uopi.
|
|
//
|
|
// # Build requirements
|
|
//
|
|
// This file and all other files tagged "epics" require CGo and a working EPICS
|
|
// Base installation. Set the following environment variables before building:
|
|
//
|
|
// 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"
|
|
//
|
|
// Then build with:
|
|
//
|
|
// CGO_ENABLED=1 go build -tags epics ./...
|
|
package epics
|
|
|
|
/*
|
|
#cgo CFLAGS: -Wall -Wno-unused-function
|
|
#include "ca_wrapper.h"
|
|
#include <stdlib.h>
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/uopi/uopi/internal/datasource"
|
|
)
|
|
|
|
// connEntry holds the one-shot channel used during channel creation to wait for
|
|
// the initial connection callback.
|
|
type connEntry struct {
|
|
connCh chan struct{}
|
|
}
|
|
|
|
// handleTable maps Go-side handle IDs to the subscriber's value channel.
|
|
// Protected by handleMu.
|
|
var (
|
|
handleMu sync.Mutex
|
|
handleTable = make(map[uintptr]chan<- datasource.Value)
|
|
connTable = make(map[uintptr]*connEntry)
|
|
)
|
|
|
|
// handleSeq is an atomically-incremented counter used to generate unique handle
|
|
// IDs that are passed through CA as void* user pointers.
|
|
var handleSeq atomic.Uintptr
|
|
|
|
func nextHandle() uintptr {
|
|
return handleSeq.Add(1)
|
|
}
|
|
|
|
// caChannel holds the CA resources for a single PV.
|
|
type caChannel struct {
|
|
chid C.chid
|
|
evid C.evid // monitor event ID; zero if no monitor installed
|
|
handle uintptr
|
|
}
|
|
|
|
// EPICS is the Channel Access data source.
|
|
type EPICS struct {
|
|
caAddrList string
|
|
archiveURL string
|
|
cfURL string
|
|
autoSyncFilter string
|
|
autoSyncFromArchiver bool
|
|
|
|
// caCtx is the CA context created in Connect(). Stored as unsafe.Pointer
|
|
// because the C type (ca_client_context *) is opaque. Every goroutine
|
|
// that calls CA functions must call caAttachContext(caCtx) first, because
|
|
// Go goroutines can run on any OS thread and CA contexts are thread-local.
|
|
caCtx unsafe.Pointer
|
|
|
|
mu sync.Mutex
|
|
channels map[string]*caChannel // PV name → CA channel
|
|
metadata map[string]datasource.Metadata
|
|
}
|
|
|
|
// New creates a new EPICS Channel Access data source.
|
|
//
|
|
// caAddrList is used to set EPICS_CA_ADDR_LIST at runtime (may be empty to
|
|
// rely on the environment). archiveURL is the base URL of an EPICS Archive
|
|
// Appliance instance for history queries (may be empty).
|
|
// pvNames is accepted for API compatibility with the pure-Go build but ignored
|
|
// in the CGo build (the CGo implementation populates ListSignals via callbacks).
|
|
func New(caAddrList, archiveURL, cfURL, autoSyncFilter string, autoSyncFromArchiver bool, pvNames []string) datasource.DataSource {
|
|
return &EPICS{
|
|
caAddrList: caAddrList,
|
|
archiveURL: archiveURL,
|
|
cfURL: cfURL,
|
|
autoSyncFilter: autoSyncFilter,
|
|
autoSyncFromArchiver: autoSyncFromArchiver,
|
|
channels: make(map[string]*caChannel),
|
|
metadata: make(map[string]datasource.Metadata),
|
|
}
|
|
}
|
|
|
|
// Available reports whether the EPICS data source is compiled in.
|
|
// Always returns true when built with the "epics" build tag.
|
|
func Available() bool { return true }
|
|
|
|
// Name implements datasource.DataSource.
|
|
func (e *EPICS) Name() string { return "epics" }
|
|
|
|
// Connect initialises a CA context with preemptive callbacks so that monitor
|
|
// callbacks are delivered on CA's background threads without needing a
|
|
// ca_pend_event polling loop.
|
|
func (e *EPICS) Connect(_ context.Context) error {
|
|
// Apply CA address list overrides before creating the context so the
|
|
// library picks them up during initialisation.
|
|
// os.Setenv is used instead of C.putenv: putenv takes ownership of the
|
|
// string pointer, which would race with Go's garbage collector.
|
|
if e.caAddrList != "" {
|
|
os.Setenv("EPICS_CA_ADDR_LIST", e.caAddrList)
|
|
os.Setenv("EPICS_CA_AUTO_ADDR_LIST", "NO")
|
|
}
|
|
|
|
status := C.ca_context_create(C.ca_enable_preemptive_callback)
|
|
if status != C.ECA_NORMAL {
|
|
return fmt.Errorf("epics: ca_context_create failed: status %d", int(status))
|
|
}
|
|
// Save the context so goroutines on other OS threads can attach to it.
|
|
e.caCtx = C.caCurrentContext()
|
|
|
|
// Perform background sync from Channel Finder if configured.
|
|
if e.cfURL != "" && e.autoSyncFilter != "" {
|
|
go e.syncFromChannelFinder(context.Background())
|
|
}
|
|
|
|
// Perform background sync from Archive Appliance if configured.
|
|
if e.archiveURL != "" && e.autoSyncFromArchiver {
|
|
go e.syncFromArchiver(context.Background())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EPICS) syncFromChannelFinder(ctx context.Context) {
|
|
time.Sleep(500 * time.Millisecond)
|
|
slog.Info("epics: syncing signals from Channel Finder", "url", e.cfURL, "filter", e.autoSyncFilter)
|
|
channels, err := queryChannelFinder(e.cfURL, e.autoSyncFilter)
|
|
if err != nil {
|
|
slog.Error("epics: Channel Finder sync failed", "err", err)
|
|
return
|
|
}
|
|
|
|
for _, ch := range channels {
|
|
// Convert CFS properties and tags
|
|
props := make(map[string]string)
|
|
for _, p := range ch.Properties {
|
|
props[p.Name] = p.Value
|
|
}
|
|
tags := make([]string, len(ch.Tags))
|
|
for i, t := range ch.Tags {
|
|
tags[i] = t.Name
|
|
}
|
|
|
|
e.mu.Lock()
|
|
if _, exists := e.metadata[ch.Name]; !exists {
|
|
e.metadata[ch.Name] = datasource.Metadata{
|
|
Name: ch.Name,
|
|
Properties: props,
|
|
Tags: tags,
|
|
}
|
|
}
|
|
e.mu.Unlock()
|
|
|
|
// Background pre-fetch of actual record info
|
|
go e.prefetchPV(ctx, ch.Name)
|
|
}
|
|
slog.Info("epics: synced signals from Channel Finder", "count", len(channels))
|
|
}
|
|
|
|
func (e *EPICS) prefetchPV(ctx context.Context, pv string) {
|
|
mctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
_, _ = e.GetMetadata(mctx, pv)
|
|
}
|
|
|
|
// attachCAContext attaches the saved CA context to the calling OS thread.
|
|
// Must be called at the top of every goroutine that uses CA functions,
|
|
// because Go goroutines can be scheduled onto any OS thread.
|
|
// Safe to call redundantly (ECA_ISATTACHED is not an error).
|
|
func (e *EPICS) attachCAContext() {
|
|
if e.caCtx != nil {
|
|
C.caAttachContext(e.caCtx)
|
|
}
|
|
}
|
|
|
|
// Subscribe creates a CA channel for signal and returns immediately.
|
|
// Connection and monitor setup happen in a background goroutine so that
|
|
// the caller is never blocked waiting for the IOC to respond.
|
|
// A QualityBad value is sent to ch if the channel cannot connect within
|
|
// 60 seconds. The returned CancelFunc tears down the subscription.
|
|
func (e *EPICS) Subscribe(ctx context.Context, signal string, ch chan<- datasource.Value) (datasource.CancelFunc, error) {
|
|
// Attach the CA context to this OS thread before any CA calls.
|
|
e.attachCAContext()
|
|
|
|
handle := nextHandle()
|
|
|
|
// Register the handle → channel mapping before creating the CA channel so
|
|
// that any early callbacks are not lost.
|
|
handleMu.Lock()
|
|
handleTable[handle] = ch
|
|
entry := &connEntry{connCh: make(chan struct{}, 1)}
|
|
connTable[handle] = entry
|
|
handleMu.Unlock()
|
|
|
|
// Create the CA channel — this call returns immediately; the connection
|
|
// callback fires asynchronously on CA's internal thread.
|
|
pvName := C.CString(signal)
|
|
defer C.free(unsafe.Pointer(pvName))
|
|
|
|
var chid C.chid
|
|
status := C.caCreateChannel(pvName, C.uintptr_t(handle), &chid)
|
|
if status != C.ECA_NORMAL {
|
|
handleMu.Lock()
|
|
delete(handleTable, handle)
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
return nil, fmt.Errorf("epics: ca_create_channel(%q) failed: status %d", signal, int(status))
|
|
}
|
|
C.ca_flush_io()
|
|
|
|
// Track the channel immediately so Write can find the chid.
|
|
e.mu.Lock()
|
|
e.channels[signal] = &caChannel{chid: chid, handle: handle}
|
|
e.mu.Unlock()
|
|
|
|
// Use a done channel to signal the goroutine to stop.
|
|
done := make(chan struct{})
|
|
var closeOnce sync.Once
|
|
stopFn := func() { closeOnce.Do(func() { close(done) }) }
|
|
|
|
// Background goroutine: wait for connection, then install the monitor.
|
|
go func() {
|
|
// Attach CA context to this OS thread before making any CA calls.
|
|
e.attachCAContext()
|
|
|
|
var evid C.evid
|
|
connected := false
|
|
|
|
select {
|
|
case <-entry.connCh:
|
|
connected = true
|
|
case <-time.After(60 * time.Second):
|
|
// IOC unreachable; report bad quality so the widget shows an error state.
|
|
select {
|
|
case ch <- datasource.Value{Timestamp: time.Now(), Data: float64(0), Quality: datasource.QualityBad}:
|
|
default:
|
|
}
|
|
case <-ctx.Done():
|
|
case <-done:
|
|
}
|
|
|
|
if connected {
|
|
// Refresh metadata now that the channel is connected so type, unit,
|
|
// and display limits are accurate (the earlier sendMeta call may have
|
|
// run before the channel was up, leaving an incomplete cache entry).
|
|
meta := e.fetchMetadata(chid, signal)
|
|
e.mu.Lock()
|
|
e.metadata[signal] = meta
|
|
e.mu.Unlock()
|
|
|
|
// Signal to the broker/dispatcher that metadata has been refreshed.
|
|
select {
|
|
case ch <- datasource.Value{MetaUpdate: true}:
|
|
default:
|
|
}
|
|
|
|
dbrType := nativeTimeType(chid)
|
|
st := C.caAddMonitor(chid, C.short(dbrType), C.uintptr_t(handle), &evid)
|
|
if st != C.ECA_NORMAL {
|
|
select {
|
|
case ch <- datasource.Value{Timestamp: time.Now(), Data: float64(0), Quality: datasource.QualityBad}:
|
|
default:
|
|
}
|
|
connected = false
|
|
} else {
|
|
C.ca_flush_io()
|
|
e.mu.Lock()
|
|
e.channels[signal] = &caChannel{chid: chid, evid: evid, handle: handle}
|
|
e.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// Wait until the subscription is cancelled.
|
|
select {
|
|
case <-done:
|
|
case <-ctx.Done():
|
|
}
|
|
|
|
// Cleanup.
|
|
if connected && evid != nil {
|
|
C.ca_clear_event(evid)
|
|
}
|
|
C.ca_clear_channel(chid)
|
|
C.ca_flush_io()
|
|
|
|
handleMu.Lock()
|
|
delete(handleTable, handle)
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
|
|
e.mu.Lock()
|
|
delete(e.channels, signal)
|
|
e.mu.Unlock()
|
|
}()
|
|
|
|
return datasource.CancelFunc(stopFn), nil
|
|
}
|
|
|
|
// GetMetadata performs a synchronous ca_get for DBR_CTRL_DOUBLE (or the
|
|
// appropriate control type) to retrieve units, display limits, enum strings,
|
|
// and writability information for the named signal.
|
|
func (e *EPICS) GetMetadata(ctx context.Context, signal string) (datasource.Metadata, error) {
|
|
// Attach the CA context to this OS thread before any CA calls.
|
|
e.attachCAContext()
|
|
|
|
// Check cache first.
|
|
e.mu.Lock()
|
|
if m, ok := e.metadata[signal]; ok {
|
|
e.mu.Unlock()
|
|
return m, nil
|
|
}
|
|
e.mu.Unlock()
|
|
|
|
// We need a connected channel to issue a ca_get. Reuse an existing one or
|
|
// create a temporary channel.
|
|
e.mu.Lock()
|
|
existingCh, exists := e.channels[signal]
|
|
e.mu.Unlock()
|
|
|
|
var chid C.chid
|
|
tempChannel := false
|
|
|
|
if exists {
|
|
chid = existingCh.chid
|
|
} else {
|
|
// Create a temporary channel for metadata retrieval.
|
|
handle := nextHandle()
|
|
entry := &connEntry{connCh: make(chan struct{}, 1)}
|
|
handleMu.Lock()
|
|
connTable[handle] = entry
|
|
handleMu.Unlock()
|
|
|
|
pvName := C.CString(signal)
|
|
defer C.free(unsafe.Pointer(pvName))
|
|
|
|
status := C.caCreateChannel(pvName, C.uintptr_t(handle), &chid)
|
|
if status != C.ECA_NORMAL {
|
|
handleMu.Lock()
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
return datasource.Metadata{}, fmt.Errorf("epics: ca_create_channel(%q) failed: status %d", signal, int(status))
|
|
}
|
|
C.ca_flush_io()
|
|
|
|
select {
|
|
case <-entry.connCh:
|
|
case <-time.After(5 * time.Second):
|
|
C.ca_clear_channel(chid)
|
|
handleMu.Lock()
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
return datasource.Metadata{}, fmt.Errorf("epics: timeout connecting to %q for metadata", signal)
|
|
case <-ctx.Done():
|
|
C.ca_clear_channel(chid)
|
|
handleMu.Lock()
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
return datasource.Metadata{}, ctx.Err()
|
|
}
|
|
|
|
handleMu.Lock()
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
tempChannel = true
|
|
}
|
|
|
|
meta := e.fetchMetadata(chid, signal)
|
|
|
|
if tempChannel {
|
|
C.ca_clear_channel(chid)
|
|
C.ca_flush_io()
|
|
}
|
|
|
|
e.mu.Lock()
|
|
e.metadata[signal] = meta
|
|
e.mu.Unlock()
|
|
|
|
return meta, nil
|
|
}
|
|
|
|
// fetchMetadata retrieves metadata from a connected chid using ca_get.
|
|
func (e *EPICS) fetchMetadata(chid C.chid, name string) datasource.Metadata {
|
|
// ca_write_access returns 1 if CA security permits puts from this client.
|
|
// This reflects the IOC's actual security policy (host/user ACLs), so we
|
|
// use it directly rather than defaulting to true and relying on put failures.
|
|
meta := datasource.Metadata{Name: name, Writable: C.ca_write_access(chid) != 0}
|
|
|
|
fieldType := C.ca_field_type(chid)
|
|
count := C.ca_element_count(chid)
|
|
|
|
// Determine the Go DataType and the DBR_CTRL type to request.
|
|
var ctrlType C.chtype
|
|
switch int(fieldType) {
|
|
case int(C.DBF_DOUBLE), int(C.DBF_FLOAT):
|
|
if count > 1 {
|
|
meta.Type = datasource.TypeFloat64Array
|
|
} else {
|
|
meta.Type = datasource.TypeFloat64
|
|
}
|
|
ctrlType = C.DBR_CTRL_DOUBLE
|
|
|
|
case int(C.DBF_LONG), int(C.DBF_SHORT), int(C.DBF_CHAR):
|
|
meta.Type = datasource.TypeInt64
|
|
ctrlType = C.DBR_CTRL_LONG
|
|
|
|
case int(C.DBF_ENUM):
|
|
meta.Type = datasource.TypeEnum
|
|
ctrlType = C.DBR_CTRL_ENUM
|
|
|
|
case int(C.DBF_STRING):
|
|
meta.Type = datasource.TypeString
|
|
ctrlType = C.DBR_CTRL_STRING
|
|
|
|
default:
|
|
meta.Type = datasource.TypeFloat64
|
|
ctrlType = C.DBR_CTRL_DOUBLE
|
|
}
|
|
|
|
// Perform a synchronous ca_get with a 3-second timeout.
|
|
switch ctrlType {
|
|
case C.DBR_CTRL_DOUBLE:
|
|
var buf C.struct_dbr_ctrl_double
|
|
status := C.caGet(C.DBR_CTRL_DOUBLE, chid, unsafe.Pointer(&buf))
|
|
if status == C.ECA_NORMAL {
|
|
C.ca_pend_io(3.0)
|
|
meta.Unit = C.GoString((*C.char)(unsafe.Pointer(&buf.units[0])))
|
|
meta.DisplayLow = float64(buf.lower_disp_limit)
|
|
meta.DisplayHigh = float64(buf.upper_disp_limit)
|
|
meta.DriveLow = float64(buf.lower_ctrl_limit)
|
|
meta.DriveHigh = float64(buf.upper_ctrl_limit)
|
|
}
|
|
|
|
case C.DBR_CTRL_LONG:
|
|
var buf C.struct_dbr_ctrl_long
|
|
status := C.caGet(C.DBR_CTRL_LONG, chid, unsafe.Pointer(&buf))
|
|
if status == C.ECA_NORMAL {
|
|
C.ca_pend_io(3.0)
|
|
meta.Unit = C.GoString((*C.char)(unsafe.Pointer(&buf.units[0])))
|
|
meta.DisplayLow = float64(buf.lower_disp_limit)
|
|
meta.DisplayHigh = float64(buf.upper_disp_limit)
|
|
meta.DriveLow = float64(buf.lower_ctrl_limit)
|
|
meta.DriveHigh = float64(buf.upper_ctrl_limit)
|
|
}
|
|
|
|
case C.DBR_CTRL_ENUM:
|
|
var buf C.struct_dbr_ctrl_enum
|
|
status := C.caGet(C.DBR_CTRL_ENUM, chid, unsafe.Pointer(&buf))
|
|
if status == C.ECA_NORMAL {
|
|
C.ca_pend_io(3.0)
|
|
n := int(buf.no_str)
|
|
strs := make([]string, n)
|
|
for i := 0; i < n; i++ {
|
|
strs[i] = C.GoString((*C.char)(unsafe.Pointer(&buf.strs[i][0])))
|
|
}
|
|
meta.EnumStrings = strs
|
|
}
|
|
}
|
|
|
|
return meta
|
|
}
|
|
|
|
// ListSignals returns metadata for all currently tracked channels.
|
|
func (e *EPICS) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
slog.Debug("epics: listing signals", "count", len(e.metadata))
|
|
|
|
// Start with all fully-fetched metadata entries.
|
|
out := make([]datasource.Metadata, 0, len(e.channels))
|
|
seen := make(map[string]bool, len(e.metadata))
|
|
for name, m := range e.metadata {
|
|
out = append(out, m)
|
|
seen[name] = true
|
|
}
|
|
// Add stub entries for channels that are tracked but not yet fetched.
|
|
for name := range e.channels {
|
|
if !seen[name] {
|
|
out = append(out, datasource.Metadata{Name: name, Type: datasource.TypeFloat64})
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Write puts a new value onto a CA channel.
|
|
// If the signal is not currently subscribed (e.g. a button in oneshot mode),
|
|
// a temporary CA channel is created, used for the put, then torn down.
|
|
//
|
|
// NOTE: per-session end-user identity (datasource.WithUser) is NOT honoured in
|
|
// the CGo/libca build: libca uses a single process-wide CA context whose client
|
|
// name is fixed at startup. Writes therefore use the server identity here. Use
|
|
// the default pure-Go build for per-user write attribution.
|
|
func (e *EPICS) Write(ctx context.Context, signal string, value any) error {
|
|
e.attachCAContext()
|
|
|
|
// Resolve the chid: use an existing subscribed channel if available,
|
|
// otherwise create a temporary one (mirrors GetMetadata's approach).
|
|
e.mu.Lock()
|
|
caCh, ok := e.channels[signal]
|
|
e.mu.Unlock()
|
|
|
|
var chid C.chid
|
|
tempChannel := false
|
|
|
|
if ok {
|
|
chid = caCh.chid
|
|
} else {
|
|
// Create a temporary channel for the put.
|
|
handle := nextHandle()
|
|
entry := &connEntry{connCh: make(chan struct{}, 1)}
|
|
handleMu.Lock()
|
|
connTable[handle] = entry
|
|
handleMu.Unlock()
|
|
|
|
pvName := C.CString(signal)
|
|
defer C.free(unsafe.Pointer(pvName))
|
|
|
|
status := C.caCreateChannel(pvName, C.uintptr_t(handle), &chid)
|
|
if status != C.ECA_NORMAL {
|
|
handleMu.Lock()
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
return fmt.Errorf("epics: ca_create_channel(%q) failed: status %d", signal, int(status))
|
|
}
|
|
C.ca_flush_io()
|
|
|
|
select {
|
|
case <-entry.connCh:
|
|
case <-time.After(5 * time.Second):
|
|
C.ca_clear_channel(chid)
|
|
handleMu.Lock()
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
return fmt.Errorf("epics: timeout connecting to %q for write", signal)
|
|
case <-ctx.Done():
|
|
C.ca_clear_channel(chid)
|
|
handleMu.Lock()
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
return ctx.Err()
|
|
}
|
|
|
|
handleMu.Lock()
|
|
delete(connTable, handle)
|
|
handleMu.Unlock()
|
|
tempChannel = true
|
|
}
|
|
|
|
var status C.int
|
|
switch v := value.(type) {
|
|
case float64:
|
|
cv := C.double(v)
|
|
status = C.caPut(C.DBR_DOUBLE, chid, unsafe.Pointer(&cv))
|
|
case int64:
|
|
cv := C.long(v)
|
|
status = C.caPut(C.DBR_LONG, chid, unsafe.Pointer(&cv))
|
|
case string:
|
|
cs := C.CString(v)
|
|
defer C.free(unsafe.Pointer(cs))
|
|
status = C.caPut(C.DBR_STRING, chid, unsafe.Pointer(cs))
|
|
case bool:
|
|
var iv C.short
|
|
if v {
|
|
iv = 1
|
|
}
|
|
status = C.caPut(C.DBR_SHORT, chid, unsafe.Pointer(&iv))
|
|
default:
|
|
if tempChannel {
|
|
C.ca_clear_channel(chid)
|
|
C.ca_flush_io()
|
|
}
|
|
return fmt.Errorf("epics: unsupported value type %T for Write", value)
|
|
}
|
|
|
|
if status != C.ECA_NORMAL {
|
|
if tempChannel {
|
|
C.ca_clear_channel(chid)
|
|
C.ca_flush_io()
|
|
}
|
|
return fmt.Errorf("epics: ca_put(%q) failed: status %d", signal, int(status))
|
|
}
|
|
|
|
if tempChannel {
|
|
C.ca_pend_io(3.0) // flush + wait for put completion before tearing down
|
|
C.ca_clear_channel(chid)
|
|
C.ca_flush_io()
|
|
} else {
|
|
C.ca_flush_io()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// History delegates to the Archive Appliance client, or returns
|
|
// ErrHistoryUnavailable if no archive URL is configured.
|
|
func (e *EPICS) History(ctx context.Context, signal string, start, end time.Time, maxPoints int) ([]datasource.Value, error) {
|
|
if e.archiveURL == "" {
|
|
return nil, datasource.ErrHistoryUnavailable
|
|
}
|
|
return fetchArchiveHistory(ctx, e.archiveURL, signal, start, end, maxPoints)
|
|
}
|
|
|
|
// nativeTimeType returns the DBR_TIME_* type corresponding to the native field
|
|
// type of the channel.
|
|
func nativeTimeType(chid C.chid) C.chtype {
|
|
ft := C.ca_field_type(chid)
|
|
count := C.ca_element_count(chid)
|
|
|
|
switch int(ft) {
|
|
case int(C.DBF_DOUBLE):
|
|
if count > 1 {
|
|
return C.DBR_TIME_DOUBLE // waveform; caller assembles []float64 separately
|
|
}
|
|
return C.DBR_TIME_DOUBLE
|
|
case int(C.DBF_FLOAT):
|
|
return C.DBR_TIME_FLOAT
|
|
case int(C.DBF_LONG):
|
|
return C.DBR_TIME_LONG
|
|
case int(C.DBF_SHORT), int(C.DBF_CHAR):
|
|
return C.DBR_TIME_SHORT
|
|
case int(C.DBF_ENUM):
|
|
return C.DBR_TIME_ENUM
|
|
case int(C.DBF_STRING):
|
|
return C.DBR_TIME_STRING
|
|
default:
|
|
return C.DBR_TIME_DOUBLE
|
|
}
|
|
}
|
|
|
|
func (e *EPICS) syncFromArchiver(ctx context.Context) {
|
|
time.Sleep(1 * time.Second) // wait for network
|
|
slog.Info("epics: syncing signals from Archiver", "url", e.archiveURL)
|
|
pvs, err := searchArchivePVs(ctx, e.archiveURL, "*")
|
|
if err != nil {
|
|
slog.Error("epics: Archiver sync failed", "err", err)
|
|
return
|
|
}
|
|
|
|
for _, name := range pvs {
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
e.mu.Lock()
|
|
if _, exists := e.metadata[name]; !exists {
|
|
e.metadata[name] = datasource.Metadata{Name: name}
|
|
}
|
|
e.mu.Unlock()
|
|
// Background pre-fetch of record info
|
|
go e.prefetchPV(ctx, name)
|
|
}
|
|
slog.Info("epics: synced signals from Archiver", "count", len(pvs))
|
|
}
|