From 97427bb3c0e601280c83276ea704feb8c3e8aab5 Mon Sep 17 00:00:00 2001 From: Giuseppe Tufo Date: Fri, 6 Feb 2026 15:31:53 +0100 Subject: [PATCH] simulator v1 --- .gitignore | 1 + README.md | 110 +++++++++++++++++++++++++- cmd/simulator/main.go | 79 +++++++++++++++++++ docs.toon | 39 ++++++++++ go.mod | 17 +++++ go.sum | 35 +++++++++ internal/ble/service.go | 48 ++++++++++++ internal/ble/uuids.go | 28 +++++++ internal/device/logic.go | 161 +++++++++++++++++++++++++++++++++++++++ internal/device/state.go | 122 +++++++++++++++++++++++++++++ tasks.yaml | 81 ++++++++++++++++++++ 11 files changed, 720 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 cmd/simulator/main.go create mode 100644 docs.toon create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/ble/service.go create mode 100644 internal/ble/uuids.go create mode 100644 internal/device/logic.go create mode 100644 internal/device/state.go create mode 100644 tasks.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6050701 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +simulator diff --git a/README.md b/README.md index 89ff1bd..6b47a51 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,110 @@ -# BLE_emulator +# V-BLACK BLE Simulator +A real BLE GATT server in Go that simulates a V-BLACK device using `tinygo.org/x/bluetooth`. + +## Platform Support + +- **Linux**: Requires BlueZ 5.48+. May need `sudo` for BLE access. +- **Windows**: Works out of the box with WinRT. +- **macOS**: NOT SUPPORTED - CoreBluetooth doesn't allow peripheral mode. + +## Building + +```bash +go build ./cmd/simulator +``` + +## Running + +```bash +# Linux (may require root) +sudo ./simulator + +# Windows +simulator.exe +``` + +## BLE UUIDs + +Uses **Nordic UART Service (NUS)** UUIDs - same as real V-BLACK sensors: + +| Characteristic | UUID | Properties | +|----------------|------|------------| +| Service | `6e400001-b5a3-f393-e0a9-e50e24dcca9e` | - | +| Command (RX) | `6e400002-b5a3-f393-e0a9-e50e24dcca9e` | Write | +| Notify (TX) | `6e400003-b5a3-f393-e0a9-e50e24dcca9e` | Read, Notify | + +### Flutter Example + +```dart +const serviceUuid = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; +const commandCharUuid = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; +const notifyCharUuid = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; +``` + +## Commands + +### Global Commands + +| Command | Response | Description | +|---------|----------|-------------| +| `PROG_ON` | `PROG_MODE: ON` | Enable programming mode | +| `PROG_OFF` | `PROG_MODE: OFF` | Exit programming mode | +| `RESET` | `INFO: Uscita allarme resettata (BT).` | Reset alarm output | +| `CALI` | `--- MODALITA CALIBRAZIONE ATTIVA ---` | Toggle calibration mode | +| `FACTORY` | `FACTORY RESET DONE` | Restore factory defaults | + +### Write Parameters (requires PROG_ON first) + +| Command | Range | Default | Description | +|---------|-------|---------|-------------| +| `W1 ` | 0-2000 | 1200 | Min threshold | +| `W2 ` | 0-4095 | 4000 | Max threshold | +| `W3 ` | 1-50 | 2 | Pulse count | +| `W4 ` | 10-1000 | 40 | Time window (ms) | +| `W20 ` | 0-255 | 128 | Gain (wiper) | + +### Read Parameters (requires PROG_ON first) + +| Command | Response | +|---------|----------| +| `R1` | `PARAM: W1=` | +| `R2` | `PARAM: W2=` | +| `R20` | `PARAM: W20=` | + +## Notifications + +The simulator sends sensor readings every 500ms: +``` +SENSOR:2067 +SENSOR:2063 +SENSOR:2071 +``` + +## Testing with nRF Connect + +1. Run the simulator +2. Open nRF Connect app on your phone +3. Scan for "GO_SIMULATOR" +4. Connect to the device +5. Find service `6e400001-...` (Nordic UART Service) +6. Subscribe to notify characteristic `6e400003-...` (NUS TX) +7. You should see `SENSOR:xxxx` every 500ms +8. Write `PROG_ON` to command characteristic `6e400002-...` (NUS RX) +9. You should receive `PROG_MODE: ON` on notify + +## Console Output + +``` +[INFO] BLE Simulator v1.0 +[INFO] Advertising as GO_SIMULATOR... +[INFO] Waiting for connections... +[CONN] Client Connected: AA:BB:CC:DD:EE:FF +[RX] Command: PROG_ON +[TX] Response: PROG_MODE: ON +[TX] Sending: SENSOR:2063 +[TX] Sending: SENSOR:2071 +[RX] Command: R1 +[TX] Response: PARAM: W1=1200 +[CONN] Client Disconnected +``` diff --git a/cmd/simulator/main.go b/cmd/simulator/main.go new file mode 100644 index 0000000..e852f4d --- /dev/null +++ b/cmd/simulator/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "time" + + "ble_simulator/internal/ble" + "ble_simulator/internal/device" + + "tinygo.org/x/bluetooth" +) + +func main() { + log.Println("[INFO] BLE Simulator v1.0") + + adapter := bluetooth.DefaultAdapter + must("enable BLE", adapter.Enable()) + + // Connection handler + adapter.SetConnectHandler(func(dev bluetooth.Device, connected bool) { + if connected { + log.Printf("[CONN] Client Connected: %s", dev.Address.String()) + } else { + log.Println("[CONN] Client Disconnected") + } + }) + + // Setup device state and service + state := device.NewDeviceState() + notifyChar, err := ble.SetupService(adapter, state) + must("add service", err) + + // Configure advertising + adv := adapter.DefaultAdvertisement() + must("configure adv", adv.Configure(bluetooth.AdvertisementOptions{ + LocalName: "GO_SIMULATOR", + ServiceUUIDs: []bluetooth.UUID{ble.ServiceUUID}, + })) + + must("start advertising", adv.Start()) + + log.Println("[INFO] Advertising as GO_SIMULATOR...") + log.Println("[INFO] Using Nordic UART Service (NUS) UUIDs") + log.Println("[INFO] Service UUID: 6e400001-b5a3-f393-e0a9-e50e24dcca9e") + log.Println("[INFO] Command Char: 6e400002-b5a3-f393-e0a9-e50e24dcca9e (Write/RX)") + log.Println("[INFO] Notify Char: 6e400003-b5a3-f393-e0a9-e50e24dcca9e (Notify/TX)") + log.Println("[INFO] Waiting for connections...") + + // Start notification loop (500ms heartbeat with sensor data) + go notificationLoop(notifyChar, state) + + // Block forever + select {} +} + +func notificationLoop(char *bluetooth.Characteristic, state *device.DeviceState) { + ticker := time.NewTicker(500 * time.Millisecond) + for range ticker.C { + value := state.GetSensorValue() + // Add small jitter for realism (-10 to +10) + jitter := rand.Intn(21) - 10 + msg := fmt.Sprintf("SENSOR:%d", value+jitter) + + _, err := char.Write([]byte(msg + "\n")) + if err != nil { + // Silently ignore write errors (no subscribers) + continue + } + log.Printf("[TX] Sending: %s", msg) + } +} + +func must(action string, err error) { + if err != nil { + log.Fatalf("Failed to %s: %v", action, err) + } +} diff --git a/docs.toon b/docs.toon new file mode 100644 index 0000000..023cf0c --- /dev/null +++ b/docs.toon @@ -0,0 +1,39 @@ +# V-BLACK Command Protocol Manual (nRF52) +# Date: 2026-01-19 | Transport: BLE NUS + +introduction: + rx_char: Commands (Write) + tx_char: Data/Responses (Notify) + mandatory_step: Send PROG_ON before any W or R command + +global_commands[5]{cmd, description, response}: + PROG_ON, Enable Program Mode / Stop Alarm, PROG_MODE: ON + PROG_OFF, Exit Program Mode / Save Data, PROG_MODE: OFF + RESET, Reset alarm output and counters, INFO: Uscita allarme resettata (BT). + CALI, Toggle Diagnostic RAW Mode, --- MODALITA CALIBRAZIONE ATTIVA --- + FACTORY, Restore factory defaults, FACTORY RESET DONE + +write_parameters[7]{id, name, range, default, desc}: + W1, Min Threshold, 0-2000, 1200, Impact sensitivity (100-500 high) + W2, Max Threshold, 0-4095, 4000, Upper noise filter + W3, Pulse Count, 1-50, 2, Pulses for alarm trigger + W4, Time Window, 10-1000, 40, Analysis cycle in ms + W20, Gain (Wiper), 0-255, 128, Digital pot (0:max) + W11, Min Pulse Dur, 0-1000, -, Advanced/Reserved + W12, Max Pulse Dur, 0-1000, -, Advanced/Reserved + +read_commands[3]{cmd, response_format, meaning}: + R1, PARAM: W1=, Read Min Threshold + R2, PARAM: W2=, Read Max Threshold + R20, PARAM: W20=, Read Gain (Wiper) + +notifications: + heartbeat: {format: "SENSOR:", interval: 500ms, ideal_rest: 2067} + alarms[3]{msg, cause}: + ALARM: TRIGGERED, Vibration threshold met + ALARM: TAMPER, Wire cut/short (0 or 4095) + INFO: Allarme resettato automaticamente, Siren timeout (5s) + errors: + - ERRORE: I comandi 'W' sono accettati solo in modalità programmazione. + - ERRORE: ID Sconosciuto + - ERRORE: Formato comando non valido diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..41eb2f6 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module ble_simulator + +go 1.25.6 + +require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af // indirect + github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect + github.com/tinygo-org/cbgo v0.0.4 // indirect + github.com/tinygo-org/pio v0.2.0 // indirect + golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect + golang.org/x/sys v0.11.0 // indirect + tinygo.org/x/bluetooth v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f621139 --- /dev/null +++ b/go.sum @@ -0,0 +1,35 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik= +github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af h1:ZfFq94aH/BCSWWKd9RPUgdHOdgGKCnfl2VdvU9UksTA= +github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af/go.mod h1:MUaGO5m6X7xrkHrPDmnaxCEcuCCFN/0ZFh9oie+exbU= +github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA= +github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU= +github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk= +github.com/tinygo-org/pio v0.2.0 h1:vo3xa6xDZ2rVtxrks/KcTZHF3qq4lyWOntvEvl2pOhU= +github.com/tinygo-org/pio v0.2.0/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +tinygo.org/x/bluetooth v0.14.0 h1:rrUaT+Fu6O0phGm4Y5UZULL8F7UahOq/JwGAPjJm+V4= +tinygo.org/x/bluetooth v0.14.0/go.mod h1:YnyJRVX09i+wkFeHpXut0b+qHq+T2WwKBRRiF/scANA= diff --git a/internal/ble/service.go b/internal/ble/service.go new file mode 100644 index 0000000..6b0ebac --- /dev/null +++ b/internal/ble/service.go @@ -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: ¬ifyChar, + }, + }, + }) + + if err != nil { + return nil, err + } + + return ¬ifyChar, nil +} diff --git a/internal/ble/uuids.go b/internal/ble/uuids.go new file mode 100644 index 0000000..0932b01 --- /dev/null +++ b/internal/ble/uuids.go @@ -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, + }) +) diff --git a/internal/device/logic.go b/internal/device/logic.go new file mode 100644 index 0000000..b74ed25 --- /dev/null +++ b/internal/device/logic.go @@ -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 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 " + 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] +} diff --git a/internal/device/state.go b/internal/device/state.go new file mode 100644 index 0000000..b1a0b8f --- /dev/null +++ b/internal/device/state.go @@ -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 +} diff --git a/tasks.yaml b/tasks.yaml new file mode 100644 index 0000000..23c785b --- /dev/null +++ b/tasks.yaml @@ -0,0 +1,81 @@ +project_name: ble_simulator_real_go +context: + role: Senior Go Developer & BLE Protocol Engineer + goal: Create a CLI application in Go that acts as a real BLE Peripheral (GATT Server). + target_hardware: The Go app runs on a PC (Windows/Linux/macOS) with a Bluetooth adapter. + client: A Flutter mobile app (running on a physical device) will connect to this simulator. + inputs: + protocol_docs: "./docs.toon" # Contains commands and expected logic. + +tasks: + - id: 1_setup_dependencies + title: Initialize Project and BLE Library + description: | + Initialize a Go module. + Add dependency: 'tinygo.org/x/bluetooth'. + Ensure the code structure supports OS-specific threads (required for Bluetooth adapters). + validation: + - "go.mod contains tinygo.org/x/bluetooth" + + - id: 2_define_ble_structure + title: Define UUIDs and Service Architecture + description: | + Based on the nature of 'docs.toon', define the GATT structure constants: + - SERVICE_UUID: Generate a random 128-bit or 16-bit UUID. + - COMMAND_CHAR_UUID: A writable characteristic (for receiving commands from App). + - NOTIFY_CHAR_UUID: A notifiable characteristic (for sending data to App). + Output these UUIDs clearly so they can be copied into the Flutter app code. + validation: + - "Valid UUIDs are defined as constants." + + - id: 3_parse_logic_docs + title: Map Protocol Docs to Go Logic + description: | + Read 'docs.toon'. + Create a 'DeviceLogic' struct that holds the internal state (simulated values). + Map every command in the docs to a specific action. + Example: + - If docs says "CMD_RESET", the Go code should reset internal variables. + - If docs says "SET_COLOR_RED", update internal state to 'Red'. + validation: + - "Switch/Case logic exists handling commands found in docs.toon." + + - id: 4_implement_gatt_server + title: Configure GATT Server and Advertising + description: | + Implement the main function to: + 1. Enable the Bluetooth adapter (`adapter.Enable()`). + 2. configure the Advertisement (use a local name like "GO_SIMULATOR" so it's easy to find). + 3. Add the Service and Characteristics defined in Task 2. + validation: + - "Code calls adapter.AddService and adv.Start()." + + - id: 5_handle_write_requests + title: Implement Write Event Handler + description: | + Inside the COMMAND_CHAR_UUID configuration, implement the `WriteEvent` callback. + When the App writes bytes to this characteristic: + 1. Convert bytes to string/command. + 2. Pass it to the 'DeviceLogic' (Task 3). + 3. Log the received command to the console ("[RX] Command: ..."). + validation: + - "Console logs incoming Bluetooth writes in real-time." + + - id: 6_handle_notifications + title: Implement Notification Loop + description: | + Create a Goroutine that simulates data generation based on 'docs.toon'. + Every X milliseconds (simulate sensor frequency): + 1. Update the value of NOTIFY_CHAR_UUID. + 2. Trigger a notification sending the new bytes to the connected device. + 3. Log the sent data ("[TX] Sending: ..."). + validation: + - "Code simulates data flow and updates the characteristic value periodically." + + - id: 7_run_and_monitor + title: CLI Dashboard + description: | + Provide a clean CLI output indicating: + - "Advertising as [Name]..." + - "Client Connected / Disconnected" + - Real-time log of traffic.