simulator v1
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
simulator
|
||||
110
README.md
110
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 <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
79
cmd/simulator/main.go
Normal 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
39
docs.toon
Normal 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
17
go.mod
Normal 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
35
go.sum
Normal 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
48
internal/ble/service.go
Normal 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: ¬ifyChar,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ¬ifyChar, nil
|
||||
}
|
||||
28
internal/ble/uuids.go
Normal file
28
internal/ble/uuids.go
Normal 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
161
internal/device/logic.go
Normal 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
122
internal/device/state.go
Normal 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
81
tasks.yaml
Normal 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.
|
||||
Reference in New Issue
Block a user