refactor(ssd1306): drop the i2c/spi aliases; CS-only auto-detect + protocol pin

Follow-up to the SSD1306 picker consolidation. All 68 saved projects that used
the retired ssd1306-i2c / ssd1306-spi ids have been migrated to the single
`ssd1306` (metadataId rewritten, protocol pinned), so the simulation aliases
are no longer needed and are removed.

- Auto-detect refined to CS-only: chip-select is the SPI-exclusive signal;
  DC does NOT imply SPI (on the 8-pin module DC doubles as the I2C address /
  SA0 line, so many I2C circuits wire it). Fixes false-SPI on those circuits.
- The `ssd1306` part honors an explicit `protocol` property when present
  (migrated legacy projects carry it) and auto-detects otherwise.
- loadProjectState normalizes any lingering ssd1306-i2c/spi ids (old .vlx
  files, pre-migration snapshots) to `ssd1306` + the matching protocol, so
  removing the aliases can never blank an old import.
This commit is contained in:
David Montero 2026-07-09 22:13:31 +02:00
parent b6dd2f5201
commit b51bbcf06d
3 changed files with 61 additions and 50 deletions

View File

@ -17,6 +17,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { PartSimulationRegistry } from '../simulation/parts/PartSimulationRegistry'; import { PartSimulationRegistry } from '../simulation/parts/PartSimulationRegistry';
import { dispatchSensorUpdate } from '../simulation/SensorUpdateRegistry'; import { dispatchSensorUpdate } from '../simulation/SensorUpdateRegistry';
import { useSimulatorStore } from '../store/useSimulatorStore';
import '../simulation/parts/ProtocolParts'; import '../simulation/parts/ProtocolParts';
// ─── Globals ────────────────────────────────────────────────────────────────── // ─── Globals ──────────────────────────────────────────────────────────────────
@ -228,31 +229,35 @@ describe('ssd1306 — protocol auto-detect', () => {
expect(sim.addI2CDevice).not.toHaveBeenCalled(); expect(sim.addI2CDevice).not.toHaveBeenCalled();
}); });
it('runs SPI when DC is wired to a GPIO', () => { it('runs I2C when only DC is wired (DC is the I2C address-select, not SPI)', () => {
const sim = makeSPISim(); const sim = makeI2CSim();
PartSimulationRegistry.get('ssd1306')!.attachEvents!( PartSimulationRegistry.get('ssd1306')!.attachEvents!(
makeElement(), makeElement(),
sim as any, sim as any,
pinMap({ DC: 4 }), pinMap({ DC: 4 }),
); );
expect(typeof sim.spi.onByte).toBe('function');
expect(sim.addI2CDevice).not.toHaveBeenCalled();
});
it('legacy ssd1306-i2c alias forces I2C even with CS wired', () => {
const sim = makeI2CSim();
PartSimulationRegistry.get('ssd1306-i2c')!.attachEvents!(
makeElement(),
sim as any,
pinMap({ CS: 5 }),
);
expect(sim.addI2CDevice).toHaveBeenCalledOnce(); expect(sim.addI2CDevice).toHaveBeenCalledOnce();
}); });
it('legacy ssd1306-spi alias forces SPI even with nothing wired', () => { it('honors an explicit protocol property (migrated legacy projects)', () => {
const sim = makeSPISim(); // A project migrated from the old ssd1306-spi entry carries protocol:'spi';
PartSimulationRegistry.get('ssd1306-spi')!.attachEvents!(makeElement(), sim as any, noPins); // it must run SPI even though nothing SPI-specific is wired (CS absent).
expect(typeof sim.spi.onByte).toBe('function'); useSimulatorStore.setState({
components: [{ id: 'oled-legacy', metadataId: 'ssd1306', properties: { protocol: 'spi' } }],
} as any);
try {
const sim = makeSPISim();
PartSimulationRegistry.get('ssd1306')!.attachEvents!(
makeElement(),
sim as any,
noPins,
'oled-legacy',
);
expect(typeof sim.spi.onByte).toBe('function');
expect(sim.addI2CDevice).not.toHaveBeenCalled();
} finally {
useSimulatorStore.setState({ components: [] } as any);
}
}); });
}); });

View File

@ -329,8 +329,8 @@ function attachSSD1306SPI(
/** /**
* Internal: SSD1306 attach logic, parameterised over the wire protocol. * Internal: SSD1306 attach logic, parameterised over the wire protocol.
* Used by the three picker entries (the generic `ssd1306` plus the two * Called by the single `ssd1306` entry once the protocol has been resolved
* dedicated `ssd1306-i2c` / `ssd1306-spi` shortcuts). * (auto-detected from the wiring, or read from an explicit `protocol` property).
*/ */
function attachSSD1306( function attachSSD1306(
element: HTMLElement, element: HTMLElement,
@ -376,52 +376,43 @@ function attachSSD1306(
} }
/** /**
* Which wire protocol did the user build? A real SSD1306 breakout is ONE * Which wire protocol did the user build? A real SSD1306 breakout is ONE board
* board that talks either I2C or SPI depending on how it is wired: SPI drives * that talks either I2C or SPI depending on how it is wired. The definitive
* the chip-select (CS) and data/command (DC) lines from MCU GPIOs, while I2C * SPI-only signal is chip-select (CS): I2C never uses it. (DC deliberately does
* leaves them tied to power (address select) or unconnected. So if CS or DC is * NOT count on the 8-pin module DC doubles as the I2C address-select/SA0 line,
* wired to a GPIO we decode SPI; otherwise I2C. This mirrors the physical part * so many I2C circuits wire it too.) So CS wired to a GPIO => SPI, otherwise
* one component, no protocol switch to set, just wire it up. * I2C. This mirrors the physical part one component, no protocol switch to
* set, just wire it up.
* *
* Pure wiring check: it deliberately does NOT read `simulator.spi`, whose * Pure wiring check: it deliberately does NOT read `simulator.spi`, whose getter
* getter on some boards (RP2040, and the ESP32/STM32 bridge shims) lazily * on some boards (RP2040, and the ESP32/STM32 bridge shims) lazily re-routes the
* re-routes the board SPI bus as a side effect and must not fire in I2C mode. * board SPI bus as a side effect and must not fire in I2C mode.
*/ */
function detectSSD1306Protocol(getPin: (n: string) => number | null): 'i2c' | 'spi' { function detectSSD1306Protocol(getPin: (n: string) => number | null): 'i2c' | 'spi' {
return getPin('CS') !== null || getPin('DC') !== null ? 'spi' : 'i2c'; return getPin('CS') !== null ? 'spi' : 'i2c';
} }
/** /**
* SSD1306 OLED a single component that works on every board with an I2C or * SSD1306 OLED a single component that works on every board with an I2C or
* SPI bus (AVR, RP2040, ESP32, STM32), auto-detecting the protocol from the * SPI bus (AVR, RP2040, ESP32, STM32). New projects just wire it up and the
* wiring like the physical module. Consolidates the old ssd1306 / ssd1306-i2c * protocol is auto-detected from the wiring like the physical module; a
* / ssd1306-spi picker entries into one (issues #101 / #215). * `protocol` property, when present, pins it explicitly (projects migrated from
* the old ssd1306-i2c / ssd1306-spi entries carry it so their behaviour is
* preserved exactly). Consolidates the old three picker entries into one
* (issues #101 / #215).
*/ */
PartSimulationRegistry.register('ssd1306', { PartSimulationRegistry.register('ssd1306', {
attachEvents: (element, simulator, getPin, componentId) => { attachEvents: (element, simulator, getPin, componentId) => {
const { components } = useSimulatorStore.getState(); const { components } = useSimulatorStore.getState();
const comp = components.find((c) => c.id === componentId); const comp = components.find((c) => c.id === componentId);
const i2cAddr = parseI2cAddress(comp?.properties?.i2cAddress, 0x3c); const i2cAddr = parseI2cAddress(comp?.properties?.i2cAddress, 0x3c);
const protocol = detectSSD1306Protocol(getPin); const explicit = comp?.properties?.protocol;
const protocol: 'i2c' | 'spi' =
explicit === 'i2c' || explicit === 'spi' ? explicit : detectSSD1306Protocol(getPin);
return attachSSD1306(element, simulator, getPin, protocol, i2cAddr); return attachSSD1306(element, simulator, getPin, protocol, i2cAddr);
}, },
}); });
/**
* Legacy aliases kept ONLY for projects saved before the picker entries merged
* into the single auto-detecting `ssd1306` above. New projects never carry
* these ids. They force a fixed protocol (no auto-detect) to reproduce the old
* behaviour exactly.
*/
PartSimulationRegistry.register('ssd1306-i2c', {
attachEvents: (element, simulator, getPin) =>
attachSSD1306(element, simulator, getPin, 'i2c'),
});
PartSimulationRegistry.register('ssd1306-spi', {
attachEvents: (element, simulator, getPin) =>
attachSSD1306(element, simulator, getPin, 'spi'),
});
// ─── DS1307 RTC ────────────────────────────────────────────────────────────── // ─── DS1307 RTC ──────────────────────────────────────────────────────────────
/** /**

View File

@ -1374,8 +1374,23 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
// (createFileGroup is a no-op for existing ids) — overwrite their files. // (createFileGroup is a no-op for existing ids) — overwrite their files.
useEditorStore.getState().replaceFileGroups(payload.fileGroups); useEditorStore.getState().replaceFileGroups(payload.fileGroups);
// Components and wires // Components and wires. Normalize the retired ssd1306-i2c / ssd1306-spi
setComponents(payload.components); // ids (merged into the single auto-detecting `ssd1306`, issues #101/#215)
// so old .vlx files and pre-migration snapshots still render and simulate;
// the old id's protocol is pinned so behaviour is preserved exactly.
const normalizedComponents = payload.components.map((c) =>
c.metadataId === 'ssd1306-i2c' || c.metadataId === 'ssd1306-spi'
? {
...c,
metadataId: 'ssd1306',
properties: {
protocol: c.metadataId === 'ssd1306-spi' ? 'spi' : 'i2c',
...(c.properties ?? {}),
},
}
: c,
);
setComponents(normalizedComponents);
setWires(payload.wires); setWires(payload.wires);
// Active board: prefer the saved one, fall back to the first. // Active board: prefer the saved one, fall back to the first.