From a2a613796cec684671aa9839673b4a1970ae0cbf Mon Sep 17 00:00:00 2001 From: ladyada Date: Sun, 4 Nov 2018 14:09:56 -0500 Subject: [PATCH 1/5] update to remove enum classes (they are not used externally), add some delays (now stable), RX/TX with bytes, and some packet parsing --- adafruit_bluefruitspi.py | 202 ++++++++++++++++------------ examples/bluefruitspi_simpletest.py | 60 ++++++--- 2 files changed, 157 insertions(+), 105 deletions(-) diff --git a/adafruit_bluefruitspi.py b/adafruit_bluefruitspi.py index 20b00d3..566a82f 100644 --- a/adafruit_bluefruitspi.py +++ b/adafruit_bluefruitspi.py @@ -42,92 +42,63 @@ * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice """ -# imports - __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BluefruitSPI.git" import time +import struct from digitalio import Direction, Pull from adafruit_bus_device.spi_device import SPIDevice from micropython import const -import struct - -class MsgType: #pylint: disable=too-few-public-methods,bad-whitespace - """An enum-like class representing the possible message types. - Possible values are: - - ``MsgType.COMMAND`` - - ``MsgType.RESPONSE`` - - ``MsgType.ALERT`` - - ``MsgType.ERROR`` - """ - COMMAND = const(0x10) # Command message - RESPONSE = const(0x20) # Response message - ALERT = const(0x40) # Alert message - ERROR = const(0x80) # Error message - - -class SDEPCommand: #pylint: disable=too-few-public-methods,bad-whitespace - """An enum-like class representing the possible SDEP commands. - Possible values are: - - ``SDEPCommand.INITIALIZE`` - - ``SDEPCommand.ATCOMMAND`` - - ``SDEPCommand.BLEUART_TX`` - - ``SDEPCommand.BLEUART_RX`` - """ - INITIALIZE = const(0xBEEF) # Resets the Bluefruit device - ATCOMMAND = const(0x0A00) # AT command wrapper - BLEUART_TX = const(0x0A01) # BLE UART transmit data - BLEUART_RX = const(0x0A02) # BLE UART read data - - -class ArgType: #pylint: disable=too-few-public-methods,bad-whitespace - """An enum-like class representing the possible argument types. - Possible values are - - ``ArgType.STRING`` - - ``ArgType.BYTEARRAY`` - - ``ArgType.INT32`` - - ``ArgType.UINT32`` - - ``ArgType.INT16`` - - ``ArgType.UINT16`` - - ``ArgType.INT8`` - - ``ArgType.UINT8`` - """ - STRING = const(0x0100) # String data type - BYTEARRAY = const(0x0200) # Byte array data type - INT32 = const(0x0300) # Signed 32-bit integer data type - UINT32 = const(0x0400) # Unsigned 32-bit integer data type - INT16 = const(0x0500) # Signed 16-bit integer data type - UINT16 = const(0x0600) # Unsigned 16-bit integer data type - INT8 = const(0x0700) # Signed 8-bit integer data type - UINT8 = const(0x0800) # Unsigned 8-bit integer data type - - -class ErrorCode: #pylint: disable=too-few-public-methods,bad-whitespace - """An enum-like class representing possible error codes. - Possible values are - - ``ErrorCode.`` - """ - INVALIDMSGTYPE = const(0x8021) # SDEP: Unexpected SDEP MsgType - INVALIDCMDID = const(0x8022) # SDEP: Unknown command ID - INVALIDPAYLOAD = const(0x8023) # SDEP: Payload problem - INVALIDLEN = const(0x8024) # SDEP: Indicated len too large - INVALIDINPUT = const(0x8060) # AT: Invalid data - UNKNOWNCMD = const(0x8061) # AT: Unknown command name - INVALIDPARAM = const(0x8062) # AT: Invalid param value - UNSUPPORTED = const(0x8063) # AT: Unsupported command +# pylint: disable=bad-whitespace +_MSG_COMMAND = const(0x10) # Command message +_MSG_RESPONSE = const(0x20) # Response message +_MSG_ALERT = const(0x40) # Alert message +_MSG_ERROR = const(0x80) # Error message + +_SDEP_INITIALIZE = const(0xBEEF) # Resets the Bluefruit device +_SDEP_ATCOMMAND = const(0x0A00) # AT command wrapper +_SDEP_BLEUART_TX = const(0x0A01) # BLE UART transmit data +_SDEP_BLEUART_RX = const(0x0A02) # BLE UART read data + +_ARG_STRING = const(0x0100) # String data type +_ARG_BYTEARRAY = const(0x0200) # Byte array data type +_ARG_INT32 = const(0x0300) # Signed 32-bit integer data type +_ARG_UINT32 = const(0x0400) # Unsigned 32-bit integer data type +_ARG_INT16 = const(0x0500) # Signed 16-bit integer data type +_ARG_UINT16 = const(0x0600) # Unsigned 16-bit integer data type +_ARG_INT8 = const(0x0700) # Signed 8-bit integer data type +_ARG_UINT8 = const(0x0800) # Unsigned 8-bit integer data type + +_ERROR_INVALIDMSGTYPE = const(0x8021) # SDEP: Unexpected SDEP MsgType +_ERROR_INVALIDCMDID = const(0x8022) # SDEP: Unknown command ID +_ERROR_INVALIDPAYLOAD = const(0x8023) # SDEP: Payload problem +_ERROR_INVALIDLEN = const(0x8024) # SDEP: Indicated len too large +_ERROR_INVALIDINPUT = const(0x8060) # AT: Invalid data +_ERROR_UNKNOWNCMD = const(0x8061) # AT: Unknown command name +_ERROR_INVALIDPARAM = const(0x8062) # AT: Invalid param value +_ERROR_UNSUPPORTED = const(0x8063) # AT: Unsupported command + +# For the Bluefruit Connect packets +_PACKET_BUTTON_LEN = const(5) +_PACKET_COLOR_LEN = const(6) + +# pylint: enable=bad-whitespace class BluefruitSPI: """Helper for the Bluefruit LE SPI Friend""" - def __init__(self, spi, cs, irq, reset, debug=False): + def __init__(self, spi, cs, irq, reset, debug=False): # pylint: disable=too-many-arguments self._irq = irq self._buf_tx = bytearray(20) self._buf_rx = bytearray(20) self._debug = debug + # a cache of data, used for packet parsing + self._buffer = [] + # Reset reset.direction = Direction.OUTPUT reset.value = False @@ -146,7 +117,7 @@ def __init__(self, spi, cs, irq, reset, debug=False): self._spi_device = SPIDevice(spi, cs, baudrate=4000000, phase=0, polarity=0) - def _cmd(self, cmd): + def _cmd(self, cmd): # pylint: disable=too-many-branches """ Executes the supplied AT command, which must be terminated with a new-line character. @@ -172,10 +143,13 @@ def _cmd(self, cmd): plen = 16 # Note the 'more' value in bit 8 of the packet len struct.pack_into(" 3: + connection_timestamp = time.monotonic() + if not bluefruit.connected: + break + + # OK we're still connected, see if we have any data waiting + resp = bluefruit.uart_rx() + if not resp: + continue # nothin' + print("Read %d bytes: %s" % (len(resp), resp)) + # Now write it! + print("Writing reverse...") + send = [] + for i in range(len(resp), 0, -1): + send.append(resp[i-1]) + print(bytes(send)) + bluefruit.uart_tx(bytes(send)) + print("Connection lost.") From 1db909ba7b92a9b563fe6a87f500317828f399eb Mon Sep 17 00:00:00 2001 From: ladyada Date: Sun, 4 Nov 2018 18:20:15 -0500 Subject: [PATCH 2/5] linted --- examples/bluefruitspi_simpletest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/bluefruitspi_simpletest.py b/examples/bluefruitspi_simpletest.py index 91b3cf7..58e2a17 100644 --- a/examples/bluefruitspi_simpletest.py +++ b/examples/bluefruitspi_simpletest.py @@ -1,10 +1,10 @@ # A simple echo test for the Feather M0 Bluefruit -# Sets the name, then +# Sets the name, then echo's all RX'd data with a reversed packet +import time import busio -from digitalio import DigitalInOut, Direction import board -import time +from digitalio import DigitalInOut from adafruit_bluefruitspi import BluefruitSPI spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) @@ -57,5 +57,5 @@ send.append(resp[i-1]) print(bytes(send)) bluefruit.uart_tx(bytes(send)) - + print("Connection lost.") From 99503c3d99ee31b72444e8145b3317ff76bc9d43 Mon Sep 17 00:00:00 2001 From: ladyada Date: Sun, 4 Nov 2018 18:22:28 -0500 Subject: [PATCH 3/5] this is a pylint bug, ignore it! --- adafruit_bluefruitspi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_bluefruitspi.py b/adafruit_bluefruitspi.py index 566a82f..7e7a071 100644 --- a/adafruit_bluefruitspi.py +++ b/adafruit_bluefruitspi.py @@ -155,7 +155,7 @@ def _cmd(self, cmd): # pylint: disable=too-many-branches # Send out the SPI bus with self._spi_device as spi: - spi.write(self._buf_tx, end=len(cmd) + 4) + spi.write(self._buf_tx, end=len(cmd) + 4) # pylint: disable=no-member # Wait up to 200ms for a response timeout = 0.2 @@ -207,7 +207,7 @@ def init(self): # Send out the SPI bus with self._spi_device as spi: - spi.write(self._buf_tx, end=4) + spi.write(self._buf_tx, end=4) # pylint: disable=no-member # Wait 1 second for the command to complete. time.sleep(1) From 3407acdc9936e438c1cae13a7e1dbfd8004dec53 Mon Sep 17 00:00:00 2001 From: ladyada Date: Sun, 4 Nov 2018 18:47:20 -0500 Subject: [PATCH 4/5] 2 more examples --- examples/bluefruitspi_neocolorpicker.py | 82 +++++++++++++++++++++++++ examples/bluefruitspi_ruggedechotest.py | 81 ++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 examples/bluefruitspi_neocolorpicker.py create mode 100644 examples/bluefruitspi_ruggedechotest.py diff --git a/examples/bluefruitspi_neocolorpicker.py b/examples/bluefruitspi_neocolorpicker.py new file mode 100644 index 0000000..075d3e3 --- /dev/null +++ b/examples/bluefruitspi_neocolorpicker.py @@ -0,0 +1,82 @@ +# NeoPixel Color Picker demo - wire up some NeoPixels and set their color +# using Adafruit Bluefruit Connect App on your phone + +import time +import busio +import board +from digitalio import DigitalInOut +from adafruit_bluefruitspi import BluefruitSPI +import neopixel + +ADVERT_NAME = b'BlinkaNeoLamp' + +# 16 neopixels on a digital pin, adjust as necessary! +pixels = neopixel.NeoPixel(board.D5, 16) +pixels.fill(0) + +spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) +cs = DigitalInOut(board.D8) +irq = DigitalInOut(board.D7) +rst = DigitalInOut(board.D4) +bluefruit = BluefruitSPI(spi_bus, cs, irq, rst, debug=False) + +def init_bluefruit(): + # Initialize the device and perform a factory reset + print("Initializing the Bluefruit LE SPI Friend module") + bluefruit.init() + bluefruit.command_check_OK(b'AT+FACTORYRESET', delay=1) + # Print the response to 'ATI' (info request) as a string + print(str(bluefruit.command_check_OK(b'ATI'), 'utf-8')) + # Change advertised name + bluefruit.command_check_OK(b'AT+GAPDEVNAME='+ADVERT_NAME) + +def wait_for_connection(): + print("Waiting for a connection to Bluefruit LE Connect ...") + # Wait for a connection ... + dotcount = 0 + while not bluefruit.connected: + print(".", end="") + dotcount = (dotcount + 1) % 80 + if dotcount == 79: + print("") + time.sleep(0.5) + +# This code will check the connection but only query the module if it has been +# at least 'n_sec' seconds. Otherwise it 'caches' the response, to keep from +# hogging the Bluefruit connection with constant queries +connect_check_timestamp = None +is_connected = None +def check_connection(n_sec): + if (not connect_check_timestamp) or (time.monotonic() - connection_timestamp > n_sec): + connection_timestamp = time.monotonic() + is_connected = bluefruit.connected + return is_connected + +# Unlike most circuitpython code, this runs in two loops +# one outer loop manages reconnecting bluetooth if we lose connection +# then one inner loop for doing what we want when connected! +while True: + # Initialize the module + try: # Wireless connections can have corrupt data or other runtime failures + # This try block will reset the module if that happens + init_bluefruit() + wait_for_connection() + print("\n *Connected!*") + + # Once connected, check for incoming BLE UART data + while check_connection(3): # Check our connection status every 3 seconds + # OK we're still connected, see if we have any data waiting + resp = bluefruit.read_packet() + if not resp: + continue # nothin' + print("Read packet", resp) + # Look for a 'C'olor packet + if resp[0] != 'C': + continue + # Set the neopixels to the three bytes in the packet + pixels.fill(resp[1:4]) + print("Connection lost.") + + except RuntimeError as e: + print(e) # Print what happened + continue # retry! diff --git a/examples/bluefruitspi_ruggedechotest.py b/examples/bluefruitspi_ruggedechotest.py new file mode 100644 index 0000000..333554f --- /dev/null +++ b/examples/bluefruitspi_ruggedechotest.py @@ -0,0 +1,81 @@ +# A more 'rugged' echo test for the Feather M0 Bluefruit +# Sets the name, then echo's all RX'd data with a reversed packet +# we wrap the loop in a try/except block and have multiple loops +# and functions to keep our connection up and running no matter what + +import time +import busio +import board +from digitalio import DigitalInOut +from adafruit_bluefruitspi import BluefruitSPI + +ADVERT_NAME = b'BlinkaBLE' + +spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) +cs = DigitalInOut(board.D8) +irq = DigitalInOut(board.D7) +rst = DigitalInOut(board.D4) +bluefruit = BluefruitSPI(spi_bus, cs, irq, rst, debug=False) + +def init_bluefruit(): + # Initialize the device and perform a factory reset + print("Initializing the Bluefruit LE SPI Friend module") + bluefruit.init() + bluefruit.command_check_OK(b'AT+FACTORYRESET', delay=1) + # Print the response to 'ATI' (info request) as a string + print(str(bluefruit.command_check_OK(b'ATI'), 'utf-8')) + # Change advertised name + bluefruit.command_check_OK(b'AT+GAPDEVNAME='+ADVERT_NAME) + +def wait_for_connection(): + print("Waiting for a connection to Bluefruit LE Connect ...") + # Wait for a connection ... + dotcount = 0 + while not bluefruit.connected: + print(".", end="") + dotcount = (dotcount + 1) % 80 + if dotcount == 79: + print("") + time.sleep(0.5) + +# This code will check the connection but only query the module if it has been +# at least 'n_sec' seconds. Otherwise it 'caches' the response, to keep from +# hogging the Bluefruit connection with constant queries +connect_check_timestamp = None +is_connected = None +def check_connection(n_sec): + if (not connect_check_timestamp) or (time.monotonic() - connection_timestamp > n_sec): + connection_timestamp = time.monotonic() + is_connected = bluefruit.connected + return is_connected + +# Unlike most circuitpython code, this runs in two loops +# one outer loop manages reconnecting bluetooth if we lose connection +# then one inner loop for doing what we want when connected! +while True: + # Initialize the module + try: # Wireless connections can have corrupt data or other runtime failures + # This try block will reset the module if that happens + init_bluefruit() + wait_for_connection() + print("\n *Connected!*") + + # Once connected, check for incoming BLE UART data + while check_connection(3): # Check our connection status every 3 seconds + # OK we're still connected, see if we have any data waiting + resp = bluefruit.uart_rx() + if not resp: + continue # nothin' + print("Read %d bytes: %s" % (len(resp), resp)) + # Now write it! + print("Writing reverse...") + send = [] + for i in range(len(resp), 0, -1): + send.append(resp[i-1]) + print(bytes(send)) + bluefruit.uart_tx(bytes(send)) + print("Connection lost.") + + except RuntimeError as e: + print(e) # Print what happened + continue # retry! From d481a64096da041a285b4622a8d035044ccd2079 Mon Sep 17 00:00:00 2001 From: ladyada Date: Sun, 4 Nov 2018 18:57:49 -0500 Subject: [PATCH 5/5] la la la lint --- examples/bluefruitspi_neocolorpicker.py | 6 ++++-- examples/bluefruitspi_ruggedechotest.py | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/bluefruitspi_neocolorpicker.py b/examples/bluefruitspi_neocolorpicker.py index 075d3e3..a263c57 100644 --- a/examples/bluefruitspi_neocolorpicker.py +++ b/examples/bluefruitspi_neocolorpicker.py @@ -44,10 +44,12 @@ def wait_for_connection(): # This code will check the connection but only query the module if it has been # at least 'n_sec' seconds. Otherwise it 'caches' the response, to keep from # hogging the Bluefruit connection with constant queries -connect_check_timestamp = None +connection_timestamp = None is_connected = None def check_connection(n_sec): - if (not connect_check_timestamp) or (time.monotonic() - connection_timestamp > n_sec): + # pylint: disable=global-statement + global connection_timestamp, is_connected + if (not connection_timestamp) or (time.monotonic() - connection_timestamp > n_sec): connection_timestamp = time.monotonic() is_connected = bluefruit.connected return is_connected diff --git a/examples/bluefruitspi_ruggedechotest.py b/examples/bluefruitspi_ruggedechotest.py index 333554f..27394cd 100644 --- a/examples/bluefruitspi_ruggedechotest.py +++ b/examples/bluefruitspi_ruggedechotest.py @@ -36,19 +36,21 @@ def wait_for_connection(): dotcount = (dotcount + 1) % 80 if dotcount == 79: print("") - time.sleep(0.5) + time.sleep(0.5) # This code will check the connection but only query the module if it has been # at least 'n_sec' seconds. Otherwise it 'caches' the response, to keep from # hogging the Bluefruit connection with constant queries -connect_check_timestamp = None +connection_timestamp = None is_connected = None def check_connection(n_sec): - if (not connect_check_timestamp) or (time.monotonic() - connection_timestamp > n_sec): + # pylint: disable=global-statement + global connection_timestamp, is_connected + if (not connection_timestamp) or (time.monotonic() - connection_timestamp > n_sec): connection_timestamp = time.monotonic() is_connected = bluefruit.connected return is_connected - + # Unlike most circuitpython code, this runs in two loops # one outer loop manages reconnecting bluetooth if we lose connection # then one inner loop for doing what we want when connected!