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