From 7fc8b4b5a574ad0673821a6db3c17b5d9594546c Mon Sep 17 00:00:00 2001 From: vanous Date: Tue, 26 May 2026 21:26:07 +0200 Subject: [PATCH] Screenshot tool and a build flag for conditional screenshot functionality: - Adds the possibility to capture the device screen and save it as a PNG image - Wrap the code behind ENABLE_SCREENSHOT build flag, as per instructions in README --- .gitignore | 1 + .python-version | 1 + README.md | 44 +++++ examples/companion_radio/MyMesh.cpp | 86 ++++++++ examples/companion_radio/MyMesh.h | 10 + pyproject.toml | 10 + src/helpers/ui/SH1106Display.h | 5 + src/helpers/ui/SSD1306Display.h | 5 + tools/README.md | 42 ++++ tools/screenshot.py | 294 ++++++++++++++++++++++++++++ uv.lock | 114 +++++++++++ 11 files changed, 612 insertions(+) create mode 100644 .python-version create mode 100644 pyproject.toml create mode 100644 tools/README.md create mode 100755 tools/screenshot.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 8c229550..5d11de82 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ compile_commands.json venv/ platformio.local.ini CODE_REVIEW.md +tools/pngs/ diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..2c073331 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/README.md b/README.md index bd1323a7..1ef6db9d 100644 --- a/README.md +++ b/README.md @@ -279,3 +279,47 @@ This fork tracks the upstream [MeshCore](https://github.com/ripplebiz/MeshCore) ```sh git config merge.ours.driver true ``` +--- + +#### Display Screenshot Tool + +> **⚠️ Note:** The screenshot feature requires the `ENABLE_SCREENSHOT` build flag to be enabled. Add `-D ENABLE_SCREENSHOT` to your PlatformIO build flags, then recompile and flash the firmware. + +The firmware supports capturing the current display contents and transmitting +it over USB serial. This is useful for debugging, remote monitoring, or +creating documentation. + +**Important:** When the device is connected to the companion app, the USB +serial port is not available for communication. To use the screenshot tool, +ensure the device is not connected to the companion app. + +**Usage:** + +1. Build and flash firmware with `-D ENABLE_SCREENSHOT` build flag enabled + +Example for the OLED dual firmware: + +``` +PLATFORMIO_BUILD_FLAGS="-D ENABLE_SCREENSHOT" pio run -e WioTrackerL1_companion_dual -t upload +``` + +2. Connect the device to your computer via USB (ensure no companion app connection) +3. Install dependencies and run the screenshot tool. To manage python and python dependencies, use uv: https://docs.astral.sh/uv/ + + ```sh + cd tools + uv run tools/screenshot.py + ``` + + Options: + - `--port PORT` — Serial port to use (default: auto-detect) + - `--scale SCALE` — Upscale factor for the output image (1=no upscale, 2=2x, 3=3x, 4=4x, etc.; default: 1) + +4. In the tool's interactive menu, press **S** to capture a screenshot +5. The tool will save the screenshot as a PNG file in `tools/pngs/` with a timestamp-based filename + +**How it works:** +- The tool sends the `CMD_GET_SCREENSHOT` command (66) to the device +- The device responds with `RESP_CODE_SCREENSHOT` (29) containing the framebuffer data +- The framebuffer is transmitted in chunks (128×64 display = 1024 bytes, split across multiple frames) +- The tool reassembles the chunks and converts them to a PNG image diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 06c90786..495ecc3b 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -5,6 +5,13 @@ #include // needed for PlatformIO #include +#ifdef DISPLAY_CLASS +#include "helpers/ui/DisplayDriver.h" +#include "helpers/ui/SH1106Display.h" +#include "helpers/ui/SSD1306Display.h" +#include "UITask.h" +#endif + #define CMD_APP_START 1 #define CMD_SEND_TXT_MSG 2 #define CMD_SEND_CHANNEL_TXT_MSG 3 @@ -63,6 +70,9 @@ #define CMD_SEND_CHANNEL_DATA 62 #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 +#ifdef ENABLE_SCREENSHOT +#define CMD_GET_SCREENSHOT 66 // Request screenshot from display +#endif // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -98,6 +108,9 @@ #define RESP_ALLOWED_REPEAT_FREQ 26 #define RESP_CODE_CHANNEL_DATA_RECV 27 #define RESP_CODE_DEFAULT_FLOOD_SCOPE 28 +#ifdef ENABLE_SCREENSHOT +#define RESP_CODE_SCREENSHOT 29 // Response with screenshot data +#endif #define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9) @@ -2046,12 +2059,85 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(&out_frame[i], &r->upper_freq, 4); i += 4; } _serial->writeFrame(out_frame, i); +#ifdef ENABLE_SCREENSHOT + } else if (cmd_frame[0] == CMD_GET_SCREENSHOT) { + handleScreenshotRequest(); +#endif } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); } } +#ifdef ENABLE_SCREENSHOT +void MyMesh::handleScreenshotRequest() { + #ifdef DISPLAY_CLASS + UITask* ui_task = getUITask(); + if (!ui_task || !ui_task->hasDisplay()) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + return; + } + + DisplayDriver* display = ui_task->getDisplay(); + if (!display) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + return; + } + + uint8_t* buffer = nullptr; + int bufferSize = 0; + + // Try SH1106Display first (used by Wio L1) + // Use C-style cast since RTTI is disabled (-fno-rtti) + SH1106Display* sh1106 = (SH1106Display*)display; + if (sh1106) { + buffer = sh1106->getBuffer(); + bufferSize = (display->width() * display->height()) / 8; + } else { + // Fallback: try SSD1306Display + SSD1306Display* ssd1306 = (SSD1306Display*)display; + if (ssd1306) { + buffer = ssd1306->getBuffer(); + bufferSize = (display->width() * display->height()) / 8; + } + } + + if (buffer && bufferSize > 0) { + sendScreenshotResponse(display, buffer, bufferSize); + } else { + // No supported display type + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } + #else + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + #endif +} + +void MyMesh::sendScreenshotResponse(DisplayDriver* display, uint8_t* buffer, int bufferSize) { + // MAX_FRAME_SIZE is 172 bytes, but we need to send 1026 bytes (3 header + 1024 buffer) + // We need to split into multiple frames + // Frame format: RESP_CODE_SCREENSHOT, width, height, chunk_index, total_chunks, [chunk_data...] + + const int MAX_DATA_PER_FRAME = MAX_FRAME_SIZE - 5; // 5 bytes header + int totalChunks = (bufferSize + MAX_DATA_PER_FRAME - 1) / MAX_DATA_PER_FRAME; + + for (int chunkIdx = 0; chunkIdx < totalChunks; chunkIdx++) { + int i = 0; + out_frame[i++] = RESP_CODE_SCREENSHOT; + out_frame[i++] = display->width(); + out_frame[i++] = display->height(); + out_frame[i++] = chunkIdx; + out_frame[i++] = totalChunks; + + int chunkSize = min(MAX_DATA_PER_FRAME, bufferSize - chunkIdx * MAX_DATA_PER_FRAME); + memcpy(&out_frame[i], buffer + chunkIdx * MAX_DATA_PER_FRAME, chunkSize); + i += chunkSize; + + _serial->writeFrame(out_frame, i); + } +} +#endif // ENABLE_SCREENSHOT + void MyMesh::enterCLIRescue() { _cli_rescue = true; cli_command[0] = 0; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 47378d76..c7aa7366 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -3,6 +3,10 @@ #include #include #include "AbstractUITask.h" +#include + +// Forward declaration for UITask +class UITask; /*------------ Frame Protocol --------------*/ #define FIRMWARE_VER_CODE 11 @@ -223,6 +227,12 @@ private: void checkCLIRescueCmd(); void checkSerialInterface(); bool isValidClientRepeatFreq(uint32_t f) const; +#ifdef ENABLE_SCREENSHOT + void handleScreenshotRequest(); + void sendScreenshotResponse(DisplayDriver* display, uint8_t* buffer, int bufferSize); +#endif + + UITask* getUITask() { return (UITask*)_ui; } // helpers, short-cuts void saveChannels() { _store->saveChannels(this); } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..70e07fb8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "meshcore" +version = "0.1.0" +description = "Managing python dependencies for tools" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "pillow>=12.2.0", + "pyserial>=3.5", +] diff --git a/src/helpers/ui/SH1106Display.h b/src/helpers/ui/SH1106Display.h index e4eed0d5..134db133 100644 --- a/src/helpers/ui/SH1106Display.h +++ b/src/helpers/ui/SH1106Display.h @@ -58,4 +58,9 @@ public: void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override; void setBrightness(uint8_t level) override; void endFrame() override; + +#ifdef ENABLE_SCREENSHOT + // Screenshot support + uint8_t* getBuffer() { return display.getBuffer(); } +#endif }; diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index d843da85..a5ce696e 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -45,4 +45,9 @@ public: void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; uint16_t getTextWidth(const char* str) override; void endFrame() override; + +#ifdef ENABLE_SCREENSHOT + // Screenshot support + uint8_t* getBuffer() { return display.getBuffer(); } +#endif }; diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000..6132fe29 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,42 @@ +#### Display Screenshot Tool + +> **⚠️ Note:** The screenshot feature requires the `ENABLE_SCREENSHOT` build flag to be enabled. Add `-D ENABLE_SCREENSHOT` to your PlatformIO build flags, then recompile and flash the firmware. + +The firmware supports capturing the current display contents and transmitting +it over USB serial. This is useful for debugging, remote monitoring, or +creating documentation. + +**Important:** When the device is connected to the companion app, the USB +serial port is not available for communication. To use the screenshot tool, +ensure the device is not connected to the companion app. + +**Usage:** + +1. Build and flash firmware with `-D ENABLE_SCREENSHOT` build flag enabled + +Example for the OLED dual firmware: + +``` +PLATFORMIO_BUILD_FLAGS="-D ENABLE_SCREENSHOT" pio run -e WioTrackerL1_companion_dual -t upload +``` + +2. Connect the device to your computer via USB (ensure no companion app connection) +3. Install dependencies and run the screenshot tool. To manage python and python dependencies, use uv: https://docs.astral.sh/uv/ + + ```sh + cd tools + uv run tools/screenshot.py + ``` + + Options: + - `--port PORT` — Serial port to use (default: auto-detect) + - `--scale SCALE` — Upscale factor for the output image (1=no upscale, 2=2x, 3=3x, 4=4x, etc.; default: 1) + +4. In the tool's interactive menu, press **S** to capture a screenshot +5. The tool will save the screenshot as a PNG file in `tools/pngs/` with a timestamp-based filename + +**How it works:** +- The tool sends the `CMD_GET_SCREENSHOT` command (66) to the device +- The device responds with `RESP_CODE_SCREENSHOT` (29) containing the framebuffer data +- The framebuffer is transmitted in chunks (128×64 display = 1024 bytes, split across multiple frames) +- The tool reassembles the chunks and converts them to a PNG image diff --git a/tools/screenshot.py b/tools/screenshot.py new file mode 100755 index 00000000..750488dd --- /dev/null +++ b/tools/screenshot.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +""" +Screenshot tool for Wio L1 Tracker Pro with SH1106 display. +Connects via USB serial using mesh companion protocol, captures framebuffer, saves as PNG. + +Usage: + python screenshot.py [--port PORT] [--scale SCALE] + +Options: + --port PORT Serial port to use (default: auto-detect) + --scale SCALE Upscale factor: 1=no upscale, 2=2x, 3=3x, 4=4x, etc. (default: 1) +""" + +import argparse +import serial +import serial.tools.list_ports +from datetime import datetime +from PIL import Image, ImageDraw +import sys +import time +import os + +# Mesh protocol constants +FRAME_START_OUT = 0x3C # '<' - used when sending TO device +FRAME_START_IN = 0x3E # '>' - used when receiving FROM device +CMD_GET_SCREENSHOT = 66 +RESP_CODE_SCREENSHOT = 29 +RESP_CODE_ERR = 1 +DISPLAY_WIDTH = 128 +DISPLAY_HEIGHT = 64 +BUFFER_SIZE = (DISPLAY_WIDTH * DISPLAY_HEIGHT) // 8 # 1024 bytes + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Capture screenshots from Wio L1 Tracker Pro with SH1106 display" + ) + parser.add_argument( + "--port", "-p", help="Serial port to use (default: auto-detect)" + ) + parser.add_argument( + "--scale", + "-s", + type=int, + default=1, + help="Upscale factor: 1=no upscale, 2=2x, 3=3x, 4=4x, etc. (default: 1)", + ) + return parser.parse_args() + + +def find_wio_port(): + """Find Wio Tracker L1 serial port""" + ports = serial.tools.list_ports.comports() + for port in ports: + desc = str(port.description).lower() + if "wio" in desc or "seeed" in desc: + return port.device + + # Fallback: check common port names + for port in ports: + if "ACM" in port.device or port.device.startswith("COM"): + return port.device + return None + + +def read_frame(ser): + """Read a complete frame from the mesh protocol (from device)""" + header = ser.read(3) + if len(header) != 3: + return None + + if header[0] != FRAME_START_IN: + print( + f"Error: Invalid frame start byte: {header[0]:02x} (expected {FRAME_START_IN:02x})" + ) + return None + + frame_len = header[1] | (header[2] << 8) + + if frame_len > 0: + data = bytearray() + while len(data) < frame_len: + chunk = ser.read(frame_len - len(data)) + if not chunk: + print(f"Error: Timeout waiting for frame data") + return None + data.extend(chunk) + return bytes(data) + else: + return b"" + + +def send_command(ser, command, data=b""): + """Send a command frame using mesh protocol (to device)""" + frame_len = len(data) + 1 + header = bytes([FRAME_START_OUT, frame_len & 0xFF, (frame_len >> 8) & 0xFF]) + frame = header + bytes([command]) + data + ser.write(frame) + ser.flush() + return frame_len + 3 + + +def receive_screenshot(ser, timeout=5): + """Receive screenshot data (may be split across multiple frames)""" + start_time = time.time() + buffer_data = bytearray() + width = None + height = None + total_chunks = None + received_chunks = 0 + + while time.time() - start_time < timeout: + frame = read_frame(ser) + if frame is None: + continue + + if len(frame) < 5: + print(f"Error: Frame too short: {len(frame)} bytes") + return None + + resp_code = frame[0] + + if resp_code == RESP_CODE_SCREENSHOT: + w = frame[1] + h = frame[2] + chunk_idx = frame[3] + num_chunks = frame[4] + chunk_data = frame[5:] + + if chunk_idx == 0: + width = w + height = h + total_chunks = num_chunks + buffer_data = bytearray() + received_chunks = 0 + + if width is None or width != w or height != h: + print(f"Error: Inconsistent dimensions in chunk {chunk_idx}") + return None + + if total_chunks is None or total_chunks != num_chunks: + print(f"Error: Inconsistent chunk count in chunk {chunk_idx}") + return None + + buffer_data.extend(chunk_data) + received_chunks += 1 + + if received_chunks >= total_chunks: + expected_size = (width * height) // 8 + if len(buffer_data) == expected_size: + return bytes(buffer_data), width, height + else: + print( + f"Error: Expected {expected_size} bytes, got {len(buffer_data)}" + ) + return None + + elif resp_code == RESP_CODE_ERR: + print("Error: Device returned error") + return None + + else: + continue + + print( + f"Error: Timeout waiting for screenshot (received {received_chunks}/{total_chunks or '?'} chunks)" + ) + return None + + +def buffer_to_image(buffer, width, height, scale=1): + """Convert SH1106/SH1106 framebuffer to PIL Image with white text on black background and black frame""" + image = Image.new("1", (width, height), 0) + pixels = image.load() + + bytes_per_page = width + + for page in range(0, height, 8): + page_start = (page // 8) * bytes_per_page + for col in range(width): + byte_idx = page_start + col + if byte_idx < len(buffer): + byte_val = buffer[byte_idx] + for bit in range(8): + if page + bit < height: + pixel_value = 1 if (byte_val & (1 << bit)) else 0 + pixels[col, page + bit] = pixel_value + + # Convert to RGB: white text on black background (no inversion) + image_rgb = image.convert("RGB") + + # Add 3-pixel black frame around the image + FRAME_WIDTH = 3 + new_width = width + FRAME_WIDTH * 2 + new_height = height + FRAME_WIDTH * 2 + final_image = Image.new("RGB", (new_width, new_height), (0, 0, 0)) + + # Paste the display content in the center + final_image.paste(image_rgb, (FRAME_WIDTH, FRAME_WIDTH)) + + # Upscale if requested + if scale > 1: + final_image = final_image.resize( + (final_image.width * scale, final_image.height * scale), + Image.Resampling.NEAREST, + ) + + return final_image + + +def generate_filename(): + """Generate timestamp filename in local time with seconds""" + now = datetime.now() + os.makedirs("tools/pngs", exist_ok=True) + return os.path.join("tools/pngs", now.strftime("screenshot_%Y-%m-%d_%H-%M-%S.png")) + + +def main(): + args = parse_args() + + port = args.port + if not port: + port = find_wio_port() + + if not port: + print("Error: Could not find Wio Tracker L1 serial port") + print("\nAvailable ports:") + for p in serial.tools.list_ports.comports(): + print(f" {p.device}: {p.description}") + + port = input("\nEnter serial port manually: ") + + print(f"\nConnecting to {port} at 115200 baud (scale: {args.scale}x)...") + + try: + ser = serial.Serial(port, 115200, timeout=3) + print("Connected!\n") + except serial.SerialException as e: + print(f"Error opening serial port: {e}") + sys.exit(1) + + try: + print("Screenshot Tool for Wio L1 Tracker Pro") + print("Press 'S' to capture screenshot, 'Q' to quit\n") + + while True: + try: + key = input("> ").strip().upper() + except (KeyboardInterrupt, EOFError): + print("\nExiting...") + break + + if key == "Q": + print("Exiting...") + break + + if key == "S": + print("Requesting screenshot...") + + ser.reset_input_buffer() + send_command(ser, CMD_GET_SCREENSHOT) + time.sleep(0.1) + + result = receive_screenshot(ser) + if result: + buffer_data, width, height = result + print( + f"Received framebuffer: {width}x{height}, {len(buffer_data)} bytes" + ) + + try: + image = buffer_to_image( + buffer_data, width, height, scale=args.scale + ) + filename = generate_filename() + image.save(filename) + print(f"Saved as {filename} ({image.width}x{image.height})") + except Exception as e: + print(f"Error saving image: {e}") + else: + print("Error: Failed to receive screenshot") + else: + print("Unknown command. Press 'S' for screenshot, 'Q' to quit") + + except KeyboardInterrupt: + print("\nExiting...") + + finally: + ser.close() + print("Connection closed.") + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..b580a12b --- /dev/null +++ b/uv.lock @@ -0,0 +1,114 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "meshcore" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pillow" }, + { name = "pyserial" }, +] + +[package.metadata] +requires-dist = [ + { name = "pillow", specifier = ">=12.2.0" }, + { name = "pyserial", specifier = ">=3.5" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "pyserial" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, +]