""" Based on the Bluefruit TFT Gizmo ANCS Notifier for iOS Learn guide: https://learn.adafruit.com/ancs-gizmo?view=all """ import time import math import array import board import digitalio import analogio import audiobusio import displayio import adafruit_ble import adafruit_imageload import adafruit_lis3mdl import adafruit_sht31d from adafruit_ble.advertising.standard import SolicitServicesAdvertisement from adafruit_ble_apple_notification_center import AppleNotificationCenterService from babel.babel import Babel # used for Unifont support but you can BYO font from adafruit_display_text import label # apps we want to listen for WHITELIST = ["com.apple.reminders", "com.grailr.CARROTweather", "com.google.Maps", "com.tinyspeck.chatlyio"] # buttons (not currently used) DELAY_AFTER_PRESS = 15 DEBOUNCE = 0.1 a = digitalio.DigitalInOut(board.BUTTON_A) a.switch_to_input(pull=digitalio.Pull.DOWN) b = digitalio.DigitalInOut(board.BUTTON_B) b.switch_to_input(pull=digitalio.Pull.DOWN) # sensors i2c = board.I2C() sht31 = adafruit_sht31d.SHT31D(i2c) lis3mdl = adafruit_lis3mdl.LIS3MDL(i2c) mic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA, sample_rate=16000, bit_depth=16) samples = array.array('H', [0] * 160) # fonts babel = Babel() def normalized_rms(values): mean_values = int(sum(values) / len(values)) return math.sqrt(sum(float(sample - mean_values) * (sample - mean_values) for sample in values) / len(values)) def find_connection(): for connection in radio.connections: if AppleNotificationCenterService not in connection: continue if not connection.paired: connection.pair() return connection, connection[AppleNotificationCenterService] return None, None # Start advertising before messing with the display so that we can connect immediately. radio = adafruit_ble.BLERadio() advertisement = SolicitServicesAdvertisement() advertisement.solicited_services.append(AppleNotificationCenterService) def wrap_in_tilegrid(open_file): odb = displayio.OnDiskBitmap(open_file) return displayio.TileGrid(odb, pixel_shader=displayio.ColorConverter()) display = board.DISPLAY group = displayio.Group(max_size=10) group.append(wrap_in_tilegrid(open("/background.bmp", "rb"))) compass_sheet, palette = adafruit_imageload.load("/compass.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette) compass = displayio.TileGrid(compass_sheet, pixel_shader=palette, width = 1, height = 1, tile_width = 31, tile_height = 31) compass.x = 209 group.append(compass) arrow_sheet, arrow_palette = adafruit_imageload.load("/arrows.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette) arrow = displayio.TileGrid(arrow_sheet, pixel_shader=arrow_palette, width = 1, height = 1, tile_width = 64, tile_height = 64) arrow.y = 176 group.append(arrow) heading_label = label.Label(babel.font, max_glyphs=30, color=0xFFFFFF) heading_label.y = 8 group.append(heading_label) temp_label = label.Label(babel.font, max_glyphs=10, color=0xFFFFFF) temp_label.x = 0 temp_label.y = 24 group.append(temp_label) humidity_label = label.Label(babel.font, max_glyphs=10, color=0xFFFFFF) humidity_label.x = 72 humidity_label.y = 24 group.append(humidity_label) noise_label = label.Label(babel.font, max_glyphs=30, color=0xFFFFFF) noise_label.x = 144 noise_label.y = 24 group.append(noise_label) main_label = label.Label(babel.font, max_glyphs=7 * 30, color=0xFFFFFF) main_label.y = 102 group.append(main_label) directions_label = label.Label(babel.font, max_glyphs=4 * 22, color=0x000000) directions_label.x = 68 directions_label.y = 208 group.append(directions_label) display.show(group) current_notifications = {} all_ids = [] last_update = None active_connection, notification_service = find_connection() # this method hilariously doesn't return anything remotely resembling a standard timestamp, # i kind of gave up halfway through, but it mostly seems to order things in order. def iso_to_timestamp(isodate): years = int(isodate[0:4]) months = int(isodate[4:6]) days = int(isodate[6:8]) hours = int(isodate[9:11]) minutes = int(isodate[11:13]) seconds = int(isodate[13:15]) centuries = years // 100 leaps = centuries // 4 leapDays = 2 - centuries + leaps yearDays = int(365.25 * (years + 4716)) monthDays = int(30.6001 * (months + 1)) julian_date = int(leapDays + days + monthDays + yearDays -1524.5) julian_date -= 2458800 return julian_date + (seconds + minutes * 60 + hours * 3600) / 26400 def wrap(string, length): words = string.split(' ') wrapped = "" line_length = 0 for word in words: if line_length + len(word) <= length: wrapped += word + ' ' line_length += len(word) + 1 else: wrapped += '\n' + word + ' ' line_length = len(word) + 1 return wrapped while True: if not active_connection: radio.start_advertising(advertisement) while not active_connection: active_connection, notification_service = find_connection() # Connected while active_connection.connected: start_time = None all_ids.clear() current_notifications = notification_service.active_notifications for notif_id in current_notifications: notification = current_notifications[notif_id] t = iso_to_timestamp(notification._raw_date) if start_time is None or t > start_time: start_time = t if notification.app_id in WHITELIST: all_ids.append(notif_id) else: del current_notifications[notif_id] # i have way too many notifications to keep in memory all_ids.sort(key=lambda x: current_notifications[x]._raw_date) if len(all_ids) == 0: continue latest_update = current_notifications[all_ids[len(all_ids) - 1]] if latest_update != last_update: # print('LATEST UPDATE:', latest_update._raw_date) reminders = list() inthehouse = set() weather = None directions = None text = "" for notif_id in reversed(all_ids): current_notification = current_notifications[notif_id] t = iso_to_timestamp(current_notification._raw_date) # print(current_notification._raw_date, current_notification.id, start_time - t, current_notification.app_id, current_notification) if current_notification.app_id == "com.apple.reminders": reminders.append('- ' + current_notification.title) elif current_notification.app_id == "com.grailr.CARROTweather" and weather is None: weather = current_notification.message.replace('↑', 'H').replace('↓', 'L').split('.')[0] + '.' elif current_notification.app_id == "com.google.Maps" and directions is None: directions = current_notification.message elif current_notification.app_id == "com.tinyspeck.chatlyio" and (start_time - t) < 0.5 and current_notification.message.endswith("in the house!"): # my workshop's slack says something like "Joey is in the house!"; adapt to whatever you want from Slack. elements = current_notification.message.split(' ') inthehouse.add(elements[3]) inthehouse_text = ', '.join(inthehouse) if inthehouse else 'No one' weather_wrapped = wrap(weather, 30) if weather else "No weather forecast." main_label.text = weather_wrapped + '\nReminders:\n' + ('\n'.join(reminders) if reminders else ' None') + '\nAt the Workshop:\n ' + inthehouse_text if directions is not None: directions_label.text = wrap(directions, 21) directions = directions.lower() slight = "slight" in directions if "left" in directions: arrow[0] = 5 if slight else 1 elif "right" in directions: arrow[0] = 6 if slight else 2 elif "straight" in directions: arrow[0] = 3 elif "u-turn" in directions: arrow[0] = 7 elif "arrive" in directions: arrow[0] = 4 else: arrow[0] = 0 else: arrow[0] = 0 last_update = latest_update temp = sht31.temperature * 9 / 5 + 32 if temp >= 100: temp_label.color = 0xFF0000 elif temp >= 90: temp_label.color = 0xFF9300 elif temp >= 80: temp_label.color = 0xFFD479 elif temp >= 70: temp_label.color = 0xD4FB79 elif temp >= 60: temp_label.color = 0x73FCD6 elif temp >= 50: temp_label.color = 0x73FDFF elif temp >= 40: temp_label.color = 0x76D6FF elif temp >= 30: temp_label.color = 0x0096FF elif temp >= 20: temp_label.color = 0x0433FF else: temp_label.color = 0x0000FF temp_label.text = str(int(temp)) + '° F' humidity = sht31.relative_humidity if humidity <= 20: humidity_label.color = 0xFFFC79 elif humidity <= 40: humidity_label.color = 0xD4FB79 elif humidity <= 60: humidity_label.color = 0x73FA79 elif humidity <= 80: humidity_label.color = 0x73FCD6 else: humidity_label.color = 0x73FDFF humidity_label.text = str(int(humidity)) + '% RH' mic.record(samples, len(samples)) rms = normalized_rms(samples) db = 24 + 20 * math.log(rms, 10) if db < 80: noise_label.color = 0x00F900 elif db < 100: noise_label.color = 0xFFCC00 else: noise_label.color = 0xCC0000 noise_label.text = str(int(db)) + ' dB' mag_x, mag_y, mag_z = lis3mdl.magnetic heading = 180 * (math.atan2(mag_y, mag_x) / math.pi) # print('X:{0:10.2f}, Y:{1:10.2f}, Z:{2:10.2f} uT'.format(mag_x, mag_y, mag_z)) # print(heading) heading_label.text = "Heading:" + str(int(heading)) compass[0] = int((heading + 22.5 ) / 45) % 8 # Bluetooth Disconnected active_connection = None notification_service = None