254 lines
6.2 KiB
Go
254 lines
6.2 KiB
Go
// 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)
|
|
}
|