Skip to content

Commit

Permalink
Remove nagging error message at connection time
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kesyog committed Oct 7, 2023
1 parent b51eb88 commit bc4b926
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 20 deletions.
1 change: 1 addition & 0 deletions .cargo/config
Original file line number Diff line number Diff line change
Expand Up @@ -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"
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<img src ="boards/proto1_0/assembled.jpg" width="600" alt="Assembled prototype P1.0 unit">
</p>

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
Expand All @@ -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.

Expand Down
9 changes: 5 additions & 4 deletions doc/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<img src ="assets/assembled.jpg" width="600" alt="Assembled prototype P1.0 unit">
</p>

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],
Expand All @@ -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

Expand Down
26 changes: 23 additions & 3 deletions src/ble/gatt_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<CalibrationCurve> = 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");
}
}
_ => (),
}
}
Expand Down
48 changes: 39 additions & 9 deletions src/ble/gatt_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,56 +12,83 @@
// 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<T: Into<u64>>(input: T) -> ArrayVec<u8, 8> {
let input = input.into();
if input == 0 {
return ArrayVec::try_from([0_u8].as_slice()).unwrap();
}
let mut out: ArrayVec<u8, 8> = 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 {
fn opcode(&self) -> u8 {
match self {
DataOpcode::BatteryVoltage(..)
| DataOpcode::AppVersion(..)
| DataOpcode::ProgressorId(..) => 0x00,
| DataOpcode::ProgressorId(..)
| DataOpcode::CalibrationCurve(..) => 0x00,
DataOpcode::Weight(..) => 0x01,
DataOpcode::LowPowerWarning => 0x04,
}
}

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(&timestamp.to_le_bytes());
value[4..8].copy_from_slice(&timestamp.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
}
Expand All @@ -72,15 +99,15 @@ impl DataOpcode {
pub(crate) struct DataPoint {
opcode: u8,
length: u8,
value: [u8; 8],
value: [u8; DATA_PAYLOAD_SIZE],
}

impl DataPoint {
/// Create a new `DataPoint` from scratch
///
/// 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,
Expand Down Expand Up @@ -123,6 +150,7 @@ pub(crate) enum ControlOpcode {
StartPeakRfdMeasurementSeries,
AddCalibrationPoint(f32),
SaveCalibration,
GetCalibrationCurve,
GetAppVersion,
GetErrorInfo,
ClearErrorInfo,
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -200,6 +229,7 @@ impl GattValue for ControlOpcode {
0x6E => Self::Shutdown,
0x6F => Self::SampleBattery,
0x70 => Self::GetProgressorID,
0x72 => Self::GetCalibrationCurve,
_ => Self::Unknown(opcode),
}
}
Expand Down

0 comments on commit bc4b926

Please sign in to comment.