// Command pvtools provides PV Access command-line utilities using // the pure-Go gopva library. // // Usage: // // pvtools get [pv...] Get current value(s) via PVA // pvtools put Write a value via PVA // pvtools monitor [pv...] Stream live updates (Ctrl-C to stop) // pvtools info [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...] Get current value(s) pvtools put Write a value pvtools monitor [pv...] Stream live updates (Ctrl-C to stop) pvtools info [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 ") } 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 "" } 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) }