diff --git a/Cargo.lock b/Cargo.lock index 487f877..a32ae7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3459,7 +3459,7 @@ dependencies = [ [[package]] name = "tether-artnet-controller" -version = "0.10.0" +version = "0.11.0" dependencies = [ "anyhow", "artnet_protocol", diff --git a/Cargo.toml b/Cargo.toml index 6f8961e..6995547 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tether-artnet-controller" -version = "0.10.0" +version = "0.11.0" edition = "2021" repository = "https://github.com/RandomStudio/tether-artnet-controller" homepage = "https://github.com/RandomStudio/tether-artnet-controller" diff --git a/example.project.json b/example.project.json index 0b1a35b..3702df0 100644 --- a/example.project.json +++ b/example.project.json @@ -1,436 +1,22 @@ { "fixtures": [ - { - "label": "Hero Left", - "configName": "Varytec HERO 340FX", - "offsetChannels": 27, - "modeIndex": 0 - }, - { - "label": "Hero Right", - "configName": "Varytec HERO 340FX", - "offsetChannels": 70, - "modeIndex": 0 - }, { "label": "Hex Left", "configName": "ADJ Vizi Hex Wash 7", - "offsetChannels": 43, + "startChannel": 44, "modeIndex": 0 }, { "label": "Hex Right", "configName": "ADJ Vizi Hex Wash 7", - "offsetChannels": 0, + "startChannel": 1, "modeIndex": 0 } ], - "scenes": [ - { - "label": "Beamsplitting", - "state": { - "Hero Left": { - "beamsplit": { - "ControlValue": 13000 - }, - "brightness": { - "ControlValue": 65535 - }, - "colour": { - "ColourValue": [ - 0, - 0, - 0, - 0 - ] - }, - "pan": { - "ControlValue": 35089 - }, - "tilt": { - "ControlValue": 38185 - }, - "zoom": { - "ControlValue": 65535 - } - }, - "Hero Right": { - "beamsplit": { - "ControlValue": 13000 - }, - "brightness": { - "ControlValue": 37669 - }, - "colour": { - "ColourValue": [ - 0, - 0, - 0, - 0 - ] - }, - "pan": { - "ControlValue": 26317 - }, - "tilt": { - "ControlValue": 35605 - }, - "zoom": { - "ControlValue": 65535 - } - }, - "Hex Left": { - "Colour": { - "ColourValue": [ - 255, - 255, - 224, - 255 - ] - }, - "brightness": { - "ControlValue": 0 - }, - "pan": { - "ControlValue": 0 - }, - "tilt": { - "ControlValue": 34057 - }, - "zoom": { - "ControlValue": 38701 - } - }, - "Hex Right": { - "Colour": { - "ColourValue": [ - 255, - 255, - 224, - 255 - ] - }, - "brightness": { - "ControlValue": 65535 - }, - "pan": { - "ControlValue": 0 - }, - "tilt": { - "ControlValue": 31993 - }, - "zoom": { - "ControlValue": 31477 - } - } - } - }, - { - "label": "ambience", - "state": { - "Hero Left": { - "beamsplit": { - "ControlValue": 10000 - }, - "brightness": { - "ControlValue": 46000 - }, - "colour": { - "ColourValue": [ - 255, - 0, - 245, - 255 - ] - }, - "pan": { - "ControlValue": 37669 - }, - "tilt": { - "ControlValue": 29 - }, - "zoom": { - "ControlValue": 65535 - } - }, - "Hero Right": { - "beamsplit": { - "ControlValue": 8000 - }, - "brightness": { - "ControlValue": 65535 - }, - "colour": { - "ColourValue": [ - 49, - 81, - 255, - 255 - ] - }, - "pan": { - "ControlValue": 4128 - }, - "tilt": { - "ControlValue": 80 - }, - "zoom": { - "ControlValue": 122 - } - }, - "Hex Left": { - "Colour": { - "ColourValue": [ - 255, - 255, - 224, - 255 - ] - }, - "brightness": { - "ControlValue": 35000 - }, - "pan": { - "ControlValue": 17544 - }, - "tilt": { - "ControlValue": 0 - }, - "zoom": { - "ControlValue": 22189 - } - }, - "Hex Right": { - "Colour": { - "ColourValue": [ - 255, - 255, - 224, - 255 - ] - }, - "brightness": { - "ControlValue": 41000 - }, - "pan": { - "ControlValue": 25801 - }, - "tilt": { - "ControlValue": 0 - }, - "zoom": { - "ControlValue": 0 - } - } - } - }, - { - "label": "speaker", - "state": { - "Hero Left": { - "beamsplit": { - "ControlValue": 10000 - }, - "brightness": { - "ControlValue": 65535 - }, - "colour": { - "ColourValue": [ - 37, - 0, - 243, - 255 - ] - }, - "pan": { - "ControlValue": 17028 - }, - "tilt": { - "ControlValue": 24253 - }, - "zoom": { - "ControlValue": 61406 - } - }, - "Hero Right": { - "beamsplit": { - "ControlValue": 5000 - }, - "brightness": { - "ControlValue": 65535 - }, - "colour": { - "ColourValue": [ - 237, - 255, - 0, - 255 - ] - }, - "pan": { - "ControlValue": 46958 - }, - "tilt": { - "ControlValue": 23221 - }, - "zoom": { - "ControlValue": 46958 - } - }, - "Hex Left": { - "Colour": { - "ColourValue": [ - 255, - 255, - 224, - 255 - ] - }, - "brightness": { - "ControlValue": 35000 - }, - "pan": { - "ControlValue": 13932 - }, - "tilt": { - "ControlValue": 25801 - }, - "zoom": { - "ControlValue": 65535 - } - }, - "Hex Right": { - "Colour": { - "ColourValue": [ - 255, - 255, - 224, - 255 - ] - }, - "brightness": { - "ControlValue": 65535 - }, - "pan": { - "ControlValue": 29413 - }, - "tilt": { - "ControlValue": 26317 - }, - "zoom": { - "ControlValue": 41797 - } - } - } - }, - { - "label": "white down", - "state": { - "Hero Left": { - "beamsplit": { - "ControlValue": 0 - }, - "brightness": { - "ControlValue": 65535 - }, - "colour": { - "ColourValue": [ - 0, - 0, - 0, - 0 - ] - }, - "pan": { - "ControlValue": 0 - }, - "tilt": { - "ControlValue": 32509 - }, - "zoom": { - "ControlValue": 45410 - } - }, - "Hero Right": { - "beamsplit": { - "ControlValue": 0 - }, - "brightness": { - "ControlValue": 65535 - }, - "colour": { - "ColourValue": [ - 0, - 0, - 0, - 0 - ] - }, - "pan": { - "ControlValue": 0 - }, - "tilt": { - "ControlValue": 30445 - }, - "zoom": { - "ControlValue": 39217 - } - }, - "Hex Left": { - "Colour": { - "ColourValue": [ - 255, - 255, - 224, - 255 - ] - }, - "brightness": { - "ControlValue": 65535 - }, - "pan": { - "ControlValue": 0 - }, - "tilt": { - "ControlValue": 34057 - }, - "zoom": { - "ControlValue": 38701 - } - }, - "Hex Right": { - "Colour": { - "ColourValue": [ - 255, - 255, - 224, - 255 - ] - }, - "brightness": { - "ControlValue": 65535 - }, - "pan": { - "ControlValue": 0 - }, - "tilt": { - "ControlValue": 31993 - }, - "zoom": { - "ControlValue": 31477 - } - } - } - } - ], + "scenes": [], "midiConfig": { "controllerStart": 48, "noteStart": 49 }, - "artnetConfig": { - "Unicast": [ - "127.0.0.1", - "127.0.0.1" - ] - } + "artnetConfig": "Broadcast" } \ No newline at end of file diff --git a/fixtures/Godox_Knowled_F400Bi.json b/fixtures/Godox_Knowled_F400Bi.json new file mode 100644 index 0000000..4e97303 --- /dev/null +++ b/fixtures/Godox_Knowled_F400Bi.json @@ -0,0 +1,35 @@ +{ + "name": "Godox KNOWLED F400Bi", + "reference": "https://www.godox.com/static/upload/file/20231205/1701740691518042.pdf", + "modes": [ + { + "name": "CCT 8bit Mode", + "mappings": [ + { + "channel": 1, + "label": "DIM", + "notes": "Brightness" + }, + { + "channel": 2, + "label": "CCT", + "notes": "Colour temperature 2700K-8500K" + } + ], + "macros": [ + { + "control": { + "label": "brightness", + "channels": [{ "LoRes": 1 }] + } + }, + { + "control": { + "label": "colourTemp", + "channels": [{ "LoRes": 2 }] + } + } + ] + } + ] +} diff --git a/fixtures/Godox_Knowled_F600Bi.json b/fixtures/Godox_Knowled_F600Bi.json new file mode 100644 index 0000000..1804e13 --- /dev/null +++ b/fixtures/Godox_Knowled_F600Bi.json @@ -0,0 +1,35 @@ +{ + "name": "Godox KNOWLED F600Bi", + "reference": "https://www.godox.com/static/upload/file/20230713/1689216265265540.pdf", + "modes": [ + { + "name": "CCT 8bit Mode", + "mappings": [ + { + "channel": 1, + "label": "DIM", + "notes": "Brightness" + }, + { + "channel": 2, + "label": "CCT", + "notes": "Colour temperature 2700K-8500K" + } + ], + "macros": [ + { + "control": { + "label": "brightness", + "channels": [{ "LoRes": 1 }] + } + }, + { + "control": { + "label": "colourTemp", + "channels": [{ "LoRes": 2 }] + } + } + ] + } + ] +} diff --git a/src/all_fixtures.json b/src/all_fixtures.json index 1afd4e6..f8ff7e4 100644 --- a/src/all_fixtures.json +++ b/src/all_fixtures.json @@ -1373,6 +1373,41 @@ } ] } +,{ + "name": "Godox KNOWLED F400Bi", + "reference": "https://www.godox.com/static/upload/file/20231205/1701740691518042.pdf", + "modes": [ + { + "name": "CCT 8bit Mode", + "mappings": [ + { + "channel": 1, + "label": "DIM", + "notes": "Brightness" + }, + { + "channel": 2, + "label": "CCT", + "notes": "Colour temperature 2700K-8500K" + } + ], + "macros": [ + { + "control": { + "label": "brightness", + "channels": [{ "LoRes": 1 }] + } + }, + { + "control": { + "label": "colourTemp", + "channels": [{ "LoRes": 2 }] + } + } + ] + } + ] +} ,{ "name": "Cameo Opus S5", "reference": "https://www.cameolight.com/de/loesungen/dj-musiker/bewegtes-licht/moving-heads/25591/movo-beam-200#detail-description", @@ -2643,6 +2678,41 @@ } ] } +,{ + "name": "Godox KNOWLED F600Bi", + "reference": "https://www.godox.com/static/upload/file/20230713/1689216265265540.pdf", + "modes": [ + { + "name": "CCT 8bit Mode", + "mappings": [ + { + "channel": 1, + "label": "DIM", + "notes": "Brightness" + }, + { + "channel": 2, + "label": "CCT", + "notes": "Colour temperature 2700K-8500K" + } + ], + "macros": [ + { + "control": { + "label": "brightness", + "channels": [{ "LoRes": 1 }] + } + }, + { + "control": { + "label": "colourTemp", + "channels": [{ "LoRes": 2 }] + } + } + ] + } + ] +} ,{ "name": "ADJ Vizi Hex Wash 7", "reference": "http://adjmedia.s3-website-eu-west-1.amazonaws.com/manuals/ADJ%20Vizi%20Hex%20Wash7%20User%20Manual.pdf", diff --git a/src/artnet.rs b/src/artnet.rs index 5776908..fe3a028 100644 --- a/src/artnet.rs +++ b/src/artnet.rs @@ -108,7 +108,7 @@ impl ArtNetInterface { match c { ChannelWithResolution::LoRes(single_channel) => { let target_channel = - (*single_channel - 1 + f.offset_channels) as usize; + (*single_channel - 1 + f.start_channel - 1) as usize; let scaled_value = ((control_macro.current_value as f32 / u16::MAX as f32) * 255.0) @@ -126,9 +126,11 @@ impl ArtNetInterface { // Assume coarse+fine 16-bit values are "big endian" (be): let [b1, b2] = control_macro.current_value.to_be_bytes(); // coarse channel: - self.channels[(*c1 - 1 + f.offset_channels) as usize] = b1; + self.channels[(*c1 - 1 + f.start_channel - 1) as usize] = + b1; // fine channel: - self.channels[(*c2 - 1 + f.offset_channels) as usize] = b2; + self.channels[(*c2 - 1 + f.start_channel - 1) as usize] = + b2; } } } @@ -146,15 +148,15 @@ impl ArtNetInterface { // Convert all rgb values from "opaque" version (ignoring alpha) let opaque = colour_macro.current_value.to_opaque(); for c in red.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = + self.channels[(*c - 1 + f.start_channel - 1) as usize] = opaque.r(); } for c in green.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = + self.channels[(*c - 1 + f.start_channel - 1) as usize] = opaque.g(); } for c in blue.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = + self.channels[(*c - 1 + f.start_channel - 1) as usize] = opaque.b(); } @@ -163,7 +165,7 @@ impl ArtNetInterface { // alpha = 0% => RGB the same, but mix in full white let white_inverse = 255 - colour_macro.current_value.a(); for c in white.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = + self.channels[(*c - 1 + f.start_channel - 1) as usize] = white_inverse; } } @@ -182,17 +184,17 @@ impl ArtNetInterface { for channel in cyan.iter() { self.channels - [(*channel - 1 + f.offset_channels) as usize] = + [(*channel - 1 + f.start_channel - 1) as usize] = 255 - opaque.r(); } for channel in magenta.iter() { self.channels - [(*channel - 1 + f.offset_channels) as usize] = + [(*channel - 1 + f.start_channel - 1) as usize] = 255 - opaque.g(); } for channel in yellow.iter() { self.channels - [(*channel - 1 + f.offset_channels) as usize] = + [(*channel - 1 + f.start_channel - 1) as usize] = 255 - opaque.b(); } } @@ -209,9 +211,9 @@ impl ArtNetInterface { // let [b1, b2] = value.to_be_bytes(); // // coarse channel: - // self.channels[(*c1 - 1 + f.offset_channels) as usize] = b1; + // self.channels[(*c1 + f.offset_channels) as usize] = b1; // // fine channel: - // self.channels[(*c2 - 1 + f.offset_channels) as usize] = b2; + // self.channels[(*c2 + f.offset_channels) as usize] = b2; // } todo!("Not yet implemented; current Colour Macros are 8-bit channels only!"); } @@ -223,15 +225,15 @@ impl ArtNetInterface { // Convert all rgb values from "opaque" version (ignoring alpha) let opaque = colour_macro.current_value.to_opaque(); for c in red.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = + self.channels[(*c - 1 + f.start_channel - 1) as usize] = opaque.r(); } for c in green.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = + self.channels[(*c - 1 + f.start_channel - 1) as usize] = opaque.g(); } for c in blue.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = + self.channels[(*c - 1 + f.start_channel - 1) as usize] = opaque.b(); } // Ignore lime, since we don't represent it in standard colour macros diff --git a/src/model.rs b/src/model.rs index 5d1318a..8b0a160 100644 --- a/src/model.rs +++ b/src/model.rs @@ -111,7 +111,7 @@ impl Model { for fixture in fixtures_clone.iter() { let current_mode = &fixture.config.modes[0]; for m in ¤t_mode.mappings { - let channel_index = m.channel + fixture.offset_channels - 1; + let channel_index = m.channel + fixture.start_channel - 1; channels_assigned[channel_index as usize] = true; } } @@ -593,7 +593,7 @@ impl Model { let current_mode = &fixture.config.active_mode; for m in ¤t_mode.mappings { if let Some(default_value) = m.home { - let channel_index = m.channel + fixture.offset_channels - 1; + let channel_index = (m.channel - 1) + (fixture.start_channel - 1); self.channels_state[channel_index as usize] = default_value; } } diff --git a/src/project/fixture.rs b/src/project/fixture.rs index 44a660c..7210840 100644 --- a/src/project/fixture.rs +++ b/src/project/fixture.rs @@ -13,7 +13,8 @@ pub struct FixtureInstance { pub label: String, /// The exact match for the fixture name as it appears in the fixture config JSON pub config_name: String, - pub offset_channels: u16, + /// The **one-indexed** starting channel for this fixture instance + pub start_channel: u16, #[serde(default)] pub mode_index: usize, #[serde(skip)] @@ -47,7 +48,7 @@ impl From<&FixtureConfig> for FixtureInstance { FixtureInstance { label: format!("My {}", config.name), config_name: String::from(&config.name), - offset_channels: 0, + start_channel: 0, mode_index: 0, config: config.clone(), } diff --git a/src/ui/fixture_controls.rs b/src/ui/fixture_controls.rs index 840ceee..d379dcb 100644 --- a/src/ui/fixture_controls.rs +++ b/src/ui/fixture_controls.rs @@ -17,7 +17,7 @@ pub fn render_fixture_controls(model: &mut Model, ui: &mut Ui) { ui.horizontal(|ui| { ui.label("Offset channels:"); ui.add( - DragValue::new(&mut new_fixture.offset_channels) + DragValue::new(&mut new_fixture.start_channel) .clamp_range(0..=512) .speed(1), ); @@ -86,7 +86,7 @@ fn fixture_controls_in_project(model: &mut Model, ui: &mut Ui) { ui.horizontal(|ui| { ui.label("Offset channels:"); ui.add( - DragValue::new(&mut fixture.offset_channels) + DragValue::new(&mut fixture.start_channel) .clamp_range(0..=512) .speed(1), ); @@ -97,35 +97,34 @@ fn fixture_controls_in_project(model: &mut Model, ui: &mut Ui) { ui.heading("Mappings"); Grid::new(format!("mappings_{}", i)) - .num_columns(3) + .num_columns(4) .show(ui, |ui| { for m in ¤t_mode.mappings { - let channel_index = m.channel + fixture.offset_channels - 1; + let channel_zero_index = (m.channel - 1) + (fixture.start_channel - 1); + let channel_one_index = channel_zero_index + 1; ui.horizontal(|ui| { ui.label(&m.label); - if let Some(notes) = &m.notes { - ui.label("ℹ").on_hover_text(format!( - "#Channel {}: {}", - channel_index + 1, - notes - )); - } + ui.label("ℹ").on_hover_text(format!( + "Fixture CH{} => DMX CH{} (idx [{}])", + m.channel, channel_one_index, channel_zero_index + )); }); if ui .add(Slider::new( - &mut model.channels_state[(channel_index) as usize], + &mut model.channels_state[(channel_zero_index) as usize], 0..=255, )) .changed() { model.apply_macros = false; }; + ui.label(m.notes.as_deref().unwrap_or(" ")); if let Some(range_sections) = &m.ranges { ui.label("Mode/Programme:"); let current_range = range_sections.iter().find(|x| { let [start, end] = x.range; - model.channels_state[(channel_index) as usize] >= start - && model.channels_state[(channel_index) as usize] <= end + model.channels_state[(channel_zero_index) as usize] >= start + && model.channels_state[(channel_zero_index) as usize] <= end }); match current_range { Some(r) => { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 55a258e..d3b5e11 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -217,9 +217,10 @@ pub fn render_sliders(model: &mut Model, ui: &mut Ui) { .auto_shrink([false, false]) .show(ui, |ui| { Grid::new("sliders").num_columns(2).show(ui, |ui| { - for i in 0..CHANNELS_PER_UNIVERSE { - let text = format!("Channel #{}", i + 1); - let is_assigned = model.channels_assigned[i as usize]; + for i in 0..(CHANNELS_PER_UNIVERSE - 1) { + let one_indexed_channel = i + 1; + let text = format!("Channel #{}", one_indexed_channel); + let is_assigned = model.channels_assigned[one_indexed_channel as usize]; ui.label(RichText::new(text).color(if is_assigned { Color32::GREEN } else {