Skip to content

fix(hardware): restore informative serial-port names (Arduino IDE parity) - #947

Merged
thiagoralves merged 4 commits into
developmentfrom
fix/serial-port-board-names
Jul 20, 2026
Merged

fix(hardware): restore informative serial-port names (Arduino IDE parity)#947
thiagoralves merged 4 commits into
developmentfrom
fix/serial-port-board-names

Conversation

@thiagoralves

@thiagoralves thiagoralves commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Problem

Since 4.2.3 (the xml2st retirement, #843), the Communication Port dropdown shows only the vendor/manufacturer string, so every port of the same vendor collapses to an identical label the user can't tell apart. Reported on the forum: works on 4.1.4, broken on 4.2.3→4.2.8.

Root cause: the old xml2st --list-ports path used pyserial's description (the friendly name, distinct per port). The rewrite to the serialport npm package mapped name to port.manufacturer (vendor only), dropping the per-port distinction.

Fix — mimic the Arduino IDE

The device path is always the primary, unique, cross-platform label, enriched with a descriptor in parentheses when available. getAvailableSerialPorts() runs two best-effort scans in parallel and merges them by path:

  1. arduino-cli board list --format json — identifies the connected board from the installed core's VID/PID (boards.txt), exactly like the Arduino IDE → COM3 (Arduino Uno). Reuses the module's existing binary path + --config-file, so it sees the same cores as compile/upload.
  2. serialport — supplies the reliable port set plus the manufacturer fallback → COM6 (com0com - serial port emulator); bare path when nothing is known.

Each scan degrades independently (empty map on failure), so a missing binary / no installed cores / malformed JSON still yields plain serialport output. Running both is cheap: the list is static after build and only re-scanned on explicit user refresh.

Resulting labels: COM3 (Arduino Uno)COM6 (com0com - serial port emulator)/dev/ttyUSB0. The regression can't recur because the path always leads.

Files

  • serial-port-list.ts (new) — pure mergeSerialPortList() helper (no IO/electron) holding the label/merge rules.
  • hardware-module.ts — two-scan enumeration + #identifyBoardsByPath() / #listSerialPortManufacturers().
  • __tests__/serial-port-list.test.ts (new) — 7 tests: all label tiers, board-over-manufacturer preference, union/dedup/ordering, empty-descriptor, empty list.

Scope / compatibility

  • Editor-only. No shared surface (frontend, middleware/shared, backend/shared, __architecture__) touched; the DevicePort interface and { name, address } shape are unchanged — no openplc-web mirror change needed.
  • Web already returns [] for local port enumeration by design (browsers can't enumerate local serial ports).

Verification

  • npm run validate:arch — passed (no layer violations)
  • tsc --noEmit — 0 errors project-wide
  • ESLint — clean on changed files
  • Jest — 7/7 pass
  • Validated live against bundled arduino-cli 1.4.1: identifies a connected board; degrades gracefully (valid JSON, exit 0) with a missing config file

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements

    • Serial port discovery now merges board identification and manufacturer data to produce clearer device names.
    • Discovery runs as two independent best-effort scans and still returns ports if one source fails.
    • macOS devices are de-duplicated by collapsing corresponding /dev/tty.* and /dev/cu.* entries, preferring the /dev/cu.* address and board name when available.
    • Ordering is kept stable and ports without identification are listed using their address.
  • Tests

    • Added/expanded coverage for naming, fallback behavior, merging, deduplication, ordering, empty results, and platform-specific macOS vs Linux behavior.

…ity)

Serial ports have shown only the vendor/manufacturer string since 4.2.3
(the xml2st retirement, #843), collapsing every port of the same vendor
to an identical label. The old xml2st path used pyserial's `description`
(friendly name, distinct per port); the rewrite mapped `name` to
`serialport`'s `manufacturer` field instead, which drops the per-port
distinction.

Mimic the Arduino IDE: the device path is always the primary, unique,
cross-platform label, enriched with a descriptor in parentheses when
available. `getAvailableSerialPorts()` now runs two best-effort scans in
parallel and merges them by path:

- arduino-cli `board list --format json` identifies the connected board
  from the installed core's VID/PID (boards.txt) -> "COM3 (Arduino Uno)".
  Reuses the module's existing binary path + --config-file, so it sees
  the same cores as compile/upload.
- serialport supplies the reliable port set plus a manufacturer fallback
  -> "COM6 (com0com - serial port emulator)"; bare path when unknown.

Each scan degrades independently (empty map on failure), so a missing
binary / no cores / malformed JSON still yields plain serialport output.
The merge/label rules live in a pure `mergeSerialPortList()` helper with
unit tests. No shared surface, port interface, or {name,address} shape
changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@thiagoralves, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 29be027c-83c1-47f6-b40c-c901d5128f16

📥 Commits

Reviewing files that changed from the base of the PR and between ce53b00 and 2fe1f20.

📒 Files selected for processing (2)
  • src/backend/editor/hardware/__tests__/serial-port-list.test.ts
  • src/backend/editor/hardware/serial-port-list.ts

Walkthrough

Serial port discovery now performs concurrent Arduino CLI and serial-port scans, reconciles macOS device paths, merges results, and labels ports using board names or manufacturers. Tests cover fallback labeling, ordering, deduplication, and empty scans.

Changes

Serial port discovery

Layer / File(s) Summary
Merge and label serial ports
src/backend/editor/hardware/serial-port-list.ts, src/backend/editor/hardware/__tests__/serial-port-list.test.ts
mergeSerialPortList combines scan results, reconciles macOS tty/cu pairs, prioritizes board names, falls back to manufacturers or bare paths, preserves ordering, deduplicates devices, and is covered by Jest tests.
Parallel hardware scans
src/backend/editor/hardware/hardware-module.ts
getAvailableSerialPorts concurrently runs Arduino CLI board detection and serial-port manufacturer enumeration, merges their results, and logs scan failures while returning partial results.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant getAvailableSerialPorts
  participant arduino-cli
  participant NodeSerialPort
  participant mergeSerialPortList
  getAvailableSerialPorts->>arduino-cli: board list --format json
  getAvailableSerialPorts->>NodeSerialPort: list serial ports
  arduino-cli-->>getAvailableSerialPorts: board paths and names
  NodeSerialPort-->>getAvailableSerialPorts: port paths and manufacturers
  getAvailableSerialPorts->>mergeSerialPortList: merge scan maps
  mergeSerialPortList-->>getAvailableSerialPorts: deduplicated SerialPort[]
Loading

Suggested labels: enhancement

Poem

I’m a rabbit with ports in a row,
Merging names wherever they go.
Boards hop in first, vendors trail,
Bare paths remain when names fail.
Duplicates vanish—carrots prevail!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the PR’s main change: restoring informative serial-port names in hardware.
Description check ✅ Passed The description is detailed and covers problem, fix, files, scope, and verification, though it omits the exact template’s references and DOD checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/serial-port-board-names

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

thiagoralves and others added 3 commits July 20, 2026 11:10
macOS exposes each serial device twice — a call-in node (/dev/tty.*) and
a call-out node (/dev/cu.*). serialport reports the tty.* node (with the
manufacturer) while arduino-cli reports the cu.* node (with the board id),
so the two scans keyed on different paths and the same device appeared
twice in the dropdown (e.g. "/dev/tty.usbmodem11301 (Arduino)" AND
"/dev/cu.usbmodem11301 (Opta)").

Group the two scans on the shared suffix after the tty./cu. prefix and
emit a single entry per device, displaying the call-out (cu.*) node —
the one used for talking to a device and the one the Arduino IDE selects.
Board name still wins over the manufacturer descriptor. Linux
(/dev/ttyUSB0) and Windows (COM3) paths don't match the dotted pattern
and are never merged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the fragile tty./cu. cross-node grouping with a single
canonicalization: `toCalloutPath` rewrites `/dev/tty.` -> `/dev/cu.`
before merging. serialport's native macOS binding hardcodes the dial-in
(tty.*) node while arduino-cli reports the call-out (cu.*) node; rewriting
tty. -> cu. at the source makes both scans agree on one path, so the merge
collapses back to a plain union deduped by path.

The call-out (cu.*) node is the one callers must use to talk to a device
and the one the Arduino IDE selects; IOKit always publishes both nodes for
a serial service, so the rewrite is guaranteed to resolve to a real device.
node-serialport itself can't be configured to emit cu.* (the path is
hardcoded in the compiled binding, `darwin_list.cpp` kIODialinDeviceKey;
serialport#1729), so canonicalizing in JS is the build-free equivalent.

Linux (/dev/ttyUSB0) and Windows (COM3) paths don't match the dotted
pattern and pass through untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Format Check (Prettier) collapsed a single-object toEqual([...]) that fits
within the 120-col width onto one line. No behavioural change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thiagoralves
thiagoralves merged commit 5f9b636 into development Jul 20, 2026
12 checks passed
@thiagoralves
thiagoralves deleted the fix/serial-port-board-names branch July 20, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant