Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions conductor-core/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,9 @@ impl DeviceMatcher {
/// against the provided identity. Returns false if identity is absent.
/// For all other matchers, delegates to `matches(port_name)`.
///
/// Note: `PortResolver::resolve()` calls `matches_with_usb()` (not this method).
/// SysEx Identity metadata is not yet passed to the resolver. `SysExIdentity`
/// matchers will not affect binding resolution until the resolver is updated.
/// Note: `PortResolver::resolve()` dispatches by matcher type: `SysExIdentity`
/// matchers use this method with `port.sysex_identity` from `PortInfo`; all other
/// matchers use `matches_with_usb()`. Active in resolution when identity data exists.
pub fn matches_with_sysex(
&self,
port_name: &str,
Expand Down
23 changes: 21 additions & 2 deletions conductor-core/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
//! Pure logic: `(Vec<PortInfo>, Vec<DeviceIdentityConfig>) → Vec<BindingResult>`

use crate::config::DeviceIdentityConfig;
use crate::device_intelligence::sysex_identity::SysExIdentity;
use crate::identity::DeviceId;

/// Information about an available MIDI port.
///
/// Prefer constructors (`PortInfo::new()`, `new_with_usb()`) over struct literals
/// to avoid breakage when new metadata fields are added.
#[derive(Debug, Clone)]
pub struct PortInfo {
pub name: String,
Expand All @@ -17,16 +21,19 @@ pub struct PortInfo {
pub vendor_id: Option<u16>,
/// USB Product ID (populated when available from platform APIs)
pub product_id: Option<u16>,
/// SysEx Identity Reply data (populated by probing, when available)
pub sysex_identity: Option<SysExIdentity>,
}

impl PortInfo {
/// Construct a PortInfo when USB IDs are unknown or unavailable.
/// Construct a PortInfo when no metadata is available.
pub fn new(name: impl Into<String>, index: usize) -> Self {
Self {
name: name.into(),
index,
vendor_id: None,
product_id: None,
sysex_identity: None,
}
}

Expand All @@ -42,6 +49,7 @@ impl PortInfo {
index,
vendor_id: Some(vendor_id),
product_id: Some(product_id),
sysex_identity: None,
}
}
}
Expand Down Expand Up @@ -92,7 +100,18 @@ impl PortResolver {
}
// Check all matchers on this identity; take highest specificity match
for matcher in &identity.matchers {
if matcher.matches_with_usb(&port.name, port.vendor_id, port.product_id) {
// Use the appropriate matching method based on matcher type
// to avoid redundant name checks (matches_with_usb handles
// name-based and USB matchers; matches_with_sysex only for SysEx)
let matched = if matches!(
matcher,
crate::identity::DeviceMatcher::SysExIdentity { .. }
) {
matcher.matches_with_sysex(&port.name, port.sysex_identity.as_ref())
} else {
matcher.matches_with_usb(&port.name, port.vendor_id, port.product_id)
};
if matched {
let specificity = matcher.specificity();
if best_match
.as_ref()
Expand Down
58 changes: 58 additions & 0 deletions conductor-core/tests/resolver_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,61 @@ fn test_name_matcher_still_works_with_usb_fields() {
matches!(&results[0], BindingResult::Bound { device_id, .. } if device_id.as_str() == "pads")
);
}

// #752: SysExIdentity matcher works in PortResolver when identity data provided
#[test]
fn test_sysex_identity_matches_with_metadata() {
use conductor_core::device_intelligence::sysex_identity::SysExIdentity;

let mut port = PortInfo::new("Generic MIDI Port", 0);
port.sysex_identity = Some(SysExIdentity {
manufacturer_id: vec![0x42], // KORG
family: 0x0034,
model: 0x0001,
version: [1, 0, 0, 0],
});
let ports = vec![port];
let identities = vec![DeviceIdentityConfig {
alias: "korg".to_string(),
matchers: vec![DeviceMatcher::SysExIdentity {
manufacturer_id: vec![0x42],
family: Some(0x0034),
model: None,
}],
description: None,
enabled: true,
input: None,
output: None,
protocol: None,
channels: vec![],
}];

let results = PortResolver::resolve(&ports, &identities);
assert_eq!(results.len(), 1);
assert!(
matches!(&results[0], BindingResult::Bound { device_id, .. } if device_id.as_str() == "korg")
);
}

#[test]
fn test_sysex_identity_no_match_without_metadata() {
let ports = vec![PortInfo::new("Generic MIDI Port", 0)];
let identities = vec![DeviceIdentityConfig {
alias: "korg".to_string(),
matchers: vec![DeviceMatcher::SysExIdentity {
manufacturer_id: vec![0x42],
family: None,
model: None,
}],
description: None,
enabled: true,
input: None,
output: None,
protocol: None,
channels: vec![],
}];

let results = PortResolver::resolve(&ports, &identities);
assert_eq!(results.len(), 1);
assert!(matches!(&results[0], BindingResult::Unbound { .. }));
}
Loading