Skip to content
Merged
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
50 changes: 45 additions & 5 deletions doc/PDL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ shipped parts and draws them.
| `cr2032` — coin cell | `PHYSICS voltageSource` |
| `diode_1n4148` | `PHYSICS` only — no rules at all |
| `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 |
| `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

Expand Down Expand Up @@ -403,6 +410,8 @@ on I²C means being on `A4` and `A5` specifically.
| | |
| --- | --- |
| `drain(address)` | every transaction sent to `address` since the last call, oldest first; **asking claims the address** |
| `serve(address, size:, pointerBytes:)` | makes `address` answer the sketch's reads from a bank of `size` registers (default 256, pointer 1 byte); claims the address too |
| `setRegisters(address, offset, bytes)` | publishes `bytes` into those registers from `offset`, wrapping at the end |

A bus is a different shape from everything else here. A pin has a *level* a
part can sample whenever it likes; a bus has *traffic*, and traffic missed is
Expand All @@ -415,8 +424,36 @@ sketch finds the devices on the canvas and nothing else. Traffic sent *before*
anyone claims is still kept, because a sketch initialises its display inside
`setup()` — before any logic has run.

Narrow like the rest: a part listens at its own address. It cannot see another
device's traffic and cannot write to the bus. Only the ATmega328P's hardware
**Answering reads.** A read has to be answered the moment the sketch clocks
it, in the middle of the CPU's frame, and a logic only runs between frames. So
a part that talks back does what real I²C devices do: it `serve`s registers
and keeps them current with `setRegisters`, and the bus answers from them. The
sketch writes a register pointer (the first `pointerBytes` bytes of a write,
most significant first), then reads, and the pointer advances after every
byte — which is how `Wire.requestFrom` fetches a six-byte accelerometer sample
in one go. Bytes the sketch writes after the pointer are stored, so a register
reads back what the sketch put there, as setting an RTC's clock expects; and
those writes still arrive through `drain`, so the part can react to them.
`pointerBytes: 0` is for devices with nothing to select (a PCF8574), whose
every read starts at register 0. Serving the same shape again every frame
keeps the registers; a reading refreshed each frame is at most 16 ms old. An
address nothing serves reads `0xFF`, like a pulled-up, undriven bus.

```dart
void mpu6050(PartLogicContext context) {
const address = 0x68;
context.i2c.serve(address); // MPU6050: 1-byte pointer
context.i2c.setRegisters(address, 0x75, [0x68]); // WHO_AM_I
context.i2c.setRegisters(address, 0x3B, sample()); // accelerometer, 6 bytes
for (final write in context.i2c.drain(address)) {
// [0x6B, 0x00] wakes it up, [0x1C, range] sets the range…
}
}
```

Narrow like the rest: a part listens at and serves its own address. It cannot
see another device's traffic, and it never drives the bus — the sketch is
always the one clocking bytes. Only the ATmega328P's hardware
TWI is emulated, which is `A4`/`A5`; a sketch bit-banging I²C on other pins is
not modelled.

Expand Down Expand Up @@ -459,8 +496,9 @@ emulator is.
| `led` | lit from element current, brightness from the driving pin's duty |
| `buzzer` | sounding and pitch from the frequency detected on its `plus` port |
| `ssd1306` | an OLED display's registers and 1 KB of graphics RAM, decoded from the I²C traffic sent to its `I2C Address` |
| `bh1750`, `mpu6050`, `ds1307`, `aht20` | I²C sensors, in `i2c_sensors.dart`. Each answers only when its `vcc`/`gnd` reach the board's supply and ground and its `sda`/`scl` reach `A4`/`A5`; see `assets/parts/<id>/` |

The last four have no `.pdl` file — they are built-in parts with hand-written
`led`, `servo`, `buzzer` and `ssd1306` have no `.pdl` file — they are built-in parts with hand-written
painters that declare `logic:` on their `PartModel`. They are what this
mechanism replaced a per-part frame updater in the engine with.

Expand Down Expand Up @@ -497,8 +535,10 @@ Warnings load the part anyway: no pins at all, an unknown pin role, an unknown
Honest about what is not there yet:

- **One protocol, and only from Dart.** `context.i2c` gives a `LOGIC` the I²C
traffic addressed to it; SPI, UART, radio and IR have no equivalent, and no
protocol is expressible as `BEHAVIOR` rules.
traffic addressed to it and a register bank to answer reads from; SPI, UART,
1-Wire, radio and IR have no equivalent, and no protocol is expressible as
`BEHAVIOR` rules. A device whose reply depends on the exact moment it is
read, rather than on registers, can only be approximated.
- **A logic cannot drive the board.** It reads its own ports and the pins they
reach; it cannot write them. Modelling a part that pulls a line low needs a
capability that does not exist.
Expand Down
Loading