Skip to content

Commit ee6d881

Browse files
authored
Cfu splitter (#332)
Adds code that can broadcast a CFU update to multiple other CFU devices.
1 parent 2522ae8 commit ee6d881

File tree

6 files changed

+517
-0
lines changed

6 files changed

+517
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cfu-service/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ license = "MIT"
77
[dependencies]
88
defmt = { workspace = true, optional = true }
99
embassy-executor.workspace = true
10+
embassy-futures.workspace = true
1011
embassy-sync.workspace = true
1112
embassy-time.workspace = true
1213
embedded-cfu-protocol.workspace = true

cfu-service/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use embedded_services::cfu::{CfuError, ContextToken};
77
use embedded_services::{comms, error, info};
88

99
pub mod host;
10+
pub mod splitter;
1011

1112
pub struct CfuClient {
1213
/// Cfu Client context

cfu-service/src/splitter.rs

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
//! Module that can broadcast CFU messages to multiple devices
2+
//! This allows devices to share a single component ID
3+
4+
use core::{future::Future, iter::zip};
5+
6+
use embassy_futures::join::{join, join3, join4};
7+
use embedded_cfu_protocol::protocol_definitions::*;
8+
use embedded_services::{
9+
cfu::{
10+
self,
11+
component::{CfuDevice, InternalResponseData, RequestData},
12+
},
13+
error, intrusive_list, trace,
14+
};
15+
16+
/// Trait containing customization functionality for [`Splitter`]
17+
pub trait Customization {
18+
/// Decides which firmware version to use based on the provided versions from all devices.
19+
fn resolve_fw_versions(&self, versions: &[GetFwVersionResponse]) -> GetFwVersionResponse;
20+
21+
/// Decides which offer response to send based on the provided responses from all devices.
22+
fn resolve_offer_response(&self, offer_responses: &[FwUpdateOfferResponse]) -> FwUpdateOfferResponse;
23+
24+
/// Decides which content response to send based on the provided responses from all devices.
25+
fn resolve_content_response(&self, content_responses: &[FwUpdateContentResponse]) -> FwUpdateContentResponse;
26+
}
27+
28+
/// Splitter struct
29+
pub struct Splitter<'a, C: Customization> {
30+
/// CFU device
31+
cfu_device: CfuDevice,
32+
/// Component ID for each individual device
33+
devices: &'a [ComponentId],
34+
/// Customization for the Splitter
35+
customization: C,
36+
}
37+
38+
/// Maximum number of devices supported
39+
pub const MAX_SUPPORTED_DEVICES: usize = 4;
40+
41+
impl<'a, C: Customization> Splitter<'a, C> {
42+
/// Create a new Splitter, returns None if the devices slice is empty or too large
43+
pub fn new(component_id: ComponentId, devices: &'a [ComponentId], customization: C) -> Option<Self> {
44+
if devices.is_empty() || devices.len() > MAX_SUPPORTED_DEVICES {
45+
None
46+
} else {
47+
Some(Self {
48+
cfu_device: CfuDevice::new(component_id),
49+
devices,
50+
customization,
51+
})
52+
}
53+
}
54+
55+
/// Create a new invalid FW version response
56+
fn create_invalid_fw_version_response(&self) -> InternalResponseData {
57+
let dev_inf = FwVerComponentInfo::new(FwVersion::new(0xffffffff), self.cfu_device.component_id());
58+
let comp_info: [FwVerComponentInfo; MAX_CMPT_COUNT] = [dev_inf; MAX_CMPT_COUNT];
59+
InternalResponseData::FwVersionResponse(GetFwVersionResponse {
60+
header: GetFwVersionResponseHeader::new(1, GetFwVerRespHeaderByte3::NoSpecialFlags),
61+
component_info: comp_info,
62+
})
63+
}
64+
65+
/// Create a content rejection response
66+
fn create_content_rejection(sequence: u16) -> InternalResponseData {
67+
InternalResponseData::ContentResponse(FwUpdateContentResponse::new(
68+
sequence,
69+
CfuUpdateContentResponseStatus::ErrorInvalid,
70+
))
71+
}
72+
73+
/// Process a fw version request
74+
async fn process_get_fw_version(&self) -> InternalResponseData {
75+
let mut versions = [GetFwVersionResponse {
76+
header: Default::default(),
77+
component_info: Default::default(),
78+
}; MAX_SUPPORTED_DEVICES];
79+
80+
let success = map_slice_join(self.devices, &mut versions, |device_id| async move {
81+
if let Ok(InternalResponseData::FwVersionResponse(version_info)) =
82+
cfu::route_request(*device_id, RequestData::FwVersionRequest).await
83+
{
84+
Some(version_info)
85+
} else {
86+
error!("Failed to get FW version for device {}", device_id);
87+
None
88+
}
89+
})
90+
.await;
91+
92+
if !success {
93+
self.create_invalid_fw_version_response()
94+
} else {
95+
let mut overall_version = self.customization.resolve_fw_versions(&versions[..self.devices.len()]);
96+
// The overall component version comes first
97+
overall_version.component_info[0].component_id = self.cfu_device.component_id();
98+
InternalResponseData::FwVersionResponse(overall_version)
99+
}
100+
}
101+
102+
/// Process a give offer request
103+
async fn process_give_offer(&self, offer: &FwUpdateOffer) -> InternalResponseData {
104+
let mut offer_responses = [FwUpdateOfferResponse::default(); MAX_SUPPORTED_DEVICES];
105+
106+
let success = map_slice_join(self.devices, &mut offer_responses, |device_id| async move {
107+
let mut offer = *offer;
108+
109+
// Override with the correct component ID for the device
110+
offer.component_info.component_id = *device_id;
111+
if let Ok(InternalResponseData::OfferResponse(response)) =
112+
cfu::route_request(*device_id, RequestData::GiveOffer(offer)).await
113+
{
114+
Some(response)
115+
} else {
116+
error!("Failed to get FW version for device {}", device_id);
117+
None
118+
}
119+
})
120+
.await;
121+
122+
if !success {
123+
self.create_invalid_fw_version_response()
124+
} else {
125+
InternalResponseData::OfferResponse(
126+
self.customization
127+
.resolve_offer_response(&offer_responses[..self.devices.len()]),
128+
)
129+
}
130+
}
131+
132+
/// Process update content
133+
async fn process_give_content(&self, content: &FwUpdateContentCommand) -> InternalResponseData {
134+
let mut content_responses = [FwUpdateContentResponse::default(); MAX_SUPPORTED_DEVICES];
135+
136+
let success = map_slice_join(self.devices, &mut content_responses, |device_id| async move {
137+
if let Ok(InternalResponseData::ContentResponse(response)) =
138+
cfu::route_request(*device_id, RequestData::GiveContent(*content)).await
139+
{
140+
Some(response)
141+
} else {
142+
error!("Failed to get FW version for device {}", device_id);
143+
None
144+
}
145+
})
146+
.await;
147+
148+
if !success {
149+
Self::create_content_rejection(content.header.sequence_num)
150+
} else {
151+
InternalResponseData::ContentResponse(
152+
self.customization
153+
.resolve_content_response(&content_responses[..self.devices.len()]),
154+
)
155+
}
156+
}
157+
158+
/// Wait for a CFU message
159+
pub async fn wait_request(&self) -> RequestData {
160+
self.cfu_device.wait_request().await
161+
}
162+
163+
/// Process a CFU message and produce a response
164+
pub async fn process_request(&self, request: RequestData) -> InternalResponseData {
165+
match request {
166+
RequestData::FwVersionRequest => {
167+
trace!("Got FwVersionRequest");
168+
self.process_get_fw_version().await
169+
}
170+
RequestData::GiveOffer(offer) => {
171+
trace!("Got GiveOffer");
172+
self.process_give_offer(&offer).await
173+
}
174+
RequestData::GiveContent(content) => {
175+
trace!("Got GiveContent");
176+
self.process_give_content(&content).await
177+
}
178+
RequestData::FinalizeUpdate => {
179+
trace!("Got FinalizeUpdate");
180+
InternalResponseData::ComponentPrepared
181+
}
182+
RequestData::PrepareComponentForUpdate => {
183+
trace!("Got PrepareComponentForUpdate");
184+
InternalResponseData::ComponentPrepared
185+
}
186+
}
187+
}
188+
189+
/// Send a response to the CFU message
190+
pub async fn send_response(&self, response: InternalResponseData) {
191+
self.cfu_device.send_response(response).await;
192+
}
193+
194+
pub async fn register(&'static self) -> Result<(), intrusive_list::Error> {
195+
cfu::register_device(&self.cfu_device).await
196+
}
197+
}
198+
199+
/// Map items in an input slice to an output slice using an async closure.
200+
///
201+
/// This function will execute the closure concurrently in groups up to four items at a time.
202+
/// Four is an arbitrary but is a balance between two (easy to implement, but not very concurrent) and eight (more implementation work).
203+
/// This will exit early and return false if any item results in `None`.
204+
async fn map_slice_join<'i, 'o, I, O, F: Future<Output = Option<O>>>(
205+
input: &'i [I],
206+
output: &'o mut [O],
207+
f: impl Fn(&'i I) -> F,
208+
) -> bool {
209+
let mut iter = zip(input.iter(), output.iter_mut());
210+
loop {
211+
match (iter.next(), iter.next(), iter.next(), iter.next()) {
212+
(None, None, None, None) => {
213+
// No more items to process
214+
return true;
215+
}
216+
(Some((i0, o0)), None, None, None) => {
217+
if let Some(result) = f(i0).await {
218+
*o0 = result;
219+
} else {
220+
return false;
221+
}
222+
}
223+
(Some((i0, o0)), Some((i1, o1)), None, None) => {
224+
let results = join(f(i0), f(i1)).await;
225+
if let (Some(r0), Some(r1)) = results {
226+
*o0 = r0;
227+
*o1 = r1;
228+
} else {
229+
return false;
230+
}
231+
}
232+
(Some((i0, o0)), Some((i1, o1)), Some((i2, o2)), None) => {
233+
let results = join3(f(i0), f(i1), f(i2)).await;
234+
if let (Some(r0), Some(r1), Some(r2)) = results {
235+
*o0 = r0;
236+
*o1 = r1;
237+
*o2 = r2;
238+
} else {
239+
return false;
240+
}
241+
}
242+
(Some((i0, o0)), Some((i1, o1)), Some((i2, o2)), Some((i3, o3))) => {
243+
let results = join4(f(i0), f(i1), f(i2), f(i3)).await;
244+
if let (Some(r0), Some(r1), Some(r2), Some(r3)) = results {
245+
*o0 = r0;
246+
*o1 = r1;
247+
*o2 = r2;
248+
*o3 = r3;
249+
} else {
250+
return false;
251+
}
252+
}
253+
_ => {
254+
// Other combinations aren't possible because we're using a fused iterator
255+
unreachable!()
256+
}
257+
}
258+
}
259+
}

examples/std/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)