package main import ( "context" "math" "sync" "sync/atomic" "time" "github.com/wailsapp/wails/v2/pkg/runtime" ) // App struct — cada metodo publico es un binding IPC al frontend. type App struct { ctx context.Context hz atomic.Int64 running atomic.Bool mu sync.Mutex cancelGen context.CancelFunc stats Stats } type Stats struct { TotalEmitted int64 `json:"totalEmitted"` StartedAt int64 `json:"startedAt"` // unix ms CurrentHz int64 `json:"currentHz"` AvgLatency float64 `json:"avgLatency"` // ns per emit } type DataPoint struct { Value float64 `json:"value"` Timestamp int64 `json:"timestamp"` // unix ms Sequence int64 `json:"sequence"` } func NewApp() *App { a := &App{} a.hz.Store(10) return a } func (a *App) startup(ctx context.Context) { a.ctx = ctx } // --- Generador: Lorenz attractor (componente X) --- // dx/dt = sigma*(y-x), dy/dt = x*(rho-z)-y, dz/dt = x*y - beta*z // Produce valores caóticos deterministas — visualmente interesante. type lorenzState struct { x, y, z float64 } func lorenzStep(s lorenzState, dt float64) lorenzState { const sigma = 10.0 const rho = 28.0 const beta = 8.0 / 3.0 dx := sigma * (s.y - s.x) dy := s.x*(rho-s.z) - s.y dz := s.x*s.y - beta*s.z return lorenzState{ x: s.x + dx*dt, y: s.y + dy*dt, z: s.z + dz*dt, } } // Start arranca el generador de numeros. func (a *App) Start() { if a.running.Load() { return } a.running.Store(true) a.mu.Lock() ctx, cancel := context.WithCancel(a.ctx) a.cancelGen = cancel a.stats = Stats{StartedAt: time.Now().UnixMilli()} a.mu.Unlock() go a.generateLoop(ctx) } // Stop detiene el generador. func (a *App) Stop() { a.running.Store(false) a.mu.Lock() if a.cancelGen != nil { a.cancelGen() a.cancelGen = nil } a.mu.Unlock() } // SetHz cambia la frecuencia del generador en tiempo real. func (a *App) SetHz(hz int64) { if hz < 1 { hz = 1 } a.hz.Store(hz) } // GetHz retorna la frecuencia actual. func (a *App) GetHz() int64 { return a.hz.Load() } // GetStats retorna estadisticas del generador. func (a *App) GetStats() Stats { a.mu.Lock() defer a.mu.Unlock() s := a.stats s.CurrentHz = a.hz.Load() return s } // IsRunning retorna si el generador esta activo. func (a *App) IsRunning() bool { return a.running.Load() } // Ping verifica IPC. func (a *App) Ping() string { return "pong" } func (a *App) generateLoop(ctx context.Context) { state := lorenzState{x: 1.0, y: 1.0, z: 1.0} dt := 0.005 var seq int64 // Usar ticker dinámico — recalculamos el intervalo en cada iteración for { select { case <-ctx.Done(): return default: } hz := a.hz.Load() interval := time.Second / time.Duration(hz) start := time.Now() // Paso del atractor de Lorenz state = lorenzStep(state, dt) // Normalizar X a rango visible (~[-20, 20] -> [0, 100]) normalized := (state.x + 25.0) * (100.0 / 50.0) normalized = math.Max(0, math.Min(100, normalized)) seq++ point := DataPoint{ Value: math.Round(normalized*1000) / 1000, Timestamp: time.Now().UnixMilli(), Sequence: seq, } runtime.EventsEmit(a.ctx, "data:point", point) a.mu.Lock() a.stats.TotalEmitted = seq elapsed := time.Since(start).Nanoseconds() if a.stats.AvgLatency == 0 { a.stats.AvgLatency = float64(elapsed) } else { a.stats.AvgLatency = a.stats.AvgLatency*0.99 + float64(elapsed)*0.01 } a.mu.Unlock() // Sleep el tiempo restante del intervalo sleepTime := interval - time.Since(start) if sleepTime > 0 { time.Sleep(sleepTime) } } }