Add synthetic array (waveform) DSP support + UX improvements
Adds full array/waveform support through the synthetic DSP engine: a dsp.Sample value model (scalar or []float64), array ops (index, slice, sum, mean, min, max, length, fft) with an in-tree radix-2 FFT, and static type propagation (OpOutputType) that the editor mirrors to colour wires by data type and flag invalid wirings. Stateful filters and lua stay scalar-only. Adds a waveform plot mode (x-vs-index trace). Also: errored-node hover reasons, S/N add-signal/add-node HUD shortcuts in the synthetic editor, and view-mode widgets that blend with the canvas background (chrome kept in edit mode). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/uopi/uopi/internal/broker"
|
||||
"github.com/uopi/uopi/internal/datasource"
|
||||
"github.com/uopi/uopi/internal/dsp"
|
||||
)
|
||||
|
||||
const definitionsFile = "synthetic.json"
|
||||
@@ -78,7 +79,7 @@ func (s *Synthetic) ListSignals(_ context.Context) ([]datasource.Metadata, error
|
||||
|
||||
out := make([]datasource.Metadata, 0, len(s.signals))
|
||||
for _, st := range s.signals {
|
||||
out = append(out, defToMetadata(st.def))
|
||||
out = append(out, defToMetadata(st.def, outTypeOf(st)))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -93,7 +94,7 @@ func (s *Synthetic) FilteredMetadata(keep func(SignalDef) bool) []datasource.Met
|
||||
out := make([]datasource.Metadata, 0, len(s.signals))
|
||||
for _, st := range s.signals {
|
||||
if keep(st.def) {
|
||||
out = append(out, defToMetadata(st.def))
|
||||
out = append(out, defToMetadata(st.def, outTypeOf(st)))
|
||||
}
|
||||
}
|
||||
return out
|
||||
@@ -108,7 +109,7 @@ func (s *Synthetic) GetMetadata(_ context.Context, signal string) (datasource.Me
|
||||
if !ok {
|
||||
return datasource.Metadata{}, datasource.ErrNotFound
|
||||
}
|
||||
return defToMetadata(st.def), nil
|
||||
return defToMetadata(st.def, outTypeOf(st)), nil
|
||||
}
|
||||
|
||||
// Subscribe registers ch to receive computed values for the named signal.
|
||||
@@ -138,7 +139,7 @@ func (s *Synthetic) Subscribe(ctx context.Context, signal string, ch chan<- data
|
||||
defer cancel()
|
||||
|
||||
// Latest value and timestamp per source node id.
|
||||
latest := make(map[string]float64, len(refs))
|
||||
latest := make(map[string]dsp.Sample, len(refs))
|
||||
latestTs := make([]time.Time, len(refs))
|
||||
ready := make([]bool, len(refs))
|
||||
|
||||
@@ -163,7 +164,7 @@ func (s *Synthetic) Subscribe(ctx context.Context, signal string, ch chan<- data
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
val := toFloat64(u.Value.Data)
|
||||
val := toSample(u.Value.Data)
|
||||
select {
|
||||
case updateCh <- indexedUpdate{idx: idx, val: val, ts: u.Value.Timestamp}:
|
||||
default:
|
||||
@@ -224,7 +225,7 @@ func (s *Synthetic) Subscribe(ctx context.Context, signal string, ch chan<- data
|
||||
return
|
||||
}
|
||||
|
||||
result, err := cur.rg.eval(latest)
|
||||
result, err := cur.rg.evalSample(latest)
|
||||
if err != nil {
|
||||
s.log.Warn("synthetic: pipeline error", "signal", signal, "err", err)
|
||||
continue
|
||||
@@ -232,7 +233,7 @@ func (s *Synthetic) Subscribe(ctx context.Context, signal string, ch chan<- data
|
||||
|
||||
v := datasource.Value{
|
||||
Timestamp: outTs,
|
||||
Data: result,
|
||||
Data: result.AsAny(),
|
||||
Quality: datasource.QualityGood,
|
||||
}
|
||||
select {
|
||||
@@ -421,11 +422,17 @@ func (s *Synthetic) startSignal(def SignalDef) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// defToMetadata converts a SignalDef into a datasource.Metadata.
|
||||
func defToMetadata(def SignalDef) datasource.Metadata {
|
||||
// defToMetadata converts a SignalDef into a datasource.Metadata. outType is the
|
||||
// compiled graph's best-effort output type; an array output is reported as a
|
||||
// waveform (TypeFloat64Array) so widgets can pick a compatible view.
|
||||
func defToMetadata(def SignalDef, outType dsp.ValType) datasource.Metadata {
|
||||
dt := datasource.TypeFloat64
|
||||
if outType == dsp.ValArray {
|
||||
dt = datasource.TypeFloat64Array
|
||||
}
|
||||
return datasource.Metadata{
|
||||
Name: def.Name,
|
||||
Type: datasource.TypeFloat64,
|
||||
Type: dt,
|
||||
Unit: def.Meta.Unit,
|
||||
Description: def.Meta.Description,
|
||||
DisplayLow: def.Meta.DisplayLow,
|
||||
@@ -434,7 +441,38 @@ func defToMetadata(def SignalDef) datasource.Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
// toFloat64 coerces any numeric value from a datasource.Value.Data to float64.
|
||||
// outTypeOf returns the compiled output type for a signal state, or unknown.
|
||||
func outTypeOf(st *signalState) dsp.ValType {
|
||||
if st == nil || st.rg == nil {
|
||||
return dsp.ValUnknown
|
||||
}
|
||||
return st.rg.outType
|
||||
}
|
||||
|
||||
// toSample coerces a datasource.Value.Data into a dsp.Sample: arrays become
|
||||
// array Samples (waveforms), everything else a scalar Sample.
|
||||
func toSample(v any) dsp.Sample {
|
||||
switch val := v.(type) {
|
||||
case []float64:
|
||||
return dsp.Array(val)
|
||||
case []float32:
|
||||
out := make([]float64, len(val))
|
||||
for i, e := range val {
|
||||
out[i] = float64(e)
|
||||
}
|
||||
return dsp.Array(out)
|
||||
case []int:
|
||||
out := make([]float64, len(val))
|
||||
for i, e := range val {
|
||||
out[i] = float64(e)
|
||||
}
|
||||
return dsp.Array(out)
|
||||
default:
|
||||
return dsp.Scalar(toFloat64(v))
|
||||
}
|
||||
}
|
||||
|
||||
// toFloat64 coerces any numeric scalar value from a datasource.Value.Data to float64.
|
||||
func toFloat64(v any) float64 {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
@@ -460,6 +498,6 @@ func toFloat64(v any) float64 {
|
||||
// indexedUpdate carries a value from one upstream goroutine to the pipeline runner.
|
||||
type indexedUpdate struct {
|
||||
idx int
|
||||
val float64
|
||||
val dsp.Sample
|
||||
ts time.Time
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user