Skip to main content

secs_types/
ipc.rs

1//! IPC between the screen and the service.
2//!
3//! The screen sends a [`Request`] as `{"method": "...", "params": {...}}` and
4//! always receives an envelope: `{"ok": true, "data": Response}` or
5//! `{"ok": false, "error": IpcError}`. The service never throws across this
6//! seam. Long jobs report progress through the event sink supplied with the call.
7//!
8//! Errors carry a code and parameters, not prose: the screen turns them into
9//! a sentence in the current language and keeps `detail` (the raw text) for
10//! the details view and the log.
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15use ts_rs::TS;
16
17use crate::calibration::{CalibrationSettings, Mode};
18use crate::compute::{ComputePreference, ComputeReport, ComputeStatus};
19pub use crate::settings_edit::CalibrationSettingsEditParams;
20
21/// A call from the screen.
22///
23/// The method names come from the variant names in one place: serde and
24/// strum both rename them to snake_case, and `method_names_match_the_wire`
25/// checks that the two agree.
26#[derive(
27    Debug, Clone, PartialEq, Serialize, Deserialize, TS, strum::VariantNames, strum::IntoStaticStr,
28)]
29#[serde(tag = "method", content = "params", rename_all = "snake_case")]
30#[strum(serialize_all = "snake_case")]
31#[ts(export)]
32pub enum Request {
33    /// Version, the upstream release this build is ported from, and the
34    /// platform. The first call the screen makes.
35    Bootstrap,
36    /// The `ProjectNew` Request variant.
37    ProjectNew(ProjectNewParams),
38    /// The `ProjectOpen` Request variant.
39    ProjectOpen(ProjectPathParams),
40    /// The `ProjectSave` Request variant.
41    ProjectSave(ProjectSaveParams),
42    /// The `ProjectInfo` Request variant.
43    ProjectInfo,
44    /// The `SampleLoad` Request variant.
45    SampleLoad(SampleLoadParams),
46    /// The `Calibrate` Request variant.
47    Calibrate(CalibrateParams),
48    /// The `RecommendXo` Request variant.
49    RecommendXo,
50    /// The `Mimo` Request variant.
51    Mimo(MimoParams),
52    /// The `Results` Request variant.
53    Results,
54    /// The `Export` Request variant.
55    Export(ExportParams),
56    /// The `JobCancel` Request variant.
57    JobCancel(JobCancelParams),
58    /// The `Jobs` Request variant.
59    Jobs,
60    /// The `MeasureMethods` Request variant.
61    MeasureMethods,
62    /// The `MeasureConfigure` Request variant.
63    MeasureConfigure(MeasureConfigureParams),
64    /// Returns the measurement settings stored in the open project.
65    MeasureConfigGet,
66    /// Returns one recording-status entry per arranged position.
67    MeasurementRecords,
68    /// Returns the project's microphone calibration and attachment counts.
69    MicCalibrationGet,
70    /// Reads, validates and stores a microphone calibration file in the project.
71    MicCalibrationSet(MicCalibrationSetParams),
72    /// Removes the project's selected microphone calibration file.
73    MicCalibrationClear,
74    /// Attaches the selected file to sweep measurements that have no mic curve.
75    MicCalibrationAttach,
76    /// Returns stored per-role level and SPL values.
77    LevelsGet,
78    /// Fills only missing per-role level values with upstream defaults.
79    LevelsFillDefaults,
80    /// Stops the current 2.5-second level burst without stopping its monitor.
81    LevelStop,
82    /// The `MeasurePosition` Request variant.
83    MeasurePosition(MeasurePositionParams),
84    /// The `PositionClear` Request variant.
85    PositionClear(PositionClearParams),
86    /// The `Undo` Request variant.
87    Undo(UndoParams),
88    /// The `RewLaunch` Request variant.
89    RewLaunch,
90    /// The `MdatScan` Request variant.
91    MdatScan(MdatScanParams),
92    /// The `MdatImport` Request variant.
93    MdatImport(MdatImportParams),
94    /// Returns the editable speaker configuration.
95    ConfigGet,
96    /// Replaces the speaker configuration with a built-in preset.
97    ConfigApplyPreset(ConfigPresetParams),
98    /// Changes the number of configured subwoofers.
99    ConfigSetSubCount(ConfigSubCountParams),
100    /// Changes one speaker or one of its bands.
101    ConfigSetSpeaker(ConfigSetSpeakerParams),
102    /// Adds the first unused upstream speaker role.
103    ConfigAddSpeaker,
104    /// Removes one configured speaker.
105    ConfigRemoveSpeaker(ConfigIndexParams),
106    /// Renumbers outputs in configuration order.
107    ConfigRenumber,
108    /// Returns structured configuration validation notices.
109    ConfigValidate,
110    /// Writes the upstream patch-sheet CSV.
111    PatchSheetCsv(PathParams),
112    /// Returns the editable DSP profile.
113    DspGet,
114    /// Applies a DSP preset or manual fields.
115    DspSet(DspSetParams),
116    /// Returns measurement arrangement and timing options.
117    LayoutGet,
118    /// Changes measurement arrangement or timing options.
119    LayoutSet(LayoutSetParams),
120    /// Returns the current per-position and total estimate.
121    PositionEstimate,
122    /// Returns the target curve and presets.
123    TargetGet,
124    /// Replaces target control points.
125    TargetSetPoints(TargetPointsParams),
126    /// Applies a built-in target preset.
127    TargetApplyPreset(TargetPresetParams),
128    /// Saves the target curve as upstream JSON.
129    TargetSave(PathParams),
130    /// Loads the target curve from upstream JSON.
131    TargetLoad(PathParams),
132    /// Returns calibration settings stored in the project.
133    CalibrationSettingsGet,
134    /// Stores calibration settings in the project.
135    CalibrationSettingsSet(CalibrationSettingsParams),
136    /// Applies one upstream calibration-screen edit and stores the settings.
137    CalibrationSettingsEdit(CalibrationSettingsEditParams),
138    /// Filter warnings from current settings (gain warnings require a run).
139    SubWarnings,
140    /// Optional REW facts read through the injected transport.
141    RewInfo,
142    /// Changes REW's audio driver and returns refreshed REW facts.
143    RewDriverSet(RewDriverSetParams),
144    /// Returns numeric capabilities for one calibration mode.
145    ModeSpec(ModeSpecParams),
146    /// Returns application-local settings.
147    AppSettingsGet,
148    /// Stores application-local settings.
149    AppSettingsSet(AppSettingsParams),
150    /// Reads CamillaDSP and Equalizer APO application state.
151    ApplyStatus,
152    /// Exports and applies the current design to CamillaDSP.
153    ApplyCamilla(ApplyCamillaParams),
154    /// Exports and applies the current design to Equalizer APO.
155    ApplyEqapo,
156    /// Removes SECS 2R's managed Equalizer APO include.
157    RevertEqapo,
158    /// Validates and stores application settings for both apply targets.
159    ApplySettingsSet(ApplySettings),
160    /// Installs the Windows managed host service with an elevation prompt.
161    HostInstall,
162    /// Stops and removes the Windows managed host service with an elevation prompt.
163    HostUninstall,
164    /// Clears every target persisted by the managed host service.
165    HostStopManaging,
166    /// Reports the opt-in local API server and token.
167    LocalApiStatus,
168    /// Replaces the local API bearer token and restarts its server when needed.
169    LocalApiTokenRegenerate,
170    /// Available compute devices and the effective application preference.
171    ComputeStatus,
172    /// Opens the official GPU runtime installation guide for this platform.
173    OpenGpuHelp,
174    /// Smoothed, normalized measurement curves and their shared frequency grid.
175    MeasurementCurves(MeasurementCurvesParams),
176    /// Ready-to-draw calibration curves and axis limits for one result tab.
177    ResultCurves(ResultCurvesParams),
178    /// Numeric metrics, filter counts, BA rows and coded result notices.
179    ResultLines,
180    /// Copies the last BA output alignment into the manual sub settings.
181    SubTakeValues,
182    /// Opens a project-file picker through the application host.
183    DialogOpenProject,
184    /// Opens a project save-file picker through the application host.
185    DialogSaveProject(DialogSuggestedParams),
186    /// Opens an mdat-file picker through the application host.
187    DialogPickMdat,
188    /// Opens a folder picker through the application host.
189    DialogPickFolder(DialogPickFolderParams),
190    /// Opens a typed save-file picker through the application host.
191    DialogSaveFile(DialogSaveFileParams),
192    /// Opens a typed file picker through the application host.
193    DialogOpenFile(DialogOpenFileParams),
194    /// Opens a folder in the operating-system file manager.
195    OpenFolder(OpenFolderParams),
196    /// Starts the live input level monitor.
197    LevelMonitorStart(LevelMonitorStartParams),
198    /// Stops the live input level monitor.
199    LevelMonitorStop,
200    /// Plays a role's level-check signal for 2.5 seconds.
201    LevelPlay(LevelPlayParams),
202    /// Starts watching measurement-method status.
203    StatusWatchStart,
204    /// Stops watching measurement-method status.
205    StatusWatchStop,
206    /// Returns the stored or derived export base folder.
207    ExportFolderGet,
208    /// Stores the export base folder in the open project.
209    ExportFolderSet(ExportFolderSetParams),
210}
211
212impl Request {
213    /// Every method name, as it appears on the wire, derived from the
214    /// variants. `secs-policy` checks that each one is registered in
215    /// `features.toml` as `ipc.<method>`.
216    pub const METHODS: &'static [&'static str] = <Self as strum::VariantNames>::VARIANTS;
217
218    /// The wire name of this request's method.
219    pub fn method(&self) -> &'static str {
220        self.into()
221    }
222}
223
224/// The successful result of a [`Request`], tagged with the same method name.
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
226#[serde(tag = "method", content = "data", rename_all = "snake_case")]
227#[ts(export)]
228pub enum Response {
229    /// The `Bootstrap` Response variant.
230    Bootstrap(BootstrapInfo),
231    /// The `ProjectNew` Response variant.
232    ProjectNew(ProjectInfo),
233    /// The `ProjectOpen` Response variant.
234    ProjectOpen(ProjectInfo),
235    /// The `ProjectSave` Response variant.
236    ProjectSave(ProjectInfo),
237    /// The `ProjectInfo` Response variant.
238    ProjectInfo(ProjectInfo),
239    /// The `SampleLoad` Response variant.
240    SampleLoad(JobStarted),
241    /// The `Calibrate` Response variant.
242    Calibrate(JobStarted),
243    /// The `RecommendXo` Response variant.
244    RecommendXo(JobStarted),
245    /// The `Mimo` Response variant.
246    Mimo(JobStarted),
247    /// The `Results` Response variant.
248    Results(ResultsSummary),
249    /// The `Export` Response variant.
250    Export(JobStarted),
251    /// The `JobCancel` Response variant.
252    JobCancel(JobSnapshot),
253    /// The `Jobs` Response variant.
254    Jobs(Vec<JobSnapshot>),
255    /// The `MeasureMethods` Response variant.
256    MeasureMethods(MeasureMethodsResult),
257    /// The `MeasureConfigure` Response variant.
258    MeasureConfigure(MeasureConfigResult),
259    /// Measurement settings stored in the open project.
260    MeasureConfigGet(MeasureConfigResult),
261    /// Recording status for every arranged position.
262    MeasurementRecords(MeasurementRecordsResult),
263    /// Selected microphone calibration and attachment counts.
264    MicCalibrationGet(MicCalibrationStatus),
265    /// Selected microphone calibration after storing a file.
266    MicCalibrationSet(MicCalibrationStatus),
267    /// Empty microphone calibration status after clearing the selection.
268    MicCalibrationClear(MicCalibrationStatus),
269    /// Microphone calibration status after attaching it to eligible sweeps.
270    MicCalibrationAttach(MicCalibrationStatus),
271    /// Stored per-role level and SPL values.
272    LevelsGet(LevelsResult),
273    /// Stored values after filling missing level defaults.
274    LevelsFillDefaults(LevelsResult),
275    /// Confirms that the current level burst was stopped.
276    LevelStop(()),
277    /// The `MeasurePosition` Response variant.
278    MeasurePosition(JobStarted),
279    /// The `PositionClear` Response variant.
280    PositionClear(UndoResult),
281    /// The `Undo` Response variant.
282    Undo(ProjectInfo),
283    /// The `RewLaunch` Response variant.
284    RewLaunch(RewStatusResult),
285    /// The `MdatScan` Response variant.
286    MdatScan(MdatScanResult),
287    /// The `MdatImport` Response variant.
288    MdatImport(JobStarted),
289    /// The `ConfigGet` Response variant.
290    ConfigGet(ConfigResult),
291    /// The `ConfigApplyPreset` Response variant.
292    ConfigApplyPreset(ConfigMutationResult),
293    /// The `ConfigSetSubCount` Response variant.
294    ConfigSetSubCount(ConfigMutationResult),
295    /// The `ConfigSetSpeaker` Response variant.
296    ConfigSetSpeaker(ConfigResult),
297    /// The `ConfigAddSpeaker` Response variant.
298    ConfigAddSpeaker(ConfigResult),
299    /// The `ConfigRemoveSpeaker` Response variant.
300    ConfigRemoveSpeaker(ConfigResult),
301    /// The `ConfigRenumber` Response variant.
302    ConfigRenumber(ConfigResult),
303    /// The `ConfigValidate` Response variant.
304    ConfigValidate(Vec<Notice>),
305    /// The `PatchSheetCsv` Response variant.
306    PatchSheetCsv(PathResult),
307    /// The `DspGet` Response variant.
308    DspGet(DspResult),
309    /// The `DspSet` Response variant.
310    DspSet(DspResult),
311    /// The `LayoutGet` Response variant.
312    LayoutGet(LayoutResult),
313    /// The `LayoutSet` Response variant.
314    LayoutSet(LayoutMutationResult),
315    /// The `PositionEstimate` Response variant.
316    PositionEstimate(PositionEstimateResult),
317    /// The `TargetGet` Response variant.
318    TargetGet(TargetResult),
319    /// The `TargetSetPoints` Response variant.
320    TargetSetPoints(TargetResult),
321    /// The `TargetApplyPreset` Response variant.
322    TargetApplyPreset(TargetResult),
323    /// The `TargetSave` Response variant.
324    TargetSave(PathResult),
325    /// The `TargetLoad` Response variant.
326    TargetLoad(TargetResult),
327    /// The `CalibrationSettingsGet` Response variant.
328    CalibrationSettingsGet(CalibrationSettings),
329    /// The `CalibrationSettingsSet` Response variant.
330    CalibrationSettingsSet(CalibrationSettings),
331    /// Settings after one edit.
332    CalibrationSettingsEdit(CalibrationSettings),
333    /// Current settings-only filter warnings.
334    SubWarnings(Vec<Notice>),
335    /// Current REW facts; unavailable fields are null.
336    RewInfo(RewInfoResult),
337    /// Current REW facts after changing its driver.
338    RewDriverSet(RewInfoResult),
339    /// The `ModeSpec` Response variant.
340    ModeSpec(ModeSpecResult),
341    /// The `AppSettingsGet` Response variant.
342    AppSettingsGet(AppSettings),
343    /// The `AppSettingsSet` Response variant.
344    AppSettingsSet(AppSettings),
345    /// Current state of both application targets.
346    ApplyStatus(ApplyStatus),
347    /// The CamillaDSP application job.
348    ApplyCamilla(JobStarted),
349    /// The Equalizer APO application job.
350    ApplyEqapo(JobStarted),
351    /// Confirms removal of the managed Equalizer APO include.
352    RevertEqapo(()),
353    /// Stored application settings for both targets.
354    ApplySettingsSet(ApplySettings),
355    /// Confirms that the Windows managed host service installer was started.
356    HostInstall(()),
357    /// Confirms that the Windows managed host service uninstaller was started.
358    HostUninstall(()),
359    /// Confirms that every managed target was cleared.
360    HostStopManaging(()),
361    /// Current local API server and token state.
362    LocalApiStatus(LocalApiStatus),
363    /// Local API state after replacing its bearer token.
364    LocalApiTokenRegenerate(LocalApiStatus),
365    /// Device capability and automatic-selection result.
366    ComputeStatus(ComputeStatus),
367    /// GPU installation documentation opened by the application host.
368    OpenGpuHelp(()),
369    /// Measurement curves on the common display grid.
370    MeasurementCurves(MeasurementCurvesResult),
371    /// Calibration tab curves, ranges and staleness.
372    ResultCurves(ResultCurvesResult),
373    /// Numeric summary lines and BA table.
374    ResultLines(ResultLinesResult),
375    /// Stored settings after taking the last sub alignment.
376    SubTakeValues(CalibrationSettings),
377    /// The chosen project path, or null when cancelled.
378    DialogOpenProject(Option<String>),
379    /// The chosen project destination, or null when cancelled.
380    DialogSaveProject(Option<String>),
381    /// The chosen mdat path, or null when cancelled.
382    DialogPickMdat(Option<String>),
383    /// The chosen folder, or null when cancelled.
384    DialogPickFolder(Option<String>),
385    /// The chosen save destination, or null when cancelled.
386    DialogSaveFile(Option<String>),
387    /// The chosen source path, or null when cancelled.
388    DialogOpenFile(Option<String>),
389    /// Confirms that the folder-open request reached the host.
390    OpenFolder(()),
391    /// The live level-monitor job.
392    LevelMonitorStart(JobStarted),
393    /// The stopped live level-monitor job, when one was running.
394    LevelMonitorStop(Option<JobSnapshot>),
395    /// Confirms that role playback was accepted.
396    LevelPlay(()),
397    /// The measurement-status watch job.
398    StatusWatchStart(JobStarted),
399    /// The stopped status-watch job, when one was running.
400    StatusWatchStop(Option<JobSnapshot>),
401    /// The current export base folder.
402    ExportFolderGet(ExportFolderResult),
403    /// The stored export base folder.
404    ExportFolderSet(ExportFolderResult),
405}
406
407/// Optional measurement selection; an absent selection includes every recorded pair.
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
409#[ts(export)]
410pub struct MeasurementCurvesParams {
411    /// Position numbers to include.
412    #[ts(type = "number[] | null")]
413    pub positions: Option<Vec<i64>>,
414    /// Speaker roles to include.
415    pub roles: Option<Vec<String>>,
416}
417/// A smoothed measurement curve shifted by the result's shared reference.
418#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
419#[ts(export)]
420pub struct MeasurementCurve {
421    /// Speaker role.
422    pub role: String,
423    /// Measurement position.
424    #[ts(type = "number")]
425    pub position: i64,
426    /// Configuration kind: main, sub or top.
427    pub kind: String,
428    /// Levels in dB on the shared grid.
429    pub values: Vec<f64>,
430}
431/// Display limits; both endpoints are data values, not logarithms.
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
433#[ts(export)]
434pub struct CurveAxes {
435    /// Frequency bounds in Hz.
436    pub frequency_hz: [f64; 2],
437    /// Level or delay bounds.
438    pub value: [f64; 2],
439    /// Vertical unit: db or ms.
440    pub unit: String,
441}
442/// Frequency band used to set zero dB on the measurement graph.
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
444#[serde(rename_all = "snake_case")]
445#[ts(export)]
446pub enum CurveReferenceBand {
447    /// 200–2000 Hz mean of a main speaker.
448    Mid,
449    /// 40–120 Hz mean of a subwoofer.
450    Sub,
451}
452/// The stored measurement that sets zero dB for every displayed curve.
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
454#[ts(export)]
455pub struct CurveReference {
456    /// Configured speaker role.
457    pub role: String,
458    /// Measurement position.
459    #[ts(type = "number")]
460    pub position: i64,
461    /// Frequency band used for the mean.
462    pub band: CurveReferenceBand,
463}
464/// Measurement graph data.
465#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
466#[ts(export)]
467pub struct MeasurementCurvesResult {
468    /// Shared 280-point logarithmic frequency grid in Hz.
469    pub grid: Vec<f64>,
470    /// One curve per recorded role and position.
471    pub curves: Vec<MeasurementCurve>,
472    /// Stored curve and band that set zero dB, or absent when nothing is measured.
473    pub reference: Option<CurveReference>,
474    /// Upstream default axis bounds.
475    pub axes: CurveAxes,
476}
477/// The calibration graph to display.
478#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
479#[serde(rename_all = "snake_case")]
480#[ts(export)]
481pub enum ResultTab {
482    /// Sound pressure level relative to the target reference.
483    Spl,
484    /// Group delay in milliseconds.
485    Gd,
486    /// Crossover preview.
487    Xo,
488    /// Bass alignment.
489    Ba,
490}
491/// Selection for one ready-to-draw result graph.
492#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
493#[ts(export)]
494pub struct ResultCurvesParams {
495    /// Graph tab.
496    pub tab: ResultTab,
497    /// Selected main role; absent selects the first available channel.
498    pub channel: Option<String>,
499    /// Crossover group; absent selects the selected channel's group.
500    pub group: Option<String>,
501    /// Dragged crossover in Hz; absent uses the configured value.
502    pub xo_hz: Option<f64>,
503    /// Apply input correction to the XO preview; defaults to true.
504    pub corrected: Option<bool>,
505}
506/// One named series on a result graph.
507#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
508#[ts(export)]
509pub struct ResultCurve {
510    /// Series identity: before, after, target, mimo, main, sub, sum_before or sum_after.
511    pub kind: String,
512    /// Speaker role for per-sub curves, otherwise absent.
513    pub role: Option<String>,
514    /// Values on the response's shared grid.
515    pub values: Vec<f64>,
516}
517/// A result graph whose calculations are entirely owned by the service.
518#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
519#[ts(export)]
520pub struct ResultCurvesResult {
521    /// Requested tab.
522    pub tab: ResultTab,
523    /// Shared frequency grid in Hz.
524    pub grid: Vec<f64>,
525    /// Ordered drawable series.
526    pub curves: Vec<ResultCurve>,
527    /// Upstream default axis bounds.
528    pub axes: CurveAxes,
529    /// Whether the retained result no longer matches current inputs.
530    pub stale: bool,
531    /// Whether the graph comes from a dedicated subs-only result.
532    pub subs_only: bool,
533    /// RC lower and upper bounds, with an unknown auto upper bound represented as null.
534    pub rc_band_hz: Option<(f64, Option<f64>)>,
535    /// Selected crossover frequency for the draggable line.
536    pub xo_hz: Option<f64>,
537}
538/// Values behind one channel's metrics and filters lines.
539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
540#[ts(export)]
541pub struct ChannelResultLine {
542    /// Main role.
543    pub role: String,
544    /// Before and after percentage within one dB, 20–200 Hz.
545    pub within_1db_pct: [f64; 2],
546    /// Before and after group-delay spread, 20–100 Hz, in ms.
547    pub gd_spread_ms: [f64; 2],
548    /// Number of input PEQs.
549    pub peq_count: usize,
550    /// FIR taps, absent when no FIR is used.
551    pub fir_taps: Option<usize>,
552    /// Crossover frequency, absent without bass management.
553    pub xo_hz: Option<f64>,
554    /// Fresh, enabled MIMO support path count, absent when not shown.
555    pub mimo_support_count: Option<usize>,
556}
557/// One sub's output alignment and fixed filters.
558#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
559#[ts(export)]
560pub struct SubResultRow {
561    /// Sub role.
562    pub role: String,
563    /// Alignment mode code.
564    pub mode: String,
565    /// Output gain in dB.
566    pub gain_db: f64,
567    /// Output delay in ms.
568    pub delay_ms: f64,
569    /// Output polarity, plus or minus one.
570    pub polarity: i8,
571    /// Ordered all-pass frequency and Q pairs.
572    pub allpass: Vec<(f64, f64)>,
573    /// High-pass frequency and filter type.
574    pub high_pass: (f64, String),
575    /// Low-pass frequency and filter type.
576    pub low_pass: (f64, String),
577    /// Fixed and generated peaking filters as frequency, Q and gain.
578    pub peq: Vec<(f64, f64, f64)>,
579}
580/// A typed BA layout summary.
581#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
582#[ts(export)]
583pub struct SubLayoutLine {
584    /// Bundle name.
585    pub bundle: String,
586    /// Layout code, dba or multi.
587    pub kind: String,
588    /// All members.
589    pub members: Vec<String>,
590    /// Front row roles.
591    pub front: Vec<String>,
592    /// Rear row roles.
593    pub rear: Vec<String>,
594    /// Rear delay in ms.
595    pub rear_ms: f64,
596    /// Automatic rear delay in ms, when available.
597    pub auto_ms: Option<f64>,
598    /// Manual rear trim in ms.
599    pub trim_ms: f64,
600    /// Shared peaking filters.
601    pub peq: Vec<(f64, f64, f64)>,
602}
603/// BA metrics and table, independent of the selected main channel.
604#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
605#[ts(export)]
606pub struct SubResultLines {
607    /// Whether this BA result is stale.
608    pub stale: bool,
609    /// Whether a dedicated subs-only run supplies these values.
610    pub subs_only: bool,
611    /// Metric band in Hz.
612    pub band_hz: [f64; 2],
613    /// Before and after flatness in dB.
614    pub flatness_db: [f64; 2],
615    /// Before and after across-seat spread, absent for a single seat.
616    pub seat_spread_db: Option<[f64; 2]>,
617    /// Number of complete sub positions.
618    pub positions: usize,
619    /// Output rows in configuration order.
620    pub rows: Vec<SubResultRow>,
621    /// Bundle layouts.
622    pub layouts: Vec<SubLayoutLine>,
623    /// Filter and gain warning codes and their arguments.
624    pub warnings: Vec<Notice>,
625}
626/// Numeric calibration summary lines; no localized sentences cross IPC.
627#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
628#[ts(export)]
629pub struct ResultLinesResult {
630    /// Whether the full calibration result is stale.
631    pub stale: bool,
632    /// Per-main metrics and filter counts.
633    pub channels: Vec<ChannelResultLine>,
634    /// Total delay in milliseconds, absent before calibration.
635    pub total_delay_ms: Option<f64>,
636    /// Engine notes as codes and arguments.
637    pub notes: Vec<Notice>,
638    /// RC ceiling provenance and room-volume notices.
639    pub rc: Vec<Notice>,
640    /// BA metrics, table, layouts and warnings when at least two subs are available.
641    pub ba: Option<SubResultLines>,
642}
643
644/// What the screen needs before it draws anything.
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
646#[ts(export)]
647pub struct BootstrapInfo {
648    /// Product name shown in the title bar and the about view.
649    pub app_name: String,
650    /// The SECS 2R version (the workspace version).
651    pub version: String,
652    /// True while the major version is 0: every 0.x build is a prerelease.
653    pub prerelease: bool,
654    /// The SECS 2 release this build follows.
655    pub upstream: UpstreamBase,
656    /// The operating-system platform running this build.
657    pub platform: Platform,
658}
659/// The upstream SECS 2 release a build is ported from (`UPSTREAM.toml`).
660#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
661#[ts(export)]
662pub struct UpstreamBase {
663    /// Upstream product name from `UPSTREAM.toml`.
664    pub name: String,
665    /// Upstream repository URL from `UPSTREAM.toml`.
666    pub repository: String,
667    /// Upstream release version from `UPSTREAM.toml`.
668    pub version: String,
669    /// Forty-character upstream Git commit from `UPSTREAM.toml`.
670    pub commit: String,
671    /// Upstream snapshot date from `UPSTREAM.toml`.
672    pub date: String,
673}
674/// The operating-system platform running the service.
675#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
676#[ts(export)]
677pub struct Platform {
678    /// `windows`, `macos` or `linux` (`std::env::consts::OS`).
679    pub os: String,
680    /// `x86_64` or `aarch64` (`std::env::consts::ARCH`).
681    pub arch: String,
682}
683
684/// Inputs for creating a project.
685#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
686#[ts(export)]
687pub struct ProjectNewParams {
688    /// Destination `.secs` file path.
689    pub path: String,
690    /// Speaker-layout preset name.
691    pub preset: String,
692    /// Number of subwoofers.
693    pub sub_count: u8,
694    /// Whether to create the fixed 7.1.4 sample-room project.
695    pub sample: bool,
696    /// Whether this is a tutorial session using the sample room. The session
697    /// cannot be saved or exported; its measurements and results live in a
698    /// temporary folder that is removed when the session ends, and `path` is
699    /// only reported back.
700    #[serde(default)]
701    pub tutorial: bool,
702}
703/// Inputs for loading embedded or synthetic sample measurements.
704#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
705#[ts(export)]
706pub struct SampleLoadParams {
707    /// Synthetic position count, 2 to 17 (the largest layout); absent loads
708    /// the one-position embedded room. Other counts are refused with
709    /// `measurement_input` (`field = positions`).
710    pub positions: Option<u32>,
711}
712/// A project file path.
713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
714#[ts(export)]
715pub struct ProjectPathParams {
716    /// Source `.secs` file path.
717    pub path: String,
718}
719/// Inputs for saving the open project.
720#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
721#[ts(export)]
722pub struct ProjectSaveParams {
723    /// Optional save-as `.secs` path; absent saves in place.
724    pub path: Option<String>,
725}
726/// Screen-facing information about the open project.
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
728#[ts(export)]
729pub struct ProjectInfo {
730    /// Open `.secs` file path.
731    pub path: String,
732    /// Project display name.
733    pub name: String,
734    /// Speaker-layout preset name.
735    pub preset: String,
736    /// Number of configured subwoofers.
737    pub sub_count: usize,
738    /// Measured position numbers.
739    pub positions: Vec<i64>,
740    /// Whether the in-memory project has unsaved changes.
741    pub dirty: bool,
742    /// Whether this open project is a non-writable tutorial session.
743    pub tutorial: bool,
744    /// Warnings produced while loading legacy project entries.
745    pub migration_warnings: Vec<MigrationWarning>,
746}
747/// A project migration warning code and affected entry.
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
749#[ts(export)]
750pub struct MigrationWarning {
751    /// Stable warning code translated by the screen.
752    pub code: String,
753    /// Project entry affected by the warning.
754    pub entry: String,
755}
756
757/// Optional settings for a calibration run.
758#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
759#[ts(export)]
760pub struct CalibrateParams {
761    /// Settings override; absent uses the open project.
762    pub settings: Option<CalibrationSettings>,
763    /// Designs the subwoofers only (upstream [서브만 계산]); the result is
764    /// kept apart and shown by the BA tab until the next full run.
765    #[serde(default)]
766    pub subs_only: bool,
767    /// Computes MIMO after the correction in the same job. Absent on the
768    /// wire means false, so the TypeScript field is optional.
769    #[serde(default)]
770    #[ts(as = "Option<bool>", optional)]
771    pub mimo: bool,
772}
773/// Optional settings for a MIMO run.
774#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
775#[ts(export)]
776pub struct MimoParams {
777    /// Settings override; absent uses the open project.
778    pub settings: Option<CalibrationSettings>,
779}
780
781/// A service-assigned long-running job identifier.
782#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
783#[serde(transparent)]
784#[ts(export)]
785pub struct JobId(#[ts(type = "number")] pub u64);
786/// The operation performed by a long-running job.
787#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
788#[serde(rename_all = "snake_case")]
789#[ts(export)]
790pub enum JobKind {
791    /// Records all configured roles at one measurement position.
792    Measure,
793    /// Loads embedded or deterministic synthetic sample measurements.
794    SampleLoad,
795    /// Imports assigned measurements from a REW mdat file.
796    MdatImport,
797    /// Computes a calibration design from project measurements.
798    Calibrate,
799    /// Scores crossover-frequency candidates.
800    RecommendXo,
801    /// Computes MIMO support paths.
802    Mimo,
803    /// Writes one selected export bundle.
804    Export,
805    /// Exports and applies the current design to a running target.
806    Apply,
807    /// Streams microphone levels and controls level-check playback.
808    LevelMonitor,
809    /// Watches measurement-method status changes.
810    StatusWatch,
811}
812/// The current lifecycle state of a job.
813#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
814#[serde(rename_all = "snake_case")]
815#[ts(export)]
816pub enum JobStatus {
817    /// The worker is still running and may accept cancellation.
818    Running,
819    /// Cancellation was requested and the worker has not stopped yet.
820    CancelRequested,
821    /// The worker completed and produced a result.
822    Succeeded,
823    /// The worker stopped with an error.
824    Failed,
825    /// The caller cancelled the operation.
826    Cancelled,
827}
828impl JobStatus {
829    /// Whether the job can no longer change state.
830    pub fn is_terminal(self) -> bool {
831        matches!(self, Self::Succeeded | Self::Failed | Self::Cancelled)
832    }
833}
834/// The immediate response after a job starts.
835#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
836#[ts(export)]
837pub struct JobStarted {
838    /// Identifier assigned to the job.
839    pub job: JobId,
840    /// Operation the job performs.
841    pub kind: JobKind,
842}
843/// The latest observable state of a job.
844#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
845#[ts(export)]
846pub struct JobSnapshot {
847    /// Identifier assigned to the job.
848    pub job: JobId,
849    /// Operation the job performs.
850    pub kind: JobKind,
851    /// Current lifecycle state.
852    pub status: JobStatus,
853    /// Whether a cancellation request can still prevent commit.
854    pub cancellable: bool,
855    /// Most recently reported stage progress.
856    pub progress: Option<JobProgress>,
857    /// Failure once finished unsuccessfully.
858    pub error: Option<IpcError>,
859}
860/// Progress within one named job stage.
861#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
862#[ts(export)]
863pub struct JobProgress {
864    /// Stable stage code translated by the screen.
865    pub stage: String,
866    #[ts(type = "number")]
867    /// Completed work units in this stage.
868    pub done: u64,
869    #[ts(type = "number")]
870    /// Total work units in this stage.
871    pub total: u64,
872}
873/// A pushed lifecycle event for a long-running job.
874#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
875#[serde(tag = "event", rename_all = "snake_case")]
876#[ts(export)]
877pub enum JobEvent {
878    /// Announces a newly accepted job and its operation.
879    Started { job: JobId, kind: JobKind },
880    /// Reports completed and total work units for a stable stage code.
881    Progress {
882        job: JobId,
883        stage: String,
884        #[ts(type = "number")]
885        done: u64,
886        #[ts(type = "number")]
887        total: u64,
888    },
889    /// Reports a presentation-neutral log code and template parameters.
890    Log {
891        job: JobId,
892        code: String,
893        params: BTreeMap<String, String>,
894    },
895    /// Reports a terminal status and an ephemeral operation result or error.
896    /// Calibration and MIMO carry only a marker; read `results` for their summary.
897    Finished {
898        job: JobId,
899        status: JobStatus,
900        result: Option<Box<JobResult>>,
901        error: Option<IpcError>,
902    },
903    /// Reports a live input level at the monitor cadence.
904    Meter {
905        job: JobId,
906        rms_dbfs: f64,
907        peak_dbfs: f64,
908        spl_db: Option<f64>,
909        clipping: bool,
910    },
911    /// Reports a changed measurement-method status snapshot.
912    MeasureStatus {
913        job: JobId,
914        status: MeasureMethodsResult,
915    },
916}
917/// The successful result produced by a job.
918#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
919#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
920#[ts(export)]
921pub enum JobResult {
922    /// Calibration committed; fetch the current screen summary with `results`.
923    Calibration,
924    /// Crossover recommendation computed for the current project.
925    Xo(XoRecommendation),
926    /// MIMO committed; fetch the current screen summary with `results`.
927    Mimo,
928    /// Paths written by an exporter.
929    Export(ExportResult),
930    /// A design applied to CamillaDSP or Equalizer APO.
931    Apply(ApplyResult),
932    /// Project state after a completed measurement.
933    Measurement(ProjectInfo),
934    /// Project state after an mdat import.
935    MdatImport(ProjectInfo),
936    /// A long-lived monitor or watcher stopped without a payload.
937    Stopped,
938}
939/// The job to cancel.
940#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
941#[ts(export)]
942pub struct JobCancelParams {
943    /// Identifier of the job to cancel.
944    pub job: JobId,
945}
946
947/// The latest calibration and MIMO results.
948#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
949#[ts(export)]
950pub struct ResultsSummary {
951    /// Latest calibration result, including stale state.
952    pub calibration: Option<CalibrationSummary>,
953    /// Latest MIMO result, including stale state.
954    pub mimo: Option<MimoSummary>,
955    /// Whether the MIMO result goes into an export, and if not, why.
956    pub mimo_export: MimoExportDecision,
957}
958/// Whether the MIMO result goes into an export, and if not, why.
959#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
960#[serde(rename_all = "snake_case")]
961#[ts(export)]
962pub enum MimoExportDecision {
963    /// The current MIMO support paths go into the export.
964    Included,
965    /// MIMO is switched off in the current settings.
966    Off,
967    /// No MIMO result has been computed or restored.
968    None,
969    /// The MIMO result does not match the current inputs or compute environment.
970    Stale,
971    /// MIMO needs at least three measured positions.
972    TooFewPositions,
973    /// The current MIMO result contains no support paths.
974    NoSupports,
975}
976/// A presentation-neutral note or warning code with template parameters.
977#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
978#[ts(export)]
979pub struct Notice {
980    /// Stable snake_case code derived from the engine note variant.
981    pub code: String,
982    /// String values for the screen message template.
983    pub params: BTreeMap<String, String>,
984}
985
986/// The screen-facing summary of one calibration design.
987#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
988#[ts(export)]
989pub struct CalibrationSummary {
990    /// Whether this result was restored from the project cache instead of computed in this
991    /// session; staged and before/after mdat need a live design.
992    #[serde(default)]
993    pub cached: bool,
994    /// Whether current project inputs differ from this result.
995    pub stale: bool,
996    /// Which input first differs: measurements, settings or target.
997    pub stale_reason: Option<String>,
998    /// Calibration mode used to compute this result.
999    pub mode: Mode,
1000    /// Ultra-mode latency limit used to compute this result, in milliseconds.
1001    pub ultra_ms: f64,
1002    /// Values for the upstream verdict card.
1003    pub verdict: VerdictSummary,
1004    /// Per-channel before-and-after metrics.
1005    pub scorecard: Vec<ChannelScore>,
1006    /// Late 20–150 Hz energy before and after correction at every measured position.
1007    #[serde(default)]
1008    pub late_energy: Vec<LateEnergy>,
1009    /// Total filter latency in milliseconds.
1010    pub latency_ms: f64,
1011    /// Informational codes and values translated by the screen.
1012    pub notes: Vec<Notice>,
1013    /// Main-channel roles in design order; filter coefficients stay in Rust.
1014    pub main_roles: Vec<String>,
1015    /// Subwoofer roles in design order; filter coefficients stay in Rust.
1016    pub sub_roles: Vec<String>,
1017}
1018/// Values shown by the upstream `step_calibration.py` verdict card.
1019#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1020#[ts(export)]
1021pub struct VerdictSummary {
1022    /// Share within ±1 dB before correction, in percent.
1023    pub within_1db_before_pct: f64,
1024    /// Share within ±1 dB after correction, in percent.
1025    pub within_1db_after_pct: f64,
1026    /// Low-band group-delay spread before correction, in milliseconds.
1027    pub low_band_time_spread_before_ms: f64,
1028    /// Low-band group-delay spread after correction, in milliseconds.
1029    pub low_band_time_spread_after_ms: f64,
1030    /// Delay added by correction, in milliseconds.
1031    pub added_delay_ms: f64,
1032    /// Across-position level spread after correction, in dB.
1033    pub position_spread_db: f64,
1034}
1035/// Before-and-after scorecard metrics for one speaker role.
1036#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1037#[ts(export)]
1038pub struct ChannelScore {
1039    /// Internal speaker role.
1040    pub role: String,
1041    /// Metrics before correction.
1042    pub before: MetricsSummary,
1043    /// Metrics after correction.
1044    pub after: MetricsSummary,
1045}
1046/// Late 20–150 Hz energy for one main role at one measured position.
1047#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1048#[ts(export)]
1049pub struct LateEnergy {
1050    /// Internal speaker role.
1051    pub role: String,
1052    /// Measured position number.
1053    pub position: u32,
1054    /// Late-to-early energy before correction, in dB.
1055    pub before_db: f64,
1056    /// Late-to-early energy after correction, in dB.
1057    pub after_db: f64,
1058}
1059/// The upstream `Metrics` fields for one response.
1060#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, TS)]
1061#[ts(export)]
1062pub struct MetricsSummary {
1063    /// Reference level from upstream `Metrics.level`, in dB.
1064    pub level: f64,
1065    /// Share within ±1 dB from 20 Hz, in percent.
1066    pub in1_20: f64,
1067    /// Share within ±2 dB from 20 Hz, in percent.
1068    pub in2_20: f64,
1069    /// Share within ±1 dB from 200 Hz, in percent.
1070    pub in1_200: f64,
1071    /// Share within ±2 dB from 200 Hz, in percent.
1072    pub in2_200: f64,
1073    /// Share within ±1 dB from 2,000 Hz, in percent.
1074    pub in1_2000: f64,
1075    /// Share within ±2 dB from 2,000 Hz, in percent.
1076    pub in2_2000: f64,
1077    /// Phase rotation from upstream `Metrics.turns`, in turns.
1078    pub turns: f64,
1079    /// Group-delay spread near 20 Hz, in milliseconds.
1080    pub gd_20: f64,
1081    /// Group-delay spread near 100 Hz, in milliseconds.
1082    pub gd_100: f64,
1083    /// Group-delay spread near 500 Hz, in milliseconds.
1084    pub gd_500: f64,
1085    /// Largest absolute 1/24-octave group-delay peak, in milliseconds.
1086    pub gd24_abs: f64,
1087    /// Frequency of the largest absolute group-delay peak, in Hz.
1088    pub gd24_abs_hz: f64,
1089    /// Largest signed 1/24-octave group-delay peak, in milliseconds.
1090    pub gd24_max: f64,
1091    /// Span of 1/24-octave group delay, in milliseconds.
1092    pub gd24_span: f64,
1093    /// Group-delay spread near 30 Hz, in milliseconds.
1094    pub gd_30: f64,
1095    /// Pre-ringing level from upstream `Metrics.pre_db`, in dB.
1096    pub pre_db: f64,
1097}
1098/// The screen-facing summary of one MIMO run.
1099#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1100#[ts(export)]
1101pub struct MimoSummary {
1102    /// Actual computation backend, retained with the result.
1103    #[serde(default)]
1104    pub compute: ComputeReport,
1105    /// Whether current calibration inputs differ from this MIMO result.
1106    pub stale: bool,
1107    /// Realization kind, `iir` or `fir`.
1108    pub kind: String,
1109    /// Upper support frequency in Hz.
1110    pub fsiso: f64,
1111    /// Number of generated support paths.
1112    pub support_count: usize,
1113    /// Total FIR-tap budget across all generated paths.
1114    pub fir_taps_total: usize,
1115    /// Informational codes and values translated by the screen.
1116    pub notes: Vec<Notice>,
1117    /// Number of distinct input roles with at least one generated support path.
1118    pub input_count: usize,
1119    /// Per-channel metrics after MIMO at the main position.
1120    #[serde(default)]
1121    pub scorecard: Vec<MimoChannelScore>,
1122    /// Late 20–150 Hz energy after MIMO at every measured position.
1123    #[serde(default)]
1124    pub late_energy: Vec<MimoLateEnergy>,
1125}
1126/// Scorecard metrics after MIMO for one main role.
1127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1128#[ts(export)]
1129pub struct MimoChannelScore {
1130    /// Internal speaker role.
1131    pub role: String,
1132    /// Metrics after the MIMO support contributions are added.
1133    pub after_mimo: MetricsSummary,
1134}
1135/// Late 20–150 Hz energy after MIMO for one main role and measured position.
1136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1137#[ts(export)]
1138pub struct MimoLateEnergy {
1139    /// Internal speaker role.
1140    pub role: String,
1141    /// Measured position number.
1142    pub position: u32,
1143    /// Late-to-early energy after MIMO, in dB.
1144    pub after_mimo_db: f64,
1145}
1146/// Crossover candidates and the best value for each group.
1147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1148#[ts(export)]
1149pub struct XoRecommendation {
1150    /// Best crossover frequency in Hz for each speaker group.
1151    pub best: Vec<(String, f64)>,
1152    /// Evaluated crossover frequencies in Hz.
1153    pub candidates: Vec<f64>,
1154}
1155
1156/// The supported export bundle.
1157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1158#[serde(rename_all = "snake_case")]
1159#[ts(export)]
1160pub enum ExportKind {
1161    /// CamillaDSP configuration and FIR coefficient WAV files.
1162    CamillaDsp,
1163    /// REW mdat containing the upstream staged responses.
1164    StagedMdat,
1165    /// REW mdat containing the original measurements, the correction results
1166    /// or both ([`ExportParams::mdat_scope`]; upstream's before-and-after mdat).
1167    BeforeAfterMdat,
1168    /// Per-channel filter values as text plus FIR WAV files (upstream
1169    /// `calibration.export_filter_text`). Results with RC only.
1170    FilterText,
1171    /// A complete Equalizer APO configuration: `config.txt`, the filter file
1172    /// and FIR WAV files (upstream `calibration.export_eqapo`). RC, BA and
1173    /// MIMO support paths, which upstream refuses (docs/DEVIATIONS.md F12).
1174    EqualizerApo,
1175    /// One complete 48 kHz impulse response and response CSV per reachable
1176    /// input-to-output path, including RC, BA, LFE and MIMO operations.
1177    Convolution,
1178}
1179
1180/// Why a filter export cannot carry the current result (upstream
1181/// `calibration.filter_export_block`).
1182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1183#[serde(rename_all = "snake_case")]
1184#[ts(export)]
1185pub enum ExportBlock {
1186    /// The result has subs (BA); filter text carries RC only.
1187    Subs,
1188    /// The result includes MIMO; filter text does not carry it.
1189    Mimo,
1190    /// The result was restored from the cache and is stale; mdat needs the measurements' spectra,
1191    /// so it must be recalibrated first.
1192    StaleCache,
1193}
1194
1195impl ExportBlock {
1196    /// The `params.reason` value of an [`ErrorCode::ExportScope`] error.
1197    pub fn name(self) -> &'static str {
1198        match self {
1199            Self::Subs => "subs",
1200            Self::Mimo => "mimo",
1201            Self::StaleCache => "stale_cache",
1202        }
1203    }
1204}
1205
1206/// Upstream `filter_export_block`: whether `kind` can carry a result with
1207/// subs (`has_subs`) and MIMO support paths (`has_mimo`). CamillaDSP, the
1208/// mdat exports, Equalizer APO and convolution carry everything; filter text
1209/// carries RC only (subs are checked first, as upstream). Upstream stops MIMO at
1210/// Equalizer APO as well; its writer already routes the support paths, and
1211/// `secs-filters/tests/eqapo_playback.rs` plays them through the engine
1212/// (DEVIATIONS F12). The screen mirrors this rule (`screens/export/types.ts`).
1213pub fn filter_export_block(
1214    kind: ExportKind,
1215    has_subs: bool,
1216    has_mimo: bool,
1217) -> Option<ExportBlock> {
1218    match kind {
1219        ExportKind::FilterText if has_subs => Some(ExportBlock::Subs),
1220        ExportKind::FilterText if has_mimo => Some(ExportBlock::Mimo),
1221        _ => None,
1222    }
1223}
1224
1225/// Whether an export format can carry the current result. Stale cached
1226/// calibration blocks mdat; filter formats retain [`filter_export_block`].
1227pub fn export_block(
1228    kind: ExportKind,
1229    has_subs: bool,
1230    has_mimo: bool,
1231    stale_cache: bool,
1232) -> Option<ExportBlock> {
1233    match kind {
1234        ExportKind::StagedMdat | ExportKind::BeforeAfterMdat if stale_cache => {
1235            Some(ExportBlock::StaleCache)
1236        }
1237        _ => filter_export_block(kind, has_subs, has_mimo),
1238    }
1239}
1240/// What the REW mdat export ([`ExportKind::BeforeAfterMdat`]) carries
1241/// (docs/DEVIATIONS.md F19). The file name says which: `원 측정` for the
1242/// measurements, `보정 결과` for the computed responses.
1243#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
1244#[serde(rename_all = "snake_case")]
1245#[ts(export)]
1246pub enum MdatScope {
1247    /// The main position's before-and-after responses (upstream's file) and
1248    /// the original measurements of the other positions.
1249    #[default]
1250    All,
1251    /// The original measurements of every position, as stored; needs no
1252    /// correction result.
1253    Measurements,
1254    /// The computed responses at the main position only.
1255    Results,
1256}
1257
1258/// Sample formats accepted by CamillaDSP's Wasapi backend.
1259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1260#[ts(export)]
1261pub enum CamillaWasapiFormat {
1262    /// Signed 32-bit integer.
1263    #[serde(rename = "S32")]
1264    S32,
1265    /// Signed 24-bit integer.
1266    #[serde(rename = "S24")]
1267    S24,
1268    /// Signed 16-bit integer.
1269    #[serde(rename = "S16")]
1270    S16,
1271    /// 32-bit floating point.
1272    #[serde(rename = "F32")]
1273    F32,
1274}
1275
1276impl CamillaWasapiFormat {
1277    /// CamillaDSP configuration spelling.
1278    pub fn name(self) -> &'static str {
1279        match self {
1280            Self::S32 => "S32",
1281            Self::S24 => "S24",
1282            Self::S16 => "S16",
1283            Self::F32 => "F32",
1284        }
1285    }
1286}
1287
1288/// CamillaDSP capture backend. Channel counts come from the export plan.
1289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1290#[serde(tag = "type", rename_all = "snake_case")]
1291#[ts(export)]
1292pub enum CamillaCaptureDevice {
1293    /// File placeholder compatible with the existing export.
1294    #[default]
1295    File,
1296    /// Windows Audio Session API capture.
1297    Wasapi {
1298        /// Endpoint name; absent selects the system default.
1299        device: Option<String>,
1300        /// Sample representation.
1301        format: CamillaWasapiFormat,
1302        /// Request exclusive access.
1303        exclusive: bool,
1304        /// Capture a playback endpoint in shared mode.
1305        loopback: bool,
1306    },
1307    /// ASIO capture. Requires CamillaDSP's ASIO build (`camilladsp-windows-asio`).
1308    Asio {
1309        /// ASIO driver name.
1310        device: String,
1311    },
1312}
1313
1314/// CamillaDSP playback backend. Channel counts come from the export plan.
1315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1316#[serde(tag = "type", rename_all = "snake_case")]
1317#[ts(export)]
1318pub enum CamillaPlaybackDevice {
1319    /// File placeholder compatible with the existing export.
1320    #[default]
1321    File,
1322    /// Windows Audio Session API playback.
1323    Wasapi {
1324        /// Endpoint name; absent selects the system default.
1325        device: Option<String>,
1326        /// Sample representation.
1327        format: CamillaWasapiFormat,
1328        /// Request exclusive access.
1329        exclusive: bool,
1330    },
1331    /// ASIO playback. Requires CamillaDSP's ASIO build (`camilladsp-windows-asio`).
1332    Asio {
1333        /// ASIO driver name.
1334        device: String,
1335    },
1336}
1337
1338/// Capture and playback backends for a CamillaDSP export.
1339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1340#[ts(export)]
1341pub struct CamillaDevices {
1342    /// Capture backend.
1343    pub capture: CamillaCaptureDevice,
1344    /// Playback backend.
1345    pub playback: CamillaPlaybackDevice,
1346}
1347
1348/// The export bundle and destination folder.
1349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1350#[ts(export)]
1351pub struct ExportParams {
1352    /// Export bundle to write.
1353    pub kind: ExportKind,
1354    /// Destination folder path. An empty path uses the project's export folder
1355    /// and applies the upstream timestamped subfolder convention.
1356    pub folder: String,
1357    /// What the REW mdat carries; absent means [`MdatScope::All`]. Only
1358    /// [`ExportKind::BeforeAfterMdat`] takes it; other kinds refuse it.
1359    #[serde(default, skip_serializing_if = "Option::is_none")]
1360    #[ts(optional)]
1361    pub mdat_scope: Option<MdatScope>,
1362    /// Optional stable export name. Named exports replace the matching bundle.
1363    #[serde(default, skip_serializing_if = "Option::is_none")]
1364    #[ts(optional)]
1365    pub design_name: Option<String>,
1366    /// Optional CamillaDSP Windows backends. File placeholders are the default.
1367    #[serde(default, skip_serializing_if = "Option::is_none")]
1368    #[ts(optional)]
1369    pub camilla_devices: Option<CamillaDevices>,
1370    /// Additional convolution output rates. Other export kinds refuse a non-empty list.
1371    #[serde(default)]
1372    #[ts(optional = nullable)]
1373    pub extra_rates: Vec<u32>,
1374}
1375/// Files written by a completed export job.
1376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1377#[ts(export)]
1378pub struct ExportResult {
1379    /// Paths of all files written by the exporter.
1380    pub files: Vec<String>,
1381    /// Sample rate of the files in Hz. REW mdat always reports 48000.
1382    pub output_rate: u32,
1383    /// Informational notices produced while resolving the output rate.
1384    #[serde(default)]
1385    pub notes: Vec<Notice>,
1386    /// Actual export folder written by the job.
1387    pub folder: String,
1388    /// Whether the exported calibration differed from current project inputs.
1389    pub stale: bool,
1390}
1391
1392// P28 host, live-monitor, watch and export-folder types. Keep this block
1393// contiguous so the parallel M5 IPC additions merge without interleaving.
1394/// Optional suggested name for a save dialog.
1395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1396#[ts(export)]
1397pub struct DialogSuggestedParams {
1398    /// Suggested file name.
1399    pub suggested: String,
1400}
1401/// Optional starting folder for a folder picker.
1402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1403#[ts(export)]
1404pub struct DialogPickFolderParams {
1405    /// Starting folder, when known.
1406    pub start: Option<String>,
1407}
1408/// The kind of file a host save dialog accepts.
1409#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1410#[serde(rename_all = "snake_case")]
1411#[ts(export)]
1412pub enum SaveFileKind {
1413    /// UTF-8 patch-sheet CSV.
1414    PatchSheet,
1415    /// Target-curve JSON.
1416    Target,
1417}
1418/// Typed save-file dialog parameters.
1419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1420#[ts(export)]
1421pub struct DialogSaveFileParams {
1422    /// File type and filter.
1423    pub kind: SaveFileKind,
1424    /// Suggested file name.
1425    pub suggested: String,
1426}
1427/// The kind of file a host open dialog accepts.
1428#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1429#[serde(rename_all = "snake_case")]
1430#[ts(export)]
1431pub enum OpenFileKind {
1432    /// Target-curve JSON.
1433    Target,
1434    /// Microphone response calibration text (`.txt`, `.cal` or `.frd`).
1435    MicCalibration,
1436    /// CamillaDSP executable selected for managed launching.
1437    CamillaExe,
1438}
1439/// Typed open-file dialog parameters.
1440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1441#[ts(export)]
1442pub struct DialogOpenFileParams {
1443    /// File type and filter.
1444    pub kind: OpenFileKind,
1445}
1446/// Folder to reveal in the operating-system file manager.
1447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1448#[ts(export)]
1449pub struct OpenFolderParams {
1450    /// Folder path.
1451    pub path: String,
1452}
1453/// Optional role to associate with a level-monitor start.
1454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1455#[ts(export)]
1456pub struct LevelMonitorStartParams {
1457    /// Speaker role initially selected for playback.
1458    pub role: Option<String>,
1459}
1460/// Role and output level for one 2.5-second level-check burst.
1461#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1462#[ts(export)]
1463pub struct LevelPlayParams {
1464    /// Internal speaker role.
1465    pub role: String,
1466    /// Master output level in dBFS.
1467    pub master_dbfs: f64,
1468}
1469/// Export base-folder update.
1470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1471#[ts(export)]
1472pub struct ExportFolderSetParams {
1473    /// Folder path to store in the open project.
1474    pub folder: String,
1475}
1476/// Current export base folder and whether it came from project storage.
1477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1478#[ts(export)]
1479pub struct ExportFolderResult {
1480    /// Export base folder.
1481    pub folder: String,
1482    /// True when `extra.export.folder` supplied this value.
1483    pub stored: bool,
1484}
1485
1486// Measurement request and response types.
1487/// A validated microphone calibration file selected for the open project.
1488#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1489#[ts(export)]
1490pub struct MicCalibrationFile {
1491    /// File name as selected by the user.
1492    pub name: String,
1493    /// Full selected path; the project retains the decoded text separately.
1494    pub path: String,
1495    /// miniDSP serial number from `SERNO`, when present.
1496    pub serial: Option<String>,
1497    /// USB microphone sensitivity factor in dB.
1498    pub sens_factor_db: Option<f64>,
1499    /// Analog input gain from `AGain`, in dB.
1500    pub analog_gain_db: Option<f64>,
1501    /// Analog microphone sensitivity at 94 dB SPL, in dBFS.
1502    pub sensitivity_dbfs: Option<f64>,
1503    /// Quoted metadata lines other than the sensitivity header.
1504    pub notes: Vec<String>,
1505    /// Number of frequency-response points.
1506    pub points: u32,
1507    /// Lowest tabulated frequency in Hz.
1508    pub min_hz: f64,
1509    /// Highest tabulated frequency in Hz.
1510    pub max_hz: f64,
1511    /// Whether any response point includes phase.
1512    pub has_phase: bool,
1513}
1514/// Selected microphone calibration and recorded-measurement attachment counts.
1515#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1516#[ts(export)]
1517pub struct MicCalibrationStatus {
1518    /// The project's selected file, or absent when none is selected.
1519    pub file: Option<MicCalibrationFile>,
1520    /// Recorded position-role pairs whose sidecar attaches a `meterCal` curve.
1521    pub attached: u32,
1522    /// Sweep measurements without a `meterCal` curve that can receive the file.
1523    pub attachable: u32,
1524}
1525/// A microphone calibration file selected by full path.
1526#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1527#[ts(export)]
1528pub struct MicCalibrationSetParams {
1529    /// Full path selected by the user.
1530    pub path: String,
1531}
1532
1533/// Measurement methods available on this host.
1534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1535#[ts(export)]
1536pub struct MeasureMethodsResult {
1537    /// Stable identifiers for available measurement methods.
1538    pub methods: Vec<String>,
1539    /// Native endpoints with their channel counts.
1540    pub native_devices: Vec<MeasureDevice>,
1541    /// Native enumeration failure, without hiding the other methods.
1542    pub native_error: Option<IpcError>,
1543    /// REW process, API, and license status.
1544    pub rew: RewStatusResult,
1545    /// Positions present in the unmodified sample room.
1546    pub mock_positions: Vec<i64>,
1547    /// Roles present in the unmodified sample room.
1548    pub mock_roles: Vec<String>,
1549}
1550/// An enumerated native audio endpoint.
1551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1552#[ts(export)]
1553pub struct MeasureDevice {
1554    /// Backend endpoint identifier.
1555    pub id: String,
1556    /// Backend endpoint display name.
1557    pub name: String,
1558    /// Maximum number of input channels.
1559    pub input_channels: usize,
1560    /// Maximum number of output channels.
1561    pub output_channels: usize,
1562}
1563/// Measurement method and device settings.
1564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
1565#[ts(export)]
1566pub struct MeasureConfigureParams {
1567    /// Measurement method identifier.
1568    pub method: String,
1569    /// Optional input device identifier.
1570    pub input_device: Option<String>,
1571    /// Optional output device identifier.
1572    pub output_device: Option<String>,
1573    /// Optional one-based input channel number.
1574    pub input_channel: Option<u16>,
1575    /// One-based output channels in measurement-plan role order; empty preserves wiring.
1576    #[serde(default)]
1577    pub output_channels: Vec<u16>,
1578    /// Optional sweep length in samples.
1579    pub sweep_length: Option<usize>,
1580    /// Optional sweep repetition count; allowed values are 1, 2, 4 and 8.
1581    pub repetitions: Option<u8>,
1582    /// Optional sweep level in dBFS.
1583    pub sweep_level_dbfs: Option<f64>,
1584    /// Optional timing-reference mode.
1585    pub timing: Option<String>,
1586    /// Optional one-based loopback input channel.
1587    pub loopback_input_channel: Option<u16>,
1588    /// Optional acoustic timing-reference output channel.
1589    pub timing_ref_output: Option<u16>,
1590    /// Optional share preference: auto, exclusive, or shared.
1591    pub share_mode: Option<String>,
1592}
1593/// The active measurement settings.
1594#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
1595#[ts(export)]
1596pub struct MeasureConfigResult {
1597    /// Active measurement method identifier.
1598    pub method: String,
1599    /// Active sweep level in dBFS.
1600    pub sweep_level_dbfs: Option<f64>,
1601    /// Active input endpoint identifier; empty selects the default.
1602    pub input_device: String,
1603    /// Active output endpoint identifier; empty selects the default.
1604    pub output_device: String,
1605    /// One-based microphone input channel.
1606    pub input_channel: u16,
1607    /// Roles in the same order as output_channels.
1608    pub roles: Vec<String>,
1609    /// One-based output channels in measurement-plan role order.
1610    pub output_channels: Vec<u16>,
1611    /// Sweep length in samples.
1612    pub sweep_length: usize,
1613    /// Active repetition count after timing-reference normalization.
1614    pub repetitions: u8,
1615    /// Sweep sample rate in Hz.
1616    pub sample_rate: u32,
1617    /// Supported sweep lengths in samples.
1618    pub sweep_lengths: Vec<usize>,
1619    /// Timing-reference mode.
1620    pub timing: String,
1621    /// Optional one-based loopback input channel.
1622    pub loopback_input_channel: Option<u16>,
1623    /// Optional acoustic timing-reference output channel.
1624    pub timing_ref_output: Option<u16>,
1625    /// Share preference: auto, exclusive, or shared.
1626    pub share_mode: String,
1627}
1628/// Recording status and upstream source values for one arranged position.
1629#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1630#[ts(export)]
1631pub struct PositionRecord {
1632    /// One-based arranged position number.
1633    #[ts(type = "number")]
1634    pub position: i64,
1635    /// Whether every planned role is recorded at this position.
1636    pub done: bool,
1637    /// Planned roles that have a recording at this position.
1638    pub roles: Vec<String>,
1639    /// Source file/value by recorded role; native and REW sweeps use `measured`.
1640    pub sources: BTreeMap<String, String>,
1641}
1642/// Recording status for the complete measurement arrangement.
1643#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1644#[ts(export)]
1645pub struct MeasurementRecordsResult {
1646    /// One entry per arranged position.
1647    pub records: Vec<PositionRecord>,
1648}
1649/// Stored level-check readings, keyed by measurement role (upstream `extra['levels']`).
1650#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1651#[ts(export)]
1652pub struct LevelsResult {
1653    /// Measured input level in dBFS by role: the meter reading while that channel played.
1654    pub levels: BTreeMap<String, f64>,
1655    /// Measured sound-pressure level in dB SPL by role.
1656    pub spl: BTreeMap<String, f64>,
1657}
1658/// The measurement position to record.
1659#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1660#[ts(export)]
1661pub struct MeasurePositionParams {
1662    /// One-based measurement position number.
1663    pub position: i64,
1664}
1665/// The measurement position to clear.
1666#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1667#[ts(export)]
1668pub struct PositionClearParams {
1669    /// One-based measurement position number.
1670    pub position: i64,
1671}
1672/// A service-issued undo token.
1673#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1674#[ts(export)]
1675pub struct UndoParams {
1676    /// Opaque service-issued undo token.
1677    pub token: String,
1678}
1679/// An undo token and the project state it restores.
1680#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1681#[ts(export)]
1682pub struct UndoResult {
1683    /// Opaque token for the inverse action.
1684    pub token: String,
1685    /// Project state after the action.
1686    pub project: ProjectInfo,
1687}
1688/// Whether REW is running, reachable, and licensed as Pro.
1689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1690#[ts(export)]
1691pub struct RewStatusResult {
1692    /// Whether an REW process is running.
1693    pub running: bool,
1694    /// Whether the REW API responds.
1695    pub reachable: bool,
1696    /// Observed automatic-sweep license state: unknown until success or rejection.
1697    pub pro: Option<bool>,
1698}
1699/// A requested REW audio driver.
1700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1701#[ts(export)]
1702pub struct RewDriverSetParams {
1703    /// Driver name as REW reports it (`ASIO` or `Java`).
1704    pub driver: String,
1705}
1706/// Facts from REW, distinct from project-stored measurement preferences.
1707#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
1708#[ts(export)]
1709pub struct RewInfoResult {
1710    pub executable_path: Option<String>,
1711    pub running: bool,
1712    pub reachable: bool,
1713    pub version: Option<String>,
1714    /// REW's current audio driver, or null when it could not be read.
1715    pub driver: Option<String>,
1716    /// Observed automatic-sweep license state: unknown until success or rejection.
1717    pub pro: Option<bool>,
1718    /// REW's current input device, read from its own audio preferences.
1719    pub input_device: Option<String>,
1720    /// REW's current output device, read from its own audio preferences.
1721    pub output_device: Option<String>,
1722    /// REW's current input channel name.
1723    pub input: Option<String>,
1724    pub blocking: Option<bool>,
1725    /// Empty means no file loaded; null means unknown or per-input calibration.
1726    pub input_calibration_file: Option<String>,
1727    pub input_devices: Option<Vec<RewDeviceInfo>>,
1728    pub output_devices: Option<Vec<RewDeviceInfo>>,
1729    pub input_channel_names: Option<Vec<String>>,
1730    pub sample_rate: Option<f64>,
1731    pub sweep: Option<RewSweepInfo>,
1732    pub level_dbfs: Option<f64>,
1733}
1734
1735#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1736#[ts(export)]
1737pub struct RewDeviceInfo {
1738    pub id: String,
1739    pub name: String,
1740    pub channels: usize,
1741}
1742
1743#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1744#[ts(export)]
1745pub struct RewSweepInfo {
1746    pub start_hz: Option<f64>,
1747    pub end_hz: Option<f64>,
1748    pub length: Option<String>,
1749}
1750
1751/// The REW mdat file to scan.
1752#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1753#[ts(export)]
1754pub struct MdatScanParams {
1755    /// Source `.mdat` file path.
1756    pub path: String,
1757}
1758/// Measurements, suggested assignments, and problem codes found in an mdat file.
1759#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1760#[ts(export)]
1761pub struct MdatScanResult {
1762    /// Measurements in file order, including assignment guesses.
1763    pub measurements: Vec<MdatScanMeasurement>,
1764    /// Suggested measurement-to-role assignments.
1765    pub suggestions: Vec<MdatAssignment>,
1766    /// Stable problem codes and template parameters.
1767    pub problems: Vec<Notice>,
1768}
1769/// One scanned measurement with an unambiguous row identifier.
1770#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1771#[ts(export)]
1772pub struct MdatScanMeasurement {
1773    /// Zero-based row index, also usable when titles repeat.
1774    pub index: usize,
1775    /// Original measurement title.
1776    pub title: String,
1777    /// Suggested internal role, or no assignment.
1778    pub role: Option<String>,
1779    /// Guessed one-based measurement position.
1780    pub position: i64,
1781    /// Whether the title explicitly names a position, including position 1.
1782    pub position_tagged: bool,
1783    /// Low-band classifier result, absent when no IR is available.
1784    pub looks_like_sub: Option<bool>,
1785}
1786/// An mdat measurement assigned to a speaker role.
1787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1788#[ts(export)]
1789pub struct MdatAssignment {
1790    /// Measurement title from the mdat file.
1791    pub measurement: String,
1792    /// Internal speaker role.
1793    pub role: String,
1794    /// Optional zero-based row index to disambiguate repeated titles.
1795    pub index: Option<usize>,
1796    /// Optional per-row position overriding the import destination.
1797    pub position: Option<i64>,
1798}
1799/// An mdat file, its assignments, and destination position.
1800#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1801#[ts(export)]
1802pub struct MdatImportParams {
1803    /// Source `.mdat` file path.
1804    pub path: String,
1805    /// Accepted measurement-to-role assignments.
1806    pub assignments: Vec<MdatAssignment>,
1807    /// Destination measurement position number.
1808    pub position: i64,
1809}
1810
1811// Project editing request and response types.
1812/// A filesystem path used by a project editor.
1813#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1814#[ts(export)]
1815pub struct PathParams {
1816    /// File system path chosen by the user.
1817    pub path: String,
1818}
1819/// A path written by an editor.
1820#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1821#[ts(export)]
1822pub struct PathResult {
1823    /// File system path that was written.
1824    pub path: String,
1825}
1826/// A preset change which may need measurement-clearing confirmation.
1827#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1828#[ts(export)]
1829pub struct ConfigPresetParams {
1830    /// Preset name (`2.0`, `2.1`, `2.2`, `4.0`, `4.1`, `4.2`, `5.1`, `7.1`, `7.1.4`, `9.1.6`, or `custom` for an empty layout).
1831    pub preset: String,
1832    /// Clears existing measurements; without it a project with measurements answers `confirm_required` (U5).
1833    #[serde(default)]
1834    pub confirm: bool,
1835}
1836/// A sub-count change which may need measurement-clearing confirmation.
1837#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1838#[ts(export)]
1839pub struct ConfigSubCountParams {
1840    /// Number of subwoofers, 0 to 8.
1841    pub count: i64,
1842    /// Clears existing measurements; without it a project with measurements answers `confirm_required` (U5).
1843    #[serde(default)]
1844    pub confirm: bool,
1845}
1846/// One speaker index.
1847#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1848#[ts(export)]
1849pub struct ConfigIndexParams {
1850    /// Speaker index in `ConfigResult.speakers`.
1851    pub index: usize,
1852}
1853/// Optional edits to one speaker or one of its bands.
1854#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1855#[ts(export)]
1856pub struct ConfigSetSpeakerParams {
1857    /// Speaker index in `ConfigResult.speakers`.
1858    pub index: usize,
1859    /// New role code; a sub role forces a single SUB band (`step2_config.py:988-1002`).
1860    pub role: Option<String>,
1861    /// New size: `small` (crossover applied) or `large` (full range).
1862    pub size: Option<String>,
1863    /// Band the output and polarity edits apply to; absent means the first band.
1864    pub band_index: Option<usize>,
1865    /// New measurement output channel (1-based).
1866    pub out: Option<i64>,
1867    /// New polarity: `+` normal or `-` inverted.
1868    pub pol: Option<String>,
1869}
1870/// One configured output band.
1871#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1872#[ts(export)]
1873pub struct ConfigBand {
1874    /// Band name: `FULL`, `LF`, `MF`, `HF` or `SUB`.
1875    pub band: String,
1876    /// Measurement output channel (1-based).
1877    pub out: i64,
1878    /// Polarity: `+` normal or `-` inverted.
1879    pub pol: String,
1880}
1881/// One configured speaker.
1882#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1883#[ts(export)]
1884pub struct ConfigSpeaker {
1885    /// Role code (`L`, `C`, `SW1`, `Tfl`, ...).
1886    pub role: String,
1887    /// `small` (crossover applied) or `large` (full range); subs are `sub`.
1888    pub size: String,
1889    /// Bands in order; one band unless bi-amped.
1890    pub bands: Vec<ConfigBand>,
1891}
1892/// One upstream speaker group in upstream order.
1893#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1894#[ts(export)]
1895pub struct ConfigGroup {
1896    /// Stable presentation group id: front, center, sub, surround, back, wide, top, height or other.
1897    pub id: String,
1898    /// Upstream group name used as the project and crossover-map key.
1899    pub key: String,
1900    /// Indices into `ConfigResult.speakers` that belong to this group.
1901    pub speaker_indices: Vec<usize>,
1902}
1903/// Values derived from the complete speaker configuration.
1904#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1905#[ts(export)]
1906pub struct ConfigSummary {
1907    /// Number of speakers.
1908    pub speaker_count: usize,
1909    /// Number of measurement outputs in use.
1910    pub outputs: Vec<i64>,
1911    /// Highest output channel in use.
1912    pub max_channel: i64,
1913    /// Number of output channels used twice or more.
1914    pub duplicate_outputs: Vec<i64>,
1915    /// Number of subwoofers.
1916    pub sub_count: usize,
1917}
1918/// Screen-facing editable speaker configuration.
1919#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1920#[ts(export)]
1921pub struct ConfigResult {
1922    /// Speakers in table order.
1923    pub speakers: Vec<ConfigSpeaker>,
1924    /// Preset name, or `custom` after any manual edit.
1925    pub preset: String,
1926    /// Measurement output device name (`config.device`).
1927    pub device: String,
1928    /// Table group header rows in upstream order.
1929    pub groups: Vec<ConfigGroup>,
1930    /// Counts the summary line shows.
1931    pub summary: ConfigSummary,
1932}
1933/// Configuration after a destructive edit and its optional inverse token.
1934#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1935#[ts(export)]
1936pub struct ConfigMutationResult {
1937    /// The configuration after the edit.
1938    pub config: ConfigResult,
1939    /// Token for `undo` when the edit cleared measurements (U5).
1940    pub undo_token: Option<String>,
1941}
1942/// Output sample-rate selection stored with a DSP profile.
1943#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1944#[serde(rename_all = "snake_case")]
1945#[ts(export)]
1946pub enum OutputRateChoice {
1947    /// Resolve the rate from the default output endpoint when exporting.
1948    Auto,
1949    /// Always export at this rate in Hz.
1950    Fixed(u32),
1951}
1952
1953/// The manually editable DSP profile fields.
1954#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1955#[ts(export)]
1956pub struct DspFields {
1957    /// Output sample rate.
1958    pub output_rate: OutputRateChoice,
1959    /// Whether this profile permits changing the output rate.
1960    pub output_rate_editable: bool,
1961    /// FIR taps per channel.
1962    pub fir_taps: i64,
1963    /// Total FIR taps the DSP can run across channels.
1964    pub fir_budget: i64,
1965    /// IIR (PEQ) bands per channel.
1966    pub iir_bands: i64,
1967    /// All-pass bands per channel.
1968    pub allpass_bands: i64,
1969    /// Bass-management path delays the DSP offers.
1970    pub bm_path_delays: i64,
1971}
1972/// One DSP preset in the built-in table.
1973#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1974#[ts(export)]
1975pub struct DspPreset {
1976    /// Preset key (`camilla`, `tesira`, `qsys`, `minidsp`, `custom`).
1977    pub key: String,
1978    /// Device name shown in the device list.
1979    pub name: String,
1980    /// The preset's filter resources.
1981    pub fields: DspFields,
1982}
1983/// A preset selection or a complete manual edit.
1984#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1985#[ts(export)]
1986pub struct DspSetParams {
1987    /// Selects a preset; absent with `fields` set means a manual edit.
1988    pub preset: Option<String>,
1989    /// Manual values; any manual edit makes the profile `custom`.
1990    pub fields: Option<DspFields>,
1991}
1992/// Editable DSP state and derived resolution values.
1993#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1994#[ts(export)]
1995pub struct DspResult {
1996    /// Current profile key.
1997    pub profile: String,
1998    /// Current device name.
1999    pub name: String,
2000    /// Current filter resources.
2001    pub fields: DspFields,
2002    /// Every preset, for the device list.
2003    pub presets: Vec<DspPreset>,
2004    /// FIR window length the taps give, in ms (note line).
2005    pub window_ms: f64,
2006    /// Frequency resolution the taps give, in Hz (note line).
2007    pub resolution_hz: f64,
2008}
2009/// Measurement timing-reference settings stored under `extra.devices`.
2010#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2011#[ts(export)]
2012pub struct LayoutTiming {
2013    /// Timing reference: `acoustic`, `loopback` or `none`.
2014    pub reference: String,
2015    /// Reference speaker role for the acoustic reference (default `R`).
2016    pub role: String,
2017    /// Reference output channel for the loopback reference (1-based).
2018    pub output: i64,
2019}
2020/// One measurement microphone point in centimetres.
2021#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2022#[ts(export)]
2023pub struct LayoutPoint {
2024    /// Position number; 1 is the listening position.
2025    pub position: usize,
2026    /// Left-right offset from the centre seat, in cm (`layout_map.py`).
2027    pub x_cm: f64,
2028    /// Front-back offset from the centre seat, in cm (`layout_map.py`).
2029    pub y_cm: f64,
2030    /// Height step: 0 at ear height, +1 or -1 for ±20 cm.
2031    pub height: i8,
2032}
2033/// One supported measurement arrangement.
2034#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2035#[ts(export)]
2036pub struct LayoutArrangement {
2037    /// Number of positions: 1, 9, 13 or 17.
2038    pub count: usize,
2039    /// Microphone points of this arrangement.
2040    pub points: Vec<LayoutPoint>,
2041}
2042/// Editable arrangement and measurement options.
2043#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2044#[ts(export)]
2045pub struct LayoutResult {
2046    /// Chosen arrangement (`extra['array']`).
2047    pub count: usize,
2048    /// Start delay before each position as stored (`extra['start_delay']`):
2049    /// one of [`crate::rules::START_DELAYS`], `끄기` or `5 초` to `15 초`.
2050    pub start_delay: String,
2051    /// Timing reference and its reference speaker or output.
2052    pub timing: LayoutTiming,
2053    /// Every arrangement with its geometry, for the cards and maps.
2054    pub arrangements: Vec<LayoutArrangement>,
2055}
2056/// Optional arrangement and timing edits.
2057#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
2058#[ts(export)]
2059pub struct LayoutSetParams {
2060    /// New arrangement; changing it clears measurements (U5).
2061    pub count: Option<usize>,
2062    /// New start delay, one of [`crate::rules::START_DELAYS`]; any other
2063    /// value answers `invalid_request` (field `start_delay`).
2064    pub start_delay: Option<String>,
2065    /// New timing reference.
2066    pub timing: Option<LayoutTiming>,
2067    /// Clears existing measurements; without it a project with measurements answers `confirm_required` (U5).
2068    #[serde(default)]
2069    pub confirm: bool,
2070}
2071/// Arrangement after editing and its optional inverse token.
2072#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2073#[ts(export)]
2074pub struct LayoutMutationResult {
2075    /// The arrangement and options after the edit.
2076    pub layout: LayoutResult,
2077    /// Token for `undo` when the edit cleared measurements (U5).
2078    pub undo_token: Option<String>,
2079}
2080/// Position-duration estimate from upstream step 4 semantics.
2081#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2082#[ts(export)]
2083pub struct PositionEstimateResult {
2084    /// Number of positions in the arrangement.
2085    pub positions: usize,
2086    /// Estimated time per position, in seconds (`step4_layout.position_seconds`).
2087    pub per_position_seconds: f64,
2088    /// Estimated time for every position, without moving the microphone, in seconds.
2089    pub total_seconds: f64,
2090}
2091/// Target control points.
2092#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2093#[ts(export)]
2094pub struct TargetPointsParams {
2095    /// Target points as (Hz, dB); clamped to 10–24000 Hz and ±30 dB, at least two.
2096    pub points: Vec<(f64, f64)>,
2097}
2098/// One built-in target preset selection.
2099#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2100#[ts(export)]
2101pub struct TargetPresetParams {
2102    /// Target preset: `flat`, `harman` or `cinema`.
2103    pub preset: String,
2104}
2105/// Target curve, selected preset, and built-in preset table.
2106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2107#[ts(export)]
2108pub struct TargetResult {
2109    /// Target points as (Hz, dB).
2110    pub points: Vec<(f64, f64)>,
2111    /// Preset name, or absent when the points were edited (custom).
2112    pub preset: Option<String>,
2113    /// Preset names the target menu offers.
2114    pub presets: Vec<(String, Vec<(f64, f64)>)>,
2115}
2116/// Complete calibration settings replacement.
2117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2118#[ts(export)]
2119pub struct CalibrationSettingsParams {
2120    /// The complete calibration settings to store.
2121    pub settings: CalibrationSettings,
2122}
2123/// Mode whose numeric specification should be returned.
2124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2125#[ts(export)]
2126pub struct ModeSpecParams {
2127    /// Correction mode: `zero`, `mixed`, `linear` or `ultra`.
2128    pub mode: Mode,
2129    /// Latency limit for the ultra mode, in ms.
2130    pub ultra_ms: Option<f64>,
2131    /// Whether MIMO support paths use FIR instead of IIR.
2132    #[serde(default, skip_serializing_if = "Option::is_none")]
2133    #[ts(optional)]
2134    pub mimo_fir: Option<bool>,
2135}
2136/// Presentation-neutral values underlying the upstream mode specification line.
2137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2138#[ts(export)]
2139pub struct ModeSpecResult {
2140    /// Room-correction filters: `iir` (IIR only) or `iir_fir` (IIR plus FIR).
2141    pub rc_filter: String,
2142    /// FIR taps the mode uses (0 without FIR).
2143    pub fir_taps: usize,
2144    /// PEQ bands per channel, when the mode uses IIR.
2145    pub peq_bands: Option<usize>,
2146    /// MIMO support filter kind (`fir` when requested, otherwise `iir`).
2147    pub mimo_filter: String,
2148    /// Delay the FIR may add ahead of the main arrival (`lift_ms`), in ms.
2149    pub lift_ms: f64,
2150    /// Octave fraction of the excess-phase smoothing (6, or 24 in linear and ultra modes).
2151    pub phase_frac: f64,
2152    /// Upper latency bound of the mode, in ms.
2153    pub max_latency_ms: f64,
2154}
2155/// Application theme preference.
2156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
2157#[serde(rename_all = "snake_case")]
2158#[ts(export)]
2159pub enum AppTheme {
2160    /// Follow the operating-system preference.
2161    #[default]
2162    System,
2163    /// Always use the light theme.
2164    Light,
2165    /// Always use the dark theme.
2166    Dark,
2167}
2168/// The skin set the screen draws with (docs/skins, ADR 0010).
2169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
2170#[serde(rename_all = "snake_case")]
2171#[ts(export)]
2172pub enum AppSkin {
2173    /// The instrument skin: the default tokens.
2174    #[default]
2175    Default,
2176    /// Hardware Rack: a 19-inch rack faceplate.
2177    Rack,
2178    /// Signal Matrix: a routing board of cells on a grid.
2179    Matrix,
2180}
2181/// The logo on the rail and the window (docs/DEVIATIONS.md U12). The
2182/// installed program's own icon is always `Expert`.
2183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
2184#[serde(rename_all = "snake_case")]
2185#[ts(export)]
2186pub enum AppIcon {
2187    /// "전문가 모드": upstream's confirmed logo (`logo_b8.svg`).
2188    #[default]
2189    Expert,
2190    /// "킹반인 모드": SECS 2R's own logo (`apps/secs2r/icons/logo/civilian.svg`),
2191    /// a measured response and its correction on the target.
2192    Civilian,
2193}
2194/// Opt-in loopback API settings.
2195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
2196#[ts(export)]
2197pub struct LocalApiSettings {
2198    /// Whether the app starts the local API server.
2199    pub enabled: bool,
2200    /// Loopback TCP port; zero asks the operating system for a free port.
2201    pub port: u16,
2202}
2203impl Default for LocalApiSettings {
2204    fn default() -> Self {
2205        Self {
2206            enabled: false,
2207            port: crate::local_api::DEFAULT_PORT,
2208        }
2209    }
2210}
2211
2212/// How far SECS 2R controls one application target.
2213#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2214#[serde(rename_all = "snake_case")]
2215#[ts(export)]
2216pub enum ApplyLevel {
2217    /// Only write ordinary export files.
2218    #[default]
2219    Files,
2220    /// Apply on explicit request while the app is running.
2221    Apply,
2222    /// Keep the applied design active through the managed service.
2223    Manage,
2224}
2225
2226/// CamillaDSP application settings.
2227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2228#[ts(export)]
2229pub struct CamillaApply {
2230    /// Selected control level.
2231    pub level: ApplyLevel,
2232    /// Plain websocket address in `host:port` form.
2233    pub address: String,
2234    /// CamillaDSP executable selected by the user for managed operation.
2235    #[serde(default, skip_serializing_if = "Option::is_none")]
2236    #[ts(optional)]
2237    pub exe_path: Option<String>,
2238}
2239impl Default for CamillaApply {
2240    fn default() -> Self {
2241        Self {
2242            level: ApplyLevel::Files,
2243            address: "127.0.0.1:1234".into(),
2244            exe_path: None,
2245        }
2246    }
2247}
2248
2249/// Equalizer APO application settings.
2250#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2251#[ts(export)]
2252pub struct EqApoApply {
2253    /// Selected control level.
2254    pub level: ApplyLevel,
2255}
2256
2257/// Settings for applying designs to external processors.
2258#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2259#[ts(export)]
2260pub struct ApplySettings {
2261    /// CamillaDSP settings.
2262    pub camilla: CamillaApply,
2263    /// Equalizer APO settings.
2264    pub eqapo: EqApoApply,
2265}
2266
2267/// Optional device choices for a CamillaDSP application export.
2268#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2269#[ts(export)]
2270pub struct ApplyCamillaParams {
2271    /// Capture and playback choices; absent keeps file placeholders.
2272    #[serde(default, skip_serializing_if = "Option::is_none")]
2273    #[ts(optional)]
2274    pub camilla_devices: Option<CamillaDevices>,
2275}
2276
2277/// Equalizer APO edition detected from its installed path.
2278#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
2279#[serde(rename_all = "snake_case")]
2280#[ts(export)]
2281pub enum EqApoVariant {
2282    /// The SourceForge Equalizer APO release.
2283    Official,
2284    /// EqualizerAPO-XT.
2285    Xt,
2286    /// An installation whose path does not identify either edition.
2287    Unknown,
2288}
2289
2290/// Equalizer APO installation values supplied by the operating-system adapter.
2291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2292pub struct EqApoInstall {
2293    /// Equalizer APO configuration directory.
2294    pub config_path: String,
2295    /// Equalizer APO installation directory.
2296    pub install_path: String,
2297    /// Detected edition.
2298    pub variant: EqApoVariant,
2299}
2300
2301/// State of SECS 2R's managed lines in Equalizer APO's `config.txt`.
2302#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
2303#[serde(rename_all = "snake_case")]
2304#[ts(export)]
2305pub enum IncludeState {
2306    /// No managed marker exists.
2307    Absent,
2308    /// The marker and include line form one intact pair.
2309    Present,
2310    /// A marker exists without exactly one intact include pair.
2311    Damaged,
2312}
2313
2314/// One CamillaDSP process kept running by the host service.
2315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2316#[ts(export)]
2317pub struct ManagedCamilla {
2318    pub exe_path: String,
2319    pub config_path: String,
2320    pub port: u16,
2321    pub design_id: String,
2322}
2323
2324/// One Equalizer APO include kept intact by the host service.
2325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2326#[ts(export)]
2327pub struct ManagedEqApo {
2328    pub config_path: String,
2329    pub design_id: String,
2330}
2331
2332/// Targets persisted by the host service.
2333#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2334#[ts(export)]
2335pub struct HostManaged {
2336    #[serde(default, skip_serializing_if = "Option::is_none")]
2337    #[ts(optional)]
2338    pub camilla: Option<ManagedCamilla>,
2339    #[serde(default, skip_serializing_if = "Option::is_none")]
2340    #[ts(optional)]
2341    pub eqapo: Option<ManagedEqApo>,
2342    /// Interactive session that selected these targets.
2343    #[serde(default, skip_serializing_if = "Option::is_none")]
2344    #[ts(skip)]
2345    pub owner_session_id: Option<u32>,
2346    /// Stable SID bytes of that session's user token.
2347    #[serde(default, skip_serializing_if = "Option::is_none")]
2348    #[ts(skip)]
2349    pub owner_user_sid: Option<String>,
2350}
2351
2352/// A request sent over the local managed-host pipe.
2353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2354#[serde(tag = "method", content = "params", rename_all = "snake_case")]
2355#[ts(export)]
2356pub enum HostRequest {
2357    Status,
2358    SetManaged(HostManaged),
2359    StopManaging,
2360    Log { lines: u32 },
2361}
2362
2363/// One JSON Lines entry from the managed host log.
2364#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2365#[ts(export)]
2366pub struct HostLogLine {
2367    /// UTC timestamp in RFC 3339 form.
2368    pub timestamp: String,
2369    /// Stable machine-readable event name.
2370    pub event: String,
2371    /// Event-specific string values such as a PID, path, or reason.
2372    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2373    pub fields: BTreeMap<String, String>,
2374}
2375
2376/// Why a managed CamillaDSP process is not simply running.
2377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2378#[serde(tag = "kind", rename_all = "snake_case")]
2379#[ts(export)]
2380pub enum ManagedProcessState {
2381    Running,
2382    BackingOff { until: String },
2383    GivenUp { reason: String },
2384    Stopped,
2385    NoSession,
2386    OtherUser,
2387}
2388
2389/// Current CamillaDSP supervision state.
2390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2391#[ts(export)]
2392pub struct ManagedProcess {
2393    pub state: ManagedProcessState,
2394    #[ts(type = "number | null")]
2395    pub pid: Option<u32>,
2396    pub restarts_in_window: u32,
2397}
2398
2399impl Default for ManagedProcess {
2400    fn default() -> Self {
2401        Self {
2402            state: ManagedProcessState::Stopped,
2403            pid: None,
2404            restarts_in_window: 0,
2405        }
2406    }
2407}
2408
2409/// Status returned by the managed host service.
2410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2411#[ts(export)]
2412pub struct HostStatus {
2413    pub version: String,
2414    pub managed: HostManaged,
2415    pub camilla: ManagedProcess,
2416    pub eqapo_include: IncludeState,
2417    pub last_repair: Option<String>,
2418}
2419
2420/// One response returned over the local managed-host pipe.
2421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2422#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
2423#[ts(export)]
2424pub enum HostResponse {
2425    Status(Box<HostStatus>),
2426    Ok,
2427    Log(Vec<HostLogLine>),
2428    Error(IpcError),
2429}
2430
2431/// Windows service state visible to the app before a pipe call.
2432#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2433#[serde(rename_all = "snake_case")]
2434#[ts(export)]
2435pub enum ServiceState {
2436    #[default]
2437    NotInstalled,
2438    Stopped,
2439    Running,
2440    Unreachable,
2441}
2442
2443/// How the running CamillaDSP configuration compares with the current result.
2444#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2445#[serde(tag = "kind", rename_all = "snake_case")]
2446#[ts(export)]
2447pub enum DesignMatch {
2448    /// The title and embedded identity match the current design.
2449    This { design_id: String },
2450    /// CamillaDSP runs another titled configuration.
2451    Other { title: String },
2452    /// There is not enough information to identify the running design.
2453    Unknown,
2454}
2455
2456/// CamillaDSP status mirrored from the platform-neutral application client.
2457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2458#[ts(export)]
2459pub struct CamillaStatus {
2460    pub version: String,
2461    pub state: String,
2462    pub stop_reason: Option<String>,
2463    pub title: Option<String>,
2464    pub description: Option<String>,
2465    pub config_path: Option<String>,
2466    pub capture_rate: Option<u32>,
2467    #[ts(type = "number | null")]
2468    pub clipped_samples: Option<u64>,
2469    pub processing_load_pct: Option<f64>,
2470    pub playback_peak_db: Option<Vec<f64>>,
2471}
2472
2473/// Reachability and running design reported by CamillaDSP.
2474#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2475#[ts(export)]
2476pub struct CamillaApplyStatus {
2477    pub reachable: bool,
2478    pub error: Option<IpcError>,
2479    pub status: Option<CamillaStatus>,
2480    pub design: DesignMatch,
2481}
2482
2483/// Installed and applied state of Equalizer APO.
2484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2485#[ts(export)]
2486pub struct EqApoApplyStatus {
2487    pub installed: bool,
2488    pub variant: EqApoVariant,
2489    pub config_path: Option<String>,
2490    pub include: IncludeState,
2491    pub applied_design_id: Option<String>,
2492}
2493
2494/// Current state of both application targets.
2495#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2496#[ts(export)]
2497pub struct ApplyStatus {
2498    pub camilla: CamillaApplyStatus,
2499    pub eqapo: EqApoApplyStatus,
2500    /// Windows managed-host service state.
2501    #[serde(default)]
2502    pub host: ServiceState,
2503    /// Detailed host state when the service is running and reachable.
2504    #[serde(default, skip_serializing_if = "Option::is_none")]
2505    #[ts(optional)]
2506    pub host_status: Option<Box<HostStatus>>,
2507    /// Design ID of the current result, absent without one; the screen
2508    /// compares Equalizer APO's `applied_design_id` with it.
2509    #[serde(default)]
2510    pub current_design_id: Option<String>,
2511}
2512
2513/// Result of applying an exported design.
2514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2515#[serde(tag = "kind", rename_all = "snake_case")]
2516#[ts(export)]
2517pub enum ApplyOutcome {
2518    /// The target accepted the design.
2519    Applied,
2520    /// No CamillaDSP was running; the managed host will start it with the design.
2521    HandedOver,
2522    /// CamillaDSP rejected reload and the previous path was restored.
2523    RolledBack { error: IpcError },
2524}
2525
2526/// Completed application job result.
2527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2528#[ts(export)]
2529pub struct ApplyResult {
2530    pub outcome: ApplyOutcome,
2531    pub design_id: String,
2532    pub folder: String,
2533    pub stale: bool,
2534}
2535
2536/// Application-local settings, independent of an open project.
2537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2538#[ts(export)]
2539pub struct AppSettings {
2540    /// Application-local compute preference; never stored in an upstream project.
2541    #[serde(default)]
2542    pub compute: ComputePreference,
2543    /// Screen language: `ko` or `en`.
2544    pub language: String,
2545    /// Application theme preference.
2546    #[serde(default)]
2547    pub theme: AppTheme,
2548    /// Logo on the rail and the window.
2549    #[serde(default)]
2550    pub icon: AppIcon,
2551    /// Skin set the screen draws with.
2552    #[serde(default)]
2553    pub skin: AppSkin,
2554    /// Opt-in loopback API server.
2555    #[serde(default)]
2556    pub local_api: LocalApiSettings,
2557    /// Applying designs to CamillaDSP and Equalizer APO.
2558    #[serde(default)]
2559    pub apply: ApplySettings,
2560    /// Recently opened project paths, newest first (at most 10).
2561    pub recent_projects: Vec<String>,
2562    /// Last window size in logical pixels.
2563    pub last_window_size: Option<(u32, u32)>,
2564}
2565impl Default for AppSettings {
2566    fn default() -> Self {
2567        Self {
2568            language: "ko".into(),
2569            compute: ComputePreference::Auto,
2570            theme: AppTheme::System,
2571            icon: AppIcon::Expert,
2572            skin: AppSkin::Default,
2573            local_api: LocalApiSettings::default(),
2574            apply: ApplySettings::default(),
2575            recent_projects: Vec::new(),
2576            last_window_size: None,
2577        }
2578    }
2579}
2580/// Complete application-local settings replacement.
2581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2582#[ts(export)]
2583pub struct AppSettingsParams {
2584    /// The complete app settings to store.
2585    pub settings: AppSettings,
2586}
2587
2588/// App-facing state of the opt-in local API server.
2589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2590#[ts(export)]
2591pub struct LocalApiStatus {
2592    /// Configured enabled state.
2593    pub enabled: bool,
2594    /// Configured port, or the selected free port while running with port zero.
2595    pub port: u16,
2596    /// Whether a listener is currently running.
2597    pub running: bool,
2598    /// Most recent startup failure, when present.
2599    pub error: Option<String>,
2600    /// Loopback base URL.
2601    pub url: String,
2602    /// Current bearer token.
2603    pub token: String,
2604    /// Full token-file path.
2605    pub token_path: String,
2606}
2607
2608/// Why a call failed. The screen maps each code to a written message with the
2609/// cause and the next action.
2610#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
2611#[serde(rename_all = "snake_case")]
2612#[ts(export)]
2613pub enum ErrorCode {
2614    /// The request did not parse or named an unknown method.
2615    InvalidRequest,
2616    /// A bug: the service failed in a way it does not expect.
2617    Internal,
2618    /// The method is registered but not connected yet.
2619    NotImplemented,
2620    /// The method opens a window or program and is unavailable to local API clients.
2621    UiOnly,
2622    /// The selected operation or control level is unavailable on this host.
2623    Unsupported,
2624    /// A target file could not be changed because access was denied.
2625    PermissionDenied,
2626    /// CamillaDSP or Equalizer APO refused or failed an application request.
2627    ApplyFailed,
2628    /// The call requires an open project.
2629    ProjectNotOpen,
2630    /// Opening, reading, or writing project data failed.
2631    ProjectIo,
2632    /// Another job in the same group is running.
2633    JobBusy,
2634    /// The named job does not exist.
2635    JobNotFound,
2636    /// The caller cancelled the operation.
2637    Cancelled,
2638    /// Project measurements or settings are not valid engine inputs.
2639    EngineInput,
2640    /// Explicit GPU execution was requested but no usable GPU backend is available.
2641    ComputeUnavailable,
2642    /// The requested exporter failed.
2643    ExportFailed,
2644    /// The chosen export cannot carry the result (upstream
2645    /// `filter_export_block`); `params.reason` is an [`ExportBlock`] name.
2646    ExportScope,
2647    /// Cached results no longer match project inputs.
2648    StaleInputs,
2649    /// The audio backend or selected endpoint is unavailable.
2650    DeviceUnavailable,
2651    /// Recording completed but was discarded because it had a gap or corrupt samples.
2652    RecordingFailed,
2653    /// The backend refused a pinned audio share mode.
2654    ShareModeRefused,
2655    /// The REW API cannot be reached.
2656    RewUnreachable,
2657    /// REW requires a Pro license for this operation.
2658    RewProRequired,
2659    /// REW returned an HTTP or schema failure.
2660    RewFailed,
2661    /// A measurement setting or plan field is invalid.
2662    MeasurementInput,
2663    /// Tutorial sessions cannot save or export project data.
2664    TutorialBlocked,
2665    /// A destructive edit needs explicit confirmation while measurements exist.
2666    ConfirmRequired,
2667}
2668/// A coded IPC failure with template parameters and diagnostic detail.
2669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2670#[ts(export)]
2671pub struct IpcError {
2672    /// Stable error code translated by the screen.
2673    pub code: ErrorCode,
2674    /// Values for the message template of `code` (file names, counts, ...).
2675    pub params: BTreeMap<String, String>,
2676    /// Whether trying the same call again can succeed.
2677    pub retryable: bool,
2678    /// The raw error text, for the details view and the log only.
2679    pub detail: String,
2680}
2681impl IpcError {
2682    /// Creates a non-retryable error without template parameters.
2683    pub fn new(code: ErrorCode, detail: impl Into<String>) -> Self {
2684        Self {
2685            code,
2686            params: BTreeMap::new(),
2687            retryable: false,
2688            detail: detail.into(),
2689        }
2690    }
2691    /// Adds one screen message-template parameter.
2692    pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
2693        self.params.insert(key.into(), value.into());
2694        self
2695    }
2696}
2697
2698/// The reply to every call: `{"ok": true, "data": ...}` or
2699/// `{"ok": false, "error": ...}`.
2700///
2701/// The TypeScript side declares the same union in `src/ipc/client.ts`
2702/// (`Envelope`); `envelope_wire_shape` below pins the JSON both sides rely on.
2703#[derive(Debug, Clone, PartialEq)]
2704pub enum Envelope {
2705    /// The `Ok` Envelope variant.
2706    Ok(Box<Response>),
2707    /// The `Err` Envelope variant.
2708    Err(IpcError),
2709}
2710impl Serialize for Envelope {
2711    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2712        use serde::ser::SerializeStruct;
2713        let mut state = serializer.serialize_struct("Envelope", 2)?;
2714        match self {
2715            Self::Ok(data) => {
2716                state.serialize_field("ok", &true)?;
2717                state.serialize_field("data", data)?;
2718            }
2719            Self::Err(error) => {
2720                state.serialize_field("ok", &false)?;
2721                state.serialize_field("error", error)?;
2722            }
2723        }
2724        state.end()
2725    }
2726}
2727
2728#[cfg(test)]
2729mod tests {
2730    use super::*;
2731
2732    /// One value of every variant. A new variant fails to compile here until
2733    /// it gets an example, which `method_names_match_the_wire` then checks.
2734    fn examples() -> Vec<Request> {
2735        vec![
2736            Request::Bootstrap,
2737            Request::ProjectNew(ProjectNewParams {
2738                path: "x.secs".into(),
2739                preset: "2.1".into(),
2740                sub_count: 1,
2741                sample: false,
2742                tutorial: false,
2743            }),
2744            Request::ProjectOpen(ProjectPathParams {
2745                path: "x.secs".into(),
2746            }),
2747            Request::ProjectSave(ProjectSaveParams::default()),
2748            Request::ProjectInfo,
2749            Request::SampleLoad(SampleLoadParams::default()),
2750            Request::Calibrate(CalibrateParams::default()),
2751            Request::RecommendXo,
2752            Request::Mimo(MimoParams::default()),
2753            Request::Results,
2754            Request::Export(ExportParams {
2755                kind: ExportKind::CamillaDsp,
2756                folder: ".".into(),
2757                mdat_scope: None,
2758                design_name: None,
2759                camilla_devices: None,
2760                extra_rates: Vec::new(),
2761            }),
2762            Request::JobCancel(JobCancelParams { job: JobId(1) }),
2763            Request::Jobs,
2764            Request::MeasureMethods,
2765            Request::MeasureConfigure(MeasureConfigureParams::default()),
2766            Request::MeasureConfigGet,
2767            Request::MeasurementRecords,
2768            Request::MicCalibrationGet,
2769            Request::MicCalibrationSet(MicCalibrationSetParams {
2770                path: "mic.txt".into(),
2771            }),
2772            Request::MicCalibrationClear,
2773            Request::MicCalibrationAttach,
2774            Request::LevelsGet,
2775            Request::LevelsFillDefaults,
2776            Request::LevelStop,
2777            Request::MeasurePosition(MeasurePositionParams { position: 1 }),
2778            Request::PositionClear(PositionClearParams { position: 1 }),
2779            Request::Undo(UndoParams { token: "x".into() }),
2780            Request::RewLaunch,
2781            Request::MdatScan(MdatScanParams {
2782                path: "x.mdat".into(),
2783            }),
2784            Request::MdatImport(MdatImportParams {
2785                path: "x.mdat".into(),
2786                assignments: vec![],
2787                position: 1,
2788            }),
2789            Request::ConfigGet,
2790            Request::ConfigApplyPreset(ConfigPresetParams {
2791                preset: "2.1".into(),
2792                confirm: false,
2793            }),
2794            Request::ConfigSetSubCount(ConfigSubCountParams {
2795                count: 2,
2796                confirm: false,
2797            }),
2798            Request::ConfigSetSpeaker(ConfigSetSpeakerParams {
2799                index: 0,
2800                role: Some("L".into()),
2801                size: None,
2802                band_index: Some(0),
2803                out: Some(1),
2804                pol: Some("+".into()),
2805            }),
2806            Request::ConfigAddSpeaker,
2807            Request::ConfigRemoveSpeaker(ConfigIndexParams { index: 0 }),
2808            Request::ConfigRenumber,
2809            Request::ConfigValidate,
2810            Request::PatchSheetCsv(PathParams {
2811                path: "patch.csv".into(),
2812            }),
2813            Request::DspGet,
2814            Request::DspSet(DspSetParams {
2815                preset: Some("tesira".into()),
2816                fields: None,
2817            }),
2818            Request::LayoutGet,
2819            Request::LayoutSet(LayoutSetParams {
2820                count: Some(9),
2821                ..Default::default()
2822            }),
2823            Request::PositionEstimate,
2824            Request::TargetGet,
2825            Request::TargetSetPoints(TargetPointsParams {
2826                points: vec![(20.0, 0.0), (20_000.0, 0.0)],
2827            }),
2828            Request::TargetApplyPreset(TargetPresetParams {
2829                preset: "flat".into(),
2830            }),
2831            Request::TargetSave(PathParams {
2832                path: "target.json".into(),
2833            }),
2834            Request::TargetLoad(PathParams {
2835                path: "target.json".into(),
2836            }),
2837            Request::CalibrationSettingsGet,
2838            Request::CalibrationSettingsEdit(CalibrationSettingsEditParams {
2839                edit: crate::settings_edit::SettingEdit::MimoOn { on: true },
2840            }),
2841            Request::SubWarnings,
2842            Request::RewInfo,
2843            Request::RewDriverSet(RewDriverSetParams {
2844                driver: "ASIO".into(),
2845            }),
2846            Request::CalibrationSettingsSet(CalibrationSettingsParams {
2847                settings: CalibrationSettings::default(),
2848            }),
2849            Request::ModeSpec(ModeSpecParams {
2850                mode: Mode::Mixed,
2851                ultra_ms: None,
2852                mimo_fir: None,
2853            }),
2854            Request::AppSettingsGet,
2855            Request::AppSettingsSet(AppSettingsParams {
2856                settings: AppSettings::default(),
2857            }),
2858            Request::ApplyStatus,
2859            Request::ApplyCamilla(ApplyCamillaParams::default()),
2860            Request::ApplyEqapo,
2861            Request::RevertEqapo,
2862            Request::ApplySettingsSet(ApplySettings::default()),
2863            Request::HostInstall,
2864            Request::HostUninstall,
2865            Request::HostStopManaging,
2866            Request::LocalApiStatus,
2867            Request::LocalApiTokenRegenerate,
2868            Request::ComputeStatus,
2869            Request::OpenGpuHelp,
2870            Request::MeasurementCurves(MeasurementCurvesParams::default()),
2871            Request::ResultCurves(ResultCurvesParams {
2872                tab: ResultTab::Spl,
2873                channel: None,
2874                group: None,
2875                xo_hz: None,
2876                corrected: None,
2877            }),
2878            Request::ResultLines,
2879            Request::SubTakeValues,
2880            Request::DialogOpenProject,
2881            Request::DialogSaveProject(DialogSuggestedParams {
2882                suggested: "x.secs".into(),
2883            }),
2884            Request::DialogPickMdat,
2885            Request::DialogPickFolder(DialogPickFolderParams { start: None }),
2886            Request::DialogSaveFile(DialogSaveFileParams {
2887                kind: SaveFileKind::PatchSheet,
2888                suggested: "x.csv".into(),
2889            }),
2890            Request::DialogOpenFile(DialogOpenFileParams {
2891                kind: OpenFileKind::Target,
2892            }),
2893            Request::OpenFolder(OpenFolderParams { path: ".".into() }),
2894            Request::LevelMonitorStart(LevelMonitorStartParams::default()),
2895            Request::LevelMonitorStop,
2896            Request::LevelPlay(LevelPlayParams {
2897                role: "L".into(),
2898                master_dbfs: -20.0,
2899            }),
2900            Request::StatusWatchStart,
2901            Request::StatusWatchStop,
2902            Request::ExportFolderGet,
2903            Request::ExportFolderSet(ExportFolderSetParams { folder: ".".into() }),
2904        ]
2905    }
2906    #[test]
2907    fn method_names_match_the_wire() {
2908        let examples = examples();
2909        assert_eq!(examples.len(), Request::METHODS.len());
2910        for request in examples {
2911            let wire = serde_json::to_value(&request).unwrap();
2912            assert_eq!(wire["method"], request.method());
2913        }
2914    }
2915    #[test]
2916    fn every_method_name_is_listed_and_parses() {
2917        for request in examples() {
2918            let wire = serde_json::to_value(&request).unwrap();
2919            let parsed: Request = serde_json::from_value(wire).unwrap();
2920            assert_eq!(parsed.method(), request.method());
2921        }
2922    }
2923    #[test]
2924    fn old_app_settings_without_local_api_use_the_disabled_default() {
2925        let settings: AppSettings = serde_json::from_str(
2926            r#"{"language":"ko","recent_projects":[],"last_window_size":null}"#,
2927        )
2928        .unwrap();
2929        assert_eq!(settings.local_api, LocalApiSettings::default());
2930        assert!(!settings.local_api.enabled);
2931        assert_eq!(settings.local_api.port, crate::local_api::DEFAULT_PORT);
2932    }
2933
2934    #[test]
2935    fn calibrate_mimo_defaults_false_on_the_wire() {
2936        let old: CalibrateParams =
2937            serde_json::from_str(r#"{"settings":null,"subs_only":false}"#).unwrap();
2938        assert!(!old.mimo);
2939        assert_eq!(CalibrateParams::default(), old);
2940        let wire = serde_json::to_value(CalibrateParams {
2941            mimo: true,
2942            ..Default::default()
2943        })
2944        .unwrap();
2945        assert_eq!(wire["mimo"], true);
2946    }
2947
2948    #[test]
2949    fn mdat_scope_is_optional_on_the_wire() {
2950        let old: ExportParams =
2951            serde_json::from_str(r#"{"kind":"before_after_mdat","folder":""}"#).unwrap();
2952        assert_eq!(old.mdat_scope, None);
2953        assert!(old.extra_rates.is_empty());
2954        assert_eq!(old.mdat_scope.unwrap_or_default(), MdatScope::All);
2955        let wire = serde_json::to_value(&old).unwrap();
2956        assert!(wire.get("mdat_scope").is_none());
2957        let scoped = ExportParams {
2958            mdat_scope: Some(MdatScope::Measurements),
2959            design_name: None,
2960            camilla_devices: None,
2961            ..old
2962        };
2963        let wire = serde_json::to_value(&scoped).unwrap();
2964        assert_eq!(wire["mdat_scope"], "measurements");
2965        assert_eq!(
2966            serde_json::from_value::<ExportParams>(wire).unwrap(),
2967            scoped
2968        );
2969    }
2970    #[test]
2971    fn envelope_wire_shape() {
2972        let ok = Envelope::Ok(Box::new(Response::Bootstrap(BootstrapInfo {
2973            app_name: "SECS 2R".into(),
2974            version: "0.0.1".into(),
2975            prerelease: true,
2976            upstream: UpstreamBase {
2977                name: "SECS".into(),
2978                repository: "https://example.invalid/secs".into(),
2979                version: "Beta".into(),
2980                commit: "0".repeat(40),
2981                date: "2026-10-12".into(),
2982            },
2983            platform: Platform {
2984                os: "linux".into(),
2985                arch: "x86_64".into(),
2986            },
2987        })));
2988        let value = serde_json::to_value(ok).unwrap();
2989        assert_eq!(value["ok"], true);
2990        assert_eq!(value["data"]["method"], "bootstrap");
2991        let value = serde_json::to_value(Envelope::Err(IpcError::new(
2992            ErrorCode::InvalidRequest,
2993            "bad",
2994        )))
2995        .unwrap();
2996        assert_eq!(value["ok"], false);
2997        assert_eq!(value["error"]["code"], "invalid_request");
2998    }
2999
3000    #[test]
3001    fn compute_completion_events_are_payload_free_markers() {
3002        for (result, kind) in [
3003            (JobResult::Calibration, "calibration"),
3004            (JobResult::Mimo, "mimo"),
3005        ] {
3006            let event = JobEvent::Finished {
3007                job: JobId(1),
3008                status: JobStatus::Succeeded,
3009                result: Some(Box::new(result)),
3010                error: None,
3011            };
3012            let wire = serde_json::to_vec(&event).unwrap();
3013            let value: serde_json::Value = serde_json::from_slice(&wire).unwrap();
3014            assert_eq!(value["result"], serde_json::json!({ "kind": kind }));
3015            assert!(wire.len() < 128, "completion must not carry a design");
3016            assert_eq!(serde_json::from_slice::<JobEvent>(&wire).unwrap(), event);
3017        }
3018    }
3019
3020    #[test]
3021    fn job_snapshot_is_metadata_without_a_result_payload() {
3022        let snapshot = JobSnapshot {
3023            job: JobId(42),
3024            kind: JobKind::Mimo,
3025            status: JobStatus::Succeeded,
3026            cancellable: false,
3027            progress: None,
3028            error: None,
3029        };
3030        let wire = serde_json::to_value(&snapshot).unwrap();
3031        assert!(wire.get("result").is_none());
3032        assert_eq!(
3033            serde_json::from_value::<JobSnapshot>(wire).unwrap(),
3034            snapshot
3035        );
3036    }
3037
3038    #[test]
3039    fn filter_export_scope_follows_upstream_but_eqapo_carries_mimo_f12() {
3040        use ExportKind::*;
3041        for kind in [
3042            CamillaDsp,
3043            StagedMdat,
3044            BeforeAfterMdat,
3045            EqualizerApo,
3046            Convolution,
3047        ] {
3048            for (subs, mimo) in [(false, false), (true, false), (false, true), (true, true)] {
3049                assert_eq!(filter_export_block(kind, subs, mimo), None, "{kind:?}");
3050            }
3051        }
3052        assert_eq!(filter_export_block(FilterText, false, false), None);
3053        assert_eq!(
3054            export_block(StagedMdat, false, false, true),
3055            Some(ExportBlock::StaleCache)
3056        );
3057        assert_eq!(
3058            export_block(BeforeAfterMdat, true, true, true),
3059            Some(ExportBlock::StaleCache)
3060        );
3061        assert_eq!(export_block(CamillaDsp, true, true, true), None);
3062        assert_eq!(
3063            filter_export_block(FilterText, true, false),
3064            Some(ExportBlock::Subs)
3065        );
3066        assert_eq!(
3067            filter_export_block(FilterText, true, true),
3068            Some(ExportBlock::Subs)
3069        );
3070        assert_eq!(
3071            filter_export_block(FilterText, false, true),
3072            Some(ExportBlock::Mimo)
3073        );
3074        assert_eq!(serde_json::to_value(FilterText).unwrap(), "filter_text");
3075        assert_eq!(serde_json::to_value(EqualizerApo).unwrap(), "equalizer_apo");
3076        assert_eq!(serde_json::to_value(Convolution).unwrap(), "convolution");
3077        assert_eq!(
3078            serde_json::to_value(ExportBlock::Subs).unwrap(),
3079            ExportBlock::Subs.name()
3080        );
3081        assert_eq!(
3082            serde_json::to_value(ExportBlock::Mimo).unwrap(),
3083            ExportBlock::Mimo.name()
3084        );
3085        assert_eq!(
3086            serde_json::to_value(ErrorCode::ExportScope).unwrap(),
3087            "export_scope"
3088        );
3089    }
3090}