breadkit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.codespellignore +2 -0
- data/.yamllint +16 -0
- data/README.md +109 -6
- data/data/boards/double_full.yml +18 -0
- data/data/boards/stripboard.yml +9 -0
- data/data/boards/universal.yml +8 -0
- data/data/parts/2n3904.yml +11 -0
- data/data/parts/2n7000_onsemi_to92.yml +10 -0
- data/data/parts/74hc00.yml +22 -0
- data/data/parts/74hc04.yml +22 -0
- data/data/parts/74hc08.yml +22 -0
- data/data/parts/74hc14.yml +22 -0
- data/data/parts/74hc165.yml +24 -0
- data/data/parts/74hc32.yml +22 -0
- data/data/parts/74hc595.yml +24 -0
- data/data/parts/active_buzzer.yml +8 -0
- data/data/parts/ams1117_3v3.yml +12 -0
- data/data/parts/arduino_nano.yml +70 -0
- data/data/parts/arduino_uno.yml +29 -26
- data/data/parts/atmega328p.yml +37 -0
- data/data/parts/attiny85.yml +16 -0
- data/data/parts/battery_box.yml +8 -0
- data/data/parts/cd4017.yml +24 -0
- data/data/parts/ck_bd04.yml +29 -0
- data/data/parts/crystal.yml +7 -0
- data/data/parts/dc_jack_2wire.yml +8 -0
- data/data/parts/diode.yml +1 -0
- data/data/parts/esp32_devkitc_v4_wroom32e.yml +86 -0
- data/data/parts/irlz44n.yml +10 -0
- data/data/parts/l293d.yml +24 -0
- data/data/parts/led.yml +1 -0
- data/data/parts/lm358.yml +16 -0
- data/data/parts/lm393.yml +16 -0
- data/data/parts/ne555.yml +2 -2
- data/data/parts/ntc_thermistor.yml +7 -0
- data/data/parts/photoresistor.yml +8 -0
- data/data/parts/pico.yml +89 -0
- data/data/parts/resistor.yml +1 -0
- data/data/parts/sc56_11ewa.yml +30 -0
- data/data/parts/seeed_xiao_samd21.yml +37 -0
- data/data/parts/shillehtek_mb102_4pin.yml +23 -0
- data/data/parts/slide_switch_spst.yml +8 -0
- data/data/parts/sparkfun_pro_micro_5v.yml +58 -0
- data/data/parts/transistor.yml +0 -1
- data/data/parts/ua7805.yml +10 -0
- data/data/parts/uln2003a.yml +23 -0
- data/data/parts/wp154a4sureqbfzgc.yml +11 -0
- data/docs/CLI.md +108 -0
- data/docs/COMPATIBILITY.md +60 -0
- data/docs/DECLARATIVE.md +138 -0
- data/docs/IR.md +42 -0
- data/docs/JUMPER_KIT.md +45 -0
- data/docs/LOCK.md +75 -0
- data/docs/LSP.md +59 -0
- data/docs/MB102_POWER.md +11 -0
- data/docs/MCP.md +44 -0
- data/docs/RAKE.md +19 -0
- data/docs/dsl.md +197 -6
- data/exe/breadkit-lsp +13 -0
- data/exe/breadkit-mcp +12 -0
- data/lib/breadkit/analysis.rb +174 -50
- data/lib/breadkit/board.rb +152 -4
- data/lib/breadkit/cli.rb +229 -13
- data/lib/breadkit/color.rb +38 -0
- data/lib/breadkit/dc_analysis.rb +261 -0
- data/lib/breadkit/dsl.rb +205 -24
- data/lib/breadkit/exporters.rb +202 -0
- data/lib/breadkit/formatter.rb +52 -0
- data/lib/breadkit/fritzing_export.rb +256 -0
- data/lib/breadkit/ir.rb +154 -22
- data/lib/breadkit/jumper_kit.rb +127 -0
- data/lib/breadkit/lsp_server.rb +248 -0
- data/lib/breadkit/mcp_server.rb +133 -0
- data/lib/breadkit/model.rb +47 -5
- data/lib/breadkit/part_library.rb +156 -3
- data/lib/breadkit/part_lock.rb +107 -0
- data/lib/breadkit/patterns.rb +119 -0
- data/lib/breadkit/rake_task.rb +25 -0
- data/lib/breadkit/resolver.rb +160 -28
- data/lib/breadkit/structured_input.rb +275 -0
- data/lib/breadkit/templates/555.bk.rb +34 -0
- data/lib/breadkit/templates/arduino.bk.rb +17 -0
- data/lib/breadkit/templates/led.bk.rb +21 -0
- data/lib/breadkit/value.rb +42 -3
- data/lib/breadkit/version.rb +1 -1
- data/lib/breadkit/wire_suggestions.rb +106 -0
- data/lib/breadkit.rb +28 -3
- data/package.json +1 -1
- data/schema/board-v1.json +38 -0
- data/schema/ir-v1.json +56 -11
- data/schema/ir-v2.json +173 -0
- data/schema/part-v1.json +52 -4
- data/sig/breadkit.rbs +107 -5
- metadata +119 -14
- data/docs/assets/01_led_button.svg +0 -12
- data/site/assets/01_led_button.svg +0 -14
- data/site/assets/03_arduino_blink.svg +0 -14
- data/site/favicon.svg +0 -7
- data/site/guide/components/index.html +0 -113
- data/site/guide/dsl/index.html +0 -132
- data/site/guide/index.html +0 -163
- data/site/index.html +0 -95
- data/site/styles.css +0 -14
data/docs/dsl.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Breadkit DSL
|
|
2
2
|
|
|
3
|
-
Breadkit evaluates a Ruby file into a breadboard circuit, resolves pin and wire locations, and exposes the result as nets and JSON IR. DSL files are executable Ruby; only load files you trust. Use
|
|
3
|
+
Breadkit evaluates a Ruby file into a breadboard circuit, resolves pin and wire locations, and exposes the result as nets and JSON IR. DSL files are executable Ruby; only load files you trust. Use [YAML or TOML circuit files](DECLARATIVE.md) when the input must remain data-only.
|
|
4
4
|
|
|
5
5
|
## A small circuit
|
|
6
6
|
|
|
@@ -28,35 +28,226 @@ end
|
|
|
28
28
|
| Method | Purpose |
|
|
29
29
|
| --- | --- |
|
|
30
30
|
| `title(text)` | Diagram title. |
|
|
31
|
-
| `board(id, split_rails: false)` | Select `:full`, `:half`, `:mini`, or a custom board ID. |
|
|
32
|
-
| `use_parts(path)` / `use_boards(path)` | Load additional YAML definitions relative to the DSL file. Paths may use globs. |
|
|
33
|
-
| `
|
|
31
|
+
| `board(id, split_rails: false, as: nil)` | Select `:double_full` (1660 holes), `:full`, `:half`, `:mini`, `:universal`, `:stripboard`, or a custom board ID. Name each board with `as:` when using multiple boards. |
|
|
32
|
+
| `use_parts(path)` / `use_boards(path)` | Load additional YAML definitions relative to the DSL file. Paths may use globs. Run `breadkit lock FILE` to pin the matched local definitions. |
|
|
33
|
+
| `include(path)` | Evaluate another trusted DSL file in the same circuit. Relative paths resolve from the including file; circular includes are rejected. |
|
|
34
|
+
| `block(name) { ... }` / `use_block(name, *args, **kwargs)` | Define and expand a reusable group of DSL declarations. Each name is unique within the circuit. |
|
|
35
|
+
| `bus(name, **lines)` | Label the explicit reference for each line as `NAME_LINE`, for example `I2C_SCL`. |
|
|
36
|
+
| `step(number, title: nil) { ... }` | Group assembly declarations under a numbered step. Numbers start at 1 and increase by 1. |
|
|
37
|
+
| `supply(name, voltage:, plus:, minus:, current_limit: nil)` | Add a DC source. Both terminals occupy board holes. Optional `current_limit:` is the supply's positive output limit in amperes. |
|
|
38
|
+
| `supply(from:, plus:, minus:)` | Route a defined offboard power output and its return pin to board holes or rails. |
|
|
34
39
|
| `net(name, at:)` | Label a hole or component pin. |
|
|
35
40
|
| `part(ref, type, value = nil, pins: ..., at: ..., **attrs)` | Place a defined part. `pins:` accepts pin order arrays or pin-name hashes. |
|
|
36
41
|
| `wire(from, to, color: nil, id: nil, route: :straight, layer: nil, electrical: true, dashed: false)` | Connect two holes or pin references. `route: :arc` curves the wire; `route: :edge` routes around an outer board edge or from an external module along its terminal row. `layer:` groups wires in interactive SVG output; `electrical: false` draws a visual alternative without changing circuit connectivity. |
|
|
42
|
+
| `connect(from, to, when: nil)` | Declare that two references should be connected. This is an intent assertion; it does not place a wire. |
|
|
37
43
|
| `offboard(name, type, side: :left, at: nil, unused: [], **attrs)` | Place a module beside the board; `at:` aligns its first pin to a board position, and attrs such as `address:` are shown on the module. |
|
|
38
|
-
| `expect { ... }` | Declare `connected`, `isolated`, or named `net` expectations. `strict: true` also rejects unlisted pins on declared nets. |
|
|
44
|
+
| `expect { ... }` | Declare `connected`, `isolated`, or named `net` expectations. `strict: true` also rejects unlisted pins on declared nets. Use `when: "SW1"` to check a selected switch state. |
|
|
45
|
+
| `expect_voltage(ref, range)` / `expect_current(ref, range)` | Declare inclusive DC ranges for a net or component. Current is compared by magnitude in amperes. These methods also work inside `expect(when: "SW1")`. |
|
|
39
46
|
| `lint_disable(rule, on: nil, reason: nil)` | Suppress a lint rule, optionally for one target. |
|
|
40
47
|
|
|
41
48
|
Short forms are available for `resistor`, `capacitor`, `electrolytic`, `diode`, `led`, `transistor`, `pot`, `button`, and `ic`.
|
|
42
49
|
|
|
50
|
+
Standalone supplies require a positive voltage. Use the positive and negative
|
|
51
|
+
terminal positions to express polarity; an inclusive voltage range must have
|
|
52
|
+
two positive, ascending limits. By default, net labels `GND`, `0V`, `VSS`, and
|
|
53
|
+
`GROUND` anchor the potential calculation at 0 V regardless of case. A custom
|
|
54
|
+
board definition can replace that list with `ground_labels: [RETURN, AGND]`.
|
|
55
|
+
|
|
56
|
+
## Connection intent before placement
|
|
57
|
+
|
|
58
|
+
Declare an intended connection before placing parts:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
connect "R1.1", "D1.anode"
|
|
62
|
+
board :mini
|
|
63
|
+
resistor :R1, "330", pins: %w[a1 a3]
|
|
64
|
+
led :D1, color: :red, anode: "b5", cathode: "b6"
|
|
65
|
+
wire "b1", "a5"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`connect` records an expectation and does not add a physical wire or change
|
|
69
|
+
the resolved nets. Run `bklint` to find an `Intent/ConnectionMismatch` when
|
|
70
|
+
the placed circuit fails to join the references. Add or correct a `wire`
|
|
71
|
+
declaration to satisfy it. The references may be declared before their parts;
|
|
72
|
+
unknown pins are reported after resolution. Use `when: "SW1"` to check a
|
|
73
|
+
selected switch state. This declaration uses the existing `connected`
|
|
74
|
+
expectation in JSON IR. Run `breadkit suggest FILE` to see conservative
|
|
75
|
+
free-hole candidates for unmet default-state intent. If the wire above is
|
|
76
|
+
omitted, it prints JSON such as
|
|
77
|
+
`[{"from":"c1","to":"c5","refs":["R1.1","D1.anode"]}]` without editing the
|
|
78
|
+
circuit. State-specific intent, offboard pins, occupied strips, holes under
|
|
79
|
+
component bodies, and known voltage conflicts are skipped. The proposed route
|
|
80
|
+
and physical fit still need review before adding a `wire` declaration.
|
|
81
|
+
|
|
82
|
+
## Power from an offboard module
|
|
83
|
+
|
|
84
|
+
The Arduino Uno definition declares 5 V and 3.3 V outputs relative to GND.
|
|
85
|
+
Connect one output and its return to breadboard rails with:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
board :half
|
|
89
|
+
offboard :UNO, :arduino_uno
|
|
90
|
+
supply from: "UNO.5V", plus: "T+", minus: "T-"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`T+` and `T-` each select a free rail hole. The call expands to wires from
|
|
94
|
+
`UNO.5V` and `UNO.GND`; it does not add another voltage source. Use
|
|
95
|
+
`from: "UNO.3V3"` for the 3.3 V output. The source must match a `provides`
|
|
96
|
+
entry on the placed offboard part, and both destinations are required. For
|
|
97
|
+
named boards, qualify the destinations, for example `B1.T+` and `B1.T-`.
|
|
98
|
+
The resolved IR stores the two ordinary wires.
|
|
99
|
+
|
|
100
|
+
## Reusable blocks and buses
|
|
101
|
+
|
|
102
|
+
A block runs in the same DSL context each time it is used. Pass the references
|
|
103
|
+
and holes for each instance explicitly so every component gets a distinct name
|
|
104
|
+
and placement:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
board :mini
|
|
108
|
+
|
|
109
|
+
block :indicator do |number, resistor_pins:, led_pins:|
|
|
110
|
+
resistor "R#{number}", "330", pins: resistor_pins
|
|
111
|
+
led "D#{number}", color: :red, **led_pins
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
use_block :indicator, 1,
|
|
115
|
+
resistor_pins: %w[a1 a3], led_pins: { anode: "b3", cathode: "b4" }
|
|
116
|
+
use_block :indicator, 2,
|
|
117
|
+
resistor_pins: %w[a6 a8], led_pins: { anode: "b8", cathode: "b9" }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Blocks defined in an included DSL file are available to the including file.
|
|
121
|
+
Recursive calls and duplicate block names are rejected. The resolved circuit
|
|
122
|
+
and JSON IR contain the expanded parts and wires, with no separate block
|
|
123
|
+
record.
|
|
124
|
+
|
|
125
|
+
Use a bus to name related nets at their explicit board holes or pin references:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
bus :I2C, scl: "f22", sda: "f24"
|
|
129
|
+
# Creates I2C_SCL at f22 and I2C_SDA at f24.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The call adds labels. Add `wire` declarations for physical connections to
|
|
133
|
+
devices on each line. The bus helper accepts any named lines; it does not
|
|
134
|
+
perform I2C protocol analysis.
|
|
135
|
+
|
|
136
|
+
## Assembly steps
|
|
137
|
+
|
|
138
|
+
Place the declarations for each assembly stage inside a numbered `step` block:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
board :mini
|
|
142
|
+
|
|
143
|
+
step 1, title: "Install the resistor" do
|
|
144
|
+
resistor :R1, "330", pins: %w[b1 b3]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
step 2, title: "Add the LED and return wire" do
|
|
148
|
+
led :D1, color: :red, anode: "c3", cathode: "c4"
|
|
149
|
+
wire "d4", "b2", color: :black
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Steps must be declared in order, starting at 1. Nested steps are rejected.
|
|
154
|
+
Supplies, net labels, parts, and wires declared inside a step keep that step
|
|
155
|
+
number; declarations outside steps have no number. Normal placement and
|
|
156
|
+
connectivity checks still apply. `circuit.steps` holds each number and title,
|
|
157
|
+
while each resolved item exposes `.step`. JSON IR carries the same `steps`
|
|
158
|
+
list and optional `step` field on those items. Existing IR files without steps
|
|
159
|
+
remain valid.
|
|
160
|
+
|
|
161
|
+
## Multiple boards
|
|
162
|
+
|
|
163
|
+
Name each board and qualify every physical hole or rail reference when a
|
|
164
|
+
circuit has more than one breadboard:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
board :half, as: :B1
|
|
168
|
+
board :mini, as: :B2
|
|
169
|
+
|
|
170
|
+
supply :BAT, voltage: 5, plus: "B1.b1", minus: "B2.b2"
|
|
171
|
+
resistor :R1, "330", pins: %w[B1.a1 B1.a3]
|
|
172
|
+
led :D1, color: :red, anode: "B2.a1", cathode: "B2.a2"
|
|
173
|
+
wire "B1.b3", "B2.b1", color: :red
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The boards have separate conductive strips. A wire may cross between boards;
|
|
177
|
+
one component's placed pins must stay on one board. Board names are unique and
|
|
178
|
+
use letters, digits, and underscores, starting with a letter. Unqualified
|
|
179
|
+
holes such as `a1` and rails such as `T+` are invalid in a named-board circuit.
|
|
180
|
+
Component pin references such as `R1.1` remain global. An unindexed named rail
|
|
181
|
+
such as `B2.T+` still selects a free hole on that board's rail.
|
|
182
|
+
|
|
183
|
+
`circuit.boards` maps names to boards, and `circuit.board` exposes the combined
|
|
184
|
+
qualified hole and strip lookup. Named-board circuits export IR schema version
|
|
185
|
+
2 with a `boards` array of names, definitions, and options. Existing unnamed
|
|
186
|
+
single-board circuits continue to export version 1. The version 2 schema is
|
|
187
|
+
[`schema/ir-v2.json`](../schema/ir-v2.json).
|
|
188
|
+
|
|
43
189
|
## Hole and pin references
|
|
44
190
|
|
|
191
|
+
The built-in `universal` model has 30 × 20 isolated pads. The built-in
|
|
192
|
+
`stripboard` model has 20 continuous copper strips, one per row, with 30 holes
|
|
193
|
+
per strip. The stripboard model assumes uncut tracks; cuts and custom copper
|
|
194
|
+
patterns require a custom board definition or explicit layout support. These
|
|
195
|
+
are example sizes, not a universal hardware standard. A custom board can set
|
|
196
|
+
`terminal.strip_direction: row` to connect each terminal row across its
|
|
197
|
+
columns; the default `column` connects each declared group within a column.
|
|
198
|
+
Set `terminal.wire_attachment: solder` for a soldered board. A wire naming a
|
|
199
|
+
placed component pin uses another free hole on the same strip when available;
|
|
200
|
+
when none exists, it joins that pin's occupied pad. Explicit wires can also
|
|
201
|
+
share solder pads. The default `socket` behavior keeps breadboard holes
|
|
202
|
+
exclusive to one inserted lead or wire. Component leads still cannot share a
|
|
203
|
+
hole in either model. For example:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
board :universal
|
|
207
|
+
resistor :R1, "330", pins: %w[a1 a2]
|
|
208
|
+
wire "R1.1", "b1"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
No generic MB102 power-supply model is included because jumper-selected
|
|
212
|
+
output voltages and pin locations vary by module.
|
|
213
|
+
|
|
45
214
|
- Terminal holes use rows `a` through `j` and 1-based columns, such as `a10` or `J30`.
|
|
46
215
|
- Rail holes use `T+`, `T-`, `B+`, and `B-`, optionally followed by a 1-based index. A rail without an index selects the nearest free hole.
|
|
47
216
|
- A custom board may define other row and rail IDs in its YAML. For example, rows `u` and `v` use `u1` and `v1`; a rail with `id: PWR` uses `PWR1` or the unindexed `PWR`. Set each rail's `polarity:` to `+` or `-` for polarity-aware rendering.
|
|
217
|
+
- Custom rails use `side: top|bottom|left|right|center` and a zero-based `order`. Left and right rails run vertically; `rail_layout.start_row` (default `1`) positions their first hole. Center rails occupy the two free rows of the declared `terminal.ravine_between` gap, so only orders `0` and `1` fit. Horizontal rails use `rail_layout.start_column` as before.
|
|
48
218
|
- Component pins use `R1.1`, `D1.anode`, `U1.8`, or `U1.VCC`. A wire endpoint naming a placed pin selects a free hole in that pin's conductive strip.
|
|
49
219
|
- The built-in `ne555` and generic `dip` definitions must straddle the center gap. For a generic package, set `pin_count`, for example `part :U2, :dip, pin_count: 14, at: "e20"`.
|
|
50
220
|
- A generic pin header can be sized with `part :J1, :pin_header, pin_count: 4, pins: %w[a1 a2 a3 a4]`.
|
|
221
|
+
- Footprint parts accept `rotate: 0|90|180|270` and `mirror: true|false`, for example `part :J1, :pin_header, pin_count: 3, at: "c10", rotate: 90`. Rotation is clockwise on the board; mirroring reflects left to right before rotation. The same orientation is checked when pins are placed explicitly.
|
|
222
|
+
- A custom `placement: footprint` part may use `footprint_mm` instead of `footprint` to declare every pin offset in millimeters, for example `footprint_mm: {"1": [0, 0], "2": [5.08, 0]}`. Offsets must land on the 2.54 mm hole grid; Breadkit rejects a dimension that cannot fit rather than snapping it to another hole. Set the anchor at the first pin with `at:`. Use `render.size_mm` for body dimensions, which need not be multiples of the hole pitch.
|
|
223
|
+
- A placed part with `render.size_mm` exposes its physical rectangle as `component.body_bounds(circuit.board)`, in board hole pitch units (`[x, y, width, height]`).
|
|
224
|
+
- A custom two-pin part with `placement: leads` may set `max_lead_span_mm` to a positive number. This is the maximum supported distance between the two occupied hole centers after bending its leads. Set it from the actual package and usable lead length; generic built-in parts leave it unspecified. The linter can check placed parts that provide this limit.
|
|
51
225
|
|
|
52
226
|
Values accept SI suffixes and RKM notation such as `4.7k`, `4k7`, `1M`, `100n`, `10uF`, and `4.7kΩ`.
|
|
53
227
|
|
|
54
228
|
## Switch states and IR
|
|
55
229
|
|
|
56
|
-
`circuit.states("none")`, `circuit.states("single")`, and `circuit.states("all")` control switch contact simulation. `Breadkit.load(path)` reads `.bk.rb` DSL or `.json` IR. `circuit.to_ir` returns the resolved circuit representation; automatically selected holes are fixed in IR and are not selected again when loaded.
|
|
230
|
+
`circuit.states("none")`, `circuit.states("single")`, and `circuit.states("all")` control switch contact simulation. `Breadkit.load(path)` reads `.bk.rb` DSL, `.bk.yml` / `.bk.yaml` / `.bk.toml` circuit files, or `.json` IR. `circuit.to_ir` returns the resolved circuit representation; automatically selected holes are fixed in IR and are not selected again when loaded.
|
|
231
|
+
|
|
232
|
+
For independent SPST positions in a custom part, set `switch: [[P1A, P1B], [P2A, P2B]]` and `independent_switches: true`. Every contact pair must use distinct pins. The corresponding state names are `SW1.1` and `SW1.2` for an instance named `SW1`; `circuit.state("SW1.1,SW1.2")` selects both. The built-in C&K BD04 uses this model. Place it with `part :SW1, :ck_bd04, at: "e20"`; its four contacts occupy rows 20–23 across the center gap. The `PnA`/`PnB` labels are logical position-side names because the [C&K BD datasheet](https://www.littelfuse.com/assetdocs/littelfuse-ck-dip-bd-series-datasheet?assetguid=c1d4e4f2-7607-4309-afba-ad965b566d35) does not specify every terminal number in its drawing. Check the physical switch's marked position 1 and orientation before wiring it.
|
|
233
|
+
|
|
234
|
+
`old_circuit.diff(new_circuit)` returns a hash of changed board, supply, component, label, and wire entries. Each value is `[before, after]`, with `nil` for an added or removed entry. The CLI `breadkit diff OLD NEW` prints the same changes.
|
|
57
235
|
|
|
58
236
|
The core CLI provides `breadkit nets`, `breadkit parts`, and `breadkit ir`.
|
|
59
237
|
|
|
60
238
|
Custom module pins can declare their kind with `type:` in the part YAML, for example `power`, `ground`, `clock`, `data`, `address`, or `interrupt`. The renderer colors typed pin markers and dims pins without a wire connection.
|
|
239
|
+
For a rail-mounted supply with independent output selectors, see the [ShillehTek MB102 four-pin guide](MB102_POWER.md). This variant requires explicit rail-hole positions and jumper settings; its data file does not assume a universal MB102 contact layout.
|
|
240
|
+
Custom part YAML may set `datasheet_url: https://example.com/part.pdf` to
|
|
241
|
+
retain a source link in part listings and JSON IR. Use an actual HTTPS
|
|
242
|
+
datasheet URL for a published part; URLs with embedded credentials are rejected.
|
|
243
|
+
An individual pin may also declare `max_current: 0.012` for a 12 mA limit.
|
|
244
|
+
Use a verified limit from that part's datasheet; pins without this field have
|
|
245
|
+
no inferred current limit. The value is preserved in JSON IR for lint tools.
|
|
246
|
+
For a pin explicitly named as `provides.positive`, DC analysis reports the
|
|
247
|
+
source current under `REF.PIN`. Other GPIO output states remain unknown to the
|
|
248
|
+
DC solver; `max_current` alone does not assign an output voltage.
|
|
249
|
+
Use `type:` for new definitions; the legacy `role:` spelling is accepted and
|
|
250
|
+
normalized to `type:`. A pin cannot declare both. Unknown part and pin keys,
|
|
251
|
+
and unsupported pin types, are rejected when the definition is loaded.
|
|
61
252
|
|
|
62
253
|
Assign the same `layer:` to wires and components to make them appear together in the interactive SVG layer controls. A layer may be a string or a list of strings when an item belongs to multiple views. Components without a layer remain visible in every view. Use `electrical: false, dashed: true` for an alternate connection that must not affect connectivity analysis.
|
data/exe/breadkit-lsp
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "breadkit/lsp_server"
|
|
6
|
+
|
|
7
|
+
options = { trusted_ruby: false }
|
|
8
|
+
OptionParser.new do |parser|
|
|
9
|
+
parser.banner = "Usage: breadkit-lsp [--root DIRECTORY] [--trusted-ruby]"
|
|
10
|
+
parser.on("--root DIRECTORY") { |value| options[:root] = value }
|
|
11
|
+
parser.on("--trusted-ruby") { options[:trusted_ruby] = true }
|
|
12
|
+
end.parse!
|
|
13
|
+
Breadkit::LSPServer.new(**options).run
|
data/exe/breadkit-mcp
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "breadkit/mcp_server"
|
|
5
|
+
|
|
6
|
+
unless ARGV.empty? || (ARGV.length == 2 && ARGV.first == "--root")
|
|
7
|
+
warn "Usage: breadkit-mcp [--root DIRECTORY]"
|
|
8
|
+
exit 2
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
root = ARGV.empty? ? Dir.pwd : ARGV.last
|
|
12
|
+
Breadkit::MCPServer.new(root: root).run
|
data/lib/breadkit/analysis.rb
CHANGED
|
@@ -28,45 +28,70 @@ module Breadkit
|
|
|
28
28
|
@parent[right] = left
|
|
29
29
|
@rank[left] += 1 if @rank[left] == @rank[right]
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
def snapshot
|
|
33
|
+
copy = dup
|
|
34
|
+
copy.instance_variable_set(:@parent, @parent.dup)
|
|
35
|
+
copy.instance_variable_set(:@rank, @rank.dup)
|
|
36
|
+
copy
|
|
37
|
+
end
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
class Circuit
|
|
34
41
|
attr_reader :title, :board, :components, :wires, :supplies, :labels, :expectations,
|
|
35
|
-
:lint_disables, :diagnostics
|
|
42
|
+
:lint_disables, :diagnostics, :source_root, :steps
|
|
36
43
|
|
|
37
|
-
def initialize(title:, board:, components:, wires:, supplies:, labels:, expectations:, lint_disables:, diagnostics:)
|
|
44
|
+
def initialize(title:, board:, components:, wires:, supplies:, labels:, expectations:, lint_disables:, diagnostics:, steps: [], source_root: nil)
|
|
38
45
|
@title, @board, @components, @wires, @supplies, @labels = title, board, components, wires, supplies, labels
|
|
39
46
|
@expectations, @lint_disables, @diagnostics = expectations, lint_disables, diagnostics
|
|
47
|
+
@steps = steps
|
|
48
|
+
@source_root = source_root
|
|
40
49
|
@net_cache, @net_index, @potential_cache = {}, {}, {}
|
|
41
50
|
end
|
|
42
51
|
|
|
43
|
-
def states(mode = "single")
|
|
44
|
-
|
|
52
|
+
def states(mode = "single", budget: nil)
|
|
53
|
+
raise ArgumentError, "state budget must be a positive integer" if budget && (!budget.is_a?(Integer) || budget <= 0)
|
|
54
|
+
|
|
55
|
+
switches = switch_units
|
|
45
56
|
return [State.new(name: nil, closed_switches: [])] if mode.to_s == "none" || switches.empty?
|
|
46
|
-
|
|
47
|
-
if mode.to_s == "all" &&
|
|
57
|
+
combinations = 1 << switches.length
|
|
58
|
+
raise ArgumentError, "#{combinations} switch states exceed budget #{budget}" if mode.to_s == "all" && budget && combinations > budget
|
|
59
|
+
|
|
60
|
+
# ponytail: calls without an explicit budget keep the legacy eight-switch fallback.
|
|
61
|
+
if mode.to_s == "all" && (budget ? combinations <= budget : switches.length <= 8)
|
|
48
62
|
(0...(1 << switches.length)).map do |bits|
|
|
49
|
-
selected = switches.each_with_index.filter_map { |
|
|
50
|
-
|
|
51
|
-
State.new(name: selected.empty? ? nil : selected.map(&:ref).join(","), closed_switches: closed)
|
|
63
|
+
selected = switches.each_with_index.filter_map { |unit, index| unit if bits[index] == 1 }
|
|
64
|
+
State.new(name: selected.empty? ? nil : selected.map(&:first).join(","), closed_switches: selected.flat_map(&:last))
|
|
52
65
|
end
|
|
53
66
|
else
|
|
54
|
-
singles = switches.map do |
|
|
55
|
-
|
|
56
|
-
State.new(name: component.ref, closed_switches: pairs)
|
|
67
|
+
singles = switches.map do |name, pairs|
|
|
68
|
+
State.new(name: name, closed_switches: pairs)
|
|
57
69
|
end
|
|
58
70
|
all_pairs = singles.flat_map(&:closed_switches)
|
|
59
|
-
singles << State.new(name: switches.map(&:
|
|
71
|
+
singles << State.new(name: switches.map(&:first).join(","), closed_switches: all_pairs) if mode.to_s == "all"
|
|
60
72
|
[State.new(name: nil, closed_switches: [])] + singles
|
|
61
73
|
end
|
|
62
74
|
end
|
|
63
75
|
|
|
76
|
+
def state(name)
|
|
77
|
+
return nil unless name
|
|
78
|
+
|
|
79
|
+
names = name.split(",", -1)
|
|
80
|
+
units = switch_units.to_h
|
|
81
|
+
if names.uniq.length != names.length || names.any? { |unit| !units.key?(unit) }
|
|
82
|
+
raise ArgumentError, "unknown switch state #{name}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
State.new(name: name, closed_switches: names.flat_map { |unit| units.fetch(unit) })
|
|
86
|
+
end
|
|
87
|
+
|
|
64
88
|
def nets(state = nil)
|
|
65
89
|
state ||= State.new(name: nil, closed_switches: [])
|
|
66
90
|
key = state_key(state)
|
|
67
91
|
return @net_cache[key] if @net_cache.key?(key)
|
|
68
92
|
|
|
69
|
-
|
|
93
|
+
@connectivity ||= Connectivity.new(self)
|
|
94
|
+
resolved = @connectivity.build(state)
|
|
70
95
|
@net_index[key] = resolved.each_with_object({}) do |net, index|
|
|
71
96
|
index[net.name] ||= net
|
|
72
97
|
net.members.each do |member|
|
|
@@ -90,6 +115,17 @@ module Breadkit
|
|
|
90
115
|
@potential_cache.fetch(state_key(state))
|
|
91
116
|
end
|
|
92
117
|
|
|
118
|
+
def voltage_sources
|
|
119
|
+
supplies + components.values.flat_map do |component|
|
|
120
|
+
component.provided_sources.map do |source|
|
|
121
|
+
Supply.new(name: "#{component.ref}.#{source.fetch('positive')}", voltage: Value.parse(source.fetch("voltage")),
|
|
122
|
+
plus: "#{component.ref}.#{source.fetch('positive')}",
|
|
123
|
+
minus: "#{component.ref}.#{source.fetch('negative')}", location: component.location,
|
|
124
|
+
isolated: false, voltage_range: nil)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
93
129
|
def net_of(reference, state = nil)
|
|
94
130
|
text = reference.to_s
|
|
95
131
|
nets(state)
|
|
@@ -101,6 +137,55 @@ module Breadkit
|
|
|
101
137
|
IR::Writer.new.write(self)
|
|
102
138
|
end
|
|
103
139
|
|
|
140
|
+
def boards
|
|
141
|
+
board.is_a?(BoardSet) ? board.boards : {}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def multi_board?
|
|
145
|
+
board.is_a?(BoardSet)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def diff(other)
|
|
149
|
+
raise ArgumentError, "expected a Breadkit::Circuit" unless other.is_a?(Circuit)
|
|
150
|
+
|
|
151
|
+
before, after = diff_items, other.diff_items
|
|
152
|
+
(before.keys | after.keys).sort.each_with_object({}) do |key, changes|
|
|
153
|
+
changes[key] = [before[key], after[key]] unless before[key] == after[key]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
protected
|
|
158
|
+
|
|
159
|
+
def diff_items
|
|
160
|
+
items = if multi_board?
|
|
161
|
+
boards.to_h { |name, item| ["board #{name}", "#{item.definition.id}#{' (split rails)' if item.split_rails}"] }
|
|
162
|
+
else
|
|
163
|
+
{ "board" => "#{board.definition.id}#{' (split rails)' if board.split_rails}" }
|
|
164
|
+
end
|
|
165
|
+
supplies.each do |supply|
|
|
166
|
+
voltage = supply.voltage_range ? supply.voltage_range.join("..") : supply.voltage
|
|
167
|
+
limit = supply.current_limit ? ", #{supply.current_limit} A limit" : ""
|
|
168
|
+
items["supply #{supply.name}"] = "#{voltage} V, #{supply.plus} to #{supply.minus}#{limit}"
|
|
169
|
+
end
|
|
170
|
+
labels.each { |label| items["label #{label.name}@#{label.at}"] = true }
|
|
171
|
+
components.each_value do |component|
|
|
172
|
+
pins = component.pins.values.map { |pin| "#{pin.name}=#{pin.hole_id || '-'}" }.join(", ")
|
|
173
|
+
attrs = component.attrs.map { |key, value| [key.to_s, value] }.sort_by(&:first).to_h
|
|
174
|
+
details = [component.part.id, component.value, pins]
|
|
175
|
+
details << "attrs=#{attrs.inspect}" unless attrs.empty?
|
|
176
|
+
details << "unused=#{component.unused.inspect}" unless component.unused.empty?
|
|
177
|
+
items["component #{component.ref}"] = details.compact.join(" ")
|
|
178
|
+
end
|
|
179
|
+
wires.each do |wire|
|
|
180
|
+
options = [wire.color, wire.route, wire.layer, wire.electrical, wire.dashed]
|
|
181
|
+
key = "wire #{[wire.from, wire.to].sort.join(' ↔ ')} #{options.inspect}"
|
|
182
|
+
items[key] = items.fetch(key, 0) + 1
|
|
183
|
+
end
|
|
184
|
+
items
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
public
|
|
188
|
+
|
|
104
189
|
def shortest_path(terminal_a, terminal_b, state = nil)
|
|
105
190
|
start, finish = hole_for_reference(terminal_a.to_s), hole_for_reference(terminal_b.to_s)
|
|
106
191
|
return [] unless start && finish
|
|
@@ -146,6 +231,9 @@ module Breadkit
|
|
|
146
231
|
end
|
|
147
232
|
|
|
148
233
|
def node_for_reference(reference)
|
|
234
|
+
hole = board.hole(reference)
|
|
235
|
+
return "hole:#{hole.id}" if hole
|
|
236
|
+
|
|
149
237
|
if reference.include?(".")
|
|
150
238
|
prefix, pin = reference.split(".", 2)
|
|
151
239
|
if components[prefix]
|
|
@@ -155,7 +243,6 @@ module Breadkit
|
|
|
155
243
|
supply = supplies.find { |item| item.name == prefix }
|
|
156
244
|
return "supply:#{reference}" if supply && %w[+ -].include?(pin)
|
|
157
245
|
end
|
|
158
|
-
return "hole:#{board.hole(reference).id}" if board.hole(reference)
|
|
159
246
|
"label:#{reference}" if labels.any? { |item| item.name == reference }
|
|
160
247
|
rescue ArgumentError
|
|
161
248
|
nil
|
|
@@ -163,7 +250,23 @@ module Breadkit
|
|
|
163
250
|
|
|
164
251
|
private
|
|
165
252
|
|
|
253
|
+
def switch_units
|
|
254
|
+
components.values.flat_map do |component|
|
|
255
|
+
pairs = Array(component.part.data["switch"])
|
|
256
|
+
next [] if pairs.empty?
|
|
257
|
+
|
|
258
|
+
if component.part.data["independent_switches"]
|
|
259
|
+
pairs.each_with_index.map { |pair, index| ["#{component.ref}.#{index + 1}", [[component, pair]]] }
|
|
260
|
+
else
|
|
261
|
+
[[component.ref, pairs.map { |pair| [component, pair] }]]
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
166
266
|
def hole_for_reference(reference)
|
|
267
|
+
hole = board.hole(reference)
|
|
268
|
+
return hole.id if hole
|
|
269
|
+
|
|
167
270
|
if reference.include?(".")
|
|
168
271
|
prefix, pin = reference.split(".", 2)
|
|
169
272
|
supply = supplies.find { |item| item.name == prefix }
|
|
@@ -195,30 +298,18 @@ module Breadkit
|
|
|
195
298
|
class Connectivity
|
|
196
299
|
def initialize(circuit)
|
|
197
300
|
@circuit = circuit
|
|
301
|
+
@uf = UnionFind.new
|
|
302
|
+
build_static_connections
|
|
303
|
+
@base_uf = @uf
|
|
304
|
+
@exposed_nodes = exposed_nodes
|
|
198
305
|
end
|
|
199
306
|
|
|
200
307
|
def build(state)
|
|
201
|
-
@uf =
|
|
202
|
-
circuit.board.strips.each_value { |ids| ids.each_cons(2) { |a, b| @uf.union(hole_node(a), hole_node(b)) } }
|
|
203
|
-
circuit.components.each_value do |component|
|
|
204
|
-
component.pins.each_value { |pin| @uf.union(pin.node_id, hole_node(pin.hole_id)) if pin.hole_id }
|
|
205
|
-
Array(component.part.data["internal"]).each { |pair| join_pins(component, pair) }
|
|
206
|
-
end
|
|
207
|
-
circuit.wires.each do |wire|
|
|
208
|
-
next if wire.electrical == false
|
|
209
|
-
|
|
210
|
-
id = wire_node(wire.id)
|
|
211
|
-
@uf.union(id, endpoint_node(wire.from))
|
|
212
|
-
@uf.union(id, endpoint_node(wire.to))
|
|
213
|
-
end
|
|
214
|
-
circuit.supplies.each do |supply|
|
|
215
|
-
@uf.union(supply_node(supply.name, "+"), hole_node(supply.plus))
|
|
216
|
-
@uf.union(supply_node(supply.name, "-"), hole_node(supply.minus))
|
|
217
|
-
end
|
|
308
|
+
@uf = @base_uf.snapshot
|
|
218
309
|
state.closed_switches.each { |component, pair| join_pins(component, pair) }
|
|
219
310
|
|
|
220
311
|
groups = Hash.new { |hash, key| hash[key] = { members: [], holes: [], labels: [], supplies: [] } }
|
|
221
|
-
exposed_nodes.each do |node, member, kind|
|
|
312
|
+
@exposed_nodes.each do |node, member, kind|
|
|
222
313
|
entry = groups[@uf.find(node)]
|
|
223
314
|
entry[:members] << member if member
|
|
224
315
|
entry[:holes] << node.delete_prefix("hole:") if kind == :hole
|
|
@@ -231,10 +322,14 @@ module Breadkit
|
|
|
231
322
|
roots = groups.keys.select { |root| !groups[root][:members].empty? || !groups[root][:labels].empty? }
|
|
232
323
|
.sort_by { |root| order_for(groups[root][:holes]) }
|
|
233
324
|
used_names = {}
|
|
325
|
+
unnamed_count = 0
|
|
234
326
|
roots.map.with_index do |root, index|
|
|
235
327
|
data = groups[root]
|
|
236
328
|
chosen = data[:labels].first&.name || supply_name(data[:supplies])
|
|
237
|
-
chosen
|
|
329
|
+
unless chosen
|
|
330
|
+
unnamed_count += 1
|
|
331
|
+
chosen = "N#{unnamed_count}"
|
|
332
|
+
end
|
|
238
333
|
name = chosen
|
|
239
334
|
if used_names[name]
|
|
240
335
|
# Multiple nets can have the same label; retain deterministic unique display names.
|
|
@@ -252,6 +347,25 @@ module Breadkit
|
|
|
252
347
|
|
|
253
348
|
attr_reader :circuit
|
|
254
349
|
|
|
350
|
+
def build_static_connections
|
|
351
|
+
circuit.board.strips.each_value { |ids| ids.each_cons(2) { |a, b| @uf.union(hole_node(a), hole_node(b)) } }
|
|
352
|
+
circuit.components.each_value do |component|
|
|
353
|
+
component.pins.each_value { |pin| @uf.union(pin.node_id, hole_node(pin.hole_id)) if pin.hole_id }
|
|
354
|
+
Array(component.part.data["internal"]).each { |pair| join_pins(component, pair) }
|
|
355
|
+
end
|
|
356
|
+
circuit.wires.each do |wire|
|
|
357
|
+
next if wire.electrical == false
|
|
358
|
+
|
|
359
|
+
id = wire_node(wire.id)
|
|
360
|
+
@uf.union(id, endpoint_node(wire.from))
|
|
361
|
+
@uf.union(id, endpoint_node(wire.to))
|
|
362
|
+
end
|
|
363
|
+
circuit.supplies.each do |supply|
|
|
364
|
+
@uf.union(supply_node(supply.name, "+"), hole_node(supply.plus))
|
|
365
|
+
@uf.union(supply_node(supply.name, "-"), hole_node(supply.minus))
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
255
369
|
def exposed_nodes
|
|
256
370
|
list = []
|
|
257
371
|
circuit.board.holes.each_key { |id| list << [hole_node(id), nil, :hole] }
|
|
@@ -323,54 +437,60 @@ module Breadkit
|
|
|
323
437
|
end
|
|
324
438
|
|
|
325
439
|
def solve(state)
|
|
326
|
-
edges = circuit.
|
|
327
|
-
[supply, circuit.net_of(
|
|
440
|
+
edges = circuit.voltage_sources.map do |supply|
|
|
441
|
+
[supply, circuit.net_of(supply.minus, state), circuit.net_of(supply.plus, state)]
|
|
328
442
|
end
|
|
329
443
|
values, conflicts = {}, []
|
|
330
444
|
adjacency = Hash.new { |hash, key| hash[key] = [] }
|
|
331
445
|
edges.each do |supply, from, to|
|
|
332
446
|
next unless from && to
|
|
333
|
-
adjacency[from.name] << [supply, to.name, supply.voltage,
|
|
334
|
-
adjacency[to.name] << [supply, from.name, -supply.voltage,
|
|
447
|
+
adjacency[from.name] << [supply, to.name, supply.voltage, terminal_for(supply, "+")]
|
|
448
|
+
adjacency[to.name] << [supply, from.name, -supply.voltage, terminal_for(supply, "-")]
|
|
335
449
|
end
|
|
336
|
-
|
|
337
|
-
|
|
450
|
+
ground_labels = Array(circuit.board.definition.data["ground_labels"] || %w[GND 0V VSS GROUND]).map(&:upcase)
|
|
451
|
+
ground = circuit.nets(state).find { |net| net.labels.any? { |label| ground_labels.include?(label.upcase) } }&.name
|
|
452
|
+
components, witnesses, witness_sources, seen_conflicts = [], {}, {}, {}
|
|
338
453
|
starts = adjacency.keys
|
|
339
454
|
starts = [ground, *(starts - [ground])] if starts.include?(ground)
|
|
340
455
|
starts.each do |start|
|
|
341
456
|
next if values.key?(start)
|
|
342
457
|
components << start
|
|
343
458
|
values[start] = 0.0
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
elsif to&.name == start
|
|
348
|
-
"#{supply.name}.+"
|
|
349
|
-
end
|
|
350
|
-
end.first
|
|
459
|
+
origin, from = edges.find { |_supply, negative, positive| negative&.name == start || positive&.name == start }
|
|
460
|
+
witnesses[start] = terminal_for(origin, from&.name == start ? "-" : "+")
|
|
461
|
+
witness_sources[start] = origin
|
|
351
462
|
queue = [start]
|
|
352
463
|
until queue.empty?
|
|
353
464
|
current = queue.shift
|
|
354
465
|
adjacency[current].each do |supply, target, delta, terminal|
|
|
355
466
|
proposed = values[current] + delta
|
|
356
467
|
if values.key?(target)
|
|
357
|
-
next if (values[target] - proposed).abs <= 1e-9
|
|
468
|
+
next if (values[target] - proposed).abs <= 1e-9
|
|
358
469
|
|
|
359
470
|
first, second = witnesses[target], terminal
|
|
471
|
+
next if first == second
|
|
472
|
+
pair = [first, second].sort
|
|
473
|
+
next if seen_conflicts[pair]
|
|
360
474
|
path = circuit.shortest_path(first, second, state)
|
|
361
475
|
path_wires = circuit.wires.select { |wire| path.include?(wire.id) }
|
|
362
476
|
conflicts << { supply: supply, net: target, expected: values[target], actual: proposed,
|
|
363
477
|
terminal_a: first, terminal_b: second, path: path,
|
|
364
|
-
wires: path_wires.map(&:id), location: path_wires.max_by { |wire| wire.location&.line.to_i }&.location || supply.location
|
|
365
|
-
|
|
478
|
+
wires: path_wires.map(&:id), location: path_wires.max_by { |wire| wire.location&.line.to_i }&.location || supply.location,
|
|
479
|
+
source_pair: [witness_sources[target].name, supply.name].sort }
|
|
480
|
+
seen_conflicts[pair] = true
|
|
366
481
|
else
|
|
367
482
|
values[target] = proposed
|
|
368
483
|
witnesses[target] = terminal
|
|
484
|
+
witness_sources[target] = supply
|
|
369
485
|
queue << target
|
|
370
486
|
end
|
|
371
487
|
end
|
|
372
488
|
end
|
|
373
489
|
end
|
|
490
|
+
# A wired short can be seen again on an already shared return or series junction.
|
|
491
|
+
wired_pairs = conflicts.filter_map { |item| item[:source_pair] unless item[:wires].empty? }
|
|
492
|
+
conflicts.reject! { |item| item[:wires].empty? && wired_pairs.include?(item[:source_pair]) }
|
|
493
|
+
conflicts.each { |item| item.delete(:source_pair) }
|
|
374
494
|
values[ground] = 0.0 if ground && !values.key?(ground)
|
|
375
495
|
PotentialResult.new(values: values, conflicts: conflicts, components: components)
|
|
376
496
|
end
|
|
@@ -378,5 +498,9 @@ module Breadkit
|
|
|
378
498
|
private
|
|
379
499
|
|
|
380
500
|
attr_reader :circuit
|
|
501
|
+
|
|
502
|
+
def terminal_for(supply, side)
|
|
503
|
+
circuit.supplies.include?(supply) ? "#{supply.name}.#{side}" : (side == "+" ? supply.plus : supply.minus)
|
|
504
|
+
end
|
|
381
505
|
end
|
|
382
506
|
end
|