Initial working fully go release
This commit is contained in:
+46
-55
@@ -1,12 +1,12 @@
|
||||
// Command catools provides caget/caput/camonitor/cainfo equivalents using
|
||||
// the pure-Go goca CA client library.
|
||||
// Command catools provides CA (Channel Access) command-line utilities using
|
||||
// the pure-Go goca library.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// catools caget <pv> [pv...] # get current value(s)
|
||||
// catools caput <pv> <value> # write a value
|
||||
// catools cainfo <pv> [pv...] # print metadata (units, limits, type)
|
||||
// catools camonitor <pv> [pv...] # stream live updates (Ctrl-C to stop)
|
||||
// catools get <pv> [pv...] Get current value(s)
|
||||
// catools put <pv> <value> Write a value
|
||||
// catools monitor <pv> [pv...] Stream live updates (Ctrl-C to stop)
|
||||
// catools info <pv> [pv...] Print metadata (type, units, limits)
|
||||
//
|
||||
// Environment:
|
||||
//
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, `catools — pure-Go EPICS CA command-line tools
|
||||
fmt.Fprintln(os.Stderr, `catools — EPICS Channel Access command-line tool
|
||||
|
||||
Commands:
|
||||
caget <pv> [pv...] Get current value(s)
|
||||
caput <pv> <value> Write a value
|
||||
cainfo <pv> [pv...] Print metadata (type, units, limits)
|
||||
camonitor <pv> [pv...] Stream live updates (Ctrl-C to stop)
|
||||
Usage:
|
||||
catools get <pv> [pv...] Get current value(s)
|
||||
catools put <pv> <value> Write a value
|
||||
catools monitor <pv> [pv...] Stream live updates (Ctrl-C to stop)
|
||||
catools info <pv> [pv...] Print metadata (type, units, limits)
|
||||
|
||||
Environment:
|
||||
EPICS_CA_ADDR_LIST Space-separated CA server addresses
|
||||
@@ -63,36 +63,36 @@ func main() {
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "caget":
|
||||
case "get":
|
||||
if len(args) == 0 {
|
||||
fatalf("caget: no PV names specified")
|
||||
fatalf("get: no PV names specified")
|
||||
}
|
||||
runCaget(ctx, cli, args)
|
||||
case "caput":
|
||||
runGet(ctx, cli, args)
|
||||
case "put":
|
||||
if len(args) < 2 {
|
||||
fatalf("caput: usage: catools caput <pv> <value>")
|
||||
fatalf("put: usage: catools put <pv> <value>")
|
||||
}
|
||||
runCaput(ctx, cli, args[0], args[1])
|
||||
case "cainfo":
|
||||
runPut(ctx, cli, args[0], args[1])
|
||||
case "info":
|
||||
if len(args) == 0 {
|
||||
fatalf("cainfo: no PV names specified")
|
||||
fatalf("info: no PV names specified")
|
||||
}
|
||||
runCainfo(ctx, cli, args)
|
||||
case "camonitor":
|
||||
runInfo(ctx, cli, args)
|
||||
case "monitor":
|
||||
if len(args) == 0 {
|
||||
fatalf("camonitor: no PV names specified")
|
||||
fatalf("monitor: no PV names specified")
|
||||
}
|
||||
runCamonitor(ctx, cli, args)
|
||||
runMonitor(ctx, cli, args)
|
||||
default:
|
||||
fatalf("unknown command %q — use caget, caput, cainfo, or camonitor", cmd)
|
||||
fatalf("unknown command %q — use get, put, monitor, or info", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// caget //
|
||||
// get //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func runCaget(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
func runGet(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -107,14 +107,14 @@ func runCaget(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// caput //
|
||||
// put //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func runCaput(ctx context.Context, cli *ca.Client, pv, valueStr string) {
|
||||
func runPut(ctx context.Context, cli *ca.Client, pv, valueStr string) {
|
||||
tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First get metadata to determine the right type.
|
||||
// Get metadata to determine the right type.
|
||||
ci, err := cli.GetCtrl(tctx, pv)
|
||||
if err != nil {
|
||||
fatalf("%s: get metadata: %v", pv, err)
|
||||
@@ -125,22 +125,18 @@ func runCaput(ctx context.Context, cli *ca.Client, pv, valueStr string) {
|
||||
case proto.DBFString:
|
||||
value = valueStr
|
||||
case proto.DBFEnum:
|
||||
// Try numeric first, then string.
|
||||
if n, err := strconv.ParseInt(valueStr, 10, 32); err == nil {
|
||||
value = int32(n)
|
||||
} else {
|
||||
// Search enum strings.
|
||||
if ci.Enum != nil {
|
||||
for i, s := range ci.Enum.Strings {
|
||||
if strings.EqualFold(s, valueStr) {
|
||||
value = int32(i)
|
||||
break
|
||||
}
|
||||
} else if ci.Enum != nil {
|
||||
for i, s := range ci.Enum.Strings {
|
||||
if strings.EqualFold(s, valueStr) {
|
||||
value = int32(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
if value == nil {
|
||||
fatalf("%s: unknown enum value %q", pv, valueStr)
|
||||
}
|
||||
}
|
||||
if value == nil {
|
||||
fatalf("%s: unknown enum value %q", pv, valueStr)
|
||||
}
|
||||
default:
|
||||
if n, err := strconv.ParseFloat(valueStr, 64); err == nil {
|
||||
@@ -156,20 +152,19 @@ func runCaput(ctx context.Context, cli *ca.Client, pv, valueStr string) {
|
||||
fatalf("%s: put: %v", pv, err)
|
||||
}
|
||||
|
||||
// Read back the new value.
|
||||
tv, err := cli.Get(tctx, pv)
|
||||
if err != nil {
|
||||
fmt.Printf("Old: ?\nNew: %s (write sent, readback failed: %v)\n", valueStr, err)
|
||||
fmt.Printf("%-30s %s (write sent, readback failed: %v)\n", pv, valueStr, err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("%-30s %s\n", pv, formatValue(tv))
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// cainfo //
|
||||
// info //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func runCainfo(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
func runInfo(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -220,10 +215,10 @@ func printInfo(pv string, ci ca.CtrlInfo) {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// camonitor //
|
||||
// monitor //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func runCamonitor(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
func runMonitor(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
type entry struct {
|
||||
pv string
|
||||
ch chan proto.TimeValue
|
||||
@@ -249,8 +244,6 @@ func runCamonitor(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fan out: one goroutine per PV, print to stdout.
|
||||
done := make(chan struct{})
|
||||
for _, e := range entries {
|
||||
go func(e *entry) {
|
||||
for {
|
||||
@@ -269,8 +262,6 @@ func runCamonitor(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
close(done)
|
||||
|
||||
for _, e := range entries {
|
||||
e.can()
|
||||
}
|
||||
@@ -283,14 +274,14 @@ func runCamonitor(ctx context.Context, cli *ca.Client, pvs []string) {
|
||||
func formatValue(tv proto.TimeValue) string {
|
||||
var val string
|
||||
switch {
|
||||
case tv.Doubles != nil:
|
||||
case tv.Str != "":
|
||||
val = tv.Str
|
||||
case len(tv.Doubles) > 1:
|
||||
parts := make([]string, len(tv.Doubles))
|
||||
for i, v := range tv.Doubles {
|
||||
parts[i] = strconv.FormatFloat(v, 'g', -1, 64)
|
||||
}
|
||||
val = "[" + strings.Join(parts, " ") + "]"
|
||||
case tv.Str != "":
|
||||
val = tv.Str
|
||||
default:
|
||||
val = strconv.FormatFloat(tv.Double, 'g', -1, 64)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// Command pvtools provides PV Access command-line utilities using
|
||||
// the pure-Go gopva library.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// pvtools get <pv> [pv...] Get current value(s) via PVA
|
||||
// pvtools put <pv> <value> Write a value via PVA
|
||||
// pvtools monitor <pv> [pv...] Stream live updates (Ctrl-C to stop)
|
||||
// pvtools info <pv> [pv...] Print PVA structure info
|
||||
//
|
||||
// Environment:
|
||||
//
|
||||
// EPICS_PVA_ADDR_LIST — space-separated PVA server addresses
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
pva "github.com/uopi/gopva"
|
||||
"github.com/uopi/gopva/pvdata"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, `pvtools — EPICS PV Access command-line tool
|
||||
|
||||
Usage:
|
||||
pvtools get <pv> [pv...] Get current value(s)
|
||||
pvtools put <pv> <value> Write a value
|
||||
pvtools monitor <pv> [pv...] Stream live updates (Ctrl-C to stop)
|
||||
pvtools info <pv> [pv...] Print structure info
|
||||
|
||||
Environment:
|
||||
EPICS_PVA_ADDR_LIST Space-separated PVA server addresses`)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
}
|
||||
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
switch cmd {
|
||||
case "get":
|
||||
if len(args) == 0 {
|
||||
fatalf("get: no PV names specified")
|
||||
}
|
||||
runGet(ctx, args)
|
||||
case "put":
|
||||
if len(args) < 2 {
|
||||
fatalf("put: usage: pvtools put <pv> <value>")
|
||||
}
|
||||
fatalf("put: PVA write not yet implemented")
|
||||
case "monitor":
|
||||
if len(args) == 0 {
|
||||
fatalf("monitor: no PV names specified")
|
||||
}
|
||||
runMonitor(ctx, args)
|
||||
case "info":
|
||||
if len(args) == 0 {
|
||||
fatalf("info: no PV names specified")
|
||||
}
|
||||
runInfo(ctx, args)
|
||||
default:
|
||||
fatalf("unknown command %q — use get, put, monitor, or info", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// get //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func runGet(ctx context.Context, pvs []string) {
|
||||
cl, err := pva.NewClient()
|
||||
if err != nil {
|
||||
fatalf("connect: %v", err)
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
for _, name := range pvs {
|
||||
sv, err := cl.Get(tctx, name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: error: %v\n", name, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%-30s %s\n", name, formatValue(sv))
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// monitor //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func runMonitor(ctx context.Context, pvs []string) {
|
||||
cl, err := pva.NewClient()
|
||||
if err != nil {
|
||||
fatalf("connect: %v", err)
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
for _, name := range pvs {
|
||||
ch := cl.Monitor(ctx, name)
|
||||
go func(pvName string, ch <-chan pva.MonitorEvent) {
|
||||
for evt := range ch {
|
||||
if evt.Err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: %v\n", pvName, evt.Err)
|
||||
return
|
||||
}
|
||||
ts := time.Now().Local().Format("15:04:05.000")
|
||||
fmt.Printf("%s %-30s %s\n", ts, pvName, formatValue(evt.Value))
|
||||
}
|
||||
}(name, ch)
|
||||
fmt.Printf("Monitoring %s (PVA) — press Ctrl-C to stop\n", name)
|
||||
}
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// info //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func runInfo(ctx context.Context, pvs []string) {
|
||||
cl, err := pva.NewClient()
|
||||
if err != nil {
|
||||
fatalf("connect: %v", err)
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
for _, name := range pvs {
|
||||
sv, err := cl.Get(tctx, name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: error: %v\n", name, err)
|
||||
continue
|
||||
}
|
||||
printInfo(name, sv)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func printInfo(name string, sv pvdata.StructValue) {
|
||||
fmt.Printf("Name : %s\n", name)
|
||||
fmt.Printf("Fields : %d\n", len(sv.Fields))
|
||||
printStruct(sv, " ")
|
||||
}
|
||||
|
||||
func printStruct(sv pvdata.StructValue, indent string) {
|
||||
for _, f := range sv.Fields {
|
||||
switch v := f.Value.(type) {
|
||||
case pvdata.StructValue:
|
||||
fmt.Printf("%s%s (struct):\n", indent, f.Name)
|
||||
printStruct(v, indent+" ")
|
||||
default:
|
||||
fmt.Printf("%s%s = %v\n", indent, f.Name, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// Formatting helpers //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func formatValue(sv pvdata.StructValue) string {
|
||||
if len(sv.Fields) == 0 {
|
||||
return "<empty>"
|
||||
}
|
||||
val := fieldByName(sv, "value")
|
||||
alarm := ""
|
||||
if a := structByName(sv, "alarm"); a != nil {
|
||||
if sev := fieldByName(*a, "severity"); sev != nil {
|
||||
if s, ok := sev.(int32); ok && s != 0 {
|
||||
alarm = fmt.Sprintf(" (alarm sev=%d)", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
ts := ""
|
||||
if t := structByName(sv, "timeStamp"); t != nil {
|
||||
sec := int64(0)
|
||||
nsec := int32(0)
|
||||
if v := fieldByName(*t, "secondsPastEpoch"); v != nil {
|
||||
sec, _ = v.(int64)
|
||||
}
|
||||
if v := fieldByName(*t, "nanoseconds"); v != nil {
|
||||
nsec, _ = v.(int32)
|
||||
}
|
||||
if sec != 0 {
|
||||
const epicsEpoch = 631152000
|
||||
ut := time.Unix(sec+epicsEpoch, int64(nsec)).Local()
|
||||
ts = " [" + ut.Format("2006-01-02 15:04:05.000") + "]"
|
||||
}
|
||||
}
|
||||
if val == nil {
|
||||
val = sv.Fields[0].Value
|
||||
}
|
||||
return fmt.Sprintf("%v%s%s", formatScalar(val), alarm, ts)
|
||||
}
|
||||
|
||||
func formatScalar(v any) string {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
return strconv.FormatFloat(x, 'g', -1, 64)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(x), 'g', -1, 32)
|
||||
case string:
|
||||
return x
|
||||
case []string:
|
||||
return strings.Join(x, " ")
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func fieldByName(sv pvdata.StructValue, name string) interface{} {
|
||||
for _, f := range sv.Fields {
|
||||
if f.Name == name {
|
||||
return f.Value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func structByName(sv pvdata.StructValue, name string) *pvdata.StructValue {
|
||||
for _, f := range sv.Fields {
|
||||
if f.Name == name {
|
||||
if s, ok := f.Value.(pvdata.StructValue); ok {
|
||||
return &s
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "pvtools: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/uopi/uopi/internal/broker"
|
||||
"github.com/uopi/uopi/internal/config"
|
||||
"github.com/uopi/uopi/internal/datasource/epics"
|
||||
"github.com/uopi/uopi/internal/datasource/pva"
|
||||
"github.com/uopi/uopi/internal/datasource/stub"
|
||||
"github.com/uopi/uopi/internal/datasource/synthetic"
|
||||
"github.com/uopi/uopi/internal/server"
|
||||
@@ -95,6 +96,16 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// PV Access data source.
|
||||
if cfg.Datasource.PVA.Enabled {
|
||||
pvaDS := pva.New(cfg.Datasource.PVA.AddrList)
|
||||
if err := pvaDS.Connect(ctx); err != nil {
|
||||
log.Error("pva connect", "err", err)
|
||||
} else {
|
||||
brk.Register(pvaDS)
|
||||
}
|
||||
}
|
||||
|
||||
srv := server.New(cfg.Server.Listen, webFS, brk, synthDS, store, cfg.Datasource.EPICS.ChannelFinderURL, log)
|
||||
|
||||
if err := srv.Start(ctx); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user