Skip to content

Commit

Permalink
examples: add example that makes connections to multiple peripherals
Browse files Browse the repository at this point in the history
Signed-off-by: deadprogram <[email protected]>
  • Loading branch information
deadprogram committed Jan 3, 2025
1 parent fb043fd commit 9f0eb08
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 0 deletions.
146 changes: 146 additions & 0 deletions examples/multiples/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// This example scans and then connects to multiple Bluetooth peripherals
// that provide the Heart Rate Service (HRS).
//
// Once connected to all the desired devices, it subscribes to notifications.
//
// To run on bare metal microcontroller:
// tinygo flash -target metro-m4-airlift -ldflags="-X main.Devices=D9:2A:A1:5C:ED:56,4D:A1:3C:24:F0:46" -monitor ./examples/multiples/
//
// To run on OS:
// go run ./examples/multiples/ D9:2A:A1:5C:ED:56,64:0B:1D:46:D8:1D
package main

import (
"context"
"os"
"slices"
"time"

"tinygo.org/x/bluetooth"
)

var (
adapter = bluetooth.DefaultAdapter

heartRateServiceUUID = bluetooth.ServiceUUIDHeartRate
heartRateCharacteristicUUID = bluetooth.CharacteristicUUIDHeartRateMeasurement

exitCtx context.Context
)

func main() {
exitCtx = initExitHandler()

println("enabling")

// Enable BLE interface.
must("enable BLE stack", adapter.Enable())

scanResults := make(map[string]bluetooth.ScanResult)
finished := make(chan bool, 1)

searchList, _ := connectAddresses()

// Start scanning.
println("scanning...")
err := adapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {
print(".")
// is the scanned device one of the ones we want?
if slices.Contains(searchList, result.Address.String()) {
if _, ok := scanResults[result.Address.String()]; !ok {
println(".")
println("found device:", result.Address.String(), result.RSSI, result.LocalName())
scanResults[result.Address.String()] = result
}

if len(scanResults) == len(searchList) {
println("All found.")
adapter.StopScan()
finished <- true
}
}
select {
case <-exitCtx.Done():
println("exiting.")
os.Exit(0)
default:
}
})
must("scan", err)

devices := []bluetooth.Device{}
select {
case <-time.After(5 * time.Second):
failMessage("timed out")
return
case <-exitCtx.Done():
println("exiting.")
return
case <-finished:
}

defer func() {
for _, device := range devices {
device.Disconnect()
}
}()

// now connect to all devices
for _, result := range scanResults {
device, err := adapter.Connect(result.Address, bluetooth.ConnectionParams{})
if err != nil {
failMessage(err.Error())
return
}

println("connected to", result.Address.String())
devices = append(devices, device)
}

// get services
println("discovering services/characteristics")

for _, device := range devices {
srvcs, err := device.DiscoverServices([]bluetooth.UUID{heartRateServiceUUID})
must("discover services", err)

if len(srvcs) == 0 {
failMessage("could not find heart rate service")
return
}

srvc := srvcs[0]

println("found service", srvc.UUID().String(), "for device", device.Address.String())

chars, err := srvc.DiscoverCharacteristics([]bluetooth.UUID{heartRateCharacteristicUUID})
if err != nil {
failMessage(err.Error())
return
}

if len(chars) == 0 {
failMessage("could not find heart rate characteristic")
return
}

char := chars[0]
addr := device.Address.String()
println("found characteristic", char.UUID().String(), "for device", addr)

char.EnableNotifications(func(buf []byte) {
println(addr, "data:", uint8(buf[1]))
})
}

// wait for exit
<-exitCtx.Done()
println("exiting.")
}

func must(action string, err error) {
if err != nil {
failMessage("failed to " + action + ": " + err.Error())
return
}
}
37 changes: 37 additions & 0 deletions examples/multiples/mcu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//go:build baremetal

package main

import (
"context"
"errors"
"strings"
"time"
)

// Devices are the MAC addresses of the Bluetooth peripherals you want to connect to.
// Replace this by using -ldflags="-X main.Devices='[MAC ADDRESS],[MAC ADDRESS]'"
// where [MAC ADDRESS] is the actual MAC address of the peripheral.
// For example:
// tinygo flash -target nano-rp2040 -ldflags="-X main.Devices='7B:36:98:8C:41:1C,7B:36:98:8C:41:1D" ./examples/heartrate-monitor/
var Devices string

func initExitHandler() context.Context {
return context.Background()
}

func connectAddresses() ([]string, error) {
addrs := strings.Split(Devices, ",")
if len(addrs) == 0 {
return nil, errors.New("no devices specified")
}

return addrs, nil
}

func failMessage(msg string) {
for {
println(msg)
time.Sleep(1 * time.Second)
}
}
49 changes: 49 additions & 0 deletions examples/multiples/os.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//go:build !baremetal

package main

import (
"context"
"errors"
"os"
"os/signal"
"strings"
"syscall"
)

func initExitHandler() context.Context {
return contextWithSignal(context.Background())
}

// ContextWithSignal creates a context canceled when SIGINT or SIGTERM are notified
func contextWithSignal(ctx context.Context) context.Context {
newCtx, cancel := context.WithCancel(ctx)
signals := make(chan os.Signal)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
go func() {
select {
case <-signals:
cancel()
}
}()
return newCtx
}

func connectAddresses() ([]string, error) {
if len(os.Args) < 2 {
println("usage: multiples [address],[address]")
os.Exit(1)
}

addrs := strings.Split(os.Args[1], ",")
if len(addrs) == 0 {
return nil, errors.New("no devices specified")
}

return addrs, nil
}

func failMessage(msg string) {
println(msg)
exitCtx.Done()
}

0 comments on commit 9f0eb08

Please sign in to comment.