Skip to main content

rmk_config/resolved/
build_constants.rs

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/// Parsed representation of `subscriber_default.toml`.
8#[derive(Deserialize)]
9struct SubscriberConfig {
10    subscriber: Vec<SubscriberEntry>,
11}
12
13/// A single entry: bump `subs` for each listed event when all `features` are enabled.
14#[derive(Deserialize)]
15struct SubscriberEntry {
16    features: Vec<String>,
17    events: Vec<SubscriberEventEntry>,
18}
19
20/// Per-event subscriber bump. `count` defaults to 1.
21#[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
32/// Compile-time constants emitted as `pub const` items by `rmk-types/build.rs`.
33pub 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    /// Rynk RX/TX buffer size (bytes).
53    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    /// Build compile-time constants from the configuration.
73    ///
74    /// `active_features` contains feature names enabled on the
75    /// **downstream crate** (e.g. `["split", "_ble"]`). These are matched
76    /// against `subscriber_default.toml` to auto-bump event subscriber counts.
77    pub fn build_constants(&self, active_features: &[&str]) -> Result<BuildConstants, String> {
78        let rmk = &self.rmk;
79
80        // Fix split_peripherals_num: when split feature is enabled, ensure at least 1
81        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        // Build event channels
88        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        // Auto-bump subscriber counts based on enabled feature flags.
122        // Declarations live in subscriber_default.toml.
123        apply_feature_subscriber_bumps(&mut events, active_features);
124
125        // Only validate passkey settings when the build will emit passkey constants.
126        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        // Validate that config values do not exceed protocol ceilings.
133        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        // Host capability fields are u8/u16 on the wire; check the values no deserializer bound
177        // covers (morse_max_num and split_peripherals_num can also be auto-raised past 255).
178        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
228/// Bump event subscriber counts based on feature flags declared in `subscriber_default.toml`.
229///
230/// `active_features` contains lowercase feature names (e.g. `"split"`, `"_ble"`).
231fn 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        // Three indicator processors, the display, two split peripherals, and USB/BLE Rynk sessions.
280        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        // ble/mod.rs subscribes to KeyboardEvent/PointingEvent when advertising
352        // times out, on top of every permanent subscriber. Without a reserved
353        // slot that call panics instead of sleeping until the next key press.
354        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}