From ed1f708583a258650358240a3cef52e17fd4b709 Mon Sep 17 00:00:00 2001 From: Joe Scotto Date: Thu, 15 Jan 2026 15:52:37 -0500 Subject: [PATCH] Add ScottoScripts --- .clang-format | 1 + .../OLED Video/create_frames_array.py | 45 +++++++++++++ .../OLED Video/extract_frames.py | 66 +++++++++++++++++++ Extras/ScottoScripts/OLED Video/frames.txt | 1 + Extras/ScottoScripts/OLED Video/oled.c | 37 +++++++++++ Extras/ScottoScripts/OLED Video/readme.md | 32 +++++++++ 6 files changed, 182 insertions(+) create mode 100644 Extras/ScottoScripts/OLED Video/create_frames_array.py create mode 100644 Extras/ScottoScripts/OLED Video/extract_frames.py create mode 100644 Extras/ScottoScripts/OLED Video/frames.txt create mode 100644 Extras/ScottoScripts/OLED Video/oled.c create mode 100644 Extras/ScottoScripts/OLED Video/readme.md diff --git a/.clang-format b/.clang-format index f8d11f2..2f44796 100644 --- a/.clang-format +++ b/.clang-format @@ -3,3 +3,4 @@ IndentCaseLabels: true AllowShortEnumsOnASingleLine: false ColumnLimit: 120 BinPackArguments: false +SortIncludes: false diff --git a/Extras/ScottoScripts/OLED Video/create_frames_array.py b/Extras/ScottoScripts/OLED Video/create_frames_array.py new file mode 100644 index 0000000..77b15a3 --- /dev/null +++ b/Extras/ScottoScripts/OLED Video/create_frames_array.py @@ -0,0 +1,45 @@ +import re + +# Read your exported hex file +with open("frames.txt", "r") as f: + data = f.read() + +frames = [] +current_frame = [] + +for line in data.splitlines(): + # Skip empty lines + if not line.strip(): + continue + + # If line is a frame comment, start a new frame + if line.strip().startswith("// 'out_"): + if current_frame: + frames.append(current_frame) + current_frame = [] + continue + + # Extract all hex numbers in the line + hex_values = re.findall(r"0x[0-9a-fA-F]+", line) + current_frame.extend(hex_values) + +# Append last frame +if current_frame: + frames.append(current_frame) + +# Write formatted array to frames.c and overwrite on subsequent runs +with open("frames.c", "w") as f: + f.write( + "const uint8_t PROGMEM frames[{}][{}] = {{\n".format( + len(frames), len(frames[0]) + ) + ) + + for f_idx, frame in enumerate(frames): + f.write(" {") + f.write(", ".join(frame)) + f.write("}}, // frame {}\n".format(f_idx + 1)) + + f.write("};\n") + +print("Export complete: frames.c with {} frames.".format(len(frames))) diff --git a/Extras/ScottoScripts/OLED Video/extract_frames.py b/Extras/ScottoScripts/OLED Video/extract_frames.py new file mode 100644 index 0000000..4791a39 --- /dev/null +++ b/Extras/ScottoScripts/OLED Video/extract_frames.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import argparse +import os +import subprocess + +# Arguments +parser = argparse.ArgumentParser(description="Extract frames from video for QMK OLED") + +parser.add_argument("input", help="Path to input video file") +parser.add_argument( + "--fps", type=float, default=15, help="Frames per second (default: 15)" +) +parser.add_argument( + "--width", type=int, default=128, help="Output frame width (default: 128)" +) +parser.add_argument( + "--height", type=int, default=64, help="Output frame height (default: 64)" +) +parser.add_argument( + "--frames", + type=int, + default=120, + help="Number of frames to extract (default: 120)", +) +parser.add_argument( + "--format", + default="gray", + choices=["gray", "rgb24", "monow"], + help="Output pixel format (default: gray)", +) +parser.add_argument( + "--speed", + type=float, + default=1.0, + help="Playback speed, maps to setpts (default: 1.0)", +) + +args = parser.parse_args() + +# Create output directory +os.makedirs("frames", exist_ok=True) + +# Create ffmpeg command +output_pattern = os.path.join(args.output, "out_%03d.png") + +# setpts expression: PTS = 1/speed * PTS +setpts_expr = f"setpts={1/args.speed}*PTS" + +ffmpeg_cmd = [ + "ffmpeg", + "-i", + args.input, + "-filter:v", + f"{setpts_expr},fps={args.fps},scale={args.width}:{args.height},format={args.format}", + "-frames:v", + str(args.frames), + output_pattern, +] + +# Print command for debugging +print("Running command:\n", " ".join(ffmpeg_cmd)) + +# Run command +subprocess.run(ffmpeg_cmd, check=True) + +print(f"Frames exported to {args.output}") diff --git a/Extras/ScottoScripts/OLED Video/frames.txt b/Extras/ScottoScripts/OLED Video/frames.txt new file mode 100644 index 0000000..3e349a9 --- /dev/null +++ b/Extras/ScottoScripts/OLED Video/frames.txt @@ -0,0 +1 @@ +Paste your output from Image2CPP here... \ No newline at end of file diff --git a/Extras/ScottoScripts/OLED Video/oled.c b/Extras/ScottoScripts/OLED Video/oled.c new file mode 100644 index 0000000..a853f8c --- /dev/null +++ b/Extras/ScottoScripts/OLED Video/oled.c @@ -0,0 +1,37 @@ +#include "frames.c" +#include "quantum.h" + +#ifdef OLED_ENABLE + +oled_rotation_t oled_init_user(oled_rotation_t rotation) { + return OLED_ROTATION_180; // flips the display 180 degrees if offhand +} + +#define NUM_FRAMES 120 +#define FRAME_WIDTH 128 +#define FRAME_HEIGHT 64 +#define FRAME_SIZE \ + (FRAME_WIDTH * FRAME_HEIGHT / 8) // 128*64 / 8 = 1024 bytes per frame +#define FRAME_DELAY 100 // ms per frame, ~10 fps + +bool oled_task_user(void) { + static uint16_t frame_index = 0; + static uint32_t last_update = 0; + + // Only update frame every FRAME_DELAY ms + if (timer_elapsed32(last_update) > FRAME_DELAY) { + last_update = timer_read32(); + + // Write current frame to OLED (cast to const char* to fix signedness) + oled_write_raw_P((const char *)frames[frame_index], FRAME_SIZE); + + // Advance to next frame, loop back to 0 + frame_index++; + if (frame_index >= NUM_FRAMES) + frame_index = 0; + } + + return false; // keep other OLED content disabled +} + +#endif diff --git a/Extras/ScottoScripts/OLED Video/readme.md b/Extras/ScottoScripts/OLED Video/readme.md new file mode 100644 index 0000000..ac8f75b --- /dev/null +++ b/Extras/ScottoScripts/OLED Video/readme.md @@ -0,0 +1,32 @@ +# OLED Video + +Used to convert a video for display on a QMK OLED. + +This should work on pretty much any QMK keyboard but I highly recommend using a controller with a decent amount of flash memory such as an RP2040. + +# Instructions + +1. Generate your frames using [`extract_frames.py`](#extract_frames.py) + - They will output to the `frames` directory. +2. Upload those to [Image2CPP](https://javl.github.io/image2cpp/) + 1. Choose your dithering mode if you want to play with the look. + 2. Output should be set to `Draw mode: Vertical - 1 bit per pixel` + 3. Click `Generate code` and copy the output to `frames.txt`. +3. Generate your `frames.c` using [create_frames_array.py](create_frames_array.py). + 1. Copy `frames.c` and `oled.c` to your QMK keymap. + 2. Import `oled.c` into your `keymap.c`. + 3. Compile and flash. +4. You should now see a video playing on your OLED! + +# extract_frames.py + +This is the script used to generate the video frames you will use to convert. +| Argument | Type | Default | Description | +| ---------- | ------- | ---------- | ------------------------------------------------- | +| `input` | `str` | _required_ | Path to the input video file. | +| `--fps` | `float` | `15` | Playback frames per second. | +| `--width` | `int` | `128` | Width of the output frames in pixels. | +| `--height` | `int` | `64` | Height of the output frames in pixels. | +| `--frames` | `int` | `120` | Number of frames to extract from the video. | +| `--format` | `str` | `gray` | Output pixel format: `gray`, `rgb24`, or `monow`. | +| `--speed` | `float` | `1.0` | Playback speed of the video. |