Skip to content

Commit df3477e

Browse files
authored
Merge pull request #28 from tannewt/cp_native_bridge
Add native bridge example
2 parents f096580 + 9283c52 commit df3477e

File tree

1 file changed

+192
-0
lines changed

1 file changed

+192
-0
lines changed

examples/ble_broadcastnet_bridge.py

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
"""This example bridges from BLE to Adafruit IO on a CircuitPython device that
5+
supports both WiFi and BLE. (The first chip is the ESP32-S3.)"""
6+
from secrets import secrets # pylint: disable=no-name-in-module
7+
import ssl
8+
import time
9+
import adafruit_requests as requests
10+
from adafruit_ble.advertising.standard import ManufacturerDataField
11+
import adafruit_ble
12+
import board
13+
import socketpool
14+
import wifi
15+
import adafruit_ble_broadcastnet
16+
17+
# To get a status neopixel flashing, install the neopixel library as well.
18+
19+
if hasattr(board, "NEOPIXEL"):
20+
try:
21+
import neopixel
22+
import rainbowio
23+
except ImportError:
24+
print("No status pixel due to missing library")
25+
neopixel = None
26+
27+
aio_auth_header = {"X-AIO-KEY": secrets["aio_key"]}
28+
aio_base_url = "https://io.adafruit.com/api/v2/" + secrets["aio_username"]
29+
30+
print("Connecting to %s" % secrets["ssid"])
31+
wifi.radio.connect(secrets["ssid"], secrets["password"])
32+
print("Connected to %s!" % secrets["ssid"])
33+
print("My IP address is", wifi.radio.ipv4_address)
34+
35+
socket = socketpool.SocketPool(wifi.radio)
36+
https = requests.Session(socket, ssl.create_default_context())
37+
38+
status_pixel = None
39+
if neopixel and hasattr(board, "NEOPIXEL"):
40+
status_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.02)
41+
42+
43+
def aio_post(path, **kwargs):
44+
kwargs["headers"] = aio_auth_header
45+
return https.post(aio_base_url + path, **kwargs)
46+
47+
48+
def aio_get(path, **kwargs):
49+
kwargs["headers"] = aio_auth_header
50+
return https.get(aio_base_url + path, **kwargs)
51+
52+
53+
# Disable outer names check because we frequently collide.
54+
# pylint: disable=redefined-outer-name
55+
56+
57+
def create_group(name):
58+
response = aio_post("/groups", json={"name": name})
59+
if response.status_code != 201:
60+
print(name)
61+
print(response.content)
62+
print(response.status_code)
63+
raise RuntimeError("unable to create new group")
64+
return response.json()["key"]
65+
66+
67+
def create_feed(group_key, name):
68+
response = aio_post(
69+
"/groups/{}/feeds".format(group_key), json={"feed": {"name": name}}
70+
)
71+
if response.status_code != 201:
72+
print(name)
73+
print(response.content)
74+
print(response.status_code)
75+
raise RuntimeError("unable to create new feed")
76+
return response.json()["key"]
77+
78+
79+
def create_data(group_key, data):
80+
response = aio_post("/groups/{}/data".format(group_key), json={"feeds": data})
81+
if response.status_code == 429:
82+
print("Throttled!")
83+
return False
84+
if response.status_code != 200:
85+
print(response.status_code, response.json())
86+
raise RuntimeError("unable to create new data")
87+
response.close()
88+
return True
89+
90+
91+
def convert_to_feed_data(values, attribute_name, attribute_instance):
92+
feed_data = []
93+
# Wrap single value entries for enumeration.
94+
if not isinstance(values, tuple) or (
95+
attribute_instance.element_count > 1 and not isinstance(values[0], tuple)
96+
):
97+
values = (values,)
98+
for i, value in enumerate(values):
99+
key = attribute_name.replace("_", "-") + "-" + str(i)
100+
if isinstance(value, tuple):
101+
for j in range(attribute_instance.element_count):
102+
feed_data.append(
103+
{
104+
"key": key + "-" + attribute_instance.field_names[j],
105+
"value": value[j],
106+
}
107+
)
108+
else:
109+
feed_data.append({"key": key, "value": value})
110+
return feed_data
111+
112+
113+
ble = adafruit_ble.BLERadio()
114+
bridge_address = adafruit_ble_broadcastnet.device_address
115+
print("This is BroadcastNet bridge:", bridge_address)
116+
print()
117+
118+
print("Fetching existing feeds for this bridge.")
119+
120+
existing_feeds = {}
121+
response = aio_get("/groups")
122+
for group in response.json():
123+
if "-" not in group["key"]:
124+
continue
125+
pieces = group["key"].split("-")
126+
if len(pieces) != 4 or pieces[0] != "bridge" or pieces[2] != "sensor":
127+
continue
128+
_, bridge, _, sensor_address = pieces
129+
if bridge != bridge_address:
130+
continue
131+
existing_feeds[sensor_address] = []
132+
for feed in group["feeds"]:
133+
feed_key = feed["key"].split(".")[-1]
134+
existing_feeds[sensor_address].append(feed_key)
135+
136+
print(existing_feeds)
137+
138+
print("scanning")
139+
print()
140+
sequence_numbers = {}
141+
# By providing Advertisement as well we include everything, not just specific advertisements.
142+
for measurement in ble.start_scan(
143+
adafruit_ble_broadcastnet.AdafruitSensorMeasurement, interval=0.5
144+
):
145+
reversed_address = [measurement.address.address_bytes[i] for i in range(5, -1, -1)]
146+
sensor_address = "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}".format(*reversed_address)
147+
if sensor_address not in sequence_numbers:
148+
sequence_numbers[sensor_address] = measurement.sequence_number - 1 % 256
149+
# Skip if we are getting the same broadcast more than once.
150+
if measurement.sequence_number == sequence_numbers[sensor_address]:
151+
continue
152+
number_missed = measurement.sequence_number - sequence_numbers[sensor_address] - 1
153+
if number_missed < 0:
154+
number_missed += 256
155+
# Derive the status color from the sensor address.
156+
if status_pixel:
157+
status_pixel[0] = rainbowio.colorwheel(sum(reversed_address))
158+
group_key = "bridge-{}-sensor-{}".format(bridge_address, sensor_address)
159+
if sensor_address not in existing_feeds:
160+
create_group("Bridge {} Sensor {}".format(bridge_address, sensor_address))
161+
create_feed(group_key, "Missed Message Count")
162+
existing_feeds[sensor_address] = ["missed-message-count"]
163+
164+
data = [{"key": "missed-message-count", "value": number_missed}]
165+
for attribute in dir(measurement.__class__):
166+
attribute_instance = getattr(measurement.__class__, attribute)
167+
if issubclass(attribute_instance.__class__, ManufacturerDataField):
168+
if attribute != "sequence_number":
169+
values = getattr(measurement, attribute)
170+
if values is not None:
171+
data.extend(
172+
convert_to_feed_data(values, attribute, attribute_instance)
173+
)
174+
175+
for feed_data in data:
176+
if feed_data["key"] not in existing_feeds[sensor_address]:
177+
create_feed(group_key, feed_data["key"])
178+
existing_feeds[sensor_address].append(feed_data["key"])
179+
180+
start_time = time.monotonic()
181+
print(group_key, data)
182+
# Only update the previous sequence if we logged successfully.
183+
if create_data(group_key, data):
184+
sequence_numbers[sensor_address] = measurement.sequence_number
185+
186+
duration = time.monotonic() - start_time
187+
if status_pixel:
188+
status_pixel[0] = 0x000000
189+
print("Done logging measurement to IO. Took {} seconds".format(duration))
190+
print()
191+
192+
print("scan done")

0 commit comments

Comments
 (0)