├── .gitignore
├── .vscode
└── settings.json
├── CREDITS.md
├── Cargo.toml
├── LICENSE
├── README.md
├── apex-ctl
├── Cargo.toml
└── src
│ └── main.rs
├── apex-engine
├── Cargo.toml
└── src
│ ├── engine.rs
│ └── lib.rs
├── apex-hardware
├── Cargo.toml
├── README.md
└── src
│ ├── device.rs
│ ├── lib.rs
│ └── usb.rs
├── apex-input
├── Cargo.toml
└── src
│ ├── hotkey.rs
│ ├── input.rs
│ └── lib.rs
├── apex-mpris2
├── Cargo.toml
└── src
│ ├── generated.rs
│ ├── lib.rs
│ └── player.rs
├── apex-music
├── Cargo.toml
└── src
│ ├── lib.rs
│ └── player.rs
├── apex-simulator
├── Cargo.toml
└── src
│ ├── lib.rs
│ └── simulator.rs
├── apex-windows
├── Cargo.toml
└── src
│ ├── lib.rs
│ └── music.rs
├── assets
├── btc.bmp
├── discord.bmp
├── gif_missing.gif
├── note.bmp
└── pause.bmp
├── images
└── sample_1.gif
├── resources
├── btc.png
├── music.png
├── simulator-btc.png
├── simulator-clock.png
├── simulator-music.png
└── system-metrics.png
├── rust-toolchain.toml
├── rustfmt.toml
├── settings.toml
└── src
├── dbus
├── mod.rs
└── notifications.rs
├── main.rs
├── providers
├── clock.rs
├── coindesk.rs
├── image.rs
├── mod.rs
├── music.rs
└── sysinfo.rs
└── render
├── debug.rs
├── display.rs
├── image.rs
├── mod.rs
├── notifications.rs
├── scheduler.rs
├── stream.rs
├── text.rs
└── util.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | debug/
4 | target/
5 |
6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8 | Cargo.lock
9 |
10 | # These are backup files generated by rustfmt
11 | **/*.rs.bk
12 |
13 | # MSVC Windows builds of rustc generate these, which store debugging information
14 | *.pdb
15 | .idea/
16 | .env
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "rust-analyzer.cargo.features": ["default", "sysinfo"]
3 | }
4 |
--------------------------------------------------------------------------------
/CREDITS.md:
--------------------------------------------------------------------------------
1 | - ./assets/note.bmp : Based on an [icon](https://freeicons.io/business-and-online-icons/music-icon-icon) by [Raj Dev](https://freeicons.io/profile/714) on [freeicons.io](https://freeicons.io)
2 | - ./assets/pause.bmp : Based on an [icon](https://freeicons.io/business-and-online-icons/pause-icon-icon-3) by [Raj Dev](https://freeicons.io/profile/714) on [freeicons.io](https://freeicons.io)
3 |
4 |
5 | https://freeicons.io/icon-list/business-and-online-icons
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "apex-tux"
3 | version = "1.0.3"
4 | edition = "2021"
5 |
6 | [workspace]
7 |
8 | members = [
9 | "apex-ctl",
10 | "apex-hardware",
11 | "apex-mpris2",
12 | "apex-music",
13 | "apex-simulator",
14 | "apex-input",
15 | "apex-engine",
16 | "apex-windows"
17 | ]
18 |
19 |
20 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
21 |
22 | [dependencies]
23 | apex-hardware = { path = "./apex-hardware", features = ["async"] }
24 |
25 |
26 | anyhow = "1.0.45"
27 | tokio = { version = "1", features = ["time", "net", "macros", "rt-multi-thread", "sync"] }
28 | num_enum = "0.5"
29 | embedded-graphics = "0.7.1"
30 | tinybmp = "0.3.1"
31 | config = { version = "0.11.0", features = ["toml"] }
32 | futures-core = "0.3"
33 | async-stream = "0.3"
34 | futures = "0.3"
35 | linkme = "0.2"
36 | log = "0.4.14"
37 |
38 | ctrlc = "3.2.0"
39 | simplelog = "0.10.0"
40 | pin-project-lite = "0.2.7"
41 | itertools = "0.10.1"
42 | async-rwlock = "1.3.0"
43 | serde = { version = "1.0", optional = true, features = ["derive"] }
44 |
45 | serde_json = { version = "1.0", optional = true }
46 | reqwest = { version = "0.11.4", optional = true, features = ["json", "brotli", "stream", "gzip", "deflate"] }
47 | chrono = "0.4.19"
48 | toml = "0.5.8"
49 | num-traits = "0.2.14"
50 | apex-input = {path = "./apex-input" }
51 | apex-music = { path = "./apex-music" }
52 | apex-simulator = { path = "./apex-simulator", optional = true }
53 | apex-engine = { path = "./apex-engine", optional = true }
54 | sysinfo = { version = "0.27.7", optional = true }
55 | lazy_static = "1.4.0"
56 | image = { version = "0.24.6", optional = true }
57 | dirs = "5.0.1"
58 |
59 |
60 | [target.'cfg(target_os = "windows")'.dependencies]
61 | apex-windows = {path = "./apex-windows"}
62 |
63 |
64 | [target.'cfg(target_os = "linux")'.dependencies]
65 | apex-mpris2 = { path = "./apex-mpris2", optional = true }
66 | dbus = { version = "0.9", optional = true }
67 | dbus-tokio = { version = "0.7.4", optional = true }
68 |
69 | [features]
70 | default = ["dbus-support", "crypto", "usb"]
71 | dbus-support = ["dbus", "dbus-tokio", "apex-mpris2"]
72 | http = ["serde", "serde_json", "reqwest"]
73 | crypto = ["http"]
74 | simulator = ["apex-simulator"]
75 | usb = ["apex-hardware/usb"]
76 | hotkeys = ["apex-input/hotkeys"]
77 | engine = ["apex-engine"]
78 | sysinfo = ["dep:sysinfo"]
79 | image = ["dep:image"]
80 | debug = []
81 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # apex-tux - Linux support for the Apex series OLED screens
2 |
3 | Make use of your OLED screen instead of letting the SteelSeries logo burn itself in :-)
4 |
5 | ## Screenshots
6 |
7 | 
8 | 
9 | 
10 |
11 | 
12 | 
13 | 
14 |
15 | ## Features
16 |
17 | - Music player integration (requires DBus)
18 | - Discord notifications (requires DBus)
19 | - Bitcoin price
20 | - Clock
21 | - System metrics
22 | - Scrolling text
23 | - No burn-in from constantly displaying a static image
24 |
25 | ## Supported media players
26 |
27 | - [Lollypop](https://gitlab.gnome.org/World/lollypop) (tested)
28 | - Firefox (Results may vary)
29 | - Chromium / Chrome (Results may vary)
30 | - mpv
31 | - Telegram
32 | - VLC
33 | - Spotify
34 |
35 | Source: [Arch Wiki](https://wiki.archlinux.org/title/MPRIS#Supported_clients)
36 |
37 | ## Supported devices
38 |
39 | This currently supports the following devices:
40 |
41 | - Apex Pro
42 | - Apex 5
43 | - Apex 7
44 |
45 | Other devices may be compatible and all that is needed is to add the ID to apex-hardware/src/usb.rs.
46 |
47 | ## Installation
48 |
49 | For installing this software, follow these steps:
50 |
51 | ### UDev
52 |
53 | 1. Get the device id: `lsusb | grep "SteelSeries Apex"`:
54 |
55 | ```shell
56 | $ lsusb | grep "SteelSeries Apex"
57 | Bus 001 Device 002: ID 1038:1610 SteelSeries ApS SteelSeries Apex Pro
58 | ```
59 |
60 | The **id** is the right part of the ID.
61 |
62 | 2. Enter the following data from [here](https://gist.github.com/ToadKing/d26f8f046a3b707e9e4b9821be5c9efc) (Shoutout [to @ToadKing](https://github.com/ToadKing)).
63 |
64 | If those don't work and lead to an "Access denied" error please try the following rules and save the rules as `97-steelseries.rules`:
65 |
66 | ```shell
67 | cat /etc/udev/rules.d/97-steelseries.rules
68 | SUBSYSTEM=="input", GROUP="input", MODE="0666"
69 |
70 | SUBSYSTEM=="usb", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="", MODE="0666", GROUP="plugdev"
71 | KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="", MODE="0666", GROUP="plugdev"
72 | ```
73 |
74 | 1. Replace the `ATTRS{idProduct}==` value with the device **id**.
75 |
76 | 2. Save all files to `/etc/udev/rules.d/97-steelseries.rules`.
77 |
78 | 3. Finally, reload the `udev` rules: `sudo udevadm control --reload && sudo udevadm trigger`
79 |
80 | ### Rust
81 |
82 | - Install Rust **nightly** using [rustup](https://rustup.rs/)
83 | - Install required dependencies
84 | - For Ubuntu: `sudo apt install libssl-dev libdbus-1-dev libusb-1.0-0-dev`
85 | - Clone the repository: `git clone git@github.com:not-jan/apex-tux.git`
86 | - Change the directory into the repository: `cd apex-tux`
87 | - Compile the app using the features you want
88 | - If you **don't** run DBus you have to disable the dbus feature: `cargo build --release --no-default-features --features crypto,usb`
89 | - Otherwise just run `cargo build --release --features sysinfo,hotkeys,image`
90 | - If you **don't** have an Apex device around at the moment or want to develop more easily you can enable the simulator: `cargo build --release --no-default-features --features crypto,clock,dbus-support,simulator`
91 |
92 | ## Configuration
93 |
94 | The default configuration is in `settings.toml`.
95 | This repository ships with a default configuration that covers most parts and contains documentation for the important keys.
96 | The program will look for configuration first in the platform-specific `$USER_CONFIG_DIR/apex-tux/`, then in the current working directory.
97 | You can also override specific settings with `APEX_*` environment variables.
98 |
99 | You can also run the software to find errors on configuration and to decide what is the right setup you need:
100 |
101 | ```shell
102 | $ target/release/apex-tux
103 | 23:43:05 [INFO] Registering MPRIS2 display source.
104 | 23:43:05 [INFO] Registering Sysinfo display source.
105 | 23:43:05 [WARN] Couldn't find network interface `eth0`
106 | 23:43:05 [INFO] Instead, found those interfaces:
107 | 23:43:05 [INFO] lo
108 | 23:43:05 [INFO] wlp3s0
109 | 23:43:05 [INFO] enp2s0
110 | 23:43:05 [INFO] docker0
111 | 23:43:05 [WARN] Couldn't find sensor `hwmon0 CPU Temperature`
112 | 23:43:05 [INFO] Instead, found those sensors:
113 | 23:43:05 [INFO] acpitz temp1: 67°C (max: 67°C / critical: 120°C)
114 | 23:43:05 [INFO] amdgpu edge: 47°C (max: 47°C)
115 | 23:43:05 [INFO] iwlwifi_1 temp1: 39°C (max: 39°C)
116 | 23:43:05 [INFO] k10temp Tctl: 66.5°C (max: 66.5°C)
117 | 23:43:05 [INFO] nvme Composite HFM001TD3JX013N temp1: 36.85°C (max: 36.85°C / critical: 84.85°C)
118 | 23:43:05 [INFO] nvme Composite Samsung SSD 980 PRO 1TB temp1: 32.85°C (max: 32.85°C / critical: 84.85°C)
119 | 23:43:05 [INFO] nvme Sensor 1 HFM001TD3JX013N temp2: 36.85°C (max: 36.85°C)
120 | 23:43:05 [INFO] nvme Sensor 1 Samsung SSD 980 PRO 1TB temp2: 32.85°C (max: 32.85°C)
121 | 23:43:05 [INFO] nvme Sensor 2 HFM001TD3JX013N temp3: 43.85°C (max: 43.85°C)
122 | 23:43:05 [INFO] nvme Sensor 2 Samsung SSD 980 PRO 1TB temp3: 38.85°C (max: 38.85°C)
123 | 23:43:05 [INFO] Registering Clock display source.
124 | 23:43:05 [INFO] Registering Gif display source.
125 | 23:43:05 [INFO] Registering Coindesk display source.
126 | 23:43:05 [INFO] Registering DBUS notification source.
127 | 23:43:05 [INFO] Found 5 registered providers
128 | 23:43:05 [INFO] Trying to connect to DBUS with player preference: Some("spotify")
129 | 23:43:05 [INFO] Trying to connect to DBUS with player preference: Some("spotify")
130 | 23:43:05 [INFO] Connected to music player: "org.mpris.MediaPlayer2.spotify"
131 | ```
132 |
133 | In our case we need to set a right value for the sensor(`acpitz temp1`, critical temperatured one, i.e., cpu) and the network interface(`wlp3s0`, wifi) in the `[sysinfo]` section.
134 |
135 | You can set your default media player on the `[mpris2]` section.
136 |
137 |
138 | ## Usage
139 |
140 | Simply run the binary under `target/release/apex-tux` and make sure the settings.toml is in your current directory.
141 | The output should look something like this:
142 |
143 | ```shell
144 | 23:18:14 [INFO] Registering Coindesk display source.
145 | 23:18:14 [INFO] Registering Clock display source.
146 | 23:18:14 [INFO] Registering MPRIS2 display source.
147 | 23:18:14 [INFO] Registering DBUS notification source.
148 | 23:18:14 [INFO] Found 3 registered providers
149 | 23:18:14 [INFO] Trying to connect to DBUS with player preference: Some("Lollypop")
150 | 23:18:18 [INFO] Trying to connect to DBUS with player preference: Some("Lollypop")
151 | 23:18:18 [INFO] Connected to music player: "org.mpris.MediaPlayer2.Lollypop"
152 | 23:34:01 [INFO] Ctrl + C received, shutting down!
153 | 23:34:01 [INFO] unregister hotkey ALT+SHIFT+A
154 | 23:34:01 [INFO] unregister hotkey ALT+SHIFT+D
155 | ```
156 |
157 | You may change sources by pressing **Alt+Shift+A** or **Alt+Shift+D** (This might not work on Wayland). The simulator uses the arrow keys.
158 |
159 | ## Autostarting
160 |
161 | To start on boot the binary must be started under an interactive daemon, i.e. by your Desktop Environment. A systemd service will fail unless compiled without hotkey support. Most DEs support the following method/path but you may have to find your equivalent.
162 |
163 | -Create `apex-tux.desktop` in `~/.config/autostart`
164 | -Edit `apex-tux.desktop` to contain:
165 | ```shell
166 | [Desktop Entry]
167 | Exec=/path/to/apex-tux/apex-tux
168 | Name=apex-tux
169 | Path=/path/to/apex-tux
170 | Terminal=true
171 | Type=Application
172 | ```
173 |
174 | ## Development
175 |
176 | If you have a feature to add or a bug to fix please feel free to open an issue or submit a pull request.
177 |
178 | ## TODO
179 |
180 | - Windows support
181 | - Test this on more than one Desktop Environment on X11
182 | - More providers
183 | - Games?
184 | - GIFs?
185 | - Change the USB crate to something async instead
186 | - Add documentation on how to add custom providers
187 | - Switch from GATs to async traits once there here
188 | - Add support for more notifications
189 | - Package this up for Debian/Arch/Flatpak etc.
190 |
191 | ## Windows support ETA, when?
192 |
193 | I've written a stub for SteelSeries Engine support on Windows, there is an [API for mediaplayer metadata](https://microsoft.github.io/windows-docs-rs/doc/windows/Media/Control/struct.GlobalSystemMediaTransportControlsSessionManager.html) but my time is kind of limited and I don't run Windows all that often.
194 | It will happen eventually but it's not a priority.
195 |
196 | ## Why nightly Rust?
197 |
198 | Way too many cool features to pass up on :D
199 |
--------------------------------------------------------------------------------
/apex-ctl/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "apex-ctl"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | anyhow = "1.0.44"
10 | clap = { version = "4.0.26", features = ["derive"] }
11 | log = "0.4.14"
12 | simplelog = "0.10.2"
13 | apex-hardware = { path = "../apex-hardware", features= ["usb"] }
--------------------------------------------------------------------------------
/apex-ctl/src/main.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use apex_hardware::{Device, USBDevice};
3 | use clap::{ArgAction, Parser, Subcommand};
4 | use log::{info, LevelFilter};
5 | use simplelog::{Config as LoggerConfig, SimpleLogger};
6 |
7 | #[derive(Parser)]
8 | #[clap(version = "1.0", author = "not-jan")]
9 | struct Opts {
10 | /// A level of verbosity, and can be used multiple times
11 | #[arg(short, long, action = ArgAction::Count)]
12 | verbose: u8,
13 | #[command(subcommand)]
14 | subcmd: SubCommand,
15 | }
16 |
17 | #[derive(Subcommand)]
18 | enum SubCommand {
19 | /// Clear the OLED screen
20 | Clear,
21 | /// Fill the OLED screen
22 | Fill,
23 | }
24 |
25 | fn main() -> Result<()> {
26 | let opts: Opts = Opts::parse();
27 |
28 | let filter = match opts.verbose {
29 | 0 => LevelFilter::Info,
30 | 1 => LevelFilter::Debug,
31 | _ => LevelFilter::Trace,
32 | };
33 |
34 | SimpleLogger::init(filter, LoggerConfig::default())?;
35 |
36 | info!("Connecting to the USB device");
37 |
38 | let mut device = USBDevice::try_connect()?;
39 |
40 | match opts.subcmd {
41 | SubCommand::Clear => device.clear()?,
42 | SubCommand::Fill => device.fill()?,
43 | };
44 |
45 | Ok(())
46 | }
47 |
--------------------------------------------------------------------------------
/apex-engine/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "apex-engine"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | gamesense = { git = "https://github.com/not-jan/gamesense" }
10 | anyhow = "1"
11 | apex-hardware = { path = "../apex-hardware", features = ["async"] }
12 | tokio = {version = "1", features=["time", "net", "macros", "rt-multi-thread", "sync"]}
13 | log = "0.4.14"
14 |
--------------------------------------------------------------------------------
/apex-engine/src/engine.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use apex_hardware::{AsyncDevice, FrameBuffer};
3 | use gamesense::raw_client::{
4 | BindGameEvent, FrameContainer, GameEvent, Heartbeat, RawGameSenseClient, RegisterGame,
5 | RemoveEvent, RemoveGame, Screen, ScreenFrameData, ScreenHandler, Sendable,
6 | };
7 | use std::future::Future;
8 |
9 | use log::info;
10 | const GAME: &str = "APEXTUX";
11 | const EVENT: &str = "SCREEN";
12 |
13 | const REGISTER_GAME: RegisterGame = RegisterGame {
14 | game: GAME,
15 | display_name: Some("apex-tux"),
16 | developer: Some("not-jan"),
17 | timeout: None,
18 | };
19 |
20 | pub const REMOVE_EVENT: RemoveEvent = RemoveEvent {
21 | game: GAME,
22 | event: EVENT,
23 | };
24 |
25 | pub const REMOVE_GAME: RemoveGame = RemoveGame { game: GAME };
26 |
27 | pub const HEARTBEAT: Heartbeat = Heartbeat { game: GAME };
28 |
29 | #[derive(Debug, Clone)]
30 | pub struct Engine {
31 | client: RawGameSenseClient,
32 | }
33 |
34 | impl Engine {
35 | pub async fn new() -> Result {
36 | let client = RawGameSenseClient::new()?;
37 |
38 | info!("{}", REGISTER_GAME.send(&client).await?);
39 |
40 | let x = BindGameEvent {
41 | game: GAME,
42 | event: EVENT,
43 | min_value: None,
44 | max_value: None,
45 | icon_id: None,
46 | value_optional: Some(true),
47 | handlers: vec![ScreenHandler {
48 | device: "screened-128x40",
49 | mode: "screen",
50 | zone: "one",
51 | datas: vec![Screen {
52 | has_text: false,
53 | image_data: vec![0u8; 640],
54 | }],
55 | }],
56 | }
57 | .send(&client)
58 | .await?;
59 | info!("{}", x);
60 |
61 | Ok(Self { client })
62 | }
63 |
64 | pub async fn heartbeat(&self) -> Result<()> {
65 | info!("{}", HEARTBEAT.send(&self.client).await?);
66 | Ok(())
67 | }
68 | }
69 |
70 | impl AsyncDevice for Engine {
71 | type ClearResult<'a> = impl Future