// Command catools provides CA (Channel Access) command-line utilities using // the pure-Go goca library. // // Usage: // // catools get [pv...] Get current value(s) // catools put Write a value // catools monitor [pv...] Stream live updates (Ctrl-C to stop) // catools info [pv...] Print metadata (type, units, limits) // // Environment: // // EPICS_CA_ADDR_LIST — space-separated CA server addresses // EPICS_CA_AUTO_ADDR_LIST — set to "NO" to disable broadcast search // UOPI_CA_DEBUG — set to "1" for CA protocol debug logging package main import ( "context" "fmt" "os" "os/signal" "strconv" "strings" "syscall" "time" ca "github.com/uopi/goca" "github.com/uopi/goca/proto" ) func usage() { fmt.Fprintln(os.Stderr, `catools — EPICS Channel Access command-line tool Usage: catools get [pv...] Get current value(s) catools put Write a value catools monitor [pv...] Stream live updates (Ctrl-C to stop) catools info [pv...] Print metadata (type, units, limits) Environment: EPICS_CA_ADDR_LIST Space-separated CA server addresses EPICS_CA_AUTO_ADDR_LIST Set to NO to disable broadcast search UOPI_CA_DEBUG Set to 1 for CA protocol debug logging`) 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() cfg := ca.ConfigFromEnv() cli, err := ca.NewClient(ctx, cfg) if err != nil { fatalf("connect: %v", err) } switch cmd { case "get": if len(args) == 0 { fatalf("get: no PV names specified") } runGet(ctx, cli, args) case "put": if len(args) < 2 { fatalf("put: usage: catools put ") } runPut(ctx, cli, args[0], args[1]) case "info": if len(args) == 0 { fatalf("info: no PV names specified") } runInfo(ctx, cli, args) case "monitor": if len(args) == 0 { fatalf("monitor: no PV names specified") } runMonitor(ctx, cli, args) default: fatalf("unknown command %q — use get, put, monitor, or info", cmd) } } // -------------------------------------------------------------------------- // // get // // -------------------------------------------------------------------------- // func runGet(ctx context.Context, cli *ca.Client, pvs []string) { tctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() for _, pv := range pvs { tv, err := cli.Get(tctx, pv) if err != nil { fmt.Fprintf(os.Stderr, "%s: error: %v\n", pv, err) continue } fmt.Printf("%-30s %s\n", pv, formatValue(tv)) } } // -------------------------------------------------------------------------- // // put // // -------------------------------------------------------------------------- // func runPut(ctx context.Context, cli *ca.Client, pv, valueStr string) { tctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() // Get metadata to determine the right type. ci, err := cli.GetCtrl(tctx, pv) if err != nil { fatalf("%s: get metadata: %v", pv, err) } var value any switch ci.DBFType { case proto.DBFString: value = valueStr case proto.DBFEnum: if n, err := strconv.ParseInt(valueStr, 10, 32); err == nil { value = int32(n) } 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) } default: if n, err := strconv.ParseFloat(valueStr, 64); err == nil { value = n } else if n, err := strconv.ParseInt(valueStr, 10, 64); err == nil { value = int64(n) } else { value = valueStr } } if err := cli.Put(tctx, pv, value); err != nil { fatalf("%s: put: %v", pv, err) } tv, err := cli.Get(tctx, pv) if err != nil { fmt.Printf("%-30s %s (write sent, readback failed: %v)\n", pv, valueStr, err) return } fmt.Printf("%-30s %s\n", pv, formatValue(tv)) } // -------------------------------------------------------------------------- // // info // // -------------------------------------------------------------------------- // func runInfo(ctx context.Context, cli *ca.Client, pvs []string) { tctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() for _, pv := range pvs { ci, err := cli.GetCtrl(tctx, pv) if err != nil { fmt.Fprintf(os.Stderr, "%s: error: %v\n", pv, err) continue } printInfo(pv, ci) fmt.Println() } } func printInfo(pv string, ci ca.CtrlInfo) { fmt.Printf("Name : %s\n", pv) fmt.Printf("DBF type : %s (%d)\n", dbfTypeName(ci.DBFType), ci.DBFType) fmt.Printf("Count : %d\n", ci.Count) writable := "No" if ci.Access&proto.AccessWrite != 0 { writable = "Yes" } fmt.Printf("Writable : %s\n", writable) switch { case ci.Double != nil: d := ci.Double fmt.Printf("Units : %s\n", d.Units) fmt.Printf("Disp range : [%g, %g]\n", d.LowerDispLimit, d.UpperDispLimit) fmt.Printf("Ctrl range : [%g, %g]\n", d.LowerCtrlLimit, d.UpperCtrlLimit) fmt.Printf("Alarm LOLO : %g HIHI: %g\n", d.LowerAlarmLimit, d.UpperAlarmLimit) fmt.Printf("Warn LOW : %g HIGH: %g\n", d.LowerWarnLimit, d.UpperWarnLimit) case ci.Long != nil: l := ci.Long fmt.Printf("Units : %s\n", l.Units) fmt.Printf("Disp range : [%d, %d]\n", l.LowerDispLimit, l.UpperDispLimit) fmt.Printf("Ctrl range : [%d, %d]\n", l.LowerCtrlLimit, l.UpperCtrlLimit) fmt.Printf("Alarm LOLO : %d HIHI: %d\n", l.LowerAlarmLimit, l.UpperAlarmLimit) fmt.Printf("Warn LOW : %d HIGH: %d\n", l.LowerWarnLimit, l.UpperWarnLimit) case ci.Enum != nil: fmt.Printf("Enum states: %d\n", len(ci.Enum.Strings)) for i, s := range ci.Enum.Strings { fmt.Printf(" [%d] %s\n", i, s) } case ci.Str != nil: fmt.Printf("Type : string\n") } } // -------------------------------------------------------------------------- // // monitor // // -------------------------------------------------------------------------- // func runMonitor(ctx context.Context, cli *ca.Client, pvs []string) { type entry struct { pv string ch chan proto.TimeValue can context.CancelFunc } entries := make([]*entry, 0, len(pvs)) tctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() for _, pv := range pvs { ch := make(chan proto.TimeValue, 32) cancelFn, err := cli.Subscribe(tctx, pv, ch) if err != nil { fmt.Fprintf(os.Stderr, "%s: subscribe error: %v\n", pv, err) continue } entries = append(entries, &entry{pv: pv, ch: ch, can: cancelFn}) fmt.Printf("Monitoring %s — press Ctrl-C to stop\n", pv) } if len(entries) == 0 { return } for _, e := range entries { go func(e *entry) { for { select { case tv, ok := <-e.ch: if !ok { return } ts := tv.Timestamp.Local().Format("15:04:05.000") fmt.Printf("%s %-30s %s\n", ts, e.pv, formatValue(tv)) case <-ctx.Done(): return } } }(e) } <-ctx.Done() for _, e := range entries { e.can() } } // -------------------------------------------------------------------------- // // Formatting helpers // // -------------------------------------------------------------------------- // func formatValue(tv proto.TimeValue) string { var val string switch { 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, " ") + "]" default: val = strconv.FormatFloat(tv.Double, 'g', -1, 64) } severity := "" switch tv.Severity { case proto.SeverityMinor: severity = " MINOR" case proto.SeverityMajor: severity = " MAJOR" case proto.SeverityInvalid: severity = " INVALID" } ts := tv.Timestamp.Local().Format("2006-01-02 15:04:05.000") return fmt.Sprintf("%s%s [%s]", val, severity, ts) } func dbfTypeName(t int) string { switch t { case proto.DBFString: return "DBF_STRING" case proto.DBFShort: return "DBF_SHORT" case proto.DBFFloat: return "DBF_FLOAT" case proto.DBFEnum: return "DBF_ENUM" case proto.DBFChar: return "DBF_CHAR" case proto.DBFLong: return "DBF_LONG" case proto.DBFDouble: return "DBF_DOUBLE" default: return "DBF_UNKNOWN" } } func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "catools: "+format+"\n", args...) os.Exit(1) }