-
-
Save Gadgetoid/8ede61a26899ccf894146c6ac8d8fb61 to your computer and use it in GitHub Desktop.
Python scripts to set animations and clock on a Chilkey ND75
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from PIL import Image | |
| import numpy | |
| import sys | |
| import math | |
| import pathlib | |
| import struct | |
| try: | |
| import usb | |
| except ImportError: | |
| print("Did you forget to `unzip usb.zip`?") | |
| sys.exit(0) | |
| WIDTH = 135 | |
| HEIGHT = 240 | |
| BYTES_PER_PIXEL = 2 # RGB565 | |
| BYTES_PER_FRAME = WIDTH * HEIGHT * BYTES_PER_PIXEL | |
| BYTES_PER_PACKET = 4096 # Probably matches the flash erase block size | |
| HEADER_SIZE = 256 | |
| DEBUG = False | |
| VID = 0x36b5 | |
| PID = 0x2ba7 | |
| DATA_EP = 0x03 | |
| if len(sys.argv) < 2: | |
| print(f"Usage: {sys.argv[0]} image_file") | |
| sys.exit(1) | |
| image_file = pathlib.Path(sys.argv[1]) | |
| if not image_file.is_file(): | |
| print(f"Image file \"{image_file}\" does not exist?") | |
| sys.exit(1) | |
| img = Image.open(image_file) | |
| def img_to_rgb565(img, w, h): | |
| data = img.convert("RGB").resize((w, h)).tobytes() | |
| data = numpy.array(list(data), dtype=numpy.uint16) | |
| rgb = (data[0::3] & 0b11111000) << 8 | |
| rgb |= (data[1::3] & 0b11111100) << 3 | |
| rgb |= (data[2::3] & 0b11111000) >> 3 | |
| return rgb.tobytes() | |
| def extract_frames(img, w, h): | |
| while True: | |
| try: | |
| # Duration is in 2ms intervals, with a maximum of 255 (510ms) | |
| time = (img.info["duration"] // 2) & 0xff | |
| yield img_to_rgb565(img, w, h), time | |
| img.seek(img.tell() + 1) | |
| except EOFError: | |
| break | |
| if getattr(img, "is_animated", False): | |
| frames = list(extract_frames(img, WIDTH, HEIGHT)) | |
| else: | |
| frames = [img_to_rgb565(img)] | |
| num_frames = len(frames) | |
| if num_frames > 59: | |
| print(f"Animation has too many frames! {num_frames} > 59") | |
| sys.exit(1) | |
| dev = usb.core.find(idVendor=VID, idProduct=PID) | |
| if dev is None: | |
| print("ND75 not found!") | |
| sys.exit(1) | |
| if dev.is_kernel_driver_active(0): | |
| dev.detach_kernel_driver(0) | |
| def hexstr(data): | |
| return ",".join(f"{c:02x}" for c in data) | |
| def send_feature_report(data): | |
| payload = bytearray(64) | |
| payload[0:len(data)] = data | |
| if DEBUG: | |
| print(f"Command: {hexstr(data)}") | |
| dev.ctrl_transfer(0x21, 0x09, 0x300, 0, payload) | |
| response = dev.ctrl_transfer(0xA1, 0x01, 0x300, 0, 64) | |
| if DEBUG: | |
| print(f"Response: {hexstr(response)}") | |
| return response | |
| # The first 256 bytes of the image are the frame count and duration values | |
| # Then we have the WIDTH * HEIGHT * BYTES_PER_PIXEL multiplied by the number of frames | |
| size_needed = WIDTH * HEIGHT * num_frames * BYTES_PER_PIXEL + HEADER_SIZE | |
| # We need to send full 4096 byte packets, these are probably the flash erase block size | |
| # The official software pads them with 0xFF and we should maybe do that | |
| size_rounded = math.ceil(size_needed / BYTES_PER_PACKET) * BYTES_PER_PACKET | |
| # The number of packets to send | |
| num_packets = size_rounded // BYTES_PER_PACKET | |
| print(f"Sending {image_file.name}: {num_frames} frame(s) as {num_packets} packets...") | |
| # Standard command start (same as set clock) | |
| send_feature_report(bytearray((4, 24))) | |
| # Start the transfer and send the number of packets expected | |
| send_feature_report(struct.pack("<8bH", 4, 114, 2, 0, 0, 0, 0, 0, num_packets)) | |
| # Allocate a buffer big enough for our frames and header | |
| image = bytearray(size_rounded) | |
| # Fill out the padding byte, presumably 0xff is the flash erase value so this saves us some flash write wear? | |
| for i in range(0, len(image)): | |
| image[i] = 0xff | |
| # Add the number of frames to the header | |
| image[0] = num_frames | |
| # Copy the frame durations and data into our buffer | |
| index = 0 | |
| for frame, time in frames: | |
| image[1 + index] = time # frame duration (in 2ms increments I think) | |
| offset = HEADER_SIZE + (index * BYTES_PER_FRAME) | |
| image[offset:offset + BYTES_PER_FRAME] = frame | |
| index += 1 | |
| # Chunk the image and write it to the display | |
| total_packets = len(image) // BYTES_PER_PACKET | |
| for n in range(total_packets): | |
| print(f"Packet: {n + 1} / {total_packets} ({(n + 1) / total_packets * 100:0.2f}%)", end="\r") | |
| offset = n * BYTES_PER_PACKET | |
| # For subsequent frames of data | |
| if n > 0: | |
| send_feature_report(bytearray((4, 2))) | |
| dev.write(DATA_EP, image[offset:offset + BYTES_PER_PACKET], 30 * 1000) | |
| response = dev.read(0x84, 64, timeout=30 * 1000) | |
| if DEBUG: | |
| print(f"Response: {hexstr(response)}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import sys | |
| from datetime import datetime | |
| try: | |
| import usb | |
| except ImportError: | |
| print("Did you forget to `unzip usb.zip`?") | |
| sys.exit(0) | |
| # From a venv try: sudo --preserve-env=PATH python nd75_clock.py | |
| VID = 0x36b5 | |
| PID = 0x2ba7 | |
| dev = usb.core.find(idVendor=VID, idProduct=PID) | |
| if dev is None: | |
| print("ND75 not found!") | |
| sys.exit(1) | |
| if dev.is_kernel_driver_active(0): | |
| dev.detach_kernel_driver(0) | |
| def send_feature_report(data): | |
| def hexstr(data): | |
| return ",".join(f"{c:02x}" for c in data) | |
| payload = bytearray(64) | |
| payload[0:len(data)] = data | |
| print(f"Command: {hexstr(data)}") | |
| dev.ctrl_transfer(0x21, 0x09, 0x300, 0, payload) | |
| response = dev.ctrl_transfer(0xA1, 0x01, 0x300, 0, 64) | |
| print(f"Response: {hexstr(response)}") | |
| return response | |
| if len(sys.argv) > 1: | |
| dt = datetime.strptime(sys.argv[1], "%y-%m-%dT%H:%M:%S") | |
| else: | |
| dt = datetime.now() | |
| print(f"\nSetting date/time to {dt} - customise with: `{sys.argv[0]} YY-MM-DDTHH:MM:SS`") | |
| # ??? | |
| send_feature_report(bytearray((4, 24))) | |
| # ??? | |
| send_feature_report(bytearray((4, 40, 0, 0, 0, 0, 0, 1))) | |
| # Set the time | |
| payload = bytearray(64) | |
| payload[0] = 0 | |
| payload[1] = 1 | |
| payload[2] = 90 | |
| payload[3] = dt.year - 2000 # Year | |
| payload[4] = dt.month # Month | |
| payload[5] = dt.day # Day | |
| payload[6] = dt.hour # Hours | |
| payload[7] = dt.minute # Minutes | |
| payload[8] = dt.second # Seconds | |
| payload[9] = 0 # Unused | |
| payload[10] = dt.isoweekday() # Day of Week | |
| # ... unused | |
| payload[62] = 170 | |
| payload[63] = 85 | |
| send_feature_report(payload) | |
| # ??? | |
| send_feature_report(bytearray((4, 2))) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You'll need some knowledge of Python if you want to upload images, since it uses the PIL/Pillow and Numpy libraries.