Repo of no-std crates for my personal embedded projects

Initial commit

sachy.dev 19718da0

+1074
+1
.gitignore
··· 1 + /target
+170
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "bitflags" 7 + version = "1.3.2" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 10 + 11 + [[package]] 12 + name = "byteorder" 13 + version = "1.5.0" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 16 + 17 + [[package]] 18 + name = "defmt" 19 + version = "1.0.1" 20 + source = "registry+https://github.com/rust-lang/crates.io-index" 21 + checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" 22 + dependencies = [ 23 + "bitflags", 24 + "defmt-macros", 25 + ] 26 + 27 + [[package]] 28 + name = "defmt-macros" 29 + version = "1.0.1" 30 + source = "registry+https://github.com/rust-lang/crates.io-index" 31 + checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" 32 + dependencies = [ 33 + "defmt-parser", 34 + "proc-macro-error2", 35 + "proc-macro2", 36 + "quote", 37 + "syn", 38 + ] 39 + 40 + [[package]] 41 + name = "defmt-parser" 42 + version = "1.0.0" 43 + source = "registry+https://github.com/rust-lang/crates.io-index" 44 + checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" 45 + dependencies = [ 46 + "thiserror", 47 + ] 48 + 49 + [[package]] 50 + name = "hash32" 51 + version = "0.3.1" 52 + source = "registry+https://github.com/rust-lang/crates.io-index" 53 + checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 54 + dependencies = [ 55 + "byteorder", 56 + ] 57 + 58 + [[package]] 59 + name = "heapless" 60 + version = "0.9.2" 61 + source = "registry+https://github.com/rust-lang/crates.io-index" 62 + checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" 63 + dependencies = [ 64 + "defmt", 65 + "hash32", 66 + "stable_deref_trait", 67 + ] 68 + 69 + [[package]] 70 + name = "proc-macro-error-attr2" 71 + version = "2.0.0" 72 + source = "registry+https://github.com/rust-lang/crates.io-index" 73 + checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" 74 + dependencies = [ 75 + "proc-macro2", 76 + "quote", 77 + ] 78 + 79 + [[package]] 80 + name = "proc-macro-error2" 81 + version = "2.0.1" 82 + source = "registry+https://github.com/rust-lang/crates.io-index" 83 + checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" 84 + dependencies = [ 85 + "proc-macro-error-attr2", 86 + "proc-macro2", 87 + "quote", 88 + "syn", 89 + ] 90 + 91 + [[package]] 92 + name = "proc-macro2" 93 + version = "1.0.103" 94 + source = "registry+https://github.com/rust-lang/crates.io-index" 95 + checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 96 + dependencies = [ 97 + "unicode-ident", 98 + ] 99 + 100 + [[package]] 101 + name = "quote" 102 + version = "1.0.42" 103 + source = "registry+https://github.com/rust-lang/crates.io-index" 104 + checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 105 + dependencies = [ 106 + "proc-macro2", 107 + ] 108 + 109 + [[package]] 110 + name = "sachy-battery" 111 + version = "0.1.0" 112 + 113 + [[package]] 114 + name = "sachy-bthome" 115 + version = "0.1.0" 116 + dependencies = [ 117 + "defmt", 118 + "heapless", 119 + "sachy-fmt", 120 + ] 121 + 122 + [[package]] 123 + name = "sachy-fmt" 124 + version = "0.1.0" 125 + dependencies = [ 126 + "defmt", 127 + ] 128 + 129 + [[package]] 130 + name = "stable_deref_trait" 131 + version = "1.2.1" 132 + source = "registry+https://github.com/rust-lang/crates.io-index" 133 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 134 + 135 + [[package]] 136 + name = "syn" 137 + version = "2.0.111" 138 + source = "registry+https://github.com/rust-lang/crates.io-index" 139 + checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 140 + dependencies = [ 141 + "proc-macro2", 142 + "quote", 143 + "unicode-ident", 144 + ] 145 + 146 + [[package]] 147 + name = "thiserror" 148 + version = "2.0.17" 149 + source = "registry+https://github.com/rust-lang/crates.io-index" 150 + checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 151 + dependencies = [ 152 + "thiserror-impl", 153 + ] 154 + 155 + [[package]] 156 + name = "thiserror-impl" 157 + version = "2.0.17" 158 + source = "registry+https://github.com/rust-lang/crates.io-index" 159 + checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 160 + dependencies = [ 161 + "proc-macro2", 162 + "quote", 163 + "syn", 164 + ] 165 + 166 + [[package]] 167 + name = "unicode-ident" 168 + version = "1.0.22" 169 + source = "registry+https://github.com/rust-lang/crates.io-index" 170 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+11
Cargo.toml
··· 1 + [workspace] 2 + resolver = "3" 3 + members = ["sachy-battery","sachy-bthome", "sachy-fmt"] 4 + 5 + [workspace.package] 6 + authors = ["Sachymetsu <sachymetsu@tutamail.com>"] 7 + edition = "2024" 8 + repository = "https://tangled.org/sachy.dev/sachy-embed-core/" 9 + license = "MIT OR Apache-2.0" 10 + version = "0.1.0" 11 + rust-version = "1.88.0"
+201
LICENSE-APACHE
··· 1 + Apache License 2 + Version 2.0, January 2004 3 + http://www.apache.org/licenses/ 4 + 5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 + 7 + 1. Definitions. 8 + 9 + "License" shall mean the terms and conditions for use, reproduction, 10 + and distribution as defined by Sections 1 through 9 of this document. 11 + 12 + "Licensor" shall mean the copyright owner or entity authorized by 13 + the copyright owner that is granting the License. 14 + 15 + "Legal Entity" shall mean the union of the acting entity and all 16 + other entities that control, are controlled by, or are under common 17 + control with that entity. For the purposes of this definition, 18 + "control" means (i) the power, direct or indirect, to cause the 19 + direction or management of such entity, whether by contract or 20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 + outstanding shares, or (iii) beneficial ownership of such entity. 22 + 23 + "You" (or "Your") shall mean an individual or Legal Entity 24 + exercising permissions granted by this License. 25 + 26 + "Source" form shall mean the preferred form for making modifications, 27 + including but not limited to software source code, documentation 28 + source, and configuration files. 29 + 30 + "Object" form shall mean any form resulting from mechanical 31 + transformation or translation of a Source form, including but 32 + not limited to compiled object code, generated documentation, 33 + and conversions to other media types. 34 + 35 + "Work" shall mean the work of authorship, whether in Source or 36 + Object form, made available under the License, as indicated by a 37 + copyright notice that is included in or attached to the work 38 + (an example is provided in the Appendix below). 39 + 40 + "Derivative Works" shall mean any work, whether in Source or Object 41 + form, that is based on (or derived from) the Work and for which the 42 + editorial revisions, annotations, elaborations, or other modifications 43 + represent, as a whole, an original work of authorship. For the purposes 44 + of this License, Derivative Works shall not include works that remain 45 + separable from, or merely link (or bind by name) to the interfaces of, 46 + the Work and Derivative Works thereof. 47 + 48 + "Contribution" shall mean any work of authorship, including 49 + the original version of the Work and any modifications or additions 50 + to that Work or Derivative Works thereof, that is intentionally 51 + submitted to Licensor for inclusion in the Work by the copyright owner 52 + or by an individual or Legal Entity authorized to submit on behalf of 53 + the copyright owner. For the purposes of this definition, "submitted" 54 + means any form of electronic, verbal, or written communication sent 55 + to the Licensor or its representatives, including but not limited to 56 + communication on electronic mailing lists, source code control systems, 57 + and issue tracking systems that are managed by, or on behalf of, the 58 + Licensor for the purpose of discussing and improving the Work, but 59 + excluding communication that is conspicuously marked or otherwise 60 + designated in writing by the copyright owner as "Not a Contribution." 61 + 62 + "Contributor" shall mean Licensor and any individual or Legal Entity 63 + on behalf of whom a Contribution has been received by Licensor and 64 + subsequently incorporated within the Work. 65 + 66 + 2. Grant of Copyright License. Subject to the terms and conditions of 67 + this License, each Contributor hereby grants to You a perpetual, 68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 + copyright license to reproduce, prepare Derivative Works of, 70 + publicly display, publicly perform, sublicense, and distribute the 71 + Work and such Derivative Works in Source or Object form. 72 + 73 + 3. Grant of Patent License. Subject to the terms and conditions of 74 + this License, each Contributor hereby grants to You a perpetual, 75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 + (except as stated in this section) patent license to make, have made, 77 + use, offer to sell, sell, import, and otherwise transfer the Work, 78 + where such license applies only to those patent claims licensable 79 + by such Contributor that are necessarily infringed by their 80 + Contribution(s) alone or by combination of their Contribution(s) 81 + with the Work to which such Contribution(s) was submitted. If You 82 + institute patent litigation against any entity (including a 83 + cross-claim or counterclaim in a lawsuit) alleging that the Work 84 + or a Contribution incorporated within the Work constitutes direct 85 + or contributory patent infringement, then any patent licenses 86 + granted to You under this License for that Work shall terminate 87 + as of the date such litigation is filed. 88 + 89 + 4. Redistribution. You may reproduce and distribute copies of the 90 + Work or Derivative Works thereof in any medium, with or without 91 + modifications, and in Source or Object form, provided that You 92 + meet the following conditions: 93 + 94 + (a) You must give any other recipients of the Work or 95 + Derivative Works a copy of this License; and 96 + 97 + (b) You must cause any modified files to carry prominent notices 98 + stating that You changed the files; and 99 + 100 + (c) You must retain, in the Source form of any Derivative Works 101 + that You distribute, all copyright, patent, trademark, and 102 + attribution notices from the Source form of the Work, 103 + excluding those notices that do not pertain to any part of 104 + the Derivative Works; and 105 + 106 + (d) If the Work includes a "NOTICE" text file as part of its 107 + distribution, then any Derivative Works that You distribute must 108 + include a readable copy of the attribution notices contained 109 + within such NOTICE file, excluding those notices that do not 110 + pertain to any part of the Derivative Works, in at least one 111 + of the following places: within a NOTICE text file distributed 112 + as part of the Derivative Works; within the Source form or 113 + documentation, if provided along with the Derivative Works; or, 114 + within a display generated by the Derivative Works, if and 115 + wherever such third-party notices normally appear. The contents 116 + of the NOTICE file are for informational purposes only and 117 + do not modify the License. You may add Your own attribution 118 + notices within Derivative Works that You distribute, alongside 119 + or as an addendum to the NOTICE text from the Work, provided 120 + that such additional attribution notices cannot be construed 121 + as modifying the License. 122 + 123 + You may add Your own copyright statement to Your modifications and 124 + may provide additional or different license terms and conditions 125 + for use, reproduction, or distribution of Your modifications, or 126 + for any such Derivative Works as a whole, provided Your use, 127 + reproduction, and distribution of the Work otherwise complies with 128 + the conditions stated in this License. 129 + 130 + 5. Submission of Contributions. Unless You explicitly state otherwise, 131 + any Contribution intentionally submitted for inclusion in the Work 132 + by You to the Licensor shall be under the terms and conditions of 133 + this License, without any additional terms or conditions. 134 + Notwithstanding the above, nothing herein shall supersede or modify 135 + the terms of any separate license agreement you may have executed 136 + with Licensor regarding such Contributions. 137 + 138 + 6. Trademarks. This License does not grant permission to use the trade 139 + names, trademarks, service marks, or product names of the Licensor, 140 + except as required for reasonable and customary use in describing the 141 + origin of the Work and reproducing the content of the NOTICE file. 142 + 143 + 7. Disclaimer of Warranty. Unless required by applicable law or 144 + agreed to in writing, Licensor provides the Work (and each 145 + Contributor provides its Contributions) on an "AS IS" BASIS, 146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 + implied, including, without limitation, any warranties or conditions 148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 + PARTICULAR PURPOSE. You are solely responsible for determining the 150 + appropriateness of using or redistributing the Work and assume any 151 + risks associated with Your exercise of permissions under this License. 152 + 153 + 8. Limitation of Liability. In no event and under no legal theory, 154 + whether in tort (including negligence), contract, or otherwise, 155 + unless required by applicable law (such as deliberate and grossly 156 + negligent acts) or agreed to in writing, shall any Contributor be 157 + liable to You for damages, including any direct, indirect, special, 158 + incidental, or consequential damages of any character arising as a 159 + result of this License or out of the use or inability to use the 160 + Work (including but not limited to damages for loss of goodwill, 161 + work stoppage, computer failure or malfunction, or any and all 162 + other commercial damages or losses), even if such Contributor 163 + has been advised of the possibility of such damages. 164 + 165 + 9. Accepting Warranty or Additional Liability. While redistributing 166 + the Work or Derivative Works thereof, You may choose to offer, 167 + and charge a fee for, acceptance of support, warranty, indemnity, 168 + or other liability obligations and/or rights consistent with this 169 + License. However, in accepting such obligations, You may act only 170 + on Your own behalf and on Your sole responsibility, not on behalf 171 + of any other Contributor, and only if You agree to indemnify, 172 + defend, and hold each Contributor harmless for any liability 173 + incurred by, or claims asserted against, such Contributor by reason 174 + of your accepting any such warranty or additional liability. 175 + 176 + END OF TERMS AND CONDITIONS 177 + 178 + APPENDIX: How to apply the Apache License to your work. 179 + 180 + To apply the Apache License to your work, attach the following 181 + boilerplate notice, with the fields enclosed by brackets "[]" 182 + replaced with your own identifying information. (Don't include 183 + the brackets!) The text should be enclosed in the appropriate 184 + comment syntax for the file format. We also recommend that a 185 + file or class name and description of purpose be included on the 186 + same "printed page" as the copyright notice for easier 187 + identification within third-party archives. 188 + 189 + Copyright 2025 Sachymetsu 190 + 191 + Licensed under the Apache License, Version 2.0 (the "License"); 192 + you may not use this file except in compliance with the License. 193 + You may obtain a copy of the License at 194 + 195 + http://www.apache.org/licenses/LICENSE-2.0 196 + 197 + Unless required by applicable law or agreed to in writing, software 198 + distributed under the License is distributed on an "AS IS" BASIS, 199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 + See the License for the specific language governing permissions and 201 + limitations under the License.
+21
LICENSE-MIT
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Sachymetsu 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+3
README.md
··· 1 + # Sachy's Embeddable Core crates 2 + 3 + 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
··· 1 + [package] 2 + name = "sachy-battery" 3 + description = "A crate for defining battery life profiles" 4 + authors.workspace = true 5 + edition.workspace = true 6 + repository.workspace = true 7 + license.workspace = true 8 + version.workspace = true 9 + rust-version.workspace = true 10 + 11 + [dependencies]
+108
sachy-battery/src/lib.rs
··· 1 + //! Crate for calculating Battery levels as percentages, based on voltage/pct profiles via 2 + //! [`BatteryDischargeProfile`]. 3 + #![no_std] 4 + 5 + use core::ops::Range; 6 + 7 + pub struct BatteryDischargeProfile { 8 + voltage_range: Range<f32>, 9 + pct_range: Range<f32>, 10 + } 11 + 12 + impl BatteryDischargeProfile { 13 + /// Creates a new discharge profile. Internally, it stores the voltages high/low and pct high/low 14 + /// as ranges. 15 + #[inline] 16 + pub const fn new(voltage_high: f32, voltage_low: f32, pct_high: f32, pct_low: f32) -> Self { 17 + Self { 18 + voltage_range: voltage_low..voltage_high, 19 + pct_range: pct_low..pct_high, 20 + } 21 + } 22 + 23 + /// Calculates a battery percentage according to the specified range of the discharge profile. 24 + /// If the voltage is outside of the discharge profile, this method returns `None`. 25 + /// 26 + /// ``` 27 + /// use sachy_battery::BatteryDischargeProfile; 28 + /// 29 + /// let level = BatteryDischargeProfile::new(3.0, 2.0, 1.0, 0.0); 30 + /// 31 + /// assert_eq!(level.calc_pct(2.5), Some(0.5)); 32 + /// ``` 33 + pub fn calc_pct(&self, voltage: f32) -> Option<f32> { 34 + if self.voltage_range.contains(&voltage) { 35 + Some( 36 + self.pct_range.start 37 + + (voltage - self.voltage_range.start) 38 + * ((self.pct_range.end - self.pct_range.start) 39 + / (self.voltage_range.end - self.voltage_range.start)), 40 + ) 41 + } else { 42 + None 43 + } 44 + } 45 + 46 + /// Calculates a battery level from a range of discharge profiles. Assumes the first 47 + /// discharge level is the highest, so the levels go from high to low. Percentages values 48 + /// are from 1.0 to 0.0. 49 + /// 50 + /// ``` 51 + /// use sachy_battery::BatteryDischargeProfile; 52 + /// 53 + /// let levels = [ 54 + /// BatteryDischargeProfile::new(3.0, 2.5, 1.0, 0.5), 55 + /// BatteryDischargeProfile::new(2.5, 2.0, 0.5, 0.0), 56 + /// ]; 57 + /// 58 + /// assert_eq!(BatteryDischargeProfile::calc_pct_from_profile_range(2.75, levels.iter()), 0.75); 59 + /// ``` 60 + pub fn calc_pct_from_profile_range<'a>( 61 + voltage: f32, 62 + levels: impl Iterator<Item = &'a BatteryDischargeProfile>, 63 + ) -> f32 { 64 + let mut levels = levels.peekable(); 65 + 66 + if levels 67 + .peek() 68 + .is_some_and(|&level| voltage >= level.voltage_range.end) 69 + { 70 + return 1.0; 71 + } 72 + 73 + levels 74 + .find_map(|level| level.calc_pct(voltage)) 75 + .unwrap_or(0.0) 76 + } 77 + } 78 + 79 + #[cfg(test)] 80 + mod tests { 81 + use super::*; 82 + 83 + #[test] 84 + fn battery_level_from_one_profile() { 85 + let level = BatteryDischargeProfile::new(3.0, 2.0, 1.0, 0.0); 86 + 87 + assert_eq!(level.calc_pct(2.5), Some(0.5)); 88 + assert_eq!(level.calc_pct(3.5), None); 89 + assert_eq!(level.calc_pct(1.5), None); 90 + } 91 + 92 + #[test] 93 + fn battery_level_from_profile_range() { 94 + let levels = [ 95 + BatteryDischargeProfile::new(3.0, 2.5, 1.0, 0.5), 96 + BatteryDischargeProfile::new(2.5, 2.0, 0.5, 0.0), 97 + ]; 98 + 99 + let expect_results: [(f32, f32); 4] = [(3.5, 1.0), (2.75, 0.75), (2.25, 0.25), (1.5, 0.0)]; 100 + 101 + for (voltage, pct) in expect_results { 102 + assert_eq!( 103 + BatteryDischargeProfile::calc_pct_from_profile_range(voltage, levels.iter()), 104 + pct 105 + ); 106 + } 107 + } 108 + }
+17
sachy-bthome/Cargo.toml
··· 1 + [package] 2 + name = "sachy-bthome" 3 + description = "A crate for providing BTHome advertisement packets" 4 + version = { workspace = true } 5 + edition = { workspace = true } 6 + authors = { workspace = true } 7 + repository = { workspace = true } 8 + license = { workspace = true } 9 + rust-version = { workspace = true } 10 + 11 + [dependencies] 12 + heapless = "0.9.2" 13 + defmt = { version = "1", optional = true } 14 + sachy-fmt = { path = "../sachy-fmt" } 15 + 16 + [features] 17 + defmt = ["dep:defmt", "heapless/defmt", "sachy-fmt/defmt"]
+273
sachy-bthome/src/lib.rs
··· 1 + #![no_std] 2 + 3 + use heapless::Vec; 4 + use sachy_fmt::assert; 5 + 6 + const BR_EDR_NOT_SUPPORTED: u8 = 4; 7 + const LE_GENERAL_DISCOVERABLE: u8 = 2; 8 + 9 + const BTHOME_AD_HEADER: [u8; 8] = [ 10 + 0x02, 11 + 0x01, 12 + LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED, 13 + 0x04, 14 + 0x16, 15 + 0xD2, 16 + 0xFC, 17 + 0x40, 18 + ]; 19 + 20 + pub const BTHOME_UUID16: u16 = 0xFCD2; 21 + 22 + macro_rules! impl_fields { 23 + { $(($name:ident, $id:literal, $internal_repr:ty, $external_repr:ty),)+ } => { 24 + $( 25 + #[derive(Debug, Clone)] 26 + #[cfg_attr(feature = "defmt", derive(::defmt::Format))] 27 + pub struct $name($internal_repr); 28 + 29 + impl $name { 30 + const ID: u8 = $id; 31 + const SIZE: usize = core::mem::size_of::<$internal_repr>() - 1; 32 + 33 + #[inline] 34 + pub fn get(&self) -> $external_repr { 35 + let mut bytes = [0u8; core::mem::size_of::<$external_repr>()]; 36 + bytes[0..Self::SIZE].copy_from_slice(&self.0[1..]); 37 + <$external_repr>::from_le_bytes(bytes) 38 + } 39 + } 40 + 41 + impl From<$name> for BtHomeEnum { 42 + fn from(value: $name) -> Self { 43 + Self::$name(value) 44 + } 45 + } 46 + 47 + impl From<$external_repr> for $name { 48 + #[inline] 49 + fn from(value: $external_repr) -> Self { 50 + let mut bytes = [0u8; core::mem::size_of::<$internal_repr>()]; 51 + bytes[0] = Self::ID; 52 + bytes[1..].copy_from_slice(&value.to_le_bytes()[0..Self::SIZE]); 53 + $name(bytes) 54 + } 55 + } 56 + )* 57 + 58 + #[derive(Debug, Clone)] 59 + #[cfg_attr(feature = "defmt", derive(::defmt::Format))] 60 + pub enum BtHomeEnum { 61 + $( 62 + $name($name), 63 + )* 64 + } 65 + 66 + impl PartialEq for BtHomeEnum { 67 + fn eq(&self, other: &Self) -> bool { 68 + self.id() == other.id() 69 + } 70 + } 71 + 72 + impl Eq for BtHomeEnum {} 73 + 74 + impl PartialOrd for BtHomeEnum { 75 + fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> { 76 + Some(self.cmp(other)) 77 + } 78 + } 79 + 80 + impl Ord for BtHomeEnum { 81 + fn cmp(&self, other: &Self) -> core::cmp::Ordering { 82 + self.id().cmp(&other.id()) 83 + } 84 + } 85 + 86 + impl BtHomeEnum { 87 + pub const fn id(&self) -> u8 { 88 + match self { 89 + $( 90 + Self::$name(_) => $id, 91 + )* 92 + } 93 + } 94 + 95 + pub fn encode(&self) -> &[u8] { 96 + match self { 97 + $( 98 + Self::$name(repr) => &repr.0, 99 + )* 100 + } 101 + } 102 + } 103 + } 104 + } 105 + 106 + impl_fields! { 107 + (Battery1Per, 0x01, [u8; 2], u8), 108 + (Temperature10mK, 0x02, [u8; 3], i16), 109 + (Humidity10mPer, 0x03, [u8; 3], u16), 110 + (Illuminance10mLux, 0x05, [u8; 4], u32), 111 + (Voltage1mV, 0x0C, [u8; 3], u16), 112 + (Moisture10mPer, 0x14, [u8; 3], u16), 113 + (Humidity1Per, 0x2E, [u8; 2], u8), 114 + (Moisture1Per, 0x2F, [u8; 2], u8), 115 + } 116 + 117 + #[derive(Debug, Clone)] 118 + #[cfg_attr(feature = "defmt", derive(defmt::Format))] 119 + pub struct BtHomeAd<const N: usize> { 120 + buffer: Vec<u8, N>, 121 + } 122 + 123 + impl<const N: usize> BtHomeAd<N> { 124 + pub fn new() -> Self { 125 + assert!(N >= BTHOME_AD_HEADER.len(), "Ad buffer is too small"); 126 + 127 + let buffer = Vec::from_iter(BTHOME_AD_HEADER); 128 + 129 + Self { buffer } 130 + } 131 + 132 + pub fn add_data(&mut self, payload: impl Into<BtHomeEnum>) -> &mut Self { 133 + let payload = payload.into(); 134 + let encoded = payload.encode(); 135 + 136 + assert!( 137 + self.buffer.len() + encoded.len() < N, 138 + "Can't fit data into buffer! {}+{}", 139 + self.buffer.len(), 140 + encoded.len() 141 + ); 142 + 143 + self.buffer[3] += encoded.len() as u8; 144 + self.buffer.extend_from_slice(encoded).ok(); 145 + 146 + self 147 + } 148 + 149 + pub fn add_local_name(&mut self, name: &str) -> &Self { 150 + let len = name.len() + 1; 151 + 152 + assert!( 153 + self.buffer.len() + len < N, 154 + "Can't fit local name into buffer!" 155 + ); 156 + 157 + self.buffer.extend_from_slice(&[len as u8, 0x09]).ok(); 158 + self.buffer.extend_from_slice(name.as_bytes()).ok(); 159 + 160 + // Reborrow as ref to prevent further mutation after we have 161 + // added the local name to the ad. 162 + &*self 163 + } 164 + 165 + pub fn encode(&self) -> &[u8] { 166 + &self.buffer 167 + } 168 + } 169 + 170 + impl Default for BtHomeAd<31> { 171 + #[inline] 172 + fn default() -> Self { 173 + Self::new() 174 + } 175 + } 176 + 177 + #[cfg(test)] 178 + mod tests { 179 + use super::*; 180 + 181 + #[test] 182 + fn basic_add_name() { 183 + let mut home = BtHomeAd::default(); 184 + 185 + let name = "hello"; 186 + 187 + home.add_local_name(name); 188 + 189 + assert_eq!(home.buffer.len(), 15); 190 + 191 + assert_eq!( 192 + home.encode(), 193 + &[ 194 + 0x02, 195 + 0x01, 196 + LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED, 197 + 0x04, 198 + 0x16, 199 + 0xD2, 200 + 0xFC, 201 + 0x40, 202 + (name.len() + 1) as u8, 203 + 0x09, 204 + b"h"[0], 205 + b"e"[0], 206 + b"l"[0], 207 + b"l"[0], 208 + b"o"[0] 209 + ] 210 + ); 211 + } 212 + 213 + #[test] 214 + fn add_data() { 215 + let mut home = BtHomeAd::default(); 216 + 217 + home.add_data(Battery1Per::from(34)) 218 + .add_data(Temperature10mK::from(2255)); 219 + 220 + assert_eq!( 221 + home.encode(), 222 + &[ 223 + 0x02, 224 + 0x01, 225 + LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED, 226 + 0x09, 227 + 0x16, 228 + 0xD2, 229 + 0xFC, 230 + 0x40, 231 + 0x01, 232 + 34, 233 + 0x02, 234 + 207, 235 + 8, 236 + ] 237 + ); 238 + } 239 + 240 + #[test] 241 + fn full_payload() { 242 + let mut home = BtHomeAd::default(); 243 + 244 + let encoded = home 245 + .add_data(Battery1Per::from(34)) 246 + .add_data(Temperature10mK::from(2255)) 247 + .add_data(Humidity10mPer::from(3400)) 248 + .add_data(Illuminance10mLux::from(45000)) 249 + .add_data(Moisture10mPer::from(3632)) 250 + .add_local_name("rsachy") 251 + .encode(); 252 + 253 + // Final payload is within the max size for the advertising payload 254 + assert_eq!(encoded.len(), 31); 255 + assert_eq!(home.buffer[3], 19); 256 + 257 + let mut home = BtHomeAd::default(); 258 + 259 + let encoded = home 260 + .add_data(Battery1Per::from(34)) 261 + .add_data(Temperature10mK::from(2255)) 262 + .add_data(Illuminance10mLux::from(45000)) 263 + .add_data(Voltage1mV::from(2800)) 264 + .add_data(Humidity1Per::from(34)) 265 + .add_data(Moisture1Per::from(36)) 266 + .add_local_name("sachy") 267 + .encode(); 268 + 269 + // Final payload is within the max size for the advertising payload 270 + assert_eq!(encoded.len(), 31); 271 + assert_eq!(home.buffer[3], 20); 272 + } 273 + }
+15
sachy-fmt/Cargo.toml
··· 1 + [package] 2 + name = "sachy-fmt" 3 + description = "A crate for providing fmt shims for defmt" 4 + version = { workspace = true } 5 + edition = { workspace = true } 6 + authors = { workspace = true } 7 + repository = { workspace = true } 8 + license = { workspace = true } 9 + rust-version = { workspace = true } 10 + 11 + [dependencies] 12 + defmt = { version = "1", optional = true } 13 + 14 + [features] 15 + defmt = ["dep:defmt"]
+243
sachy-fmt/src/lib.rs
··· 1 + #![no_std] 2 + #![allow(unused)] 3 + 4 + #[macro_export] 5 + macro_rules! assert { 6 + ($($x:tt)*) => { 7 + { 8 + #[cfg(not(feature = "defmt"))] 9 + ::core::assert!($($x)*); 10 + #[cfg(feature = "defmt")] 11 + ::defmt::assert!($($x)*); 12 + } 13 + }; 14 + } 15 + 16 + #[macro_export] 17 + macro_rules! assert_eq { 18 + ($($x:tt)*) => { 19 + { 20 + #[cfg(not(feature = "defmt"))] 21 + ::core::assert_eq!($($x)*); 22 + #[cfg(feature = "defmt")] 23 + ::defmt::assert_eq!($($x)*); 24 + } 25 + }; 26 + } 27 + 28 + #[macro_export] 29 + macro_rules! assert_ne { 30 + ($($x:tt)*) => { 31 + { 32 + #[cfg(not(feature = "defmt"))] 33 + ::core::assert_ne!($($x)*); 34 + #[cfg(feature = "defmt")] 35 + ::defmt::assert_ne!($($x)*); 36 + } 37 + }; 38 + } 39 + 40 + #[macro_export] 41 + macro_rules! debug_assert { 42 + ($($x:tt)*) => { 43 + { 44 + #[cfg(not(feature = "defmt"))] 45 + ::core::debug_assert!($($x)*); 46 + #[cfg(feature = "defmt")] 47 + ::defmt::debug_assert!($($x)*); 48 + } 49 + }; 50 + } 51 + 52 + #[macro_export] 53 + macro_rules! debug_assert_eq { 54 + ($($x:tt)*) => { 55 + { 56 + #[cfg(not(feature = "defmt"))] 57 + ::core::debug_assert_eq!($($x)*); 58 + #[cfg(feature = "defmt")] 59 + ::defmt::debug_assert_eq!($($x)*); 60 + } 61 + }; 62 + } 63 + 64 + #[macro_export] 65 + macro_rules! debug_assert_ne { 66 + ($($x:tt)*) => { 67 + { 68 + #[cfg(not(feature = "defmt"))] 69 + ::core::debug_assert_ne!($($x)*); 70 + #[cfg(feature = "defmt")] 71 + ::defmt::debug_assert_ne!($($x)*); 72 + } 73 + }; 74 + } 75 + 76 + #[macro_export] 77 + macro_rules! todo { 78 + ($($x:tt)*) => { 79 + { 80 + #[cfg(not(feature = "defmt"))] 81 + ::core::todo!($($x)*); 82 + #[cfg(feature = "defmt")] 83 + ::defmt::todo!($($x)*); 84 + } 85 + }; 86 + } 87 + 88 + #[macro_export] 89 + #[cfg(not(feature = "defmt"))] 90 + macro_rules! unreachable { 91 + ($($x:tt)*) => { 92 + ::core::unreachable!($($x)*) 93 + }; 94 + } 95 + 96 + #[macro_export] 97 + #[cfg(feature = "defmt")] 98 + macro_rules! unreachable { 99 + ($($x:tt)*) => { 100 + ::defmt::unreachable!($($x)*) 101 + }; 102 + } 103 + 104 + #[macro_export] 105 + macro_rules! panic { 106 + ($($x:tt)*) => { 107 + { 108 + #[cfg(not(feature = "defmt"))] 109 + ::core::panic!($($x)*); 110 + #[cfg(feature = "defmt")] 111 + ::defmt::panic!($($x)*); 112 + } 113 + }; 114 + } 115 + 116 + #[macro_export] 117 + macro_rules! trace { 118 + ($s:literal $(, $x:expr)* $(,)?) => { 119 + { 120 + #[cfg(feature = "defmt")] 121 + ::defmt::trace!($s $(, $x)*); 122 + #[cfg(feature="defmt")] 123 + let _ = ($( & $x ),*); 124 + } 125 + }; 126 + } 127 + 128 + #[macro_export] 129 + macro_rules! debug { 130 + ($s:literal $(, $x:expr)* $(,)?) => { 131 + { 132 + #[cfg(feature = "defmt")] 133 + ::defmt::debug!($s $(, $x)*); 134 + #[cfg(not(feature="defmt"))] 135 + let _ = ($( & $x ),*); 136 + } 137 + }; 138 + } 139 + 140 + #[macro_export] 141 + macro_rules! info { 142 + ($s:literal $(, $x:expr)* $(,)?) => { 143 + { 144 + #[cfg(feature = "defmt")] 145 + ::defmt::info!($s $(, $x)*); 146 + #[cfg(not(feature="defmt"))] 147 + let _ = ($( & $x ),*); 148 + } 149 + }; 150 + } 151 + 152 + #[macro_export] 153 + macro_rules! _warn { 154 + ($s:literal $(, $x:expr)* $(,)?) => { 155 + { 156 + #[cfg(feature = "defmt")] 157 + ::defmt::warn!($s $(, $x)*); 158 + #[cfg(not(feature="defmt"))] 159 + let _ = ($( & $x ),*); 160 + } 161 + }; 162 + } 163 + 164 + #[macro_export] 165 + macro_rules! error { 166 + ($s:literal $(, $x:expr)* $(,)?) => { 167 + { 168 + #[cfg(feature = "defmt")] 169 + ::defmt::error!($s $(, $x)*); 170 + #[cfg(not(feature="defmt"))] 171 + let _ = ($( & $x ),*); 172 + } 173 + }; 174 + } 175 + 176 + #[macro_export] 177 + #[cfg(feature = "defmt")] 178 + macro_rules! unwrap { 179 + ($($x:tt)*) => { 180 + ::defmt::unwrap!($($x)*) 181 + }; 182 + } 183 + 184 + #[macro_export] 185 + #[cfg(not(feature = "defmt"))] 186 + macro_rules! unwrap { 187 + ($arg:expr) => { 188 + match $crate::Try::into_result($arg) { 189 + ::core::result::Result::Ok(t) => t, 190 + ::core::result::Result::Err(_) => { 191 + ::core::panic!(); 192 + } 193 + } 194 + }; 195 + ($arg:expr, $($msg:expr),+ $(,)? ) => { 196 + match $crate::Try::into_result($arg) { 197 + ::core::result::Result::Ok(t) => t, 198 + ::core::result::Result::Err(_) => { 199 + ::core::panic!(); 200 + } 201 + } 202 + }; 203 + } 204 + 205 + #[derive(Debug, Copy, Clone, Eq, PartialEq)] 206 + pub struct NoneError; 207 + 208 + pub trait Try { 209 + type Ok; 210 + type Error; 211 + fn into_result(self) -> Result<Self::Ok, Self::Error>; 212 + } 213 + 214 + impl<T> Try for Option<T> { 215 + type Ok = T; 216 + type Error = NoneError; 217 + 218 + #[inline] 219 + fn into_result(self) -> Result<T, NoneError> { 220 + self.ok_or(NoneError) 221 + } 222 + } 223 + 224 + impl<T, E> Try for Result<T, E> { 225 + type Ok = T; 226 + type Error = E; 227 + 228 + #[inline] 229 + fn into_result(self) -> Self { 230 + self 231 + } 232 + } 233 + 234 + pub(crate) struct Bytes<'a>(pub &'a [u8]); 235 + 236 + #[cfg(feature = "defmt")] 237 + impl defmt::Format for Bytes<'_> { 238 + fn format(&self, fmt: defmt::Formatter) { 239 + defmt::write!(fmt, "{:02x}", self.0) 240 + } 241 + } 242 + 243 + pub use _warn as warn;