|
15 | 15 |
|
16 | 16 | **Hardware:** |
17 | 17 |
|
18 | | -* `Adafruit Fruit Jam <url>`_" |
| 18 | +* `Adafruit Fruit Jam <https://www.adafruit.com/product/6200>`_ |
19 | 19 |
|
20 | 20 | **Software and Dependencies:** |
21 | 21 |
|
|
29 | 29 | __version__ = "0.0.0+auto.0" |
30 | 30 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_FruitJam.git" |
31 | 31 |
|
| 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 |
32 | 46 | from adafruit_fruitjam.peripherals import Peripherals |
33 | 47 |
|
34 | 48 |
|
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 | + |
37 | 161 | self.peripherals = Peripherals() |
38 | 162 |
|
| 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 | + |
39 | 340 | @property |
40 | 341 | def neopixels(self): |
41 | 342 | return self.peripherals.neopixels |
|
0 commit comments