1use super::{PARAM_DEFINITIONS, PARAMS_COUNT, ParamDefinition, ParamId, ParamValue, Params};
4
5include!(concat!(env!("OUT_DIR"), "/veloxity_version.rs"));
6
7pub const ROSFLIGHT_C_PARAM_STORAGE_SIZE: usize = 7004;
9
10const ROSFLIGHT_C_PARAM_COUNT: usize = 333;
11const ROSFLIGHT_C_PARAM_NAME_SIZE: usize = 16;
12const PARAM_STORAGE_VALUE_SIZE: usize = size_of::<u32>();
13const PARAM_STORAGE_TYPE_SIZE: usize = size_of::<u8>();
15const PARAM_STORAGE_VALUES_OFFSET: usize = 8;
16const PARAM_STORAGE_NAMES_OFFSET: usize =
17 PARAM_STORAGE_VALUES_OFFSET + ROSFLIGHT_C_PARAM_COUNT * PARAM_STORAGE_VALUE_SIZE;
18const PARAM_STORAGE_TYPES_OFFSET: usize =
19 PARAM_STORAGE_NAMES_OFFSET + ROSFLIGHT_C_PARAM_COUNT * ROSFLIGHT_C_PARAM_NAME_SIZE;
20const PARAM_STORAGE_MAGIC_EF_OFFSET: usize =
21 PARAM_STORAGE_TYPES_OFFSET + ROSFLIGHT_C_PARAM_COUNT * PARAM_STORAGE_TYPE_SIZE;
22const PARAM_STORAGE_CHECKSUM_OFFSET: usize = PARAM_STORAGE_MAGIC_EF_OFFSET + 1;
23const RUST_ONLY_PARAM_COUNT: usize = 16;
24const ROSFLIGHT_C_TAIL_ORDER: [ParamId; 5] = [
25 ParamId::PARAM_OFFBOARD_TIMEOUT,
26 ParamId::PARAM_BATTERY_VOLTAGE_MULTIPLIER,
27 ParamId::PARAM_BATTERY_CURRENT_MULTIPLIER,
28 ParamId::PARAM_BATTERY_VOLTAGE_ALPHA,
29 ParamId::PARAM_BATTERY_CURRENT_ALPHA,
30];
31
32const _: () = assert!(PARAMS_COUNT - RUST_ONLY_PARAM_COUNT == ROSFLIGHT_C_PARAM_COUNT);
33const _: () = assert!(PARAM_STORAGE_CHECKSUM_OFFSET < ROSFLIGHT_C_PARAM_STORAGE_SIZE);
34
35fn is_rosflight_c_param(id: ParamId) -> bool {
36 !matches!(
37 id,
38 ParamId::PARAM_CHANNEL_OUTPUT_MASK
39 | ParamId::PARAM_ALLOW_UNHEALTHY_ESTIMATOR
40 | ParamId::PARAM_EST_ANGLE_LOCKOUT
41 | ParamId::PARAM_RC_OUTPUT_KILL_CHANNEL
42 | ParamId::PARAM_TELEM_HEARTBEAT_HZ
43 | ParamId::PARAM_TELEM_STATUS_HZ
44 | ParamId::PARAM_TELEM_IMU_HZ
45 | ParamId::PARAM_TELEM_ATTITUDE_HZ
46 | ParamId::PARAM_TELEM_OUTPUT_RAW_HZ
47 | ParamId::PARAM_TELEM_DIFF_PRESSURE_HZ
48 | ParamId::PARAM_TELEM_BARO_HZ
49 | ParamId::PARAM_TELEM_MAG_HZ
50 | ParamId::PARAM_TELEM_RANGE_HZ
51 | ParamId::PARAM_TELEM_BATTERY_HZ
52 | ParamId::PARAM_TELEM_GNSS_HZ
53 | ParamId::PARAM_TELEM_RC_HZ
54 )
55}
56
57fn rosflight_c_param_type(value: ParamValue) -> Option<u8> {
58 match value {
59 ParamValue::Int(_) => Some(0),
60 ParamValue::Float(_) => Some(1),
61 ParamValue::Uint(_) | ParamValue::Bool(_) => None,
62 }
63}
64
65fn is_rosflight_c_tail_param(id: ParamId) -> bool {
66 ROSFLIGHT_C_TAIL_ORDER.contains(&id)
67}
68
69fn rosflight_c_params() -> impl Iterator<Item = &'static ParamDefinition> {
76 PARAM_DEFINITIONS
77 .iter()
78 .filter(|definition| {
79 is_rosflight_c_param(definition.id) && !is_rosflight_c_tail_param(definition.id)
80 })
81 .chain(
82 ROSFLIGHT_C_TAIL_ORDER
83 .iter()
84 .map(|id| &PARAM_DEFINITIONS[*id as usize]),
85 )
86}
87
88fn rosflight_c_checksum(bytes: &[u8; ROSFLIGHT_C_PARAM_STORAGE_SIZE]) -> u8 {
89 bytes[PARAM_STORAGE_VALUES_OFFSET..PARAM_STORAGE_MAGIC_EF_OFFSET]
90 .iter()
91 .fold(0, |checksum, byte| checksum ^ byte)
92}
93
94fn encode_param_value(value: ParamValue, definition: &ParamDefinition) -> Option<[u8; 4]> {
95 match (value, definition.default) {
96 (ParamValue::Float(value), ParamValue::Float(_)) => Some(value.to_bits().to_le_bytes()),
97 (ParamValue::Int(value), ParamValue::Int(_)) => Some(value.to_le_bytes()),
98 _ => None,
99 }
100}
101
102fn decode_param_value(bytes: [u8; 4], definition: &ParamDefinition) -> Option<ParamValue> {
103 Some(match definition.default {
104 ParamValue::Float(_) => ParamValue::Float(f32::from_bits(u32::from_le_bytes(bytes))),
105 ParamValue::Int(_) => ParamValue::Int(i32::from_le_bytes(bytes)),
106 ParamValue::Uint(_) | ParamValue::Bool(_) => return None,
107 })
108}
109
110pub fn encode_rosflight_c_params(
112 params: &Params,
113 bytes: &mut [u8; ROSFLIGHT_C_PARAM_STORAGE_SIZE],
114) -> bool {
115 bytes.fill(0);
116 bytes[..4].copy_from_slice(&VELOXITY_VERSION.to_le_bytes());
117 bytes[4..6].copy_from_slice(&(ROSFLIGHT_C_PARAM_STORAGE_SIZE as u16).to_le_bytes());
118 bytes[6] = 0xBE;
119
120 let mut count = 0;
121 for (index, definition) in rosflight_c_params().enumerate() {
122 count += 1;
123 let offset = PARAM_STORAGE_VALUES_OFFSET + index * PARAM_STORAGE_VALUE_SIZE;
124 let Some(encoded) = encode_param_value(params.get_by_id(definition.id), definition) else {
125 return false;
126 };
127 bytes[offset..offset + PARAM_STORAGE_VALUE_SIZE].copy_from_slice(&encoded);
128
129 let name_offset = PARAM_STORAGE_NAMES_OFFSET + index * ROSFLIGHT_C_PARAM_NAME_SIZE;
130 let name = definition.name.as_bytes();
131 if name.len() > ROSFLIGHT_C_PARAM_NAME_SIZE {
132 return false;
133 }
134 bytes[name_offset..name_offset + name.len()].copy_from_slice(name);
135
136 let type_offset = PARAM_STORAGE_TYPES_OFFSET + index * PARAM_STORAGE_TYPE_SIZE;
137 let Some(param_type) = rosflight_c_param_type(definition.default) else {
138 return false;
139 };
140 bytes[type_offset] = param_type;
141 }
142 if count != ROSFLIGHT_C_PARAM_COUNT {
143 return false;
144 }
145
146 bytes[PARAM_STORAGE_MAGIC_EF_OFFSET] = 0xEF;
147 bytes[PARAM_STORAGE_CHECKSUM_OFFSET] = rosflight_c_checksum(bytes);
148 true
149}
150
151fn decode_rosflight_c_params_in_order(
152 bytes: &[u8; ROSFLIGHT_C_PARAM_STORAGE_SIZE],
153 definitions: impl Iterator<Item = &'static ParamDefinition>,
154) -> Option<Params> {
155 let mut params = Params::default();
156 let mut count = 0;
157 for (index, definition) in definitions.enumerate() {
158 count += 1;
159 let name_offset = PARAM_STORAGE_NAMES_OFFSET + index * ROSFLIGHT_C_PARAM_NAME_SIZE;
160 let stored_name = &bytes[name_offset..name_offset + ROSFLIGHT_C_PARAM_NAME_SIZE];
161 let expected_name = definition.name.as_bytes();
162 if stored_name[..expected_name.len()] != *expected_name
163 || (expected_name.len() < ROSFLIGHT_C_PARAM_NAME_SIZE
164 && stored_name[expected_name.len()] != 0)
165 {
166 return None;
167 }
168
169 let type_offset = PARAM_STORAGE_TYPES_OFFSET + index * PARAM_STORAGE_TYPE_SIZE;
170 if bytes[type_offset] != rosflight_c_param_type(definition.default)? {
171 return None;
172 }
173
174 let offset = PARAM_STORAGE_VALUES_OFFSET + index * PARAM_STORAGE_VALUE_SIZE;
175 let value = decode_param_value(
176 bytes[offset..offset + PARAM_STORAGE_VALUE_SIZE]
177 .try_into()
178 .ok()?,
179 definition,
180 )?;
181 params.set_by_id(definition.id, value);
182 }
183
184 (count == ROSFLIGHT_C_PARAM_COUNT).then_some(params)
185}
186
187pub fn decode_rosflight_c_params(bytes: &[u8; ROSFLIGHT_C_PARAM_STORAGE_SIZE]) -> Option<Params> {
190 let version = u32::from_le_bytes(bytes[..4].try_into().ok()?);
191 if version != VELOXITY_VERSION
192 || usize::from(u16::from_le_bytes(bytes[4..6].try_into().ok()?))
193 != ROSFLIGHT_C_PARAM_STORAGE_SIZE
194 || bytes[6] != 0xBE
195 || bytes[PARAM_STORAGE_MAGIC_EF_OFFSET] != 0xEF
196 || bytes[PARAM_STORAGE_CHECKSUM_OFFSET] != rosflight_c_checksum(bytes)
197 {
198 return None;
199 }
200
201 decode_rosflight_c_params_in_order(bytes, rosflight_c_params())
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 fn stored_name(bytes: &[u8; ROSFLIGHT_C_PARAM_STORAGE_SIZE], index: usize) -> &str {
209 let offset = PARAM_STORAGE_NAMES_OFFSET + index * ROSFLIGHT_C_PARAM_NAME_SIZE;
210 let field = &bytes[offset..offset + ROSFLIGHT_C_PARAM_NAME_SIZE];
211 let length = field
212 .iter()
213 .position(|byte| *byte == 0)
214 .unwrap_or(field.len());
215 core::str::from_utf8(&field[..length]).unwrap()
216 }
217
218 #[test]
219 fn storage_format_round_trips_only_c_parameters() {
220 let mut params = Params::default();
221 params.set_by_id(ParamId::PARAM_SYSTEM_ID, ParamValue::Int(42));
222 params.set_by_id(ParamId::PARAM_GYRO_X_BIAS, ParamValue::Float(-0.25));
223 params.set_by_id(ParamId::PARAM_CHANNEL_OUTPUT_MASK, ParamValue::Int(0));
224 params.set_by_id(ParamId::PARAM_ALLOW_UNHEALTHY_ESTIMATOR, ParamValue::Int(0));
225 params.set_by_id(ParamId::PARAM_EST_ANGLE_LOCKOUT, ParamValue::Int(1));
226 params.set_by_id(ParamId::PARAM_RC_OUTPUT_KILL_CHANNEL, ParamValue::Int(4));
227 params.set_by_id(ParamId::PARAM_TELEM_BARO_HZ, ParamValue::Int(20));
228
229 let mut bytes = [0; ROSFLIGHT_C_PARAM_STORAGE_SIZE];
230 assert!(encode_rosflight_c_params(¶ms, &mut bytes));
231 let decoded = decode_rosflight_c_params(&bytes).expect("valid image must decode");
232
233 assert_eq!(
234 decoded.get_by_id(ParamId::PARAM_SYSTEM_ID),
235 ParamValue::Int(42)
236 );
237 assert_eq!(
238 decoded.get_by_id(ParamId::PARAM_GYRO_X_BIAS),
239 ParamValue::Float(-0.25)
240 );
241 assert_eq!(
242 decoded.get_by_id(ParamId::PARAM_CHANNEL_OUTPUT_MASK),
243 ParamValue::Int(0x0f)
244 );
245 assert_eq!(
246 decoded.get_by_id(ParamId::PARAM_ALLOW_UNHEALTHY_ESTIMATOR),
247 ParamValue::Int(1)
248 );
249 assert_eq!(
250 decoded.get_by_id(ParamId::PARAM_EST_ANGLE_LOCKOUT),
251 ParamValue::Int(0)
252 );
253 assert_eq!(
254 decoded.get_by_id(ParamId::PARAM_RC_OUTPUT_KILL_CHANNEL),
255 ParamValue::Int(-1)
256 );
257 assert_eq!(
258 decoded.get_by_id(ParamId::PARAM_TELEM_BARO_HZ),
259 ParamValue::Int(0)
260 );
261 }
262
263 #[test]
264 fn storage_matches_rosflight_c_arm_layout() {
265 let mut bytes = [0; ROSFLIGHT_C_PARAM_STORAGE_SIZE];
266 assert!(encode_rosflight_c_params(&Params::default(), &mut bytes));
267
268 assert_eq!(&bytes[..4], &VELOXITY_VERSION.to_le_bytes());
269 assert_eq!(
270 &bytes[4..6],
271 &(ROSFLIGHT_C_PARAM_STORAGE_SIZE as u16).to_le_bytes()
272 );
273 assert_eq!(bytes[6], 0xBE);
274 assert_eq!(bytes[7], 0);
275 assert_eq!(bytes[PARAM_STORAGE_MAGIC_EF_OFFSET], 0xEF);
276 assert_eq!(
277 bytes[PARAM_STORAGE_CHECKSUM_OFFSET],
278 rosflight_c_checksum(&bytes)
279 );
280 assert_eq!(
281 &bytes[PARAM_STORAGE_NAMES_OFFSET..PARAM_STORAGE_NAMES_OFFSET + 10],
282 b"BAUD_RATE\0"
283 );
284 assert_eq!(bytes[PARAM_STORAGE_TYPES_OFFSET], 0);
285 }
286
287 #[test]
288 fn storage_tail_matches_rosflight_c_param_ids() {
289 let mut bytes = [0; ROSFLIGHT_C_PARAM_STORAGE_SIZE];
290 assert!(encode_rosflight_c_params(&Params::default(), &mut bytes));
291
292 let expected = [
293 (328, "OFFBOARD_TIMEOUT", 0),
294 (329, "BATT_VOLT_MULT", 1),
295 (330, "BATT_CURR_MULT", 1),
296 (331, "BATT_VOLT_LPF", 1),
297 (332, "BATT_CURR_LPF", 1),
298 ];
299 for (index, name, param_type) in expected {
300 assert_eq!(stored_name(&bytes, index), name);
301 assert_eq!(bytes[PARAM_STORAGE_TYPES_OFFSET + index], param_type);
302 }
303 }
304
305 #[test]
306 fn storage_rejects_misordered_tail() {
307 let mut bytes = [0; ROSFLIGHT_C_PARAM_STORAGE_SIZE];
308 assert!(encode_rosflight_c_params(&Params::default(), &mut bytes));
309
310 let first = PARAM_STORAGE_NAMES_OFFSET + 328 * ROSFLIGHT_C_PARAM_NAME_SIZE;
311 let second = PARAM_STORAGE_NAMES_OFFSET + 329 * ROSFLIGHT_C_PARAM_NAME_SIZE;
312 let mut first_name = [0; ROSFLIGHT_C_PARAM_NAME_SIZE];
313 let mut second_name = [0; ROSFLIGHT_C_PARAM_NAME_SIZE];
314 first_name.copy_from_slice(&bytes[first..first + ROSFLIGHT_C_PARAM_NAME_SIZE]);
315 second_name.copy_from_slice(&bytes[second..second + ROSFLIGHT_C_PARAM_NAME_SIZE]);
316 bytes[first..first + ROSFLIGHT_C_PARAM_NAME_SIZE].copy_from_slice(&second_name);
317 bytes[second..second + ROSFLIGHT_C_PARAM_NAME_SIZE].copy_from_slice(&first_name);
318 bytes[PARAM_STORAGE_CHECKSUM_OFFSET] = rosflight_c_checksum(&bytes);
319
320 assert!(decode_rosflight_c_params(&bytes).is_none());
321 }
322
323 #[test]
324 fn storage_rejects_version_mismatch() {
325 let mut bytes = [0; ROSFLIGHT_C_PARAM_STORAGE_SIZE];
326 assert!(encode_rosflight_c_params(&Params::default(), &mut bytes));
327 bytes[0] ^= 1;
328 assert!(decode_rosflight_c_params(&bytes).is_none());
329 }
330
331 #[test]
332 fn storage_rejects_schema_mismatch_even_with_valid_checksum() {
333 let mut bytes = [0; ROSFLIGHT_C_PARAM_STORAGE_SIZE];
334 assert!(encode_rosflight_c_params(&Params::default(), &mut bytes));
335 bytes[PARAM_STORAGE_NAMES_OFFSET] ^= 1;
336 bytes[PARAM_STORAGE_CHECKSUM_OFFSET] = rosflight_c_checksum(&bytes);
337 assert!(decode_rosflight_c_params(&bytes).is_none());
338 }
339}