From bc4b926b6ad0e4955a8b764d14be75a883667a44 Mon Sep 17 00:00:00 2001 From: Kesavan Yogeswaran Date: Wed, 4 Oct 2023 22:28:18 -0400 Subject: [PATCH] Remove nagging error message at connection time * Provide response to get calibration curve control message. The default value can be overridden at compile time via the `CALIBRATION_CURVE` environment variable. * Add support for device ids up to 8 bytes --- .cargo/config | 1 + Cargo.lock | 7 ++++++ Cargo.toml | 1 + README.md | 9 ++++---- doc/src/README.md | 9 ++++---- src/ble/gatt_server.rs | 26 ++++++++++++++++++++--- src/ble/gatt_types.rs | 48 ++++++++++++++++++++++++++++++++++-------- 7 files changed, 81 insertions(+), 20 deletions(-) diff --git a/.cargo/config b/.cargo/config index b202fd2..b10b9c5 100644 --- a/.cargo/config +++ b/.cargo/config @@ -22,3 +22,4 @@ target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU) ADVERTISED_NAME = "Progressor_1719" DEVICE_ID = "42" DEVICE_VERSION_NUMBER = "1.2.3.4" +CALIBRATION_CURVE = "FFFFFFFFFFFFFFFF00000000" diff --git a/Cargo.lock b/Cargo.lock index 8531911..c25c255 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -702,6 +702,7 @@ dependencies = [ "embedded-storage", "embedded-storage-async", "fix-hidden-lifetime-bug", + "hex", "median", "nrf-softdevice", "nrf52832-hal", @@ -736,6 +737,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "ident_case" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 4b34a12..35f5a34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ embedded-alloc = "0.5" embedded-storage = "0.3" embedded-storage-async = "0.4" fix-hidden-lifetime-bug = "0.2.5" +hex = { version = "0.4", default-features = false } median = { version = "0.3", default-features = false } nrf-softdevice = { git = "https://github.com/embassy-rs/nrf-softdevice", features = ["s113", "ble-gatt-server", "ble-peripheral", "critical-section-impl", "defmt", "nightly"] } nrf52832-hal = { version = "0.16.0", default-features = false, optional = true } diff --git a/README.md b/README.md index fe9cb9f..18fa416 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Assembled prototype P1.0 unit

-A Bluetooth-enabled crane scale compatible with the custom [Tindeq Progressor Bluetooth service][API], -which allows it to be used with compatible tools like the Tindeq mobile app. +Hangman is a Bluetooth-enabled crane scale. It's intended use is as a climbing training and rehab +tool, but it can be used anywhere that requires measuring force or weight. The hardware retrofits a cheap (~$23) 150kg crane scale from Amazon with a custom PCB based around a Nordic nRF52 microcontroller and a differential ADC. The firmware uses [Embassy][Embassy], an @@ -20,8 +20,9 @@ help my fingers get stronger. ## Status -The scale is feature-complete. Weight measurement works great with the Tindeq mobile app. Battery -life is guesstimated to be in the range of several months to a couple of years depending on usage. +The scale is feature-complete. Weight measurement works great with the [Progressor API][API] and +compatible tools. Battery life is guesstimated to be in the range of several months to a couple of +years depending on usage. There are still a few more software updates planned. See the Issues section for the major ones. diff --git a/doc/src/README.md b/doc/src/README.md index eff31ce..23b6d6a 100644 --- a/doc/src/README.md +++ b/doc/src/README.md @@ -4,8 +4,8 @@ Assembled prototype P1.0 unit

-Hangman is a Bluetooth-enabled crane scale compatible with the custom [Tindeq Progressor Bluetooth service][API], -which allows it to be used with compatible tools like the Tindeq mobile app. +Hangman is a Bluetooth-enabled crane scale. It's intended use is as a climbing training and rehab +tool, but it can be used anywhere that requires measuring force or weight. The hardware retrofits a cheap (~$23) 150kg crane scale from [Amazon][Amazon scale] with a custom PCB based around a Nordic nRF52 microcontroller and a differential ADC. The firmware uses [Embassy][Embassy], @@ -20,8 +20,9 @@ help my fingers get stronger. ## Status -The scale is feature-complete. Weight measurement works great with the Tindeq mobile app. Battery -life is guesstimated to be in the range of several months to a couple of years depending on usage. +The scale is feature-complete. Weight measurement works great with the [Progressor API][API] and +compatible tools. Battery life is guesstimated to be in the range of several months to a couple of +years depending on usage. ## Disclaimer diff --git a/src/ble/gatt_server.rs b/src/ble/gatt_server.rs index fe541b0..7e72aaf 100644 --- a/src/ble/gatt_server.rs +++ b/src/ble/gatt_server.rs @@ -14,7 +14,9 @@ extern crate alloc; -use super::gatt_types::{ControlOpcode, DataOpcode, DataPoint}; +use super::gatt_types::{ + CalibrationCurve, ControlOpcode, DataOpcode, DataPoint, DATA_PAYLOAD_SIZE, +}; use super::MeasureChannel; use crate::{battery_voltage, weight}; use alloc::boxed::Box; @@ -67,8 +69,8 @@ fn raw_notify_data( raw_payload: &[u8], connection: &Connection, ) -> Result<(), NotifyValueError> { - assert!(raw_payload.len() <= 8); - let mut payload = [0; 8]; + assert!(raw_payload.len() <= DATA_PAYLOAD_SIZE); + let mut payload = [0; DATA_PAYLOAD_SIZE]; payload[0..raw_payload.len()].copy_from_slice(raw_payload); let data = DataPoint::from_parts(opcode, raw_payload.len().try_into().unwrap(), payload); @@ -164,6 +166,24 @@ fn on_control_message(message: ControlOpcode, conn: &Connection, measure_ch: &Me defmt::error!("Failed to send SaveCalibration"); } } + ControlOpcode::GetCalibrationCurve => { + // The calibration curve is passed in via environment variable as a string of + // hex-encoded bytes for convenience. Cache the decoded bytes. + static CALIBRATION_CURVE: OnceCell = OnceCell::new(); + let curve = CALIBRATION_CURVE.get_or_init(|| { + let mut buffer: CalibrationCurve = CalibrationCurve::default(); + let Ok(_) = hex::decode_to_slice( + env!("CALIBRATION_CURVE").as_bytes(), + buffer.as_mut_slice(), + ) else { + defmt::panic!("Invalid hex string provided for calibration curve"); + }; + buffer + }); + if notify_data(DataOpcode::CalibrationCurve(*curve), conn).is_err() { + defmt::error!("Failed to notify calibration curve"); + } + } _ => (), } } diff --git a/src/ble/gatt_types.rs b/src/ble/gatt_types.rs index 4a199a4..ae77992 100644 --- a/src/ble/gatt_types.rs +++ b/src/ble/gatt_types.rs @@ -12,17 +12,39 @@ // See the License for the specific language governing permissions and // limitations under the License. +use arrayvec::ArrayVec; use bytemuck_derive::{Pod, Zeroable}; use defmt::Format; use nrf_softdevice::ble::GattValue; +/// Sized to hold the largest possible data payload +pub(crate) const DATA_PAYLOAD_SIZE: usize = 12; +pub(crate) type CalibrationCurve = [u8; 12]; + +/// Convert an integer into an array of bytes with any zeros on the MSB side trimmed +fn to_le_bytes_without_trailing_zeros>(input: T) -> ArrayVec { + let input = input.into(); + if input == 0 { + return ArrayVec::try_from([0_u8].as_slice()).unwrap(); + } + let mut out: ArrayVec = input + .to_le_bytes() + .into_iter() + .rev() + .skip_while(|&i| i == 0) + .collect(); + out.reverse(); + out +} + #[derive(Copy, Clone)] pub(crate) enum DataOpcode { BatteryVoltage(u32), Weight(f32, u32), LowPowerWarning, AppVersion(&'static [u8]), - ProgressorId(u32), + ProgressorId(u64), + CalibrationCurve(CalibrationCurve), } impl DataOpcode { @@ -30,7 +52,8 @@ impl DataOpcode { match self { DataOpcode::BatteryVoltage(..) | DataOpcode::AppVersion(..) - | DataOpcode::ProgressorId(..) => 0x00, + | DataOpcode::ProgressorId(..) + | DataOpcode::CalibrationCurve(..) => 0x00, DataOpcode::Weight(..) => 0x01, DataOpcode::LowPowerWarning => 0x04, } @@ -38,30 +61,34 @@ impl DataOpcode { fn length(&self) -> u8 { match self { - DataOpcode::BatteryVoltage(..) | DataOpcode::ProgressorId(..) => 4, + DataOpcode::BatteryVoltage(..) => 4, DataOpcode::Weight(..) => 8, + DataOpcode::ProgressorId(id) => to_le_bytes_without_trailing_zeros(*id).len() as u8, DataOpcode::LowPowerWarning => 0, DataOpcode::AppVersion(version) => version.len() as u8, + DataOpcode::CalibrationCurve(curve) => curve.len() as u8, } } - fn value(&self) -> [u8; 8] { - let mut value = [0; 8]; + fn value(&self) -> [u8; DATA_PAYLOAD_SIZE] { + let mut value = [0; DATA_PAYLOAD_SIZE]; match self { DataOpcode::BatteryVoltage(voltage) => { value[0..4].copy_from_slice(&voltage.to_le_bytes()); } DataOpcode::Weight(weight, timestamp) => { value[0..4].copy_from_slice(&weight.to_le_bytes()); - value[4..].copy_from_slice(×tamp.to_le_bytes()); + value[4..8].copy_from_slice(×tamp.to_le_bytes()); } DataOpcode::LowPowerWarning => (), DataOpcode::ProgressorId(id) => { - value[0..4].copy_from_slice(&id.to_le_bytes()); + let bytes = to_le_bytes_without_trailing_zeros(*id); + value[0..bytes.len()].copy_from_slice(&bytes); } DataOpcode::AppVersion(version) => { value[0..version.len()].copy_from_slice(version); } + DataOpcode::CalibrationCurve(curve) => value = *curve, }; value } @@ -72,7 +99,7 @@ impl DataOpcode { pub(crate) struct DataPoint { opcode: u8, length: u8, - value: [u8; 8], + value: [u8; DATA_PAYLOAD_SIZE], } impl DataPoint { @@ -80,7 +107,7 @@ impl DataPoint { /// /// One should prefer creating a `DataPoint` from a `DataOpcode` to ensure that the packet is /// correctly formed. - pub(crate) fn from_parts(opcode: u8, length: u8, value: [u8; 8]) -> Self { + pub(crate) fn from_parts(opcode: u8, length: u8, value: [u8; DATA_PAYLOAD_SIZE]) -> Self { DataPoint { opcode, length, @@ -123,6 +150,7 @@ pub(crate) enum ControlOpcode { StartPeakRfdMeasurementSeries, AddCalibrationPoint(f32), SaveCalibration, + GetCalibrationCurve, GetAppVersion, GetErrorInfo, ClearErrorInfo, @@ -153,6 +181,7 @@ impl Format for ControlOpcode { defmt::write!(fmt, "AddCalibrationPoint {=f32}", val); } ControlOpcode::SaveCalibration => defmt::write!(fmt, "SaveCalibration"), + ControlOpcode::GetCalibrationCurve => defmt::write!(fmt, "GetCalibrationCurve"), ControlOpcode::GetAppVersion => defmt::write!(fmt, "GetAppVersion"), ControlOpcode::GetErrorInfo => defmt::write!(fmt, "GetErrorInfo"), ControlOpcode::ClearErrorInfo => defmt::write!(fmt, "ClearErrorInfo"), @@ -200,6 +229,7 @@ impl GattValue for ControlOpcode { 0x6E => Self::Shutdown, 0x6F => Self::SampleBattery, 0x70 => Self::GetProgressorID, + 0x72 => Self::GetCalibrationCurve, _ => Self::Unknown(opcode), } }