Skip to content

Commit 729a90e

Browse files
authored
Merge pull request #99 from makermelissa/master
Refactored PyPortal to use PortalBase
2 parents db6f45b + 61bdf96 commit 729a90e

File tree

8 files changed

+935
-1216
lines changed

8 files changed

+935
-1216
lines changed

adafruit_pyportal.py

Lines changed: 0 additions & 1208 deletions
This file was deleted.

adafruit_pyportal/__init__.py

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: Unlicense
4+
"""
5+
`adafruit_pyportal`
6+
================================================================================
7+
8+
CircuitPython driver for Adafruit PyPortal.
9+
10+
* Author(s): Limor Fried, Kevin J. Walters, Melissa LeBlanc-Williams
11+
12+
Implementation Notes
13+
--------------------
14+
15+
**Hardware:**
16+
17+
* `Adafruit PyPortal <https://www.adafruit.com/product/4116>`_
18+
19+
**Software and Dependencies:**
20+
21+
* Adafruit CircuitPython firmware for the supported boards:
22+
https://github.com/adafruit/circuitpython/releases
23+
24+
"""
25+
26+
import os
27+
import gc
28+
import time
29+
import board
30+
import terminalio
31+
import supervisor
32+
from adafruit_portalbase import PortalBase
33+
from adafruit_pyportal.network import Network, CONTENT_JSON, CONTENT_TEXT
34+
from adafruit_pyportal.graphics import Graphics
35+
from adafruit_pyportal.peripherals import Peripherals
36+
37+
if hasattr(board, "TOUCH_XL"):
38+
import adafruit_touchscreen
39+
elif hasattr(board, "BUTTON_CLOCK"):
40+
from adafruit_cursorcontrol.cursorcontrol import Cursor
41+
from adafruit_cursorcontrol.cursorcontrol_cursormanager import CursorManager
42+
43+
__version__ = "0.0.0-auto.0"
44+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PyPortal.git"
45+
46+
47+
class PyPortal(PortalBase):
48+
"""Class representing the Adafruit PyPortal.
49+
50+
:param url: The URL of your data source. Defaults to ``None``.
51+
:param headers: The headers for authentication, typically used by Azure API's.
52+
:param json_path: The list of json traversal to get data out of. Can be list of lists for
53+
multiple data points. Defaults to ``None`` to not use json.
54+
:param regexp_path: The list of regexp strings to get data out (use a single regexp group). Can
55+
be list of regexps for multiple data points. Defaults to ``None`` to not
56+
use regexp.
57+
:param convert_image: Determine whether or not to use the AdafruitIO image converter service.
58+
Set as False if your image is already resized. Defaults to True.
59+
:param default_bg: The path to your default background image file or a hex color.
60+
Defaults to 0x000000.
61+
:param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board
62+
NeoPixel. Defaults to ``None``, not the status LED
63+
:param str text_font: The path to your font file for your data text display.
64+
:param text_position: The position of your extracted text on the display in an (x, y) tuple.
65+
Can be a list of tuples for when there's a list of json_paths, for example
66+
:param text_color: The color of the text, in 0xRRGGBB format. Can be a list of colors for when
67+
there's multiple texts. Defaults to ``None``.
68+
:param text_wrap: Whether or not to wrap text (for long text data chunks). Defaults to
69+
``False``, no wrapping.
70+
:param text_maxlen: The max length of the text for text wrapping. Defaults to 0.
71+
:param text_transform: A function that will be called on the text before display
72+
:param int text_scale: The factor to scale the default size of the text by
73+
:param json_transform: A function or a list of functions to call with the parsed JSON.
74+
Changes and additions are permitted for the ``dict`` object.
75+
:param image_json_path: The JSON traversal path for a background image to display. Defaults to
76+
``None``.
77+
:param image_resize: What size to resize the image we got from the json_path, make this a tuple
78+
of the width and height you want. Defaults to ``None``.
79+
:param image_position: The position of the image on the display as an (x, y) tuple. Defaults to
80+
``None``.
81+
:param image_dim_json_path: The JSON traversal path for the original dimensions of image tuple.
82+
Used with fetch(). Defaults to ``None``.
83+
:param success_callback: A function we'll call if you like, when we fetch data successfully.
84+
Defaults to ``None``.
85+
:param str caption_text: The text of your caption, a fixed text not changed by the data we get.
86+
Defaults to ``None``.
87+
:param str caption_font: The path to the font file for your caption. Defaults to ``None``.
88+
:param caption_position: The position of your caption on the display as an (x, y) tuple.
89+
Defaults to ``None``.
90+
:param caption_color: The color of your caption. Must be a hex value, e.g. ``0x808000``.
91+
:param image_url_path: The HTTP traversal path for a background image to display.
92+
Defaults to ``None``.
93+
:param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used
94+
before calling the pyportal class. Defaults to ``None``.
95+
:param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``.
96+
:param debug: Turn on debug print outs. Defaults to False.
97+
98+
"""
99+
100+
# pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements
101+
def __init__(
102+
self,
103+
*,
104+
url=None,
105+
headers=None,
106+
json_path=None,
107+
regexp_path=None,
108+
convert_image=True,
109+
default_bg=0x000000,
110+
status_neopixel=None,
111+
text_font=terminalio.FONT,
112+
text_position=None,
113+
text_color=0x808080,
114+
text_wrap=False,
115+
text_maxlen=0,
116+
text_transform=None,
117+
text_scale=1,
118+
json_transform=None,
119+
image_json_path=None,
120+
image_resize=None,
121+
image_position=None,
122+
image_dim_json_path=None,
123+
caption_text=None,
124+
caption_font=None,
125+
caption_position=None,
126+
caption_color=0x808080,
127+
image_url_path=None,
128+
success_callback=None,
129+
esp=None,
130+
external_spi=None,
131+
debug=False
132+
):
133+
134+
graphics = Graphics(
135+
default_bg=default_bg,
136+
debug=debug,
137+
)
138+
139+
self._default_bg = default_bg
140+
141+
if external_spi: # If SPI Object Passed
142+
spi = external_spi
143+
else: # Else: Make ESP32 connection
144+
spi = board.SPI()
145+
146+
if image_json_path or image_url_path:
147+
if debug:
148+
print("Init image path")
149+
if not image_position:
150+
image_position = (0, 0) # default to top corner
151+
if not image_resize:
152+
image_resize = (
153+
self.display.width,
154+
self.display.height,
155+
) # default to full screen
156+
157+
network = Network(
158+
status_neopixel=status_neopixel,
159+
esp=esp,
160+
external_spi=spi,
161+
extract_values=False,
162+
convert_image=convert_image,
163+
image_url_path=image_url_path,
164+
image_json_path=image_json_path,
165+
image_resize=image_resize,
166+
image_position=image_position,
167+
image_dim_json_path=image_dim_json_path,
168+
debug=debug,
169+
)
170+
171+
self.url = url
172+
173+
super().__init__(
174+
network,
175+
graphics,
176+
url=url,
177+
headers=headers,
178+
json_path=json_path,
179+
regexp_path=regexp_path,
180+
json_transform=json_transform,
181+
success_callback=success_callback,
182+
debug=debug,
183+
)
184+
185+
# Convenience Shortcuts for compatibility
186+
self.peripherals = Peripherals(
187+
spi, display=self.display, splash_group=self.splash, debug=debug
188+
)
189+
self.set_backlight = self.peripherals.set_backlight
190+
self.sd_check = self.peripherals.sd_check
191+
self.play_file = self.peripherals.play_file
192+
193+
self.image_converter_url = self.network.image_converter_url
194+
self.wget = self.network.wget
195+
# pylint: disable=invalid-name
196+
self.show_QR = self.graphics.qrcode
197+
self.hide_QR = self.graphics.hide_QR
198+
# pylint: enable=invalid-name
199+
200+
if hasattr(self.peripherals, "touchscreen"):
201+
self.touchscreen = self.peripherals.touchscreen
202+
if hasattr(self.peripherals, "mouse_cursor"):
203+
self.mouse_cursor = self.peripherals.mouse_cursor
204+
if hasattr(self.peripherals, "cursor"):
205+
self.cursor = self.peripherals.cursor
206+
207+
# show thank you and bootup file if available
208+
for bootscreen in ("/thankyou.bmp", "/pyportal_startup.bmp"):
209+
try:
210+
os.stat(bootscreen)
211+
for i in range(100, -1, -1): # dim down
212+
self.set_backlight(i / 100)
213+
time.sleep(0.005)
214+
self.set_background(bootscreen)
215+
try:
216+
self.display.refresh(target_frames_per_second=60)
217+
except AttributeError:
218+
self.display.wait_for_frame()
219+
for i in range(100): # dim up
220+
self.set_backlight(i / 100)
221+
time.sleep(0.005)
222+
time.sleep(2)
223+
except OSError:
224+
pass # they removed it, skip!
225+
226+
try:
227+
self.peripherals.play_file("pyportal_startup.wav")
228+
except OSError:
229+
pass # they deleted the file, no biggie!
230+
231+
if default_bg is not None:
232+
self.graphics.set_background(default_bg)
233+
234+
if self._debug:
235+
print("Init caption")
236+
if caption_font:
237+
self._caption_font = self._load_font(caption_font)
238+
self.set_caption(caption_text, caption_position, caption_color)
239+
240+
if text_font:
241+
if text_position is not None and isinstance(
242+
text_position[0], (list, tuple)
243+
):
244+
num = len(text_position)
245+
if not text_wrap:
246+
text_wrap = [0] * num
247+
if not text_maxlen:
248+
text_maxlen = [0] * num
249+
if not text_transform:
250+
text_transform = [None] * num
251+
if not isinstance(text_scale, (list, tuple)):
252+
text_scale = [text_scale] * num
253+
else:
254+
num = 1
255+
text_position = (text_position,)
256+
text_color = (text_color,)
257+
text_wrap = (text_wrap,)
258+
text_maxlen = (text_maxlen,)
259+
text_transform = (text_transform,)
260+
text_scale = (text_scale,)
261+
for i in range(num):
262+
self.add_text(
263+
text_position=text_position[i],
264+
text_font=text_font,
265+
text_color=text_color[i],
266+
text_wrap=text_wrap[i],
267+
text_maxlen=text_maxlen[i],
268+
text_transform=text_transform[i],
269+
text_scale=text_scale[i],
270+
)
271+
else:
272+
self._text_font = None
273+
self._text = None
274+
275+
gc.collect()
276+
277+
def set_caption(self, caption_text, caption_position, caption_color):
278+
# pylint: disable=line-too-long
279+
"""A caption. Requires setting ``caption_font`` in init!
280+
281+
:param caption_text: The text of the caption.
282+
:param caption_position: The position of the caption text.
283+
:param caption_color: The color of your caption text. Must be a hex value, e.g.
284+
``0x808000``.
285+
"""
286+
# pylint: enable=line-too-long
287+
if self._debug:
288+
print("Setting caption to", caption_text)
289+
290+
if (not caption_text) or (not self._caption_font) or (not caption_position):
291+
return # nothing to do!
292+
293+
index = self.add_text(
294+
text_position=caption_position,
295+
text_font=self._caption_font,
296+
text_color=caption_color,
297+
is_data=False,
298+
)
299+
self.set_text(caption_text, index)
300+
301+
def fetch(self, refresh_url=None, timeout=10):
302+
"""Fetch data from the url we initialized with, perfom any parsing,
303+
and display text or graphics. This function does pretty much everything
304+
Optionally update the URL
305+
"""
306+
307+
if refresh_url:
308+
self.url = refresh_url
309+
310+
response = self.network.fetch(self.url, timeout=timeout)
311+
312+
json_out = None
313+
content_type = self.network.check_response(response)
314+
json_path = self._json_path
315+
316+
if content_type == CONTENT_JSON:
317+
if json_path is not None:
318+
# Drill down to the json path and set json_out as that node
319+
if isinstance(json_path, (list, tuple)) and (
320+
not json_path or not isinstance(json_path[0], (list, tuple))
321+
):
322+
json_path = (json_path,)
323+
try:
324+
gc.collect()
325+
json_out = response.json()
326+
if self._debug:
327+
print(json_out)
328+
gc.collect()
329+
except ValueError: # failed to parse?
330+
print("Couldn't parse json: ", response.text)
331+
raise
332+
except MemoryError:
333+
supervisor.reload()
334+
335+
try:
336+
filename, position = self.network.process_image(
337+
json_out, self.peripherals.sd_check()
338+
)
339+
if filename and position is not None:
340+
self.graphics.set_background(filename, position)
341+
except ValueError as error:
342+
print("Error displaying cached image. " + error.args[0])
343+
if self._default_bg is not None:
344+
self.graphics.set_background(self._default_bg)
345+
except KeyError as error:
346+
print("Error finding image data. '" + error.args[0] + "' not found.")
347+
self.set_background(self._default_bg)
348+
349+
if content_type == CONTENT_JSON:
350+
values = self.network.process_json(json_out, json_path)
351+
elif content_type == CONTENT_TEXT:
352+
values = self.network.process_text(response.text, self._regexp_path)
353+
354+
# if we have a callback registered, call it now
355+
if self._success_callback:
356+
self._success_callback(values)
357+
358+
self._fill_text_labels(values)
359+
# Clean up
360+
json_out = None
361+
response = None
362+
gc.collect()
363+
364+
return values

0 commit comments

Comments
 (0)