Wworking on improving the tool
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/uopi/uopi/internal/datasource"
|
||||
@@ -117,6 +118,55 @@ func fetchArchiveHistory(ctx context.Context, archiveURL, signal string, start,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// searchArchivePVs queries the Archive Appliance management API for PV names
|
||||
// matching the given pattern (glob style, e.g. "PV:*").
|
||||
func searchArchivePVs(ctx context.Context, archiveURL, pattern string) ([]string, error) {
|
||||
if archiveURL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Build the request URL.
|
||||
// Format: GET {archiveURL}/mgmt/bpl/getAllPVs?pv={pattern}
|
||||
reqURL, err := url.Parse(archiveURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("epics archive: invalid archive URL %q: %w", archiveURL, err)
|
||||
}
|
||||
reqURL = reqURL.JoinPath("mgmt", "bpl", "getAllPVs")
|
||||
|
||||
q := reqURL.Query()
|
||||
if pattern == "" {
|
||||
pattern = "*"
|
||||
}
|
||||
// AA expects glob patterns. If no glob characters, wrap in stars.
|
||||
if !strings.ContainsAny(pattern, "*?[]") {
|
||||
pattern = "*" + pattern + "*"
|
||||
}
|
||||
q.Set("pv", pattern)
|
||||
reqURL.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("epics archive: building request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("epics archive: HTTP request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("epics archive: unexpected status %d for search %q", resp.StatusCode, pattern)
|
||||
}
|
||||
|
||||
var pvs []string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&pvs); err != nil {
|
||||
return nil, fmt.Errorf("epics archive: decoding response: %w", err)
|
||||
}
|
||||
|
||||
return pvs, nil
|
||||
}
|
||||
|
||||
// coerceArchiveValue converts the raw JSON value (which json.Unmarshal decodes
|
||||
// as float64, string, bool, []interface{}, or nil) into one of the types
|
||||
// accepted by datasource.Value.Data.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package epics
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// cfChannel is the JSON structure returned by the Channel Finder Service.
|
||||
type cfChannel struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Properties []cfProperty `json:"properties"`
|
||||
Tags []cfTag `json:"tags"`
|
||||
}
|
||||
|
||||
type cfProperty struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
type cfTag struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
// queryChannelFinder fetches channels from CFS matching the given query string.
|
||||
func queryChannelFinder(cfURL, query string) ([]cfChannel, error) {
|
||||
if cfURL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
url := strings.TrimRight(cfURL, "/") + "/resources/channels"
|
||||
if query != "" {
|
||||
if strings.Contains(query, "=") || strings.Contains(query, "~") {
|
||||
// Query is already a structured filter (e.g. area=L1)
|
||||
url += "?" + query
|
||||
} else {
|
||||
// Query is just a PV name pattern
|
||||
url += "?~name=*" + query + "*"
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
slog.Info("CFS query", "url", url)
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CFS request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("CFS returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var channels []cfChannel
|
||||
if err := json.NewDecoder(resp.Body).Decode(&channels); err != nil {
|
||||
return nil, fmt.Errorf("CFS response parse error: %w", err)
|
||||
}
|
||||
slog.Info("CFS results", "count", len(channels))
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package epics_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/uopi/goca/proto"
|
||||
"github.com/uopi/goca/testca"
|
||||
"github.com/uopi/uopi/internal/datasource/epics"
|
||||
)
|
||||
|
||||
func TestDiscovery_EnvironmentVariables(t *testing.T) {
|
||||
// 1. Setup a fake CA server.
|
||||
srv, err := testca.New([]testca.PVSpec{
|
||||
{Name: "DISCO:PV", DBFType: proto.DBFDouble, Count: 1, Value: 123.45},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("testca.New: %v", err)
|
||||
}
|
||||
defer srv.Close()
|
||||
|
||||
// 2. Set the environment variable to point to this server.
|
||||
os.Setenv("EPICS_CA_ADDR_LIST", srv.UDPAddr())
|
||||
os.Setenv("EPICS_CA_AUTO_ADDR_LIST", "NO")
|
||||
defer os.Unsetenv("EPICS_CA_ADDR_LIST")
|
||||
defer os.Unsetenv("EPICS_CA_AUTO_ADDR_LIST")
|
||||
|
||||
// 3. Create datasource with NO explicit address (should pick up from env).
|
||||
ds := epics.New("", "", "", "", false, nil)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := ds.Connect(ctx); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
|
||||
// 4. Verify discovery works.
|
||||
meta, err := ds.GetMetadata(ctx, "DISCO:PV")
|
||||
if err != nil {
|
||||
t.Fatalf("GetMetadata failed (discovery failed): %v", err)
|
||||
}
|
||||
if meta.Name != "DISCO:PV" {
|
||||
t.Errorf("got %q, want DISCO:PV", meta.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscovery_ConfigOverride(t *testing.T) {
|
||||
// 1. Setup TWO fake CA servers.
|
||||
srv1, _ := testca.New([]testca.PVSpec{{Name: "PV:1", DBFType: proto.DBFDouble, Value: 1.0}})
|
||||
defer srv1.Close()
|
||||
srv2, _ := testca.New([]testca.PVSpec{{Name: "PV:2", DBFType: proto.DBFDouble, Value: 2.0}})
|
||||
defer srv2.Close()
|
||||
|
||||
// 2. Point env to srv1.
|
||||
os.Setenv("EPICS_CA_ADDR_LIST", srv1.UDPAddr())
|
||||
os.Setenv("EPICS_CA_AUTO_ADDR_LIST", "NO")
|
||||
defer os.Unsetenv("EPICS_CA_ADDR_LIST")
|
||||
defer os.Unsetenv("EPICS_CA_AUTO_ADDR_LIST")
|
||||
|
||||
// 3. Create datasource pointing explicitly to srv2 (should OVERRIDE env).
|
||||
ds := epics.New(srv2.UDPAddr(), "", "", "", false, nil)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := ds.Connect(ctx); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
|
||||
// 4. PV from srv2 should be found.
|
||||
if _, err := ds.GetMetadata(ctx, "PV:2"); err != nil {
|
||||
t.Errorf("PV:2 from config address not found: %v", err)
|
||||
}
|
||||
|
||||
// 5. PV from srv1 should NOT be found (since AutoAddrList is disabled by config override).
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel2()
|
||||
if _, err := ds.GetMetadata(ctx2, "PV:1"); err == nil {
|
||||
t.Error("PV:1 from env address was found, but config should have overridden it")
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import "C"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -66,10 +67,14 @@ type caChannel struct {
|
||||
|
||||
// EPICS is the Channel Access data source.
|
||||
type EPICS struct {
|
||||
caAddrList string
|
||||
archiveURL string
|
||||
caAddrList string
|
||||
archiveURL string
|
||||
cfURL string
|
||||
autoSyncFilter string
|
||||
autoSyncFromArchiver bool
|
||||
|
||||
// caCtx is the CA context created in Connect(). Stored as unsafe.Pointer
|
||||
// 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.
|
||||
@@ -87,12 +92,15 @@ type EPICS struct {
|
||||
// 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 string, pvNames []string) datasource.DataSource {
|
||||
func New(caAddrList, archiveURL, cfURL, autoSyncFilter string, autoSyncFromArchiver bool, pvNames []string) datasource.DataSource {
|
||||
return &EPICS{
|
||||
caAddrList: caAddrList,
|
||||
archiveURL: archiveURL,
|
||||
channels: make(map[string]*caChannel),
|
||||
metadata: make(map[string]datasource.Metadata),
|
||||
caAddrList: caAddrList,
|
||||
archiveURL: archiveURL,
|
||||
cfURL: cfURL,
|
||||
autoSyncFilter: autoSyncFilter,
|
||||
autoSyncFromArchiver: autoSyncFromArchiver,
|
||||
channels: make(map[string]*caChannel),
|
||||
metadata: make(map[string]datasource.Metadata),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +130,62 @@ func (e *EPICS) Connect(_ context.Context) error {
|
||||
}
|
||||
// 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.
|
||||
@@ -422,13 +483,12 @@ func (e *EPICS) fetchMetadata(chid C.chid, name string) datasource.Metadata {
|
||||
}
|
||||
|
||||
// ListSignals returns metadata for all currently tracked channels.
|
||||
// Channels with cached metadata return full info; others return a stub entry
|
||||
// with the PV name so the signal tree can display them immediately after
|
||||
// a manual add, before GetMetadata has been called.
|
||||
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))
|
||||
@@ -583,3 +643,27 @@ func nativeTimeType(chid C.chid) C.chtype {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func newTestDS(t *testing.T, pvs []testca.PVSpec) (datasource.DataSource, *testc
|
||||
}
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
ds := epics.New(srv.UDPAddr(), "", nil)
|
||||
ds := epics.New(srv.UDPAddr(), "", "", "", false, nil)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
if err := ds.Connect(ctx); err != nil {
|
||||
|
||||
@@ -28,9 +28,12 @@ func Available() bool { return true }
|
||||
|
||||
// EPICS is the pure-Go Channel Access data source.
|
||||
type EPICS struct {
|
||||
caAddrList string
|
||||
archiveURL string
|
||||
pvNames []string // pre-fetched at connect time for ListSignals
|
||||
caAddrList string
|
||||
archiveURL string
|
||||
cfURL string
|
||||
autoSyncFilter string
|
||||
autoSyncFromArchiver bool
|
||||
pvNames []string // pre-fetched at connect time for ListSignals
|
||||
|
||||
client *ca.Client
|
||||
|
||||
@@ -47,12 +50,15 @@ type EPICS struct {
|
||||
// pvNames is an optional list of PV names whose metadata will be pre-fetched
|
||||
// at connect time so they appear in ListSignals immediately (useful for the
|
||||
// edit-mode signal tree).
|
||||
func New(caAddrList, archiveURL string, pvNames []string) datasource.DataSource {
|
||||
func New(caAddrList, archiveURL, cfURL, autoSyncFilter string, autoSyncFromArchiver bool, pvNames []string) datasource.DataSource {
|
||||
return &EPICS{
|
||||
caAddrList: caAddrList,
|
||||
archiveURL: archiveURL,
|
||||
pvNames: pvNames,
|
||||
metadata: make(map[string]datasource.Metadata),
|
||||
caAddrList: caAddrList,
|
||||
archiveURL: archiveURL,
|
||||
cfURL: cfURL,
|
||||
autoSyncFilter: autoSyncFilter,
|
||||
autoSyncFromArchiver: autoSyncFromArchiver,
|
||||
pvNames: pvNames,
|
||||
metadata: make(map[string]datasource.Metadata),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,29 +81,24 @@ func (e *EPICS) Connect(ctx context.Context) error {
|
||||
e.client = cli
|
||||
slog.Info("epics: CA client started", "addrs", cfg.AddrList)
|
||||
|
||||
// Pre-fetch metadata for any configured PV names so they appear in
|
||||
// ListSignals as soon as the datasource is ready. We open a short-lived
|
||||
// subscription (which drives channel connection) and then fetch CTRL info.
|
||||
// Perform background sync from Channel Finder if configured.
|
||||
if e.cfURL != "" && e.autoSyncFilter != "" {
|
||||
go e.syncFromChannelFinder(ctx)
|
||||
}
|
||||
|
||||
// Perform background sync from Archive Appliance if configured.
|
||||
if e.archiveURL != "" && e.autoSyncFromArchiver {
|
||||
go e.syncFromArchiver(ctx)
|
||||
}
|
||||
|
||||
// Pre-fetch metadata for any configured PV names.
|
||||
if len(e.pvNames) > 0 {
|
||||
go func() {
|
||||
for _, pv := range e.pvNames {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
func() {
|
||||
tvCh := make(chan proto.TimeValue, 1)
|
||||
subCtx, subCancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer subCancel()
|
||||
cancel, err := e.client.Subscribe(subCtx, pv, tvCh)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer cancel()
|
||||
// Channel is now connected; fetch CTRL metadata.
|
||||
mctx, mcancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer mcancel()
|
||||
_, _ = e.GetMetadata(mctx, pv)
|
||||
}()
|
||||
e.prefetchPV(ctx, pv)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -105,6 +106,62 @@ func (e *EPICS) Connect(ctx context.Context) error {
|
||||
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 {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
// Convert CFS properties and tags to datasource.Metadata
|
||||
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
|
||||
}
|
||||
|
||||
// Initialize metadata with CFS info. Full metadata (type, units, etc.)
|
||||
// will be populated on first access via DBR_CTRL GET.
|
||||
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 (type, etc.)
|
||||
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) {
|
||||
tvCh := make(chan proto.TimeValue, 1)
|
||||
subCtx, subCancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer subCancel()
|
||||
cancel, err := e.client.Subscribe(subCtx, pv, tvCh)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer cancel()
|
||||
// Channel is now connected; fetch CTRL metadata.
|
||||
mctx, mcancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer mcancel()
|
||||
_, _ = e.GetMetadata(mctx, pv)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// Subscribe //
|
||||
// -------------------------------------------------------------------------- //
|
||||
@@ -273,6 +330,7 @@ func (e *EPICS) ctrlToMeta(name string, ci ca.CtrlInfo) datasource.Metadata {
|
||||
func (e *EPICS) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
slog.Debug("epics: listing signals", "count", len(e.metadata))
|
||||
out := make([]datasource.Metadata, 0, len(e.metadata))
|
||||
for _, m := range e.metadata {
|
||||
out = append(out, m)
|
||||
@@ -305,3 +363,27 @@ func (e *EPICS) History(ctx context.Context, signal string, start, end time.Time
|
||||
}
|
||||
return fetchArchiveHistory(ctx, e.archiveURL, signal, start, end, maxPoints)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ type Metadata struct {
|
||||
DriveHigh float64
|
||||
EnumStrings []string // non-nil only for TypeEnum
|
||||
Writable bool
|
||||
Properties map[string]string // Channel Finder properties
|
||||
Tags []string // Channel Finder tags
|
||||
}
|
||||
|
||||
// CancelFunc cancels a subscription started by DataSource.Subscribe.
|
||||
|
||||
Reference in New Issue
Block a user