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