simulator v1

This commit is contained in:
2026-02-06 15:31:53 +01:00
parent 69a9bea65b
commit 97427bb3c0
11 changed files with 720 additions and 1 deletions

48
internal/ble/service.go Normal file
View File

@@ -0,0 +1,48 @@
package ble
import (
"ble_simulator/internal/device"
"log"
"tinygo.org/x/bluetooth"
)
// SetupService configures the GATT service with command and notify characteristics
func SetupService(adapter *bluetooth.Adapter, state *device.DeviceState) (*bluetooth.Characteristic, error) {
var notifyChar bluetooth.Characteristic
err := adapter.AddService(&bluetooth.Service{
UUID: ServiceUUID,
Characteristics: []bluetooth.CharacteristicConfig{
{
UUID: CommandCharUUID,
Flags: bluetooth.CharacteristicWritePermission |
bluetooth.CharacteristicWriteWithoutResponsePermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
cmd := string(value)
log.Printf("[RX] Command: %s", cmd)
response := state.ProcessCommand(cmd)
log.Printf("[TX] Response: %s", response)
_, err := notifyChar.Write([]byte(response + "\n"))
if err != nil {
log.Printf("[ERR] Failed to send response: %v", err)
}
},
},
{
UUID: NotifyCharUUID,
Flags: bluetooth.CharacteristicReadPermission |
bluetooth.CharacteristicNotifyPermission,
Handle: &notifyChar,
},
},
})
if err != nil {
return nil, err
}
return &notifyChar, nil
}

28
internal/ble/uuids.go Normal file
View File

@@ -0,0 +1,28 @@
package ble
import "tinygo.org/x/bluetooth"
// Nordic UART Service (NUS) UUIDs - same as real V-BLACK sensor
// This allows the Flutter app to discover the simulator using the same scan filter
var (
// ServiceUUID - Nordic UART Service
// 6e400001-b5a3-f393-e0a9-e50e24dcca9e
ServiceUUID = bluetooth.NewUUID([16]byte{
0x6e, 0x40, 0x00, 0x01, 0xb5, 0xa3, 0xf3, 0x93,
0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e,
})
// CommandCharUUID - NUS RX (Write from app's perspective)
// 6e400002-b5a3-f393-e0a9-e50e24dcca9e
CommandCharUUID = bluetooth.NewUUID([16]byte{
0x6e, 0x40, 0x00, 0x02, 0xb5, 0xa3, 0xf3, 0x93,
0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e,
})
// NotifyCharUUID - NUS TX (Notify to app)
// 6e400003-b5a3-f393-e0a9-e50e24dcca9e
NotifyCharUUID = bluetooth.NewUUID([16]byte{
0x6e, 0x40, 0x00, 0x03, 0xb5, 0xa3, 0xf3, 0x93,
0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e,
})
)

161
internal/device/logic.go Normal file
View File

@@ -0,0 +1,161 @@
package device
import (
"fmt"
"strconv"
"strings"
)
// ProcessCommand handles incoming BLE commands and returns the response
func (s *DeviceState) ProcessCommand(cmd string) string {
cmd = strings.TrimSpace(cmd)
// Handle global commands
switch cmd {
case "PROG_ON":
s.SetProgMode(true)
return "PROG_MODE: ON"
case "PROG_OFF":
s.SetProgMode(false)
return "PROG_MODE: OFF"
case "RESET":
s.ResetAlarm()
return "INFO: Uscita allarme resettata (BT)."
case "CALI":
active := s.ToggleCaliMode()
if active {
return "--- MODALITA CALIBRAZIONE ATTIVA ---"
}
return "--- MODALITA CALIBRAZIONE DISATTIVA ---"
case "FACTORY":
s.ResetToDefaults()
return "FACTORY RESET DONE"
}
// Handle write commands (W1, W2, etc.)
if strings.HasPrefix(cmd, "W") {
return s.handleWriteCommand(cmd)
}
// Handle read commands (R1, R2, etc.)
if strings.HasPrefix(cmd, "R") {
return s.handleReadCommand(cmd)
}
return "ERRORE: Formato comando non valido"
}
// handleWriteCommand processes Wxx <value> commands
func (s *DeviceState) handleWriteCommand(cmd string) string {
// Check if in programming mode
if !s.GetProgMode() {
return "ERRORE: I comandi 'W' sono accettati solo in modalità programmazione."
}
// Parse command format: "W<id> <value>"
parts := strings.Fields(cmd)
if len(parts) != 2 {
return "ERRORE: Formato comando non valido"
}
// Extract parameter ID
idStr := strings.TrimPrefix(parts[0], "W")
id, err := strconv.Atoi(idStr)
if err != nil {
return "ERRORE: Formato comando non valido"
}
// Check if parameter ID is valid
if !isValidParamID(id) {
return "ERRORE: ID Sconosciuto"
}
// Parse value
value, err := strconv.Atoi(parts[1])
if err != nil {
return "ERRORE: Formato comando non valido"
}
// Validate range based on parameter ID
if !validateParamRange(id, value) {
return fmt.Sprintf("ERRORE: Valore fuori range per W%d", id)
}
// Set the parameter
s.SetParam(id, value)
return fmt.Sprintf("PARAM: W%d=%d", id, value)
}
// handleReadCommand processes Rxx commands
func (s *DeviceState) handleReadCommand(cmd string) string {
// Check if in programming mode
if !s.GetProgMode() {
return "ERRORE: I comandi 'W' sono accettati solo in modalità programmazione."
}
// Extract parameter ID
idStr := strings.TrimPrefix(cmd, "R")
id, err := strconv.Atoi(idStr)
if err != nil {
return "ERRORE: Formato comando non valido"
}
// Check if parameter ID is valid for reading
if !isReadableParamID(id) {
return "ERRORE: ID Sconosciuto"
}
// Get the parameter value
value, ok := s.GetParam(id)
if !ok {
return "ERRORE: ID Sconosciuto"
}
return fmt.Sprintf("PARAM: W%d=%d", id, value)
}
// isValidParamID checks if the parameter ID is writable
func isValidParamID(id int) bool {
validIDs := []int{1, 2, 3, 4, 11, 12, 20}
for _, v := range validIDs {
if id == v {
return true
}
}
return false
}
// isReadableParamID checks if the parameter ID is readable
func isReadableParamID(id int) bool {
// According to docs.toon, only R1, R2, R20 are documented
readableIDs := []int{1, 2, 20}
for _, v := range readableIDs {
if id == v {
return true
}
}
return false
}
// validateParamRange checks if value is within acceptable range for parameter
func validateParamRange(id, value int) bool {
ranges := map[int][2]int{
1: {0, 2000}, // W1 - Min Threshold
2: {0, 4095}, // W2 - Max Threshold
3: {1, 50}, // W3 - Pulse Count
4: {10, 1000}, // W4 - Time Window
11: {0, 1000}, // W11 - Min Pulse Duration
12: {0, 1000}, // W12 - Max Pulse Duration
20: {0, 255}, // W20 - Gain (Wiper)
}
r, ok := ranges[id]
if !ok {
return false
}
return value >= r[0] && value <= r[1]
}

122
internal/device/state.go Normal file
View File

@@ -0,0 +1,122 @@
package device
import "sync"
// DeviceState holds the simulated device state with thread-safe access
type DeviceState struct {
mu sync.RWMutex
progMode bool
caliMode bool
alarmActive bool
// Parameters from docs.toon
// W1=1200 (Min Threshold), W2=4000 (Max Threshold), W3=2 (Pulse Count),
// W4=40 (Time Window), W20=128 (Gain)
params map[int]int
sensorValue int // Default 2067 (ideal rest value)
}
// NewDeviceState creates a new DeviceState with default values
func NewDeviceState() *DeviceState {
return &DeviceState{
progMode: false,
caliMode: false,
alarmActive: false,
params: map[int]int{
1: 1200, // W1 - Min Threshold
2: 4000, // W2 - Max Threshold
3: 2, // W3 - Pulse Count
4: 40, // W4 - Time Window
11: 0, // W11 - Min Pulse Duration (reserved)
12: 0, // W12 - Max Pulse Duration (reserved)
20: 128, // W20 - Gain (Wiper)
},
sensorValue: 2067,
}
}
// GetProgMode returns whether programming mode is active
func (s *DeviceState) GetProgMode() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.progMode
}
// SetProgMode sets the programming mode
func (s *DeviceState) SetProgMode(enabled bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.progMode = enabled
}
// GetCaliMode returns whether calibration mode is active
func (s *DeviceState) GetCaliMode() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.caliMode
}
// ToggleCaliMode toggles the calibration mode
func (s *DeviceState) ToggleCaliMode() bool {
s.mu.Lock()
defer s.mu.Unlock()
s.caliMode = !s.caliMode
return s.caliMode
}
// GetAlarmActive returns whether the alarm is active
func (s *DeviceState) GetAlarmActive() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.alarmActive
}
// ResetAlarm resets the alarm state
func (s *DeviceState) ResetAlarm() {
s.mu.Lock()
defer s.mu.Unlock()
s.alarmActive = false
}
// GetParam returns a parameter value by ID
func (s *DeviceState) GetParam(id int) (int, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.params[id]
return val, ok
}
// SetParam sets a parameter value by ID
func (s *DeviceState) SetParam(id, value int) {
s.mu.Lock()
defer s.mu.Unlock()
s.params[id] = value
}
// GetSensorValue returns the current sensor value
func (s *DeviceState) GetSensorValue() int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.sensorValue
}
// ResetToDefaults resets all parameters to factory defaults
func (s *DeviceState) ResetToDefaults() {
s.mu.Lock()
defer s.mu.Unlock()
s.progMode = false
s.caliMode = false
s.alarmActive = false
s.params = map[int]int{
1: 1200,
2: 4000,
3: 2,
4: 40,
11: 0,
12: 0,
20: 128,
}
s.sensorValue = 2067
}