Last active
April 15, 2024 17:41
-
-
Save 4ndv/fa19b2183c3154634892b12a6cabb867 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use btleplug::api::bleuuid::uuid_from_u16; | |
use btleplug::api::{Central, CentralEvent, Manager as _, Peripheral as _, ScanFilter, WriteType}; | |
use btleplug::platform::{Adapter, Manager, Peripheral}; | |
use clap::Command; | |
use futures::stream::StreamExt; | |
use std::error::Error; | |
use std::fmt::Debug; | |
use std::time::{SystemTime, UNIX_EPOCH}; | |
fn cli() -> Command { | |
Command::new("picooc-listener") | |
.about("Small tool for communicating with Picooc smart scales") | |
.subcommand_required(true) | |
.arg_required_else_help(true) | |
.subcommand(Command::new("scan").about("Lists devices and their capabilities")) | |
.subcommand(Command::new("listen").about("Listens measurements from scale")) | |
} | |
#[tokio::main] | |
async fn main() -> Result<(), Box<dyn Error>> { | |
pretty_env_logger::init(); | |
let matches = cli().get_matches(); | |
match matches.subcommand() { | |
Some(("scan", _)) => { | |
scan().await; | |
} | |
Some(("listen", _)) => { | |
listen().await?; | |
} | |
_ => unreachable!(), | |
} | |
Ok(()) | |
} | |
async fn get_central(manager: &Manager) -> Adapter { | |
let adapters = manager.adapters().await.unwrap(); | |
adapters.into_iter().nth(0).unwrap() | |
} | |
async fn get_scale() -> Result<Option<Peripheral>, Box<dyn Error>> { | |
let manager = Manager::new().await?; | |
let central = get_central(&manager).await; | |
println!("Scanning for devices"); | |
let mut events = central.events().await?; | |
central.start_scan(ScanFilter::default()).await?; | |
while let Some(event) = events.next().await { | |
match event { | |
CentralEvent::DeviceDiscovered(id) => { | |
let peripheral = central.peripheral(&id).await?; | |
let properties = peripheral.properties().await?.unwrap(); | |
if !properties | |
.local_name | |
.is_some_and(|name| name.contains("PICOOC")) | |
{ | |
continue; | |
} | |
println!("Discovered device {:?}", id); | |
return Ok(Some(peripheral)); | |
} | |
_ => {} | |
} | |
} | |
return Ok(None); | |
} | |
async fn scan() -> Result<(), Box<dyn Error>> { | |
let peripheral = get_scale().await?; | |
match peripheral { | |
Some(peripheral) => { | |
println!("Peripheral info {:?}", peripheral); | |
peripheral.connect().await?; | |
peripheral.discover_services().await?; | |
println!("Characteristics: {:?}", peripheral.characteristics()); | |
} | |
None => println!("No device discovered"), | |
} | |
Ok(()) | |
} | |
fn build_request() -> Vec<u8> { | |
let time = SystemTime::now() | |
.duration_since(UNIX_EPOCH) | |
.unwrap() | |
.as_secs(); | |
let time_bytes = (time as i32).to_be_bytes(); | |
let mut packet: Vec<u8> = vec![0xF1, 0x09, 0x3A]; | |
packet.extend_from_slice(&time_bytes); | |
packet.extend_from_slice(&[0xA5, 0x00]); | |
println!("Request packet: {:#04x?}", packet); | |
packet | |
} | |
#[derive(Debug)] | |
struct ScalesResponse { | |
timestamp: i32, | |
weight: f32, | |
} | |
fn parse_response(packet: &Vec<u8>) -> Option<ScalesResponse> { | |
println!("Response packet: {:#04x?}", packet); | |
if packet.len() != 13 { | |
eprintln!("Incorrect packet length: {}, expected 13", packet.len()); | |
return None; | |
} | |
if packet[0] != 0x39 || packet[1] != 0x0D { | |
eprintln!("Incorrect packet header, expected 390D"); | |
return None; | |
} | |
let timestamp = i32::from_be_bytes(packet[2..6].try_into().unwrap()); | |
let weight = i32::from_be_bytes([0, 0, packet[6], packet[7]]) as f32 / 20.0; | |
Some(ScalesResponse { timestamp, weight }) | |
} | |
async fn listen() -> Result<(), Box<dyn Error>> { | |
let Some(peripheral) = get_scale().await? else { | |
println!("No device discovered"); | |
return Ok(()); | |
}; | |
peripheral.connect().await?; | |
peripheral.discover_services().await?; | |
let chars = peripheral.characteristics(); | |
println!("Chars: {:#?}", chars); | |
let rx_char = chars | |
.iter() | |
.find(|c| c.uuid == uuid_from_u16(0xFFF1)) | |
.expect("Cannot find RX characteristic"); | |
let tx_char = chars | |
.iter() | |
.find(|c| c.uuid == uuid_from_u16(0xFFF2)) | |
.expect("Cannot find TX characteristic"); | |
println!("Sending request"); | |
let _ = peripheral | |
.write(&tx_char, &build_request(), WriteType::WithoutResponse) | |
.await?; | |
println!("Listening for responses"); | |
peripheral.subscribe(&rx_char).await?; | |
let mut notifications = peripheral.notifications().await?; | |
if let Some(response) = notifications.next().await { | |
let parsed_response = parse_response(&response.value); | |
println!("Response: {:#?}", parsed_response.unwrap()); | |
} | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment