From 903887a149ad5879dd9ec5dce1b0afe181ecc361 Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Fri, 19 Jun 2026 04:02:58 +0200 Subject: [PATCH] feat: Add basic support for bambu printer --- Cargo.lock | 137 ++++++++++++++++++++++++++++ Dockerfile | 2 +- automation_devices/Cargo.toml | 1 + automation_devices/src/bambu.rs | 141 +++++++++++++++++++++++++++++ automation_devices/src/lib.rs | 30 ++++++ definitions/automation:devices.lua | 19 ++++ 6 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 automation_devices/src/bambu.rs diff --git a/Cargo.lock b/Cargo.lock index 43ac2f7..2707f33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -105,6 +117,7 @@ dependencies = [ "async-trait", "automation_lib", "automation_macro", + "bambulab", "bytes", "dyn-clone", "eui48", @@ -231,6 +244,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "bambulab" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25534ead4a23a9ecee1530f1169246eabc2602ab704ffc57d41b59811167d83" +dependencies = [ + "futures", + "nanoid", + "paho-mqtt", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "base64" version = "0.22.1" @@ -349,6 +376,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.15.22" @@ -398,6 +434,21 @@ dependencies = [ "strum", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "darling" version = "0.20.11" @@ -576,6 +627,27 @@ dependencies = [ "serde", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -697,6 +769,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.32" @@ -1402,6 +1480,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "nanoid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628de41fe064cc3f0cf07f3d299ee3e73521adaff72278731d5c8cae3797873" +dependencies = [ + "rand", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1443,6 +1530,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-float" version = "2.10.1" @@ -1452,6 +1551,38 @@ dependencies = [ "num-traits", ] +[[package]] +name = "paho-mqtt" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c6e899549be260b8c8d7fb6908d72eacb9a1e10229c7e294a7a8ff1c768620" +dependencies = [ + "async-channel", + "crossbeam-channel", + "futures", + "futures-timer", + "libc", + "log", + "paho-mqtt-sys", + "thiserror", +] + +[[package]] +name = "paho-mqtt-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e508bdadfa0d58d67792060fa447ec20729db5dc76e3dc7de9f4d29e521a43" +dependencies = [ + "cmake", + "openssl-sys", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2577,6 +2708,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "wakey" version = "0.4.1" diff --git a/Dockerfile b/Dockerfile index dd6e080..7f5c614 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ COPY . . RUN cargo chef prepare --recipe-path recipe.json FROM base AS builder -RUN apk add --no-cache g++=15.2.0-r2 cmake=4.1.3-r0 make=4.4.1-r3 +RUN apk add --no-cache g++=15.2.0-r2 cmake=4.1.3-r0 make=4.4.1-r3 openssl-dev=3.5.7-r0 openssl-libs-static=3.5.7-r0 # HACK: Now we can use unstable feature while on stable rust! ENV RUSTC_BOOTSTRAP=1 COPY --from=planner /app/recipe.json recipe.json diff --git a/automation_devices/Cargo.toml b/automation_devices/Cargo.toml index 5aba41f..3491da8 100644 --- a/automation_devices/Cargo.toml +++ b/automation_devices/Cargo.toml @@ -25,3 +25,4 @@ thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } wakey = { workspace = true } +bambulab = { version = "0.4.30", default-features = false } diff --git a/automation_devices/src/bambu.rs b/automation_devices/src/bambu.rs new file mode 100644 index 0000000..b60909a --- /dev/null +++ b/automation_devices/src/bambu.rs @@ -0,0 +1,141 @@ +use std::convert::Infallible; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::time::Duration; + +use async_trait::async_trait; +use automation_lib::action_callback::ActionCallback; +use automation_lib::device::Device; +use automation_macro::{Device, LuaDeviceConfig}; +use bambulab::client::Client; +use bambulab::{Command, Message}; +use google_home::errors::{self}; +use google_home::traits::OnOff; +use lua_typed::Typed; +use tracing::trace; + +use crate::{DebugWrap, LuaDeviceCreate}; + +#[derive(Debug, Clone, LuaDeviceConfig, Typed, Default)] +#[typed(as = "BambuCallbacks")] +pub struct Callbacks { + #[device_config(from_lua, default)] + #[typed(default)] + pub state: ActionCallback, + #[device_config(from_lua, default)] + #[typed(default)] + pub connected: ActionCallback, +} +crate::register_type!(Callbacks); + +#[derive(Debug, Clone, LuaDeviceConfig, Typed)] +#[typed(as = "BambuConfig")] +pub struct Config { + pub host: String, + pub device_id: String, + pub access_code: String, + #[device_config(from_lua, default)] + pub callbacks: Callbacks, +} +crate::register_type!(Config); + +#[derive(Debug, Clone, Device)] +#[device(traits(OnOff))] +pub struct Bambu { + config: Config, + + client: DebugWrap, + + state: Arc, +} +crate::register_device!(Bambu); + +#[async_trait] +impl LuaDeviceCreate for Bambu { + type Config = Config; + type Error = Infallible; + + async fn create(config: Self::Config) -> Result { + trace!(id = config.device_id, "Setting up bambu"); + + let (tx, mut rx) = tokio::sync::broadcast::channel(25); + let client = Client::new(&config.host, &config.access_code, &config.device_id, tx); + + let state = Arc::new(AtomicBool::new(false)); + let bambu = Self { + config, + client: DebugWrap(client.clone()), + state: state.clone(), + }; + + tokio::spawn({ + let mut bambu = bambu.clone(); + async move { + // The printer might be offline so periodically try to reconnecct + loop { + bambu.client.run().await.ok(); + + tokio::time::sleep(Duration::from_secs(60)).await; + } + } + }); + + tokio::spawn({ + let bambu = bambu.clone(); + async move { + loop { + let message = rx.recv().await.unwrap(); + + match message { + Message::Print(data) => 'print: { + // Extract the state of the chamber light + let Some(light_report) = data.print.lights_report else { + break 'print; + }; + + let on = light_report + .iter() + .find(|report| report.node == "chamber_light") + .map(|report| report.mode == "on") + .unwrap_or(false); + + state.store(on, std::sync::atomic::Ordering::Relaxed); + + bambu.config.callbacks.state.call(bambu.clone()).await; + } + Message::Connected => { + client.publish(Command::PushAll).await.unwrap(); + + bambu.config.callbacks.connected.call(bambu.clone()).await; + } + // Ignore everything else + _ => {} + } + } + } + }); + + Ok(bambu) + } +} + +impl Device for Bambu { + fn get_id(&self) -> String { + self.config.device_id.clone() + } +} + +#[async_trait] +impl OnOff for Bambu { + async fn on(&self) -> Result { + Ok(self.state.load(std::sync::atomic::Ordering::Relaxed)) + } + + async fn set_on(&self, on: bool) -> Result<(), errors::ErrorCode> { + // NOTE: This will error in case the printer is offline, but we don't really care in that + // case so we just ignore the error + self.client.publish(Command::SetChamberLight(on)).await.ok(); + + Ok(()) + } +} diff --git a/automation_devices/src/lib.rs b/automation_devices/src/lib.rs index 0407c6a..ed5a667 100644 --- a/automation_devices/src/lib.rs +++ b/automation_devices/src/lib.rs @@ -1,5 +1,7 @@ +#![feature(debug_closure_helpers)] #![feature(iter_intersperse)] mod air_filter; +mod bambu; mod contact_sensor; mod hue_bridge; mod hue_group; @@ -13,6 +15,9 @@ mod wake_on_lan; mod washer; mod zigbee; +use std::fmt; +use std::ops::{Deref, DerefMut}; + use automation_lib::Module; use automation_lib::device::{Device, LuaDeviceCreate}; use tracing::{debug, warn}; @@ -20,6 +25,31 @@ use tracing::{debug, warn}; type DeviceNameFn = fn() -> String; type RegisterDeviceFn = fn(lua: &mlua::Lua) -> mlua::Result; +#[derive(Clone)] +struct DebugWrap(T); + +impl DerefMut for DebugWrap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Deref for DebugWrap { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Debug for DebugWrap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("DebugWrap") + .field_with(|f| f.write_str(stringify!(T))) + .finish() + } +} + pub struct RegisteredDevice { name_fn: DeviceNameFn, register_fn: RegisterDeviceFn, diff --git a/definitions/automation:devices.lua b/definitions/automation:devices.lua index a182df4..6cb8ab1 100644 --- a/definitions/automation:devices.lua +++ b/definitions/automation:devices.lua @@ -24,6 +24,25 @@ function devices.AirFilter.new(config) end ---@field url string local AirFilterConfig +---@class Bambu: DeviceInterface, OnOffInterface +local Bambu +devices.Bambu = {} +---@param config BambuConfig +---@return Bambu +function devices.Bambu.new(config) end + +---@class BambuCallbacks +---@field state (fun(_: Bambu) | fun(_: Bambu)[])? +---@field connected (fun(_: Bambu) | fun(_: Bambu)[])? +local BambuCallbacks + +---@class BambuConfig +---@field host string +---@field device_id string +---@field access_code string +---@field callbacks BambuCallbacks +local BambuConfig + ---@class ConfigLightLightStateBrightness ---@field name string ---@field room (string)?