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