The code for the key fob requires several imports. BLE Radio is part of the CircuitPython Bundle. It affords control of the nRF52840 BLE radio. The Adafruit LED Animation library is included in the bundle too. This helper library facilitates creating LED animations. AnalogIn is used to read the Feather’s ADC pins. Array is imported to create arrays with typed elements. I2SOut
exposes the I²S interface. RawSample
and WaveFile
play audio samples and recorded WAV files. DigitalInOut
controls the GPIO pins. The NeoPixel library exposes the Feather’s built-in NeoPixel.
from adafruit_ble import BLERadio from adafruit_led_animation.animation import Pulse, Solid import adafruit_led_animation.color as color from analogio import AnalogIn from array import array from audiobusio import I2SOut from audiocore import RawSample, WaveFile from board import BATTERY, D5, D6, D9, NEOPIXEL, RX, TX from digitalio import DigitalInOut, Direction, Pull from math import pi, sin from neopixel import NeoPixel from time import sleep
AnalogIn
is used to track the current battery voltage. The BLE Radio is instantiated. Hit status is a list of colors which represents the number of beacons hit by the BLE scan. Red indicates no beacons found, orange is 1 beacon, amber is 2 beacons and green is all 3 beacons.
battery = AnalogIn(BATTERY) ble = BLERadio() hit_status = [color.RED, color.ORANGE, color.AMBER, color.GREEN]
The Feather’s built-in NeoPixel is instantiated. A pulse LED animation is defined. It will pulse a purplish pink for the female icon and cyan for the male icon. A solid LED animation is defined and the initial state is green. This really isn’t an animation because it just sets the NeoPixel to the specified color.
pixel = NeoPixel(NEOPIXEL, 1) pulse = Pulse(pixel, speed=0.01, color=color.PURPLE, # Use CYAN for Male Key period=3, min_intensity=0.0, max_intensity=0.5) solid = Solid(pixel, color.GREEN)
The magnetic reed switch is defined and set to GPIO 5. When the switch is exposed to a magnetic field, the pin will be pulled low. The amp enable pin is defined and set to GPIO 6. It is an output and the initial state is set low to disable the amp.
reed_switch = DigitalInOut(D5) reed_switch.direction = Direction.INPUT reed_switch.pull = Pull.UP amp_enable = DigitalInOut(D6) amp_enable.direction = Direction.OUTPUT amp_enable.value = False
A method called PlayTone
is defined. It generates a loud warning tone. The length of the array is the sample rate divided by the frequency in Hertz. A sample is created using the RawSample
method and passed the sine wave array. An I²S interface is implemented using the I2SOut
method. The play
method plays the sample. Loop equals true causes the sample to repeat indefinitely. A 1 second sleep, plays the sample for 1 second. Then stop cancels playback. Deinit
is used to dispose of the sample and the interface.
def play_tone(): """Generate tone and transmit to I2S amp.""" length = 4000 // 440 sine_wave = array("H", [0] * length) for i in range(length): sine_wave[i] = int(sin(pi * 2 * i / 18) * (2 ** 15) + 2 ** 15) sample = RawSample(sine_wave, sample_rate=8000) i2s = I2SOut(TX, RX, D9) i2s.play(sample, loop=True) sleep(1) i2s.stop() sample.deinit() i2s.deinit()
A method called PlayMessage
is defined. It will play a recorded WAV audio file. A WAV file called D1 is loaded. The WaveFile
method instantiates the loaded WAV file. An I²S interface is implemented. The play
method plays the WAV file. A while
loop suspends the program until the playback completes. Again deinit
cleans up.
def play_message(): """Play recorded WAV message and transmit to I2S amp.""" with open("d1.wav", "rb") as file: wave = WaveFile(file) i2s = I2SOut(TX, RX, D9) i2s.play(wave) while i2s.playing: pass wave.deinit() i2s.deinit()
Boundary_violations tracks the amount of time that a fob resides outside the range of the BLE beacons. The main program loop is an infinite while
. A high reed switch indicates the fob is not docked in the charging cradle. Hits tracks how many BLE beacons are within range.
boundary_violations = 0 while True: if reed_switch.value: # Not docked hits = 0
The BLE commands are wrapped in a try
statement to catch errors. Advertisements stores the results of a BLE scan. Timeout limits the scan to 3 seconds. BLE devices transmit advertisements so they can be identified by other BLE devices. A for
loop processes all detected Bluetooth BLE advertisements. Addr
holds the advertisement address details. The BLE beacons will show up as a scan response with a type of random static. All other advertisements can be ignored. Each BLE beacon broadcasts a unique name. If the name matches the name of the first beacon then the hits variable is or’d by 1. Bitwise operations are used instead of incrementing a counter because the BLE scan often returns duplicate advertisements which would result in an incorrect beacon count. If the 2nd or 3rd beacon is detected then the corresponding digit of the hits variable is or’d. Any errors are printed to the console.
# Wrap BLE commands in try to catch errors try: advertisements = ble.start_scan(timeout=3) for advertisement in advertisements: addr = advertisement.address if (advertisement.scan_response and addr.type == addr.RANDOM_STATIC): if advertisement.complete_name == '<Your 1st beacon name here>': hits |= 0b001 elif advertisement.complete_name == '<Your 2nd beacon name here>': hits |= 0b010 elif advertisement.complete_name == '<Your 3rd beacon name here>': hits |= 0b100 except Exception as e: print(repr(e))
The actual beacon hit count is determined by using list comprehension to add up the flipped bits of the hits byte. The corresponding color is then set using the hit status list indexed color. Solid animate displays the color on the NeoPixel. The LED color warns the user as the FOB strays from proximity to the office.
# Retrieve actual beacon hit count and update NeoPixel hit_count = len([ones for ones in bin(hits) if ones == '1']) solid.color = hit_status[hit_count] solid.animate() sleep(1)
If the hit count is zero then the fob has been taken out of range of all the beacons. The remainder of boundary violations divided by 60 will issue the audible alarm every 60 cycles. Each BLE scan takes more than a second. So, the alarm fires every few minutes. The I²S amp is enabled. The programs sleeps for a second. The warning tone is played. Another 1 second sleep then the WAV file message is played. After 1 second pause and the amp is disabled. Boundary violations is incremented. If at least 1 beacon is hit then the boundary violations is reset to zero.
# Determine key fob proximity if hit_count == 0: # Key fob out of range of beacons if boundary_violations % 60 == 0: # Play message every 60 cycles amp_enable.value = True sleep(1) play_tone() sleep(1) play_message() sleep(1) amp_enable.value = False boundary_violations += 1 else: # Key fob in range of at least 1 beacon boundary_violations = 0
If the fob is docked in the cradle then none of the BLE code above needs to run. Instead the boundary violations is reset. The battery voltage is retrieved using the battery.value
to poll the board’s ADC pin. The ADC value is multiplied by the reference voltage of 3.3 volts and divided by the ADC resolution. This converts the ADC value to the actual voltage which still needs to multiplied by 2 because there is a voltage divider on the battery ADC pin. The fully charged voltage of the LiPo battery can exceed 4 volts which is greater than the ADC’s maximum rating. A voltage divider scales the voltage to an acceptable range. If the voltage is less than the nominal LiPo battery voltage of 3.7 volts than the NeoPixel pulse period is reduced to 1 second to speed up the flashing.
# Key fob docked in cradle else: boundary_violations = 0 voltage = battery.value * 3.3 / 65535 * 2 if voltage < 3.7: pulse.period = 1 # Speed up LED pulse for low battery else: pulse.period = 3 pulse.animate()
Text editor powered by tinymce.