Skip to main content

secs_types/
rules.rs

1//! Service rule values the screen shows and the mock service reuses (ADR 0014).
2//!
3//! The service judges with these named constants instead of literals. This
4//! crate's tests also write their values into `ipc/bindings/rules.ts` next to
5//! the generated types, so the screen and the mock import them rather than
6//! copying numbers by hand; CI's bindings diff (ADR 0003) covers that file.
7
8use serde::Serialize;
9use ts_rs::TS;
10
11use crate::role::{HEIGHT_GROUP, TOP_GROUP};
12
13/// Inclusive `(low, high)` range of each value `calibration_settings_edit`
14/// clamps (upstream calibration.py and step_calibration.py constants).
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, TS)]
16#[ts(export)]
17pub struct CalibrationLimits {
18    /// RC low edge (Hz).
19    pub rc_lo: (f64, f64),
20    /// RC high edge (Hz) when it is not automatic.
21    pub rc_hi: (f64, f64),
22    /// Room volume (m³); a value at or below the low end clears it.
23    pub volume: (f64, f64),
24    /// One room dimension (m) as entered.
25    pub dim: (f64, f64),
26    /// One room dimension (m) that yields a room volume.
27    pub dim_valid: (f64, f64),
28    /// Maximum boost (dB).
29    pub boost: (f64, f64),
30    /// Latency budget of the ultra mode (ms).
31    pub ultra_ms: (f64, f64),
32    /// Crossover of one group (Hz).
33    pub xo: (f64, f64),
34    /// XO PEQ band edges (Hz).
35    pub xo_peq: (f64, f64),
36    /// Infra crossover (Hz).
37    pub infra_hz: (f64, f64),
38    /// Maximum infra boost (dB).
39    pub infra_boost: (f64, f64),
40    /// Extra delay of the mains against the subs (ms).
41    pub main_delay: (f64, f64),
42    /// Rear-row trim of a sub group (ms).
43    pub rear_trim: (f64, f64),
44    /// MIMO support upper limit (Hz).
45    pub fsiso: (f64, f64),
46    /// MIMO low limit (Hz) when it is not automatic.
47    pub mimo_low: (f64, f64),
48    /// Maximum number of MIMO supports (0 = no cap).
49    pub mimo_max: (f64, f64),
50}
51
52pub const CALIBRATION_LIMITS: CalibrationLimits = CalibrationLimits {
53    rc_lo: (10.0, 200.0),
54    rc_hi: (100.0, 20000.0),
55    volume: (0.0, 100000.0),
56    dim: (0.0, 100.0),
57    dim_valid: (0.5, 100.0),
58    boost: (0.0, 10.0),
59    ultra_ms: (30.0, 200.0),
60    xo: (40.0, 200.0),
61    xo_peq: (0.0, 1000.0),
62    infra_hz: (10.0, 300.0),
63    infra_boost: (0.0, 24.0),
64    main_delay: (0.0, 100.0),
65    rear_trim: (-20.0, 20.0),
66    fsiso: (50.0, 150.0),
67    mimo_low: (15.0, 150.0),
68    mimo_max: (0.0, 12.0),
69};
70
71/// Sweep lengths a measurement plan accepts, shortest first.
72pub const SWEEP_LENGTHS: [&str; 7] = ["64k", "128k", "256k", "512k", "1M", "2M", "4M"];
73
74/// Start delays before each measured position, as upstream stores them in
75/// `extra['start_delay']` (step 4 of the wizard).
76pub const START_DELAYS: [&str; 4] = ["끄기", "5 초", "10 초", "15 초"];
77
78/// How many recent projects the app settings keep.
79pub const RECENT_PROJECTS_MAX: usize = 10;
80
81/// Configuration groups in the order the service lists them: the upstream
82/// (Korean) group name that keys the XO map, then the presentation id.
83pub const CONFIG_GROUPS: [(&str, &str); 9] = [
84    ("프론트", "front"),
85    ("센터", "center"),
86    ("서브", "sub"),
87    ("서라운드", "surround"),
88    ("백", "back"),
89    ("와이드", "wide"),
90    (TOP_GROUP, "top"),
91    (HEIGHT_GROUP, "height"),
92    ("기타", "other"),
93];
94
95/// Measurement arrangements by position count.
96pub const ARRANGEMENT_COUNTS: [usize; 4] = [1, 9, 13, 17];
97
98/// Finished jobs the service keeps in its job list beside every active one
99/// (ADR 0009).
100pub const JOB_HISTORY_MAX: usize = 32;
101
102#[cfg(test)]
103mod typescript {
104    //! `rules.ts`: the values above as TypeScript, deterministic text.
105    use serde::Serialize;
106    use serde_json::Value;
107
108    use super::*;
109
110    const HEADER: &str = "// This file was generated by `cargo test -p secs-types` from crates/secs-types/src/rules.rs. Do not edit this file manually.\n";
111
112    fn literal(value: &Value) -> String {
113        match value {
114            Value::Null => "null".into(),
115            Value::Bool(value) => value.to_string(),
116            // Display prints the shortest round-trip form without a trailing
117            // `.0`, as JavaScript does for these magnitudes.
118            Value::Number(number) => match (number.as_i64(), number.as_f64()) {
119                (Some(int), _) => int.to_string(),
120                (None, Some(float)) => float.to_string(),
121                (None, None) => number.to_string(),
122            },
123            Value::String(_) => value.to_string(),
124            Value::Array(items) => {
125                format!(
126                    "[{}]",
127                    items.iter().map(literal).collect::<Vec<_>>().join(", ")
128                )
129            }
130            Value::Object(fields) => format!(
131                "{{\n{}}}",
132                fields
133                    .iter()
134                    .map(|(key, value)| format!("  {key}: {},\n", literal(value)))
135                    .collect::<String>()
136            ),
137        }
138    }
139
140    /// One `export const`; arrays and objects are `as const`.
141    pub(super) fn constant(name: &str, ty: Option<&str>, value: &impl Serialize) -> String {
142        let value = serde_json::to_value(value).expect("rule constants serialize");
143        let annotation = ty.map(|ty| format!(": {ty}")).unwrap_or_default();
144        let suffix = if value.is_array() || value.is_object() {
145            " as const"
146        } else {
147            ""
148        };
149        format!(
150            "export const {name}{annotation} = {}{suffix};\n",
151            literal(&value)
152        )
153    }
154
155    pub(super) fn rules_ts() -> String {
156        [
157            HEADER.to_owned(),
158            "import type { CalibrationLimits } from \"./CalibrationLimits\";\n\n".to_owned(),
159            constant(
160                "CALIBRATION_LIMITS",
161                Some("CalibrationLimits"),
162                &CALIBRATION_LIMITS,
163            ),
164            constant("SWEEP_LENGTHS", None, &SWEEP_LENGTHS),
165            constant("START_DELAYS", None, &START_DELAYS),
166            constant("RECENT_PROJECTS_MAX", None, &RECENT_PROJECTS_MAX),
167            constant("CONFIG_GROUPS", None, &CONFIG_GROUPS),
168            constant("ARRANGEMENT_COUNTS", None, &ARRANGEMENT_COUNTS),
169            constant("JOB_HISTORY_MAX", None, &JOB_HISTORY_MAX),
170        ]
171        .concat()
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::typescript::{constant, rules_ts};
178    use super::*;
179
180    #[test]
181    fn renders_constants_as_typescript() {
182        assert_eq!(
183            constant("RECENT_PROJECTS_MAX", None, &RECENT_PROJECTS_MAX),
184            "export const RECENT_PROJECTS_MAX = 10;\n"
185        );
186        assert_eq!(
187            constant("EDGES", None, &[(0.5, 100.0), (-20.0, 0.71)]),
188            "export const EDGES = [[0.5, 100], [-20, 0.71]] as const;\n"
189        );
190        assert_eq!(
191            constant("GROUPS", None, &[("탑", "top")]),
192            "export const GROUPS = [[\"탑\", \"top\"]] as const;\n"
193        );
194        let limits = constant("LIMITS", Some("CalibrationLimits"), &CALIBRATION_LIMITS);
195        assert!(
196            limits.starts_with(
197                "export const LIMITS: CalibrationLimits = {\n  rc_lo: [10, 200],\n  rc_hi: [100, 20000],\n"
198            ),
199            "{limits}"
200        );
201        assert!(
202            limits.ends_with("  mimo_max: [0, 12],\n} as const;\n"),
203            "{limits}"
204        );
205    }
206
207    /// Writes `rules.ts` where ts-rs writes the bindings (`TS_RS_EXPORT_DIR`,
208    /// `./bindings` by default), so CI's bindings diff compares it too.
209    #[test]
210    fn export_rules_ts() {
211        let dir = ts_rs::Config::from_env().out_dir().to_path_buf();
212        std::fs::create_dir_all(&dir).unwrap();
213        std::fs::write(dir.join("rules.ts"), rules_ts()).unwrap();
214    }
215}