155 lines
4.1 KiB
Go
155 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|