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

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
simulator

110
README.md
View File

@@ -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 <val>` | 0-2000 | 1200 | Min threshold |
| `W2 <val>` | 0-4095 | 4000 | Max threshold |
| `W3 <val>` | 1-50 | 2 | Pulse count |
| `W4 <val>` | 10-1000 | 40 | Time window (ms) |
| `W20 <val>` | 0-255 | 128 | Gain (wiper) |
### Read Parameters (requires PROG_ON first)
| Command | Response |
|---------|----------|
| `R1` | `PARAM: W1=<val>` |
| `R2` | `PARAM: W2=<val>` |
| `R20` | `PARAM: W20=<val>` |
## 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
```

79
cmd/simulator/main.go Normal file
View File

@@ -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)
}
}

39
docs.toon Normal file
View File

@@ -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=<val>, Read Min Threshold
R2, PARAM: W2=<val>, Read Max Threshold
R20, PARAM: W20=<val>, Read Gain (Wiper)
notifications:
heartbeat: {format: "SENSOR:<val>", 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

17
go.mod Normal file
View File

@@ -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
)

35
go.sum Normal file
View File

@@ -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=

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
}

81
tasks.yaml Normal file
View File

@@ -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.