Files
Martino Ferrari 6ff8fb5c25 Improved perfs
2026-05-12 10:16:48 +02:00

229 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package stub provides a synthetic data source that emits deterministic test
// signals (sine waves, ramp, boolean toggle) without any external dependencies.
// It is used during development and for integration tests.
package stub
import (
"context"
"fmt"
"math"
"math/rand/v2"
"sync"
"time"
"github.com/uopi/uopi/internal/datasource"
)
const updateInterval = 100 * time.Millisecond // 10 Hz default
type signalDef struct {
meta datasource.Metadata
interval time.Duration // 0 → updateInterval
fn func(t time.Time) any
}
// Stub is a data source that emits canned signals for development and testing.
type Stub struct {
signals map[string]signalDef
// writable values (signal name → current value)
mu sync.RWMutex
values map[string]any
}
// New returns a Stub with a predefined set of test signals.
func New() *Stub {
s := &Stub{
signals: make(map[string]signalDef),
values: make(map[string]any),
}
s.register(signalDef{
meta: datasource.Metadata{
Name: "sine_1hz", Type: datasource.TypeFloat64,
Unit: "V", DisplayLow: -1, DisplayHigh: 1,
Description: "1 Hz sine wave",
},
fn: func(t time.Time) any {
return math.Sin(2 * math.Pi * float64(t.UnixNano()) / 1e9)
},
})
s.register(signalDef{
meta: datasource.Metadata{
Name: "sine_01hz", Type: datasource.TypeFloat64,
Unit: "A", DisplayLow: -1, DisplayHigh: 1,
Description: "0.1 Hz sine wave",
},
fn: func(t time.Time) any {
return math.Sin(2 * math.Pi * 0.1 * float64(t.UnixNano()) / 1e9)
},
})
s.register(signalDef{
meta: datasource.Metadata{
Name: "ramp_10s", Type: datasource.TypeFloat64,
Unit: "mm", DisplayLow: 0, DisplayHigh: 100,
Description: "10-second sawtooth ramp, 0100",
},
fn: func(t time.Time) any {
return math.Mod(float64(t.UnixNano())/1e10, 100)
},
})
s.register(signalDef{
meta: datasource.Metadata{
Name: "toggle_1hz", Type: datasource.TypeBool,
Description: "1 Hz square wave (boolean)",
},
fn: func(t time.Time) any { return (t.Unix() % 2) == 0 },
})
s.register(signalDef{
meta: datasource.Metadata{
Name: "noise", Type: datasource.TypeFloat64,
Unit: "dB", DisplayLow: -1, DisplayHigh: 1,
Description: "White noise",
},
fn: func(t time.Time) any {
// Seed a per-tick RNG from the timestamp so results are
// reproducible within a tick but vary across ticks.
r := rand.New(rand.NewPCG(uint64(t.UnixNano()), 0))
return r.Float64()*2 - 1
},
})
s.register(signalDef{
meta: datasource.Metadata{
Name: "setpoint", Type: datasource.TypeFloat64,
Unit: "°C", DisplayLow: 0, DisplayHigh: 200,
DriveLow: 0, DriveHigh: 200,
Description: "Writable setpoint (stub)",
Writable: true,
},
fn: func(t time.Time) any { return 25.0 }, // default; overwritten by Write
})
s.values["setpoint"] = 25.0
s.register(signalDef{
meta: datasource.Metadata{
Name: "counter_fast", Type: datasource.TypeFloat64,
Description: "Fast monotonic counter (1 ms tick) for benchmarks",
},
interval: time.Millisecond,
fn: func(t time.Time) any { return float64(t.UnixMilli()) },
})
return s
}
// NewN creates a Stub with n dynamically generated signals named "pv_0" … "pv_{n-1}".
// Each signal is a sine wave at a distinct frequency, emitted at 10 Hz.
// Use this for stress and scale testing.
func NewN(n int) *Stub {
s := &Stub{
signals: make(map[string]signalDef),
values: make(map[string]any),
}
for i := range n {
freq := 0.1 + float64(i)*0.01 // spread frequencies so signals differ
s.register(signalDef{
meta: datasource.Metadata{
Name: fmt.Sprintf("pv_%d", i),
Type: datasource.TypeFloat64,
Unit: "V",
DisplayLow: -1,
DisplayHigh: 1,
Description: fmt.Sprintf("Synthetic PV #%d (%.2f Hz sine)", i, freq),
},
fn: func(t time.Time) any {
return math.Sin(2 * math.Pi * freq * float64(t.UnixNano()) / 1e9)
},
})
}
return s
}
func (s *Stub) register(d signalDef) {
s.signals[d.meta.Name] = d
}
// Name implements datasource.DataSource.
func (s *Stub) Name() string { return "stub" }
// Connect is a no-op for the stub source.
func (s *Stub) Connect(_ context.Context) error { return nil }
// ListSignals returns metadata for all built-in stub signals.
func (s *Stub) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
out := make([]datasource.Metadata, 0, len(s.signals))
for _, d := range s.signals {
out = append(out, d.meta)
}
return out, nil
}
// GetMetadata returns the metadata for the named signal.
func (s *Stub) GetMetadata(_ context.Context, signal string) (datasource.Metadata, error) {
d, ok := s.signals[signal]
if !ok {
return datasource.Metadata{}, datasource.ErrNotFound
}
return d.meta, nil
}
// Subscribe starts a ticker that pushes generated values into ch.
// The tick interval is the signal's configured interval (default 10 Hz).
func (s *Stub) Subscribe(ctx context.Context, signal string, ch chan<- datasource.Value) (datasource.CancelFunc, error) {
def, ok := s.signals[signal]
if !ok {
return nil, datasource.ErrNotFound
}
interval := def.interval
if interval == 0 {
interval = updateInterval
}
ctx, cancel := context.WithCancel(ctx)
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
var data any
if def.meta.Writable {
s.mu.RLock()
data = s.values[signal]
s.mu.RUnlock()
} else {
data = def.fn(t)
}
v := datasource.Value{Timestamp: t, Data: data, Quality: datasource.QualityGood}
select {
case ch <- v:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}()
return datasource.CancelFunc(cancel), nil
}
// Write updates the current value of a writable signal.
func (s *Stub) Write(_ context.Context, signal string, value any) error {
def, ok := s.signals[signal]
if !ok {
return datasource.ErrNotFound
}
if !def.meta.Writable {
return datasource.ErrNotWritable
}
s.mu.Lock()
s.values[signal] = value
s.mu.Unlock()
return nil
}
// History is not supported by the stub source.
func (s *Stub) History(_ context.Context, _ string, _, _ time.Time, _ int) ([]datasource.Value, error) {
return nil, datasource.ErrHistoryUnavailable
}