Skip to content

feat(pcb): set and read netclass differential-pair settings - #866

Open
davidtdab wants to merge 1 commit into
mixelpixx:mainfrom
davidtdab:feat/777-netclass-diff-pair
Open

davidtdab wants to merge 1 commit into
mixelpixx:mainfrom
davidtdab:feat/777-netclass-diff-pair

Conversation

@davidtdab

Copy link
Copy Markdown
Contributor

Summary

KiCad keeps a differential-pair width, gap and via gap on every netclass, and Konnect's complete Default already writes all three. But create_netclass could not set them, and get_netclasses did not report them. create_netclass now takes diff_pair_width, diff_pair_gap and diff_pair_via_gap, and both tools report them under the contract the four existing settings follow.

Closes #777.

One reading for you to confirm. The issue lists a differential field in inherits "when supplied by Default".

  • When KiCad 10.0.6 resolves a net's class, addMissingDefaults fills in the width and gap the class omits, taking them from the Default (common/project/net_settings.cpp:1094-1106).
  • The via gap's block is commented out: "Currently this is only on the default netclass, and not editable in the setup panel" (:1108-1114).
  • So a named class without its own via gap reports null. It is in neither inherits nor missing_fields, since missing_fields is carried only by the Default and names only what the Default lacks.
  • Width and gap inherit as the issue describes.

If you would rather report the Default's via gap there, I'll change it in this PR: one condition (NOT_INHERITED), the two tests that assert null, and the sentences that say so.

Approach

  • One mapping. The handler's local FIELDS and the module's NETCLASS_FIELDS mapped the same KiCad keys to the same argument names, one for writing and one for reading. They are now one table, NETCLASS_FIELDS, with the creation default as a third column, so the two tools cannot disagree on which settings exist. The schema, the handler's bound and the reply still name the three new settings separately, and tests cover each.
  • Arguments.
    • Each is optional, typed number with exclusiveMinimum: 0.
    • The handler holds a direct call to the same bound with opt_positive_f64. It checks this before the guard against a board KiCad holds, as it checks every argument error.
    • KiCad's own loader takes any number, zero and negatives included (net_settings.cpp:44-51).
  • Updates change only the values named, as before.
  • A new named class without them leaves them out of the project file, as KiCad does for a class that does not set them (net_settings.cpp:116-123). So its width and gap keep resolving from the Default. The four older settings keep their creation defaults.
  • The Default is still written complete from kicad_default_class(), which already carried KiCad's 0.2 / 0.25 / 0.25 (common/netclass.cpp:46-48). An incomplete one is still repaired without touching values already set.
  • The reply reports the class as stored, null where it sets none.
  • get_netclasses reports each value resolved, naming in inherits those taken from the Default. The via gap is the exception above.
  • A named class's via gap is not used by KiCad 10.0.6's router or DRC, and Board Setup drops it.
    • When the router sizes a pair from the netclass, it takes the via gap from the gap constraint, floored at the board's minimum clearance (pcbnew/router/pns_kicad_iface.cpp:1222, :1258-1262).
    • Otherwise the router uses the size selected in it: a custom one, a predefined one, or, for the netclass entry, the Default's via gap, falling back to the gap (pcbnew/board_design_settings.cpp:1587-1604).
    • DRC builds its netclass differential-pair rules from width and gap alone (pcbnew/drc/drc_engine.cpp:313-364).
    • Board Setup has no via-gap column. When its changes are applied, it rebuilds every named class from its grid (common/dialogs/panel_setup_netclasses.cpp:464-531).
    • The value is still stored and read back as the class's own, and the migration entry says so.
  • route_differential_pair is untouched.

KiCad line numbers are at tag 10.0.6.

Architectural fit

This extends the two netclass tools and the field mapping they now share. It adds no tool, no IPC command and no dependency, and it touches no board file.

The scope is #777's acceptance. Explicit exclusions:

  • Bounds on the four older settings. They still go through opt_f64, and their schema has no minimum. So zero or a negative is still accepted for them, through tools/call and on a direct call alike.
  • A malformed net_settings on read. get_netclasses still reads a net_settings that is not an object as no classes. create_netclass refuses it.
  • Clearing a value. Once a class sets a differential-pair value, the tool has no way back to inheriting it: the schema refuses null, and an omitted argument leaves the value. The four older settings are the same. Hand-edit the project file, or delete the class in Board Setup and create it again.
  • Types on the four older settings. On a direct call, opt_f64 still reads a wrong-typed value for them as omitted, as before.

Branch and dependencies

Compatibility and safety

  • Schema: three optional arguments. Nothing is renamed or removed.
  • Responses: create_netclass's reply and each class get_netclasses reports gain the three fields. Migration entry: "create_netclass and get_netclasses carry the differential-pair settings" (minor release, since a response changes shape).
  • File mutation: unchanged. Only the sibling .kicad_pro is written, atomically, and not at all when nothing changes. A new named class gets no differential-pair key unless one is named. The board is never touched.
  • Docs:
    • docs/KICAD_NETCLASS_DEFAULTS.md said addMissingDefaults fills any field a class omits. It now names the two of its twelve fields that function does not fill, the via gap and line_style, and its two line references to the function point at 10.0.6.
    • The signature line in kicad-pcb/SKILL.md now names the three arguments.

Validation

Acceptance (issue body)

#777 Where
Optional positive finite fields Schema exclusiveMinimum: 0, and opt_positive_f64 in the handler. JSON carries no non-finite number.
Existing-class updates change only named values create_netclass_updates_one_differential_pair_setting_alone
New named classes stay sparse, inheritance kept create_netclass_leaves_a_named_class_sparse
Default complete, repair kept create_netclass_writes_the_default_class_complete, create_netclass_backfills_an_incomplete_default_without_touching_set_values
Resolved values, inherits, missing fields get_netclasses_reports_differential_pair_settings_as_kicad_resolves_them, an_unresolved_differential_pair_setting_is_named_missing_not_invented. The via gap is as in the Summary.
Response from stored settings, not echoed Built from the stored class, the one that is written, as for the four older settings. A value not named in the call is reported (create_netclass_updates_one_differential_pair_setting_alone).
route_differential_pair separate Untouched.
KiCad-authored fixture, explicit and inherited netclass_diff_pair_kicad10.* and its README, below. a_kicad_written_project_reads_explicit_and_inherited_differential_pair_settings reads USB as explicit and Power as inherited.
Served schema/type/bounds, all three differential_pair_settings_out_of_bounds_are_refused_by_name sends 0, -0.1, "0.2" and null for each of the three fields, to the compiled validator and through tools/call.
New class, partial update, Default, inheritance, override, no-op, malformed/missing project The behavior table below.
Board byte-identical, unrelated settings, classes and patterns unchanged updating_a_kicad_written_project_changes_only_the_value_named, on the KiCad-written fixture.
Reopen or native readback; GUI evidence recorded Real KiCad, below. Board Setup was not looked at, and the record says so.
Negative control on the shared mapping The first row of the controls table. every_setting_create_netclass_writes_reads_back_as_its_own writes all seven settings and reads each back as the class's own.

Changed tool behavior

Behavior Contract and evidence for this change
Accepted inputs and declared defaults Three optional numbers above zero, with no declared default. A new named class omits them, and its reply reports them null (create_netclass_leaves_a_named_class_sparse). The four older creation defaults are unchanged (a_new_class_is_still_created_with_the_documented_defaults). Values given are written as given (create_netclass_writes_the_differential_pair_settings_it_is_given), on the Default too (create_netclass_lets_the_caller_override_the_default_class).
Invalid/unsupported inputs and structured errors Zero, a negative, a string and null are refused by the compiled schema. Through tools/call each is refused as invalid_argument naming the field, and nothing is written. A direct call with zero gets the same refusal from the handler, even while a mock KiCad holds the board (differential_pair_settings_out_of_bounds_are_refused_by_name).
Target, data source and prerequisite state Unchanged: the sibling .kicad_pro. A missing file is refused (create_netclass_without_a_project_file_refuses_and_writes_nothing). A malformed one is refused and left as it was: invalid JSON by both tools, and a net_settings that is not an object by create_netclass (a_malformed_project_file_is_refused_and_left_alone).
Observed changes and preserved unrelated objects An update changes only the values named (create_netclass_updates_one_differential_pair_setting_alone). On a project KiCad wrote, changing one value leaves every other setting, class and pattern as KiCad left it, and the board byte-identical (updating_a_kicad_written_project_changes_only_the_value_named). A call that changes nothing writes nothing (a_call_that_changes_nothing_leaves_the_project_file_untouched). The Default's repair fills only what is absent (create_netclass_backfills_an_incomplete_default_without_touching_set_values).
Failure before/after mutation, including applied work Arguments and the project file are checked before the write, which is one atomic replace of the file.
Recovery from partial/uncertain results without repeating applied work N/A: there is no partial state, and repeating a call changes nothing.

Commands, on Linux with Rust 1.96.0 from rust-toolchain.toml, on the tree of this head:

  • cargo fmt --all -- --check: clean.
  • cargo test --workspace --locked --lib --tests: 2,398 passed, 0 failed, 45 ignored.
  • cargo test --workspace --locked --doc: 9 passed, 2 ignored.
  • cargo clippy --workspace --locked --all-targets -- -D warnings: clean.
  • Real KiCad: below.

Real KiCad. KiCad 10.0.6 on Linux, standalone pcbnew under Xvfb with the API enabled.

Setup:

  1. Konnect's create_project laid out a fresh project, and pcbnew saved it through the API's SaveDocument, which writes the board and the project file (pcbnew/api/api_handler_pcb.cpp:185-198, pcbnew/files.cpp:376-382, :999-1002).
  2. With pcbnew closed, this branch's create_netclass wrote USB with all three differential-pair values and Power with only a clearance, and assign_net_to_class added a pattern.
  3. pcbnew opened the board and saved it again.

Results:

  • KiCad held every value.
    • It rebuilt the classes from its own model. It reordered them, and added pcb_color, schematic_color and tuning_profile, which it always writes (net_settings.cpp:76-80).
    • It wrote back all three of USB's values unchanged, the via gap included. A differential-pair key is written only when the class holds one (:116-123), so that is what KiCad had read.
  • It invented none. Power came back with no differential-pair key.
  • Nothing else moved. The Default, netclass_patterns and every other key were identical, and the board was saved byte-identical.
  • The file is KiCad's fixed point. A second open and save left the project file byte-identical, with its timestamp unchanged.
  • Read back on this head. get_netclasses on KiCad's file gives:
    • USB: 0.18 / 0.15 / 0.3, nothing inherited;
    • Power: the Default's 0.2 / 0.25, both in inherits, and a null via gap.
  • Which build. The run used an earlier, unpushed build of this branch on the same base. The same calls on this head write a byte-identical project file (SHA-256 74449740d24dbd3b02b6df536327447088197856ab049ad29f883f68431c6ac3 before KiCad's resave), and get_netclasses on KiCad's file returns the same classes.

Not run:

  • A look at Board Setup in the GUI. KiCad's own reading and resave above stands in its place.
  • A DRC check of the gap. The board has no nets, and the run could not route a pair onto a net the board lacks, so there was nothing to check.
  • The Windows and macOS legs of Check & Test.
  • The Plugin (Python), Nix flake, Schematic viewer, Dependency licenses and PCM packaging checks. This changes no plugin, viewer, packaging or dependency source.

The fixture. netclass_diff_pair_kicad10.kicad_pro and .kicad_pcb are the files of that run as KiCad saved them the second time, with nothing edited. The README beside them has the recipe and the SHA-256s. This branch wrote the classes before KiCad read and resaved them, which is why USB can carry a via gap: Board Setup cannot author one on a named class.

Tests. 9 new and 5 extended in netclass_tests. The acceptance and behavior tables above name them.

Negative controls. A baseline run of the 43 tests cargo test -p konnect-core --lib netclass selects passed. Then each guard was neutered on its own, those tests were run, and the file was restored. 13 of 13 were caught, none by a build break:

Guard neutered Failed
diff_pair_gap removed from the shared mapping, the array's length adjusted (the issue's control) 8: every_setting_create_netclass_writes_reads_back_as_its_own, create_netclass_writes_the_differential_pair_settings_it_is_given, create_netclass_updates_one_differential_pair_setting_alone, create_netclass_lets_the_caller_override_the_default_class, get_netclasses_reports_differential_pair_settings_as_kicad_resolves_them, an_unresolved_differential_pair_setting_is_named_missing_not_invented, a_kicad_written_project_reads_explicit_and_inherited_differential_pair_settings, updating_a_kicad_written_project_changes_only_the_value_named
diff_pair_width removed from the shared mapping, the array's length adjusted 5, among them every_setting_create_netclass_writes_reads_back_as_its_own and a_kicad_written_project_reads_explicit_and_inherited_differential_pair_settings
The via gap inherited like the others get_netclasses_reports_differential_pair_settings_as_kicad_resolves_them, a_kicad_written_project_reads_explicit_and_inherited_differential_pair_settings
The three given creation defaults create_netclass_leaves_a_named_class_sparse
A new class written with every key, null where unset create_netclass_leaves_a_named_class_sparse
The handler's bound removed differential_pair_settings_out_of_bounds_are_refused_by_name
The bound checked after the guard against a board KiCad holds differential_pair_settings_out_of_bounds_are_refused_by_name
exclusiveMinimum removed from one property differential_pair_settings_out_of_bounds_are_refused_by_name
One property left untyped and unbounded differential_pair_settings_out_of_bounds_are_refused_by_name
The three properties removed from the schema differential_pair_settings_out_of_bounds_are_refused_by_name, updating_a_kicad_written_project_changes_only_the_value_named, a_malformed_project_file_is_refused_and_left_alone
A differential-pair value left out of the reply create_netclass_writes_the_differential_pair_settings_it_is_given, create_netclass_updates_one_differential_pair_setting_alone, create_netclass_leaves_a_named_class_sparse
The via gap left out of the complete Default create_netclass_writes_the_default_class_complete, create_netclass_backfills_an_incomplete_default_without_touching_set_values
The project file saved when nothing changed a_call_that_changes_nothing_leaves_the_project_file_untouched

Review checklist

  • The diff is focused and contains no generated output, personal data, or unrelated cleanup.
  • The branch includes current upstream/main, has no merge conflicts, and CI passed on this exact head. (After CI runs on this head.)
  • For a fork PR, maintainer edits are enabled, or the author accepts
    responsibility for the final current-main refresh.
  • The branch was based on latest upstream/main, not a release tag (unless this is an approved backport).
  • The PR shows only its unique commits and diff; dependencies and series position are explicit.
  • Every review conversation is resolved; any post-review push or base refresh
    has been reviewed on the new exact head. An unchanged unique diff may use a
    focused refresh review; changed behavior received substantive re-review.
  • New names follow docs/NAMING_CONVENTIONS.md; public renames include compatibility handling.
  • New behavior and failure paths have regression coverage.
  • File mutations are atomic and preserve unrelated content.
  • IPC mutations verify the requested board; atomic/partial behavior and safe recovery are explicit under the reliability contract. N/A: no IPC mutation.
  • If tools were added/removed: counts and docs updated per CONTRIBUTING.md (registry tool_count, tool-directory.md, DEV.md stats, README count). N/A: no tool added.

Maintainer merge state

  • The PR has exactly one current status:* workflow label.
  • status:ready-to-merge applies to this exact head SHA.
  • The completed review is recorded; only stale automatic CODEOWNERS requests
    were cleared under the review-request workflow (manual requests remain open).
  • All required checks and review conversations satisfy the main ruleset.
  • Merge authorization is established: standing authorization applies to a
    @mixelpixx/@neusse PR, or explicit authorization names this exact head.
  • Auto-merge uses a merge commit, or an already-green PR will be merged with gh pr merge N --merge.
  • Terminal issue closure and the next PR to promote are identified.

🤖 Generated with Claude Code

create_netclass takes diff_pair_width, diff_pair_gap and
diff_pair_via_gap, each optional and greater than zero, and
get_netclasses reports them beside the four settings it already did.
Both read the one field mapping. An update changes only the values
named. A new named class without them leaves them out of the project
file, so its width and gap keep resolving from the Default; the four
older settings keep their creation defaults. The Default is still
written complete and repaired without touching values already set. The
bound is checked before the guard against a board KiCad holds, as every
argument error is.

The reply reports the class as stored. get_netclasses reports each
setting resolved, naming in inherits those taken from the Default,
except diff_pair_via_gap: KiCad 10.0.6 never fills a named class's from the
Default, so a named class without its own reports null.

The fixture is a project KiCad 10.0.6 saved after reading the classes
create_netclass wrote, so its bytes are KiCad's.

Closes mixelpixx#777

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Expose differential-pair settings in netclass create and readback

1 participant