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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU (for multi-arch builds)
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
|
@ -50,6 +53,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile.standalone
|
file: Dockerfile.standalone
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,33 @@
|
||||||
# ---- Stage 0: QEMU .so + ROM binaries ----
|
# ---- Stage 0: QEMU .so + ROM binaries ----
|
||||||
# Two modes:
|
# Downloads arch-specific .so from GitHub Release (e.g. libqemu-xtensa-amd64.so)
|
||||||
# 1. Local: place files in prebuilt/qemu/ (from build-qemu.sh or manual copy)
|
# and renames to libqemu-xtensa.so so the backend needs no changes.
|
||||||
# 2. CI/CD: downloads from GitHub Release (requires qemu-lcgamboa repo to be public)
|
# Local prebuilt files (prebuilt/qemu/) are used if present.
|
||||||
# The COPY always runs; the RUN only downloads missing files.
|
|
||||||
FROM ubuntu:22.04 AS qemu-provider
|
FROM ubuntu:22.04 AS qemu-provider
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
|
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
ARG QEMU_RELEASE_URL=https://github.com/davidmonterocrespo24/velxio/releases/download/qemu-prebuilt
|
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)
|
# Copy the prebuilt directory (may contain .so+ROM files or just the .gitkeep)
|
||||||
RUN mkdir -p /qemu
|
RUN mkdir -p /qemu
|
||||||
COPY prebuilt/qemu/ /qemu/
|
COPY prebuilt/qemu/ /qemu/
|
||||||
|
|
||||||
# Download any missing files from GitHub Release
|
# Download arch-specific .so and arch-independent ROM files
|
||||||
RUN cd /qemu \
|
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 \
|
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" ; \
|
curl -fSL -o "$f" "${QEMU_RELEASE_URL}/$f" ; \
|
||||||
else \
|
else \
|
||||||
echo "Using local $f ($(stat -c%s "$f") bytes)" ; \
|
echo "Using local $f ($(stat -c%s "$f") bytes)" ; \
|
||||||
|
|
|
||||||
|
|
@ -120,26 +120,51 @@ describe('PartSimulationRegistry — registration', () => {
|
||||||
|
|
||||||
// ─── LED ─────────────────────────────────────────────────────────────────────
|
// ─── LED ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('LED — onPinStateChange', () => {
|
describe('LED — attachEvents (anode + cathode check)', () => {
|
||||||
it('sets element.value = true when anode pin goes HIGH', () => {
|
it('LED turns on when anode HIGH and cathode wired to GND', () => {
|
||||||
const logic = PartSimulationRegistry.get('led')!;
|
const logic = PartSimulationRegistry.get('led')!;
|
||||||
const el = makeElement({ value: false });
|
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);
|
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 logic = PartSimulationRegistry.get('led')!;
|
||||||
const el = makeElement({ value: true });
|
const el = makeElement({ value: false });
|
||||||
logic.onPinStateChange!('A', false, el);
|
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);
|
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 logic = PartSimulationRegistry.get('led')!;
|
||||||
const el = makeElement({ value: false });
|
const el = makeElement({ value: false });
|
||||||
logic.onPinStateChange!('K', true, el);
|
const sim = makeSimulator();
|
||||||
expect((el as any).value).toBe(false); // unchanged
|
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', {
|
PartSimulationRegistry.register('led', {
|
||||||
onPinStateChange: (pinName, state, element) => {
|
attachEvents: (element, simulator, getArduinoPinHelper) => {
|
||||||
if (pinName === 'A') { // Anode
|
const pinManager = (simulator as any).pinManager;
|
||||||
(element as any).value = state;
|
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