package main import ( "fmt" "log" "math" "math/rand" "os" "sync" "time" "ble_simulator/internal/ble" "ble_simulator/internal/device" "ble_simulator/internal/tui" tea "github.com/charmbracelet/bubbletea" "tinygo.org/x/bluetooth" ) func main() { // Create shared state and log buffer state := device.NewDeviceState() logBuffer := tui.NewLogBuffer(500) logBuffer.Info("BLE Simulator v1.0") adapter := bluetooth.DefaultAdapter if err := adapter.Enable(); err != nil { log.Fatalf("Failed to enable BLE: %v", err) } // Channel for alarm notifications from TUI notifyCh := make(chan string, 10) // Channel for disconnect-all-clients signal disconnectCh := make(chan struct{}, 1) // Connection registry for disconnect-on-tamper connRegistry := make(map[string]bluetooth.Device) var connMu sync.Mutex // Connection handler adapter.SetConnectHandler(func(dev bluetooth.Device, connected bool) { connMu.Lock() if connected { connRegistry[dev.Address.String()] = dev state.IncrConnectedClients() logBuffer.Conn("Client Connected: %s", dev.Address.String()) } else { delete(connRegistry, dev.Address.String()) state.DecrConnectedClients() logBuffer.Conn("Client Disconnected") // Auto-disable prog mode when no clients connected if state.GetConnectedClients() == 0 { state.SetProgMode(false) } } connMu.Unlock() }) // Setup BLE service with logger notifyChar, err := ble.SetupService(adapter, state, logBuffer) if err != nil { log.Fatalf("Failed to add service: %v", err) } // Configure advertising adv := adapter.DefaultAdvertisement() if err := adv.Configure(bluetooth.AdvertisementOptions{ LocalName: "GO_SIMULATOR", ServiceUUIDs: []bluetooth.UUID{ble.ServiceUUID}, }); err != nil { log.Fatalf("Failed to configure advertising: %v", err) } if err := adv.Start(); err != nil { log.Fatalf("Failed to start advertising: %v", err) } logBuffer.Info("Advertising as GO_SIMULATOR...") logBuffer.Info("Using Nordic UART Service (NUS) UUIDs") logBuffer.Info("Service UUID: 6e400001-b5a3-f393-e0a9-e50e24dcca9e") logBuffer.Info("Command Char: 6e400002-b5a3-f393-e0a9-e50e24dcca9e (Write/RX)") logBuffer.Info("Notify Char: 6e400003-b5a3-f393-e0a9-e50e24dcca9e (Notify/TX)") logBuffer.Info("Waiting for connections...") // Start disconnect handler goroutine go func() { for range disconnectCh { connMu.Lock() for addr, dev := range connRegistry { if err := dev.Disconnect(); err != nil { logBuffer.Err("Failed to disconnect %s: %v", addr, err) } else { logBuffer.Info("Disconnected client: %s", addr) } } connMu.Unlock() } }() // Start notification loop (500ms heartbeat with sensor data) go notificationLoop(notifyChar, state, logBuffer, notifyCh) // Create and run TUI model := tui.NewModel(state, logBuffer, notifyCh, disconnectCh) p := tea.NewProgram(model, tea.WithAltScreen()) if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err) os.Exit(1) } } func notificationLoop(char *bluetooth.Characteristic, state *device.DeviceState, logger *tui.LogBuffer, notifyCh chan string) { ticker := time.NewTicker(500 * time.Millisecond) wasCali := false for { select { case <-ticker.C: // Adjust ticker rate based on calibration mode cali := state.GetCaliMode() if cali != wasCali { wasCali = cali if cali { ticker.Reset(250 * time.Millisecond) } else { ticker.Reset(500 * time.Millisecond) } } // Check for alarm state and send alarm message if active if alarmActive, alarmType := state.GetAlarmState(); alarmActive { msg := fmt.Sprintf("ALARM: %s", alarmType) _, err := char.Write([]byte(msg + "\r\n")) if err == nil { logger.TX("Sending: %s", msg) } } // Only send sensor data in programming mode if state.GetProgMode() { value := state.GetSensorValue() jitter := state.GetJitterRange() jitterVal := 0 if jitter > 0 { jitterVal = rand.Intn(jitter*2+1) - jitter } w20, _ := state.GetParam(20) gain := state.GetGain() nw := (float64(w20) - 1.0) / 127.0 - 1.0 // [1,255] → [-1, +1] ng := (float64(gain) - 1.0) / 49.5 - 1.0 // [1,100] → [-1, +1] jitterWeight := math.Exp2(nw + ng) // 2^(nw+ng): range [0.25, 4.0] valueWeight := 500.0 * (nw + ng + 2.0) // range [0, 2000], center 1000 sensor := (float64(value) + valueWeight) + (float64(jitterVal) * jitterWeight) msg := fmt.Sprintf("SENSOR:%d", int(sensor)) _, err := char.Write([]byte(msg + "\r\n")) if err != nil { // Silently ignore write errors (no subscribers) continue } logger.TX("Sending: %s", msg) } case alarmMsg := <-notifyCh: // Handle alarm notifications from TUI _, err := char.Write([]byte(alarmMsg + "\r\n")) if err == nil { logger.TX("Sending: %s", alarmMsg) } } } }