aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-02 20:34:45 +0200
committerDanilo M. <danix@danix.xyz>2026-10-02 20:34:45 +0200
commit0687e489e0e2b273f8c3c466ad77cb2066bc168f (patch)
tree8a1b7481b7e475fe1ea96f208f5abc2887003b6e
downloadak820-upload-0687e489e0e2b273f8c3c466ad77cb2066bc168f.tar.gz
ak820-upload-0687e489e0e2b273f8c3c466ad77cb2066bc168f.zip
Add ak820-upload: AK820 Pro screen uploader over hidraw
Std-only Rust CLI. ffmpeg decodes any image/GIF/video to 128x128 RGB565LE frames; the upload follows a USB capture of the official Windows software: report-ID-0 feature reports each followed by a 64-byte GET_REPORT, IMAGE sub 0x02 with chunk count, 4096-byte chunks on interface 2 paced by the keyboard's acks on EP 0x84, then SAVE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--.gitignore1
-rw-r--r--AGENTS.md49
-rw-r--r--CLAUDE.md6
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml8
-rw-r--r--LICENSE338
-rw-r--r--README.md57
-rw-r--r--src/main.rs186
8 files changed, 652 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..d5f5081
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,49 @@
+# AGENTS.md
+
+Guidance for AI coding agents working in this repository.
+
+## What this is
+
+`ak820-upload`: single-file Rust CLI (`src/main.rs`, std only, no crates)
+that writes an animation to the AJAZZ AK820 Pro screen over Linux hidraw.
+ffmpeg (subprocess) decodes any input to raw RGB565LE frames; the program
+builds the payload and drives the upload. `README.md` documents the protocol.
+
+On the maintainer's machine the release binary is linked into PATH, so a
+`cargo build --release` is live immediately:
+
+ ~/bin/ak820-upload -> target/release/ak820-upload
+
+## Protocol facts (verified on hardware, do not "simplify" away)
+
+The protocol comes from a USB capture of the official Windows app. Earlier
+attempts based on community projects (gohv, ajazz-ak820-config) failed in
+instructive ways:
+
+- The 64-byte GET_REPORT after every SET_REPORT is required. Without it the
+ keyboard accepts the bytes and does nothing. A zero-length GET is rejected
+ by the kernel with EINVAL and never reaches the device.
+- Control reports go out as report ID 0 with `04` as the first data byte
+ (`wValue 0x0300`), not report ID 4.
+- IMAGE sub-command is `0x02`. Chunks are plain 4096 bytes, no filler or
+ short packet (gohv's 4123-byte chunks misalign every other chunk).
+- Chunk acks arrive on endpoint 0x84, which belongs to interface 2 (the data
+ interface), not interface 3. Wait for each ack before the next chunk.
+- Frame delay byte is in 2 ms units (app value 50 -> 0x19, 250 -> 0x7d).
+- A still image is just a 1-frame animation; there is no separate path.
+
+Interfaces by `HID_PHYS` suffix: `/input2` = data (EP 0x03 OUT, 0x84 IN),
+`/input3` = control.
+
+## Testing
+
+`cargo test` checks the packet and payload layout against the captured
+bytes. `cargo clippy --release` must stay clean. A real check needs the
+keyboard: upload a few distinct, numbered frames and watch them cycle on the
+GIF page. If an upload is interrupted the keyboard shows "loading NN%";
+replug it before retrying.
+
+## Repo facts
+
+- License: GPL-2.0-only (`LICENSE`, SPDX header in `src/main.rs`).
+- Companion repo: `video2ak820` (bash, video to GIF).
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..0d86339
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,6 @@
+# CLAUDE.md
+
+This file is intentionally thin. AGENTS.md is the single source of truth for
+project guidance, shared across every agent tool. Edit AGENTS.md instead.
+
+@AGENTS.md
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..fb78042
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ak820-upload"
+version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..9337d11
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "ak820-upload"
+version = "0.1.0"
+edition = "2024"
+description = "Upload images, GIFs and videos to the AJAZZ AK820 Pro screen over hidraw"
+license = "GPL-2.0-only"
+
+[dependencies]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9efa6fb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,338 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Moe Ghoul>, 1 April 1989
+ Moe Ghoul, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..680e683
--- /dev/null
+++ b/README.md
@@ -0,0 +1,57 @@
+# ak820-upload
+
+Upload an image, GIF or video to the 128x128 TFT screen of the AJAZZ AK820 Pro
+keyboard from Linux, without the official Windows software or a browser.
+
+Any input ffmpeg can read works. It is center-cropped to a square, scaled to
+128x128, resampled to the chosen frame rate and capped at 255 frames, then
+written to the keyboard's stored animation (the "GIF" page, Fn + knob press
+to switch pages).
+
+## Requirements
+
+- Rust (build only, no crates)
+- ffmpeg at runtime
+- the keyboard on the USB-C cable in wired mode (VID:PID `0c45:8009`)
+- read/write access to its `/dev/hidraw*` nodes (desktop sessions usually
+ grant this through `uaccess`; otherwise add a udev rule for `0c45:8009`)
+
+## Build and usage
+
+```sh
+cargo build --release
+./target/release/ak820-upload <image|gif|video> [fps]
+```
+
+`fps` defaults to 15 and must be between 2 and 50: the keyboard stores each
+frame's delay in 2 ms units in one byte, so no frame can last longer than
+about 510 ms. At 15 fps the 255-frame limit gives about 17 seconds.
+
+## Protocol
+
+Taken from a USB capture of the official software (v1.0.0.5):
+
+1. On interface 3, feature reports with report ID 0 and 64 bytes of data:
+ `04 18` (start), then `04 72 02 .. .. .. .. .. <chunks lo> <chunks hi>`.
+ Every SET_REPORT is followed by a 64-byte GET_REPORT, without which the
+ keyboard ignores the upload.
+2. On interface 2 (interrupt OUT endpoint 0x03), the payload in 4096-byte
+ chunks. The keyboard acks each chunk with `01 5a 02` on endpoint 0x84, and
+ the next chunk is only sent after the ack.
+3. `04 02` (save) on interface 3.
+
+Payload: a 256-byte header (frame count, one delay byte per frame in 2 ms
+units, 0xFF padding), then every frame as 128x128 RGB565 little-endian, padded
+with 0xFF to a whole number of chunks.
+
+## License
+
+GPL-2.0-only. See [LICENSE](LICENSE).
+
+## Development Approach
+
+This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback.
+
+All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions.
+
+The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability.
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..fe2e00d
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,186 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// Upload an image, GIF or video to the AJAZZ AK820 Pro 128x128 screen over
+// Linux hidraw. The protocol below was taken from a USB capture of the
+// official Windows software (v1.0.0.5) uploading a 3-frame GIF.
+
+use std::fs::{File, OpenOptions};
+use std::io::{Read, Write};
+use std::os::unix::io::AsRawFd;
+use std::process::{Command, exit};
+use std::thread::sleep;
+use std::time::Duration;
+
+const HID_ID: &str = "HID_ID=0003:00000C45:00008009";
+const CONTROL_IFACE: &str = "/input3";
+const DATA_IFACE: &str = "/input2";
+
+const FRAME_BYTES: usize = 128 * 128 * 2; // RGB565 little-endian
+const CHUNK: usize = 4096; // sent as 64 full USB packets, no short packet
+const HEADER: usize = 256; // frame count + one delay byte per frame, 0xFF padded
+const MAX_FRAMES: usize = 255; // frame count is a u8 in the header
+// Captured: 50 ms in the app -> 0x19, 250 ms -> 0x7d. Max ~510 ms per frame.
+const DELAY_UNIT_MS: u32 = 2;
+
+const CMD_START: u8 = 0x18;
+const CMD_IMAGE: u8 = 0x72;
+const CMD_SAVE: u8 = 0x02;
+
+unsafe extern "C" {
+ fn ioctl(fd: i32, req: u64, ...) -> i32;
+ fn poll(fds: *mut PollFd, n: u64, timeout_ms: i32) -> i32;
+}
+
+#[repr(C)]
+struct PollFd {
+ fd: i32,
+ events: i16,
+ revents: i16,
+}
+
+// HIDIOC[SG]FEATURE(len) = _IOC(_IOC_READ|_IOC_WRITE, 'H', 0x06/0x07, len)
+fn hidioc(nr: u64, len: usize) -> u64 {
+ (3 << 30) | ((len as u64) << 16) | ((b'H' as u64) << 8) | nr
+}
+
+fn die(msg: impl std::fmt::Display) -> ! {
+ eprintln!("ak820-upload: {msg}");
+ exit(1)
+}
+
+/// Find the keyboard's hidraw node for one USB interface.
+fn open_iface(iface: &str) -> File {
+ let dir = std::fs::read_dir("/sys/class/hidraw").unwrap_or_else(|e| die(e));
+ for entry in dir.flatten() {
+ let uevent = std::fs::read_to_string(entry.path().join("device/uevent")).unwrap_or_default();
+ if uevent.contains(HID_ID) && uevent.lines().any(|l| l.starts_with("HID_PHYS=") && l.ends_with(iface)) {
+ let dev = format!("/dev/{}", entry.file_name().to_string_lossy());
+ return OpenOptions::new().read(true).write(true).open(&dev)
+ .unwrap_or_else(|e| die(format!("{dev}: {e}")));
+ }
+ }
+ die("AK820 Pro not found. Connect it with the USB-C cable in wired mode.")
+}
+
+/// Feature report buffer: report ID 0, then the 64-byte packet
+/// [0x04, cmd, sub, 0, 0, 0, 0, 0, lo, hi, 0...].
+fn control(cmd: u8, sub: u8, arg: u16) -> [u8; 65] {
+ let mut p = [0u8; 65];
+ p[1] = 0x04;
+ p[2] = cmd;
+ p[3] = sub;
+ p[9] = arg as u8;
+ p[10] = (arg >> 8) as u8;
+ p
+}
+
+/// SET_REPORT, then the 64-byte GET_REPORT read-back the app always follows
+/// it with. Without the read-back the keyboard ignores the upload.
+fn send_control(ctl: &File, pkt: [u8; 65]) {
+ let fd = ctl.as_raw_fd();
+ if unsafe { ioctl(fd, hidioc(0x06, pkt.len()), pkt.as_ptr()) } < 0 {
+ die(format!("control packet {:#04x}: {}", pkt[2], std::io::Error::last_os_error()));
+ }
+ sleep(Duration::from_millis(40));
+ let mut buf = [0u8; 65]; // buf[0] = report ID 0
+ if unsafe { ioctl(fd, hidioc(0x07, buf.len()), buf.as_mut_ptr()) } < 0 {
+ die(format!("read-back after {:#04x}: {}", pkt[2], std::io::Error::last_os_error()));
+ }
+ sleep(Duration::from_millis(40));
+}
+
+/// Wait for the keyboard's per-chunk ack (an input report on the control
+/// endpoint 0x84 of the data interface, `01 5a 02 ...`).
+fn wait_ack(dat: &mut File, chunk: usize) {
+ let mut pfd = PollFd { fd: dat.as_raw_fd(), events: 1, revents: 0 };
+ if unsafe { poll(&mut pfd, 1, 2000) } <= 0 {
+ die(format!("no ack from the keyboard for chunk {chunk}"));
+ }
+ let mut buf = [0u8; 64];
+ let _ = dat.read(&mut buf);
+}
+
+/// Header + frames, padded with 0xFF to whole chunks.
+fn payload(frames: &[u8], delay_ms: u32) -> Vec<u8> {
+ let n = frames.len() / FRAME_BYTES;
+ let mut out = vec![0xffu8; HEADER];
+ out[0] = n as u8;
+ out[1..=n].fill((delay_ms / DELAY_UNIT_MS).clamp(1, 255) as u8);
+ out.extend_from_slice(frames);
+ out.resize(out.len().div_ceil(CHUNK) * CHUNK, 0xff);
+ out
+}
+
+/// Decode any ffmpeg-readable input to raw 128x128 RGB565LE frames.
+fn decode(input: &str, fps: u32) -> Vec<u8> {
+ let vf = format!("fps={fps},crop='min(iw,ih)':'min(iw,ih)',scale=128:128:flags=lanczos");
+ let out = Command::new("ffmpeg")
+ .args(["-v", "error", "-i", input, "-vf", &vf, "-frames:v"])
+ .arg(MAX_FRAMES.to_string())
+ .args(["-f", "rawvideo", "-pix_fmt", "rgb565le", "-"])
+ .output()
+ .unwrap_or_else(|e| die(format!("ffmpeg: {e}")));
+ if !out.status.success() || out.stdout.len() < FRAME_BYTES {
+ die(format!("ffmpeg failed on {input}: {}", String::from_utf8_lossy(&out.stderr).trim()));
+ }
+ out.stdout
+}
+
+fn main() {
+ let args: Vec<String> = std::env::args().skip(1).collect();
+ let (input, fps) = match args.as_slice() {
+ [i] => (i.as_str(), 15),
+ [i, f] => (i.as_str(), f.parse().ok().filter(|f| (2..=50).contains(f))
+ .unwrap_or_else(|| die("fps must be 2..50 (max frame time is ~510 ms)"))),
+ _ => die("usage: ak820-upload <image|gif|video> [fps, default 15]"),
+ };
+
+ let frames = decode(input, fps);
+ let n = frames.len() / FRAME_BYTES;
+ let data = payload(&frames, 1000 / fps);
+ let chunks = data.len() / CHUNK;
+
+ let ctl = open_iface(CONTROL_IFACE);
+ let mut dat = open_iface(DATA_IFACE);
+ eprintln!("uploading {n} frame(s), {chunks} chunks");
+
+ // START -> IMAGE sub 2 (chunk count) -> chunks -> SAVE.
+ send_control(&ctl, control(CMD_START, 0, 0));
+ send_control(&ctl, control(CMD_IMAGE, 2, chunks as u16));
+
+ let mut report = vec![0u8; 1 + CHUNK]; // leading 0 = unnumbered output report
+ for (i, chunk) in data.chunks(CHUNK).enumerate() {
+ report[1..].copy_from_slice(chunk);
+ dat.write_all(&report).unwrap_or_else(|e| die(format!("chunk {i}: {e}")));
+ wait_ack(&mut dat, i);
+ eprint!("\r{}/{chunks}", i + 1);
+ }
+ eprintln!();
+
+ send_control(&ctl, control(CMD_SAVE, 0, 0));
+ eprintln!("done");
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn layout() {
+ // Bytes as captured from the official app.
+ let c = control(CMD_IMAGE, 2, 0x19);
+ assert_eq!(&c[..11], &[0, 0x04, 0x72, 2, 0, 0, 0, 0, 0, 0x19, 0]);
+
+ // 3 frames at 50 ms: 25 chunks, header 03 19 19 19 ff.
+ let three = payload(&vec![0u8; 3 * FRAME_BYTES], 50);
+ assert_eq!(three.len(), 25 * CHUNK);
+ assert_eq!(&three[..5], &[3, 0x19, 0x19, 0x19, 0xff]);
+ assert_eq!(three[HEADER], 0);
+ assert_eq!(three[HEADER + 3 * FRAME_BYTES], 0xff);
+
+ let one = payload(&vec![0u8; FRAME_BYTES], 66);
+ assert_eq!(&one[..3], &[1, 33, 0xff]);
+ assert_eq!(one.len(), 9 * CHUNK);
+ }
+}