update emulator
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ble_simulator/internal/ble"
|
"ble_simulator/internal/ble"
|
||||||
@@ -30,15 +31,30 @@ func main() {
|
|||||||
// Channel for alarm notifications from TUI
|
// Channel for alarm notifications from TUI
|
||||||
notifyCh := make(chan string, 10)
|
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
|
// Connection handler
|
||||||
adapter.SetConnectHandler(func(dev bluetooth.Device, connected bool) {
|
adapter.SetConnectHandler(func(dev bluetooth.Device, connected bool) {
|
||||||
|
connMu.Lock()
|
||||||
if connected {
|
if connected {
|
||||||
|
connRegistry[dev.Address.String()] = dev
|
||||||
state.IncrConnectedClients()
|
state.IncrConnectedClients()
|
||||||
logBuffer.Conn("Client Connected: %s", dev.Address.String())
|
logBuffer.Conn("Client Connected: %s", dev.Address.String())
|
||||||
} else {
|
} else {
|
||||||
|
delete(connRegistry, dev.Address.String())
|
||||||
state.DecrConnectedClients()
|
state.DecrConnectedClients()
|
||||||
logBuffer.Conn("Client Disconnected")
|
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
|
// Setup BLE service with logger
|
||||||
@@ -67,11 +83,26 @@ func main() {
|
|||||||
logBuffer.Info("Notify Char: 6e400003-b5a3-f393-e0a9-e50e24dcca9e (Notify/TX)")
|
logBuffer.Info("Notify Char: 6e400003-b5a3-f393-e0a9-e50e24dcca9e (Notify/TX)")
|
||||||
logBuffer.Info("Waiting for connections...")
|
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)
|
// Start notification loop (500ms heartbeat with sensor data)
|
||||||
go notificationLoop(notifyChar, state, logBuffer, notifyCh)
|
go notificationLoop(notifyChar, state, logBuffer, notifyCh)
|
||||||
|
|
||||||
// Create and run TUI
|
// Create and run TUI
|
||||||
model := tui.NewModel(state, logBuffer, notifyCh)
|
model := tui.NewModel(state, logBuffer, notifyCh, disconnectCh)
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||||
|
|
||||||
if _, err := p.Run(); err != nil {
|
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
|
// Check for alarm state and send alarm message if active
|
||||||
if alarmActive, alarmType := state.GetAlarmState(); alarmActive {
|
if alarmActive, alarmType := state.GetAlarmState(); alarmActive {
|
||||||
msg := fmt.Sprintf("ALARM: %s", alarmType)
|
msg := fmt.Sprintf("ALARM: %s", alarmType)
|
||||||
_, err := char.Write([]byte(msg + "\n"))
|
_, err := char.Write([]byte(msg + "\r\n"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.TX("Sending: %s", msg)
|
logger.TX("Sending: %s", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send regular sensor data
|
// Only send sensor data in programming mode
|
||||||
value := state.GetSensorValue()
|
if state.GetProgMode() {
|
||||||
jitter := state.GetJitterRange()
|
value := state.GetSensorValue()
|
||||||
jitterVal := 0
|
jitter := state.GetJitterRange()
|
||||||
if jitter > 0 {
|
jitterVal := 0
|
||||||
jitterVal = rand.Intn(jitter*2+1) - jitter
|
if jitter > 0 {
|
||||||
}
|
jitterVal = rand.Intn(jitter*2+1) - jitter
|
||||||
msg := fmt.Sprintf("SENSOR:%d", value+jitterVal)
|
}
|
||||||
|
msg := fmt.Sprintf("SENSOR:%d", value+jitterVal)
|
||||||
|
|
||||||
_, err := char.Write([]byte(msg + "\n"))
|
_, err := char.Write([]byte(msg + "\r\n"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Silently ignore write errors (no subscribers)
|
// Silently ignore write errors (no subscribers)
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
logger.TX("Sending: %s", msg)
|
||||||
}
|
}
|
||||||
logger.TX("Sending: %s", msg)
|
|
||||||
|
|
||||||
case alarmMsg := <-notifyCh:
|
case alarmMsg := <-notifyCh:
|
||||||
// Handle alarm notifications from TUI
|
// Handle alarm notifications from TUI
|
||||||
_, err := char.Write([]byte(alarmMsg + "\n"))
|
_, err := char.Write([]byte(alarmMsg + "\r\n"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.TX("Sending: %s", alarmMsg)
|
logger.TX("Sending: %s", alarmMsg)
|
||||||
}
|
}
|
||||||
|
|||||||
1
docs.md
1
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). |
|
| **W20** | Gain (Wiper) | 0 - 255 | Potenziometro digitale (0: max, 255: min). |
|
||||||
| **W11** | Durata Min Impulso | 0 - 1000 | Parametro avanzato/riservato. |
|
| **W11** | Durata Min Impulso | 0 - 1000 | Parametro avanzato/riservato. |
|
||||||
| **W12** | Durata Max 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`
|
**Risposta tipica:** `OK: Parametro W1 impostato e salvato: 1200`
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func SetupService(adapter *bluetooth.Adapter, state *device.DeviceState, logger
|
|||||||
|
|
||||||
// Non-blocking write in goroutine to avoid blocking the BLE stack
|
// Non-blocking write in goroutine to avoid blocking the BLE stack
|
||||||
go func() {
|
go func() {
|
||||||
_, err := notifyChar.Write([]byte(response + "\n"))
|
_, err := notifyChar.Write([]byte(response + "\r\n"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Err("Failed to send response: %v", err)
|
logger.Err("Failed to send response: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,17 @@ func (s *DeviceState) handleWriteCommand(cmd string) string {
|
|||||||
return "ERRORE: I comandi 'W' sono accettati solo in modalità programmazione."
|
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<id>=<value>"
|
// Parse command format: "W<id>=<value>"
|
||||||
parts := strings.SplitN(cmd, "=", 2)
|
parts := strings.SplitN(cmd, "=", 2)
|
||||||
if len(parts) != 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."
|
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
|
// Extract parameter ID
|
||||||
idStr := strings.TrimPrefix(cmd, "R")
|
idStr := strings.TrimPrefix(cmd, "R")
|
||||||
id, err := strconv.Atoi(idStr)
|
id, err := strconv.Atoi(idStr)
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ type DeviceState struct {
|
|||||||
// W4=40 (Time Window), W20=128 (Gain)
|
// W4=40 (Time Window), W20=128 (Gain)
|
||||||
params map[int]int
|
params map[int]int
|
||||||
|
|
||||||
|
ncParam bool // NC parameter: true = NO (1), false = NC (0), default is NO
|
||||||
|
|
||||||
sensorValue int // Default 2067 (ideal rest value)
|
sensorValue int // Default 2067 (ideal rest value)
|
||||||
jitterRange int // Default 10 (means ±10)
|
jitterRange int // Default 10 (means ±10)
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ func NewDeviceState() *DeviceState {
|
|||||||
12: 1000, // W12 - Max Pulse Duration (max)
|
12: 1000, // W12 - Max Pulse Duration (max)
|
||||||
20: 127, // W20 - Gain (middle of 0-255)
|
20: 127, // W20 - Gain (middle of 0-255)
|
||||||
},
|
},
|
||||||
|
ncParam: true, // Default NO mode (1)
|
||||||
sensorValue: 2067,
|
sensorValue: 2067,
|
||||||
jitterRange: 10,
|
jitterRange: 10,
|
||||||
connectedClients: 0,
|
connectedClients: 0,
|
||||||
@@ -129,6 +132,7 @@ func (s *DeviceState) ResetToDefaults() {
|
|||||||
12: 1000,
|
12: 1000,
|
||||||
20: 127,
|
20: 127,
|
||||||
}
|
}
|
||||||
|
s.ncParam = true // Default NO mode (1)
|
||||||
s.sensorValue = 2067
|
s.sensorValue = 2067
|
||||||
s.jitterRange = 10
|
s.jitterRange = 10
|
||||||
}
|
}
|
||||||
@@ -176,6 +180,20 @@ func (s *DeviceState) GetAlarmState() (bool, string) {
|
|||||||
return s.alarmActive, s.alarmType
|
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
|
// GetAllParams returns a copy of all parameters
|
||||||
func (s *DeviceState) GetAllParams() map[int]int {
|
func (s *DeviceState) GetAllParams() map[int]int {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ const (
|
|||||||
|
|
||||||
// Model is the main Bubble Tea model
|
// Model is the main Bubble Tea model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
state *device.DeviceState
|
state *device.DeviceState
|
||||||
logBuffer *LogBuffer
|
logBuffer *LogBuffer
|
||||||
notifyCh chan string // Channel to send alarm notifications
|
notifyCh chan string // Channel to send alarm notifications
|
||||||
|
disconnectCh chan struct{} // Channel to trigger disconnect all clients
|
||||||
|
|
||||||
controls []Control
|
controls []Control
|
||||||
focusedCtrl int
|
focusedCtrl int
|
||||||
@@ -51,11 +52,12 @@ type Model struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewModel creates a new TUI model
|
// 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{
|
m := Model{
|
||||||
state: state,
|
state: state,
|
||||||
logBuffer: logBuffer,
|
logBuffer: logBuffer,
|
||||||
notifyCh: notifyCh,
|
notifyCh: notifyCh,
|
||||||
|
disconnectCh: disconnectCh,
|
||||||
focusedCtrl: 0,
|
focusedCtrl: 0,
|
||||||
focusedPanel: PanelControls,
|
focusedPanel: PanelControls,
|
||||||
logOffset: 0,
|
logOffset: 0,
|
||||||
@@ -84,13 +86,19 @@ func NewModel(state *device.DeviceState, logBuffer *LogBuffer, notifyCh chan str
|
|||||||
Type: ControlButton,
|
Type: ControlButton,
|
||||||
Action: func() {
|
Action: func() {
|
||||||
state.TriggerAlarm("TAMPER")
|
state.TriggerAlarm("TAMPER")
|
||||||
logBuffer.Info("Tamper alarm triggered manually")
|
logBuffer.Info("Tamper alarm triggered - disconnecting all clients")
|
||||||
if notifyCh != nil {
|
if notifyCh != nil {
|
||||||
select {
|
select {
|
||||||
case notifyCh <- "ALARM: TAMPER":
|
case notifyCh <- "ALARM: TAMPER":
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if disconnectCh != nil {
|
||||||
|
select {
|
||||||
|
case disconnectCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -172,6 +172,13 @@ func (m Model) renderControlsPanel(width int) string {
|
|||||||
b.WriteString(fmt.Sprintf("%s %s %d\n", sliderLabelStyle.Render(name), bar, val))
|
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
|
// Status section
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(titleStyle.Render("Status"))
|
b.WriteString(titleStyle.Render("Status"))
|
||||||
|
|||||||
Reference in New Issue
Block a user