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 @@
-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 @@
-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),
}
}