From efeb21c2e310c1a05d0336f36fe852f4caca2ecb Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Sun, 24 Nov 2024 19:11:54 +0100 Subject: [PATCH] Improve BLE advertisements. Split out the fields and make them accessible. This makes it possible to move more code into Toit, and also gives more control to users that need more advanced advertisement fields. --- examples/ble/advertise.toit | 12 +- examples/ble/connect.toit | 2 +- examples/ble/heart_rate.toit | 2 +- examples/ble/scan.toit | 2 +- lib/ble/ble.toit | 820 +++++++++++++++++- lib/ble/local.toit | 61 +- lib/ble/remote.toit | 61 +- lib/core/collections.toit | 28 + .../propagation/type_primitive_ble.cc | 1 + src/primitive.h | 3 +- src/resources/ble_darwin.mm | 8 +- src/resources/ble_esp32.cc | 194 +---- tests/ble-advertisement-test.toit | 369 ++++++++ .../tests__ble-advertisement-test.toit.gold | 3 + tests/hw/esp32/{ble2.toit => ble-util.toit} | 15 +- tests/hw/esp32/ble1-board1.toit | 2 +- tests/hw/esp32/ble1-board2.toit | 2 +- tests/hw/esp32/ble1-shared.toit | 12 +- tests/hw/esp32/ble2-board1.toit | 2 +- tests/hw/esp32/ble2-board2.toit | 2 +- tests/hw/esp32/ble2-shared.toit | 12 +- tests/hw/esp32/ble3-board1.toit | 2 +- tests/hw/esp32/ble3-board2.toit | 2 +- tests/hw/esp32/ble3-shared.toit | 12 +- tests/hw/esp32/ble4-board1.toit | 2 +- tests/hw/esp32/ble4-board2.toit | 2 +- tests/hw/esp32/ble4-shared.toit | 13 +- tests/hw/esp32/ble5-board1.toit | 2 +- tests/hw/esp32/ble5-board2.toit | 2 +- tests/hw/esp32/ble5-shared.toit | 15 +- tests/hw/esp32/ble6-advertise-board1.toit | 12 + tests/hw/esp32/ble6-advertise-board2.toit | 12 + tests/hw/esp32/ble6-advertise-shared.toit | 169 ++++ 33 files changed, 1530 insertions(+), 328 deletions(-) create mode 100644 tests/ble-advertisement-test.toit create mode 100644 tests/health/gold/sdk/tests__ble-advertisement-test.toit.gold rename tests/hw/esp32/{ble2.toit => ble-util.toit} (50%) create mode 100644 tests/hw/esp32/ble6-advertise-board1.toit create mode 100644 tests/hw/esp32/ble6-advertise-board2.toit create mode 100644 tests/hw/esp32/ble6-advertise-shared.toit diff --git a/examples/ble/advertise.toit b/examples/ble/advertise.toit index 8d0b65f0c..3cf4dd0ba 100644 --- a/examples/ble/advertise.toit +++ b/examples/ble/advertise.toit @@ -12,8 +12,16 @@ main: data := ble.AdvertisementData --name="Toit device" - --service-classes=[BATTERY-SERVICE] - --manufacturer-data=#[0xFF, 0xFF, 't', 'o', 'i', 't'] + --services=[BATTERY-SERVICE] + --manufacturer-specific=#[0xFF, 0xFF, 't', 'o', 'i', 't'] + if false: + // An equivalent way to create the data would use data blocks. + data = ble.AdvertisementData [ + ble.DataBlock.name "Toit device", + ble.DataBlock.services-16 [BATTERY-SERVICE], + // The company-id is not included here, as its default is #[0xFF, 0xFF]. + ble.DataBlock.manufacturer-specific "toit", + ] peripheral.start-advertise data sleep --ms=1000000 diff --git a/examples/ble/connect.toit b/examples/ble/connect.toit index 2e13e51f1..c27f394a5 100644 --- a/examples/ble/connect.toit +++ b/examples/ble/connect.toit @@ -11,7 +11,7 @@ SCAN-DURATION ::= Duration --s=3 find-with-service central/ble.Central service/ble.BleUuid: central.scan --duration=SCAN-DURATION: | device/ble.RemoteScannedDevice | - if device.data.service-classes.contains service: + if device.data.contains-service service: return device.address throw "no device found" diff --git a/examples/ble/heart_rate.toit b/examples/ble/heart_rate.toit index 8d734088c..21c43dbdd 100644 --- a/examples/ble/heart_rate.toit +++ b/examples/ble/heart_rate.toit @@ -34,8 +34,8 @@ main: ? BLE-CONNECT-MODE-NONE : BLE-CONNECT-MODE-UNDIRECTIONAL peripheral.start-advertise - AdvertisementData --name="Toit heart rate demo" --connection-mode=connection-mode + AdvertisementData --name="Toit heart rate demo" task:: simulated-heart-rate := 60 diff --git a/examples/ble/scan.toit b/examples/ble/scan.toit index f2341e7b6..d90601f3f 100644 --- a/examples/ble/scan.toit +++ b/examples/ble/scan.toit @@ -14,7 +14,7 @@ main: addresses := [] central.scan --duration=SCAN-DURATION: | device/ble.RemoteScannedDevice | - if device.data.service-classes.contains BATTERY-SERVICE: + if device.data.contains-service BATTERY-SERVICE: addresses.add device.address print addresses diff --git a/lib/ble/ble.toit b/lib/ble/ble.toit index f98f92c2e..29d007f88 100644 --- a/lib/ble/ble.toit +++ b/lib/ble/ble.toit @@ -25,38 +25,57 @@ The 128-bit UUID is referred to as the vendor specific UUID. These must be used 16-bit UUIDs of the form "XXXX" are short-hands for "0000XXXX-0000-1000-8000-00805F9B34FB", where "00000000-0000-1000-8000-00805F9B34FB" comes from the BLE standard and is called - the "base UUID" + the "base UUID". +Similarly, a 32-bit UUID of the form "XXXXXXXX" is a short-hand for + "XXXXXXXX-0000-1000-8000-00805F9B34FB". -See https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf - for a list of the available 16-bit UUIDs. +See https://www.bluetooth.com/specifications/assigned-numbers/ for a list of + assigned UUIDs. */ class BleUuid: - data_/any - constructor .data_: + data_/io.Data // Either a ByteArray or a string. + + /** + Constructs a new UUID from a byte array or a string. + + Does not check if a UUID can be shrunk by using the base UUID. + */ + constructor data/io.Data: + if data is not ByteArray and data is not string: + data = ByteArray.from data + data_ = data if data_ is ByteArray: - if data_.size != 2 and data_.size != 4 and data_.size != 16: throw "INVALID UUID" + bytes := data_ as ByteArray + if bytes.size != 2 and bytes.size != 4 and bytes.size != 16: throw "INVALID UUID" else if data_ is string: - if data_.size != 4 and data_.size != 8 and data_.size != 36: throw "INVALID UUID" - if data_.size == 36: - uuid.Uuid.parse data_ // This throws an exception if the format is incorrect. + str := data_ as string + if str.size != 4 and str.size != 8 and str.size != 36: throw "INVALID UUID" + if str.size == 36: + uuid.Uuid.parse str // This throws an exception if the format is incorrect. else: - if (catch: hex.decode data_): - throw "INVALID UUID $data_" - data_ = data_.to-ascii-lower - else: - throw "TYPE ERROR: data is not a string or byte array" + if (catch: hex.decode str): + throw "INVALID UUID $str" + str = str.to-ascii-lower + + /** + Constructs a new UUID from a 16-bit UUID where the $bytes are reversed. + */ + constructor.from-reversed bytes/ByteArray: + return BleUuid bytes.reverse /** - Returns the UUID as a string of the form "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX". + Returns the UUID as a string of the form "XXXX" (16-bit UUID), "XXXXXXXX" + (32-bit UUID), or "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" (other UUIDs). */ to-string -> string: if data_ is ByteArray: - if data_.size <= 4: - return hex.encode data_ + bytes := data_ as ByteArray + if bytes.size <= 8: + return hex.encode bytes else: - return (uuid.Uuid data_).stringify + return (uuid.Uuid bytes).stringify else: - return data_ + return data_ as string /** Returns a string representation of this UUID. @@ -66,25 +85,47 @@ class BleUuid: stringify -> string: return to-string - to-byte-array: + /** + Returns the UUID as a byte array. + + The result is 2 bytes long for 16-bit UUIDs, 4 bytes long for 32-bit UUIDs, + and 16 bytes long for 128-bit UUIDs. + */ + to-byte-array --reversed/bool=false -> ByteArray: + result/ByteArray := ? if data_ is string: - if data_.size <= 4: return hex.decode data_ - return (uuid.Uuid.parse data_).to-byte-array + str := data_ as string + if str.size <= 8: + result = hex.decode str + else: + result = (uuid.Uuid.parse str).to-byte-array else: - return data_ + result = data_ as ByteArray + if reversed: result = result.copy + if reversed: result.reverse --in-place + return result encode-for-platform_: if ble-platform-requires-uuid-as-byte-array_: return to-byte-array else: - return stringify + return to-string - hash-code: + hash-code -> int: return to-byte-array.hash-code operator== other/BleUuid: return to-byte-array == other.to-byte-array + /** The size, in bytes, of the UUID. */ + byte-size -> int: + if data_ is ByteArray: return (data_ as ByteArray).size + return to-byte-array.size + + /** The size, in bits, of the UUID. */ + bit-size -> int: + return byte-size * 8 + /** An attribute is the smallest data entity of GATT (Generic Attribute Profile). @@ -99,16 +140,543 @@ Conceptually, attributes are on the server, and can be accessed (read and/or wri interface Attribute: uuid -> BleUuid -BLE-CONNECT-MODE-NONE ::= 0 -BLE-CONNECT-MODE-DIRECTIONAL ::= 1 +/** +This device should not be connected to. + +See the core specification Section 9.3.2. +https://www.bluetooth.com/specifications/specs/core-specification-6-0/ +*/ +BLE-CONNECT-MODE-NONE ::= 0 + +/** +This device accepts a connection from a known peer device. + +See the core specification Section 9.3.3. +https://www.bluetooth.com/specifications/specs/core-specification-6-0/ +*/ +BLE-CONNECT-MODE-DIRECTIONAL ::= 1 + +/** +This device accepts connections from any device. + +See the core specification Section 9.3.4. +https://www.bluetooth.com/specifications/specs/core-specification-6-0/ +*/ BLE-CONNECT-MODE-UNDIRECTIONAL ::= 2 +/** +A device that is discoverable for a limited period of time. + +See the core specification Section 9.2.3. +https://www.bluetooth.com/specifications/specs/core-specification-6-0/ +*/ BLE-ADVERTISE-FLAGS-LIMITED-DISCOVERY ::= 0x01 + +/** +A device that is discoverable for an indefinite period of time. + +See the core specification Section 9.2.4. +https://www.bluetooth.com/specifications/specs/core-specification-6-0/ +*/ BLE-ADVERTISE-FLAGS-GENERAL-DISCOVERY ::= 0x02 + +/** +A device that doesn't support the Bluetooth Classic radio. + +Since Toit only supports BLE, this flag should always be set. +*/ BLE-ADVERTISE-FLAGS-BREDR-UNSUPPORTED ::= 0x04 BLE-DEFAULT-PREFERRED-MTU_ ::= 23 +/** +A Bluetooth data block. + +Data blocks are used in advertising data (AD) and scan response data (SRD) to + provide information about the device. They are also used in an extended + inquiry response (EIR), additional controller advertising data (ACAD), and + OOB data blocks. + +The possible types are listed in section 2.3 of the Bluetooth + "Assigned Numbers" document: + https://www.bluetooth.com/specifications/assigned-numbers/ + +The core specification supplement discusses the encoding of the data: + https://www.bluetooth.com/specifications/specs/core-specification-supplement/ +*/ +// I found the following link helpful: +// https://jimmywongiot.com/2019/08/13/advertising-payload-format-on-ble/ +class DataBlock: + static TYPE-FLAGS ::= 0x01 + static TYPE-SERVICE-UUIDS-16-INCOMPLETE ::= 0x02 + static TYPE-SERVICE-UUIDS-16-COMPLETE ::= 0x03 + static TYPE-SERVICE-UUIDS-32-INCOMPLETE ::= 0x04 + static TYPE-SERVICE-UUIDS-32-COMPLETE ::= 0x05 + static TYPE-SERVICE-UUIDS-128-INCOMPLETE ::= 0x06 + static TYPE-SERVICE-UUIDS-128-COMPLETE ::= 0x07 + static TYPE-NAME-SHORTENED ::= 0x08 + static TYPE-NAME-COMPLETE ::= 0x09 + static TYPE-TX-POWER-LEVEL ::= 0x0A + static TYPE-SERVICE-DATA-16 ::= 0x16 + static TYPE-SERVICE-DATA-32 ::= 0x20 + static TYPE-SERVICE-DATA-128 ::= 0x21 + static TYPE-MANUFACTURER-SPECIFIC ::= 0xFF + + /** + The type of the data block. + + The types are defined in the Bluetooth "Assigned Numbers" document: + https://www.bluetooth.com/specifications/assigned-numbers/, section 2.3. + + Some types have constants defined in this class: $TYPE-FLAGS, ... + */ + type/int + + /** + The data of the data block. + */ + data/ByteArray + + static encode-uuids_ uuids/List --uuid-byte-size/int -> ByteArray: + result := ByteArray uuids.size * uuid-byte-size + pos := 0 + uuids.do: | uuid/BleUuid | + bytes := uuid.to-byte-array --reversed + if bytes.size != uuid-byte-size: throw "INVALID_UUID_SIZE" + result.replace pos bytes + pos += uuid-byte-size + return result + + static decode-uuids_ bytes/ByteArray --uuid-byte-size/int -> List: + if bytes.size % uuid-byte-size != 0: throw "INVALID_UUID_SIZE" + result := [] + (bytes.size / uuid-byte-size).repeat: | i/int | + uuid-bytes := bytes[i * uuid-byte-size .. (i + 1) * uuid-byte-size] + result.add (BleUuid.from-reversed uuid-bytes) + return result + + static encode-service-data_ uuid/BleUuid service-data/io.Data [block] -> none: + uuid-bytes := uuid.to-byte-array --reversed + data := ByteArray uuid-bytes.size + service-data.byte-size + data.replace 0 uuid-bytes + service-data.write-to-byte-array data --at=uuid-bytes.size 0 service-data.byte-size + block.call uuid-bytes.size data + + static decode-service-data_ bytes/ByteArray --uuid-byte-size/int [block] -> any: + if bytes.size < uuid-byte-size: throw "INVALID_DATA" + uuid := BleUuid.from-reversed bytes[0 .. uuid-byte-size] + service-data := bytes[uuid-byte-size ..] + return block.call uuid service-data + + /** + Decodes a raw advertisement data packet into a list of data blocks. + */ + static decode raw/ByteArray -> List: + result := [] + pos := 0 + while pos < raw.size: + if pos + 1 >= raw.size: throw "INVALID_DATA" + size := raw[pos] + if size == 0: throw "INVALID_DATA" + if pos + size >= raw.size: throw "INVALID_DATA" + type := raw[pos + 1] + data := raw[pos + 2 .. pos + size + 1].copy + result.add (DataBlock type data) + pos += 1 + size + return result + + /** + Constructs a new advertisement data field. + + No check is made to ensure that the data is valid for the given type. + */ + constructor .type .data: + + /** + Constructs a field of flags for discovery. + + Each bit of the $flags value encodes a boolean. + + Bit 0: LE Limited Discoverable Mode, $BLE-ADVERTISE-FLAGS-LIMITED-DISCOVERY. + Bit 1: LE General Discoverable Mode, $BLE-ADVERTISE-FLAGS-GENERAL-DISCOVERY. + Bit 2: BR/EDR Not Supported (i.e., bit 37 of LMP Feature Mask Page 0). "BR/EDR" is + the Bluetooth Classic radio, and not supported by Toit. $BLE-ADVERTISE-FLAGS-BREDR-UNSUPPORTED. + Bit 3: Simultaneous LE and BR/EDR to Same Device Capable (controller). + Bit 4: Previously Used. + + The flags field may be 0 or multiple octets long. Currently, only the first octet + is used. + + The flags field must not be present in the scan response data; only in + the advertising data. + The flags field is optional and may only be present once. + */ + constructor.flags flags/int: + if not 0 <= flags < 256: throw "INVALID_FLAGS" + type = TYPE-FLAGS + if flags == 0: + data = #[] + else: + data = #[flags] + + /** + Variant of $DataBlock.flags. + + Allows to specify some flags using named arguments. + */ + constructor.flags + --limited-discovery/True + --bredr-supported/bool=false: + flags := BLE-ADVERTISE-FLAGS-LIMITED-DISCOVERY + if not bredr-supported: flags |= BLE-ADVERTISE-FLAGS-BREDR-UNSUPPORTED + return DataBlock.flags flags + + /** + Variant of $DataBlock.flags. + + Allows to specify some flags using named arguments. + */ + constructor.flags + --general-discovery/True + --bredr-supported/bool=false: + flags := BLE-ADVERTISE-FLAGS-GENERAL-DISCOVERY + if not bredr-supported: flags |= BLE-ADVERTISE-FLAGS-BREDR-UNSUPPORTED + return DataBlock.flags flags + + /** + Constructs a field with a list of 16-bit service UUIDs. + + If $incomplete is true, then the list is incomplete. + + Omitting the service UUIDs is equivalent to providing an empty + *incomplete* list. Provide an empty list to indicate that no service UUIDs + are present. + + UUID service fields are optional. Only one field per size (16, 32, 128 bits) + may be present. + The specification is not clear on whether the advertising data may contain + an incomplete list of service UUIDs and the scan response contain the + complete list. + */ + constructor.services-16 uuids/List --incomplete/bool=false: + if incomplete: + type = TYPE-SERVICE-UUIDS-16-INCOMPLETE + else: + type = TYPE-SERVICE-UUIDS-16-COMPLETE + data = encode-uuids_ uuids --uuid-byte-size=2 + + /** + Constructs a field with a list of 32-bit service UUIDs. + + See $DataBlock.services-16 for more information. + */ + constructor.services-32 uuids/List --incomplete/bool=false: + if incomplete: + type = TYPE-SERVICE-UUIDS-32-INCOMPLETE + else: + type = TYPE-SERVICE-UUIDS-32-COMPLETE + data = encode-uuids_ uuids --uuid-byte-size=4 + + /** + Constructs a field with a list of 128-bit service UUIDs. + + See $DataBlock.services-16 for more information. + */ + constructor.services-128 uuids/List --incomplete/bool=false: + if incomplete: + type = TYPE-SERVICE-UUIDS-128-INCOMPLETE + else: + type = TYPE-SERVICE-UUIDS-128-COMPLETE + data = encode-uuids_ uuids --uuid-byte-size=16 + + /** + Constructs a field with the name of the device. + + If $shortened is true, then the name is not complete. The complete name may + be retrieved by reading the device name characteristic after the connection + has been established using GATT. + It might also be allowed to have an incomplete name in the advertising data, and + the complete name in the scan response. The specification isn't clear on this. + + If an incomplete name is provided, it must be a prefix of the complete name. + The name field is optional and may only be present once. + */ + constructor.name name/string --shortened/bool=false: + if name.size > 31: throw "NAME_TOO_LONG" + if shortened: + type = TYPE-NAME-SHORTENED + else: + type = TYPE-NAME-COMPLETE + data = name.to-byte-array + + /** + Constructs a field with the transmit power level. + + The transmit power level is the power level at which the packet was transmitted. + + The power level may be used to calculate path loss on a received packet using + the following equation: path-loss = tx-power - rssi (where 'rssi' is the + received signal strength indicator). + + For example, if the TX power level is +4 (dBm) and the RSSI on the received + packet is -60 (dBm) the the total path loss is +4 - (-60) = +64 dB. + + The TX power level field is optional. + */ + constructor.tx-power-level tx-power-level/int: + if not -127 <= tx-power-level <= 127: throw "INVALID_TX_POWER_LEVEL" + type = TYPE-TX-POWER-LEVEL + data = #[tx-power-level] + + /** + Constructs a field with data for a service UUID. + + This field consists of a service UUID with the data associated with that service. + + This field is optional and may appear multiple times. + */ + constructor.service-data uuid/BleUuid service-data/io.Data: + data = #[] // Needed to make the compiler happy. + type = 0 // Needed to make the compiler happy. + encode-service-data_ uuid service-data: | uuid-byte-size encoded-data | + this.data = encoded-data + if uuid-byte-size == 2: + type = TYPE-SERVICE-DATA-16 + else if uuid-byte-size == 4: + type = TYPE-SERVICE-DATA-32 + else if uuid-byte-size == 16: + type = TYPE-SERVICE-DATA-128 + else: + throw "INVALID_UUID_SIZE" + + /** + Constructs a field with manufacturer specific data. + + This field is optional and may appear multiple times. + + The $company-id is a 16-bit value that is assigned by the Bluetooth SIG. The + value 0xFFFF is reserved for internal use. + */ + constructor.manufacturer-specific manufacturer-data/io.Data --company-id/ByteArray=#[0xFF, 0xFF]: + if company-id.size != 2: throw "INVALID_COMPANY_ID" + type = TYPE-MANUFACTURER-SPECIFIC + bytes := ByteArray 2 + manufacturer-data.byte-size + bytes.replace 0 company-id + manufacturer-data.write-to-byte-array bytes --at=2 0 manufacturer-data.byte-size + data = bytes + + /** Whether this data block encodes the flags field ($TYPE-FLAGS). */ + is-flags -> bool: + return type == TYPE-FLAGS + + /** + Returns the value of the flags field. + + See $DataBlock.flags for more information on the bits. + */ + flags -> int: + if not is-flags: throw "INVALID_TYPE" + if data.is-empty: return 0 + return data[0] + + /** + Whether this data block encodes a name ($TYPE-NAME-SHORTENED or $TYPE-NAME-COMPLETE). + + Check the $type against $TYPE-NAME-COMPLETE to know whether the name is complete. + */ + is-name -> bool: + return type == TYPE-NAME-SHORTENED or type == TYPE-NAME-COMPLETE + + /** + Returns the (possibly shortened) name of the device. + + If the name is incomplete, it is a prefix of the complete name. + + Check the $type against $TYPE-NAME-COMPLETE to know whether the name is complete. + */ + name -> string: + if not is-name: throw "INVALID_TYPE" + return data.to-string + + /** + Whether this data block encodes service UUIDs. + + Check the $type against $TYPE-SERVICE-UUIDS-16-COMPLETE to know whether + the list is complete. + */ + is-services-16 -> bool: + return type == TYPE-SERVICE-UUIDS-16-INCOMPLETE or type == TYPE-SERVICE-UUIDS-16-COMPLETE + + /** + Returns a (potentially incomplete) list of 16-bit service UUIDs. + + Check the $type against $TYPE-SERVICE-UUIDS-16-COMPLETE to know whether + the list is complete. + + See $DataBlock.services-16 for more information. + */ + services-16 -> List: + if not is-services-16: throw "INVALID_TYPE" + return decode-uuids_ data --uuid-byte-size=2 + + /** + Whether this data block encodes 32-bit service UUIDs. + + See $is-services-16. + */ + is-services-32 -> bool: + return type == TYPE-SERVICE-UUIDS-32-INCOMPLETE or type == TYPE-SERVICE-UUIDS-32-COMPLETE + + /** + Returns a (potentially incomplete) list of 32-bit service UUIDs. + + See $services-16. + */ + services-32 -> List: + if not is-services-32: throw "INVALID_TYPE" + return decode-uuids_ data --uuid-byte-size=4 + + /** + Whether this data block encodes 128-bit service UUIDs. + + See $is-services-16. + */ + is-services-128 -> bool: + return type == TYPE-SERVICE-UUIDS-128-INCOMPLETE or type == TYPE-SERVICE-UUIDS-128-COMPLETE + + /** + Returns a (potentially incomplete) list of 128-bit service UUIDs. + + See $services-16. + */ + services-128 -> List: + if not is-services-128: throw "INVALID_TYPE" + return decode-uuids_ data --uuid-byte-size=16 + + /** + Whether this data block encodes service uuids. + + This is a convenience function that checks all three types of service UUIDs. + See $is-services-16, $is-services-32, and $is-services-128. + */ + is-services -> bool: + return is-services-16 or is-services-32 or is-services-128 + + /** + Returns a list of service UUIDs. + + This is a convenience function that checks all three types of service UUIDs. + See $services-16, $services-32, and $services-128. + */ + services -> List: + if is-services-16: return services-16 + if is-services-32: return services-32 + if is-services-128: return services-128 + throw "INVALID_TYPE" + + /** + Returns whether this data block contains the given service UUID. + */ + contains-service uuid/BleUuid -> bool: + byte-size := uuid.byte-size + if byte-size == 2 and is-services-16 or + byte-size == 4 and is-services-32 or + byte-size == 16 and is-services-128: + bytes := uuid.to-byte-array --reversed + for i := 0; i < data.size; i += byte-size: + j := 0 + while j < byte-size: + if data[i + j] != bytes[j]: break + j++ + if j == byte-size: return true + return false + + /** + Whether this data block encodes the transmit power level ($TYPE-TX-POWER-LEVEL). + */ + is-tx-power-level -> bool: + return type == TYPE-TX-POWER-LEVEL + + /** + Returns the transmit power level. + + See $DataBlock.tx-power-level for more information. + */ + tx-power-level -> int: + if not is-tx-power-level: throw "INVALID_TYPE" + value := data[0] + if value >= 128: return value - 256 + return value + + /** Whether this data block encodes data for a service UUID. */ + is-service-data -> bool: + return type == TYPE-SERVICE-DATA-16 or + type == TYPE-SERVICE-DATA-32 or + type == TYPE-SERVICE-DATA-128 + + /** Whether this data block encodes data for the given uuid. */ + is-service-data-for uuid/BleUuid -> bool: + uuid-bytes := uuid.to-byte-array --reversed + if uuid-bytes.size == 2: + return type == TYPE-SERVICE-DATA-16 and data[0 .. 1] == uuid-bytes + else if uuid-bytes.size == 4: + return type == TYPE-SERVICE-DATA-32 and data[0 .. 3] == uuid-bytes + else if uuid-bytes.size == 16: + return type == TYPE-SERVICE-DATA-128 and data[0 .. 15] == uuid-bytes + return false + + /** + Calls the given block with the UUID and data of the service data block. + + Returns the result of calling the block. + + See $DataBlock.service-data for more information. + */ + service-data [block] -> any: + if not is-service-data: throw "INVALID_TYPE" + byte-size/int := ? + if type == TYPE-SERVICE-DATA-16: byte-size = 2 + else if type == TYPE-SERVICE-DATA-32: byte-size = 4 + else if type == TYPE-SERVICE-DATA-128: byte-size = 16 + else: unreachable + return decode-service-data_ data --uuid-byte-size=byte-size block + + /** Whether this data block encodes manufacturer specific data. */ + is-manufacturer-specific -> bool: + return type == TYPE-MANUFACTURER-SPECIFIC + + /** + Calls the given $block with the company ID and manufacturer specific data. + + Returns the result of calling the block. + + See $DataBlock.manufacturer-specific for more information. + */ + manufacturer-specific [block] -> any: + if not is-manufacturer-specific: throw "INVALID_TYPE" + return block.call data[0 .. 2] data[2 ..] + + /** + Writes this field into the given $bytes at the given position $at. + */ + write bytes/ByteArray --at/int [--on-error] -> int: + if bytes.size < at + 2: + return on-error.call "BUFFER_TOO_SMALL" + bytes[at] = data.size + 1 + bytes[at + 1] = type + bytes.replace (at + 2) data + return at + data.size + 2 + + /** + Converts this datablock to a raw byte array. + */ + to-raw -> ByteArray: + result := ByteArray data.size + 2 + result[0] = data.size + 1 + result[1] = type + result.replace 2 data + return result + /** Advertisement data as either sent by advertising or received through scanning. @@ -116,47 +684,202 @@ The size of an advertisement packet is limited to 31 bytes. This includes the na and bytes that are required to structure the packet. */ class AdvertisementData: + /** The $DataBlock fields of this instance. */ + data-blocks/List // Of DataBlock. + + /** + Whether connections are allowed. + + Deprecated: Use $RemoteScannedDevice.is-connectable instead. + */ + connectable/bool + + /** + Constructs an advertisement data packet with the given data blocks. + + Advertisement packets are limited to 31 data bytes. If $check-size is true, then + the size of the data blocks is checked to ensure that the packet size does not + exceed 31 bytes. + + The $connectable parameter is only used to set the deprecated field of the same name. + It is safe to ignore it if the field is not used. + */ + constructor .data-blocks --.connectable/bool=false --check-size/bool=true: + if check-size and size > 31: throw "PACKET_SIZE_EXCEEDED" + + /** + Constructs an advertisement data packet from the $raw data. + + Advertisement packets are limited to 31 data bytes. + + The $connectable parameter is only used to set the deprecated field of the same name. + It is safe to ignore it if the field is not used. + */ + constructor.raw raw/ByteArray? --.connectable/bool=false: + data-blocks = raw ? DataBlock.decode raw : [] + + /** + Deprecated. Use the $(constructor --services --manufacturer-specific) instead. + */ + constructor + --name/string?=null + --service-classes/List + --manufacturer-data/io.Data=#[] + --connectable/bool=false + --flags/int=0 + --check-size/bool=true: + return AdvertisementData + --name=name + --services=service-classes + --manufacturer-specific=manufacturer-data.byte-size > 0 ? manufacturer-data : null + --connectable=connectable + --flags=flags + --check-size=check-size + + /** + Constructs an advertisement packet. + + The $connectable parameter is only used to set the deprecated field of the same name. + It is safe to ignore it if the field is not used. + + If the $services parameter is not empty, then the list is split into 16-bit, 32-bit, + and 128-bit UUIDs. Each of the lists that isn't empty is then encoded into the + advertisement data. + */ + constructor + --name/string?=null + --services/List=[] + --manufacturer-specific/io.Data?=null + --.connectable=false + --flags/int?=null + --check-size/bool=true: + blocks := [] + if name: blocks.add (DataBlock.name name) + if not services.is-empty: + uuids-16 := [] + uuids-32 := [] + uuids-128 := [] + services.do: | uuid/BleUuid | + if uuid.to-byte-array.size == 2: uuids-16.add uuid + else if uuid.to-byte-array.size == 4: uuids-32.add uuid + else: uuids-128.add uuid + if not uuids-16.is-empty: blocks.add (DataBlock.services-16 uuids-16) + if not uuids-32.is-empty: blocks.add (DataBlock.services-32 uuids-32) + if not uuids-128.is-empty: blocks.add (DataBlock.services-128 uuids-128) + if manufacturer-specific: blocks.add (DataBlock.manufacturer-specific manufacturer-specific) + if flags: blocks.add (DataBlock.flags flags) + data-blocks = blocks + + if check-size: + size := 0 + data-blocks.do: | block/DataBlock | + size += 2 + block.data.size + if size > 31: throw "PACKET_SIZE_EXCEEDED" + /** The advertised name of the device. */ - name/string? + name -> string?: + data-blocks.do: | block/DataBlock | + if block.is-name: return block.name + return null /** Advertised service classes as a list of $BleUuid. + + Deprecated. Use $services instead. */ - service-classes/List + service-classes -> List: return services /** - Advertised manufacturer-specific data. + Advertised services as a list of $BleUuid. + + Returns the empty list if no services are present. */ - manufacturer-data_/io.Data + services -> List?: + result := [] + data-blocks.do: | block/DataBlock | + if block.is-services: + result.add-all block.services + return result /** - Whether connections are allowed. + Whether this advertisement contains the given service UUID. */ - connectable/bool + contains-service uuid/BleUuid -> bool: + data-blocks.do: | block/DataBlock | + if block.is-services and block.contains-service uuid: return true + return false /** Advertise flags. This must be a bitwise 'or' of the BLE-ADVERTISE-FLAG_* constants (see $BLE-ADVERTISE-FLAGS-GENERAL-DISCOVERY and similar). + + For backwards compatibility, returns 0 if no flags are present. */ - flags/int + flags -> int: + data-blocks.do: | block/DataBlock | + if block.is-flags: return block.flags + return 0 - constructor --.name=null --.service-classes=[] --manufacturer-data/io.Data=#[] - --.connectable=false --.flags=0 --check-size=true: - manufacturer-data_ = manufacturer-data - size := 0 - if name: size += 2 + name.size - service-classes.do: | uuid/BleUuid | - size += 2 + uuid.to-byte-array.size - if manufacturer-data.byte-size > 0: size += 2 + manufacturer-data.byte-size - if size > 31 and check-size: throw "PACKET_SIZE_EXCEEDED" + /** + The transmit power level. + */ + tx-power-level -> int?: + data-blocks.do: | block/DataBlock | + if block.is-tx-power-level: return block.tx-power-level + return null + + /** + Manufacturer data as a byte array. + + For backwards compatibility, returns an empty byte array if no manufacturer data is present. + + Returns the concatenation of the manufacturer-id and the manufacturer-specific data. + Deprecated. Use $manufacturer-specific instead. + */ manufacturer-data -> ByteArray: - if manufacturer-data_ is ByteArray: - return manufacturer-data_ as ByteArray - return ByteArray.from manufacturer-data_ + data-blocks.do: | block/DataBlock | + if block.is-manufacturer-specific: return block.data.copy + return ByteArray 0 + + /** + Calls the given $block with the first field of manufacturer specific data. + + Calls the block with the company ID and manufacturer specific data. + If no manufacturer specific data is present, the block is not called. + Returns the result of calling the block, or null if no manufacturer specific data is present. + */ + manufacturer-specific [block] -> any: + data-blocks.do: | data-block/DataBlock | + if data-block.is-manufacturer-specific: + return data-block.manufacturer-specific block + return null + + /** + The size of the advertisement data packet. + + Returns the size of all the data blocks. + */ + size -> int: + size := 0 + data-blocks.do: | block/DataBlock | + size += 2 + block.data.size + return size + + /** + Encodes the advertisement data into a byte array. + + Does not check if the size of the advertisement data exceeds 31 bytes. + */ + to-raw -> ByteArray: + result := ByteArray size + pos := 0 + data-blocks.do: | block/DataBlock | + pos = block.write result --at=pos --on-error=: throw it + return result CHARACTERISTIC-PROPERTY-BROADCAST ::= 0x0001 CHARACTERISTIC-PROPERTY-READ ::= 0x0002 @@ -418,9 +1141,12 @@ ble-handle_ resource: ble-set-characteristic-notify_ characteristic value: #primitive.ble.set-characteristic-notify -ble-advertise-start_ peripheral-manager name services manufacturer-data interval connection-mode flags: +ble-advertise-start_ peripheral-manager name services interval_us connection-mode flags: #primitive.ble.advertise-start +ble-advertise-start-raw_ peripheral-manager advertising-packet/ByteArray scan-response/ByteArray? interval_us/int connection-mode/int: + #primitive.ble.advertise-start-raw + ble-advertise-stop_ peripheral-manager: #primitive.ble.advertise-stop diff --git a/lib/ble/local.toit b/lib/ble/local.toit index 8c3d9a8af..bf694dbcb 100644 --- a/lib/ble/local.toit +++ b/lib/ble/local.toit @@ -4,7 +4,6 @@ import io import system -import system show platform import .ble import .remote show RemoteCharacteristic // For Toitdoc. @@ -59,28 +58,55 @@ class Peripheral extends Resource_: */ start-advertise data/AdvertisementData + --scan-response/AdvertisementData?=null --interval/Duration=DEFAULT-INTERVAL --connection-mode/int=BLE-CONNECT-MODE-NONE: - if platform == system.PLATFORM-MACOS: + if system.platform == system.PLATFORM-MACOS: if interval != DEFAULT-INTERVAL or connection-mode != BLE-CONNECT-MODE-NONE: throw "INVALID_ARGUMENT" - - raw-service-classes := Array_ data.service-classes.size null - - data.service-classes.size.repeat: - id/BleUuid := data.service-classes[it] - raw-service-classes[it] = id.encode-for-platform_ - ble-advertise-start_ - resource_ - data.name or "" - raw-service-classes - data.manufacturer-data_ - interval.in-us - connection-mode - data.flags + data.manufacturer-specific: throw "INVALID_ARGUMENT" + + services := data.services + raw-service-classes := Array_ services.size null + + services.size.repeat: + id/BleUuid := services[it] + raw-service-classes[it] = id.encode-for-platform_ + ble-advertise-start_ + resource_ + data.name or "" + raw-service-classes + interval.in-us + connection-mode + data.flags + else: + raw := data.to-raw + if raw.size > 31: throw "INVALID_ARGUMENT" + response-raw/ByteArray? := null + if scan-response: + response-raw = scan-response.to-raw + if response-raw.size > 31: throw "INVALID_ARGUMENT" + ble-advertise-start-raw_ + resource_ + raw + response-raw + interval.in-us + connection-mode state := resource-state_.wait-for-state ADVERTISE-START-SUCEEDED-EVENT_ | ADVERTISE-START-FAILED-EVENT_ if state & ADVERTISE-START-FAILED-EVENT_ != 0: throw "Failed to start advertising" + /** + Variant of $(start-advertise data). + + Sets the connection-mode to $BLE-CONNECT-MODE-UNDIRECTIONAL. + */ + start-advertise + data/AdvertisementData + --scan-response/AdvertisementData?=null + --interval/Duration=DEFAULT-INTERVAL + --allow-connections/True: + start-advertise data --scan-response=scan-response --interval=interval --connection-mode=BLE-CONNECT-MODE-UNDIRECTIONAL + /** Stops advertising. */ @@ -464,6 +490,7 @@ class LocalCharacteristic extends LocalReadWriteElement_ implements Attribute: // In case of a write-handler, we also accept data-received-events, just in case // data was received before the handler was set. event := for-read ? DATA-READ-REQUEST-EVENT_ : (DATA-WRITE-REQUEST-EVENT_ | DATA-RECEIVED-EVENT_) + if not resource_: throw "ALREADY_CLOSED" ble-callback-init_ resource_ timeout-ms for-read try: while true: @@ -606,6 +633,6 @@ class LocalReadWriteElement_ extends Resource_: resource-state_.wait-for-state DATA-RECEIVED-EVENT_ ble-retrieve-adapters_: - if platform == system.PLATFORM-FREERTOS or platform == system.PLATFORM-MACOS: + if system.platform == system.PLATFORM-FREERTOS or system.platform == system.PLATFORM-MACOS: return [["default", #[], true, true, null]] throw "Unsupported platform" diff --git a/lib/ble/remote.toit b/lib/ble/remote.toit index a05110b66..3fad7c181 100644 --- a/lib/ble/remote.toit +++ b/lib/ble/remote.toit @@ -3,6 +3,7 @@ // found in the lib/LICENSE file. import io +import system import .ble @@ -57,6 +58,7 @@ class Central extends Resource_: duration-us := duration ? (max 0 duration.in-us) : -1 resource-state_.clear-state COMPLETED-EVENT_ ble-scan-start_ resource_ duration-us + is-macos := system.platform == system.PLATFORM_MACOS try: while true: state := wait-for-state-with-oom_ DISCOVERY-EVENT_ | COMPLETED-EVENT_ @@ -66,23 +68,35 @@ class Central extends Resource_: if state & COMPLETED-EVENT_ != 0: return continue - service-classes := [] - raw-service-classes := next[3] - if raw-service-classes: - raw-service-classes.size.repeat: - service-classes.add - BleUuid raw-service-classes[it] - - discovery := RemoteScannedDevice - next[0] - next[1] - AdvertisementData - --name=next[2] - --service-classes=service-classes - --manufacturer-data=(next[4] ? next[4] : #[]) - --flags=next[5] - --connectable=next[6] - --check-size=false + discovery/RemoteScannedDevice := ? + if is-macos: + service-classes := [] + raw-service-classes := next[3] + if raw-service-classes: + raw-service-classes.size.repeat: + service-classes.add + BleUuid raw-service-classes[it] + + discovery = RemoteScannedDevice + next[0] + next[1] + --is-connectable=next[6] + --is-scan-response=false + AdvertisementData + --name=next[2] + --services=service-classes + --manufacturer-specific=(next[4] ? next[4] : #[]) + --flags=next[5] + --connectable=next[6] + --check-size=false + else: + discovery = RemoteScannedDevice + next[0] + next[1] + --is-connectable=next[3] + --is-scan-response=next[4] + AdvertisementData.raw next[2] --connectable=next[3] + block.call discovery finally: ble-scan-stop_ resource_ @@ -115,10 +129,21 @@ class RemoteScannedDevice: The advertisement data received from the remote device. */ data/AdvertisementData + + /** + Whether connections are allowed. + */ + is-connectable/bool + + /** + Whether this information was received in a scan response. + */ + is-scan-response/bool + /** Constructs a remote device with the given $address, $rssi, and $data. */ - constructor .address .rssi .data: + constructor .address .rssi .data --.is-connectable --.is-scan-response: /** See $super. diff --git a/lib/core/collections.toit b/lib/core/collections.toit index 36f807f43..2ec84c663 100644 --- a/lib/core/collections.toit +++ b/lib/core/collections.toit @@ -1277,6 +1277,17 @@ interface ByteArray extends io.Data: */ last -> int + /** + Reverses the order of the bytes in this instance. + */ + reverse --in-place/True -> none + + /** + Returns a copy of this instance with the order of the bytes reversed. + */ + reverse -> ByteArray + + /** Returns the $n'th byte. */ @@ -1649,6 +1660,17 @@ abstract class ByteArrayBase_ implements ByteArray: write-to-byte-array target/ByteArray --at/int from/int to/int -> none: target.replace at this from to + reverse --in-place/True -> none: + (size >> 1).repeat: | i/int | + j := size - i - 1 + tmp := this[i] + this[i] = this[j] + this[j] = tmp + + reverse -> ByteArray: + result := ByteArray size: this[size - it - 1] + return result + /** A container specialized for bytes. @@ -1813,6 +1835,12 @@ class CowByteArray_ implements ByteArray: last -> int: return backing_.last + reverse --in-place/True -> none: + backing_.reverse --in-place + + reverse -> ByteArray: + return backing_.reverse + operator [] n/int -> int: return backing_[n] diff --git a/src/compiler/propagation/type_primitive_ble.cc b/src/compiler/propagation/type_primitive_ble.cc index 643c1aa30..b57703c72 100644 --- a/src/compiler/propagation/type_primitive_ble.cc +++ b/src/compiler/propagation/type_primitive_ble.cc @@ -43,6 +43,7 @@ TYPE_PRIMITIVE_ANY(write_value) TYPE_PRIMITIVE_ANY(handle) TYPE_PRIMITIVE_ANY(set_characteristic_notify) TYPE_PRIMITIVE_ANY(advertise_start) +TYPE_PRIMITIVE_ANY(advertise_start_raw) TYPE_PRIMITIVE_ANY(advertise_stop) TYPE_PRIMITIVE_ANY(add_service) TYPE_PRIMITIVE_ANY(reserve_services) diff --git a/src/primitive.h b/src/primitive.h index df40eea09..8a260be07 100644 --- a/src/primitive.h +++ b/src/primitive.h @@ -362,7 +362,8 @@ namespace toit { PRIMITIVE(write_value, 4) \ PRIMITIVE(handle, 1) \ PRIMITIVE(set_characteristic_notify, 2) \ - PRIMITIVE(advertise_start, 7) \ + PRIMITIVE(advertise_start, 6) \ + PRIMITIVE(advertise_start_raw, 5) \ PRIMITIVE(advertise_stop, 1) \ PRIMITIVE(add_service, 2) \ PRIMITIVE(add_characteristic, 5) \ diff --git a/src/resources/ble_darwin.mm b/src/resources/ble_darwin.mm index 58f7962e1..be2ed47b8 100644 --- a/src/resources/ble_darwin.mm +++ b/src/resources/ble_darwin.mm @@ -1149,15 +1149,13 @@ _new BleRemoteDeviceResource( PRIMITIVE(advertise_start) { ARGS(BlePeripheralManagerResource, peripheral_manager, Blob, name, Array, service_classes, - Blob, manufacturing_data, int, interval_us, int, conn_mode, int, flags); + int, interval_us, int, conn_mode, int, flags); USE(interval_us); USE(conn_mode); USE(flags); NSMutableDictionary* data = [NSMutableDictionary new]; - if (manufacturing_data.length() > 0) FAIL(INVALID_ARGUMENT); - if (name.length() > 0) { data[CBAdvertisementDataLocalNameKey] = ns_string_from_blob(name); } @@ -1173,6 +1171,10 @@ _new BleRemoteDeviceResource( return process->null_object(); } +PRIMITIVE(advertise_start_raw) { + FAIL(UNIMPLEMENTED); +} + PRIMITIVE(advertise_stop) { ARGS(BlePeripheralManagerResource, peripheral_manager); diff --git a/src/resources/ble_esp32.cc b/src/resources/ble_esp32.cc index 8beef4e9f..3e27761cd 100644 --- a/src/resources/ble_esp32.cc +++ b/src/resources/ble_esp32.cc @@ -2470,7 +2470,7 @@ PRIMITIVE(scan_next) { DiscoveredPeripheral* next = central_manager->get_discovered_peripheral(); if (!next) return process->null_object(); - Array* array = process->object_heap()->allocate_array(7, process->null_object()); + Array* array = process->object_heap()->allocate_array(5, process->null_object()); if (!array) FAIL(ALLOCATION_FAILED); ByteArray* id = process->object_heap()->allocate_internal_byte_array(7); @@ -2483,59 +2483,17 @@ PRIMITIVE(scan_next) { array->at_put(1, Smi::from(next->rssi())); if (next->data_length() > 0) { - ble_hs_adv_fields fields{}; - int rc = ble_hs_adv_parse_fields(&fields, next->data(), next->data_length()); - if (rc == 0) { - if (fields.name_len > 0) { - String* name = process->allocate_string((const char*)fields.name, fields.name_len); - if (!name) FAIL(ALLOCATION_FAILED); - array->at_put(2, name); - } - - int uuids = fields.num_uuids16 + fields.num_uuids32 + fields.num_uuids128; - Array* service_classes = process->object_heap()->allocate_array(uuids, Smi::from(0)); - if (!service_classes) FAIL(ALLOCATION_FAILED); - - int index = 0; - for (int i = 0; i < fields.num_uuids16; i++) { - ByteArray* service_class = process->object_heap()->allocate_internal_byte_array(2); - if (!service_class) FAIL(ALLOCATION_FAILED); - ByteArray::Bytes service_class_bytes(service_class); - *reinterpret_cast(service_class_bytes.address()) = __builtin_bswap16(fields.uuids16[i].value); - service_classes->at_put(index++, service_class); - } - - for (int i = 0; i < fields.num_uuids32; i++) { - ByteArray* service_class = process->object_heap()->allocate_internal_byte_array(4); - if (!service_class) FAIL(ALLOCATION_FAILED); - ByteArray::Bytes service_class_bytes(service_class); - *reinterpret_cast(service_class_bytes.address()) = __builtin_bswap32(fields.uuids32[i].value); - service_classes->at_put(index++, service_class); - } - - for (int i = 0; i < fields.num_uuids128; i++) { - ByteArray* service_class = process->object_heap()->allocate_internal_byte_array(16); - if (!service_class) FAIL(ALLOCATION_FAILED); - ByteArray::Bytes service_class_bytes(service_class); - memcpy_reverse(service_class_bytes.address(), fields.uuids128[i].value, 16); - service_classes->at_put(index++, service_class); - } - array->at_put(3, service_classes); - - if (fields.mfg_data_len > 0 && fields.mfg_data) { - ByteArray* custom_data = process->object_heap()->allocate_internal_byte_array(fields.mfg_data_len); - if (!custom_data) FAIL(ALLOCATION_FAILED); - ByteArray::Bytes custom_data_bytes(custom_data); - memcpy(custom_data_bytes.address(), fields.mfg_data, fields.mfg_data_len); - array->at_put(4, custom_data); - } - - array->at_put(5, Smi::from(fields.flags)); - } - - array->at_put(6, BOOL(next->event_type() == BLE_HCI_ADV_RPT_EVTYPE_ADV_IND || - next->event_type() == BLE_HCI_ADV_RPT_EVTYPE_DIR_IND)); - } + ByteArray* data = process->object_heap()->allocate_internal_byte_array(next->data_length()); + if (!data) FAIL(ALLOCATION_FAILED); + ByteArray::Bytes data_bytes(data); + memcpy(data_bytes.address(), next->data(), next->data_length()); + array->at_put(2, data); + } + bool is_connectable = next->event_type() == BLE_HCI_ADV_RPT_EVTYPE_ADV_IND || + next->event_type() == BLE_HCI_ADV_RPT_EVTYPE_DIR_IND; + array->at_put(3, BOOL(is_connectable)); + bool is_scan_response = next->event_type() == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP; + array->at_put(4, BOOL(is_scan_response)); central_manager->remove_discovered_peripheral(); delete next; @@ -2950,138 +2908,36 @@ PRIMITIVE(set_characteristic_notify) { } PRIMITIVE(advertise_start) { - ARGS(BlePeripheralManagerResource, peripheral_manager, Blob, name, Array, service_classes, - Blob, manufacturing_data, int, interval_us, int, conn_mode, int, flags) + FAIL(UNIMPLEMENTED); +} + +PRIMITIVE(advertise_start_raw) { + ARGS(BlePeripheralManagerResource, peripheral_manager, Blob, data, Object, scan_response, int, interval_us, int, connection_mode) Locker locker(peripheral_manager->group()->mutex()); if (BlePeripheralManagerResource::is_advertising()) FAIL(ALREADY_EXISTS); if (!peripheral_manager->ensure_token()) FAIL(ALLOCATION_FAILED); - // The advertisement packet. - ble_hs_adv_fields fields{}; - // The size of the data that was already stored in the 'fields'. - int advertisement_size = 0; - // The scan response. Only used, if the advertising packet would become too big. - ble_hs_adv_fields response_fields{}; - bool uses_scan_response = false; - - if (manufacturing_data.length() > 0) { - int additional_size = 2 + manufacturing_data.length(); - ble_hs_adv_fields* target_fields = &fields; - if (advertisement_size + additional_size > BLE_HS_ADV_MAX_SZ) { - // Doesn't fit into the packet. - // Store it in the scan response instead. - target_fields = &response_fields; - fields.mfg_data = null; - } else { - advertisement_size += additional_size; - } - target_fields->mfg_data = manufacturing_data.address(); - target_fields->mfg_data_len = manufacturing_data.length(); - } - - fields.flags = flags; - advertisement_size += flags > 0 ? (2 + 1) : 0; - - ble_uuid16_t uuids_16[service_classes->length()]; - fields.uuids16 = uuids_16; - fields.uuids16_is_complete = 1; - ble_uuid32_t uuids_32[service_classes->length()]; - fields.uuids32 = uuids_32; - fields.uuids32_is_complete = 1; - ble_uuid128_t uuids_128[service_classes->length()]; - fields.uuids128 = uuids_128; - fields.uuids128_is_complete = 1; - ble_uuid16_t response_uuids_16[service_classes->length()]; - response_fields.uuids16 = response_uuids_16; - response_fields.uuids16_is_complete = 1; - ble_uuid32_t response_uuids_32[service_classes->length()]; - response_fields.uuids32 = response_uuids_32; - response_fields.uuids32_is_complete = 1; - ble_uuid128_t response_uuids_128[service_classes->length()]; - response_fields.uuids128 = response_uuids_128; - response_fields.uuids128_is_complete = 1; - for (int i = 0; i < service_classes->length(); i++) { - Object* obj = service_classes->at(i); - Blob blob; - if (!obj->byte_content(process->program(), &blob, BlobKind::STRINGS_OR_BYTE_ARRAYS)) FAIL(WRONG_OBJECT_TYPE); - - ble_uuid_any_t uuid = uuid_from_blob(blob); - if (uuid.u.type == BLE_UUID_TYPE_16) { - // Make sure the additional UUID fits into the packet. - // For the first UUID we also have to include the 2 byte header of the list. - int additional_size = fields.num_uuids16 == 0 ? 4 : 2; - ble_hs_adv_fields* target_fields = &fields; - if (advertisement_size + additional_size > BLE_HS_ADV_MAX_SZ) { - fields.uuids16_is_complete = 0; - target_fields = &response_fields; - uses_scan_response = true; - } else { - advertisement_size += additional_size; - } - const_cast(target_fields->uuids16)[target_fields->num_uuids16++] = uuid.u16; - } else if (uuid.u.type == BLE_UUID_TYPE_32) { - // Make sure the additional UUID fits into the packet. - // For the first UUID we also have to include the 2 byte header of the list. - int additional_size = fields.num_uuids32 == 0 ? 6 : 4; - ble_hs_adv_fields* target_fields = &fields; - if (advertisement_size + additional_size > BLE_HS_ADV_MAX_SZ) { - fields.uuids32_is_complete = 0; - target_fields = &response_fields; - uses_scan_response = true; - } else { - advertisement_size += additional_size; - } - const_cast(target_fields->uuids32)[target_fields->num_uuids32++] = uuid.u32; - } else { - // Make sure the additional UUID fits into the packet. - // For the first UUID we also have to include the 2 byte header of the list. - int additional_size = fields.num_uuids128 == 0 ? 18 : 16; - ble_hs_adv_fields* target_fields = &fields; - if (advertisement_size + additional_size > BLE_HS_ADV_MAX_SZ) { - fields.uuids128_is_complete = 0; - target_fields = &response_fields; - uses_scan_response = true; - } else { - advertisement_size += additional_size; - } - const_cast(target_fields->uuids128)[target_fields->num_uuids128++] = uuid.u128; - } - } - - if (name.length() > 0) { - int additional_size = 2 + name.length(); - ble_hs_adv_fields* target_fields = &fields; - if (advertisement_size + additional_size > BLE_HS_ADV_MAX_SZ) { - // Without any name, there is no need to change the 'name_is_complete' field. - // We could cut the name and send a part of it, but that's not necessary. - fields.name = null; - target_fields = &response_fields; - uses_scan_response = true; - } else { - advertisement_size += additional_size; - } - target_fields->name = name.address(); - target_fields->name_len = name.length(); - target_fields->name_is_complete = 1; - } - - int err = ble_gap_adv_set_fields(&fields); + int err = ble_gap_adv_set_data(data.address(), data.length()); if (err != BLE_ERR_SUCCESS) { if (err == BLE_HS_EMSGSIZE) FAIL(OUT_OF_RANGE); return nimble_stack_error(process, err); } - if (uses_scan_response) { - err = ble_gap_adv_rsp_set_fields(&response_fields); + if (scan_response != process->null_object()) { + Blob scan_response_data; + if (!scan_response->byte_content(process->program(), &scan_response_data, STRINGS_OR_BYTE_ARRAYS)) { + FAIL(WRONG_OBJECT_TYPE); + } + err = ble_gap_adv_rsp_set_data(scan_response_data.address(), scan_response_data.length()); if (err != BLE_ERR_SUCCESS) { if (err == BLE_HS_EMSGSIZE) FAIL(OUT_OF_RANGE); return nimble_stack_error(process, err); } } - peripheral_manager->advertising_params().conn_mode = conn_mode; + peripheral_manager->advertising_params().conn_mode = connection_mode; // TODO(anders): Be able to tune this. peripheral_manager->advertising_params().disc_mode = BLE_GAP_DISC_MODE_GEN; diff --git a/tests/ble-advertisement-test.toit b/tests/ble-advertisement-test.toit new file mode 100644 index 000000000..7209248e8 --- /dev/null +++ b/tests/ble-advertisement-test.toit @@ -0,0 +1,369 @@ +// Copyright (C) 2024 Toitware ApS. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import ble show * +import expect show * + +main: + test-data-blocks + test-advertisement-packets + test-real-world-examples + +test-data-blocks: + uuid16-1 := BleUuid "1234" + uuid16-2 := BleUuid "5678" + uuid32-1 := BleUuid "12345678" + uuid32-2 := BleUuid "9ABCDEF0" + uuid128-1 := BleUuid "12345678-9ABC-DEF0-4444-0FEDCBA98765" + uuid128-2 := BleUuid "FEDCBA98-7654-3210-4444-123456789ABC" + + block := DataBlock.flags 0 + expect-equals #[0x01, 0x01] block.to-raw + expect block.is-flags + expect-equals 0 block.flags + + block = DataBlock.flags BLE-ADVERTISE-FLAGS-BREDR-UNSUPPORTED + expect-equals #[0x02, 0x01, 0x04] block.to-raw + expect-equals 0x04 BLE-ADVERTISE-FLAGS-BREDR-UNSUPPORTED + expect block.is-flags + expect-equals 0x04 block.flags + + block = DataBlock.flags BLE-ADVERTISE-FLAGS-GENERAL-DISCOVERY + expect-equals #[0x02, 0x01, 0x02] block.to-raw + expect-equals 0x02 BLE-ADVERTISE-FLAGS-GENERAL-DISCOVERY + expect block.is-flags + expect-equals 0x02 block.flags + + // By default, the 'flags' constructor with named arguments sets the BR/EDR + // unsupported flag. + block = DataBlock.flags --limited-discovery + expect-equals #[0x02, 0x01, 0x01 | 0x04] block.to-raw + expect block.is-flags + expect-equals (0x01 | 0x04) block.flags + + block = DataBlock.flags --bredr-supported --limited-discovery + expect-equals #[0x02, 0x01, 0x01] block.to-raw + expect block.is-flags + expect-equals 0x01 block.flags + + block = DataBlock.services-16 [] + expect-equals #[0x01, 0x03] block.to-raw + expect block.is-services-16 + expect block.is-services + expect block.services.is-empty + expect block.services-16.is-empty + expect-not (block.contains-service uuid16-1) + expect-not (block.contains-service uuid16-2) + + block = DataBlock.services-16 [] --incomplete + expect-equals #[0x01, 0x02] block.to-raw + expect block.is-services-16 + expect block.is-services + expect block.services.is-empty + expect block.services-16.is-empty + expect-not (block.contains-service uuid16-1) + expect-not (block.contains-service uuid16-2) + + block = DataBlock.services-16 [uuid16-1] + expect-equals #[0x03, 0x03, 0x34, 0x12] block.to-raw + expect block.is-services-16 + expect block.is-services + expect-not block.services.is-empty + expect-not block.services-16.is-empty + expect-equals [uuid16-1] block.services-16 + expect-equals [uuid16-1] block.services + expect (block.contains-service uuid16-1) + expect-not (block.contains-service uuid16-2) + + block = DataBlock.services-16 [uuid16-1, uuid16-2] + expect-equals #[0x05, 0x03, 0x34, 0x12, 0x78, 0x56] block.to-raw + expect block.is-services-16 + expect block.is-services + expect-not block.services.is-empty + expect-not block.services-16.is-empty + expect-equals [uuid16-1, uuid16-2] block.services-16 + expect-equals [uuid16-1, uuid16-2] block.services + expect (block.contains-service uuid16-1) + expect (block.contains-service uuid16-2) + + block = DataBlock.services-32 [] + expect-equals #[0x01, 0x05] block.to-raw + expect block.is-services-32 + expect block.is-services + expect block.services.is-empty + expect block.services-32.is-empty + expect-not (block.contains-service uuid32-1) + expect-not (block.contains-service uuid32-2) + + block = DataBlock.services-32 [] --incomplete + expect-equals #[0x01, 0x04] block.to-raw + expect block.is-services-32 + expect block.is-services + expect block.services.is-empty + expect block.services-32.is-empty + expect-not (block.contains-service uuid32-1) + expect-not (block.contains-service uuid32-2) + + block = DataBlock.services-32 [uuid32-1] + expect-equals #[0x05, 0x05, 0x78, 0x56, 0x34, 0x12] block.to-raw + expect block.is-services-32 + expect block.is-services + expect-not block.services.is-empty + expect-not block.services-32.is-empty + expect-equals [uuid32-1] block.services-32 + expect-equals [uuid32-1] block.services + expect (block.contains-service uuid32-1) + expect-not (block.contains-service uuid32-2) + + block = DataBlock.services-32 [uuid32-1, uuid32-2] + expect-equals #[0x09, 0x05, 0x78, 0x56, 0x34, 0x12, 0xF0, 0xDE, 0xBC, 0x9A] block.to-raw + expect block.is-services-32 + expect block.is-services + expect-not block.services.is-empty + expect-not block.services-32.is-empty + expect-equals [uuid32-1, uuid32-2] block.services-32 + expect-equals [uuid32-1, uuid32-2] block.services + expect (block.contains-service uuid32-1) + expect (block.contains-service uuid32-2) + + block = DataBlock.services-128 [] + expect-equals #[0x01, 0x07] block.to-raw + expect block.is-services-128 + expect block.is-services + expect block.services.is-empty + expect block.services-128.is-empty + expect-not (block.contains-service uuid128-1) + expect-not (block.contains-service uuid128-2) + + block = DataBlock.services-128 [] --incomplete + expect-equals #[0x01, 0x06] block.to-raw + expect block.is-services-128 + expect block.is-services + expect block.services.is-empty + expect block.services-128.is-empty + expect-not (block.contains-service uuid128-1) + expect-not (block.contains-service uuid128-2) + + block = DataBlock.services-128 [uuid128-1] + expect-equals #[ + 0x11, 0x07, + 0x65, 0x87, 0xa9, 0xcb, 0xed, 0x0f, 0x44, 0x44, + 0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, + ] + block.to-raw + expect block.is-services-128 + expect block.is-services + expect-not block.services.is-empty + expect-not block.services-128.is-empty + expect-equals [uuid128-1] block.services-128 + expect-equals [uuid128-1] block.services + expect (block.contains-service uuid128-1) + expect-not (block.contains-service uuid128-2) + + block = DataBlock.services-128 [uuid128-1, uuid128-2] + expect-equals #[ + 0x21, 0x07, + 0x65, 0x87, 0xa9, 0xcb, 0xed, 0x0f, 0x44, 0x44, + 0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, + 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x44, 0x44, + 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, + ] + block.to-raw + expect block.is-services-128 + expect block.is-services + expect-not block.services.is-empty + expect-not block.services-128.is-empty + expect-equals [uuid128-1, uuid128-2] block.services-128 + expect-equals [uuid128-1, uuid128-2] block.services + expect (block.contains-service uuid128-1) + expect (block.contains-service uuid128-2) + + block = DataBlock.name "foobar" + expect-equals #[0x07, 0x09, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72] block.to-raw + expect block.is-name + expect-equals "foobar" block.name + + block = DataBlock.name "fooba" --shortened + expect-equals #[0x06, 0x08, 0x66, 0x6f, 0x6f, 0x62, 0x61] block.to-raw + expect block.is-name + expect-equals "fooba" block.name + + block = DataBlock.tx-power-level 42 + expect-equals #[0x02, 0x0A, 0x2A] block.to-raw + expect block.is-tx-power-level + expect-equals 42 block.tx-power-level + + block = DataBlock.tx-power-level -5 + expect-equals #[0x02, 0x0A, 0xfb] block.to-raw + expect block.is-tx-power-level + expect-equals -5 block.tx-power-level + + block = DataBlock.service-data uuid16-1 #[0x01, 0x02, 0x03] + expect-equals #[0x06, 0x16, 0x34, 0x12, 0x01, 0x02, 0x03] block.to-raw + expect block.is-service-data + data := block.service-data: | uuid data | + expect-equals uuid16-1 uuid + expect-equals #[0x01, 0x02, 0x03] data + data + expect-equals #[0x01, 0x02, 0x03] data + uuid := block.service-data: | uuid data | uuid + expect-equals uuid16-1 uuid + + block = DataBlock.service-data uuid32-1 #[0x01, 0x02, 0x03] + expect-equals #[0x08, 0x20, 0x78, 0x56, 0x34, 0x12, 0x01, 0x02, 0x03] block.to-raw + expect block.is-service-data + data = block.service-data: | uuid data | + expect-equals uuid32-1 uuid + expect-equals #[0x01, 0x02, 0x03] data + data + expect-equals #[0x01, 0x02, 0x03] data + + block = DataBlock.service-data uuid128-1 #[0x01, 0x02, 0x03] + expect-equals #[ + 0x14, 0x21, + 0x65, 0x87, 0xa9, 0xcb, 0xed, 0x0f, 0x44, 0x44, + 0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, + 0x01, 0x02, 0x03, + ] + block.to-raw + expect block.is-service-data + data = block.service-data: | uuid data | + expect-equals uuid128-1 uuid + expect-equals #[0x01, 0x02, 0x03] data + data + expect-equals #[0x01, 0x02, 0x03] data + + // By default use 0xff, 0xff as the manufacturer ID. + block = DataBlock.manufacturer-specific #[0x01, 0x02, 0x03] + expect-equals #[0x06, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03] block.to-raw + expect block.is-manufacturer-specific + block.manufacturer-specific: | id data | + expect-equals #[0xff, 0xff] id + expect-equals #[0x01, 0x02, 0x03] data + + block = DataBlock.manufacturer-specific #[0x01, 0x02, 0x03] --company-id=#[0x04, 0x05] + expect-equals #[0x06, 0xff, 0x04, 0x05, 0x01, 0x02, 0x03] block.to-raw + expect block.is-manufacturer-specific + block.manufacturer-specific: | id data | + expect-equals #[0x04, 0x05] id + expect-equals #[0x01, 0x02, 0x03] data + +test-advertisement-packets: + packet := AdvertisementData [DataBlock.flags BLE-ADVERTISE-FLAGS-BREDR-UNSUPPORTED] + expect-equals #[ + 0x02, 0x01, 0x04, + ] + packet.to-raw + expect-null packet.name + expect packet.services.is-empty + expect-equals 0x04 packet.flags + packet.manufacturer-specific: unreachable + expect-equals #[] packet.manufacturer-data + expect-equals [] packet.services + expect-equals packet.to-raw (AdvertisementData.raw packet.to-raw).to-raw + + packet = AdvertisementData [ + DataBlock.flags --limited-discovery, + DataBlock.name "foobar", + DataBlock.manufacturer-specific #[0x01, 0x02, 0x03], + ] + expect-equals #[ + 0x02, 0x01, 0x01 | 0x04, // Flags + 0x07, 0x09, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, // Name + 0x06, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03, // Manufacturer specific + ] + packet.to-raw + expect-equals packet.to-raw (AdvertisementData.raw packet.to-raw).to-raw + + // 27 bytes are fine. + packet = AdvertisementData [ + DataBlock.manufacturer-specific (ByteArray 27), + ] + expect-throw "PACKET_SIZE_EXCEEDED": + AdvertisementData [ + DataBlock.manufacturer-specific (ByteArray 28) + ] + + // We can ignore the check. + packet = AdvertisementData --no-check-size [ + DataBlock.manufacturer-specific (ByteArray 28) + ] + +test-real-world-examples: + // Some packets from + // https://jimmywongiot.com/2019/08/13/advertising-payload-format-on-ble/ + + company-id := #[0x59, 0x00] // Nordic Semiconductor. + manufacturer-data := #[ + 0x01, 0xc0, 0x11, 0x11, 0x11, 0xcc, 0x64, + 0xf0, 0xa0, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x07, + ] + real := #[ + 0x02, 0x01, 0x06, // Flags. + 0x1b, 0xff, // Manufacturer Specific. + ] + company-id + manufacturer-data + + packet := AdvertisementData [ + DataBlock.flags --general-discovery --bredr-supported=false, + DataBlock.manufacturer-specific --company-id=company-id manufacturer-data + ] + expect-equals 0x06 packet.flags + expect-equals manufacturer-data + (packet.manufacturer-specific: | id data | + expect-equals company-id id + data) + + expect-equals real packet.to-raw + expect-equals real (AdvertisementData.raw packet.to-raw).to-raw + + + real = #[ + 0x02, 0x01, 0x05, // Flags. + 0x02, 0x0a, 0xfc, // Tx Power Level. + 0x05, 0x12, 0x06, 0x00, 0x14, 0x00, // Slave connection interval range. + ] + + packet = AdvertisementData [ + DataBlock.flags --limited-discovery --bredr-supported=false, + DataBlock.tx-power-level -4, + DataBlock 0x12 #[0x06, 0x00, 0x14, 0x00], + ] + expect-equals 0x05 packet.flags + expect-equals -4 packet.tx-power-level + + expect-equals real packet.to-raw + expect-equals real (AdvertisementData.raw packet.to-raw).to-raw + + expect-equals 3 packet.data-blocks.size + + + real = #[ + 0x11, 0x07, 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, 0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e, // Services. + ] + uuid := BleUuid "6e400001-b5a3-f393-e0a9-e50e24dcca9e" + + packet = AdvertisementData [ + DataBlock.services-128 [uuid], + ] + expect-equals [uuid] packet.services + + expect-equals real packet.to-raw + expect-equals real (AdvertisementData.raw packet.to-raw).to-raw + + + real = #[ + 0x11, 0x07, 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, 0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e, // Services. + 0x0c, 0x09, 0x4e, 0x6f, 0x72, 0x64, 0x69, 0x63, 0x5f, 0x55, 0x41, 0x52, 0x54, // Name. + ] + packet = AdvertisementData [ + DataBlock.services-128 [uuid], + DataBlock.name "Nordic_UART", + ] + expect-equals [uuid] packet.services + expect-equals "Nordic_UART" packet.name + + expect-equals real packet.to-raw + expect-equals real (AdvertisementData.raw packet.to-raw).to-raw diff --git a/tests/health/gold/sdk/tests__ble-advertisement-test.toit.gold b/tests/health/gold/sdk/tests__ble-advertisement-test.toit.gold new file mode 100644 index 000000000..540f3d9d6 --- /dev/null +++ b/tests/health/gold/sdk/tests__ble-advertisement-test.toit.gold @@ -0,0 +1,3 @@ +tests/ble-advertisement-test.toit:262:28: warning: Deprecated 'AdvertisementData.manufacturer-data'. Use 'manufacturer-specific' instead + expect-equals #[] packet.manufacturer-data + ^~~~~~~~~~~~~~~~~ diff --git a/tests/hw/esp32/ble2.toit b/tests/hw/esp32/ble-util.toit similarity index 50% rename from tests/hw/esp32/ble2.toit rename to tests/hw/esp32/ble-util.toit index 8d6d9af34..2368e9d04 100644 --- a/tests/hw/esp32/ble2.toit +++ b/tests/hw/esp32/ble-util.toit @@ -3,16 +3,11 @@ // be found in the tests/LICENSE file. import ble show * -import expect show * - -TEST-SERVICE ::= BleUuid "c6fbc686-fa22-4252-9dd5-092ffd33432c" - -main: - adapter := Adapter - central := adapter.central +find-device-with-service central/Central service/BleUuid -> any: central.scan --duration=(Duration --s=3): | device/RemoteScannedDevice | - if device.data.service_classes.contains TEST-SERVICE: - unreachable + if device.data.contains-service service: + print "Found device with service $service: $device" + return device.address - adapter.close + throw "No device found with service $service" diff --git a/tests/hw/esp32/ble1-board1.toit b/tests/hw/esp32/ble1-board1.toit index c62a9764c..0e8be4340 100644 --- a/tests/hw/esp32/ble1-board1.toit +++ b/tests/hw/esp32/ble1-board1.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble1_shared.toit' +See 'ble1-shared.toit' */ import .ble1-shared as shared diff --git a/tests/hw/esp32/ble1-board2.toit b/tests/hw/esp32/ble1-board2.toit index 53015301c..afd5a0498 100644 --- a/tests/hw/esp32/ble1-board2.toit +++ b/tests/hw/esp32/ble1-board2.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble1_shared.toit' +See 'ble1-shared.toit' */ import .ble1-shared as shared diff --git a/tests/hw/esp32/ble1-shared.toit b/tests/hw/esp32/ble1-shared.toit index b669d11b6..61600c38f 100644 --- a/tests/hw/esp32/ble1-shared.toit +++ b/tests/hw/esp32/ble1-shared.toit @@ -13,6 +13,8 @@ import ble show * import expect show * import monitor +import .ble-util + SERVICE-TEST ::= BleUuid "df451d2d-e899-4346-a8fd-bca9cbfebc0b" SERVICE-TEST2 ::= BleUuid "94a11d6a-fa23-4a09-aa6f-2ca0b7cdbb70" @@ -73,7 +75,7 @@ main-peripheral --iteration/int: advertisement := AdvertisementData --name="Test" - --service-classes=[SERVICE-TEST] + --services=[SERVICE-TEST] peripheral.start-advertise --connection-mode=BLE-CONNECT-MODE-UNDIRECTIONAL advertisement data := #[] @@ -144,14 +146,6 @@ main-peripheral --iteration/int: callback-task-done.get print "end of iteration" -find-device-with-service central/Central service/BleUuid -> any: - central.scan --duration=(Duration --s=3): | device/RemoteScannedDevice | - if device.data.service_classes.contains service: - print "Found device with service $service: $device" - return device.address - - throw "No device found with service $service" - main-central --iteration/int: print "Iteration $iteration" adapter := Adapter diff --git a/tests/hw/esp32/ble2-board1.toit b/tests/hw/esp32/ble2-board1.toit index 087f45206..bdc066d87 100644 --- a/tests/hw/esp32/ble2-board1.toit +++ b/tests/hw/esp32/ble2-board1.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble2_shared.toit' +See 'ble2-shared.toit' */ import .ble2-shared as shared diff --git a/tests/hw/esp32/ble2-board2.toit b/tests/hw/esp32/ble2-board2.toit index 4691f2b25..4f52ed40c 100644 --- a/tests/hw/esp32/ble2-board2.toit +++ b/tests/hw/esp32/ble2-board2.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble2_shared.toit' +See 'ble2-shared.toit' */ import .ble2-shared as shared diff --git a/tests/hw/esp32/ble2-shared.toit b/tests/hw/esp32/ble2-shared.toit index cb36eb4ca..893aa591c 100644 --- a/tests/hw/esp32/ble2-shared.toit +++ b/tests/hw/esp32/ble2-shared.toit @@ -13,6 +13,8 @@ import ble show * import expect show * import monitor +import .ble-util + SERVICE-TEST ::= BleUuid "a1bcf0ba-7557-4968-91f8-6b0f187af2b5" CHARACTERISTIC-WRITE-ONLY ::= BleUuid "c8aa5ee4-f93e-48cd-b32c-703965a8798f" @@ -41,7 +43,7 @@ main-peripheral: advertisement := AdvertisementData --name="Test" - --service-classes=[SERVICE-TEST] + --services=[SERVICE-TEST] peripheral.start-advertise --connection-mode=BLE-CONNECT-MODE-UNDIRECTIONAL advertisement received := 0 @@ -53,14 +55,6 @@ main-peripheral: done := write-only-with-response.read print "all data received" -find-device-with-service central/Central service/BleUuid -> any: - central.scan --duration=(Duration --s=3): | device/RemoteScannedDevice | - if device.data.service_classes.contains service: - print "Found device with service $service: $device" - return device.address - - throw "No device found with service $service" - main-central: adapter := Adapter adapter.set-preferred-mtu MTU diff --git a/tests/hw/esp32/ble3-board1.toit b/tests/hw/esp32/ble3-board1.toit index 8a9fc38ae..28341a2d7 100644 --- a/tests/hw/esp32/ble3-board1.toit +++ b/tests/hw/esp32/ble3-board1.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble3_shared.toit' +See 'ble3-shared.toit' */ import .ble3-shared as shared diff --git a/tests/hw/esp32/ble3-board2.toit b/tests/hw/esp32/ble3-board2.toit index f1278995e..d4df3f16f 100644 --- a/tests/hw/esp32/ble3-board2.toit +++ b/tests/hw/esp32/ble3-board2.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble3_shared.toit' +See 'ble3-shared.toit' */ import .ble3-shared as shared diff --git a/tests/hw/esp32/ble3-shared.toit b/tests/hw/esp32/ble3-shared.toit index cca7d7c1a..1b452c762 100644 --- a/tests/hw/esp32/ble3-shared.toit +++ b/tests/hw/esp32/ble3-shared.toit @@ -20,6 +20,8 @@ import ble show * import expect show * import monitor +import .ble-util + ITERATIONS ::= 100 UUIDS ::= [ @@ -92,7 +94,7 @@ main-peripheral: advertisement := AdvertisementData --name="Test" - --service-classes=[first-service.uuid] + --services=[first-service.uuid] peripheral.start-advertise --connection-mode=BLE-CONNECT-MODE-UNDIRECTIONAL advertisement done-characteristic.read @@ -100,14 +102,6 @@ main-peripheral: adapter.close -find-device-with-service central/Central service/BleUuid -> any: - central.scan --duration=(Duration --s=3): | device/RemoteScannedDevice | - if device.data.service_classes.contains service: - print "Found device with service $service: $device" - return device.address - - throw "No device found with service $service" - main-central: done := false diff --git a/tests/hw/esp32/ble4-board1.toit b/tests/hw/esp32/ble4-board1.toit index c4f56386b..48df04f02 100644 --- a/tests/hw/esp32/ble4-board1.toit +++ b/tests/hw/esp32/ble4-board1.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble4_shared.toit' +See 'ble4-shared.toit' */ import .ble4-shared as shared diff --git a/tests/hw/esp32/ble4-board2.toit b/tests/hw/esp32/ble4-board2.toit index 911eeff5e..7ccc46501 100644 --- a/tests/hw/esp32/ble4-board2.toit +++ b/tests/hw/esp32/ble4-board2.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble4_shared.toit' +See 'ble4-shared.toit' */ import .ble4-shared as shared diff --git a/tests/hw/esp32/ble4-shared.toit b/tests/hw/esp32/ble4-shared.toit index 17d43e4ee..5a05dd8a6 100644 --- a/tests/hw/esp32/ble4-shared.toit +++ b/tests/hw/esp32/ble4-shared.toit @@ -13,6 +13,8 @@ import ble show * import expect show * import monitor +import .ble-util + TEST-SERVICE ::= BleUuid "df451d2d-e899-4346-a8fd-bca9cbfebc0b" TEST-CHARACTERISTIC ::= BleUuid "77d0b04e-bf49-4048-a4cd-fb46be32ebd0" @@ -49,21 +51,12 @@ main-peripheral: advertisement := AdvertisementData --name="Test" - --service-classes=[TEST-SERVICE] + --services=[TEST-SERVICE] peripheral.start-advertise --connection-mode=BLE-CONNECT-MODE-UNDIRECTIONAL advertisement done-latch.get print "done" - -find-device-with-service central/Central service/BleUuid -> any: - central.scan --duration=(Duration --s=3): | device/RemoteScannedDevice | - if device.data.service_classes.contains service: - print "Found device with service $service: $device" - return device.address - - throw "No device found with service $service" - main-central: adapter := Adapter central := adapter.central diff --git a/tests/hw/esp32/ble5-board1.toit b/tests/hw/esp32/ble5-board1.toit index 1521c3c79..fa400d829 100644 --- a/tests/hw/esp32/ble5-board1.toit +++ b/tests/hw/esp32/ble5-board1.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble5_shared.toit' +See 'ble5-shared.toit' */ import .ble5-shared as shared diff --git a/tests/hw/esp32/ble5-board2.toit b/tests/hw/esp32/ble5-board2.toit index db00c7c84..f187514e4 100644 --- a/tests/hw/esp32/ble5-board2.toit +++ b/tests/hw/esp32/ble5-board2.toit @@ -3,7 +3,7 @@ // be found in the tests/LICENSE file. /** -See 'ble5_shared.toit' +See 'ble5-shared.toit' */ import .ble5-shared as shared diff --git a/tests/hw/esp32/ble5-shared.toit b/tests/hw/esp32/ble5-shared.toit index bb6995f31..fcca96522 100644 --- a/tests/hw/esp32/ble5-shared.toit +++ b/tests/hw/esp32/ble5-shared.toit @@ -13,7 +13,9 @@ import ble show * import expect show * import monitor -TEST-SERVICE ::= BleUuid "df451d2d-e899-4346-a8fd-bca9cbfebc0b" +import .ble-util + +TEST-SERVICE ::= BleUuid "e5c245a3-1b7e-44cf-bc37-7040b719fe46" TEST-CHARACTERISTIC ::= BleUuid "77d0b04e-bf49-4048-a4cd-fb46be32ebd0" main-peripheral: @@ -40,21 +42,12 @@ main-peripheral: advertisement := AdvertisementData --name="Test" - --service-classes=[TEST-SERVICE] + --services=[TEST-SERVICE] peripheral.start-advertise --connection-mode=BLE-CONNECT-MODE-UNDIRECTIONAL advertisement done-latch.get print "done" - -find-device-with-service central/Central service/BleUuid -> any: - central.scan --duration=(Duration --s=3): | device/RemoteScannedDevice | - if device.data.service_classes.contains service: - print "Found device with service $service: $device" - return device.address - - throw "No device found with service $service" - main-central: adapter := Adapter central := adapter.central diff --git a/tests/hw/esp32/ble6-advertise-board1.toit b/tests/hw/esp32/ble6-advertise-board1.toit new file mode 100644 index 000000000..6f82f8c5e --- /dev/null +++ b/tests/hw/esp32/ble6-advertise-board1.toit @@ -0,0 +1,12 @@ +// Copyright (C) 2024 Toitware ApS. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +/** +See 'ble6-advertise-shared.toit' +*/ + +import .ble6-advertise-shared as shared + +main: + shared.main-peripheral diff --git a/tests/hw/esp32/ble6-advertise-board2.toit b/tests/hw/esp32/ble6-advertise-board2.toit new file mode 100644 index 000000000..d392b2160 --- /dev/null +++ b/tests/hw/esp32/ble6-advertise-board2.toit @@ -0,0 +1,12 @@ +// Copyright (C) 2024 Toitware ApS. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +/** +See 'ble6-advertise-shared.toit' +*/ + +import .ble6-advertise-shared as shared + +main: + shared.main-central diff --git a/tests/hw/esp32/ble6-advertise-shared.toit b/tests/hw/esp32/ble6-advertise-shared.toit new file mode 100644 index 000000000..af745bda3 --- /dev/null +++ b/tests/hw/esp32/ble6-advertise-shared.toit @@ -0,0 +1,169 @@ +// Copyright (C) 2024 Toitware ApS. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +/** +Tests that we can advertise and read different fields. + +Also tests the scan response. + +Run `ble6-advertise-board1.toit` on board1, first. +Once that one is running, run `ble6-advertise-board2.toit` on board2. +*/ + +import ble show * +import expect show * +import monitor + +import .ble-util + +TEST-SERVICE ::= BleUuid "eede145e-b6a6-4d61-8156-ed10d5b75903" +TEST-CHARACTERISTIC ::= BleUuid "6b50d82f-ae33-4e0e-b161-50db92c49ad2" + +COMPANY-ID ::= #[0x12, 0x34] +MANUFACTURER-DATA ::= #[0x56, 0x78] + +main-peripheral: + adapter := Adapter + peripheral := adapter.peripheral + + service := peripheral.add-service TEST-SERVICE + characteristic := service.add-characteristic TEST-CHARACTERISTIC + --properties=CHARACTERISTIC-PROPERTY-READ | CHARACTERISTIC-PROPERTY-WRITE + --permissions=CHARACTERISTIC-PERMISSION-READ | CHARACTERISTIC-PERMISSION-WRITE + --value=null + + peripheral.deploy + + next-semaphore := monitor.Semaphore + done := false + + task:: + count := 0 + while not done: + characteristic.handle-write-request: | data/ByteArray | + if data.is-empty: throw "Unexpected empty data" + if data[0] == count: + print "." + count++ + next-semaphore.up + else: + throw "Unexpected data: $data" + + advertisement := AdvertisementData + --name="Test" + --services=[TEST-SERVICE] + peripheral.start-advertise --allow-connections advertisement + next-semaphore.down + peripheral.stop-advertise + + advertisement = AdvertisementData [] + peripheral.start-advertise advertisement + next-semaphore.down + peripheral.stop-advertise + + advertise := : | blocks | + advertisement = AdvertisementData blocks + peripheral.start-advertise --allow-connections advertisement + next-semaphore.down + peripheral.stop-advertise + + advertise.call [] + advertise.call [ + DataBlock.flags --general-discovery --bredr-supported=false, + DataBlock.manufacturer-specific --company-id=COMPANY-ID MANUFACTURER-DATA, + ] + advertise.call [ + DataBlock.name "Test", + DataBlock.services-128 [TEST-SERVICE], + ] + advertise.call [ + DataBlock.manufacturer-specific (ByteArray 27: it) + ] + + done = true + + peripheral.close + adapter.close + + print "done" + +scan address/ByteArray --central/Central -> RemoteScannedDevice: + central.scan --duration=(Duration --s=3): | device/RemoteScannedDevice | + if device.address == address: + return device + throw "Device not found" + +central-test-counter := 0 + +test-data + address/any + characteristic/RemoteCharacteristic + --central/Central + --is-connectable/bool=true [block]: + remote-scanned := scan address --central=central + expect-equals address remote-scanned.address + expect-equals is-connectable remote-scanned.is-connectable + block.call remote-scanned.data + characteristic.write #[central-test-counter++] + +main-central: + adapter := Adapter + central := adapter.central + + address := find-device-with-service central TEST-SERVICE + + remote-device := central.connect address + all-services := remote-device.discover-services + services := remote-device.discovered-services + + characteristic/RemoteCharacteristic? := null + + services.do: | service/RemoteService | + characteristics := service.discover-characteristics + characteristics.do: | found/RemoteCharacteristic | + if found.uuid == TEST-CHARACTERISTIC: characteristic = found + + test-data address characteristic --central=central: | data/AdvertisementData | + blocks := data.data-blocks + expect-equals 2 blocks.size + expect-equals [TEST-SERVICE] data.services + expect-equals "Test" data.name + + + test-data address characteristic --central=central --no-is-connectable: | data/AdvertisementData | + blocks := data.data-blocks + expect-equals 0 blocks.size + + test-data address characteristic --central=central: | data/AdvertisementData | + blocks := data.data-blocks + expect-equals 0 blocks.size + + test-data address characteristic --central=central: | data/AdvertisementData | + blocks := data.data-blocks + expect-equals 2 blocks.size + expect-equals 0x06 data.flags + expect-equals COMPANY-ID + (data.manufacturer-specific: | id data | + expect-equals MANUFACTURER-DATA data + id) + + test-data address characteristic --central=central: | data/AdvertisementData | + blocks := data.data-blocks + expect-equals 2 blocks.size + expect-equals "Test" data.name + expect-equals [TEST-SERVICE] data.services + + test-data address characteristic --central=central: | data/AdvertisementData | + blocks := data.data-blocks + expect-equals 1 blocks.size + expect-equals #[0xff, 0xff] + (data.manufacturer-specific: | id data | + expect-equals (ByteArray 27: it) data + id) + + remote-device.close + central.close + adapter.close + + print "done"