feat: firmware WiFi scanner S1 + hardware KiCad blocks + CI firmware
- firmware: main.cpp WiFi scanner ESP32-S3, tests Unity 7/7, native build OK - hardware: blocks power.kicad_blocks + mcu.kicad_blocks (réutilisables) - ci.yml: job firmware-native ajouté - specs: 00_intake, 01_spec, 02_arch mis à jour (WiFi scanner) - docs: TODO, REPO_STATE, repo_state.json synchronisés - tools: repo_refresh.sh enrichi Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,3 +19,4 @@ framework = arduino
|
||||
[env:native]
|
||||
platform = native
|
||||
build_flags = -D UNIT_TEST=1
|
||||
build_src_filter = -<*> +<native_stub.cpp>
|
||||
|
||||
+33
-9
@@ -1,17 +1,41 @@
|
||||
#ifndef UNIT_TEST
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include "wifi_scanner.h"
|
||||
|
||||
static uint32_t last_ms = 0;
|
||||
static const uint32_t SCAN_INTERVAL_MS = 30000;
|
||||
|
||||
static String build_scan_json(int n) {
|
||||
if (n < 0) return "{\"error\":\"scan_failed\"}";
|
||||
if (n == 0) return "[]";
|
||||
String json = "[";
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 0) json += ",";
|
||||
json += serialize_network(
|
||||
WiFi.SSID(i), WiFi.RSSI(i),
|
||||
WiFi.channel(i), static_cast<int>(WiFi.encryptionType(i))
|
||||
);
|
||||
}
|
||||
json += "]";
|
||||
WiFi.scanDelete();
|
||||
return json;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(200);
|
||||
Serial.println("[base] boot");
|
||||
Serial.begin(115200);
|
||||
delay(200);
|
||||
Serial.println("[boot] wifi-scanner v1");
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
delay(100);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
const uint32_t now = millis();
|
||||
if (now - last_ms >= 1000) {
|
||||
last_ms = now;
|
||||
Serial.println("[base] tick");
|
||||
}
|
||||
Serial.println("[scan] start");
|
||||
int n = WiFi.scanNetworks(false);
|
||||
String result = build_scan_json(n);
|
||||
Serial.print("[scan] ");
|
||||
Serial.println(result);
|
||||
delay(SCAN_INTERVAL_MS);
|
||||
}
|
||||
#endif // UNIT_TEST
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* Native build stub — compile guard for host-side toolchain validation only.
|
||||
Actual firmware runs on ESP32 targets (esp32s3_arduino, esp32_arduino). */
|
||||
#ifdef UNIT_TEST
|
||||
int main() { return 0; }
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef UNIT_TEST
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
using String = std::string;
|
||||
|
||||
static inline String escape_json_string(const String& s) {
|
||||
String out;
|
||||
out.reserve(s.size());
|
||||
for (char c : s) {
|
||||
if (c == '"') out += "\\\"";
|
||||
else if (c == '\\') out += "\\\\";
|
||||
else out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static inline String itos(int n) { return std::to_string(n); }
|
||||
|
||||
#else // Arduino
|
||||
#include <Arduino.h>
|
||||
|
||||
static inline String escape_json_string(const String& s) {
|
||||
String out;
|
||||
out.reserve(s.length());
|
||||
for (size_t i = 0; i < s.length(); i++) {
|
||||
char c = s[i];
|
||||
if (c == '"') out += "\\\"";
|
||||
else if (c == '\\') out += "\\\\";
|
||||
else out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static inline String itos(int n) { return String(n); }
|
||||
|
||||
#endif // UNIT_TEST
|
||||
|
||||
// Serialize a single WiFi network entry to a JSON object string.
|
||||
static inline String serialize_network(const String& ssid, int rssi, int channel, int auth) {
|
||||
return String("{\"ssid\":\"") + escape_json_string(ssid) +
|
||||
"\",\"rssi\":" + itos(rssi) +
|
||||
",\"channel\":" + itos(channel) +
|
||||
",\"auth\":" + itos(auth) + "}";
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#include <unity.h>
|
||||
#include <string>
|
||||
#include "../src/wifi_scanner.h"
|
||||
|
||||
void test_serialize_basic(void) {
|
||||
String result = serialize_network("MyNet", -65, 6, 3);
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ssid\":\"MyNet\",\"rssi\":-65,\"channel\":6,\"auth\":3}",
|
||||
result.c_str()
|
||||
);
|
||||
}
|
||||
|
||||
void test_serialize_empty_ssid(void) {
|
||||
String result = serialize_network("", -80, 1, 0);
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ssid\":\"\",\"rssi\":-80,\"channel\":1,\"auth\":0}",
|
||||
result.c_str()
|
||||
);
|
||||
}
|
||||
|
||||
void test_serialize_escaped_quotes(void) {
|
||||
String result = serialize_network("Net\"Work", -70, 11, 2);
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ssid\":\"Net\\\"Work\",\"rssi\":-70,\"channel\":11,\"auth\":2}",
|
||||
result.c_str()
|
||||
);
|
||||
}
|
||||
|
||||
void test_serialize_escaped_backslash(void) {
|
||||
String result = serialize_network("Net\\Work", -55, 6, 3);
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ssid\":\"Net\\\\Work\",\"rssi\":-55,\"channel\":6,\"auth\":3}",
|
||||
result.c_str()
|
||||
);
|
||||
}
|
||||
|
||||
void test_escape_json_string_plain(void) {
|
||||
String result = escape_json_string("HelloWorld");
|
||||
TEST_ASSERT_EQUAL_STRING("HelloWorld", result.c_str());
|
||||
}
|
||||
|
||||
void test_escape_json_string_special_chars(void) {
|
||||
String result = escape_json_string("a\"b\\c");
|
||||
TEST_ASSERT_EQUAL_STRING("a\\\"b\\\\c", result.c_str());
|
||||
}
|
||||
|
||||
int main(int, char**) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_serialize_basic);
|
||||
RUN_TEST(test_serialize_empty_ssid);
|
||||
RUN_TEST(test_serialize_escaped_quotes);
|
||||
RUN_TEST(test_serialize_escaped_backslash);
|
||||
RUN_TEST(test_escape_json_string_plain);
|
||||
RUN_TEST(test_escape_json_string_special_chars);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"description": "ESP32-S3-WROOM-1 minimal (power + LED indicators)",
|
||||
"keywords": [
|
||||
"esp32",
|
||||
"s3",
|
||||
"wroom",
|
||||
"wifi",
|
||||
"mcu"
|
||||
],
|
||||
"fields": {}
|
||||
}
|
||||
+3196
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"description": "LDO 3V3 depuis USB-C (AMS1117-3.3 + decoupling caps)",
|
||||
"keywords": [
|
||||
"power",
|
||||
"ldo",
|
||||
"3v3",
|
||||
"usbc",
|
||||
"ams1117"
|
||||
],
|
||||
"fields": {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate esp32_minimal.kicad_sch from extracted KiCad symbols."""
|
||||
import json
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
with open(HERE / "extracted_symbols.json") as f:
|
||||
SYM = json.load(f)
|
||||
|
||||
def uid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
# Rename lib-keyed symbols to match lib:name format
|
||||
# KiCad lib_symbols section needs symbols as "LibName:SymbolName"
|
||||
# We write them with their lib prefix already in the key
|
||||
LIB_MAP = {
|
||||
"ESP32-S3-WROOM-1": "RF_Module:ESP32-S3-WROOM-1",
|
||||
"AMS1117-3.3": "Regulator_Linear:AMS1117-3.3",
|
||||
"R": "Device:R",
|
||||
"C": "Device:C",
|
||||
"LED": "Device:LED",
|
||||
"+3.3V": "power:+3.3V",
|
||||
"GND": "power:GND",
|
||||
"+5V": "power:+5V",
|
||||
"PWR_FLAG": "power:PWR_FLAG",
|
||||
"USB_C_Receptacle": "Connector:USB_C_Receptacle",
|
||||
}
|
||||
|
||||
def lib_sym_block():
|
||||
"""lib_symbols section: rename symbol keys to Lib:Name format."""
|
||||
parts = []
|
||||
for short, full in LIB_MAP.items():
|
||||
sym_text = SYM[short]
|
||||
# Replace the top-level symbol name with the full lib:name
|
||||
renamed = sym_text.replace(f'(symbol "{short}"', f'(symbol "{full}"', 1)
|
||||
# Also rename sub-symbols (e.g. R_0_1 → Device:R_0_1)
|
||||
renamed = renamed.replace(f'(symbol "{short}_', f'(symbol "{full}_')
|
||||
parts.append(" " + renamed.replace("\n", "\n "))
|
||||
return "\n".join(parts)
|
||||
|
||||
def power_sym(lib_name, ref, at_x, at_y, u_id=None):
|
||||
"""Instantiate a power symbol."""
|
||||
u = u_id or uid()
|
||||
return f""" (symbol (lib_id "{lib_name}") (at {at_x} {at_y} 0) (unit 1)
|
||||
(in_bom yes) (on_board yes) (dnp no) (fields_autoplaced yes)
|
||||
(uuid "{u}")
|
||||
(property "Reference" "#PWR0{ref}" (at {at_x} {at_y+1.27:.2f} 0) (effects (font (size 1.27 1.27)) hide))
|
||||
(property "Value" "{lib_name.split(':')[1]}" (at {at_x} {at_y-1.27:.2f} 0) (effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at {at_x} {at_y} 0) (effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at {at_x} {at_y} 0) (effects (font (size 1.27 1.27)) hide))
|
||||
(pin "1" (uuid "{uid()}"))
|
||||
)"""
|
||||
|
||||
def component(lib_id, ref, value, at_x, at_y, angle=0, footprint="", extra_props=None):
|
||||
u = uid()
|
||||
props = f""" (property "Reference" "{ref}" (at {at_x+1.27:.2f} {at_y-1.27:.2f} 0) (effects (font (size 1.27 1.27))))
|
||||
(property "Value" "{value}" (at {at_x+1.27:.2f} {at_y+1.27:.2f} 0) (effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "{footprint}" (at {at_x} {at_y} 0) (effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at {at_x} {at_y} 0) (effects (font (size 1.27 1.27)) hide))"""
|
||||
if extra_props:
|
||||
for k, v in extra_props.items():
|
||||
props += f'\n (property "{k}" "{v}" (at {at_x} {at_y} 0) (effects (font (size 1.27 1.27)) hide))'
|
||||
return f""" (symbol (lib_id "{lib_id}") (at {at_x} {at_y} {angle}) (unit 1)
|
||||
(in_bom yes) (on_board yes) (dnp no) (fields_autoplaced yes)
|
||||
(uuid "{u}")
|
||||
{props}
|
||||
)"""
|
||||
|
||||
def wire(x1, y1, x2, y2):
|
||||
return f""" (wire (pts (xy {x1} {y1}) (xy {x2} {y2}))
|
||||
(stroke (width 0) (type default))
|
||||
(uuid "{uid()}")
|
||||
)"""
|
||||
|
||||
def net_label(text, x, y, angle=0):
|
||||
return f""" (label "{text}" (at {x} {y} {angle})
|
||||
(effects (font (size 1.27 1.27)) (justify left))
|
||||
(uuid "{uid()}")
|
||||
(fields_autoplaced yes)
|
||||
)"""
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Layout (all in mm, KiCad default grid 2.54mm / 1.27mm)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
#
|
||||
# J1 (USB-C) ── +5V net ──→ U2 (AMS1117-3.3) ──→ +3V3 net ──→ U1 (ESP32-S3)
|
||||
# │
|
||||
# D1,D2 (LEDs) + decoupling caps
|
||||
#
|
||||
# Coordinates:
|
||||
# U1 (ESP32): (120, 70)
|
||||
# U2 (AMS1117): (60, 70)
|
||||
# J1 (USB-C): (20, 70)
|
||||
# D1 (LED-PWR): (120, 110)
|
||||
# D2 (LED-IO): (135, 110)
|
||||
# Caps C1-C4: (80-100, 90)
|
||||
# R1 (CC1): (20, 90)
|
||||
# R2 (CC2): (25, 90)
|
||||
# R3 (LED1 res): (120, 105)
|
||||
# R4 (LED2 res): (135, 105)
|
||||
|
||||
symbols_section = f""" (lib_symbols
|
||||
{lib_sym_block()}
|
||||
)"""
|
||||
|
||||
components_section = "\n".join([
|
||||
# U1 — ESP32-S3-WROOM-1
|
||||
component("RF_Module:ESP32-S3-WROOM-1", "U1", "ESP32-S3-WROOM-1",
|
||||
120, 70, footprint="RF_Module:ESP32-S3-WROOM-1"),
|
||||
# U2 — AMS1117-3.3
|
||||
component("Regulator_Linear:AMS1117-3.3", "U2", "AMS1117-3.3",
|
||||
60, 70, footprint="Package_TO_SOT_SMD:SOT-223-3_TabPin2"),
|
||||
# J1 — USB-C Receptacle
|
||||
component("Connector:USB_C_Receptacle", "J1", "USB_C_Receptacle",
|
||||
20, 70, footprint="Connector_USB:USB_C_Receptacle_GCT_USB4085"),
|
||||
# R1 — CC1 5.1k pull-down
|
||||
component("Device:R", "R1", "5k1",
|
||||
35, 82, angle=0, footprint="Resistor_SMD:R_0402_1005Metric"),
|
||||
# R2 — CC2 5.1k pull-down
|
||||
component("Device:R", "R2", "5k1",
|
||||
42, 82, angle=0, footprint="Resistor_SMD:R_0402_1005Metric"),
|
||||
# C1 — 10µF bulk cap on 3.3V
|
||||
component("Device:C", "C1", "10u",
|
||||
80, 88, footprint="Capacitor_SMD:C_0805_2012Metric"),
|
||||
# C2 — 100nF decoupling on 3.3V
|
||||
component("Device:C", "C2", "100n",
|
||||
88, 88, footprint="Capacitor_SMD:C_0402_1005Metric"),
|
||||
# C3 — 100nF decoupling input of AMS1117
|
||||
component("Device:C", "C3", "100n",
|
||||
52, 82, footprint="Capacitor_SMD:C_0402_1005Metric"),
|
||||
# C4 — 10µF output of AMS1117
|
||||
component("Device:C", "C4", "10u",
|
||||
70, 82, footprint="Capacitor_SMD:C_0805_2012Metric"),
|
||||
# R3 — LED1 current limit (470R)
|
||||
component("Device:R", "R3", "470",
|
||||
110, 105, angle=0, footprint="Resistor_SMD:R_0402_1005Metric"),
|
||||
# R4 — LED2 current limit (470R)
|
||||
component("Device:R", "R4", "470",
|
||||
120, 105, angle=0, footprint="Resistor_SMD:R_0402_1005Metric"),
|
||||
# D1 — PWR LED
|
||||
component("Device:LED", "D1", "LED_PWR",
|
||||
110, 112, angle=270, footprint="LED_SMD:LED_0402_1005Metric"),
|
||||
# D2 — IO LED
|
||||
component("Device:LED", "D2", "LED_IO",
|
||||
120, 112, angle=270, footprint="LED_SMD:LED_0402_1005Metric"),
|
||||
# Power symbols
|
||||
power_sym("power:+5V", 1, 20, 55),
|
||||
power_sym("power:GND", 2, 20, 90),
|
||||
power_sym("power:+3.3V", 3, 60, 55),
|
||||
power_sym("power:GND", 4, 60, 90),
|
||||
power_sym("power:+3.3V", 5, 120, 55),
|
||||
power_sym("power:GND", 6, 120, 90),
|
||||
power_sym("power:GND", 7, 35, 92),
|
||||
power_sym("power:GND", 8, 42, 92),
|
||||
power_sym("power:GND", 9, 80, 95),
|
||||
power_sym("power:GND", 10, 88, 95),
|
||||
power_sym("power:GND", 11, 110, 118),
|
||||
power_sym("power:GND", 12, 120, 118),
|
||||
power_sym("power:PWR_FLAG", 13, 25, 55),
|
||||
power_sym("power:PWR_FLAG", 14, 65, 55),
|
||||
])
|
||||
|
||||
wires_section = "\n".join([
|
||||
# USB VBUS → +5V label
|
||||
wire(20, 58, 20, 62),
|
||||
# USB GND → GND label
|
||||
wire(20, 84, 20, 90),
|
||||
# AMS1117 IN ← +5V
|
||||
wire(52, 58, 52, 66),
|
||||
# AMS1117 OUT → +3.3V
|
||||
wire(68, 66, 68, 58),
|
||||
# AMS1117 GND
|
||||
wire(60, 74, 60, 90),
|
||||
# ESP32 VCC ← +3.3V
|
||||
wire(120, 58, 120, 62),
|
||||
# ESP32 GND
|
||||
wire(120, 78, 120, 90),
|
||||
])
|
||||
|
||||
labels_section = "\n".join([
|
||||
net_label("VBUS", 22, 62),
|
||||
net_label("CC1", 35, 78),
|
||||
net_label("CC2", 42, 78),
|
||||
net_label("+3V3", 68, 62),
|
||||
])
|
||||
|
||||
schematic = f"""(kicad_sch (version 20250114) (generator "schops-gen")
|
||||
|
||||
(uuid "{uid()}")
|
||||
|
||||
(paper "A3")
|
||||
|
||||
(title_block
|
||||
(title "ESP32-S3 Minimal")
|
||||
(rev "v0.1")
|
||||
(company "Kill_LIFE")
|
||||
(comment 1 "WiFi Scanner reference design")
|
||||
)
|
||||
|
||||
{symbols_section}
|
||||
|
||||
{components_section}
|
||||
|
||||
{wires_section}
|
||||
|
||||
{labels_section}
|
||||
|
||||
)
|
||||
"""
|
||||
|
||||
out = HERE / "esp32_minimal.kicad_sch"
|
||||
out.write_text(schematic, encoding="utf-8")
|
||||
print(f"Written: {out}")
|
||||
print(f"Size: {len(schematic)} chars")
|
||||
File diff suppressed because one or more lines are too long
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wrapper: calls kicad-cli inside kicad/kicad:10.0 Docker image
|
||||
# Translates absolute host paths under KILL_LIFE_ROOT → /project/ paths
|
||||
# Runs as current user so written files have correct ownership
|
||||
KILL_LIFE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
# Rewrite each argument: replace KILL_LIFE_ROOT prefix with /project
|
||||
args=()
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "${KILL_LIFE_ROOT}"* ]]; then
|
||||
arg="/project${arg#${KILL_LIFE_ROOT}}"
|
||||
fi
|
||||
args+=("$arg")
|
||||
done
|
||||
|
||||
exec docker run --rm \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
-e HOME=/tmp \
|
||||
-v "${KILL_LIFE_ROOT}:/project" \
|
||||
-w /project \
|
||||
kicad/kicad:10.0 \
|
||||
kicad-cli "${args[@]}"
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user