Files
uopi/cmd/catools/main.go
T
2026-04-30 23:01:01 +02:00

328 lines
8.6 KiB
Go

// Command catools provides CA (Channel Access) command-line utilities using
// the pure-Go goca library.
//
// 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
// 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> [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
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 <pv> <value>")
}
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)
}