Repository: ManiacalLabs/BiblioPixel
Pull request #230 (2017-06-19 01:42:11)

Full rework of SPI drivers
- Consolidation of SPI drivers via single metaclass
- LED Type and interface type determined by enum
- Cleaner standard SPI param handling
- unit tests updated
- WS281X driver is still of dubious functionality
- Fixed completely broken APA102 and WS2801 drivers

Yeah, yeah... I did it anyways. A tiny bit more complexity for a *much* simpler driver model for SPI LEDs that is designed to mirror how the AllPixel driver works.
I fully expect comments, but I'm fully committed to keeping this as it's better than what we had.
One thing to note is that you can still actually use all the drivers completely stand-alone (without the SPI() metaclass). I have not done this yet, but I could very easily bring back the same driver paths by importing them into bibliopixel.drivers.__init__.py --- That way, ```from bibliopixel.drivers.APA102 import APA102``` would still work as it did before.

I've still not gotten the WS281X driver from @TuxCoder to work. We may need to mark that as experimental as we are unsure of the reason why it doesn't work yet.

rec: I relented on everything except the metaclass.  If you can give me one code sample where the metaclass is somehow better, I'll relent on that even.  :-D
rec: So I'm out of time for today to review this further!

I think we should check it in with the metaclass but you have to promise me
a chance for a rebuttal with a separate pull request.

(I still believe that my snap judgement that it's a mixin
<https://en.wikipedia.org/wiki/Mixin> issue and I'm pretty sure that
there's a good solution accordingly...)

On Tue, Jun 20, 2017 at 1:11 PM, Adam Haile <notifications@github.com>
wrote:

> *@adammhaile* commented on this pull request.
> ------------------------------
>
> In bibliopixel/drivers/SPI/driver.py
> <https://github.com/ManiacalLabs/BiblioPixel/pull/230#discussion_r122946176>
> :
>
> > +    WS281X = 2
> +    WS2812 = 2
> +    WS2812B = 2
> +    NEOPIXEL = 2
> +    LPD8806 = 3
> +
> +
> +SPI_DRIVERS = [
> +    APA102,
> +    WS2801,
> +    WS281XSPI,
> +    LPD8806
> +]
> +
> +
> +class SPI(DriverBase):
>
> How does reflection know that a regular old function is a driver? Show me.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/ManiacalLabs/BiblioPixel/pull/230#discussion_r122946176>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAPdsndCkqtXV1Jyiujf8TJgK6FZhO7bks5sF6jXgaJpZM4N9rE1>
> .
>



-- 
     /t

*https://tom.ritchford.com <https://tom.ritchford.com>*
*https://tom.swirly.com <https://tom.swirly.com>*


Patch:
diff --git a/bibliopixel/drivers/APA102.py b/bibliopixel/drivers/APA102.py
deleted file mode 100644
index dfd500e..0000000
--- a/bibliopixel/drivers/APA102.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from . channel_order import ChannelOrder
-from . spi_driver_base import DriverSPIBase
-
-
-class APA102(DriverSPIBase):
-    """Main driver for APA102 based LED strips on devices like the Raspberry Pi and BeagleBone"""
-
-    def __init__(self, num, c_order=ChannelOrder.RGB, **kwargs):
-        super().__init__(num, c_order=c_order, **kwargs)
-
-        # APA102 requires latch bytes at the end
-        self._latchBytes = (int(num / 64.0) + 1)
-
-    def _render(self):
-        super()._render()
-        newBuf = [0xFF] * (self.bufByteCount() + self.numLEDs)
-        newBuf[1::4] = self._buf[0::3]
-        newBuf[2::4] = self._buf[1::3]
-        newBuf[3::4] = self._buf[2::3]
-        self._buf[:] = [0, 0, 0, 0] + newBuf
-        self._buf.extend([0xFF, 0, 0, 0] * self._latchBytes)
-
-
-# This is DEPRECATED.
-DriverAPA102 = APA102
diff --git a/bibliopixel/drivers/SPI/APA102.py b/bibliopixel/drivers/SPI/APA102.py
new file mode 100644
index 0000000..10ce124
--- /dev/null
+++ b/bibliopixel/drivers/SPI/APA102.py
@@ -0,0 +1,24 @@
+from ... import gamma
+from .. channel_order import ChannelOrder
+from . base import SPIBase
+
+
+class APA102(SPIBase):
+    """Driver for APA102 based LED strips on devices like the Raspberry Pi and BeagleBone"""
+
+    def __init__(self, num, gamma=gamma.APA102, **kwargs):
+        super().__init__(num, gamma=gamma, **kwargs)
+
+        # APA102 requires latch bytes at the end
+        self._latch_bytes = [0xFF, 0, 0, 0] * ((num // 64) + 1)
+
+    def _compute_packet(self):
+        self._render()
+        # TODO: Do all this surgery in place instead
+        newBuf = [0xFF] * (self.bufByteCount() + self.numLEDs)
+        newBuf[1::4] = self._buf[0::3]
+        newBuf[2::4] = self._buf[1::3]
+        newBuf[3::4] = self._buf[2::3]
+        newBuf = [0, 0, 0, 0] + newBuf
+        newBuf.extend(self._latch_bytes)
+        self._packet = newBuf
diff --git a/bibliopixel/drivers/LPD8806.py b/bibliopixel/drivers/SPI/LPD8806.py
similarity index 51%
rename from bibliopixel/drivers/LPD8806.py
rename to bibliopixel/drivers/SPI/LPD8806.py
index 161be91..03d224a 100644
--- a/bibliopixel/drivers/LPD8806.py
+++ b/bibliopixel/drivers/SPI/LPD8806.py
@@ -1,16 +1,14 @@
-from .. import gamma
-from . channel_order import ChannelOrder
-from . spi_driver_base import DriverSPIBase
+from ... import gamma
+from .. channel_order import ChannelOrder
+from . base import SPIBase
 
 
-class LPD8806(DriverSPIBase):
+class LPD8806(SPIBase):
     """Main driver for LPD8806 based LED strips on devices like the Raspberry Pi
        and BeagleBone."""
 
-    def __init__(self, num,
-                 c_order=ChannelOrder.RGB,
-                 spi_speed=2, gamma=gamma.LPD8806, **kwargs):
-        super().__init__(num, c_order=c_order, spi_speed=spi_speed, gamma=gamma, **kwargs)
+    def __init__(self, num, gamma=gamma.LPD8806, **kwargs):
+        super().__init__(num, gamma=gamma, **kwargs)
 
         # LPD8806 requires latch bytes at the end
         self._latchBytes = (self.numLEDs + 31) // 32
@@ -19,7 +17,3 @@ class LPD8806(DriverSPIBase):
 
     # LPD8806 requires gamma correction and only supports 7-bits per channel
     # running each value through gamma will fix all of this.
-
-
-# This is DEPRECATED.
-DriverLPD8806 = LPD8806
diff --git a/bibliopixel/drivers/SPI/WS2801.py b/bibliopixel/drivers/SPI/WS2801.py
new file mode 100644
index 0000000..aed8847
--- /dev/null
+++ b/bibliopixel/drivers/SPI/WS2801.py
@@ -0,0 +1,13 @@
+from .. channel_order import ChannelOrder
+from . base import SPIBase
+from ... import gamma
+
+
+class WS2801(SPIBase):
+    """Main driver for WS2801 based LED strips on devices like the Raspberry Pi and BeagleBone"""
+
+    def __init__(self, num, gamma=gamma.WS2801, spi_speed=1, **kwargs):
+        if not (0 < spi_speed <= 1):
+            raise ValueError(
+                'WS2801 requires an SPI speed of 1MHz but was set to {}MHz'.format(spi_speed))
+        super().__init__(num, gamma=gamma, spi_speed=spi_speed, **kwargs)
diff --git a/bibliopixel/drivers/WS281X.py b/bibliopixel/drivers/SPI/WS281X.py
similarity index 63%
rename from bibliopixel/drivers/WS281X.py
rename to bibliopixel/drivers/SPI/WS281X.py
index 834cd1a..6b06138 100644
--- a/bibliopixel/drivers/WS281X.py
+++ b/bibliopixel/drivers/SPI/WS281X.py
@@ -1,18 +1,26 @@
-from . spi_driver_base import DriverSPIBase, ChannelOrder
-from .. import gamma as _gamma
+from .. channel_order import ChannelOrder
+from . base import SPIBase
+from ... import gamma as _gamma
+from . import interfaces
 
 
-class WS281XSPI(DriverSPIBase):
+MAX_PIXELS = 455
+
+
+class WS281X(SPIBase):
     """
     SPI driver for WS2812(b) based LED strips on devices like
     Raspberry Pi, OrangePi, BeagleBone,..
     """
 
-    def __init__(self, num, spi_speed=3.2, **kwargs):
+    def __init__(self, num, gamma=_gamma.WS2812, spi_speed=3.2, **kwargs):
         # WS281x need a base clock of ~1MHz with the encoding we need 3 times this clock
         # After testing 3.0 to 3.2 looks like a good value
-        super().__init__(num, c_order=ChannelOrder.GRB, spi_speed=spi_speed,
-                         gamma=_gamma.WS2812, **kwargs)
+        super().__init__(num, gamma=gamma, spi_speed=spi_speed, **kwargs)
+        if isinstance(self._interface, interfaces.SpiFileInterface):
+            raise ValueError('SPI File interface is unsupported by WS281X')
+        if num > MAX_PIXELS:
+            raise ValueError('WS2812X SPI driver only supports {} pixels max.'.format(MAX_PIXELS))
 
     # WS2812 requires gamma correction so we run it through gamma as the
     # channels are ordered
@@ -21,6 +29,7 @@ class WS281XSPI(DriverSPIBase):
     # 0b0  -> 0b100
     # 0b1  -> 0b110
     def _compute_packet(self):
+        # self._render()
         super()._compute_packet()
 
         # buffer for result
diff --git a/bibliopixel/drivers/SPI/__init__.py b/bibliopixel/drivers/SPI/__init__.py
new file mode 100644
index 0000000..a3a50bf
--- /dev/null
+++ b/bibliopixel/drivers/SPI/__init__.py
@@ -0,0 +1,2 @@
+from . driver import SPI, SPI_LEDTYPE
+from . interfaces import SPI_INTERFACES
diff --git a/bibliopixel/drivers/SPI/base.py b/bibliopixel/drivers/SPI/base.py
new file mode 100644
index 0000000..c75cc73
--- /dev/null
+++ b/bibliopixel/drivers/SPI/base.py
@@ -0,0 +1,27 @@
+from .. channel_order import ChannelOrder
+from .. driver_base import DriverBase
+from ... import gamma as _gamma
+from ... util.enum import resolve_enum
+from . import interfaces
+
+
+class SPIBase(DriverBase):
+    """Base driver for controling SPI devices on systems like the Raspberry Pi and BeagleBone"""
+    def __init__(self, num, dev='/dev/spidev0.0',
+                 interface='FILE', spi_speed=1,
+                 **kwargs):
+
+        super().__init__(num, **kwargs)
+
+        iface = resolve_enum(interfaces.SPI_INTERFACES, interface)
+        if iface >= len(interfaces._SPI_INTERFACES):
+            raise ValueError('{} is not a valid interface.'.format(interface))
+
+        self._interface = interfaces._SPI_INTERFACES[iface](dev=dev, spi_speed=spi_speed)
+
+    def _send_packet(self):
+        self._interface.send_packet(self._packet)
+
+    def _compute_packet(self):
+        self._render()
+        self._packet = self._interface.compute_packet(self._buf)
diff --git a/bibliopixel/drivers/SPI/driver.py b/bibliopixel/drivers/SPI/driver.py
new file mode 100644
index 0000000..c1fedc1
--- /dev/null
+++ b/bibliopixel/drivers/SPI/driver.py
@@ -0,0 +1,38 @@
+from .. channel_order import ChannelOrder
+from .. driver_base import DriverBase
+from ... util.enum import resolve_enum
+from . import interfaces
+from enum import IntEnum
+# SubDriver imports
+from . APA102 import APA102
+from . LPD8806 import LPD8806
+from . WS281X import WS281X
+from . WS2801 import WS2801
+
+
+class SPI_LEDTYPE(IntEnum):
+    APA102 = 0
+    DOTSTAR = 0
+    WS2801 = 1
+    WS281X = 2
+    WS2812 = 2
+    WS2812B = 2
+    NEOPIXEL = 2
+    LPD8806 = 3
+
+
+SPI_DRIVERS = [
+    APA102,
+    WS2801,
+    WS281X,
+    LPD8806
+]
+
+
+def SPI(type, num, **kwargs):
+    """Wrapper function for using SPI device drivers on systems like the Raspberry Pi and BeagleBone"""
+    ledtype = resolve_enum(SPI_LEDTYPE, type)
+    if ledtype >= len(SPI_DRIVERS):
+        raise ValueError('{} is not a valid LED type.'.format(type))
+
+    return SPI_DRIVERS[ledtype](num, **kwargs)
diff --git a/bibliopixel/drivers/SPI/errors.py b/bibliopixel/drivers/SPI/errors.py
new file mode 100644
index 0000000..f116b14
--- /dev/null
+++ b/bibliopixel/drivers/SPI/errors.py
@@ -0,0 +1,36 @@
+PERMISSION_ERROR = """Cannot access SPI device.
+Please see
+
+    https://github.com/maniacallabs/bibliopixel/wiki/SPI-Setup
+
+for details.
+"""
+
+CANT_FIND_ERROR = """Cannot find SPI device.
+Please see
+
+    https://github.com/maniacallabs/bibliopixel/wiki/SPI-Setup
+
+for details.
+"""
+
+CANT_IMPORT_PERIPHERY_ERROR = """
+Unable to import periphery. Please install:
+    pip install python-periphery
+
+Please see
+    https://github.com/maniacallabs/bibliopixel/wiki/SPI-Setup
+
+for details.
+"""
+
+BAD_FORMAT_ERROR = """
+When using py-spidev, `dev` must be in the format /dev/spidev*.*
+Please see https://github.com/maniacallabs/bibliopixel/wiki/SPI-Setup
+"""
+
+CANT_IMPORT_SPIDEV_ERROR = """
+Unable to import spidev. Please install:
+
+    pip install spidev
+"""
diff --git a/bibliopixel/drivers/spi_driver_base.py b/bibliopixel/drivers/SPI/interfaces.py
similarity index 63%
rename from bibliopixel/drivers/spi_driver_base.py
rename to bibliopixel/drivers/SPI/interfaces.py
index 0284765..d77a5e5 100644
--- a/bibliopixel/drivers/spi_driver_base.py
+++ b/bibliopixel/drivers/SPI/interfaces.py
@@ -1,8 +1,8 @@
-import math
 import os
-from . channel_order import ChannelOrder
-from . driver_base import DriverBase
-from .. import log
+import math
+from enum import IntEnum
+from . import errors
+from ... import log
 
 
 class SpiBaseInterface(object):
@@ -18,6 +18,11 @@ class SpiBaseInterface(object):
     def compute_packet(self, data):
         return data
 
+    def error(self, text):
+        msg = 'Error with dev: {}, spi_speed: {} - {}'.format(self._dev, self._spi_speed, text)
+        log.error(msg)
+        raise IOError(msg)
+
 
 class SpiFileInterface(SpiBaseInterface):
     """ using os open/write to send data"""
@@ -25,7 +30,7 @@ class SpiFileInterface(SpiBaseInterface):
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
         if not os.path.exists(self._dev):
-            error(CANT_FIND_ERROR)
+            self.error(errors.CANT_FIND_ERROR)
 
         self._spi = open(self._dev, 'wb')
 
@@ -46,13 +51,13 @@ class SpiPeripheryInterface(SpiBaseInterface):
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
         if not os.path.exists(self._dev):
-            error(CANT_FIND_ERROR)
+            self.error(errors.CANT_FIND_ERROR)
 
         try:
             from periphery import SPI
             self._spi = SPI(self._dev, 0, self._spi_speed * 1e6)
         except ImportError:
-            error(CANT_IMPORT_PERIPHERY_ERROR)
+            self.error(errors.CANT_IMPORT_PERIPHERY_ERROR)
 
         log.info('periphery spi dev {:s} speed @ {:.2f} MHz'.format(
             self._dev, self._spi.max_speed / 1e6))
@@ -76,17 +81,17 @@ class SpiPyDevInterface(SpiBaseInterface):
         try:
             self._device_id, self._device_cs = ids
         except:
-            error(BAD_FORMAT_ERROR)
+            self.error(errors.BAD_FORMAT_ERROR)
 
         if not os.path.exists(self._dev):
-            error(CANT_FIND_ERROR)
+            self.error(errors.CANT_FIND_ERROR)
         # permissions check
         try:
             fd = open(self._dev, 'r')
             fd.close()
         except IOError as e:
             if e.errno == 13:
-                error(PERMISSION_ERROR)
+                self.error(errors.PERMISSION_ERROR)
             else:
                 raise e
         # import spidev and cache error
@@ -94,7 +99,7 @@ class SpiPyDevInterface(SpiBaseInterface):
             import spidev
             self._spi = spidev.SpiDev()
         except ImportError:
-            error(CANT_IMPORT_SPIDEV_ERROR)
+            self.error(errors.CANT_IMPORT_SPIDEV_ERROR)
         self._spi.open(self._device_id, self._device_cs)
         self._spi.max_speed_hz = int(self._spi_speed * 1e6)
         log.info(
@@ -112,60 +117,16 @@ class SpiDummyInterface(SpiBaseInterface):
         pass
 
 
-class DriverSPIBase(DriverBase):
-    """Base driver for controling SPI devices on systems like the Raspberry Pi and BeagleBone"""
-
-    def __init__(self, num, c_order=ChannelOrder.GRB, interface=SpiFileInterface,
-                 dev='/dev/spidev0.0', spi_speed=2, gamma=None):
-        super().__init__(num, c_order=c_order, gamma=gamma)
-
-        self._interface = interface(dev=dev, spi_speed=spi_speed)
-
-    def _send_packet(self):
-        self._interface.send_packet(self._packet)
-
-    def _compute_packet(self):
-        self._render()
-        self._packet = self._interface.compute_packet(self._buf)
-
-
-PERMISSION_ERROR = """Cannot access SPI device.
-Please see
-
-    https://github.com/maniacallabs/bibliopixel/wiki/SPI-Setup
-
-for details.
-"""
-
-CANT_FIND_ERROR = """Cannot find SPI device.
-Please see
-
-    https://github.com/maniacallabs/bibliopixel/wiki/SPI-Setup
-
-for details.
-"""
-
-CANT_IMPORT_PERIPHERY_ERROR = """
-Unable to import periphery. Please install:
-    pip install python-periphery
-
-Please see
-    https://github.com/maniacallabs/bibliopixel/wiki/SPI-Setup
-
-for details.
-"""
-
-BAD_FORMAT_ERROR = """
-When using py-spidev, `dev` must be in the format /dev/spidev*.*
-"""
-
-CANT_IMPORT_SPIDEV_ERROR = """
-Unable to import spidev. Please install:
-
-    pip install spidev
-"""
+_SPI_INTERFACES = [
+    SpiFileInterface,
+    SpiPyDevInterface,
+    SpiPeripheryInterface,
+    SpiDummyInterface
+]
 
 
-def error(text):
-    log.error(text)
-    raise IOError(text)
+class SPI_INTERFACES(IntEnum):
+    FILE = 0
+    PYDEV = 1
+    PERIPHERY = 2
+    DUMMY = 3
diff --git a/bibliopixel/drivers/WS2801.py b/bibliopixel/drivers/WS2801.py
deleted file mode 100644
index 7cab37c..0000000
--- a/bibliopixel/drivers/WS2801.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from . channel_order import ChannelOrder
-from . spi_driver_base import DriverSPIBase
-from .. import gamma
-
-
-class WS2801(DriverSPIBase):
-    """Main driver for WS2801 based LED strips on devices like the Raspberry Pi and BeagleBone"""
-
-    def __init__(self, num, c_order=ChannelOrder.RGB,
-                 spi_speed=1, gamma=gamma.WS2801, **kwargs):
-        if spi_speed > 1 or spi_speed <= 0:
-            raise ValueError(
-                "WS2801 requires an SPI speed no greater than 1MHz or SPI speed was set <= 0")
-        super().__init__(num, c_order=c_order, spi_speed=spi_speed,
-                         gamma=gamma, **kwargs)
-
-
-# This is DEPRECATED.
-DriverWS2801 = WS2801
diff --git a/bibliopixel/drivers/__init__.py b/bibliopixel/drivers/__init__.py
index e69de29..570419b 100644
--- a/bibliopixel/drivers/__init__.py
+++ b/bibliopixel/drivers/__init__.py
@@ -0,0 +1,1 @@
+from . channel_order import ChannelOrder
diff --git a/bibliopixel/drivers/serial/codes.py b/bibliopixel/drivers/serial/codes.py
index 2f91c0b..ef0ff27 100644
--- a/bibliopixel/drivers/serial/codes.py
+++ b/bibliopixel/drivers/serial/codes.py
@@ -34,12 +34,6 @@ class LEDTYPE(IntEnum):
     P9813 = 11
 
 
-def make_ledtype(s):
-    if isinstance(s, int):
-        return s
-    return getattr(LEDTYPE, s.upper())
-
-
 SPIChipsets = [
     LEDTYPE.LPD8806,
     LEDTYPE.WS2801,
diff --git a/bibliopixel/drivers/serial/driver.py b/bibliopixel/drivers/serial/driver.py
index cd098a9..a8b963b 100644
--- a/bibliopixel/drivers/serial/driver.py
+++ b/bibliopixel/drivers/serial/driver.py
@@ -1,10 +1,11 @@
 import os, sys, time, traceback
 
-from . codes import CMDTYPE, LEDTYPE, SPIChipsets, BufferChipsets, make_ledtype
+from . codes import CMDTYPE, LEDTYPE, SPIChipsets, BufferChipsets
 from . devices import Devices
 from .. channel_order import ChannelOrder
 from .. driver_base import DriverBase
 from ... import log, util
+from ... util.enum import resolve_enum
 from ... return_codes import RETURN_CODES, print_error, BiblioSerialError
 
 
@@ -25,7 +26,7 @@ class Serial(DriverBase):
 
         self._SPISpeed = SPISpeed
         self._com = None
-        self._type = make_ledtype(type)
+        self._type = resolve_enum(LEDTYPE, type)
         self._bufPad = 0
         self.dev = dev
         self.device_version = 0
diff --git a/bibliopixel/gamma.py b/bibliopixel/gamma.py
index 6ee4d4b..61e12fc 100644
--- a/bibliopixel/gamma.py
+++ b/bibliopixel/gamma.py
@@ -17,7 +17,7 @@ class Gamma(object):
 # From https://github.com/scottjgibson/PixelPi/blob/master/pixelpi.py
 APA102 = Gamma(gamma=2.5, offset=0.5)
 WS2801 = SM16716 = Gamma(gamma=2.5)
-DEFAULT = Gamma()
+NONE = DEFAULT = Gamma()  # For when you need no correction
 
 # From http://rgb-123.com/ws2812-color-output/
 WS2812B = NEOPIXEL = WS2812 = Gamma(gamma=1 / 0.45)
diff --git a/bibliopixel/project/aliases.py b/bibliopixel/project/aliases.py
index 308f47f..b149b6c 100644
--- a/bibliopixel/project/aliases.py
+++ b/bibliopixel/project/aliases.py
@@ -3,16 +3,19 @@ from .importer import import_symbol
 
 ALIASES = {
     'driver': {
-        'apa102': 'bibliopixel.drivers.APA102.APA102',
+        'apa102': 'bibliopixel.drivers.API.APA102.APA102',
         'dummy': 'bibliopixel.drivers.dummy_driver.Dummy',
         'hue': 'bibliopixel.drivers.hue.Hue',
         'image': 'bibliopixel.drivers.image_sequence.ImageSequence',
-        'lpd8806': 'bibliopixel.drivers.LPD8806.LPD8806',
+        'lpd8806': 'bibliopixel.drivers.API.LPD8806.LPD8806',
         'network': 'bibliopixel.drivers.network.Network',
         'network_udp': 'bibliopixel.drivers.network.NetworkUDP',
         'serial': 'bibliopixel.drivers.serial.Serial',
         'simpixel': 'bibliopixel.drivers.SimPixel.SimPixel',
-        'ws2801': 'bibliopixel.drivers.WS2801.WS2801',
+        'ws281x': 'bibliopixel.drivers.API.WS281X.WS281X',
+        'ws2801': 'bibliopixel.drivers.API.WS2801.WS2801',
+        'spi': 'bibliopixel.drivers.SPI.SPI',
+        'pi_ws281x': 'bibliopixel.drivers.PiWS281X.PiWS281X'
     },
 
     'led': {
diff --git a/bibliopixel/project/project.py b/bibliopixel/project/project.py
index 32cfacf..208f0bf 100644
--- a/bibliopixel/project/project.py
+++ b/bibliopixel/project/project.py
@@ -68,7 +68,6 @@ def apply_defaults(project, defaults):
 
     ledtype = defaults.get('ledtype')
     if ledtype:
-        ledtype = codes.make_ledtype(ledtype)
         if 'led' not in project:
             project['led'] = {}
 
diff --git a/bibliopixel/util/enum.py b/bibliopixel/util/enum.py
new file mode 100644
index 0000000..7ea6abe
--- /dev/null
+++ b/bibliopixel/util/enum.py
@@ -0,0 +1,8 @@
+from enum import IntEnum
+
+
+def resolve_enum(enum_type, value):
+    try:
+        return enum_type[value]  # It's a string.
+    except:
+        return enum_type(value)   # It's an int.


Test patch:
diff --git a/test/all_test.py b/test/all_test.py
index 76ea8d1..9e4129f 100644
--- a/test/all_test.py
+++ b/test/all_test.py
@@ -20,10 +20,8 @@ from bibliopixel import (
 )
 
 from bibliopixel.drivers import (
-    APA102,
-    LPD8806,
+    SPI,
     # SimPixel,
-    WS2801,
     driver_base,
     dummy_driver,
     hue,
@@ -32,7 +30,6 @@ from bibliopixel.drivers import (
     network_receiver,
     network_udp,
     serial,
-    spi_driver_base,
     # timedata_visualizer,
 )
 
diff --git a/test/driver_test.py b/test/driver_test.py
index 722c742..5d427b1 100644
--- a/test/driver_test.py
+++ b/test/driver_test.py
@@ -2,10 +2,7 @@ import unittest
 
 from bibliopixel import gamma
 from bibliopixel.drivers.driver_base import DriverBase, ChannelOrder
-from bibliopixel.drivers.spi_driver_base import SpiDummyInterface
-from bibliopixel.drivers.APA102 import APA102
-from bibliopixel.drivers.LPD8806 import LPD8806
-from bibliopixel.drivers.WS2801 import WS2801
+from bibliopixel.drivers.SPI import SPI, SPI_INTERFACES
 
 
 class DriverTest(unittest.TestCase):
@@ -14,7 +11,7 @@ class DriverTest(unittest.TestCase):
               (2, 16, 128),
               (3, 24, 192)]
 
-    SPD = dict(interface=SpiDummyInterface)
+    SPD = dict(interface=SPI_INTERFACES.DUMMY)
 
     def do_test(self, driver, expected):
         driver.set_colors(self.COLORS, 0)
@@ -51,18 +48,17 @@ class DriverTest(unittest.TestCase):
         self.do_test(driver, expected)
 
     def test_apa102(self):
-        driver = APA102(num=4, **self.SPD)
-        expected = [0, 0, 0, 0, 255, 0, 0, 0, 255, 1, 8, 64,
-                    255, 2, 16, 128, 255, 3, 24, 192, 255, 0, 0, 0]
+        driver = SPI(type='APA102', num=4, **self.SPD)
+        expected = [0, 0, 0, 0, 0, 8, 0, 0, 46, 0, 1, 125]
         self.do_test(driver, expected)
 
     def test_lpd8806(self):
-        driver = LPD8806(num=4, **self.SPD)
+        driver = SPI(type='LPD8806', num=4, **self.SPD)
         expected = [
             128, 128, 128, 128, 128, 132, 128, 128, 151, 128, 128, 190, 0]
         self.do_test(driver, expected)
 
     def test_ws2801(self):
-        driver = WS2801(num=4, **self.SPD)
+        driver = SPI(type='WS2801', num=4, **self.SPD)
         expected = [0, 0, 0, 0, 0, 8, 0, 0, 45, 0, 0, 125]
         self.do_test(driver, expected)