gled-protocol 2.32.1

What gled's user interfaces and its engine send each other
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
//! What the engine publishes to its user interfaces, and what they send it
//! besides actions. In-process the desktop app gets these over channels;
//! remote interfaces get them as `message`s.

use std::{
    collections::{BTreeMap, BTreeSet},
    path::PathBuf,
    sync::Arc,
};

use gled_model::{
    asset::{
        animation::Animation,
        midi_controller::{MidiEffectParameter, MidiModeHighlight},
        output_device::routing::{OutputRouting, OutputRoutings},
        project::{Project, io::ArtnetConfig},
        scene::grid::GridLocation,
    },
    asset_id::AssetId,
    input::key_code::KeyCode,
    patching::color_orders::ColorOrders,
    sound::{AudioDeviceId, trigger_params::FREQ_BINS, trigger_ref::SoundLevels},
    timing::BeatState,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{
    action::UiAction,
    assets::{AnyAsset, AnyAssetId},
    bytes::SharedBytes,
    midi::TestCommand,
};

/// Which universes are rendered, where they are routed and in which channel
/// order their lamps take red, green and blue.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct OutputConfig {
    pub universes: Arc<BTreeSet<u16>>,
    pub routings: Arc<OutputRoutings>,
    /// Lamps that are not RGB, derived from the patch with the universes.
    #[serde(default)]
    pub color_orders: Arc<ColorOrders>,
}

/// How the render thread keeps up, over the last second.
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct RenderStats {
    pub fps: f32,
    /// The longest time between two frames.
    pub max_frame_ms: f32,
}

/// What the machine running the engine is doing besides the show.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerStatus {
    /// What the asset repository is busy with, `None` while idle.
    pub storage_task: Option<String>,
    /// How far the task is, 0 to 1, when it can tell.
    pub storage_progress: Option<f32>,
    pub storage_error: Option<String>,
    /// The repository is being read, written or synchronized.
    pub storage_working: bool,
    /// Changed files not yet committed.
    pub staged_files: usize,
    pub branches: Vec<String>,
    pub current_branch: Option<String>,
    /// Where the asset repository is on the engine's machine.
    pub storage_dir: String,
    /// Art-Net and DMX traffic in Mbit/s.
    pub network_in: f32,
    pub network_out: f32,
    /// The hottest temperature sensor of the engine's machine in °C, `None`
    /// on a machine without one.
    pub temperature: Option<f32>,
    /// The engine machine's host name, which tells its readings apart from
    /// those of the machine showing the interface.
    pub host_name: String,
    /// Other Ableton Link peers.
    pub link_peers: u64,
    pub settings: ServerSettings,
    /// The running Art-Net input: the project's settings and, per universe
    /// seen, how much data arrived.
    pub artnet: ArtnetConfig,
    /// The devices of the engine's machine, while an interface watches them.
    pub devices: Devices,
}

/// The devices the engine's machine has, for choosing among them.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Devices {
    /// Audio inputs sound triggers can listen to, with a label to show.
    pub audio_inputs: Vec<(AudioDeviceId, String)>,
    pub midi_outputs: Vec<String>,
    /// Serial numbers of the connected Enttec DMX USB Pro interfaces.
    pub enttec: Vec<String>,
}

/// Highest selectable output frame rate. Choosing it means unlimited.
pub const FPS_LIMIT_MAX: f32 = 333.0;

/// Settings of the machine running the engine, which any interface may change.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerSettings {
    /// The render and output frame rate limit; `FPS_LIMIT_MAX` and above
    /// mean unlimited.
    pub fps_limit: f32,
    /// Follow the Ableton Link tempo without changing it.
    pub ableton_link_read_only: bool,
    /// The asset repository's remote.
    pub git_url: String,
    /// The SSH key for the repository, a path on the engine's machine.
    pub git_private_key: Option<String>,
    pub git_use_passphrase: bool,
}

/// Work on the asset repository an interface asks for.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum StorageCommand {
    Commit {
        message: String,
    },
    Pull,
    Push,
    SwitchBranch(String),
    /// Deletes the local repository and clones it again from the git URL.
    Reclone,
    /// The passphrase of the SSH key, kept in the engine machine's keyring.
    SetGitPassphrase(String),
}

/// A line of the scene grid through the selected scene.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GridLine {
    Row,
    Column,
}

/// A control a MIDI controller's touch-sensitive fader is touched on, which
/// the interfaces outline so the player sees what the fader will change.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TouchedControl {
    MainDimmer,
    SceneOpacity(GridLocation),
    EffectParameter {
        scene: GridLocation,
        effect: usize,
        parameter: MidiEffectParameter,
    },
}

/// A controller's switch to another mode, numbered so an interface sees
/// each one, also a press of the mode already active.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModeSwitch {
    pub number: u64,
    pub highlight: Option<MidiModeHighlight>,
}

/// The engine's state as user interfaces see it, published every tick.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EngineState {
    /// The engine's project, shared: the engine publishes a new `Arc` only
    /// when the project changed, so an unchanged one is the same `Arc` and
    /// compares in O(1). It goes over the wire as a plain project.
    pub project: Option<Arc<Project>>,
    pub project_id: Option<AssetId<Project>>,
    pub selected_scene_instance: GridLocation,
    /// The effect selected in the selected scene, by its position; it may
    /// be past the last effect while an added one is on its way.
    pub selected_scene_effect: usize,
    /// The line a MIDI controller last moved the selection along, which
    /// the interfaces mark through the selected scene; `None` until one did.
    pub controller_line: Option<GridLine>,
    /// The mode of each controller that switched modes, by port key; the
    /// others are in their first mode.
    pub midi_modes: BTreeMap<String, u8>,
    pub last_mode_switch: Option<ModeSwitch>,
    pub touched_controls: Vec<TouchedControl>,
    /// The MIDI input ports of the engine's machine.
    pub midi_inputs: Vec<String>,
    pub blackout: bool,
    pub blackout_hold: bool,
    pub midi_output_active: bool,
    pub output_config: OutputConfig,
    pub beat: BeatState,
    /// Taps counted for the tap button's indicator.
    pub tap_count: usize,
    /// Changes whenever the lamp positions changed: the patch or one of the
    /// fixture definitions it uses.
    pub layout_generation: u64,
    pub render: RenderStats,
    pub status: ServerStatus,
}

impl EngineState {
    /// A copy without the project, which goes as a patch.
    pub fn without_project(&self) -> Self {
        let Self {
            project: _,
            project_id,
            selected_scene_instance,
            selected_scene_effect,
            controller_line,
            midi_modes,
            last_mode_switch,
            touched_controls,
            midi_inputs,
            blackout,
            blackout_hold,
            midi_output_active,
            output_config,
            beat,
            tap_count,
            layout_generation,
            render,
            status,
        } = self;
        Self {
            project: None,
            project_id: *project_id,
            selected_scene_instance: *selected_scene_instance,
            selected_scene_effect: *selected_scene_effect,
            controller_line: *controller_line,
            midi_modes: midi_modes.clone(),
            last_mode_switch: *last_mode_switch,
            touched_controls: touched_controls.clone(),
            midi_inputs: midi_inputs.clone(),
            blackout: *blackout,
            blackout_hold: *blackout_hold,
            midi_output_active: *midi_output_active,
            output_config: output_config.clone(),
            beat: *beat,
            tap_count: *tap_count,
            layout_generation: *layout_generation,
            render: *render,
            status: status.clone(),
        }
    }

    /// Whether both differ only in their timing: the beat, which changes
    /// every tick and which remote interfaces extrapolate between updates, and
    /// the render stats.
    pub fn same_but_timing(&self, other: &Self) -> bool {
        self.same_but_timing_and_project(other)
            && match (self.project.as_deref(), other.project.as_deref()) {
                // The engine's unchanged project is the same value.
                (Some(a), Some(b)) => std::ptr::eq(a, b) || a == b,
                (a, b) => a.is_none() && b.is_none(),
            }
    }

    /// Whether both differ at most in their timing and their project, which
    /// an update carries on its own (`ProjectUpdate`).
    pub fn same_but_timing_and_project(&self, other: &Self) -> bool {
        let Self {
            project: _,
            project_id,
            selected_scene_instance,
            selected_scene_effect,
            controller_line,
            midi_modes,
            last_mode_switch,
            touched_controls,
            midi_inputs,
            blackout,
            blackout_hold,
            midi_output_active,
            output_config,
            beat: _,
            tap_count,
            layout_generation,
            render: _,
            status,
        } = self;
        *project_id == other.project_id
            && *selected_scene_instance == other.selected_scene_instance
            && *selected_scene_effect == other.selected_scene_effect
            && *controller_line == other.controller_line
            && *midi_modes == other.midi_modes
            && *last_mode_switch == other.last_mode_switch
            && *touched_controls == other.touched_controls
            && *midi_inputs == other.midi_inputs
            && *blackout == other.blackout
            && *blackout_hold == other.blackout_hold
            && *midi_output_active == other.midi_output_active
            && *tap_count == other.tap_count
            && *layout_generation == other.layout_generation
            && *output_config == other.output_config
            && *status == other.status
    }
}

/// What a user interface sends the engine besides actions.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ClientInput {
    /// An action from the interface, numbered in the order it was sent.
    /// Number 0 is for actions outside that order, such as a MIDI
    /// controller's: the engine applies them without counting them.
    Action {
        sequence: u64,
        action: UiAction,
    },
    /// The keys held in the interface, for key inputs.
    Keys(BTreeSet<KeyCode>),
    /// The tempo set with the BPM slider.
    SetBeatsPerMinute(f32),
    /// Stores an asset in the collections and the repository.
    SaveAsset(AnyAsset),
    /// Removes an asset from the collections and the repository.
    DeleteAsset(AnyAssetId),
    Storage(StorageCommand),
    SetServerSettings(ServerSettings),
    /// The output routing the pointer is over, whose universe then blinks so
    /// the installation shows which lamps it drives. `None` when it left.
    HoverOutputRouting(Option<OutputRouting>),
    /// Whether this interface shows a device list, so the engine scans for
    /// devices.
    WatchDevices(bool),
    /// Whether this interface shows the MIDI monitor, so the engine streams
    /// the traffic.
    WatchMidi(bool),
    /// Whether this interface waits for MIDI learn input.
    MidiLearning(bool),
    MidiTest(TestCommand),
    /// The sound triggers this interface edits, whose spectra it needs.
    WatchSoundTriggers(BTreeSet<Uuid>),
    /// Stores a picture chosen in the interface in the asset repository, at
    /// `path` there (see `gled_model::asset::picture`).
    SavePicture {
        path: PathBuf,
        bytes: SharedBytes,
    },
    /// Asks for the picture at `path` in the asset repository. The engine
    /// answers this interface alone, with `EngineEvent::Picture`.
    RequestPicture(PathBuf),
}

/// Something the user interfaces have to react to once.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EngineEvent {
    /// The code of this animation (all for `None`) changed: rebuild previews.
    ShaderReloaded(Option<AssetId<Animation>>),
    /// An error to show the user.
    Error(String),
    /// The picture an interface asked for, or why there is none.
    Picture {
        path: PathBuf,
        picture: Result<SharedBytes, String>,
    },
}

/// The sound triggers' levels and the input spectrum, as interfaces need them
/// to draw effects that follow the music.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct SoundSnapshot {
    pub levels: BTreeMap<Uuid, f32>,
    /// The envelope input of every trigger, 0 to 1.
    pub impulses: BTreeMap<Uuid, f32>,
    /// `FREQ_BINS` little-endian `f32`s, as effects read them.
    pub spectrum: SharedBytes,
    /// The averaged spectrum of the triggers an interface edits, as
    /// little-endian `f32`s.
    pub trigger_spectra: BTreeMap<Uuid, SharedBytes>,
}

impl SoundSnapshot {
    /// What the trigger editor shows of a running trigger: impulse, level
    /// and its averaged spectrum (empty unless the trigger is watched).
    pub fn reading(&self, id: &Uuid) -> Option<(f32, f32, Vec<f32>)> {
        let level = *self.levels.get(id)?;
        let impulse = self.impulses.get(id).copied().unwrap_or_default();
        let spectrum = self
            .trigger_spectra
            .get(id)
            .map(|bytes| {
                bytes
                    .0
                    .as_chunks::<4>()
                    .0
                    .iter()
                    .map(|chunk| f32::from_le_bytes(*chunk))
                    .collect()
            })
            .unwrap_or_default();
        Some((impulse, level, spectrum))
    }
}

impl SoundLevels for SoundSnapshot {
    fn level(&self, id: &Uuid) -> f32 {
        self.levels.get(id).copied().unwrap_or(0.0)
    }

    fn spectrum_bytes(&self) -> [u8; FREQ_BINS * 4] {
        let mut bytes = [0; FREQ_BINS * 4];
        let len = self.spectrum.0.len().min(bytes.len());
        bytes[..len].copy_from_slice(&self.spectrum.0[..len]);
        bytes
    }
}

/// Where a thread's client inputs go.
type InputRoute = Box<dyn Fn(ClientInput)>;

thread_local! {
    /// Where client inputs enqueued on this thread go: the interface's
    /// connection to the engine.
    static INPUT_ROUTE: std::cell::RefCell<Option<InputRoute>> =
        const { std::cell::RefCell::new(None) };
}

/// Sends every client input enqueued on the calling thread to `route`.
pub fn route_inputs_on_this_thread(route: impl Fn(ClientInput) + 'static) {
    INPUT_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
}

impl ClientInput {
    /// Sends this input to the engine through the calling thread's route.
    pub fn enqueue(self) {
        INPUT_ROUTE.with(|cell| match cell.borrow().as_ref() {
            Some(route) => route(self),
            None => tracing::warn!("Dropped {self:?}: this thread has no connection to the engine"),
        });
    }
}