1use serde::Deserialize;
2
3use crate::{DEFAULT_PASSKEY_ENTRY_TIMEOUT_SECS, MIN_PASSKEY_ENTRY_TIMEOUT_SECS};
4
5const SUBSCRIBER_DEFAULT_CONFIG: &str = include_str!("../default_config/subscriber_default.toml");
6
7#[derive(Deserialize)]
9struct SubscriberConfig {
10 subscriber: Vec<SubscriberEntry>,
11}
12
13#[derive(Deserialize)]
15struct SubscriberEntry {
16 features: Vec<String>,
17 events: Vec<SubscriberEventEntry>,
18}
19
20#[derive(Deserialize)]
22struct SubscriberEventEntry {
23 name: String,
24 #[serde(default = "default_sub_count")]
25 count: usize,
26}
27
28fn default_sub_count() -> usize {
29 1
30}
31
32pub struct BuildConstants {
34 pub combo_max_num: usize,
35 pub combo_max_length: usize,
36 pub fork_max_num: usize,
37 pub morse_max_num: usize,
38 pub morse_profile_max_num: usize,
39 pub max_patterns_per_key: usize,
40 pub macro_space_size: usize,
41 pub debounce_time: u16,
42 pub mouse_key_interval: u16,
43 pub mouse_wheel_interval: u16,
44 pub report_channel_size: usize,
45 pub vial_channel_size: usize,
46 pub flash_channel_size: usize,
47 pub split_peripherals_num: usize,
48 pub ble_profiles_num: usize,
49 pub split_central_sleep_timeout_seconds: u32,
50 pub protocol_macro_chunk_size: usize,
51 pub auto_mouse_layer_max_num: usize,
52 pub rynk_buffer_size: usize,
54 pub dongle_pairing_window_secs: u32,
55 pub events: Vec<EventChannel>,
56 pub passkey: Option<Passkey>,
57}
58
59pub struct EventChannel {
60 pub name: String,
61 pub channel_size: usize,
62 pub pubs: usize,
63 pub subs: usize,
64}
65
66pub struct Passkey {
67 pub enabled: bool,
68 pub timeout_secs: u32,
69}
70
71impl crate::KeyboardTomlConfig {
72 pub fn build_constants(&self, active_features: &[&str]) -> Result<BuildConstants, String> {
78 let rmk = &self.rmk;
79
80 let split_peripherals_num = if active_features.contains(&"split") && rmk.split_peripherals_num < 1 {
82 1
83 } else {
84 rmk.split_peripherals_num
85 };
86
87 macro_rules! event_channels {
89 ($($field:ident),* $(,)?) => {
90 vec![$(
91 EventChannel {
92 name: stringify!($field).to_string(),
93 channel_size: self.event.$field.channel_size,
94 pubs: self.event.$field.pubs,
95 subs: self.event.$field.subs,
96 },
97 )*]
98 };
99 }
100
101 let mut events = event_channels!(
102 connection_status_change,
103 modifier,
104 keyboard,
105 layer_change,
106 wpm_update,
107 led_indicator,
108 sleep_state,
109 battery_status,
110 battery_adc,
111 charging_state,
112 pointing,
113 peripheral_connected,
114 central_connected,
115 peripheral_battery,
116 clear_peer,
117 dfu_status,
118 action,
119 );
120
121 apply_feature_subscriber_bumps(&mut events, active_features);
124
125 let passkey = if active_features.contains(&"passkey_entry") {
127 self.ble.as_ref().map(resolve_passkey_enabled).transpose()?
128 } else {
129 None
130 };
131
132 use crate::protocol_limits;
134 if rmk.combo_max_length > protocol_limits::MAX_COMBO_SIZE {
135 return Err(format!(
136 "combo_max_length ({}) exceeds protocol ceiling MAX_COMBO_SIZE ({})",
137 rmk.combo_max_length,
138 protocol_limits::MAX_COMBO_SIZE
139 ));
140 }
141 if rmk.max_patterns_per_key > protocol_limits::MAX_MORSE_SIZE {
142 return Err(format!(
143 "max_patterns_per_key ({}) exceeds protocol ceiling MAX_MORSE_SIZE ({})",
144 rmk.max_patterns_per_key,
145 protocol_limits::MAX_MORSE_SIZE
146 ));
147 }
148 if rmk.protocol_macro_chunk_size > protocol_limits::MAX_MACRO_DATA_SIZE {
149 return Err(format!(
150 "protocol_macro_chunk_size ({}) exceeds protocol ceiling MAX_MACRO_DATA_SIZE ({})",
151 rmk.protocol_macro_chunk_size,
152 protocol_limits::MAX_MACRO_DATA_SIZE
153 ));
154 }
155 let auto_mouse_layer_max_num = rmk
156 .auto_mouse_layer_max_num
157 .unwrap_or(crate::resolved::behavior::DEFAULT_AUTO_MOUSE_LAYER_MAX_NUM);
158 if let Some(entries) = self.behavior.as_ref().and_then(|b| b.auto_mouse_layer.as_ref()) {
159 if entries.len() > auto_mouse_layer_max_num {
160 return Err(format!(
161 "number of [[behavior.auto_mouse_layer]] entries ({}) exceeds auto_mouse_layer_max_num ({})",
162 entries.len(),
163 auto_mouse_layer_max_num
164 ));
165 }
166 let uses_action_event = entries
167 .iter()
168 .any(|e| e.deactivate_on_key == Some(true) || e.reset_timeout_on_key == Some(true));
169 if uses_action_event && events.iter().any(|e| e.name == "action" && e.subs == 0) {
170 return Err(
171 "[[behavior.auto_mouse_layer]].deactivate_on_key / reset_timeout_on_key require [event.action] subs to be at least 1".to_string(),
172 );
173 }
174 }
175
176 validate_u8_capability("morse_max_num", rmk.morse_max_num)?;
179 validate_u8_capability("split_peripherals_num", split_peripherals_num)?;
180 validate_u8_capability("ble_profiles_num", rmk.ble_profiles_num)?;
181 validate_u16_capability("macro_space_size", rmk.macro_space_size)?;
182 validate_u16_capability("rynk_buffer_size", rmk.rynk_buffer_size)?;
183 Ok(BuildConstants {
184 combo_max_num: rmk.combo_max_num,
185 combo_max_length: rmk.combo_max_length,
186 fork_max_num: rmk.fork_max_num,
187 morse_max_num: rmk.morse_max_num,
188 morse_profile_max_num: rmk.morse_profile_max_num,
189 max_patterns_per_key: rmk.max_patterns_per_key,
190 macro_space_size: rmk.macro_space_size,
191 debounce_time: rmk.debounce_time,
192 mouse_key_interval: rmk.mouse_key_interval,
193 mouse_wheel_interval: rmk.mouse_wheel_interval,
194 report_channel_size: rmk.report_channel_size,
195 vial_channel_size: rmk.vial_channel_size,
196 flash_channel_size: rmk.flash_channel_size,
197 split_peripherals_num,
198 ble_profiles_num: rmk.ble_profiles_num,
199 split_central_sleep_timeout_seconds: rmk.split_central_sleep_timeout_seconds,
200 protocol_macro_chunk_size: rmk.protocol_macro_chunk_size,
201 auto_mouse_layer_max_num,
202 rynk_buffer_size: rmk.rynk_buffer_size,
203 dongle_pairing_window_secs: rmk.dongle_pairing_window_secs,
204 events,
205 passkey,
206 })
207 }
208}
209
210fn validate_u8_capability(name: &str, value: usize) -> Result<(), String> {
211 if value > u8::MAX as usize {
212 return Err(format!(
213 "{name} ({value}) exceeds the u8 host capability field (max 255)"
214 ));
215 }
216 Ok(())
217}
218
219fn validate_u16_capability(name: &str, value: usize) -> Result<(), String> {
220 if value > u16::MAX as usize {
221 return Err(format!(
222 "{name} ({value}) exceeds the u16 host capability field (max 65535)"
223 ));
224 }
225 Ok(())
226}
227
228fn apply_feature_subscriber_bumps(events: &mut [EventChannel], active_features: &[&str]) {
232 let sub_config: SubscriberConfig =
233 toml::from_str(SUBSCRIBER_DEFAULT_CONFIG).expect("Failed to parse subscriber_default.toml");
234
235 for entry in &sub_config.subscriber {
236 let all_enabled = entry.features.iter().all(|f| active_features.contains(&f.as_str()));
237 if all_enabled {
238 for sub_event in &entry.events {
239 if let Some(event) = events.iter_mut().find(|e| e.name == sub_event.name) {
240 event.subs += sub_event.count;
241 } else {
242 println!(
243 "cargo:warning=subscriber_default.toml: unknown event \"{}\"",
244 sub_event.name
245 );
246 }
247 }
248 }
249 }
250}
251
252fn resolve_passkey_enabled(ble: &crate::BleConfig) -> Result<Passkey, String> {
253 let enabled = ble.passkey_entry.unwrap_or(false);
254 let timeout_secs = ble.passkey_entry_timeout.unwrap_or(DEFAULT_PASSKEY_ENTRY_TIMEOUT_SECS);
255 if timeout_secs < MIN_PASSKEY_ENTRY_TIMEOUT_SECS {
256 return Err(format!(
257 "keyboard.toml: [ble.passkey_entry_timeout] must be at least {} seconds, got {}",
258 MIN_PASSKEY_ENTRY_TIMEOUT_SECS, timeout_secs
259 ));
260 }
261 Ok(Passkey { enabled, timeout_secs })
262}
263
264#[cfg(test)]
265mod tests {
266 use super::{BuildConstants, resolve_passkey_enabled, validate_u8_capability, validate_u16_capability};
267 use crate::{BleConfig, DEFAULT_PASSKEY_ENTRY_TIMEOUT_SECS, KeyboardTomlConfig, MIN_PASSKEY_ENTRY_TIMEOUT_SECS};
268
269 #[test]
270 fn reserves_led_subscribers_for_display_split_and_dual_rynk_sessions() {
271 let config: KeyboardTomlConfig = toml::from_str("").unwrap();
272 let constants = config.build_constants(&["display", "split", "rynk", "_ble"]).unwrap();
273 let led_indicator = constants
274 .events
275 .iter()
276 .find(|event| event.name == "led_indicator")
277 .unwrap();
278
279 assert_eq!(led_indicator.subs, 8);
281 }
282
283 #[test]
284 fn validates_passkey_timeout() {
285 let ble = BleConfig {
286 passkey_entry_timeout: Some(MIN_PASSKEY_ENTRY_TIMEOUT_SECS - 1),
287 ..Default::default()
288 };
289
290 let err = match resolve_passkey_enabled(&ble) {
291 Ok(_) => panic!("expected passkey timeout validation failure"),
292 Err(err) => err,
293 };
294 assert_eq!(
295 err,
296 format!(
297 "keyboard.toml: [ble.passkey_entry_timeout] must be at least {} seconds, got {}",
298 MIN_PASSKEY_ENTRY_TIMEOUT_SECS,
299 MIN_PASSKEY_ENTRY_TIMEOUT_SECS - 1
300 )
301 );
302 }
303
304 #[test]
305 fn uses_default_timeout() {
306 let ble = BleConfig::default();
307 let passkey = resolve_passkey_enabled(&ble).unwrap();
308
309 assert!(!passkey.enabled);
310 assert_eq!(passkey.timeout_secs, DEFAULT_PASSKEY_ENTRY_TIMEOUT_SECS);
311 }
312
313 fn parse(toml: &str) -> crate::KeyboardTomlConfig {
314 toml::from_str(toml).expect("Failed to parse keyboard config")
315 }
316
317 #[test]
318 fn auto_mouse_layer_max_num_explicitly_too_small_is_rejected() {
319 let toml = "[rmk]\nauto_mouse_layer_max_num = 0\n\n[[behavior.auto_mouse_layer]]\ntarget_layer = 1\n";
320 let err = match parse(toml).build_constants(&[]) {
321 Ok(_) => panic!("expected auto_mouse_layer_max_num validation failure"),
322 Err(err) => err,
323 };
324 assert!(err.contains("auto_mouse_layer_max_num"));
325 }
326
327 #[test]
328 fn auto_mouse_layer_within_capacity_is_accepted() {
329 let toml = "[rmk]\nauto_mouse_layer_max_num = 1\n\n[[behavior.auto_mouse_layer]]\ntarget_layer = 1\nextra_mouse_keys = [\"LCtrl\"]\n";
330 assert!(parse(toml).build_constants(&[]).is_ok());
331 }
332
333 #[test]
334 fn deactivate_on_key_without_action_subs_is_rejected() {
335 let toml = "[[behavior.auto_mouse_layer]]\ntarget_layer = 1\ndeactivate_on_key = true\n";
336 let err = match parse(toml).build_constants(&[]) {
337 Ok(_) => panic!("expected action subs validation failure"),
338 Err(err) => err,
339 };
340 assert!(err.contains("[event.action]"));
341 }
342
343 #[test]
344 fn deactivate_on_key_with_action_subs_set_is_accepted() {
345 let toml = "[event.action]\nchannel_size = 16\npubs = 1\nsubs = 1\n\n[[behavior.auto_mouse_layer]]\ntarget_layer = 1\ndeactivate_on_key = true\n";
346 assert!(parse(toml).build_constants(&[]).is_ok());
347 }
348
349 #[test]
350 fn ble_reserves_advertising_timeout_wake_subscribers() {
351 let base = parse("").build_constants(&[]).unwrap();
355 let ble = parse("").build_constants(&["_ble"]).unwrap();
356
357 let subs =
358 |constants: &BuildConstants, event: &str| constants.events.iter().find(|e| e.name == event).unwrap().subs;
359 for event in ["keyboard", "pointing"] {
360 assert_eq!(
361 subs(&ble, event),
362 subs(&base, event) + 1,
363 "{event} needs a wake subscriber slot under _ble"
364 );
365 }
366 }
367
368 #[test]
369 fn validates_capability_wire_widths() {
370 assert!(validate_u8_capability("ble_profiles_num", 255).is_ok());
371 assert_eq!(
372 validate_u8_capability("ble_profiles_num", 256),
373 Err("ble_profiles_num (256) exceeds the u8 host capability field (max 255)".to_string())
374 );
375
376 assert!(validate_u16_capability("macro_space_size", 65535).is_ok());
377 assert_eq!(
378 validate_u16_capability("macro_space_size", 65536),
379 Err("macro_space_size (65536) exceeds the u16 host capability field (max 65535)".to_string())
380 );
381 }
382}