working epics ioc and tested
This commit is contained in:
@@ -45,6 +45,8 @@ func (h *Handler) Register(mux *http.ServeMux, prefix string) {
|
||||
// Synthetic signal CRUD
|
||||
mux.HandleFunc("GET "+prefix+"/synthetic", h.listSynthetic)
|
||||
mux.HandleFunc("POST "+prefix+"/synthetic", h.createSynthetic)
|
||||
mux.HandleFunc("GET "+prefix+"/synthetic/{name}", h.getSynthetic)
|
||||
mux.HandleFunc("PUT "+prefix+"/synthetic/{name}", h.updateSynthetic)
|
||||
mux.HandleFunc("DELETE "+prefix+"/synthetic/{name}", h.deleteSynthetic)
|
||||
}
|
||||
|
||||
@@ -307,6 +309,48 @@ func (h *Handler) createSynthetic(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOK(w, def)
|
||||
}
|
||||
|
||||
func (h *Handler) getSynthetic(w http.ResponseWriter, r *http.Request) {
|
||||
if h.synthetic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
|
||||
return
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
def, err := h.synthetic.GetSignal(name)
|
||||
if err != nil {
|
||||
if errors.Is(err, datasource.ErrNotFound) {
|
||||
jsonError(w, http.StatusNotFound, "synthetic signal not found: "+name)
|
||||
} else {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
jsonOK(w, def)
|
||||
}
|
||||
|
||||
func (h *Handler) updateSynthetic(w http.ResponseWriter, r *http.Request) {
|
||||
if h.synthetic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
|
||||
return
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
var def synthetic.SignalDef
|
||||
if err := json.NewDecoder(r.Body).Decode(&def); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
// Ensure the name in the URL matches the body (or fill it in).
|
||||
def.Name = name
|
||||
if err := h.synthetic.UpdateSignal(def); err != nil {
|
||||
if errors.Is(err, datasource.ErrNotFound) {
|
||||
jsonError(w, http.StatusNotFound, "synthetic signal not found: "+name)
|
||||
} else {
|
||||
jsonError(w, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
jsonOK(w, def)
|
||||
}
|
||||
|
||||
func (h *Handler) deleteSynthetic(w http.ResponseWriter, r *http.Request) {
|
||||
if h.synthetic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
|
||||
|
||||
@@ -342,10 +342,10 @@ func (e *EPICS) GetMetadata(ctx context.Context, signal string) (datasource.Meta
|
||||
|
||||
// fetchMetadata retrieves metadata from a connected chid using ca_get.
|
||||
func (e *EPICS) fetchMetadata(chid C.chid, name string) datasource.Metadata {
|
||||
// In EPICS, write access is governed by CA security (host/user rules), not
|
||||
// by the field type. Default to writable=true; the IOC will reject puts
|
||||
// that violate its security policy.
|
||||
meta := datasource.Metadata{Name: name, Writable: true}
|
||||
// ca_write_access returns 1 if CA security permits puts from this client.
|
||||
// This reflects the IOC's actual security policy (host/user ACLs), so we
|
||||
// use it directly rather than defaulting to true and relying on put failures.
|
||||
meta := datasource.Metadata{Name: name, Writable: C.ca_write_access(chid) != 0}
|
||||
|
||||
fieldType := C.ca_field_type(chid)
|
||||
count := C.ca_element_count(chid)
|
||||
|
||||
@@ -286,6 +286,52 @@ func (s *Synthetic) RemoveSignal(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSignal returns the definition of a single named signal.
|
||||
func (s *Synthetic) GetSignal(name string) (SignalDef, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
st, ok := s.signals[name]
|
||||
if !ok {
|
||||
return SignalDef{}, datasource.ErrNotFound
|
||||
}
|
||||
return st.def, nil
|
||||
}
|
||||
|
||||
// UpdateSignal replaces the pipeline of an existing synthetic signal at runtime
|
||||
// and persists the change. The signal must already exist.
|
||||
func (s *Synthetic) UpdateSignal(def SignalDef) error {
|
||||
if def.Name == "" {
|
||||
return errors.New("signal name must not be empty")
|
||||
}
|
||||
|
||||
nodes, err := BuildPipeline(def.Pipeline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build pipeline: %w", err)
|
||||
}
|
||||
|
||||
states := make([]map[string]any, len(nodes))
|
||||
for i := range states {
|
||||
states[i] = make(map[string]any)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
old, exists := s.signals[def.Name]
|
||||
if !exists {
|
||||
s.mu.Unlock()
|
||||
return datasource.ErrNotFound
|
||||
}
|
||||
if old.cancel != nil {
|
||||
old.cancel()
|
||||
}
|
||||
s.signals[def.Name] = &signalState{def: def, nodes: nodes, states: states}
|
||||
s.mu.Unlock()
|
||||
|
||||
if err := s.saveDefs(); err != nil {
|
||||
return fmt.Errorf("persist definitions: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDefs returns a copy of all current signal definitions (for the REST API).
|
||||
func (s *Synthetic) GetDefs() []SignalDef {
|
||||
s.mu.RLock()
|
||||
|
||||
+150
-33
@@ -277,12 +277,21 @@ func (n *ExprNode) Process(inputs []float64, _ map[string]any) (float64, error)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// exprParser is a recursive-descent parser for simple arithmetic expressions.
|
||||
// exprParser is a recursive-descent parser for arithmetic expressions.
|
||||
// Grammar:
|
||||
//
|
||||
// expr = term (('+' | '-') term)*
|
||||
// term = factor (('*' | '/') factor)*
|
||||
// expr = term (('+' | '-') term)*
|
||||
// term = power (('*' | '/') power)*
|
||||
// power = unary ('^' power)* — right-associative exponentiation
|
||||
// unary = '-' unary | call
|
||||
// call = ident '(' expr ')' | factor
|
||||
// factor = '(' expr ')' | number | variable
|
||||
//
|
||||
// Supported functions: exp, log, log2, log10, sqrt, abs, sin, cos, tan,
|
||||
//
|
||||
// asin, acos, atan, floor, ceil, round, pow
|
||||
//
|
||||
// Variables: a, b, c, d (bound to inputs[0..3]).
|
||||
type exprParser struct {
|
||||
src string
|
||||
pos int
|
||||
@@ -328,7 +337,7 @@ func (p *exprParser) parseExpr() (float64, error) {
|
||||
}
|
||||
|
||||
func (p *exprParser) parseTerm() (float64, error) {
|
||||
left, err := p.parseFactor()
|
||||
left, err := p.parsePower()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -338,7 +347,7 @@ func (p *exprParser) parseTerm() (float64, error) {
|
||||
break
|
||||
}
|
||||
p.pos++
|
||||
right, err := p.parseFactor()
|
||||
right, err := p.parsePower()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -354,6 +363,136 @@ func (p *exprParser) parseTerm() (float64, error) {
|
||||
return left, nil
|
||||
}
|
||||
|
||||
// parsePower handles right-associative exponentiation: a^b^c = a^(b^c).
|
||||
// Unary minus is consumed here so that -a^2 = -(a^2), not (-a)^2 — matching
|
||||
// standard mathematical and Python/Julia precedence rules.
|
||||
func (p *exprParser) parsePower() (float64, error) {
|
||||
// Collect leading unary minuses; an even count cancels out.
|
||||
sign := 1.0
|
||||
p.skipSpaces()
|
||||
for p.pos < len(p.src) && p.src[p.pos] == '-' {
|
||||
p.pos++
|
||||
sign = -sign
|
||||
p.skipSpaces()
|
||||
}
|
||||
|
||||
base, err := p.parseCall()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
p.skipSpaces()
|
||||
if p.pos < len(p.src) && p.src[p.pos] == '^' {
|
||||
p.pos++
|
||||
exp, err := p.parsePower() // right-associative recursion
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sign * math.Pow(base, exp), nil
|
||||
}
|
||||
return sign * base, nil
|
||||
}
|
||||
|
||||
// parseCall handles function calls like exp(a) or pow(a, 2).
|
||||
func (p *exprParser) parseCall() (float64, error) {
|
||||
p.skipSpaces()
|
||||
if p.pos >= len(p.src) {
|
||||
return 0, errors.New("unexpected end of expression")
|
||||
}
|
||||
|
||||
// Try to read an identifier (function name or variable).
|
||||
if p.src[p.pos] >= 'a' && p.src[p.pos] <= 'z' || p.src[p.pos] >= 'A' && p.src[p.pos] <= 'Z' {
|
||||
start := p.pos
|
||||
for p.pos < len(p.src) && (p.src[p.pos] >= 'a' && p.src[p.pos] <= 'z' ||
|
||||
p.src[p.pos] >= 'A' && p.src[p.pos] <= 'Z' ||
|
||||
p.src[p.pos] >= '0' && p.src[p.pos] <= '9' ||
|
||||
p.src[p.pos] == '_') {
|
||||
p.pos++
|
||||
}
|
||||
name := p.src[start:p.pos]
|
||||
|
||||
// Check for function call syntax: name(...)
|
||||
p.skipSpaces()
|
||||
if p.pos < len(p.src) && p.src[p.pos] == '(' {
|
||||
p.pos++ // consume '('
|
||||
arg1, err := p.parseExpr()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Optional second argument for two-argument functions.
|
||||
var arg2 float64
|
||||
p.skipSpaces()
|
||||
if p.pos < len(p.src) && p.src[p.pos] == ',' {
|
||||
p.pos++
|
||||
arg2, err = p.parseExpr()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
p.skipSpaces()
|
||||
}
|
||||
if p.pos >= len(p.src) || p.src[p.pos] != ')' {
|
||||
return 0, fmt.Errorf("missing ')' after %s(...)", name)
|
||||
}
|
||||
p.pos++ // consume ')'
|
||||
|
||||
switch name {
|
||||
case "exp":
|
||||
return math.Exp(arg1), nil
|
||||
case "log", "ln":
|
||||
return math.Log(arg1), nil
|
||||
case "log2":
|
||||
return math.Log2(arg1), nil
|
||||
case "log10":
|
||||
return math.Log10(arg1), nil
|
||||
case "sqrt":
|
||||
return math.Sqrt(arg1), nil
|
||||
case "abs":
|
||||
return math.Abs(arg1), nil
|
||||
case "sin":
|
||||
return math.Sin(arg1), nil
|
||||
case "cos":
|
||||
return math.Cos(arg1), nil
|
||||
case "tan":
|
||||
return math.Tan(arg1), nil
|
||||
case "asin":
|
||||
return math.Asin(arg1), nil
|
||||
case "acos":
|
||||
return math.Acos(arg1), nil
|
||||
case "atan":
|
||||
return math.Atan(arg1), nil
|
||||
case "atan2":
|
||||
return math.Atan2(arg1, arg2), nil
|
||||
case "pow":
|
||||
return math.Pow(arg1, arg2), nil
|
||||
case "floor":
|
||||
return math.Floor(arg1), nil
|
||||
case "ceil":
|
||||
return math.Ceil(arg1), nil
|
||||
case "round":
|
||||
return math.Round(arg1), nil
|
||||
case "min":
|
||||
return math.Min(arg1, arg2), nil
|
||||
case "max":
|
||||
return math.Max(arg1, arg2), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown function %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Not a function call — must be a single-letter variable.
|
||||
if len(name) != 1 {
|
||||
return 0, fmt.Errorf("unknown identifier %q (use a–d for variables, or a known function name)", name)
|
||||
}
|
||||
val, ok := p.vars[name]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unknown variable %q (allowed: a, b, c, d)", name)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return p.parseFactor()
|
||||
}
|
||||
|
||||
func (p *exprParser) parseFactor() (float64, error) {
|
||||
p.skipSpaces()
|
||||
if p.pos >= len(p.src) {
|
||||
@@ -362,7 +501,7 @@ func (p *exprParser) parseFactor() (float64, error) {
|
||||
|
||||
ch := p.src[p.pos]
|
||||
|
||||
// Parenthesised subexpression
|
||||
// Parenthesised subexpression.
|
||||
if ch == '(' {
|
||||
p.pos++
|
||||
val, err := p.parseExpr()
|
||||
@@ -377,35 +516,13 @@ func (p *exprParser) parseFactor() (float64, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Unary minus
|
||||
if ch == '-' {
|
||||
p.pos++
|
||||
val, err := p.parseFactor()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return -val, nil
|
||||
}
|
||||
|
||||
// Variable (a, b, c, d only)
|
||||
if ch >= 'a' && ch <= 'z' {
|
||||
name := string(ch)
|
||||
p.pos++
|
||||
// Make sure we only accept single-letter variables
|
||||
if p.pos < len(p.src) && (p.src[p.pos] >= 'a' && p.src[p.pos] <= 'z' || p.src[p.pos] >= 'A' && p.src[p.pos] <= 'Z' || p.src[p.pos] == '_') {
|
||||
return 0, fmt.Errorf("unknown identifier starting with %q", name)
|
||||
}
|
||||
val, ok := p.vars[name]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unknown variable %q (allowed: a, b, c, d)", name)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Number (including optional decimal point and exponent)
|
||||
// Number (including optional decimal point and exponent notation).
|
||||
if ch >= '0' && ch <= '9' || ch == '.' {
|
||||
start := p.pos
|
||||
for p.pos < len(p.src) && (p.src[p.pos] >= '0' && p.src[p.pos] <= '9' || p.src[p.pos] == '.' || p.src[p.pos] == 'e' || p.src[p.pos] == 'E' || ((p.src[p.pos] == '+' || p.src[p.pos] == '-') && p.pos > start && (p.src[p.pos-1] == 'e' || p.src[p.pos-1] == 'E'))) {
|
||||
for p.pos < len(p.src) && (p.src[p.pos] >= '0' && p.src[p.pos] <= '9' ||
|
||||
p.src[p.pos] == '.' || p.src[p.pos] == 'e' || p.src[p.pos] == 'E' ||
|
||||
((p.src[p.pos] == '+' || p.src[p.pos] == '-') && p.pos > start &&
|
||||
(p.src[p.pos-1] == 'e' || p.src[p.pos-1] == 'E'))) {
|
||||
p.pos++
|
||||
}
|
||||
f, err := strconv.ParseFloat(p.src[start:p.pos], 64)
|
||||
|
||||
+11
-5
@@ -115,7 +115,7 @@ func (h *wsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
wg.Add(3)
|
||||
go func() { defer wg.Done(); c.readLoop(ctx, cancel) }()
|
||||
go func() { defer wg.Done(); c.dispatchLoop(ctx) }()
|
||||
go func() { defer wg.Done(); c.writeLoop(ctx) }()
|
||||
go func() { defer wg.Done(); c.writeLoop(ctx, cancel) }()
|
||||
wg.Wait()
|
||||
|
||||
// Cancel all subscriptions on disconnect.
|
||||
@@ -193,7 +193,10 @@ func (c *wsClient) dispatchLoop(ctx context.Context) {
|
||||
}
|
||||
|
||||
// writeLoop sends queued messages over the WebSocket connection.
|
||||
func (c *wsClient) writeLoop(ctx context.Context) {
|
||||
// It cancels ctx on any write error so that readLoop and dispatchLoop exit
|
||||
// promptly rather than blocking forever waiting for outCh to drain.
|
||||
func (c *wsClient) writeLoop(ctx context.Context, cancel context.CancelFunc) {
|
||||
defer cancel()
|
||||
for {
|
||||
select {
|
||||
case msg := <-c.outCh:
|
||||
@@ -251,8 +254,11 @@ func (c *wsClient) handleSubscribe(ctx context.Context, refs []sigRef) {
|
||||
c.subs[ref] = cancelSub
|
||||
c.subsMu.Unlock()
|
||||
|
||||
// Send metadata once on subscribe.
|
||||
c.sendMeta(ctx, ref)
|
||||
// Send metadata in a goroutine: for EPICS signals GetMetadata may block
|
||||
// for several seconds waiting for the CA channel to connect and for
|
||||
// ca_pend_io to complete. Running it inline would stall readLoop and
|
||||
// prevent any further messages from this client from being processed.
|
||||
go c.sendMeta(ctx, ref)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +347,7 @@ func (c *wsClient) handleHistory(ctx context.Context, msg inMsg) {
|
||||
b, _ := json.Marshal(resp)
|
||||
select {
|
||||
case c.outCh <- b:
|
||||
default:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user