Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions embedded/libs/esp-web-server/src/esp_web_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ void ESPWebServer::handleRoot() {
if (status.connected) {
el.className = 'status connected';
el.innerHTML = 'Connected to <strong>' + status.ssid + '</strong><br>IP: ' + status.ip + ' | Signal: ' + status.rssi + ' dBm';
} else if (status.ap_mode) {
el.className = 'status disconnected';
el.innerHTML = 'Access Point Mode<br>IP: ' + status.ip + '<br><small>Connect to a WiFi network below</small>';
} else {
el.className = 'status disconnected';
el.textContent = 'Not connected';
Expand Down Expand Up @@ -453,6 +456,11 @@ void ESPWebServer::handleWiFiConnect() {
const char* ssid = doc["ssid"];
const char* password = doc["password"] | "";

// Stop AP mode if running before connecting to a new network
if (WiFiMgr.isAPMode()) {
WiFiMgr.stopAP();
}

WiFiMgr.connect(ssid, password);

sendJson(200, "{\"success\":true,\"message\":\"Connecting...\"}");
Expand All @@ -463,9 +471,10 @@ void ESPWebServer::handleWiFiStatus() {
JsonDocument doc;

doc["connected"] = WiFiMgr.isConnected();
doc["ssid"] = WiFiMgr.getSSID();
doc["ip"] = WiFiMgr.getIP();
doc["rssi"] = WiFiMgr.getRSSI();
doc["ap_mode"] = WiFiMgr.isAPMode();
doc["ssid"] = WiFiMgr.isAPMode() ? "" : WiFiMgr.getSSID();
doc["ip"] = WiFiMgr.isAPMode() ? WiFiMgr.getAPIP() : WiFiMgr.getIP();
doc["rssi"] = WiFiMgr.isAPMode() ? 0 : WiFiMgr.getRSSI();

sendJson(200, doc);
}
Expand Down
63 changes: 63 additions & 0 deletions embedded/libs/lilygo-display/src/lilygo_display.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,69 @@ void LilyGoDisplay::showConfigPortal(const char* apName, const char* ip) {
_display.setTextDatum(lgfx::top_left);
}

void LilyGoDisplay::showSetupScreen(const char* apName) {
_display.fillScreen(COLOR_BACKGROUND);

// Header
_display.setFont(&fonts::FreeSansBold9pt7b);
_display.setTextColor(COLOR_ACCENT);
_display.setTextDatum(lgfx::top_center);
_display.drawString("WiFi Setup", SCREEN_WIDTH / 2, 8);

// Step 1: Connect to WiFi AP
_display.setFont(&fonts::Font2);
_display.setTextColor(COLOR_TEXT);
_display.drawString("1. Connect to WiFi:", SCREEN_WIDTH / 2, 38);

_display.setFont(&fonts::FreeSansBold9pt7b);
_display.setTextColor(COLOR_STATUS_OK);
_display.drawString(apName, SCREEN_WIDTH / 2, 58);

// QR Code section - generate QR for http://192.168.4.1
const char* configUrl = "http://192.168.4.1";
QRCode qrCode;
qrcode_initText(&qrCode, _qrCodeData, QR_VERSION, ECC_LOW, configUrl);

// Calculate QR code size and position
int qrSize = qrCode.size;
int pixelSize = 100 / qrSize; // Target ~100px QR code
if (pixelSize < 1) pixelSize = 1;

int actualQrSize = pixelSize * qrSize;
int qrX = (SCREEN_WIDTH - actualQrSize) / 2;
int qrY = 90;

// Draw white background for QR code
_display.fillRect(qrX - 4, qrY - 4, actualQrSize + 8, actualQrSize + 8, COLOR_QR_BG);

// Draw QR code modules
for (uint8_t y = 0; y < qrSize; y++) {
for (uint8_t x = 0; x < qrSize; x++) {
if (qrcode_getModule(&qrCode, x, y)) {
_display.fillRect(qrX + x * pixelSize, qrY + y * pixelSize, pixelSize, pixelSize, COLOR_QR_FG);
}
}
}

// Step 2: Instructions below QR code
int instructionY = qrY + actualQrSize + 16;

_display.setFont(&fonts::Font2);
_display.setTextColor(COLOR_TEXT);
_display.drawString("2. Scan QR code or", SCREEN_WIDTH / 2, instructionY);
_display.drawString("open in browser:", SCREEN_WIDTH / 2, instructionY + 18);

_display.setFont(&fonts::FreeSansBold9pt7b);
_display.setTextColor(COLOR_ACCENT);
_display.drawString("192.168.4.1", SCREEN_WIDTH / 2, instructionY + 40);

_display.setFont(&fonts::Font0);
_display.setTextColor(COLOR_TEXT_DIM);
_display.drawString("to configure settings", SCREEN_WIDTH / 2, instructionY + 65);

_display.setTextDatum(lgfx::top_left);
}

void LilyGoDisplay::setSessionId(const char* sessionId) {
_sessionId = sessionId ? sessionId : "";
}
Expand Down
1 change: 1 addition & 0 deletions embedded/libs/lilygo-display/src/lilygo_display.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ class LilyGoDisplay {
void showConnecting();
void showError(const char* message, const char* ipAddress = nullptr);
void showConfigPortal(const char* apName, const char* ip);
void showSetupScreen(const char* apName);

// Climb display
void showClimb(const char* name, const char* grade, const char* gradeColor, int angle, const char* uuid,
Expand Down
44 changes: 44 additions & 0 deletions embedded/libs/wifi-utils/src/wifi_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,41 @@ void WiFiUtils::disconnect() {
setState(WiFiConnectionState::DISCONNECTED);
}

bool WiFiUtils::startAP(const char* apName) {
// Stop any existing connection first
WiFi.disconnect();

// Configure AP mode
WiFi.mode(WIFI_AP);

// Start the access point
bool success = WiFi.softAP(apName);
if (success) {
setState(WiFiConnectionState::AP_MODE);
}
return success;
}

void WiFiUtils::stopAP() {
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
setState(WiFiConnectionState::DISCONNECTED);
}

bool WiFiUtils::isAPMode() {
return state == WiFiConnectionState::AP_MODE;
}

String WiFiUtils::getAPIP() {
return WiFi.softAPIP().toString();
}

bool WiFiUtils::hasSavedCredentials() {
String ssid = Config.getString(KEY_SSID);
return ssid.length() > 0;
}

bool WiFiUtils::isConnected() {
return WiFi.status() == WL_CONNECTED;
}
Expand Down Expand Up @@ -83,6 +118,11 @@ void WiFiUtils::setState(WiFiConnectionState newState) {
}

void WiFiUtils::checkConnection() {
// Don't check STA connection in AP mode
if (state == WiFiConnectionState::AP_MODE) {
return;
}

bool connected = WiFi.status() == WL_CONNECTED;

switch (state) {
Expand All @@ -109,5 +149,9 @@ void WiFiUtils::checkConnection() {
connect(currentSSID.c_str(), currentPassword.c_str(), false);
}
break;

case WiFiConnectionState::AP_MODE:
// Handled at the top of the function
break;
}
}
11 changes: 10 additions & 1 deletion embedded/libs/wifi-utils/src/wifi_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

#define WIFI_CONNECT_TIMEOUT_MS 30000
#define WIFI_RECONNECT_INTERVAL_MS 5000
#define DEFAULT_AP_NAME "Boardsesh-Setup"
#define DEFAULT_AP_IP "192.168.4.1"

enum class WiFiConnectionState { DISCONNECTED, CONNECTING, CONNECTED, CONNECTION_FAILED };
enum class WiFiConnectionState { DISCONNECTED, CONNECTING, CONNECTED, CONNECTION_FAILED, AP_MODE };

typedef void (*WiFiStateCallback)(WiFiConnectionState state);

Expand All @@ -24,6 +26,13 @@ class WiFiUtils {
bool connectSaved();
void disconnect();

// Access Point mode
bool startAP(const char* apName = DEFAULT_AP_NAME);
void stopAP();
bool isAPMode();
String getAPIP();
bool hasSavedCredentials();

bool isConnected();
WiFiConnectionState getState();

Expand Down
126 changes: 94 additions & 32 deletions embedded/projects/board-controller/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
// State
bool wifiConnected = false;
bool backendConnected = false;
bool bleInitialized = false;

#ifdef ENABLE_DISPLAY
// Navigation mutation debounce - wait for rapid presses to stop before sending mutation
Expand Down Expand Up @@ -86,6 +87,7 @@ void onBLEData(const uint8_t* data, size_t len);
void onBLELedData(const LedCommand* commands, int count, int angle);
void onGraphQLStateChange(GraphQLConnectionState state);
void onGraphQLMessage(JsonDocument& doc);
void initializeBLE();
#ifdef ENABLE_DISPLAY
void handleLedUpdateExtended(JsonObject& data);
void onQueueSync(const ControllerQueueSyncData& data);
Expand All @@ -106,6 +108,47 @@ void sendToAppViaBLE(const uint8_t* data, size_t len) {
}
#endif

/**
* Initialize BLE - called after WiFi is configured
* This is deferred until WiFi is set up so we don't waste resources
* scanning for boards when we can't connect to the backend anyway
*/
void initializeBLE() {
if (bleInitialized) {
return; // Already initialized
}

Logger.logln("Initializing BLE as '%s'...", BLE_DEVICE_NAME);
#ifdef ENABLE_BLE_PROXY
// When proxy is enabled, don't advertise yet - connect to board first
// Advertising will start after successful connection to real board
BLE.begin(BLE_DEVICE_NAME, false);
#else
BLE.begin(BLE_DEVICE_NAME, true);
#endif
BLE.setConnectCallback(onBLEConnect);
BLE.setDataCallback(onBLEData);
BLE.setLedDataCallback(onBLELedData);

#ifdef ENABLE_BLE_PROXY
// Set up raw data forwarding for proxy mode
BLE.setRawForwardCallback(onBLERawForward);

// Initialize proxy
String targetMac = Config.getString("proxy_mac");
Proxy.begin(targetMac);
Proxy.setStateCallback(onProxyStateChange);
Proxy.setSendToAppCallback(sendToAppViaBLE);
#endif

#ifdef ENABLE_DISPLAY
Display.setBleStatus(true, false); // BLE enabled, not connected
#endif

bleInitialized = true;
Logger.logln("BLE initialization complete");
}

void setup() {
Serial.begin(115200);
delay(3000); // Longer delay to ensure serial monitor catches boot messages
Expand Down Expand Up @@ -162,36 +205,27 @@ void setup() {

// Try to connect to saved WiFi
if (!WiFiMgr.connectSaved()) {
Logger.logln("No saved WiFi credentials");
}

// Initialize BLE - always use BLE_DEVICE_NAME for Kilter app compatibility
Logger.logln("Initializing BLE as '%s'...", BLE_DEVICE_NAME);
#ifdef ENABLE_BLE_PROXY
// When proxy is enabled, don't advertise yet - connect to board first
// Advertising will start after successful connection to real board
BLE.begin(BLE_DEVICE_NAME, false);
Logger.logln("No saved WiFi credentials - starting AP mode");
#ifdef ENABLE_DISPLAY
// Start AP mode for WiFi configuration
if (WiFiMgr.startAP()) {
Logger.logln("AP mode started: %s", DEFAULT_AP_NAME);
Display.showSetupScreen(DEFAULT_AP_NAME);
} else {
Logger.logln("Failed to start AP mode");
Display.showError("AP Failed");
}
#else
BLE.begin(BLE_DEVICE_NAME, true);
#endif
BLE.setConnectCallback(onBLEConnect);
BLE.setDataCallback(onBLEData);
BLE.setLedDataCallback(onBLELedData);

#ifdef ENABLE_BLE_PROXY
// Set up raw data forwarding for proxy mode
BLE.setRawForwardCallback(onBLERawForward);

// Initialize proxy
String targetMac = Config.getString("proxy_mac");
Proxy.begin(targetMac);
Proxy.setStateCallback(onProxyStateChange);
Proxy.setSendToAppCallback(sendToAppViaBLE);
// Without display, just start AP mode silently
WiFiMgr.startAP();
#endif
// Don't initialize BLE yet - wait for WiFi to be configured
} else {
// We have saved WiFi credentials, initialize BLE now
initializeBLE();
}

#ifdef ENABLE_DISPLAY
Display.setBleStatus(true, false); // BLE enabled, not connected

// Initialize button pins for navigation
pinMode(BUTTON_1_PIN, INPUT_PULLUP);
pinMode(BUTTON_2_PIN, INPUT_PULLUP);
Expand All @@ -202,27 +236,36 @@ void setup() {
WebConfig.begin();

Logger.logln("Setup complete!");
Logger.logln("IP: %s", WiFiMgr.getIP().c_str());
if (WiFiMgr.isAPMode()) {
Logger.logln("AP IP: %s", WiFiMgr.getAPIP().c_str());
} else {
Logger.logln("IP: %s", WiFiMgr.getIP().c_str());
}

// Green blink to indicate ready
LEDs.blink(0, 255, 0, 3, 100);

// Don't refresh display if in AP mode - keep showing setup screen
#ifdef ENABLE_DISPLAY
Display.refresh();
if (!WiFiMgr.isAPMode()) {
Display.refresh();
}
#endif
}

void loop() {
// Process WiFi
WiFiMgr.loop();

// Process BLE
BLE.loop();
// Process BLE (only if initialized - deferred until WiFi configured)
if (bleInitialized) {
BLE.loop();

#ifdef ENABLE_BLE_PROXY
// Process BLE proxy
Proxy.loop();
// Process BLE proxy
Proxy.loop();
#endif
}

// Process WebSocket if WiFi connected
if (wifiConnected) {
Expand Down Expand Up @@ -323,8 +366,13 @@ void onWiFiStateChange(WiFiConnectionState state) {

#ifdef ENABLE_DISPLAY
Display.setWiFiStatus(true);
// Show normal UI now that we're connected
Display.showNoClimb();
#endif

// Initialize BLE now that WiFi is connected (if not already done)
initializeBLE();

// Get backend config
String host = Config.getString("backend_host", DEFAULT_BACKEND_HOST);
int port = Config.getInt("backend_port", DEFAULT_BACKEND_PORT);
Expand Down Expand Up @@ -368,6 +416,20 @@ void onWiFiStateChange(WiFiConnectionState state) {

case WiFiConnectionState::CONNECTION_FAILED:
Logger.logln("WiFi connection failed");
#ifdef ENABLE_DISPLAY
Display.setWiFiStatus(false);
// If connection failed and we don't have saved credentials, start AP mode
if (!WiFiMgr.hasSavedCredentials()) {
Logger.logln("No saved credentials - starting AP mode for configuration");
if (WiFiMgr.startAP()) {
Display.showSetupScreen(DEFAULT_AP_NAME);
}
}
#endif
break;

case WiFiConnectionState::AP_MODE:
Logger.logln("WiFi in AP mode: %s", WiFiMgr.getAPIP().c_str());
#ifdef ENABLE_DISPLAY
Display.setWiFiStatus(false);
#endif
Expand Down
Loading