Showing posts with label adafruit_ble. Show all posts
Showing posts with label adafruit_ble. Show all posts

Sunday, September 24, 2023

Project: Govee E-Ink display

 The Govee H5074 E-Ink display project!

All complete! This project uses an Adafruit nrf52840 board running CircuitPython and their 2.13 inch e-ink display (the tri-color one) to pull in data via Bluetooth LE from a Govee H5074 temperate and humidity sensor.

Watch it on YouTube. Also, the code is up on Github; take a look!

Some of the challenging / fun parts: on reset, the device will look around for a Bluetooth "Current Time Service" so that the clock is set automatically (no need to every manually set it!). Reading in Bluetooth advertisements for the Govee H5074 was not as obvious as it should have been (and I've got a blog post about it), and the e-ink display was much more challenging than I though it would be.



Reading Bluetooth LE (BLE) sensor data advertisements in CircuitPython

 Bluetooth Sensors -- reading the Govee H5074 advertisement data




Reading Bluetooth LE (BLE) sensor data from CircuitPython isn't always obvious. In this example, I show how I pull data from the Govee H5074 Bluetooth sensor. The sensor is a small, battery-powered sensor that reads the temperate and humidity and transmits it inside of Bluetooth advertisements.

The code is all up on Github. You'll want to look in the code directory at the GoveeScanner.py file. There's also a Govee5074.py file which can parse an advertisement once it's been seen, and a GoveeDisplay.py file that can display the data to an E-Ink display.


The key to the code is a pair of methods: the Scan() method and the ScanOnce() method. ScanOnce will start a scan ble.start_scan(timeout=...) . In practice, I use very short timeouts (.1 second) but then do a bunch of them in a loop in the Scan method. The start_scan returns a  generator, which is like a list but the contents will flow in over time. The contents will be both advertisements and advertisement scan responses. A scan response is like additional data in the advertisement, but it only comes in when it's requested. The request will be automatic; we don't have to do anything to request a scan_response.

The Govee sensor data is send in the scan_response, in what's called the "Manufacturer Data" section.

The critical thing to know is that the two values -- the advertisements and scan_responses -- aren't linked together by CircuitPython. We have to do that ourselves, and we do it by saving the advertisements in a dictionary that's indexed by the Bluetooth address.
  

The advertisements will have the name of the device. We're looking just for Govee H5074. In the code at about line 88 we look for the complete_name :

            name = advert.complete_name
            if (name is not None) and ("Govee_H5074" in name):
                # print("TRACE: 30: Found main govee advert", advert.address, name)
                goveeAdverts[advert.address] = advert


If the device is one we want, then we save away the advert in the goveeeAdverts dictionary, indexed by the Bluetooth address. This is critical because when we see an advertisement scan_response, we actually don't get the name but we do get the address.

The other part of the ScanOnce method is reading the scan responses. We filter first based on whether it's a response we want (we'll get lots of responses from lots of devices, but we only care about the Govee H5074 devices here).

            if advert.scan_response:
                # Check to make sure that this response address is in the list
                # of adverts that have the right name ("Govee_H5074...")
                # You can't tell from just the response advert; you need the
                # original advert, too.
                if advert.address not in goveeAdverts:
                    # very frequent -- set is only govee adverts, but the
                    # scan_response can be for anything
                    # print("TRACE: 15: not in set", advert.address)
                    continue

Once we know it's a scan_response we want, we pull out the manufacturer data and parse it. If it parses OK, then we prepare to return it.

                MANUFACTURER_SECTION = 0xFF  # Per Bluetooth SIG
                if (MANUFACTURER_SECTION in advert.data_dict):
                    annunciator.Found()
                    buffer = advert.data_dict[MANUFACTURER_SECTION]

                    g5074 = Govee5074(buffer)
                    if (g5074.IsOk):
                        annunciator.Read()
                        if retval is None:  # only print the first one
                            print("TRACE: 28: GOT DATA", g5074)
                        retval = g5074


The annunciator is the code that lets the user know what's going on. "Read" is an indication that we've read the data OK.


TL/DR: when reading Bluetooth advertisements, be sure to check the scan_responses too, because that's where the data might be.

Friday, June 30, 2023

Clocks that set themselves!

The Adafruit Clue-Clock


I'm always frustrated when I have to reset a bunch of clocks after a power outage. Did you know that setting the time on an IOT device can be easy when you add support for the Bluetooth Current Time Service? (On the SIG site you will want the first doc, "Current Time Service 1.1"). In this blog post I show some of the code I wrote for the Adafruit Clue using CircuitPython and the adafruit_ble library. I've even got a Youtube video! to show the device and step through the code. 

There's also a handy Windows app that is the server side of the time setting; it will broadcast out the current time. Download "Simple Bluetooth Time Service" on the Windows store; that's how I set the time. The complete source code for it is on GithubUpdate: see also my [Govee Ink Display](ElectronicsProjects/2023-Adafruit-Python-InkGoveeListener at main · pedasmith/ElectronicsProjects (github.com) project; it has a newer and easier-to-use version of the clock code.

In the video I step through three interesting features of the clock.

Feature 1: Display stuff to the screen

We'll want to print text to the screen; this is done with the clue.simple_text_display() object. The clock uses the simple_text_display, so we can display several lines of text, but nothing fancier. 


A key point (that took me far to long to figure out!) is that after you update one or more a lines of text, you must call the .show() method -- otherwise, nothing gets displayed!


Sample Code

from adafruit_clue import clue

colors = ((0xff, 0xff, 0xff),)
display = clue.simple_text_display(title="Clock", 
                                   title_scale=2, text_scale=4,
                                   title_color=(0xa0, 0xa0, 0xa0),
                                   colors=colors
                                   )
str = "{:02d}:{:02d}:{:02d}".format(currHour, currMinute, currSecond)
clue_display[0].text = str
clue_display.show()


The simple_text_display is documented on the circuitpython site.

The text_scale value of 4 fits a time display with a format of HH:MM:SS (8 characters long) with room for two more characters (eg, enough room for an AM/PM indicator, if desired)

The title_scale is relative to the text_scale.

The colors set the colors of each line. I set it so tht the time and date are white, and the day (and the bluetooth scan results) are blue.

Feature 2: Use the Real Time Clock

The chip used to track time accurately is the "real time clock" -- without it, the code would slowly drift. Fun fact: the first IBM PC did not include a battery-backed real-time clock chip. Every time you turned on the computer, you had to enter the date and time.

The real-time clock uses struct_time for many operations; it's just a tuple where you can grab values by index. the current hour, for example, is index 3.

The real-time clock needs power to work; if it loses power, it will stop stracking an accurate date and time (it says "real time clock", but it does dates, too). How we set it is the topic of the next section.

The CircuitPython rtc module is a delight to use: there's just a simple way to set the initial value and a simple way to pull out the current time.

Feature 3: Connect to Bluetooth Current Time Service

Now we get the hard stuff: reading data from an external Bluetooth "Current Time Service" source. The idea is that a nearby PC will broadcast out a "current time" (there's a standard for this); the clock will pick it up and use it to set its time.

To make it work, you should have already added the adafruit_ble to your lib directory. It's not on the list of libraries in the Adafruit Clue documentation.

The bulk of the Bluetooth code is in BtCurrentTimeServiceClient.py. There's two critical classes in that file: the BtCurrentTimeServiceClient class which matches the Bluetooth Special Interest Group (SIG) standard and which is compatible with the Adafruit CircuitPython Bluetooth setup, plus a helpful wrapper class BtCurrentTimeServiceClientRunner class which listens for Bluetooth advertisements and connects to the time service.

You will want to look at the code while reading this description :-)

 BtCurrentTimeServiceClient

The BtCurrentTimeServiceClient class is less than 20 lines of code. The Adafruit CircuitPython Bluetooth system isn't too hard to use, but there isn't a very good tutorial on it. Hopefully this explanation will help!

The BtCurrentTimeServiceClient class exists for only one reason: it's the "glue" between the Bluetooth system and your code. When you get an advertisement for a Bluetooth device you want to connect to, you'll provide this class (the class and not an object) and will get back an object that's mostly this class (it will have been updated)

The object you get back will only be valid until the connection is broken. In the code, the connection is broken almost as soon as the data is read.


class BtCurrentTimeServiceClient(Service):
    uuid = StandardUUID(0x1805)
    data = StructCharacteristic(
        uuid=StandardUUID(0x2A2B),
        # Don't need to provide these; they should be discovered
        # by the Bluetooth system.
        # properties=Characteristic.READ | Characteristic.NOTIFY,
        struct_format="<HBBBBBBBB"
    )
    def GetTimeString(self):
        (y, m, d, hh, mm, ss, j1, j2, j3) = self.data
        retval = "{0}-{1}-{2} {3}:{4}:{5}".format(y, m, d, hh, mm, ss)
        return retval


The data value is set to be a StructCharacteristic. But when you examine the data later on (like after it's been updated by the remote side!), it will instead be a tuple of the data, parsed by the struct_format string. You just have to know from other sources what the data values actually mean.


BtCurrentTimeServiceClientRunner

The BtCurrentTimeServiceClientRunner is the class you'll actually call to get the Bluetooth current time data. Just call Scan, passing in a bluetooth "ble object; it's the Bluetooth from the clue device (```ble = adafruit_ble.BLERadio()```). There aren't any other methods in the class that should be called.

The runner Scan method will scan for Bluetooth advertisements for a set amount of time (15 seconds in this case); the scans can complete in less time, so I loop around as needed. The inner loop of Scan calls ScanOnce to do a single advertisement scan, returning a connected Bluetooth device. Once this method has a connected Bluetooth device, we hook up the service connection with the ConnectToCurrentTimeService method (yeah, I'm using the word "connected" here in kind of two different ways). Once we have a service connection, we can pull out the time data directly.

The ScanOnce returns a connected connection to the remote device (or None, of course). It does a single advertisement scan, up to a maximum amount of time, looking for an advertisement that says it supports the current time service. When one of those is found, we connect to that device. In my case, the device will just be my laptop when it's running the Simple Bluetooth Current Time Service app. 

The ConnectToCurrentTimeService creates a 'live' (connected) service object given a connection to a device. 

To convert a connection to a bluetooth device into a useable per-service object, you need to provide a class with a uuid that matches the service you need to use, plus a data object which needs to be one of the Characteristic types (for example, StructCharacteristic). When you "get" an object from the connection, the smart connection "array" will create a brand-new object for you, of the class you specify, that's hooked to (connected to) the live Bluetooth object. As part of this, the "data" value in the class, which had been, e.g., a StructCharacteristic, will now just be a tuple of data. Reading that tuple will get you the latest data.

To recap: the Scan method will scan advertisements for an appropriate BT device, will connect to it, will make a service connection, read the characteristic data, put that data into a tuple, and return the tuple. In case of errors, it will just return None.

Once the tuple of date is read, we just set up the real-time clock at about line 74 of the code.py file. Once this happens, the clock will be updated!

You can make this work for your device, too -- just pop in the BtCurrentTimeServiceClient.py, and call the Scan() method with a BT radio. Just don't forget to include the adafruit_ble library on your device!


Good luck!