init commit
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file BleManager.h
|
||||
* @brief BLE Central (client) manager using NimBLE-Arduino.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Scan for the Marine Gateway peripheral ("MarineGateway")
|
||||
* - Establish a secure, bonded connection (passkey entry via Serial/Web)
|
||||
* - Subscribe to all five NOTIFY characteristics and parse incoming JSON
|
||||
* - Provide thread-safe access to the aggregated BoatState
|
||||
* - Send autopilot and admin commands
|
||||
* - Manage automatic reconnection
|
||||
*/
|
||||
|
||||
#include "Config.h"
|
||||
#include <NimBLEDevice.h>
|
||||
|
||||
class BleManager : public NimBLEClientCallbacks,
|
||||
public NimBLEScanCallbacks {
|
||||
public:
|
||||
/**
|
||||
* @brief Initialise the NimBLE stack and start scanning.
|
||||
* Must be called once from setup().
|
||||
*/
|
||||
void begin();
|
||||
|
||||
/**
|
||||
* @brief Call from loop() — handles reconnect scheduling and
|
||||
* dispatches any pending state changes.
|
||||
*/
|
||||
void update();
|
||||
|
||||
// ── Data access (thread-safe) ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @brief Copy the current BoatState snapshot into @p dst.
|
||||
* Protected by an internal mutex.
|
||||
*/
|
||||
void getBoatState(BoatState& dst) const;
|
||||
|
||||
/** @brief Returns true when actively connected to the gateway. */
|
||||
bool isConnected() const;
|
||||
|
||||
/** @brief Current BLE status for status LED / UI. */
|
||||
AppState::BleStatus status() const { return _status; }
|
||||
|
||||
// ── Command senders ───────────────────────────────────────────────────────
|
||||
|
||||
/** @brief Send an autopilot command (e.g. "adjust+1"). */
|
||||
bool sendAutopilotCmd(const char* command);
|
||||
|
||||
/** @brief Send a raw admin JSON command string. */
|
||||
bool sendAdminCmd(const char* jsonCmd);
|
||||
|
||||
// Convenience admin helpers
|
||||
bool sendRestart();
|
||||
bool sendWifiSta(const char* ssid, const char* password);
|
||||
bool sendWifiAp(const char* ssid, const char* password);
|
||||
|
||||
// ── Passkey (pairing) ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @brief Provide the passkey entered by the user.
|
||||
* Called by the WiFi web UI when the user submits the PIN.
|
||||
*/
|
||||
void providePasskey(uint32_t pin);
|
||||
|
||||
private:
|
||||
// ── NimBLEClientCallbacks ─────────────────────────────────────────────────
|
||||
void onConnect(NimBLEClient* pClient) override;
|
||||
void onDisconnect(NimBLEClient* pClient, int reason) override;
|
||||
bool onConfirmPasskey(NimBLEConnInfo& connInfo, uint32_t pin) override;
|
||||
void onAuthenticationComplete(NimBLEConnInfo& connInfo) override;
|
||||
|
||||
// ── NimBLEScanCallbacks ───────────────────────────────────────────────────
|
||||
void onResult(const NimBLEAdvertisedDevice* device) override;
|
||||
void onScanEnd(const NimBLEScanResults& results, int reason) override;
|
||||
|
||||
// ── Internal helpers ──────────────────────────────────────────────────────
|
||||
bool _connectToServer(const NimBLEAddress& addr);
|
||||
bool _subscribeAll();
|
||||
void _startScan();
|
||||
void _scheduleReconnect();
|
||||
|
||||
// Notification callbacks (one per characteristic)
|
||||
void _onNavNotify (NimBLERemoteCharacteristic*, uint8_t*, size_t, bool);
|
||||
void _onWindNotify (NimBLERemoteCharacteristic*, uint8_t*, size_t, bool);
|
||||
void _onAutopilotNotify(NimBLERemoteCharacteristic*, uint8_t*, size_t, bool);
|
||||
void _onPerfNotify (NimBLERemoteCharacteristic*, uint8_t*, size_t, bool);
|
||||
void _onAdminNotify (NimBLERemoteCharacteristic*, uint8_t*, size_t, bool);
|
||||
|
||||
// JSON parsers
|
||||
void _parseNav (const char* json);
|
||||
void _parseWind (const char* json);
|
||||
void _parseAutopilot(const char* json);
|
||||
void _parsePerf (const char* json);
|
||||
void _parseAdmin (const char* json);
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────
|
||||
NimBLEClient* _pClient = nullptr;
|
||||
NimBLEAddress _targetAddr;
|
||||
bool _targetFound = false;
|
||||
bool _doConnect = false;
|
||||
uint32_t _reconnectAt = 0; ///< millis() when next reconnect is due
|
||||
|
||||
AppState::BleStatus _status = AppState::BleStatus::IDLE;
|
||||
|
||||
mutable portMUX_TYPE _mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
BoatState _state;
|
||||
|
||||
// Pending passkey from the web UI
|
||||
volatile bool _passkeyPending = false;
|
||||
volatile uint32_t _passkey = 0;
|
||||
};
|
||||
|
||||
// Singleton accessor
|
||||
extern BleManager bleManager;
|
||||
@@ -0,0 +1,320 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file Config.h
|
||||
* @brief Central configuration constants, data structures and enumerations
|
||||
* for the Marine Navigation Display.
|
||||
*
|
||||
* All hardware pin assignments, BLE UUIDs, display layout parameters and
|
||||
* application-level structs are defined here so that every module shares a
|
||||
* single source of truth.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// =============================================================================
|
||||
// Firmware version
|
||||
// =============================================================================
|
||||
#ifndef APP_VERSION
|
||||
#define APP_VERSION "1.0.0"
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// Hardware — SPI pins for the 4.2" GxEPD2 e-ink display
|
||||
// Waveshare ESP32-S3-Zero default SPI wiring
|
||||
// =============================================================================
|
||||
namespace Pins {
|
||||
// ── E-Ink display ─────────────────────────────────────────────────────────
|
||||
constexpr uint8_t EPD_CS = 9; ///< Chip Select
|
||||
constexpr uint8_t EPD_DC = 8; ///< Data/Command
|
||||
constexpr uint8_t EPD_RST = 7; ///< Reset
|
||||
constexpr uint8_t EPD_BUSY = 6; ///< Busy signal (active LOW)
|
||||
constexpr uint8_t EPD_SCK = 10; ///< SPI clock (HSPI SCK)
|
||||
constexpr uint8_t EPD_MOSI = 11; ///< SPI MOSI (HSPI MOSI)
|
||||
|
||||
// ── Capacitive touch buttons (ESP32-S3 native touch) ─────────────────────
|
||||
// Override via platformio.ini build_flags if needed.
|
||||
constexpr uint8_t TOUCH_NEXT = TOUCH_PIN_NEXT; ///< Next page / +
|
||||
constexpr uint8_t TOUCH_PREV = TOUCH_PIN_PREV; ///< Previous page / -
|
||||
constexpr uint8_t TOUCH_ACTION = TOUCH_PIN_ACTION; ///< Action / full refresh
|
||||
|
||||
// ── NeoPixel RGB status LED ───────────────────────────────────────────────
|
||||
constexpr uint8_t LED = STATUS_LED_PIN;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Touch input parameters
|
||||
// =============================================================================
|
||||
namespace Touch {
|
||||
constexpr uint16_t THRESHOLD = 40; ///< Raw touch value below = touched
|
||||
constexpr uint32_t DEBOUNCE_MS = 50; ///< Minimum ms between events
|
||||
constexpr uint32_t LONG_PRESS_MS = 1500; ///< Hold duration for long-press
|
||||
constexpr uint32_t AP_COMBO_MS = 3000; ///< Hold NEXT+PREV to enter AP mode
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BLE — Marine Gateway protocol UUIDs
|
||||
// Base: 4D475743-xxxx-4E41-5649-474154494F4E
|
||||
// =============================================================================
|
||||
namespace BleUUID {
|
||||
// ── Services ──────────────────────────────────────────────────────────────
|
||||
constexpr const char* NAV_SVC = "4d475743-0001-4e41-5649-474154494f4e";
|
||||
constexpr const char* WIND_SVC = "4d475743-0002-4e41-5649-474154494f4e";
|
||||
constexpr const char* AUTOPILOT_SVC = "4d475743-0003-4e41-5649-474154494f4e";
|
||||
constexpr const char* PERF_SVC = "4d475743-0004-4e41-5649-474154494f4e";
|
||||
constexpr const char* ADMIN_SVC = "4d475743-0005-4e41-5649-474154494f4e";
|
||||
|
||||
// ── Characteristics (read / notify) ───────────────────────────────────────
|
||||
constexpr const char* NAV_DATA = "4d475743-0101-4e41-5649-474154494f4e";
|
||||
constexpr const char* WIND_DATA = "4d475743-0201-4e41-5649-474154494f4e";
|
||||
constexpr const char* AUTOPILOT_DATA = "4d475743-0301-4e41-5649-474154494f4e";
|
||||
constexpr const char* AUTOPILOT_CMD = "4d475743-0302-4e41-5649-474154494f4e";
|
||||
constexpr const char* PERF_DATA = "4d475743-0401-4e41-5649-474154494f4e";
|
||||
constexpr const char* ADMIN_DATA = "4d475743-0501-4e41-5649-474154494f4e";
|
||||
constexpr const char* ADMIN_CMD = "4d475743-0502-4e41-5649-474154494f4e";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BLE runtime parameters
|
||||
// =============================================================================
|
||||
namespace BleConfig {
|
||||
constexpr const char* DEVICE_NAME = BLE_DEVICE_NAME;
|
||||
constexpr uint32_t SCAN_DURATION = 10; ///< seconds per scan window
|
||||
constexpr uint32_t RECONNECT_MS = 5000; ///< delay before reconnect attempt
|
||||
constexpr uint32_t STALE_TIMEOUT = 15; ///< seconds before data is marked stale
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Display layout
|
||||
// 400×300 px divided into a 2×2 grid → 4 tiles
|
||||
// =============================================================================
|
||||
namespace Display {
|
||||
constexpr uint16_t WIDTH = 400;
|
||||
constexpr uint16_t HEIGHT = 300;
|
||||
constexpr uint8_t TILE_COLS = 2;
|
||||
constexpr uint8_t TILE_ROWS = 2;
|
||||
constexpr uint8_t TILE_COUNT = TILE_COLS * TILE_ROWS;
|
||||
constexpr uint16_t TILE_W = WIDTH / TILE_COLS; // 200 px
|
||||
constexpr uint16_t TILE_H = HEIGHT / TILE_ROWS; // 150 px
|
||||
constexpr uint8_t BORDER_PX = 2; ///< separator line width
|
||||
constexpr uint8_t LABEL_H = 22; ///< px reserved for the tile label
|
||||
constexpr uint32_t FULL_REFRESH_INTERVAL_MS = 300000; ///< forced full refresh every 5 min
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Available data fields that can be assigned to a tile
|
||||
// =============================================================================
|
||||
enum class DataField : uint8_t {
|
||||
NONE = 0,
|
||||
// Navigation
|
||||
LAT, ///< GPS Latitude
|
||||
LON, ///< GPS Longitude
|
||||
SOG, ///< Speed Over Ground (kn)
|
||||
COG, ///< Course Over Ground (°)
|
||||
STW, ///< Speed Through Water (kn)
|
||||
HDG_MAG, ///< Magnetic Heading (°)
|
||||
DEPTH, ///< Depth (m)
|
||||
// Wind
|
||||
AWS, ///< Apparent Wind Speed (kn)
|
||||
AWA, ///< Apparent Wind Angle (°)
|
||||
TWS, ///< True Wind Speed (kn)
|
||||
TWA, ///< True Wind Angle (°)
|
||||
TWD, ///< True Wind Direction (°)
|
||||
// Autopilot
|
||||
AP_MODE, ///< Autopilot mode string
|
||||
AP_TARGET, ///< Autopilot target heading (°)
|
||||
AP_RUDDER, ///< Rudder angle (°)
|
||||
// Performance
|
||||
VMG, ///< Velocity Made Good (kn)
|
||||
POLAR_PCT, ///< Polar efficiency (%)
|
||||
TARGET_STW, ///< Polar target speed (kn)
|
||||
// Admin / System
|
||||
UPTIME, ///< Device uptime (s)
|
||||
WIFI_MODE, ///< WiFi mode string
|
||||
FREE_HEAP, ///< Free heap (bytes)
|
||||
_COUNT ///< Sentinel — keep last
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Tile configuration (persisted in NVS / LittleFS)
|
||||
// =============================================================================
|
||||
struct TileConfig {
|
||||
DataField field = DataField::NONE;
|
||||
char label[16] = ""; ///< User-defined label override (empty = auto)
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Page: a set of 4 tiles
|
||||
// =============================================================================
|
||||
constexpr uint8_t MAX_PAGES = 8;
|
||||
|
||||
struct PageConfig {
|
||||
char name[24] = "";
|
||||
TileConfig tiles[Display::TILE_COUNT];
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Application-wide configuration (serialised to LittleFS as JSON)
|
||||
// =============================================================================
|
||||
struct AppConfig {
|
||||
uint8_t pageCount = 1;
|
||||
uint8_t activePage = 0;
|
||||
PageConfig pages[MAX_PAGES];
|
||||
|
||||
// WiFi AP credentials (used when the display itself opens an AP)
|
||||
char apSSID[32] = "MarineDisplay";
|
||||
char apPassword[32] = "marine123";
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Live data snapshot — filled by BleManager, read by DisplayManager
|
||||
// =============================================================================
|
||||
struct NavData {
|
||||
float lat = NAN;
|
||||
float lon = NAN;
|
||||
float sog = NAN;
|
||||
float cog = NAN;
|
||||
float stw = NAN;
|
||||
float hdgMag = NAN;
|
||||
float depth = NAN;
|
||||
uint32_t updatedAt = 0; ///< millis() of last valid update
|
||||
};
|
||||
|
||||
struct WindData {
|
||||
float aws = NAN;
|
||||
float awa = NAN;
|
||||
float tws = NAN;
|
||||
float twa = NAN;
|
||||
float twd = NAN;
|
||||
uint32_t updatedAt = 0;
|
||||
};
|
||||
|
||||
struct AutopilotData {
|
||||
char mode[16] = "";
|
||||
char status[16] = "";
|
||||
float headingTarget = NAN;
|
||||
float windTarget = NAN;
|
||||
float rudder = NAN;
|
||||
float lockedHeading = NAN;
|
||||
uint32_t updatedAt = 0;
|
||||
};
|
||||
|
||||
struct PerfData {
|
||||
float vmg = NAN;
|
||||
float polarPct = NAN;
|
||||
float targetStw = NAN;
|
||||
bool polarLoaded = false;
|
||||
uint32_t updatedAt = 0;
|
||||
};
|
||||
|
||||
struct AdminData {
|
||||
uint32_t uptimeS = 0;
|
||||
uint64_t datetimeUtc = 0;
|
||||
char wifiMode[8] = "";
|
||||
char wifiSSID[33] = "";
|
||||
char ip[16] = "";
|
||||
uint32_t freeHeap = 0;
|
||||
uint32_t updatedAt = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Aggregated snapshot of all BLE-received data.
|
||||
* Access must be guarded by the SemaphoreHandle in AppState.
|
||||
*/
|
||||
struct BoatState {
|
||||
NavData nav;
|
||||
WindData wind;
|
||||
AutopilotData autopilot;
|
||||
PerfData perf;
|
||||
AdminData admin;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Application state — shared between all modules (extern in AppState.cpp)
|
||||
// =============================================================================
|
||||
namespace AppState {
|
||||
enum class BleStatus : uint8_t {
|
||||
IDLE,
|
||||
SCANNING,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTED,
|
||||
ERROR
|
||||
};
|
||||
|
||||
enum class WifiStatus : uint8_t {
|
||||
OFF,
|
||||
AP_ACTIVE,
|
||||
STA_CONNECTING,
|
||||
STA_CONNECTED
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NeoPixel LED colour palette for status indication
|
||||
// =============================================================================
|
||||
namespace LedColor {
|
||||
constexpr uint32_t OFF = 0x000000;
|
||||
constexpr uint32_t BLE_SCANNING = 0x0000FF; ///< Blue pulsing
|
||||
constexpr uint32_t BLE_CONNECTED= 0x00FF00; ///< Solid green
|
||||
constexpr uint32_t BLE_ERROR = 0xFF0000; ///< Red
|
||||
constexpr uint32_t WIFI_AP = 0xFF8800; ///< Amber
|
||||
constexpr uint32_t WIFI_STA = 0x00FFFF; ///< Cyan
|
||||
constexpr uint32_t OTA = 0xFF00FF; ///< Magenta pulsing
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utility: readable name for a DataField
|
||||
// =============================================================================
|
||||
inline const char* fieldName(DataField f) {
|
||||
switch (f) {
|
||||
case DataField::LAT: return "Latitude";
|
||||
case DataField::LON: return "Longitude";
|
||||
case DataField::SOG: return "SOG";
|
||||
case DataField::COG: return "COG";
|
||||
case DataField::STW: return "STW";
|
||||
case DataField::HDG_MAG: return "Heading";
|
||||
case DataField::DEPTH: return "Depth";
|
||||
case DataField::AWS: return "AWS";
|
||||
case DataField::AWA: return "AWA";
|
||||
case DataField::TWS: return "TWS";
|
||||
case DataField::TWA: return "TWA";
|
||||
case DataField::TWD: return "TWD";
|
||||
case DataField::AP_MODE: return "AP Mode";
|
||||
case DataField::AP_TARGET: return "AP Target";
|
||||
case DataField::AP_RUDDER: return "Rudder";
|
||||
case DataField::VMG: return "VMG";
|
||||
case DataField::POLAR_PCT: return "Polar %";
|
||||
case DataField::TARGET_STW:return "Tgt STW";
|
||||
case DataField::UPTIME: return "Uptime";
|
||||
case DataField::WIFI_MODE: return "WiFi Mode";
|
||||
case DataField::FREE_HEAP: return "Free Heap";
|
||||
default: return "---";
|
||||
}
|
||||
}
|
||||
|
||||
inline const char* fieldUnit(DataField f) {
|
||||
switch (f) {
|
||||
case DataField::LAT:
|
||||
case DataField::LON:
|
||||
case DataField::COG:
|
||||
case DataField::HDG_MAG:
|
||||
case DataField::TWD:
|
||||
case DataField::AP_TARGET:
|
||||
case DataField::AP_RUDDER:
|
||||
case DataField::AWA:
|
||||
case DataField::TWA: return "°";
|
||||
case DataField::SOG:
|
||||
case DataField::STW:
|
||||
case DataField::AWS:
|
||||
case DataField::TWS:
|
||||
case DataField::VMG:
|
||||
case DataField::TARGET_STW:return "kn";
|
||||
case DataField::DEPTH: return "m";
|
||||
case DataField::POLAR_PCT: return "%";
|
||||
case DataField::UPTIME: return "s";
|
||||
case DataField::FREE_HEAP: return "B";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file ConfigManager.h
|
||||
* @brief Persistent storage of application configuration using LittleFS + JSON.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Load/save AppConfig (pages, tiles, WiFi credentials) to /config.json
|
||||
* - Provide typed accessors for individual fields
|
||||
* - Expose helpers for the web UI to read/write pages as JSON fragments
|
||||
*/
|
||||
|
||||
#include "Config.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
class ConfigManager {
|
||||
public:
|
||||
/**
|
||||
* @brief Initialise LittleFS and load configuration from disk.
|
||||
* Must be called before any other method.
|
||||
* @return true on success, false if LittleFS mount failed.
|
||||
*/
|
||||
bool begin();
|
||||
|
||||
/** @brief Access the live configuration object (read-only). */
|
||||
const AppConfig& config() const { return _cfg; }
|
||||
|
||||
/** @brief Write a complete AppConfig and persist it immediately. */
|
||||
bool save(const AppConfig& cfg);
|
||||
|
||||
/** @brief Persist the current in-memory config to disk. */
|
||||
bool persist();
|
||||
|
||||
// ── Page helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/** @brief Return the currently active page index. */
|
||||
uint8_t activePage() const { return _cfg.activePage; }
|
||||
|
||||
/** @brief Switch active page; persists. */
|
||||
bool setActivePage(uint8_t index);
|
||||
|
||||
/** @brief Advance to the next page (wraps around). */
|
||||
void nextPage();
|
||||
|
||||
/** @brief Go back to the previous page (wraps around). */
|
||||
void prevPage();
|
||||
|
||||
/** @brief Update a single tile in a given page; persists. */
|
||||
bool setTile(uint8_t page, uint8_t tile, DataField field, const char* label = nullptr);
|
||||
|
||||
// ── JSON serialisation for the web UI ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @brief Serialise the full configuration to a JSON string.
|
||||
* @param[out] out Destination String.
|
||||
*/
|
||||
void toJson(String& out) const;
|
||||
|
||||
/**
|
||||
* @brief Parse a JSON string produced by the web UI and merge it into
|
||||
* the current config. Persists on success.
|
||||
* @return true if parsing and saving succeeded.
|
||||
*/
|
||||
bool fromJson(const String& json);
|
||||
|
||||
// ── Factory reset ─────────────────────────────────────────────────────────
|
||||
void resetToDefaults();
|
||||
|
||||
private:
|
||||
static constexpr const char* CONFIG_PATH = "/config.json";
|
||||
|
||||
AppConfig _cfg;
|
||||
|
||||
bool _load();
|
||||
bool _write();
|
||||
void _applyDefaults();
|
||||
};
|
||||
|
||||
// Singleton accessor
|
||||
extern ConfigManager configManager;
|
||||
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file DisplayManager.h
|
||||
* @brief E-Ink display driver abstraction using GxEPD2_BW.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Manage the 400×300 Waveshare 4.2" GxEPD2 display via SPI.
|
||||
* - Render a 2×2 tile grid; each tile shows one data field.
|
||||
* - Perform partial refresh for numeric value updates (fast, no flicker).
|
||||
* - Perform full refresh on page change or periodically to prevent ghosting.
|
||||
* - Show a splash/boot screen, BLE status overlay and AP-mode notice.
|
||||
*
|
||||
* Partial refresh strategy:
|
||||
* Each tile's "value area" (excluding the fixed label and border) is
|
||||
* re-drawn with partial update. The borders and labels are only redrawn
|
||||
* during a full refresh, saving ~250 ms per cycle.
|
||||
*/
|
||||
|
||||
#include "Config.h"
|
||||
#include <GxEPD2_BW.h>
|
||||
#include <Fonts/FreeMonoBold24pt7b.h>
|
||||
#include <Fonts/FreeMonoBold12pt7b.h>
|
||||
#include <Fonts/FreeMono9pt7b.h>
|
||||
|
||||
// ── Display model — Waveshare 4.2" mono GDEY042T81 (400×300) ─────────────────
|
||||
// Adjust the model constant if your display differs.
|
||||
using DisplayType = GxEPD2_BW<GxEPD2_420_GDEY042T81,
|
||||
GxEPD2_420_GDEY042T81::HEIGHT>;
|
||||
|
||||
class DisplayManager {
|
||||
public:
|
||||
/**
|
||||
* @brief Initialise the SPI bus and display. Shows the splash screen.
|
||||
* Must be called from setup() before any render methods.
|
||||
*/
|
||||
void begin();
|
||||
|
||||
/**
|
||||
* @brief Main render call — invoke from loop().
|
||||
* Decides between partial and full refresh based on:
|
||||
* - Whether data has changed since the last render.
|
||||
* - Whether FULL_REFRESH_INTERVAL_MS has elapsed.
|
||||
* - An explicit forceFullRefresh() request.
|
||||
* @param state Current boat data snapshot.
|
||||
* @param cfg Current page configuration.
|
||||
*/
|
||||
void render(const BoatState& state, const PageConfig& cfg);
|
||||
|
||||
/** @brief Request a full (slow) refresh on the next render() call. */
|
||||
void forceFullRefresh() { _needFullRefresh = true; }
|
||||
|
||||
/** @brief Show a BLE scanning / connecting notice. */
|
||||
void showBleStatus(AppState::BleStatus status);
|
||||
|
||||
/** @brief Show the AP mode access screen with SSID and IP. */
|
||||
void showApScreen(const char* ssid, const char* ip);
|
||||
|
||||
/** @brief Show the OTA progress screen. */
|
||||
void showOtaScreen(uint8_t percent);
|
||||
|
||||
/** @brief Show the passkey entry prompt. */
|
||||
void showPasskeyPrompt(uint32_t pin);
|
||||
|
||||
/** @brief Hibernate the display to save power (full blank). */
|
||||
void hibernate();
|
||||
|
||||
// Internal access for GxEPD2
|
||||
DisplayType& epd() { return _display; }
|
||||
|
||||
private:
|
||||
DisplayType _display {
|
||||
GxEPD2_420_GDEY042T81(Pins::EPD_CS,
|
||||
Pins::EPD_DC,
|
||||
Pins::EPD_RST,
|
||||
Pins::EPD_BUSY)
|
||||
};
|
||||
|
||||
bool _initialised = false;
|
||||
bool _needFullRefresh = true;
|
||||
uint32_t _lastFullRefresh = 0;
|
||||
uint32_t _lastRender = 0;
|
||||
|
||||
// Cached value strings per tile to detect changes
|
||||
char _prevValue[Display::TILE_COUNT][16];
|
||||
|
||||
// ── Drawing helpers ───────────────────────────────────────────────────────
|
||||
|
||||
void _drawSplash();
|
||||
void _drawGrid();
|
||||
void _drawTile(uint8_t idx, const TileConfig& tile, const BoatState& state, bool partial);
|
||||
void _drawTileLabel(uint8_t idx, const TileConfig& tile);
|
||||
void _drawTileValue(uint8_t idx, const char* value, bool partial);
|
||||
|
||||
// Compute the pixel rect for a tile
|
||||
struct Rect { uint16_t x, y, w, h; };
|
||||
Rect _tileRect(uint8_t idx) const;
|
||||
Rect _valuRect(uint8_t idx) const; ///< Sub-rect for the value only
|
||||
|
||||
// Extract a display string from the boat state for a given field
|
||||
void _fieldToString(DataField field, const BoatState& state,
|
||||
char* out, size_t outLen) const;
|
||||
|
||||
// Centre text in a rect
|
||||
void _drawCentred(const char* text, uint16_t x, uint16_t y,
|
||||
uint16_t w, uint16_t h);
|
||||
};
|
||||
|
||||
// Singleton accessor
|
||||
extern DisplayManager displayManager;
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file StatusLed.h
|
||||
* @brief NeoPixel RGB LED status indicator.
|
||||
*
|
||||
* Encodes BLE and WiFi operational state as colour + blink pattern:
|
||||
*
|
||||
* BLE Scanning → Blue pulse (on 100 ms / off 900 ms)
|
||||
* BLE Connecting → Blue pulse (on 300 ms / off 300 ms)
|
||||
* BLE Connected → Solid green
|
||||
* BLE Disconnected → Red blink (on 200 ms / off 800 ms)
|
||||
* BLE Error → Solid red
|
||||
* WiFi AP active → Amber pulse (on 500 ms / off 500 ms)
|
||||
* OTA in progress → Magenta rapid pulse
|
||||
* All off → Black
|
||||
*/
|
||||
|
||||
#include "Config.h"
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
|
||||
class StatusLed {
|
||||
public:
|
||||
void begin();
|
||||
|
||||
/**
|
||||
* @brief Call from loop() to update the LED animation.
|
||||
* Non-blocking — uses millis() internally.
|
||||
*/
|
||||
void update(AppState::BleStatus ble, AppState::WifiStatus wifi, bool ota = false);
|
||||
|
||||
/** @brief Immediately set a solid colour (overrides animation until next update()). */
|
||||
void setColor(uint32_t color, uint8_t brightness = 40);
|
||||
|
||||
/** @brief Turn off the LED. */
|
||||
void off();
|
||||
|
||||
private:
|
||||
Adafruit_NeoPixel _strip{STATUS_LED_COUNT, STATUS_LED_PIN, NEO_GRB + NEO_KHZ800};
|
||||
|
||||
uint32_t _lastToggle = 0;
|
||||
bool _ledOn = false;
|
||||
|
||||
AppState::BleStatus _prevBle = AppState::BleStatus::IDLE;
|
||||
AppState::WifiStatus _prevWifi = AppState::WifiStatus::OFF;
|
||||
bool _prevOta = false;
|
||||
|
||||
void _pulse(uint32_t color, uint32_t onMs, uint32_t offMs, uint8_t brightness = 40);
|
||||
void _solid(uint32_t color, uint8_t brightness = 40);
|
||||
};
|
||||
|
||||
extern StatusLed statusLed;
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file TouchManager.h
|
||||
* @brief Capacitive touch button handler for ESP32-S3 native touch pins.
|
||||
*
|
||||
* Supports three touch pads:
|
||||
* TOUCH_NEXT — Advance to the next display page
|
||||
* TOUCH_PREV — Go to the previous display page
|
||||
* TOUCH_ACTION — Trigger a full e-ink refresh
|
||||
*
|
||||
* Combo detection:
|
||||
* TOUCH_NEXT + TOUCH_PREV held for AP_COMBO_MS → enter WiFi AP mode
|
||||
*
|
||||
* The ESP32-S3 touch peripheral returns a raw capacitance value; a reading
|
||||
* *below* the threshold means the pad is being touched (lower = more contact).
|
||||
*/
|
||||
|
||||
#include "Config.h"
|
||||
|
||||
class TouchManager {
|
||||
public:
|
||||
/** @brief Initialise touch pads. Call from setup(). */
|
||||
void begin();
|
||||
|
||||
/** @brief Poll all pads; must be called from loop(). */
|
||||
void update();
|
||||
|
||||
// ── Event accessors (consumed once per call) ──────────────────────────────
|
||||
bool nextPressed() { bool v = _evNext; _evNext = false; return v; }
|
||||
bool prevPressed() { bool v = _evPrev; _evPrev = false; return v; }
|
||||
bool actionPressed() { bool v = _evAction; _evAction = false; return v; }
|
||||
bool apCombo() { bool v = _evApCombo; _evApCombo = false; return v; }
|
||||
|
||||
private:
|
||||
// Per-pad state machine
|
||||
struct PadState {
|
||||
uint8_t pin;
|
||||
bool wasDown = false;
|
||||
uint32_t pressedAt = 0;
|
||||
bool longFired = false;
|
||||
};
|
||||
|
||||
PadState _pads[3] = {
|
||||
{Pins::TOUCH_NEXT},
|
||||
{Pins::TOUCH_PREV},
|
||||
{Pins::TOUCH_ACTION}
|
||||
};
|
||||
|
||||
uint32_t _lastDebounce = 0;
|
||||
uint32_t _comboPressAt = 0;
|
||||
bool _comboActive = false;
|
||||
|
||||
bool _evNext = false;
|
||||
bool _evPrev = false;
|
||||
bool _evAction = false;
|
||||
bool _evApCombo = false;
|
||||
|
||||
bool _isTouched(uint8_t pin) const;
|
||||
};
|
||||
|
||||
extern TouchManager touchManager;
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file WifiManager.h
|
||||
* @brief WiFi Access Point + Async Web Server for configuration and OTA.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Start a WPA2 Access Point with credentials from AppConfig.
|
||||
* - Host a lightweight web UI on port 80:
|
||||
* GET / → configuration page (tile/page editor)
|
||||
* GET /api/config → current config as JSON
|
||||
* POST /api/config → apply new config (JSON body)
|
||||
* POST /api/ble/cmd → forward BLE command (restart, wifi_sta, wifi_ap)
|
||||
* GET /api/state → current BoatState as JSON (live data snapshot)
|
||||
* POST /update → OTA firmware upload
|
||||
* - Signal OTA progress to the display.
|
||||
* - Accept a BLE passkey submission from the web UI.
|
||||
*/
|
||||
|
||||
#include "Config.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
class WifiManager {
|
||||
public:
|
||||
/**
|
||||
* @brief Start the AP and the async web server.
|
||||
* Must be called from setup() or on button combo.
|
||||
*/
|
||||
void begin(const char* ssid, const char* password);
|
||||
|
||||
/** @brief Stop the AP and server (e.g. when switching back to BLE-only). */
|
||||
void stop();
|
||||
|
||||
/** @brief Returns true when the AP is active and the server is running. */
|
||||
bool isActive() const { return _active; }
|
||||
|
||||
/** @brief IP address of the AP interface (always 192.168.4.1). */
|
||||
static constexpr const char* AP_IP = "192.168.4.1";
|
||||
|
||||
/** @brief Call from loop() — no-op currently, kept for future use. */
|
||||
void update();
|
||||
|
||||
private:
|
||||
AsyncWebServer _server{80};
|
||||
bool _active = false;
|
||||
|
||||
void _setupRoutes();
|
||||
|
||||
// Route handlers
|
||||
void _handleRoot (AsyncWebServerRequest*);
|
||||
void _handleGetConfig (AsyncWebServerRequest*);
|
||||
void _handlePostConfig (AsyncWebServerRequest*, uint8_t*, size_t, size_t, size_t);
|
||||
void _handleGetState (AsyncWebServerRequest*);
|
||||
void _handleBleCmd (AsyncWebServerRequest*, uint8_t*, size_t, size_t, size_t);
|
||||
void _handlePasskey (AsyncWebServerRequest*, uint8_t*, size_t, size_t, size_t);
|
||||
void _handleOtaUpload (AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool);
|
||||
void _handleNotFound (AsyncWebServerRequest*);
|
||||
|
||||
// Serve the embedded HTML (stored in PROGMEM)
|
||||
static const char INDEX_HTML[] PROGMEM;
|
||||
};
|
||||
|
||||
// Singleton accessor
|
||||
extern WifiManager wifiManager;
|
||||
Reference in New Issue
Block a user