Skip to content

Commit a482f60

Browse files
authored
Merge pull request #2 from ladyada/master
second draft, with echo example
2 parents 50d78b0 + d481a64 commit a482f60

File tree

4 files changed

+328
-109
lines changed

4 files changed

+328
-109
lines changed

adafruit_bluefruitspi.py

Lines changed: 120 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -42,92 +42,63 @@
4242
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
4343
"""
4444

45-
# imports
46-
4745
__version__ = "0.0.0-auto.0"
4846
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BluefruitSPI.git"
4947

5048
import time
49+
import struct
5150
from digitalio import Direction, Pull
5251
from adafruit_bus_device.spi_device import SPIDevice
5352
from micropython import const
54-
import struct
55-
5653

57-
class MsgType: #pylint: disable=too-few-public-methods,bad-whitespace
58-
"""An enum-like class representing the possible message types.
59-
Possible values are:
60-
- ``MsgType.COMMAND``
61-
- ``MsgType.RESPONSE``
62-
- ``MsgType.ALERT``
63-
- ``MsgType.ERROR``
64-
"""
65-
COMMAND = const(0x10) # Command message
66-
RESPONSE = const(0x20) # Response message
67-
ALERT = const(0x40) # Alert message
68-
ERROR = const(0x80) # Error message
69-
70-
71-
class SDEPCommand: #pylint: disable=too-few-public-methods,bad-whitespace
72-
"""An enum-like class representing the possible SDEP commands.
73-
Possible values are:
74-
- ``SDEPCommand.INITIALIZE``
75-
- ``SDEPCommand.ATCOMMAND``
76-
- ``SDEPCommand.BLEUART_TX``
77-
- ``SDEPCommand.BLEUART_RX``
78-
"""
79-
INITIALIZE = const(0xBEEF) # Resets the Bluefruit device
80-
ATCOMMAND = const(0x0A00) # AT command wrapper
81-
BLEUART_TX = const(0x0A01) # BLE UART transmit data
82-
BLEUART_RX = const(0x0A02) # BLE UART read data
83-
84-
85-
class ArgType: #pylint: disable=too-few-public-methods,bad-whitespace
86-
"""An enum-like class representing the possible argument types.
87-
Possible values are
88-
- ``ArgType.STRING``
89-
- ``ArgType.BYTEARRAY``
90-
- ``ArgType.INT32``
91-
- ``ArgType.UINT32``
92-
- ``ArgType.INT16``
93-
- ``ArgType.UINT16``
94-
- ``ArgType.INT8``
95-
- ``ArgType.UINT8``
96-
"""
97-
STRING = const(0x0100) # String data type
98-
BYTEARRAY = const(0x0200) # Byte array data type
99-
INT32 = const(0x0300) # Signed 32-bit integer data type
100-
UINT32 = const(0x0400) # Unsigned 32-bit integer data type
101-
INT16 = const(0x0500) # Signed 16-bit integer data type
102-
UINT16 = const(0x0600) # Unsigned 16-bit integer data type
103-
INT8 = const(0x0700) # Signed 8-bit integer data type
104-
UINT8 = const(0x0800) # Unsigned 8-bit integer data type
105-
106-
107-
class ErrorCode: #pylint: disable=too-few-public-methods,bad-whitespace
108-
"""An enum-like class representing possible error codes.
109-
Possible values are
110-
- ``ErrorCode.``
111-
"""
112-
INVALIDMSGTYPE = const(0x8021) # SDEP: Unexpected SDEP MsgType
113-
INVALIDCMDID = const(0x8022) # SDEP: Unknown command ID
114-
INVALIDPAYLOAD = const(0x8023) # SDEP: Payload problem
115-
INVALIDLEN = const(0x8024) # SDEP: Indicated len too large
116-
INVALIDINPUT = const(0x8060) # AT: Invalid data
117-
UNKNOWNCMD = const(0x8061) # AT: Unknown command name
118-
INVALIDPARAM = const(0x8062) # AT: Invalid param value
119-
UNSUPPORTED = const(0x8063) # AT: Unsupported command
54+
# pylint: disable=bad-whitespace
55+
_MSG_COMMAND = const(0x10) # Command message
56+
_MSG_RESPONSE = const(0x20) # Response message
57+
_MSG_ALERT = const(0x40) # Alert message
58+
_MSG_ERROR = const(0x80) # Error message
59+
60+
_SDEP_INITIALIZE = const(0xBEEF) # Resets the Bluefruit device
61+
_SDEP_ATCOMMAND = const(0x0A00) # AT command wrapper
62+
_SDEP_BLEUART_TX = const(0x0A01) # BLE UART transmit data
63+
_SDEP_BLEUART_RX = const(0x0A02) # BLE UART read data
64+
65+
_ARG_STRING = const(0x0100) # String data type
66+
_ARG_BYTEARRAY = const(0x0200) # Byte array data type
67+
_ARG_INT32 = const(0x0300) # Signed 32-bit integer data type
68+
_ARG_UINT32 = const(0x0400) # Unsigned 32-bit integer data type
69+
_ARG_INT16 = const(0x0500) # Signed 16-bit integer data type
70+
_ARG_UINT16 = const(0x0600) # Unsigned 16-bit integer data type
71+
_ARG_INT8 = const(0x0700) # Signed 8-bit integer data type
72+
_ARG_UINT8 = const(0x0800) # Unsigned 8-bit integer data type
73+
74+
_ERROR_INVALIDMSGTYPE = const(0x8021) # SDEP: Unexpected SDEP MsgType
75+
_ERROR_INVALIDCMDID = const(0x8022) # SDEP: Unknown command ID
76+
_ERROR_INVALIDPAYLOAD = const(0x8023) # SDEP: Payload problem
77+
_ERROR_INVALIDLEN = const(0x8024) # SDEP: Indicated len too large
78+
_ERROR_INVALIDINPUT = const(0x8060) # AT: Invalid data
79+
_ERROR_UNKNOWNCMD = const(0x8061) # AT: Unknown command name
80+
_ERROR_INVALIDPARAM = const(0x8062) # AT: Invalid param value
81+
_ERROR_UNSUPPORTED = const(0x8063) # AT: Unsupported command
82+
83+
# For the Bluefruit Connect packets
84+
_PACKET_BUTTON_LEN = const(5)
85+
_PACKET_COLOR_LEN = const(6)
86+
87+
# pylint: enable=bad-whitespace
12088

12189

12290
class BluefruitSPI:
12391
"""Helper for the Bluefruit LE SPI Friend"""
12492

125-
def __init__(self, spi, cs, irq, reset, debug=False):
93+
def __init__(self, spi, cs, irq, reset, debug=False): # pylint: disable=too-many-arguments
12694
self._irq = irq
12795
self._buf_tx = bytearray(20)
12896
self._buf_rx = bytearray(20)
12997
self._debug = debug
13098

99+
# a cache of data, used for packet parsing
100+
self._buffer = []
101+
131102
# Reset
132103
reset.direction = Direction.OUTPUT
133104
reset.value = False
@@ -146,7 +117,7 @@ def __init__(self, spi, cs, irq, reset, debug=False):
146117
self._spi_device = SPIDevice(spi, cs,
147118
baudrate=4000000, phase=0, polarity=0)
148119

149-
def _cmd(self, cmd):
120+
def _cmd(self, cmd): # pylint: disable=too-many-branches
150121
"""
151122
Executes the supplied AT command, which must be terminated with
152123
a new-line character.
@@ -172,16 +143,19 @@ def _cmd(self, cmd):
172143
plen = 16
173144
# Note the 'more' value in bit 8 of the packet len
174145
struct.pack_into("<BHB16s", self._buf_tx, 0,
175-
MsgType.COMMAND, SDEPCommand.ATCOMMAND,
146+
_MSG_COMMAND, _SDEP_ATCOMMAND,
176147
plen | more, cmd[pos:pos+plen])
177148
if self._debug:
178149
print("Writing: ", [hex(b) for b in self._buf_tx])
150+
else:
151+
time.sleep(0.05)
152+
179153
# Update the position if there is data remaining
180154
pos += plen
181155

182156
# Send out the SPI bus
183157
with self._spi_device as spi:
184-
spi.write(self._buf_tx, end=len(cmd) + 4)
158+
spi.write(self._buf_tx, end=len(cmd) + 4) # pylint: disable=no-member
185159

186160
# Wait up to 200ms for a response
187161
timeout = 0.2
@@ -212,7 +186,8 @@ def _cmd(self, cmd):
212186
rsp += self._buf_rx[4:rsplen+4]
213187
if self._debug:
214188
print("Reading: ", [hex(b) for b in self._buf_rx])
215-
189+
else:
190+
time.sleep(0.05)
216191
# Clean up the response buffer
217192
if self._debug:
218193
print(rsp)
@@ -226,46 +201,105 @@ def init(self):
226201
"""
227202
# Construct the SDEP packet
228203
struct.pack_into("<BHB", self._buf_tx, 0,
229-
MsgType.COMMAND, SDEPCommand.INITIALIZE, 0)
204+
_MSG_COMMAND, _SDEP_INITIALIZE, 0)
230205
if self._debug:
231206
print("Writing: ", [hex(b) for b in self._buf_tx])
232207

233208
# Send out the SPI bus
234209
with self._spi_device as spi:
235-
spi.write(self._buf_tx, end=4)
210+
spi.write(self._buf_tx, end=4) # pylint: disable=no-member
236211

237212
# Wait 1 second for the command to complete.
238213
time.sleep(1)
239214

240-
def uarttx(self, txt):
215+
@property
216+
def connected(self):
217+
"""Whether the Bluefruit module is connected to the central"""
218+
return int(self.command_check_OK(b'AT+GAPGETCONN')) == 1
219+
220+
def uart_tx(self, data):
241221
"""
242-
Sends the specific string out over BLE UART.
243-
:param txt: The new-line terminated string to send.
222+
Sends the specific bytestring out over BLE UART.
223+
:param data: The bytestring to send.
244224
"""
245-
return self._cmd("AT+BLEUARTTX="+txt+"\n")
225+
return self._cmd(b'AT+BLEUARTTX='+data+b'\r\n')
246226

247-
def uartrx(self):
227+
def uart_rx(self):
248228
"""
249-
Reads data from the BLE UART FIFO.
229+
Reads byte data from the BLE UART FIFO.
250230
"""
251-
return self._cmd("AT+BLEUARTRX\n")
231+
data = self.command_check_OK(b'AT+BLEUARTRX')
232+
if data:
233+
# remove \r\n from ending
234+
return data[:-2]
235+
return None
252236

253237
def command(self, string):
238+
"""Send a command and check response code"""
254239
try:
255240
msgtype, msgid, rsp = self._cmd(string+"\n")
256-
if msgtype == MsgType.ERROR:
241+
if msgtype == _MSG_ERROR:
257242
raise RuntimeError("Error (id:{0})".format(hex(msgid)))
258-
if msgtype == MsgType.RESPONSE:
243+
if msgtype == _MSG_RESPONSE:
259244
return rsp
245+
else:
246+
raise RuntimeError("Unknown response (id:{0})".format(hex(msgid)))
260247
except RuntimeError as error:
261248
raise RuntimeError("AT command failure: " + repr(error))
262249

263-
def command_check_OK(self, string, delay=0.0):
264-
ret = self.command(string)
250+
def command_check_OK(self, command, delay=0.0): # pylint: disable=invalid-name
251+
"""Send a fully formed bytestring AT command, and check
252+
whether we got an 'OK' back. Returns payload bytes if there is any"""
253+
ret = self.command(command)
265254
time.sleep(delay)
266255
if not ret or not ret[-4:]:
267256
raise RuntimeError("Not OK")
268257
if ret[-4:] != b'OK\r\n':
269258
raise RuntimeError("Not OK")
270259
if ret[:-4]:
271-
return str(ret[:-4], 'utf-8')
260+
return ret[:-4]
261+
return None
262+
263+
def read_packet(self): # pylint: disable=too-many-return-statements
264+
"""
265+
Will read a Bluefruit Connect packet and return it in a parsed format.
266+
Currently supports Button and Color packets only
267+
"""
268+
data = self.uart_rx()
269+
if not data:
270+
return None
271+
# convert to an array of character bytes
272+
self._buffer += [chr(b) for b in data]
273+
# Find beginning of new packet, starts with a '!'
274+
while self._buffer and self._buffer[0] != '!':
275+
self._buffer.pop(0)
276+
# we need at least 2 bytes in the buffer
277+
if len(self._buffer) < 2:
278+
return None
279+
280+
# Packet beginning found
281+
if self._buffer[1] == 'B':
282+
plen = _PACKET_BUTTON_LEN
283+
elif self._buffer[1] == 'C':
284+
plen = _PACKET_COLOR_LEN
285+
else:
286+
# unknown packet type
287+
self._buffer.pop(0)
288+
return None
289+
290+
# split packet off of buffer cache
291+
packet = self._buffer[0:plen]
292+
293+
self._buffer = self._buffer[plen:] # remove packet from buffer
294+
if sum([ord(x) for x in packet]) % 256 != 255: # check sum
295+
return None
296+
297+
# OK packet's good!
298+
if packet[1] == 'B': # buttons have 2 int args to parse
299+
# button number & True/False press
300+
return ('B', int(packet[2]), packet[3] == '1')
301+
if packet[1] == 'C': # colorpick has 3 int args to parse
302+
# red, green and blue
303+
return ('C', ord(packet[2]), ord(packet[3]), ord(packet[4]))
304+
# We don't nicely parse this yet
305+
return packet[1:-1]
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# NeoPixel Color Picker demo - wire up some NeoPixels and set their color
2+
# using Adafruit Bluefruit Connect App on your phone
3+
4+
import time
5+
import busio
6+
import board
7+
from digitalio import DigitalInOut
8+
from adafruit_bluefruitspi import BluefruitSPI
9+
import neopixel
10+
11+
ADVERT_NAME = b'BlinkaNeoLamp'
12+
13+
# 16 neopixels on a digital pin, adjust as necessary!
14+
pixels = neopixel.NeoPixel(board.D5, 16)
15+
pixels.fill(0)
16+
17+
spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
18+
cs = DigitalInOut(board.D8)
19+
irq = DigitalInOut(board.D7)
20+
rst = DigitalInOut(board.D4)
21+
bluefruit = BluefruitSPI(spi_bus, cs, irq, rst, debug=False)
22+
23+
def init_bluefruit():
24+
# Initialize the device and perform a factory reset
25+
print("Initializing the Bluefruit LE SPI Friend module")
26+
bluefruit.init()
27+
bluefruit.command_check_OK(b'AT+FACTORYRESET', delay=1)
28+
# Print the response to 'ATI' (info request) as a string
29+
print(str(bluefruit.command_check_OK(b'ATI'), 'utf-8'))
30+
# Change advertised name
31+
bluefruit.command_check_OK(b'AT+GAPDEVNAME='+ADVERT_NAME)
32+
33+
def wait_for_connection():
34+
print("Waiting for a connection to Bluefruit LE Connect ...")
35+
# Wait for a connection ...
36+
dotcount = 0
37+
while not bluefruit.connected:
38+
print(".", end="")
39+
dotcount = (dotcount + 1) % 80
40+
if dotcount == 79:
41+
print("")
42+
time.sleep(0.5)
43+
44+
# This code will check the connection but only query the module if it has been
45+
# at least 'n_sec' seconds. Otherwise it 'caches' the response, to keep from
46+
# hogging the Bluefruit connection with constant queries
47+
connection_timestamp = None
48+
is_connected = None
49+
def check_connection(n_sec):
50+
# pylint: disable=global-statement
51+
global connection_timestamp, is_connected
52+
if (not connection_timestamp) or (time.monotonic() - connection_timestamp > n_sec):
53+
connection_timestamp = time.monotonic()
54+
is_connected = bluefruit.connected
55+
return is_connected
56+
57+
# Unlike most circuitpython code, this runs in two loops
58+
# one outer loop manages reconnecting bluetooth if we lose connection
59+
# then one inner loop for doing what we want when connected!
60+
while True:
61+
# Initialize the module
62+
try: # Wireless connections can have corrupt data or other runtime failures
63+
# This try block will reset the module if that happens
64+
init_bluefruit()
65+
wait_for_connection()
66+
print("\n *Connected!*")
67+
68+
# Once connected, check for incoming BLE UART data
69+
while check_connection(3): # Check our connection status every 3 seconds
70+
# OK we're still connected, see if we have any data waiting
71+
resp = bluefruit.read_packet()
72+
if not resp:
73+
continue # nothin'
74+
print("Read packet", resp)
75+
# Look for a 'C'olor packet
76+
if resp[0] != 'C':
77+
continue
78+
# Set the neopixels to the three bytes in the packet
79+
pixels.fill(resp[1:4])
80+
print("Connection lost.")
81+
82+
except RuntimeError as e:
83+
print(e) # Print what happened
84+
continue # retry!

0 commit comments

Comments
 (0)