Source code for adafruit_is31fl3731

# SPDX-FileCopyrightText: Tony DiCola 2017 for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
`adafruit_is31fl3731`
====================================================

CircuitPython driver for the IS31FL3731 charlieplex IC.

Base library.

* Author(s): Tony DiCola, Melissa LeBlanc-Williams, David Glaude, E. A. Graham Jr.

Implementation Notes
--------------------

**Hardware:**

* `Adafruit 16x9 Charlieplexed PWM LED Matrix Driver - IS31FL3731
  <https://www.adafruit.com/product/2946>`_

* `Adafruit 15x7 CharliePlex LED Matrix Display FeatherWings
  <https://www.adafruit.com/product/2965>`_

* `Adafruit 16x8 CharliePlex LED Matrix Bonnets
  <https://www.adafruit.com/product/4127>`_

* `Pimoroni 17x7 Scroll pHAT HD
  <https://www.adafruit.com/product/3473>`_

* `Pimoroni 28x3 (r,g,b) Led Shim
  <https://www.adafruit.com/product/3831>`_

* `Pimoroni LED SHIM
  <https://shop.pimoroni.com/products/led-shim>`_

* `Pimoroni Keybow 2040
  <https://shop.pimoroni.com/products/keybow-2040>`_

* `Pimoroni 11x7 LED Matrix Breakout
  <https://shop.pimoroni.com/products/11x7-led-matrix-breakout>`_

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases

"""

# imports
import math
import time
from micropython import const

from adafruit_bus_device.i2c_device import I2CDevice

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_IS31FL3731.git"

_MODE_REGISTER = const(0x00)
_FRAME_REGISTER = const(0x01)
_AUTOPLAY1_REGISTER = const(0x02)
_AUTOPLAY2_REGISTER = const(0x03)
_BLINK_REGISTER = const(0x05)
_AUDIOSYNC_REGISTER = const(0x06)
_BREATH1_REGISTER = const(0x08)
_BREATH2_REGISTER = const(0x09)
_SHUTDOWN_REGISTER = const(0x0A)
_GAIN_REGISTER = const(0x0B)
_ADC_REGISTER = const(0x0C)

_CONFIG_BANK = const(0x0B)
_BANK_ADDRESS = const(0xFD)

_PICTURE_MODE = const(0x00)
_AUTOPLAY_MODE = const(0x08)
_AUDIOPLAY_MODE = const(0x18)

_ENABLE_OFFSET = const(0x00)
_BLINK_OFFSET = const(0x12)
_COLOR_OFFSET = const(0x24)


[docs] class IS31FL3731: """ The IS31FL3731 is an abstract class contain the main function related to this chip. Each board needs to define width, height and pixel_addr. :param ~busio.I2C i2c: the connected i2c bus i2c_device :param int address: the device address; defaults to 0x74 """ width = 16 height = 9 def __init__(self, i2c, address=0x74, frames=None): self.i2c_device = I2CDevice(i2c, address) self._frame = None self._init(frames=frames) def _i2c_read_reg(self, reg, result): # Read a buffer of data from the specified 8-bit I2C register address. # The provided result parameter will be filled to capacity with bytes # of data read from the register. with self.i2c_device as i2c: i2c.write_then_readinto(bytes([reg]), result) return result return None def _i2c_write_reg(self, reg, data): # Write a contiguous block of data (bytearray) starting at the # specified I2C register address (register passed as argument). self._i2c_write_block(bytes([reg]) + data) def _i2c_write_block(self, data): # Write a buffer of data (byte array) to the specified I2C register # address. with self.i2c_device as i2c: i2c.write(data) def _bank(self, bank=None): if bank is None: result = bytearray(1) return self._i2c_read_reg(_BANK_ADDRESS, result)[0] self._i2c_write_reg(_BANK_ADDRESS, bytearray([bank])) return None def _register(self, bank, register, value=None): self._bank(bank) if value is None: result = bytearray(1) return self._i2c_read_reg(register, result)[0] self._i2c_write_reg(register, bytearray([value])) return None def _mode(self, mode=None): return self._register(_CONFIG_BANK, _MODE_REGISTER, mode) def _init(self, frames=None): self.sleep(True) # Clear config; sets to Picture Mode, no audio sync, maintains sleep self._bank(_CONFIG_BANK) self._i2c_write_block(bytes([0] * 14)) enable_data = bytes([_ENABLE_OFFSET] + [255] * 18) fill_data = bytearray([0] * 25) # Initialize requested frames, or all 8 if unspecified for frame in frames if frames else range(8): self._bank(frame) self._i2c_write_block(enable_data) # Set all enable bits for row in range(6): # Barebones quick fill() w/0 fill_data[0] = _COLOR_OFFSET + row * 24 self._i2c_write_block(fill_data) self._frame = 0 # To match config bytes above self.sleep(False)
[docs] def reset(self): """Kill the display for 10MS""" self.sleep(True) time.sleep(0.01) # 10 MS pause to reset. self.sleep(False)
[docs] def sleep(self, value): """ Set the Software Shutdown Register bit :param value: True to set software shutdown bit; False unset """ return self._register(_CONFIG_BANK, _SHUTDOWN_REGISTER, not value)
[docs] def autoplay(self, delay=0, loops=0, frames=0): """ Start autoplay :param delay: in ms :param loops: number of loops - 0->7 :param frames: number of frames: 0->7 """ if delay == 0: self._mode(_PICTURE_MODE) return delay //= 11 if not 0 <= loops <= 7: raise ValueError("Loops out of range") if not 0 <= frames <= 7: raise ValueError("Frames out of range") if not 1 <= delay <= 64: raise ValueError("Delay out of range") self._register(_CONFIG_BANK, _AUTOPLAY1_REGISTER, loops << 4 | frames) self._register(_CONFIG_BANK, _AUTOPLAY2_REGISTER, delay % 64) self._mode(_AUTOPLAY_MODE | self._frame)
[docs] def fade(self, fade_in=None, fade_out=None, pause=0): """ Start and stop the fade feature. If both fade_in and fade_out are None (the default), the breath feature is used for fading. if fade_in is None, then fade_in = fade_out. If fade_out is None, then fade_out = fade_in :param fade_in: positive number; 0->100 :param fade-out: positive number; 0->100 :param pause: breath register 2 pause value """ if fade_in is None and fade_out is None: self._register(_CONFIG_BANK, _BREATH2_REGISTER, 0) return if fade_in is None: fade_in = fade_out elif fade_out is None: fade_out = fade_in if fade_in != 0: fade_in = int(math.log(fade_in / 26, 2)) if fade_out != 0: fade_out = int(math.log(fade_out / 26, 2)) if pause != 0: pause = int(math.log(pause / 26, 2)) if not 0 <= fade_in <= 7: raise ValueError("Fade in out of range") if not 0 <= fade_out <= 7: raise ValueError("Fade out out of range") if not 0 <= pause <= 7: raise ValueError("Pause out of range") self._register(_CONFIG_BANK, _BREATH1_REGISTER, fade_out << 4 | fade_in) self._register(_CONFIG_BANK, _BREATH2_REGISTER, 1 << 4 | pause)
[docs] def frame(self, frame=None, show=True): """ Set the current frame :param frame: frame number; 0-7 or None. If None function returns current frame :param show: True to show the frame; False to not show. """ if frame is None: return self._frame if not 0 <= frame <= 8: raise ValueError("Frame out of range") self._frame = frame if show: self._register(_CONFIG_BANK, _FRAME_REGISTER, frame) return None
[docs] def audio_sync(self, value=None): """Set the audio sync feature register""" return self._register(_CONFIG_BANK, _AUDIOSYNC_REGISTER, value)
[docs] def audio_play(self, sample_rate, audio_gain=0, agc_enable=False, agc_fast=False): """Controls the audio play feature""" if sample_rate == 0: self._mode(_PICTURE_MODE) return sample_rate //= 46 if not 1 <= sample_rate <= 256: raise ValueError("Sample rate out of range") self._register(_CONFIG_BANK, _ADC_REGISTER, sample_rate % 256) audio_gain //= 3 if not 0 <= audio_gain <= 7: raise ValueError("Audio gain out of range") self._register( _CONFIG_BANK, _GAIN_REGISTER, bool(agc_enable) << 3 | bool(agc_fast) << 4 | audio_gain, ) self._mode(_AUDIOPLAY_MODE)
[docs] def fill(self, color=None, blink=None, frame=None): """ Fill the display with a brightness level :param color: brightness 0->255 :param blink: True if blinking is required :param frame: which frame to fill 0->7 """ if frame is None: frame = self._frame self._bank(frame) if color is not None: if not 0 <= color <= 255: raise ValueError("Color out of range") data = bytearray([color] * 25) # Extra byte at front for address. with self.i2c_device as i2c: for row in range(6): data[0] = _COLOR_OFFSET + row * 24 i2c.write(data) if blink is not None: data = bool(blink) * 0xFF for col in range(18): self._register(frame, _BLINK_OFFSET + col, data)
# This function must be replaced for each board
[docs] @staticmethod def pixel_addr(x, y): """Calulate the offset into the device array for x,y pixel""" return x + y * 16
# pylint: disable-msg=too-many-arguments
[docs] def pixel(self, x, y, color=None, blink=None, frame=None): """ Blink or brightness for x-, y-pixel :param x: horizontal pixel position :param y: vertical pixel position :param color: brightness value 0->255 :param blink: True to blink :param frame: the frame to set the pixel """ if not 0 <= x <= self.width: return None if not 0 <= y <= self.height: return None pixel = self.pixel_addr(x, y) if color is None and blink is None: return self._register(self._frame, pixel) if frame is None: frame = self._frame if color is not None: if not 0 <= color <= 255: raise ValueError("Color out of range") self._register(frame, _COLOR_OFFSET + pixel, color) if blink is not None: addr, bit = divmod(pixel, 8) bits = self._register(frame, _BLINK_OFFSET + addr) if blink: bits |= 1 << bit else: bits &= ~(1 << bit) self._register(frame, _BLINK_OFFSET + addr, bits) return None
# pylint: enable-msg=too-many-arguments
[docs] def image(self, img, blink=None, frame=None): """Set buffer to value of Python Imaging Library image. The image should be in 8-bit mode (L) and a size equal to the display size. :param img: Python Imaging Library image :param blink: True to blink :param frame: the frame to set the image """ if img.mode != "L": raise ValueError("Image must be in mode L.") imwidth, imheight = img.size if imwidth != self.width or imheight != self.height: raise ValueError( "Image must be same dimensions as display ({0}x{1}).".format( self.width, self.height ) ) # Grab all the pixels from the image, faster than getpixel. pixels = img.load() # Iterate through the pixels for x in range(self.width): # yes this double loop is slow, for y in range(self.height): # but these displays are small! self.pixel(x, y, pixels[(x, y)], blink=blink, frame=frame)