package main import ( "fmt" "log" "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) for { select { case <-ticker.C: // 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 } msg := fmt.Sprintf("SENSOR:%d", value+jitterVal) _, 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) } } } }