Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nina: BLE implementation on nina-fw #207

Merged
merged 5 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ smoketest-tinygo:
@md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=microbit-v2-s113v7 ./examples/nusserver
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/scanner
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/discover
@md5sum test.hex

smoketest-linux:
# Test on Linux.
Expand Down
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,17 @@ func must(action string, err error) {

## Current support

| | Linux | macOS | Windows | Nordic Semi |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Write peripheral characteristics | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Receive notifications | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: |
| Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: |
| Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: |
| Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: |
| | Linux | macOS | Windows | Nordic Semi | ESP32 (NINA-FW) |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Write peripheral characteristics | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Receive notifications | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: |
| Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: |
| Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: |
| Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: |

## Linux

Expand Down Expand Up @@ -264,6 +264,16 @@ After that, don't reset the board but instead flash a new program to it. For exa

Flashing will normally reset the board.

## ESP32 (NINA)

Go Bluetooth has bare metal support for boards that include a separate ESP32 Bluetooth Low Energy radio co-processor. The ESP32 must be running the Arduino or Adafruit `nina_fw` firmware.

See https://github.com/arduino/nina-fw for more information.

The only currently supported board is the Arduino Nano RP2040 Connect.

More info soon...

## API stability

**The API is not stable!** Because many features are not yet implemented and some platforms (e.g. Windows and macOS) are not yet fully supported, it's hard to say what a good API will be. Therefore, if you want stability you should pick a particular git commit and use that. Go modules can be useful for this purpose.
Expand Down
209 changes: 209 additions & 0 deletions adapter_ninafw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//go:build ninafw

package bluetooth

import (
"machine"
"runtime"

"time"
)

const maxConnections = 1

// Adapter represents the UART connection to the NINA fw.
type Adapter struct {
hci *hci
att *att

isDefault bool
scanning bool

reset func()
connectHandler func(device Address, connected bool)

connectedDevices []*Device
notificationsStarted bool
}

// DefaultAdapter is the default adapter on the current system.
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
isDefault: true,
reset: resetNINAInverted,
connectHandler: func(device Address, connected bool) {
return
},
connectedDevices: make([]*Device, 0, maxConnections),
}

// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() error {
// reset the NINA in BLE mode
machine.NINA_CS.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_CS.Low()
a.reset()

// serial port for nina chip
uart := machine.UART1
uart.Configure(machine.UARTConfig{
TX: machine.NINA_TX,
RX: machine.NINA_RX,
BaudRate: 115200,
CTS: machine.NINA_CTS,
RTS: machine.NINA_RTS,
})

a.hci, a.att = newBLEStack(uart)

a.hci.start()

if err := a.hci.reset(); err != nil {
return err
}

time.Sleep(150 * time.Millisecond)

if err := a.hci.setEventMask(0x3FFFFFFFFFFFFFFF); err != nil {
return err
}

if err := a.hci.setLeEventMask(0x00000000000003FF); err != nil {
return err
}

return nil
}

func (a *Adapter) Address() (MACAddress, error) {
if err := a.hci.readBdAddr(); err != nil {
return MACAddress{}, err
}

return MACAddress{MAC: makeAddress(a.hci.address)}, nil
}

func newBLEStack(uart *machine.UART) (*hci, *att) {
h := newHCI(uart)
a := newATT(h)
h.att = a

return h, a
}

// Convert a NINA MAC address into a Go MAC address.
func makeAddress(mac [6]uint8) MAC {
return MAC{
uint8(mac[0]),
uint8(mac[1]),
uint8(mac[2]),
uint8(mac[3]),
uint8(mac[4]),
uint8(mac[5]),
}
}

// Convert a Go MAC address into a NINA MAC Address.
func makeNINAAddress(mac MAC) [6]uint8 {
return [6]uint8{
uint8(mac[0]),
uint8(mac[1]),
uint8(mac[2]),
uint8(mac[3]),
uint8(mac[4]),
uint8(mac[5]),
}
}

func resetNINA() {
machine.NINA_RESETN.High()
time.Sleep(100 * time.Millisecond)
machine.NINA_RESETN.Low()
time.Sleep(1000 * time.Millisecond)
}

func resetNINAInverted() {
machine.NINA_RESETN.Low()
time.Sleep(100 * time.Millisecond)
machine.NINA_RESETN.High()
time.Sleep(1000 * time.Millisecond)
}

func (a *Adapter) startNotifications() {
if a.notificationsStarted {
return
}

if _debug {
println("starting notifications...")
}

a.notificationsStarted = true

// go routine to poll for HCI events for ATT notifications
go func() {
for {
if err := a.att.poll(); err != nil {
// TODO: handle error
if _debug {
println("error polling for notifications:", err.Error())
}
}

time.Sleep(250 * time.Millisecond)
}
}()

// go routine to handle characteristic notifications
go func() {
for {
select {
case not := <-a.att.notifications:
if _debug {
println("notification received", not.connectionHandle, not.handle, not.data)
}

d := a.findDevice(not.connectionHandle)
if d == nil {
if _debug {
println("no device found for handle", not.connectionHandle)
}
continue
}

n := d.findNotificationRegistration(not.handle)
if n == nil {
if _debug {
println("no notification registered for handle", not.handle)
}
continue
}

if n.callback != nil {
n.callback(not.data)
}

default:
}

runtime.Gosched()
}
}()
}

func (a *Adapter) findDevice(handle uint16) *Device {
for _, d := range a.connectedDevices {
if d.handle == handle {
if _debug {
println("found device", handle, d.Address.String(), "with notifications registered", len(d.notificationRegistrations))
}

return d
}
}

return nil
}
Loading
Loading