feat: multi-arch Docker (amd64+arm64) and fix LED ground check
Docker multi-arch: - Dockerfile downloads arch-specific QEMU .so via TARGETARCH - docker-publish.yml adds setup-qemu-action and platforms: linux/amd64,linux/arm64 - qemu-lcgamboa submodule updated (matrix build for both architectures) LED fix: - LEDs now require cathode wired to GND (or LOW GPIO) to light up - Previously LEDs turned on with anode HIGH regardless of cathode connection - Updated tests to verify anode+cathode behavior
This commit is contained in:
parent
97f8f6cd52
commit
a3db014d2d
|
|
@ -21,6 +21,9 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU (for multi-arch builds)
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
|
@ -50,6 +53,7 @@ jobs:
|
|||
with:
|
||||
context: .
|
||||
file: Dockerfile.standalone
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,33 @@
|
|||
# ---- Stage 0: QEMU .so + ROM binaries ----
|
||||
# Two modes:
|
||||
# 1. Local: place files in prebuilt/qemu/ (from build-qemu.sh or manual copy)
|
||||
# 2. CI/CD: downloads from GitHub Release (requires qemu-lcgamboa repo to be public)
|
||||
# The COPY always runs; the RUN only downloads missing files.
|
||||
# Downloads arch-specific .so from GitHub Release (e.g. libqemu-xtensa-amd64.so)
|
||||
# and renames to libqemu-xtensa.so so the backend needs no changes.
|
||||
# Local prebuilt files (prebuilt/qemu/) are used if present.
|
||||
FROM ubuntu:22.04 AS qemu-provider
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG QEMU_RELEASE_URL=https://github.com/davidmonterocrespo24/velxio/releases/download/qemu-prebuilt
|
||||
|
||||
# Copy the prebuilt directory (may contain .so+ROM files or just the .gitkeep)
|
||||
RUN mkdir -p /qemu
|
||||
COPY prebuilt/qemu/ /qemu/
|
||||
|
||||
# Download any missing files from GitHub Release
|
||||
# Download arch-specific .so and arch-independent ROM files
|
||||
RUN cd /qemu \
|
||||
&& for f in libqemu-xtensa.so libqemu-riscv32.so esp32-v3-rom.bin esp32-v3-rom-app.bin esp32c3-rom.bin; do \
|
||||
&& for base in libqemu-xtensa libqemu-riscv32; do \
|
||||
f="${base}.so" ; \
|
||||
if [ ! -f "$f" ]; then \
|
||||
echo "Downloading $f from release..." ; \
|
||||
echo "Downloading ${base}-${TARGETARCH}.so → $f ..." ; \
|
||||
curl -fSL -o "$f" "${QEMU_RELEASE_URL}/${base}-${TARGETARCH}.so" ; \
|
||||
else \
|
||||
echo "Using local $f ($(stat -c%s "$f") bytes)" ; \
|
||||
fi ; \
|
||||
done \
|
||||
&& for f in esp32-v3-rom.bin esp32-v3-rom-app.bin esp32c3-rom.bin; do \
|
||||
if [ ! -f "$f" ]; then \
|
||||
echo "Downloading $f ..." ; \
|
||||
curl -fSL -o "$f" "${QEMU_RELEASE_URL}/$f" ; \
|
||||
else \
|
||||
echo "Using local $f ($(stat -c%s "$f") bytes)" ; \
|
||||
|
|
|
|||
|
|
@ -120,26 +120,51 @@ describe('PartSimulationRegistry — registration', () => {
|
|||
|
||||
// ─── LED ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('LED — onPinStateChange', () => {
|
||||
it('sets element.value = true when anode pin goes HIGH', () => {
|
||||
describe('LED — attachEvents (anode + cathode check)', () => {
|
||||
it('LED turns on when anode HIGH and cathode wired to GND', () => {
|
||||
const logic = PartSimulationRegistry.get('led')!;
|
||||
const el = makeElement({ value: false });
|
||||
logic.onPinStateChange!('A', true, el);
|
||||
const sim = makeSimulator();
|
||||
// A → GPIO pin 13, C → GND (-1)
|
||||
logic.attachEvents!(el, sim as any, pinMap({ 'A': 13, 'C': -1 }), 'led-1');
|
||||
|
||||
// pinManager.onPinChange should be called for anode (pin 13)
|
||||
const calls = sim.pinManager.onPinChange.mock.calls;
|
||||
const anodeCall = calls.find((c: any) => c[0] === 13);
|
||||
expect(anodeCall).toBeDefined();
|
||||
|
||||
// Simulate anode going HIGH
|
||||
anodeCall;
|
||||
expect((el as any).value).toBe(true);
|
||||
});
|
||||
|
||||
it('sets element.value = false when anode pin goes LOW', () => {
|
||||
it('LED stays off when anode HIGH but cathode not wired', () => {
|
||||
const logic = PartSimulationRegistry.get('led')!;
|
||||
const el = makeElement({ value: true });
|
||||
logic.onPinStateChange!('A', false, el);
|
||||
const el = makeElement({ value: false });
|
||||
const sim = makeSimulator();
|
||||
// A → pin 13, C → not wired (null)
|
||||
logic.attachEvents!(el, sim as any, pinMap({ 'A': 13 }), 'led-2');
|
||||
|
||||
const calls = sim.pinManager.onPinChange.mock.calls;
|
||||
const anodeCall = calls.find((c: any) => c[0] === 13);
|
||||
expect(anodeCall).toBeDefined();
|
||||
|
||||
// Simulate anode going HIGH — should NOT light up
|
||||
anodeCall;
|
||||
expect((el as any).value).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores non-anode pins (e.g. cathode K)', () => {
|
||||
it('LED turns off when anode goes LOW', () => {
|
||||
const logic = PartSimulationRegistry.get('led')!;
|
||||
const el = makeElement({ value: false });
|
||||
logic.onPinStateChange!('K', true, el);
|
||||
expect((el as any).value).toBe(false); // unchanged
|
||||
const sim = makeSimulator();
|
||||
logic.attachEvents!(el, sim as any, pinMap({ 'A': 13, 'C': -1 }), 'led-3');
|
||||
|
||||
const anodeCall = sim.pinManager.onPinChange.mock.calls.find((c: any) => c[0] === 13);
|
||||
anodeCall;
|
||||
expect((el as any).value).toBe(true);
|
||||
anodeCall;
|
||||
expect((el as any).value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -125,14 +125,49 @@ PartSimulationRegistry.register('dip-switch-8', {
|
|||
});
|
||||
|
||||
/**
|
||||
* Basic LED implementation
|
||||
* Basic LED implementation.
|
||||
*
|
||||
* An LED lights up only when current can flow: anode HIGH **and** cathode
|
||||
* connected to GND (or a LOW GPIO). If the cathode is not wired at all the
|
||||
* LED stays off regardless of the anode state.
|
||||
*/
|
||||
PartSimulationRegistry.register('led', {
|
||||
onPinStateChange: (pinName, state, element) => {
|
||||
if (pinName === 'A') { // Anode
|
||||
(element as any).value = state;
|
||||
attachEvents: (element, simulator, getArduinoPinHelper) => {
|
||||
const pinManager = (simulator as any).pinManager;
|
||||
if (!pinManager) return () => {};
|
||||
|
||||
const el = element as any;
|
||||
const unsubs: (() => void)[] = [];
|
||||
let anodeHigh = false;
|
||||
let cathodeLow = false;
|
||||
|
||||
const update = () => { el.value = anodeHigh && cathodeLow; };
|
||||
|
||||
// Cathode pin: -1 means wired to GND (always LOW), >=0 means GPIO
|
||||
const cathodePin = getArduinoPinHelper('C');
|
||||
if (cathodePin === -1) {
|
||||
// Wired to GND — always LOW
|
||||
cathodeLow = true;
|
||||
} else if (cathodePin !== null && cathodePin >= 0) {
|
||||
// Wired to a GPIO — track its state
|
||||
unsubs.push(pinManager.onPinChange(cathodePin, (_: number, state: boolean) => {
|
||||
cathodeLow = !state; // cathode needs to be LOW for current to flow
|
||||
update();
|
||||
}));
|
||||
}
|
||||
// cathodePin === null → not wired → cathodeLow stays false → LED off
|
||||
|
||||
// Anode pin
|
||||
const anodePin = getArduinoPinHelper('A');
|
||||
if (anodePin !== null && anodePin >= 0) {
|
||||
unsubs.push(pinManager.onPinChange(anodePin, (_: number, state: boolean) => {
|
||||
anodeHigh = state;
|
||||
update();
|
||||
}));
|
||||
}
|
||||
|
||||
return () => { unsubs.forEach(u => u()); };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 4694eb508ed17d7166b51f5e62c04167f3f8cc19
|
||||
Subproject commit 092ad4333d148ac57d28035ee5ae3e8f9b819313
|
||||
Loading…
Reference in New Issue