feat: add protocol selection and editable properties in component dialogs
This commit is contained in:
parent
5e372f1418
commit
97f8f6cd52
|
|
@ -247,9 +247,17 @@
|
||||||
"name": "imageData",
|
"name": "imageData",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"control": "text"
|
"control": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "protocol",
|
||||||
|
"type": "select",
|
||||||
|
"control": "select",
|
||||||
|
"options": ["i2c", "spi"],
|
||||||
|
"defaultValue": "i2c",
|
||||||
|
"description": "Communication protocol"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultValues": {},
|
"defaultValues": { "protocol": "i2c" },
|
||||||
"pinCount": 0,
|
"pinCount": 0,
|
||||||
"tags": [
|
"tags": [
|
||||||
"ssd1306"
|
"ssd1306"
|
||||||
|
|
@ -1324,9 +1332,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "color",
|
"name": "color",
|
||||||
"type": "string",
|
"type": "select",
|
||||||
"defaultValue": "red",
|
"defaultValue": "red",
|
||||||
"control": "text"
|
"control": "select",
|
||||||
|
"options": ["red", "green", "blue", "yellow", "orange", "white", "purple"],
|
||||||
|
"description": "LED color"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "lightColor",
|
"name": "lightColor",
|
||||||
|
|
|
||||||
|
|
@ -266,11 +266,31 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
||||||
marginTop: '4px',
|
marginTop: '4px',
|
||||||
color: '#666',
|
color: '#666',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '4px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{properties.pin !== undefined
|
{properties.pin !== undefined
|
||||||
? `Pin ${properties.pin}`
|
? `Pin ${properties.pin}`
|
||||||
: metadata.name}
|
: metadata.name}
|
||||||
|
{properties.protocol && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '9px',
|
||||||
|
padding: '1px 4px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
backgroundColor: properties.protocol === 'spi' ? '#e67e22' : '#3498db',
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
lineHeight: '1.2',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{String(properties.protocol)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,56 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.property-edit-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-edit-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-edit-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-edit-select {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 100px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-edit-select:hover {
|
||||||
|
border-color: #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-edit-select:focus {
|
||||||
|
border-color: #007acc;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-edit-select option {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.property-actions {
|
.property-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ interface ComponentPropertyDialogProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onRotate: (componentId: string) => void;
|
onRotate: (componentId: string) => void;
|
||||||
onDelete: (componentId: string) => void;
|
onDelete: (componentId: string) => void;
|
||||||
|
onPropertyChange?: (componentId: string, propertyName: string, value: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = ({
|
export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = ({
|
||||||
|
|
@ -29,6 +30,7 @@ export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = (
|
||||||
onClose,
|
onClose,
|
||||||
onRotate,
|
onRotate,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onPropertyChange,
|
||||||
}) => {
|
}) => {
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
const [dialogPosition, setDialogPosition] = useState({ x: 0, y: 0 });
|
const [dialogPosition, setDialogPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
@ -142,6 +144,36 @@ export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = (
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Editable Properties (select dropdowns) */}
|
||||||
|
{componentMetadata.properties
|
||||||
|
.filter((p: any) => p.control === 'select' && p.options)
|
||||||
|
.length > 0 && (
|
||||||
|
<div className="property-edit-section">
|
||||||
|
{componentMetadata.properties
|
||||||
|
.filter((p: any) => p.control === 'select' && p.options)
|
||||||
|
.map((prop: any) => (
|
||||||
|
<div key={prop.name} className="property-edit-row">
|
||||||
|
<label className="property-edit-label">
|
||||||
|
{prop.description || prop.name}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="property-edit-select"
|
||||||
|
value={String(componentProperties[prop.name] ?? prop.defaultValue ?? '')}
|
||||||
|
onChange={(e) =>
|
||||||
|
onPropertyChange?.(componentId, prop.name, e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{prop.options.map((opt: string) => (
|
||||||
|
<option key={opt} value={opt}>
|
||||||
|
{opt.toUpperCase()}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="property-actions">
|
<div className="property-actions">
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1533,6 +1533,14 @@ export const SimulatorCanvas = () => {
|
||||||
removeComponent(id);
|
removeComponent(id);
|
||||||
setShowPropertyDialog(false);
|
setShowPropertyDialog(false);
|
||||||
}}
|
}}
|
||||||
|
onPropertyChange={(id, propName, value) => {
|
||||||
|
const comp = components.find((c) => c.id === id);
|
||||||
|
if (comp) {
|
||||||
|
updateComponent(id, {
|
||||||
|
properties: { ...comp.properties, [propName]: value },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { PartSimulationRegistry } from './PartSimulationRegistry';
|
||||||
import { VirtualDS1307, VirtualBMP280, VirtualDS3231, VirtualPCF8574 } from '../I2CBusManager';
|
import { VirtualDS1307, VirtualBMP280, VirtualDS3231, VirtualPCF8574 } from '../I2CBusManager';
|
||||||
import type { I2CDevice } from '../I2CBusManager';
|
import type { I2CDevice } from '../I2CBusManager';
|
||||||
import { registerSensorUpdate, unregisterSensorUpdate } from '../SensorUpdateRegistry';
|
import { registerSensorUpdate, unregisterSensorUpdate } from '../SensorUpdateRegistry';
|
||||||
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -37,30 +38,24 @@ function removeI2CDevice(simulator: any, address: number): void {
|
||||||
// ─── SSD1306 OLED ────────────────────────────────────────────────────────────
|
// ─── SSD1306 OLED ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Virtual SSD1306 OLED — full I2C command & GDDRAM decoder.
|
* SSD1306Core — shared GDDRAM buffer, command decoder, and rendering logic.
|
||||||
*
|
*
|
||||||
* Supported features:
|
* The SSD1306 command set is identical for I2C and SPI; only the transport
|
||||||
* - Control byte 0x00 = command stream, 0x40 = GDDRAM data stream
|
* differs. This core is used by both VirtualSSD1306 (I2C) and
|
||||||
|
* attachSSD1306SPI (SPI).
|
||||||
|
*
|
||||||
|
* Supported commands:
|
||||||
* - 0x20 Set Memory Addressing Mode (horizontal / vertical / page)
|
* - 0x20 Set Memory Addressing Mode (horizontal / vertical / page)
|
||||||
* - 0x21 Set Column Address
|
* - 0x21 Set Column Address
|
||||||
* - 0x22 Set Page Address
|
* - 0x22 Set Page Address
|
||||||
* - 0x40–0x7F Set Display Start Line
|
* - 0x40–0x7F Set Display Start Line
|
||||||
* - 0xAF Display ON / 0xAE Display OFF
|
* - 0xAF Display ON / 0xAE Display OFF
|
||||||
* - All other single-byte and parameterized commands are parsed but ignored
|
* - All other parameterized commands are parsed but ignored.
|
||||||
* (contrast, charge pump, COM pin config, etc.)
|
|
||||||
*
|
|
||||||
* On STOP the 1024-byte framebuffer is written to element.buffer so that
|
|
||||||
* wokwi-ssd1306 renders the pixels.
|
|
||||||
*/
|
*/
|
||||||
class VirtualSSD1306 implements I2CDevice {
|
class SSD1306Core {
|
||||||
address: number;
|
|
||||||
|
|
||||||
/** 1024-byte GDDRAM: 8 pages × 128 columns. Each byte = 8 vertical pixels. */
|
/** 1024-byte GDDRAM: 8 pages × 128 columns. Each byte = 8 vertical pixels. */
|
||||||
readonly buffer = new Uint8Array(128 * 8);
|
readonly buffer = new Uint8Array(128 * 8);
|
||||||
|
|
||||||
private ctrlByte = true; // waiting for control byte after I2C address
|
|
||||||
private isData = false; // true → GDDRAM write; false → command stream
|
|
||||||
|
|
||||||
// GDDRAM cursor
|
// GDDRAM cursor
|
||||||
private col = 0;
|
private col = 0;
|
||||||
private page = 0;
|
private page = 0;
|
||||||
|
|
@ -72,14 +67,10 @@ class VirtualSSD1306 implements I2CDevice {
|
||||||
|
|
||||||
// Multi-byte command accumulation
|
// Multi-byte command accumulation
|
||||||
private cmdBuf: number[] = [];
|
private cmdBuf: number[] = [];
|
||||||
private cmdWant = 0; // remaining param bytes for current command
|
private cmdWant = 0;
|
||||||
|
|
||||||
constructor(address: number, private element: HTMLElement) {
|
|
||||||
this.address = address;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** How many parameter bytes does this command require? */
|
/** How many parameter bytes does this command require? */
|
||||||
private static cmdParams(cmd: number): number {
|
static cmdParams(cmd: number): number {
|
||||||
if (cmd === 0x20 || cmd === 0x81 || cmd === 0x8D ||
|
if (cmd === 0x20 || cmd === 0x81 || cmd === 0x8D ||
|
||||||
cmd === 0xA8 || cmd === 0xD3 || cmd === 0xD5 ||
|
cmd === 0xA8 || cmd === 0xD3 || cmd === 0xD5 ||
|
||||||
cmd === 0xD8 || cmd === 0xD9 || cmd === 0xDA || cmd === 0xDB) return 1;
|
cmd === 0xD8 || cmd === 0xD9 || cmd === 0xDA || cmd === 0xDB) return 1;
|
||||||
|
|
@ -87,35 +78,23 @@ class VirtualSSD1306 implements I2CDevice {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeByte(value: number): boolean {
|
/** Write a data byte to GDDRAM and advance cursor. */
|
||||||
// ── Control byte (first after I2C address) ──────────────────────────
|
writeData(value: number): void {
|
||||||
if (this.ctrlByte) {
|
this.buffer[this.page * 128 + this.col] = value;
|
||||||
this.isData = (value & 0x40) !== 0;
|
this.advanceCursor();
|
||||||
this.ctrlByte = false;
|
}
|
||||||
this.cmdBuf = [];
|
|
||||||
this.cmdWant = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GDDRAM write ────────────────────────────────────────────────────
|
/** Feed a command or parameter byte. Multi-byte commands are accumulated. */
|
||||||
if (this.isData) {
|
writeCommand(value: number): void {
|
||||||
this.buffer[this.page * 128 + this.col] = value;
|
|
||||||
this.advanceCursor();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Command stream ──────────────────────────────────────────────────
|
|
||||||
if (this.cmdWant > 0) {
|
if (this.cmdWant > 0) {
|
||||||
this.cmdBuf.push(value);
|
this.cmdBuf.push(value);
|
||||||
this.cmdWant--;
|
this.cmdWant--;
|
||||||
if (this.cmdWant === 0) this.applyCmd();
|
if (this.cmdWant === 0) this.applyCmd();
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
this.cmdBuf = [value];
|
||||||
this.cmdBuf = [value];
|
this.cmdWant = SSD1306Core.cmdParams(value);
|
||||||
this.cmdWant = VirtualSSD1306.cmdParams(value);
|
|
||||||
if (this.cmdWant === 0) this.applyCmd();
|
if (this.cmdWant === 0) this.applyCmd();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyCmd(): void {
|
private applyCmd(): void {
|
||||||
|
|
@ -133,8 +112,7 @@ class VirtualSSD1306 implements I2CDevice {
|
||||||
this.page = this.pageStart;
|
this.page = this.pageStart;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Display start line 0x40–0x7F
|
if (cmd >= 0x40 && cmd <= 0x7F) { /* display start line — visual, skip */ }
|
||||||
if (cmd >= 0x40 && cmd <= 0x7F) { /* start line — visual, skip */ }
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -160,28 +138,17 @@ class VirtualSSD1306 implements I2CDevice {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readByte(): number { return 0xFF; }
|
|
||||||
|
|
||||||
stop(): void {
|
|
||||||
this.ctrlByte = true;
|
|
||||||
this.syncElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push the 1-bit GDDRAM buffer to the wokwi-ssd1306 web component.
|
* Push the 1-bit GDDRAM buffer to the wokwi-ssd1306 web component.
|
||||||
*
|
*
|
||||||
* wokwi-ssd1306 API:
|
* wokwi-ssd1306 API:
|
||||||
* - `element.imageData` — a 128×64 ImageData (RGBA, 4 bytes/pixel)
|
* - `element.imageData` — a 128×64 ImageData (RGBA, 4 bytes/pixel)
|
||||||
* - `element.redraw()` — flushes imageData to the internal canvas
|
* - `element.redraw()` — flushes imageData to the internal canvas
|
||||||
*
|
|
||||||
* GDDRAM layout: 8 pages × 128 columns.
|
|
||||||
* Each byte holds 8 vertical pixels; bit 0 = topmost pixel in the page.
|
|
||||||
*/
|
*/
|
||||||
private syncElement(): void {
|
syncElement(element: HTMLElement): void {
|
||||||
const el = this.element as any;
|
const el = element as any;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
// Obtain the ImageData object (initialised by wokwi-ssd1306 constructor)
|
|
||||||
let imgData: ImageData | undefined = el.imageData;
|
let imgData: ImageData | undefined = el.imageData;
|
||||||
if (!imgData || imgData.width !== 128 || imgData.height !== 64) {
|
if (!imgData || imgData.width !== 128 || imgData.height !== 64) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -191,16 +158,15 @@ class VirtualSSD1306 implements I2CDevice {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const px = imgData.data; // Uint8ClampedArray, RGBA
|
const px = imgData.data;
|
||||||
|
|
||||||
for (let page = 0; page < 8; page++) {
|
for (let page = 0; page < 8; page++) {
|
||||||
for (let col = 0; col < 128; col++) {
|
for (let col = 0; col < 128; col++) {
|
||||||
const byte = this.buffer[page * 128 + col];
|
const byte = this.buffer[page * 128 + col];
|
||||||
for (let bit = 0; bit < 8; bit++) {
|
for (let bit = 0; bit < 8; bit++) {
|
||||||
const row = page * 8 + bit;
|
const row = page * 8 + bit;
|
||||||
const lit = (byte >> bit) & 1;
|
const lit = (byte >> bit) & 1;
|
||||||
const idx = (row * 128 + col) * 4;
|
const idx = (row * 128 + col) * 4;
|
||||||
// Lit pixel: bright cyan-white typical of OLED; off pixel: full black
|
|
||||||
px[idx] = lit ? 200 : 0; // R
|
px[idx] = lit ? 200 : 0; // R
|
||||||
px[idx + 1] = lit ? 230 : 0; // G
|
px[idx + 1] = lit ? 230 : 0; // G
|
||||||
px[idx + 2] = lit ? 255 : 0; // B
|
px[idx + 2] = lit ? 255 : 0; // B
|
||||||
|
|
@ -210,14 +176,122 @@ class VirtualSSD1306 implements I2CDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
el.imageData = imgData;
|
el.imageData = imgData;
|
||||||
if (typeof el.redraw === 'function') {
|
if (typeof el.redraw === 'function') el.redraw();
|
||||||
el.redraw();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VirtualSSD1306 — I2C wrapper around SSD1306Core.
|
||||||
|
*
|
||||||
|
* Handles the I2C control byte (0x00 = command stream, 0x40 = data stream)
|
||||||
|
* and delegates command/data writes to the shared core.
|
||||||
|
*/
|
||||||
|
class VirtualSSD1306 implements I2CDevice {
|
||||||
|
address: number;
|
||||||
|
private readonly core = new SSD1306Core();
|
||||||
|
|
||||||
|
private ctrlByte = true;
|
||||||
|
private isData = false;
|
||||||
|
|
||||||
|
constructor(address: number, private element: HTMLElement) {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expose core buffer for tests. */
|
||||||
|
get buffer(): Uint8Array { return this.core.buffer; }
|
||||||
|
|
||||||
|
writeByte(value: number): boolean {
|
||||||
|
if (this.ctrlByte) {
|
||||||
|
this.isData = (value & 0x40) !== 0;
|
||||||
|
this.ctrlByte = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.isData) {
|
||||||
|
this.core.writeData(value);
|
||||||
|
} else {
|
||||||
|
this.core.writeCommand(value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
readByte(): number { return 0xFF; }
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
this.ctrlByte = true;
|
||||||
|
this.core.syncElement(this.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach SSD1306 in SPI mode — intercepts the AVR SPI bus.
|
||||||
|
*
|
||||||
|
* Follows the same pattern as ILI9341 (ComplexParts.ts): hook spi.onByte,
|
||||||
|
* track DC pin state via PinManager, and render GDDRAM to the element.
|
||||||
|
*/
|
||||||
|
function attachSSD1306SPI(
|
||||||
|
element: HTMLElement,
|
||||||
|
simulator: any,
|
||||||
|
getPin: (name: string) => number | null,
|
||||||
|
): () => void {
|
||||||
|
const pinManager = simulator.pinManager;
|
||||||
|
const spi = simulator.spi;
|
||||||
|
if (!pinManager || !spi) return () => {};
|
||||||
|
|
||||||
|
const core = new SSD1306Core();
|
||||||
|
let dcState = false;
|
||||||
|
const unsubs: (() => void)[] = [];
|
||||||
|
|
||||||
|
// Track DC pin (LOW = command, HIGH = data)
|
||||||
|
const pinDC = getPin('DC');
|
||||||
|
if (pinDC !== null) {
|
||||||
|
unsubs.push(
|
||||||
|
pinManager.onPinChange(pinDC, (_: number, s: boolean) => { dcState = s; }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttle rendering to ~60 fps
|
||||||
|
let dirty = false;
|
||||||
|
let rafId: number | null = null;
|
||||||
|
const scheduleSync = () => {
|
||||||
|
if (rafId !== null) return;
|
||||||
|
rafId = requestAnimationFrame(() => {
|
||||||
|
rafId = null;
|
||||||
|
if (dirty) { core.syncElement(element); dirty = false; }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook AVR SPI bus (onByte + completeTransfer)
|
||||||
|
const prevOnByte = spi.onByte;
|
||||||
|
spi.onByte = (value: number) => {
|
||||||
|
if (!dcState) {
|
||||||
|
core.writeCommand(value);
|
||||||
|
} else {
|
||||||
|
core.writeData(value);
|
||||||
|
dirty = true;
|
||||||
|
scheduleSync();
|
||||||
|
}
|
||||||
|
spi.completeTransfer(0xFF);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
spi.onByte = prevOnByte;
|
||||||
|
if (rafId !== null) cancelAnimationFrame(rafId);
|
||||||
|
unsubs.forEach(u => u());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
PartSimulationRegistry.register('ssd1306', {
|
PartSimulationRegistry.register('ssd1306', {
|
||||||
attachEvents: (element, simulator, _getPin) => {
|
attachEvents: (element, simulator, getPin, componentId) => {
|
||||||
|
// Read the protocol property from the component in the store
|
||||||
|
const { components } = useSimulatorStore.getState();
|
||||||
|
const comp = components.find(c => c.id === componentId);
|
||||||
|
const protocol = (comp?.properties?.protocol as string) ?? 'i2c';
|
||||||
|
|
||||||
|
if (protocol === 'spi') {
|
||||||
|
return attachSSD1306SPI(element, simulator, getPin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// I2C mode (default)
|
||||||
const sim = simulator as any;
|
const sim = simulator as any;
|
||||||
if (typeof sim.addI2CDevice !== 'function') return () => {};
|
if (typeof sim.addI2CDevice !== 'function') return () => {};
|
||||||
const device = new VirtualSSD1306(0x3C, element);
|
const device = new VirtualSSD1306(0x3C, element);
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit ab2c9f1868b22b78510e7d7f7b88036486bef4f7
|
Subproject commit 4694eb508ed17d7166b51f5e62c04167f3f8cc19
|
||||||
Loading…
Reference in New Issue