use std::{collections::HashMap, io::Write, str::FromStr, time::Duration}; use bluer::{ Adapter, Address, Device, DiscoveryFilter, Uuid, gatt::{ WriteOp, remote::{Characteristic, CharacteristicWriteRequest}, }, }; use futures_lite::StreamExt; use tokio::time::timeout; const ADDRESS: &str = "B2:E5:7C:A7:A6:23"; const CHAR_UUID: &str = "0000fa02-0000-1000-8000-00805f9b34fb"; pub struct LEDDevice { pub width: u8, pub height: u8, pub address: Address, pub device: Device, pub characterstic: Characteristic, } #[derive(Copy, Clone)] pub struct Position(pub u8, pub u8); #[derive(Eq, Hash, PartialEq, Copy, Clone)] pub struct RGB(pub u8, pub u8, pub u8); pub struct Pixel { pub position: Position, pub color: RGB, } pub struct PixelCluster { pub positions: Vec, pub color: RGB, } pub trait PixelToByte { type Output; fn to_byte(&self) -> Self::Output; } impl PixelToByte for Pixel { type Output = Vec; fn to_byte(&self) -> Self::Output { let rgb = &self.color; let pos = &self.position; vec![ 0x0a, 0x00, 0x05, 0x01, 0x00, rgb.0, rgb.1, rgb.2, pos.0, pos.1, ] } } impl PixelToByte for PixelCluster { type Output = Vec>; fn to_byte(&self) -> Vec> { let color = &self.color; let chunks = self.positions.as_chunks::<120>(); let mut buf = Vec::with_capacity(chunks.0.len() + 1); let mut push_chunk = |positions: &[Position]| { if positions.is_empty() { return; } let payload_len = positions.len() * 2; let total = 8 + payload_len; let mut out = Vec::with_capacity(total); out.extend_from_slice(&[total as u8, 0, 5, 1, 0, color.0, color.1, color.2]); for p in positions { out.push(p.0); out.push(p.1); } buf.push(out); }; for c in chunks.0 { push_chunk(c); } push_chunk(chunks.1); buf } } pub trait PixelsToCluster { fn to_cluster(&self) -> Vec; } impl PixelsToCluster for Vec { fn to_cluster(&self) -> Vec { let mut map: HashMap> = HashMap::new(); for px in self { map.entry(px.color).or_default().push(px.position); } map.into_iter() .map(|(color, positions)| PixelCluster { positions: positions, color, }) .collect() } } impl LEDDevice { pub async fn new( width: u8, height: u8, addr: Address, adapter: Adapter, ) -> bluer::Result { adapter.set_powered(true).await?; adapter .set_discovery_filter(DiscoveryFilter { pattern: Some(addr.to_string()), ..Default::default() }) .await?; let mut discover = adapter.discover_devices().await?; let device = loop { match timeout(Duration::from_secs(10), discover.next()).await { Ok(Some(bluer::AdapterEvent::DeviceAdded(a))) if a == addr => { break adapter.device(addr)?; } Ok(Some(_)) => continue, Ok(None) | Err(_) => { return Err(bluer::Error { kind: bluer::ErrorKind::DoesNotExist, message: "device not found".to_string(), }); } } }; device.connect().await?; let uuid = Uuid::from_str(CHAR_UUID).unwrap(); let chr = 'outer: { for svc in device.services().await? { for ch in svc.characteristics().await? { if ch.uuid().await? == uuid { break 'outer ch; } } } panic!("characteristic {} not found on peripheral", uuid); }; Ok(LEDDevice { width: width, height: height, address: addr, device: device, characterstic: chr, }) } async fn write_data(&self, data: &[u8]) -> bluer::Result<()> { let chr = &self.characterstic; chr.write_ext( data, &CharacteristicWriteRequest { op_type: WriteOp::Request, ..Default::default() }, ) .await?; Ok(()) } pub async fn write_pixel(&self, pixel: &Pixel) -> bluer::Result<()> { self.write_data(&pixel.to_byte()).await } pub async fn write_pixelcluster(&self, pixelcluster: &PixelCluster) -> bluer::Result<()> { for data in pixelcluster.to_byte() { self.write_data(&data).await?; } Ok(()) } pub async fn write_pixelclusters(&self, pixelclusters: &[PixelCluster]) -> bluer::Result<()> { for cluster in pixelclusters { self.write_pixelcluster(cluster).await?; } Ok(()) } }