[{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eHave you always wanted to get your Circuit Playground Bluefruit working with Home Assistant? Have some Circuit Playground Bluefruit devices sitting around and you'd like to use them as temperature sensors or use the buttons to control aspects of your home?\u003c/p\u003e\n\u003cp\u003eWell, you can. Using Circuit Python and the \u003ca href=\"https://bthome.io\"\u003eBTHome specification\u003c/a\u003e for advertising data over Bluetooth, you can broadcast the temperature from a Circuit Playground over Bluetooth to your Home Assistant. \u0026nbsp;\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"image_url","content":"https://cdn-shop.adafruit.com/640x480/4333-11.jpg","metadata":{"alt":"Circuit","caption":""}},{"element_type":"alert","content":"\u003cp\u003eBTHome is made for devices to broadcast sensor data. There is no input capability, so there is no ability to control, for example, the LEDs.\u003c/p\u003e","metadata":{"class":"element row-fluid alert-element build-alert alert-info","markdown":"BTHome is made for devices to broadcast sensor data. There is no input capability, so there is no ability to control, for example, the LEDs.","alert_type":"info"}},{"element_type":"text","content":"\n        \u003ch3\u003eBLE Advertisement Primer\u003c/h3\u003e\n      ","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThe \u003ca href=\"https://bthome.io/format/\"\u003eBTHome website\u003c/a\u003e has a nice, quick primer on the advertisement format for BLE. An even shorter primer is here. BLE defines the format for advertisements and data packets. We're interested in the Advertisement payload format, which is part of the overall data packet.\u003c/p\u003e\n\u003cp\u003eThe advertisement payload consists of a series of advertising (AD) elements. Each AD element has a length (1 byte), an ID (1 byte), followed by ad element specific data (variable length).\u0026nbsp;\u003c/p\u003e\n      \n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/099/original/adelement.png?1723554940","metadata":{"caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eA partial list of element types and their associated IDs are given in the table below.\u003c/p\u003e\n      \n\n\n\n\n\n\n","metadata":{}},{"element_type":"markdown","content":"\u003ctable\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003cth\u003eAd element type\u003c/th\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eFlags\u003c/td\u003e\n\u003ctd\u003e0x01\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003ePartial List of 16 bit UUIDs\u003c/td\u003e\n\u003ctd\u003e0x02\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eComplete List of 16 bit UUIDs\u003c/td\u003e\n\u003ctd\u003e0x03\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003ePartial List of 32 bit UUIDs\u003c/td\u003e\n\u003ctd\u003e0x04\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eComplete List of 32 bit UUIDs\u003c/td\u003e\n\u003ctd\u003e0x05\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003ePartial List of 128 bit UUIDS\u003c/td\u003e\n\u003ctd\u003e0x06\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eComplete List of 128 bit UUIDs\u003c/td\u003e\n\u003ctd\u003e0x07\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eShort Local Name\u003c/td\u003e\n\u003ctd\u003e0x08\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eComplete Local Name\u003c/td\u003e\n\u003ctd\u003e0x09\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eTransmit Power Level\u003c/td\u003e\n\u003ctd\u003e0x0A\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eClass of Device\u003c/td\u003e\n\u003ctd\u003e0x0D\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSimple Pairing Hash\u003c/td\u003e\n\u003ctd\u003e0x0E\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSimple Pairing Randomizer\u003c/td\u003e\n\u003ctd\u003e0x0F\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSecurity Manager  TK Value\u003c/td\u003e\n\u003ctd\u003e0x 10\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSecurity Manager Out of Band Flags\u003c/td\u003e\n\u003ctd\u003e0x11\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSlave Connection Interval Range\u003c/td\u003e\n\u003ctd\u003e0x12\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eList of 16 bit Service Solicitation IDs\u003c/td\u003e\n\u003ctd\u003e0x14\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eList of 128 bit Service Solicitation IDs\u003c/td\u003e\n\u003ctd\u003e0x15\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eService Data\u003c/td\u003e\n\u003ctd\u003e0x16\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003ePublic Target Address\u003c/td\u003e\n\u003ctd\u003e0x17\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eRandom Target Address\u003c/td\u003e\n\u003ctd\u003e0x18\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n","metadata":{"markdown":"| Ad element type | ID |\n|-----------| -----------|\n| Flags   |  0x01  |\n| Partial List of 16 bit UUIDs | 0x02 |\n| Complete List of 16 bit UUIDs | 0x03 |\n| Partial List of 32 bit UUIDs | 0x04 |\n| Complete List of 32 bit UUIDs | 0x05 |\n| Partial List of 128 bit UUIDS | 0x06 |\n| Complete List of 128 bit UUIDs | 0x07 |\n| Short Local Name | 0x08 |\n| Complete Local Name | 0x09 |\n| Transmit Power Level | 0x0A |\n| Class of Device | 0x0D |\n| Simple Pairing Hash | 0x0E |\n| Simple Pairing Randomizer | 0x0F |\n| Security Manager  TK Value | 0x 10 |\n| Security Manager Out of Band Flags | 0x11 |\n| Slave Connection Interval Range | 0x12 |\n| List of 16 bit Service Solicitation IDs | 0x14 |\n| List of 128 bit Service Solicitation IDs | 0x15 |\n| Service Data | 0x16 |\n| Public Target Address | 0x17 |\n| Random Target Address | 0x18 |\n"}},{"element_type":"text","content":"\n  \n  \n  \n        \u003cp\u003eThe \u003cstrong\u003eFlags\u003c/strong\u003e element for BTHome has the format 0x020106. The first byte is 0x02, indicating that the rest of the ad element is 2 bytes. The second is the ID, which is 0x01. The last byte is a set of flags meaning that the BLE is in discoverable mode. See the \u003ca href=\"https://bthome.io/format/\"\u003eBTHome format\u003c/a\u003e for more info.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eThe \u003cstrong\u003eComplete Local Name\u003c/strong\u003e element simply has the length of the element, the ID (0x09), and a series of bytes for for the UTF-8 encoded name. Example: \u003cspan\u003e0a 09 43 50 20 42 54 48 6f 6d 65. First byte is 0x0A, meaning there are 10 further bytes. The ID is 0x09. And the following bytes are the ASCII-encoded letters representing \"CP BTHome\".\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003eThe \u003cstrong\u003eService Data\u003c/strong\u003e element type allows arbitrary data to be advertised within it's structure. The format is: length (1 byte), ID of 0x16 (1 byte), Service UUID (2 bytes), followed by a variable number of bytes defined by the Service. The Service UUID is unique to the service and typically it is purchased by a company for use in it's products.\u0026nbsp;\u003c/p\u003e\n      \n\n\n","metadata":{}},{"element_type":"alert","content":"\u003cp\u003eThe name field cannot be too long! I tried \"Circuit Playground Bluefruit\" as the name and it's length seemed to prevent it from advertising correctly. Simply shortening it fixed the issue.\u003c/p\u003e","metadata":{"class":"element row-fluid alert-element build-alert alert-warning","markdown":"The name field cannot be too long! I tried \"Circuit Playground Bluefruit\" as the name and it's length seemed to prevent it from advertising correctly. Simply shortening it fixed the issue.","alert_type":"warning"}},{"element_type":"text","content":"\n        \u003ch3\u003eBTHome Service\u003c/h3\u003e\n      ","metadata":{}},{"element_type":"text","content":"\n  \n        \u003cp\u003eThe BTHome specification provides a format for information within the Service Data element type. This service requires a Service UUID that is unique, to differentiate it from other possible services that can be transmitted within the Service Element. The company Allterco Robotics (manufacturer of Shelly products) purchased a UUID and has \u003ca href=\"https://bthome.io/images/License_Statement_-_BTHOME.pdf\"\u003egiven permission\u003c/a\u003e for anyone to use it with BTHome. The UUID is 0xFCD2. The \u003ca href=\"https://bthome.io/format/\"\u003eformat\u003c/a\u003e of all the various sensors are given on the BTHome site.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eThe BTHome format consists of the UUID, followed by a byte of flags (determining encryption, trigger-based, and version number) which is normally 0x40 (meaning no encryption, not trigger-based, version 2), followed by bytes that depend on the sensor information being sent.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eFor example, the temperature data is given by the format of temperature ID (0x45), followed by 2 bytes for the temperature data.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eAn example of a complete ad payload would be:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ccode\u003e\u003cspan\u003e[02 01 06 0a 09 43 50 20 42 54 48 6f 6d 65 07 16 d2 fc 40 45 15 01]\u003cbr\u003e\u003c/span\u003e\u003cspan\u003e|--Flags-|-------Complete Local Name -----|Srv Dt|UUID|Flg|Temp---|\u003c/span\u003e\u003c/code\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThe other BTHome formats are similar. A complete advertisement for the code below is:\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e[02 01 06 0a 09 43 50 20 42 54 48 6f 6d 65 10 16 d2 fc 40 05 a4 27 01 3a 00 45 15 01 51 82 24]\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eThis has the Flags, Complete Local Name (\"CP BTHome\"), the Service Data packet with the BTHome elements of illuminance (ID=0x05), button (ID=0x3A), temperature (ID=0x45), and acceleration (ID=0x51).\u0026nbsp;\u003c/p\u003e\n      \n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eBy leveraging the existing \u003ca href=\"https://github.com/adafruit/Adafruit_CircuitPython_BLE/tree/main/adafruit_ble\"\u003eAdafruit Circuit Python Bluetooth BLE code framework\u003c/a\u003e, we can add a Service that advertises data in the BTHome format. The Bluetooth advertising payload has various advertising data elements that it supports. Below is a list of the advertising data elements and their associated IDs. (Refer to the header file on\u0026nbsp;\u003ca href=\"https://jimmywongiot.com/2019/08/13/advertising-payload-format-on-ble/\"\u003ethis page\u003c/a\u003e) \u003cspan\u003eThe ones we are interested in are the Flags (0x01), Complete Local Name (0x09), and Service Data (0x16).\u003c/span\u003e\u003cspan\u003e\u0026nbsp;\u003c/span\u003e\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eCode\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eSo we first include all the headers that we will need:\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"import board\nimport neopixel\nimport time\nimport math\nimport array\nimport digitalio\nimport busio\nimport analogio\nimport simpleio\nimport adafruit_thermistor\nimport adafruit_lis3dh\nimport microcontroller\nimport audiobusio\nfrom rainbowio import colorwheel\n\nfrom adafruit_ble import BLERadio\nfrom adafruit_ble import advertising\n#from adafruit_ble.advertising.standard import ProvideServicesAdvertisement\nfrom adafruit_ble.uuid import StandardUUID\nfrom adafruit_ble.services import Service\nimport adafruit_ble.advertising.standard\nfrom adafruit_ble.characteristics.int import Uint8Characteristic, Int16Characteristic","metadata":{"language":"auto","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThen we write the Python code for all the sensors we will want to advertise. In this case, it is the temperature sensor, light sensor, accelerometer, and one of the buttons. It seems that, despite the BTHome spec allowing it, the Home Assistant BTHome implementation doesn't allow more than one sensor of the same type to be defined in the payload. (At least I couldn't get it to work).\u003c/p\u003e\n\u003cp\u003eNote that I also set up the microphone for sound, but I don't actually use it because I don't really need it. The sound BTHome element is a boolean, so we would need determine the threshold which would constitute a sound detected. I'll leave that as an exercise for the reader.\u0026nbsp;\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"thermistor = adafruit_thermistor.Thermistor(board.TEMPERATURE, 10000, 10000, 23.65, 3950)\n\n# The Circuit Playground uses the Everlight ALS-PT19 as the sensor.\n# The datasheet shows a graph of lux vs voltage and the lux ranges from 0 to 10000\n# The output in Python ranges from 0 to 65535. \n# So I'll map the range 0 65535 to 0 to 10000 as a rough estimate\nlight = analogio.AnalogIn(board.LIGHT)\n\nmic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA,\n                       sample_rate=16000, bit_depth=16)\n\nbuttonA = digitalio.DigitalInOut(board.BUTTON_A)\nbuttonA.switch_to_input(pull=digitalio.Pull.DOWN)\nbuttonB = digitalio.DigitalInOut(board.BUTTON_B)\nbuttonB.switch_to_input(pull=digitalio.Pull.DOWN)\n\ni2c = busio.I2C(board.ACCELEROMETER_SCL, board.ACCELEROMETER_SDA)\nint1 = digitalio.DigitalInOut(board.ACCELEROMETER_INTERRUPT)\nlis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, address=0x19, int1=int1)\nlis3dh.range = adafruit_lis3dh.RANGE_8_G","metadata":{"language":"auto","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eNow we get to the parts that were more difficult to figure out. We need to define a class for the overall advertisement that will be used for Bluetooth. This advertisement needs to have the 3 components mentioned above - flags, name, and Service Data. Adafruit builds in the flags and name components into its base Advertisement class. So that part becomes easy. However, we need to have a Service Data component in the Advertisement class as well. But in order to instantiate a Service Data class, we need to give it a Service. So we need to first define a Service class.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eWe first define a BTHomeService.\u0026nbsp;\u003cspan\u003eThis will be a simple class that defines a UUID (Universally Unique Identifier). \u003c/span\u003e\u003cspan\u003eEvery Service needs to have a Universally Unique Identifier and the Allterco Robotics company has \u003c/span\u003e\u003ca href=\"bthome.io/images/License_Statement_-_BTHOME.pdf\"\u003epurchased the UUID of 0xFCD2\u003c/a\u003e\u003cspan\u003e for use for the implementation of BTHome.\u003c/span\u003e\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"class BTHomeService(Service):\n    # UUID given by Alterco Robotics\n    uuid = StandardUUID(0xFCD2)\n    # NOTE: There are no characteristics for this service.\n    # That is, no explicitely defined BLE characteristics will be used. Instead we just have whatever BTHome \n    # has defined internally in their list. But these are not official characteristics.\n\n    # bit 0: Encryption flag - 0 non-encrypted, 1 encrypted\n    # bit 1: reserved\n    # bit 2: Trigger based device flag - 0 regular interval, 1 non-regular interval\n    # bit 3-4: Reserved\n    # bit 5-7: BTHome Version  - version 2, so 010\n    # This gives 01000000b = 0x40\n    #device_information = Uint8Characteristic(min_value=0x40,max_value=0x40, initial_value=0x40)\n    #temperatureField = 0x45 # Data type for 0.1 precision temperature field in BTHome\n    #temperatureValue = 0x0000  # Dummy info for now\n    \n    \n    def __get__(self, obj, objtype=None):\n        super.__get__(obj, objtype)","metadata":{"language":"auto","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eWith the basic Service defined, we can define the advertisement. The only thing we need is to create the service inside the class. The advertisement class will call into the Service to get the bytes it needs to make up the actual advertisement.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eclass BTHomeAd(advertising.Advertisement):\u003c/code\u003e\u003cbr\u003e\u003ccode\u003e\u0026nbsp; \u0026nbsp; btService = BTHomeService()\u003c/code\u003e\u003cbr\u003e\u003ccode\u003e\u0026nbsp; \u0026nbsp; s = advertising.standard.ServiceData(btService)\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eWith that defined, we can now create classes for the Circuit Playground and all the sensors we need. The BTHome format defines different IDs for the various sensor types. These can be found \u003ca href=\"https://bthome.io/format/\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"class BTHomeTemperature:\n    global thermistor\n    objectID = 0x45     # temperature\n    \n    def __get__(self, obj, objtype=None):\n        b = bytearray(3)\n        b[0] = self.objectID\n        temperature = int(thermistor.temperature * 10)\n        b[1] =  temperature \u0026 0xFF\n        b[2] = (temperature \u0026 0xFF00) \u003e\u003e 8\n        return b\n\nclass BTHomeLight:\n    objectID = 0x05     # illuminance\n    \n    def __get__(self, obj, objtype=None):\n        b = bytearray(4)\n        b[0] = self.objectID\n        lux = int(simpleio.map_range(light.value, 0, 65535, 0, 10000) * 100)\n        b[3] = (lux \u0026 0xFF0000) \u003e\u003e 16\n        b[2] = (lux \u0026 0x00FF00) \u003e\u003e 8\n        b[1] = lux \u0026 0x00FF\n        return b\n\nclass BTHomeSound:\n    objectID = 0x2A     # sound - boolean\n    # Number of samples to read at once.\n    NUM_SAMPLES = 160\n    \n    \n    def __init__(self):\n        # Record an initial sample to calibrate. Assume it's quiet when we start.\n        self.samples = array.array('H', [0] * self.NUM_SAMPLES)\n        mic.record(self.samples, len(self.samples))\n    \n    def mean(self, values):\n        return sum(values) / len(values)\n    \n    # Remove DC bias before computing RMS.\n    def normalized_rms(self, values):\n        minbuf = int(self.mean(values))\n        samples_sum = sum(\n            float(sample - minbuf) * (sample - minbuf)\n            for sample in values\n        )\n        return math.sqrt(samples_sum / len(values))\n        \n    def get(self):\n        mic.record(self.samples, len(self.samples))\n        magnitude = self.normalized_rms(self.samples)\n        return(magnitude)\n        # You might want to print this to see the values.\n        # print(magnitude)\n\nclass BTHomeButton:\n    objectID = 0x3A     # button\n    \n    def __init__(self, button):\n        self.button = button\n        self.current_value = button.value\n    \n    def __get__(self, obj, objtype=None):\n        b = bytearray(2)\n        b[0] = self.objectID\n        if self.button.value != self.current_value:\n            if not self.current_value:\n                # Button pressed\n                self.current_value = True\n                b[1] = 0x01\n            else:\n                # Button released\n                self.current_value = False\n                b[1] = 0x00\n        else:\n            b[1] =0x00\n        return b\n    \nclass BTHomeAccelerometer:\n    objectID = 0x51\n    \n    def __get__(self, obj, objtype=None):\n         b = bytearray(3)\n         b[0] = self.objectID\n         x,y,z = lis3dh.acceleration\n         \n         '''\n         x = int(abs(x) * 1000)\n         b[1] = x \u0026 0x0FF\n         b[2] = (x \u0026 0x00FF00) \u003e\u003e 8\n\n         b[3] = self.objectID\n         y = int(y * 1000)\n         b[4] = y \u0026 0x0FF\n         b[5] = (y \u0026 0x00FF00) \u003e\u003e 8\n         '''\n         b[0] = self.objectID\n         z = int(z * 1000)\n         b[1] = z \u0026 0x0FF\n         b[2] = (z \u0026 0x00FF00) \u003e\u003e 8\n         \n         return b","metadata":{"language":"auto","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThen we just need a class to contain all the sensors, representing our Circuit Playground Bluefruit.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"class BTHomeCircuitPlayground:\n    device_info = bytearray(1)\n    device_info[0] = 0x40\n    temp = BTHomeTemperature()\n    lux = BTHomeLight()\n    A = BTHomeButton(buttonA)\n    B = BTHomeButton(buttonB)\n    z_accel = BTHomeAccelerometer()\n    \n    def get(self):\n        # In objectID order    0x05     0x3A        0x45           0x51\n        return self.device_info + self.lux + self.A + self.temp + self.z_accel","metadata":{"language":"auto","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eNow we can just instantiate all the classes, fill in the necessary pieces, and call the \u003ccode\u003eget()\u003c/code\u003e function on our CP Bluefruit class to grab all the sensor data and put it into the Service Data.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eAs mentioned above, the Adafruit Advertisement class has the flags and complete_name parameters built into it, so there is no need to define a class for those. We just need to fill them in. The\u0026nbsp;\u003ccode\u003egeneral_discovery \u003c/code\u003eflag and the \u003ccode\u003ele_only \u003c/code\u003eflag are the only ones we need to have set for the flags element.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eThe actual service is simply set to the \u003ccode\u003ebytearray()\u003c/code\u003e we get from our CP Bluefruit class. The \u003ccode\u003eServiceData \u003c/code\u003eclass is written in such a way that setting it will keep the \u003ccode\u003eServiceData \u003c/code\u003eUUID in tact and append whatever data you give it. This is why we can simply create the BTHome payload without regard for the ServiceData UUID itself. The BTHome payload consists of the device information followed by all sensor data.\u0026nbsp;\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"bthomeCP = BTHomeCircuitPlayground()\nbtad = BTHomeAd()\nbtad.complete_name = \"CP BTHome\"\nbtad.flags.general_discovery = True\nbtad.flags.le_only = True\nbtad.s = bthomeCP.get()\nprint('[' + advertising.to_hex(btad.__bytes__()) + ']')\n\nble = BLERadio()\npixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.2, auto_write=False)\n\nwhile True:\n    btad.s = bthomeCP.get()\n    #print('[' + advertising.to_hex(btad.__bytes__()) + ']')\n    \n    ble.start_advertising(btad)\n    # Change this to a non-blocking time comparison\n    # That way we can check for button presses in the meantime.\n    time.sleep(INTERVAL)\n    ble.stop_advertising()","metadata":{"language":"auto","linenums":false}},{"element_type":"code","content":"import board\nimport neopixel\nimport time\nimport math\nimport array\nimport digitalio\nimport busio\nimport analogio\nimport simpleio\nimport adafruit_thermistor\nimport adafruit_lis3dh\nimport microcontroller\nimport audiobusio\nfrom rainbowio import colorwheel\n\nfrom adafruit_ble import BLERadio\nfrom adafruit_ble import advertising\n#from adafruit_ble.advertising.standard import ProvideServicesAdvertisement\nfrom adafruit_ble.uuid import StandardUUID\nfrom adafruit_ble.services import Service\nimport adafruit_ble.advertising.standard\nfrom adafruit_ble.characteristics.int import Uint8Characteristic, Int16Characteristic\n\nDEVICE_NAME = \"CP Bluefruit - Living Room\"\nINTERVAL = 0.25\nSENSITIVITY = 0.01\n\na = advertising.Advertisement()\na.complete_name = \"CP Bluefruit\"\na.flags.general_discovery = 1\na.flags.le_only = 1\n\nprint('[' + advertising.to_hex(a.__bytes__()) + ']')\nuuid = StandardUUID(0xD2FC) # The UUID of BTHome Data, as given by Allterco Robotics.\nprint('UUID: ' + uuid.__str__())\n\nthermistor = adafruit_thermistor.Thermistor(board.TEMPERATURE, 10000, 10000, 23.65, 3950)\n\n# The Circuit Playground uses the Everlight ALS-PT19 as the sensor.\n# The datasheet shows a graph of lux vs voltage and the lux ranges from 0 to 10000\n# The output in Python ranges from 0 to 65535. \n# So I'll map the range 0 65535 to 0 to 10000 as a rough estimate\nlight = analogio.AnalogIn(board.LIGHT)\n\nmic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA,\n                       sample_rate=16000, bit_depth=16)\n\nbuttonA = digitalio.DigitalInOut(board.BUTTON_A)\nbuttonA.switch_to_input(pull=digitalio.Pull.DOWN)\nbuttonB = digitalio.DigitalInOut(board.BUTTON_B)\nbuttonB.switch_to_input(pull=digitalio.Pull.DOWN)\n\ni2c = busio.I2C(board.ACCELEROMETER_SCL, board.ACCELEROMETER_SDA)\nint1 = digitalio.DigitalInOut(board.ACCELEROMETER_INTERRUPT)\nlis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, address=0x19, int1=int1)\nlis3dh.range = adafruit_lis3dh.RANGE_8_G\n\nclass BTHomeService(Service):\n    # UUID given by Alterco Robotics\n    uuid = StandardUUID(0xFCD2)\n    # NOTE: There are no characteristics for this service.\n    # That is, no explicitely defined BLE characteristics will be used. Instead we just have whatever BTHome \n    # has defined internally in their list. But these are not official characteristics.\n\n    # bit 0: Encryption flag - 0 non-encrypted, 1 encrypted\n    # bit 1: reserved\n    # bit 2: Trigger based device flag - 0 regular interval, 1 non-regular interval\n    # bit 3-4: Reserved\n    # bit 5-7: BTHome Version  - version 2, so 010\n    # This gives 01000000b = 0x40\n    #device_information = Uint8Characteristic(min_value=0x40,max_value=0x40, initial_value=0x40)\n    #temperatureField = 0x45 # Data type for 0.1 precision temperature field in BTHome\n    #temperatureValue = 0x0000  # Dummy info for now\n    \n    \n    def __get__(self, obj, objtype=None):\n        super.__get__(obj, objtype)\n    \nclass BTHomeAd(advertising.Advertisement):\n    btService = BTHomeService()\n    s = advertising.standard.ServiceData(btService)\n    \nclass BTHomeTemperature:\n    global thermistor\n    objectID = 0x45     # temperature\n    \n    def __get__(self, obj, objtype=None):\n        b = bytearray(3)\n        b[0] = self.objectID\n        temperature = int(thermistor.temperature * 10)\n        b[1] =  temperature \u0026 0xFF\n        b[2] = (temperature \u0026 0xFF00) \u003e\u003e 8\n        return b\n\nclass BTHomeLight:\n    objectID = 0x05     # illuminance\n    \n    def __get__(self, obj, objtype=None):\n        b = bytearray(4)\n        b[0] = self.objectID\n        lux = int(simpleio.map_range(light.value, 0, 65535, 0, 10000) * 100)\n        b[3] = (lux \u0026 0xFF0000) \u003e\u003e 16\n        b[2] = (lux \u0026 0x00FF00) \u003e\u003e 8\n        b[1] = lux \u0026 0x00FF\n        return b\n\nclass BTHomeSound:\n    objectID = 0x2A     # sound - boolean\n    # Number of samples to read at once.\n    NUM_SAMPLES = 160\n    \n    \n    def __init__(self):\n        # Record an initial sample to calibrate. Assume it's quiet when we start.\n        self.samples = array.array('H', [0] * self.NUM_SAMPLES)\n        mic.record(self.samples, len(self.samples))\n    \n    def mean(self, values):\n        return sum(values) / len(values)\n    \n    # Remove DC bias before computing RMS.\n    def normalized_rms(self, values):\n        minbuf = int(self.mean(values))\n        samples_sum = sum(\n            float(sample - minbuf) * (sample - minbuf)\n            for sample in values\n        )\n        return math.sqrt(samples_sum / len(values))\n        \n    def get(self):\n        mic.record(self.samples, len(self.samples))\n        magnitude = self.normalized_rms(self.samples)\n        return(magnitude)\n        # You might want to print this to see the values.\n        # print(magnitude)\n\nclass BTHomeButton:\n    objectID = 0x3A     # button\n    \n    def __init__(self, button):\n        self.button = button\n        self.current_value = button.value\n    \n    def __get__(self, obj, objtype=None):\n        b = bytearray(2)\n        b[0] = self.objectID\n        if self.button.value != self.current_value:\n            if not self.current_value:\n                # Button pressed\n                self.current_value = True\n                b[1] = 0x01\n            else:\n                # Button released\n                self.current_value = False\n                b[1] = 0x00\n        else:\n            b[1] =0x00\n        return b\n    \nclass BTHomeAccelerometer:\n    objectID = 0x51\n    \n    def __get__(self, obj, objtype=None):\n         b = bytearray(3)\n         b[0] = self.objectID\n         x,y,z = lis3dh.acceleration\n         \n         '''\n         x = int(abs(x) * 1000)\n         b[1] = x \u0026 0x0FF\n         b[2] = (x \u0026 0x00FF00) \u003e\u003e 8\n\n         b[3] = self.objectID\n         y = int(y * 1000)\n         b[4] = y \u0026 0x0FF\n         b[5] = (y \u0026 0x00FF00) \u003e\u003e 8\n         '''\n         b[0] = self.objectID\n         z = int(z * 1000)\n         b[1] = z \u0026 0x0FF\n         b[2] = (z \u0026 0x00FF00) \u003e\u003e 8\n         \n         return b\n         \nclass BTHomeCircuitPlayground:\n    device_info = bytearray(1)\n    device_info[0] = 0x40\n    temp = BTHomeTemperature()\n    lux = BTHomeLight()\n    A = BTHomeButton(buttonA)\n    B = BTHomeButton(buttonB)\n    z_accel = BTHomeAccelerometer()\n    \n    def get(self):\n        # In objectID order    0x05     0x3A        0x45           0x51\n        return self.device_info + self.lux + self.A + self.temp + self.z_accel\n    \nbthomeCP = BTHomeCircuitPlayground()\nbtad = BTHomeAd()\nbtad.complete_name = \"CP BTHome\"\nbtad.flags.general_discovery = True\nbtad.flags.le_only = True\nbtad.s = bthomeCP.get()\nprint('[' + advertising.to_hex(btad.__bytes__()) + ']')\n\nble = BLERadio()\npixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.2, auto_write=False)\n\nwhile True:\n    btad.s = bthomeCP.get()\n    #print('[' + advertising.to_hex(btad.__bytes__()) + ']')\n    \n    ble.start_advertising(btad)\n    # Change this to a non-blocking time comparison\n    # That way we can check for button presses in the meantime.\n    time.sleep(INTERVAL)\n    ble.stop_advertising()","metadata":{"language":"auto","linenums":false}}]