SARDONYX-sard / bluetooth-battery-monitor

(Wip88%, but work at a minimum)For Windows lightweight bluetooth battery monitor
Apache License 2.0
16 stars 0 forks source link

Transition to real-time event triggers #11

Open SARDONYX-sard opened 1 year ago

SARDONYX-sard commented 1 year ago

Currently this application is not at the paid level.

The current spec is to get the battery level via a PowerShell script at set time intervals (default is 1 hour). However, paid apps of the same type are likely to use some kind of Socket communication and event-triggered type processing instead of interval processing.

For example, the Windows API WSAPoll function.

However, the AT command call of the IPhone extension via Socket communication seems to be very useless as it disconnects the Bluetooth classic device with an error sound when sent (it may just be a bad usage).

In summary, we need the following two things.

Possible reference sites

AT Commnad(soket send())

SARDONYX-sard commented 1 year ago

By the way, the battery acquisition by AT command is as follows.

Note that there is a dependency on the io_bluetooth crate.

extern crate io_bluetooth;

use std::collections::HashMap;
use std::io;
use std::iter;

use regex::Regex;

use io_bluetooth::bt::{self, BtStream};

fn main() -> io::Result<()> {
    let devices = bt::discover_devices()?;
    println!("Devices:");
    for (idx, device) in devices.iter().enumerate() {
        println!("{}: {}", idx, *device);
    }

    if devices.len() == 0 {
        return Err(io::Error::new(
            io::ErrorKind::NotFound,
            "No Bluetooth devices found.",
        ));
    }
    let mut device_idx = 0;
    if let Some(idx) = devices
        .iter()
        .position(|&x| x.to_string() == "your BT MAC")
    {
        device_idx = idx;
    };
    // let device_idx = request_device_idx(devices.len())?;

    let socket = BtStream::connect(iter::once(&devices[device_idx]), bt::BtProtocol::RFCOMM)?;

    match socket.peer_addr() {
        Ok(name) => println!("Peername: {}.", name),
        Err(err) => println!("An error occurred while retrieving the peername: {:?}", err),
    }

    match socket.local_addr() {
        Ok(name) => println!("Socket name: {}", name),
        Err(err) => println!("An error occurred while retrieving the sockname: {:?}", err),
    }

    let mut buffer = vec![0; 1024];
    let result = loop {
        match socket.recv(&mut buffer[..]) {
            // recv & send data. AT Command
            // In AT commands, AT stands for Attention and these commands are used for controlling MODEMs.
            // - https://www.elprocus.com/at-commands-tutorial/
            //
            // Test Command e.g.: AD =?
            // Read Command e.g.: AT+CBC =?
            //  Set Command e.g.: AT+CBC =”+923140”, 110
            Ok(len) if len == 0 => continue,
            Ok(len) => {
                let recv_cmd = String::from_utf8_lossy(&buffer).to_string();
                println!("Received {} bytes. {}", len, recv_cmd);

                match recv_cmd {
                    recv_cmd if recv_cmd.contains("BRSF") => {
                        // BRSF: AG Supported features
                        // - https://stackoverflow.com/questions/26342372/android-communicating-to-hfp-device-via-at-commands
                        socket.send(b"\r\n+BRSF: 1024\r\n")?;
                        socket.send(b"\r\nOK\r\n")?;
                        println!("send BRSF");
                    }
                    recv_cmd if recv_cmd.contains("CIND=") => {
                        // Set device's indicator.
                        // - https://m2msupport.net/m2msupport/atcind-indicator-control/
                        socket.send(b"\r\n+CIND:(\"service\",(0-1)),(\"call\",(0-1)),(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"battchg\",(0-5))\r\n",)?;
                        socket.send(b"\r\nOK\r\n")?;
                        println!("send CIND");
                    }
                    recv_cmd if recv_cmd.contains("CIND?") => {
                        socket.send(b"\r\n+CIND: 0,0,0,0,3\r\n")?;
                        socket.send(b"\r\nOK\r\n")?;
                        println!("send CIND 3");
                    }
                    recv_cmd if recv_cmd.contains("BIND=?") => {
                        // Announce that we support the battery level HF indicator
                        // https://www.bluetooth.com/specifications/assigned-numbers/hands-free-profile/
                        socket.send(b"\r\n+BIND: (2)\r\n")?;
                        socket.send(b"\r\nOK\r\n")?;
                        println!("send BIND");
                    }
                    recv_cmd if recv_cmd.contains("BIND?") => {
                        // Enable battery level HF indicator
                        socket.send(b"\r\n+BIND: 2,1\r\n")?;
                        socket.send(b"\r\nOK\r\n")?;
                        println!("send BIND2,1");
                    }
                    recv_cmd if recv_cmd.contains("XAPL=") => {
                        // AT command to check if battery reporting is supported.
                        // - P.79(https://www.bluetooth.com/wp-content/uploads/attachments/BluetoothDesignGuidelines.pdf)
                        socket.send(b"\r\n+XAPL=iPhone,7\r\n")?;
                        socket.send(b"\r\nOK\r\n")?;
                    }
                    recv_cmd if recv_cmd.contains("IPHONEACCEV") => {
                        // # IPHONEACCEV
                        //
                        // Parse Battery level. Apple's own extension, but supported by many Bluetooth.
                        //
                        // - P.80(https://www.bluetooth.com/wp-content/uploads/attachments/BluetoothDesignGuidelines.pdf)
                        //
                        // Format: AT+IPHONEACCEV=Number of key/value pairs,key1,val1,key2,val2,...
                        // - Number of key/value pairs: The number of parameters coming next.
                        // - key: the type of change being reported:
                        //   - 1 = Battery Level
                        //   - 2 = Dock State
                        // - val: the value of the change:
                        //   - Battery Level: string value between '0' and '9'
                        //   - Dock State: 0 = undocked, 1 = docked
                        let re = Regex::new(r"(?:IPHONEACCEV=)(\d),(\d),(\d)").unwrap();
                        let binding = re
                            .find(recv_cmd.as_str())
                            .unwrap()
                            .as_str()
                            .replace("IPHONEACCEV=", "");
                        println!("{binding}");
                        let parts = binding.split(",").collect::<Vec<_>>()[1..].to_vec();

                        println!("parts: {:?}", parts);
                        if parts.len() > 1 && (parts.len() % 2) == 0 {
                            let mut parts = parts.iter();
                            let params = parts
                                .clone()
                                .zip(parts.skip(1))
                                .step_by(2)
                                .map(|(k, v)| (k.clone(), v.clone()))
                                .collect::<HashMap<_, _>>();
                            println!("{:?}", params);
                            if params.contains_key("1") {
                                // 0~9(9 == 100%)
                                break Some((params["1"].parse::<i32>().unwrap() + 1) * 10);
                            }
                        }
                    }
                    recv_cmd if recv_cmd.contains("BIEV=") => {
                        println!("try send BIEV");
                        let params = recv_cmd
                            .trim()
                            .split("=")
                            .nth(1)
                            .unwrap()
                            .split(",")
                            .collect::<Vec<_>>();
                        if params[0] == "2" {
                            break Some(params[1].parse::<i32>().unwrap());
                        }
                        socket.send(b"\r\nOK\r\n")?;
                    }
                    recv_cmd if recv_cmd.contains("XEVENT=BATTERY") => {
                        // For Android >=3.0
                        // https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/722
                        let params = recv_cmd
                            .trim()
                            .split("=")
                            .nth(1)
                            .unwrap()
                            .split(",")
                            .collect::<Vec<_>>();
                        if params.len() >= 3 {
                            // AT+XEVENT=BATTERY,6,11,461,0
                            break Some(
                                params[1].parse::<i32>().unwrap() * 100
                                    / params[2].parse::<i32>().unwrap(),
                            );
                        } else {
                            // AT+XEVENT=B
                            break Some((params[1].parse::<i32>().unwrap() + 1) * 10);
                        };
                    }
                    _ => {
                        socket.send(b"\r\nOK\r\n")?;
                    }
                };
            }
            Err(err) => return Err(err),
        };
    };
    match result {
        Some(buf) => {
            println!("{}", buf);
            Ok(())
        }
        None => panic!("Failed to get recv."),
    }
}