diff --git a/examples/multiples/main.go b/examples/multiples/main.go new file mode 100644 index 0000000..9d00d16 --- /dev/null +++ b/examples/multiples/main.go @@ -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.wanted=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(".") + 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 + } +} diff --git a/examples/multiples/mcu.go b/examples/multiples/mcu.go new file mode 100644 index 0000000..af08128 --- /dev/null +++ b/examples/multiples/mcu.go @@ -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) + } +} diff --git a/examples/multiples/os.go b/examples/multiples/os.go new file mode 100644 index 0000000..2d9d13f --- /dev/null +++ b/examples/multiples/os.go @@ -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() +}