diff --git a/cmd/simulator/main.go b/cmd/simulator/main.go index 94968e8..53ffbfb 100644 --- a/cmd/simulator/main.go +++ b/cmd/simulator/main.go @@ -5,6 +5,7 @@ import ( "log" "math/rand" "os" + "sync" "time" "ble_simulator/internal/ble" @@ -30,15 +31,30 @@ func main() { // 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 @@ -67,11 +83,26 @@ func main() { 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) + model := tui.NewModel(state, logBuffer, notifyCh, disconnectCh) p := tea.NewProgram(model, tea.WithAltScreen()) if _, err := p.Run(); err != nil { @@ -88,31 +119,33 @@ func notificationLoop(char *bluetooth.Characteristic, state *device.DeviceState, // 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 + "\n")) + _, err := char.Write([]byte(msg + "\r\n")) if err == nil { logger.TX("Sending: %s", msg) } } - // Send regular sensor data - 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) + // 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 + "\n")) - if err != nil { - // Silently ignore write errors (no subscribers) - continue + _, err := char.Write([]byte(msg + "\r\n")) + if err != nil { + // Silently ignore write errors (no subscribers) + continue + } + logger.TX("Sending: %s", msg) } - logger.TX("Sending: %s", msg) case alarmMsg := <-notifyCh: // Handle alarm notifications from TUI - _, err := char.Write([]byte(alarmMsg + "\n")) + _, err := char.Write([]byte(alarmMsg + "\r\n")) if err == nil { logger.TX("Sending: %s", alarmMsg) } diff --git a/docs.md b/docs.md index c35e4de..0bef582 100644 --- a/docs.md +++ b/docs.md @@ -42,6 +42,7 @@ Possono essere inviati in qualsiasi momento. Sono case-insensitive. | **W20** | Gain (Wiper) | 0 - 255 | Potenziometro digitale (0: max, 255: min). | | **W11** | Durata Min Impulso | 0 - 1000 | Parametro avanzato/riservato. | | **W12** | Durata Max Impulso | 0 - 1000 | Parametro avanzato/riservato. | +| **WNC** | Normalmente Chiuso | 0 o 1 | Configurazione ingresso (0: NC, 1: NO). Default: 1. | **Risposta tipica:** `OK: Parametro W1 impostato e salvato: 1200` diff --git a/internal/ble/service.go b/internal/ble/service.go index a488d58..0d56de1 100644 --- a/internal/ble/service.go +++ b/internal/ble/service.go @@ -27,7 +27,7 @@ func SetupService(adapter *bluetooth.Adapter, state *device.DeviceState, logger // Non-blocking write in goroutine to avoid blocking the BLE stack go func() { - _, err := notifyChar.Write([]byte(response + "\n")) + _, err := notifyChar.Write([]byte(response + "\r\n")) if err != nil { logger.Err("Failed to send response: %v", err) } diff --git a/internal/device/logic.go b/internal/device/logic.go index 0b6948c..6ae2b25 100644 --- a/internal/device/logic.go +++ b/internal/device/logic.go @@ -56,6 +56,17 @@ func (s *DeviceState) handleWriteCommand(cmd string) string { return "ERRORE: I comandi 'W' sono accettati solo in modalità programmazione." } + // Handle special WNC command + if strings.HasPrefix(cmd, "WNC=") { + valueStr := strings.TrimPrefix(cmd, "WNC=") + value, err := strconv.Atoi(valueStr) + if err != nil || (value != 0 && value != 1) { + return "ERRORE: Valore fuori range per WNC" + } + s.SetNCParam(value == 1) + return fmt.Sprintf("OK: Parametro WNC impostato e salvato: %d", value) + } + // Parse command format: "W=" parts := strings.SplitN(cmd, "=", 2) if len(parts) != 2 { @@ -97,6 +108,15 @@ func (s *DeviceState) handleReadCommand(cmd string) string { return "ERRORE: I comandi 'W' sono accettati solo in modalità programmazione." } + // Handle special RNC command + if cmd == "RNC" { + value := 0 + if s.GetNCParam() { + value = 1 + } + return fmt.Sprintf("PARAM: WNC=%d", value) + } + // Extract parameter ID idStr := strings.TrimPrefix(cmd, "R") id, err := strconv.Atoi(idStr) diff --git a/internal/device/state.go b/internal/device/state.go index 64be144..4410afb 100644 --- a/internal/device/state.go +++ b/internal/device/state.go @@ -19,6 +19,8 @@ type DeviceState struct { // W4=40 (Time Window), W20=128 (Gain) params map[int]int + ncParam bool // NC parameter: true = NO (1), false = NC (0), default is NO + sensorValue int // Default 2067 (ideal rest value) jitterRange int // Default 10 (means ±10) @@ -41,6 +43,7 @@ func NewDeviceState() *DeviceState { 12: 1000, // W12 - Max Pulse Duration (max) 20: 127, // W20 - Gain (middle of 0-255) }, + ncParam: true, // Default NO mode (1) sensorValue: 2067, jitterRange: 10, connectedClients: 0, @@ -129,6 +132,7 @@ func (s *DeviceState) ResetToDefaults() { 12: 1000, 20: 127, } + s.ncParam = true // Default NO mode (1) s.sensorValue = 2067 s.jitterRange = 10 } @@ -176,6 +180,20 @@ func (s *DeviceState) GetAlarmState() (bool, string) { return s.alarmActive, s.alarmType } +// GetNCParam returns the NC parameter value (true = NO, false = NC) +func (s *DeviceState) GetNCParam() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.ncParam +} + +// SetNCParam sets the NC parameter value (true = NO, false = NC) +func (s *DeviceState) SetNCParam(value bool) { + s.mu.Lock() + defer s.mu.Unlock() + s.ncParam = value +} + // GetAllParams returns a copy of all parameters func (s *DeviceState) GetAllParams() map[int]int { s.mu.RLock() diff --git a/internal/tui/model.go b/internal/tui/model.go index 0b9900c..11f6fa8 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -34,9 +34,10 @@ const ( // Model is the main Bubble Tea model type Model struct { - state *device.DeviceState - logBuffer *LogBuffer - notifyCh chan string // Channel to send alarm notifications + state *device.DeviceState + logBuffer *LogBuffer + notifyCh chan string // Channel to send alarm notifications + disconnectCh chan struct{} // Channel to trigger disconnect all clients controls []Control focusedCtrl int @@ -51,11 +52,12 @@ type Model struct { } // NewModel creates a new TUI model -func NewModel(state *device.DeviceState, logBuffer *LogBuffer, notifyCh chan string) Model { +func NewModel(state *device.DeviceState, logBuffer *LogBuffer, notifyCh chan string, disconnectCh chan struct{}) Model { m := Model{ state: state, logBuffer: logBuffer, notifyCh: notifyCh, + disconnectCh: disconnectCh, focusedCtrl: 0, focusedPanel: PanelControls, logOffset: 0, @@ -84,13 +86,19 @@ func NewModel(state *device.DeviceState, logBuffer *LogBuffer, notifyCh chan str Type: ControlButton, Action: func() { state.TriggerAlarm("TAMPER") - logBuffer.Info("Tamper alarm triggered manually") + logBuffer.Info("Tamper alarm triggered - disconnecting all clients") if notifyCh != nil { select { case notifyCh <- "ALARM: TAMPER": default: } } + if disconnectCh != nil { + select { + case disconnectCh <- struct{}{}: + default: + } + } }, }, { diff --git a/internal/tui/view.go b/internal/tui/view.go index d82f9ff..a6126dd 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -172,6 +172,13 @@ func (m Model) renderControlsPanel(width int) string { b.WriteString(fmt.Sprintf("%s %s %d\n", sliderLabelStyle.Render(name), bar, val)) } + // NC parameter display + ncValue := "True" + if !m.state.GetNCParam() { + ncValue = "False" + } + b.WriteString(fmt.Sprintf("%s %s\n", sliderLabelStyle.Render("WNC (Tru/Fal)"), ncValue)) + // Status section b.WriteString("\n") b.WriteString(titleStyle.Render("Status"))