Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Rotation and Typing #54

Merged
merged 33 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b756b5e
Add Rotation and Typing
DJDevon3 Mar 22, 2024
3232d28
Cleaning up pylint errors
DJDevon3 Mar 22, 2024
35b4ac8
Attempt #2 also ran black formatter
DJDevon3 Apr 2, 2024
757584a
attempt 3 (import Optional)
DJDevon3 Apr 2, 2024
17d9241
attempt 4 (import Iterable)
DJDevon3 Apr 2, 2024
6d70dd7
lets just throw stuff at pylint and see what works...
DJDevon3 Apr 2, 2024
9f6ab0c
making progress, down to 3 errors.
DJDevon3 Apr 2, 2024
927c33f
1 error for black so close!
DJDevon3 Apr 2, 2024
2e9d20b
oh come on! fine set it on both imports, see how ya like that pylint.
DJDevon3 Apr 2, 2024
e44eae5
Ran to ChatGPT for help and here's what it says to do.
DJDevon3 Apr 2, 2024
e406fb9
remove if TYPE_CHECKING, this will probably fail in Blinka , let Meli…
DJDevon3 Apr 2, 2024
d7ff776
let's see if pylint fails with a class does not exist
DJDevon3 Apr 2, 2024
a65e271
this is not fun
DJDevon3 Apr 2, 2024
57f2d1b
breaking out the None hammer
DJDevon3 Apr 2, 2024
4dc9054
delete and skip this part?
DJDevon3 Apr 2, 2024
58d95bd
change it to Optional[int], deal with it later.
DJDevon3 Apr 2, 2024
4fb28c3
can has WriteableBuffer?
DJDevon3 Apr 2, 2024
de5a15c
every...time... black
DJDevon3 Apr 2, 2024
5c5f45b
this is why i can't have nice things
DJDevon3 Apr 2, 2024
888bd32
lets try all the hundreds of different possible syntaxes...
DJDevon3 Apr 2, 2024
13236c4
Using variable before assignment
DJDevon3 Apr 2, 2024
c80cc25
Leroyyyyy Jenkins
DJDevon3 Apr 2, 2024
1cbd7c4
at least i got chicken
DJDevon3 Apr 2, 2024
393aeb8
i'm drowning
DJDevon3 Apr 2, 2024
e2b91c2
in my hour of deperation, chatgpt
DJDevon3 Apr 15, 2024
d4e54c9
check for linux if attempting to use PIL
DJDevon3 Apr 15, 2024
b5b921a
trying different import order
DJDevon3 Apr 15, 2024
cbdf431
pil is pil.py not pillow?
DJDevon3 Apr 15, 2024
251155c
pil Image is part of circuitpython_typing?
DJDevon3 Apr 15, 2024
c56891c
can i make all the things optional?
DJDevon3 Apr 15, 2024
72a3262
Welcome to Costco, I love you.
DJDevon3 Apr 15, 2024
c2af4be
typing tweaks
FoamyGuy May 13, 2024
9cf7264
add PIL to optional reqs
FoamyGuy May 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 143 additions & 45 deletions adafruit_is31fl3731/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,36 @@
https://github.com/adafruit/circuitpython/releases

"""

# imports
import math
import time
from micropython import const

from adafruit_bus_device.i2c_device import I2CDevice

try:
import typing
import busio
from circuitpython_typing import TypeAlias, Union
from circuitpython_typing import (
WriteableBuffer,
ReadableBuffer,
) # Import ReadableBuffer here

from typing import (
TYPE_CHECKING,
List,
Tuple,
Optional,
Iterable,
)

from PIL import Image

except ImportError as e:
pass


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

Expand Down Expand Up @@ -89,17 +111,25 @@ class IS31FL3731:

:param ~busio.I2C i2c: the connected i2c bus i2c_device
:param int address: the device address; defaults to 0x74
:param Iterable frames: list of frame indexes to use. int's 0-7.
"""

width = 16
height = 9
width: int = 16
height: int = 9

def __init__(self, i2c, address=0x74, frames=None):
def __init__(
self,
i2c: busio.I2C,
frames: Optional[Iterable] = None,
address: int = 0x74,
):
self.i2c_device = I2CDevice(i2c, address)
self._frame = None
self._init(frames=frames)

def _i2c_read_reg(self, reg, result):
def _i2c_read_reg(
self, reg: Optional[int] = None, result: Optional[WriteableBuffer] = None
) -> Optional[WriteableBuffer]:
# 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.
Expand All @@ -108,36 +138,44 @@ def _i2c_read_reg(self, reg, result):
return result
return None

def _i2c_write_reg(self, reg, data):
def _i2c_write_reg(
self, reg: Optional[int] = None, data: Optional[ReadableBuffer] = None
) -> None:
# 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):
def _i2c_write_block(self, data: Optional[ReadableBuffer]) -> None:
# 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):
def _bank(self, bank: Optional[int] = None) -> Optional[int]:
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):
def _register(
self,
bank: Optional[int] = None,
register: Optional[int] = None,
value: Optional[int] = None,
) -> Optional[int]:
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):
def _mode(self, mode: Optional[int] = None) -> int:
"""Function for setting _register mode"""
return self._register(_CONFIG_BANK, _MODE_REGISTER, mode)

def _init(self, frames=None):
def _init(self, frames: Iterable) -> None:
self.sleep(True)
# Clear config; sets to Picture Mode, no audio sync, maintains sleep
self._bank(_CONFIG_BANK)
Expand All @@ -154,21 +192,26 @@ def _init(self, frames=None):
self._frame = 0 # To match config bytes above
self.sleep(False)

def reset(self):
def reset(self) -> None:
"""Kill the display for 10MS"""
self.sleep(True)
time.sleep(0.01) # 10 MS pause to reset.
self.sleep(False)

def sleep(self, value):
def sleep(self, value: bool) -> Optional[int]:
"""
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)

def autoplay(self, delay=0, loops=0, frames=0):
def autoplay(
self,
delay: int = 0,
loops: int = 0,
frames: int = 0,
) -> None:
"""
Start autoplay

Expand All @@ -190,15 +233,20 @@ def autoplay(self, delay=0, loops=0, frames=0):
self._register(_CONFIG_BANK, _AUTOPLAY2_REGISTER, delay % 64)
self._mode(_AUTOPLAY_MODE | self._frame)

def fade(self, fade_in=None, fade_out=None, pause=0):
def fade(
self,
fade_in: Optional[int] = None,
fade_out: Optional[int] = None,
pause: int = 0,
) -> int:
"""
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
:param fade_in: int positive number; 0->100
:param fade-out: int positive number; 0->100
:param pause: int breath register 2 pause value
FoamyGuy marked this conversation as resolved.
Show resolved Hide resolved
"""
if fade_in is None and fade_out is None:
self._register(_CONFIG_BANK, _BREATH2_REGISTER, 0)
Expand All @@ -223,12 +271,12 @@ def fade(self, fade_in=None, fade_out=None, pause=0):
self._register(_CONFIG_BANK, _BREATH1_REGISTER, fade_out << 4 | fade_in)
self._register(_CONFIG_BANK, _BREATH2_REGISTER, 1 << 4 | pause)

def frame(self, frame=None, show=True):
def frame(self, frame: Optional[int] = None, show: bool = True) -> Optional[int]:
"""
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.
:param frame: int frame number; 0-7 or None. If None function returns current frame
:param show: bool True to show the frame; False to not show.
"""
if frame is None:
return self._frame
Expand All @@ -239,11 +287,17 @@ def frame(self, frame=None, show=True):
self._register(_CONFIG_BANK, _FRAME_REGISTER, frame)
return None

def audio_sync(self, value=None):
def audio_sync(self, value: Optional[int]) -> Optional[int]:
"""Set the audio sync feature register"""
return self._register(_CONFIG_BANK, _AUDIOSYNC_REGISTER, value)

def audio_play(self, sample_rate, audio_gain=0, agc_enable=False, agc_fast=False):
def audio_play(
self,
sample_rate: int,
audio_gain: int = 0,
agc_enable: bool = False,
agc_fast: bool = False,
) -> None:
"""Controls the audio play feature"""
if sample_rate == 0:
self._mode(_PICTURE_MODE)
Expand All @@ -262,7 +316,7 @@ def audio_play(self, sample_rate, audio_gain=0, agc_enable=False, agc_fast=False
)
self._mode(_AUDIOPLAY_MODE)

def blink(self, rate=None):
def blink(self, rate: Optional[int] = None) -> Optional[int]:
"""Updates the blink register"""
# pylint: disable=no-else-return
# This needs to be refactored when it can be tested
Expand All @@ -275,13 +329,18 @@ def blink(self, rate=None):
self._register(_CONFIG_BANK, _BLINK_REGISTER, rate & 0x07 | 0x08)
return None

def fill(self, color=None, blink=None, frame=None):
def fill(
self,
color: Optional[int] = None,
frame: Optional[int] = None,
blink: bool = False,
):
"""
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
:param blink: bool True to blink
:param frame: int the frame to set the pixel, default 0
"""
if frame is None:
frame = self._frame
Expand All @@ -301,35 +360,72 @@ def fill(self, color=None, blink=None, frame=None):

# This function must be replaced for each board
@staticmethod
def pixel_addr(x, y):
def pixel_addr(x: int, y: int) -> int:
"""Calulate the offset into the device array for x,y pixel"""
return x + y * 16

# pylint: disable-msg=too-many-arguments
def pixel(self, x, y, color=None, blink=None, frame=None):
def pixel(
self,
x: int,
y: int,
color: Optional[int] = None,
frame: Optional[int] = None,
blink: bool = False,
rotate: int = 0,
) -> Optional[int]:
"""
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
Matrix display configuration

:param x: int horizontal pixel position
:param y: int vertical pixel position
:param color: int brightness value 0->255
:param blink: bool True to blink
:param frame: int the frame to set the pixel, default 0
:param rotate: int display rotation (0, 90, 180, 270)
"""
if not 0 <= x <= self.width:
return None
if not 0 <= y <= self.height:
return None
pixel = self.pixel_addr(x, y)
# pylint: disable=too-many-branches

if rotate not in (0, 90, 180, 270):
raise ValueError("Rotation must be 0, 90, 180, or 270 degrees")

if rotate == 0:
check_x = 0 <= x <= self.width
check_y = 0 <= y <= self.height
if not (check_x and check_y):
return None
pixel = self.pixel_addr(x, y)
elif rotate == 90:
check_x = 0 <= y <= self.width
check_y = 0 <= x <= self.height
if not (check_x and check_y):
return None
pixel = self.pixel_addr(y, self.height - x - 1)
elif rotate == 180:
check_x = 0 <= x <= self.width
check_y = 0 <= y <= self.height
if not (check_x and check_y):
return None
pixel = self.pixel_addr(self.width - x - 1, self.height - y - 1)
elif rotate == 270:
check_x = 0 <= y <= self.width
check_y = 0 <= x <= self.height
if not (check_x and check_y):
return None
pixel = self.pixel_addr(self.width - y - 1, x)

if color is None and blink is None:
return self._register(self._frame, pixel)
# frames other than 0 only used in animation. allow None.
if frame is None:
frame = self._frame
# Brightness
if color is not None:
if not 0 <= color <= 255:
raise ValueError("Color out of range")
raise ValueError("Brightness or Color out of range (0-255)")
self._register(frame, _COLOR_OFFSET + pixel, color)
if blink is not None:
# Blink works but not well while animated
if blink:
addr, bit = divmod(pixel, 8)
bits = self._register(frame, _BLINK_OFFSET + addr)
if blink:
Expand All @@ -341,13 +437,15 @@ def pixel(self, x, y, color=None, blink=None, frame=None):

# pylint: enable-msg=too-many-arguments

def image(self, img, blink=None, frame=None):
def image(
self, img: Image, frame: Optional[int] = None, blink: bool = False
) -> 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
:param frame: the frame to set the image, default 0
"""
if img.mode != "L":
raise ValueError("Image must be in mode L.")
Expand Down
Loading
Loading