gled-core 2.34.0

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
use std::sync::Arc;

use crate::storage::{
    asset::{
        Asset,
        project::Project,
        scene::{Scene, grid::GridLocation, instance::SceneInstance},
    },
    asset_id::AssetId,
    collections::Collections,
};
use deku::prelude::*;
use kanal::Receiver;

const ARTNET_CONTROL_SLOTS: usize = 10;
const SCENE_SPECIFIC_PARAMETERS: usize = 10;

// INVARIANT: All fields are required for correct binary protocol layout (DekuRead from ArtNet DMX bytes).
// Not all fields are used by the current business logic, but they must be present for
// correct offset calculations during deserialization.
#[allow(dead_code)]
#[derive(Debug, DekuRead, Default, Copy, Clone)]
pub(crate) struct ArtnetSceneControlState {
    pub(crate) scene_index: u8, // 0: disabled
    /// Was the group index; remote layout selection was dropped, so the
    /// tile's own layout and mode always win.
    pub(crate) _reserved: u8,
    pub(crate) opacity: u8,
    pub(crate) offset: u8,
    pub(crate) speed_multiplier: u8, // x -> 2^x; 126 = 1/2, 127 = 1, 128 = 2, ...

    pub(crate) color_mode: u8, // GledDefault = 0..=84, ColorOverride = 85..=170, PalletOverride = 171..
    pub(crate) pallet_override: u8,
    //pub color_override_primary: Color,
    // pub color_override_secondary: Color,
    pub(crate) dummy: [u8; 6],
    pub(crate) scene_specific_parameters: [u8; SCENE_SPECIFIC_PARAMETERS],
}

pub(crate) struct ExternalControlState {
    prev_values: [ArtnetSceneControlState; ARTNET_CONTROL_SLOTS],
    artnet_control_receiver: Receiver<Vec<u8>>,
}

impl ExternalControlState {
    pub(crate) fn new(artnet_control_receiver: Receiver<Vec<u8>>) -> Self {
        Self {
            prev_values: [ArtnetSceneControlState::default(); ARTNET_CONTROL_SLOTS],
            artnet_control_receiver,
        }
    }

    /// Forgets the control values seen so far, so the next packet counts as
    /// new for the next project.
    pub(crate) fn reset(&mut self) {
        self.prev_values = [ArtnetSceneControlState::default(); ARTNET_CONTROL_SLOTS];
    }

    /// Applies the Art-Net control packets that arrived. Each one sets the
    /// scene instances it controls, so the shared project is copied on a
    /// tick with packets (`Arc::make_mut`) and left shared on one without.
    /// While the project has control switched off, packets are dropped, so
    /// none of them is applied later when it is switched on.
    pub(crate) fn process_events(
        &mut self,
        project: &mut Option<Arc<Project>>,
        collections: &Collections,
    ) {
        let active = project
            .as_deref()
            .is_some_and(|project| project.artnet_control_config.active);
        while let Ok(Some(dmx_data)) = self.artnet_control_receiver.try_recv() {
            if !active {
                continue;
            }
            self.process_dmx(project.as_mut().map(Arc::make_mut), &dmx_data, collections);
        }
    }

    fn process_dmx(
        &mut self,
        mut project: Option<&mut Project>,
        dmx_data: &[u8],
        collections: &Collections,
    ) {
        let mut remaining = (dmx_data, 0);
        let mut scene_state: ArtnetSceneControlState;

        for i in 0..ARTNET_CONTROL_SLOTS {
            (remaining, scene_state) = match ArtnetSceneControlState::from_bytes(remaining) {
                Ok(result) => result,
                Err(_) => break,
            };
            if let Some(project) = project.as_deref_mut() {
                let grid_location = project
                    .grid_location_from_continuous_index(i, &GridLocation { row: 0, col: 0 });
                let scene_instance = project.scenes_instances_grid.get_mut(&grid_location);

                let asset_id =
                    Asset::get_asset_from_index(scene_state.scene_index as usize, collections)
                        .map(|asset| asset.id);
                let scene_data = match (asset_id, scene_instance) {
                    (None, _) => {
                        project.scenes_instances_grid.remove(&grid_location);
                        None
                    }
                    (Some(asset_id), None) => {
                        project.add_scene_instance(
                            grid_location,
                            SceneInstance::from_scene_id(asset_id, collections),
                        );
                        project
                            .scenes_instances_grid
                            .get_mut(&grid_location)
                            .map(|scene_instance| (asset_id, scene_instance))
                    }
                    (Some(asset_id), Some(scene_instance)) => Some((asset_id, scene_instance)),
                };
                if let Some((asset_id, scene_instance)) = scene_data {
                    // check if the scene index has changed
                    let prev_state = &self.prev_values[i];
                    update_scene_instance(
                        &mut scene_state,
                        asset_id,
                        scene_instance,
                        prev_state,
                        collections,
                    );
                }
            } else {
                tracing::warn!("Received Artnet DMX data, but no project is loaded");
            }
            self.prev_values[i] = scene_state;
        }
    }
}

fn update_scene_instance(
    scene_state: &mut ArtnetSceneControlState,
    asset_id: AssetId<Scene>,
    scene_instance: &mut SceneInstance,
    prev_state: &ArtnetSceneControlState,
    collections: &Collections,
) {
    if prev_state.scene_index != scene_state.scene_index {
        scene_instance.scene = Asset::get(asset_id, collections)
            .unwrap_or_default()
            .data
            .clone();
    }
    scene_instance.active = scene_state.opacity != 0;
    scene_instance.opacity.multiplier = scene_state.opacity as f32 / 255.0;
    scene_instance.beat_progression_offset.multiplier = scene_state.speed_multiplier as f32 / 127.0;
    // TODO rewrite
    //scene_instance
    //    .effect_states
    //    .iter_mut()
    //    .for_each(|effect_state| {
    //        effect_state.speed_exponent = scene_state.speed_multiplier as i32;
    //    })
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A project with a scene in the cell the first control slot drives.
    fn project_with_a_scene(control_active: bool) -> Arc<Project> {
        let mut project = Project::default();
        project.artnet_control_config.active = control_active;
        project.add_scene_instance(
            GridLocation::default(),
            SceneInstance::from_scene_id(AssetId::default(), &Collections::default()),
        );
        Arc::new(project)
    }

    /// An all-zero packet empties every controlled cell when applied.
    fn send_empty_packet() -> ExternalControlState {
        let (sender, receiver) = kanal::unbounded();
        sender.send(vec![0; 512]).expect("the receiver is alive");
        ExternalControlState::new(receiver)
    }

    #[test]
    fn packets_are_applied_while_control_is_on() {
        let mut state = send_empty_packet();
        let mut project = Some(project_with_a_scene(true));
        state.process_events(&mut project, &Collections::default());
        let project = project.expect("the project stays loaded");
        assert!(project.scenes_instances_grid.is_empty());
    }

    #[test]
    fn packets_are_dropped_while_control_is_off() {
        let mut state = send_empty_packet();
        let mut project = Some(project_with_a_scene(false));
        state.process_events(&mut project, &Collections::default());
        let project = project.expect("the project stays loaded");
        assert_eq!(project.scenes_instances_grid.len(), 1);
        assert!(state.artnet_control_receiver.is_empty());
    }
}