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.
|
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
|
.PHONY: build buildfs upload uploadfs monitor clean ota flash erase
|
||||||
|
|
||||||
# Serial port — override with: make upload PORT=/dev/ttyACM0
|
# Serial port — override with: make upload PORT=/dev/ttyACM1
|
||||||
PORT ?= /dev/ttyACM2
|
PORT ?= /dev/ttyACM0
|
||||||
BAUD ?= 921600
|
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)
|
# ESP32-S3 bootloader lives at 0x0
|
||||||
BOOTLOADER_OFFSET = 0x1000
|
BOOTLOADER_OFFSET = 0x0
|
||||||
|
|
||||||
# LittleFS partition offset (from partition table)
|
# LittleFS partition offset (from partition table)
|
||||||
FS_OFFSET = 0x290000
|
FS_OFFSET = 0x290000
|
||||||
@ -20,14 +20,14 @@ buildfs:
|
|||||||
pio run -t buildfs
|
pio run -t buildfs
|
||||||
|
|
||||||
upload: build
|
upload: build
|
||||||
$(ESPTOOL) --chip esp32s2 --port $(PORT) --baud $(BAUD) \
|
$(ESPTOOL) --chip esp32s3 --port $(PORT) --baud $(BAUD) \
|
||||||
write_flash \
|
write_flash \
|
||||||
$(BOOTLOADER_OFFSET) $(BUILD_DIR)/bootloader.bin \
|
$(BOOTLOADER_OFFSET) $(BUILD_DIR)/bootloader.bin \
|
||||||
0x8000 $(BUILD_DIR)/partitions.bin \
|
0x8000 $(BUILD_DIR)/partitions.bin \
|
||||||
0x10000 $(BUILD_DIR)/firmware.bin
|
0x10000 $(BUILD_DIR)/firmware.bin
|
||||||
|
|
||||||
uploadfs: buildfs
|
uploadfs: buildfs
|
||||||
$(ESPTOOL) --chip esp32s2 --port $(PORT) --baud $(BAUD) \
|
$(ESPTOOL) --chip esp32s3 --port $(PORT) --baud $(BAUD) \
|
||||||
write_flash $(FS_OFFSET) $(BUILD_DIR)/littlefs.bin
|
write_flash $(FS_OFFSET) $(BUILD_DIR)/littlefs.bin
|
||||||
|
|
||||||
flash: upload uploadfs
|
flash: upload uploadfs
|
||||||
@ -36,7 +36,7 @@ monitor:
|
|||||||
pio device monitor --port $(PORT)
|
pio device monitor --port $(PORT)
|
||||||
|
|
||||||
erase:
|
erase:
|
||||||
$(ESPTOOL) --chip esp32s2 --port $(PORT) erase_flash
|
$(ESPTOOL) --chip esp32s3 --port $(PORT) erase_flash
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
pio run -t clean
|
pio run -t clean
|
||||||
|
|||||||
@ -9,14 +9,22 @@
|
|||||||
// --- WiFi Credentials ---
|
// --- WiFi Credentials ---
|
||||||
// Override via build_flags or edit directly
|
// Override via build_flags or edit directly
|
||||||
#ifndef WIFI_SSID
|
#ifndef WIFI_SSID
|
||||||
#define WIFI_SSID "your-ssid"
|
#define WIFI_SSID "tsunami4"
|
||||||
#endif
|
#endif
|
||||||
#ifndef WIFI_PASS
|
#ifndef WIFI_PASS
|
||||||
#define WIFI_PASS "your-password"
|
#define WIFI_PASS "2089916341"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define WIFI_TIMEOUT_MS 15000
|
#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) ---
|
// --- HMC472A Control Pins (active-low) ---
|
||||||
// Optimized mapping: GPIO number = step bit position + 1
|
// Optimized mapping: GPIO number = step bit position + 1
|
||||||
// Enables single-instruction bitwise ops instead of loop
|
// Enables single-instruction bitwise ops instead of loop
|
||||||
@ -46,6 +54,13 @@ static constexpr uint32_t ATTEN_PIN_MASK = 0x7E;
|
|||||||
// --- Status LED ---
|
// --- Status LED ---
|
||||||
static constexpr uint8_t PIN_LED = 15; // Built-in LED, active HIGH
|
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 ---
|
// --- Attenuator Limits ---
|
||||||
static constexpr float DB_MIN = 0.0f;
|
static constexpr float DB_MIN = 0.0f;
|
||||||
static constexpr float DB_MAX = 31.5f;
|
static constexpr float DB_MAX = 31.5f;
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
[env:lolin_s2_mini]
|
[env:esp32-s3-devkitc-1]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = lolin_s2_mini
|
board = esp32-s3-devkitc-1
|
||||||
framework = arduino
|
framework = arduino
|
||||||
|
board_build.arduino.memory_type = qio_opi ; Use octal PSRAM
|
||||||
lib_deps =
|
lib_deps =
|
||||||
mathieucarbou/ESPAsyncWebServer @ ^3.6.0
|
mathieucarbou/ESPAsyncWebServer @ ^3.6.0
|
||||||
bblanchon/ArduinoJson @ ^7.3.0
|
bblanchon/ArduinoJson @ ^7.3.0
|
||||||
|
adafruit/Adafruit SSD1306 @ ^2.5.13
|
||||||
|
adafruit/Adafruit GFX Library @ ^1.11.11
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
board_build.filesystem = littlefs
|
board_build.filesystem = littlefs
|
||||||
build_flags =
|
build_flags =
|
||||||
-DCORE_DEBUG_LEVEL=3
|
-DCORE_DEBUG_LEVEL=0
|
||||||
|
-Os
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
#include "attenuator.h"
|
#include "attenuator.h"
|
||||||
#include <USB.h>
|
|
||||||
#include <soc/gpio_struct.h>
|
#include <soc/gpio_struct.h>
|
||||||
|
|
||||||
Attenuator::Attenuator() : _step(0) {}
|
Attenuator::Attenuator() : _step(0) {}
|
||||||
@ -14,7 +13,7 @@ void Attenuator::begin() {
|
|||||||
_step = loadFromNVS();
|
_step = loadFromNVS();
|
||||||
applyToGPIO();
|
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) {
|
float Attenuator::setDB(float db) {
|
||||||
@ -36,7 +35,7 @@ uint8_t Attenuator::setStep(uint8_t step) {
|
|||||||
applyToGPIO();
|
applyToGPIO();
|
||||||
saveToNVS();
|
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;
|
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 <Arduino.h>
|
||||||
#include <USB.h>
|
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <ESPmDNS.h>
|
#include <ESPmDNS.h>
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
@ -8,10 +7,15 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "attenuator.h"
|
#include "attenuator.h"
|
||||||
#include "web_server.h"
|
#include "web_server.h"
|
||||||
|
#include "display.h"
|
||||||
|
|
||||||
// --- Global instances ---
|
// --- Global instances ---
|
||||||
Attenuator attenuator;
|
Attenuator attenuator;
|
||||||
|
|
||||||
|
// --- Display update tracking ---
|
||||||
|
static uint32_t lastDisplayUpdate = 0;
|
||||||
|
static const uint32_t DISPLAY_UPDATE_MS = 100; // Update every 100ms
|
||||||
|
|
||||||
// --- LED State Machine ---
|
// --- LED State Machine ---
|
||||||
enum class LEDState {
|
enum class LEDState {
|
||||||
Off, // Headless mode (no WiFi)
|
Off, // Headless mode (no WiFi)
|
||||||
@ -69,14 +73,14 @@ void startSweep(bool up, uint32_t dwellMs) {
|
|||||||
sweepRunning = true;
|
sweepRunning = true;
|
||||||
lastSweepStep = millis();
|
lastSweepStep = millis();
|
||||||
setLEDState(LEDState::FastBlink);
|
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);
|
up ? "up" : "down", sweepDwellMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopSweep() {
|
void stopSweep() {
|
||||||
sweepRunning = false;
|
sweepRunning = false;
|
||||||
setLEDState(LEDState::Solid);
|
setLEDState(LEDState::Solid);
|
||||||
Serial.println("[Sweep] Stopped");
|
Serial0.println("[Sweep] Stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSweeping() {
|
bool isSweeping() {
|
||||||
@ -110,13 +114,34 @@ void updateSweep() {
|
|||||||
attenuator.setStep(newStep);
|
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 ---
|
// --- WiFi Connection ---
|
||||||
bool connectWiFi() {
|
bool connectWiFi() {
|
||||||
Serial.printf("[WiFi] Connecting to %s...\n", WIFI_SSID);
|
Serial0.printf("[WiFi] Connecting to %s...\n", WIFI_SSID);
|
||||||
setLEDState(LEDState::SlowBlink);
|
setLEDState(LEDState::SlowBlink);
|
||||||
|
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
WiFi.setHostname(FW_HOSTNAME);
|
WiFi.setHostname(FW_HOSTNAME);
|
||||||
|
WiFi.setTxPower(wifiTxPower);
|
||||||
|
Serial0.printf("[WiFi] TX power: %.1f dBm\n", wifiPowerToDbm(wifiTxPower));
|
||||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||||
|
|
||||||
uint32_t startAttempt = millis();
|
uint32_t startAttempt = millis();
|
||||||
@ -126,13 +151,13 @@ bool connectWiFi() {
|
|||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
|
|
||||||
if (millis() - startAttempt > WIFI_TIMEOUT_MS) {
|
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);
|
setLEDState(LEDState::Off);
|
||||||
return false;
|
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);
|
setLEDState(LEDState::Solid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -141,9 +166,9 @@ bool connectWiFi() {
|
|||||||
void setupMDNS() {
|
void setupMDNS() {
|
||||||
if (MDNS.begin(FW_HOSTNAME)) {
|
if (MDNS.begin(FW_HOSTNAME)) {
|
||||||
MDNS.addService("http", "tcp", WEB_PORT);
|
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 {
|
} else {
|
||||||
Serial.println("[mDNS] Failed to start");
|
Serial0.println("[mDNS] Failed to start");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,26 +182,27 @@ void enableOTA() {
|
|||||||
ArduinoOTA.onStart([]() {
|
ArduinoOTA.onStart([]() {
|
||||||
stopSweep();
|
stopSweep();
|
||||||
setLEDState(LEDState::FastBlink);
|
setLEDState(LEDState::FastBlink);
|
||||||
Serial.println("[OTA] Update starting...");
|
Serial0.println("[OTA] Update starting...");
|
||||||
});
|
});
|
||||||
ArduinoOTA.onEnd([]() {
|
ArduinoOTA.onEnd([]() {
|
||||||
Serial.println("[OTA] Update complete, rebooting...");
|
Serial0.println("[OTA] Update complete, rebooting...");
|
||||||
});
|
});
|
||||||
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
|
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) {
|
ArduinoOTA.onError([](ota_error_t error) {
|
||||||
Serial.printf("[OTA] Error %u: ", error);
|
Serial0.printf("[OTA] Error %u: ", error);
|
||||||
if (error == OTA_AUTH_ERROR) Serial.println("Auth failed");
|
if (error == OTA_AUTH_ERROR) Serial0.println("Auth failed");
|
||||||
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin failed");
|
else if (error == OTA_BEGIN_ERROR) Serial0.println("Begin failed");
|
||||||
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect failed");
|
else if (error == OTA_CONNECT_ERROR) Serial0.println("Connect failed");
|
||||||
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive failed");
|
else if (error == OTA_RECEIVE_ERROR) Serial0.println("Receive failed");
|
||||||
else if (error == OTA_END_ERROR) Serial.println("End failed");
|
else if (error == OTA_END_ERROR) Serial0.println("End failed");
|
||||||
setLEDState(LEDState::Solid);
|
setLEDState(LEDState::Solid);
|
||||||
});
|
});
|
||||||
ArduinoOTA.begin();
|
ArduinoOTA.begin();
|
||||||
otaEnabled = true;
|
otaEnabled = true;
|
||||||
Serial.println("[OTA] Enabled");
|
Serial0.println("[OTA] Enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isOTAEnabled() {
|
bool isOTAEnabled() {
|
||||||
@ -185,19 +211,26 @@ bool isOTAEnabled() {
|
|||||||
|
|
||||||
// --- Setup ---
|
// --- Setup ---
|
||||||
void setup() {
|
void setup() {
|
||||||
USB.begin();
|
// UART0 on ESP32-S3-DevKitC-1 (CH343 bridge): TX=GPIO43, RX=GPIO44
|
||||||
Serial.begin(115200);
|
// These are the default pins for Serial0, no need to specify
|
||||||
delay(2000); // Allow USB CDC to enumerate on host
|
Serial0.begin(115200);
|
||||||
|
delay(100); // Brief delay for UART to stabilize
|
||||||
|
|
||||||
Serial.println();
|
Serial0.println();
|
||||||
Serial.println("================================");
|
Serial0.println("================================");
|
||||||
Serial.printf("HMC472A Attenuator Controller %s\n", FW_VERSION);
|
Serial0.printf("HMC472A Attenuator Controller %s\n", FW_VERSION);
|
||||||
Serial.println("================================");
|
Serial0.println("================================");
|
||||||
|
|
||||||
// Initialize LED
|
// Initialize LED
|
||||||
pinMode(PIN_LED, OUTPUT);
|
pinMode(PIN_LED, OUTPUT);
|
||||||
setLEDState(LEDState::Off);
|
setLEDState(LEDState::Off);
|
||||||
|
|
||||||
|
// Initialize OLED display
|
||||||
|
if (initDisplay()) {
|
||||||
|
showSplash(FW_VERSION);
|
||||||
|
delay(1500); // Show splash briefly
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize watchdog (ESP-IDF v5.x API)
|
// Initialize watchdog (ESP-IDF v5.x API)
|
||||||
esp_task_wdt_config_t wdt_config = {
|
esp_task_wdt_config_t wdt_config = {
|
||||||
.timeout_ms = WDT_TIMEOUT_S * 1000,
|
.timeout_ms = WDT_TIMEOUT_S * 1000,
|
||||||
@ -206,7 +239,7 @@ void setup() {
|
|||||||
};
|
};
|
||||||
esp_task_wdt_init(&wdt_config);
|
esp_task_wdt_init(&wdt_config);
|
||||||
esp_task_wdt_add(NULL);
|
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)
|
// Initialize attenuator (restores from NVS)
|
||||||
attenuator.begin();
|
attenuator.begin();
|
||||||
@ -222,8 +255,8 @@ void setup() {
|
|||||||
setupWebServer(attenuator);
|
setupWebServer(attenuator);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.println("[Setup] Complete");
|
Serial0.println("[Setup] Complete");
|
||||||
Serial.println();
|
Serial0.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Main Loop ---
|
// --- Main Loop ---
|
||||||
@ -236,5 +269,18 @@ void loop() {
|
|||||||
ArduinoOTA.handle();
|
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
|
delay(1); // Yield to other tasks
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
#include "web_server.h"
|
#include "web_server.h"
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <USB.h>
|
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <ESPAsyncWebServer.h>
|
#include <ESPAsyncWebServer.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
@ -17,6 +16,9 @@ extern int8_t getSweepDirection();
|
|||||||
extern uint32_t getSweepDwellMs();
|
extern uint32_t getSweepDwellMs();
|
||||||
extern void enableOTA();
|
extern void enableOTA();
|
||||||
extern bool isOTAEnabled();
|
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 AsyncWebServer server(WEB_PORT);
|
||||||
static Attenuator* pAtten = nullptr;
|
static Attenuator* pAtten = nullptr;
|
||||||
@ -128,6 +130,11 @@ static void handleConfig(AsyncWebServerRequest* request) {
|
|||||||
doc["mac"] = WiFi.macAddress();
|
doc["mac"] = WiFi.macAddress();
|
||||||
doc["ota_enabled"] = isOTAEnabled();
|
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;
|
String output;
|
||||||
serializeJson(doc, output);
|
serializeJson(doc, output);
|
||||||
|
|
||||||
@ -199,15 +206,87 @@ static void handleOTAEnable(AsyncWebServerRequest* request) {
|
|||||||
request->send(response);
|
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 ---
|
// --- Setup ---
|
||||||
void setupWebServer(Attenuator& atten) {
|
void setupWebServer(Attenuator& atten) {
|
||||||
pAtten = &atten;
|
pAtten = &atten;
|
||||||
|
|
||||||
// Initialize LittleFS
|
// Initialize LittleFS
|
||||||
if (!LittleFS.begin(true)) {
|
if (!LittleFS.begin(true)) {
|
||||||
Serial.println("[WebServer] LittleFS mount failed!");
|
Serial0.println("[WebServer] LittleFS mount failed!");
|
||||||
} else {
|
} else {
|
||||||
Serial.println("[WebServer] LittleFS mounted");
|
Serial0.println("[WebServer] LittleFS mounted");
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORS preflight handler for all routes
|
// CORS preflight handler for all routes
|
||||||
@ -223,12 +302,15 @@ void setupWebServer(Attenuator& atten) {
|
|||||||
server.on("/sweep", HTTP_GET, handleSweepStatus);
|
server.on("/sweep", HTTP_GET, handleSweepStatus);
|
||||||
server.on("/sweep/stop", HTTP_POST, handleSweepStop);
|
server.on("/sweep/stop", HTTP_POST, handleSweepStop);
|
||||||
server.on("/ota", HTTP_POST, handleOTAEnable);
|
server.on("/ota", HTTP_POST, handleOTAEnable);
|
||||||
|
server.on("/wifi/power", HTTP_GET, handleWiFiPowerGet);
|
||||||
|
|
||||||
// Routes with body parsing
|
// Routes with body parsing
|
||||||
server.on("/set", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
server.on("/set", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||||
NULL, handleSet);
|
NULL, handleSet);
|
||||||
server.on("/sweep", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
server.on("/sweep", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||||
NULL, handleSweepStart);
|
NULL, handleSweepStart);
|
||||||
|
server.on("/wifi/power", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||||
|
NULL, handleWiFiPowerSet);
|
||||||
|
|
||||||
// Static files from LittleFS (index.html, style.css, app.js, favicon.svg)
|
// Static files from LittleFS (index.html, style.css, app.js, favicon.svg)
|
||||||
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
|
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
|
||||||
@ -242,5 +324,5 @@ void setupWebServer(Attenuator& atten) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.begin();
|
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