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