tested and updated BLE tutorial
This commit is contained in:
parent
7d8399152c
commit
043b884263
4 changed files with 188 additions and 106 deletions
90
TUTORIAL.md
90
TUTORIAL.md
|
|
@ -520,7 +520,7 @@ Connect the badge to a computer. The joystick moves the mouse cursor; button A i
|
||||||
|
|
||||||
## BLE
|
## BLE
|
||||||
|
|
||||||
The nice!nano's nRF52840 chip has built-in Bluetooth Low Energy. These examples use the [`github.com/tinygo-org/bluetooth`](https://github.com/tinygo-org/bluetooth) library.
|
The nice!nano's nRF52840 chip has built-in Bluetooth Low Energy. These examples use the [`tinygo.org/x/bluetooth`](https://tinygo.org/x/bluetooth) library.
|
||||||
|
|
||||||
**Recommended mobile apps**
|
**Recommended mobile apps**
|
||||||
|
|
||||||
|
|
@ -558,28 +558,50 @@ This example implements the **Nordic UART Service (NUS)** — a de-facto standar
|
||||||
| RX | `6E400002-…` | Write, WriteWithoutResponse | Central → Badge |
|
| RX | `6E400002-…` | Write, WriteWithoutResponse | Central → Badge |
|
||||||
| TX | `6E400003-…` | Notify, Read | Badge → Central |
|
| TX | `6E400003-…` | Notify, Read | Badge → Central |
|
||||||
|
|
||||||
The counter increments every second. `txChar.Write()` sends a notification to any subscribed central. If `Write` returns an error no device is listening — that is how connection state is tracked.
|
Connection state is tracked via `adapter.SetConnectHandler`, which receives real events from the nRF52840 SoftDevice. The handler only sets flags — display and advertising calls happen in the main loop to avoid re-entering the SoftDevice from its own event callback.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
_, err := txChar.Write([]byte(strconv.Itoa(counter) + "\n"))
|
adapter.SetConnectHandler(func(device bluetooth.Device, c bool) {
|
||||||
connected = err == nil
|
connected = c
|
||||||
|
connChanged = true
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
if connChanged {
|
||||||
|
connChanged = false
|
||||||
|
if connected {
|
||||||
|
drawStatus("Connected ")
|
||||||
|
} else {
|
||||||
|
drawStatus("Advertising...")
|
||||||
|
adv.Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
drawCounter(counter)
|
||||||
|
if connected {
|
||||||
|
txChar.Write([]byte(strconv.Itoa(counter) + "\n"))
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`AddService` must be called before `adv.Start()` — the nRF52840 SoftDevice needs the complete GATT table before advertising begins.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
tinygo flash -target nicenano ./ble/step1
|
tinygo flash -target nicenano ./ble/step1
|
||||||
```
|
```
|
||||||
|
|
||||||
**How to test**
|
**How to test**
|
||||||
1. Flash the badge. The display shows `BLE: Advertising...`.
|
1. Flash the badge. The display shows `BLE: Advertising...`.
|
||||||
2. Open **nRF Toolbox** → UART → Connect → search for `NiceBadge`.
|
2. Open **nRF Connect** → SCANNER → search for `NiceBadge`.
|
||||||
3. Once connected the display shows `BLE: Connected` and the counter appears in the terminal.
|
3. Once connected the display shows `BLE: Connected` and the counter appears in the terminal.
|
||||||
4. Type `reset` and send it — the counter resets to zero.
|
4. Type `reset` and send it — the counter resets to zero.
|
||||||
|
|
||||||
**Key concepts**
|
**Key concepts**
|
||||||
- `adapter.Enable()` — starts the BLE stack (SoftDevice on nRF52840). Must be called before anything else.
|
- `adapter.Enable()` — starts the BLE stack (SoftDevice on nRF52840). Must be called before anything else.
|
||||||
- `adapter.AddService` — registers the GATT service and its characteristics.
|
- `adapter.AddService` must be called **before** `adv.Start()` on nRF52840 — the GATT table is frozen once advertising starts.
|
||||||
- `adv.Start()` — begins advertising; the badge is now discoverable.
|
- `adapter.SetConnectHandler` — the correct way to track connection state; `txChar.Write()` always returns `nil` on nRF52840 regardless of whether a central is connected.
|
||||||
- The `WriteEvent` callback runs when the central writes to a characteristic. It runs in a BLE interrupt context — keep it short.
|
- Keep BLE callbacks short and flag-only — calling SoftDevice functions (like `adv.Start()`) or SPI ops from within a SoftDevice event handler causes a deadlock.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -594,6 +616,8 @@ A custom service exposes a single writable characteristic. The central writes 3
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| LED Color | `BADA5502-…` | Write, WriteWithoutResponse | Central → Badge |
|
| LED Color | `BADA5502-…` | Write, WriteWithoutResponse | Central → Badge |
|
||||||
|
|
||||||
|
The `WriteEvent` callback handles the color update directly (SPI and GPIO — no SoftDevice re-entry). Connection tracking uses the same flag pattern as step1.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
|
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
|
||||||
if len(value) < 3 {
|
if len(value) < 3 {
|
||||||
|
|
@ -605,7 +629,25 @@ WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
After all services are registered the main goroutine simply blocks with `select {}` — all activity is driven by the BLE callback.
|
```go
|
||||||
|
adapter.SetConnectHandler(func(device bluetooth.Device, c bool) {
|
||||||
|
connected = c
|
||||||
|
connChanged = true
|
||||||
|
})
|
||||||
|
// ...
|
||||||
|
for {
|
||||||
|
if connChanged {
|
||||||
|
connChanged = false
|
||||||
|
if connected {
|
||||||
|
drawStatus("Connected ")
|
||||||
|
} else {
|
||||||
|
drawStatus("Advertising...")
|
||||||
|
adv.Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
tinygo flash -target nicenano ./ble/step2
|
tinygo flash -target nicenano ./ble/step2
|
||||||
|
|
@ -619,7 +661,7 @@ tinygo flash -target nicenano ./ble/step2
|
||||||
|
|
||||||
**Key concepts**
|
**Key concepts**
|
||||||
- Custom 128-bit UUIDs let you define entirely private services not shared with any standard profile.
|
- Custom 128-bit UUIDs let you define entirely private services not shared with any standard profile.
|
||||||
- `select {}` is idiomatic Go for blocking forever; it is more explicit than `for {}` with a sleep.
|
- `WriteEvent` can call SPI and GPIO safely — it only avoids re-entering the SoftDevice (e.g. `adv.Start()`).
|
||||||
- Always validate `len(value)` in `WriteEvent` — a malformed write should not panic.
|
- Always validate `len(value)` in `WriteEvent` — a malformed write should not panic.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -628,20 +670,28 @@ tinygo flash -target nicenano ./ble/step2
|
||||||
|
|
||||||
**Goal:** put the radio in observer mode and display nearby BLE devices.
|
**Goal:** put the radio in observer mode and display nearby BLE devices.
|
||||||
|
|
||||||
In scanner mode the badge does not advertise — it only listens. `adapter.Scan` is a blocking call, so it runs in a goroutine while the main loop updates the display every 500 ms.
|
`adapter.Scan` is a blocking call that drives its own internal event loop via `sd_app_evt_wait` — the nRF52840 SoftDevice primitive for waiting on BLE events. While inside this loop, **TinyGo's cooperative scheduler never gets CPU time**, so goroutines spawned to call `adapter.StopScan()` after a timeout never run.
|
||||||
|
|
||||||
|
Running `adapter.Scan` and SPI display operations concurrently causes a second problem: the SoftDevice can hold interrupts during event processing, and TinyGo's SPI driver waits for a DMA-completion interrupt using `wfe` (Wait For Event). If the SoftDevice consumes that wake-up event, the SPI transfer hangs forever.
|
||||||
|
|
||||||
|
The solution for both problems is to call `adapter.StopScan()` **from inside the scan callback** using `time.Since`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
go func() {
|
scanStart := time.Now()
|
||||||
adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
|
adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
|
||||||
name := result.LocalName()
|
if time.Since(scanStart) >= scanWindow {
|
||||||
if name == "" {
|
adapter.StopScan() // causes adapter.Scan to return
|
||||||
name = result.Address.String()
|
return
|
||||||
}
|
}
|
||||||
// deduplicate by address, update RSSI
|
// deduplicate by address, update RSSI, copy name bytes
|
||||||
})
|
})
|
||||||
}()
|
|
||||||
|
// adapter.Scan has returned — SPI is safe to use now
|
||||||
|
drawDevices()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Device names and addresses are stored as fixed-size byte arrays, not `string` fields. Strings returned by `result.LocalName()` and `result.Address.String()` may point into SoftDevice-managed buffers that are recycled after the callback returns; accessing them later from the main loop causes memory corruption.
|
||||||
|
|
||||||
RSSI (Received Signal Strength Indicator) is expressed in dBm — closer to 0 is stronger. The display colors devices by signal quality:
|
RSSI (Received Signal Strength Indicator) is expressed in dBm — closer to 0 is stronger. The display colors devices by signal quality:
|
||||||
|
|
||||||
| RSSI | Color | Meaning |
|
| RSSI | Color | Meaning |
|
||||||
|
|
@ -657,8 +707,10 @@ tinygo flash -target nicenano ./ble/step3
|
||||||
Up to 5 nearby devices are listed by name and signal strength. Press **button A** to clear the list and start fresh.
|
Up to 5 nearby devices are listed by name and signal strength. Press **button A** to clear the list and start fresh.
|
||||||
|
|
||||||
**Key concepts**
|
**Key concepts**
|
||||||
- `result.LocalName()` returns the advertised name, if any. Devices that don't advertise a name are identified by their MAC address.
|
- `adapter.Scan` runs its own internal `sd_app_evt_wait` loop — it never yields to TinyGo's cooperative scheduler. A goroutine that calls `StopScan()` after a `time.Sleep` will never execute while `Scan` is running.
|
||||||
- TinyGo uses a cooperative scheduler — goroutines yield at blocking calls (`Scan`, `Sleep`, channel ops). Shared variables accessed from both the goroutine and the main loop are safe here because the scheduler is cooperative, but in general you should use channels or atomics.
|
- On nRF52840, **never run SPI and `adapter.Scan` at the same time**. The SoftDevice can consume the WFE wake-up that TinyGo's SPI driver needs to detect DMA completion, causing the SPI bus to hang indefinitely. Always stop the scan before doing any display update.
|
||||||
|
- **BLE callbacks must not store strings** that point into SoftDevice buffers. Use fixed-size `[N]byte` arrays and `copy()` in callbacks; convert to `string` only in the main loop.
|
||||||
|
- `result.LocalName()` returns the advertised name, if any. Devices that don't advertise a name are shown by their MAC address (`result.Address.String()`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tinygo-org/bluetooth"
|
"tinygo.org/x/bluetooth"
|
||||||
"tinygo.org/x/drivers/st7789"
|
"tinygo.org/x/drivers/st7789"
|
||||||
"tinygo.org/x/tinyfont"
|
"tinygo.org/x/tinyfont"
|
||||||
"tinygo.org/x/tinyfont/freesans"
|
"tinygo.org/x/tinyfont/freesans"
|
||||||
|
|
@ -38,6 +38,7 @@ var (
|
||||||
var (
|
var (
|
||||||
display st7789.Device
|
display st7789.Device
|
||||||
connected bool
|
connected bool
|
||||||
|
connChanged bool
|
||||||
counter int
|
counter int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -49,12 +50,21 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// sometimes, a little wait is needed to initialize it at hardware level
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
initDisplay()
|
initDisplay()
|
||||||
drawStatus("Advertising...")
|
drawStatus("Advertising...")
|
||||||
drawCounter(0)
|
drawCounter(0)
|
||||||
|
|
||||||
must("enable BLE", adapter.Enable())
|
must("enable BLE", adapter.Enable())
|
||||||
|
|
||||||
|
adv := adapter.DefaultAdvertisement()
|
||||||
|
|
||||||
|
must("configure adv", adv.Configure(bluetooth.AdvertisementOptions{
|
||||||
|
LocalName: "NiceBadge",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// AddService must be called before adv.Start() on nRF52840 SoftDevice.
|
||||||
var txChar bluetooth.Characteristic
|
var txChar bluetooth.Characteristic
|
||||||
|
|
||||||
must("add service", adapter.AddService(&bluetooth.Service{
|
must("add service", adapter.AddService(&bluetooth.Service{
|
||||||
|
|
@ -78,32 +88,31 @@ func main() {
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
adv := adapter.DefaultAdvertisement()
|
// Only set flags here — calling adv.Start() or display ops from within
|
||||||
must("configure adv", adv.Configure(bluetooth.AdvertisementOptions{
|
// the SoftDevice event callback causes a deadlock on nRF52840.
|
||||||
LocalName: "NiceBadge",
|
adapter.SetConnectHandler(func(device bluetooth.Device, c bool) {
|
||||||
ServiceUUIDs: []bluetooth.UUID{serviceUUID},
|
connected = c
|
||||||
}))
|
connChanged = true
|
||||||
|
})
|
||||||
|
|
||||||
must("start adv", adv.Start())
|
must("start adv", adv.Start())
|
||||||
|
|
||||||
for {
|
for {
|
||||||
counter++
|
if connChanged {
|
||||||
drawCounter(counter)
|
connChanged = false
|
||||||
|
|
||||||
// Write sends a BLE notification to subscribed centrals.
|
|
||||||
// If no device is subscribed, Write returns an error.
|
|
||||||
_, err := txChar.Write([]byte(strconv.Itoa(counter) + "\n"))
|
|
||||||
|
|
||||||
wasConnected := connected
|
|
||||||
connected = err == nil
|
|
||||||
if wasConnected != connected {
|
|
||||||
if connected {
|
if connected {
|
||||||
drawStatus("Connected ")
|
drawStatus("Connected ")
|
||||||
} else {
|
} else {
|
||||||
drawStatus("Advertising...")
|
drawStatus("Advertising...")
|
||||||
must("restart adv", adv.Start())
|
adv.Start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
counter++
|
||||||
|
drawCounter(counter)
|
||||||
|
if connected {
|
||||||
|
txChar.Write([]byte(strconv.Itoa(counter) + "\n"))
|
||||||
|
}
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,9 @@ import (
|
||||||
"image/color"
|
"image/color"
|
||||||
"machine"
|
"machine"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/tinygo-org/bluetooth"
|
"tinygo.org/x/bluetooth"
|
||||||
"tinygo.org/x/drivers/st7789"
|
"tinygo.org/x/drivers/st7789"
|
||||||
"tinygo.org/x/drivers/ws2812"
|
"tinygo.org/x/drivers/ws2812"
|
||||||
"tinygo.org/x/tinyfont"
|
"tinygo.org/x/tinyfont"
|
||||||
|
|
@ -39,24 +40,27 @@ var (
|
||||||
display st7789.Device
|
display st7789.Device
|
||||||
leds ws2812.Device
|
leds ws2812.Device
|
||||||
connected bool
|
connected bool
|
||||||
|
connChanged bool
|
||||||
ledColor color.RGBA
|
ledColor color.RGBA
|
||||||
|
colorChanged bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
black = color.RGBA{0, 0, 0, 255}
|
black = color.RGBA{0, 0, 0, 255}
|
||||||
cyan = color.RGBA{0, 200, 255, 255}
|
cyan = color.RGBA{0, 200, 255, 255}
|
||||||
white = color.RGBA{255, 255, 255, 255}
|
white = color.RGBA{255, 255, 255, 255}
|
||||||
gray = color.RGBA{150, 150, 150, 255}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
initDisplay()
|
initDisplay()
|
||||||
initLEDs()
|
initLEDs()
|
||||||
drawStatus("Advertising...")
|
drawStatus("Advertising...")
|
||||||
drawColor(color.RGBA{0, 0, 0, 255})
|
drawColor(color.RGBA{})
|
||||||
|
|
||||||
must("enable BLE", adapter.Enable())
|
must("enable BLE", adapter.Enable())
|
||||||
|
|
||||||
|
// AddService must be called before adv.Start() on nRF52840 SoftDevice.
|
||||||
must("add service", adapter.AddService(&bluetooth.Service{
|
must("add service", adapter.AddService(&bluetooth.Service{
|
||||||
UUID: ledServiceUUID,
|
UUID: ledServiceUUID,
|
||||||
Characteristics: []bluetooth.CharacteristicConfig{
|
Characteristics: []bluetooth.CharacteristicConfig{
|
||||||
|
|
@ -67,29 +71,45 @@ func main() {
|
||||||
if len(value) < 3 {
|
if len(value) < 3 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// value[0]=R, value[1]=G, value[2]=B
|
// Only assign values — no heap allocs (strconv, string concat)
|
||||||
|
// allowed in interrupt context on nRF52840.
|
||||||
ledColor = color.RGBA{value[0], value[1], value[2], 255}
|
ledColor = color.RGBA{value[0], value[1], value[2], 255}
|
||||||
setLEDs(ledColor)
|
colorChanged = true
|
||||||
drawColor(ledColor)
|
|
||||||
|
|
||||||
if !connected {
|
|
||||||
connected = true
|
|
||||||
drawStatus("Connected ")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Only set flags here — calling adv.Start() or display ops from within
|
||||||
|
// the SoftDevice event callback causes a deadlock on nRF52840.
|
||||||
|
adapter.SetConnectHandler(func(device bluetooth.Device, c bool) {
|
||||||
|
connected = c
|
||||||
|
connChanged = true
|
||||||
|
})
|
||||||
|
|
||||||
adv := adapter.DefaultAdvertisement()
|
adv := adapter.DefaultAdvertisement()
|
||||||
must("configure adv", adv.Configure(bluetooth.AdvertisementOptions{
|
must("configure adv", adv.Configure(bluetooth.AdvertisementOptions{
|
||||||
LocalName: "NiceBadge",
|
LocalName: "NiceBadge",
|
||||||
ServiceUUIDs: []bluetooth.UUID{ledServiceUUID},
|
|
||||||
}))
|
}))
|
||||||
must("start adv", adv.Start())
|
must("start adv", adv.Start())
|
||||||
|
|
||||||
// wait forever; all logic is driven by the BLE WriteEvent callback
|
for {
|
||||||
select {}
|
if connChanged {
|
||||||
|
connChanged = false
|
||||||
|
if connected {
|
||||||
|
drawStatus("Connected ")
|
||||||
|
} else {
|
||||||
|
drawStatus("Advertising...")
|
||||||
|
adv.Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if colorChanged {
|
||||||
|
colorChanged = false
|
||||||
|
setLEDs(ledColor)
|
||||||
|
drawColor(ledColor)
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDisplay() {
|
func initDisplay() {
|
||||||
|
|
@ -127,7 +147,6 @@ func drawStatus(status string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawColor(c color.RGBA) {
|
func drawColor(c color.RGBA) {
|
||||||
// show a filled rectangle with the current color and its RGB values below
|
|
||||||
display.FillRectangle(0, 38, 240, 60, color.RGBA{c.R, c.G, c.B, 255})
|
display.FillRectangle(0, 38, 240, 60, color.RGBA{c.R, c.G, c.B, 255})
|
||||||
display.FillRectangle(0, 100, 240, 35, black)
|
display.FillRectangle(0, 100, 240, 35, black)
|
||||||
rgb := "R:" + strconv.Itoa(int(c.R)) +
|
rgb := "R:" + strconv.Itoa(int(c.R)) +
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// BLE scanner example.
|
// BLE scanner example.
|
||||||
// Scans for nearby BLE devices and shows their names, addresses and signal
|
// Scans for nearby BLE devices and shows their names and signal strength
|
||||||
// strength (RSSI) on the display. Press button A to clear the list.
|
// (RSSI) on the display. Press button A to clear the list.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
@ -10,17 +10,21 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tinygo-org/bluetooth"
|
"tinygo.org/x/bluetooth"
|
||||||
"tinygo.org/x/drivers/st7789"
|
"tinygo.org/x/drivers/st7789"
|
||||||
"tinygo.org/x/tinyfont"
|
"tinygo.org/x/tinyfont"
|
||||||
"tinygo.org/x/tinyfont/freesans"
|
"tinygo.org/x/tinyfont/freesans"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxDevices = 5
|
const maxDevices = 5
|
||||||
|
const maxNameLen = 16
|
||||||
|
const scanWindow = 500 * time.Millisecond
|
||||||
|
|
||||||
|
// Fixed-size byte arrays avoid dangling pointers into SoftDevice buffers.
|
||||||
type bleDevice struct {
|
type bleDevice struct {
|
||||||
name string
|
name [maxNameLen]byte
|
||||||
addr string
|
nLen uint8
|
||||||
|
addr bluetooth.Address
|
||||||
rssi int16
|
rssi int16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,53 +45,56 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
initDisplay()
|
initDisplay()
|
||||||
|
|
||||||
btnA := machine.P1_06
|
btnA := machine.P1_06
|
||||||
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
||||||
|
|
||||||
must("enable BLE", adapter.Enable())
|
must("enable BLE", adapter.Enable())
|
||||||
|
|
||||||
drawHeader()
|
drawHeader()
|
||||||
|
|
||||||
// scan runs in a goroutine; found devices are stored and displayed in the main loop
|
|
||||||
go func() {
|
|
||||||
adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
|
|
||||||
name := result.LocalName()
|
|
||||||
if name == "" {
|
|
||||||
name = result.Address.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// update existing entry if already seen, otherwise add new
|
|
||||||
found := false
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
if devices[i].addr == result.Address.String() {
|
|
||||||
devices[i].rssi = result.RSSI
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found && count < maxDevices {
|
|
||||||
devices[count] = bleDevice{
|
|
||||||
name: name,
|
|
||||||
addr: result.Address.String(),
|
|
||||||
rssi: result.RSSI,
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// button A clears the list
|
|
||||||
if !btnA.Get() {
|
if !btnA.Get() {
|
||||||
count = 0
|
count = 0
|
||||||
display.FillRectangle(0, 32, 240, 103, black)
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adapter.Scan never yields to TinyGo's scheduler, so a goroutine
|
||||||
|
// calling StopScan() never gets CPU time.
|
||||||
|
// Fix: check elapsed time inside the callback and stop from there.
|
||||||
|
scanStart := time.Now()
|
||||||
|
adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
|
||||||
|
if time.Since(scanStart) >= scanWindow {
|
||||||
|
adapter.StopScan()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
if devices[i].addr == result.Address {
|
||||||
|
devices[i].rssi = result.RSSI
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count >= maxDevices {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n := result.LocalName()
|
||||||
|
if n == "" {
|
||||||
|
n = result.Address.String()
|
||||||
|
}
|
||||||
|
nLen := len(n)
|
||||||
|
if nLen > maxNameLen {
|
||||||
|
nLen = maxNameLen
|
||||||
|
}
|
||||||
|
copy(devices[count].name[:], n)
|
||||||
|
devices[count].nLen = uint8(nLen)
|
||||||
|
devices[count].addr = result.Address
|
||||||
|
devices[count].rssi = result.RSSI
|
||||||
|
count++
|
||||||
|
})
|
||||||
|
|
||||||
|
// Scan has stopped — safe to use SPI now.
|
||||||
drawDevices()
|
drawDevices()
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,11 +118,10 @@ func initDisplay() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawHeader() {
|
func drawHeader() {
|
||||||
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 24, "BLE Scanner", cyan)
|
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 24, "BLE Scanner", cyan)
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawDevices() {
|
func drawDevices() {
|
||||||
// clear device list area
|
|
||||||
display.FillRectangle(0, 32, 240, 103, black)
|
display.FillRectangle(0, 32, 240, 103, black)
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
|
|
@ -125,13 +131,9 @@ func drawDevices() {
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
y := int16(52 + i*20)
|
y := int16(52 + i*20)
|
||||||
name := devices[i].name
|
name := string(devices[i].name[:devices[i].nLen])
|
||||||
if len(name) > 16 {
|
|
||||||
name = name[:16]
|
|
||||||
}
|
|
||||||
rssi := " " + strconv.Itoa(int(devices[i].rssi)) + "dB"
|
rssi := " " + strconv.Itoa(int(devices[i].rssi)) + "dB"
|
||||||
|
|
||||||
// color by signal strength
|
|
||||||
c := white
|
c := white
|
||||||
if devices[i].rssi > -60 {
|
if devices[i].rssi > -60 {
|
||||||
c = green
|
c = green
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue