~jojo/huelia

551c9aa01f280e96d109dcaafe6e7aa6303eef9e — JoJo 3 months ago 8485c9c
adjustable current limit per device & fix current reduction coloring
M Cargo.lock => Cargo.lock +49 -51
@@ 130,7 130,7 @@ dependencies = [
 "regex",
 "rustc-hash",
 "shlex",
 "syn",
 "syn 1.0.109",
 "which",
]



@@ 179,7 179,7 @@ dependencies = [
 "once_cell",
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 443,7 443,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "scratch",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 460,7 460,7 @@ checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 483,7 483,7 @@ dependencies = [
 "ident_case",
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 494,7 494,7 @@ checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
 "darling_core",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 517,7 517,7 @@ dependencies = [
 "proc-macro-error",
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 695,7 695,7 @@ dependencies = [
 "darling",
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 782,6 782,12 @@ dependencies = [
]

[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"

[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 803,15 809,6 @@ dependencies = [
]

[[package]]
name = "find-crate"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
dependencies = [
 "toml",
]

[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 895,7 892,7 @@ checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 1146,7 1143,7 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"

[[package]]
name = "huelia-conductor"
version = "0.2.0"
version = "0.3.0"
dependencies = [
 "hostname",
 "http",


@@ 1164,7 1161,7 @@ dependencies = [

[[package]]
name = "huelia-performer"
version = "0.2.0"
version = "0.3.0"
dependencies = [
 "anyhow",
 "embedded-hal 1.0.0-alpha.9",


@@ 1545,7 1542,7 @@ dependencies = [
 "proc-macro-crate",
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 1580,12 1577,12 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"

[[package]]
name = "palette"
version = "0.6.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f9cd68f7112581033f157e56c77ac4a5538ec5836a2e39284e65bd7d7275e49"
checksum = "e1641aee47803391405d0a1250e837d2336fdddd18b27f3ddb8c1d80ce8d7f43"
dependencies = [
 "approx",
 "num-traits",
 "fast-srgb8",
 "palette_derive",
 "phf",
 "serde",


@@ 1593,14 1590,13 @@ dependencies = [

[[package]]
name = "palette_derive"
version = "0.6.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05eedf46a8e7c27f74af0c9cfcdb004ceca158cb1b918c6f68f8d7a549b3e427"
checksum = "3c02bfa6b3ba8af5434fa0531bf5701f750d983d4260acd6867faca51cdc4484"
dependencies = [
 "find-crate",
 "proc-macro2",
 "quote",
 "syn",
 "syn 2.0.18",
]

[[package]]


@@ 1645,7 1641,7 @@ dependencies = [
 "phf_shared",
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 1674,7 1670,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 1720,7 1716,7 @@ dependencies = [
 "proc-macro-error-attr",
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
 "version_check",
]



@@ 1737,9 1733,9 @@ dependencies = [

[[package]]
name = "proc-macro2"
version = "1.0.52"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224"
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
dependencies = [
 "unicode-ident",
]


@@ 1752,9 1748,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"

[[package]]
name = "quote"
version = "1.0.26"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
dependencies = [
 "proc-macro2",
]


@@ 2093,7 2089,7 @@ checksum = "d7e29c4601e36bcec74a223228dce795f4cd3616341a4af93520ca1a837c087d"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 2252,7 2248,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "rustversion",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 2265,7 2261,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "rustversion",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 2280,6 2276,17 @@ dependencies = [
]

[[package]]
name = "syn"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "tempfile"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 2318,7 2325,7 @@ checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 2411,7 2418,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
]

[[package]]


@@ 2463,15 2470,6 @@ dependencies = [
]

[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
 "serde",
]

[[package]]
name = "toml_datetime"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 2737,7 2735,7 @@ dependencies = [
 "once_cell",
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
 "wasm-bindgen-shared",
]



@@ 2759,7 2757,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 1.0.109",
 "wasm-bindgen-backend",
 "wasm-bindgen-shared",
]

M Cargo.toml => Cargo.toml +2 -2
@@ 4,13 4,13 @@ default-members = ["conductor"]

[workspace.package]
authors = ["JoJo <jo@jo.zone>"]
version = "0.2.0"
version = "0.3.0"
edition = "2021"

[workspace.dependencies]
log = { version = "0.4", features = ["max_level_debug", "release_max_level_info"] }
microcrisp = { git = "https://git.sr.ht/~jojo/microcrisp" }
palette = "0.6.1"
palette = "0.7.2"
simple_logger = "4.0"

[patch.crates-io]

M conductor/src/main.rs => conductor/src/main.rs +21 -3
@@ 86,6 86,14 @@ async fn main() {
                            n_leds.to_string())
                        .await
                        .unwrap(),
                    Cmd::SetCurrentLimit { performer, limit_ma } => mqtt_client
                        .publish(
                            format!("huelia/performer/{performer}/set-current-limit"),
                            QoS::ExactlyOnce,
                            false,
                            limit_ma.to_string())
                        .await
                        .unwrap(),
                    Cmd::Identify(performer) => mqtt_client
                        .publish(
                            format!("huelia/performer/{performer}/color"),


@@ 167,6 175,13 @@ async fn main() {
            .and(warp::body::json())
            .then(move |performer, n_leds| send_cmd1(Cmd::SetNumLeds { performer, n_leds }));
        let send_cmd1 = send_cmd.clone();
        let performer_set_current_limit = warp::post()
            .and(warp::path("performer"))
            .and(warp::path::param())
            .and(warp::path("current-limit"))
            .and(warp::body::json())
            .then(move |performer, limit_ma| send_cmd1(Cmd::SetCurrentLimit { performer, limit_ma }));
        let send_cmd1 = send_cmd.clone();
        let performer_identify = warp::post()
            .and(warp::path("performer"))
            .and(warp::path::param())


@@ 185,6 200,7 @@ async fn main() {
            .or(eval)
            .or(performer_set_prefix)
            .or(performer_set_n_leds)
            .or(performer_set_current_limit)
            .or(performer_identify)
            .or(get_performers);
        warp::serve(routes).run(([0, 0, 0, 0], 8558))


@@ 205,7 221,7 @@ async fn main() {
                        }
                    };
                    match_expr!(expr, {
                        ('online, prefix, i(n_leds), str(ver)) => {
                        ('online, prefix, i(n_leds), str(ver), i(current_limit_ma)) => {
                            let prefix = match_expr!(prefix, {
                                ('some, str(prefix)) => Some(prefix.to_owned()),
                                'none => None,


@@ 214,8 230,8 @@ async fn main() {
                                    continue
                                }
                            });
                            log::info!("performer {} (v{ver}) came online", performer);
                            performers.lock().unwrap().insert(performer.to_owned(), Performer { name: performer.to_owned(), prefix, n_leds: *n_leds as usize, version: ver.to_owned() });
                            log::info!("performer {} came online. ver = {ver}, limit = {current_limit_ma} mA", performer);
                            performers.lock().unwrap().insert(performer.to_owned(), Performer { name: performer.to_owned(), prefix, n_leds: *n_leds as usize, version: ver.to_owned(), current_limit_ma: *current_limit_ma as u16 });
                        },
                        'offline => {
                            log::info!("performer {} went offline", performer);


@@ 240,6 256,7 @@ struct Performer {
    prefix: Option<String>,
    n_leds: usize,
    version: String,
    current_limit_ma: u16,
}

enum Refresh {


@@ 265,6 282,7 @@ enum Cmd {
    Cycle(Cycle),
    SetNamePrefix { performer: String, prefix: String },
    SetNumLeds { performer: String, n_leds: u16 },
    SetCurrentLimit { performer: String, limit_ma: u16 },
    Identify(String),
    Eval(f32, Lisp),
}

M conductor/static/main.css => conductor/static/main.css +12 -0
@@ 79,3 79,15 @@ input[type=range] {
    margin: 20px;
    border-radius: 10px;
}
#performers > div > section > form {
    display: flex;
}
#performers > div > section > form > label {
    margin-right: 0.5em;
}
#performers > div > section > form > input {
    text-align: right;
}
#performers > div > section > form > .grow {
    flex-grow: 1;
}

M conductor/static/main.js => conductor/static/main.js +8 -2
@@ 77,15 77,21 @@ function fetchPerformers() {
                    let set_prefix = document.createElement("form");
                    set_prefix.action = `/performer/${performer.name}/prefix`;
                    set_prefix.addEventListener("submit", postFormSingletonJson);
                    set_prefix.innerHTML = `<label for="prefix">prefix</label> <input type="text" name="prefix" value="${performer.prefix}"> <input type="submit" value="set">`;
                    set_prefix.innerHTML = `<label for="prefix">prefix</label> <input type="text" name="prefix" value="${performer.prefix}" class="grow"> <input type="submit" value="set">`;
                    section.appendChild(set_prefix);

                    let set_n_leds = document.createElement("form");
                    set_n_leds.action = `/performer/${performer.name}/n-leds`;
                    set_n_leds.addEventListener("submit", postFormSingletonJson);
                    set_n_leds.innerHTML = `<label for="n-leds">n leds</label> <input type="number" name="n_leds" min="0" max="65535" value="${performer.n_leds}"> <input type="submit" value="set">`;
                    set_n_leds.innerHTML = `<label for="n-leds">n leds</label> <input type="number" name="n_leds" min="0" max="65535" value="${performer.n_leds}" class="grow"> <input type="submit" value="set">`;
                    section.appendChild(set_n_leds);

                    let set_current_limit = document.createElement("form");
                    set_current_limit.action = `/performer/${performer.name}/current-limit`;
                    set_current_limit.addEventListener("submit", postFormSingletonJson);
                    set_current_limit.innerHTML = `<label for="current-limit">mA limit</label> <input type="number" name="current_limit" min="0" max="5000" value="${performer.current_limit_ma}" class="grow"> <input type="submit" value="set">`;
                    section.appendChild(set_current_limit);

                    let identify = document.createElement("button");
                    identify.addEventListener("click", (event) => {
                        fetch(new Request(`/performer/${performer.name}/identify`, {

M performer/Makefile => performer/Makefile +1 -1
@@ 29,7 29,7 @@ force-publish: Cargo.toml release.bin
	wait

release.bin: elf-release partitions.csv
	espflash save-image --chip esp32c3 release.bin target/riscv32imc-esp-espidf/release/huelia-performer
	espflash save-image --chip esp32c3 release.bin ../target/riscv32imc-esp-espidf/release/huelia-performer

debug.bin: elf-debug partitions.csv
	espflash save-image --chip esp32c3 debug.bin ../target/riscv32imc-esp-espidf/debug/huelia-performer

M performer/bin/crate-version-lisp.py => performer/bin/crate-version-lisp.py +1 -1
@@ 2,6 2,6 @@

import sys, tomllib

semver = [int(x) for x in tomllib.load(open("Cargo.toml", "rb"))["package"]["version"].split(".")]
semver = [int(x) for x in tomllib.load(open("../Cargo.toml", "rb"))["workspace"]["package"]["version"].split(".")]
[major, minor, patch] = semver
print(f"(version {major} {minor} {patch})")

M performer/src/main.rs => performer/src/main.rs +48 -8
@@ 13,7 13,7 @@ use esp_idf_svc::{
// If using the `binstart` feature of `esp-idf-sys`, always keep this module imported
use esp_idf_sys::ESP_ERR_NVS_INVALID_LENGTH;
use microcrisp::{match_expr, try_match_expr, Lisp};
use palette::{rgb::channels, FromColor, Srgb};
use palette::{rgb::channels, Srgb};
use simple_logger::SimpleLogger;
use smart_leds::SmartLedsWrite;
use std::{


@@ 105,6 105,21 @@ fn main_() -> Result<()> {
        }
    };
    log::info!("n leds: {n_leds}");
    let current_limit_ma = {
        let mut buf = [0u8; 2];
        let default_limit_ma = 250u16;
        match storage.get_raw("current-limit", &mut buf) {
            Ok(Some(_)) => u16::from_le_bytes(buf),
            Ok(None) => default_limit_ma,
            Err(e) if e.code() == ESP_ERR_NVS_INVALID_LENGTH => {
                log::error!("invalid length error when reading \"current-limit\" from nvs. The field length might've changed between updates. Setting it to the default value.");
                storage.set_raw("current-limit", &default_limit_ma.to_le_bytes())?;
                restart()
            }
            Err(e) => Err(e)?,
        }
    };
    log::info!("current limit mA: {current_limit_ma}");

    // == HTTP client ==
    {


@@ 121,6 136,9 @@ fn main_() -> Result<()> {
    let color_chan_full1 = color_chan_full.clone();
    let color_chan_full2 = color_chan_full.clone();

    let ma_per_channel = 20.0f32;
    let limit_ma_per_led = current_limit_ma as f32 / n_leds as f32;

    log::info!("starting display thread");
    std::thread::Builder::new()
        .stack_size(4096)


@@ 132,14 150,16 @@ fn main_() -> Result<()> {
                let was_full = color_chan_full1.swap(false, Relaxed);
                match color_rx.try_recv() {
                    Ok(color) => {
                        let mut color = palette::Hsl::from_color(color.into_format::<f32>());
                        let a_per_led = 0.06;
                        if n_leds as f32 * a_per_led * color.lightness > 2.0 {
                            color.lightness = 2.0 / (n_leds as f32 * a_per_led);
                        let mut color: Srgb<f32> = color.into_format();
                        let ma_per_led = ma_per_channel * (color.red + color.green + color.blue);
                        if ma_per_led > limit_ma_per_led {
                            color *= limit_ma_per_led / ma_per_led;
                        }
                        let color = Srgb::from_color(color).into_format();
                        led_strip
                            .write(std::iter::repeat(smart_leds::RGB::from(color.into_components())).take(n_leds))
                            .write(
                                std::iter::repeat(smart_leds::RGB::from(color.into_format().into_components()))
                                    .take(n_leds),
                            )
                            .ok();
                        if was_full {
                            too_long_delay = delay;


@@ 177,6 197,7 @@ fn main_() -> Result<()> {
    log::info!("setting up mqtt");
    let storage1 = Arc::new(Mutex::new(storage));
    let storage2 = storage1.clone();
    let storage3 = storage1.clone();
    let name_prefix1 = name_prefix.clone();
    let color_tx1 = color_tx.clone();
    let subscriptions = [


@@ 224,6 245,25 @@ fn main_() -> Result<()> {
            }),
        ),
        (
            format!("huelia/performer/{name}/set-current-limit"),
            mqtt::QoS::ExactlyOnce,
            batch_lisp(move |_topic, expr| match expr {
                Lisp::Int(n) if n < 0 || n > 5000 => Err(anyhow!("current limit (mA) must be 0 < X <= 5000, was {n}")),
                Lisp::Int(n) if n == current_limit_ma as i64 => {
                    log::info!("already configured for current limit of {n} mA");
                    Ok(())
                }
                Lisp::Int(n) => {
                    match storage3.lock().unwrap().set_raw("current-limit", &(n as u16).to_le_bytes()) {
                        Ok(_) => log::info!("set current limit to {n} mA"),
                        Err(e) => log::error!("error storing current limit, {e}"),
                    }
                    restart()
                }
                _ => Err(anyhow!("expected integral expression, got {expr}")),
            }),
        ),
        (
            format!("huelia/performer/{name}/color"),
            mqtt::QoS::AtMostOnce,
            batch(move |topic, data| color_handler(topic, data, &color_tx1, &color_chan_full2)),


@@ 237,7 277,7 @@ fn main_() -> Result<()> {
    mqtt::connect(
        &name,
        &format!("mqtt://{}", env!("HUELIA_MQTT_HOST")),
        (name_prefix.as_deref(), n_leds),
        (name_prefix.as_deref(), n_leds, current_limit_ma),
        subscriptions.into_iter(),
    )
}

M performer/src/mqtt.rs => performer/src/mqtt.rs +3 -2
@@ 34,7 34,7 @@ pub enum Handler {
pub fn connect<I>(
    client_name: &str,
    server: &str,
    (prefix, n_leds): (Option<&str>, usize),
    (prefix, n_leds, current_limit_ma): (Option<&str>, usize, u16),
    subscriptions: I,
) -> Result<()>
where


@@ 138,7 138,8 @@ where
                        &topic_status,
                        QoS::ExactlyOnce,
                        true,
                        format!("(online {prefix_lisp} {n_leds} {:?})", crate::VERSION_STR).as_bytes(),
                        format!("(online {prefix_lisp} {n_leds} {:?} {current_limit_ma})", crate::VERSION_STR)
                            .as_bytes(),
                    )
                    .unwrap();
                if let Some((topic, qos)) = topics.next() {