Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 181 additions & 19 deletions doc/PDL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ A `.pdl` file describes a circuit part as data: its artwork, its pins, the
properties a user can edit, and how it behaves in a simulation. Parts written
this way need no Dart and no rebuild — drop the file and its SVG into
`packages/pinbench_parts/assets/parts/<id>/` and it appears in the palette.
A part can also take its drawing from a Dart painter instead of an SVG, and
keep everything else as data — see [PAINTER](#painter).

This exists because most parts do not need bespoke drawing code. A sensor, a
module, a passive component: a photograph-accurate body from an SVG, a few
Expand All @@ -21,40 +23,49 @@ shipped parts and draws them.

| Part | Exercises |
| --- | --- |
| `ldr` — photoresistor | `PHYSICS resistor` driven by a property |
| `ldr` — photoresistor | `PHYSICS resistor` driven by a property; a `PAINTER` body and an `ALIAS` |
| `thermistor` — 10k NTC | an exponential curve (`exp`) over two steps of state |
| `slide_switch` — SPST | a boolean property, and `visual.*` driving a shape |
| `cr2032` — coin cell | `PHYSICS voltageSource` |
| `diode_1n4148` | `PHYSICS` only — no rules at all |
| `cr2032` — coin cell | `PHYSICS voltageSource`; a `PAINTER` body |
| `capacitor_electrolytic` — polarized | `PHYSICS capacitor` driven by a µF property; a `PAINTER` body |
| `diode_1n4148` — switching | `PHYSICS` only — no rules at all; a `PAINTER` body |
| `diode_1n4007` — rectifier, `diode_1n5819` — Schottky | `PHYSICS diode` with each part's own model numbers; one `PAINTER` shared by three parts |
| `pir_sensor` — PIR motion | `LOGIC` — a one-shot needing a clock |
| `tmp36`, `lm35` — analog temperature | `PHYSICS voltageSource` driven by a property, read by `analogRead` |
| `soil_moisture` — capacitive probe | an inverted, offset output range in two steps of state |
| `battery_9v` | `PHYSICS voltageSource`, like the coin cell |
| `battery_9v` | `PHYSICS voltageSource`, like the coin cell; a `PAINTER` body |
| `transistor` — seven types, TO-92 and TO-220 | `CONFIGURATION` — one part picked by its Type: `PHYSICS npn`/`pnp`/`nmos`/`pmos`/`npnDarlington`, power MOSFETs with their own `vto` and `kp` |
| `slide_switch_spdt` — SPDT | `PHYSICS spdt` thrown by an `enum` property; a `PAINTER` that slides its lever to match |
| `seven_segment` — common cathode | `PHYSICS ledArray` of eight dies and two tied cathode pins; `LOGIC` into `STATE` per segment; a `PAINTER` that lights each |
| `ir_receiver` — VS1838B | `PHYSICS resistor` only — its internal pull-up; the IR Remote part's presses arrive on OUT as timed NEC codes, played in by the engine |
| `rgb_led` — common cathode | `PHYSICS rgbLed`, the `led_array` `LOGIC` reading a current per pin into `STATE`, and a `PAINTER` that lights up from that state |
| `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` |
| `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 |
| `ds1307` — real-time clock, I²C | `LOGIC` keeping time on the simulated clock; an `enum` property |
| `aht20` — temperature + humidity, I²C | `LOGIC` with no `STATE` at all: a reading republished every frame |

## A complete example

This is the part shipped at `assets/parts/ldr/ldr.pdl`. Nothing else is
needed to make a working photoresistor.
This is the part shipped at `assets/parts/ldr/ldr.pdl`. Everything about how
it behaves is here; the only Dart it leans on is its drawing, the `ldr`
painter.

```pdl
# Photoresistor (LDR) — resistance falls as light rises.
# LDR (photoresistor) — resistance falls as light rises.

PART "Photoresistor"
PART "LDR"
ALIAS "Photoresistor"
ID ldr
CATEGORY sensors
DESCRIPTION "Light-dependent resistor"

SIZE 12.7mm 6.35mm
SVG "ldr.svg"
SIZE 56px 52px
PAINTER ldr

PINS
# id x y role label
a 4px 20px passive "A"
b 68px 20px passive "B"
a 20px 52px passive "A"
b 36px 52px passive "B"

PROPERTIES
darkResistance number 1000000 unit:"Ω" label:"Dark resistance"
Expand Down Expand Up @@ -104,18 +115,80 @@ body sizes so the part is drawn the size the real thing is.
| `ID <id>` | yes | Stable key. **Permanent** — see below. |
| `SIZE <w> <h>` | yes | Footprint. |
| `SVG "<path>"` | yes¹ | Artwork, relative to this file. |
| `PAINTER <name>` | yes¹ | Draws the body with a registered Dart painter — see [PAINTER](#painter). |
| `ALIAS "<name>"` | no | Another name the part answers to. Repeatable. |
| `CATEGORY <name>` | no | Palette group (`sensors`, `basic`, `actuators`, …). |
| `DESCRIPTION "<text>"` | no | Palette tooltip. |
| `ORIGIN <x> <y>` | no | Part origin inside the footprint. Defaults to the centre. |
| `LOGIC <name>` | no | Names a Dart behaviour — see [LOGIC](#logic). |
| `CONFIGURATION <property> "<option>"` | no | Starts one configuration of a part with several — see [CONFIGURATION](#configuration). |

¹ Either `SVG` or a `VISUALS` section. A part with neither draws nothing and
is rejected.
¹ One of `SVG`, `PAINTER` or a `VISUALS` section. A part with none draws
nothing and is rejected; `SVG` and `PAINTER` together are rejected too, since
both would draw the body.

`ALIAS` is for renames. The palette search matches an alias, and so does a
`.cdl` type token, so a file saved under the old name still loads — and is
written back under the new one. The LDR shipped as `Photoresistor`.

> **`ID` is permanent.** It is stored as `definitionId` on every placed
> component in every saved `.cdl`. Changing it orphans them all. The name can
> change freely; the id cannot.

## CONFIGURATION

One part in several configurations, picked by a property — the way one LED is
any colour and one resistor any value, except that each configuration may
differ in everything: size, pins, drawing, physics. The bundled `transistor`
is NPN, PNP, a MOSFET or a TO-220 power part by its Type:

```
PART "Transistor"
ID transistor
CATEGORY basic

PROPERTIES
type enum "NPN (2N3904)" options:"NPN (2N3904),N-MOSFET (2N7000)" label:"Type"

CONFIGURATION type "NPN (2N3904)"
ID transistor_npn
ALIAS "2N3904"
SIZE 40px 72px
PAINTER transistor_npn
PINS
collector 4px 68px passive "C"
base 20px 68px passive "B"
emitter 36px 68px passive "E"
PHYSICS npn
c = collector
b = base
e = emitter

CONFIGURATION type "N-MOSFET (2N7000)"
ID transistor_nmos
…
```

Everything before the first `CONFIGURATION` is shared. Each block runs to the
next, and is read as the shared lines followed by its own: it adds pins,
properties, rules and parameters to what is shared, and its single-value
directives — `SIZE`, `PAINTER`, `DESCRIPTION` — take the place of shared ones.

- The property must be an `enum` declared in the shared part, with a
`CONFIGURATION` for every option and none for anything else.
- Every configuration needs an **`ID` of its own**: it is what a placed part
saves, and is as permanent as any other. The shared `ID` names the part as a
whole.
- The palette lists the part once, as the configuration for the property's
default, and finds it by any configuration's `ALIAS`.

Changing the property on the canvas swaps the placed part for that
configuration. Each wire moves to the pin with the same id, or else to the pin
in the same place in the new `PHYSICS` mapping — so listing every
configuration's nodes in the same order (`c`, `b`, `e` and `d`, `g`, `s`) is
what turns a collector into a drain. A wire whose pin has no counterpart is
removed, and one undo brings the part and its wires back.

## PINS

```
Expand Down Expand Up @@ -246,8 +319,9 @@ bad rule stops the part loading rather than surfacing as a wrong number later.

## VISUALS

Vector shapes drawn **over** the SVG. Use these only for what changes; a
static body belongs in the artwork.
Vector shapes drawn **over** the body, whether that is an SVG or a
`PAINTER`. Use these only for what changes; a static body belongs in the
artwork.

```
VISUALS
Expand Down Expand Up @@ -281,6 +355,43 @@ An attribute value cannot contain spaces unless it is quoted, because
attributes are single tokens — so a multi-term expression needs quoting:
`color:"lerp(#000, #fff, state.x)"`.

## PAINTER

A body can be drawn by Dart instead of an SVG, while the part stays a `.pdl`:

```
PAINTER ldr
```

The name is looked up in `PartPainterRegistry` in `pinbench_parts`, which maps
it to a painter. That painter draws only the body: it is scaled onto `SIZE`,
`VISUALS` still draw over it, and its pins, properties, behaviour and physics
are all this file's. Only the drawing is code.

This is the shape to reach for when the artwork comes from a design tool as
vector paths rather than as an SVG file. `battery_9v`, `cr2032`,
`capacitor_electrolytic`, the diodes, `ir_receiver`, `ldr`, `rgb_led` and the transistors were drawn
in Figma, one layer per colour, and converted to Flutter paths with the
Flutter Da Vinci plugin; their painters (`battery_9v_painter.dart`,
`cr2032_painter.dart`, `electrolytic_capacitor_painter.dart`,
`axial_diode_painter.dart`, `ldr_painter.dart`, `rgb_led_painter.dart`,
`transistor_painter.dart`, `to220_painter.dart`) are those paths, drawn through
the `PathArtPainter` mixin. A painter also receives the placed part's
properties and state, so it can draw what changes too: `rgb_led` mixes its
glow from the `red`, `green` and `blue` state its logic writes. One painter can serve several parts: the TO-92
transistors share a body and register a `transistor_<kind>` name each, and the
TO-220 parts a `to220_<kind>`.
The part itself is still the `.pdl` beside them.

A painter whose body is smaller than `SIZE` reports it through `bodyRect`, so
the part is hit on the body and not across its whole bounds. An unregistered
name is not fatal — the part loads and draws its `VISUALS` — and the
bundled-parts test fails on it.

Use `PAINTER` for a static body. A part whose look is the simulation — a
servo horn, a display's pixels — is still better as a built-in part with a
hand-written painter.

## PHYSICS

How the part participates in the analog solve.
Expand All @@ -300,14 +411,63 @@ SPICE node name to a pin id. Mapping to an undeclared pin is a parse error.
| `resistor` | `R_…` | `resistance` | yes |
| `voltageSource` (`vsource`) | `V_…` | `voltage` | yes |
| `capacitor` | `C_…` | `capacitance` | yes |
| `diode` | `D_…` | — | no |
| `diode` | `D_…` | model parameters (`is`, `n`) | no |
| `currentSource` (`isource`) | — | — | not yet built |
| `npn`, `pnp` | `Q_…` | model parameters | no |
| `nmos`, `pmos` | `M_…` | model parameters | no |
| `npnDarlington`, `pnpDarlington` | two `Q_…` | model parameters, `r1`, `r2` | no |
| `rgbLed` | three `D_…` | — | no |
| `spdt` | two `R_…` | `position` (0 = `a`, 1 = `b`) | yes |
| `ledArray` | one `D_…` per mapped die | model parameters (`is`, `n`) | no |

A matching `physics.<param>` rule overrides the static parameter and is pushed
into ngspice each frame via `alter`, the same mechanism the Arduino's pin
sources use. That avoids rebuilding the circuit, which would reset every node
voltage mid-run.

### Transistors

A transistor maps three nodes: `c`, `b`, `e` for a bipolar one, `d`, `g`, `s`
for a MOSFET, whose body is tied to its source. Its numeric parameters are the
SPICE `.model` parameters, applied over the engine's defaults — a 2N3904 for
`npn`, a 2N3906 for `pnp`, and level-1 MOSFETs near a 2N7000 and its
P-channel counterpart:

```
PHYSICS npn
c = collector
b = base
e = emitter
bf = 300 # a higher-gain part than the default
```

A Darlington pair maps the same three nodes as one bipolar transistor and is
built as two, the first driving the second's base, with a resistor across each
base-emitter junction as in a TIP120: `r1` (8 kΩ) across the driver, `r2`
(120 Ω) across the output. Its other parameters apply to both transistors.

An `rgbLed` maps each colour's anode — `r`, `g`, `b` — and the shared cathode
`k`. The red die drops about 2 V at 20 mA, green and blue about 3 V, and each
is measured like a single LED, so a `LOGIC` reads a colour's current with
`context.spice.pinCurrent('<its anode pin>')`. The bundled part uses the
`led_array` logic below, which turns those into `state.red`, `state.green`
and `state.blue`, and its painter reads them to light the lens.

An `ledArray` generalises that to any number of dies on one cathode: each
role but `k` maps a die's anode — a 7-segment display maps `a` to `g` and
`dp` — and a second cathode pin maps `k2`, tied to `k` inside the part as it
is in the real one. The `led_array` logic writes each die's brightness into
`STATE` under its pin's id.

A power MOSFET is the same `nmos`/`pmos` element with bigger numbers: the
transistor's `power_nmos` and `power_pmos` configurations declare `vto` and
`kp` near an IRLZ44N and an IRF9540N rather than adding a type.

The solve is an operating point, so only DC parameters matter (`is`, `bf`,
`br`, `vaf`, … and `vto`, `kp`, `lambda`). A MOSFET's gate is tied to its
source through 1 GΩ so that a gate left unwired is off rather than an
unsolvable floating node.

## LOGIC

Some parts cannot be a rule. `BEHAVIOR` is total by design — no loops, no
Expand Down Expand Up @@ -504,7 +664,8 @@ mechanism replaced a per-part frame updater in the engine with.

## Assets

A part is a directory: `assets/parts/<id>/<id>.pdl` plus its SVG. Add the
A part is a directory: `assets/parts/<id>/<id>.pdl` plus its SVG, if it draws
with one. Add the
directory to `packages/pinbench_parts/pubspec.yaml` under `flutter: assets:`.

A relative `SVG "foo.svg"` resolves next to the `.pdl` that named it, so a part
Expand Down Expand Up @@ -554,7 +715,8 @@ Honest about what is not there yet:

1. `mkdir packages/pinbench_parts/assets/parts/<id>/`
2. Draw the body as an SVG. Its viewBox is scaled onto `SIZE`, so the units
inside it do not matter — only the aspect.
inside it do not matter — only the aspect. Or, for vector art exported as
paths, register a painter and name it with [`PAINTER`](#painter).
3. Write `<id>.pdl`. Put pins on the lattice (`4, 20, 36, …`).
4. Declare the directory in `packages/pinbench_parts/pubspec.yaml`.
5. `cd packages/pinbench_parts && flutter test` — the bundled-parts test will tell
Expand Down
Loading
Loading