]> Repositories - Adafruit_Blinka-hackapet.git/commitdiff
Merge pull request #8 from cefn/master
authorLimor "Ladyada" Fried <limor@ladyada.net>
Tue, 27 Feb 2018 17:34:16 +0000 (12:34 -0500)
committerGitHub <noreply@github.com>
Tue, 27 Feb 2018 17:34:16 +0000 (12:34 -0500)
PR now structure and approach somewhat stable

43 files changed:
.gitignore
.gitmodules [new file with mode: 0644]
README.rst
examples/index.md [new file with mode: 0644]
src/adafruit_blinka/__init__.py [new file with mode: 0644]
src/adafruit_blinka/agnostic/__init__.py [new file with mode: 0644]
src/adafruit_blinka/agnostic/time.py [new file with mode: 0644]
src/adafruit_blinka/board/__init__.py [moved from examples/blinka_simpletest.py with 100% similarity]
src/adafruit_blinka/board/feather_huzzah.py [new file with mode: 0644]
src/adafruit_blinka/board/nodemcu.py [new file with mode: 0644]
src/adafruit_blinka/board/pyboard.py [new file with mode: 0644]
src/adafruit_blinka/microcontroller/__init__.py [new file with mode: 0644]
src/adafruit_blinka/microcontroller/esp8266/__init__.py [moved from requirements.txt with 100% similarity]
src/adafruit_blinka/microcontroller/esp8266/pin.py [new file with mode: 0644]
src/adafruit_blinka/microcontroller/stm32/__init__.py [new file with mode: 0644]
src/adafruit_blinka/microcontroller/stm32/pin.py [new file with mode: 0644]
src/bitbangio.py [new file with mode: 0644]
src/board.py [moved from adafruit_blinka.py with 66% similarity]
src/busio.py [new file with mode: 0644]
src/digitalio.py [new file with mode: 0644]
src/microcontroller/__init__.py [new file with mode: 0644]
src/microcontroller/pin.py [new file with mode: 0644]
test/libraries/bme280 [new submodule]
test/libraries/bno055 [new submodule]
test/libraries/bus_device [new submodule]
test/libraries/gps [new submodule]
test/libraries/mma8451 [new submodule]
test/libraries/register [new submodule]
test/libraries/rfm69 [new submodule]
test/scripts/upload_feather_huzzah_circuitpython_put.sh [new file with mode: 0755]
test/scripts/upload_feather_huzzah_micropython_put.sh [new file with mode: 0755]
test/scripts/upload_pyboard_micropython_cp.sh [new file with mode: 0755]
test/src/testing/__init__.py [new file with mode: 0644]
test/src/testing/adafruit_blinka.py [new file with mode: 0644]
test/src/testing/board/__init__.py [new file with mode: 0644]
test/src/testing/board/i2c.py [new file with mode: 0644]
test/src/testing/microcontroller/__init__.py [new file with mode: 0644]
test/src/testing/universal/__init__.py [new file with mode: 0644]
test/src/testing/universal/digitalio.py [new file with mode: 0644]
test/src/testing/universal/i2c.py [new file with mode: 0644]
test/src/testing/universal/microcontroller.py [new file with mode: 0644]
test/src/testing/universal/uart.py [new file with mode: 0644]
test/src/unittest.py [new file with mode: 0644]

index 0dd8629dcd1cbe4e4be83174176002e74e674da4..cc5b9ac44a2918a5cf50f88461be14fdb8f7f428 100644 (file)
@@ -1,3 +1,5 @@
+*.mpy
+.idea
 __pycache__
 _build
 *.pyc
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..41bbf17
--- /dev/null
@@ -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
index 37d09da34225ecc6f2d9304c3fad47880ffbf08a..127131d434ec12a81a2c83b3c4dd97d0a160b003 100644 (file)
@@ -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 <https://github.com/cefn/Adafruit_Micropython_Blinka/tree/7fc1f8ab477124628a5afebbf6826005955805f9>`_),
+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 <https://github.com/cefn/Adafruit_Micropython_Blinka/issues/2#issuecomment-366713394>`_ .
+
 
 Contributing
 ============
diff --git a/examples/index.md b/examples/index.md
new file mode 100644 (file)
index 0000000..184ca33
--- /dev/null
@@ -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 (file)
index 0000000..08ec27e
--- /dev/null
@@ -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 (file)
index 0000000..85670bf
--- /dev/null
@@ -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 (file)
index 0000000..078b833
--- /dev/null
@@ -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/src/adafruit_blinka/board/feather_huzzah.py b/src/adafruit_blinka/board/feather_huzzah.py
new file mode 100644 (file)
index 0000000..60dd863
--- /dev/null
@@ -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 (file)
index 0000000..83e4ca8
--- /dev/null
@@ -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 (file)
index 0000000..4bf49de
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/src/adafruit_blinka/microcontroller/esp8266/pin.py b/src/adafruit_blinka/microcontroller/esp8266/pin.py
new file mode 100644 (file)
index 0000000..c37e053
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..3f46781
--- /dev/null
@@ -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 (file)
index 0000000..101bfff
--- /dev/null
@@ -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)
similarity index 66%
rename from adafruit_blinka.py
rename to src/board.py
index 94bee66d9616783b30f0e1463bab88fd16c59707..536bc6b761077ddd4aba9d627940b07b6c8b87ff 100644 (file)
 # 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 (file)
index 0000000..0c6e296
--- /dev/null
@@ -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 (file)
index 0000000..45cada2
--- /dev/null
@@ -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 (file)
index 0000000..c1b0027
--- /dev/null
@@ -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 (file)
index 0000000..2b6199b
--- /dev/null
@@ -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 (submodule)
index 0000000..3219f5c
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 3219f5ccd5451f1e11a519d9d0d5407237f84d45
diff --git a/test/libraries/bno055 b/test/libraries/bno055
new file mode 160000 (submodule)
index 0000000..978c4c5
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 978c4c55270c5538dfa84117f426b7ace66ef017
diff --git a/test/libraries/bus_device b/test/libraries/bus_device
new file mode 160000 (submodule)
index 0000000..eb7720b
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit eb7720b0d8dff377e687157e3f052f18a478e4a5
diff --git a/test/libraries/gps b/test/libraries/gps
new file mode 160000 (submodule)
index 0000000..cd71268
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit cd71268274f6a025ff2d944e0e1781b7435dd426
diff --git a/test/libraries/mma8451 b/test/libraries/mma8451
new file mode 160000 (submodule)
index 0000000..f389f1f
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit f389f1f57f5567a8e23901346f264285f432a061
diff --git a/test/libraries/register b/test/libraries/register
new file mode 160000 (submodule)
index 0000000..f86b454
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit f86b4549c1f8ec10682cb5e5148693dc29c85602
diff --git a/test/libraries/rfm69 b/test/libraries/rfm69
new file mode 160000 (submodule)
index 0000000..1abb4f7
--- /dev/null
@@ -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 (executable)
index 0000000..146dfb9
--- /dev/null
@@ -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 (executable)
index 0000000..38ed597
--- /dev/null
@@ -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 (executable)
index 0000000..a565e6a
--- /dev/null
@@ -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 (file)
index 0000000..d8c29fb
--- /dev/null
@@ -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 (file)
index 0000000..f27abb5
--- /dev/null
@@ -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 (file)
index 0000000..03f6f31
--- /dev/null
@@ -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 (file)
index 0000000..0109560
--- /dev/null
@@ -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 (file)
index 0000000..deb21aa
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/test/src/testing/universal/digitalio.py b/test/src/testing/universal/digitalio.py
new file mode 100644 (file)
index 0000000..78db5cb
--- /dev/null
@@ -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 (file)
index 0000000..f6eaf25
--- /dev/null
@@ -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 (file)
index 0000000..5df1f9b
--- /dev/null
@@ -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 (file)
index 0000000..05a917f
--- /dev/null
@@ -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 (file)
index 0000000..aab8192
--- /dev/null
@@ -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