Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f42a0d5370 | ||
|
|
e47266f410 | ||
|
|
9a820356ff | ||
| 75bad1e97a |
13 changed files with 206 additions and 11 deletions
|
|
@ -1,2 +1,2 @@
|
||||||
stack-size-threshold = 1024
|
stack-size-threshold = 1024
|
||||||
doc-valid-idents = ["WiFi", "GeoNetworking", "QoS", ".."]
|
doc-valid-idents = ["WiFi", "GeoNetworking", "QoS", "LiPo", ".."]
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,6 @@ Additional hardware:
|
||||||
The housing is a FreeCAD model at [hardware/housing/c_its-companion-housing.FCStd](./hardware/housing/c_its-companion-housing.FCStd), but the same folder also contains finished 3mf models that can be loaded into a 3D printer slicer.
|
The housing is a FreeCAD model at [hardware/housing/c_its-companion-housing.FCStd](./hardware/housing/c_its-companion-housing.FCStd), but the same folder also contains finished 3mf models that can be loaded into a 3D printer slicer.
|
||||||
Exports are available for:
|
Exports are available for:
|
||||||
|
|
||||||
- 2mm: no battery (just display standoffs)
|
|
||||||
- 7mm: EREMIT 654060 (6.5x40x60 mm) LiPo battery
|
- 7mm: EREMIT 654060 (6.5x40x60 mm) LiPo battery
|
||||||
- 9mm: Some 9x40x60 mm LiPo battery, e.g. LP803860 (2000 mAh)
|
- 9mm: Some 9x40x60 mm LiPo battery, e.g. LP803860 (2000 mAh)
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
hardware/housing/c_its-companion-housing-bottom-v1.1-7mm.3mf
Normal file
BIN
hardware/housing/c_its-companion-housing-bottom-v1.1-7mm.3mf
Normal file
Binary file not shown.
BIN
hardware/housing/c_its-companion-housing-bottom-v1.1-9mm.3mf
Normal file
BIN
hardware/housing/c_its-companion-housing-bottom-v1.1-9mm.3mf
Normal file
Binary file not shown.
BIN
hardware/housing/c_its-companion-housing-lid-v1.1.3mf
Normal file
BIN
hardware/housing/c_its-companion-housing-lid-v1.1.3mf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
91
src/ble.rs
91
src/ble.rs
|
|
@ -23,6 +23,8 @@ pub static TX_DATA: Mutex<RefCell<Option<ArrayQueue<BleSendable>>>> =
|
||||||
pub static RX_DATA: Mutex<RefCell<Option<io::msg::PositionState>>> = Mutex::new(RefCell::new(None));
|
pub static RX_DATA: Mutex<RefCell<Option<io::msg::PositionState>>> = Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
pub enum BleSendable {
|
pub enum BleSendable {
|
||||||
|
BatteryLevel(u8),
|
||||||
|
|
||||||
#[cfg(feature = "cam")]
|
#[cfg(feature = "cam")]
|
||||||
CamRaw(io::msg::RawMsgRx),
|
CamRaw(io::msg::RawMsgRx),
|
||||||
#[cfg(feature = "cam")]
|
#[cfg(feature = "cam")]
|
||||||
|
|
@ -42,6 +44,16 @@ pub enum BleSendable {
|
||||||
#[gatt_server]
|
#[gatt_server]
|
||||||
struct Server {
|
struct Server {
|
||||||
c_its_events: CITSEvents,
|
c_its_events: CITSEvents,
|
||||||
|
battery_service: BatteryService,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gatt_service(uuid = service::BATTERY)]
|
||||||
|
struct BatteryService {
|
||||||
|
/// Battery Level
|
||||||
|
#[descriptor(uuid = descriptors::VALID_RANGE, read, value = [0, 100])]
|
||||||
|
#[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "main", read, value = "Battery Level", type = &'static str)]
|
||||||
|
#[characteristic(uuid = characteristic::BATTERY_LEVEL, read, notify, value = 0)]
|
||||||
|
level: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gatt_service(uuid = "c0b70000-d4f4-4000-ada8-f99a02ee315c")]
|
#[gatt_service(uuid = "c0b70000-d4f4-4000-ada8-f99a02ee315c")]
|
||||||
|
|
@ -110,6 +122,7 @@ pub async fn run(
|
||||||
CONTROLLER_SLOTS,
|
CONTROLLER_SLOTS,
|
||||||
>,
|
>,
|
||||||
name: alloc::string::String,
|
name: alloc::string::String,
|
||||||
|
short_name: alloc::string::String,
|
||||||
) {
|
) {
|
||||||
let mut resources: HostResources<DefaultPacketPool, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> =
|
let mut resources: HostResources<DefaultPacketPool, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> =
|
||||||
HostResources::new();
|
HostResources::new();
|
||||||
|
|
@ -120,6 +133,7 @@ pub async fn run(
|
||||||
..
|
..
|
||||||
} = stack.build();
|
} = stack.build();
|
||||||
|
|
||||||
|
info!("BLE name: {name} (short: {short_name})");
|
||||||
info!(
|
info!(
|
||||||
"Starting advertising and GATT service for {}",
|
"Starting advertising and GATT service for {}",
|
||||||
crate::util::get_rx_feature_list().join(",")
|
crate::util::get_rx_feature_list().join(",")
|
||||||
|
|
@ -132,8 +146,11 @@ pub async fn run(
|
||||||
|
|
||||||
let _ = embassy_futures::join::join(ble_task(runner), async {
|
let _ = embassy_futures::join::join(ble_task(runner), async {
|
||||||
loop {
|
loop {
|
||||||
match advertise(&mut peripheral, &server).await {
|
// run advertisement and set data until a client connects
|
||||||
Ok(conn) => {
|
let a = advertise(&mut peripheral, &server, &short_name);
|
||||||
|
let b = data_update_task(&server);
|
||||||
|
match embassy_futures::select::select(a, b).await {
|
||||||
|
embassy_futures::select::Either::First(Ok(conn)) => {
|
||||||
info!("BLE: New client connected");
|
info!("BLE: New client connected");
|
||||||
|
|
||||||
// set up tasks when the connection is established to a central, so they don't run when no one is connected.
|
// set up tasks when the connection is established to a central, so they don't run when no one is connected.
|
||||||
|
|
@ -148,15 +165,17 @@ pub async fn run(
|
||||||
let _ = value_ref.replace(false);
|
let _ = value_ref.replace(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
embassy_futures::select::Either::First(Err(e)) => {
|
||||||
panic!("BLE ADV: fatal error: {e:?}");
|
panic!("BLE ADV: fatal error: {e:?}");
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queues data for BLE notification (only when client is connected)
|
||||||
pub fn update(payload: BleSendable) {
|
pub fn update(payload: BleSendable) {
|
||||||
critical_section::with(|cs| {
|
critical_section::with(|cs| {
|
||||||
// only add to queue when someone is connected
|
// only add to queue when someone is connected
|
||||||
|
|
@ -172,20 +191,41 @@ pub fn update(payload: BleSendable) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queues data for characteristic update (may notify a connected client)
|
||||||
|
///
|
||||||
|
/// This can be used instead of `update()`, but will always write the data to the characteristic even when no client is connected.
|
||||||
|
/// So a newly connected client will be able to read an up to date value at connection and not just when new data was received.
|
||||||
|
///
|
||||||
|
/// This is useful for (device) information which is not time-critical (like received V2X data).
|
||||||
|
pub fn set(payload: BleSendable) {
|
||||||
|
critical_section::with(|cs| {
|
||||||
|
let queue_rc = TX_DATA.borrow(cs).borrow();
|
||||||
|
// unwrap is fine b/c we stored something in it before
|
||||||
|
let queue = queue_rc.as_ref().unwrap();
|
||||||
|
|
||||||
|
if queue.push(payload).is_err() {
|
||||||
|
log::error!("BLE TX queue is full");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create an advertiser to use to connect to a BLE Central, and wait for it to connect.
|
/// Create an advertiser to use to connect to a BLE Central, and wait for it to connect.
|
||||||
async fn advertise<'values, 'server, C: Controller>(
|
async fn advertise<'values, 'server, C: Controller>(
|
||||||
peripheral: &mut Peripheral<'values, C, DefaultPacketPool>,
|
peripheral: &mut Peripheral<'values, C, DefaultPacketPool>,
|
||||||
server: &'server Server<'values>,
|
server: &'server Server<'values>,
|
||||||
|
// name in advertisement, max. 8 characters
|
||||||
|
short_name: &'values str,
|
||||||
) -> Result<GattConnection<'values, 'server, DefaultPacketPool>, BleHostError<C::Error>> {
|
) -> Result<GattConnection<'values, 'server, DefaultPacketPool>, BleHostError<C::Error>> {
|
||||||
let mut advertiser_data = [0; 35];
|
let mut advertiser_data = [0; 31]; // 37 bytes payload - 6 byte address
|
||||||
let service_uuid = Uuid::from(0xc0b7_0000_d4f4_4000_ada8_f99a_02ee_315c_u128)
|
let service_uuid = Uuid::from(0xc0b7_0000_d4f4_4000_ada8_f99a_02ee_315c_u128)
|
||||||
.as_raw()
|
.as_raw()
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let len = AdStructure::encode_slice(
|
let len = AdStructure::encode_slice(
|
||||||
&[
|
&[
|
||||||
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
|
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), // 2+ 1 byte
|
||||||
AdStructure::ServiceUuids128(&[service_uuid]),
|
AdStructure::ServiceUuids128(&[service_uuid]), // 2+16 byte = 21
|
||||||
|
AdStructure::ShortenedLocalName(short_name.as_bytes()), // 2+ 8 byte = 31
|
||||||
],
|
],
|
||||||
&mut advertiser_data[..],
|
&mut advertiser_data[..],
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -193,7 +233,11 @@ async fn advertise<'values, 'server, C: Controller>(
|
||||||
let advertiser = peripheral
|
let advertiser = peripheral
|
||||||
.advertise(
|
.advertise(
|
||||||
&trouble_host::advertise::AdvertisementParameters {
|
&trouble_host::advertise::AdvertisementParameters {
|
||||||
timeout: Some(embassy_time::Duration::from_secs(2)),
|
primary_phy: PhyKind::Le2M,
|
||||||
|
secondary_phy: PhyKind::Le2M,
|
||||||
|
timeout: Some(embassy_time::Duration::from_secs(3)),
|
||||||
|
interval_min: embassy_time::Duration::from_millis(150),
|
||||||
|
interval_max: embassy_time::Duration::from_secs(1),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Advertisement::ConnectableScannableUndirected {
|
Advertisement::ConnectableScannableUndirected {
|
||||||
|
|
@ -286,6 +330,12 @@ async fn publishing_task<P: PacketPool>(server: &Server<'_>, conn: &GattConnecti
|
||||||
});
|
});
|
||||||
|
|
||||||
match new_data {
|
match new_data {
|
||||||
|
Some(BleSendable::BatteryLevel(value)) => {
|
||||||
|
if let Err(err) = server.battery_service.level.notify(conn, &value).await {
|
||||||
|
warn!("BLE: failed notifying connection of new battery level: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cam")]
|
#[cfg(feature = "cam")]
|
||||||
Some(BleSendable::Cam(cam_event)) => match cam_event.serialize_ble_proto() {
|
Some(BleSendable::Cam(cam_event)) => match cam_event.serialize_ble_proto() {
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
|
|
@ -387,6 +437,33 @@ async fn publishing_task<P: PacketPool>(server: &Server<'_>, conn: &GattConnecti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn data_update_task(server: &Server<'_>) {
|
||||||
|
loop {
|
||||||
|
embassy_time::Timer::after(embassy_time::Duration::from_millis(10)).await;
|
||||||
|
|
||||||
|
let mut new_data = None;
|
||||||
|
critical_section::with(|cs| {
|
||||||
|
let queue_rc = TX_DATA.borrow(cs).borrow();
|
||||||
|
// unwrap is fine b/c we stored something in it before
|
||||||
|
let queue = queue_rc.as_ref().unwrap();
|
||||||
|
|
||||||
|
if let Some(data) = queue.pop() {
|
||||||
|
new_data = Some(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match new_data {
|
||||||
|
Some(BleSendable::BatteryLevel(value)) => {
|
||||||
|
if let Err(err) = server.battery_service.level.set(server, &value) {
|
||||||
|
warn!("BLE: failed setting new battery level: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_raw_notify<P: PacketPool>(
|
async fn send_raw_notify<P: PacketPool>(
|
||||||
server: &Server<'_>,
|
server: &Server<'_>,
|
||||||
conn: &GattConnection<'_, '_, P>,
|
conn: &GattConnection<'_, '_, P>,
|
||||||
|
|
|
||||||
29
src/main.rs
29
src/main.rs
|
|
@ -59,7 +59,9 @@ mod screen;
|
||||||
#[cfg(feature = "gnss_ubx")]
|
#[cfg(feature = "gnss_ubx")]
|
||||||
mod ubx;
|
mod ubx;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod xiao;
|
||||||
|
|
||||||
|
const BAT_UPDATE_INTERVAL: esp_hal::time::Duration = esp_hal::time::Duration::from_secs(30);
|
||||||
const WIFI_CHANNEL: radio::Channel = 180;
|
const WIFI_CHANNEL: radio::Channel = 180;
|
||||||
const WIFI_TXPOWER: i8 = 10; // dBm, 2..20
|
const WIFI_TXPOWER: i8 = 10; // dBm, 2..20
|
||||||
#[cfg(feature = "spat")]
|
#[cfg(feature = "spat")]
|
||||||
|
|
@ -144,6 +146,10 @@ async fn main(spawner: Spawner) -> ! {
|
||||||
|
|
||||||
info!("Embassy initialized!");
|
info!("Embassy initialized!");
|
||||||
|
|
||||||
|
// Setup battery voltage readout (GPIO6 as input and GPIO26 as enable)
|
||||||
|
let mut battery = xiao::Battery::new(peripherals.ADC1, peripherals.GPIO6, peripherals.GPIO26);
|
||||||
|
// TODO: expose via BLE as battery service!
|
||||||
|
|
||||||
// Setup ST7789 screen
|
// Setup ST7789 screen
|
||||||
// SCL: D8/ GPIO8 (SPI_SCK)
|
// SCL: D8/ GPIO8 (SPI_SCK)
|
||||||
// SDA: D10/ GPIO10 (SPI_MOSI)
|
// SDA: D10/ GPIO10 (SPI_MOSI)
|
||||||
|
|
@ -222,12 +228,13 @@ async fn main(spawner: Spawner) -> ! {
|
||||||
let controller = ble::make_controller(peripherals.BT);
|
let controller = ble::make_controller(peripherals.BT);
|
||||||
let serial = &mac.as_bytes()[3..6];
|
let serial = &mac.as_bytes()[3..6];
|
||||||
let name = alloc::format!(
|
let name = alloc::format!(
|
||||||
"ESP32 C-ITS {:02x}{:02x}{:02x}",
|
"C-ITS ESP {:02x}:{:02x}:{:02x}",
|
||||||
serial[0],
|
serial[0],
|
||||||
serial[1],
|
serial[1],
|
||||||
serial[2]
|
serial[2]
|
||||||
);
|
);
|
||||||
spawner.spawn(ble::run(controller, name).expect("Failed to spawn BLE task"));
|
let short_name = alloc::format!("V2X {:02x}{:02x}", serial[1], serial[2]); // max 8 chars!
|
||||||
|
spawner.spawn(ble::run(controller, name, short_name).expect("Failed to spawn BLE task"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup GNSS:
|
// Setup GNSS:
|
||||||
|
|
@ -272,6 +279,13 @@ async fn main(spawner: Spawner) -> ! {
|
||||||
#[cfg(feature = "_gnss")]
|
#[cfg(feature = "_gnss")]
|
||||||
info!("GNSS task started, waiting for GNSS fix...");
|
info!("GNSS task started, waiting for GNSS fix...");
|
||||||
|
|
||||||
|
// Setup main loop
|
||||||
|
let (bat_percent, bat_millivolt) = battery.read();
|
||||||
|
log::info!("Battery: {bat_millivolt} mV, {bat_percent} %");
|
||||||
|
#[cfg(feature = "ble")]
|
||||||
|
ble::set(ble::BleSendable::BatteryLevel(bat_percent));
|
||||||
|
let mut last_bat_update = esp_hal::time::Instant::now();
|
||||||
|
|
||||||
#[cfg(any(feature = "cam_tx", feature = "uart"))]
|
#[cfg(any(feature = "cam_tx", feature = "uart"))]
|
||||||
let (mut seq_no, mac_bytes) = {
|
let (mut seq_no, mac_bytes) = {
|
||||||
// unwrap is fine since [`esp_hal::efuse::MacAddress`] is just an `[u8; 6]`
|
// unwrap is fine since [`esp_hal::efuse::MacAddress`] is just an `[u8; 6]`
|
||||||
|
|
@ -614,6 +628,17 @@ async fn main(spawner: Spawner) -> ! {
|
||||||
|
|
||||||
// maintenance
|
// maintenance
|
||||||
state.prune();
|
state.prune();
|
||||||
|
|
||||||
|
let cpu_now = esp_hal::time::Instant::now();
|
||||||
|
if last_bat_update + BAT_UPDATE_INTERVAL < cpu_now {
|
||||||
|
let (bat_percent, bat_millivolt) = battery.read();
|
||||||
|
log::info!("Battery: {bat_millivolt} mV, {bat_percent} %");
|
||||||
|
|
||||||
|
#[cfg(feature = "ble")]
|
||||||
|
ble::set(ble::BleSendable::BatteryLevel(bat_percent));
|
||||||
|
|
||||||
|
last_bat_update = cpu_now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
94
src/xiao.rs
Normal file
94
src/xiao.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
//! XIAO ESP32-C5 Helpers
|
||||||
|
|
||||||
|
use esp_hal::analog::adc;
|
||||||
|
use esp_hal::gpio;
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
type AdcPeripheral<'a> = esp_hal::peripherals::ADC1<'a>;
|
||||||
|
|
||||||
|
pub struct Battery<'a, IoAdc>
|
||||||
|
where
|
||||||
|
IoAdc: adc::AdcChannel + gpio::AnalogPin,
|
||||||
|
{
|
||||||
|
// using `AdcCalCurve` here gives readings in millivolts
|
||||||
|
adc_pin: adc::AdcPin<IoAdc, AdcPeripheral<'a>, adc::AdcCalCurve<AdcPeripheral<'a>>>,
|
||||||
|
adc: adc::Adc<'a, AdcPeripheral<'a>, esp_hal::Blocking>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, IoAdc> Battery<'a, IoAdc>
|
||||||
|
where
|
||||||
|
IoAdc: adc::AdcChannel + gpio::AnalogPin,
|
||||||
|
{
|
||||||
|
pub fn new<IoEnable: gpio::OutputPin>(
|
||||||
|
adc_periph: AdcPeripheral<'a>,
|
||||||
|
adc_pin: IoAdc,
|
||||||
|
enable_pin: IoEnable,
|
||||||
|
) -> Self {
|
||||||
|
// enable battery voltage readout
|
||||||
|
let _ = gpio::Output::new(enable_pin, gpio::Level::High, gpio::OutputConfig::default());
|
||||||
|
|
||||||
|
// configure ADC
|
||||||
|
let mut adc_config = adc::AdcConfig::new();
|
||||||
|
let adc_pin = adc_config.enable_pin_with_cal(adc_pin, adc::Attenuation::_11dB);
|
||||||
|
let adc = adc::Adc::new(adc_periph, adc_config);
|
||||||
|
|
||||||
|
Self { adc_pin, adc }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads from ADC and returns battery voltage in millivolts
|
||||||
|
pub fn read_millivolts(&mut self) -> u16 {
|
||||||
|
// average over 16 samples
|
||||||
|
let mut accu = 0u16;
|
||||||
|
for _ in 0..16 {
|
||||||
|
let val = loop {
|
||||||
|
match self.adc.read_oneshot(&mut self.adc_pin) {
|
||||||
|
Ok(value) => break value,
|
||||||
|
Err(err) => {
|
||||||
|
debug!("Failed to read from ADC: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
accu += val;
|
||||||
|
}
|
||||||
|
let adc_val = accu / 16;
|
||||||
|
|
||||||
|
// convert to battery voltage: 1:2 voltage divider
|
||||||
|
adc_val * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns estimated battery charge state in percent
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn read_percent(&mut self) -> u8 {
|
||||||
|
Self::millivolts_to_percent(self.read_millivolts())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns estimated battery charge state in percent and millivolts
|
||||||
|
pub fn read(&mut self) -> (u8, u16) {
|
||||||
|
let millivolt = self.read_millivolts();
|
||||||
|
let percent = Self::millivolts_to_percent(millivolt);
|
||||||
|
|
||||||
|
(percent, millivolt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts single cell LiPo battery voltage to charge state in percent
|
||||||
|
///
|
||||||
|
/// Note: The implementation is a drastically simplified linear estimation.
|
||||||
|
/// This is known to be insufficient for an actual state of charge, but should be fine for now.
|
||||||
|
pub fn millivolts_to_percent(millivolt: u16) -> u8 {
|
||||||
|
let (full_mv, empty_mv) = (4200, 3270); // 100% to 0% SoC
|
||||||
|
|
||||||
|
if millivolt < empty_mv {
|
||||||
|
0
|
||||||
|
} else if millivolt >= full_mv {
|
||||||
|
100
|
||||||
|
} else {
|
||||||
|
let percent = i32::from(millivolt - empty_mv) * 100 / i32::from(full_mv - empty_mv);
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
let result = percent as u8;
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue