feat(#760): Wire UsbIdentifier into PortResolver (#753)#776
feat(#760): Wire UsbIdentifier into PortResolver (#753)#776amiable-dev merged 7 commits intomainfrom
Conversation
Extend PortInfo with vendor_id/product_id and update PortResolver to use matches_with_usb() instead of matches(). UsbIdentifier matchers now resolve when USB metadata is provided. - Add vendor_id: Option<u16>, product_id: Option<u16> to PortInfo - PortResolver::resolve() calls matches_with_usb() for all matchers - Name-based matchers continue working unchanged (fall through) - All daemon PortInfo constructors pass None (metadata from platform APIs later) - TDD: 3 new tests (USB match, no-match without metadata, name matcher compat) 14 resolver tests pass (11 existing + 3 new). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR wires the existing USB VID/PID matcher path into the runtime binding resolver by extending PortInfo with optional USB metadata and having PortResolver use matches_with_usb(...) during identity resolution.
Changes:
- Extend
conductor_core::resolver::PortInfowithvendor_id/product_idfields. - Update
PortResolver::resolve()to useDeviceMatcher::matches_with_usb(...)soUsbIdentifiercan match when metadata is present. - Add resolver tests covering USB matching with/without metadata and ensuring name-based matcher behavior remains unchanged; update daemon
PortInfoconstruction sites shown in the diff to populateNone.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| conductor-daemon/src/input_manager.rs | Updates PortInfo construction to include vendor_id/product_id: None (metadata plumbing deferred). |
| conductor-core/src/resolver.rs | Adds USB metadata to PortInfo and switches resolver matching to matches_with_usb(...). |
| conductor-core/tests/resolver_test.rs | Updates existing tests for new PortInfo fields and adds 3 tests for USB matcher behavior and backward compatibility. |
| /// USB Product ID (populated when available from platform APIs) | ||
| pub product_id: Option<u16>, | ||
| } | ||
|
|
There was a problem hiding this comment.
PortInfo gained required vendor_id/product_id fields, but there are still PortInfo { name, index } literals elsewhere in the workspace (e.g. conductor-daemon/tests/hot_plug_test.rs, conductor-daemon/tests/multi_device_test.rs, conductor-core/tests/proptest_multidevice.rs). This will fail to compile cargo test --workspace unless those call sites are updated (likely setting both fields to None, or adding a PortInfo::new(...)/Default helper to reduce churn).
| impl PortInfo { | |
| /// Construct a `PortInfo` when USB IDs are unknown or unavailable. | |
| pub fn new(name: String, index: usize) -> Self { | |
| Self { | |
| name, | |
| index, | |
| vendor_id: None, | |
| product_id: None, | |
| } | |
| } | |
| } | |
| impl Default for PortInfo { | |
| fn default() -> Self { | |
| Self { | |
| name: String::new(), | |
| index: 0, | |
| vendor_id: None, | |
| product_id: None, | |
| } | |
| } | |
| } |
| for matcher in &identity.matchers { | ||
| if matcher.matches(&port.name) { | ||
| if matcher.matches_with_usb(&port.name, port.vendor_id, port.product_id) { | ||
| let specificity = matcher.specificity(); |
There was a problem hiding this comment.
Now that PortResolver::resolve() calls matches_with_usb(...), the doc comment on DeviceMatcher::matches_with_usb() (in conductor-core/src/identity.rs) is outdated where it says the resolver still calls matches(port_name) and needs updating for #753. Please update that comment to avoid misleading future changes.
…pdate doc - Add PortInfo::new(name, index) convenience constructor - Fix PortInfo constructors in hot_plug_test, multi_device_test, proptest - Update matches_with_usb() doc: PortResolver now calls this method Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| port_infos.push(PortInfo { | ||
| name, | ||
| index, | ||
| vendor_id: None, | ||
| product_id: None, | ||
| }); |
There was a problem hiding this comment.
Now that PortInfo has vendor_id/product_id, this struct literal is duplicated in multiple places just to set those fields to None. Since PortInfo::new(name, index) was added, consider using it here to reduce boilerplate and avoid future churn if more fields are added.
| port_infos.push(PortInfo { | |
| name, | |
| index, | |
| vendor_id: None, | |
| product_id: None, | |
| }); | |
| port_infos.push(PortInfo::new(name, index)); |
| port_infos.push(PortInfo { | ||
| name, | ||
| index, | ||
| vendor_id: None, | ||
| product_id: None, | ||
| }); |
There was a problem hiding this comment.
Same as above: prefer PortInfo::new(name, index) (or another constructor/builder) instead of repeating vendor_id: None, product_id: None at each call site.
| port_infos.push(PortInfo { | |
| name, | |
| index, | |
| vendor_id: None, | |
| product_id: None, | |
| }); | |
| port_infos.push(PortInfo::new(name, index)); |
conductor-core/src/identity.rs
Outdated
| /// Note: The `PortResolver` currently calls `matches(port_name)`, not this method. | ||
| /// To use `UsbIdentifier` matchers in binding resolution, the resolver must be | ||
| /// updated to pass USB metadata from `PortInfo`. See #753. | ||
| /// Note: `PortResolver::resolve()` calls this method (since PR #776), passing |
There was a problem hiding this comment.
The doc comment references “since PR #776”, but this PR is wiring UsbIdentifier into the resolver now (and the PR metadata references #753/#760). Please update the reference to the correct PR/issue number or rephrase to avoid hardcoding a PR number that can become inaccurate.
| /// Note: `PortResolver::resolve()` calls this method (since PR #776), passing | |
| /// Note: `PortResolver::resolve()` calls this method, passing |
…ound 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| /// Note: `PortResolver::resolve()` calls this method, passing | ||
| /// `port.vendor_id` and `port.product_id` from `PortInfo`. | ||
| pub fn matches_with_usb( |
There was a problem hiding this comment.
There’s now an inconsistency in the docs: matches_with_sysex() still says the binding resolver only calls matches(port_name), but PortResolver::resolve() was updated to call matches_with_usb(...). Please update the matches_with_sysex() note so it accurately reflects the current resolver behavior (it still doesn’t pass SysEx identity, but it no longer calls matches() directly).
…h_usb (round 3) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| PortInfo { | ||
| name: "IAC Driver Bus 1".to_string(), | ||
| index: 0, | ||
| vendor_id: None, | ||
| product_id: None, | ||
| }, | ||
| PortInfo { | ||
| name: "Mikro MK3".to_string(), | ||
| index: 1, | ||
| vendor_id: None, | ||
| product_id: None, | ||
| }, | ||
| PortInfo { | ||
| name: "Launchpad Mini".to_string(), | ||
| index: 2, | ||
| vendor_id: None, | ||
| product_id: None, | ||
| }, | ||
| PortInfo { | ||
| name: "MIDI Through".to_string(), | ||
| index: 3, | ||
| vendor_id: None, | ||
| product_id: None, | ||
| }, |
There was a problem hiding this comment.
This test file repeats vendor_id: None / product_id: None in many PortInfo literals. Consider switching these to PortInfo::new(name, index) (or struct update syntax on top of it) to reduce churn when USB metadata gets plumbed in later.
| PortInfo { | |
| name: "IAC Driver Bus 1".to_string(), | |
| index: 0, | |
| vendor_id: None, | |
| product_id: None, | |
| }, | |
| PortInfo { | |
| name: "Mikro MK3".to_string(), | |
| index: 1, | |
| vendor_id: None, | |
| product_id: None, | |
| }, | |
| PortInfo { | |
| name: "Launchpad Mini".to_string(), | |
| index: 2, | |
| vendor_id: None, | |
| product_id: None, | |
| }, | |
| PortInfo { | |
| name: "MIDI Through".to_string(), | |
| index: 3, | |
| vendor_id: None, | |
| product_id: None, | |
| }, | |
| PortInfo::new("IAC Driver Bus 1".to_string(), 0), | |
| PortInfo::new("Mikro MK3".to_string(), 1), | |
| PortInfo::new("Launchpad Mini".to_string(), 2), | |
| PortInfo::new("MIDI Through".to_string(), 3), |
| .map(|i| PortInfo { | ||
| name: format!("Port {}", i), | ||
| index: i, | ||
| vendor_id: None, | ||
| product_id: None, | ||
| }) |
There was a problem hiding this comment.
Since PortInfo::new() now exists to represent the common "USB IDs unknown" case, consider using it here inside the generator to avoid duplicating the vendor_id/product_id: None defaults and keep the proptest setup aligned with the canonical constructor.
| .map(|i| PortInfo { | |
| name: format!("Port {}", i), | |
| index: i, | |
| vendor_id: None, | |
| product_id: None, | |
| }) | |
| .map(|i| PortInfo::new(format!("Port {}", i), i)) |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| port_infos.push(PortInfo { | ||
| name, | ||
| index, | ||
| vendor_id: None, | ||
| product_id: None, | ||
| }); |
There was a problem hiding this comment.
Consider using PortInfo::new(name, index) here instead of repeating a struct literal with vendor_id: None / product_id: None. This keeps the default construction in one place and makes later USB metadata wiring less error-prone.
| port_infos.push(PortInfo { | |
| name, | |
| index, | |
| vendor_id: None, | |
| product_id: None, | |
| }); | |
| port_infos.push(PortInfo::new(name, index)); |
3c85784 to
9a750eb
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9a750eb to
75fc773
Compare
| vendor_id: None, | ||
| product_id: None, | ||
| } | ||
| } |
There was a problem hiding this comment.
Right now PortInfo::new() only supports the no-USB-metadata case, so tests that need USB IDs are forced to use full struct literals. Consider adding a PortInfo::new_with_usb(...) (or builder-style setters) so call sites can avoid repeating all fields and so future PortInfo expansions don’t cascade into many test updates.
| } | |
| } | |
| /// Construct a PortInfo with known USB Vendor and Product IDs. | |
| pub fn new_with_usb( | |
| name: impl Into<String>, | |
| index: usize, | |
| vendor_id: u16, | |
| product_id: u16, | |
| ) -> Self { | |
| Self { | |
| name: name.into(), | |
| index, | |
| vendor_id: Some(vendor_id), | |
| product_id: Some(product_id), | |
| } | |
| } |
| let ports = vec![PortInfo { | ||
| name: "Generic MIDI Port".to_string(), | ||
| index: 0, | ||
| vendor_id: Some(0x17CC), // NI | ||
| product_id: Some(0x1620), // Mikro MK3 | ||
| }]; |
There was a problem hiding this comment.
These new tests construct PortInfo using full struct literals to set USB IDs. If a convenience constructor (e.g., PortInfo::new_with_usb) is added, prefer using it here to reduce duplication and avoid future breakage if PortInfo gains additional fields.
Use in USB-specific tests to reduce struct literal boilerplate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Wire the existing
matches_with_usb()method into the actual runtime pipeline. UsbIdentifier matchers now resolve ports when USB metadata is provided.Changes
PortInfowithvendor_id: Option<u16>,product_id: Option<u16>PortResolver::resolve()callsmatches_with_usb()instead ofmatches()PortInfoconstructors passNone(platform USB metadata is a follow-up)TDD
Test plan
cargo clippy --workspacecleancargo test --workspace🤖 Generated with Claude Code