diff --git a/README.md b/README.md index d2c2588..a1eb79f 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ forking is welcome and always will be, just under your own name. | State | Riverpod 3.x with codegen | | Canvas | Custom `CustomPainter` | | Editor | `re_editor` | -| AVR emulation | `avr8_dart` (native), `avr8js` (web) | +| AVR emulation | `avr8_dart` (native, and WebAssembly on the web) | | Analog solver | `ngspice_dart` — a pure-Dart port, no FFI or native libngspice | | Audio | `flutter_soloud` (native), Web Audio (web) | | Multi-window | `multiview_desktop` | diff --git a/lib/core/telemetry/otel_backend.dart b/lib/core/telemetry/otel_backend.dart index 7ce294d..6f28d09 100644 --- a/lib/core/telemetry/otel_backend.dart +++ b/lib/core/telemetry/otel_backend.dart @@ -3,8 +3,8 @@ // `dartastic_opentelemetry`'s OTLP exporters import `dart:io`, so they can only // compile for native targets. Native builds use the real backend; the web (and // wasm) build overrides to the stub, which never references the package — -// keeping `dart:io` out of the web compilation. This mirrors the `avr_interop` -// idiom used elsewhere in the app (native default, web override). +// keeping `dart:io` out of the web compilation (native default, web +// override). // // Both variants expose `Future initOpenTelemetry(OtelConfig)`. export 'otel_backend_io.dart' if (dart.library.js_interop) 'otel_backend_stub.dart'; diff --git a/packages/pinbench_parts/assets/parts/aht20/aht20.pdl b/packages/pinbench_parts/assets/parts/aht20/aht20.pdl new file mode 100644 index 0000000..0cf9f0e --- /dev/null +++ b/packages/pinbench_parts/assets/parts/aht20/aht20.pdl @@ -0,0 +1,26 @@ +# AHT20 temperature and humidity sensor. +# +# The I²C answer to "DHT11/22": the same two readings, but over a bus the +# emulator already models, where a DHT's single-wire timing would need a part +# that can drive a pin. Served by `LOGIC aht20` at its fixed address, 0x38: +# a status byte, 20 bits each of humidity and temperature, and a CRC. + +PART "Temperature + Humidity (AHT20)" +ID aht20 +CATEGORY sensors +DESCRIPTION "I²C temperature and relative humidity sensor" + +SIZE 10.16mm 12.7mm +SVG "aht20.svg" + +LOGIC aht20 + +PINS + vcc 4px 68px power "VIN" + gnd 20px 68px ground "GND" + scl 36px 68px input "SCL" + sda 52px 68px passive "SDA" + +PROPERTIES + temperature number 25 unit:"°C" label:"Temperature" + humidity number 50 unit:"%" label:"Relative humidity" diff --git a/packages/pinbench_parts/assets/parts/aht20/aht20.svg b/packages/pinbench_parts/assets/parts/aht20/aht20.svg new file mode 100644 index 0000000..6b3644e --- /dev/null +++ b/packages/pinbench_parts/assets/parts/aht20/aht20.svg @@ -0,0 +1,18 @@ + + + + + AHT20 + VIN + GND + SCL + SDA + + + + + + + + + diff --git a/packages/pinbench_parts/assets/parts/battery_9v/battery_9v.pdl b/packages/pinbench_parts/assets/parts/battery_9v/battery_9v.pdl new file mode 100644 index 0000000..98b6d1d --- /dev/null +++ b/packages/pinbench_parts/assets/parts/battery_9v/battery_9v.pdl @@ -0,0 +1,27 @@ +# 9 V battery (PP3). +# +# Like the coin cell: an ideal source. It does not sag under load or run +# down. Enough to power a circuit off-board, or to show what 9 V does to an +# LED with no resistor. + +PART "Battery (9V)" +ID battery_9v +CATEGORY basic +DESCRIPTION "9V PP3 battery" + +SIZE 48.26mm 25.4mm +SVG "battery_9v.svg" + +PINS + plus 292px 52px power "+" + minus 292px 100px ground "-" + +PROPERTIES + voltage number 9 unit:"V" label:"Battery voltage" + +BEHAVIOR + physics.voltage = max(prop.voltage, 0) + +PHYSICS voltageSource + n1 = plus + n2 = minus diff --git a/packages/pinbench_parts/assets/parts/battery_9v/battery_9v.svg b/packages/pinbench_parts/assets/parts/battery_9v/battery_9v.svg new file mode 100644 index 0000000..6139553 --- /dev/null +++ b/packages/pinbench_parts/assets/parts/battery_9v/battery_9v.svg @@ -0,0 +1,12 @@ + + + + + 9V + + + + + + + − + diff --git a/packages/pinbench_parts/assets/parts/bh1750/bh1750.pdl b/packages/pinbench_parts/assets/parts/bh1750/bh1750.pdl new file mode 100644 index 0000000..8bae493 --- /dev/null +++ b/packages/pinbench_parts/assets/parts/bh1750/bh1750.pdl @@ -0,0 +1,36 @@ +# BH1750 ambient light sensor on the GY-302 module. +# +# An I²C part: everything the sketch sees is two bytes of result, so the whole +# behaviour is `LOGIC bh1750` serving them. The logic tracks the opcodes the +# sketch sends (the measurement mode and time) in STATE, and answers 0 until a +# measurement has been asked for, as a freshly powered chip does. +# +# ADDR low or unconnected puts it at 0x23; tied to 5V or 3.3V, at 0x5C. It is +# only acknowledged when powered and wired to A4/A5. + +PART "Light Sensor (BH1750)" +ID bh1750 +CATEGORY sensors +DESCRIPTION "I²C ambient light sensor, 1–65535 lx" + +SIZE 12.7mm 19.05mm +SVG "bh1750.svg" + +LOGIC bh1750 + +PINS + vcc 4px 116px power "VCC" + gnd 20px 116px ground "GND" + scl 36px 116px input "SCL" + sda 52px 116px passive "SDA" + addr 68px 116px input "ADDR" + +PROPERTIES + illuminance number 250 unit:"lx" label:"Light level" + +STATE + # The last measurement opcode the sketch sent; 0 until it sends one. + mode integer 0 + # Measurement time register. 69 is the chip's default; libraries change it + # to trade range for resolution. + mtreg integer 69 diff --git a/packages/pinbench_parts/assets/parts/bh1750/bh1750.svg b/packages/pinbench_parts/assets/parts/bh1750/bh1750.svg new file mode 100644 index 0000000..8ec7c22 --- /dev/null +++ b/packages/pinbench_parts/assets/parts/bh1750/bh1750.svg @@ -0,0 +1,23 @@ + + + + + + GY-302 + BH1750 + VCC + GND + SCL + SDA + ADD + + + + + + + + + + + diff --git a/packages/pinbench_parts/assets/parts/ds1307/ds1307.pdl b/packages/pinbench_parts/assets/parts/ds1307/ds1307.pdl new file mode 100644 index 0000000..46ee6e7 --- /dev/null +++ b/packages/pinbench_parts/assets/parts/ds1307/ds1307.pdl @@ -0,0 +1,41 @@ +# DS1307 real-time clock module. +# +# An I²C part served by `LOGIC ds1307`: seven BCD time registers, plus control +# and 56 bytes of RAM that the bus stores and reads back on its own. The clock +# counts simulated time, so it keeps pace with millis() and stops while the run +# is paused. +# +# "Clock at start" picks between a module whose battery kept it set (it starts +# at the computer's current time) and a fresh chip, halted at 2000-01-01 until +# the sketch sets it — the case `if (!rtc.isrunning()) rtc.adjust(...)` is for. +# +# The DS1307 needs 5 V; wired to 3.3V it does not answer. + +PART "Real-Time Clock (DS1307)" +ID ds1307 +CATEGORY sensors +DESCRIPTION "I²C real-time clock with battery backup" + +SIZE 20.32mm 15.24mm +SVG "ds1307.svg" + +LOGIC ds1307 + +PINS + gnd 36px 84px ground "GND" + vcc 52px 84px power "VCC" + sda 68px 84px passive "SDA" + scl 84px 84px input "SCL" + +PROPERTIES + startsAt enum now options:"now,unset" label:"Clock at start" + +STATE + booted boolean false + # Clock-halt bit (bit 7 of the seconds register). + halted boolean false + # The time, as seconds since 2000-01-01, when it was last set... + baseSeconds decimal 0 + # ...and the simulated millisecond it was set at. The time now is the one + # plus however far the other has moved on. + anchorMs decimal 0 diff --git a/packages/pinbench_parts/assets/parts/ds1307/ds1307.svg b/packages/pinbench_parts/assets/parts/ds1307/ds1307.svg new file mode 100644 index 0000000..a77e868 --- /dev/null +++ b/packages/pinbench_parts/assets/parts/ds1307/ds1307.svg @@ -0,0 +1,22 @@ + + + + + CR2032 + + DS1307 + + RTC + GND + VCC + SDA + SCL + + + + + + + + + diff --git a/packages/pinbench_parts/assets/parts/lm35/lm35.pdl b/packages/pinbench_parts/assets/parts/lm35/lm35.pdl new file mode 100644 index 0000000..f114bde --- /dev/null +++ b/packages/pinbench_parts/assets/parts/lm35/lm35.pdl @@ -0,0 +1,31 @@ +# LM35 analog temperature sensor (TO-92). +# +# Vout = 10 mV/°C from 0 V at 0 °C, so 250 mV at 25 °C. Wired with a single +# supply, as in every Arduino tutorial, it cannot go below 0 V, so it reads +# 2 °C to 150 °C. Modelled as a voltage source between Vout and GND; +Vs is +# there because the real part needs it, and the model does not read it. +# +# Legs left to right with the flat face towards you: +Vs, Vout, GND. + +PART "Temperature Sensor (LM35)" +ID lm35 +CATEGORY sensors +DESCRIPTION "Analog temperature sensor, 10 mV/°C" + +SIZE 7.62mm 10.16mm +SVG "lm35.svg" + +PINS + vs 4px 52px power "+Vs" + vout 20px 52px output "Vout" + gnd 36px 52px ground "GND" + +PROPERTIES + temperature number 25 unit:"°C" label:"Temperature" + +BEHAVIOR + physics.voltage = clamp(prop.temperature, 2, 150) * 0.01 + +PHYSICS voltageSource + n1 = vout + n2 = gnd diff --git a/packages/pinbench_parts/assets/parts/lm35/lm35.svg b/packages/pinbench_parts/assets/parts/lm35/lm35.svg new file mode 100644 index 0000000..fa08173 --- /dev/null +++ b/packages/pinbench_parts/assets/parts/lm35/lm35.svg @@ -0,0 +1,7 @@ + + + + + + LM35 + diff --git a/packages/pinbench_parts/assets/parts/mpu6050/mpu6050.pdl b/packages/pinbench_parts/assets/parts/mpu6050/mpu6050.pdl new file mode 100644 index 0000000..7b0408f --- /dev/null +++ b/packages/pinbench_parts/assets/parts/mpu6050/mpu6050.pdl @@ -0,0 +1,50 @@ +# MPU-6050 accelerometer and gyroscope on the GY-521 module. +# +# An I²C part served by `LOGIC mpu6050`: WHO_AM_I, the power and range +# registers, and the 14-byte sample from 0x3B. Like the chip it boots asleep, +# so a sketch that never clears the sleep bit reads zeros, and it scales each +# reading to the range the sketch selected. +# +# The readings are properties: set the tilt and rotation you want the sketch to +# see. At rest and level, a sensor feels 1 g on Z. +# +# AD0 low or unconnected puts it at 0x68; tied to 5V or 3.3V, at 0x69. XDA, +# XCL and INT are there because the module has them; they are not modelled. + +PART "Accelerometer + Gyro (MPU-6050)" +ID mpu6050 +CATEGORY sensors +DESCRIPTION "I²C 6-axis motion sensor" + +SIZE 20.32mm 15.24mm +SVG "mpu6050.svg" + +LOGIC mpu6050 + +PINS + vcc 4px 84px power "VCC" + gnd 20px 84px ground "GND" + scl 36px 84px input "SCL" + sda 52px 84px passive "SDA" + xda 68px 84px passive "XDA" + xcl 84px 84px passive "XCL" + ad0 100px 84px input "AD0" + int 116px 84px output "INT" + +PROPERTIES + accelX number 0 unit:"g" label:"Acceleration X" + accelY number 0 unit:"g" label:"Acceleration Y" + accelZ number 1 unit:"g" label:"Acceleration Z" + gyroX number 0 unit:"°/s" label:"Rotation X" + gyroY number 0 unit:"°/s" label:"Rotation Y" + gyroZ number 0 unit:"°/s" label:"Rotation Z" + temperature number 25 unit:"°C" label:"Die temperature" + +STATE + # Set once the power-on register values have been published this run. + booted boolean false + # PWR_MGMT_1, GYRO_CONFIG and ACCEL_CONFIG as the sketch last wrote them. + # 0x40 is asleep, the chip's power-on value. + powerManagement integer 64 + gyroConfig integer 0 + accelConfig integer 0 diff --git a/packages/pinbench_parts/assets/parts/mpu6050/mpu6050.svg b/packages/pinbench_parts/assets/parts/mpu6050/mpu6050.svg new file mode 100644 index 0000000..8502ef2 --- /dev/null +++ b/packages/pinbench_parts/assets/parts/mpu6050/mpu6050.svg @@ -0,0 +1,35 @@ + + + + + + + GY-521 + + X + Y + VCC + GND + SCL + SDA + XDA + XCL + AD0 + INT + + + + + + + + + + + + + + + + + diff --git a/packages/pinbench_parts/assets/parts/soil_moisture/soil_moisture.pdl b/packages/pinbench_parts/assets/parts/soil_moisture/soil_moisture.pdl new file mode 100644 index 0000000..f902ac2 --- /dev/null +++ b/packages/pinbench_parts/assets/parts/soil_moisture/soil_moisture.pdl @@ -0,0 +1,39 @@ +# Capacitive soil moisture sensor (v1.2). +# +# The output falls as the soil gets wetter: about 2.8 V in dry air down to +# about 1.3 V in water, which is roughly analogRead() 570 dry and 265 wet on a +# 5 V board. Calibrating those two numbers is the first step of every +# soil-moisture tutorial, so the model keeps the inverted, offset range rather +# than a tidy 0–5 V. +# +# Modelled as a voltage source between AOUT and GND. VCC is there because the +# real module needs it; the model does not read it. + +PART "Soil Moisture Sensor" +ID soil_moisture +CATEGORY sensors +DESCRIPTION "Capacitive soil moisture probe with analog output" + +SIZE 10.16mm 35.56mm +SVG "soil_moisture.svg" + +PINS + gnd 4px 4px ground "GND" + vcc 20px 4px power "VCC" + aout 36px 4px output "AOUT" + +PROPERTIES + moisture number 40 unit:"%" label:"Soil moisture" + dryVolts number 2.8 unit:"V" label:"Output in dry air" + wetVolts number 1.3 unit:"V" label:"Output in water" + +STATE + level decimal 0 + +BEHAVIOR + state.level = clamp(prop.moisture, 0, 100) / 100 + physics.voltage = prop.dryVolts - state.level * (prop.dryVolts - prop.wetVolts) + +PHYSICS voltageSource + n1 = aout + n2 = gnd diff --git a/packages/pinbench_parts/assets/parts/soil_moisture/soil_moisture.svg b/packages/pinbench_parts/assets/parts/soil_moisture/soil_moisture.svg new file mode 100644 index 0000000..196037c --- /dev/null +++ b/packages/pinbench_parts/assets/parts/soil_moisture/soil_moisture.svg @@ -0,0 +1,17 @@ + + + + + + Capacitive + Soil v1.2 + + + + + + + GND + VCC + AOUT + diff --git a/packages/pinbench_parts/assets/parts/tmp36/tmp36.pdl b/packages/pinbench_parts/assets/parts/tmp36/tmp36.pdl new file mode 100644 index 0000000..0659caf --- /dev/null +++ b/packages/pinbench_parts/assets/parts/tmp36/tmp36.pdl @@ -0,0 +1,33 @@ +# TMP36 analog temperature sensor (TO-92). +# +# Vout = 0.5 V + 10 mV/°C, so 750 mV at 25 °C: the offset is what lets it +# read below freezing from a single supply. Modelled as a voltage source +# between Vout and GND, so wire GND to the board's GND and Vout to an analog +# pin. +Vs is there because the real part needs it; the model does not read +# it. +# +# Legs left to right with the flat face towards you: +Vs, Vout, GND. + +PART "Temperature Sensor (TMP36)" +ID tmp36 +CATEGORY sensors +DESCRIPTION "Analog temperature sensor, 10 mV/°C with 500 mV offset" + +SIZE 7.62mm 10.16mm +SVG "tmp36.svg" + +PINS + vs 4px 52px power "+Vs" + vout 20px 52px output "Vout" + gnd 36px 52px ground "GND" + +PROPERTIES + temperature number 25 unit:"°C" label:"Temperature" + +BEHAVIOR + # Rated from -40 °C to +125 °C; the output saturates outside that. + physics.voltage = 0.5 + clamp(prop.temperature, -40, 125) * 0.01 + +PHYSICS voltageSource + n1 = vout + n2 = gnd diff --git a/packages/pinbench_parts/assets/parts/tmp36/tmp36.svg b/packages/pinbench_parts/assets/parts/tmp36/tmp36.svg new file mode 100644 index 0000000..55827e7 --- /dev/null +++ b/packages/pinbench_parts/assets/parts/tmp36/tmp36.svg @@ -0,0 +1,7 @@ + + + + + + TMP36 + diff --git a/packages/pinbench_parts/lib/cdl/src/circuit_model_writer.dart b/packages/pinbench_parts/lib/cdl/src/circuit_model_writer.dart index cb638cd..a1171fa 100644 --- a/packages/pinbench_parts/lib/cdl/src/circuit_model_writer.dart +++ b/packages/pinbench_parts/lib/cdl/src/circuit_model_writer.dart @@ -5,6 +5,7 @@ import '../../models/component_instance.dart'; import '../../models/part_model.dart'; import '../../models/wire_model.dart'; import '../../parser_utils.dart'; +import '../../part_registry.dart'; import '../circuit_colors.dart'; import 'cdl_property_keys.dart'; @@ -50,9 +51,18 @@ abstract final class CircuitModelWriter { // used verbatim as the properties-panel label and dropdown option); the // `.cdl` presents them lowercase (`color: red`) for a cleaner, consistent // look, and `CircuitParser.parse` maps them back. Unknown keys pass through. + // + // A `.pdl` part's declared `STATE` is the same kind of value — what a run + // measured, recreated every run — so it is skipped too. Saving a PIR's + // `holdUntilMs` or a sensor's register settings would dirty the file the + // same way, and hand the next run a stale value to start from. + final definitionId = node.part.definitionId; + final declaredState = definitionId == null + ? const {} + : PartRegistry.getPart(definitionId)?.state.keys.toSet() ?? const {}; final properties = { for (final entry in node.properties.entries) - if (!ComponentProps.runtimeFlags.contains(entry.key)) + if (!ComponentProps.runtimeFlags.contains(entry.key) && !declaredState.contains(entry.key)) CdlPropertyKeys.toCdl(entry.key): entry.key == ComponentProps.color ? entry.value.toString().toLowerCase() : _unitless(entry.value.toString()), diff --git a/packages/pinbench_parts/lib/logic/built_in_part_logic.dart b/packages/pinbench_parts/lib/logic/built_in_part_logic.dart index 9a9e8dd..c2601ba 100644 --- a/packages/pinbench_parts/lib/logic/built_in_part_logic.dart +++ b/packages/pinbench_parts/lib/logic/built_in_part_logic.dart @@ -1,5 +1,6 @@ import '../models/part_model.dart'; +import 'i2c_sensors.dart'; import 'part_logic.dart'; import 'ssd1306.dart'; @@ -34,6 +35,10 @@ abstract final class BuiltInPartLogic { PartLogicRegistry.register('led', led); PartLogicRegistry.register('buzzer', buzzer); PartLogicRegistry.register('ssd1306', ssd1306); + PartLogicRegistry.register('bh1750', I2cSensors.bh1750); + PartLogicRegistry.register('mpu6050', I2cSensors.mpu6050); + PartLogicRegistry.register('ds1307', I2cSensors.ds1307); + PartLogicRegistry.register('aht20', I2cSensors.aht20); }); /// The I²C address an SSD1306 module answers on when its property says @@ -84,30 +89,16 @@ abstract final class BuiltInPartLogic { /// R3 header's separate `SDA`/`SCL` pins count too: they are the same two /// pads brought out twice, so a display wired to them is on the same bus. static bool _isOnTheBus(PartLogicContext context) => - const {'A4', 'SDA'}.contains(context.pins.boardPortFor('SDA')) && - const {'A5', 'SCL'}.contains(context.pins.boardPortFor('SCL')); + I2cWiring.isOnTheBus(context, sda: 'SDA', scl: 'SCL'); /// The address this display listens on, read from its `I2C Address` /// property. Accepts the hex spelling people copy out of a sketch (`0x3C`) /// as well as a plain number, and falls back to /// [ssd1306DefaultAddress] for anything it cannot read — a typo should cost /// a wrong address at worst, not a part that silently never runs. - static int _i2cAddress(PartLogicContext context) { - final raw = context.properties[ComponentProps.i2cAddress]; - final address = switch (raw) { - final num n => n.toInt(), - final String s => _parseAddress(s), - _ => null, - }; - if (address == null || address < 0 || address > 0x7F) return ssd1306DefaultAddress; - return address; - } - - static int? _parseAddress(String value) { - final text = value.trim().toLowerCase(); - if (text.startsWith('0x')) return int.tryParse(text.substring(2), radix: 16); - return int.tryParse(text); - } + static int _i2cAddress(PartLogicContext context) => + I2cWiring.parseAddress(context.properties[ComponentProps.i2cAddress]) ?? + ssd1306DefaultAddress; /// A piezo buzzer: sounding, and at what pitch, from the frequency the /// emulator detects on its driving pin. diff --git a/packages/pinbench_parts/lib/logic/i2c_sensors.dart b/packages/pinbench_parts/lib/logic/i2c_sensors.dart new file mode 100644 index 0000000..d57c79f --- /dev/null +++ b/packages/pinbench_parts/lib/logic/i2c_sensors.dart @@ -0,0 +1,402 @@ +import 'part_logic.dart'; + +/// Where an I²C module sits relative to the board: on the bus, powered, and +/// at which address its jumpers put it. +/// +/// Every check reads the wiring rather than a setting, because the wiring is +/// the lesson. A sensor with swapped SDA/SCL, or no VCC, is not acknowledged — +/// an I²C scanner finds nothing and the library reports "chip not found", +/// exactly as on a desk. +abstract final class I2cWiring { + /// The board ports a module's supply pin counts as powered from. + static const supplies = {'5V', '3.3V'}; + + /// Whether [sda] and [scl] reach the ATmega328P's TWI pads. + /// + /// `A4`/`A5` specifically, because the hardware I²C is wired to those two + /// pads and nothing else. The R3 header's separate `SDA`/`SCL` pins count + /// too: they are the same two pads brought out twice. + static bool isOnTheBus(PartLogicContext context, {String sda = 'sda', String scl = 'scl'}) => + const {'A4', 'SDA'}.contains(context.pins.boardPortFor(sda)) && + const {'A5', 'SCL'}.contains(context.pins.boardPortFor(scl)); + + /// Whether [vcc] reaches one of [accepted] and [gnd] reaches a ground. + static bool isPowered( + PartLogicContext context, { + String vcc = 'vcc', + String gnd = 'gnd', + Set accepted = supplies, + }) => + accepted.contains(context.pins.boardPortFor(vcc)) && + (context.pins.boardPortFor(gnd)?.startsWith('GND') ?? false); + + /// Whether [pin] is tied to a supply — how an address jumper (AD0, ADDR) is + /// set. Anything else, including unconnected, reads low: the modules pull + /// these pins down on board. + static bool isTiedHigh(PartLogicContext context, String pin) => + supplies.contains(context.pins.boardPortFor(pin)); + + /// An address as people write it: `0x3C`, `60`, or a number from the + /// properties panel. Null for anything that is not a 7-bit address. + static int? parseAddress(Object? raw) { + final address = switch (raw) { + final num n => n.toInt(), + final String s => _parse(s.trim().toLowerCase()), + _ => null, + }; + if (address == null || address < 0 || address > 0x7F) return null; + return address; + } + + static int? _parse(String text) => + text.startsWith('0x') ? int.tryParse(text.substring(2), radix: 16) : int.tryParse(text); +} + +/// I²C sensors: parts whose whole behaviour is a register bank the sketch +/// reads. +/// +/// Each one serves its registers and republishes its reading every frame, so +/// the bus can answer a read the moment the sketch clocks it (see +/// [PartI2cApi]). What the sketch *writes* — a mode, a range, the time — comes +/// back through `drain` and is tracked in declared `STATE`, which is the only +/// memory a `.pdl` part keeps between frames. +abstract final class I2cSensors { + // --- BH1750 --------------------------------------------------------------- + + /// A BH1750 ambient light sensor (the GY-302 module). + /// + /// It has no registers to select: the sketch writes one-byte opcodes and + /// reads two bytes of result, so it is served with `pointerBytes: 0`. Until + /// the sketch sends a measurement opcode the result reads 0, as a chip + /// that has only been powered up does. + static void bh1750(PartLogicContext context) { + if (!I2cWiring.isPowered(context) || !I2cWiring.isOnTheBus(context)) return; + + final address = I2cWiring.isTiedHigh(context, 'addr') ? 0x5C : 0x23; + context.i2c.serve(address, size: 2, pointerBytes: 0); + + var mode = _int(context.state['mode']); + var mtreg = _int(context.state['mtreg'], 69); + for (final transaction in context.i2c.drain(address)) { + for (final opcode in transaction) { + (mode, mtreg) = bh1750Command(opcode, mode: mode, mtreg: mtreg); + } + } + context.state['mode'] = mode; + context.state['mtreg'] = mtreg; + + if (mode == 0) { + context.i2c.setRegisters(address, 0, const [0, 0]); + return; + } + final counts = bh1750Counts(context.number('illuminance', 250), mode: mode, mtreg: mtreg); + context.i2c.setRegisters(address, 0, [counts >> 8, counts & 0xFF]); + } + + /// The BH1750's measurement opcodes: continuous and one-time, in H, H2 and + /// L resolution. + static const bh1750Modes = {0x10, 0x11, 0x13, 0x20, 0x21, 0x23}; + + /// Applies one opcode to the chip's mode and measurement time (MTreg). + /// Mode 0 means no measurement is running. + static (int mode, int mtreg) bh1750Command(int opcode, {required int mode, required int mtreg}) { + if (bh1750Modes.contains(opcode)) return (opcode, mtreg); + // Power down stops measuring; reset clears the result. + if (opcode == 0x00 || opcode == 0x07) return (0, mtreg); + // MTreg is written in two halves: 01000_hhh and 011_lllll. + if (opcode & 0xF8 == 0x40) return (mode, (mtreg & 0x1F) | ((opcode & 0x07) << 5)); + if (opcode & 0xE0 == 0x60) return (mode, (mtreg & 0xE0) | (opcode & 0x1F)); + return (mode, mtreg); + } + + /// The raw count the chip reports for [lux]: 1.2 counts per lux at the + /// default measurement time of 69, scaled by MTreg, doubled in H2 mode — + /// the inverse of what every BH1750 library divides by. + static int bh1750Counts(double lux, {required int mode, required int mtreg}) { + final highResolution2 = mode & 0x0F == 0x01; + final counts = lux * 1.2 * (mtreg.clamp(31, 254) / 69) * (highResolution2 ? 2 : 1); + return counts.round().clamp(0, 0xFFFF); + } + + // --- MPU-6050 ------------------------------------------------------------- + + static const _mpuWhoAmI = 0x75; + static const _mpuPowerManagement = 0x6B; + static const _mpuGyroConfig = 0x1B; + static const _mpuAccelConfig = 0x1C; + static const _mpuData = 0x3B; + + /// The PWR_MGMT_1 value after power-on or a reset: asleep. + static const _mpuAsleep = 0x40; + + /// An MPU-6050 accelerometer and gyroscope (the GY-521 module). + /// + /// Behaves like the chip where libraries look: it answers WHO_AM_I, boots + /// asleep so a sketch that never wakes it reads zeros, clears its own reset + /// bit (`Adafruit_MPU6050.begin()` waits for exactly that), and scales its + /// samples to whatever range the sketch set. + static void mpu6050(PartLogicContext context) { + if (!I2cWiring.isPowered(context) || !I2cWiring.isOnTheBus(context)) return; + + final address = I2cWiring.isTiedHigh(context, 'ad0') ? 0x69 : 0x68; + context.i2c.serve(address, size: 128); + + var power = _int(context.state['powerManagement'], _mpuAsleep); + var accelConfig = _int(context.state['accelConfig']); + var gyroConfig = _int(context.state['gyroConfig']); + if (context.state['booted'] != true) { + context.i2c.setRegisters(address, _mpuPowerManagement, const [_mpuAsleep]); + context.state['booted'] = true; + } + + for (final write in context.i2c.drain(address)) { + // The first byte is the register pointer; a lone pointer precedes a read. + for (var i = 1; i < write.length; i++) { + final value = write[i]; + switch (write[0] + i - 1) { + case _mpuPowerManagement when value & 0x80 != 0: + // DEVICE_RESET: registers back to their defaults, and the bit + // clears itself, which is what the sketch polls for. + (power, accelConfig, gyroConfig) = (_mpuAsleep, 0, 0); + context.i2c.setRegisters(address, _mpuGyroConfig, const [0, 0]); + context.i2c.setRegisters(address, _mpuPowerManagement, const [_mpuAsleep]); + case _mpuPowerManagement: + power = value; + case _mpuGyroConfig: + gyroConfig = value; + case _mpuAccelConfig: + accelConfig = value; + } + } + } + context.state['powerManagement'] = power; + context.state['accelConfig'] = accelConfig; + context.state['gyroConfig'] = gyroConfig; + + // Read-only on the chip; republished in case the sketch wrote over it. + context.i2c.setRegisters(address, _mpuWhoAmI, const [0x68]); + if (power & _mpuAsleep != 0) return; + + context.i2c.setRegisters( + address, + _mpuData, + mpu6050Sample( + accel: (context.number('accelX'), context.number('accelY'), context.number('accelZ', 1)), + gyro: (context.number('gyroX'), context.number('gyroY'), context.number('gyroZ')), + celsius: context.number('temperature', 25), + accelConfig: accelConfig, + gyroConfig: gyroConfig, + ), + ); + } + + /// The 14 bytes from ACCEL_XOUT_H: acceleration, temperature and rotation, + /// each a big-endian signed 16-bit value, at the ranges the two config + /// registers select. + static List mpu6050Sample({ + required (double, double, double) accel, + required (double, double, double) gyro, + required double celsius, + required int accelConfig, + required int gyroConfig, + }) { + const accelCountsPerG = [16384.0, 8192.0, 4096.0, 2048.0]; + const gyroCountsPerDps = [131.0, 65.5, 32.8, 16.4]; + final perG = accelCountsPerG[(accelConfig >> 3) & 3]; + final perDps = gyroCountsPerDps[(gyroConfig >> 3) & 3]; + return [ + ..._int16(accel.$1 * perG), + ..._int16(accel.$2 * perG), + ..._int16(accel.$3 * perG), + // The datasheet's formula, inverted: °C = raw / 340 + 36.53. + ..._int16((celsius - 36.53) * 340), + ..._int16(gyro.$1 * perDps), + ..._int16(gyro.$2 * perDps), + ..._int16(gyro.$3 * perDps), + ]; + } + + // --- DS1307 --------------------------------------------------------------- + + static const _ds1307Address = 0x68; + + /// Registers 0–6 hold the time; the rest is control and 56 bytes of RAM, + /// which the bus stores and reads back without this logic touching them. + static const _ds1307TimeRegisters = 7; + + static final _epoch = DateTime.utc(2000); + + /// A DS1307 real-time clock. + /// + /// Counts simulated time, so it runs at the speed `millis()` does and stops + /// when the run is paused. Starts either at the host's current time, like a + /// module whose battery kept it set, or halted at 2000-01-01 like a chip + /// that never was — the case `if (!rtc.isrunning()) rtc.adjust(...)` exists + /// for. Needs 5 V: the DS1307 does not run from 3.3 V. + static void ds1307(PartLogicContext context) { + if (!I2cWiring.isPowered(context, accepted: const {'5V'}) || !I2cWiring.isOnTheBus(context)) { + return; + } + context.i2c.serve(_ds1307Address, size: 64); + + final nowMs = context.elapsed.inMicroseconds / 1000; + var halted = context.state['halted'] == true; + var baseSeconds = _double(context.state['baseSeconds']); + var anchorMs = _double(context.state['anchorMs']); + if (context.state['booted'] != true) { + halted = context.properties['startsAt'] == 'unset'; + baseSeconds = halted ? 0 : secondsSince2000(DateTime.now()).toDouble(); + anchorMs = nowMs; + context.state['booted'] = true; + } + + double current() => halted ? baseSeconds : baseSeconds + (nowMs - anchorMs) / 1000; + + for (final write in context.i2c.drain(_ds1307Address)) { + if (write.length < 2 || write[0] >= _ds1307TimeRegisters) continue; + // Written registers over the current time, so setting only the minutes + // keeps the rest of the clock. + final registers = ds1307Registers(current().floor(), halted: halted); + for (var i = 1; i < write.length && write[0] + i - 1 < _ds1307TimeRegisters; i++) { + registers[write[0] + i - 1] = write[i]; + } + halted = registers[0] & 0x80 != 0; + baseSeconds = ds1307Seconds(registers).toDouble(); + anchorMs = nowMs; + } + + context.state['halted'] = halted; + context.state['baseSeconds'] = baseSeconds; + context.state['anchorMs'] = anchorMs; + context.i2c.setRegisters(_ds1307Address, 0, ds1307Registers(current().floor(), halted: halted)); + } + + /// [time]'s wall-clock fields as seconds since 2000-01-01, ignoring its + /// zone: the clock shows local time, whatever the host's offset. + static int secondsSince2000(DateTime time) => DateTime.utc( + time.year, + time.month, + time.day, + time.hour, + time.minute, + time.second, + ).difference(_epoch).inSeconds; + + /// Registers 0–6 for [seconds] since 2000-01-01: BCD seconds (bit 7 is + /// the clock-halt flag), minutes, 24-hour hours, day of week (Monday = 1), + /// date, month, two-digit year. + static List ds1307Registers(int seconds, {bool halted = false}) { + final t = _epoch.add(Duration(seconds: seconds)); + return [ + _bcd(t.second) | (halted ? 0x80 : 0), + _bcd(t.minute), + _bcd(t.hour), + t.weekday, + _bcd(t.day), + _bcd(t.month), + _bcd(t.year % 100), + ]; + } + + /// The inverse of [ds1307Registers]. Reads 12-hour hours too, since a + /// sketch may set bit 6 of the hours register. + static int ds1307Seconds(List registers) { + final hoursRegister = registers[2]; + final hours = hoursRegister & 0x40 != 0 + ? _fromBcd(hoursRegister & 0x1F) % 12 + (hoursRegister & 0x20 != 0 ? 12 : 0) + : _fromBcd(hoursRegister & 0x3F); + return DateTime.utc( + 2000 + _fromBcd(registers[6]), + _fromBcd(registers[5] & 0x1F), + _fromBcd(registers[4] & 0x3F), + hours, + _fromBcd(registers[1] & 0x7F), + _fromBcd(registers[0] & 0x7F), + ).difference(_epoch).inSeconds; + } + + // --- AHT20 ---------------------------------------------------------------- + + static const _aht20Address = 0x38; + + /// Status byte: calibrated (bit 3), never busy — a measurement here is + /// ready the moment it is asked for, so a sketch polling bit 7 moves on. + static const _aht20Status = 0x18; + + /// An AHT20 temperature and humidity sensor. + /// + /// Like the BH1750 it has no registers to select: the sketch sends commands + /// (trigger a measurement, soft reset) and reads a status byte followed by + /// 20 bits of humidity, 20 bits of temperature and a CRC. The reading is + /// always current, so it is published every frame rather than on trigger. + static void aht20(PartLogicContext context) { + if (!I2cWiring.isPowered(context) || !I2cWiring.isOnTheBus(context)) return; + + context.i2c.serve(_aht20Address, size: 7, pointerBytes: 0); + // Commands need no answer beyond the reading itself; draining keeps the + // queue from growing. + context.i2c.drain(_aht20Address); + context.i2c.setRegisters( + _aht20Address, + 0, + aht20Reading( + humidity: context.number('humidity', 50), + celsius: context.number('temperature', 25), + ), + ); + } + + /// The seven bytes the AHT20 answers a read with: status, humidity + /// (`RH% = raw / 2^20 × 100`), temperature (`°C = raw / 2^20 × 200 − 50`), + /// and a CRC-8 over the first six. + static List aht20Reading({required double humidity, required double celsius}) { + const fullScale = 1 << 20; + final rawHumidity = (humidity.clamp(0, 100) / 100 * fullScale).round().clamp(0, fullScale - 1); + final rawTemperature = ((celsius.clamp(-50, 150) + 50) / 200 * fullScale).round().clamp( + 0, + fullScale - 1, + ); + final bytes = [ + _aht20Status, + rawHumidity >> 12, + (rawHumidity >> 4) & 0xFF, + (rawHumidity & 0x0F) << 4 | rawTemperature >> 16, + (rawTemperature >> 8) & 0xFF, + rawTemperature & 0xFF, + ]; + return [...bytes, _crc8(bytes)]; + } + + /// CRC-8 as the AHT20 computes it: polynomial 0x31, initial value 0xFF. + static int _crc8(List bytes) { + var crc = 0xFF; + for (final byte in bytes) { + crc ^= byte; + for (var bit = 0; bit < 8; bit++) { + crc = crc & 0x80 != 0 ? ((crc << 1) ^ 0x31) & 0xFF : (crc << 1) & 0xFF; + } + } + return crc; + } + + // --- helpers -------------------------------------------------------------- + + static int _bcd(int value) => (value ~/ 10) << 4 | value % 10; + + static int _fromBcd(int value) => (value >> 4) * 10 + (value & 0x0F); + + static List _int16(double value) { + final raw = value.round().clamp(-32768, 32767) & 0xFFFF; + return [raw >> 8, raw & 0xFF]; + } + + static int _int(Object? value, [int orElse = 0]) => switch (value) { + final num n => n.toInt(), + _ => orElse, + }; + + static double _double(Object? value) => switch (value) { + final num n => n.toDouble(), + _ => 0, + }; +} diff --git a/packages/pinbench_parts/lib/logic/part_logic.dart b/packages/pinbench_parts/lib/logic/part_logic.dart index 3327d6b..71f24ea 100644 --- a/packages/pinbench_parts/lib/logic/part_logic.dart +++ b/packages/pinbench_parts/lib/logic/part_logic.dart @@ -173,13 +173,20 @@ abstract interface class PartPinApi { /// bytes would show a corrupt picture forever after. So this hands over a /// queue rather than a reading, and hands it over exactly once. /// -/// Narrow in the same way as [PartPinApi]: a part may listen at its own -/// address. It cannot see another device's traffic, and it cannot write to the -/// bus — a peripheral that could put bytes on the wire would be modelling the -/// sketch's side of the conversation. -// One method today, and still an interface: it is the seam a fake bus is -// installed at in tests, and the shape the other capability APIs here take. -abstract interface class PartI2cApi() { +/// A part talks back the way real I²C devices do: through **registers**. It +/// [serve]s a bank of them at its address and keeps them current with +/// [setRegisters]; the sketch writes a register pointer, then reads, and the +/// pointer advances after every byte. The bus answers from those registers +/// the moment the sketch clocks a read, mid-frame, which a part running +/// between frames could not. A reading refreshed every frame is at most 16 ms +/// old — fresher than any real sensor's conversion. +/// +/// Narrow in the same way as [PartPinApi]: a part may listen at and serve its +/// own address. It cannot see another device's traffic, and it never drives +/// the bus itself — the sketch is always the one clocking bytes. +// An interface, not a concrete class: it is the seam a fake bus is installed +// at in tests, and the shape the other capability APIs here take. +abstract interface class PartI2cApi { /// Every transaction the sketch has addressed to [address] since the last /// call, oldest first, each holding the bytes that followed the address. /// @@ -188,6 +195,25 @@ abstract interface class PartI2cApi() { /// nobody listens on costs nothing. The first answer after a fresh claim is /// therefore empty, and a display simply keeps the frame it already had. List> drain(int address); + + /// Makes [address] answer the sketch's reads from a bank of [size] + /// registers, and claims it, so the address is acknowledged. + /// + /// [pointerBytes] is how many bytes at the start of a write select the + /// register: 1 for most sensors (MPU6050, DS1307, BME280), 2 for large + /// EEPROMs, 0 for devices with nothing to select (a PCF8574), whose every + /// read starts at register 0. Bytes the sketch writes after the pointer are + /// stored, so reading back a register returns what the sketch put there — + /// which is what setting an RTC's clock relies on. + /// + /// Safe to call every frame: the same shape keeps the registers and the + /// pointer, a different one starts over, zero-filled. + void serve(int address, {int size = 256, int pointerBytes = 1}); + + /// Writes [bytes] into [address]'s registers from [offset], wrapping at the + /// end of the bank — how a part publishes a new reading. Ignored unless the + /// address is [serve]d. + void setRegisters(int address, int offset, List bytes); } /// The bus a part sees when there is no emulator behind it — in a unit test, @@ -195,6 +221,12 @@ abstract interface class PartI2cApi() { class const NoI2cBus() implements PartI2cApi { @override List> drain(int address) => const []; + + @override + void serve(int address, {int size = 256, int pointerBytes = 1}) {} + + @override + void setRegisters(int address, int offset, List bytes) {} } /// Maps the name in a `LOGIC` line to an implementation. diff --git a/packages/pinbench_parts/pubspec.yaml b/packages/pinbench_parts/pubspec.yaml index fe6e858..961a963 100644 --- a/packages/pinbench_parts/pubspec.yaml +++ b/packages/pinbench_parts/pubspec.yaml @@ -43,10 +43,18 @@ flutter: # Referenced as `packages/pinbench_parts/assets/...` by consumers, which is what # Flutter prefixes package assets with. `PartRegistry` scans the manifest # for that prefix to find `.pdl` definitions. + - assets/parts/aht20/ - assets/parts/arduino/ + - assets/parts/battery_9v/ + - assets/parts/bh1750/ - assets/parts/cr2032/ - assets/parts/diode_1n4148/ + - assets/parts/ds1307/ - assets/parts/ldr/ + - assets/parts/lm35/ + - assets/parts/mpu6050/ - assets/parts/pir_sensor/ - assets/parts/slide_switch/ + - assets/parts/soil_moisture/ - assets/parts/thermistor/ + - assets/parts/tmp36/ diff --git a/packages/pinbench_parts/test/cdl/circuit_parser_test.dart b/packages/pinbench_parts/test/cdl/circuit_parser_test.dart index d248d26..9c75dc6 100644 --- a/packages/pinbench_parts/test/cdl/circuit_parser_test.dart +++ b/packages/pinbench_parts/test/cdl/circuit_parser_test.dart @@ -69,6 +69,24 @@ void main() { expect(generated, isNot(contains('hasError'))); }); + test("omits a .pdl part's declared STATE but keeps its properties", () async { + // The same false-dirty as above, for data-driven parts: every frame + // writes a part's STATE onto the canvas, and saving it would also hand + // the next run the last run's value to start from. + TestWidgetsFlutterBinding.ensureInitialized(); + await PartRegistry.initializeAsync(); + final ldr = ComponentInstance( + key: const ValueKey('ldr'), + position: Offset.zero, + part: PartModel(name: 'Photoresistor', size: const Size(80, 40), definitionId: 'ldr'), + properties: const {'illumination': 30, 'level': 0.3}, + ); + + final generated = CircuitParser.generate([ldr], const []); + expect(generated, contains('illumination: 30')); + expect(generated, isNot(contains('level'))); + }); + test('regenerating after a simulated run produces identical text (no false-dirty)', () { // End-to-end regression for the "circuit.cdl shows unsaved after // simulation ends" bug: parse a real template, apply it to canvas diff --git a/packages/pinbench_parts/test/i2c_sensors_test.dart b/packages/pinbench_parts/test/i2c_sensors_test.dart new file mode 100644 index 0000000..a20af82 --- /dev/null +++ b/packages/pinbench_parts/test/i2c_sensors_test.dart @@ -0,0 +1,304 @@ +import 'package:pinbench_pdl/pinbench_pdl.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinbench_parts/logic/built_in_part_logic.dart'; +import 'package:pinbench_parts/logic/i2c_sensors.dart'; +import 'package:pinbench_parts/logic/part_logic.dart'; +import 'package:pinbench_parts/part_registry.dart'; + +/// The I²C sensors: `LOGIC` parts whose whole behaviour is a register bank. +/// +/// These drive each logic frame by frame against a bus that keeps registers +/// the way the emulator's does, so a test reads exactly what a sketch would. +/// `test/i2c_sensor_sketches_test.dart` in `pinbench_sim` runs the same parts +/// under the real Arduino libraries. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + BuiltInPartLogic.ensureRegistered(); + await PartRegistry.initializeAsync(); + }); + + /// The wiring every module here is happy with: powered from 5 V, on A4/A5. + const onTheBus = {'vcc': '5V', 'gnd': 'GND_2', 'sda': 'A4', 'scl': 'A5'}; + + /// One placed sensor, run frame by frame. [state] carries between frames + /// like the engine's `lastState`, starting from the part's declared values. + _Sensor sensor( + String id, { + Map wiring = onTheBus, + Map properties = const {}, + }) { + final definition = PartRegistry.getPart(id)!; + return _Sensor( + logic: PartLogicRegistry.find(definition.logic!)!, + definition: definition, + wiring: wiring, + properties: {...definition.defaultProperties(), ...properties}, + state: definition.initialState(), + ); + } + + group('BH1750', () { + test('reads 0 until the sketch starts a measurement', () { + final light = sensor('bh1750')..frame(); + expect(light.bus.read(0x23, 0, 2), [0, 0]); + + light + ..bus.write(0x23, [0x10]) // continuous high-resolution mode + ..frame(); + // 250 lx x 1.2 counts/lx = 300 = 0x012C. + expect(light.bus.read(0x23, 0, 2), [0x01, 0x2C]); + }); + + test('high-resolution mode 2 doubles the count, as libraries expect', () { + final light = sensor('bh1750') + ..bus.write(0x23, [0x11]) + ..frame(); + expect(light.bus.read(0x23, 0, 2), [0x02, 0x58]); + }); + + test('ADDR tied high moves it to 0x5C', () { + final light = sensor('bh1750', wiring: {...onTheBus, 'addr': '5V'})..frame(); + expect(light.bus.served, {0x5C}); + }); + + test('a changed MTreg scales the count', () { + expect(I2cSensors.bh1750Counts(100, mode: 0x10, mtreg: 138), 240); + final (_, mtreg) = I2cSensors.bh1750Command(0x44, mode: 0x10, mtreg: 69); + expect(mtreg, 0x85, reason: 'high bits 100 over the default low bits 00101'); + }); + }); + + group('MPU-6050', () { + test('answers WHO_AM_I and boots asleep, reading zeros', () { + final imu = sensor('mpu6050')..frame(); + expect(imu.bus.read(0x68, 0x75, 1), [0x68]); + expect(imu.bus.read(0x68, 0x6B, 1), [0x40]); + expect(imu.bus.read(0x68, 0x3B, 6), [0, 0, 0, 0, 0, 0]); + }); + + test('once woken, reports 1 g on Z at the ±2 g range', () { + final imu = sensor('mpu6050') + ..frame() + ..bus.write(0x68, [0x6B, 0x00]) + ..frame(); + // X, Y, Z: 0, 0, 16384 counts. + expect(imu.bus.read(0x68, 0x3B, 6), [0, 0, 0, 0, 0x40, 0x00]); + }); + + test('scales to the range the sketch sets', () { + final imu = sensor('mpu6050', properties: {'gyroZ': 100.0}) + ..frame() + ..bus.write(0x68, [0x6B, 0x00]) + ..bus.write(0x68, [0x1B, 0x08, 0x10]) // gyro ±500 °/s, accel ±8 g + ..frame(); + expect(imu.bus.read(0x68, 0x3F, 2), [0x10, 0x00], reason: '1 g at 4096 counts/g'); + // 100 °/s x 65.5 counts = 6550 = 0x1996. + expect(imu.bus.read(0x68, 0x47, 2), [0x19, 0x96]); + }); + + test('clears its own reset bit, which Adafruit_MPU6050 waits for', () { + final imu = sensor('mpu6050') + ..frame() + ..bus.write(0x68, [0x6B, 0x80]); + expect(imu.bus.read(0x68, 0x6B, 1), [0x80], reason: 'as the sketch wrote it, mid-frame'); + imu.frame(); + expect(imu.bus.read(0x68, 0x6B, 1), [0x40]); + }); + + test('the die temperature follows the datasheet formula', () { + final imu = sensor('mpu6050', properties: {'temperature': 36.53}) + ..frame() + ..bus.write(0x68, [0x6B, 0x00]) + ..frame(); + expect(imu.bus.read(0x68, 0x41, 2), [0, 0]); + }); + + test('AD0 tied high moves it to 0x69', () { + final imu = sensor('mpu6050', wiring: {...onTheBus, 'ad0': '3.3V'})..frame(); + expect(imu.bus.served, {0x69}); + }); + }); + + group('DS1307', () { + test('a fresh chip is halted at 2000-01-01 until the sketch sets it', () { + final rtc = sensor('ds1307', properties: {'startsAt': 'unset'})..frame(); + // CH set, 00:00:00, Saturday, 01/01/00. + expect(rtc.bus.read(0x68, 0, 7), [0x80, 0, 0, 6, 0x01, 0x01, 0x00]); + + rtc.elapsed = const Duration(seconds: 5); + rtc.frame(); + expect(rtc.bus.read(0x68, 0, 1), [0x80], reason: 'a halted clock does not count'); + }); + + test('keeps the time the sketch sets, and counts simulated seconds from it', () { + final rtc = sensor('ds1307', properties: {'startsAt': 'unset'})..frame(); + // 2026-09-30 12:34:56, clock-halt cleared, as RTClib's adjust() writes it. + rtc.bus.write(0x68, [0x00, 0x56, 0x34, 0x12, 0x03, 0x30, 0x09, 0x26]); + rtc.frame(); + expect(rtc.bus.read(0x68, 0, 7), [0x56, 0x34, 0x12, 0x03, 0x30, 0x09, 0x26]); + + rtc.elapsed = const Duration(seconds: 2); + rtc.frame(); + expect(rtc.bus.read(0x68, 0, 1), [0x58]); + }); + + test('RAM reads back what the sketch stored', () { + final rtc = sensor('ds1307') + ..frame() + ..bus.write(0x68, [0x08, 0xAB, 0xCD]) + ..frame(); + expect(rtc.bus.read(0x68, 0x08, 2), [0xAB, 0xCD]); + }); + + test('does not run from 3.3 V', () { + final rtc = sensor('ds1307', wiring: {...onTheBus, 'vcc': '3.3V'})..frame(); + expect(rtc.bus.served, isEmpty); + }); + + test('the register encoding round-trips, 12-hour hours included', () { + final seconds = I2cSensors.secondsSince2000(DateTime(2031, 2, 28, 23, 59, 58)); + final registers = I2cSensors.ds1307Registers(seconds); + expect(I2cSensors.ds1307Seconds(registers), seconds); + + // 11 PM written in 12-hour mode: bit 6 set, bit 5 = PM, BCD 11. + final twelveHour = [...registers]..[2] = 0x40 | 0x20 | 0x11; + expect(I2cSensors.ds1307Seconds(twelveHour), seconds); + }); + }); + + group('AHT20', () { + test('answers with status, humidity, temperature and a valid CRC', () { + final aht = sensor('aht20', properties: {'humidity': 50.0, 'temperature': 25.0})..frame(); + final bytes = aht.bus.read(0x38, 0, 7); + + expect(bytes[0] & 0x08, 0x08, reason: 'calibrated'); + expect(bytes[0] & 0x80, 0, reason: 'never busy'); + final humidity = (bytes[1] << 12 | bytes[2] << 4 | bytes[3] >> 4) / (1 << 20) * 100; + final celsius = ((bytes[3] & 0x0F) << 16 | bytes[4] << 8 | bytes[5]) / (1 << 20) * 200 - 50; + expect(humidity, closeTo(50, 0.001)); + expect(celsius, closeTo(25, 0.001)); + expect(_crc8(bytes), 0, reason: 'a CRC over the data and its own CRC is 0'); + }); + }); + + group('every I²C sensor', () { + test('stays off the bus unless powered and wired to A4/A5', () { + for (final id in ['bh1750', 'mpu6050', 'ds1307', 'aht20']) { + for (final wiring in const [ + {'sda': 'A4', 'scl': 'A5'}, // no power + {'vcc': '5V', 'sda': 'A4', 'scl': 'A5'}, // no ground + {'vcc': '5V', 'gnd': 'GND_2', 'sda': 'A5', 'scl': 'A4'}, // swapped + {'vcc': '5V', 'gnd': 'GND_2', 'sda': '2', 'scl': '3'}, + ]) { + final part = sensor(id, wiring: wiring)..frame(); + expect(part.bus.served, isEmpty, reason: '$id wired as $wiring'); + } + } + }); + }); +} + +/// One placed sensor and the bus it sits on. +class _Sensor({ + required final PartLogic logic, + required final PartDefinition definition, + required final Map wiring, + required final Map properties, + required final Map state, +}) { + final bus = _RegisterBus(); + var elapsed = Duration.zero; + + void frame() => logic( + PartLogicContext( + definition: definition, + state: state, + properties: properties, + physics: {}, + elapsed: elapsed, + analog: (_) => 0, + pins: _Wiring(wiring), + spice: const _NoSpice(), + i2c: bus, + ), + ); +} + +/// A bus that keeps registers the way the emulator's recorder does: a write +/// sets the pointer from its first byte and stores the rest (pointer-less +/// devices store nothing), and a read starts at the pointer. +class _RegisterBus implements PartI2cApi { + final served = {}; + final _registers = >{}; + final _pointerBytes = {}; + final _pending = >>{}; + + /// What the sketch sends, as `Wire.write` would, landing mid-frame. + void write(int address, List bytes) { + _pending.putIfAbsent(address, () => []).add(bytes); + final registers = _registers[address]; + if (registers == null || _pointerBytes[address] == 0) return; + for (var i = 1; i < bytes.length; i++) { + registers[(bytes[0] + i - 1) % registers.length] = bytes[i]; + } + } + + List read(int address, int from, int count) { + final registers = _registers[address]!; + return [for (var i = 0; i < count; i++) registers[(from + i) % registers.length]]; + } + + @override + List> drain(int address) => _pending.remove(address) ?? const []; + + @override + void serve(int address, {int size = 256, int pointerBytes = 1}) { + served.add(address); + _pointerBytes[address] = pointerBytes; + if (_registers[address]?.length != size) _registers[address] = List.filled(size, 0); + } + + @override + void setRegisters(int address, int offset, List bytes) { + final registers = _registers[address]; + if (registers == null) return; + for (var i = 0; i < bytes.length; i++) { + registers[(offset + i) % registers.length] = bytes[i]; + } + } +} + +class const _Wiring(final Map ports) implements PartPinApi { + @override + String? boardPortFor(String portId) => ports[portId]; + @override + int? connectedTo(String portId) => int.tryParse(ports[portId] ?? ''); + @override + double duty(int pin) => 0; + @override + double pulseUs(int pin) => 0; + @override + bool isHigh(int pin) => false; + @override + double? frequencyOn(int pin) => null; +} + +class const _NoSpice() implements PartSpiceApi { + @override + bool get isActive => false; + @override + double current() => 0; +} + +int _crc8(List bytes) { + var crc = 0xFF; + for (final byte in bytes) { + crc ^= byte; + for (var bit = 0; bit < 8; bit++) { + crc = crc & 0x80 != 0 ? ((crc << 1) ^ 0x31) & 0xFF : (crc << 1) & 0xFF; + } + } + return crc; +} diff --git a/packages/pinbench_parts/test/part_logic_test.dart b/packages/pinbench_parts/test/part_logic_test.dart index 5fc8cea..7894db2 100644 --- a/packages/pinbench_parts/test/part_logic_test.dart +++ b/packages/pinbench_parts/test/part_logic_test.dart @@ -689,6 +689,12 @@ class _FakeBus(final Map>> traffic) implements PartI2cApi { drained.add(address); return traffic[address] ?? const []; } + + @override + void serve(int address, {int size = 256, int pointerBytes = 1}) {} + + @override + void setRegisters(int address, int offset, List bytes) {} } /// A minimal definition for the helper tests — nothing about it matters except diff --git a/packages/pinbench_pdl b/packages/pinbench_pdl index 3b2956d..714d715 160000 --- a/packages/pinbench_pdl +++ b/packages/pinbench_pdl @@ -1 +1 @@ -Subproject commit 3b2956d2a42c20515c70d4cd76e09ef994cf8a61 +Subproject commit 714d715c4fc4b4fb79d7176bf91e8e12dfff1f09 diff --git a/packages/pinbench_sim/lib/core/avr_interop.dart b/packages/pinbench_sim/lib/core/avr_interop.dart index 6db0a5c..60200d6 100644 --- a/packages/pinbench_sim/lib/core/avr_interop.dart +++ b/packages/pinbench_sim/lib/core/avr_interop.dart @@ -1,8 +1,407 @@ -// Selects the AVR emulator backend at compile time. -// -// Native platforms use the pure-Dart `avr8_dart` port. The web delegates to -// `avr8js` (optimized JS, real-time on the 16 MHz ATmega328P) via the bridge in -// `web/avr_bridge.js`, because `avr8_dart` compiled by dart2js is too slow to -// keep real time (so time-based sketches lag). Both expose the same `AVRBridge` -// static API used by the simulation engine. -export 'avr_interop_io.dart' if (dart.library.js_interop) 'avr_interop_web.dart'; +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:avr8_dart/avr8_dart.dart'; + +import 'frequency_detector.dart'; +import 'i2c_recorder.dart'; + +/// Static facade over the `avr8_dart` ATmega328P emulator. +/// +/// Loads a compiled HEX, steps the CPU ([tick]), and exposes the I/O the +/// simulator needs: digital pin levels and PWM duty ([getPinState], +/// [getPinDuty]), ADC injection ([setAnalogVoltage]) for analogRead, serial +/// RX queueing ([queueSerialInput]) and TX (via onSerialPrint), and buzzer +/// frequency detection. State is static because the emulator is a singleton per +/// process; [loadHex] fully re-creates the CPU on each run. +/// +/// The same code on every platform: desktop runs it in the simulation +/// isolate, and the web app, compiled to WebAssembly, runs it inline. The web +/// once used a separate JavaScript emulator (`avr8js`, then a dart2js build of +/// this file); since `avr8_dart` 0.2.0, dart2wasm runs this loop as fast as +/// dart2js does (≈4.7× real time), so the web needs nothing of its own. +class AVRBridge { + static late CPU _cpu; + + /// Whether a program has been loaded. Everything that reads the CPU answers + /// a harmless default before then, because the canvas asks for pin states + /// before the first Run. + static var _loaded = false; + static CPU get cpu => _cpu; + static late AVRIOPort _portB; + static late AVRIOPort _portC; + static late AVRIOPort _portD; + static late AVRADC _adc; + + // Required to keep a reference to prevent garbage collection + // Reason: required by avr8_dart to keep timer active + // ignore: unused_field + static late AVRTimer _timer0; + // Reason: required by avr8_dart to keep timer active + // ignore: unused_field + static late AVRTimer _timer1; + // Reason: required by avr8_dart to keep timer active + // ignore: unused_field + static late AVRTimer _timer2; + static late AVRUSART _usart; + // The TWI peripheral installs a CPU write hook and is driven from it, so + // nothing else references it once constructed — but it must stay reachable, + // like the timers above. + static late AVRTWI _twi; + + /// Everything the sketch has put on the I²C bus, waiting for the parts that + /// listen for it. See [I2cRecorder] for why bus traffic is queued rather + /// than sampled like a pin. + static final _i2c = I2cRecorder(); + + /// Claims [address] on the bus, so a device at it acknowledges and its + /// traffic is kept for [drainI2c]. + static void listenI2c(int address) => _i2c.listenAt(address); + + /// Every transaction written to [address] since the last call. + static List> drainI2c(int address) => _i2c.drain(address); + + /// Makes [address] answer the sketch's reads from a bank of [size] + /// registers. See [I2cRecorder.serve]. + static void serveI2c(int address, {int size = 256, int pointerBytes = 1}) => + _i2c.serve(address, size: size, pointerBytes: pointerBytes); + + /// Publishes [bytes] into [address]'s registers from [offset]. + static void setI2cRegisters(int address, int offset, List bytes) => + _i2c.setRegisters(address, offset, bytes); + + static void Function(double?)? onBuzzerFrequencyChanged; + + /// The Arduino pin number connected to the piezo buzzer. + /// Frequency detection is ONLY performed on this pin to avoid false triggers + /// from serial TX, LED PWM, or other toggling pins. + /// Set by SimulationRunner before starting the emulation loop. + static int? buzzerPin; + + /// Arduino pins driving servo signal lines. HIGH-pulse widths are measured + /// only on these pins (set from the traced netlist, like [buzzerPin]) so + /// serial TX / LED PWM toggles can't masquerade as servo pulses. + static List servoPins = const []; + + // Per-pin servo pulse measurement: cycle stamp of the last rising edge, and + // the width of the last completed HIGH pulse in microseconds. + static final _servoRiseCycle = List.filled(_dutyPinCount, -1); + static final _servoPulseUs = List.filled(_dutyPinCount, 0); + + /// Width (µs) of the most recent completed HIGH pulse on [pin], or 0 if none + /// has been seen since the current program was loaded. Only meaningful for + /// pins registered in [servoPins]. + static double getServoPulseUs(int pin) => + (pin >= 0 && pin < _dutyPinCount) ? _servoPulseUs[pin] : 0; + + static final _freqDetector = FrequencyDetector(); + + // Bytes the user typed in the Serial Monitor, drip-fed into the USART receiver + // as it becomes free (mirrors a real serial line into Serial.read/available). + static final _rxQueue = Queue(); + + /// Queues [text] to be delivered to the running sketch's serial receiver. + static void queueSerialInput(String text) { + _rxQueue.addAll(text.codeUnits); + } + + // Per-frame PWM duty-cycle sampling for digital pins 0..13. The pin is sampled + // periodically across a [tick] call; getPinDuty returns the fraction of + // samples the pin was high — i.e. the analogWrite() duty cycle. + static const _dutyPinCount = 14; + static const _dutySampleInterval = 64; // sample once every N CPU cycles + static final _dutyHighSamples = List.filled(_dutyPinCount, 0); + static var _dutyTotalSamples = 0; + + /// Fraction of the last [tick] window that [pin] was driven high (0.0–1.0). + /// Falls back to the instantaneous level if no samples were taken yet. + static double getPinDuty(int pin) { + if (!_loaded) return 0; + if (pin < 0 || pin >= _dutyPinCount) return getPinState(pin) ? 1.0 : 0.0; + if (_dutyTotalSamples == 0) return getPinState(pin) ? 1.0 : 0.0; + return _dutyHighSamples[pin] / _dutyTotalSamples; + } + + static void loadHex(String hexString, {void Function(String)? onSerialPrint}) { + _rxQueue.clear(); + _servoRiseCycle.fillRange(0, _servoRiseCycle.length, -1); + _servoPulseUs.fillRange(0, _servoPulseUs.length, 0); + final program = Uint16List(0x4000); // 32KB flash is 16K x 16-bit + _parseHex(hexString, program); + _cpu = CPU(program); + _timer0 = AVRTimer(_cpu, timer0Config); + _timer1 = AVRTimer(_cpu, timer1Config); + _timer2 = AVRTimer(_cpu, timer2Config); + _usart = AVRUSART(_cpu, usart0Config, 16000000); + // `Serial.println` ends lines with CRLF and the USART splits on the LF; + // the CR is dropped here so no consumer has to. + _usart.onLineTransmit = (line) { + onSerialPrint?.call(line.endsWith('\r') ? line.substring(0, line.length - 1) : line); + }; + _portB = AVRIOPort(_cpu, portBConfig); + _portB.addListener(_handlePortBChange); + + _portC = AVRIOPort(_cpu, portCConfig); + _portC.addListener(_handlePortCChange); + + _portD = AVRIOPort(_cpu, portDConfig); + _portD.addListener(_handlePortDChange); + + _adc = AVRADC(_cpu, adcConfig); + + // The bus is rebuilt with the program: claims belong to the parts of the + // run that just ended, and its traffic to the sketch that just stopped. + _i2c.reset(); + _twi = AVRTWI(_cpu, twiConfig, 16000000); + _twi.eventHandler = _RecordingTwiHandler(_twi, _i2c); + + _freqDetector.reset(); + _loaded = true; + } + + static bool isPinOutput(int pin) { + if (!_loaded) return false; + if (pin >= 0 && pin <= 7) { + return (_cpu.data[portDConfig.DDR] & (1 << pin)) != 0; + } else if (pin >= 8 && pin <= 13) { + return (_cpu.data[portBConfig.DDR] & (1 << (pin - 8))) != 0; + } else if (pin >= 14 && pin <= 19) { + return (_cpu.data[portCConfig.DDR] & (1 << (pin - 14))) != 0; + } + return false; + } + + static void _handlePortDChange(int value, int oldValue) => _handlePortChange(0, value, oldValue); + static void _handlePortBChange(int value, int oldValue) => _handlePortChange(8, value, oldValue); + static void _handlePortCChange(int value, int oldValue) => _handlePortChange(14, value, oldValue); + + static void _handlePortChange(int pinOffset, int value, int oldValue) { + final changedBits = value ^ oldValue; + if (changedBits == 0) return; + + final bPin = buzzerPin; + if (bPin != null && + bPin >= pinOffset && + bPin < pinOffset + 8 && + (changedBits & (1 << (bPin - pinOffset))) != 0) { + _freqDetector.onPinToggle(_cpu.cycles, (freq) => onBuzzerFrequencyChanged?.call(freq)); + } + + for (final sPin in servoPins) { + if (sPin < pinOffset || sPin >= pinOffset + 8) continue; + final bit = 1 << (sPin - pinOffset); + if ((changedBits & bit) == 0) continue; + if ((value & bit) != 0) { + _servoRiseCycle[sPin] = _cpu.cycles; + } else { + final rise = _servoRiseCycle[sPin]; + if (rise >= 0) { + final us = (_cpu.cycles - rise) / 16.0; // 16 MHz clock + // Servo control pulses are ~0.4–3 ms; anything else on the pin + // (PWM, glitches) is ignored. + if (us >= 400 && us <= 3000) _servoPulseUs[sPin] = us; + } + } + } + } + + /// Runs the CPU for [cycles] clock cycles — simulated time, which the frame + /// loop derives from wall-clock time. + /// + /// Counted in *cycles*, not instructions: most AVR instructions take one + /// cycle but branches, calls and memory access take two to four, so a loop + /// over instructions ran typical sketches about 30% fast (`delay(1000)` + /// lasted 0.77 s). + static void tick(int cycles) { + if (!_loaded) return; + // Everything the loop touches is read into a local first. `_cpu` and the + // other statics are lazily initialised, so every access checks whether + // they are set yet, and on the web that check costs far more than a + // local read in a loop that runs about 60 million times a second. + final cpu = _cpu; + final usart = _usart; + final portB = _portB; + final portD = _portD; + final rxQueue = _rxQueue; + final freqDetector = _freqDetector; + final dutyHigh = _dutyHighSamples; + final ddrD = portDConfig.DDR; + final ddrB = portBConfig.DDR; + + // Reset PWM duty accumulators for this frame. + var dutyTotal = 0; + for (var p = 0; p < _dutyPinCount; p++) { + dutyHigh[p] = 0; + } + + final limit = cpu.cycles + cycles; + for (var i = 0; cpu.cycles < limit; i++) { + avrInstruction(cpu); + + // Periodically sample digital pin levels to estimate PWM duty cycles: + // pins 0-7 are PORTD, 8-13 are PORTB. A pin is driven high exactly when + // `AVRIOPort.pinState` would say `High` — an output, last written 1, not + // open-collector — and computing that as one mask per port instead of + // fourteen enum lookups keeps the sampler off the profile. + if ((i & (_dutySampleInterval - 1)) == 0) { + dutyTotal++; + final highD = cpu.data[ddrD] & portD.lastValue & ~portD.openCollector; + final highB = cpu.data[ddrB] & portB.lastValue & ~portB.openCollector; + if (highD != 0) { + for (var p = 0; p < 8; p++) { + if ((highD & (1 << p)) != 0) dutyHigh[p]++; + } + } + if (highB != 0) { + for (var p = 8; p < _dutyPinCount; p++) { + if ((highB & (1 << (p - 8))) != 0) dutyHigh[p]++; + } + } + } + + // Process all pending clock events for the current CPU cycle + while (cpu.nextClockEvent != null && cpu.nextClockEvent!.cycles <= cpu.cycles) { + final event = cpu.nextClockEvent!; + cpu.nextClockEvent = event.next; + event.callback(); + if (cpu.clockEventPool.length < 10) { + cpu.clockEventPool.add(event); + } + } + + // Feed queued serial input into the receiver as it frees up. writeByte + // returns false while the USART is busy or RX is disabled, so the byte + // stays queued until the sketch is ready for it. + if (rxQueue.isNotEmpty && !usart.rxBusy && usart.writeByte(rxQueue.first)) { + rxQueue.removeFirst(); + } + + // Process interrupts + if (cpu.interruptsEnabled && cpu.nextInterrupt >= 0) { + final interrupt = cpu.pendingInterrupts[cpu.nextInterrupt]; + if (interrupt != null) { + avrInterrupt(cpu, interrupt.address); + if (!interrupt.constant) { + cpu.clearInterrupt(interrupt); + } + } + } + + if (freqDetector.checkTimeout(cpu.cycles)) { + onBuzzerFrequencyChanged?.call(null); + } + } + _dutyTotalSamples = dutyTotal; + } + + static bool getPin13State() => _loaded && _portB.pinState(5) == PinState.High; + + /// Whether the Arduino Uno's on-board LED is currently lit. + /// + /// The on-board LED is hard-wired to digital pin 13 (PB5), so this reflects + /// the raw AVR port state directly. Unlike an attached LED component — whose + /// brightness comes from the analog (SPICE) circuit solve — this signal is + /// available from the emulator alone, which makes it the canonical way to + /// observe a "blink" sketch. + static bool get isBuiltinLedOn => getPin13State(); + + static bool getPinState(int pin) { + if (!_loaded) return false; + if (pin >= 0 && pin <= 7) { + return _portD.pinState(pin) == PinState.High; + } else if (pin >= 8 && pin <= 13) { + return _portB.pinState(pin - 8) == PinState.High; + } else if (pin >= 14 && pin <= 19) { + return _portC.pinState(pin - 14) == PinState.High; + } + return false; + } + + static void setAnalogVoltage(int channel, double voltage) { + if (!_loaded) return; + if (channel >= 0 && channel < _adc.channelValues.length) { + _adc.channelValues[channel] = voltage; + } + } + + static void setDigitalPin(int pin, {required bool isHigh}) { + if (!_loaded) return; + if (pin >= 0 && pin <= 7) { + _portD.setPin(pin, isHigh); + } else if (pin >= 8 && pin <= 13) { + _portB.setPin(pin - 8, isHigh); + } else if (pin >= 14 && pin <= 19) { + _portC.setPin(pin - 14, isHigh); + } + } + + static int getPortBValue() => _loaded ? _cpu.data[0x25] : 0; // PORTB address + + static int getCycles() => _loaded ? _cpu.cycles : 0; + + static void _parseHex(String hexString, Uint16List flash) { + flash.fillRange(0, flash.length, 0); + final lines = hexString.split('\n'); + for (var line in lines) { + line = line.trim(); + if (line.isEmpty || line[0] != ':') continue; + + final type = line.substring(7, 9); + if (type == '00') { + // Data record + final length = int.parse(line.substring(1, 3), radix: 16); + final addr = int.parse(line.substring(3, 7), radix: 16); + + for (var i = 0; i < length; i += 2) { + final lsb = int.parse(line.substring(9 + i * 2, 11 + i * 2), radix: 16); + var msb = 0; + if (i + 1 < length) { + msb = int.parse(line.substring(11 + i * 2, 13 + i * 2), radix: 16); + } + flash[(addr + i) >> 1] = (msb << 8) | lsb; + } + } + } + } +} + +/// Feeds the emulator's TWI (I²C) peripheral into an [I2cRecorder]. +/// +/// The peripheral drives this: every event asks a question and waits for the +/// matching `complete*` call, which is what lets a real implementation model +/// bus timing. Nothing here needs to — a simulated peripheral answers as fast +/// as the wire allows — so each event completes immediately, and the whole +/// class is the translation from bus events to recorded transactions. +class _RecordingTwiHandler(final AVRTWI twi, final I2cRecorder recorder) + implements TWIEventHandler { + @override + void start(bool repeated) => twi.completeStart(); + + @override + void stop() { + recorder.endTransaction(); + twi.completeStop(); + } + + @override + void connectToSlave(int addr, bool write) { + recorder.beginTransaction(addr, write: write); + // The ACK is the only answer that reaches the sketch: it is what + // `Wire.endTransmission()` returns and what an I²C scanner counts, so a + // device that is not on the canvas must not answer for one that is. + twi.completeConnect(recorder.acknowledges(addr)); + } + + @override + void writeByte(int value) { + recorder.writeByte(value); + twi.completeWrite(true); + } + + @override + void readByte(bool ack) { + // From the registers a part serves, or 0xFF — what a pulled-up, undriven + // bus reads — when nothing on the canvas answers at this address. + twi.completeRead(recorder.readByte()); + } +} diff --git a/packages/pinbench_sim/lib/core/avr_interop_io.dart b/packages/pinbench_sim/lib/core/avr_interop_io.dart deleted file mode 100644 index 5bcced0..0000000 --- a/packages/pinbench_sim/lib/core/avr_interop_io.dart +++ /dev/null @@ -1,346 +0,0 @@ -import 'dart:collection'; -import 'dart:typed_data'; - -import 'package:avr8_dart/avr8_dart.dart'; - -import 'sim_log.dart'; -import 'frequency_detector.dart'; -import 'i2c_recorder.dart'; - -/// Static facade over the `avr8_dart` ATmega328P emulator. -/// -/// Loads a compiled HEX, steps the CPU ([tick]), and exposes the I/O the -/// simulator needs: digital pin levels and PWM duty ([getPinState], -/// [getPinDuty]), ADC injection ([setAnalogVoltage]) for analogRead, serial -/// RX queueing ([queueSerialInput]) and TX (via onSerialPrint), and buzzer -/// frequency detection. State is static because the emulator is a singleton per -/// process; [loadHex] fully re-creates the CPU on each run. -class AVRBridge { - static const _log = SimLog('app.simulation.avr'); - static late CPU _cpu; - static CPU get cpu => _cpu; - static late AVRIOPort _portB; - static late AVRIOPort _portC; - static late AVRIOPort _portD; - static late AVRADC _adc; - - // Required to keep a reference to prevent garbage collection - // Reason: required by avr8_dart to keep timer active - // ignore: unused_field - static late AVRTimer _timer0; - // Reason: required by avr8_dart to keep timer active - // ignore: unused_field - static late AVRTimer _timer1; - // Reason: required by avr8_dart to keep timer active - // ignore: unused_field - static late AVRTimer _timer2; - static late AVRUSART _usart; - // The TWI peripheral installs a CPU write hook and is driven from it, so - // nothing else references it once constructed — but it must stay reachable, - // like the timers above. - static late AVRTWI _twi; - - /// Everything the sketch has put on the I²C bus, waiting for the parts that - /// listen for it. See [I2cRecorder] for why bus traffic is queued rather - /// than sampled like a pin. - static final _i2c = I2cRecorder(); - - /// Claims [address] on the bus, so a device at it acknowledges and its - /// traffic is kept for [drainI2c]. - static void listenI2c(int address) => _i2c.listenAt(address); - - /// Every transaction written to [address] since the last call. - static List> drainI2c(int address) => _i2c.drain(address); - - static void Function(double?)? onBuzzerFrequencyChanged; - - /// The Arduino pin number connected to the piezo buzzer. - /// Frequency detection is ONLY performed on this pin to avoid false triggers - /// from serial TX, LED PWM, or other toggling pins. - /// Set by SimulationRunner before starting the emulation loop. - static int? buzzerPin; - - /// Arduino pins driving servo signal lines. HIGH-pulse widths are measured - /// only on these pins (set from the traced netlist, like [buzzerPin]) so - /// serial TX / LED PWM toggles can't masquerade as servo pulses. - static List servoPins = const []; - - // Per-pin servo pulse measurement: cycle stamp of the last rising edge, and - // the width of the last completed HIGH pulse in microseconds. - static final _servoRiseCycle = List.filled(_dutyPinCount, -1); - static final _servoPulseUs = List.filled(_dutyPinCount, 0); - - /// Width (µs) of the most recent completed HIGH pulse on [pin], or 0 if none - /// has been seen since the current program was loaded. Only meaningful for - /// pins registered in [servoPins]. - static double getServoPulseUs(int pin) => - (pin >= 0 && pin < _dutyPinCount) ? _servoPulseUs[pin] : 0; - - static final _freqDetector = FrequencyDetector(); - - // Bytes the user typed in the Serial Monitor, drip-fed into the USART receiver - // as it becomes free (mirrors a real serial line into Serial.read/available). - static final _rxQueue = Queue(); - - /// Queues [text] to be delivered to the running sketch's serial receiver. - static void queueSerialInput(String text) { - _rxQueue.addAll(text.codeUnits); - } - - // Per-frame PWM duty-cycle sampling for digital pins 0..13. The pin is sampled - // periodically across a [tick] call; getPinDuty returns the fraction of - // samples the pin was high — i.e. the analogWrite() duty cycle. - static const _dutyPinCount = 14; - static const _dutySampleInterval = 64; // sample once every N CPU cycles - static final _dutyHighSamples = List.filled(_dutyPinCount, 0); - static var _dutyTotalSamples = 0; - - /// Fraction of the last [tick] window that [pin] was driven high (0.0–1.0). - /// Falls back to the instantaneous level if no samples were taken yet. - static double getPinDuty(int pin) { - if (pin < 0 || pin >= _dutyPinCount) return getPinState(pin) ? 1.0 : 0.0; - if (_dutyTotalSamples == 0) return getPinState(pin) ? 1.0 : 0.0; - return _dutyHighSamples[pin] / _dutyTotalSamples; - } - - static void loadHex(String hexString, {void Function(String)? onSerialPrint}) { - _rxQueue.clear(); - _servoRiseCycle.fillRange(0, _servoRiseCycle.length, -1); - _servoPulseUs.fillRange(0, _servoPulseUs.length, 0); - final program = Uint16List(0x4000); // 32KB flash is 16K x 16-bit - _parseHex(hexString, program); - _cpu = CPU(program); - _timer0 = AVRTimer(_cpu, timer0Config); - _timer1 = AVRTimer(_cpu, timer1Config); - _timer2 = AVRTimer(_cpu, timer2Config); - _usart = AVRUSART(_cpu, usart0Config, 16000000); - _usart.onLineTransmit = (line) { - onSerialPrint?.call(line); - }; - _portB = AVRIOPort(_cpu, portBConfig); - _portB.addListener(_handlePortBChange); - - _portC = AVRIOPort(_cpu, portCConfig); - _portC.addListener(_handlePortCChange); - - _portD = AVRIOPort(_cpu, portDConfig); - _portD.addListener(_handlePortDChange); - - _adc = AVRADC(_cpu, adcConfig); - - // The bus is rebuilt with the program: claims belong to the parts of the - // run that just ended, and its traffic to the sketch that just stopped. - _i2c.reset(); - _twi = AVRTWI(_cpu, twiConfig, 16000000); - _twi.eventHandler = _RecordingTwiHandler(_twi, _i2c); - - _freqDetector.reset(); - } - - static bool isPinOutput(int pin) { - if (pin >= 0 && pin <= 7) { - return (_cpu.data[portDConfig.DDR] & (1 << pin)) != 0; - } else if (pin >= 8 && pin <= 13) { - return (_cpu.data[portBConfig.DDR] & (1 << (pin - 8))) != 0; - } else if (pin >= 14 && pin <= 19) { - return (_cpu.data[portCConfig.DDR] & (1 << (pin - 14))) != 0; - } - return false; - } - - static void _handlePortDChange(int value, int oldValue) => _handlePortChange(0, value, oldValue); - static void _handlePortBChange(int value, int oldValue) => _handlePortChange(8, value, oldValue); - static void _handlePortCChange(int value, int oldValue) => _handlePortChange(14, value, oldValue); - - static void _handlePortChange(int pinOffset, int value, int oldValue) { - final changedBits = value ^ oldValue; - if (changedBits == 0) return; - - final bPin = buzzerPin; - if (bPin != null && - bPin >= pinOffset && - bPin < pinOffset + 8 && - (changedBits & (1 << (bPin - pinOffset))) != 0) { - _freqDetector.onPinToggle(_cpu.cycles, (freq) => onBuzzerFrequencyChanged?.call(freq)); - } - - for (final sPin in servoPins) { - if (sPin < pinOffset || sPin >= pinOffset + 8) continue; - final bit = 1 << (sPin - pinOffset); - if ((changedBits & bit) == 0) continue; - if ((value & bit) != 0) { - _servoRiseCycle[sPin] = _cpu.cycles; - } else { - final rise = _servoRiseCycle[sPin]; - if (rise >= 0) { - final us = (_cpu.cycles - rise) / 16.0; // 16 MHz clock - // Servo control pulses are ~0.4–3 ms; anything else on the pin - // (PWM, glitches) is ignored. - if (us >= 400 && us <= 3000) _servoPulseUs[sPin] = us; - } - } - } - } - - static void tick(int cycles) { - // Reset PWM duty accumulators for this frame. - _dutyTotalSamples = 0; - for (var p = 0; p < _dutyPinCount; p++) { - _dutyHighSamples[p] = 0; - } - - for (var i = 0; i < cycles; i++) { - avrInstruction(_cpu); - - // Periodically sample digital pin levels to estimate PWM duty cycles. - if ((i & (_dutySampleInterval - 1)) == 0) { - _dutyTotalSamples++; - for (var p = 0; p < _dutyPinCount; p++) { - if (getPinState(p)) _dutyHighSamples[p]++; - } - } - - // Process all pending clock events for the current CPU cycle - while (_cpu.nextClockEvent != null && _cpu.nextClockEvent!.cycles <= _cpu.cycles) { - final event = _cpu.nextClockEvent!; - _cpu.nextClockEvent = event.next; - event.callback(); - if (_cpu.clockEventPool.length < 10) { - _cpu.clockEventPool.add(event); - } - } - - // Feed queued serial input into the receiver as it frees up. writeByte - // returns false while the USART is busy or RX is disabled, so the byte - // stays queued until the sketch is ready for it. - if (_rxQueue.isNotEmpty && !_usart.rxBusy && _usart.writeByte(_rxQueue.first)) { - _rxQueue.removeFirst(); - } - - // Process interrupts - if (_cpu.interruptsEnabled && _cpu.nextInterrupt >= 0) { - final interrupt = _cpu.pendingInterrupts[_cpu.nextInterrupt]; - if (interrupt != null) { - avrInterrupt(_cpu, interrupt.address); - if (!interrupt.constant) { - _cpu.clearInterrupt(interrupt); - } - } - } - - if (_freqDetector.checkTimeout(_cpu.cycles)) { - _log.trace('Tone stopped at cycle ${_cpu.cycles}'); - onBuzzerFrequencyChanged?.call(null); - } - } - } - - static bool getPin13State() => _portB.pinState(5) == PinState.High; - - /// Whether the Arduino Uno's on-board LED is currently lit. - /// - /// The on-board LED is hard-wired to digital pin 13 (PB5), so this reflects - /// the raw AVR port state directly. Unlike an attached LED component — whose - /// brightness comes from the analog (SPICE) circuit solve — this signal is - /// available from the emulator alone, which makes it the canonical way to - /// observe a "blink" sketch. - static bool get isBuiltinLedOn => getPin13State(); - - static bool getPinState(int pin) { - if (pin >= 0 && pin <= 7) { - return _portD.pinState(pin) == PinState.High; - } else if (pin >= 8 && pin <= 13) { - return _portB.pinState(pin - 8) == PinState.High; - } else if (pin >= 14 && pin <= 19) { - return _portC.pinState(pin - 14) == PinState.High; - } - return false; - } - - static void setAnalogVoltage(int channel, double voltage) { - if (channel >= 0 && channel < _adc.channelValues.length) { - _adc.channelValues[channel] = voltage; - } - } - - static void setDigitalPin(int pin, {required bool isHigh}) { - if (pin >= 0 && pin <= 7) { - _portD.setPin(pin, isHigh); - } else if (pin >= 8 && pin <= 13) { - _portB.setPin(pin - 8, isHigh); - } else if (pin >= 14 && pin <= 19) { - _portC.setPin(pin - 14, isHigh); - } - } - - static int getPortBValue() => _cpu.data[0x25]; // PORTB address - - static int getCycles() => _cpu.cycles; - - static void _parseHex(String hexString, Uint16List flash) { - flash.fillRange(0, flash.length, 0); - final lines = hexString.split('\n'); - for (var line in lines) { - line = line.trim(); - if (line.isEmpty || line[0] != ':') continue; - - final type = line.substring(7, 9); - if (type == '00') { - // Data record - final length = int.parse(line.substring(1, 3), radix: 16); - final addr = int.parse(line.substring(3, 7), radix: 16); - - for (var i = 0; i < length; i += 2) { - final lsb = int.parse(line.substring(9 + i * 2, 11 + i * 2), radix: 16); - var msb = 0; - if (i + 1 < length) { - msb = int.parse(line.substring(11 + i * 2, 13 + i * 2), radix: 16); - } - flash[(addr + i) >> 1] = (msb << 8) | lsb; - } - } - } - } -} - -/// Feeds the emulator's TWI (I²C) peripheral into an [I2cRecorder]. -/// -/// The peripheral drives this: every event asks a question and waits for the -/// matching `complete*` call, which is what lets a real implementation model -/// bus timing. Nothing here needs to — a simulated peripheral answers as fast -/// as the wire allows — so each event completes immediately, and the whole -/// class is the translation from bus events to recorded transactions. -class _RecordingTwiHandler(final AVRTWI twi, final I2cRecorder recorder) - implements TWIEventHandler { - @override - void start(bool repeated) => twi.completeStart(); - - @override - void stop() { - recorder.endTransaction(); - twi.completeStop(); - } - - @override - void connectToSlave(int addr, bool write) { - recorder.beginTransaction(addr, write: write); - // The ACK is the only answer that reaches the sketch: it is what - // `Wire.endTransmission()` returns and what an I²C scanner counts, so a - // device that is not on the canvas must not answer for one that is. - twi.completeConnect(recorder.acknowledges(addr)); - } - - @override - void writeByte(int value) { - recorder.writeByte(value); - twi.completeWrite(true); - } - - @override - void readByte(bool ack) { - // Nothing on the canvas talks back yet. 0xFF is what an idle bus reads, - // pulled up and undriven, which is also what a sketch reading from a - // device that is not there would see. - twi.completeRead(0xFF); - } -} diff --git a/packages/pinbench_sim/lib/core/avr_interop_web.dart b/packages/pinbench_sim/lib/core/avr_interop_web.dart deleted file mode 100644 index 73e24b5..0000000 --- a/packages/pinbench_sim/lib/core/avr_interop_web.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'dart:js_interop'; - -/// Web [AVRBridge] backed by `avr8js` via the `window.AVR8` bridge defined in -/// `web/avr_bridge.js`. Mirrors the native `avr_interop_io.dart` API but runs -/// the emulator in optimized JS so the 16 MHz AVR keeps real time. -@JS('AVR8') -external _Avr8 get _avr8; - -extension type _Avr8._(JSObject _) implements JSObject { - external void setSerialPrint(JSFunction cb); - external void setBuzzerFrequencyCallback(JSFunction cb); - external void setBuzzerPin(JSAny? pin); - external void setServoPins(JSArray pins); - external double getServoPulseUs(int pin); - external void loadHex(String hex); - external void queueSerialInput(String text); - external void tick(int cycles); - external bool getPinState(int pin); - external double getPinDuty(int pin); - external bool isPinOutput(int pin); - // Positional args mirror the JS bridge signature. - // ignore: avoid_positional_boolean_parameters - external void setDigitalPin(int pin, bool isHigh); - external void setAnalogVoltage(int channel, double voltage); - external int getPortBValue(); - external int getCycles(); - external bool getPin13State(); - external void listenI2c(int address); - external JSInt32Array drainI2c(int address); -} - -class AVRBridge._() { - static void Function(double?)? onBuzzerFrequencyChanged; - - static int? _buzzerPin; - static int? get buzzerPin => _buzzerPin; - static set buzzerPin(int? pin) { - _buzzerPin = pin; - _avr8.setBuzzerPin(pin?.toJS); - } - - static List _servoPins = const []; - static List get servoPins => _servoPins; - static set servoPins(List pins) { - _servoPins = pins; - _avr8.setServoPins(pins.map((p) => p.toJS).toList().toJS); - } - - static double getServoPulseUs(int pin) => _avr8.getServoPulseUs(pin); - - static var _wired = false; - - static void _ensureBuzzerWired() { - if (_wired) return; - _wired = true; - _avr8.setBuzzerFrequencyCallback( - ((JSAny? freq) { - onBuzzerFrequencyChanged?.call(freq == null ? null : (freq as JSNumber).toDartDouble); - }).toJS, - ); - } - - static void loadHex(String hexString, {void Function(String)? onSerialPrint}) { - _ensureBuzzerWired(); - _avr8.setSerialPrint(((String line) => onSerialPrint?.call(line)).toJS); - _avr8.loadHex(hexString); - } - - static void queueSerialInput(String text) => _avr8.queueSerialInput(text); - - static void tick(int cycles) => _avr8.tick(cycles); - - static bool getPinState(int pin) => _avr8.getPinState(pin); - - static double getPinDuty(int pin) => _avr8.getPinDuty(pin); - - static bool isPinOutput(int pin) => _avr8.isPinOutput(pin); - - static void setDigitalPin(int pin, {required bool isHigh}) => _avr8.setDigitalPin(pin, isHigh); - - static void setAnalogVoltage(int channel, double voltage) => - _avr8.setAnalogVoltage(channel, voltage); - - static int getPortBValue() => _avr8.getPortBValue(); - - static int getCycles() => _avr8.getCycles(); - - static bool getPin13State() => _avr8.getPin13State(); - - static bool get isBuiltinLedOn => getPin13State(); - - /// Claims [address] on the I²C bus, so a device at it acknowledges and its - /// traffic is kept for [drainI2c]. - static void listenI2c(int address) => _avr8.listenI2c(address); - - /// Every transaction written to [address] since the last call. - /// - /// The bridge hands the whole frame's traffic over as one flat typed array — - /// `[length, ...bytes]` per transaction — rather than a nested array of - /// numbers, so a screen refresh crosses the JS boundary as a single copy - /// instead of a thousand boxed values. - static List> drainI2c(int address) { - final flat = _avr8.drainI2c(address).toDart; - final transactions = >[]; - var at = 0; - while (at < flat.length) { - final length = flat[at++]; - if (length <= 0 || at + length > flat.length) break; - transactions.add(flat.sublist(at, at + length)); - at += length; - } - return transactions; - } -} diff --git a/packages/pinbench_sim/lib/core/engine_pin_api.dart b/packages/pinbench_sim/lib/core/engine_pin_api.dart index 35c667b..1afafd4 100644 --- a/packages/pinbench_sim/lib/core/engine_pin_api.dart +++ b/packages/pinbench_sim/lib/core/engine_pin_api.dart @@ -141,6 +141,14 @@ class const EngineI2cApi() implements PartI2cApi { AVRBridge.listenI2c(address); return AVRBridge.drainI2c(address); } + + @override + void serve(int address, {int size = 256, int pointerBytes = 1}) => + AVRBridge.serveI2c(address, size: size, pointerBytes: pointerBytes); + + @override + void setRegisters(int address, int offset, List bytes) => + AVRBridge.setI2cRegisters(address, offset, bytes); } /// [PartSpiceApi] backed by the running solver. diff --git a/packages/pinbench_sim/lib/core/i2c_recorder.dart b/packages/pinbench_sim/lib/core/i2c_recorder.dart index 0c0c5fd..3bcc627 100644 --- a/packages/pinbench_sim/lib/core/i2c_recorder.dart +++ b/packages/pinbench_sim/lib/core/i2c_recorder.dart @@ -1,4 +1,7 @@ -/// Records what the sketch writes onto the I²C bus, per slave address. +import 'dart:typed_data'; + +/// Records what the sketch writes onto the I²C bus, per slave address, and +/// answers what it reads from devices that [serve] registers. /// /// The emulator's TWI peripheral reports the bus as *events* — start, address, /// byte, stop — while a peripheral wants *transactions*, because the first byte @@ -11,11 +14,19 @@ /// refresh — a thousand bytes — can arrive between two looks. Sampling would /// lose all but the last byte and a display would show noise. /// +/// **Reads are answered here, not by the part.** A read has to be answered +/// the moment the sketch clocks it, in the middle of the CPU's frame, and a +/// part only runs between frames. So a part that talks back publishes its +/// registers with [serve] and [setRegisters], and the bus answers from them. +/// That is how almost every I²C sensor is built anyway: the sketch writes a +/// register pointer, then reads, and the pointer advances after every byte. +/// Values a part refreshes each frame are at most one frame (16 ms) old, far +/// fresher than any real sensor's conversion time. +/// /// Kept out of the emulator facades because both of them need it and neither -/// owns it: the native bridge feeds this from `avr8_dart`'s TWI callbacks, the -/// web bridge from `avr8js`'s, and the frame loop drains the same shape either -/// way. -class I2cRecorder() { +/// owns it: the bridge feeds this from `avr8_dart`'s TWI callbacks, and the +/// frame loop drains it. +class I2cRecorder { /// Addresses some part has claimed by draining them. /// /// This gates the *acknowledgement*, not the recording, and the difference @@ -50,22 +61,89 @@ class I2cRecorder() { List? _current; int? _currentAddress; + /// Devices that answer reads, by address. + final Map _devices = {}; + + /// The device the open transaction talks to, if it serves registers. + I2cRegisterDevice? _device; + + /// Whether the open transaction is a write, while [_device] is set. + var _writing = false; + + /// Pointer bytes still to come in the open write transaction. A write + /// starts by setting the register pointer, most significant byte first; + /// everything after that is data. + var _pointerBytesLeft = 0; + /// Claims [address] so its traffic is recorded from now on. void listenAt(int address) => _listening.add(address); bool isListening(int address) => _listening.contains(address); + /// Makes [address] answer reads from a bank of [size] registers. + /// + /// [pointerBytes] is how many bytes of a write set the register pointer: + /// 1 for most sensors, 2 for large EEPROMs, and 0 for devices with no + /// registers to choose between (a PCF8574 port expander), whose every read + /// starts again at register 0. + /// + /// Serving claims the address, like [listenAt]. Calling it again with the + /// same shape keeps the registers and the pointer, so a part can declare + /// itself every frame without resetting what the sketch wrote; a different + /// shape starts over, zero-filled. + void serve(int address, {int size = 256, int pointerBytes = 1}) { + assert( + size > 0 && pointerBytes >= 0 && pointerBytes <= 2, + 'size $size, pointerBytes $pointerBytes', + ); + _listening.add(address); + final existing = _devices[address]; + if (existing != null && + existing.registers.length == size && + existing.pointerBytes == pointerBytes) { + return; + } + _devices[address] = I2cRegisterDevice(size: size, pointerBytes: pointerBytes); + } + + /// Overwrites [address]'s registers from [offset], wrapping at the end of + /// the bank. How a part publishes fresh readings. Does nothing for an + /// address nobody [serve]s. + void setRegisters(int address, int offset, List bytes) { + final registers = _devices[address]?.registers; + if (registers == null) return; + for (var i = 0; i < bytes.length; i++) { + registers[(offset + i) % registers.length] = bytes[i] & 0xFF; + } + } + + /// [address]'s registers, or null when nothing serves it. For tests and + /// for parts that want to read back what the sketch wrote. + List? registersOf(int address) => _devices[address]?.registers; + /// Whether any device on the canvas answers at [address] — what the TWI /// peripheral needs in order to ACK or NACK the address byte, and therefore /// what makes `Wire.endTransmission()` report success or failure to the /// sketch exactly as a real bus would. bool acknowledges(int address) => _listening.contains(address); - /// Starts recording a write transaction to [address]. A read is not - /// recorded — nothing on the canvas talks back yet — but it still closes - /// whatever transaction was open. + /// Starts a transaction with [address]. A write is recorded, and also + /// updates the register pointer and registers of a device that [serve]s; + /// a read is not recorded, but it still closes whatever transaction was + /// open, and a pointer-less device starts its read at register 0. void beginTransaction(int address, {required bool write}) { endTransaction(); + final device = _devices[address]; + _device = device; + _writing = write; + if (device != null) { + if (write) { + _pointerBytesLeft = device.pointerBytes; + if (device.pointerBytes > 0) device.pointer = 0; + } else if (device.pointerBytes == 0) { + device.pointer = 0; + } + } if (!write) return; if (!_pending.containsKey(address) && _pending.length >= maxRecordedAddresses) return; _currentAddress = address; @@ -74,7 +152,32 @@ class I2cRecorder() { /// Records one byte of the open transaction. Ignored when the bus is idle or /// addressing a device nobody listens for. - void writeByte(int value) => _current?.add(value & 0xFF); + void writeByte(int value) { + final byte = value & 0xFF; + _current?.add(byte); + final device = _device; + if (device == null || !_writing) return; + if (_pointerBytesLeft > 0) { + device.pointer = ((device.pointer << 8) | byte) % device.registers.length; + _pointerBytesLeft--; + } else if (device.pointerBytes > 0) { + // A pointer-less device's writes drive its outputs, not a register the + // sketch can read back, so only register devices store them. + device.registers[device.pointer] = byte; + device.advance(); + } + } + + /// The byte the sketch reads next from the open read transaction: the + /// register under the pointer, which then advances. 0xFF — a pulled-up, + /// undriven bus — when nothing serves the address. + int readByte() { + final device = _device; + if (device == null || _writing) return 0xFF; + final value = device.registers[device.pointer]; + device.advance(); + return value; + } /// Closes the open transaction and queues it for its address. void endTransaction() { @@ -82,6 +185,8 @@ class I2cRecorder() { final bytes = _current; _currentAddress = null; _current = null; + _device = null; + _pointerBytesLeft = 0; if (address == null || bytes == null || bytes.isEmpty) return; final queue = _pending.putIfAbsent(address, () => []); @@ -106,7 +211,22 @@ class I2cRecorder() { _listening.clear(); _pending.clear(); _bufferedBytes.clear(); + _devices.clear(); _current = null; _currentAddress = null; + _device = null; + _pointerBytesLeft = 0; } } + +/// One device's registers and its pointer, as [I2cRecorder.serve] made them. +class I2cRegisterDevice({required int size, required final int pointerBytes}) { + final registers = Uint8List(size); + + /// The register the next read or write touches. + var pointer = 0; + + /// Moves to the next register, wrapping at the end of the bank the way an + /// auto-incrementing device does. + void advance() => pointer = (pointer + 1) % registers.length; +} diff --git a/packages/pinbench_sim/lib/core/simulation_engine.dart b/packages/pinbench_sim/lib/core/simulation_engine.dart index a334cd6..8b7b31b 100644 --- a/packages/pinbench_sim/lib/core/simulation_engine.dart +++ b/packages/pinbench_sim/lib/core/simulation_engine.dart @@ -111,6 +111,9 @@ class SimulationEngine({ /// run would silently expire it. var _runElapsedUs = 0; + /// Whether this run's parts have had their power-on pass — see [runFrame]. + var _partsPoweredOn = false; + /// Last-seen state of everything that can change circuit *connectivity* /// mid-run, so the adjacency map is only rebuilt when it actually moved. /// @@ -186,6 +189,7 @@ class SimulationEngine({ _lastTopologyState = {}; _lastPin13State = false; _runElapsedUs = 0; + _partsPoweredOn = false; for (final node in _output.simulationNodes) { _nodesByKey[node.key] = node; @@ -466,11 +470,11 @@ class SimulationEngine({ ); } - void _updateBehaviourParts() { + void _updateBehaviourParts({bool circuitSolved = true}) { PartBehaviorFrameUpdater.update( nodes: _behaviourNodes, spiceEngine: _spiceEngine, - isSpiceActive: _isSpiceActive, + isSpiceActive: circuitSolved && _isSpiceActive, lastState: _lastBehaviourState, elapsed: Duration(microseconds: _runElapsedUs), netlist: _netlist, @@ -584,6 +588,19 @@ class SimulationEngine({ int runFrame({int? cycles, bool solveSpice = true}) { final sw = Stopwatch()..start(); + // 0. Power the parts on before the CPU's first instruction. Parts normally + // run after the tick (step 7), which is too late for a sensor on the first + // frame: `setup()` probes the bus straight away, and a device that has not + // yet `serve`d its registers is NACKed, so `Adafruit_MPU6050.begin()` and + // friends report "chip not found" and never retry. On a desk the sensor is + // powered as soon as the board is. + // Nothing has been solved yet, so the pass sees the analog model as off: + // an LED asking ngspice for its current now would find no result vector. + if (!_partsPoweredOn) { + _partsPoweredOn = true; + if (_behaviourNodes.isNotEmpty) _updateBehaviourParts(circuitSolved: false); + } + // 1. Update digital inputs (push buttons → netlist → AVR pins) _updateDigitalInputs(); diff --git a/packages/pinbench_sim/lib/core/simulation_runner_backend.dart b/packages/pinbench_sim/lib/core/simulation_runner_backend.dart index b5d1576..72ce846 100644 --- a/packages/pinbench_sim/lib/core/simulation_runner_backend.dart +++ b/packages/pinbench_sim/lib/core/simulation_runner_backend.dart @@ -5,8 +5,7 @@ import 'package:pinbench_parts/models/wire_model.dart'; /// a background isolate running the AVR+SPICE engine; the web runs the /// engine inline on the UI isolate (no `Isolate.spawn` there). Selected at /// compile time via the conditional-import trio -/// (`simulation_runner_backend_io.dart`/`_web.dart`), mirroring the -/// `avr_interop_io.dart`/`_web.dart` precedent already used by this feature. +/// (`simulation_runner_backend_io.dart`/`_web.dart`). /// /// `SimulationRunner` owns everything platform-agnostic (the /// isSimulating/isPaused guards, the mic-timer's periodic bookkeeping, the diff --git a/packages/pinbench_sim/lib/core/updaters/part_behavior_frame_updater.dart b/packages/pinbench_sim/lib/core/updaters/part_behavior_frame_updater.dart index 53a49d6..a4d6244 100644 --- a/packages/pinbench_sim/lib/core/updaters/part_behavior_frame_updater.dart +++ b/packages/pinbench_sim/lib/core/updaters/part_behavior_frame_updater.dart @@ -72,7 +72,7 @@ abstract final class PartBehaviorFrameUpdater { final result = PdlBehaviorEvaluator.evaluate( definition, - node.properties, + _withoutDeclaredState(definition, node.properties), // The engine's own record of the last frame, not the node's property // map. Only one of the two `SimulationOutput`s writes updates back // onto the nodes — see [lastState] — so the node map is a starting @@ -107,6 +107,27 @@ abstract final class PartBehaviorFrameUpdater { } } + /// [properties] without the keys [definition] declares as `STATE`. + /// + /// The canvas keeps the last run's final state in each node's property map + /// (a stopped display keeps its picture), and the evaluator seeds state from + /// that map. So without this a `.pdl` part started every run where the last + /// one ended — a PIR still holding, a sensor still configured — although + /// `STATE` is documented as recreated from its initial value every run. + /// `lastState` is the memory *within* a run; this is only about where a run + /// starts. A part with no definition has no declared state to strip: its + /// logic owns the whole map. + static Map _withoutDeclaredState( + PartDefinition? definition, + Map properties, + ) { + if (definition == null || definition.state.isEmpty) return properties; + return { + for (final entry in properties.entries) + if (!definition.state.containsKey(entry.key)) entry.key: entry.value, + }; + } + /// Names already reported as missing, so an unknown `LOGIC` is logged once /// rather than sixty times a second for the life of the run. static final Set _reportedMissing = {}; diff --git a/packages/pinbench_sim/pubspec.yaml b/packages/pinbench_sim/pubspec.yaml index c1125b5..1a48ced 100644 --- a/packages/pinbench_sim/pubspec.yaml +++ b/packages/pinbench_sim/pubspec.yaml @@ -10,14 +10,18 @@ environment: sdk: ^3.13.0 dependencies: - avr8_dart: ^0.1.0 + # Not on pub.dev yet; pinned to the same commit as the app's pubspec.yaml. + avr8_dart: + git: + url: https://github.com/PinBench/avr8_dart.git + ref: 70f44156fbe8ae509313e1c0aace0b0efa079d01 collection: ^1.19.1 flutter: sdk: flutter # Local pure-Dart port (no FFI / libngspice). API-compatible Ngspice facade. ngspice_dart: git: - url: https://github.com/burhankhanzada/ngspice_dart.git + url: https://github.com/PinBench/ngspice_dart.git # Pinned to a commit, not a branch — see the app's pubspec.yaml. ref: 1061e5f824186f73671495080fe6c37433b71a1c pinbench_parts: diff --git a/packages/pinbench_sim/test/avr_tick_test.dart b/packages/pinbench_sim/test/avr_tick_test.dart new file mode 100644 index 0000000..e119980 --- /dev/null +++ b/packages/pinbench_sim/test/avr_tick_test.dart @@ -0,0 +1,27 @@ +/// `AVRBridge.tick` advances simulated time by exactly the cycles it is asked +/// for, which is what keeps `delay()` and `millis()` in step with the wall +/// clock. +library; + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinbench_sim/core/avr_interop.dart'; + +void main() { + test('tick(n) advances n cycles, not n instructions', () { + // Any real firmware will do; this one mixes Wire, Serial and delay loops, + // so its instructions average well over one cycle each. + AVRBridge.loadHex(File('test/fixtures/i2c_read/i2c_read.hex').readAsStringSync()); + const frame = 256000; // one 16 ms frame at 16 MHz + + for (var f = 0; f < 50; f++) { + final before = AVRBridge.getCycles(); + AVRBridge.tick(frame); + final ran = AVRBridge.getCycles() - before; + // The last instruction may finish a few cycles past the boundary (the + // longest AVR instructions take 4 cycles, 5 with an interrupt entry). + expect(ran, inInclusiveRange(frame, frame + 5), reason: 'frame $f ran $ran cycles'); + } + }); +} diff --git a/packages/pinbench_sim/test/fixtures/i2c_read/i2c_read.hex b/packages/pinbench_sim/test/fixtures/i2c_read/i2c_read.hex new file mode 100644 index 0000000..1b4733f --- /dev/null +++ b/packages/pinbench_sim/test/fixtures/i2c_read/i2c_read.hex @@ -0,0 +1,287 @@ +:100000000C9458000C9480000C9480000C94800098 +:100010000C9480000C9480000C9480000C94800060 +:100020000C9480000C9480000C9480000C94800050 +:100030000C9480000C9480000C9480000C94800040 +:100040000C9496050C9480000C9406060C94E00524 +:100050000C9480000C9480000C9480000C94800020 +:100060000C9438060C948000000000002500280045 +:100070002B00040404040404040402020202020229 +:10008000030303030303010204081020408001025C +:1000900004081020010204081020000000080002DB +:1000A00001000003040700000000000000005707E3 +:1000B00011241FBECFEFD8E0DEBFCDBF11E0A0E01E +:1000C000B1E0E4E8F1E102C005900D92A434B1077B +:1000D000D9F722E0A4E4B1E001C01D92A03BB20731 +:1000E000E1F710E0C8E5D0E004C02197FE010E94CE +:1000F000BA08C735D107C9F70E9490070C94C00809 +:100100000C94000090E0FC01E656FF4F2491FC01A6 +:10011000EA57FF4F3491FC01EE58FF4FE491EE2374 +:10012000C9F0222339F0233001F1A8F4213019F16C +:10013000223029F1F0E0EE0FFF1FE859FF4FA591A3 +:10014000B4918FB7F894EC91611126C030953E239D +:100150003C938FBF08952730A9F02830C9F0243090 +:1001600049F7809180008F7D03C0809180008F7758 +:1001700080938000DFCF84B58F7784BDDBCF84B5DB +:100180008F7DFBCF8091B0008F778093B000D2CF6E +:100190008091B0008F7DF9CF3E2BDACF3FB7F89436 +:1001A0008091B3019091B401A091B501B091B601D5 +:1001B00026B5A89B05C02F3F19F00196A11DB11DC2 +:1001C0003FBFBA2FA92F982F8827BC01CD01620FFE +:1001D000711D811D911D42E0660F771F881F991FB9 +:1001E0004A95D1F7089508950895AF92BF92CF929E +:1001F000DF92EF92FF920F931F93CF93DF936C01E7 +:100200007B018B01040F151FEB015E01AE18BF08C7 +:10021000C017D10759F06991D601ED91FC91019079 +:10022000F081E02DC6010995892B79F7C501DF9191 +:10023000CF911F910F91FF90EF90DF90CF90BF90E3 +:10024000AF900895FC01538D448D252F30E0842F0D +:1002500090E0821B930B541710F0CF9608950197EE +:100260000895FC01918D828D981761F0A28DAE0FDB +:10027000BF2FB11D5D968C91928D9F5F9F73928F62 +:1002800090E008958FEF9FEF0895FC01918D828D8E +:10029000981731F0828DE80FF11D858D90E008955B +:1002A0008FEF9FEF0895FC01918D228D892F90E0B3 +:1002B000805C9F4F821B91098F739927089587E077 +:1002C00092E00E94530121E0892B09F420E0822F63 +:1002D000089580E090E0892B29F00E945F01811150 +:1002E0000C9400000895FC01A48DA80FB92FB11D36 +:1002F000A35ABF4F2C91848D90E001968F7399275C +:10030000848FA689B7892C93A089B1898C91837039 +:1003100080648C93938D848D981306C00288F38932 +:10032000E02D80818F7D80830895EF92FF920F935F +:100330001F93CF93DF93EC0181E0888F9B8D8C8D91 +:1003400098131AC0E889F989808185FF15C09FB785 +:10035000F894EE89FF896083E889F9898081837048 +:10036000806480839FBF81E090E0DF91CF911F91F7 +:100370000F91FF90EF900895F62E0B8D10E00F5F18 +:100380001F4F0F731127E02E8C8D8E110CC00FB6EE +:1003900007FCFACFE889F989808185FFF5CFCE0186 +:1003A0000E947301F1CFEB8DEC0FFD2FF11DE35A8D +:1003B000FF4FF0829FB7F8940B8FEA89FB89808109 +:1003C0008062CFCFCF93DF93EC01888D8823B9F083 +:1003D000AA89BB89E889F9898C9185FD03C0808150 +:1003E00086FD0DC00FB607FCF7CF8C9185FFF2CFCD +:1003F000808185FFEDCFCE010E947301E9CFDF91AF +:10040000CF91089520918501260F3327331F213284 +:100410003105ECF42091DD01FC0190E080E0243016 +:1004200069F082E00895A09185012191AC014B59BA +:100430005E4FA40FB52FB11D2C930196861798F32C +:1004400080918501680F6093850180E0089581E0C7 +:1004500008951092DD0181E08093DC011092B701D4 +:1004600061E082E10E94820061E083E10E948200FB +:10047000E9EBF0E080818E7F808380818D7F8083B7 +:1004800088E48093B80085E48093BC000895CF93FE +:10049000DF9391E0909364018823B9F0C091B80094 +:1004A000D091BA008091BC008A7B8093BC0060E050 +:1004B00082E10E94820060E083E10E9482000E944B +:1004C0002902D093BA00C093B800DF91CF9108956C +:1004D0008F929F92AF92BF92CF92DF92EF92FF9254 +:1004E0000F931F93CF93DF93D0910602D13208F080 +:1004F000D7C0182FC09105020E94CE006B017C016D +:100500008091DD0181116BC082E08093DD01109349 +:10051000DC018FEF8093DB011092DA01D093D901D7 +:10052000A5EEB1E0E9EBF1E080E0D81391C01092C4 +:10053000B8018091B801CC0FC82BC093B80180914D +:10054000B701813009F088C01092B7010E94CE0037 +:100550006B017C018091B8018093BB008091DF0129 +:100560009091E001A091E101B091E201892B8A2BE9 +:100570008B2BA1F00E94CE000091DF011091E001D1 +:100580002091E1013091E2016C197D098E099F09EA +:10059000061717072807390708F442C08091BC00E6 +:1005A00083FDD8CF85EC8093BC000E94CE006B0108 +:1005B0007C018091DD01823009F450C08091DB0123 +:1005C0008F3F09F46FC08091DB01803209F46CC069 +:1005D0008091DB01803309F469C084E026C08091FA +:1005E000DF019091E001A091E101B091E201892B3E +:1005F0008A2B8B2B09F484CF0E94CE008090DF01E0 +:100600009090E001A090E101B090E2016C197D09A9 +:100610008E099F0986169706A806B90608F070CFBE +:100620008091DE010E94470285E01092E401109261 +:1006300006021092E301DF91CF911F910F91FF907D +:10064000EF90DF90CF90BF90AF909F908F900895E4 +:100650009D9191938F5F69CF85EEA5CF8091DF014A +:100660009091E001A091E101B091E201892B8A2BE8 +:100670008B2B09F49ECF0E94CE000091DF011091D8 +:10068000E0012091E1013091E2016C197D098E09B0 +:100690009F09061717072807390708F08ACFC0CF28 +:1006A00081E0C3CF80E0C1CF82E0BFCF83E0BDCF88 +:1006B0000895E091AD018091AC01E81730F4F0E0CD +:1006C000EC5BFE4F808190E008958FEF9FEF0895DF +:1006D0009091AD018091AC012FEF3FEF981748F456 +:1006E000E92FF0E0EC5BFE4F208130E09F5F9093BC +:1006F000AD01C90108958091AC019091AD01891BB4 +:10070000990B0895CF92DF92EF92FF920F931F9370 +:10071000CF93DF937C01CB018A012091E301222357 +:1007200089F0EB016B01C40ED51ECC15DD0569F017 +:100730006991D701ED91FC910190F081E02DC70105 +:100740000995F3CF642F0E940202C801DF91CF9177 +:100750001F910F91FF90EF90DF90CF900895CF936E +:10076000DF931F92CDB7DEB769832091E301222387 +:10077000F9F020910602203258F021E030E0FC012F +:100780003383228390E080E00F90DF91CF91089532 +:100790008091E401E82FF0E0EB51FE4F99819083C6 +:1007A0008F5F8093E4018093060281E090E0ECCFBC +:1007B00061E0CE0101960E940202F7CF90E080E056 +:1007C0000895FC0101900020E9F73197AF01481B23 +:1007D000590BBC0187E092E00C94F5008F929F9238 +:1007E000AF92BF920F931F93CF93DF93CDB7DEB736 +:1007F000A1970FB6F894DEBF0FBECDBF19A242304D +:1008000008F44AE08E010F5D1F4F842E912CB12C0D +:10081000A12CA50194010E949808E62FB901CA01F4 +:10082000EA30F4F4E05DD801EE938D01232B242B04 +:10083000252B79F790E080E0109719F0CD010E9408 +:10084000E103A1960FB6F894DEBF0FBECDBFDF91D6 +:10085000CF911F910F91BF90AF909F908F9008956F +:10086000E95CE1CFEF92FF920F931F93CF93DF9359 +:10087000F62E172F042F0E94E103CF2DD12F0C0F3E +:100880001D2F111DC017D10799F07E01888180317D +:1008900028F460E387E092E00E9495012196F70139 +:1008A000608170E090E080E040E10E94EE03EACFDA +:1008B00084E291E0DF91CF911F910F91FF90EF9033 +:1008C0000C94E1034F925F926F927F92BF92CF920E +:1008D000DF92EF92FF920F931F93CF93DF93D62F68 +:1008E000B72E142F91E09093E30198E690930502C0 +:1008F0001092E40110920602682F84EA92E00E94AE +:10090000AF0380E00E946802C82F8111C7C00E9417 +:10091000CE006B017C018091DD01811163C081E01B +:100920008093DD018093DC019FEF9093DB011092B7 +:10093000DA01910F9093D9018093B8018091B801A9 +:10094000806D8093B8018091B701813009F082C039 +:100950001092B7010E94CE006B017C018091B8011A +:100960008093BB008091DF019091E001A091E101B3 +:10097000B091E201892B8A2B8B2BA1F00E94CE0033 +:100980004090DF015090E0016090E1017090E20141 +:100990006C197D098E099F09461657066806790667 +:1009A00008F441C08091BC0083FDD8CF85EC8093D2 +:1009B000BC000E94CE006B017C018091DD01813082 +:1009C00009F44AC08091DA01811710F41091DA011C +:1009D000A9EBB1E0E4E4F1E0C11749F18D91819315 +:1009E000CF5FFACF8091DF019091E001A091E1010A +:1009F000B091E201892B8A2B8B2B09F48CCF0E94BA +:100A0000CE004090DF015090E0016090E1017090D5 +:100A1000E2016C197D098E099F0946165706680682 +:100A2000790608F078CF8091DE010E94470210928B +:100A3000AD01C093AC01FD2E0D2F1B2D802F8F1902 +:100A40008C1768F584EA92E00E946803F8018193AC +:100A50008F01F4CF85EEABCF8091DF019091E00163 +:100A6000A091E101B091E201892B8A2B8B2B09F433 +:100A7000A4CF0E94CE004090DF015090E001609032 +:100A8000E1017090E2016C197D098E099F094616FB +:100A900057066806790608F090CFC5CFC0E08C2FC6 +:100AA000DF91CF911F910F91FF90EF90DF90CF904A +:100AB000BF907F906F905F904F90089585ED8093E9 +:100AC000BC008091DF019091E001A091E101B09123 +:100AD000E2010796A11DB11D23E0B695A795979554 +:100AE00087952A95D1F72091BC0024FD03C0109270 +:100AF000DD0108954091DF015091E0016091E10135 +:100B00007091E201452B462B472B69F30097A10515 +:100B1000B10541F02AE22A95F1F700C00197A10939 +:100B2000B109E1CF8091DE010C9447021F920F9230 +:100B30000FB60F9211242F933F938F939F93AF93F0 +:100B4000BF938091AF019091B001A091B101B0919C +:100B5000B2013091AE0123E0230F2D3758F50196F5 +:100B6000A11DB11D2093AE018093AF019093B00100 +:100B7000A093B101B093B2018091B3019091B401FF +:100B8000A091B501B091B6010196A11DB11D809350 +:100B9000B3019093B401A093B501B093B601BF9196 +:100BA000AF919F918F913F912F910F900FBE0F901A +:100BB0001F90189526E8230F0296A11DB11DD2CFD4 +:100BC0001F920F920FB60F9211242F933F934F93C2 +:100BD0005F936F937F938F939F93AF93BF93EF93A5 +:100BE000FF9387E092E00E947301FF91EF91BF9124 +:100BF000AF919F918F917F916F915F914F913F91B5 +:100C00002F910F900FBE0F901F9018951F920F926B +:100C10000FB60F9211242F938F939F93EF93FF930F +:100C2000E0911702F09118028081E0911D02F0918D +:100C30001E0282FD1BC09081809120028F5F8F7306 +:100C400020912102821741F0E0912002F0E0E95F5B +:100C5000FD4F958F80932002FF91EF919F918F918F +:100C60002F910F900FBE0F901F9018958081F4CF99 +:100C70001F920F920FB60F9211242F933F934F9311 +:100C80005F936F937F938F939F93AF93BF93EF93F4 +:100C9000FF938091B900887F803609F44AC0F0F54F +:100CA000883209F4A7C018F5803109F49BC0B8F464 +:100CB000882309F4F9C0883009F494C0FF91EF91BA +:100CC000BF91AF919F918F917F916F915F914F9164 +:100CD0003F912F910F900FBE0F901F901895883164 +:100CE00009F488C0803251F78093DB0114C080344E +:100CF00009F49DC040F48033B9F38833F9F680934A +:100D0000DB0185ECB0C0803509F485C0883509F475 +:100D100096C0883499F60E945E05D0CF883909F4D0 +:100D20008CC038F5883729F050F4883611F08037B8 +:100D300029F683E08093DD011092A70157C088381F +:100D400009F47BC0803919F0803809F0B7CF809161 +:100D5000A701803208F071C0E091A70181E08E0FF9 +:100D60008093A7018091BB00F0E0E957FE4F80839C +:100D70003DC0803B39F0E0F4803A09F479C0883A0C +:100D800009F09CCF84E08093DD01109286011092DF +:100D90008501E091AA01F091AB010995809185014F +:100DA00081110FC081E0809385011092650109C017 +:100DB000803C09F4A6CF883C09F4A3CF883B09F016 +:100DC0007DCFE091860181E08E0F80938601F0E077 +:100DD000EB59FE4F80818093BB00909186018091FA +:100DE000850129C08091B8018093BB0085EC809378 +:100DF000BC0064CF9091DA018091D901981758F521 +:100E0000E091DA0181E08E0F8093DA01F0E0E7549F +:100E1000FE4F8081E9CFE091DA0181E08E0F80936F +:100E2000DA018091BB00F0E0E754FE4F808390919F +:100E3000DA018091D9019817C8F285E8D8CFE091FE +:100E4000DA0181E08E0F8093DA018091BB00F0E03F +:100E5000E754FE4F80838091DC0181115CCF81E0FB +:100E60008093B70184EA8093BC001092DD0126CF05 +:100E700085EC8093BC001092DD018091A701803247 +:100E800030F4E091A701F0E0E957FE4F1082609145 +:100E9000A70170E0E091A801F091A90187E891E035 +:100EA00009951092A7010ACF1092DB0134CFE7E039 +:100EB000F2E01382128288EE93E0A0E0B0E0848337 +:100EC0009583A683B78324E031E03183208325EC2A +:100ED00030E03587248724EC30E03787268720EC04 +:100EE00030E0318B208B21EC30E0338B228B22ECF5 +:100EF00030E0358B248B26EC30E0378B268B118E3F +:100F0000128E138E148EE4EAF2E01382128284832E +:100F10009583A683B78386E191E0918380830895CA +:100F2000CF93DF9300D000D000D0CDB7DEB7789458 +:100F300084B5826084BD84B5816084BD85B58260DE +:100F400085BD85B5816085BD80916E00816080938F +:100F50006E001092810080918100826080938100F8 +:100F60008091810081608093810080918000816008 +:100F7000809380008091B10084608093B100809163 +:100F8000B00081608093B00080917A00846080938B +:100F90007A0080917A00826080937A0080917A0052 +:100FA000816080937A0080917A00806880937A00D3 +:100FB0001092C100E0911702F091180282E0808344 +:100FC000E0911302F09114021082E0911502F09169 +:100FD000160280E1808310921F02E0911B02F091C3 +:100FE0001C0286E08083E0911902F0911A02808150 +:100FF00080618083E0911902F0911A0280818860FB +:101000008083E0911902F0911A02808180688083C8 +:10101000E0911902F0911A0280818F7D80831092F5 +:10102000AD011092AC011092E401109206020E94F0 +:10103000290283EF90E09093AB018093AA0184EFA3 +:1010400090E09093A9018093A80141E0BE016F5FF9 +:101050007F4F85E70E946204482FBE016F5F7F4F7C +:1010600087E291E00E94320446E0BE016F5F7F4F4D +:101070008BE30E946204482FBE016F5F7F4F8CE2BA +:1010800091E00E94320411E01093E30188E680931E +:1010900005021092E401109206026BE684EA92E0E7 +:1010A0000E94AF036AE584EA92E00E94AF0381E008 +:1010B0000E94680241E0BE016F5F7F4F8BE60E9495 +:1010C0006204482FBE016F5F7F4F81E391E00E9471 +:1010D00032041093E30180E5809305021092E4014D +:1010E0001092060287E391E00E94E10381E00E94F2 +:1010F0006802682F70E090E080E04AE00E94EE0312 +:1011000084E291E00E94E1038FE391E00E94E10319 +:1011100084E291E00E94E10300E010E00115110576 +:10112000E9F30E945F018823C9F30E940000F6CF13 +:10113000A1E21A2EAA1BBB1BFD010DC0AA1FBB1FDB +:10114000EE1FFF1FA217B307E407F50720F0A21B4D +:10115000B30BE40BF50B661F771F881F991F1A94BA +:1011600069F760957095809590959B01AC01BD01E4 +:10117000CF010895EE0FFF1F0590F491E02D099423 +:04118000F894FFCF11 +:10118400000000009501F5002201E2015301310144 +:10119400450100000000AF038203DE0358037B0314 +:1011A400680359030D0A0057484F3D004143433D2E +:1011B400004241434B3D00414253454E543D00449F +:0411C4004F4E450045 +:00000001FF diff --git a/packages/pinbench_sim/test/fixtures/i2c_read/i2c_read.ino b/packages/pinbench_sim/test/fixtures/i2c_read/i2c_read.ino new file mode 100644 index 0000000..f863eb5 --- /dev/null +++ b/packages/pinbench_sim/test/fixtures/i2c_read/i2c_read.ino @@ -0,0 +1,58 @@ +// Fixture for i2c_read_test.dart: reads a served I2C device the way real +// sensor libraries do, and reports each result over Serial. +// +// Rebuild after editing (from this folder): +// arduino-cli compile --fqbn arduino:avr:uno --output-dir . i2c_read.ino +// mv i2c_read.ino.hex i2c_read.hex && rm -f i2c_read.ino.* + +#include + +const uint8_t DEVICE = 0x68; // served by the test +const uint8_t ABSENT = 0x50; // nothing on the bus + +// Write the register pointer, then read [count] bytes after a repeated start. +uint8_t readRegisters(uint8_t reg, uint8_t *out, uint8_t count) { + Wire.beginTransmission(DEVICE); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return 0; + uint8_t got = Wire.requestFrom(DEVICE, count); + for (uint8_t i = 0; i < got; i++) out[i] = Wire.read(); + return got; +} + +void printHex(const char *label, const uint8_t *bytes, uint8_t count) { + Serial.print(label); + for (uint8_t i = 0; i < count; i++) { + if (bytes[i] < 0x10) Serial.print('0'); + Serial.print(bytes[i], HEX); + } + Serial.println(); +} + +void setup() { + Serial.begin(115200); + Wire.begin(); + uint8_t buf[6]; + + // One register, like a WHO_AM_I check. + printHex("WHO=", buf, readRegisters(0x75, buf, 1)); + + // Six consecutive registers in one read, like an accelerometer sample. + printHex("ACC=", buf, readRegisters(0x3B, buf, 6)); + + // Write a register, then read it back, like setting an RTC. + Wire.beginTransmission(DEVICE); + Wire.write(0x6B); + Wire.write(0x5A); + Wire.endTransmission(); + printHex("BACK=", buf, readRegisters(0x6B, buf, 1)); + + // An address with nothing behind it is not acknowledged. + Wire.beginTransmission(ABSENT); + Serial.print("ABSENT="); + Serial.println(Wire.endTransmission()); + + Serial.println("DONE"); +} + +void loop() {} diff --git a/packages/pinbench_sim/test/fixtures/sensors/sensors.hex b/packages/pinbench_sim/test/fixtures/sensors/sensors.hex new file mode 100644 index 0000000..1d6a913 --- /dev/null +++ b/packages/pinbench_sim/test/fixtures/sensors/sensors.hex @@ -0,0 +1,1009 @@ +:100000000C943B010C9463010C9463010C94630108 +:100010000C9463010C9463010C9463010C946301D0 +:100020000C9463010C9463010C9463010C946301C0 +:100030000C9463010C9463010C9463010C946301B0 +:100040000C945F130C9463010C94CF130C94A913BC +:100050000C9463010C9463010C9463010C94630190 +:100060000C9401140C9463015B4248313735305DC8 +:10007000204552524F523A20756E646566696E652E +:1000800064206572726F72005B4248313735305DB3 +:10009000204552524F523A206F7468657220657243 +:1000A000726F72005B4248313735305D20455252E5 +:1000B0004F523A207265636569766564204E41430C +:1000C0004B206F6E207472616E736D6974206F6661 +:1000D0002064617461005B4248313735305D2045F2 +:1000E00052524F523A207265636569766564204EBC +:1000F00041434B206F6E207472616E736D69742082 +:100100006F662061646472657373005B42483137C7 +:1001100035305D204552524F523A20746F6F206C3B +:100120006F6E6720666F72207472616E736D697492 +:1001300020627566666572005B4248313735305D16 +:10014000204552524F523A20756E646566696E655D +:1001500064206572726F72005B4248313735305DE2 +:10016000204552524F523A206F7468657220657272 +:10017000726F72005B4248313735305D2045525214 +:100180004F523A207265636569766564204E41433B +:100190004B206F6E207472616E736D6974206F6690 +:1001A0002064617461005B4248313735305D204521 +:1001B00052524F523A207265636569766564204EEB +:1001C00041434B206F6E207472616E736D697420B1 +:1001D0006F662061646472657373005B42483137F7 +:1001E00035305D204552524F523A20746F6F206C6B +:1001F0006F6E6720666F72207472616E736D6974C2 +:1002000020627566666572005B4248313735305D45 +:1002100020446576696365206973206E6F7420637E +:100220006F6E6669677572656421000000000025C5 +:100230000028002B00040404040404040402020245 +:100240000202020303030303030102040810204017 +:1002500080010204081020010204081020000000A0 +:100260000800020100000304070000000000000075 +:1002700000002015111B11241FBECFEFD8E0DEBFF8 +:10028000CDBF23E0AEEEB1E001C01D92AA3BB207A4 +:10029000E1F711E0A0E0B1E0ECE0FEE302C0059080 +:1002A0000D92AE3EB107D9F711E0CAE3D1E004C028 +:1002B0002197FE010E94BF1DC933D107C9F70E94D3 +:1002C00086150C94F91E0C9400000895089590E092 +:1002D000FC01E35AFD4F2491FC01E75BFD4F349193 +:1002E000FC01EB5CFD4FE491EE23C9F0222339F0D1 +:1002F000233001F1A8F4213019F1223029F1F0E086 +:10030000EE0FFF1FE55DFD4FA591B4918FB7F894F7 +:10031000EC91611126C030953E233C938FBF089528 +:100320002730A9F02830C9F0243049F780918000A7 +:100330008F7D03C0809180008F7780938000DFCF16 +:1003400084B58F7784BDDBCF84B58F7DFBCF809163 +:10035000B0008F778093B000D2CF8091B0008F7DB6 +:10036000F9CF3E2BDACF8E50806480937C00809151 +:100370007A00806480937A0080917A0086FDFCCFB9 +:10038000809178009091790008958230910538F03D +:10039000880F991F880F991F05970197F1F7089506 +:1003A0003FB7F8948091370290913802A0913902BA +:1003B000B0913A0226B5A89B05C02F3F19F00196CF +:1003C000A11DB11D3FBFBA2FA92F982F8827BC01AF +:1003D000CD01620F711D811D911D42E0660F771FD7 +:1003E000881F991F4A95D1F708958F929F92AF92D7 +:1003F000BF92CF92DF92EF92FF924B015C010E947D +:10040000D0016B017C010E94D0016C197D098E091D +:100410009F09683E734081059105A8F321E0821A87 +:100420009108A108B10888EEC80E83E0D81EE11C2F +:10043000F11C81149104A104B10429F7FF90EF90FD +:10044000DF90CF90BF90AF909F908F9008952FB77F +:10045000F89460913302709134028091350290914A +:1004600036022FBF0895AF92BF92CF92DF92EF92E4 +:10047000FF920F931F93CF93DF936C017B018B014E +:10048000040F151FEB015E01AE18BF08C017D1079E +:1004900059F06991D601ED91FC910190F081E02D28 +:1004A000C6010995892B79F7C501DF91CF911F917D +:1004B0000F91FF90EF90DF90CF90BF90AF90089595 +:1004C000FC01538D448D252F30E0842F90E0821B5A +:1004D000930B541710F0CF96089501970895FC01DF +:1004E000918D828D981761F0A28DAE0FBF2FB11D37 +:1004F0005D968C91928D9F5F9F73928F90E008958F +:100500008FEF9FEF0895FC01918D828D981731F048 +:10051000828DE80FF11D858D90E008958FEF9FEF9C +:100520000895FC01918D228D892F90E0805C9F4F72 +:10053000821B91098F739927089583EB92E00E94A3 +:10054000910221E0892B09F420E0822F089580E0B8 +:1005500090E0892B29F00E949D0281110C940000EB +:100560000895FC01A48DA80FB92FB11DA35ABF4F48 +:100570002C91848D90E001968F739927848FA689A2 +:10058000B7892C93A089B1898C91837080648C93F6 +:10059000938D848D981306C00288F389E02D8081A5 +:1005A0008F7D80830895EF92FF920F931F93CF93D7 +:1005B000DF93EC0181E0888F9B8D8C8D98131AC09E +:1005C000E889F989808185FF15C09FB7F894EE8985 +:1005D000FF896083E889F9898081837080648083E2 +:1005E0009FBF81E090E0DF91CF911F910F91FF902D +:1005F000EF900895F62E0B8D10E00F5F1F4F0F73D5 +:100600001127E02E8C8D8E110CC00FB607FCFACF8F +:10061000E889F989808185FFF5CFCE010E94B1027A +:10062000F1CFEB8DEC0FFD2FF11DE35AFF4FF08260 +:100630009FB7F8940B8FEA89FB8980818062CFCFC6 +:10064000682F83EB92E00C94D302CF93DF93EC01FD +:10065000888D8823B9F0AA89BB89E889F9898C91AA +:1006600085FD03C0808186FD0DC00FB607FCF7CF66 +:100670008C9185FFF2CF808185FFEDCFCE010E9466 +:10068000B102E9CFDF91CF9108950895DC01ED919A +:10069000FC910190F081E02D0994FC0183858F3F4E +:1006A00019F061E00C9467010895FC01808191814B +:1006B000892B39F0A281B3819C9111968C919CBDBC +:1006C0008DBD83858F3F19F060E00C94670108951C +:1006D000BF92CF92DF92EF92FF920F931F93CF932F +:1006E000DF93EC0188819981892B81F06EBD0DB477 +:1006F00007FEFDCF0EB5802FDF91CF911F910F9197 +:10070000FF90EF90DF90CF90BF90089588859985F6 +:1007100011E0892B09F010E8BB24B394862F8123C4 +:1007200009F0B12CF62E2C813D814E815F8160E273 +:1007300071EA87E090E00E949D1DE22E00E0C22E4B +:10074000D12CEE2019F0C6010E94C5018A85982F90 +:10075000977F09F044C08D858F3F89F09F2D9123AD +:1007600081E009F480E08B1551F0EF85F8898F89DD +:10077000992379F19081892B8083BB24B394E989F3 +:10078000FA898081998D892B8083EE2019F0C6012A +:100790000E94C5018E858F3F39F0EB89FC898081ED +:1007A000988D892309F0012BE989FA899081898D37 +:1007B00080958923808388859985892B09F067C076 +:1007C000110F1111BECF8E858F3F09F094CF0F2DE1 +:1007D00092CF9081809589238083B12CD0CF8C30AB +:1007E00099F58D858F3F49F0EF85F8898F899F2D89 +:1007F000912331F19081892B8083E989FA89908155 +:10080000898D809589238083EE2019F0C6010E948E +:10081000C501E989FA898081998D892B8083EE2031 +:1008200019F0C6010E94C5018E858F3F21F2EB8928 +:10083000FC898081988D892309F4BDCF012BBBCF22 +:10084000908180958923D8CFE989FA898081998D13 +:10085000892B8083EE2019F0C6010E94C5018D8589 +:100860008F3F49F0EF85F8898F899F2D912359F0AB +:100870009081892B8083E989FA899081898D80957F +:1008800089238083D1CF908180958923F3CF1695DA +:1008900098CF8F929F92AF92BF92CF92DF92EF92BA +:1008A000FF920F931F93CF93DF937C014B016A015B +:1008B000B22EA32E0E945503E401C80CD91CCC15FE +:1008C000DD0569F4CB2DDA2D0C0F1D1FC017D107E4 +:1008D00059F06FEFC7010E9468038993F7CF6991C0 +:1008E000C7010E946803EBCFC7010E944D0381E05E +:1008F000DF91CF911F910F91FF90EF90DF90CF90FC +:10090000BF90AF909F908F9008958F929F92AF92DB +:10091000BF92CF92DF92EF92FF920F931F93CF93EC +:10092000DF937C01B62EA72E6A0149010E94550370 +:10093000E401080D191DC017D10761F4CB2DDA2D84 +:10094000CC0EDD1ECC15DD0551F06991C7010E946A +:100950006803F8CF6991C7010E946803ECCFC70113 +:100960000E944D0381E0DF91CF911F910F91FF9085 +:10097000EF90DF90CF90BF90AF909F908F900895B1 +:10098000089520910F02260F3327331F213231059E +:10099000ECF42091AD02FC0190E080E0243069F09D +:1009A00082E00895A0910F022191AC0141515E4F68 +:1009B000A40FB52FB11D2C930196861798F3809143 +:1009C0000F02680F60930F0280E0089581E00895A0 +:1009D0001092AD0281E080938C021092870261E058 +:1009E00082E10E94670161E083E10E946701E9EB17 +:1009F000F0E080818E7F808380818D7F808388E49A +:100A00008093B80085E48093BC0008951092420260 +:100A10001092410210924402109266020E94E80471 +:100A200085E691E09093400280933F0286E691E054 +:100A300090933E0280933D020895CF93DF9391E01F +:100A40009093EE018823B9F0C091B800D091BA001C +:100A50008091BC008A7B8093BC0060E082E10E94B0 +:100A6000670160E083E10E9467010E94E804D0937F +:100A7000BA00C093B800DF91CF9108958F929F92F2 +:100A8000AF92BF92CF92DF92EF92FF920F931F939C +:100A9000CF93DF93D82FC62F142F613208F09CC05C +:100AA0000E94D0016B017C018091AD0281116FC069 +:100AB00081E08093AD0210938C029FEF90938B02A4 +:100AC00010928A029C0F90938902809388028091F1 +:100AD0008802DD0FD82BD093880280918702813065 +:100AE00009F07CC0109287020E94D0016B017C014A +:100AF000809188028093BB008091AF029091B002F8 +:100B0000A091B102B091B202892B8A2B8B2BA1F05C +:100B10000E94D0010091AF021091B0022091B10269 +:100B20003091B2026C197D098E099F0906171707CB +:100B30002807390708F44CC08091BC0083FDD8CF4A +:100B400085EC8093BC000E94D0016B017C018091F8 +:100B5000AD02813009F444C080918A028C1710F4F0 +:100B6000C0918A02E7E6F2E0ADE8B2E080E08C13E3 +:100B700059C08C2FDF91CF911F910F91FF90EF9073 +:100B8000DF90CF90BF90AF909F908F90089580910D +:100B9000AF029091B002A091B102B091B202892B44 +:100BA0008A2B8B2B09F480CF0E94D0018090AF025A +:100BB0009090B002A090B102B090B2026C197D0981 +:100BC0008E099F0986169706A806B90608F06CCF0D +:100BD0008091AE020E941D05C0E0CBCF85EEB1CF63 +:100BE0008091AF029091B002A091B102B091B20297 +:100BF000892B8A2B8B2B09F4AACF0E94D00100915C +:100C0000AF021091B0022091B1023091B2026C1982 +:100C10007D098E099F09061717072807390708F06D +:100C200096CFD6CF91919D938F5FA1CF8F929F92B8 +:100C3000AF92BF92CF92DF92EF92FF920F931F93EA +:100C4000CF93DF93D0916602D13208F0D7C0182F2E +:100C5000C09165020E94D0016B017C018091AD02C0 +:100C600081116BC082E08093AD0210938C028FEFF4 +:100C700080938B0210928A02D0938902A5E4B2E09D +:100C8000E7E6F2E080E0D81391C0109288028091EC +:100C90008802CC0FC82BC0938802809187028130D4 +:100CA00009F088C0109287020E94D0016B017C017C +:100CB000809188028093BB008091AF029091B00236 +:100CC000A091B102B091B202892B8A2B8B2BA1F09B +:100CD0000E94D0010091AF021091B0022091B102A8 +:100CE0003091B2026C197D098E099F09061717070A +:100CF0002807390708F442C08091BC0083FDD8CF93 +:100D000085EC8093BC000E94D0016B017C01809136 +:100D1000AD02823009F450C080918B028F3F09F4FC +:100D20006FC080918B02803209F46CC080918B027D +:100D3000803309F469C084E026C08091AF029091AD +:100D4000B002A091B102B091B202892B8A2B8B2BF9 +:100D500009F484CF0E94D0018090AF029090B0023D +:100D6000A090B102B090B2026C197D098E099F0962 +:100D700086169706A806B90608F070CF8091AE02D5 +:100D80000E941D0585E010924402109266021092A6 +:100D90004302DF91CF911F910F91FF90EF90DF9071 +:100DA000CF90BF90AF909F908F9008959D91919319 +:100DB0008F5F69CF85EEA5CF8091AF029091B00291 +:100DC000A091B102B091B202892B8A2B8B2B09F42E +:100DD0009ECF0E94D0010091AF021091B0022091ED +:100DE000B1023091B2026C197D098E099F09061774 +:100DF00017072807390708F08ACFC0CF81E0C3CF93 +:100E000080E0C1CF82E0BFCF83E0BDCF8F929F92C1 +:100E1000BF92CF92DF92EF92FF920F931F93CF93E7 +:100E2000DF93DA01AE0DBF1DEC01EC81FD81EA1705 +:100E3000FB0710F490E026C0B22E6A014B018981B5 +:100E40009A81288131E03093430220936502109209 +:100E5000440210926602E114F10409F589819A8135 +:100E6000DC01ED91FC910280F381E02DA601B4013B +:100E70000995C816D906F1F68B2D0E94160691E049 +:100E80008111D8CF892FDF91CF911F910F91FF90C2 +:100E9000EF90DF90CF90BF909F908F9008950115B5 +:100EA0001105E1F2DC01ED91FC910280F381E02D6E +:100EB000A701B80109958E159F0509F0BBCFCECFCC +:100EC000CF92DF92EF92FF920F931F93CF93DF9316 +:100ED00000D0CDB7DEB7DC01211103C01A962C91EA +:100EE0001A97253008F092C051964D935D936D93FB +:100EF0007C93549790E080E0C22ED12C8C159D05F8 +:100F0000B4F41C962C911C9721110AC0FD01E80F26 +:100F1000F91F4587452F562F672F77270196EECF6C +:100F2000F601E81BF90BEA0FFB1F4487F3CFBD0165 +:100F3000635F7F4F18969C91189719964C9119975B +:100F400099834A83ED91FC9111973097B1F01B96EC +:100F5000EC90F12C8E010F5F1F4F21E0A601CF0115 +:100F60000E9406070F900F90DF91CF911F910F9174 +:100F7000FF90EF90DF90CF9008951296ED91FC9145 +:100F80001397309741F116962D913C9117971B9628 +:100F90000C9110E02330310569F44E7F49839A8328 +:100FA0000F5F1F4F9E012F5F3F4FA601CF010E9491 +:100FB0008504D8CF2115310519F49F779983F2CF95 +:100FC0002230310511F49068F9CF2130310551F705 +:100FD0009F779064F3CF1496ED91FC91159730971D +:100FE000A9F08085882309F4BDCFE680F780E1145D +:100FF000F10461F086019B011B964C91BE016F5F6D +:101000007F4F81859285F7010995ACCF80E0AACF0B +:10101000CF93DF93EC018B818823A1F0888191E04D +:1010200090934302809365021092440210926602EC +:1010300081E00E94160691E0811190E0892FDF91F6 +:10104000CF9108950E94060581E08B83CE010E9416 +:101050000808982F8111E2CFF1CF0895E091420264 +:1010600080914102E81730F4F0E0E357FD4F8081B2 +:1010700090E008958FEF9FEF089590914202809144 +:1010800041022FEF3FEF981748F4E92FF0E0E357C4 +:10109000FD4F208130E09F5F90934202C901089587 +:1010A0008091410290914202891B990B0895CF9241 +:1010B000DF92EF92FF920F931F93CF93DF937C0108 +:1010C000CB018A0120914302222389F0EB016B01BD +:1010D000C40ED51ECC15DD0569F06991D701ED91DF +:1010E000FC910190F081E02DC7010995F3CF642FA9 +:1010F0000E94C104C801DF91CF911F910F91FF9011 +:10110000EF90DF90CF900895CF93DF931F92CDB7EC +:10111000DEB76983209143022223F9F02091660211 +:10112000203258F021E030E0FC013383228390E04C +:1011300080E00F90DF91CF91089580914402E82FD5 +:10114000F0E0EB5BFD4F998190838F5F80934402C9 +:101150008093660281E090E0ECCF61E0CE010196E1 +:101160000E94C104F7CF90E080E008956F927F92D3 +:101170008F929F92AF92BF92CF92DF92EF92FF92A7 +:101180000F931F93CF93DF937C013B016A0110E023 +:1011900000E00C151D05F0F5D70114968D919C917A +:1011A000E601C01BD10B8C179D0708F4EC0141E050 +:1011B000C6018C1B9D0B0817190708F440E06C2F23 +:1011C000C13208F060E2F70180810E943E05109272 +:1011D0004202809341028C171D0661F54301800E87 +:1011E000911E5401C501881999098C179D0778F43F +:1011F000D70111968D919C91DC01ED91FC910284B7 +:10120000F385E02D0995F50181935F01EBCF0C0F7C +:101210001D1FBFCF81E0DF91CF911F910F91FF90F4 +:10122000EF90DF90CF90BF90AF909F908F907F9086 +:101230006F90089580E0EFCFEF92FF920F931F938E +:10124000CF93DF9300D0CDB7DEB71982F12CE12C1C +:1012500010E000E021E041E050E0BE016F5F7F4F11 +:1012600080913B0290913C020E94060741E050E0D1 +:10127000BE016E5F7F4F80913B0290913C020E94C5 +:10128000B6088A8190E0880F892F881F990B21E08A +:1012900082270F900F90DF91CF911F910F91FF90B8 +:1012A000EF9008958F929F92BF92CF92DF92EF922C +:1012B000FF920F931F93CF93DF9300D0CDB7DEB78C +:1012C000DC016B01B42E18969C91189719968C919D +:1012D000199799838A838D909C901197811491041A +:1012E00031F11B964C9150E0F12CE12C10E000E024 +:1012F00020E0BE016F5F7F4FC4010E94060781118D +:101300000FC080E00F900F90DF91CF911F910F9150 +:10131000FF90EF90DF90CF90BF909F908F900895B7 +:101320004B2D50E0B601C4010E94B608EBCF1296D7 +:10133000ED91FC911397309749F116962D913C91C0 +:101340001797042F10E01B964C9150E02330310585 +:1013500069F4816089839A834F5F5F4F9601BE0174 +:101360006F5F7F4FCF010E944904CCCF211531051B +:1013700019F490689983F2CF2230310511F49F77E8 +:10138000F9CF2130310551F7906CF4CF1496ED91DF +:10139000FC911597309709F4B4CF8085882309F420 +:1013A000B0CF848095808114910409F4AACF042FD2 +:1013B00010E09B011B964C91BE016F5F7F4F8185B2 +:1013C0009285F40109959ECFCF93DF93EC01BC0188 +:1013D000635F7F4F4A850E945209882301F14A8545 +:1013E00050E030E020E060E070E0CB0124173507EA +:1013F000CCF4982F872F762F6627EC85E1110AC051 +:10140000FA01E21BF30BEC0FFD1FE4856E2B2F5F3F +:101410003F4FECCFFE01E20FF31FE585F7CF6FEFF3 +:101420007FEFCB01DF91CF9108958F929F92AF9282 +:10143000BF92CF92DF92EF92FF920F931F93CF93C1 +:10144000DF93EC014A015B01888199810E94E409E4 +:101450002A8101E010E002C0000F111F2A95E2F777 +:1014600001501109012E000C220B330BAB816801D6 +:1014700079010A2E04C0CC0CDD1CEE1CFF1C0A9462 +:10148000D2F7C094D094E094F094C622D722E822F8 +:10149000F922E881F981B901A801482159216A217D +:1014A0007B2104C0440F551F661F771FAA95D2F7F2 +:1014B0004C295D296E297F292285CF01DF91CF91AB +:1014C0001F910F91FF90EF90DF90CF90BF90AF9062 +:1014D0009F908F900C9460070F931F93CF93DF938F +:1014E000EC01888199810E94E409AB01BC012A8149 +:1014F00081E090E002C0880F991F2A95E2F70197DA +:10150000092E000CAA0BBB0B2B8104C076956795A6 +:10151000579547952A95D2F78C019D0104231523F1 +:1015200026233723C901B801DF91CF911F910F9175 +:101530000895AF92BF92CF92DF92EF92FF920F93F6 +:101540001F93CF93DF93CDB7DEB7EC970FB6F89428 +:10155000DEBF0FBECDBF8C01FC01868D978D1FA213 +:1015600018A619A61AA69F8B8E8B198E188EFF248B +:10157000F394F9A28BE390E09F8F8E8F1AA28EE0F6 +:1015800088A34EE0BE01655D7F4FCE0146960E9466 +:1015900052098BA59CA5982789279827F80195A71C +:1015A00084A78DA59EA598278927982797A786A702 +:1015B0008FA598A998278927982791AB80AB69A90F +:1015C0007AA976276727762773AB62AB8BA99CA98C +:1015D00098278927982795AB84AB8DA99EA9982732 +:1015E0008927982797AB86AB8FA998AD982789272D +:1015F000982791AF80AF072E000C880B990B0E94A3 +:10160000681B20E030E04AEA53E40E94F21C28EB19 +:101610003EE142E152E40E94861CF801628373833A +:1016200084839583868D978D1A8A1B8A1C8A1D8ACE +:101630009A8389831C821B82FC868CE190E09A87C6 +:1016400089871D86FB86CE0101965C019AAF89AF22 +:1016500082E08BAF83E08CAFCE01C9960E946C0A0A +:10166000C12CD12CE12CE5E4FE2E63307105E1F0B4 +:101670006230710599F06130710509F4D6C0C12C52 +:10168000D12CF0E8EF2EFFE3FF2E672B69F4C12C7D +:10169000D12C50E8E52E56E4F52E06C0C12CD12CF5 +:1016A00070E8E72E75E4F72EF80164A575A5072EFE +:1016B000000C880B990B0E94681BA70196010E94E1 +:1016C000F21CF801668377838087918766A577A5EA +:1016D000072E000C880B990B0E94681BA70196012E +:1016E0000E94F21CF801628773878487958760A93E +:1016F00071A9072E000C880B990B0E94681BA7018B +:1017000096010E94F21CF80166877787808B918B87 +:10171000868D978D1A8A1B8A1C8A1D8A9A838983D3 +:101720001C821B8281E08C872BE130E03A8729877D +:101730001D868B87BAAEA9AE82E08BAF83E08CAFFB +:10174000CE01C9960E946C0AC12CD12C33E0E32E45 +:1017500033E4F32E6115710519F1C12CD12C23E866 +:10176000E22E22E4F22E61307105D1F093E3C92E0E +:10177000DC2C93E0E92E92E4F92E6230710581F0C1 +:10178000C12CD12C40E8E42E4FE3F42E63307105D8 +:1017900039F483E3C82EDC2C83E8E82E81E4F82EAC +:1017A000F80164A975A9072E000C880B990B0E94FB +:1017B000681BA70196010E94F21CF801628B738BD3 +:1017C000848B958B66A977A9072E000C880B990B43 +:1017D0000E94681BA70196010E94F21CF801668B0B +:1017E000778B808F918F60AD71AD072E000C880BC9 +:1017F000990B0E94681BA70196010E94F21CF80138 +:10180000628F738F848F958FEC960FB6F894DEBF3E +:101810000FBECDBFDF91CF911F910F91FF90EF9041 +:10182000DF90CF90BF90AF900895C12CD12CE12CC8 +:1018300066E4F62E39CFCF93DF931F92CDB7DEB794 +:1018400041E050E0BE016F5F7F4FFC0184859585CC +:101850000E94B608882329F089810F90DF91CF91EB +:1018600008958FEFFACFFC0101900020E9F731973E +:10187000AF01481B590BBC0183EB92E00C9433027F +:101880008F929F92AF92BF920F931F93CF93DF934C +:10189000CDB7DEB7A1970FB6F894DEBF0FBECDBFB0 +:1018A00019A2423008F44AE08E010F5D1F4F842ECA +:1018B000912CB12CA12CA50194010E949D1DE62F15 +:1018C000B901CA01EA30F4F4E05DD801EE938D016C +:1018D000232B242B252B79F790E080E0109719F02B +:1018E000CD010E94330CA1960FB6F894DEBF0FBE57 +:1018F000CDBFDF91CF911F910F91BF90AF909F907F +:101900008F900895E95CE1CFCF92DF92EF92FF9242 +:10191000CF93DF936C01990FEE08FF08F7FE1DC00F +:101920008DE20E942003EC0166277727CB016C191A +:101930007D098E099F094AE00E94400CC80FD91FFB +:1019400089E791E00E94330C8C0F9D1FDF91CF91AE +:10195000FF90EF90DF90CF9008954AE0C701B60165 +:101960000E94400CEC01ECCF0C94330C4F925F9230 +:101970006F927F928F929F92AF92BF92CF92DF929F +:10198000EF92FF921F93CF93DF936B017C01142F93 +:10199000AC019B010E94981D8823D9F08CE791E04F +:1019A0000E94330CEC0189E791E00E94330C8C0F0C +:1019B0009D1FDF91CF911F91FF90EF90DF90CF900F +:1019C000BF90AF909F908F907F906F905F904F905F +:1019D000089546015701E894B7F82FEF3FEF4FE71E +:1019E0005FE7C501B4010E94981D81110DC02FEF62 +:1019F0003FEF4FE75FE7C501B4010E94611B181676 +:101A00001CF480E891E0CCCF2FEF3FEF4FE75FE48D +:101A1000C701B6010E94931D18161CF484E891E0DA +:101A2000BFCF2FEF3FEF4FE75FECC701B6010E943A +:101A3000611B87FDF3CF20E030E0A901C701B601AB +:101A40000E94611BD0E0C0E087FF08C08DE20E94C9 +:101A50002003EC01F7FAF094F7F8F09420E060E04E +:101A600070E080E09FE3211740F420E030E040E2A6 +:101A700051E40E94F21C21E0F6CFA70196010E94DA +:101A8000861C2B013C010E94641D4B015C014AE055 +:101A90000E94400CC80FD91F112309F484CF8EE295 +:101AA0000E9420037C01C501B4010E94661B9B01BA +:101AB000AC01C301B2010E94851C20E030E040E28D +:101AC00051E40E94C71B0E94641D90E080E04AE040 +:101AD0000E94400C8E0D9F1DC80FD91F64CFCF935D +:101AE000DF93682F70E090E080E04AE00E94400CB5 +:101AF000EC0189E791E00E94330C8C0F9D1FDF9170 +:101B0000CF9108950F931F93CF93DF938C01D0E073 +:101B1000C0E0F801EC0FFD1F6491662341F083EBF8 +:101B200092E00E94D302892B11F02196F2CF89E72F +:101B300091E00E94330C8C0F9D1FDF91CF911F917C +:101B40000F9108950C94611E0C94611E0C94611EFB +:101B50000C94611E0C94611E9F92AF92BF92CF9223 +:101B6000DF92EF92FF920F931F93CF93DF93CDB746 +:101B7000DEB7AE970FB6F894DEBF0FBECDBF8C01B7 +:101B8000DC0197965C934E939697CA0101969996BD +:101B90009C938E9398974E5F5F4F9B965C934E936A +:101BA0009A975E962D913C911FA218A619A61AA687 +:101BB0003F8B2E8B198E188E81E089A34BE650E067 +:101BC0005F8F4E8F1AA288A31A8A1B8A1C8A1D8ACD +:101BD0003A8329831C821B828C8728E630E03A876F +:101BE00029871D868B875E01B6E1AB0EB11CBCA6B2 +:101BF000ABA68DA787E08EA741E050E060E070E0E3 +:101C0000CE018B960E94150ACE018B960E946C0A1B +:101C1000613071058105910539F461E070E080E083 +:101C200090E00E94F501F0CF64E670E080E090E083 +:101C30000E94F50120E047E050E060E070E0FE0126 +:101C400031966F01CF010E94600764E670E080E08A +:101C500090E00E94F501D8015E968D919C911A8AC0 +:101C60001B8A1C8A1D8A9A8389831C821B82FF24FB +:101C7000F394FC8689E190E09A8789871D86FB862C +:101C800020E040E050E0BA01C6010E946007F80180 +:101C9000868D978D1A8A1B8A1C8A1D8A9A8389834E +:101CA0001C821B82FC868AE190E09A8789871D86C8 +:101CB000FB86DF8ACE8A83E0E82EE88E198E40E02C +:101CC00050E0BA01C5010E94150AD8015E968D91B7 +:101CD0009C911A8A1B8A1C8A1D8A9A8389831C827A +:101CE0001B82FC868BE190E09A8789871D86FB86A4 +:101CF000DF8ACE8A92E0992E988EE98E41E050E0FC +:101D000060E070E0C5010E94150AF801868D978D8C +:101D10001A8A1B8A1C8A1D8A9A8389831C821B82C9 +:101D2000FC868CE190E09A8789871D86FB86DF8A96 +:101D3000CE8A988EE98E40E050E0BA01C5010E943B +:101D4000150AD8015E968D919C911A8A1B8A1C8A6D +:101D50001D8A9A8389831C821B82FC868BE690E015 +:101D60009A8789871D86FB8620E041E050E060E08D +:101D700070E0C6010E94600764E670E080E090E0D9 +:101D80000E94F501F80180A191A1009739F0DC01D2 +:101D9000ED91FC910280F381E02D0995F80182A17B +:101DA00093A1009739F0DC01ED91FC910280F38161 +:101DB000E02D0995F80184A195A1009739F0DC0187 +:101DC000ED91FC910280F381E02D099586E090E091 +:101DD0000E94C51DFC0189E291E09183808380E52A +:101DE00096E09383828315830483D8019196FC93B4 +:101DF000EE93909786E090E00E94C51DFC018BE178 +:101E000091E09183808381E596E0938382831583BB +:101E10000483D8019396FC93EE93929786E090E02A +:101E20000E94C51DFC018DE091E09183808382E5D5 +:101E300096E09383828315830483D8019596FC935F +:101E4000EE93949781E0AE960FB6F894DEBF0FBE86 +:101E5000CDBFDF91CF911F910F91FF90EF90DF9059 +:101E6000CF90BF90AF909F9008954F925F926F92E6 +:101E70007F928F929F92AF92BF92CF92DF92EF921A +:101E8000FF920F931F93CF93DF93CDB7DEB72997C0 +:101E90000FB6F894DEBF0FBECDBF6C015B014A01E7 +:101EA0000E9427022B013C018091060190910701BD +:101EB000A09108018F839887A987F12CE12C10E06D +:101EC00000E021E043E050E0BE01695F7F4FD601B2 +:101ED0001C968D919C910E940607811119C010E0FB +:101EE000812F29960FB6F894DEBF0FBECDBFDF91CC +:101EF000CF911F910F91FF90EF90DF90CF90BF9007 +:101F0000AF909F908F907F906F905F904F900895CB +:101F1000C6010E941B0C87FF07C06AE070E080E0EA +:101F200090E00E94F501F4CF46E050E0BE016F5F03 +:101F30007F4FF601848595850E94B608182F882367 +:101F400071F26A817B81762767277627CB01B0E023 +:101F5000A0E09C01AD0184E0220F331F441F551FF8 +:101F60008A95D1F70C81802F90E064E09595879554 +:101F70006A95E1F7092E000CAA0BBB0BBC01CD0141 +:101F8000622B732B842B952B0E94661B20E030E084 +:101F900048EC52E40E94C71B20E030E040E855E3E3 +:101FA0000E94C71BD60114966D937D938D939C93CD +:101FB00017970F70602F70E090E080E0982F872FC8 +:101FC000762F66272D81622B982F872F762F6627F5 +:101FD0002E81622B0E94661B20E030E048E453E42F +:101FE0000E94C71B20E030E040E855E30E94C71B79 +:101FF00020E030E048E452E40E94851CF601608352 +:102000007183828393838114910481F1F401349666 +:1020100080E2DF011D928A95E9F784E290E0A0E07A +:10202000B0E0F40180839183A283B383D6011A9632 +:102030008D919C91B0E0A0E084839583A683B783C3 +:102040008DE090E0A0E0B0E080879187A287B38721 +:10205000408A518A628A738AF60180819181A281C5 +:10206000B381F401848B958BA68BB78BA114B1043B +:1020700009F436CFF501349680E2DF011D928A958E +:10208000E9F784E290E0A0E0B0E0F501808391837D +:10209000A283B383D60118968D919C91B0E0A0E005 +:1020A00084839583A683B7838DE090E0A0E0B0E0C1 +:1020B00080879187A287B387408A518A628A738A10 +:1020C000F60184819581A681B781F501848B958B7A +:1020D000A68BB78B05CFAB0170E060E0FC0184817B +:1020E00095810E94350F81E0089550E040E0FC01A9 +:1020F000848195810E94350F81E008950F931F938D +:10210000CF93DF938C01EB01DC0114968D919C91B0 +:102110000E94990AF801048115810E942702AB01EF +:10212000BC01FE01349680E2DF011D928A95E9F739 +:1021300084E290E0A0E0B0E088839983AA83BB8327 +:10214000F80182A593A5B0E0A0E08C839D83AE83C7 +:10215000BF838DE090E0A0E0B0E088879987AA87F0 +:10216000BB87488B598B6A8B7B8B82819381A4813F +:10217000B5818C8B9D8BAE8BBF8B81E0DF91CF9136 +:102180001F910F9108958F929F92AF92BF92CF921D +:10219000DF92EF92FF920F931F93CF93DF938C0107 +:1021A000EB01DC0114968D919C910E94990AF80133 +:1021B000048115810E942702FE01349620E2DF018E +:1021C0001D922A95E9F7C12CD12C7601C394C882BF +:1021D000D982EA82FB82F80126A137A14901B12CFC +:1021E000A12C8C829D82AE82BF82C886D986EA8667 +:1021F000FB86688B798B8A8B9B8B2AE038EE4CE1CF +:1022000051E466817781808591850E94C71B6C8B24 +:102210007D8B8E8B9F8B2AE038EE4CE151E4D80108 +:102220001A966D917D918D919C911D970E94C71B6F +:10223000688F798F8A8F9B8F2AE038EE4CE151E4CA +:10224000F80166857785808991890E94C71B6C8F0C +:102250007D8F8E8F9F8F81E0DF91CF911F910F91A6 +:10226000FF90EF90DF90CF90BF90AF909F908F90B6 +:1022700008950F931F93CF93DF938C01EB01DC0143 +:1022800014968D919C910E94990AF8010481158100 +:102290000E942702AB01BC01FE01349680E2DF01FF +:1022A0001D928A95E9F781E090E0A0E0B0E0888394 +:1022B0009983AA83BB83F80180A591A5B0E0A0E033 +:1022C0008C839D83AE83BF8384E090E0A0E0B0E088 +:1022D00088879987AA87BB87488B598B6A8B7B8BAA +:1022E00025E33AEF4EE85CE3628973898489958936 +:1022F0000E94C71B6C8B7D8B8E8B9F8B25E33AEFE7 +:102300004EE85CE3D80156966D917D918D919C913C +:1023100059970E94C71B688F798F8A8F9B8F25E3FF +:102320003AEF4EE85CE3F801628D738D848D958DF4 +:102330000E94C71B6C8F7D8F8E8F9F8F81E0DF91F6 +:10234000CF911F910F9108950F931F93CF93DF9318 +:102350008C01EB0188E2FB0111928A95E9F74BE0D1 +:1023600050E068E871E0CE010E94EA1E1B8681E021 +:1023700090E0A0E0B0E08C879D87AE87BF87F80132 +:1023800082819381092E000CAA0BBB0B888B998B41 +:10239000AA8BBB8B8DE090E0A0E0B0E08C8B9D8B96 +:1023A000AE8BBF8B1CA21DA21EA21FA280E090E0DC +:1023B000A0E2B2EC8C8F9D8FAE8FBF8F80E090E05B +:1023C000AAEAB2E4888F998FAA8FBB8F8AE999E92C +:1023D000A9E9BEE388A399A3AAA3BBA3DF91CF91E8 +:1023E0001F910F9108950F931F93CF93DF938C014B +:1023F000EB0188E2FB0111928A95E9F74BE050E08E +:1024000060E971E0CE010E94EA1E1B8681E090E047 +:10241000A0E0B0E08C879D87AE87BF87F8018281FE +:102420009381092E000CAA0BBB0B888B998BAA8B6E +:10243000BB8B8CE090E0A0E0B0E08C8B9D8BAE8BF2 +:10244000BF8B1CA21DA21EA21FA21C8E1D8E1E8E43 +:102450001F8E80E090E0A8ECB2E4888F998FAA8F5D +:10246000BB8F80E090E0A0E0B0E488A399A3AAA38A +:10247000BBA3DF91CF911F910F9108950F931F93ED +:10248000CF93DF938C01EB0188E2FB0111928A95D7 +:10249000E9F74BE050E068E971E0CE010E94EA1EE6 +:1024A0001B8681E090E0A0E0B0E08C879D87AE873E +:1024B000BF87F80182819381092E000CAA0BBB0B08 +:1024C000888B998BAA8BBB8B8DE090E0A0E0B0E06D +:1024D0008C8B9D8BAE8BBF8B1CA21DA21EA21FA23C +:1024E00080E090E0A0E2B2EC8C8F9D8FAE8FBF8F2A +:1024F00080E090E0A2EDB2E4888F998FAA8FBB8F25 +:1025000084E09DEAA0E4BBE388A399A3AAA3BBA3AC +:10251000DF91CF911F910F9108950F931F93CF9348 +:10252000DF938C01EB0188E2FB0111928A95E9F7B8 +:102530004BE050E062EA71E0CE010E94EA1E1B8689 +:1025400041E050E060E070E04C875D876E877F87F8 +:10255000F80182819381092E000CAA0BBB0B888B9A +:10256000998BAA8BBB8B4C8B5D8B6E8B7F8B1CA24C +:102570001DA21EA21FA28AE098EEACE1B3EC8C8FE4 +:102580009D8FAE8FBF8F8AE098EEACE1B3E4888F69 +:10259000998FAA8FBB8F83E29BEDA9E7BDE388A348 +:1025A00099A3AAA3BBA3DF91CF911F910F91089587 +:1025B0000F931F93CF93DF938C01EB0188E2FB0114 +:1025C00011928A95E9F74BE050E06CEA71E0CE0198 +:1025D0000E94EA1E1B8681E090E0A0E0B0E08C87BC +:1025E0009D87AE87BF87F80182819381092E000CF9 +:1025F000AA0BBB0B888B998BAA8BBB8B84E090E0DA +:10260000A0E0B0E08C8B9D8BAE8BBF8B1CA21DA27B +:102610001EA21FA287ED93EAABE0B2EC8C8F9D8FD8 +:10262000AE8FBF8F87ED93EAABE0B2E4888F998FCE +:10263000AA8FBB8F8AE99BEAABE0B9E388A399A391 +:10264000AAA3BBA3DF91CF911F910F91089585EDB0 +:102650008093BC008091AF029091B002A091B10232 +:10266000B091B2020796A11DB11D23E0B695A795C2 +:10267000979587952A95D1F72091BC0024FD03C03A +:102680001092AD0208954091AF025091B002609156 +:10269000B1027091B202452B462B472B69F300978C +:1026A000A105B10541F02AE22A95F1F700C0019792 +:1026B000A109B109E1CF8091AE020C941D051F92D2 +:1026C0000F920FB60F9211242F933F938F939F93E6 +:1026D000AF93BF938091330290913402A091350261 +:1026E000B09136023091320223E0230F2D3758F596 +:1026F0000196A11DB11D2093320280933302909365 +:102700003402A0933502B093360280913702909143 +:102710003802A0913902B0913A020196A11DB11D73 +:102720008093370290933802A0933902B0933A0213 +:10273000BF91AF919F918F913F912F910F900FBEBD +:102740000F901F90189526E8230F0296A11DB11D2A +:10275000D2CF1F920F920FB60F9211242F933F9357 +:102760004F935F936F937F938F939F93AF93BF9399 +:10277000EF93FF9383EB92E00E94B102FF91EF9100 +:10278000BF91AF919F918F917F916F915F914F9189 +:102790003F912F910F900FBE0F901F9018951F9291 +:1027A0000F920FB60F9211242F938F939F93EF9355 +:1027B000FF93E091C302F091C4028081E091C902CD +:1027C000F091CA0282FD1BC090818091CC028F5F84 +:1027D0008F732091CD02821741F0E091CC02F0E09E +:1027E000ED54FD4F958F8093CC02FF91EF919F9117 +:1027F0008F912F910F900FBE0F901F901895808191 +:10280000F4CF1F920F920FB60F9211242F933F9384 +:102810004F935F936F937F938F939F93AF93BF93E8 +:10282000EF93FF938091B900887F803609F44AC006 +:10283000F0F5883209F4A7C018F5803109F49BC07F +:10284000B8F4882309F4F9C0883009F494C0FF91E2 +:10285000EF91BF91AF919F918F917F916F915F9118 +:102860004F913F912F910F900FBE0F901F90189591 +:10287000883109F488C0803251F780938B0214C0EC +:10288000803409F49DC040F48033B9F38833F9F6FD +:1028900080938B0285ECB0C0803509F485C0883503 +:1028A00009F496C0883499F60E942713D0CF88394E +:1028B00009F48CC038F5883729F050F4883611F0C7 +:1028C000803729F683E08093AD021092310257C021 +:1028D000883809F47BC0803919F0803809F0B7CF07 +:1028E00080913102803208F071C0E091310281E0C4 +:1028F0008E0F809331028091BB00F0E0EF5EFD4FC0 +:1029000080833DC0803B39F0E0F4803A09F479C01F +:10291000883A09F09CCF84E08093AD0210921002B7 +:1029200010920F02E0913F02F091400209958091D0 +:102930000F0281110FC081E080930F021092EF010E +:1029400009C0803C09F4A6CF883C09F4A3CF883B9A +:1029500009F07DCFE091100281E08E0F809310028C +:10296000F0E0E151FE4F80818093BB009091100216 +:1029700080910F0229C0809188028093BB0085EC72 +:102980008093BC0064CF90918A028091890298174D +:1029900058F5E0918A0281E08E0F80938A02F0E080 +:1029A000E959FD4F8081E9CFE0918A0281E08E0FE5 +:1029B00080938A028091BB00F0E0E959FD4F80834B +:1029C00090918A02809189029817C8F285E8D8CF41 +:1029D000E0918A0281E08E0F80938A028091BB0091 +:1029E000F0E0E959FD4F808380918C0281115CCF2A +:1029F00081E08093870284EA8093BC001092AD024C +:102A000026CF85EC8093BC001092AD0280913102FC +:102A1000803230F4E0913102F0E0EF5EFD4F108241 +:102A20006091310270E0E0913D02F0913E0281E15F +:102A300092E00995109231020ACF10928B0234CFA6 +:102A4000E3EBF2E01382128248EE53E060E070E0C4 +:102A5000448355836683778383E591E09183808304 +:102A600085EC90E09587848784EC90E09787868753 +:102A700080EC90E0918B808B81EC90E0938B828B4B +:102A800082EC90E0958B848B86EC90E0978B868B24 +:102A9000118E128E138E148EA0E5B3E013961C9245 +:102AA0001E92129714964D935D936D937C93179796 +:102AB00085E691E011969C938E93ECE7F3E087E72F +:102AC00091E091838083178E168E11A210A213A21B +:102AD00012A215A214A2EEE6F3E085E481834AE98E +:102AE00059E969E97FE342835383648375831782DD +:102AF000168283E28083B187A087ECE5F3E0158638 +:102B0000148617861686118A108A0895CF93DF934C +:102B1000CDB7DEB7C457D1090FB6F894DEBF0FBEEC +:102B2000CDBF789484B5826084BD84B5816084BD56 +:102B300085B5826085BD85B5816085BD80916E005B +:102B4000816080936E00109281008091810082608C +:102B500080938100809181008160809381008091C9 +:102B600080008160809380008091B10084608093B8 +:102B7000B1008091B00081608093B00080917A00B4 +:102B8000846080937A0080917A00826080937A00DA +:102B900080917A00816080937A0080917A008068C9 +:102BA00080937A001092C100E091C302F091C402B8 +:102BB00082E08083E091BF02F091C0021082E09138 +:102BC000C102F091C20280E180831092CB02E091B9 +:102BD000C702F091C80286E08083E091C502F091BF +:102BE000C602808180618083E091C502F091C602B7 +:102BF000808188608083E091C502F091C602808167 +:102C000080688083E091C502F091C60280818F7D4B +:102C100080830E94060586EB91E00E94B40C8091AF +:102C20009A0390919B03009711F00E94611E86E029 +:102C300090E00E94C51DFC0189E6808380E593E059 +:102C400092838183138280E290E095838483F09362 +:102C50009B03E0939A0385E0F82E01E0C0909A036D +:102C6000D0909B030E940605D60113960C93C601D3 +:102C70000E940808182F8111ADC46AE070E080E05E +:102C800090E00E94F501FA94F110E8CF812F90E0D6 +:102C90000E94840C0E9427026B017C018CE793E068 +:102CA0000E94990AFE013D9618E1DF01812F1D92D5 +:102CB0008A95E9F784E290E0A0E0B0E089839A8306 +:102CC000AB83BC838091A6039091A703B0E0A0E002 +:102CD0008D839E83AF83B8878DE090E0A0E0B0E065 +:102CE00089879A87AB87BC87C98ADA8AEB8AFC8A8C +:102CF00080917E0390917F03A0918003B091810326 +:102D00008D8B9E8BAF8BB88FE9E4F0E0EC0FFD1F4D +:102D10003C96DF01812F1D928A95E9F7812C912C39 +:102D2000540183942D968CAE9DAEAEAEBFAE2D9762 +:102D30008091A2039091A303B0E0A0E061968CAFD4 +:102D40009DAFAEAFBFAF619765968CAE9DAEAEAE98 +:102D5000BFAE65976D96CCAEDDAEEEAEFFAE6D97B5 +:102D60002AE038EE4CE151E46091820370918303D4 +:102D700080918403909185030E94C71BA1966CAF3C +:102D80007DAF8EAF9FAFA1972AE038EE4CE151E4C2 +:102D900060918603709187038091880390918903E5 +:102DA0000E94C71BA5966CAF7DAF8EAF9FAFA59756 +:102DB0002AE038EE4CE151E460918A0370918B0374 +:102DC00080918C0390918D030E94C71BA9966CAFD4 +:102DD0007DAF8EAF9FAFA997FE01F196DF011D92E7 +:102DE0001A95E9F78DA29EA2AFA2B8A68091A4037E +:102DF0009091A503B0E0A0E089A79AA7ABA7BCA7D4 +:102E000084E090E0A0E0B0E08DA79EA7AFA7B8ABAC +:102E1000CDAADEAAEFAAF8AE25E33AEF4EE85CE3CE +:102E200060918E0370918F03809190039091910334 +:102E30000E94C71B69AF7AAF8BAF9CAF25E33AEF17 +:102E40004EE85CE360919203709193038091940348 +:102E5000909195030E94C71B21966CAF7DAF8EAFFA +:102E60009FAF219725E33AEF4EE85CE3609196032C +:102E70007091970380919803909199030E94C71BCA +:102E800025966CAF7DAF8EAF9FAF25978BEB91E012 +:102E90000E94B40CA9966CAD7DAD8EAD9FADA99787 +:102EA00041E050E00E94B60C8FEB91E00E94B40C20 +:102EB00023E220936E03809176039091770301E0E3 +:102EC000009343022093650210924402109266021E +:102ED000DC01ED91FC910190F081E02D60E109951C +:102EE00081E00E941606182F6AE070E080E090E012 +:102EF0000E94F501123009F489C048F4112399F0B9 +:102F00008BED91E0113059F088E391E008C084E73F +:102F100091E0133021F088E591E01430A9F70E9488 +:102F2000820D80E07AC080E190E0909375038093F9 +:102F300074030E942702609378037093790380934F +:102F40007A0390937B03809176039091770320918D +:102F50006E03009343022093650210924402109284 +:102F6000660262E470E00E94460381E00E94160659 +:102F7000182F809176039091770320916E03009330 +:102F8000430220936502109244021092660265E6A5 +:102F900070E00E94460381E00E941606182B809183 +:102FA00076039091770320916E0300934302209360 +:102FB000650210924402109266026091740370914F +:102FC00075030E94460381E00E941606182B6AE0F2 +:102FD00070E080E090E00E94F501123009F4F6C143 +:102FE00050F41123B1F08BE091E0113009F497CF48 +:102FF00088E690E094CF84EA90E0133009F48FCF14 +:1030000088E890E0143009F48ACFF2CF86EA91E0A4 +:1030100086CF85E480936F0381E090E00E94840C6A +:1030200068EC70E080E090E00E94F50183EC91E0B4 +:103030000E94B40C8091740390917503892B09F060 +:10304000C8C188E092E00E94820DC12CD12CE12CF5 +:10305000B0ECFB2E50E040E0C701B6010E94B60C78 +:1030600088EC91E00E94B40C80913B0290913C026C +:10307000009711F00E94611E86E090E00E94C51D3D +:103080008C0188E6F801808380E593E09283818358 +:10309000138280E290E09583848310933C02009336 +:1030A0003B020E94060581E0D80113968C93C8016B +:1030B0000E94080890E00E94840C8DEC91E00E9430 +:1030C000B40C0E941C090E946F0D6E01BDE6CB0E70 +:1030D000D11C86E0F60111928A95E9F786E5D601C2 +:1030E00011968C93119784E312968C93129782E138 +:1030F00013968C93139780E315968C93159789E01C +:1031000016968C93169786E217968C93F12CE12CE9 +:1031100010E000E021E048E050E0B60180913B0281 +:1031200090913C020E940607F6011082A0903B029B +:10313000B0903C0220E041E050E0B601C5010E94A1 +:103140000607882331F047E050E0B601C5010E9430 +:10315000B608D6012C912F77822F90E034E0959518 +:1031600087953A95E1F7982F990F990FB82EB91ACC +:10317000BB0CB20E11966C911197862F90E044E033 +:10318000959587954A95E1F7982F990F990F182FE4 +:10319000191B110F160F12966C911297862F90E043 +:1031A00054E0959587955A95E1F7982F990F990FC7 +:1031B000082F091B000F060F14966C911497862F89 +:1031C00090E074E0959587957A95E1F7982F990F9F +:1031D000990FF82EF91AFF0CF60E15966C911597AB +:1031E000862F90E0E4E095958795EA95E1F7982F92 +:1031F000990F990FE82EE91AEE0CE60E1696AC9090 +:1032000082ED91E00E94B40C8A2D90E0F4E0959557 +:103210008795FA95E1F7982F990F990F891B880FD9 +:103220008A0D682F70E06053784F90E080E04AE0AC +:103230000E94400C8DE20E9420036E2D70E090E011 +:1032400080E04AE00E94400C8DE20E9420036F2D36 +:1032500070E090E080E04AE00E94400C80E20E9432 +:103260002003602F70E090E080E04AE00E94400C74 +:103270008AE30E942003612F70E090E080E04AE042 +:103280000E94400C8AE30E9420038B2D0E946F0D48 +:103290008DEC91E00E94B40C0E941C090E946F0DFD +:1032A00087ED91E00E94B40C64E170E080E090E072 +:1032B0000E94F5018091680390916903009711F0D5 +:1032C0000E94611E86E090E00E94C51D8C0188E38B +:1032D000F801808380E593E092838183138280E20A +:1032E00090E09583848310936903009368030E94A0 +:1032F000060581E0D80113968C93C8010E94080846 +:10330000B82E8823A9F08AEBF6018083F12CE12CFA +:1033100010E000E021E041E050E0B6018091680358 +:10332000909169030E940607B82E8111D5C0B12C77 +:103330008B2D90E00E94840CAE014B5D5F4FBE016F +:10334000675B7F4F8CE593E00E94350F8CED91E039 +:103350000E94B40CA1966CAD7DAD8EAD9FADA197D2 +:1033600041E050E00E94B60C89ED91E00E94B40C5F +:1033700069AD7AAD8BAD9CAD41E050E00E94B60CDA +:1033800080EE91E00E94B40C8EE00E94B3010E9496 +:10339000840C84EE91E00E94B40C8FE00E94B30193 +:1033A0000E94840C88EE91E00E94330C89E791E042 +:1033B0000E94330C00E010E001151105E9F30E94B2 +:1033C0009D028823C9F30E940000F6CF86ED90E0AD +:1033D000A6CD41E062E080916E030E943E0510920E +:1033E000420280934102C12CD12CA0E8EA2EAFEB1F +:1033F000FA2E823009F58091760390917703DC01F3 +:10340000ED91FC910284F385E02D099500E0182FE1 +:103410008091760390917703DC01ED91FC91028419 +:10342000F385E02D0995802B912BBC0190E080E085 +:103430000E94661B6B017C010E9427026093780347 +:103440007093790380937A0390937B0320E030E0BC +:1034500040E85FEBC701B6010E94611B882309F4B5 +:10346000F9CD60916F036534A9F070E090E080E0E1 +:103470000E94661B9B01AC0160E070E08AE892E468 +:103480000E94F21C9B01AC01C701B6010E94C71B40 +:103490006B017C01809174039091750381329105D9 +:1034A00011F0419751F420E030E040E05FE3C701C4 +:1034B000B6010E94C71B6B017C0120917003309103 +:1034C00071034091720350917303C701B6010E94CA +:1034D000F21C6B017C01BECD64E170E080E090E005 +:1034E0000E94F5018CE593E00E941B0C87FF05C04C +:1034F0006AE070E080E090E0F3CF81EED6018C933B +:1035000088E011968C93119712961C92F12CE12C65 +:1035100010E000E021E043E050E0B6018091680354 +:10352000909169030E9406078CE593E00E941B0CB2 +:1035300087FF07C06AE070E080E090E00E94F5013C +:10354000F3CF8CE593E00E941B0C83FFF0CE8091BB +:103550006C0390916D03009739F0DC01ED91FC91C3 +:103560000280F381E02D099580916A0390916B03AD +:10357000009739F0DC01ED91FC910280F381E02DA0 +:10358000099586E090E00E94C51DFC0185E491E06C +:103590009183808380E290E1938382830CE513E042 +:1035A00015830483F0936D03E0936C0386E090E051 +:1035B0000E94C51DFC0187E391E09183808381E235 +:1035C00090E19383828315830483F0936B03E093EC +:1035D0006A03AECE80919A0390919B031A8A1B8A4C +:1035E0001C8A1D8A9A8389831C821B820C8785E72B +:1035F00090E09A8789871D860B87CE0101960E94ED +:10360000E40910E0683671058105910509F03ECBAB +:1036100040E050E0BA018CE793E00E94AC0D182F17 +:1036200035CB80916A0390916B03009739F0DC01F0 +:10363000ED91FC910280F381E02D099580916C035E +:1036400090916D03009739F0DC01ED91FC910280BF +:10365000F381E02D099587E791E090937D038093B6 +:103660007C0380919C0390919D03009739F0DC01CD +:10367000ED91FC910280F381E02D099580919E03EC +:1036800090919F03009739F0DC01ED91FC9102804D +:10369000F381E02D09958091A0039091A1030097FB +:1036A00039F0DC01ED91FC910280F381E02D099568 +:1036B00080919A0390919B03009711F00C94611EE6 +:1036C00008950E94A31B08F481E00895E89409C0BE +:1036D00097FB3EF490958095709561957F4F8F4F45 +:1036E0009F4F9923A9F0F92F96E9BB279395F6955B +:1036F000879577956795B795F111F8CFFAF4BB0FD9 +:1037000011F460FF1BC06F5F7F4F8F4F9F4F16C03C +:10371000882311F096E911C0772321F09EE8872FC6 +:10372000762F05C0662371F096E8862F70E060E082 +:103730002AF09A95660F771F881FDAF7880F9695FB +:10374000879597F90895990F0008550FAA0BE0E89F +:10375000FEEF16161706E807F907C0F01216130653 +:10376000E407F50798F0621B730B840B950B39F493 +:103770000A2661F0232B242B252B21F408950A26F9 +:1037800009F4A140A6958FEF811D811D08950E9427 +:10379000DA1B0C944B1C0E943D1C38F00E94441C08 +:1037A00020F0952311F00C94341C0C943A1C112435 +:1037B0000C947F1C0E945C1C70F3959FC1F3950FC5 +:1037C00050E0551F629FF001729FBB27F00DB11DA5 +:1037D000639FAA27F00DB11DAA1F649F6627B00D35 +:1037E000A11D661F829F2227B00DA11D621F739F1E +:1037F000B00DA11D621F839FA00D611D221F749F2C +:103800003327A00D611D231F849F600D211D822F72 +:10381000762F6A2F11249F5750409AF0F1F0882399 +:103820004AF0EE0FFF1FBB1F661F771F881F9150C6 +:103830005040A9F79E3F510580F00C94341C0C9425 +:103840007F1C5F3FE4F3983ED4F3869577956795A8 +:10385000B795F795E7959F5FC1F7FE2B880F911DF0 +:103860009695879597F9089597F99F6780E870E096 +:1038700060E008959FEF80EC089500240A941616E6 +:10388000170618060906089500240A94121613064E +:10389000140605060895092E0394000C11F48823DC +:1038A00052F0BB0F40F4BF2B11F460FF04C06F5FF8 +:1038B0007F4F8F4F9F4F089557FD9058440F551FCE +:1038C00059F05F3F71F04795880F97FB991F61F0A2 +:1038D0009F3F79F087950895121613061406551F19 +:1038E000F2CF4695F1DF08C0161617061806991F85 +:1038F000F1CF86957105610508940895E894BB277A +:1039000066277727CB0197F908955058BB27AA2738 +:103910000E949D1C0C944B1C0E943D1C38F00E9480 +:10392000441C20F039F49F3F19F426F40C943A1CFF +:103930000EF4E095E7FB0C94341CE92F0E945C1C0C +:1039400058F3BA17620773078407950720F079F4D4 +:10395000A6F50C947E1C0EF4E0950B2EBA2FA02D2C +:103960000B01B90190010C01CA01A0011124FF272C +:10397000591B99F0593F50F4503E68F11A16F04027 +:10398000A22F232F342F4427585FF3CF4695379526 +:103990002795A795F0405395C9F77EF41F16BA0BEB +:1039A000620B730B840BBAF09150A1F0FF0FBB1F99 +:1039B000661F771F881FC2F70EC0BA0F621F731FE2 +:1039C000841F48F4879577956795B795F7959E3F3F +:1039D00008F0B0CF9395880F08F09927EE0F9795D0 +:1039E000879508950E94061D0C944B1C0E94441C50 +:1039F00058F00E943D1C40F029F45F3F29F00C94E0 +:103A0000341C51110C947F1C0C943A1C0E945C1CB9 +:103A100068F39923B1F3552391F3951B550BBB27FD +:103A2000AA2762177307840738F09F5F5F4F220F42 +:103A3000331F441FAA1FA9F335D00E2E3AF0E0E839 +:103A400032D091505040E695001CCAF72BD0FE2F83 +:103A500029D0660F771F881FBB1F26173707480717 +:103A6000AB07B0E809F0BB0B802DBF01FF279358CF +:103A70005F4F3AF09E3F510578F00C94341C0C9443 +:103A80007F1C5F3FE4F3983ED4F386957795679566 +:103A9000B795F7959F5FC9F7880F911D9695879504 +:103AA00097F90895E1E0660F771F881FBB1F621723 +:103AB00073078407BA0720F0621B730B840BBA0BE1 +:103AC000EE1F88F7E09508950E94641C88F09F57C8 +:103AD00098F0B92F9927B751B0F0E1F0660F771F32 +:103AE000881F991F1AF0BA95C9F714C0B13091F028 +:103AF0000E947E1CB1E008950C947E1C672F782FE5 +:103B00008827B85F39F0B93FCCF3869577956795EC +:103B1000B395D9F73EF490958095709561957F4F58 +:103B20008F4F9F4F08950E94A31B08F48FEF0895B5 +:103B30000E94A31B880B990B0895A1E21A2EAA1BC1 +:103B4000BB1BFD010DC0AA1FBB1FEE1FFF1FA2174D +:103B5000B307E407F50720F0A21BB30BE40BF50B4A +:103B6000661F771F881F991F1A9469F760957095D3 +:103B7000809590959B01AC01BD01CF010895EE0F9A +:103B8000FF1F0590F491E02D09940F931F93CF939D +:103B9000DF938230910510F482E090E0E091B80369 +:103BA000F091B90330E020E0B0E0A0E0309799F464 +:103BB0002115310509F44AC0281B390B2430310581 +:103BC000D8F58A819B816115710589F1FB01938389 +:103BD0008283FE0111C04081518102811381481707 +:103BE0005907E0F04817590799F4109761F01296B9 +:103BF0000C93129713961C933296CF01DF91CF91BD +:103C00001F910F9108950093B8031093B903F4CF57 +:103C10002115310551F04217530738F0A901DB0196 +:103C20009A01BD01DF01F801C1CFEF01F9CF9093F7 +:103C3000B9038093B803CDCFFE01E20FF31F819348 +:103C400091932250310939832883D7CF2091B6032D +:103C50003091B703232B41F42091020130910301ED +:103C60003093B7032093B6032091000130910101F6 +:103C70002115310541F42DB73EB740910401509113 +:103C80000501241B350BE091B603F091B703E21751 +:103C9000F307A0F42E1B3F0B2817390778F0AC016F +:103CA0004E5F5F4F2417350748F04E0F5F1F50934C +:103CB000B7034093B603819391939FCFF0E0E0E088 +:103CC0009CCFCF93DF930097E9F0FC0132971382EA +:103CD0001282A091B803B091B903ED0130E020E069 +:103CE0001097A1F420813181820F931F2091B60398 +:103CF0003091B7032817390709F061C0F093B70373 +:103D0000E093B603DF91CF910895EA01CE17DF0764 +:103D1000E8F54A815B819E0141155105B1F7E90142 +:103D2000FB83EA8349915991C40FD51FEC17FD0716 +:103D300061F4808191810296840F951FE901998336 +:103D40008883828193819B838A83F0E0E0E01296EE +:103D50008D919C9113970097B9F52D913C911197F6 +:103D6000CD010296820F931F2091B6033091B703C5 +:103D70002817390739F6309751F51092B903109288 +:103D8000B803B093B703A093B603BCCFD383C28369 +:103D900040815181840F951FC817D90761F44E5F88 +:103DA0005F4F88819981480F591F518340838A81D1 +:103DB0009B81938382832115310509F0B0CFF09365 +:103DC000B903E093B8039ECFFD01DC01C0CF13829D +:103DD0001282D7CFFB01DC014150504048F00190E6 +:103DE0000D920020C9F701C01D9241505040E0F7EC +:103DF000089511E0CAE3D1E004C0FE010E94BF1D96 +:0C3E00002196CB33D107C9F7F894FFCF0F +:103E0C000000BA038000AC3300000000004503A69C +:103E1C000DC0043911D812000000004503A80DC0D4 +:103E2C0004C3108D12000000004503AA0DC0047ECF +:103E3C00103E12000000004503A40DC0046B10A43A +:103E4C0011000000004503A20DC0047510F3110011 +:103E5C00000000D30233026002250391026F02833B +:103E6C00020000000084085708B3082D0850083DD4 +:103E7C00082E0800000000AC0D0D0A006E616E00EB +:103E8C00696E66006F76660041485478305F540066 +:103E9C0041485478305F48004D5055363035305FCE +:103EAC0054004D5055363035305F41004D5055362D +:103EBC003035305F47004D50553D00415A3D004272 +:103ECC00483D004C55583D005254433D0052554E10 +:103EDC003D004E4F573D004148543D0052483D0077 +:0E3EEC0041303D0041313D00444F4E45000045 +:00000001FF diff --git a/packages/pinbench_sim/test/fixtures/sensors/sensors.ino b/packages/pinbench_sim/test/fixtures/sensors/sensors.ino new file mode 100644 index 0000000..b3a1bce --- /dev/null +++ b/packages/pinbench_sim/test/fixtures/sensors/sensors.ino @@ -0,0 +1,78 @@ +// Fixture for sensor_sketches_test.dart: the four I2C sensor parts read +// through the libraries tutorials use, then two analog ones through the ADC, +// each result reported over Serial. +// +// Rebuild after editing (from this folder), with these libraries installed: +// Adafruit MPU6050, BH1750 (claws), RTClib, Adafruit AHTX0. +// arduino-cli compile --fqbn arduino:avr:uno --output-dir . sensors.ino +// mv sensors.ino.hex sensors.hex && rm -f sensors.ino.* + +#include +#include +#include +#include +#include + +Adafruit_MPU6050 mpu; +BH1750 light; +RTC_DS1307 rtc; +Adafruit_AHTX0 aht; + +void setup() { + Serial.begin(115200); + Wire.begin(); + sensors_event_t a, b, c; + + // AD0 is tied high in the test circuit, so the IMU is at 0x69 and the + // clock can have 0x68. + Serial.print("MPU="); + Serial.println(mpu.begin(0x69)); + mpu.getEvent(&a, &b, &c); + Serial.print("AZ="); + Serial.println(a.acceleration.z, 1); + + Serial.print("BH="); + Serial.println(light.begin()); + delay(200); + Serial.print("LUX="); + Serial.println(light.readLightLevel(), 0); + + Serial.print("RTC="); + Serial.println(rtc.begin()); + Serial.print("RUN="); + Serial.println(rtc.isrunning()); + rtc.adjust(DateTime(2026, 9, 30, 12, 34, 56)); + DateTime now = rtc.now(); + Serial.print("NOW="); + Serial.print(now.year()); + Serial.print('-'); + Serial.print(now.month()); + Serial.print('-'); + Serial.print(now.day()); + Serial.print(' '); + Serial.print(now.hour()); + Serial.print(':'); + Serial.print(now.minute()); + Serial.print(':'); + Serial.println(now.second()); + Serial.print("RUN="); + Serial.println(rtc.isrunning()); + + Serial.print("AHT="); + Serial.println(aht.begin()); + aht.getEvent(&a, &b); + Serial.print("RH="); + Serial.println(a.relative_humidity, 1); + Serial.print("T="); + Serial.println(b.temperature, 1); + + // Read last: the ADC only sees the analog solve once a frame has run. + Serial.print("A0="); + Serial.println(analogRead(A0)); + Serial.print("A1="); + Serial.println(analogRead(A1)); + + Serial.println("DONE"); +} + +void loop() {} diff --git a/packages/pinbench_sim/test/i2c_read_test.dart b/packages/pinbench_sim/test/i2c_read_test.dart new file mode 100644 index 0000000..fbe45af --- /dev/null +++ b/packages/pinbench_sim/test/i2c_read_test.dart @@ -0,0 +1,41 @@ +/// End-to-end I²C reads: a real sketch built on the Arduino `Wire` library, +/// compiled for the Uno (`test/fixtures/i2c_read/`), runs on the emulated +/// ATmega328P and reads a device the test serves. +/// +/// The recorder's own tests cover the register logic; this proves the part +/// that only the real library can: that `Wire.requestFrom` after a repeated +/// start, a multi-byte read and a write-then-read-back all reach the served +/// registers through the TWI peripheral, and that an absent address is still +/// NACKed while another one answers. +library; + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinbench_sim/core/avr_interop.dart'; + +void main() { + test('a Wire sketch reads, reads back and is refused where nothing answers', () { + final hex = File('test/fixtures/i2c_read/i2c_read.hex').readAsStringSync(); + final lines = []; + AVRBridge.loadHex(hex, onSerialPrint: lines.add); + + // Served after loading: a load resets the bus, so a new run never sees + // the previous one's devices. + AVRBridge.serveI2c(0x68); + AVRBridge.setI2cRegisters(0x68, 0x75, [0x68]); + AVRBridge.setI2cRegisters(0x68, 0x3B, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC]); + + // Two simulated seconds is ample for setup() at 115200 baud; stop early + // once the sketch reports it is done. + const cyclesPerStep = 160000; // 10 ms at 16 MHz + for (var step = 0; step < 200 && !lines.contains('DONE'); step++) { + AVRBridge.tick(cyclesPerStep); + } + + expect( + lines, + containsAllInOrder(['WHO=68', 'ACC=123456789ABC', 'BACK=5A', 'ABSENT=2', 'DONE']), + ); + }); +} diff --git a/packages/pinbench_sim/test/i2c_recorder_test.dart b/packages/pinbench_sim/test/i2c_recorder_test.dart index 7602667..788ff56 100644 --- a/packages/pinbench_sim/test/i2c_recorder_test.dart +++ b/packages/pinbench_sim/test/i2c_recorder_test.dart @@ -120,4 +120,128 @@ void main() { expect(recorder.drain(0x3C), isEmpty); expect(recorder.acknowledges(0x3C), isFalse); }); + + group('answering reads', () { + /// A register read the way `Wire` does it: write the pointer, then a + /// repeated start and a read of [count] bytes. + List readRegisters(I2cRecorder bus, int address, int register, int count) { + bus.beginTransaction(address, write: true); + bus.writeByte(register); + bus.beginTransaction(address, write: false); // repeated start + final bytes = [for (var i = 0; i < count; i++) bus.readByte()]; + bus.endTransaction(); + return bytes; + } + + test('a read returns the register the sketch pointed at', () { + final bus = I2cRecorder() + ..serve(0x68) + ..setRegisters(0x68, 0x75, [0x68]); // MPU6050 WHO_AM_I + + expect(readRegisters(bus, 0x68, 0x75, 1), [0x68]); + }); + + test('the pointer advances after every byte and wraps at the end', () { + // How a sensor hands over a multi-byte reading in one read: the + // accelerometer's six bytes start at 0x3B and follow each other. + final bus = I2cRecorder() + ..serve(0x68, size: 4) + ..setRegisters(0x68, 0, [0xA0, 0xA1, 0xA2, 0xA3]); + + expect(readRegisters(bus, 0x68, 2, 4), [0xA2, 0xA3, 0xA0, 0xA1]); + }); + + test('what the sketch writes after the pointer can be read back', () { + // Setting an RTC's clock and reading it again depends on this. + final bus = I2cRecorder()..serve(0x68); + bus.beginTransaction(0x68, write: true); + [0x00, 0x30, 0x15, 0x09].forEach(bus.writeByte); + bus.endTransaction(); + + expect(readRegisters(bus, 0x68, 0x00, 3), [0x30, 0x15, 0x09]); + }); + + test('a two-byte pointer selects the register most significant byte first', () { + final bus = I2cRecorder() + ..serve(0x50, size: 32 * 1024, pointerBytes: 2) // 24LC256 EEPROM + ..setRegisters(0x50, 0x1234, [0x5A]); + bus.beginTransaction(0x50, write: true); + bus + ..writeByte(0x12) + ..writeByte(0x34); + bus.beginTransaction(0x50, write: false); + + expect(bus.readByte(), 0x5A); + }); + + test('a pointer-less device starts every read at register 0', () { + // A PCF8574 has one register, its port; writes drive the pins rather + // than change what a read returns. + final bus = I2cRecorder() + ..serve(0x20, size: 1, pointerBytes: 0) + ..setRegisters(0x20, 0, [0xF0]); + bus.beginTransaction(0x20, write: true); + bus.writeByte(0x0F); + bus.beginTransaction(0x20, write: false); + final first = bus.readByte(); + bus.beginTransaction(0x20, write: false); + + expect([first, bus.readByte()], [0xF0, 0xF0]); + }); + + test('an address nothing serves reads as an idle bus', () { + final bus = I2cRecorder()..listenAt(0x3C); + bus.beginTransaction(0x3C, write: false); + + expect(bus.readByte(), 0xFF, reason: 'a display listens but does not answer'); + }); + + test('serving claims the address, so it is acknowledged', () { + final bus = I2cRecorder()..serve(0x68); + + expect(bus.acknowledges(0x68), isTrue); + expect(bus.acknowledges(0x69), isFalse); + }); + + test('serving the same shape again keeps the registers and the pointer', () { + // Parts declare themselves every frame; that must not wipe what the + // sketch wrote a frame ago. + final bus = I2cRecorder()..serve(0x68); + bus.beginTransaction(0x68, write: true); + [0x10, 0x42].forEach(bus.writeByte); + bus.endTransaction(); + bus.serve(0x68); + + expect(bus.registersOf(0x68)![0x10], 0x42); + bus.serve(0x68, size: 128); + expect(bus.registersOf(0x68)![0x10], 0, reason: 'a new shape starts over'); + }); + + test('writes to a served device still reach drain', () { + // A part reacts to configuration (a power-on command, a range setting) + // by reading the transactions, exactly as a display does. + final bus = I2cRecorder()..serve(0x68); + bus.beginTransaction(0x68, write: true); + [0x6B, 0x00].forEach(bus.writeByte); + bus.endTransaction(); + + expect(bus.drain(0x68), [ + [0x6B, 0x00], + ]); + }); + + test('setRegisters is ignored for an address nothing serves', () { + final bus = I2cRecorder()..setRegisters(0x68, 0, [1, 2, 3]); + + expect(bus.registersOf(0x68), isNull); + }); + + test('reset forgets every served device', () { + final bus = I2cRecorder()..serve(0x68); + bus.reset(); + + expect(bus.registersOf(0x68), isNull); + expect(bus.acknowledges(0x68), isFalse); + }); + }); } diff --git a/packages/pinbench_sim/test/part_behavior_memory_test.dart b/packages/pinbench_sim/test/part_behavior_memory_test.dart index a66d364..088cf24 100644 --- a/packages/pinbench_sim/test/part_behavior_memory_test.dart +++ b/packages/pinbench_sim/test/part_behavior_memory_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:pinbench_parts/logic/part_logic.dart'; import 'package:pinbench_parts/models/component_instance.dart'; import 'package:pinbench_parts/models/part_model.dart'; +import 'package:pinbench_parts/part_registry.dart'; import 'package:pinbench_sim/core/circuit_netlist.dart'; import 'package:pinbench_sim/core/engine_pin_api.dart'; import 'package:pinbench_sim/core/spice_engine.dart'; @@ -91,4 +92,26 @@ void main() { runFrame(node, lastState); expect(lastState[node.key]?['ticks'], 100); }); + + test("a .pdl part's STATE starts from its declared value, not the last run's", () async { + // PDL.md promises STATE is "recreated from its initial value every time a + // simulation starts". The canvas still holds where the last run ended — here + // a PIR whose hold ran far past the end of that run — and the evaluator + // used to seed state from it, so the new run began already triggered. + await PartRegistry.initializeAsync(); + final pir = ComponentInstance( + position: Offset.zero, + part: PartModel( + name: 'PIR Motion Sensor', + size: const Size(128, 96), + definitionId: 'pir_sensor', + ), + properties: {'triggered': false, 'active': true, 'holdUntilMs': 1e9}, + ); + final lastState = >{}; + + runFrame(pir, lastState); + + expect(lastState[pir.key]?['active'], isFalse); + }); } diff --git a/packages/pinbench_sim/test/sensor_sketches_test.dart b/packages/pinbench_sim/test/sensor_sketches_test.dart new file mode 100644 index 0000000..b37bcb6 --- /dev/null +++ b/packages/pinbench_sim/test/sensor_sketches_test.dart @@ -0,0 +1,136 @@ +/// The sensor parts under the libraries people actually use: a sketch built +/// on Adafruit MPU6050, BH1750, RTClib and Adafruit AHTX0, plus two +/// `analogRead`s (`test/fixtures/sensors/`), runs on the emulated ATmega328P +/// against a circuit of placed `.pdl` parts, wired the way a user wires them. +/// +/// `pinbench_parts`' own tests check each part's registers. This checks what +/// only the whole engine can: that the parts are on the bus *before* `setup()` +/// probes it (every library here gives up for good on a NACK), that their +/// wiring is traced through the netlist, and that each library's init +/// sequence ends in a reading. +library; + +import 'dart:io'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:pinbench_parts/models/component_instance.dart'; +import 'package:pinbench_parts/models/part_model.dart'; +import 'package:pinbench_parts/models/port_model.dart'; +import 'package:pinbench_parts/models/wire_model.dart'; +import 'package:pinbench_parts/painting/dsl_component_painter.dart'; +import 'package:pinbench_parts/part_registry.dart'; +import 'package:pinbench_sim/core/simulation_engine.dart'; +import 'package:pinbench_sim/core/simulation_output.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(PartRegistry.initializeAsync); + + test('each sensor reads through its Arduino library', () { + final uno = ComponentInstance( + key: const ValueKey('uno'), + position: Offset.zero, + part: PartModel(name: PartNames.arduinoUno, size: const Size(40, 40)), + ); + final parts = []; + final wires = []; + + ComponentInstance place(String id, {Map properties = const {}}) { + final definition = PartRegistry.getPart(id)!; + final node = ComponentInstance( + key: ValueKey(id), + // Far apart, so no two parts' pins overlap and join by accident. + position: Offset(600.0 * (parts.length + 1), 0), + part: PartModel( + name: definition.name, + size: Size(definition.visual.width, definition.visual.height), + definitionId: definition.id, + logic: definition.logic, + painterBuilder: ({isOutline = false, properties}) => DSLComponentPainter( + definition: definition, + isOutline: isOutline, + properties: properties, + ), + ), + properties: {...properties}, + ); + parts.add(node); + return node; + } + + void wire(ComponentInstance part, String pin, String boardPort) => wires.add( + WireModel( + id: '${part.key}.$pin-$boardPort', + start: PortLocation(nodeKey: part.key, portId: pin), + end: PortLocation(nodeKey: uno.key, portId: boardPort), + ), + ); + + void onTheBus(ComponentInstance part) { + wire(part, 'vcc', '5V'); + wire(part, 'gnd', 'GND_2'); + wire(part, 'sda', 'A4'); + wire(part, 'scl', 'A5'); + } + + final imu = place('mpu6050'); + onTheBus(imu); + wire(imu, 'ad0', '5V'); // to 0x69, leaving 0x68 to the clock + onTheBus(place('bh1750', properties: {'illuminance': 250})); + onTheBus(place('ds1307', properties: {'startsAt': 'unset'})); + onTheBus(place('aht20', properties: {'humidity': 60, 'temperature': 22.5})); + + final thermometer = place('tmp36', properties: {'temperature': 25}); + wire(thermometer, 'vs', '5V'); + wire(thermometer, 'vout', 'A0'); + wire(thermometer, 'gnd', 'GND_3'); + final soil = place('soil_moisture', properties: {'moisture': 40}); + wire(soil, 'vcc', '5V'); + wire(soil, 'aout', 'A1'); + wire(soil, 'gnd', 'GND_3'); + + final lines = []; + final engine = SimulationEngine( + output: _FakeOutput([uno, ...parts], wires), + onSerialPrint: lines.add, + )..prepareForFrameStepping(File('test/fixtures/sensors/sensors.hex').readAsStringSync()); + + // Four simulated seconds is ample: the libraries' own delays add up to + // well under one. Stop as soon as the sketch reports it is done. + const cyclesPerFrame = 160000; // 10 ms at 16 MHz + for (var frame = 0; frame < 400 && !lines.contains('DONE'); frame++) { + engine.runFrame(cycles: cyclesPerFrame); + } + + expect( + lines, + containsAllInOrder([ + 'MPU=1', + 'AZ=9.8', // 1 g, in the m/s² the Adafruit event reports + 'BH=1', + 'LUX=250', + 'RTC=1', + 'RUN=0', // a fresh chip is halted... + 'NOW=2026-9-30 12:34:56', + 'RUN=1', // ...until adjust() sets it + 'AHT=1', + 'RH=60.0', + 'T=22.5', + 'A0=153', // TMP36: 0.75 V at 25 °C + 'A1=450', // soil: 2.2 V at 40 % moisture + 'DONE', + ]), + ); + }); +} + +class _FakeOutput( + @override final List simulationNodes, + @override final List simulationWires, +) implements SimulationOutput { + @override + void applyNodeUpdates(Map> updates) {} +} diff --git a/pubspec.lock b/pubspec.lock index e041183..3de92ec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -124,11 +124,12 @@ packages: avr8_dart: dependency: "direct main" description: - name: avr8_dart - sha256: "0abb3aeec58b93a47b9213cdcd9542c4032c32ffb98699d06de62bcd73fe6379" - url: "https://pub.dev" - source: hosted - version: "0.1.0" + path: "." + ref: "70f44156fbe8ae509313e1c0aace0b0efa079d01" + resolved-ref: "70f44156fbe8ae509313e1c0aace0b0efa079d01" + url: "https://github.com/PinBench/avr8_dart.git" + source: git + version: "0.2.0" boolean_selector: dependency: transitive description: @@ -1029,7 +1030,7 @@ packages: path: "." ref: "1061e5f824186f73671495080fe6c37433b71a1c" resolved-ref: "1061e5f824186f73671495080fe6c37433b71a1c" - url: "https://github.com/burhankhanzada/ngspice_dart.git" + url: "https://github.com/PinBench/ngspice_dart.git" source: git version: "0.2.1" objective_c: diff --git a/pubspec.yaml b/pubspec.yaml index 8738007..95516ef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,13 @@ dependencies: # PWYW builds update themselves in place. Linux has no Sparkle equivalent # and falls back to the notify-only path in `core/updates/`. auto_updater: ^1.0.0 - avr8_dart: ^0.1.0 + # 0.2.0 (about 2.4x faster natively, fast enough for the web's Wasm build) + # is not on pub.dev yet, so it comes from its commit. Back to a version + # constraint once it is published. + avr8_dart: + git: + url: https://github.com/PinBench/avr8_dart.git + ref: 70f44156fbe8ae509313e1c0aace0b0efa079d01 collection: ^1.19.1 crypto: ^3.0.0 # feedback: ^3.2.0 @@ -67,7 +73,7 @@ dependencies: # Local pure-Dart port (no FFI / libngspice). API-compatible Ngspice facade. ngspice_dart: git: - url: https://github.com/burhankhanzada/ngspice_dart.git + url: https://github.com/PinBench/ngspice_dart.git # Pinned to a commit, not a branch: a moving ref lets `pub upgrade` # swap third-party code underneath us with no diff in this repo. # Bump deliberately. Was: branch main diff --git a/web/avr_bridge.js b/web/avr_bridge.js deleted file mode 100644 index 7f3d2fe..0000000 --- a/web/avr_bridge.js +++ /dev/null @@ -1,349 +0,0 @@ -// Full AVR bridge backed by avr8js (hand-optimized JS, ~16 MHz+ in-browser). -// -// On the web the pure-Dart `avr8_dart` emulator (compiled by dart2js) tops out -// around ~10 MHz — too slow to run the 16 MHz ATmega328P in real time, which -// makes time-based sketches (clap rhythm, delays) lag. avr8js runs the same -// core in optimized JS at real-time speed, so the web build delegates the AVR -// emulation here. This mirrors `lib/features/simulation/core/avr_interop.dart`. -import { - CPU, - avrInstruction, - AVRIOPort, - portBConfig, - portCConfig, - portDConfig, - PinState, - AVRTimer, - timer0Config, - timer1Config, - timer2Config, - AVRADC, - adcConfig, - AVRUSART, - usart0Config, - AVRTWI, - twiConfig, -} from 'https://unpkg.com/avr8js@0.21.0/dist/esm/index.js'; - -let cpu = null; -let portB = null, portC = null, portD = null; -let adc = null, usart = null, twi = null; -// Timers must be kept referenced so they keep ticking. -// eslint-disable-next-line no-unused-vars -let timer0 = null, timer1 = null, timer2 = null; - -let buzzerPin = null; -let onSerialPrint = null; // (String) => void (Dart callback) -let onBuzzerFrequency = null; // (double|null) => void (Dart callback) - -// Servo signal pins (set from the traced netlist, like buzzerPin). HIGH-pulse -// widths are measured only on these pins. -let servoPins = []; -const servoRise = new Float64Array(14).fill(-1); -const servoPulseUs = new Float64Array(14); - -const rxQueue = []; -let txLine = ''; - -// --- PWM duty sampling (pins 0..13) ----------------------------------------- -const DUTY_PINS = 14; -const DUTY_SAMPLE_MASK = 63; // sample every 64 cycles -const dutyHigh = new Int32Array(DUTY_PINS); -let dutyTotal = 0; - -// --- Frequency detector (port of FrequencyDetector) ------------------------- -const CLOCK = 16000000; -const fd = { - lastToggleCycle: 0, prevDelta: 0, stableDelta: 0, stableCount: 0, - timeoutCycles: 160000, playing: false, - reset() { - this.lastToggleCycle = 0; this.prevDelta = 0; this.stableDelta = 0; - this.stableCount = 0; this.timeoutCycles = 160000; this.playing = false; - }, - onToggle(cycle) { - if (this.lastToggleCycle !== 0) { - const d = cycle - this.lastToggleCycle; - if (d > 400 && d < 400000) { - if (this.stableDelta === 0 || Math.abs(this.stableDelta - d) > 2000) { - this.stableDelta = d; this.stableCount = 1; - } else { - this.stableDelta = ((this.stableDelta * 3) + d + 2) >> 2; - this.stableCount++; - } - this.timeoutCycles = Math.max(this.stableDelta * 4, 4000); - if (this.stableCount >= 4) { - if (!this.playing || Math.abs(this.prevDelta - this.stableDelta) > 2000) { - this.prevDelta = this.stableDelta; - let freq = CLOCK / (2.0 * this.stableDelta); - if (Math.abs(freq - 2000.0) < 20.0) freq = 2000.0; - this.playing = true; - if (onBuzzerFrequency) onBuzzerFrequency(freq); - } - } - } - } - this.lastToggleCycle = cycle; - }, - checkTimeout(cycles) { - if (this.playing && this.lastToggleCycle !== 0 && - (cycles - this.lastToggleCycle) > this.timeoutCycles) { - this.reset(); - return true; - } - return false; - }, -}; - -function parseHex(hexString) { - const flash = new Uint16Array(0x4000); // 32KB flash = 16K x 16-bit words - const lines = hexString.split('\n'); - for (let line of lines) { - line = line.trim(); - if (line.length === 0 || line[0] !== ':') continue; - if (line.substr(7, 2) !== '00') continue; // data records only - const length = parseInt(line.substr(1, 2), 16); - const addr = parseInt(line.substr(3, 4), 16); - for (let i = 0; i < length; i += 2) { - const lsb = parseInt(line.substr(9 + i * 2, 2), 16); - let msb = 0; - if (i + 1 < length) msb = parseInt(line.substr(11 + i * 2, 2), 16); - flash[(addr + i) >> 1] = (msb << 8) | lsb; - } - } - return flash; -} - -function pinState(pin) { - if (pin >= 0 && pin <= 7) return portD.pinState(pin) === PinState.High; - if (pin >= 8 && pin <= 13) return portB.pinState(pin - 8) === PinState.High; - if (pin >= 14 && pin <= 19) return portC.pinState(pin - 14) === PinState.High; - return false; -} - -function handlePortChange(pinOffset, value, oldValue) { - const changed = value ^ oldValue; - if (changed === 0) return; - - if (buzzerPin !== null && buzzerPin >= pinOffset && buzzerPin < pinOffset + 8 && - (changed & (1 << (buzzerPin - pinOffset))) !== 0) { - fd.onToggle(cpu.cycles); - } - - for (const sPin of servoPins) { - if (sPin < pinOffset || sPin >= pinOffset + 8) continue; - const bit = 1 << (sPin - pinOffset); - if ((changed & bit) === 0) continue; - if ((value & bit) !== 0) { - servoRise[sPin] = cpu.cycles; - } else if (servoRise[sPin] >= 0) { - const us = (cpu.cycles - servoRise[sPin]) / 16.0; // 16 MHz clock - // Servo control pulses are ~0.4-3 ms; ignore anything else on the pin. - if (us >= 400 && us <= 3000) servoPulseUs[sPin] = us; - } - } -} - -// --- I2C bus recording (port of I2cRecorder) -------------------------------- -// -// Bus traffic is queued rather than sampled: the frame loop runs the CPU for a -// whole frame before any part looks, and a screen refresh is a thousand bytes. -// See `packages/pinbench_sim/lib/core/i2c_recorder.dart` for the full reasoning — -// this is the same model, kept in JS so the per-byte callbacks never cross into -// Dart. -const MAX_I2C_BYTES_PER_ADDRESS = 64 * 1024; -const MAX_I2C_ADDRESSES = 16; -const i2c = { - listening: new Set(), - pending: new Map(), // address -> array of transactions (arrays of bytes) - buffered: new Map(), // address -> queued byte count - current: null, - currentAddress: null, - - reset() { - this.listening.clear(); - this.pending.clear(); - this.buffered.clear(); - this.current = null; - this.currentAddress = null; - }, - - begin(address, write) { - this.end(); - if (!write) return; - if (!this.pending.has(address) && this.pending.size >= MAX_I2C_ADDRESSES) return; - this.currentAddress = address; - this.current = []; - }, - - write(value) { - if (this.current) this.current.push(value & 0xff); - }, - - end() { - const address = this.currentAddress; - const bytes = this.current; - this.currentAddress = null; - this.current = null; - if (address === null || !bytes || !bytes.length) return; - - if (!this.pending.has(address)) this.pending.set(address, []); - const queue = this.pending.get(address); - queue.push(bytes); - let buffered = (this.buffered.get(address) || 0) + bytes.length; - while (buffered > MAX_I2C_BYTES_PER_ADDRESS && queue.length > 1) { - buffered -= queue.shift().length; - } - this.buffered.set(address, buffered); - }, - - // Flattened as [length, ...bytes] per transaction, so the whole frame's - // traffic crosses into Dart as one typed array rather than a thousand - // boxed numbers. - drain(address) { - const queue = this.pending.get(address); - this.pending.delete(address); - this.buffered.delete(address); - if (!queue || !queue.length) return new Int32Array(0); - let total = 0; - for (const transaction of queue) total += transaction.length + 1; - const flat = new Int32Array(total); - let at = 0; - for (const transaction of queue) { - flat[at++] = transaction.length; - flat.set(transaction, at); - at += transaction.length; - } - return flat; - }, -}; - -function attachTwi() { - twi = new AVRTWI(cpu, twiConfig, CLOCK); - twi.eventHandler = { - start() { twi.completeStart(); }, - stop() { i2c.end(); twi.completeStop(); }, - connectToSlave(addr, write) { - i2c.begin(addr, write); - // The ACK is the only answer that reaches the sketch, so a device that - // is not on the canvas must not answer for one that is. - twi.completeConnect(i2c.listening.has(addr)); - }, - writeByte(value) { i2c.write(value); twi.completeWrite(true); }, - // Nothing on the canvas talks back yet; 0xff is what an idle bus reads. - readByte() { twi.completeRead(0xff); }, - }; -} - -window.AVR8 = { - listenI2c(address) { i2c.listening.add(address); }, - drainI2c(address) { return i2c.drain(address); }, - - setSerialPrint(cb) { onSerialPrint = cb; }, - setBuzzerFrequencyCallback(cb) { onBuzzerFrequency = cb; }, - setBuzzerPin(pin) { buzzerPin = (pin === undefined || pin === null) ? null : pin; }, - setServoPins(pins) { servoPins = pins || []; }, - getServoPulseUs(pin) { - return (pin >= 0 && pin < servoPulseUs.length) ? servoPulseUs[pin] : 0; - }, - - loadHex(hexString) { - rxQueue.length = 0; - txLine = ''; - servoRise.fill(-1); - servoPulseUs.fill(0); - const program = parseHex(hexString); - cpu = new CPU(program); - timer0 = new AVRTimer(cpu, timer0Config); - timer1 = new AVRTimer(cpu, timer1Config); - timer2 = new AVRTimer(cpu, timer2Config); - usart = new AVRUSART(cpu, usart0Config, CLOCK); - usart.onByteTransmit = (value) => { - const ch = String.fromCharCode(value); - if (ch === '\n') { - if (onSerialPrint) onSerialPrint(txLine); - txLine = ''; - } else if (ch !== '\r') { - txLine += ch; - } - }; - portB = new AVRIOPort(cpu, portBConfig); - portB.addListener((v, o) => handlePortChange(8, v, o)); - portC = new AVRIOPort(cpu, portCConfig); - portC.addListener((v, o) => handlePortChange(14, v, o)); - portD = new AVRIOPort(cpu, portDConfig); - portD.addListener((v, o) => handlePortChange(0, v, o)); - adc = new AVRADC(cpu, adcConfig); - // The bus is rebuilt with the program: claims belong to the parts of the - // run that just ended, and its traffic to the sketch that just stopped. - i2c.reset(); - attachTwi(); - fd.reset(); - }, - - queueSerialInput(text) { - for (let i = 0; i < text.length; i++) rxQueue.push(text.charCodeAt(i) & 0xff); - }, - - tick(cycles) { - if (!cpu) return; - dutyTotal = 0; - dutyHigh.fill(0); - const limit = cpu.cycles + cycles; - let i = 0; - while (cpu.cycles < limit) { - avrInstruction(cpu); - cpu.tick(); - - if ((i & DUTY_SAMPLE_MASK) === 0) { - dutyTotal++; - for (let p = 0; p < DUTY_PINS; p++) if (pinState(p)) dutyHigh[p]++; - } - - // Feed queued serial input as the receiver frees up. - if (rxQueue.length && usart.writeByte && usart.writeByte(rxQueue[0])) { - rxQueue.shift(); - } - - if (fd.checkTimeout(cpu.cycles)) { - if (onBuzzerFrequency) onBuzzerFrequency(null); - } - i++; - } - }, - - getPinState(pin) { return cpu ? pinState(pin) : false; }, - - getPinDuty(pin) { - if (!cpu) return 0; - if (pin < 0 || pin >= DUTY_PINS) return pinState(pin) ? 1.0 : 0.0; - if (dutyTotal === 0) return pinState(pin) ? 1.0 : 0.0; - return dutyHigh[pin] / dutyTotal; - }, - - isPinOutput(pin) { - if (!cpu) return false; - if (pin >= 0 && pin <= 7) return (cpu.data[portDConfig.DDR] & (1 << pin)) !== 0; - if (pin >= 8 && pin <= 13) return (cpu.data[portBConfig.DDR] & (1 << (pin - 8))) !== 0; - if (pin >= 14 && pin <= 19) return (cpu.data[portCConfig.DDR] & (1 << (pin - 14))) !== 0; - return false; - }, - - setDigitalPin(pin, isHigh) { - if (!cpu) return; - if (pin >= 0 && pin <= 7) portD.setPin(pin, isHigh); - else if (pin >= 8 && pin <= 13) portB.setPin(pin - 8, isHigh); - else if (pin >= 14 && pin <= 19) portC.setPin(pin - 14, isHigh); - }, - - setAnalogVoltage(channel, voltage) { - if (adc && channel >= 0 && channel < adc.channelValues.length) { - adc.channelValues[channel] = voltage; - } - }, - - getPortBValue() { return cpu ? cpu.data[0x25] : 0; }, - getCycles() { return cpu ? cpu.cycles : 0; }, - getPin13State() { return portB ? portB.pinState(5) === PinState.High : false; }, -}; - -console.log('AVR8 (avr8js) bridge loaded.'); diff --git a/web/index.html b/web/index.html index a5e218b..b323421 100644 --- a/web/index.html +++ b/web/index.html @@ -119,7 +119,6 @@
Loading PinBench…
-