From: Limor "Ladyada" Fried Date: Tue, 27 Feb 2018 17:34:16 +0000 (-0500) Subject: Merge pull request #8 from cefn/master X-Git-Tag: 0.1.0~4 X-Git-Url: https://git.ayoreis.com/Adafruit_Blinka-hackapet.git/commitdiff_plain/eabc5490cba488c6beba6a17d295fff327b636c0?hp=1d399bb3171e129ffc97850946fb9e998e0383aa Merge pull request #8 from cefn/master PR now structure and approach somewhat stable --- diff --git a/.gitignore b/.gitignore index 0dd8629..cc5b9ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.mpy +.idea __pycache__ _build *.pyc diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..41bbf17 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,21 @@ +[submodule "test/libraries/bme280"] + path = test/libraries/bme280 + url = https://github.com/adafruit/Adafruit_CircuitPython_BME280.git +[submodule "test/libraries/gps"] + path = test/libraries/gps + url = https://github.com/adafruit/Adafruit_CircuitPython_GPS.git +[submodule "test/libraries/Adafruit_CircuitPython_RFM69"] + path = test/libraries/rfm69 + url = https://github.com/adafruit/Adafruit_CircuitPython_RFM69.git +[submodule "test/libraries/bus_device"] + path = test/libraries/bus_device + url = https://github.com/adafruit/Adafruit_CircuitPython_BusDevice.git +[submodule "test/libraries/bno055"] + path = test/libraries/bno055 + url = https://github.com/adafruit/Adafruit_CircuitPython_BNO055.git +[submodule "test/libraries/mma8451"] + path = test/libraries/mma8451 + url = https://github.com/adafruit/Adafruit_CircuitPython_MMA8451.git +[submodule "test/libraries/register"] + path = test/libraries/register + url = https://github.com/adafruit/Adafruit_CircuitPython_Register.git diff --git a/README.rst b/README.rst index 37d09da..127131d 100644 --- a/README.rst +++ b/README.rst @@ -13,18 +13,40 @@ Introduction :target: https://travis-ci.org/adafruit/Adafruit__Micropython_Blinka :alt: Build Status -.. todo:: Describe what the library does. +This repository contains a selection of packages mirroring the CircuitPython API +on hosts running micropython. Working code exists to emulate the CircuitPython packages; + +* **board** - breakout-specific pin identities +* **microcontroller** - chip-specific pin identities +* **digitalio** - digital input/output pins, using pin identities from board+microcontroller packages +* **bitbangio** - software-driven interfaces for I2C, SPI +* **busio** - hardware-driven interfaces for I2C, SPI, UART +* **time** * - substitute functions monkey-patched to time module + Dependencies ============= -This driver depends on: -* Micropython +The Micropython compatibility layers described above are intended to provide a CircuitPython-like API for devices which +are running Micropython. Since corresponding packages should be built-in to any standard +CircuitPython image, they have no value on a device already running CircuitPython and would likely conflict in unhappy ways. + +The test suites in the test/src folder under **testing.universal** are by design +intended to run on *either* CircuitPython *or* Micropython+compatibility layer to prove conformance. Usage Example ============= -.. todo:: Add a quick, simple example. It and other examples should live in the examples folder and be included in docs/examples.rst. +At the time of writing (`git:7fc1f8ab `_), +the following sequence runs through some basic testing of the digitalio compatibility layer... + +.. code-block:: python + + from testing import test_module_name + test_module_name("testing.universal.digitalio") + +An example log from running the suites is `here `_ . + Contributing ============ diff --git a/examples/index.md b/examples/index.md new file mode 100644 index 0000000..184ca33 --- /dev/null +++ b/examples/index.md @@ -0,0 +1,69 @@ +# About Adafruit_Micropython_Blinka + +This repository is structured around integration tests rooted at the `test/src` +directory, intended to test the compatibility layer rooted in `src`. + +The tests offer a procedural way to assert equivalence between 'native' CircuitPython behaviour and behaviour of the **adafruit_blinka** compatibility layer. + +The structure of the testing modules permits test suites to be imported and configured selectively on different implementations, platforms and boards (see `adafruit_blinka.agnostic.py` for definitions of these terms). + +Automated introspection of the python runtime combines with interactive prompts +to configure a scenario for testing (e.g. which platform, which board, what is wired to it) +so the same routines can be carried out on Micropython boards, dual boards running either CircuitPython or Micropython, or dedicated CircuitPython boards. + +Typically the tests have first run on a native CircuitPython platform, and are then used to +prove equivalence on a Micropython platform running the **adafruit_blinka** compatibility layer. + +# Tests so far + +Tests of compatible versions of **digitalio**, **board** and **microcontroller** have successfully demonstrated +the same code running on either platform, setting and getting pin values and using pull. + +Tests have also proven compatibility of the following unmodified CircuitPython libraries... + +* adafruit_bme280 +* adafruit_mma8451 +* adafruit_gps + +...which proves the fundamentals of bitbangio.I2C, busio.I2C and busio.UART + +# Example + +To take a minimal example, the following should assert the default behaviour of the DigitalInOut +constructor, checks the behaviour of switch_to_input/output(), configures a pin as a pull-up button, a pull-down button and an LED. + +```python +from testing import test_module_name +test_module_name("testing.universal.digitalio") +``` + +Or to take a more involved example of constructing a test suite requiring hardware, +the following should verify I2C communication with a BME280 module. + +```python +import unittest +import testing.universal.i2c +suite = unittest.TestSuite() +suite.addTest(testing.universal.i2c.TestBME280Interactive) +runner = unittest.TestRunner() +runner.run(suite) +``` + + +To prove this on a newly-flashed Feather Huzzah running Micropython 1.9.3, +it should be possible (on a posix-compliant platform with adafruit_ampy installed) +to `cd test/scripts` then run `./upload_feather_huzzah_micropython_put.sh` to +synchronize relevant files to the filesystem of the huzzah, reset the huzzah then +connect using `screen /dev/ttyUSB0 115200` before running the above commands. + +Micropython hosts require a micropython repository alongside +the Adafruit_Micropython_Blinka repository. For circuitpython, +the repository is expected to be called circuitpython_2.2.3. +In each case, the matching version should have been checked out from github +and `make` needs to have been run in the `mpy-cross` folder. This provides a tool +to make bytecode-compiled .mpy versions of all .py files before upload so that +tests can be achieved within the limited memory available on many target platforms. + +## Comments + +There are reference routines in `test/scripts` like `upload_feather_huzzah_micropython_put.sh` which execute a selective bytecode-compile to .mpy format and an ampy upload for CircuitPython/Micropython on esp8266, or `upload_pyboard_micropython_cp.sh` which selectively bytecode-compiles and synchronizes files with cp to the CIRCUITPY or PYBFLASH disk mount for stm32 and samd21 platforms. \ No newline at end of file diff --git a/src/adafruit_blinka/__init__.py b/src/adafruit_blinka/__init__.py new file mode 100644 index 0000000..08ec27e --- /dev/null +++ b/src/adafruit_blinka/__init__.py @@ -0,0 +1,61 @@ +"""Module providing runtime utility objects to support the Micro/CircuitPython api""" + + +class Enum(object): + """ + Object supporting CircuitPython-style of static symbols + as seen with Direction.OUTPUT, Pull.UP + """ + + def __repr__(self): + """ + Assumes instance will be found as attribute of own class. + Returns dot-subscripted path to instance + (assuming absolute import of containing package) + """ + cls = type(self) + for key in dir(cls): + if getattr(cls, key) is self: + return "{}.{}.{}".format(cls.__module__, cls.__qualname__, key) + return repr(self) + + @classmethod + def iteritems(cls): + """ + Inspects attributes of the class for instances of the class + and returns as key,value pairs mirroring dict#iteritems + """ + for key in dir(cls): + val = getattr(cls, key) + if type(val) is cls: + yield (key, val) + + +class ContextManaged: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.deinit() + + +class Lockable(ContextManaged): + _locked = False + + def try_lock(self): + if self._locked: + return False + else: + self._locked = True + return True + + def unlock(self): + if self._locked: + self._locked = False + else: + raise ValueError("Not locked") + +def patch_system(): + import sys + from adafruit_blinka.agnostic import time + sys.modules['time'] = time \ No newline at end of file diff --git a/src/adafruit_blinka/agnostic/__init__.py b/src/adafruit_blinka/agnostic/__init__.py new file mode 100644 index 0000000..85670bf --- /dev/null +++ b/src/adafruit_blinka/agnostic/__init__.py @@ -0,0 +1,31 @@ +"""Allows useful indirection to test Pin naming logic by switching platform in testing + or provide bootstrapping logic for board identification where auto-detection is not + feasible (e.g. multiple ESP8266 boards architecturally identical). Once runtime + environment is established, can choose various routes to make available and re-export + common modules and operations, depending on platform support +""" +import gc +import sys +gc.collect() + +try: + microcontroller = sys.platform +except: + microcontroller = None + +board = None +if microcontroller is not None: + if microcontroller == "esp8266": # TODO more conservative board-guessing + board = "feather_huzzah" + elif microcontroller == "samd21": + board = "feather_m0_express" + elif microcontroller == "pyboard": + microcontroller = "stm32" + board = "pyboard" + +implementation = sys.implementation.name +if implementation == "micropython": + from utime import sleep +elif implementation == "circuitpython": + from time import sleep +gc.collect() diff --git a/src/adafruit_blinka/agnostic/time.py b/src/adafruit_blinka/agnostic/time.py new file mode 100644 index 0000000..078b833 --- /dev/null +++ b/src/adafruit_blinka/agnostic/time.py @@ -0,0 +1,27 @@ +from adafruit_blinka import agnostic +if agnostic.implementation == "circuitpython": + from time import * +elif agnostic.implementation == "micropython": + import utime + from utime import sleep + + from ucollections import namedtuple + _struct_time = namedtuple("struct_time", ("tm_year", "tm_mon", "tm_mday", "tm_hour", "tm_min", "tm_sec", "tm_wday", "tm_yday", "tm_isdst")) + + def marshal_time(tm_year, tm_mon, tm_mday, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=-1, tm_yday=-1, tm_isdst=-1): + _struct_time(tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst) + + def struct_time(t): + return marshal_time(*t) + + total_ms = 0 + prev_ticks_ms = utime.ticks_ms() + def monotonic(): + """ + Assumes that monotonic is called more frequently than the wraparound of micropython's utime.ticks_ms() + """ + global prev_ticks_ms, total_ms + ticks_ms = utime.ticks_ms() + total_ms += utime.ticks_diff(ticks_ms, prev_ticks_ms) + prev_ticks_ms = ticks_ms + return total_ms * 0.001 \ No newline at end of file diff --git a/examples/blinka_simpletest.py b/src/adafruit_blinka/board/__init__.py similarity index 100% rename from examples/blinka_simpletest.py rename to src/adafruit_blinka/board/__init__.py diff --git a/src/adafruit_blinka/board/feather_huzzah.py b/src/adafruit_blinka/board/feather_huzzah.py new file mode 100644 index 0000000..60dd863 --- /dev/null +++ b/src/adafruit_blinka/board/feather_huzzah.py @@ -0,0 +1,27 @@ +from adafruit_blinka.microcontroller.esp8266 import pin + +# TODO need equiv of INPUT_PULL_DOWN_16 ? see https://tttapa.github.io/ESP8266/Chap04%20-%20Microcontroller.html + +GPIO0 = pin.GPIO0 +GPIO1 = pin.GPIO1 +GPIO2 = pin.GPIO2 +GPIO3 = pin.GPIO3 +GPIO4 = pin.GPIO4 +GPIO5 = pin.GPIO5 +GPIO12 = pin.GPIO12 +GPIO13 = pin.GPIO13 +GPIO14 = pin.GPIO14 +GPIO15 = pin.GPIO15 +GPIO16 = pin.GPIO16 + +ADC = pin.TOUT + +MISO = GPIO12 +MOSI = GPIO13 +SCK = GPIO14 + +RX = GPIO3 +TX = GPIO1 + +SDA = GPIO4 +SCL = GPIO5 \ No newline at end of file diff --git a/src/adafruit_blinka/board/nodemcu.py b/src/adafruit_blinka/board/nodemcu.py new file mode 100644 index 0000000..83e4ca8 --- /dev/null +++ b/src/adafruit_blinka/board/nodemcu.py @@ -0,0 +1,29 @@ +from adafruit_blinka.microcontroller.esp8266 import pin + +D0 = pin.GPIO16 +D1 = pin.GPIO5 +D2 = pin.GPIO4 +D3 = pin.GPIO0 +D4 = pin.GPIO2 +D5 = pin.GPIO14 +D6 = pin.GPIO12 +D7 = pin.GPIO13 +D8 = pin.GPIO15 +D9 = pin.GPIO3 +D10 = pin.GPIO1 + +TX1 = D4 +"""Transmit pin from second (transmit-only) UART """ + +CLK = D5 +"""SPI clock pin""" +MISO = D6 +"""SPI MISO (Master in, Slave out)""" +MOSI = D7 +"""SPI MOSI (Master out, Slave in)""" + +RX0 = D9 +TX0 = D10 + +# GPIO0 and GPIO2 have built-in pull-ups on common ESP8266 +# breakout boards making them suitable for I2C SDA and SCL \ No newline at end of file diff --git a/src/adafruit_blinka/board/pyboard.py b/src/adafruit_blinka/board/pyboard.py new file mode 100644 index 0000000..4bf49de --- /dev/null +++ b/src/adafruit_blinka/board/pyboard.py @@ -0,0 +1,51 @@ +from adafruit_blinka.microcontroller.stm32 import pin + +X1 = pin.A0 +X2 = pin.A1 +X3 = pin.A2 +X4 = pin.A3 +X5 = pin.A4 +X6 = pin.A5 +X7 = pin.A6 +X8 = pin.A7 +X9 = pin.B6 +X10 = pin.B7 +X11 = pin.C4 +X12 = pin.C5 +X17 = pin.B3 +X18 = pin.C13 +X19 = pin.C0 +X20 = pin.C1 +X21 = pin.C2 +X22 = pin.C3 +Y1 = pin.C6 +Y2 = pin.C7 +Y3 = pin.B8 +Y4 = pin.B9 +Y5 = pin.B12 +Y6 = pin.B13 +Y7 = pin.B14 +Y8 = pin.B15 +Y9 = pin.B10 +Y10 = pin.B11 +Y11 = pin.B0 +Y12 = pin.B1 +SW = pin.B3 +LED_RED = pin.A13 +LED_GREEN = pin.A14 +LED_YELLOW = pin.A15 +LED_BLUE = pin.B4 +MMA_INT = pin.B2 +MMA_AVDD = pin.B5 +SD_D0 = pin.C8 +SD_D1 = pin.C9 +SD_D2 = pin.C10 +SD_D3 = pin.C11 +SD_CMD = pin.D2 +SD_CK = pin.C12 +SD = pin.A8 +SD_SW = pin.A8 +USB_VBUS = pin.A9 +USB_ID = pin.A10 +USB_DM = pin.A11 +USB_DP = pin.A12 diff --git a/src/adafruit_blinka/microcontroller/__init__.py b/src/adafruit_blinka/microcontroller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/src/adafruit_blinka/microcontroller/esp8266/__init__.py similarity index 100% rename from requirements.txt rename to src/adafruit_blinka/microcontroller/esp8266/__init__.py diff --git a/src/adafruit_blinka/microcontroller/esp8266/pin.py b/src/adafruit_blinka/microcontroller/esp8266/pin.py new file mode 100644 index 0000000..c37e053 --- /dev/null +++ b/src/adafruit_blinka/microcontroller/esp8266/pin.py @@ -0,0 +1,25 @@ +from microcontroller import Pin + +GPIO0 = Pin(0) +GPIO1 = Pin(1) +GPIO2 = Pin(2) +GPIO3 = Pin(3) +GPIO4 = Pin(4) +GPIO5 = Pin(5) +GPIO12 = Pin(12) +GPIO13 = Pin(13) +GPIO14 = Pin(14) +GPIO15 = Pin(15) +GPIO16 = Pin(16) +TOUT = Pin("TOUT") + +# ordered as spiId, sckId, mosiId, misoId +spiPorts = ((1, GPIO14, GPIO13, GPIO12)) + +# ordered as uartId, txId, rxId +uartPorts = ( + (0, GPIO1, GPIO3), + # (0, GPIO15, GPIO13) # TODO secondary pins for UART0 configurable from Micropython? How to flag? + (1, GPIO2, None)) + +i2cPorts = () \ No newline at end of file diff --git a/src/adafruit_blinka/microcontroller/stm32/__init__.py b/src/adafruit_blinka/microcontroller/stm32/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/adafruit_blinka/microcontroller/stm32/pin.py b/src/adafruit_blinka/microcontroller/stm32/pin.py new file mode 100644 index 0000000..3f46781 --- /dev/null +++ b/src/adafruit_blinka/microcontroller/stm32/pin.py @@ -0,0 +1,67 @@ +from microcontroller import Pin + +A0 = Pin('A0') +A1 = Pin('A1') +A2 = Pin('A2') +A3 = Pin('A3') +A4 = Pin('A4') +A5 = Pin('A5') +A6 = Pin('A6') +A7 = Pin('A7') +A8 = Pin('A8') +A9 = Pin('A9') +A10 = Pin('A10') +A11 = Pin('A11') +A12 = Pin('A12') +A13 = Pin('A13') +A14 = Pin('A14') +A15 = Pin('A15') +B0 = Pin('B0') +B1 = Pin('B1') +B2 = Pin('B2') +B3 = Pin('B3') +B4 = Pin('B4') +B5 = Pin('B5') +B6 = Pin('B6') +B7 = Pin('B7') +B8 = Pin('B8') +B9 = Pin('B9') +B10 = Pin('B10') +B11 = Pin('B11') +B12 = Pin('B12') +B13 = Pin('B13') +B14 = Pin('B14') +B15 = Pin('B15') +C0 = Pin('C0') +C1 = Pin('C1') +C2 = Pin('C2') +C3 = Pin('C3') +C4 = Pin('C4') +C5 = Pin('C5') +C6 = Pin('C6') +C7 = Pin('C7') +C8 = Pin('C8') +C9 = Pin('C9') +C10 = Pin('C10') +C11 = Pin('C11') +C12 = Pin('C12') +C13 = Pin('C13') +D2 = Pin('D2') + +# ordered as spiId, sckId, mosiId, misoId +spiPorts = ((1, B13, B15, B14), (2, A5, A6, A7)) + +# ordered as uartId, txId, rxId +uartPorts = ( + (1, B6, B7), + (2, A2, A3), + (3, B10, B11), + (4, A0, A1), + (6, C6, C7), +) + +i2cPorts = ( + (1, B6, B7), + (2, B10, B11), +) + diff --git a/src/bitbangio.py b/src/bitbangio.py new file mode 100644 index 0000000..101bfff --- /dev/null +++ b/src/bitbangio.py @@ -0,0 +1,80 @@ +from adafruit_blinka import Lockable, agnostic + + +class I2C(Lockable): + def __init__(self, scl, sda, frequency=400000): + if agnostic.microcontroller == "stm32": + raise NotImplementedError("No software I2C on {}".format(agnostic.board)) + self.init(scl, sda, frequency) + + def init(self, scl, sda, frequency): + from machine import Pin + from machine import I2C as _I2C + self.deinit() + id = -1 # force bitbanging implementation - in future introspect platform if SDA/SCL matches hardware I2C + self._i2c = _I2C(id, Pin(scl.id), Pin(sda.id), freq=frequency) + + def deinit(self): + try: + del self._i2c + except AttributeError: + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.deinit() + + def scan(self): + return self._i2c.scan() + + def readfrom_into(self, address, buffer, start=0, end=None): + if start is not 0 or end is not None: + if end is None: + end = len(buffer) + buffer = memoryview(buffer)[start:end] + stop = True # remove for efficiency later + return self._i2c.readfrom_into(address, buffer, stop) + + def writeto(self, address, buffer, start=0, end=None, stop=True): + if start is not 0 or end is not None: + if end is None: + return self._i2c.writeto(address, memoryview(buffer)[start:], stop) + else: + return self._i2c.writeto(address, memoryview(buffer)[start:end], stop) + return self._i2c.writeto(address, buffer, stop) + + +# TODO untested, as actually busio.SPI was on tasklist https://github.com/adafruit/Adafruit_Micropython_Blinka/issues/2 :( +class SPI(Lockable): + def __init__(self, clock, MOSI=None, MISO=None): + from machine import SPI + self._spi = SPI(-1) + self._pins = (clock, MOSI, MISO) + + def configure(self, baudrate=100000, polarity=0, phase=0, bits=8): + from machine import SPI,Pin + if self._locked: + # TODO verify if _spi obj 'caches' sck, mosi, miso to avoid storing in _attributeIds (duplicated in busio) + # i.e. #init ignores MOSI=None rather than unsetting + self._spi.init( + baudrate=baudrate, + polarity=polarity, + phase=phase, + bits=bits, + firstbit=SPI.MSB, + sck=Pin(self._pins[0].id), + mosi=Pin(self._pins[1].id), + miso=Pin(self._pins[2].id)) + else: + raise RuntimeError("First call try_lock()") + + def write(self, buf): + return self._spi.write(buf) + + def readinto(self, buf): + return self.readinto(buf) + + def write_readinto(self, buffer_out, buffer_in): + return self.write_readinto(buffer_out, buffer_in) diff --git a/adafruit_blinka.py b/src/board.py similarity index 66% rename from adafruit_blinka.py rename to src/board.py index 94bee66..536bc6b 100644 --- a/adafruit_blinka.py +++ b/src/board.py @@ -20,15 +20,25 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_Blinka` -==================================================== - -.. todo:: Describe what the module does +`board` - Define ids for available pins +================================================= +Conditionally imports and re-exports a submodule, such as boards.esp8266 based on +platform introspection * Author(s): cefn """ -# imports +import gc +gc.collect() +from adafruit_blinka.agnostic import board +gc.collect() -__version__ = "0.0.0-auto.0" -__repo__ = "https://github.com/adafruit/Adafruit_Micropython_Blinka.git" +if board == "feather_huzzah": + from adafruit_blinka.board.feather_huzzah import * +elif board == "nodemcu": + from adafruit_blinka.board.nodemcu import * +elif board == "pyboard": + from adafruit_blinka.board.pyboard import * +else: + raise NotImplementedError("Board not supported") +gc.collect() diff --git a/src/busio.py b/src/busio.py new file mode 100644 index 0000000..0c6e296 --- /dev/null +++ b/src/busio.py @@ -0,0 +1,163 @@ +from adafruit_blinka import Enum, Lockable, agnostic + +class I2C(Lockable): + def __init__(self, scl, sda, frequency=400000): + self.init(scl, sda, frequency) + + def init(self, scl, sda, frequency): + self.deinit() + from machine import I2C as _I2C + from microcontroller.pin import i2cPorts + for portId, portScl, portSda in i2cPorts: + if scl == portScl and sda == portSda: + self._i2c = I2C(portId, mode=_I2C.MASTER, baudrate=frequency) + break + else: + raise NotImplementedError("No Hardware I2C on (scl,sda)={}\nValid UART ports".format( + (scl, sda), i2cPorts)) + + def deinit(self): + try: + del self._i2c + except AttributeError: + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.deinit() + + def scan(self): + return self._i2c.scan() + + def readfrom_into(self, address, buffer, start=0, end=None): + if start is not 0 or end is not None: + if end is None: + end = len(buffer) + buffer = memoryview(buffer)[start:end] + stop = True # remove for efficiency later + return self._i2c.readfrom_into(address, buffer, stop) + + def writeto(self, address, buffer, start=0, end=None, stop=True): + if start is not 0 or end is not None: + if end is None: + return self._i2c.writeto(address, memoryview(buffer)[start:], stop) + else: + return self._i2c.writeto(address, memoryview(buffer)[start:end], stop) + return self._i2c.writeto(address, buffer, stop) + + +class SPI(Lockable): + def __init__(self, clock, MOSI=None, MISO=None): + from microcontroller.pin import spiPorts + for portId, portSck, portMosi, portMiso in spiPorts: + if clock == portSck and MOSI == portMosi and MISO == portMiso: + self._spi = SPI(portId) + self._pins = (portSck, portMosi, portMiso) + break + else: + raise NotImplementedError( + "No Hardware SPI on (clock, MOSI, MISO)={}\nValid SPI ports:{}". + format((clock, MOSI, MISO), spiPorts)) + + def configure(self, baudrate=100000, polarity=0, phase=0, bits=8): + if self._locked: + from machine import Pin + # TODO check if #init ignores MOSI=None rather than unsetting, to save _pinIds attribute + self._spi.init( + baudrate=baudrate, + polarity=polarity, + phase=phase, + bits=bits, + firstbit=SPI.MSB, + sck=Pin(self._pins[0].id), + mosi=Pin(self._pins[1].id), + miso=Pin(self._pins[2].id) + ) + else: + raise RuntimeError("First call try_lock()") + + def deinit(self): + self._spi = None + self._pinIds = None + + def write(self, buf): + return self._spi.write(buf) + + def readinto(self, buf): + return self.readinto(buf) + + def write_readinto(self, buffer_out, buffer_in): + return self.write_readinto(buffer_out, buffer_in) + + +class UART(Lockable): + class Parity(Enum): + pass + + Parity.ODD = Parity() + Parity.EVEN = Parity() + + def __init__(self, + tx, + rx, + baudrate=9600, + bits=8, + parity=None, + stop=1, + timeout=1000, + receiver_buffer_size=64, + flow=None): + from machine import UART as _UART + from microcontroller.pin import uartPorts + + self.baudrate = baudrate + + if flow is not None: # default 0 + raise NotImplementedError( + "Parameter '{}' unsupported on {}".format( + "flow", agnostic.board)) + + # translate parity flag for Micropython + if parity is UART.Parity.ODD: + parity = 1 + elif parity is UART.Parity.EVEN: + parity = 0 + elif parity is None: + pass + else: + raise ValueError("Invalid parity") + + # check tx and rx have hardware support + for portId, portTx, portRx in uartPorts: # + if portTx == tx and portRx == rx: + self._uart = _UART( + portId, + baudrate, + bits=bits, + parity=parity, + stop=stop, + timeout=timeout, + read_buf_len=receiver_buffer_size + ) + break + else: + raise NotImplementedError( + "No Hardware UART on (tx,rx)={}\nValid UART ports".format( + (tx, rx), uartPorts)) + + def deinit(self): + self._uart = None + + def read(self, nbytes=None): + return self._uart.read(nbytes) + + def readinto(self, buf, nbytes=None): + return self._uart.readinto(buf, nbytes) + + def readline(self): + return self._uart.readline() + + def write(self, buf): + return self._uart.write(buf) diff --git a/src/digitalio.py b/src/digitalio.py new file mode 100644 index 0000000..45cada2 --- /dev/null +++ b/src/digitalio.py @@ -0,0 +1,122 @@ +from machine import Pin +from adafruit_blinka.agnostic import board as boardId +from adafruit_blinka import Enum, ContextManaged + + +class DriveMode(Enum): + PUSH_PULL = None + OPEN_DRAIN = None + + +DriveMode.PUSH_PULL = DriveMode() +DriveMode.OPEN_DRAIN = DriveMode() + + +class Direction(Enum): + INPUT = None + OUTPUT = None + + +Direction.INPUT = Direction() +Direction.OUTPUT = Direction() + + +class Pull(Enum): + UP = None + DOWN = None + #NONE=None + + +Pull.UP = Pull() +Pull.DOWN = Pull() + +#Pull.NONE = Pull() + + +class DigitalInOut(ContextManaged): + _pin = None + + def __init__(self, pin): + self._pin = Pin(pin.id) + self.direction = Direction.INPUT + + def switch_to_output(self, value=False, drive_mode=DriveMode.PUSH_PULL): + self.direction = Direction.OUTPUT + self.value = value + self.drive_mode = drive_mode + + def switch_to_input(self, pull=None): + self.direction = Direction.INPUT + self.pull = pull + + def deinit(self): + del self._pin + + @property + def direction(self): + return self.__direction + + @direction.setter + def direction(self, dir): + self.__direction = dir + if dir is Direction.OUTPUT: + self._pin.init(mode=Pin.OUT) + self.value = False + self.drive_mode = DriveMode.PUSH_PULL + elif dir is Direction.INPUT: + self._pin.init(mode=Pin.IN) + self.pull = None + else: + raise AttributeError("Not a Direction") + + @property + def value(self): + return self._pin.value() is 1 + + @value.setter + def value(self, val): + if self.direction is Direction.OUTPUT: + self._pin.value(1 if val else 0) + else: + raise AttributeError("Not an output") + + @property + def pull(self): + if self.direction is Direction.INPUT: + return self.__pull + else: + raise AttributeError("Not an input") + + @pull.setter + def pull(self, pul): + if self.direction is Direction.INPUT: + self.__pull = pul + if pul is Pull.UP: + self._pin.init(mode=Pin.IN, pull=Pin.PULL_UP) + elif pul is Pull.DOWN: + if hasattr(Pin, "PULL_DOWN"): + self._pin.init(mode=Pin.IN, pull=Pin.PULL_DOWN) + else: + raise NotImplementedError("{} unsupported on {}".format( + Pull.DOWN, boardId)) + elif pul is None: + self._pin.init(mode=Pin.IN, pull=None) + else: + raise AttributeError("Not a Pull") + else: + raise AttributeError("Not an input") + + @property + def drive_mode(self): + if self.direction is Direction.OUTPUT: + return self.__drive_mode # + else: + raise AttributeError("Not an output") + + @drive_mode.setter + def drive_mode(self, mod): + self.__drive_mode = mod + if mod is DriveMode.OPEN_DRAIN: + self._pin.init(mode=Pin.OPEN_DRAIN) + elif mod is DriveMode.PUSH_PULL: + self._pin.init(mode=Pin.OUT) diff --git a/src/microcontroller/__init__.py b/src/microcontroller/__init__.py new file mode 100644 index 0000000..c1b0027 --- /dev/null +++ b/src/microcontroller/__init__.py @@ -0,0 +1,26 @@ +from adafruit_blinka import Enum, agnostic + + +class Pin(Enum): + def __init__(self, id): + """Identifier for pin, referencing platform-specific pin id""" + self.id = id + + def __repr__(self): + import board + for key in dir(board): + if getattr(board, key) is self: + return "board.{}".format(key) + import microcontroller.pin as pin + for key in dir(pin): + if getattr(pin, key) is self: + return "microcontroller.pin.{}".format(key) + return repr(self) + + +if agnostic.microcontroller == "esp8266": + from adafruit_blinka.microcontroller.esp8266 import * +elif agnostic.microcontroller == "stm32": + from adafruit_blinka.microcontroller.stm32 import * +else: + raise NotImplementedError("Microcontroller not supported") \ No newline at end of file diff --git a/src/microcontroller/pin.py b/src/microcontroller/pin.py new file mode 100644 index 0000000..2b6199b --- /dev/null +++ b/src/microcontroller/pin.py @@ -0,0 +1,8 @@ +from adafruit_blinka import agnostic + +if agnostic.microcontroller == "esp8266": + from adafruit_blinka.microcontroller.esp8266.pin import * +elif agnostic.microcontroller == "stm32": + from adafruit_blinka.microcontroller.stm32.pin import * +else: + raise NotImplementedError("Microcontroller not supported") \ No newline at end of file diff --git a/test/libraries/bme280 b/test/libraries/bme280 new file mode 160000 index 0000000..3219f5c --- /dev/null +++ b/test/libraries/bme280 @@ -0,0 +1 @@ +Subproject commit 3219f5ccd5451f1e11a519d9d0d5407237f84d45 diff --git a/test/libraries/bno055 b/test/libraries/bno055 new file mode 160000 index 0000000..978c4c5 --- /dev/null +++ b/test/libraries/bno055 @@ -0,0 +1 @@ +Subproject commit 978c4c55270c5538dfa84117f426b7ace66ef017 diff --git a/test/libraries/bus_device b/test/libraries/bus_device new file mode 160000 index 0000000..eb7720b --- /dev/null +++ b/test/libraries/bus_device @@ -0,0 +1 @@ +Subproject commit eb7720b0d8dff377e687157e3f052f18a478e4a5 diff --git a/test/libraries/gps b/test/libraries/gps new file mode 160000 index 0000000..cd71268 --- /dev/null +++ b/test/libraries/gps @@ -0,0 +1 @@ +Subproject commit cd71268274f6a025ff2d944e0e1781b7435dd426 diff --git a/test/libraries/mma8451 b/test/libraries/mma8451 new file mode 160000 index 0000000..f389f1f --- /dev/null +++ b/test/libraries/mma8451 @@ -0,0 +1 @@ +Subproject commit f389f1f57f5567a8e23901346f264285f432a061 diff --git a/test/libraries/register b/test/libraries/register new file mode 160000 index 0000000..f86b454 --- /dev/null +++ b/test/libraries/register @@ -0,0 +1 @@ +Subproject commit f86b4549c1f8ec10682cb5e5148693dc29c85602 diff --git a/test/libraries/rfm69 b/test/libraries/rfm69 new file mode 160000 index 0000000..1abb4f7 --- /dev/null +++ b/test/libraries/rfm69 @@ -0,0 +1 @@ +Subproject commit 1abb4f7ce6fa5513c4e9c7dc280c1c2c28a7d062 diff --git a/test/scripts/upload_feather_huzzah_circuitpython_put.sh b/test/scripts/upload_feather_huzzah_circuitpython_put.sh new file mode 100755 index 0000000..146dfb9 --- /dev/null +++ b/test/scripts/upload_feather_huzzah_circuitpython_put.sh @@ -0,0 +1,51 @@ +#!/bin/sh +PORT=/dev/ttyUSB0 + +export MPYCROSS=`realpath ../../../circuitpython_2.2.3/mpy-cross/mpy-cross` + +# switch to test sources +cd ../src +# create test source directories on board +find testing -type d | \ + grep -v -E "(^./.git.*|^./.idea|^./.vscode|__pycache__)" | \ + xargs -n1 -I {} sh -c "echo Creating directory {} ...; ampy --port ${PORT} mkdir --exists-okay {}" +# compile source .py files to .mpy +find . -type f -name '*.py' | \ + xargs -n1 -I {} sh -c "echo compiling {} ...; ${MPYCROSS} {}" +# upload bytecode .mpy files +find . -type f -name '*.mpy' | \ + xargs -n1 -I {} sh -c "echo uploading {} ...; ampy --port ${PORT} put {} {}" + +#switch to test libraries +cd ../libraries/ + +# Compile adafruit libraries to bytecode and upload +for SUBMODULE in `find . -mindepth 1 -maxdepth 1 -type d ` +do + cd ${SUBMODULE} + # create adafruit library directories on board + find . -mindepth 1 -type d | \ + grep -v -E "(^./.git.*|__pycache__|^./doc.*|^./example.*)" | \ + xargs -n1 -I {} sh -c "echo Creating directory {} ...; ampy --port ${PORT} mkdir --exists-okay {}" + # compile adafruit library .py files to .mpy + find . -type f -name '*.py' | \ + grep -v -E "(^./conf.py|^./docs/conf.py|^./setup.py|^./example.*)" | \ + xargs -n1 -I {} sh -c "echo compiling {} ...; ${MPYCROSS} {}" + # upload adafruit library .mpy files + find . -type f -name '*.mpy' | \ + xargs -n1 -I {} sh -c "echo uploading {} ...; ampy --port ${PORT} put {} {}" + cd ../ +done + +# switch to adafruit_blinka source +cd ../../src + +# create adafruit_blinka agnostic package for cross-platform logic +ampy --port $PORT mkdir --exists-okay adafruit_blinka +ampy --port $PORT mkdir --exists-okay adafruit_blinka/agnostic +# upload agnostic.mpy for platform detection +$MPYCROSS adafruit_blinka/agnostic/__init__.py +ampy --port $PORT put adafruit_blinka/agnostic/__init__.mpy adafruit_blinka/agnostic/__init__.mpy +# upload time.mpy for time logic +$MPYCROSS adafruit_blinka/agnostic/time.py +ampy --port $PORT put adafruit_blinka/agnostic/time.mpy adafruit_blinka/agnostic/time.mpy diff --git a/test/scripts/upload_feather_huzzah_micropython_put.sh b/test/scripts/upload_feather_huzzah_micropython_put.sh new file mode 100755 index 0000000..38ed597 --- /dev/null +++ b/test/scripts/upload_feather_huzzah_micropython_put.sh @@ -0,0 +1,51 @@ +#!/bin/sh +PORT=/dev/ttyUSB0 + +export MPYCROSS=`realpath ../../../micropython/mpy-cross/mpy-cross` + +# switch to test sources +cd ../src +# create test source directories on board +find testing -type d | \ + grep -v -E "(^./.git.*|^./.idea|^./.vscode|__pycache__)" | \ + xargs -n1 -I {} sh -c "echo Creating directory {} ...; ampy --port ${PORT} mkdir --exists-okay {}" +# compile source .py files to .mpy +find . -type f -name '*.py' | \ + xargs -n1 -I {} sh -c "echo compiling {} ...; ${MPYCROSS} {}" +# upload bytecode .mpy files +find . -type f -name '*.mpy' | \ + xargs -n1 -I {} sh -c "echo uploading {} ...; ampy --port ${PORT} put {} {}" + +#switch to test libraries +cd ../libraries/ + +# Compile adafruit libraries to bytecode and upload +for SUBMODULE in `find . -mindepth 1 -maxdepth 1 -type d ` +do + cd ${SUBMODULE} + # create adafruit library directories on board + find . -mindepth 1 -type d | \ + grep -v -E "(^./.git.*|__pycache__|^./doc.*|^./example.*)" | \ + xargs -n1 -I {} sh -c "echo Creating directory {} ...; ampy --port ${PORT} mkdir --exists-okay {}" + # compile adafruit library .py files to .mpy + find . -type f -name '*.py' | \ + grep -v -E "(^./conf.py|^./docs/conf.py|^./setup.py|^./example.*)" | \ + xargs -n1 -I {} sh -c "echo compiling {} ...; ${MPYCROSS} {}" + # upload adafruit library .mpy files + find . -type f -name '*.mpy' | \ + xargs -n1 -I {} sh -c "echo uploading {} ...; ampy --port ${PORT} put {} {}" + cd ../ +done + +# switch to adafruit_blinka source +cd ../../src + +find . -mindepth 1 -type d | \ + grep -v -E "(^./.git.*|__pycache__)" | \ + xargs -n1 -I {} sh -c "echo Creating directory {} ...; ampy --port ${PORT} mkdir --exists-okay {}" +# compile adafruit blinka .py files to .mpy +find . -type f -name '*.py' | \ + xargs -n1 -I {} sh -c "echo compiling {} ...; ${MPYCROSS} {}" +# upload adafruit blinka .mpy files +find . -type f -name '*.mpy' | \ + xargs -n1 -I {} sh -c "echo uploading {} ...; ampy --port ${PORT} put {} {}" \ No newline at end of file diff --git a/test/scripts/upload_pyboard_micropython_cp.sh b/test/scripts/upload_pyboard_micropython_cp.sh new file mode 100755 index 0000000..a565e6a --- /dev/null +++ b/test/scripts/upload_pyboard_micropython_cp.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +export PORT="/dev/ttyUSB0" +export MPYCROSS=`realpath ../../../micropython/mpy-cross/mpy-cross` +export COPY="cp --parents " +export ROOT="/media/cefn/PYBFLASH/" + + +# switch to test sources +cd ../src +# compile source .py files to .mpy +find . -type f -name '*.py' | \ + xargs -n1 -I {} sh -c "echo compiling {} ...; ${MPYCROSS} {}" +# upload bytecode .mpy files +find ./ -type f -name '*.mpy' | \ + sed "s|^\./||" | \ + xargs -n1 -I {} sh -c "echo uploading {} ...; ${COPY} {} ${ROOT}" + +#switch to test libraries +cd ../libraries/ + +# Compile adafruit libraries to bytecode and upload +for SUBMODULE in gps # `find . -mindepth 1 -maxdepth 1 -type d ` +do + cd ${SUBMODULE} + # compile adafruit library .py files to .mpy + find . -type f -name '*.py' | \ + grep -v -E "(^./conf.py|^./docs/conf.py|^./setup.py|^./example.*)" | \ + xargs -n1 -I {} sh -c "echo compiling {} ...; ${MPYCROSS} {}" + # upload adafruit library .mpy files + find ./ -type f -name '*.mpy' | \ + sed "s|^\./||" | \ + xargs -n1 -I {} sh -c "echo uploading {} ...; ${COPY} {} ${ROOT}" + cd ../ +done + +# switch to adafruit_blinka source +cd ../../src + +# compile adafruit blinka .py files to .mpy +find . -type f -name '*.py' | \ + xargs -n1 -I {} sh -c "echo compiling {} ...; ${MPYCROSS} {}" +# upload adafruit blinka .mpy files +find ./ -type f -name '*.mpy' | \ + sed "s|^\./||" | \ + xargs -n1 -I {} sh -c "echo uploading {} ...; ${COPY} {} ${ROOT}" diff --git a/test/src/testing/__init__.py b/test/src/testing/__init__.py new file mode 100644 index 0000000..d8c29fb --- /dev/null +++ b/test/src/testing/__init__.py @@ -0,0 +1,104 @@ +# mitigate heap fragmentation issues by pre-loading major libraries +import gc +gc.collect() +import unittest +gc.collect() + +def yes_no(q, default=True): + a = input(q + " (Y/n)?" if default else " (y/N)?") + a=a.lower() + if a == '': + return default + elif a == "n": + a = False + elif a == "y": + a = True + return a + +def multi_choice(q, choices, defaultPos=None): + if defaultPos is not None: + print("{} [{}]?".format(q, defaultPos)) + else: + print(q + "?") + for pos, choice in enumerate(choices): + print("{}) {}".format(pos, choice)) + a = input() + a=a.lower() + try: + if a == '': + a = defaultPos + else: + a = int(a) + return choices[a] + except Exception as e: + print(e) + return None + +def await_true(name, fun, interval=0, patience=60): + from adafruit_blinka.agnostic.time import sleep, monotonic + print("Waiting {} sec until {} (CTRL+C give up)".format(patience, name)) + + deadline = monotonic() + patience + try: + while deadline - monotonic() > 0: + if fun(): + return True + else: + sleep(interval) + return False + except KeyboardInterrupt: + return False + + +def test_module(module, runner=None): + import unittest + if runner is None: + runner = unittest.TestRunner() + suite = unittest.TestSuite() + for key in dir(module): + val = getattr(module, key) + try: + if issubclass(val, unittest.TestCase): + suite.addTest(val) + except: + pass + return runner.run(suite) + +def test_module_name(absolute, runner=None): + try: + print("Suite begin: {}".format(absolute)) + module=__import__(absolute) + relatives = absolute.split(".") + if len(relatives) > 1: + for relative in relatives[1:]: + module = getattr(module, relative) + return test_module(module, runner) + finally: + print("Suite end: {}".format(absolute)) + +def test_interactive(*module_names): + for module_name in module_names: + if yes_no("Run suite {}".format(module_name)): + gc.collect() + test_module_name(module_name) + + +def test_prepare(casetype): + case = casetype() + case.setUp() + + +def main(): + """ + moduleNames = ["testing.implementation.universal.digitalio",] + if agnostic.implementation == "micropython": + moduleNames.extend([ "testing.implementation.micropython.digitalio",]) + + """ + moduleNames = ["testing.implementation.universal.bitbangio"] + + unittest.raiseException = True # terminates with stack information on userspace Exception + unittest.raiseBaseException = True # terminates with stack information on system Exception + test_interactive(*moduleNames) + +gc.collect() \ No newline at end of file diff --git a/test/src/testing/adafruit_blinka.py b/test/src/testing/adafruit_blinka.py new file mode 100644 index 0000000..f27abb5 --- /dev/null +++ b/test/src/testing/adafruit_blinka.py @@ -0,0 +1,52 @@ +import unittest + + +class TestEnum(unittest.TestCase): + """ + Verifies the repl() and str() behaviour of an example Enum + Enums represent configuration values such as digitalio.Direction, digitalio.Pull etc. + """ + + def setUp(self): + """Create an example Enum, mocking __module__ and __qualname__""" + import adafruit_blinka + class Cls(adafruit_blinka.Enum): + pass + Cls.one = Cls() + Cls.two = Cls() + # class refs would be implicitly populated correctly in a real module + Cls.__module__ = "ho.hum" + Cls.__qualname__ = "Example" + self.Cls = Cls + + + def test_iteritems(self): + """A subtype of Enum can list all attributes of its own type""" + items = list(self.Cls.iteritems()) + self.assertEqual( items, [("one",self.Cls.one),("two",self.Cls.two),]) + + + def test_repr(self): + """A repr() call on an Enum gives its fully-qualified name""" + name = "one" + actual = repr(getattr(self.Cls, name)) + expected = "{}.{}.{}".format(self.Cls.__module__, self.Cls.__qualname__, name) + self.assertEqual( actual, expected) + + + def test_str(self): + """A str() call on an Enum performs the same as repr()""" + self.assertEqual(str(self.Cls.one), repr(self.Cls.one)) + +class TestDigitalInOut(unittest.TestCase): + + + def test_context_manager(self): + import digitalio + from testing.board import default_pin + """Deinitialisation is triggered by __exit__() and should dispose machine.pin reference""" + dio = digitalio.DigitalInOut(default_pin) + self.assertIsNotNone(dio._pin) + with dio: + pass + self.assertIsNone(dio._pin) diff --git a/test/src/testing/board/__init__.py b/test/src/testing/board/__init__.py new file mode 100644 index 0000000..03f6f31 --- /dev/null +++ b/test/src/testing/board/__init__.py @@ -0,0 +1,24 @@ +"""Configuration of testing fixtures depending on the board layout""" +from adafruit_blinka import agnostic + +import board + +if agnostic.board == "feather_m0_express": + default_pin = board.D5 + led_pin = board.D13 + led_hardwired = True + led_inverted = False +elif agnostic.board == "feather_huzzah": + default_pin = board.GPIO4 + led_pin = board.GPIO0 # red led + led_hardwired = True + led_inverted = True +elif agnostic.board == "pyboard": + default_pin = board.X1 + led_pin = board.LED_BLUE + led_hardwired = True + led_inverted = False + uartTxId = "B6" + uartRXId = "B7" +else: + raise NotImplementedError("Board not supported") \ No newline at end of file diff --git a/test/src/testing/board/i2c.py b/test/src/testing/board/i2c.py new file mode 100644 index 0000000..0109560 --- /dev/null +++ b/test/src/testing/board/i2c.py @@ -0,0 +1,7 @@ +from adafruit_blinka import agnostic +if agnostic.board in ("feather_m0_express", "feather_huzzah"): + from bitbangio import I2C +elif agnostic.board == "pyboard": + from busio import I2C +else: + raise NotImplementedError("Board not supported") \ No newline at end of file diff --git a/test/src/testing/microcontroller/__init__.py b/test/src/testing/microcontroller/__init__.py new file mode 100644 index 0000000..deb21aa --- /dev/null +++ b/test/src/testing/microcontroller/__init__.py @@ -0,0 +1,8 @@ +from adafruit_blinka.agnostic import microcontroller + +if microcontroller == "esp8266": + pin_count = 10 +elif microcontroller == "samd21": + pin_count = 38 +else: + raise NotImplementedError("Microcontroller not supported") diff --git a/test/src/testing/universal/__init__.py b/test/src/testing/universal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/src/testing/universal/digitalio.py b/test/src/testing/universal/digitalio.py new file mode 100644 index 0000000..78db5cb --- /dev/null +++ b/test/src/testing/universal/digitalio.py @@ -0,0 +1,85 @@ +import unittest +from testing import yes_no, await_true +from testing.board import led_pin, default_pin, led_hardwired, led_inverted +from digitalio import * + +class TestDigitalInOut(unittest.TestCase): + + def test_default(self): + """DigitalInOut is input with no pull when constructed""" + with DigitalInOut(default_pin) as dio: + self.assertEqual(dio.direction, Direction.INPUT) + self.assertEqual(dio.pull, None) + + def test_switch_to_output(self): + """Default configuration of switch_to_output is respected""" + with DigitalInOut(default_pin) as dio: + dio.switch_to_output() + self.assertEqual(dio.direction, Direction.OUTPUT) + self.assertEqual(dio.value, False) + self.assertEqual(dio.drive_mode, DriveMode.PUSH_PULL) + + def test_switch_to_input(self): + """Default configuration of switch_to_input is respected""" + with DigitalInOut(default_pin) as dio: + dio.switch_to_output() # starts as input anyway + dio.switch_to_input() + self.assertEqual(dio.direction, Direction.INPUT) + self.assertEqual(dio.pull, None) + + +class TestDigitalInOutInteractive(unittest.TestCase): + + def test_blink(self): + """LED blinks when proper attributes set""" + print() + from adafruit_blinka.agnostic import sleep + if not(led_hardwired) and not(yes_no("Is LED wired to {}".format(led_pin))): + return # test trivially passed + with DigitalInOut(led_pin) as led: + led.direction = Direction.OUTPUT + # should now be OUT, PUSH_PULL, value=0, and LED should light + led.value = False if led_inverted else True + self.assertTrue(yes_no("Is LED lit")) + print("Winking LED...") + for count in range(2): + led.value = not(led.value) + sleep(0.5) + led.value = not(led.value) + sleep(0.5) + self.assertTrue(yes_no("Did LED wink twice")) + + def test_button_pull_up(self): + print() + """Pull-up button configured and detected""" + with DigitalInOut(default_pin) as button: + #button.direction = Direction.INPUT # implied + try: + button.pull = Pull.UP + except NotImplementedError as e: + print(e) + return # pull unsupported, test trivially passed + except Exception as e: + print(e) + return # pull unsupported, test trivially passed + if yes_no("Is Button wired from {} to GND".format(default_pin)): + self.assertTrue(button.value == True) + self.assertTrue(await_true("button pressed", lambda: button.value == False)) + + def test_button_pull_down(self): + print() + """Pull-down button configured and detected""" + with DigitalInOut(default_pin) as button: + #button.direction = Direction.INPUT # implied + try: + button.pull = Pull.DOWN + except NotImplementedError as e: + print(e) + return # pull unsupported, test trivially passed + except Exception as e: + print(e) + return # pull unsupported, test trivially passed + if (yes_no("Is Button wired from {} to VCC".format(default_pin))): + self.assertTrue(button.value == False) + self.assertTrue(await_true("button pressed", lambda: button.value == True)) + diff --git a/test/src/testing/universal/i2c.py b/test/src/testing/universal/i2c.py new file mode 100644 index 0000000..f6eaf25 --- /dev/null +++ b/test/src/testing/universal/i2c.py @@ -0,0 +1,92 @@ +import gc +from testing import yes_no +gc.collect() +from unittest import TestCase +gc.collect() +from testing.board.i2c import I2C +gc.collect() + +class TestBME280Interactive(TestCase): + + def test_read_value(self): + + import board + gc.collect() + import adafruit_bme280 + gc.collect() + + if not(yes_no("Is BME280 wired to SCL {} SDA {}".format(board.SCL, board.SDA))): + return # test trivially passed + + i2c = I2C(board.SCL, board.SDA) + bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c) + temperature = bme280.temperature + humidity = bme280.humidity + pressure = bme280.pressure + altitude = bme280.altitude + self.assertTrue(type(temperature) is float ) + self.assertTrue(type(humidity) is float ) + self.assertTrue(type(pressure) is float ) + self.assertTrue(type(altitude) is float ) + + self.assertTrue( -50 <= temperature <= 50) + self.assertTrue( 0 <= humidity <= 100) + self.assertTrue( 900 <= pressure <= 1100) + self.assertTrue( -1000 <= altitude <= 9,848) + + +class TestMMA8451Interactive(TestCase): + + def test_read_value(self): + import math + gc.collect() + import board + gc.collect() + + if not(yes_no("Is MMA8451 wired to SCL {} SDA {} and held still".format(board.SCL, board.SDA))): + return # test trivially passed + # from https://github.com/adafruit/Adafruit_CircuitPython_MMA8451/blob/29e31a0bb836367bc73763b83513105252b7b264/examples/simpletest.py + import adafruit_mma8451 + i2c = I2C(board.SCL, board.SDA) + sensor = adafruit_mma8451.MMA8451(i2c) + + x, y, z = sensor.acceleration + absolute = math.sqrt(x**2 + y**2 + z**2) + self.assertTrue(9 <=absolute <= 11, "Not earth gravity") + + orientation = sensor.orientation + self.assertTrue(orientation in ( + adafruit_mma8451.PL_PUF, + adafruit_mma8451.PL_PUB, + adafruit_mma8451.PL_PDF, + adafruit_mma8451.PL_PDB, + adafruit_mma8451.PL_LRF, + adafruit_mma8451.PL_LRB, + adafruit_mma8451.PL_LLF, + adafruit_mma8451.PL_LLB, + )) + +class TestBNO055Interactive(TestCase): + + def test_read_value(self): + """ + Access all sensor values as per + https://github.com/adafruit/Adafruit_CircuitPython_BNO055/blob/bdf6ada21e7552c242bc470d4d2619b480b4c574/examples/values.py + Note I have not successfully run this test. Possibly a hardware issue with module I have. + See https://forums.adafruit.com/viewtopic.php?f=60&t=131665 + """ + import board + gc.collect() + import adafruit_bno055 + gc.collect() + i2c = I2C(board.SCL, board.SDA) + sensor = adafruit_bno055.BNO055(i2c) + + self.assertTrue(9 <= sensor.gravity <= 11) + self.assertTrue(sensor.temperature != 0) + self.assertTrue(sensor.acceleration != (0,0,0)) + self.assertTrue(sensor.magnetometer != (0,0,0)) + self.assertTrue(sensor.gyroscope != (0,0,0)) + self.assertTrue(sensor.quaternion != (0,0,0,0)) + sensor.euler + sensor.linear_acceleration diff --git a/test/src/testing/universal/microcontroller.py b/test/src/testing/universal/microcontroller.py new file mode 100644 index 0000000..5df1f9b --- /dev/null +++ b/test/src/testing/universal/microcontroller.py @@ -0,0 +1,14 @@ +import unittest + +class TestMicrocontrollerModule(unittest.TestCase): + + def test_pins_exist(self): + """The microcontroller module should contain pin references""" + import microcontroller + from microcontroller import pin + from testing.microcontroller import pin_count + entries = [getattr(pin, key) for key in dir(pin)] + # is this filter line needed? any other types valid in pin module? + entries = list(filter(lambda val: type(val) is microcontroller.Pin, entries)) + self.assertTrue(len(entries) > 0) + self.assertTrue(len(entries) == pin_count) \ No newline at end of file diff --git a/test/src/testing/universal/uart.py b/test/src/testing/universal/uart.py new file mode 100644 index 0000000..05a917f --- /dev/null +++ b/test/src/testing/universal/uart.py @@ -0,0 +1,38 @@ +import gc +from unittest import TestCase +from testing import await_true +gc.collect() + + +class TestGPSInteractive(TestCase): + + def test_read_value(self): + import adafruit_blinka + adafruit_blinka.patch_system() # needed before adafruit_gps imports time + + import microcontroller.pin + gc.collect() + import busio + gc.collect() + import adafruit_gps + gc.collect() + + # configure the last available UART (first uart often for REPL) + uartId, uartTx, uartRx = microcontroller.pin.uartPorts[0] + uart = busio.UART(uartTx, uartRx, baudrate=9600, timeout=3000) + + gps = adafruit_gps.GPS(uart) + + gps.send_command('PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') + gps.send_command('PMTK220,1000') + + def try_fix(): + gps.update() + return gps.has_fix + + await_true("GPS fix", try_fix) + + self.assertTrue(gps.satellites is not None) + self.assertTrue(-90 <= gps.latitude < 90) + self.assertTrue(-180 <= gps.longitude < 180) + diff --git a/test/src/unittest.py b/test/src/unittest.py new file mode 100644 index 0000000..aab8192 --- /dev/null +++ b/test/src/unittest.py @@ -0,0 +1,233 @@ +"""Based on https://raw.githubusercontent.com/micropython/micropython-lib/cfa1b9cce0c93a3115bbff3886c9bbcddd9e8047/unittest/unittest.py """ +import sys +class SkipTest(Exception): + pass + +raiseException = False +raiseBaseException = True + +class AssertRaisesContext: + + def __init__(self, exc): + self.expected = exc + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + if exc_type is None: + assert False, "%r not raised" % self.expected + if issubclass(exc_type, self.expected): + return True + return False + + +class TestCase: + + def fail(self, msg=''): + assert False, msg + + def assertEqual(self, x, y, msg=''): + if not msg: + msg = "%r vs (expected) %r" % (x, y) + assert x == y, msg + + def assertNotEqual(self, x, y, msg=''): + if not msg: + msg = "%r not expected to be equal %r" % (x, y) + assert x != y, msg + + def assertAlmostEqual(self, x, y, places=None, msg='', delta=None): + if x == y: + return + if delta is not None and places is not None: + raise TypeError("specify delta or places not both") + + if delta is not None: + if abs(x - y) <= delta: + return + if not msg: + msg = '%r != %r within %r delta' % (x, y, delta) + else: + if places is None: + places = 7 + if round(abs(y-x), places) == 0: + return + if not msg: + msg = '%r != %r within %r places' % (x, y, places) + + assert False, msg + + def assertNotAlmostEqual(self, x, y, places=None, msg='', delta=None): + if delta is not None and places is not None: + raise TypeError("specify delta or places not both") + + if delta is not None: + if not (x == y) and abs(x - y) > delta: + return + if not msg: + msg = '%r == %r within %r delta' % (x, y, delta) + else: + if places is None: + places = 7 + if not (x == y) and round(abs(y-x), places) != 0: + return + if not msg: + msg = '%r == %r within %r places' % (x, y, places) + + assert False, msg + + def assertIs(self, x, y, msg=''): + if not msg: + msg = "%r is not %r" % (x, y) + assert x is y, msg + + def assertIsNot(self, x, y, msg=''): + if not msg: + msg = "%r is %r" % (x, y) + assert x is not y, msg + + def assertIsNone(self, x, msg=''): + if not msg: + msg = "%r is not None" % x + assert x is None, msg + + def assertIsNotNone(self, x, msg=''): + if not msg: + msg = "%r is None" % x + assert x is not None, msg + + def assertTrue(self, x, msg=''): + if not msg: + msg = "Expected %r to be True" % x + assert x, msg + + def assertFalse(self, x, msg=''): + if not msg: + msg = "Expected %r to be False" % x + assert not x, msg + + def assertIn(self, x, y, msg=''): + if not msg: + msg = "Expected %r to be in %r" % (x, y) + assert x in y, msg + + def assertIsInstance(self, x, y, msg=''): + assert isinstance(x, y), msg + + def assertRaises(self, exc, func=None, *args, **kwargs): + if func is None: + return AssertRaisesContext(exc) + + try: + func(*args, **kwargs) + assert False, "%r not raised" % exc + except Exception as e: + if isinstance(e, exc): + return + raise + + + +def skip(msg): + def _decor(fun): + # We just replace original fun with _inner + def _inner(self): + raise SkipTest(msg) + return _inner + return _decor + +def skipIf(cond, msg): + if not cond: + return lambda x: x + return skip(msg) + +def skipUnless(cond, msg): + if cond: + return lambda x: x + return skip(msg) + + +class TestSuite: + def __init__(self): + self.tests = [] + def addTest(self, cls): + self.tests.append(cls) + +class TestRunner: + def run(self, suite): + res = TestResult() + for c in suite.tests: + run_class(c, res) + + print("Ran %d tests\n" % res.testsRun) + if res.failuresNum > 0 or res.errorsNum > 0: + print("FAILED (failures=%d, errors=%d)" % (res.failuresNum, res.errorsNum)) + else: + msg = "OK" + if res.skippedNum > 0: + msg += " (%d skipped)" % res.skippedNum + print(msg) + + return res + +class TestResult: + def __init__(self): + self.errorsNum = 0 + self.failuresNum = 0 + self.skippedNum = 0 + self.testsRun = 0 + + def wasSuccessful(self): + return self.errorsNum == 0 and self.failuresNum == 0 + +# TODO: Uncompliant +def run_class(c, test_result): + o = c() + set_up = getattr(o, "setUp", lambda: None) + tear_down = getattr(o, "tearDown", lambda: None) + for name in dir(o): + if name.startswith("test"): + print("%s (%s) ..." % (name, c.__qualname__), end="") + m = getattr(o, name) + set_up() + try: + test_result.testsRun += 1 + m() + print(" ok") + except SkipTest as e: + print(" skipped:", e.args[0]) + test_result.skippedNum += 1 + except Exception as e: # user exception + print(" FAIL") + if raiseException: + raise + else: + print(e) + test_result.failuresNum += 1 + continue + except BaseException as e: # system exception + print(" FAIL") + if raiseBaseException: + raise + else: + print(e) + test_result.failuresNum += 1 + continue + finally: + tear_down() + + +def main(module="__main__"): + def test_cases(m): + for tn in dir(m): + c = getattr(m, tn) + if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase): + yield c + + m = __import__(module) # changed to permit non-top-level testing modules + suite = TestSuite() + for c in test_cases(m): + suite.addTest(c) + runner = TestRunner() + result = runner.run(suite) \ No newline at end of file