Initial kinda working OSC->DDC server

This commit is contained in:
Sam W 2022-04-07 15:23:53 +00:00
commit 3a997c0f7b
9 changed files with 501 additions and 0 deletions

5
.envrc Normal file
View File

@ -0,0 +1,5 @@
if ! has nix_direnv_version || ! nix_direnv_version 1.3.0; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/1.3.0/direnvrc" "sha256-1VWM1BnI1GvclYBky5f5Y9HqeThmQUwCWQbsFQM1Eu0="
fi
use flake

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
.direnv
result

172
Cargo.lock generated Normal file
View File

@ -0,0 +1,172 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "ddc"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba69f2c53e320fc4abad17cb02bbbf04d1a36f18e9907f347589ec5991b3c6c5"
dependencies = [
"mccs",
]
[[package]]
name = "ddc-i2c"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66503057bd41fc21b532b3ebe33b2ec57e5d4971fcfc3844306ebcb499b6c8c2"
dependencies = [
"ddc",
"i2c",
"i2c-linux",
"resize-slice",
]
[[package]]
name = "ddcmqtt"
version = "0.1.0"
dependencies = [
"ddc",
"ddc-i2c",
"rosc",
]
[[package]]
name = "i2c"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60c7b7bdd7b3a985fdcf94a0d7d98e7a47fde8b7f22fb55ce1a91cc104a2ce9a"
dependencies = [
"bitflags",
]
[[package]]
name = "i2c-linux"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0268a871aaa071221d6c2875ebedcf64710e59b0d87c68c8faf5e98b87dd2a4"
dependencies = [
"bitflags",
"i2c",
"i2c-linux-sys",
"resize-slice",
"udev",
]
[[package]]
name = "i2c-linux-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b70fd3d30157850c87f4c7c09c0857d9c2e8c996bcbe23ec1299126e0479ec"
dependencies = [
"bitflags",
"byteorder",
"libc",
]
[[package]]
name = "libc"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"
[[package]]
name = "libudev-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "mccs"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74366c6da4179141e0d4551a46799a7e667a68eda60561690d5048bd8e0f8422"
dependencies = [
"void",
]
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "pkg-config"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
[[package]]
name = "resize-slice"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a3cb2f74a9891e76958b9e0ccd269a25b466c3ae3bb3efd71db157248308c4a"
dependencies = [
"uninitialized",
]
[[package]]
name = "rosc"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d9177134b9f1158370b6df30dd4f711ba07a6b3f6bfbab35b3bbbf1bf58cbcc"
dependencies = [
"byteorder",
"nom",
]
[[package]]
name = "udev"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47504d1a49b2ea1b133e7ddd1d9f0a83cf03feb9b440c2c470d06db4589cf301"
dependencies = [
"libc",
"libudev-sys",
]
[[package]]
name = "uninitialized"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74c1aa4511c38276c548406f0b1f5f8b793f000cfb51e18f278a102abd057e81"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"

18
Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "ddcmqtt"
version = "0.1.0"
edition = "2021"
[dependencies]
ddc = "*"
ddc-i2c = { version = "0.2.1", features = [ "with-linux-enumerate" ] }
rosc = "*"
[[bin]]
name = "ddcmqtt"
path = "src/bin/ddcmqtt.rs"
[[bin]]
name = "setmon"
path = "src/bin/setmon.rs"

123
flake.lock Normal file
View File

@ -0,0 +1,123 @@
{
"nodes": {
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1647857022,
"narHash": "sha256-Aw70NWLOIwKhT60MHDGjgWis3DP3faCzr6ap9CSayek=",
"owner": "numtide",
"repo": "devshell",
"rev": "0a5ff74dacb9ea22614f64e61aeb3ca0bf0e7311",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1642700792,
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1648297722,
"narHash": "sha256-W+qlPsiZd8F3XkzXOzAoR+mpFqzm3ekQkJNa+PIh1BQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "0f8662f1319ad6abf89b3380dd2722369fc51ade",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1648544490,
"narHash": "sha256-EoBDcccV70tfz2LAs5lK0BjC7en5mzUVlgLsd5E6DW4=",
"owner": "nix-community",
"repo": "naersk",
"rev": "e30ef9a5ce9b3de8bb438f15829c50f9525ca730",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1643381941,
"narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1641687203,
"narHash": "sha256-W6Xrb/l1x+E+WMVLw4q5HUnNjt3x4WQFSYJtjJtopbU=",
"path": "/nix/store/8hd2049cr4dz9myjcjjh6cjjapz1p107-source",
"rev": "00acdb2aa817048fbe1f91ece18fe7de09762531",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1641687203,
"narHash": "sha256-W6Xrb/l1x+E+WMVLw4q5HUnNjt3x4WQFSYJtjJtopbU=",
"path": "/nix/store/8hd2049cr4dz9myjcjjh6cjjapz1p107-source",
"rev": "00acdb2aa817048fbe1f91ece18fe7de09762531",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils_2",
"naersk": "naersk",
"nixpkgs": "nixpkgs_3"
}
}
},
"root": "root",
"version": 7
}

35
flake.nix Normal file
View File

@ -0,0 +1,35 @@
{
description = "virtual environments";
inputs.devshell.url = "github:numtide/devshell";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.naersk.url = "github:nix-community/naersk";
outputs = { self, flake-utils, devshell, nixpkgs, naersk }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages."${system}";
naersk-lib = naersk.lib."${system}";
in {
defaultPackage = naersk-lib.buildPackage {
pname = "ddcmqtt";
root = ./.;
nativeBuildInputs = with pkgs; [ pkgconfig libudev ];
};
/* devshell currently not really working for stuff that needs pkgconfig/libs
devShell =
let pkgs = import nixpkgs {
inherit system;
overlays = [ devshell.overlay ];
};
in
pkgs.devshell.mkShell {
packages = with pkgs; [ rustc cargo gcc ];
nativeBuildInputs = with pkgs; [ libudev pkgconfig ];
};
*/
devshell = pkgs.mkShell{
nativeBuildInputs = with pkgs; [ rustc cargo libudev pkgconfig ];
};
});
}

7
src/bin/ddcmqtt.rs Normal file
View File

@ -0,0 +1,7 @@
use ::ddcmqtt::{main as main_, StdError};
fn main() -> StdError<()> {
main_()
}

23
src/bin/setmon.rs Normal file
View File

@ -0,0 +1,23 @@
use ddc_i2c::{I2cDeviceEnumerator, I2cDeviceDdc};
use ddc::Ddc;
use ::ddcmqtt::{StdError};
fn main() -> StdError<()> {
let args_: Vec<_> = std::env::args().skip(1).collect();
let args: Vec<&str> = args_.iter().map(|s| &**s).collect();
match args.as_slice() {
["brightness", arg] => ddc_all(0x10, arg.parse::<u16>()?),
["input", arg] => ddc_all(0x60, arg.parse::<u16>()?),
_ => Err("Bad usage".into()),
}?;
Ok(())
}
fn ddc_all(feat: u8, val: u16) -> StdError<()> {
for mut display in I2cDeviceEnumerator::new()? {
display.set_vcp_feature(feat, val)?;
}
Ok(())
}

115
src/lib.rs Normal file
View File

@ -0,0 +1,115 @@
#![allow(unreachable_code)]
use ddc::Ddc;
use ddc_i2c::{I2cDeviceEnumerator, I2cDeviceDdc};
use rosc;
use std::net::UdpSocket;
use std::thread;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::{Duration, Instant};
use std::collections::HashMap;
use std::error::Error;
pub type StdError<T> = Result<T, Box<dyn Error>>;
enum Command {
Monitor((usize, MonitorCommand)),
}
enum MonitorCommand {
Brightness(u8),
Input(u8)
}
impl MonitorCommand {
fn cmd_str(&self) -> &'static str {
match self {
Self::Brightness(_) => "brightness",
Self::Input(_) => "input",
}
}
fn vcp(&self) -> u8 {
match self {
Self::Brightness(_) => 0x10,
Self::Input(_) => 0x60,
}
}
}
// Run an i2c command handler for a specific i2c device, rate limiting each type of command
fn run_i2c(mut dev: I2cDeviceDdc, command_channel: Receiver<MonitorCommand>) {
let mut last_sent_command: HashMap<&str, Option<Instant>> = HashMap::new();
loop {
let cmd = command_channel.recv().unwrap();
if let Some(last) = last_sent_command.entry(cmd.cmd_str()).or_insert(None) {
if (*last + Duration::from_millis(100)) > Instant::now() {
// rate limit
continue;
}
}
match cmd {
MonitorCommand::Brightness(b) => dev.set_vcp_feature(0x10, b.into()).unwrap(),
// Hack - add 15 to align with DELL monitors
MonitorCommand::Input(i) => dev.set_vcp_feature(0x60, (i+15).into()).unwrap(),
}
last_sent_command.insert(cmd.cmd_str(), Some(Instant::now()));
}
}
pub fn main() -> StdError<()> {
let displays = I2cDeviceEnumerator::new().unwrap().collect::<Vec<_>>();
println!("Enumerated {} displays", displays.len());
let txes: Vec<_> = displays.into_iter().map(|d| {
let (tx, rx) = channel();
thread::spawn(move|| run_i2c(d, rx));
tx
}).collect();
let sock = UdpSocket::bind("0.0.0.0:1234").unwrap();
let mut buf = [0u8; rosc::decoder::MTU];
loop {
let (size, addr) = sock.recv_from(&mut buf).unwrap();
println!("Got {} bytes from {}", size, addr);
let (_, pack) = rosc::decoder::decode_udp(&buf[..size]).unwrap();
match pack {
rosc::OscPacket::Message(msg) => {
match osc_message_to_command(msg) {
Ok(cmd) => if let Err(e) = handle_cmd(cmd, &txes) {
println!("Error handling command: {:?}", e);
},
Err(e) => println!("Unrecognised OSC command: {:?}", e),
};
}
rosc::OscPacket::Bundle(bundle) => {
println!("OSC Bundle: {:?}", bundle);
}
}
}
Ok(())
}
fn handle_cmd(cmd: Command, txes: &Vec<Sender<MonitorCommand>>) -> StdError<()> {
match cmd {
Command::Monitor((idx, c)) => txes.get(idx).ok_or("Bad monitor index")?.send(c)?,
};
Ok(())
}
fn osc_message_to_command(msg: rosc::OscMessage) -> StdError<Command> {
println!("OSC: {}, args: {:?}", msg.addr, msg.args);
let splitaddr: Vec<_> = msg.addr.split("/").collect();
match &splitaddr[1..] {
["monitor", idx, control] => {
println!("Monitor {}, control {}, args {:?}", idx, control, msg.args);
let command = match *control {
"brightness" => Some(MonitorCommand::Brightness((msg.args[0].clone().float().unwrap() * 100.0) as u8)),
"input" => Some(MonitorCommand::Input((msg.args[0].clone().int().unwrap()) as u8)),
_ => None,
}.ok_or(format!("Unrecognised monitor control: {}", *control))?;
let idx = idx.parse::<usize>().or(Err("Bad monitor index"))?;
Ok(Command::Monitor((idx, command)))
},
_ => Err("Unsupported osc address, ignoring".into()),
}
}