Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ jobs:
name: macos-14
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: '1.23.12'
- name: Checkout
uses: actions/checkout@v6
go-version-file: 'go.mod'
- name: Run unit tests
run: go test
- name: "Run macOS smoke tests"
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ smoketest-tinygo:
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=badger2040-w ./examples/advertisement
@md5sum test.hex
$(TINYGO) build -o test.bin -size=short -target=xiao-esp32c3 ./examples/advertisement
@md5sum test.bin
$(TINYGO) build -o test.bin -size=short -target=xiao-esp32c3 ./examples/discover
@md5sum test.bin

smoketest-linux:
# Test on Linux.
Expand Down
71 changes: 71 additions & 0 deletions adapter_espradio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//go:build espradio

package bluetooth

import (
"runtime"

"tinygo.org/x/espradio"
)

const maxConnections = 1

// Adapter represents the BLE adapter on the ESP32 via espradio VHCI transport.
type Adapter struct {
hciAdapter
}

// DefaultAdapter is the default adapter on the current system.
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
hciAdapter: hciAdapter{
isDefault: true,
connectHandler: func(device Device, 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).
// For WiFi+BLE co-existence, call espradio.Enable() first.
func (a *Adapter) Enable() error {
if err := espradio.BLEInit(); err != nil {
return err
}

transport := &hciVHCI{}

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

a.enable()

return nil
}

// hciVHCI wraps espradio's BLE VHCI transport to implement the
// unexported hciTransport interface.
type hciVHCI struct {
t espradio.VHCITransport
}

func (h *hciVHCI) startRead() { runtime.Gosched() }
func (h *hciVHCI) endRead() {}

func (h *hciVHCI) Buffered() int {
return h.t.Buffered()
}

func (h *hciVHCI) ReadByte() (byte, error) {
return h.t.ReadByte()
}

func (h *hciVHCI) Read(buf []byte) (int, error) {
return h.t.Read(buf)
}

func (h *hciVHCI) Write(buf []byte) (int, error) {
return h.t.Write(buf)
}
31 changes: 30 additions & 1 deletion adapter_hci.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439
//go:build hci || ninafw || cyw43439 || espradio

package bluetooth

Expand All @@ -16,6 +16,7 @@ type hciAdapter struct {

isDefault bool
scanning bool
scanType ScanType

connectHandler func(device Device, connected bool)

Expand Down Expand Up @@ -62,6 +63,34 @@ func (a *hciAdapter) Address() (MACAddress, error) {
return a.hci.address, nil
}

// ScanType selects whether the scanner transmits while scanning.
type ScanType uint8

const (
// ScanTypeActive sends a SCAN_REQ to advertisers that allow it, so scan
// response data (usually the complete local name) is reported as well.
// This is the default.
ScanTypeActive ScanType = iota

// ScanTypePassive only listens. It uses less power and does not reveal the
// scanner's presence, but misses scan response data.
ScanTypePassive
)

// hciValue returns the scan_type field for HCI LE Set Scan Parameters.
func (t ScanType) hciValue() uint8 {
if t == ScanTypePassive {
return 0x00
}
return 0x01
}

// SetScanType sets the scan type used by Scan. Call it before Scan; changing it
// during a scan takes effect on the next call to Scan.
func (a *Adapter) SetScanType(t ScanType) {
a.scanType = t
}

func (a *Adapter) SetRandomAddress(mac MAC) error {
if err := a.hci.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|ocfLESetRandomAddress, mac[:]); err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion att_hci.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439
//go:build hci || ninafw || cyw43439 || espradio

package bluetooth

Expand Down
11 changes: 11 additions & 0 deletions examples/scanner/main.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
package main

import (
"time"

"tinygo.org/x/bluetooth"
)

var adapter = bluetooth.DefaultAdapter

func main() {
time.Sleep(time.Second) // wait for the system to initialize
println("Starting BLE")

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

// On the HCI backends scanning is active by default, so a device that puts
// its name in a scan response is reported twice: once for the
// advertisement and once for the scan response. Uncomment to listen only,
// at the cost of missing that data.
// adapter.SetScanType(bluetooth.ScanTypePassive)

// Start scanning.
println("scanning...")
err := adapter.Scan(func(adapter *bluetooth.Adapter, device bluetooth.ScanResult) {
Expand Down
90 changes: 61 additions & 29 deletions gap.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,24 +276,42 @@ func (buf *rawAdvertisementPayload) Bytes() []byte {
return buf.data[:buf.len]
}

// findField returns the data of a specific field in the advertisement packet.
// nextADField splits the first AD field off the front of an advertisement
// payload. It returns the field type, the field data (excluding the length and
// type bytes) and the remainder of the payload. It returns ok=false once there
// are no more usable fields, which includes malformed input: advertisement data
// comes straight off the air, so a field length that runs past the end of the
// buffer must not be trusted.
//
// See this list of field types:
// https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/
func nextADField(data []byte) (fieldType byte, field, rest []byte, ok bool) {
if len(data) < 2 {
return 0, nil, nil, false
}
fieldLength := int(data[0])
if fieldLength < 1 || fieldLength+1 > len(data) {
// A zero length marks the end of the significant part of the payload,
// and a length beyond the end of the buffer means it is malformed.
// Either way, stop here.
return 0, nil, nil, false
}
return data[1], data[2 : fieldLength+1], data[fieldLength+1:], true
}

// findField returns the data of a specific field in the advertisement packet.
func (buf *rawAdvertisementPayload) findField(fieldType byte) []byte {
data := buf.Bytes()
for len(data) >= 2 {
fieldLength := data[0]
if int(fieldLength)+1 > len(data) {
// Invalid field length.
for {
typ, field, rest, ok := nextADField(data)
if !ok {
return nil
}
if fieldType == data[1] {
return data[2 : fieldLength+1]
if typ == fieldType {
return field
}
data = data[fieldLength+1:]
data = rest
}
return nil
}

// LocalName returns the local name (complete or shortened) in the advertisement
Expand Down Expand Up @@ -376,19 +394,24 @@ func (buf *rawAdvertisementPayload) ServiceUUIDs() []UUID {
// ManufacturerData returns the manufacturer data in the advertisement payload.
func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement {
var manufacturerData []ManufacturerDataElement
for index := 0; index < int(buf.len); index += int(buf.data[index]) + 1 {
fieldLength := int(buf.data[index+0])
if fieldLength < 3 {
continue
data := buf.Bytes()
for {
fieldType, field, rest, ok := nextADField(data)
if !ok {
break
}
fieldType := buf.data[index+1]
data = rest

if fieldType != 0xff {
continue
}
key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8
if len(field) < 2 { // no room for the company ID
continue
}
key := uint16(field[0]) | uint16(field[1])<<8
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: key,
Data: buf.data[index+4 : index+fieldLength+1],
Data: field[2:],
})
}
return manufacturerData
Expand All @@ -397,32 +420,41 @@ func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement
// ServiceData returns the service data in the advertisment payload
func (buf *rawAdvertisementPayload) ServiceData() []ServiceDataElement {
var serviceData []ServiceDataElement
for index := 0; index < int(buf.len); index += int(buf.data[index]) + 1 {
fieldLength := int(buf.data[index+0])
if fieldLength < 3 { // field has only length and type and no data
continue
data := buf.Bytes()
for {
fieldType, field, rest, ok := nextADField(data)
if !ok {
break
}
fieldType := buf.data[index+1]
data = rest

switch fieldType {
case 0x16: // 16-bit uuid
if len(field) < 2 {
continue
}
serviceData = append(serviceData, ServiceDataElement{
UUID: New16BitUUID(uint16(buf.data[index+2]) + (uint16(buf.data[index+3]) << 8)),
Data: buf.data[index+4 : index+fieldLength+1],
UUID: New16BitUUID(uint16(field[0]) + (uint16(field[1]) << 8)),
Data: field[2:],
})
case 0x20: // 32-bit uuid
if len(field) < 4 {
continue
}
serviceData = append(serviceData, ServiceDataElement{
UUID: New32BitUUID(uint32(buf.data[index+2]) + (uint32(buf.data[index+3]) << 8) + (uint32(buf.data[index+4]) << 16) + (uint32(buf.data[index+5]) << 24)),
Data: buf.data[index+6 : index+fieldLength+1],
UUID: New32BitUUID(uint32(field[0]) + (uint32(field[1]) << 8) + (uint32(field[2]) << 16) + (uint32(field[3]) << 24)),
Data: field[4:],
})
case 0x21: // 128-bit uuid
if len(field) < 16 {
continue
}
var uuidArray [16]byte
copy(uuidArray[:], buf.data[index+2:index+18])
copy(uuidArray[:], field[:16])
serviceData = append(serviceData, ServiceDataElement{
UUID: NewUUID(uuidArray),
Data: buf.data[index+18 : index+fieldLength+1],
Data: field[16:],
})
default:
continue
}
}
return serviceData
Expand Down
13 changes: 10 additions & 3 deletions gap_hci.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439
//go:build hci || ninafw || cyw43439 || espradio

package bluetooth

Expand Down Expand Up @@ -42,8 +42,15 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
return err
}

// passive scanning, every 40ms, for 30ms
if err := a.hci.leSetScanParameters(0x00, 0x0080, 0x0030, 0x00, 0x00); err != nil {
// Active scanning transmits, so the controller needs to know which of our
// own addresses to put in the SCAN_REQ.
localRandom := uint8(0)
if a.hci.address.isRandom {
localRandom = GAPAddressTypeRandomStatic
}

// scan every 80ms, for 30ms
if err := a.hci.leSetScanParameters(a.scanType.hciValue(), 0x0080, 0x0030, localRandom, 0x00); err != nil {
return err
}

Expand Down
Loading
Loading