breadkit-lint 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/.yamllint +16 -0
- data/CHANGELOG.md +30 -2
- data/README.md +55 -40
- data/action.yml +42 -0
- data/config/default.yml +33 -0
- data/docs/REFERENCE.md +129 -0
- data/docs/rules/Electrical/CapacitorVoltageRating.md +5 -0
- data/docs/rules/Electrical/FloatingInput.md +11 -0
- data/docs/rules/Electrical/GpioOvercurrent.md +18 -0
- data/docs/rules/Electrical/I2CAddressConflict.md +5 -0
- data/docs/rules/Electrical/I2CPullupMissing.md +5 -0
- data/docs/rules/Electrical/LedOvercurrent.md +5 -0
- data/docs/rules/Electrical/MinimumResistance.md +14 -0
- data/docs/rules/Electrical/MissingBaseResistor.md +5 -0
- data/docs/rules/Electrical/MissingDecouplingCapacitor.md +5 -0
- data/docs/rules/Electrical/MissingFlybackDiode.md +18 -0
- data/docs/rules/Electrical/MissingPullResistor.md +17 -0
- data/docs/rules/Electrical/MissingSeriesResistor.md +3 -1
- data/docs/rules/Electrical/NoCommonGround.md +1 -0
- data/docs/rules/Electrical/RailPolarityMismatch.md +10 -0
- data/docs/rules/Electrical/ResistorPowerRating.md +5 -0
- data/docs/rules/Electrical/ReversePolarity.md +1 -1
- data/docs/rules/Electrical/SupplyOverload.md +11 -0
- data/docs/rules/Electrical/SupplyVoltageRange.md +1 -0
- data/docs/rules/Electrical/VoltageDomainMismatch.md +6 -0
- data/docs/rules/Intent/ConnectionMismatch.md +3 -1
- data/docs/rules/Intent/MeasurementUnavailable.md +10 -0
- data/docs/rules/Layout/AmbiguousSupplySource.md +5 -0
- data/docs/rules/Layout/BodyOverlap.md +11 -0
- data/docs/rules/Layout/HoleCovered.md +11 -0
- data/docs/rules/Layout/InvalidColor.md +3 -0
- data/docs/rules/Layout/InvalidOption.md +3 -0
- data/docs/rules/Layout/InvalidRoute.md +3 -0
- data/docs/rules/Layout/InvalidValue.md +3 -0
- data/docs/rules/Layout/InvalidWireId.md +3 -0
- data/docs/rules/Layout/LeadSpan.md +21 -0
- data/docs/rules/Layout/PartOverride.md +3 -0
- data/docs/rules/Layout/UnknownSupplySource.md +5 -0
- data/docs/rules/Layout/UnmatchedParts.md +3 -0
- data/docs/rules/Layout/WireOverIC.md +11 -0
- data/docs/rules/Lint/RedundantDisable.md +3 -0
- data/docs/rules/Lint/UnknownRuleInDisable.md +5 -0
- data/lib/breadkit/lint/baseline.rb +38 -0
- data/lib/breadkit/lint/rake_task.rb +22 -0
- data/lib/breadkit/lint/rules/electrical.rb +580 -30
- data/lib/breadkit/lint/rules/intent.rb +69 -4
- data/lib/breadkit/lint/rules/layout.rb +148 -0
- data/lib/breadkit/lint/rules/style.rb +36 -7
- data/lib/breadkit/lint/rules/support.rb +45 -11
- data/lib/breadkit/lint/rules.rb +205 -2
- data/lib/breadkit/lint/source_editor.rb +138 -0
- data/lib/breadkit/lint/version.rb +1 -1
- data/lib/breadkit/lint.rb +422 -59
- data/lib/guard/breadkit/templates/Guardfile +7 -0
- data/lib/guard/breadkit.rb +26 -0
- data/locales/en.yml +82 -0
- data/locales/ja.yml +130 -4
- data/locales/ko.yml +191 -0
- data/locales/zh.yml +191 -0
- data/package-lock.json +103 -0
- data/package.json +2 -1
- data/scripts/action-run.sh +51 -0
- data/scripts/build-rule-pages.mjs +87 -0
- data/scripts/check-rule-pages.mjs +18 -0
- data/scripts/pre-commit +24 -0
- data/sig/breadkit/lint.rbs +12 -1
- data/site/index.html +9 -6
- data/site/schemas/lint-v1.json +71 -0
- data/site/styles.css +13 -0
- metadata +64 -4
|
@@ -4,6 +4,8 @@ module Breadkit
|
|
|
4
4
|
module Lint
|
|
5
5
|
class Checks
|
|
6
6
|
def short_circuit(circuit, rule, state)
|
|
7
|
+
return [] if circuit.voltage_sources.empty?
|
|
8
|
+
|
|
7
9
|
baseline = if state.closed_switches.empty?
|
|
8
10
|
[]
|
|
9
11
|
else
|
|
@@ -61,6 +63,108 @@ module Breadkit
|
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
def floating_inputs(circuit, rule, state)
|
|
67
|
+
anchored = nil
|
|
68
|
+
circuit.components.values.flat_map do |component|
|
|
69
|
+
component.pins.values.filter_map do |pin|
|
|
70
|
+
next unless pin.role == "input" && pin.hole_id
|
|
71
|
+
next if component.unused.include?(pin.name) || component.unused.include?(pin.number)
|
|
72
|
+
reference = "#{component.ref}.#{pin.name}"
|
|
73
|
+
net = circuit.net_of(reference, state)
|
|
74
|
+
next unless net
|
|
75
|
+
anchored ||= anchored_input_nets(circuit, state)
|
|
76
|
+
next if anchored[net.name]
|
|
77
|
+
connected = net.members.any? do |member|
|
|
78
|
+
member != reference && (!member.include?(".") || member.split(".", 2).first != component.ref)
|
|
79
|
+
end
|
|
80
|
+
next unless connected
|
|
81
|
+
offense(rule.id, translate("floating_input", "#{reference} has no modeled source or pull resistor", pin: reference),
|
|
82
|
+
component.location, targets: { components: [component.ref], pins: [reference], holes: [pin.hole_id], nets: [net.name] },
|
|
83
|
+
state: state.name)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def missing_pull_resistors(circuit, rule, state)
|
|
89
|
+
switches = supply_switch_input_nets(circuit, state)
|
|
90
|
+
return [] if switches.empty?
|
|
91
|
+
anchored = anchored_input_nets(circuit, state)
|
|
92
|
+
circuit.components.values.flat_map do |component|
|
|
93
|
+
component.pins.values.filter_map do |pin|
|
|
94
|
+
next unless pin.role == "input" && pin.hole_id
|
|
95
|
+
next if component.unused.include?(pin.name) || component.unused.include?(pin.number)
|
|
96
|
+
reference = "#{component.ref}.#{pin.name}"
|
|
97
|
+
net = circuit.net_of(reference, state)
|
|
98
|
+
switch = net && switches[net.name]
|
|
99
|
+
next unless switch && !anchored[net.name]
|
|
100
|
+
message = translate("missing_pull_resistor", "#{reference} can float when #{switch.ref} is open; add a pull resistor",
|
|
101
|
+
pin: reference, switch: switch.ref)
|
|
102
|
+
offense(rule.id, message, component.location,
|
|
103
|
+
targets: { components: [component.ref, switch.ref], pins: [reference], nets: [net.name] })
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def supply_switch_input_nets(circuit, state)
|
|
109
|
+
supplies = circuit.voltage_sources.flat_map do |source|
|
|
110
|
+
[source.plus, source.minus].filter_map { |terminal| circuit.net_of(terminal, state)&.name }
|
|
111
|
+
end
|
|
112
|
+
return {} if supplies.empty?
|
|
113
|
+
|
|
114
|
+
circuit.components.values.each_with_object({}) do |component, found|
|
|
115
|
+
Array(component.part.data["switch"]).each do |pair|
|
|
116
|
+
pins = pair.map { |reference| component.pin(reference) }
|
|
117
|
+
next unless pins.all?
|
|
118
|
+
nets = pins.map { |pin| circuit.net_of("#{component.ref}.#{pin.name}", state)&.name }
|
|
119
|
+
next unless nets.all? && nets.uniq.length == 2
|
|
120
|
+
nets.each_with_index do |net, index|
|
|
121
|
+
found[net] ||= component if !supplies.include?(net) && supplies.include?(nets[1 - index])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def anchored_input_nets(circuit, state)
|
|
128
|
+
anchored = {}
|
|
129
|
+
circuit.voltage_sources.each do |source|
|
|
130
|
+
[source.plus, source.minus].each do |terminal|
|
|
131
|
+
net = circuit.net_of(terminal, state)
|
|
132
|
+
anchored[net.name] = true if net
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
circuit.components.each_value do |component|
|
|
136
|
+
component.pins.each_value do |pin|
|
|
137
|
+
output = %w[output gpio open_drain].include?(pin.role) || component.part.pin(pin.number)&.fetch("output_capable", false)
|
|
138
|
+
next unless output
|
|
139
|
+
net = circuit.net_of("#{component.ref}.#{pin.name}", state)
|
|
140
|
+
anchored[net.name] = true if net
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
neighbors = Hash.new { |hash, key| hash[key] = [] }
|
|
144
|
+
circuit.components.each_value do |component|
|
|
145
|
+
next unless conductive_resistor?(component)
|
|
146
|
+
nets = component.pins.values.map { |pin| circuit.net_of("#{component.ref}.#{pin.name}", state)&.name }
|
|
147
|
+
next unless nets.length == 2 && nets.all? && nets[0] != nets[1]
|
|
148
|
+
neighbors[nets[0]] << nets[1]
|
|
149
|
+
neighbors[nets[1]] << nets[0]
|
|
150
|
+
end
|
|
151
|
+
queue = anchored.keys
|
|
152
|
+
until queue.empty?
|
|
153
|
+
neighbors[queue.shift].each do |name|
|
|
154
|
+
next if anchored[name]
|
|
155
|
+
anchored[name] = true
|
|
156
|
+
queue << name
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
anchored
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def conductive_resistor?(component)
|
|
163
|
+
component.part.id == "resistor" && Breadkit::Value.parse(component.value) >= 0
|
|
164
|
+
rescue ArgumentError, TypeError
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
|
|
64
168
|
def dangling_wires(circuit, rule, state)
|
|
65
169
|
attachments = Hash.new { |hash, key| hash[key] = [] }
|
|
66
170
|
circuit.components.each_value do |component|
|
|
@@ -116,9 +220,50 @@ module Breadkit
|
|
|
116
220
|
circuit.wires.any? { |wire| wire.id == member } || circuit.components.keys.any? { |ref| member.start_with?("#{ref}.") }
|
|
117
221
|
end
|
|
118
222
|
next unless net && voltage.nil? && touched
|
|
119
|
-
|
|
120
|
-
|
|
223
|
+
source_holes = entries.find { |_holes, _net, potential| !potential.nil? }&.first
|
|
224
|
+
target_hole = segment_holes.first.id
|
|
225
|
+
source_hole = source_holes&.min_by { |hole| (hole.y - segment_holes.first.y).abs }&.id
|
|
226
|
+
connection = source_hole ? "#{source_hole} to #{target_hole}" : target_hole
|
|
227
|
+
location = circuit.wires.find { |wire| net.members.include?(wire.id) }&.location ||
|
|
228
|
+
circuit.components.values.find { |component| net.members.any? { |member| member.start_with?("#{component.ref}.") } }&.location
|
|
229
|
+
offense(rule.id, translate("split_rail", "used segment of #{rail} has no supply; bridge #{connection}", rail: rail, bridge: connection),
|
|
230
|
+
location, targets: { holes: [source_hole, target_hole].compact, nets: [net.name] })
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def rail_polarity_mismatch(circuit, rule, _state)
|
|
236
|
+
circuit.voltage_sources.filter_map do |source|
|
|
237
|
+
next if source.isolated
|
|
238
|
+
positive_net = circuit.net_of(source.plus)
|
|
239
|
+
negative_net = circuit.net_of(source.minus)
|
|
240
|
+
next unless positive_net && negative_net && positive_net != negative_net
|
|
241
|
+
positive_rails = polarized_rails(circuit, positive_net)
|
|
242
|
+
negative_rails = polarized_rails(circuit, negative_net)
|
|
243
|
+
next unless positive_rails.map(&:first).uniq == ["-"] && negative_rails.map(&:first).uniq == ["+"]
|
|
244
|
+
board_id = lambda do |hole|
|
|
245
|
+
circuit.board.respond_to?(:board_id_for) ? circuit.board.board_id_for(hole.id) : nil
|
|
121
246
|
end
|
|
247
|
+
positive_by_board = positive_rails.group_by { |_polarity, hole| board_id.call(hole) }
|
|
248
|
+
negative_by_board = negative_rails.group_by { |_polarity, hole| board_id.call(hole) }
|
|
249
|
+
common_boards = positive_by_board.keys & negative_by_board.keys
|
|
250
|
+
next if common_boards.empty?
|
|
251
|
+
common_board = common_boards.first
|
|
252
|
+
wrong_positive = positive_by_board.fetch(common_board).first.last
|
|
253
|
+
wrong_negative = negative_by_board.fetch(common_board).first.last
|
|
254
|
+
offense(rule.id, translate("rail_polarity_mismatch",
|
|
255
|
+
"#{source.name} positive terminal is on #{wrong_positive.rail} and negative terminal is on #{wrong_negative.rail}",
|
|
256
|
+
source: source.name, positive_rail: wrong_positive.rail, negative_rail: wrong_negative.rail),
|
|
257
|
+
source.location,
|
|
258
|
+
targets: { holes: [wrong_positive.id, wrong_negative.id], nets: [positive_net.name, negative_net.name] })
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def polarized_rails(circuit, net)
|
|
263
|
+
net.holes.filter_map do |id|
|
|
264
|
+
hole = circuit.board.hole(id)
|
|
265
|
+
polarity = hole&.rail && circuit.board.rail_polarity(hole.rail)
|
|
266
|
+
[polarity, hole] if polarity
|
|
122
267
|
end
|
|
123
268
|
end
|
|
124
269
|
|
|
@@ -136,16 +281,79 @@ module Breadkit
|
|
|
136
281
|
offense(rule.id, translate("missing_series_resistor", "#{component.ref} has no current-limiting resistor in series",
|
|
137
282
|
ref: component.ref), component.location,
|
|
138
283
|
targets: { components: [component.ref], pins: ["#{component.ref}.#{positive.name}", "#{component.ref}.#{negative.name}"],
|
|
139
|
-
nets: [high.name, low.name] }, state: state.name
|
|
284
|
+
nets: [high.name, low.name] }, state: state.name,
|
|
285
|
+
suggestion: series_resistor_recommendation(circuit, component, high.name, low.name, state))
|
|
140
286
|
end
|
|
141
287
|
end
|
|
142
288
|
|
|
143
|
-
|
|
289
|
+
E12 = [1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2].freeze
|
|
290
|
+
|
|
291
|
+
def series_resistor_recommendation(circuit, component, high, low, state)
|
|
292
|
+
forward_voltage = component.part.data["forward_voltage"]
|
|
293
|
+
current_limit = component.part.data["max_forward_current"]
|
|
294
|
+
return unless forward_voltage && current_limit && high != low
|
|
295
|
+
|
|
296
|
+
sources = circuit.voltage_sources.filter_map do |source|
|
|
297
|
+
positive = circuit.net_of(source.plus, state)&.name
|
|
298
|
+
negative = circuit.net_of(source.minus, state)&.name
|
|
299
|
+
[source, positive, negative] if positive && negative && [positive, negative].sort == [high, low].sort
|
|
300
|
+
end
|
|
301
|
+
return unless sources.one?
|
|
302
|
+
|
|
303
|
+
source, positive, negative = sources.first
|
|
304
|
+
return unless positive == high && negative == low
|
|
305
|
+
|
|
306
|
+
voltage = source.voltage_range ? source.voltage_range.last : source.voltage
|
|
307
|
+
minimum = (voltage - forward_voltage) / current_limit
|
|
308
|
+
return unless minimum.positive? && minimum.finite?
|
|
309
|
+
|
|
310
|
+
decade = 10.0**Math.log10(minimum).floor
|
|
311
|
+
recommended = (E12.map { |ratio| ratio * decade } + [10 * decade]).find { |value| value >= minimum * (1 - 1e-12) }
|
|
312
|
+
resistance = Breadkit::Value.new(recommended, category: :resistor).to_s
|
|
313
|
+
voltage_text = format("%.3g", voltage)
|
|
314
|
+
forward_text = format("%.3g", forward_voltage)
|
|
315
|
+
current_text = format("%.3g", current_limit * 1000)
|
|
316
|
+
translate("series_resistor_recommendation",
|
|
317
|
+
"Add an E12 resistor of at least #{resistance} in series. Estimate uses #{voltage_text} V maximum supply, " \
|
|
318
|
+
"#{forward_text} V declared forward voltage, and #{current_text} mA declared maximum current; verify tolerances.",
|
|
319
|
+
resistance: resistance, voltage: voltage_text, forward_voltage: forward_text, current: current_text)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def minimum_resistances(circuit, rule, state)
|
|
323
|
+
pots = circuit.components.values.select { |component| component.part.id == "pot" }
|
|
324
|
+
circuit.components.values.filter_map do |led|
|
|
325
|
+
next unless led.part.id == "led" && pots.any?
|
|
326
|
+
polarity = led.part.data["polarity"] || {}
|
|
327
|
+
high = circuit.net_of("#{led.ref}.#{polarity['positive']}", state)&.name
|
|
328
|
+
low = circuit.net_of("#{led.ref}.#{polarity['negative']}", state)&.name
|
|
329
|
+
next unless high && low
|
|
330
|
+
next if unprotected_path?(circuit, high, low, led, state)
|
|
331
|
+
|
|
332
|
+
pot = pots.find do |candidate|
|
|
333
|
+
%w[left right].any? { |end_pin| unprotected_path?(circuit, high, low, led, state, zero_pot: [candidate, end_pin]) }
|
|
334
|
+
end
|
|
335
|
+
next unless pot
|
|
336
|
+
offense(rule.id, translate("minimum_resistance", "#{led.ref} relies on #{pot.ref} for current limiting; its wiper can reach 0 Ω",
|
|
337
|
+
led: led.ref, pot: pot.ref), led.location,
|
|
338
|
+
targets: { components: [led.ref, pot.ref] }, state: state.name)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def unprotected_path?(circuit, start, finish, excluded_component, state, zero_pot: nil)
|
|
144
343
|
adjacency = Hash.new { |hash, key| hash[key] = [] }
|
|
145
344
|
circuit.components.each_value do |component|
|
|
146
|
-
next if component == excluded_component
|
|
147
|
-
|
|
148
|
-
|
|
345
|
+
next if component == excluded_component
|
|
346
|
+
pins = if component == zero_pot&.first
|
|
347
|
+
[component.pin("wiper"), component.pin(zero_pot.last)].compact
|
|
348
|
+
elsif component.part.data["category"] == "transistor"
|
|
349
|
+
[component.pin("collector"), component.pin("emitter")].compact
|
|
350
|
+
elsif component.pins.length == 2 &&
|
|
351
|
+
(component.part.data["category"] == "diode" || (component.part.id == "resistor" && !current_limiter?(component)))
|
|
352
|
+
component.pins.values
|
|
353
|
+
else
|
|
354
|
+
[]
|
|
355
|
+
end
|
|
356
|
+
next unless pins.length == 2
|
|
149
357
|
left, right = pins.map { |pin| circuit.net_of("#{component.ref}.#{pin.name}", state)&.name }
|
|
150
358
|
next unless left && right && left != right
|
|
151
359
|
adjacency[left] << right
|
|
@@ -173,19 +381,26 @@ module Breadkit
|
|
|
173
381
|
end
|
|
174
382
|
|
|
175
383
|
def source_pairs(circuit, state)
|
|
176
|
-
pairs = circuit.
|
|
177
|
-
high = circuit.net_of(
|
|
178
|
-
low = circuit.net_of(
|
|
384
|
+
pairs = circuit.voltage_sources.filter_map do |supply|
|
|
385
|
+
high = circuit.net_of(supply.plus, state)&.name
|
|
386
|
+
low = circuit.net_of(supply.minus, state)&.name
|
|
179
387
|
[high, low] if high && low
|
|
180
388
|
end
|
|
181
389
|
circuit.components.each_value do |component|
|
|
182
390
|
next unless component.part.placement == "offboard" || component.part.data["category"] == "module"
|
|
183
391
|
pins = component.pins.values
|
|
184
392
|
grounds = pins.select { |pin| pin.role == "ground" }.filter_map { |pin| circuit.net_of("#{component.ref}.#{pin.name}", state)&.name }
|
|
185
|
-
outputs = pins.select
|
|
393
|
+
outputs = pins.select do |pin|
|
|
394
|
+
%w[gpio output].include?(pin.role) || component.part.pin(pin.number)&.fetch("output_capable", false)
|
|
395
|
+
end
|
|
186
396
|
outputs.each do |pin|
|
|
187
|
-
|
|
188
|
-
|
|
397
|
+
gpio = circuit.net_of("#{component.ref}.#{pin.name}", state)&.name
|
|
398
|
+
next unless gpio
|
|
399
|
+
grounds.each { |ground| pairs << [gpio, ground] }
|
|
400
|
+
pairs.concat(circuit.voltage_sources.filter_map do |source|
|
|
401
|
+
high = circuit.net_of(source.plus, state)&.name
|
|
402
|
+
[high, gpio] if high
|
|
403
|
+
end)
|
|
189
404
|
end
|
|
190
405
|
end
|
|
191
406
|
pairs
|
|
@@ -200,6 +415,10 @@ module Breadkit
|
|
|
200
415
|
def reverse_polarity(circuit, rule, state)
|
|
201
416
|
ranges, domains = potential_ranges(circuit, state)
|
|
202
417
|
circuit.components.values.filter_map do |component|
|
|
418
|
+
flags = Array(component.part.data["flags"])
|
|
419
|
+
next unless %w[led electrolytic].include?(component.part.id) || flags.include?("needs_series_resistor") || component.part.data["category"] == "capacitor"
|
|
420
|
+
next if component.attrs[:bias].to_s == "reverse" || component.attrs["bias"].to_s == "reverse"
|
|
421
|
+
next if flags.include?("reverse_bias_ok")
|
|
203
422
|
polarity = component.part.data["polarity"]
|
|
204
423
|
next unless polarity
|
|
205
424
|
positive = component.pins[polarity["positive"]]
|
|
@@ -209,7 +428,8 @@ module Breadkit
|
|
|
209
428
|
low = circuit.net_of("#{component.ref}.#{negative.name}", state)
|
|
210
429
|
next unless high && low && ranges[high.name] && ranges[low.name]
|
|
211
430
|
next unless domains[high.name] && domains[high.name] == domains[low.name]
|
|
212
|
-
|
|
431
|
+
reverse_voltage = ranges[low.name][0] - ranges[high.name][1]
|
|
432
|
+
next unless reverse_voltage > component.part.data.fetch("max_reverse_voltage", 0).to_f
|
|
213
433
|
offense(rule.id, translate("reverse_polarity", "#{component.ref} positive pin is below its negative pin",
|
|
214
434
|
ref: component.ref), component.location,
|
|
215
435
|
targets: { components: [component.ref], pins: ["#{component.ref}.#{positive.name}", "#{component.ref}.#{negative.name}"],
|
|
@@ -219,6 +439,8 @@ module Breadkit
|
|
|
219
439
|
|
|
220
440
|
def potential_ranges(circuit, state)
|
|
221
441
|
# ponytail: resistor-only DC leaves active-device voltages unknown; add device models when those cases matter.
|
|
442
|
+
return [{}, {}] if circuit.voltage_sources.empty?
|
|
443
|
+
|
|
222
444
|
known = circuit.potentials(state).values
|
|
223
445
|
ranges = known.transform_values { |value| [value, value] }
|
|
224
446
|
domains = supply_domains(circuit, state)
|
|
@@ -271,9 +493,9 @@ module Breadkit
|
|
|
271
493
|
|
|
272
494
|
def supply_domains(circuit, state)
|
|
273
495
|
adjacency = Hash.new { |hash, key| hash[key] = [] }
|
|
274
|
-
circuit.
|
|
275
|
-
high = circuit.net_of(
|
|
276
|
-
low = circuit.net_of(
|
|
496
|
+
circuit.voltage_sources.each do |supply|
|
|
497
|
+
high = circuit.net_of(supply.plus, state)&.name
|
|
498
|
+
low = circuit.net_of(supply.minus, state)&.name
|
|
277
499
|
next unless high && low
|
|
278
500
|
adjacency[high] << low
|
|
279
501
|
adjacency[low] << high
|
|
@@ -312,9 +534,12 @@ module Breadkit
|
|
|
312
534
|
end
|
|
313
535
|
|
|
314
536
|
def power_pins(circuit, rule, state)
|
|
315
|
-
|
|
537
|
+
components = circuit.components.values.reject { |component| component.part.placement == "offboard" }
|
|
538
|
+
return [] unless components.any? { |component| component.pins.values.any? { |pin| %w[power ground].include?(pin.role) } }
|
|
539
|
+
|
|
540
|
+
grounds = circuit.voltage_sources.filter_map { |supply| circuit.net_of(supply.minus, state)&.name }
|
|
316
541
|
values = circuit.potentials(state).values
|
|
317
|
-
|
|
542
|
+
components.flat_map do |component|
|
|
318
543
|
component.pins.values.filter_map do |pin|
|
|
319
544
|
next unless %w[power ground].include?(pin.role)
|
|
320
545
|
next if component.unused.include?(pin.number) || component.unused.include?(pin.name)
|
|
@@ -336,11 +561,14 @@ module Breadkit
|
|
|
336
561
|
end
|
|
337
562
|
|
|
338
563
|
def supply_ranges(circuit, rule, state)
|
|
564
|
+
components = circuit.components.values.select { |component| component.part.data["supply_range"] }
|
|
565
|
+
return [] if components.empty?
|
|
566
|
+
|
|
339
567
|
values = circuit.potentials(state).values
|
|
340
568
|
domains = supply_domains(circuit, state)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
next [] unless
|
|
569
|
+
components.flat_map do |component|
|
|
570
|
+
rated = component.part.data["supply_range"]
|
|
571
|
+
next [] unless rated
|
|
344
572
|
powers = component.pins.values.select { |pin| pin.role == "power" }
|
|
345
573
|
grounds = component.pins.values.select { |pin| pin.role == "ground" }
|
|
346
574
|
powers.product(grounds).filter_map do |power, ground|
|
|
@@ -348,10 +576,12 @@ module Breadkit
|
|
|
348
576
|
low = circuit.net_of("#{component.ref}.#{ground.name}", state)
|
|
349
577
|
next unless high && low && values.key?(high.name) && values.key?(low.name)
|
|
350
578
|
next unless domains[high.name] && domains[high.name] == domains[low.name]
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
579
|
+
nominal = values[high.name] - values[low.name]
|
|
580
|
+
actual = direct_source_range(circuit, state, high.name, low.name) || [nominal, nominal]
|
|
581
|
+
next if actual[0] >= rated[0].to_f && actual[1] <= rated[1].to_f
|
|
582
|
+
voltage = actual[0] == actual[1] ? actual[0].round(3).to_s : "#{actual[0]}..#{actual[1]}"
|
|
583
|
+
message = translate("supply_range", "#{component.ref} #{power.name}/#{ground.name} supply is #{voltage} V; expected #{rated[0]}..#{rated[1]} V",
|
|
584
|
+
ref: component.ref, power: power.name, ground: ground.name, voltage: voltage, minimum: rated[0], maximum: rated[1])
|
|
355
585
|
offense(rule.id, message, component.location,
|
|
356
586
|
targets: { components: [component.ref], pins: ["#{component.ref}.#{power.name}", "#{component.ref}.#{ground.name}"],
|
|
357
587
|
nets: [high.name, low.name] }, state: state.name)
|
|
@@ -360,10 +590,330 @@ module Breadkit
|
|
|
360
590
|
end
|
|
361
591
|
|
|
362
592
|
def common_ground(circuit, rule, state)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
593
|
+
supplies = circuit.voltage_sources.reject { |supply| supply.respond_to?(:isolated) && supply.isolated }
|
|
594
|
+
return [] if supplies.length < 2
|
|
595
|
+
domains = supply_domains(circuit, state)
|
|
596
|
+
references = supplies.filter_map { |supply| circuit.net_of(supply.minus, state)&.name }
|
|
597
|
+
return [] if references.filter_map { |name| domains[name] }.uniq.length <= 1
|
|
598
|
+
location = supplies.find { |supply| supply.location }&.location
|
|
599
|
+
[offense(rule.id, translate("common_ground", "power supplies do not share a ground"), location,
|
|
600
|
+
targets: { nets: references.uniq })]
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def voltage_domain_mismatches(circuit, rule, state)
|
|
604
|
+
ranges, domains = potential_ranges(circuit, state)
|
|
605
|
+
circuit.components.values.flat_map do |component|
|
|
606
|
+
ground_names = component.pins.values.select { |pin| pin.role == "ground" }
|
|
607
|
+
.filter_map { |pin| circuit.net_of("#{component.ref}.#{pin.name}", state)&.name }
|
|
608
|
+
component.pins.values.filter_map do |pin|
|
|
609
|
+
limit = component.part.pin(pin.number)&.fetch("max_voltage", nil)
|
|
610
|
+
next unless limit && !ground_names.empty?
|
|
611
|
+
net = circuit.net_of("#{component.ref}.#{pin.name}", state)
|
|
612
|
+
next unless net && ranges.key?(net.name)
|
|
613
|
+
ground = ground_names.find { |name| ranges.key?(name) && domains[name] == domains[net.name] }
|
|
614
|
+
next unless ground
|
|
615
|
+
voltage = direct_source_range(circuit, state, net.name, ground)&.last || (ranges[net.name][1] - ranges[ground][0])
|
|
616
|
+
next unless voltage > limit.to_f
|
|
617
|
+
message = translate("voltage_domain", "#{component.ref}.#{pin.name} can see up to #{voltage.round(3)} V; maximum is #{limit} V",
|
|
618
|
+
pin: "#{component.ref}.#{pin.name}", voltage: voltage.round(3), maximum: limit)
|
|
619
|
+
offense(rule.id, message, component.location,
|
|
620
|
+
targets: { components: [component.ref], pins: ["#{component.ref}.#{pin.name}"], nets: [net.name] },
|
|
621
|
+
state: state.name)
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def resistor_power_ratings(circuit, rule, state)
|
|
627
|
+
ranges, domains = potential_ranges(circuit, state)
|
|
628
|
+
circuit.components.values.filter_map do |component|
|
|
629
|
+
next unless component.part.id == "resistor" && component.value
|
|
630
|
+
rating = Breadkit::Value.power_rating(component.value)
|
|
631
|
+
next unless rating
|
|
632
|
+
voltage = rated_voltage(circuit, component, state, ranges, domains)
|
|
633
|
+
next unless voltage
|
|
634
|
+
minimum = Breadkit::Value.parse(component.value) * (1.0 - (Breadkit::Value.tolerance(component.value) || 0.0))
|
|
635
|
+
next unless minimum.positive?
|
|
636
|
+
watts = voltage**2 / minimum
|
|
637
|
+
next unless watts > rating
|
|
638
|
+
offense(rule.id, "#{component.ref} may dissipate #{watts.round(3)} W; rated for #{rating} W",
|
|
639
|
+
component.location, targets: { components: [component.ref] }, state: state.name)
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def capacitor_voltage_ratings(circuit, rule, state)
|
|
644
|
+
ranges, domains = potential_ranges(circuit, state)
|
|
645
|
+
circuit.components.values.filter_map do |component|
|
|
646
|
+
next unless component.part.id == "electrolytic" && component.value
|
|
647
|
+
rating = Breadkit::Value.voltage_rating(component.value)
|
|
648
|
+
next unless rating
|
|
649
|
+
voltage = rated_voltage(circuit, component, state, ranges, domains)
|
|
650
|
+
next unless voltage && voltage > rating
|
|
651
|
+
offense(rule.id, "#{component.ref} may see #{voltage.round(3)} V; rated for #{rating} V",
|
|
652
|
+
component.location, targets: { components: [component.ref] }, state: state.name)
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def led_overcurrent(circuit, rule, state)
|
|
657
|
+
rated = circuit.components.values.select { |component| component.part.id == "led" && component.part.data["max_forward_current"] }
|
|
658
|
+
return [] if rated.empty?
|
|
659
|
+
|
|
660
|
+
analysis = circuit.dc_analysis(state)
|
|
661
|
+
return [] unless analysis.success?
|
|
662
|
+
|
|
663
|
+
rated.filter_map do |component|
|
|
664
|
+
limit = component.part.data["max_forward_current"]
|
|
665
|
+
current = analysis.currents[component.ref]
|
|
666
|
+
next unless current && current > limit
|
|
667
|
+
offense(rule.id, "#{component.ref} may carry #{(current * 1000).round(2)} mA; maximum is #{(limit * 1000).round(2)} mA",
|
|
668
|
+
component.location, targets: { components: [component.ref] }, state: state.name)
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def gpio_overcurrent(circuit, rule, state)
|
|
673
|
+
rated = circuit.components.values.flat_map do |component|
|
|
674
|
+
Array(component.part.data["provides"]).flat_map do |source|
|
|
675
|
+
%w[positive negative].filter_map do |terminal|
|
|
676
|
+
pin = component.pin(source.fetch(terminal))
|
|
677
|
+
next unless pin && %w[gpio output].include?(pin.role)
|
|
678
|
+
limit = component.part.pin(pin.number)&.fetch("max_current", nil)
|
|
679
|
+
[component, source, pin, limit] if limit
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
return [] if rated.empty?
|
|
684
|
+
|
|
685
|
+
analysis = circuit.dc_analysis(state)
|
|
686
|
+
return [] unless analysis.success?
|
|
687
|
+
|
|
688
|
+
rated.filter_map do |component, source, pin, limit|
|
|
689
|
+
current = analysis.currents["#{component.ref}.#{source.fetch('positive')}"]
|
|
690
|
+
next unless current && current.abs > limit * (1 + 1e-9)
|
|
691
|
+
actual_ma, limit_ma = [current.abs, limit].map { |value| (value * 1000).round(2) }
|
|
692
|
+
reference = "#{component.ref}.#{pin.name}"
|
|
693
|
+
message = translate("gpio_overcurrent", "#{reference} carries #{actual_ma} mA; maximum is #{limit_ma} mA",
|
|
694
|
+
pin: reference, current: actual_ma, limit: limit_ma)
|
|
695
|
+
net = circuit.net_of(reference, state)
|
|
696
|
+
offense(rule.id, message, component.location,
|
|
697
|
+
targets: { components: [component.ref], pins: [reference], nets: [net&.name].compact }, state: state.name)
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def supply_overloads(circuit, rule, state)
|
|
702
|
+
rated = circuit.supplies.select(&:current_limit)
|
|
703
|
+
return [] if rated.empty?
|
|
704
|
+
|
|
705
|
+
analysis = circuit.dc_analysis(state)
|
|
706
|
+
return [] unless analysis.success?
|
|
707
|
+
|
|
708
|
+
rated.filter_map do |supply|
|
|
709
|
+
limit = supply.current_limit
|
|
710
|
+
delivered = -analysis.currents.fetch(supply.name, 0.0)
|
|
711
|
+
next unless delivered > limit * (1 + 1e-9)
|
|
712
|
+
|
|
713
|
+
actual_ma, limit_ma = [delivered, limit].map { |value| (value * 1000).round(2) }
|
|
714
|
+
message = translate("supply_overload", "#{supply.name} supplies #{actual_ma} mA; limit is #{limit_ma} mA",
|
|
715
|
+
supply: supply.name, current: actual_ma, limit: limit_ma)
|
|
716
|
+
nets = [supply.plus, supply.minus].filter_map { |terminal| circuit.net_of(terminal, state)&.name }
|
|
717
|
+
offense(rule.id, message, supply.location, targets: { nets: nets, holes: [supply.plus, supply.minus] }, state: state.name)
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def rated_voltage(circuit, component, state, ranges, domains)
|
|
722
|
+
pins = component.pins.values
|
|
723
|
+
return unless pins.length == 2
|
|
724
|
+
left, right = pins.map { |pin| circuit.net_of("#{component.ref}.#{pin.name}", state)&.name }
|
|
725
|
+
return unless left && right && ranges[left] && ranges[right] && domains[left] && domains[left] == domains[right]
|
|
726
|
+
direct = direct_source_range(circuit, state, left, right) || direct_source_range(circuit, state, right, left)
|
|
727
|
+
return direct.map(&:abs).max if direct
|
|
728
|
+
[(ranges[left][0] - ranges[right][1]).abs, (ranges[left][1] - ranges[right][0]).abs].max
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def i2c_address_conflicts(circuit, rule, state)
|
|
732
|
+
devices = circuit.components.values.filter_map do |component|
|
|
733
|
+
address = component.attrs[:address] || component.attrs["address"]
|
|
734
|
+
next unless address && component.pin("SDA") && component.pin("SCL")
|
|
735
|
+
parsed = Integer(address.to_s, 0) rescue nil
|
|
736
|
+
next unless parsed
|
|
737
|
+
sda = circuit.net_of("#{component.ref}.SDA", state)&.name
|
|
738
|
+
scl = circuit.net_of("#{component.ref}.SCL", state)&.name
|
|
739
|
+
[[sda, scl, parsed], component] if sda && scl
|
|
740
|
+
end
|
|
741
|
+
devices.group_by(&:first).filter_map do |(sda, scl, address), entries|
|
|
742
|
+
next if entries.length < 2
|
|
743
|
+
components = entries.map(&:last)
|
|
744
|
+
refs = components.map(&:ref)
|
|
745
|
+
message = translate("i2c_address", "#{refs.join(', ')} share I2C address 0x#{address.to_s(16).upcase} on the same bus",
|
|
746
|
+
refs: refs.join(", "), address: "0x#{address.to_s(16).upcase}")
|
|
747
|
+
offense(rule.id, message, components.last.location,
|
|
748
|
+
targets: { components: refs, pins: refs.flat_map { |ref| ["#{ref}.SDA", "#{ref}.SCL"] },
|
|
749
|
+
nets: [sda, scl] }, state: state.name)
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def i2c_pullup_missing(circuit, rule, state)
|
|
754
|
+
board_id = lambda do |hole|
|
|
755
|
+
circuit.board.respond_to?(:board_id_for) ? circuit.board.board_id_for(hole) : nil
|
|
756
|
+
end
|
|
757
|
+
positive_nets = circuit.voltage_sources.filter_map do |source|
|
|
758
|
+
net = circuit.net_of(source.plus, state)
|
|
759
|
+
[net.name, net.holes.map(&board_id).uniq] if net
|
|
760
|
+
end
|
|
761
|
+
return [] if positive_nets.empty?
|
|
762
|
+
|
|
763
|
+
devices = circuit.components.values.select do |component|
|
|
764
|
+
address = component.attrs[:address] || component.attrs["address"]
|
|
765
|
+
address && component.pin("SDA") && component.pin("SCL") && (Integer(address.to_s, 0) rescue nil)
|
|
766
|
+
end
|
|
767
|
+
devices.group_by do |component|
|
|
768
|
+
%w[SDA SCL].map { |pin| circuit.net_of("#{component.ref}.#{pin}", state)&.name }
|
|
769
|
+
end.flat_map do |(sda, scl), components|
|
|
770
|
+
next [] unless sda && scl && sda != scl
|
|
771
|
+
|
|
772
|
+
{ "SDA" => sda, "SCL" => scl }.filter_map do |pin, net_name|
|
|
773
|
+
net = circuit.net_of("#{components.first.ref}.#{pin}", state)
|
|
774
|
+
next unless net && components.any? { |component| net.members.any? { |member| member != "#{component.ref}.#{pin}" } }
|
|
775
|
+
boards = net.holes.map(&board_id).uniq
|
|
776
|
+
sources = positive_nets.filter_map { |name, ids| name unless (boards & ids).empty? }
|
|
777
|
+
next if sources.empty? || i2c_pullup_resistor?(circuit, state, net_name, sources)
|
|
778
|
+
|
|
779
|
+
refs = components.map(&:ref)
|
|
780
|
+
message = translate("i2c_pullup_missing", "I2C #{pin} on #{refs.join(', ')} has no modeled pull-up resistor",
|
|
781
|
+
line: pin, refs: refs.join(", "))
|
|
782
|
+
offense(rule.id, message, components.first.location,
|
|
783
|
+
targets: { components: refs, pins: refs.map { |ref| "#{ref}.#{pin}" }, nets: [net_name] }, state: state.name)
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def i2c_pullup_resistor?(circuit, state, bus_net, positive_nets)
|
|
789
|
+
circuit.components.values.any? do |component|
|
|
790
|
+
next false unless component.part.id == "resistor"
|
|
791
|
+
resistance = Breadkit::Value.parse(component.value) rescue nil
|
|
792
|
+
next false unless resistance&.positive?
|
|
793
|
+
nets = component.pins.values.map { |pin| circuit.net_of("#{component.ref}.#{pin.name}", state)&.name }
|
|
794
|
+
nets.length == 2 && nets.include?(bus_net) && nets.uniq.length == 2 && positive_nets.include?((nets - [bus_net]).first)
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
def missing_decoupling_capacitors(circuit, rule, state)
|
|
799
|
+
source_pairs = circuit.voltage_sources.filter_map do |source|
|
|
800
|
+
high = circuit.net_of(source.plus, state)&.name
|
|
801
|
+
low = circuit.net_of(source.minus, state)&.name
|
|
802
|
+
[high, low] if high && low && high != low
|
|
803
|
+
end
|
|
804
|
+
return [] if source_pairs.empty?
|
|
805
|
+
|
|
806
|
+
circuit.components.values.flat_map do |component|
|
|
807
|
+
next [] unless component.part.data["category"] == "ic" && component.part.placement != "offboard"
|
|
808
|
+
powers = component.pins.values.select { |pin| pin.role == "power" && !component.unused.include?(pin.name) && !component.unused.include?(pin.number) }
|
|
809
|
+
grounds = component.pins.values.select { |pin| pin.role == "ground" && !component.unused.include?(pin.name) && !component.unused.include?(pin.number) }
|
|
810
|
+
pairs = {}
|
|
811
|
+
powers.product(grounds).each do |power, ground|
|
|
812
|
+
high = circuit.net_of("#{component.ref}.#{power.name}", state)&.name
|
|
813
|
+
low = circuit.net_of("#{component.ref}.#{ground.name}", state)&.name
|
|
814
|
+
pairs[[high, low]] ||= [power, ground] if source_pairs.include?([high, low])
|
|
815
|
+
end
|
|
816
|
+
pairs.filter_map do |(high, low), (power, ground)|
|
|
817
|
+
next if decoupling_capacitor?(circuit, state, high, low)
|
|
818
|
+
message = translate("missing_decoupling_capacitor", "#{component.ref} has no modeled capacitor across #{power.name} and #{ground.name}",
|
|
819
|
+
ref: component.ref, power: power.name, ground: ground.name)
|
|
820
|
+
offense(rule.id, message, component.location,
|
|
821
|
+
targets: { components: [component.ref], pins: ["#{component.ref}.#{power.name}", "#{component.ref}.#{ground.name}"],
|
|
822
|
+
nets: [high, low] }, state: state.name)
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def decoupling_capacitor?(circuit, state, high, low)
|
|
828
|
+
circuit.components.values.any? do |component|
|
|
829
|
+
next false unless %w[capacitor electrolytic].include?(component.part.id)
|
|
830
|
+
nets = component.pins.values.map { |pin| circuit.net_of("#{component.ref}.#{pin.name}", state)&.name }
|
|
831
|
+
nets.length == 2 && nets.all? && nets.sort == [high, low].sort
|
|
832
|
+
end
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def missing_base_resistors(circuit, rule, state)
|
|
836
|
+
outputs = Hash.new { |hash, key| hash[key] = [] }
|
|
837
|
+
circuit.components.each_value do |component|
|
|
838
|
+
component.pins.each_value do |pin|
|
|
839
|
+
next unless pin.role == "output"
|
|
840
|
+
next if component.unused.include?(pin.name) || component.unused.include?(pin.number)
|
|
841
|
+
net = circuit.net_of("#{component.ref}.#{pin.name}", state)
|
|
842
|
+
outputs[net.name] << [component, pin] if net
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
circuit.components.values.filter_map do |transistor|
|
|
847
|
+
next unless transistor.part.data["category"] == "transistor"
|
|
848
|
+
base = transistor.pin("base")
|
|
849
|
+
next unless base && !transistor.unused.include?(base.name) && !transistor.unused.include?(base.number)
|
|
850
|
+
net = circuit.net_of("#{transistor.ref}.#{base.name}", state)
|
|
851
|
+
next unless net
|
|
852
|
+
driver, output = outputs[net.name].find { |component, _pin| component.ref != transistor.ref }
|
|
853
|
+
next unless driver
|
|
854
|
+
message = translate("missing_base_resistor", "#{driver.ref}.#{output.name} drives #{transistor.ref}.#{base.name} without a series resistor",
|
|
855
|
+
driver: "#{driver.ref}.#{output.name}", base: "#{transistor.ref}.#{base.name}")
|
|
856
|
+
offense(rule.id, message, transistor.location,
|
|
857
|
+
targets: { components: [transistor.ref, driver.ref], pins: ["#{transistor.ref}.#{base.name}", "#{driver.ref}.#{output.name}"],
|
|
858
|
+
nets: [net.name] }, state: state.name)
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
def missing_flyback_diodes(circuit, rule, state)
|
|
863
|
+
source_nets = circuit.voltage_sources.filter_map do |source|
|
|
864
|
+
positive = circuit.net_of(source.plus, state)&.name
|
|
865
|
+
negative = circuit.net_of(source.minus, state)&.name
|
|
866
|
+
[positive, negative] if positive && negative && positive != negative
|
|
867
|
+
end
|
|
868
|
+
return [] if source_nets.empty?
|
|
869
|
+
|
|
870
|
+
circuit.components.values.filter_map do |component|
|
|
871
|
+
next unless Array(component.part.data["flags"]).include?("needs_flyback_diode")
|
|
872
|
+
polarity = component.part.data["polarity"] || {}
|
|
873
|
+
positive_pin = component.pin(polarity["positive"])
|
|
874
|
+
negative_pin = component.pin(polarity["negative"])
|
|
875
|
+
next unless positive_pin && negative_pin
|
|
876
|
+
positive = circuit.net_of("#{component.ref}.#{positive_pin.name}", state)&.name
|
|
877
|
+
negative = circuit.net_of("#{component.ref}.#{negative_pin.name}", state)&.name
|
|
878
|
+
next unless positive && negative && positive != negative
|
|
879
|
+
next unless source_nets.any? { |source_positive, source_negative| source_positive == positive || source_negative == negative }
|
|
880
|
+
next unless [positive, negative].all? { |net| flyback_terminal_attached?(circuit, state, net, component.ref, source_nets) }
|
|
881
|
+
next if reverse_flyback_diode?(circuit, state, positive, negative)
|
|
882
|
+
|
|
883
|
+
message = translate("missing_flyback_diode", "#{component.ref} has no reverse flyback diode across #{positive_pin.name} and #{negative_pin.name}",
|
|
884
|
+
ref: component.ref, positive: positive_pin.name, negative: negative_pin.name)
|
|
885
|
+
offense(rule.id, message, component.location,
|
|
886
|
+
targets: { components: [component.ref], pins: ["#{component.ref}.#{positive_pin.name}", "#{component.ref}.#{negative_pin.name}"],
|
|
887
|
+
nets: [positive, negative] }, state: state.name)
|
|
888
|
+
end
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def flyback_terminal_attached?(circuit, state, net_name, owner, source_nets)
|
|
892
|
+
return true if source_nets.any? { |positive, negative| positive == net_name || negative == net_name }
|
|
893
|
+
circuit.components.values.any? do |component|
|
|
894
|
+
component.ref != owner && component.part.data["category"] != "diode" &&
|
|
895
|
+
component.pins.values.any? { |pin| circuit.net_of("#{component.ref}.#{pin.name}", state)&.name == net_name }
|
|
896
|
+
end
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def reverse_flyback_diode?(circuit, state, positive, negative)
|
|
900
|
+
circuit.components.values.any? do |component|
|
|
901
|
+
next false unless component.part.data["category"] == "diode"
|
|
902
|
+
polarity = component.part.data["polarity"] || {}
|
|
903
|
+
anode = component.pin(polarity["positive"])
|
|
904
|
+
cathode = component.pin(polarity["negative"])
|
|
905
|
+
next false unless anode && cathode
|
|
906
|
+
circuit.net_of("#{component.ref}.#{anode.name}", state)&.name == negative &&
|
|
907
|
+
circuit.net_of("#{component.ref}.#{cathode.name}", state)&.name == positive
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def direct_source_range(circuit, state, high_name, low_name)
|
|
912
|
+
sources = circuit.voltage_sources.select do |source|
|
|
913
|
+
circuit.net_of(source.plus, state)&.name == high_name && circuit.net_of(source.minus, state)&.name == low_name
|
|
914
|
+
end
|
|
915
|
+
return unless sources.length == 1
|
|
916
|
+
sources.first.voltage_range
|
|
367
917
|
end
|
|
368
918
|
|
|
369
919
|
end
|