Skip to content

Commit 7de2ec3

Browse files
authored
Merge pull request #5 from FoamyGuy/implement_portalbase
Adding PortalBase support
2 parents 76d4f22 + 666b515 commit 7de2ec3

File tree

9 files changed

+635
-13
lines changed

9 files changed

+635
-13
lines changed

adafruit_fruitjam/__init__.py

Lines changed: 304 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
1616
**Hardware:**
1717
18-
* `Adafruit Fruit Jam <url>`_"
18+
* `Adafruit Fruit Jam <https://www.adafruit.com/product/6200>`_
1919
2020
**Software and Dependencies:**
2121
@@ -29,13 +29,314 @@
2929
__version__ = "0.0.0+auto.0"
3030
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_FruitJam.git"
3131

32+
import gc
33+
import os
34+
import time
35+
36+
import board
37+
import busio
38+
import supervisor
39+
import terminalio
40+
from adafruit_esp32spi import adafruit_esp32spi
41+
from adafruit_portalbase import PortalBase
42+
from digitalio import DigitalInOut
43+
44+
from adafruit_fruitjam.graphics import Graphics
45+
from adafruit_fruitjam.network import CONTENT_IMAGE, CONTENT_JSON, CONTENT_TEXT, Network
3246
from adafruit_fruitjam.peripherals import Peripherals
3347

3448

35-
class FruitJam:
36-
def __init__(self):
49+
class FruitJam(PortalBase):
50+
"""Class representing the Adafruit Fruit Jam.
51+
52+
:param url: The URL of your data source. Defaults to ``None``.
53+
:param headers: The headers for authentication, typically used by Azure API's.
54+
:param json_path: The list of json traversal to get data out of. Can be list of lists for
55+
multiple data points. Defaults to ``None`` to not use json.
56+
:param regexp_path: The list of regexp strings to get data out (use a single regexp group). Can
57+
be list of regexps for multiple data points. Defaults to ``None`` to not
58+
use regexp.
59+
:param convert_image: Determine whether or not to use the AdafruitIO image converter service.
60+
Set as False if your image is already resized. Defaults to True.
61+
:param default_bg: The path to your default background image file or a hex color.
62+
Defaults to 0x000000.
63+
:param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board
64+
NeoPixel. Defaults to ``None``, not the status LED
65+
:param str text_font: The path to your font file for your data text display.
66+
:param text_position: The position of your extracted text on the display in an (x, y) tuple.
67+
Can be a list of tuples for when there's a list of json_paths, for example
68+
:param text_color: The color of the text, in 0xRRGGBB format. Can be a list of colors for when
69+
there's multiple texts. Defaults to ``None``.
70+
:param text_wrap: Whether or not to wrap text (for long text data chunks). Defaults to
71+
``False``, no wrapping.
72+
:param text_maxlen: The max length of the text for text wrapping. Defaults to 0.
73+
:param text_transform: A function that will be called on the text before display
74+
:param int text_scale: The factor to scale the default size of the text by
75+
:param json_transform: A function or a list of functions to call with the parsed JSON.
76+
Changes and additions are permitted for the ``dict`` object.
77+
:param image_json_path: The JSON traversal path for a background image to display. Defaults to
78+
``None``.
79+
:param image_resize: What size to resize the image we got from the json_path, make this a tuple
80+
of the width and height you want. Defaults to ``None``.
81+
:param image_position: The position of the image on the display as an (x, y) tuple. Defaults to
82+
``None``.
83+
:param image_dim_json_path: The JSON traversal path for the original dimensions of image tuple.
84+
Used with fetch(). Defaults to ``None``.
85+
:param success_callback: A function we'll call if you like, when we fetch data successfully.
86+
Defaults to ``None``.
87+
:param str caption_text: The text of your caption, a fixed text not changed by the data we get.
88+
Defaults to ``None``.
89+
:param str caption_font: The path to the font file for your caption. Defaults to ``None``.
90+
:param caption_position: The position of your caption on the display as an (x, y) tuple.
91+
Defaults to ``None``.
92+
:param caption_color: The color of your caption. Must be a hex value, e.g. ``0x808000``.
93+
:param image_url_path: The HTTP traversal path for a background image to display.
94+
Defaults to ``None``.
95+
:param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used
96+
before calling the pyportal class. Defaults to ``None``.
97+
:param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``.
98+
:param debug: Turn on debug print outs. Defaults to False.
99+
100+
"""
101+
102+
def __init__( # noqa: PLR0912,PLR0913,Too many branches,Too many arguments in function definition
103+
self,
104+
*,
105+
url=None,
106+
headers=None,
107+
json_path=None,
108+
regexp_path=None,
109+
convert_image=True,
110+
default_bg=0x000000,
111+
status_neopixel=None,
112+
text_font=terminalio.FONT,
113+
text_position=None,
114+
text_color=0x808080,
115+
text_wrap=False,
116+
text_maxlen=0,
117+
text_transform=None,
118+
text_scale=1,
119+
json_transform=None,
120+
image_json_path=None,
121+
image_resize=None,
122+
image_position=None,
123+
image_dim_json_path=None,
124+
caption_text=None,
125+
caption_font=None,
126+
caption_position=None,
127+
caption_color=0x808080,
128+
image_url_path=None,
129+
success_callback=None,
130+
esp=None,
131+
external_spi=None,
132+
debug=False,
133+
secrets_data=None,
134+
):
135+
graphics = Graphics(
136+
default_bg=default_bg,
137+
debug=debug,
138+
)
139+
self._default_bg = default_bg
140+
141+
spi = board.SPI()
142+
143+
if image_json_path or image_url_path:
144+
if debug:
145+
print("Init image path")
146+
if not image_position:
147+
image_position = (0, 0) # default to top corner
148+
if not image_resize:
149+
image_resize = (
150+
self.display.width,
151+
self.display.height,
152+
) # default to full screen
153+
154+
if esp is None:
155+
esp32_cs = DigitalInOut(board.ESP_CS)
156+
esp32_ready = DigitalInOut(board.ESP_BUSY)
157+
esp32_reset = DigitalInOut(board.ESP_RESET)
158+
spi = board.SPI()
159+
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
160+
37161
self.peripherals = Peripherals()
38162

163+
network = Network(
164+
status_neopixel=self.peripherals.neopixels
165+
if status_neopixel is None
166+
else status_neopixel,
167+
esp=esp,
168+
external_spi=spi,
169+
extract_values=False,
170+
convert_image=convert_image,
171+
image_url_path=image_url_path,
172+
image_json_path=image_json_path,
173+
image_resize=image_resize,
174+
image_position=image_position,
175+
image_dim_json_path=image_dim_json_path,
176+
debug=debug,
177+
)
178+
self.url = url
179+
180+
super().__init__(
181+
network,
182+
graphics,
183+
url=url,
184+
headers=headers,
185+
json_path=json_path,
186+
regexp_path=regexp_path,
187+
json_transform=json_transform,
188+
success_callback=success_callback,
189+
debug=debug,
190+
)
191+
192+
# Convenience Shortcuts for compatibility
193+
194+
# self.sd_check = self.peripherals.sd_check
195+
# self.play_file = self.peripherals.play_file
196+
# self.stop_play = self.peripherals.stop_play
197+
198+
self.image_converter_url = self.network.image_converter_url
199+
self.wget = self.network.wget
200+
# self.show_QR = self.graphics.qrcode
201+
# self.hide_QR = self.graphics.hide_QR
202+
203+
if default_bg is not None:
204+
self.graphics.set_background(default_bg)
205+
206+
if self._debug:
207+
print("Init caption")
208+
if caption_font:
209+
self._caption_font = self._load_font(caption_font)
210+
self.set_caption(caption_text, caption_position, caption_color)
211+
212+
if text_font:
213+
if text_position is not None and isinstance(text_position[0], (list, tuple)):
214+
num = len(text_position)
215+
if not text_wrap:
216+
text_wrap = [0] * num
217+
if not text_maxlen:
218+
text_maxlen = [0] * num
219+
if not text_transform:
220+
text_transform = [None] * num
221+
if not isinstance(text_scale, (list, tuple)):
222+
text_scale = [text_scale] * num
223+
else:
224+
num = 1
225+
text_position = (text_position,)
226+
text_color = (text_color,)
227+
text_wrap = (text_wrap,)
228+
text_maxlen = (text_maxlen,)
229+
text_transform = (text_transform,)
230+
text_scale = (text_scale,)
231+
for i in range(num):
232+
self.add_text(
233+
text_position=text_position[i],
234+
text_font=text_font,
235+
text_color=text_color[i],
236+
text_wrap=text_wrap[i],
237+
text_maxlen=text_maxlen[i],
238+
text_transform=text_transform[i],
239+
text_scale=text_scale[i],
240+
)
241+
else:
242+
self._text_font = None
243+
self._text = None
244+
245+
gc.collect()
246+
247+
def set_caption(self, caption_text, caption_position, caption_color):
248+
"""A caption. Requires setting ``caption_font`` in init!
249+
250+
:param caption_text: The text of the caption.
251+
:param caption_position: The position of the caption text.
252+
:param caption_color: The color of your caption text. Must be a hex value, e.g.
253+
``0x808000``.
254+
"""
255+
if self._debug:
256+
print("Setting caption to", caption_text)
257+
258+
if (not caption_text) or (not self._caption_font) or (not caption_position):
259+
return # nothing to do!
260+
261+
index = self.add_text(
262+
text_position=caption_position,
263+
text_font=self._caption_font,
264+
text_color=caption_color,
265+
is_data=False,
266+
)
267+
self.set_text(caption_text, index)
268+
269+
def fetch(self, refresh_url=None, timeout=10, force_content_type=None): # noqa: PLR0912 Too many branches
270+
"""Fetch data from the url we initialized with, perfom any parsing,
271+
and display text or graphics. This function does pretty much everything
272+
Optionally update the URL
273+
"""
274+
275+
if refresh_url:
276+
self.url = refresh_url
277+
278+
response = self.network.fetch(self.url, headers=self._headers, timeout=timeout)
279+
280+
json_out = None
281+
if not force_content_type:
282+
content_type = self.network.check_response(response)
283+
else:
284+
content_type = force_content_type
285+
json_path = self._json_path
286+
287+
if content_type == CONTENT_JSON:
288+
if json_path is not None:
289+
# Drill down to the json path and set json_out as that node
290+
if isinstance(json_path, (list, tuple)) and (
291+
not json_path or not isinstance(json_path[0], (list, tuple))
292+
):
293+
json_path = (json_path,)
294+
try:
295+
gc.collect()
296+
json_out = response.json()
297+
if self._debug:
298+
print(json_out)
299+
gc.collect()
300+
except ValueError: # failed to parse?
301+
print("Couldn't parse json: ", response.text)
302+
raise
303+
except MemoryError:
304+
supervisor.reload()
305+
if content_type == CONTENT_IMAGE:
306+
try:
307+
filename, position = self.network.process_image(
308+
json_out, self.peripherals.sd_check()
309+
)
310+
if filename and position is not None:
311+
self.graphics.set_background(filename, position)
312+
except ValueError as error:
313+
print("Error displaying cached image. " + error.args[0])
314+
if self._default_bg is not None:
315+
self.graphics.set_background(self._default_bg)
316+
except KeyError as error:
317+
print("Error finding image data. '" + error.args[0] + "' not found.")
318+
self.set_background(self._default_bg)
319+
320+
if content_type == CONTENT_JSON:
321+
values = self.network.process_json(json_out, json_path)
322+
elif content_type == CONTENT_TEXT:
323+
values = self.network.process_text(response.text, self._regexp_path)
324+
325+
# if we have a callback registered, call it now
326+
if self._success_callback:
327+
self._success_callback(values)
328+
329+
self._fill_text_labels(values)
330+
# Clean up
331+
json_out = None
332+
response = None
333+
gc.collect()
334+
335+
if len(values) == 1:
336+
values = values[0]
337+
338+
return values
339+
39340
@property
40341
def neopixels(self):
41342
return self.peripherals.neopixels

adafruit_fruitjam/graphics.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries
2+
# SPDX-FileCopyrightText: 2025 Tim Cocks, written for Adafruit Industries
3+
#
4+
# SPDX-License-Identifier: Unlicense
5+
"""
6+
`adafruit_fruitjam.graphics`
7+
================================================================================
8+
9+
Graphics Helper library for the Adafruit Fruit Jam.
10+
11+
* Author(s): Melissa LeBlanc-Williams, Tim Cocks
12+
13+
Implementation Notes
14+
--------------------
15+
16+
**Hardware:**
17+
18+
* `Adafruit Fruit Jam <https://www.adafruit.com/product/6200>`_
19+
20+
**Software and Dependencies:**
21+
22+
* Adafruit CircuitPython firmware for the supported boards:
23+
https://github.com/adafruit/circuitpython/releases
24+
25+
"""
26+
27+
import supervisor
28+
from adafruit_portalbase.graphics import GraphicsBase
29+
30+
from adafruit_fruitjam.peripherals import request_display_config
31+
32+
__version__ = "0.0.0+auto.0"
33+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_FruitJam.git"
34+
35+
36+
class Graphics(GraphicsBase):
37+
"""Graphics Helper library for the Adafruit Fruit Jam.
38+
39+
:param default_bg: The path to your default background image file or a hex color.
40+
Defaults to 0x000000.
41+
:param int width: The total width of the display(s) in Pixels. Defaults to 64.
42+
:param int height: The total height of the display(s) in Pixels. Defaults to 32.
43+
:param int bit_depth: The number of bits per color channel. Defaults to 2.
44+
:param list alt_addr_pins: An alternate set of address pins to use. Defaults to None
45+
:param string color_order: A string containing the letter "R", "G", and "B" in the
46+
order you want. Defaults to "RGB"
47+
:param bool Serpentine: Used when panels are arranged in a serpentine pattern rather
48+
than a Z-pattern. Defaults to True.
49+
:param int tiles_rows: Used to indicate the number of rows the panels are arranged in.
50+
Defaults to 1.
51+
:param debug: Turn on debug print outs. Defaults to False.
52+
53+
"""
54+
55+
def __init__(
56+
self,
57+
**kwargs,
58+
):
59+
default_bg = 0x000000
60+
debug = False
61+
if "default_bg" in kwargs:
62+
default_bg = kwargs.pop("default_bg")
63+
if "debug" in kwargs:
64+
debug = kwargs.pop("debug")
65+
66+
if supervisor.runtime.display is None:
67+
request_display_config(640, 480)
68+
super().__init__(supervisor.runtime.display, default_bg=default_bg, debug=debug)

0 commit comments

Comments
 (0)