Repo of no-std crates for my personal embedded projects

Initial commit

sachy.dev 19718da0

verified
Changed files
+1074
sachy-battery
sachy-bthome
sachy-fmt
+1
.gitignore
···
+
/target
+170
Cargo.lock
···
+
# This file is automatically @generated by Cargo.
+
# It is not intended for manual editing.
+
version = 4
+
+
[[package]]
+
name = "bitflags"
+
version = "1.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+
[[package]]
+
name = "byteorder"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+
[[package]]
+
name = "defmt"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78"
+
dependencies = [
+
"bitflags",
+
"defmt-macros",
+
]
+
+
[[package]]
+
name = "defmt-macros"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e"
+
dependencies = [
+
"defmt-parser",
+
"proc-macro-error2",
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "defmt-parser"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+
dependencies = [
+
"thiserror",
+
]
+
+
[[package]]
+
name = "hash32"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+
dependencies = [
+
"byteorder",
+
]
+
+
[[package]]
+
name = "heapless"
+
version = "0.9.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
+
dependencies = [
+
"defmt",
+
"hash32",
+
"stable_deref_trait",
+
]
+
+
[[package]]
+
name = "proc-macro-error-attr2"
+
version = "2.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
]
+
+
[[package]]
+
name = "proc-macro-error2"
+
version = "2.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+
dependencies = [
+
"proc-macro-error-attr2",
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "proc-macro2"
+
version = "1.0.103"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+
dependencies = [
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "quote"
+
version = "1.0.42"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+
dependencies = [
+
"proc-macro2",
+
]
+
+
[[package]]
+
name = "sachy-battery"
+
version = "0.1.0"
+
+
[[package]]
+
name = "sachy-bthome"
+
version = "0.1.0"
+
dependencies = [
+
"defmt",
+
"heapless",
+
"sachy-fmt",
+
]
+
+
[[package]]
+
name = "sachy-fmt"
+
version = "0.1.0"
+
dependencies = [
+
"defmt",
+
]
+
+
[[package]]
+
name = "stable_deref_trait"
+
version = "1.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+
[[package]]
+
name = "syn"
+
version = "2.0.111"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "thiserror"
+
version = "2.0.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+
dependencies = [
+
"thiserror-impl",
+
]
+
+
[[package]]
+
name = "thiserror-impl"
+
version = "2.0.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "unicode-ident"
+
version = "1.0.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+11
Cargo.toml
···
+
[workspace]
+
resolver = "3"
+
members = ["sachy-battery","sachy-bthome", "sachy-fmt"]
+
+
[workspace.package]
+
authors = ["Sachymetsu <sachymetsu@tutamail.com>"]
+
edition = "2024"
+
repository = "https://tangled.org/sachy.dev/sachy-embed-core/"
+
license = "MIT OR Apache-2.0"
+
version = "0.1.0"
+
rust-version = "1.88.0"
+201
LICENSE-APACHE
···
+
Apache License
+
Version 2.0, January 2004
+
http://www.apache.org/licenses/
+
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+
1. Definitions.
+
+
"License" shall mean the terms and conditions for use, reproduction,
+
and distribution as defined by Sections 1 through 9 of this document.
+
+
"Licensor" shall mean the copyright owner or entity authorized by
+
the copyright owner that is granting the License.
+
+
"Legal Entity" shall mean the union of the acting entity and all
+
other entities that control, are controlled by, or are under common
+
control with that entity. For the purposes of this definition,
+
"control" means (i) the power, direct or indirect, to cause the
+
direction or management of such entity, whether by contract or
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
+
outstanding shares, or (iii) beneficial ownership of such entity.
+
+
"You" (or "Your") shall mean an individual or Legal Entity
+
exercising permissions granted by this License.
+
+
"Source" form shall mean the preferred form for making modifications,
+
including but not limited to software source code, documentation
+
source, and configuration files.
+
+
"Object" form shall mean any form resulting from mechanical
+
transformation or translation of a Source form, including but
+
not limited to compiled object code, generated documentation,
+
and conversions to other media types.
+
+
"Work" shall mean the work of authorship, whether in Source or
+
Object form, made available under the License, as indicated by a
+
copyright notice that is included in or attached to the work
+
(an example is provided in the Appendix below).
+
+
"Derivative Works" shall mean any work, whether in Source or Object
+
form, that is based on (or derived from) the Work and for which the
+
editorial revisions, annotations, elaborations, or other modifications
+
represent, as a whole, an original work of authorship. For the purposes
+
of this License, Derivative Works shall not include works that remain
+
separable from, or merely link (or bind by name) to the interfaces of,
+
the Work and Derivative Works thereof.
+
+
"Contribution" shall mean any work of authorship, including
+
the original version of the Work and any modifications or additions
+
to that Work or Derivative Works thereof, that is intentionally
+
submitted to Licensor for inclusion in the Work by the copyright owner
+
or by an individual or Legal Entity authorized to submit on behalf of
+
the copyright owner. For the purposes of this definition, "submitted"
+
means any form of electronic, verbal, or written communication sent
+
to the Licensor or its representatives, including but not limited to
+
communication on electronic mailing lists, source code control systems,
+
and issue tracking systems that are managed by, or on behalf of, the
+
Licensor for the purpose of discussing and improving the Work, but
+
excluding communication that is conspicuously marked or otherwise
+
designated in writing by the copyright owner as "Not a Contribution."
+
+
"Contributor" shall mean Licensor and any individual or Legal Entity
+
on behalf of whom a Contribution has been received by Licensor and
+
subsequently incorporated within the Work.
+
+
2. Grant of Copyright License. Subject to the terms and conditions of
+
this License, each Contributor hereby grants to You a perpetual,
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+
copyright license to reproduce, prepare Derivative Works of,
+
publicly display, publicly perform, sublicense, and distribute the
+
Work and such Derivative Works in Source or Object form.
+
+
3. Grant of Patent License. Subject to the terms and conditions of
+
this License, each Contributor hereby grants to You a perpetual,
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+
(except as stated in this section) patent license to make, have made,
+
use, offer to sell, sell, import, and otherwise transfer the Work,
+
where such license applies only to those patent claims licensable
+
by such Contributor that are necessarily infringed by their
+
Contribution(s) alone or by combination of their Contribution(s)
+
with the Work to which such Contribution(s) was submitted. If You
+
institute patent litigation against any entity (including a
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
+
or a Contribution incorporated within the Work constitutes direct
+
or contributory patent infringement, then any patent licenses
+
granted to You under this License for that Work shall terminate
+
as of the date such litigation is filed.
+
+
4. Redistribution. You may reproduce and distribute copies of the
+
Work or Derivative Works thereof in any medium, with or without
+
modifications, and in Source or Object form, provided that You
+
meet the following conditions:
+
+
(a) You must give any other recipients of the Work or
+
Derivative Works a copy of this License; and
+
+
(b) You must cause any modified files to carry prominent notices
+
stating that You changed the files; and
+
+
(c) You must retain, in the Source form of any Derivative Works
+
that You distribute, all copyright, patent, trademark, and
+
attribution notices from the Source form of the Work,
+
excluding those notices that do not pertain to any part of
+
the Derivative Works; and
+
+
(d) If the Work includes a "NOTICE" text file as part of its
+
distribution, then any Derivative Works that You distribute must
+
include a readable copy of the attribution notices contained
+
within such NOTICE file, excluding those notices that do not
+
pertain to any part of the Derivative Works, in at least one
+
of the following places: within a NOTICE text file distributed
+
as part of the Derivative Works; within the Source form or
+
documentation, if provided along with the Derivative Works; or,
+
within a display generated by the Derivative Works, if and
+
wherever such third-party notices normally appear. The contents
+
of the NOTICE file are for informational purposes only and
+
do not modify the License. You may add Your own attribution
+
notices within Derivative Works that You distribute, alongside
+
or as an addendum to the NOTICE text from the Work, provided
+
that such additional attribution notices cannot be construed
+
as modifying the License.
+
+
You may add Your own copyright statement to Your modifications and
+
may provide additional or different license terms and conditions
+
for use, reproduction, or distribution of Your modifications, or
+
for any such Derivative Works as a whole, provided Your use,
+
reproduction, and distribution of the Work otherwise complies with
+
the conditions stated in this License.
+
+
5. Submission of Contributions. Unless You explicitly state otherwise,
+
any Contribution intentionally submitted for inclusion in the Work
+
by You to the Licensor shall be under the terms and conditions of
+
this License, without any additional terms or conditions.
+
Notwithstanding the above, nothing herein shall supersede or modify
+
the terms of any separate license agreement you may have executed
+
with Licensor regarding such Contributions.
+
+
6. Trademarks. This License does not grant permission to use the trade
+
names, trademarks, service marks, or product names of the Licensor,
+
except as required for reasonable and customary use in describing the
+
origin of the Work and reproducing the content of the NOTICE file.
+
+
7. Disclaimer of Warranty. Unless required by applicable law or
+
agreed to in writing, Licensor provides the Work (and each
+
Contributor provides its Contributions) on an "AS IS" BASIS,
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+
implied, including, without limitation, any warranties or conditions
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+
PARTICULAR PURPOSE. You are solely responsible for determining the
+
appropriateness of using or redistributing the Work and assume any
+
risks associated with Your exercise of permissions under this License.
+
+
8. Limitation of Liability. In no event and under no legal theory,
+
whether in tort (including negligence), contract, or otherwise,
+
unless required by applicable law (such as deliberate and grossly
+
negligent acts) or agreed to in writing, shall any Contributor be
+
liable to You for damages, including any direct, indirect, special,
+
incidental, or consequential damages of any character arising as a
+
result of this License or out of the use or inability to use the
+
Work (including but not limited to damages for loss of goodwill,
+
work stoppage, computer failure or malfunction, or any and all
+
other commercial damages or losses), even if such Contributor
+
has been advised of the possibility of such damages.
+
+
9. Accepting Warranty or Additional Liability. While redistributing
+
the Work or Derivative Works thereof, You may choose to offer,
+
and charge a fee for, acceptance of support, warranty, indemnity,
+
or other liability obligations and/or rights consistent with this
+
License. However, in accepting such obligations, You may act only
+
on Your own behalf and on Your sole responsibility, not on behalf
+
of any other Contributor, and only if You agree to indemnify,
+
defend, and hold each Contributor harmless for any liability
+
incurred by, or claims asserted against, such Contributor by reason
+
of your accepting any such warranty or additional liability.
+
+
END OF TERMS AND CONDITIONS
+
+
APPENDIX: How to apply the Apache License to your work.
+
+
To apply the Apache License to your work, attach the following
+
boilerplate notice, with the fields enclosed by brackets "[]"
+
replaced with your own identifying information. (Don't include
+
the brackets!) The text should be enclosed in the appropriate
+
comment syntax for the file format. We also recommend that a
+
file or class name and description of purpose be included on the
+
same "printed page" as the copyright notice for easier
+
identification within third-party archives.
+
+
Copyright 2025 Sachymetsu
+
+
Licensed under the Apache License, Version 2.0 (the "License");
+
you may not use this file except in compliance with the License.
+
You may obtain a copy of the License at
+
+
http://www.apache.org/licenses/LICENSE-2.0
+
+
Unless required by applicable law or agreed to in writing, software
+
distributed under the License is distributed on an "AS IS" BASIS,
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
See the License for the specific language governing permissions and
+
limitations under the License.
+21
LICENSE-MIT
···
+
MIT License
+
+
Copyright (c) 2025 Sachymetsu
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy
+
of this software and associated documentation files (the "Software"), to deal
+
in the Software without restriction, including without limitation the rights
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+
copies of the Software, and to permit persons to whom the Software is
+
furnished to do so, subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+
SOFTWARE.
+3
README.md
···
+
# Sachy's Embeddable Core crates
+
+
This is a repo of various crates that I have written for myself for various embedded projects I do in my own time. Not really production ready or meant for outside use, but feel free to contribute if you do find use for them. Just no AI slop code whatsoever.
+11
sachy-battery/Cargo.toml
···
+
[package]
+
name = "sachy-battery"
+
description = "A crate for defining battery life profiles"
+
authors.workspace = true
+
edition.workspace = true
+
repository.workspace = true
+
license.workspace = true
+
version.workspace = true
+
rust-version.workspace = true
+
+
[dependencies]
+108
sachy-battery/src/lib.rs
···
+
//! Crate for calculating Battery levels as percentages, based on voltage/pct profiles via
+
//! [`BatteryDischargeProfile`].
+
#![no_std]
+
+
use core::ops::Range;
+
+
pub struct BatteryDischargeProfile {
+
voltage_range: Range<f32>,
+
pct_range: Range<f32>,
+
}
+
+
impl BatteryDischargeProfile {
+
/// Creates a new discharge profile. Internally, it stores the voltages high/low and pct high/low
+
/// as ranges.
+
#[inline]
+
pub const fn new(voltage_high: f32, voltage_low: f32, pct_high: f32, pct_low: f32) -> Self {
+
Self {
+
voltage_range: voltage_low..voltage_high,
+
pct_range: pct_low..pct_high,
+
}
+
}
+
+
/// Calculates a battery percentage according to the specified range of the discharge profile.
+
/// If the voltage is outside of the discharge profile, this method returns `None`.
+
///
+
/// ```
+
/// use sachy_battery::BatteryDischargeProfile;
+
///
+
/// let level = BatteryDischargeProfile::new(3.0, 2.0, 1.0, 0.0);
+
///
+
/// assert_eq!(level.calc_pct(2.5), Some(0.5));
+
/// ```
+
pub fn calc_pct(&self, voltage: f32) -> Option<f32> {
+
if self.voltage_range.contains(&voltage) {
+
Some(
+
self.pct_range.start
+
+ (voltage - self.voltage_range.start)
+
* ((self.pct_range.end - self.pct_range.start)
+
/ (self.voltage_range.end - self.voltage_range.start)),
+
)
+
} else {
+
None
+
}
+
}
+
+
/// Calculates a battery level from a range of discharge profiles. Assumes the first
+
/// discharge level is the highest, so the levels go from high to low. Percentages values
+
/// are from 1.0 to 0.0.
+
///
+
/// ```
+
/// use sachy_battery::BatteryDischargeProfile;
+
///
+
/// let levels = [
+
/// BatteryDischargeProfile::new(3.0, 2.5, 1.0, 0.5),
+
/// BatteryDischargeProfile::new(2.5, 2.0, 0.5, 0.0),
+
/// ];
+
///
+
/// assert_eq!(BatteryDischargeProfile::calc_pct_from_profile_range(2.75, levels.iter()), 0.75);
+
/// ```
+
pub fn calc_pct_from_profile_range<'a>(
+
voltage: f32,
+
levels: impl Iterator<Item = &'a BatteryDischargeProfile>,
+
) -> f32 {
+
let mut levels = levels.peekable();
+
+
if levels
+
.peek()
+
.is_some_and(|&level| voltage >= level.voltage_range.end)
+
{
+
return 1.0;
+
}
+
+
levels
+
.find_map(|level| level.calc_pct(voltage))
+
.unwrap_or(0.0)
+
}
+
}
+
+
#[cfg(test)]
+
mod tests {
+
use super::*;
+
+
#[test]
+
fn battery_level_from_one_profile() {
+
let level = BatteryDischargeProfile::new(3.0, 2.0, 1.0, 0.0);
+
+
assert_eq!(level.calc_pct(2.5), Some(0.5));
+
assert_eq!(level.calc_pct(3.5), None);
+
assert_eq!(level.calc_pct(1.5), None);
+
}
+
+
#[test]
+
fn battery_level_from_profile_range() {
+
let levels = [
+
BatteryDischargeProfile::new(3.0, 2.5, 1.0, 0.5),
+
BatteryDischargeProfile::new(2.5, 2.0, 0.5, 0.0),
+
];
+
+
let expect_results: [(f32, f32); 4] = [(3.5, 1.0), (2.75, 0.75), (2.25, 0.25), (1.5, 0.0)];
+
+
for (voltage, pct) in expect_results {
+
assert_eq!(
+
BatteryDischargeProfile::calc_pct_from_profile_range(voltage, levels.iter()),
+
pct
+
);
+
}
+
}
+
}
+17
sachy-bthome/Cargo.toml
···
+
[package]
+
name = "sachy-bthome"
+
description = "A crate for providing BTHome advertisement packets"
+
version = { workspace = true }
+
edition = { workspace = true }
+
authors = { workspace = true }
+
repository = { workspace = true }
+
license = { workspace = true }
+
rust-version = { workspace = true }
+
+
[dependencies]
+
heapless = "0.9.2"
+
defmt = { version = "1", optional = true }
+
sachy-fmt = { path = "../sachy-fmt" }
+
+
[features]
+
defmt = ["dep:defmt", "heapless/defmt", "sachy-fmt/defmt"]
+273
sachy-bthome/src/lib.rs
···
+
#![no_std]
+
+
use heapless::Vec;
+
use sachy_fmt::assert;
+
+
const BR_EDR_NOT_SUPPORTED: u8 = 4;
+
const LE_GENERAL_DISCOVERABLE: u8 = 2;
+
+
const BTHOME_AD_HEADER: [u8; 8] = [
+
0x02,
+
0x01,
+
LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED,
+
0x04,
+
0x16,
+
0xD2,
+
0xFC,
+
0x40,
+
];
+
+
pub const BTHOME_UUID16: u16 = 0xFCD2;
+
+
macro_rules! impl_fields {
+
{ $(($name:ident, $id:literal, $internal_repr:ty, $external_repr:ty),)+ } => {
+
$(
+
#[derive(Debug, Clone)]
+
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
+
pub struct $name($internal_repr);
+
+
impl $name {
+
const ID: u8 = $id;
+
const SIZE: usize = core::mem::size_of::<$internal_repr>() - 1;
+
+
#[inline]
+
pub fn get(&self) -> $external_repr {
+
let mut bytes = [0u8; core::mem::size_of::<$external_repr>()];
+
bytes[0..Self::SIZE].copy_from_slice(&self.0[1..]);
+
<$external_repr>::from_le_bytes(bytes)
+
}
+
}
+
+
impl From<$name> for BtHomeEnum {
+
fn from(value: $name) -> Self {
+
Self::$name(value)
+
}
+
}
+
+
impl From<$external_repr> for $name {
+
#[inline]
+
fn from(value: $external_repr) -> Self {
+
let mut bytes = [0u8; core::mem::size_of::<$internal_repr>()];
+
bytes[0] = Self::ID;
+
bytes[1..].copy_from_slice(&value.to_le_bytes()[0..Self::SIZE]);
+
$name(bytes)
+
}
+
}
+
)*
+
+
#[derive(Debug, Clone)]
+
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
+
pub enum BtHomeEnum {
+
$(
+
$name($name),
+
)*
+
}
+
+
impl PartialEq for BtHomeEnum {
+
fn eq(&self, other: &Self) -> bool {
+
self.id() == other.id()
+
}
+
}
+
+
impl Eq for BtHomeEnum {}
+
+
impl PartialOrd for BtHomeEnum {
+
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
+
Some(self.cmp(other))
+
}
+
}
+
+
impl Ord for BtHomeEnum {
+
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+
self.id().cmp(&other.id())
+
}
+
}
+
+
impl BtHomeEnum {
+
pub const fn id(&self) -> u8 {
+
match self {
+
$(
+
Self::$name(_) => $id,
+
)*
+
}
+
}
+
+
pub fn encode(&self) -> &[u8] {
+
match self {
+
$(
+
Self::$name(repr) => &repr.0,
+
)*
+
}
+
}
+
}
+
}
+
}
+
+
impl_fields! {
+
(Battery1Per, 0x01, [u8; 2], u8),
+
(Temperature10mK, 0x02, [u8; 3], i16),
+
(Humidity10mPer, 0x03, [u8; 3], u16),
+
(Illuminance10mLux, 0x05, [u8; 4], u32),
+
(Voltage1mV, 0x0C, [u8; 3], u16),
+
(Moisture10mPer, 0x14, [u8; 3], u16),
+
(Humidity1Per, 0x2E, [u8; 2], u8),
+
(Moisture1Per, 0x2F, [u8; 2], u8),
+
}
+
+
#[derive(Debug, Clone)]
+
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+
pub struct BtHomeAd<const N: usize> {
+
buffer: Vec<u8, N>,
+
}
+
+
impl<const N: usize> BtHomeAd<N> {
+
pub fn new() -> Self {
+
assert!(N >= BTHOME_AD_HEADER.len(), "Ad buffer is too small");
+
+
let buffer = Vec::from_iter(BTHOME_AD_HEADER);
+
+
Self { buffer }
+
}
+
+
pub fn add_data(&mut self, payload: impl Into<BtHomeEnum>) -> &mut Self {
+
let payload = payload.into();
+
let encoded = payload.encode();
+
+
assert!(
+
self.buffer.len() + encoded.len() < N,
+
"Can't fit data into buffer! {}+{}",
+
self.buffer.len(),
+
encoded.len()
+
);
+
+
self.buffer[3] += encoded.len() as u8;
+
self.buffer.extend_from_slice(encoded).ok();
+
+
self
+
}
+
+
pub fn add_local_name(&mut self, name: &str) -> &Self {
+
let len = name.len() + 1;
+
+
assert!(
+
self.buffer.len() + len < N,
+
"Can't fit local name into buffer!"
+
);
+
+
self.buffer.extend_from_slice(&[len as u8, 0x09]).ok();
+
self.buffer.extend_from_slice(name.as_bytes()).ok();
+
+
// Reborrow as ref to prevent further mutation after we have
+
// added the local name to the ad.
+
&*self
+
}
+
+
pub fn encode(&self) -> &[u8] {
+
&self.buffer
+
}
+
}
+
+
impl Default for BtHomeAd<31> {
+
#[inline]
+
fn default() -> Self {
+
Self::new()
+
}
+
}
+
+
#[cfg(test)]
+
mod tests {
+
use super::*;
+
+
#[test]
+
fn basic_add_name() {
+
let mut home = BtHomeAd::default();
+
+
let name = "hello";
+
+
home.add_local_name(name);
+
+
assert_eq!(home.buffer.len(), 15);
+
+
assert_eq!(
+
home.encode(),
+
&[
+
0x02,
+
0x01,
+
LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED,
+
0x04,
+
0x16,
+
0xD2,
+
0xFC,
+
0x40,
+
(name.len() + 1) as u8,
+
0x09,
+
b"h"[0],
+
b"e"[0],
+
b"l"[0],
+
b"l"[0],
+
b"o"[0]
+
]
+
);
+
}
+
+
#[test]
+
fn add_data() {
+
let mut home = BtHomeAd::default();
+
+
home.add_data(Battery1Per::from(34))
+
.add_data(Temperature10mK::from(2255));
+
+
assert_eq!(
+
home.encode(),
+
&[
+
0x02,
+
0x01,
+
LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED,
+
0x09,
+
0x16,
+
0xD2,
+
0xFC,
+
0x40,
+
0x01,
+
34,
+
0x02,
+
207,
+
8,
+
]
+
);
+
}
+
+
#[test]
+
fn full_payload() {
+
let mut home = BtHomeAd::default();
+
+
let encoded = home
+
.add_data(Battery1Per::from(34))
+
.add_data(Temperature10mK::from(2255))
+
.add_data(Humidity10mPer::from(3400))
+
.add_data(Illuminance10mLux::from(45000))
+
.add_data(Moisture10mPer::from(3632))
+
.add_local_name("rsachy")
+
.encode();
+
+
// Final payload is within the max size for the advertising payload
+
assert_eq!(encoded.len(), 31);
+
assert_eq!(home.buffer[3], 19);
+
+
let mut home = BtHomeAd::default();
+
+
let encoded = home
+
.add_data(Battery1Per::from(34))
+
.add_data(Temperature10mK::from(2255))
+
.add_data(Illuminance10mLux::from(45000))
+
.add_data(Voltage1mV::from(2800))
+
.add_data(Humidity1Per::from(34))
+
.add_data(Moisture1Per::from(36))
+
.add_local_name("sachy")
+
.encode();
+
+
// Final payload is within the max size for the advertising payload
+
assert_eq!(encoded.len(), 31);
+
assert_eq!(home.buffer[3], 20);
+
}
+
}
+15
sachy-fmt/Cargo.toml
···
+
[package]
+
name = "sachy-fmt"
+
description = "A crate for providing fmt shims for defmt"
+
version = { workspace = true }
+
edition = { workspace = true }
+
authors = { workspace = true }
+
repository = { workspace = true }
+
license = { workspace = true }
+
rust-version = { workspace = true }
+
+
[dependencies]
+
defmt = { version = "1", optional = true }
+
+
[features]
+
defmt = ["dep:defmt"]
+243
sachy-fmt/src/lib.rs
···
+
#![no_std]
+
#![allow(unused)]
+
+
#[macro_export]
+
macro_rules! assert {
+
($($x:tt)*) => {
+
{
+
#[cfg(not(feature = "defmt"))]
+
::core::assert!($($x)*);
+
#[cfg(feature = "defmt")]
+
::defmt::assert!($($x)*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! assert_eq {
+
($($x:tt)*) => {
+
{
+
#[cfg(not(feature = "defmt"))]
+
::core::assert_eq!($($x)*);
+
#[cfg(feature = "defmt")]
+
::defmt::assert_eq!($($x)*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! assert_ne {
+
($($x:tt)*) => {
+
{
+
#[cfg(not(feature = "defmt"))]
+
::core::assert_ne!($($x)*);
+
#[cfg(feature = "defmt")]
+
::defmt::assert_ne!($($x)*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! debug_assert {
+
($($x:tt)*) => {
+
{
+
#[cfg(not(feature = "defmt"))]
+
::core::debug_assert!($($x)*);
+
#[cfg(feature = "defmt")]
+
::defmt::debug_assert!($($x)*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! debug_assert_eq {
+
($($x:tt)*) => {
+
{
+
#[cfg(not(feature = "defmt"))]
+
::core::debug_assert_eq!($($x)*);
+
#[cfg(feature = "defmt")]
+
::defmt::debug_assert_eq!($($x)*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! debug_assert_ne {
+
($($x:tt)*) => {
+
{
+
#[cfg(not(feature = "defmt"))]
+
::core::debug_assert_ne!($($x)*);
+
#[cfg(feature = "defmt")]
+
::defmt::debug_assert_ne!($($x)*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! todo {
+
($($x:tt)*) => {
+
{
+
#[cfg(not(feature = "defmt"))]
+
::core::todo!($($x)*);
+
#[cfg(feature = "defmt")]
+
::defmt::todo!($($x)*);
+
}
+
};
+
}
+
+
#[macro_export]
+
#[cfg(not(feature = "defmt"))]
+
macro_rules! unreachable {
+
($($x:tt)*) => {
+
::core::unreachable!($($x)*)
+
};
+
}
+
+
#[macro_export]
+
#[cfg(feature = "defmt")]
+
macro_rules! unreachable {
+
($($x:tt)*) => {
+
::defmt::unreachable!($($x)*)
+
};
+
}
+
+
#[macro_export]
+
macro_rules! panic {
+
($($x:tt)*) => {
+
{
+
#[cfg(not(feature = "defmt"))]
+
::core::panic!($($x)*);
+
#[cfg(feature = "defmt")]
+
::defmt::panic!($($x)*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! trace {
+
($s:literal $(, $x:expr)* $(,)?) => {
+
{
+
#[cfg(feature = "defmt")]
+
::defmt::trace!($s $(, $x)*);
+
#[cfg(feature="defmt")]
+
let _ = ($( & $x ),*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! debug {
+
($s:literal $(, $x:expr)* $(,)?) => {
+
{
+
#[cfg(feature = "defmt")]
+
::defmt::debug!($s $(, $x)*);
+
#[cfg(not(feature="defmt"))]
+
let _ = ($( & $x ),*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! info {
+
($s:literal $(, $x:expr)* $(,)?) => {
+
{
+
#[cfg(feature = "defmt")]
+
::defmt::info!($s $(, $x)*);
+
#[cfg(not(feature="defmt"))]
+
let _ = ($( & $x ),*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! _warn {
+
($s:literal $(, $x:expr)* $(,)?) => {
+
{
+
#[cfg(feature = "defmt")]
+
::defmt::warn!($s $(, $x)*);
+
#[cfg(not(feature="defmt"))]
+
let _ = ($( & $x ),*);
+
}
+
};
+
}
+
+
#[macro_export]
+
macro_rules! error {
+
($s:literal $(, $x:expr)* $(,)?) => {
+
{
+
#[cfg(feature = "defmt")]
+
::defmt::error!($s $(, $x)*);
+
#[cfg(not(feature="defmt"))]
+
let _ = ($( & $x ),*);
+
}
+
};
+
}
+
+
#[macro_export]
+
#[cfg(feature = "defmt")]
+
macro_rules! unwrap {
+
($($x:tt)*) => {
+
::defmt::unwrap!($($x)*)
+
};
+
}
+
+
#[macro_export]
+
#[cfg(not(feature = "defmt"))]
+
macro_rules! unwrap {
+
($arg:expr) => {
+
match $crate::Try::into_result($arg) {
+
::core::result::Result::Ok(t) => t,
+
::core::result::Result::Err(_) => {
+
::core::panic!();
+
}
+
}
+
};
+
($arg:expr, $($msg:expr),+ $(,)? ) => {
+
match $crate::Try::into_result($arg) {
+
::core::result::Result::Ok(t) => t,
+
::core::result::Result::Err(_) => {
+
::core::panic!();
+
}
+
}
+
};
+
}
+
+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+
pub struct NoneError;
+
+
pub trait Try {
+
type Ok;
+
type Error;
+
fn into_result(self) -> Result<Self::Ok, Self::Error>;
+
}
+
+
impl<T> Try for Option<T> {
+
type Ok = T;
+
type Error = NoneError;
+
+
#[inline]
+
fn into_result(self) -> Result<T, NoneError> {
+
self.ok_or(NoneError)
+
}
+
}
+
+
impl<T, E> Try for Result<T, E> {
+
type Ok = T;
+
type Error = E;
+
+
#[inline]
+
fn into_result(self) -> Self {
+
self
+
}
+
}
+
+
pub(crate) struct Bytes<'a>(pub &'a [u8]);
+
+
#[cfg(feature = "defmt")]
+
impl defmt::Format for Bytes<'_> {
+
fn format(&self, fmt: defmt::Formatter) {
+
defmt::write!(fmt, "{:02x}", self.0)
+
}
+
}
+
+
pub use _warn as warn;