Created
November 4, 2025 16:42
-
-
Save valentinbreiz/0edc349de8b9d82523749c8780eb8b26 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @ -0,0 +1,489 @@ | |
| # ESP32-RPi Gateway Hardware Architecture | |
| ## Overview | |
| ESP32 board controls Raspberry Pi power and captures UART output for automated testing in GitHub Actions CI/CD. | |
| ## Block Diagram | |
| ``` | |
| ┌─────────────────────────────────────────────────────────────────────────┐ | |
| │ ESP32 Gateway Board │ | |
| │ │ | |
| │ ┌─────────────┐ │ | |
| │ │ USB-C │ 5V/3A Power Input │ | |
| │ │ Power In │ │ | |
| │ └──────┬──────┘ │ | |
| │ │ │ | |
| │ ├─────────────┐ │ | |
| │ │ │ │ | |
| │ ↓ ↓ │ | |
| │ ┌─────────────┐ ┌──────────────────────────────────┐ │ | |
| │ │ ESP32 │ │ Power Control Circuit │ │ | |
| │ │ Module │ │ │ │ | |
| │ │ │ │ ┌────────────────┐ │ │ | |
| │ │ WiFi/BT │ │ │ MOSFET │ 5V/3A Out │ │ | |
| │ │ Enabled │ │ │ (IRLZ44N) ├──────────────┼────────────┐ │ | |
| │ │ │ │ │ │ │ │ │ | |
| │ │ GPIO_PWR ──┼──┼─→│ Gate │ │ │ │ | |
| │ │ │ │ │ │ │ │ │ | |
| │ │ GPIO_TX ──┼──┼──┼────────────────┼───────────────┼────┐ │ │ | |
| │ │ │ │ │ │ │ │ │ │ | |
| │ │ GPIO_RX ←─┼──┼──┼────────────────┼───────────────┼────┼───┐ │ │ | |
| │ │ │ │ │ │ │ │ │ │ │ | |
| │ │ GND ──────┼──┼─→│ Source (GND) │ │ │ │ │ │ | |
| │ │ │ │ │ │ │ │ │ │ │ | |
| │ └─────────────┘ │ │ Drain (5V In) │ │ │ │ │ │ | |
| │ │ └────────────────┘ │ │ │ │ │ | |
| │ │ │ │ │ │ │ | |
| │ └───────────────────────────────────┘ │ │ │ │ | |
| │ │ │ │ │ | |
| └────────────────────────────────────────────────────────────┼───┼───┼───┘ | |
| │ │ │ | |
| ┌────────────────────────────────────┘ │ │ | |
| │ ┌───────────────────────┘ │ | |
| │ │ ┌───────────────────────┘ | |
| │ │ │ | |
| ↓ ↓ ↓ | |
| ┌─────────────────────────────────────────┐ | |
| │ Raspberry Pi 4/5 │ | |
| │ │ | |
| │ 5V Power In (GPIO Pin 2 or USB-C) ←────┤ (from MOSFET) | |
| │ │ | |
| │ GPIO 14 (TXD) ─────────────────────────┤ (to ESP32 RX) | |
| │ │ | |
| │ GPIO 15 (RXD) ←────────────────────────┤ (from ESP32 TX) | |
| │ │ | |
| │ GND ───────────────────────────────────┤ (common ground) | |
| │ │ | |
| │ Ethernet Port (for TFTP boot) ─────────┤ (to gateway server) | |
| │ │ | |
| └──────────────────────────────────────────┘ | |
| ``` | |
| ## Component Details | |
| ### Power Control Circuit | |
| ``` | |
| +5V (from USB-C) | |
| │ | |
| │ | |
| ┌──────┴──────┐ | |
| │ Drain │ | |
| │ │ | |
| │ IRLZ44N │ (Logic-Level N-Channel MOSFET) | |
| │ MOSFET │ (Handles 5A+ continuous) | |
| │ │ | |
| │ Source │ | |
| └──────┬──────┘ | |
| │ | |
| ├──────→ RPi 5V Power (GPIO Pin 2/4 or USB-C) | |
| │ | |
| ESP32 GPIO_PWR ──┤ Gate | |
| (3.3V) │ | |
| │ | |
| ┌────┴────┐ | |
| │ 10kΩ │ (Pull-down resistor) | |
| │ │ | |
| └────┬────┘ | |
| │ | |
| GND | |
| ``` | |
| ### UART Connection | |
| ``` | |
| ESP32 Raspberry Pi | |
| GPIO_TX ────────→ GPIO 15 (RXD) | |
| GPIO_RX ←──────── GPIO 14 (TXD) | |
| GND ───────────── GND | |
| ``` | |
| ## Network Architecture | |
| ``` | |
| ┌──────────────────────────────────────────────────────────────────┐ | |
| │ GitHub Actions Runner │ | |
| │ │ | |
| │ 1. Build kernel (dotnet publish) │ | |
| │ 2. POST kernel → Gateway Server API │ | |
| │ 3. Poll job status │ | |
| │ 4. Retrieve UART logs │ | |
| │ 5. Parse test results (PASS/FAIL) │ | |
| └────────────────────────┬─────────────────────────────────────────┘ | |
| │ HTTPS/REST API | |
| ↓ | |
| ┌──────────────────────────────────────────────────────────────────┐ | |
| │ Gateway Server (ndn.voxegames.eu) │ | |
| │ │ | |
| │ ┌──────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ | |
| │ │ REST API │ │ TFTP Server │ │ Job Queue │ │ | |
| │ │ │ │ │ │ │ │ | |
| │ │ POST /jobs │ │ kernel8.img │ │ ESP32 polls │ │ | |
| │ │ GET /jobs/{id} │ │ config.txt │ │ GET /pending │ │ | |
| │ │ GET /logs/{id} │ │ bootcode.bin │ │ │ │ | |
| │ │ │ │ start.elf │ │ │ │ | |
| │ └──────────────────┘ └─────────────────┘ └─────────────────┘ │ | |
| └────────────────────────┬─────────────────────────┬───────────────┘ | |
| │ TFTP (port 69) │ HTTP polling | |
| │ │ | |
| ┌────────┴─────────┐ ┌────────┴─────────┐ | |
| │ │ │ │ | |
| ↓ │ ↓ │ | |
| ┌───────────────────────────┐ │ ┌───────────────────────────┐ | |
| │ Raspberry Pi 4/5 │ │ │ ESP32 Gateway Board │ | |
| │ │ │ │ │ | |
| │ Network Boot: │ │ │ WiFi/Ethernet: │ | |
| │ - DHCP request │ │ │ - Poll /jobs/pending │ | |
| │ - Get TFTP server IP │ │ │ - Download job │ | |
| │ - Download kernel8.img ←─┼─────┘ │ - Control RPi power │ | |
| │ - Boot test kernel │ │ - Capture UART │ | |
| │ - Output to UART ────────┼────────┼→ - POST logs to gateway │ | |
| │ │ │ │ | |
| └───────────────────────────┘ └───────────────────────────┘ | |
| ``` | |
| ## Boot Sequence | |
| ### Step-by-Step Flow | |
| 1. **GitHub Actions** builds test kernel | |
| ```bash | |
| dotnet publish -r linux-arm64 -o output-arm64/ | |
| ``` | |
| 2. **GitHub Actions** submits job to gateway | |
| ```bash | |
| curl -X POST https://ndn.voxegames.eu/jobs \ | |
| -F kernel=@output-arm64/kernel8.img \ | |
| -F test_name="Runtime.Memory" | |
| # Returns: {"jobId": "abc123"} | |
| ``` | |
| 3. **Gateway Server** saves kernel to TFTP directory | |
| ```bash | |
| cp kernel8.img /var/lib/tftpboot/rpi-test/ | |
| ``` | |
| 4. **ESP32** polls gateway every 5 seconds | |
| ```bash | |
| GET https://ndn.voxegames.eu/jobs/pending | |
| # Returns: {"jobId": "abc123", "rpi_mac": "dc:a6:32:xx:xx:xx"} | |
| ``` | |
| 5. **ESP32** powers off RPi | |
| ```c | |
| digitalWrite(GPIO_PWR, LOW); // MOSFET off, no power to RPi | |
| delay(2000); // Wait for clean shutdown | |
| ``` | |
| 6. **ESP32** powers on RPi | |
| ```c | |
| digitalWrite(GPIO_PWR, HIGH); // MOSFET on, power to RPi | |
| // RPi starts booting immediately | |
| ``` | |
| 7. **Raspberry Pi** boots via network | |
| ``` | |
| RPi Bootloader: | |
| - DHCP discover | |
| - Receive IP: 192.168.1.100 | |
| - TFTP server: 192.168.1.10 | |
| - Download kernel8.img from TFTP | |
| - Execute kernel | |
| ``` | |
| 8. **Test kernel** runs and outputs to UART | |
| ``` | |
| [TEST] Suite: Runtime.Memory | |
| [TEST] Test 1: RhpNewArray... PASS | |
| [TEST] Test 2: RhNewString... PASS | |
| [TEST] Summary: 2/2 PASSED | |
| ``` | |
| 9. **ESP32** captures UART and streams to gateway | |
| ```c | |
| while (Serial.available()) { | |
| char c = Serial.read(); | |
| http.POST("/jobs/abc123/logs", c); | |
| } | |
| ``` | |
| 10. **GitHub Actions** polls for completion | |
| ```bash | |
| while [ "$(curl https://ndn.voxegames.eu/jobs/abc123/status | jq -r .status)" != "complete" ]; do | |
| sleep 5 | |
| done | |
| ``` | |
| 11. **GitHub Actions** retrieves results | |
| ```bash | |
| curl https://ndn.voxegames.eu/jobs/abc123/logs > uart.log | |
| # Parse uart.log for PASS/FAIL | |
| ``` | |
| ## Hardware Specifications | |
| ### ESP32 Gateway Board | |
| **Microcontroller:** | |
| - ESP32-WROOM-32 or ESP32-S3 | |
| - WiFi 802.11 b/g/n | |
| - Dual-core processor | |
| - 520KB SRAM | |
| **Power:** | |
| - Input: USB-C 5V 3A (15W) | |
| - Output to RPi: 5V 3A max (sufficient for RPi 4/5) | |
| - Protection: Reverse polarity, overcurrent | |
| **Connections:** | |
| - USB-C power input | |
| - 4-pin header to RPi (5V, GND, TX, RX) | |
| - Status LEDs (Power, WiFi, RPi Power) | |
| - Reset button | |
| **Dimensions:** | |
| - ~50mm x 30mm PCB | |
| - Stackable with RPi (optional) | |
| ### Raspberry Pi Requirements | |
| **Models Supported:** | |
| - Raspberry Pi 4 Model B (all RAM variants) | |
| - Raspberry Pi 5 (recommended) | |
| **Network Boot Setup (one-time):** | |
| ```bash | |
| # Enable network boot in OTP | |
| echo "program_usb_boot_mode=1" >> /boot/config.txt | |
| sudo reboot | |
| # Verify OTP bit set | |
| vcgencmd otp_dump | grep 17: | |
| # Should show: 17:3020000a | |
| # Configure TFTP boot | |
| # Gateway server must serve: bootcode.bin, start.elf, kernel8.img, config.txt | |
| ``` | |
| **Power Requirements:** | |
| - RPi 4: 5V 3A (15W) | |
| - RPi 5: 5V 5A (25W) - may need higher current board revision | |
| ## Bill of Materials (BOM) | |
| ### ESP32 Board Components | |
| | Component | Part Number | Quantity | Cost (USD) | | |
| |-----------|-------------|----------|------------| | |
| | ESP32 Module | ESP32-WROOM-32 | 1 | $3.00 | | |
| | N-Channel MOSFET | IRLZ44N | 1 | $0.50 | | |
| | USB-C Connector | USB4105-GF-A | 1 | $1.00 | | |
| | 10kΩ Resistor | 0805 | 1 | $0.01 | | |
| | 100nF Capacitor | 0805 | 3 | $0.03 | | |
| | Voltage Regulator | AMS1117-3.3V | 1 | $0.20 | | |
| | 4-pin Header | 2.54mm | 1 | $0.10 | | |
| | Status LEDs | 0805 | 3 | $0.15 | | |
| | PCB | 50x30mm | 1 | $0.40 | | |
| | **Total** | | | **~$5.39** | | |
| **Note:** Prices are approximate, bulk orders reduce cost significantly. | |
| ## Software Components | |
| ### ESP32 Firmware (Arduino/ESP-IDF) | |
| ```c | |
| // Pseudo-code structure | |
| #include <WiFi.h> | |
| #include <HTTPClient.h> | |
| #define GPIO_PWR 25 // Controls MOSFET gate | |
| #define GPIO_TX 17 // UART TX to RPi RX | |
| #define GPIO_RX 16 // UART RX from RPi TX | |
| const char* gateway_url = "https://ndn.voxegames.eu"; | |
| String current_job_id = ""; | |
| void setup() { | |
| pinMode(GPIO_PWR, OUTPUT); | |
| digitalWrite(GPIO_PWR, LOW); // RPi off initially | |
| Serial.begin(115200); // Debug | |
| Serial2.begin(115200, SERIAL_8N1, GPIO_RX, GPIO_TX); // RPi UART | |
| WiFi.begin(ssid, password); | |
| while (WiFi.status() != WL_CONNECTED) delay(500); | |
| } | |
| void loop() { | |
| // Poll for jobs every 5 seconds | |
| if (current_job_id == "") { | |
| current_job_id = pollForJob(); | |
| if (current_job_id != "") { | |
| executeJob(current_job_id); | |
| } | |
| } | |
| // Capture UART from RPi | |
| if (current_job_id != "") { | |
| captureUART(current_job_id); | |
| } | |
| delay(100); | |
| } | |
| String pollForJob() { | |
| HTTPClient http; | |
| http.begin(String(gateway_url) + "/jobs/pending"); | |
| int code = http.GET(); | |
| if (code == 200) { | |
| String job = http.getString(); | |
| return parseJobId(job); | |
| } | |
| return ""; | |
| } | |
| void executeJob(String jobId) { | |
| // Power cycle RPi | |
| digitalWrite(GPIO_PWR, LOW); | |
| delay(2000); | |
| digitalWrite(GPIO_PWR, HIGH); | |
| // RPi will boot via TFTP with new kernel | |
| } | |
| void captureUART(String jobId) { | |
| if (Serial2.available()) { | |
| String line = Serial2.readStringUntil('\n'); | |
| // POST to gateway | |
| HTTPClient http; | |
| http.begin(String(gateway_url) + "/jobs/" + jobId + "/logs"); | |
| http.POST(line); | |
| // Check if test completed | |
| if (line.indexOf("[TEST] SUMMARY") >= 0) { | |
| http.begin(String(gateway_url) + "/jobs/" + jobId + "/complete"); | |
| http.POST(""); | |
| current_job_id = ""; // Ready for next job | |
| digitalWrite(GPIO_PWR, LOW); // Power off RPi | |
| } | |
| } | |
| } | |
| ``` | |
| ### Gateway Server API (Node.js/Python example) | |
| ```javascript | |
| // Express.js endpoints | |
| app.post('/jobs', async (req, res) => { | |
| const jobId = generateId(); | |
| const kernel = req.files.kernel; | |
| // Save kernel to TFTP directory | |
| await kernel.mv(`/var/lib/tftpboot/rpi-test/kernel8.img`); | |
| // Queue job | |
| jobs[jobId] = { status: 'pending', logs: [] }; | |
| res.json({ jobId }); | |
| }); | |
| app.get('/jobs/pending', (req, res) => { | |
| const pending = Object.entries(jobs) | |
| .find(([id, job]) => job.status === 'pending'); | |
| if (pending) { | |
| const [jobId, job] = pending; | |
| job.status = 'running'; | |
| res.json({ jobId }); | |
| } else { | |
| res.json({ jobId: null }); | |
| } | |
| }); | |
| app.post('/jobs/:id/logs', (req, res) => { | |
| const { id } = req.params; | |
| jobs[id].logs.push(req.body.line); | |
| res.sendStatus(200); | |
| }); | |
| app.post('/jobs/:id/complete', (req, res) => { | |
| const { id } = req.params; | |
| jobs[id].status = 'complete'; | |
| res.sendStatus(200); | |
| }); | |
| app.get('/jobs/:id/status', (req, res) => { | |
| const { id } = req.params; | |
| res.json({ status: jobs[id].status }); | |
| }); | |
| app.get('/jobs/:id/logs', (req, res) => { | |
| const { id } = req.params; | |
| res.send(jobs[id].logs.join('\n')); | |
| }); | |
| ``` | |
| ## Scalability | |
| ### Multiple RPi Boards | |
| ``` | |
| Gateway Server | |
| │ | |
| ├─── ESP32 #1 ───→ RPi #1 (ARM64 tests) | |
| ├─── ESP32 #2 ───→ RPi #2 (x64 tests - if using x86 SBC) | |
| └─── ESP32 #3 ───→ RPi #3 (Stress tests) | |
| ``` | |
| Each ESP32+RPi pair: | |
| - Polls for jobs with unique MAC address identification | |
| - Gateway assigns jobs based on board capabilities | |
| - Parallel test execution across hardware | |
| ## Security Considerations | |
| 1. **Gateway API Authentication:** | |
| - API keys for GitHub Actions | |
| - ESP32 client certificates | |
| - Rate limiting | |
| 2. **Network Isolation:** | |
| - Test network separate from production | |
| - Firewall rules for ESP32 → Gateway only | |
| - TFTP restricted to test subnet | |
| 3. **Firmware Updates:** | |
| - OTA updates for ESP32 | |
| - Signed firmware verification | |
| ## Cost Analysis | |
| | Component | Cost | | |
| |-----------|------| | |
| | ESP32 Board (custom PCB) | $5-10 | | |
| | Raspberry Pi 4B (4GB) | $55 | | |
| | Power supply (5V 3A) | $8 | | |
| | Ethernet cable | $3 | | |
| | **Total per test node** | **~$71-76** | | |
| **Gateway server:** Existing VPS (ndn.voxegames.eu) - $0 additional cost | |
| ## Timeline | |
| 1. **PCB Design:** 1 week (Diamond Master) | |
| 2. **PCB Manufacturing:** 2 weeks (JLCPCB/PCBWay) | |
| 3. **Assembly & Testing:** 1 week | |
| 4. **Firmware Development:** 1 week (parallel with hardware) | |
| 5. **Gateway Server API:** 1 week (parallel with hardware) | |
| 6. **Integration Testing:** 1 week | |
| **Total:** ~5-6 weeks for complete system | |
| ## References | |
| - [Raspberry Pi Network Boot](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#raspberry-pi-4-bootloader-configuration) | |
| - [ESP32 Arduino Core](https://github.com/espressif/arduino-esp32) | |
| - [TFTP Server Setup](https://help.ubuntu.com/community/TFTP) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment