From 3766a96a29decf233fbb0dcef05280a6485b3e4e Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Thu, 22 May 2025 15:18:46 +0200 Subject: [PATCH 1/2] 1-indexed channels working --- Cargo.lock | 2 +- Cargo.toml | 2 +- example.project.json | 419 +------------------------------------ src/artnet.rs | 41 ++-- src/model.rs | 4 +- src/project/fixture.rs | 5 +- src/ui/fixture_controls.rs | 13 +- src/ui/mod.rs | 7 +- 8 files changed, 37 insertions(+), 456 deletions(-) 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..65b0532 100644 --- a/example.project.json +++ b/example.project.json @@ -1,436 +1,27 @@ { "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" + "10.0.0.105", + "10.0.0.99" ] } } \ No newline at end of file diff --git a/src/artnet.rs b/src/artnet.rs index 5776908..497011f 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 + f.start_channel - 1) as usize; let scaled_value = ((control_macro.current_value as f32 / u16::MAX as f32) * 255.0) @@ -120,15 +120,15 @@ impl ArtNetInterface { control_macro.current_value, scaled_value ); - self.channels[target_channel] = scaled_value; + self.channels[target_channel - 1] = scaled_value; } ChannelWithResolution::HiRes((c1, c2)) => { // 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 + f.start_channel - 2) as usize] = b1; // fine channel: - self.channels[(*c2 - 1 + f.offset_channels) as usize] = b2; + self.channels[(*c2 + f.start_channel - 2) as usize] = b2; } } } @@ -146,16 +146,13 @@ 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] = - opaque.r(); + self.channels[(*c + f.start_channel) as usize] = opaque.r(); } for c in green.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = - opaque.g(); + self.channels[(*c + f.start_channel) as usize] = opaque.g(); } for c in blue.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = - opaque.b(); + self.channels[(*c + f.start_channel) as usize] = opaque.b(); } // Use inverse of alpha for "white mix" , i.e. @@ -163,7 +160,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 + f.start_channel) as usize] = white_inverse; } } @@ -181,18 +178,15 @@ impl ArtNetInterface { let opaque = colour_macro.current_value.to_opaque(); for channel in cyan.iter() { - self.channels - [(*channel - 1 + f.offset_channels) as usize] = + self.channels[(*channel + f.start_channel) as usize] = 255 - opaque.r(); } for channel in magenta.iter() { - self.channels - [(*channel - 1 + f.offset_channels) as usize] = + self.channels[(*channel + f.start_channel) as usize] = 255 - opaque.g(); } for channel in yellow.iter() { - self.channels - [(*channel - 1 + f.offset_channels) as usize] = + self.channels[(*channel + f.start_channel) as usize] = 255 - opaque.b(); } } @@ -209,9 +203,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,16 +217,13 @@ 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] = - opaque.r(); + self.channels[(*c + f.start_channel) as usize] = opaque.r(); } for c in green.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = - opaque.g(); + self.channels[(*c + f.start_channel) as usize] = opaque.g(); } for c in blue.iter() { - self.channels[(*c - 1 + f.offset_channels) as usize] = - opaque.b(); + self.channels[(*c + f.start_channel) 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..28a3a81 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 + fixture.start_channel - 2; 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..453a5b7 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), ); @@ -100,15 +100,12 @@ fn fixture_controls_in_project(model: &mut Model, ui: &mut Ui) { .num_columns(3) .show(ui, |ui| { for m in ¤t_mode.mappings { - let channel_index = m.channel + fixture.offset_channels - 1; + let channel_index = m.channel + fixture.start_channel - 2; 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!("#Channel {}: {}", channel_index, notes)); } }); if ui 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 { From 4f9ec945dd079bd57ab651416823e8732e95d832 Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 23 May 2025 10:29:55 +0200 Subject: [PATCH 2/2] appears to be consistent channel/index calculations throughout --- example.project.json | 7 +------ src/artnet.rs | 39 ++++++++++++++++++++++++-------------- src/model.rs | 2 +- src/ui/fixture_controls.rs | 20 ++++++++++--------- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/example.project.json b/example.project.json index 65b0532..3702df0 100644 --- a/example.project.json +++ b/example.project.json @@ -18,10 +18,5 @@ "controllerStart": 48, "noteStart": 49 }, - "artnetConfig": { - "Unicast": [ - "10.0.0.105", - "10.0.0.99" - ] - } + "artnetConfig": "Broadcast" } \ No newline at end of file diff --git a/src/artnet.rs b/src/artnet.rs index 497011f..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 + f.start_channel - 1) 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) @@ -120,15 +120,17 @@ impl ArtNetInterface { control_macro.current_value, scaled_value ); - self.channels[target_channel - 1] = scaled_value; + self.channels[target_channel] = scaled_value; } ChannelWithResolution::HiRes((c1, c2)) => { // 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 + f.start_channel - 2) as usize] = b1; + self.channels[(*c1 - 1 + f.start_channel - 1) as usize] = + b1; // fine channel: - self.channels[(*c2 + f.start_channel - 2) as usize] = b2; + self.channels[(*c2 - 1 + f.start_channel - 1) as usize] = + b2; } } } @@ -146,13 +148,16 @@ 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 + f.start_channel) as usize] = opaque.r(); + self.channels[(*c - 1 + f.start_channel - 1) as usize] = + opaque.r(); } for c in green.iter() { - self.channels[(*c + f.start_channel) as usize] = opaque.g(); + self.channels[(*c - 1 + f.start_channel - 1) as usize] = + opaque.g(); } for c in blue.iter() { - self.channels[(*c + f.start_channel) as usize] = opaque.b(); + self.channels[(*c - 1 + f.start_channel - 1) as usize] = + opaque.b(); } // Use inverse of alpha for "white mix" , i.e. @@ -160,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 + f.start_channel) as usize] = + self.channels[(*c - 1 + f.start_channel - 1) as usize] = white_inverse; } } @@ -178,15 +183,18 @@ impl ArtNetInterface { let opaque = colour_macro.current_value.to_opaque(); for channel in cyan.iter() { - self.channels[(*channel + f.start_channel) as usize] = + self.channels + [(*channel - 1 + f.start_channel - 1) as usize] = 255 - opaque.r(); } for channel in magenta.iter() { - self.channels[(*channel + f.start_channel) as usize] = + self.channels + [(*channel - 1 + f.start_channel - 1) as usize] = 255 - opaque.g(); } for channel in yellow.iter() { - self.channels[(*channel + f.start_channel) as usize] = + self.channels + [(*channel - 1 + f.start_channel - 1) as usize] = 255 - opaque.b(); } } @@ -217,13 +225,16 @@ 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 + f.start_channel) as usize] = opaque.r(); + self.channels[(*c - 1 + f.start_channel - 1) as usize] = + opaque.r(); } for c in green.iter() { - self.channels[(*c + f.start_channel) as usize] = opaque.g(); + self.channels[(*c - 1 + f.start_channel - 1) as usize] = + opaque.g(); } for c in blue.iter() { - self.channels[(*c + f.start_channel) as usize] = opaque.b(); + 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 28a3a81..8b0a160 100644 --- a/src/model.rs +++ b/src/model.rs @@ -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.start_channel - 2; + let channel_index = (m.channel - 1) + (fixture.start_channel - 1); self.channels_state[channel_index as usize] = default_value; } } diff --git a/src/ui/fixture_controls.rs b/src/ui/fixture_controls.rs index 453a5b7..d379dcb 100644 --- a/src/ui/fixture_controls.rs +++ b/src/ui/fixture_controls.rs @@ -97,32 +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.start_channel - 2; + 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, 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) => {