// Command catools provides caget/caput/camonitor/cainfo equivalents using // the pure-Go goca CA client library. // // Usage: // // catools caget [pv...] # get current value(s) // catools caput # write a value // catools cainfo [pv...] # print metadata (units, limits, type) // catools camonitor [pv...] # stream live updates (Ctrl-C to stop) // // 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 — pure-Go EPICS CA command-line tools Commands: caget [pv...] Get current value(s) caput Write a value cainfo [pv...] Print metadata (type, units, limits) camonitor [pv...] Stream live updates (Ctrl-C to stop) 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 "caget": if len(args) == 0 { fatalf("caget: no PV names specified") } runCaget(ctx, cli, args) case "caput": if len(args) < 2 { fatalf("caput: usage: catools caput ") } runCaput(ctx, cli, args[0], args[1]) case "cainfo": if len(args) == 0 { fatalf("cainfo: no PV names specified") } runCainfo(ctx, cli, args) case "camonitor": if len(args) == 0 { fatalf("camonitor: no PV names specified") } runCamonitor(ctx, cli, args) default: fatalf("unknown command %q — use caget, caput, cainfo, or camonitor", cmd) } } // -------------------------------------------------------------------------- // // caget // // -------------------------------------------------------------------------- // func runCaget(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)) } } // -------------------------------------------------------------------------- // // caput // // -------------------------------------------------------------------------- // func runCaput(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. 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: // 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 } } } 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) } // 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) return } fmt.Printf("%-30s %s\n", pv, formatValue(tv)) } // -------------------------------------------------------------------------- // // cainfo // // -------------------------------------------------------------------------- // func runCainfo(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") } } // -------------------------------------------------------------------------- // // camonitor // // -------------------------------------------------------------------------- // func runCamonitor(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 } // Fan out: one goroutine per PV, print to stdout. done := make(chan struct{}) 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() close(done) for _, e := range entries { e.can() } } // -------------------------------------------------------------------------- // // Formatting helpers // // -------------------------------------------------------------------------- // func formatValue(tv proto.TimeValue) string { var val string switch { case tv.Doubles != nil: 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) } 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) }