diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-02 20:34:45 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-02 20:34:45 +0200 |
| commit | 0687e489e0e2b273f8c3c466ad77cb2066bc168f (patch) | |
| tree | 8a1b7481b7e475fe1ea96f208f5abc2887003b6e | |
| download | ak820-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-- | .gitignore | 1 | ||||
| -rw-r--r-- | AGENTS.md | 49 | ||||
| -rw-r--r-- | CLAUDE.md | 6 | ||||
| -rw-r--r-- | Cargo.lock | 7 | ||||
| -rw-r--r-- | Cargo.toml | 8 | ||||
| -rw-r--r-- | LICENSE | 338 | ||||
| -rw-r--r-- | README.md | 57 | ||||
| -rw-r--r-- | src/main.rs | 186 |
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] @@ -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); + } +} |
