Skip to content

Instantly share code, notes, and snippets.

@valentinbreiz
Created November 4, 2025 16:42
Show Gist options
  • Select an option

  • Save valentinbreiz/0edc349de8b9d82523749c8780eb8b26 to your computer and use it in GitHub Desktop.

Select an option

Save valentinbreiz/0edc349de8b9d82523749c8780eb8b26 to your computer and use it in GitHub Desktop.
@ -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