Skip to main content

gled_protocol/
state.rs

1//! What the engine publishes to its user interfaces, and what they send it
2//! besides actions. In-process the desktop app gets these over channels;
3//! remote interfaces get them as `message`s.
4
5use std::{
6    collections::{BTreeMap, BTreeSet},
7    path::PathBuf,
8    sync::Arc,
9};
10
11use gled_model::{
12    asset::{
13        animation::Animation,
14        midi_controller::{MidiEffectParameter, MidiModeHighlight},
15        output_device::routing::{OutputRouting, OutputRoutings},
16        project::{Project, io::ArtnetConfig},
17        scene::grid::GridLocation,
18    },
19    asset_id::AssetId,
20    input::key_code::KeyCode,
21    patching::color_orders::ColorOrders,
22    sound::{AudioDeviceId, trigger_params::FREQ_BINS, trigger_ref::SoundLevels},
23    timing::BeatState,
24};
25use serde::{Deserialize, Serialize};
26use uuid::Uuid;
27
28use crate::{
29    action::UiAction,
30    assets::{AnyAsset, AnyAssetId},
31    bytes::SharedBytes,
32    midi::TestCommand,
33};
34
35/// Which universes are rendered, where they are routed and in which channel
36/// order their lamps take red, green and blue.
37#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
38pub struct OutputConfig {
39    pub universes: Arc<BTreeSet<u16>>,
40    pub routings: Arc<OutputRoutings>,
41    /// Lamps that are not RGB, derived from the patch with the universes.
42    #[serde(default)]
43    pub color_orders: Arc<ColorOrders>,
44}
45
46/// How the render thread keeps up, over the last second.
47#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
48pub struct RenderStats {
49    pub fps: f32,
50    /// The longest time between two frames.
51    pub max_frame_ms: f32,
52}
53
54/// What the machine running the engine is doing besides the show.
55#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
56#[serde(default)]
57pub struct ServerStatus {
58    /// What the asset repository is busy with, `None` while idle.
59    pub storage_task: Option<String>,
60    /// How far the task is, 0 to 1, when it can tell.
61    pub storage_progress: Option<f32>,
62    pub storage_error: Option<String>,
63    /// The repository is being read, written or synchronized.
64    pub storage_working: bool,
65    /// Changed files not yet committed.
66    pub staged_files: usize,
67    pub branches: Vec<String>,
68    pub current_branch: Option<String>,
69    /// Where the asset repository is on the engine's machine.
70    pub storage_dir: String,
71    /// Art-Net and DMX traffic in Mbit/s.
72    pub network_in: f32,
73    pub network_out: f32,
74    /// The hottest temperature sensor of the engine's machine in °C, `None`
75    /// on a machine without one.
76    pub temperature: Option<f32>,
77    /// The engine machine's host name, which tells its readings apart from
78    /// those of the machine showing the interface.
79    pub host_name: String,
80    /// Other Ableton Link peers.
81    pub link_peers: u64,
82    pub settings: ServerSettings,
83    /// The running Art-Net input: the project's settings and, per universe
84    /// seen, how much data arrived.
85    pub artnet: ArtnetConfig,
86    /// The devices of the engine's machine, while an interface watches them.
87    pub devices: Devices,
88}
89
90/// The devices the engine's machine has, for choosing among them.
91#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
92#[serde(default)]
93pub struct Devices {
94    /// Audio inputs sound triggers can listen to, with a label to show.
95    pub audio_inputs: Vec<(AudioDeviceId, String)>,
96    pub midi_outputs: Vec<String>,
97    /// Serial numbers of the connected Enttec DMX USB Pro interfaces.
98    pub enttec: Vec<String>,
99}
100
101/// Highest selectable output frame rate. Choosing it means unlimited.
102pub const FPS_LIMIT_MAX: f32 = 333.0;
103
104/// Settings of the machine running the engine, which any interface may change.
105#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
106#[serde(default)]
107pub struct ServerSettings {
108    /// The render and output frame rate limit; `FPS_LIMIT_MAX` and above
109    /// mean unlimited.
110    pub fps_limit: f32,
111    /// Follow the Ableton Link tempo without changing it.
112    pub ableton_link_read_only: bool,
113    /// The asset repository's remote.
114    pub git_url: String,
115    /// The SSH key for the repository, a path on the engine's machine.
116    pub git_private_key: Option<String>,
117    pub git_use_passphrase: bool,
118}
119
120/// Work on the asset repository an interface asks for.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub enum StorageCommand {
123    Commit {
124        message: String,
125    },
126    Pull,
127    Push,
128    SwitchBranch(String),
129    /// Deletes the local repository and clones it again from the git URL.
130    Reclone,
131    /// The passphrase of the SSH key, kept in the engine machine's keyring.
132    SetGitPassphrase(String),
133}
134
135/// A line of the scene grid through the selected scene.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137pub enum GridLine {
138    Row,
139    Column,
140}
141
142/// A control a MIDI controller's touch-sensitive fader is touched on, which
143/// the interfaces outline so the player sees what the fader will change.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145pub enum TouchedControl {
146    MainDimmer,
147    SceneOpacity(GridLocation),
148    EffectParameter {
149        scene: GridLocation,
150        effect: usize,
151        parameter: MidiEffectParameter,
152    },
153}
154
155/// A controller's switch to another mode, numbered so an interface sees
156/// each one, also a press of the mode already active.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158pub struct ModeSwitch {
159    pub number: u64,
160    pub highlight: Option<MidiModeHighlight>,
161}
162
163/// The engine's state as user interfaces see it, published every tick.
164#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
165pub struct EngineState {
166    /// The engine's project, shared: the engine publishes a new `Arc` only
167    /// when the project changed, so an unchanged one is the same `Arc` and
168    /// compares in O(1). It goes over the wire as a plain project.
169    pub project: Option<Arc<Project>>,
170    pub project_id: Option<AssetId<Project>>,
171    pub selected_scene_instance: GridLocation,
172    /// The effect selected in the selected scene, by its position; it may
173    /// be past the last effect while an added one is on its way.
174    pub selected_scene_effect: usize,
175    /// The line a MIDI controller last moved the selection along, which
176    /// the interfaces mark through the selected scene; `None` until one did.
177    pub controller_line: Option<GridLine>,
178    /// The mode of each controller that switched modes, by port key; the
179    /// others are in their first mode.
180    pub midi_modes: BTreeMap<String, u8>,
181    pub last_mode_switch: Option<ModeSwitch>,
182    pub touched_controls: Vec<TouchedControl>,
183    /// The MIDI input ports of the engine's machine.
184    pub midi_inputs: Vec<String>,
185    pub blackout: bool,
186    pub blackout_hold: bool,
187    pub midi_output_active: bool,
188    pub output_config: OutputConfig,
189    pub beat: BeatState,
190    /// Taps counted for the tap button's indicator.
191    pub tap_count: usize,
192    /// Changes whenever the lamp positions changed: the patch or one of the
193    /// fixture definitions it uses.
194    pub layout_generation: u64,
195    pub render: RenderStats,
196    pub status: ServerStatus,
197}
198
199impl EngineState {
200    /// A copy without the project, which goes as a patch.
201    pub fn without_project(&self) -> Self {
202        let Self {
203            project: _,
204            project_id,
205            selected_scene_instance,
206            selected_scene_effect,
207            controller_line,
208            midi_modes,
209            last_mode_switch,
210            touched_controls,
211            midi_inputs,
212            blackout,
213            blackout_hold,
214            midi_output_active,
215            output_config,
216            beat,
217            tap_count,
218            layout_generation,
219            render,
220            status,
221        } = self;
222        Self {
223            project: None,
224            project_id: *project_id,
225            selected_scene_instance: *selected_scene_instance,
226            selected_scene_effect: *selected_scene_effect,
227            controller_line: *controller_line,
228            midi_modes: midi_modes.clone(),
229            last_mode_switch: *last_mode_switch,
230            touched_controls: touched_controls.clone(),
231            midi_inputs: midi_inputs.clone(),
232            blackout: *blackout,
233            blackout_hold: *blackout_hold,
234            midi_output_active: *midi_output_active,
235            output_config: output_config.clone(),
236            beat: *beat,
237            tap_count: *tap_count,
238            layout_generation: *layout_generation,
239            render: *render,
240            status: status.clone(),
241        }
242    }
243
244    /// Whether both differ only in their timing: the beat, which changes
245    /// every tick and which remote interfaces extrapolate between updates, and
246    /// the render stats.
247    pub fn same_but_timing(&self, other: &Self) -> bool {
248        self.same_but_timing_and_project(other)
249            && match (self.project.as_deref(), other.project.as_deref()) {
250                // The engine's unchanged project is the same value.
251                (Some(a), Some(b)) => std::ptr::eq(a, b) || a == b,
252                (a, b) => a.is_none() && b.is_none(),
253            }
254    }
255
256    /// Whether both differ at most in their timing and their project, which
257    /// an update carries on its own (`ProjectUpdate`).
258    pub fn same_but_timing_and_project(&self, other: &Self) -> bool {
259        let Self {
260            project: _,
261            project_id,
262            selected_scene_instance,
263            selected_scene_effect,
264            controller_line,
265            midi_modes,
266            last_mode_switch,
267            touched_controls,
268            midi_inputs,
269            blackout,
270            blackout_hold,
271            midi_output_active,
272            output_config,
273            beat: _,
274            tap_count,
275            layout_generation,
276            render: _,
277            status,
278        } = self;
279        *project_id == other.project_id
280            && *selected_scene_instance == other.selected_scene_instance
281            && *selected_scene_effect == other.selected_scene_effect
282            && *controller_line == other.controller_line
283            && *midi_modes == other.midi_modes
284            && *last_mode_switch == other.last_mode_switch
285            && *touched_controls == other.touched_controls
286            && *midi_inputs == other.midi_inputs
287            && *blackout == other.blackout
288            && *blackout_hold == other.blackout_hold
289            && *midi_output_active == other.midi_output_active
290            && *tap_count == other.tap_count
291            && *layout_generation == other.layout_generation
292            && *output_config == other.output_config
293            && *status == other.status
294    }
295}
296
297/// What a user interface sends the engine besides actions.
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299pub enum ClientInput {
300    /// An action from the interface, numbered in the order it was sent.
301    /// Number 0 is for actions outside that order, such as a MIDI
302    /// controller's: the engine applies them without counting them.
303    Action {
304        sequence: u64,
305        action: UiAction,
306    },
307    /// The keys held in the interface, for key inputs.
308    Keys(BTreeSet<KeyCode>),
309    /// The tempo set with the BPM slider.
310    SetBeatsPerMinute(f32),
311    /// Stores an asset in the collections and the repository.
312    SaveAsset(AnyAsset),
313    /// Removes an asset from the collections and the repository.
314    DeleteAsset(AnyAssetId),
315    Storage(StorageCommand),
316    SetServerSettings(ServerSettings),
317    /// The output routing the pointer is over, whose universe then blinks so
318    /// the installation shows which lamps it drives. `None` when it left.
319    HoverOutputRouting(Option<OutputRouting>),
320    /// Whether this interface shows a device list, so the engine scans for
321    /// devices.
322    WatchDevices(bool),
323    /// Whether this interface shows the MIDI monitor, so the engine streams
324    /// the traffic.
325    WatchMidi(bool),
326    /// Whether this interface waits for MIDI learn input.
327    MidiLearning(bool),
328    MidiTest(TestCommand),
329    /// The sound triggers this interface edits, whose spectra it needs.
330    WatchSoundTriggers(BTreeSet<Uuid>),
331    /// Stores a picture chosen in the interface in the asset repository, at
332    /// `path` there (see `gled_model::asset::picture`).
333    SavePicture {
334        path: PathBuf,
335        bytes: SharedBytes,
336    },
337    /// Asks for the picture at `path` in the asset repository. The engine
338    /// answers this interface alone, with `EngineEvent::Picture`.
339    RequestPicture(PathBuf),
340}
341
342/// Something the user interfaces have to react to once.
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub enum EngineEvent {
345    /// The code of this animation (all for `None`) changed: rebuild previews.
346    ShaderReloaded(Option<AssetId<Animation>>),
347    /// An error to show the user.
348    Error(String),
349    /// The picture an interface asked for, or why there is none.
350    Picture {
351        path: PathBuf,
352        picture: Result<SharedBytes, String>,
353    },
354}
355
356/// The sound triggers' levels and the input spectrum, as interfaces need them
357/// to draw effects that follow the music.
358#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
359#[serde(default)]
360pub struct SoundSnapshot {
361    pub levels: BTreeMap<Uuid, f32>,
362    /// The envelope input of every trigger, 0 to 1.
363    pub impulses: BTreeMap<Uuid, f32>,
364    /// `FREQ_BINS` little-endian `f32`s, as effects read them.
365    pub spectrum: SharedBytes,
366    /// The averaged spectrum of the triggers an interface edits, as
367    /// little-endian `f32`s.
368    pub trigger_spectra: BTreeMap<Uuid, SharedBytes>,
369}
370
371impl SoundSnapshot {
372    /// What the trigger editor shows of a running trigger: impulse, level
373    /// and its averaged spectrum (empty unless the trigger is watched).
374    pub fn reading(&self, id: &Uuid) -> Option<(f32, f32, Vec<f32>)> {
375        let level = *self.levels.get(id)?;
376        let impulse = self.impulses.get(id).copied().unwrap_or_default();
377        let spectrum = self
378            .trigger_spectra
379            .get(id)
380            .map(|bytes| {
381                bytes
382                    .0
383                    .as_chunks::<4>()
384                    .0
385                    .iter()
386                    .map(|chunk| f32::from_le_bytes(*chunk))
387                    .collect()
388            })
389            .unwrap_or_default();
390        Some((impulse, level, spectrum))
391    }
392}
393
394impl SoundLevels for SoundSnapshot {
395    fn level(&self, id: &Uuid) -> f32 {
396        self.levels.get(id).copied().unwrap_or(0.0)
397    }
398
399    fn spectrum_bytes(&self) -> [u8; FREQ_BINS * 4] {
400        let mut bytes = [0; FREQ_BINS * 4];
401        let len = self.spectrum.0.len().min(bytes.len());
402        bytes[..len].copy_from_slice(&self.spectrum.0[..len]);
403        bytes
404    }
405}
406
407/// Where a thread's client inputs go.
408type InputRoute = Box<dyn Fn(ClientInput)>;
409
410thread_local! {
411    /// Where client inputs enqueued on this thread go: the interface's
412    /// connection to the engine.
413    static INPUT_ROUTE: std::cell::RefCell<Option<InputRoute>> =
414        const { std::cell::RefCell::new(None) };
415}
416
417/// Sends every client input enqueued on the calling thread to `route`.
418pub fn route_inputs_on_this_thread(route: impl Fn(ClientInput) + 'static) {
419    INPUT_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
420}
421
422impl ClientInput {
423    /// Sends this input to the engine through the calling thread's route.
424    pub fn enqueue(self) {
425        INPUT_ROUTE.with(|cell| match cell.borrow().as_ref() {
426            Some(route) => route(self),
427            None => tracing::warn!("Dropped {self:?}: this thread has no connection to the engine"),
428        });
429    }
430}