Skip to content

Instantly share code, notes, and snippets.

@4ndv
Last active April 15, 2024 17:41
Show Gist options
  • Save 4ndv/fa19b2183c3154634892b12a6cabb867 to your computer and use it in GitHub Desktop.
Save 4ndv/fa19b2183c3154634892b12a6cabb867 to your computer and use it in GitHub Desktop.
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