337 lines
8.9 KiB
Go
337 lines
8.9 KiB
Go
// Command catools provides caget/caput/camonitor/cainfo equivalents using
|
|
// the pure-Go goca CA client library.
|
|
//
|
|
// Usage:
|
|
//
|
|
// catools caget <pv> [pv...] # get current value(s)
|
|
// catools caput <pv> <value> # write a value
|
|
// catools cainfo <pv> [pv...] # print metadata (units, limits, type)
|
|
// catools camonitor <pv> [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> [pv...] Get current value(s)
|
|
caput <pv> <value> Write a value
|
|
cainfo <pv> [pv...] Print metadata (type, units, limits)
|
|
camonitor <pv> [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 <pv> <value>")
|
|
}
|
|
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)
|
|
}
|