1use btleplug::api::{
2 Central, Characteristic, Manager as _, Peripheral as _, ScanFilter, WriteType,
3};
4use btleplug::platform::{Adapter, Manager, Peripheral};
5use chrono::{self, Datelike, Timelike};
6use std::sync::Arc;
7use std::time::Duration;
8use tokio::sync::{Mutex, Semaphore};
9use tokio::time;
10use tracing::{debug, error, info, instrument, trace, warn};
11use uuid::Uuid;
12
13use crate::{Error, Result};
15
16pub use crate::effects::{Effects, EFFECTS};
18pub use crate::schedule::{Days, WEEK_DAYS};
19
20#[instrument(skip(manager))]
22async fn get_central(manager: &Manager) -> Result<Adapter> {
23 debug!("Getting default Bluetooth adapter");
24 let adapters = manager.adapters().await?;
25 if adapters.is_empty() {
26 error!("No Bluetooth adapters found");
27 return Err(Error::NoBluetoothAdapters);
28 }
29
30 let adapter = adapters.into_iter().next().unwrap();
31 debug!("Using Bluetooth adapter");
32 Ok(adapter)
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DeviceType {
38 ElkBle,
40 LedBle,
42 Melk,
44 ElkBulb,
46 ElkLampl,
48 Unknown,
50}
51
52#[derive(Debug, Clone)]
54pub struct DeviceConfig {
55 pub write_uuid: Uuid,
57 pub read_uuid: Uuid,
59 pub turn_on_cmd: [u8; 9],
61 pub turn_off_cmd: [u8; 9],
63 pub min_color_temp_k: u32,
65 pub max_color_temp_k: u32,
67 pub command_delay: u64,
69}
70
71struct CommandQueue {
73 semaphore: Semaphore,
75 min_delay: Duration,
77 last_command: Mutex<std::time::Instant>,
79}
80
81impl CommandQueue {
82 fn new(min_delay_ms: u64) -> Self {
83 Self {
84 semaphore: Semaphore::new(1), min_delay: Duration::from_millis(min_delay_ms),
86 last_command: Mutex::new(std::time::Instant::now() - Duration::from_secs(1)),
87 }
88 }
89
90 async fn execute<T, F>(&self, future: F) -> T
91 where
92 F: std::future::Future<Output = T> + Send + 'static,
93 T: Send + 'static,
94 {
95 let _permit = self.semaphore.acquire().await.unwrap();
97
98 let mut last_cmd = self.last_command.lock().await;
100 let elapsed = last_cmd.elapsed();
101 if elapsed < self.min_delay {
102 let wait_time = self.min_delay - elapsed;
103 trace!("Rate limiting: waiting {:?} before next command", wait_time);
104 tokio::time::sleep(wait_time).await;
105 }
106
107 let result = future.await;
109
110 *last_cmd = std::time::Instant::now();
112
113 result
114 }
115}
116
117pub struct BleLedDevice {
119 peripheral: Peripheral,
121 write_characteristic: Characteristic,
123 #[allow(dead_code)]
127 read_characteristic: Option<Characteristic>,
128 device_type: DeviceType,
130 config: DeviceConfig,
132 command_queue: Arc<CommandQueue>,
134 pub is_on: bool,
136 pub rgb_color: (u8, u8, u8),
138 pub brightness: u8,
140 pub effect: Option<u8>,
142 pub effect_speed: Option<u8>,
144 pub color_temp_kelvin: Option<u32>,
146}
147
148impl BleLedDevice {
149 #[instrument]
152 pub async fn new() -> Result<BleLedDevice> {
153 let mut device = Self::new_without_power().await?;
154
155 info!("Powering on device");
157 device.power_on().await?;
158
159 info!(
160 "Successfully connected to {} device",
161 device.get_device_type_name()
162 );
163
164 Ok(device)
165 }
166
167 #[instrument]
170 pub async fn new_without_power() -> Result<BleLedDevice> {
171 info!("Initializing BLE LED controller");
172 let manager = Manager::new().await?;
173 let central = get_central(&manager).await?;
174
175 info!("Scanning for compatible BLE devices...");
176 central.start_scan(ScanFilter::default()).await?;
177
178 let max_discovery_time = Duration::from_secs(10);
180 let start_time = std::time::Instant::now();
181 let mut found_device = false;
182 let mut device: Option<(Peripheral, DeviceType)> = None;
183
184 while start_time.elapsed() < max_discovery_time && !found_device {
186 let peripherals = central.peripherals().await?;
188 debug!("Found {} BLE peripherals so far", peripherals.len());
189
190 if !peripherals.is_empty() {
191 info!(
192 "Checking {} BLE devices for compatibility...",
193 peripherals.len()
194 );
195
196 for p in peripherals {
198 if let Ok(Some(props)) = p.properties().await {
199 if let Some(name) = props.local_name {
200 debug!("Found device: {}", name);
201 let device_type = if name.starts_with("ELK-BLE") {
202 DeviceType::ElkBle
203 } else if name.starts_with("LEDBLE") {
204 DeviceType::LedBle
205 } else if name.starts_with("MELK") {
206 DeviceType::Melk
207 } else if name.starts_with("ELK-BULB") {
208 DeviceType::ElkBulb
209 } else if name.starts_with("ELK-LAMPL") {
210 DeviceType::ElkLampl
211 } else {
212 DeviceType::Unknown
213 };
214
215 if device_type != DeviceType::Unknown {
216 info!(
217 "Found compatible device: {} (type: {:?})",
218 name, device_type
219 );
220 device = Some((p, device_type));
221 found_device = true;
222 break;
223 }
224 }
225 }
226 }
227 }
228
229 if !found_device {
230 let elapsed = start_time.elapsed().as_secs();
232 let remaining = max_discovery_time.as_secs() - elapsed;
233 info!(
234 "Still scanning for compatible devices... ({} seconds remaining)",
235 remaining
236 );
237 time::sleep(Duration::from_millis(500)).await;
239 }
240 }
241
242 if !found_device {
244 central.stop_scan().await?;
245 error!(
246 "No compatible LED device found within {} seconds",
247 max_discovery_time.as_secs()
248 );
249 return Err(Error::NoCompatibleDevice);
250 }
251
252 if let Some((peripheral, device_type)) = device {
253 info!("Connecting to device...");
255 if !peripheral.is_connected().await? {
256 peripheral.connect().await?;
257 }
258
259 central.stop_scan().await?;
260 debug!("Discovering services...");
261 peripheral.discover_services().await?;
262
263 let config = Self::get_device_config(device_type);
265 debug!("Using config for device type: {:?}", device_type);
266
267 let command_queue = Arc::new(CommandQueue::new(config.command_delay));
269
270 let write_char = peripheral
272 .characteristics()
273 .into_iter()
274 .find(|c| c.uuid == config.write_uuid)
275 .ok_or(Error::CharacteristicNotFound(config.write_uuid.to_string()))?;
276
277 debug!("Found write characteristic: {}", write_char.uuid);
278
279 let read_char = peripheral
281 .characteristics()
282 .into_iter()
283 .find(|c| c.uuid == config.read_uuid);
284
285 if let Some(ref char) = read_char {
286 debug!("Found read characteristic: {}", char.uuid);
287 } else {
288 debug!("Read characteristic not found, but this is optional");
289 }
290
291 let device = BleLedDevice {
292 peripheral,
293 write_characteristic: write_char,
294 read_characteristic: read_char,
295 device_type,
296 config,
297 command_queue,
298 is_on: false,
299 rgb_color: (255, 255, 255),
300 brightness: 100,
301 effect: None,
302 effect_speed: None,
303 color_temp_kelvin: Some(5000),
304 };
305
306 if device_type == DeviceType::ElkBle
308 || device_type == DeviceType::ElkBulb
309 || device_type == DeviceType::ElkLampl
310 {
311 debug!("Synchronizing device time");
312 device.sync_time().await?;
313 }
314
315 info!(
316 "Successfully connected to {} device (without powering on)",
317 device.get_device_type_name()
318 );
319 Ok(device)
320 } else {
321 error!("No compatible LED device found");
322 Err(Error::NoCompatibleDevice)
323 }
324 }
325
326 fn get_device_config(device_type: DeviceType) -> DeviceConfig {
328 match device_type {
329 DeviceType::ElkBle => DeviceConfig {
330 write_uuid: Uuid::parse_str("0000fff3-0000-1000-8000-00805f9b34fb").unwrap(),
331 read_uuid: Uuid::parse_str("0000fff4-0000-1000-8000-00805f9b34fb").unwrap(),
332 turn_on_cmd: [0x7e, 0x00, 0x04, 0xf0, 0x00, 0x01, 0xff, 0x00, 0xef],
333 turn_off_cmd: [0x7e, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0x00, 0xef],
334 min_color_temp_k: 2700,
335 max_color_temp_k: 6500,
336 command_delay: 15, },
338 DeviceType::LedBle => DeviceConfig {
339 write_uuid: Uuid::parse_str("0000ffe1-0000-1000-8000-00805f9b34fb").unwrap(),
340 read_uuid: Uuid::parse_str("0000ffe2-0000-1000-8000-00805f9b34fb").unwrap(),
341 turn_on_cmd: [0x7e, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0xef],
342 turn_off_cmd: [0x7e, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0x00, 0xef],
343 min_color_temp_k: 2700,
344 max_color_temp_k: 6500,
345 command_delay: 15,
346 },
347 DeviceType::Melk => DeviceConfig {
348 write_uuid: Uuid::parse_str("0000fff3-0000-1000-8000-00805f9b34fb").unwrap(),
349 read_uuid: Uuid::parse_str("0000fff4-0000-1000-8000-00805f9b34fb").unwrap(),
350 turn_on_cmd: [0x7e, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0xef],
351 turn_off_cmd: [0x7e, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0x00, 0xef],
352 min_color_temp_k: 2700,
353 max_color_temp_k: 6500,
354 command_delay: 15,
355 },
356 DeviceType::ElkBulb | DeviceType::ElkLampl | DeviceType::Unknown => DeviceConfig {
357 write_uuid: Uuid::parse_str("0000fff3-0000-1000-8000-00805f9b34fb").unwrap(),
358 read_uuid: Uuid::parse_str("0000fff4-0000-1000-8000-00805f9b34fb").unwrap(),
359 turn_on_cmd: [0x7e, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0xef],
360 turn_off_cmd: [0x7e, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0x00, 0xef],
361 min_color_temp_k: 2700,
362 max_color_temp_k: 6500,
363 command_delay: 15,
364 },
365 }
366 }
367
368 pub fn get_device_type_name(&self) -> &'static str {
370 match self.device_type {
371 DeviceType::ElkBle => "ELK-BLE",
372 DeviceType::LedBle => "LEDBLE",
373 DeviceType::Melk => "MELK",
374 DeviceType::ElkBulb => "ELK-BULB",
375 DeviceType::ElkLampl => "ELK-LAMPL",
376 DeviceType::Unknown => "Unknown",
377 }
378 }
379
380 #[instrument(skip(self))]
382 async fn sync_time(&self) -> Result<()> {
383 let system_time = chrono::Local::now();
384 debug!(
385 "Syncing device time to {}:{}:{} day:{}",
386 system_time.hour(),
387 system_time.minute(),
388 system_time.second(),
389 system_time.weekday().number_from_monday()
390 );
391
392 self.send_command(&[
393 0x7e,
394 0x00,
395 0x83,
396 system_time.hour() as u8,
397 system_time.minute() as u8,
398 system_time.second() as u8,
399 system_time.weekday().number_from_monday() as u8,
400 0x00,
401 0xef,
402 ])
403 .await?;
404
405 debug!("Time synchronization complete");
406 Ok(())
407 }
408
409 #[instrument(skip(self))]
418 pub async fn set_custom_time(
419 &self,
420 hour: u8,
421 minute: u8,
422 second: u8,
423 day_of_week: u8,
424 ) -> Result<()> {
425 let hour = hour.min(23);
426 let minute = minute.min(59);
427 let second = second.min(59);
428 let day_of_week = day_of_week.clamp(1, 7);
429
430 debug!(
431 "Setting custom time to {}:{}:{} day:{}",
432 hour, minute, second, day_of_week
433 );
434
435 self.send_command(&[
436 0x7e,
437 0x00,
438 0x83,
439 hour,
440 minute,
441 second,
442 day_of_week,
443 0x00,
444 0xef,
445 ])
446 .await?;
447
448 debug!("Custom time set successfully");
449 Ok(())
450 }
451
452 #[instrument(skip(self))]
454 pub async fn power_on(&mut self) -> Result<()> {
455 debug!("Turning LED strip on");
456 self.send_command(&self.config.turn_on_cmd).await?;
457 self.is_on = true;
458
459 time::sleep(Duration::from_millis(200)).await;
461 info!("LED strip powered on");
462 Ok(())
463 }
464
465 #[instrument(skip(self))]
467 pub async fn power_off(&mut self) -> Result<()> {
468 debug!("Turning LED strip off");
469 self.send_command(&self.config.turn_off_cmd).await?;
470 self.is_on = false;
471
472 time::sleep(Duration::from_millis(200)).await;
474 info!("LED strip powered off");
475 Ok(())
476 }
477
478 #[instrument(skip(self))]
486 pub async fn set_color(
487 &mut self,
488 red_value: u8,
489 green_value: u8,
490 blue_value: u8,
491 ) -> Result<()> {
492 debug!(
493 "Setting color to RGB({}, {}, {})",
494 red_value, green_value, blue_value
495 );
496
497 if self.effect.is_some() {
499 debug!("Disabling active effect before setting color");
500 self.send_command(&[0x7e, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0xef])
502 .await?;
503 time::sleep(Duration::from_millis(200)).await;
505 }
506
507 trace!("Sending RGB color command");
509 self.send_command(&[
510 0x7e,
511 0x00,
512 0x05,
513 0x03,
514 red_value,
515 green_value,
516 blue_value,
517 0x00,
518 0xef,
519 ])
520 .await?;
521
522 self.rgb_color = (red_value, green_value, blue_value);
524 self.effect = None; time::sleep(Duration::from_millis(200)).await;
528 info!(
529 "Color set to RGB({}, {}, {})",
530 red_value, green_value, blue_value
531 );
532 Ok(())
533 }
534
535 #[instrument(skip(self))]
541 pub async fn set_brightness(&mut self, value: u8) -> Result<()> {
542 let limited_value = value.min(100);
543 if value > 100 {
544 warn!(
545 "Brightness value {} out of range (0-100), limiting to 100",
546 value
547 );
548 }
549
550 debug!("Setting brightness to {}%", limited_value);
551 self.send_command(&[
552 0x7e,
553 0x00,
554 0x01,
555 limited_value,
556 0x00,
557 0x00,
558 0x00,
559 0x00,
560 0xef,
561 ])
562 .await?;
563
564 self.brightness = limited_value;
565
566 info!("Brightness set to {}%", limited_value);
567 Ok(())
568 }
569
570 #[instrument(skip(self))]
576 pub async fn set_effect(&mut self, value: u8) -> Result<()> {
577 debug!("Setting effect mode to code: {:#04x}", value);
578
579 self.send_command(&[0x7e, 0x00, 0x03, value, 0x03, 0x00, 0x00, 0x00, 0xef])
581 .await?;
582
583 self.effect = Some(value);
584
585 time::sleep(Duration::from_millis(200)).await;
587 info!("Effect mode set successfully");
588 Ok(())
589 }
590
591 #[instrument(skip(self))]
597 pub async fn set_effect_speed(&mut self, value: u8) -> Result<()> {
598 let limited_value = value.min(100);
599 if value > 100 {
600 warn!(
601 "Effect speed {} out of range (0-100), limiting to 100",
602 value
603 );
604 }
605
606 if self.effect.is_none() {
607 warn!("Setting effect speed without an active effect. This may not have any effect.");
608 }
609
610 debug!("Setting effect speed to {}", limited_value);
611 self.send_command(&[
613 0x7e,
614 0x00,
615 0x02,
616 limited_value,
617 0x00,
618 0x00,
619 0x00,
620 0x00,
621 0xef,
622 ])
623 .await?;
624
625 self.effect_speed = Some(limited_value);
626
627 time::sleep(Duration::from_millis(200)).await;
629 info!("Effect speed set to {}", limited_value);
630 Ok(())
631 }
632
633 #[instrument(skip(self))]
639 pub async fn set_color_temp_kelvin(&mut self, value: u32) -> Result<()> {
640 let temp = value
642 .max(self.config.min_color_temp_k)
643 .min(self.config.max_color_temp_k);
644
645 if value < self.config.min_color_temp_k || value > self.config.max_color_temp_k {
646 warn!(
647 "Color temperature {} out of range ({}-{}), adjusting to {}",
648 value, self.config.min_color_temp_k, self.config.max_color_temp_k, temp
649 );
650 }
651
652 debug!("Setting color temperature to {}K", temp);
653
654 let color_temp_percent = ((temp - self.config.min_color_temp_k) * 100
656 / (self.config.max_color_temp_k - self.config.min_color_temp_k))
657 as u8;
658
659 let warm = color_temp_percent;
661 let cold = 100 - color_temp_percent;
662
663 if self.effect.is_some() {
665 debug!("Disabling active effect before setting color temperature");
666 self.send_command(&[0x7e, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0xef])
668 .await?;
669 time::sleep(Duration::from_millis(200)).await;
671 }
672
673 trace!(
675 "Sending color temperature command: warm={}, cold={}",
676 warm,
677 cold
678 );
679 self.send_command(&[0x7e, 0x00, 0x05, 0x02, warm, cold, 0x00, 0x00, 0xef])
680 .await?;
681
682 self.color_temp_kelvin = Some(temp);
683 self.effect = None; time::sleep(Duration::from_millis(200)).await;
687 info!("Color temperature set to {}K", temp);
688 Ok(())
689 }
690
691 #[instrument(skip(self))]
700 pub async fn set_schedule_on(
701 &self,
702 days: u8,
703 hours: u8,
704 minutes: u8,
705 enabled: bool,
706 ) -> Result<()> {
707 let hours = hours.min(23);
708 let minutes = minutes.min(59);
709 let value = if enabled { days + 0x80 } else { days };
710
711 debug!(
712 "Setting schedule to turn on at {}:{:02} on days: {:#04x}, enabled: {}",
713 hours, minutes, days, enabled
714 );
715
716 self.send_command(&[0x7e, 0x00, 0x82, hours, minutes, 0x00, 0x00, value, 0xef])
717 .await?;
718
719 time::sleep(Duration::from_millis(200)).await;
721 info!("Schedule set to turn on at {}:{:02}", hours, minutes);
722 Ok(())
723 }
724
725 #[instrument(skip(self))]
734 pub async fn set_schedule_off(
735 &self,
736 days: u8,
737 hours: u8,
738 minutes: u8,
739 enabled: bool,
740 ) -> Result<()> {
741 let hours = hours.min(23);
742 let minutes = minutes.min(59);
743 let value = if enabled { days + 0x80 } else { days };
744
745 debug!(
746 "Setting schedule to turn off at {}:{:02} on days: {:#04x}, enabled: {}",
747 hours, minutes, days, enabled
748 );
749
750 self.send_command(&[0x7e, 0x00, 0x82, hours, minutes, 0x00, 0x01, value, 0xef])
751 .await?;
752
753 time::sleep(Duration::from_millis(200)).await;
755 info!("Schedule set to turn off at {}:{:02}", hours, minutes);
756 Ok(())
757 }
758
759 #[instrument(skip(self))]
769 pub async fn generic_command(
770 &self,
771 id: u8,
772 sub_id: u8,
773 arg1: u8,
774 arg2: u8,
775 arg3: u8,
776 ) -> Result<()> {
777 debug!(
778 "Sending generic command: id={:#04x}, sub_id={:#04x}, args=[{:#04x}, {:#04x}, {:#04x}]",
779 id, sub_id, arg1, arg2, arg3
780 );
781
782 self.send_command(&[0x7e, 0x00, id, sub_id, arg1, arg2, arg3, 0x00, 0xef])
783 .await?;
784 debug!("Generic command sent successfully");
785 Ok(())
786 }
787
788 #[instrument(skip(self, command), fields(cmd_length = command.len()))]
790 async fn send_command(&self, command: &[u8]) -> Result<()> {
791 let cmd = command.to_vec();
793 let peripheral = self.peripheral.clone();
794 let write_characteristic = self.write_characteristic.clone();
795
796 self.command_queue
798 .execute(async move {
799 let max_retries = 3;
802 let mut attempt = 0;
803
804 let write_type = if write_characteristic
806 .properties
807 .contains(btleplug::api::CharPropFlags::WRITE)
808 {
809 WriteType::WithResponse
810 } else {
811 WriteType::WithoutResponse
812 };
813
814 while attempt < max_retries {
815 trace!(
816 "Sending BLE command (attempt {}/{})",
817 attempt + 1,
818 max_retries
819 );
820
821 match peripheral
822 .write(&write_characteristic, &cmd, write_type)
823 .await
824 {
825 Ok(_) => {
826 trace!("Command sent successfully");
827 return Ok(());
828 }
829 Err(e) => {
830 attempt += 1;
831 warn!(
832 "Command failed (attempt {}/{}): {}",
833 attempt, max_retries, e
834 );
835
836 if attempt < max_retries {
837 trace!("Waiting before retry...");
839 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
840 } else {
841 error!("Command failed permanently: {}", e);
843 return Err(Error::BleError(e.to_string()));
844 }
845 }
846 }
847 }
848
849 error!("Command failed after {} attempts", max_retries);
851 Err(Error::CommandTimeout(max_retries))
852 })
853 .await
854 }
855}