Skip to content

Commit 1d751d3

Browse files
authored
feat: add fake BLE API for nRF24L01 (#25)
1 parent dae2ab2 commit 1d751d3

39 files changed

Lines changed: 3051 additions & 17 deletions

.config/cliff.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,16 @@ commit_parsers = [
9999
{ field = "group", pattern = "fix", group = "<!-- 4 --> 🛠️ Fixed" },
100100
{ field = "group", pattern = "perf", group = "<!-- 4 --> 🛠️ Fixed" },
101101
{ field = "group", pattern = "build", group = "<!-- 6 --> 📦 Dependency updates" },
102+
{ message = "[t|T]est", group = "<!-- 7 -->🚦 Tests" },
102103
{ field = "group", pattern = "test", group = "<!-- 7 -->🚦 Tests" },
104+
{ message = "[d|D]ocs", group = "<!-- 8 --> 📝 Documentation" },
103105
{ field = "group", pattern = "docs", group = "<!-- 8 --> 📝 Documentation" },
104106
{ field = "group", pattern = "chore", group = "<!-- 9 --> 🗨️ Changed" },
105107
{ field = "group", pattern = "style", group = "<!-- 9 --> 🗨️ Changed" },
106108
{ field = "breaking", pattern = true, group = "<!-- 10 --> 💥 Breaking Changes" },
107109
{ message = "[r|R]emove", group = "<!-- 3 --> 🗑️ Removed" },
108110
{ message = "[d|D]eprecate", group = "<!-- 2 --> 🚫 Deprecated" },
109-
{ message = "^.*: delete", group = "<!-- 3 --> 🗑️ Removed" },
111+
{ message = "[d|D]elete", group = "<!-- 3 --> 🗑️ Removed" },
110112
{ message = "[s|S]ecurity", group = "<!-- 5 --> 🔐 Security" },
111113
{ field = "group", pattern = "refactor", group = "<!-- 9 --> 🗨️ Changed" },
112114
]

.github/workflows/bump_version.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ class _PkgPaths(NamedTuple):
3232
],
3333
path=REPO_ROOT / "crates" / "rf24-rs",
3434
),
35+
"rf24ble-rs": _PkgPaths(
36+
include=["crates/rf24ble-rs/**"],
37+
exclude=[
38+
".github/**",
39+
"docs/**",
40+
"examples/python/**",
41+
"examples/node/**",
42+
"bindings/**",
43+
".config/*",
44+
],
45+
path=REPO_ROOT / "crates" / "rf24ble-rs",
46+
),
3547
"rf24-py": _PkgPaths(
3648
include=[
3749
"crates/**/*.rs",

.github/workflows/rust.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ on:
1212
- .github/workflows/rust.yml
1313
tags:
1414
- 'rf24-rs/*'
15+
- 'rf24ble-rs/*'
1516
# - 'rf24network-rs/*'
1617
# - 'rf24mesh-rs/*'
17-
# - 'rf24ble-rs/*'
1818
pull_request:
1919
branches: [main]
2020
paths:
@@ -66,8 +66,15 @@ jobs:
6666
- uses: actions/checkout@v4
6767
with:
6868
persist-credentials: false
69+
- name: Get pkg from tag
70+
id: pkg
71+
shell: bash
72+
run: |
73+
tag=${{ github.ref_name }}
74+
pkg=$(echo "$tag" | sed -E 's;([^/]+)/.*;\1;')
75+
echo "name=$pkg" >> "$GITHUB_OUTPUT"
6976
- name: Install Rust
7077
run: rustup update stable --no-self-update
71-
- run: cargo publish -p rf24-rs
78+
- run: cargo publish -p ${{ steps.pkg.outputs.name }}
7279
env:
7380
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[workspace]
2-
members = ["crates/rf24-rs", "examples/rust", "bindings/python", "bindings/node"]
3-
default-members = ["crates/rf24-rs", "examples/rust"]
2+
members = ["crates/rf24-rs", "examples/rust", "bindings/python", "bindings/node", "crates/rf24ble-rs"]
3+
default-members = ["crates/rf24-rs", "crates/rf24ble-rs", "examples/rust"]
44
resolver = "2"
55

66
[workspace.package]

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,16 @@ All packages are developed under the MIT license.
2727
[rf24-rs-msrv]: https://img.shields.io/crates/msrv/rf24-rs
2828
[rf24-rs-docs-badge]: https://img.shields.io/docsrs/rf24-rs
2929
[rf24-rs-docs-link]: https://docs.rs/rf24-rs
30+
[rf24ble-rs-badge]: https://img.shields.io/crates/v/rf24ble-rs
31+
[rf24ble-rs-link]: https://crates.io/crates/rf24ble-rs
32+
[rf24ble-rs-msrv]: https://img.shields.io/crates/msrv/rf24ble-rs
33+
[rf24ble-rs-docs-badge]: https://img.shields.io/docsrs/rf24ble-rs
34+
[rf24ble-rs-docs-link]: https://docs.rs/rf24ble-rs
3035

3136
| name | version | API docs | Minimum Supported<br>Rust Version |
3237
|:-----|:-------:|:--------:|:---------------------------------:|
3338
| `rf24-rs` | [![Crates.io Version][rf24-rs-badge]][rf24-rs-link] | [![docs.rs][rf24-rs-docs-badge]][rf24-rs-docs-link] | ![Crates.io MSRV][rf24-rs-msrv] |
39+
| `rf24ble-rs` | [![Crates.io Version][rf24ble-rs-badge]][rf24ble-rs-link] | [![docs.rs][rf24ble-rs-docs-badge]][rf24ble-rs-docs-link] | ![Crates.io MSRV][rf24ble-rs-msrv] |
3440

3541
### Bindings
3642

@@ -83,7 +89,7 @@ Here is the intended roadmap:
8389
reimplement the same API (using [rust's `trait` feature][rust-traits])
8490
for use on nRF5x radios.
8591

86-
- [ ] implement a fake BLE API for the nRF24L01 (see [#4](https://github.com/nRF24/rf24-rs/issues/4))
92+
- [x] implement a fake BLE API for the nRF24L01
8793
- [ ] implement network layers (OTA compatible with RF24Network and RF24Mesh libraries)
8894
- [ ] implement ESB support for nRF5x MCUs. This might be guarded under [cargo features][cargo-feat].
8995

bindings/node/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ embedded-hal = "1.0.0"
1515
napi = "2.16.13"
1616
napi-derive = "2.16.13"
1717
rf24-rs = { path = "../../crates/rf24-rs", features = ["std"] }
18+
rf24ble-rs = { path = "../../crates/rf24ble-rs", features = ["std"] }
1819

1920
[build-dependencies]
2021
napi-build = "2.1.4"

bindings/node/src/fake_ble/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use crate::radio::config::RadioConfig;
2+
3+
pub mod radio;
4+
pub mod services;
5+
6+
/// Returns a {@link RadioConfig} object tailored for
7+
/// OTA compatibility with BLE specifications.
8+
///
9+
/// !!! note "See also"
10+
/// This configuration complies with inherent
11+
/// [Limitations](https://docs.rs/rf24ble-rs/latest/rf24ble/index.html#limitations).
12+
#[napi]
13+
#[allow(dead_code)]
14+
pub fn ble_config() -> RadioConfig {
15+
RadioConfig::from_inner(rf24ble::ble_config())
16+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#![cfg(target_os = "linux")]
2+
3+
use crate::radio::{interface::RF24, types::coerce_to_bool};
4+
use napi::{
5+
bindgen_prelude::{Buffer, Reference, Result},
6+
JsNumber,
7+
};
8+
use rf24ble::BleChannels;
9+
10+
use super::services::BlePayload;
11+
12+
/// A class to use the nRF24L01 as a Fake BLE beacon.
13+
///
14+
/// !!! note "See also"
15+
/// This implementation is subject to
16+
/// [Limitations](https://docs.rs/rf24ble-rs/latest/rf24ble/index.html#limitations).
17+
///
18+
/// Use {@link bleConfig} to properly configure the radio for
19+
/// BLE compatibility.
20+
///
21+
/// ```ts
22+
/// import { bleConfig, FakeBle, RF24 } from "@rf24/rf24";
23+
///
24+
/// const radio = new RF24(22, 0);
25+
/// radio.begin();
26+
/// radio.withConfig(bleConfig());
27+
/// const ble = new FakeBle(radio);
28+
///
29+
/// radio.printDetails();
30+
/// ```
31+
#[napi]
32+
pub struct FakeBle {
33+
radio: Reference<RF24>,
34+
inner: rf24ble::FakeBle,
35+
}
36+
37+
#[napi]
38+
impl FakeBle {
39+
/// Create an Fake BLE device using the given RF24 instance.
40+
#[napi(constructor)]
41+
pub fn new(radio: Reference<RF24>) -> Self {
42+
Self {
43+
radio,
44+
inner: rf24ble::FakeBle::default(),
45+
}
46+
}
47+
48+
/// Set or get the BLE device's name for included in advertisements.
49+
///
50+
/// Setting a BLE device name will occupy more bytes from the
51+
/// 18 available bytes in advertisements. The exact number of bytes occupied
52+
/// is the length of the given `name` string plus 2.
53+
///
54+
/// The maximum supported name length is 10 bytes.
55+
/// So, up to 12 bytes (10 + 2) will be used in the advertising payload.
56+
#[napi(setter, js_name = "name")]
57+
pub fn set_name(&mut self, name: String) {
58+
self.inner.set_name(&name);
59+
}
60+
61+
#[napi(getter, js_name = "name")]
62+
pub fn get_name(&self) -> Option<String> {
63+
let mut tmp = [0u8; 12];
64+
let len = self.inner.get_name(&mut tmp) as usize;
65+
if len > 0 {
66+
let result = String::from_utf8_lossy(&tmp[2..len + 2]);
67+
return Some(result.to_string());
68+
}
69+
None
70+
}
71+
72+
/// Set or get the BLE device's MAC address.
73+
///
74+
/// A MAC address is required by BLE specifications.
75+
/// Use this attribute to uniquely identify the BLE device.
76+
#[napi(setter, js_name = "macAddress")]
77+
pub fn set_mac_address(&mut self, address: &[u8]) {
78+
self.inner.mac_address.copy_from_slice(&address[0..6]);
79+
}
80+
81+
#[napi(getter, js_name = "macAddress")]
82+
pub fn get_mac_address(&self) -> [u8; 6] {
83+
let mut result = [0u8; 6];
84+
result.copy_from_slice(&self.inner.mac_address);
85+
result
86+
}
87+
88+
/// Enable or disable the inclusion of the radio's PA level in advertisements.
89+
///
90+
/// Enabling this feature occupies 3 bytes of the 18 available bytes in
91+
/// advertised payloads.
92+
#[napi(setter)]
93+
pub fn show_pa_level(
94+
&mut self,
95+
#[napi(ts_arg_type = "boolean | number")] enable: JsNumber,
96+
) -> Result<()> {
97+
let val = coerce_to_bool(Some(enable), false)?;
98+
self.inner.show_pa_level = val;
99+
Ok(())
100+
}
101+
102+
#[napi(getter, js_name = "showPaLevel")]
103+
pub fn has_pa_level(&self) -> bool {
104+
self.inner.show_pa_level
105+
}
106+
107+
/// How many bytes are available in an advertisement payload?
108+
///
109+
/// The `hypothetical` parameter shall be the same value passed to {@link FakeBle.send}.
110+
///
111+
/// In addition to the given `hypothetical` payload length, this function also
112+
/// accounts for the current state of {@link FakeBle.name} and
113+
/// {@link FakeBle.showPaLevel}.
114+
///
115+
/// If the returned value is less than `0`, then the `hypothetical` payload will not
116+
/// be broadcasted.
117+
#[napi]
118+
pub fn len_available(&self, hypothetical: &[u8]) -> i8 {
119+
self.inner.len_available(hypothetical)
120+
}
121+
122+
/// Hop the radio's current channel to the next BLE compliant frequency.
123+
///
124+
/// Use this function after {@link FakeBle.send} to comply with BLE specifications.
125+
/// This is not required, but it is recommended to avoid bandwidth pollution.
126+
///
127+
/// This function should not be called in RX mode. To ensure proper radio behavior,
128+
/// the caller must ensure that the radio is in TX mode.
129+
#[napi]
130+
pub fn hop_channel(&mut self) -> Result<()> {
131+
let channel = self.radio.get_channel()?;
132+
if let Some(channel) = BleChannels::increment(channel) {
133+
self.radio.set_channel(channel)?;
134+
}
135+
// if the current channel is not a BLE_CHANNEL, then do nothing
136+
Ok(())
137+
}
138+
139+
/// Send a BLE advertisement
140+
///
141+
/// The `buf` parameter takes a buffer that has been already formatted for
142+
/// BLE specifications.
143+
///
144+
/// See our convenient API to
145+
/// - advertise a Battery's remaining change level: {@link BatteryService}
146+
/// - advertise a Temperature measurement: {@link TemperatureService}
147+
/// - advertise a URL: {@link UrlService}
148+
///
149+
/// For a custom/proprietary BLE service, the given `buf` must adopt compliance with BLE specifications.
150+
/// For example, a buffer of `n` bytes shall be formed as follows:
151+
///
152+
/// | index | value |
153+
/// |:------|:------|
154+
/// | `0` | `n - 1` |
155+
/// | `1` | `0xFF` |
156+
/// | `2 ... n - 1` | custom data |
157+
#[napi]
158+
pub fn send(&mut self, buf: &[u8]) -> Result<bool> {
159+
if let Some(tx_queue) = self.inner.make_payload(
160+
buf,
161+
if self.inner.show_pa_level {
162+
Some(self.radio.get_pa_level()?.into_inner())
163+
} else {
164+
None
165+
},
166+
self.radio.get_channel()?,
167+
) {
168+
// Disregarding any hardware error, `RF24::send()` should
169+
// always return `Ok(true)` because auto-ack is off.
170+
self.radio.send(Buffer::from(tx_queue.to_vec()), None)
171+
} else {
172+
Ok(false)
173+
}
174+
}
175+
176+
/// Read the first available payload from the radio's RX FIFO
177+
/// and decode it into a {@link BlePayload}.
178+
///
179+
/// > [!WARNING]
180+
/// > The payload must be decoded while the radio is on
181+
/// > the same channel that it received the data.
182+
/// > Otherwise, the decoding process will fail.
183+
///
184+
/// Use {@link RF24.available} to check if there is data in the radio's RX FIFO.
185+
///
186+
/// If the payload was somehow malformed or incomplete,
187+
/// then this function returns an undefined value.
188+
#[napi]
189+
pub fn read(&mut self) -> Result<Option<BlePayload>> {
190+
let mut buf = self.radio.read(Some(32))?;
191+
let channel = self.radio.get_channel()?;
192+
Ok(BlePayload::from_bytes(&mut buf, channel))
193+
}
194+
}

0 commit comments

Comments
 (0)