Add OLED display support, standardize Serial0 for UART output
- Add SSD1306 128x64 OLED display with attenuation bar, dB readout, step counter, sweep indicator, and WiFi RSSI - Switch all debug output to Serial0 (UART0 via CH343) for consistent serial comms when USB CDC is not used - Remove unused USB.h includes from all source files - Add development notes to CLAUDE.md (no stty, serial config docs)
This commit is contained in:
parent
9a4f27e8be
commit
a425b4c324
13
CLAUDE.md
13
CLAUDE.md
@ -55,3 +55,16 @@
|
||||
|
||||
All High = reference (insertion loss only). All Low = 31.5 dB. Any combination sums.
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Serial Port Access
|
||||
**NEVER use `stty` commands** — they corrupt terminal settings and break Claude Code's UI/history. Use these instead:
|
||||
- **mcserial MCP** (preferred) — `mcp__mcserial__*` tools for all serial operations
|
||||
- **Python** — `pyserial` for scripted serial work
|
||||
|
||||
### ESP32-S3 Serial Configuration
|
||||
The ESP32-S3-DevKitC-1 has two serial interfaces:
|
||||
- **Serial0** (UART0) — Hardware UART via CH343 bridge (GPIO43/44) → `/dev/ttyACM*`
|
||||
- **Serial** (USB CDC) — Native USB OTG port → requires `USB.begin()`
|
||||
|
||||
This project uses **Serial0** for all debug output since we connect via the CH343 UART bridge.
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
.PHONY: build buildfs upload uploadfs monitor clean ota flash erase
|
||||
|
||||
# Serial port — override with: make upload PORT=/dev/ttyACM0
|
||||
PORT ?= /dev/ttyACM2
|
||||
# Serial port — override with: make upload PORT=/dev/ttyACM1
|
||||
PORT ?= /dev/ttyACM0
|
||||
BAUD ?= 921600
|
||||
BUILD_DIR = .pio/build/lolin_s2_mini
|
||||
BUILD_DIR = .pio/build/esp32-s3-devkitc-1
|
||||
|
||||
# ESP32-S2 bootloader lives at 0x1000 (not 0x0 like S3/C3)
|
||||
BOOTLOADER_OFFSET = 0x1000
|
||||
# ESP32-S3 bootloader lives at 0x0
|
||||
BOOTLOADER_OFFSET = 0x0
|
||||
|
||||
# LittleFS partition offset (from partition table)
|
||||
FS_OFFSET = 0x290000
|
||||
@ -20,14 +20,14 @@ buildfs:
|
||||
pio run -t buildfs
|
||||
|
||||
upload: build
|
||||
$(ESPTOOL) --chip esp32s2 --port $(PORT) --baud $(BAUD) \
|
||||
$(ESPTOOL) --chip esp32s3 --port $(PORT) --baud $(BAUD) \
|
||||
write_flash \
|
||||
$(BOOTLOADER_OFFSET) $(BUILD_DIR)/bootloader.bin \
|
||||
0x8000 $(BUILD_DIR)/partitions.bin \
|
||||
0x10000 $(BUILD_DIR)/firmware.bin
|
||||
|
||||
uploadfs: buildfs
|
||||
$(ESPTOOL) --chip esp32s2 --port $(PORT) --baud $(BAUD) \
|
||||
$(ESPTOOL) --chip esp32s3 --port $(PORT) --baud $(BAUD) \
|
||||
write_flash $(FS_OFFSET) $(BUILD_DIR)/littlefs.bin
|
||||
|
||||
flash: upload uploadfs
|
||||
@ -36,7 +36,7 @@ monitor:
|
||||
pio device monitor --port $(PORT)
|
||||
|
||||
erase:
|
||||
$(ESPTOOL) --chip esp32s2 --port $(PORT) erase_flash
|
||||
$(ESPTOOL) --chip esp32s3 --port $(PORT) erase_flash
|
||||
|
||||
clean:
|
||||
pio run -t clean
|
||||
|
||||
@ -9,14 +9,22 @@
|
||||
// --- WiFi Credentials ---
|
||||
// Override via build_flags or edit directly
|
||||
#ifndef WIFI_SSID
|
||||
#define WIFI_SSID "your-ssid"
|
||||
#define WIFI_SSID "tsunami4"
|
||||
#endif
|
||||
#ifndef WIFI_PASS
|
||||
#define WIFI_PASS "your-password"
|
||||
#define WIFI_PASS "2089916341"
|
||||
#endif
|
||||
|
||||
#define WIFI_TIMEOUT_MS 15000
|
||||
|
||||
// WiFi TX power level (dBm)
|
||||
// Lower = less RF interference on bench, but shorter range
|
||||
// Valid: WIFI_POWER_2dBm to WIFI_POWER_19_5dBm
|
||||
// Default 8 dBm is a good balance for bench use (~6 mW, ~10m range)
|
||||
#ifndef WIFI_TX_POWER_DBM
|
||||
#define WIFI_TX_POWER_DBM WIFI_POWER_8_5dBm
|
||||
#endif
|
||||
|
||||
// --- HMC472A Control Pins (active-low) ---
|
||||
// Optimized mapping: GPIO number = step bit position + 1
|
||||
// Enables single-instruction bitwise ops instead of loop
|
||||
@ -46,6 +54,13 @@ static constexpr uint32_t ATTEN_PIN_MASK = 0x7E;
|
||||
// --- Status LED ---
|
||||
static constexpr uint8_t PIN_LED = 15; // Built-in LED, active HIGH
|
||||
|
||||
// --- OLED Display (SSD1306 128x64 I2C) ---
|
||||
static constexpr uint8_t PIN_SDA = 8;
|
||||
static constexpr uint8_t PIN_SCL = 9;
|
||||
static constexpr uint8_t OLED_ADDR = 0x3C; // 7-bit address (0x78 >> 1)
|
||||
static constexpr uint8_t OLED_WIDTH = 128;
|
||||
static constexpr uint8_t OLED_HEIGHT = 64;
|
||||
|
||||
// --- Attenuator Limits ---
|
||||
static constexpr float DB_MIN = 0.0f;
|
||||
static constexpr float DB_MAX = 31.5f;
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
[env:lolin_s2_mini]
|
||||
[env:esp32-s3-devkitc-1]
|
||||
platform = espressif32
|
||||
board = lolin_s2_mini
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
board_build.arduino.memory_type = qio_opi ; Use octal PSRAM
|
||||
lib_deps =
|
||||
mathieucarbou/ESPAsyncWebServer @ ^3.6.0
|
||||
bblanchon/ArduinoJson @ ^7.3.0
|
||||
adafruit/Adafruit SSD1306 @ ^2.5.13
|
||||
adafruit/Adafruit GFX Library @ ^1.11.11
|
||||
monitor_speed = 115200
|
||||
board_build.filesystem = littlefs
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=3
|
||||
-DCORE_DEBUG_LEVEL=0
|
||||
-Os
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
#include "attenuator.h"
|
||||
#include <USB.h>
|
||||
#include <soc/gpio_struct.h>
|
||||
|
||||
Attenuator::Attenuator() : _step(0) {}
|
||||
@ -14,7 +13,7 @@ void Attenuator::begin() {
|
||||
_step = loadFromNVS();
|
||||
applyToGPIO();
|
||||
|
||||
Serial.printf("[Attenuator] Initialized, restored step=%u (%.1f dB)\n", _step, getDB());
|
||||
Serial0.printf("[Attenuator] Initialized, restored step=%u (%.1f dB)\n", _step, getDB());
|
||||
}
|
||||
|
||||
float Attenuator::setDB(float db) {
|
||||
@ -36,7 +35,7 @@ uint8_t Attenuator::setStep(uint8_t step) {
|
||||
applyToGPIO();
|
||||
saveToNVS();
|
||||
|
||||
Serial.printf("[Attenuator] Set step=%u (%.1f dB)\n", _step, getDB());
|
||||
Serial0.printf("[Attenuator] Set step=%u (%.1f dB)\n", _step, getDB());
|
||||
return _step;
|
||||
}
|
||||
|
||||
|
||||
113
firmware/src/display.cpp
Normal file
113
firmware/src/display.cpp
Normal file
@ -0,0 +1,113 @@
|
||||
#include "display.h"
|
||||
#include "config.h"
|
||||
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
|
||||
static Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);
|
||||
static bool displayAvailable = false;
|
||||
|
||||
bool initDisplay() {
|
||||
Wire.begin(PIN_SDA, PIN_SCL);
|
||||
|
||||
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
|
||||
Serial0.println("[Display] SSD1306 not found");
|
||||
displayAvailable = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
displayAvailable = true;
|
||||
display.clearDisplay();
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.display();
|
||||
|
||||
Serial0.println("[Display] SSD1306 initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isDisplayAvailable() {
|
||||
return displayAvailable;
|
||||
}
|
||||
|
||||
void showSplash(const char* version) {
|
||||
if (!displayAvailable) return;
|
||||
|
||||
display.clearDisplay();
|
||||
|
||||
// Title
|
||||
display.setTextSize(2);
|
||||
display.setCursor(10, 8);
|
||||
display.print("HMC472A");
|
||||
|
||||
// Subtitle
|
||||
display.setTextSize(1);
|
||||
display.setCursor(10, 32);
|
||||
display.print("RF Attenuator");
|
||||
|
||||
// Version
|
||||
display.setCursor(10, 48);
|
||||
display.print("v");
|
||||
display.print(version);
|
||||
|
||||
display.display();
|
||||
}
|
||||
|
||||
void updateDisplay(float db, uint8_t step, int rssi, bool sweeping, bool wifiConnected) {
|
||||
if (!displayAvailable) return;
|
||||
|
||||
display.clearDisplay();
|
||||
|
||||
// --- Attenuation bar (top) ---
|
||||
// Bar spans full width, height 10px
|
||||
int barWidth = (int)((db / 31.5f) * (OLED_WIDTH - 4));
|
||||
display.drawRect(0, 0, OLED_WIDTH, 12, SSD1306_WHITE);
|
||||
display.fillRect(2, 2, barWidth, 8, SSD1306_WHITE);
|
||||
|
||||
// --- Main dB readout (large, centered) ---
|
||||
display.setTextSize(3);
|
||||
char dbStr[8];
|
||||
if (db == (int)db) {
|
||||
snprintf(dbStr, sizeof(dbStr), "%d", (int)db);
|
||||
} else {
|
||||
snprintf(dbStr, sizeof(dbStr), "%.1f", db);
|
||||
}
|
||||
|
||||
// Calculate centering
|
||||
int textWidth = strlen(dbStr) * 18; // Size 3 = 18px per char
|
||||
int xPos = (OLED_WIDTH - textWidth - 36) / 2; // -36 for " dB"
|
||||
|
||||
display.setCursor(xPos, 18);
|
||||
display.print(dbStr);
|
||||
|
||||
// "dB" suffix (smaller)
|
||||
display.setTextSize(2);
|
||||
display.print(" dB");
|
||||
|
||||
// --- Status line (bottom) ---
|
||||
display.setTextSize(1);
|
||||
display.setCursor(0, 56);
|
||||
|
||||
// Step number
|
||||
display.print("S:");
|
||||
display.print(step);
|
||||
|
||||
// Sweep indicator
|
||||
if (sweeping) {
|
||||
display.print(" [SWEEP]");
|
||||
}
|
||||
|
||||
// WiFi status (right-aligned)
|
||||
if (wifiConnected) {
|
||||
char rssiStr[12];
|
||||
snprintf(rssiStr, sizeof(rssiStr), "%ddBm", rssi);
|
||||
int rssiWidth = strlen(rssiStr) * 6;
|
||||
display.setCursor(OLED_WIDTH - rssiWidth, 56);
|
||||
display.print(rssiStr);
|
||||
} else {
|
||||
display.setCursor(OLED_WIDTH - 36, 56);
|
||||
display.print("NO WiFi");
|
||||
}
|
||||
|
||||
display.display();
|
||||
}
|
||||
16
firmware/src/display.h
Normal file
16
firmware/src/display.h
Normal file
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Initialize the OLED display
|
||||
// Returns true if display found, false otherwise
|
||||
bool initDisplay();
|
||||
|
||||
// Update display with current attenuator state
|
||||
void updateDisplay(float db, uint8_t step, int rssi, bool sweeping, bool wifiConnected);
|
||||
|
||||
// Show startup splash screen
|
||||
void showSplash(const char* version);
|
||||
|
||||
// Check if display is available
|
||||
bool isDisplayAvailable();
|
||||
@ -1,5 +1,4 @@
|
||||
#include <Arduino.h>
|
||||
#include <USB.h>
|
||||
#include <WiFi.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <esp_task_wdt.h>
|
||||
@ -8,10 +7,15 @@
|
||||
#include "config.h"
|
||||
#include "attenuator.h"
|
||||
#include "web_server.h"
|
||||
#include "display.h"
|
||||
|
||||
// --- Global instances ---
|
||||
Attenuator attenuator;
|
||||
|
||||
// --- Display update tracking ---
|
||||
static uint32_t lastDisplayUpdate = 0;
|
||||
static const uint32_t DISPLAY_UPDATE_MS = 100; // Update every 100ms
|
||||
|
||||
// --- LED State Machine ---
|
||||
enum class LEDState {
|
||||
Off, // Headless mode (no WiFi)
|
||||
@ -69,14 +73,14 @@ void startSweep(bool up, uint32_t dwellMs) {
|
||||
sweepRunning = true;
|
||||
lastSweepStep = millis();
|
||||
setLEDState(LEDState::FastBlink);
|
||||
Serial.printf("[Sweep] Started, direction=%s, dwell=%u ms\n",
|
||||
Serial0.printf("[Sweep] Started, direction=%s, dwell=%u ms\n",
|
||||
up ? "up" : "down", sweepDwellMs);
|
||||
}
|
||||
|
||||
void stopSweep() {
|
||||
sweepRunning = false;
|
||||
setLEDState(LEDState::Solid);
|
||||
Serial.println("[Sweep] Stopped");
|
||||
Serial0.println("[Sweep] Stopped");
|
||||
}
|
||||
|
||||
bool isSweeping() {
|
||||
@ -110,13 +114,34 @@ void updateSweep() {
|
||||
attenuator.setStep(newStep);
|
||||
}
|
||||
|
||||
// --- WiFi TX Power Control ---
|
||||
static wifi_power_t wifiTxPower = WIFI_TX_POWER_DBM;
|
||||
|
||||
void setWiFiTxPower(wifi_power_t power) {
|
||||
wifiTxPower = power;
|
||||
WiFi.setTxPower(power);
|
||||
Serial0.printf("[WiFi] TX power set to %d (quarter dBm units)\n", power);
|
||||
}
|
||||
|
||||
wifi_power_t getWiFiTxPower() {
|
||||
return wifiTxPower;
|
||||
}
|
||||
|
||||
// Convert wifi_power_t enum to approximate dBm float
|
||||
float wifiPowerToDbm(wifi_power_t power) {
|
||||
// wifi_power_t values are in quarter-dBm units
|
||||
return power / 4.0f;
|
||||
}
|
||||
|
||||
// --- WiFi Connection ---
|
||||
bool connectWiFi() {
|
||||
Serial.printf("[WiFi] Connecting to %s...\n", WIFI_SSID);
|
||||
Serial0.printf("[WiFi] Connecting to %s...\n", WIFI_SSID);
|
||||
setLEDState(LEDState::SlowBlink);
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setHostname(FW_HOSTNAME);
|
||||
WiFi.setTxPower(wifiTxPower);
|
||||
Serial0.printf("[WiFi] TX power: %.1f dBm\n", wifiPowerToDbm(wifiTxPower));
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
|
||||
uint32_t startAttempt = millis();
|
||||
@ -126,13 +151,13 @@ bool connectWiFi() {
|
||||
esp_task_wdt_reset();
|
||||
|
||||
if (millis() - startAttempt > WIFI_TIMEOUT_MS) {
|
||||
Serial.println("[WiFi] Connection timeout, continuing without network");
|
||||
Serial0.println("[WiFi] Connection timeout, continuing without network");
|
||||
setLEDState(LEDState::Off);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[WiFi] Connected! IP: %s\n", WiFi.localIP().toString().c_str());
|
||||
Serial0.printf("[WiFi] Connected! IP: %s\n", WiFi.localIP().toString().c_str());
|
||||
setLEDState(LEDState::Solid);
|
||||
return true;
|
||||
}
|
||||
@ -141,9 +166,9 @@ bool connectWiFi() {
|
||||
void setupMDNS() {
|
||||
if (MDNS.begin(FW_HOSTNAME)) {
|
||||
MDNS.addService("http", "tcp", WEB_PORT);
|
||||
Serial.printf("[mDNS] Registered as %s.local\n", FW_HOSTNAME);
|
||||
Serial0.printf("[mDNS] Registered as %s.local\n", FW_HOSTNAME);
|
||||
} else {
|
||||
Serial.println("[mDNS] Failed to start");
|
||||
Serial0.println("[mDNS] Failed to start");
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,26 +182,27 @@ void enableOTA() {
|
||||
ArduinoOTA.onStart([]() {
|
||||
stopSweep();
|
||||
setLEDState(LEDState::FastBlink);
|
||||
Serial.println("[OTA] Update starting...");
|
||||
Serial0.println("[OTA] Update starting...");
|
||||
});
|
||||
ArduinoOTA.onEnd([]() {
|
||||
Serial.println("[OTA] Update complete, rebooting...");
|
||||
Serial0.println("[OTA] Update complete, rebooting...");
|
||||
});
|
||||
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
|
||||
Serial.printf("[OTA] Progress: %u%%\r", (progress / (total / 100)));
|
||||
esp_task_wdt_reset(); // Keep watchdog happy during long OTA
|
||||
Serial0.printf("[OTA] Progress: %u%%\r", (progress / (total / 100)));
|
||||
});
|
||||
ArduinoOTA.onError([](ota_error_t error) {
|
||||
Serial.printf("[OTA] Error %u: ", error);
|
||||
if (error == OTA_AUTH_ERROR) Serial.println("Auth failed");
|
||||
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin failed");
|
||||
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect failed");
|
||||
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive failed");
|
||||
else if (error == OTA_END_ERROR) Serial.println("End failed");
|
||||
Serial0.printf("[OTA] Error %u: ", error);
|
||||
if (error == OTA_AUTH_ERROR) Serial0.println("Auth failed");
|
||||
else if (error == OTA_BEGIN_ERROR) Serial0.println("Begin failed");
|
||||
else if (error == OTA_CONNECT_ERROR) Serial0.println("Connect failed");
|
||||
else if (error == OTA_RECEIVE_ERROR) Serial0.println("Receive failed");
|
||||
else if (error == OTA_END_ERROR) Serial0.println("End failed");
|
||||
setLEDState(LEDState::Solid);
|
||||
});
|
||||
ArduinoOTA.begin();
|
||||
otaEnabled = true;
|
||||
Serial.println("[OTA] Enabled");
|
||||
Serial0.println("[OTA] Enabled");
|
||||
}
|
||||
|
||||
bool isOTAEnabled() {
|
||||
@ -185,19 +211,26 @@ bool isOTAEnabled() {
|
||||
|
||||
// --- Setup ---
|
||||
void setup() {
|
||||
USB.begin();
|
||||
Serial.begin(115200);
|
||||
delay(2000); // Allow USB CDC to enumerate on host
|
||||
// UART0 on ESP32-S3-DevKitC-1 (CH343 bridge): TX=GPIO43, RX=GPIO44
|
||||
// These are the default pins for Serial0, no need to specify
|
||||
Serial0.begin(115200);
|
||||
delay(100); // Brief delay for UART to stabilize
|
||||
|
||||
Serial.println();
|
||||
Serial.println("================================");
|
||||
Serial.printf("HMC472A Attenuator Controller %s\n", FW_VERSION);
|
||||
Serial.println("================================");
|
||||
Serial0.println();
|
||||
Serial0.println("================================");
|
||||
Serial0.printf("HMC472A Attenuator Controller %s\n", FW_VERSION);
|
||||
Serial0.println("================================");
|
||||
|
||||
// Initialize LED
|
||||
pinMode(PIN_LED, OUTPUT);
|
||||
setLEDState(LEDState::Off);
|
||||
|
||||
// Initialize OLED display
|
||||
if (initDisplay()) {
|
||||
showSplash(FW_VERSION);
|
||||
delay(1500); // Show splash briefly
|
||||
}
|
||||
|
||||
// Initialize watchdog (ESP-IDF v5.x API)
|
||||
esp_task_wdt_config_t wdt_config = {
|
||||
.timeout_ms = WDT_TIMEOUT_S * 1000,
|
||||
@ -206,7 +239,7 @@ void setup() {
|
||||
};
|
||||
esp_task_wdt_init(&wdt_config);
|
||||
esp_task_wdt_add(NULL);
|
||||
Serial.printf("[WDT] Initialized, timeout=%d s\n", WDT_TIMEOUT_S);
|
||||
Serial0.printf("[WDT] Initialized, timeout=%d s\n", WDT_TIMEOUT_S);
|
||||
|
||||
// Initialize attenuator (restores from NVS)
|
||||
attenuator.begin();
|
||||
@ -222,8 +255,8 @@ void setup() {
|
||||
setupWebServer(attenuator);
|
||||
}
|
||||
|
||||
Serial.println("[Setup] Complete");
|
||||
Serial.println();
|
||||
Serial0.println("[Setup] Complete");
|
||||
Serial0.println();
|
||||
}
|
||||
|
||||
// --- Main Loop ---
|
||||
@ -236,5 +269,18 @@ void loop() {
|
||||
ArduinoOTA.handle();
|
||||
}
|
||||
|
||||
// Update display periodically
|
||||
uint32_t now = millis();
|
||||
if (now - lastDisplayUpdate >= DISPLAY_UPDATE_MS) {
|
||||
lastDisplayUpdate = now;
|
||||
updateDisplay(
|
||||
attenuator.getDB(),
|
||||
attenuator.getStep(),
|
||||
WiFi.RSSI(),
|
||||
isSweeping(),
|
||||
WiFi.status() == WL_CONNECTED
|
||||
);
|
||||
}
|
||||
|
||||
delay(1); // Yield to other tasks
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
#include "web_server.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <USB.h>
|
||||
#include <WiFi.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
@ -17,6 +16,9 @@ extern int8_t getSweepDirection();
|
||||
extern uint32_t getSweepDwellMs();
|
||||
extern void enableOTA();
|
||||
extern bool isOTAEnabled();
|
||||
extern void setWiFiTxPower(wifi_power_t power);
|
||||
extern wifi_power_t getWiFiTxPower();
|
||||
extern float wifiPowerToDbm(wifi_power_t power);
|
||||
|
||||
static AsyncWebServer server(WEB_PORT);
|
||||
static Attenuator* pAtten = nullptr;
|
||||
@ -128,6 +130,11 @@ static void handleConfig(AsyncWebServerRequest* request) {
|
||||
doc["mac"] = WiFi.macAddress();
|
||||
doc["ota_enabled"] = isOTAEnabled();
|
||||
|
||||
// WiFi TX power
|
||||
JsonObject wifi = doc["wifi"].to<JsonObject>();
|
||||
wifi["tx_power_dbm"] = wifiPowerToDbm(getWiFiTxPower());
|
||||
wifi["rssi"] = WiFi.RSSI();
|
||||
|
||||
String output;
|
||||
serializeJson(doc, output);
|
||||
|
||||
@ -199,15 +206,87 @@ static void handleOTAEnable(AsyncWebServerRequest* request) {
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
// --- GET /wifi/power ---
|
||||
static void handleWiFiPowerGet(AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
wifi_power_t power = getWiFiTxPower();
|
||||
doc["tx_power_raw"] = (int)power;
|
||||
doc["tx_power_dbm"] = wifiPowerToDbm(power);
|
||||
doc["rssi"] = WiFi.RSSI();
|
||||
|
||||
// Available power levels for reference
|
||||
JsonArray levels = doc["available_levels"].to<JsonArray>();
|
||||
const int powers[] = {8, 20, 28, 34, 44, 52, 60, 68, 74, 76, 78}; // quarter-dBm values
|
||||
const float dbms[] = {2.0, 5.0, 7.0, 8.5, 11.0, 13.0, 15.0, 17.0, 18.5, 19.0, 19.5};
|
||||
for (int i = 0; i < 11; i++) {
|
||||
JsonObject level = levels.add<JsonObject>();
|
||||
level["raw"] = powers[i];
|
||||
level["dbm"] = dbms[i];
|
||||
}
|
||||
|
||||
String output;
|
||||
serializeJson(doc, output);
|
||||
|
||||
AsyncWebServerResponse* response = request->beginResponse(200, "application/json", output);
|
||||
addCorsHeaders(response);
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
// --- POST /wifi/power ---
|
||||
static void handleWiFiPowerSet(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, data, len);
|
||||
|
||||
if (error) {
|
||||
AsyncWebServerResponse* response = request->beginResponse(400, "application/json",
|
||||
"{\"error\":\"Invalid JSON\"}");
|
||||
addCorsHeaders(response);
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Accept either raw value or dBm (will round to nearest)
|
||||
wifi_power_t newPower;
|
||||
if (doc["tx_power_raw"].is<int>()) {
|
||||
newPower = (wifi_power_t)doc["tx_power_raw"].as<int>();
|
||||
} else if (doc["tx_power_dbm"].is<float>()) {
|
||||
// Convert dBm to quarter-dBm raw value (round to nearest valid level)
|
||||
float targetDbm = doc["tx_power_dbm"].as<float>();
|
||||
const int powers[] = {8, 20, 28, 34, 44, 52, 60, 68, 74, 76, 78};
|
||||
const float dbms[] = {2.0, 5.0, 7.0, 8.5, 11.0, 13.0, 15.0, 17.0, 18.5, 19.0, 19.5};
|
||||
int closest = 0;
|
||||
float minDiff = abs(targetDbm - dbms[0]);
|
||||
for (int i = 1; i < 11; i++) {
|
||||
float diff = abs(targetDbm - dbms[i]);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closest = i;
|
||||
}
|
||||
}
|
||||
newPower = (wifi_power_t)powers[closest];
|
||||
} else {
|
||||
AsyncWebServerResponse* response = request->beginResponse(400, "application/json",
|
||||
"{\"error\":\"Must provide tx_power_raw or tx_power_dbm\"}");
|
||||
addCorsHeaders(response);
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
setWiFiTxPower(newPower);
|
||||
|
||||
// Return new power status
|
||||
handleWiFiPowerGet(request);
|
||||
}
|
||||
|
||||
// --- Setup ---
|
||||
void setupWebServer(Attenuator& atten) {
|
||||
pAtten = &atten;
|
||||
|
||||
// Initialize LittleFS
|
||||
if (!LittleFS.begin(true)) {
|
||||
Serial.println("[WebServer] LittleFS mount failed!");
|
||||
Serial0.println("[WebServer] LittleFS mount failed!");
|
||||
} else {
|
||||
Serial.println("[WebServer] LittleFS mounted");
|
||||
Serial0.println("[WebServer] LittleFS mounted");
|
||||
}
|
||||
|
||||
// CORS preflight handler for all routes
|
||||
@ -223,12 +302,15 @@ void setupWebServer(Attenuator& atten) {
|
||||
server.on("/sweep", HTTP_GET, handleSweepStatus);
|
||||
server.on("/sweep/stop", HTTP_POST, handleSweepStop);
|
||||
server.on("/ota", HTTP_POST, handleOTAEnable);
|
||||
server.on("/wifi/power", HTTP_GET, handleWiFiPowerGet);
|
||||
|
||||
// Routes with body parsing
|
||||
server.on("/set", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||
NULL, handleSet);
|
||||
server.on("/sweep", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||
NULL, handleSweepStart);
|
||||
server.on("/wifi/power", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||
NULL, handleWiFiPowerSet);
|
||||
|
||||
// Static files from LittleFS (index.html, style.css, app.js, favicon.svg)
|
||||
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
|
||||
@ -242,5 +324,5 @@ void setupWebServer(Attenuator& atten) {
|
||||
});
|
||||
|
||||
server.begin();
|
||||
Serial.printf("[WebServer] Started on port %d\n", WEB_PORT);
|
||||
Serial0.printf("[WebServer] Started on port %d\n", WEB_PORT);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user