diff --git a/BoatControlPanel/BoatControlPanel.ino b/BoatControlPanel/BoatControlPanel.ino
index 6941d7c..9ec145f 100644
--- a/BoatControlPanel/BoatControlPanel.ino
+++ b/BoatControlPanel/BoatControlPanel.ino
@@ -50,11 +50,7 @@
#include "ToneManager.h"
#include "RgbLedFade.h"
-// sensors
-//#include "TLVCompassHandler.h"
-
#include "GpsSensorHandler.h"
-#include "SensorCommandHandler.h"
#if defined(ARDUINO_MEGA2560)
#define NEXTION_SERIAL Serial1
@@ -178,7 +174,7 @@ void setup()
#if defined(ARDUINO_MEGA2560)
SystemFunctions::initializeSerial(NEXTION_SERIAL, 19200, false);
- SystemFunctions::initializeSerial(LINK_SERIAL, 9600, false);
+ SystemFunctions::initializeSerial(LINK_SERIAL, 19200, false);
SystemFunctions::initializeSerial(GPS_SERIAL, 9600, false);
#elif defined(ARDUINO_R4_MINIMA)
NEXTION_SERIAL.begin(19200);
@@ -269,18 +265,6 @@ void loop()
SystemCpuMonitor::update();
}
-void resetSerial(Stream& serial)
-{
- // Flush outgoing data
- serial.flush();
-
- // Clear incoming buffer
- while (serial.available() > 0)
- {
- serial.read();
- }
-}
-
void onLinkCommandReceived(SerialCommandManager* mgr)
{
char cmd[64];
@@ -288,7 +272,7 @@ void onLinkCommandReceived(SerialCommandManager* mgr)
commandMgrComputer.sendError(cmd, F("LINKHANDLER"));
// Reset serial to clear any residual data
- resetSerial(LINK_SERIAL);
+ SystemFunctions::resetSerial(LINK_SERIAL);
}
void onComputerCommandReceived(SerialCommandManager* mgr)
@@ -299,5 +283,5 @@ void onComputerCommandReceived(SerialCommandManager* mgr)
commandMgrComputer.sendError(cmd, F("PCHANDLER"));
// Reset serial to clear any residual data
- resetSerial(COMPUTER_SERIAL);
+ SystemFunctions::resetSerial(COMPUTER_SERIAL);
}
diff --git a/BoatControlPanel/BoatControlPanel.vcxproj b/BoatControlPanel/BoatControlPanel.vcxproj
index 181fe63..81b9d53 100644
--- a/BoatControlPanel/BoatControlPanel.vcxproj
+++ b/BoatControlPanel/BoatControlPanel.vcxproj
@@ -123,7 +123,7 @@
CppCode
true
-
+
@@ -182,7 +182,7 @@
VisualMicroDebugger
- $(ProjectDir)..\BoatControlPanel;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\NextionControl\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\TinyGPSPlus\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\libraries\EEPROM\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\cores\arduino;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\variants\mega;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\lib\gcc\avr\7.3.0\include;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\lib\gcc\avr\7.3.0\include-fixed;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\avr\include;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\NextionControl\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\libraries\SoftwareSerial\src;$(ProjectDir)..\Shared\Sensors
+ $(ProjectDir)..\BoatControlPanel;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\NextionControl\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\TinyGPSPlus\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\libraries\EEPROM\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\cores\arduino;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\variants\mega;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\lib\gcc\avr\7.3.0\include;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\lib\gcc\avr\7.3.0\include-fixed;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\avr\include;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\NextionControl\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\libraries\SoftwareSerial\src
$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\bin\avr-g++
$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\bin\avr-g++
false
@@ -203,7 +203,7 @@
- $(ProjectDir)..\BoatControlPanel;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\NextionControl\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\TinyGPSPlus\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\libraries\EEPROM\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\cores\arduino;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\variants\mega;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\lib\gcc\avr\7.3.0\include;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\lib\gcc\avr\7.3.0\include-fixed;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\avr\include;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\NextionControl\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\libraries\SoftwareSerial\src;$(ProjectDir)..\Shared\Sensors;%(AdditionalIncludeDirectories)
+ $(ProjectDir)..\BoatControlPanel;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\NextionControl\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\TinyGPSPlus\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\libraries\EEPROM\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\cores\arduino;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\variants\mega;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\lib\gcc\avr\7.3.0\include;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\lib\gcc\avr\7.3.0\include-fixed;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\avr\include;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\NextionControl\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\avr\1.8.6\libraries\SoftwareSerial\src;%(AdditionalIncludeDirectories)
$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\bin\avr-g++
gnu++11
gnu11
diff --git a/BoatControlPanel/BoatControlPanel.vcxproj.filters b/BoatControlPanel/BoatControlPanel.vcxproj.filters
index 1a4d880..4560cd1 100644
--- a/BoatControlPanel/BoatControlPanel.vcxproj.filters
+++ b/BoatControlPanel/BoatControlPanel.vcxproj.filters
@@ -321,7 +321,7 @@
Header Files\Sensors
-
+
Header Files\SharedCode
diff --git a/Commands.md b/Commands.md
index 5d64772..329ad28 100644
--- a/Commands.md
+++ b/Commands.md
@@ -12,8 +12,10 @@ These are commands used to configure the system settings and can only be sent fr
| `F3` — Cpu Usage | `F3` | When received will return the current CPU usage. No params. |
| `F4` — Bluetooth Enabled | `F4` | When received will return the current enabled state of bluetooth 0 off 1 on. No params. |
| `F5` — Wifi Enabled | `F5` | When received will return the current enabled state of wifi 0 off 1 on. No params. |
-| `F6` — Set DateTime | `F6:v=2025-12-04T15:30:00` or `F6:v=1733328600` | Set current date/time. Accepts ISO 8601 datetime string `YYYY-MM-DDTHH:MM:SS` or Unix timestamp (seconds since epoch). Returns current stored time. |
+| `F6` — Set DateTime | `F6:v=1733328600` | Set current date/time. Accepts Unix timestamp (seconds since epoch). Returns current stored time. |
| `F7` — Get DateTime | `F7` | Get current date/time as ISO 8601 datetime string `YYYY-MM-DDTHH:MM:SS`. |
+| `F8` — SD Card Present | `F8` | Query if SD card is present. Returns `v=0` (not present) or `v=1` (present). No params. |
+| `F9` — SD Card Log File Size | `F9` | Get current log file size in bytes. Returns `v=` where bytes is the size of the active log file. Returns `v=0` if no file is open. No params. |
### Wifi System Commands (SFB)
@@ -59,7 +61,8 @@ These are commands used to configure the system settings and can only be sent fr
| `C26` — LED Auto Switch | `C26:v=true` or `C26:v=1` | Enable/disable auto day/night switching. Param format: `v=`. `value` must be true/false or 1/0. |
| `C27` — LED Enable States | `C27:g=true;w=true;s=false` | Enable/disable individual LEDs. Param format: `g=;w=;s=`. Each value must be true/false or 1/0. `g`=GPS LED, `w`=Warning LED, `s`=System LED. |
| `C28` — Control Panel Tones | `C28:t=0;h=400;d=500;p=0;r=30000` | Configure control panel tones, 2 categories, good and bad, good is only used at startup to indicate all is working, bad is used in response to warnings. t=0 or 1 (0 good or 1 bad), h = tone Hz, d = duration in milliseconds, p = preset 0 custom tone/duration, 1 = submaring ping, 2 = double beep, 3 = rising chirp, 4 = descending alert, 5 = nautical bell, 0xFF = no sound. r = repeat interval in milliseconds where 0 is do not repeat, this is only used for bad sounds. |
-
+| `C29` — Reload Config from SD (SFB) | `C29` | **Local only (Serial/USB)**. Reload configuration from SD card config.txt file... |
+| `C30` — Export Config to SD (SFB) | `C30` | **Local only (Serial/USB)**. Export current in-memory configuration to SD card... |
Common error responses you may see: `Missing param`, `Missing params`, `Missing name`, `Empty name`, `Index out of range`, `Button out of range`, `Slot out of range`, `Relay out of range (or 255 to clear)`, `Invalid value (0 or 1)`, `Invalid mode (0=AP, 1=Client)`, `Only available in Client mode`, `Invalid port`, `Invalid offset (-12 to +14)`, `MMSI must be 9 digits`, `Invalid boat type`, `No available link slots`, `EEPROM commit failed`, `Unknown config command`, `Invalid type (0=day, 1=night)`, `Brightness must be 0-100`, `Invalid value (true/false or 0/1)`, `Missing params (t,r,g,b)`, `Missing params (t,b)`, `Missing params (g,w,s)`.
diff --git a/SD_CONFIG_IMPLEMENTATION.md b/SD_CONFIG_IMPLEMENTATION.md
new file mode 100644
index 0000000..5614d6b
--- /dev/null
+++ b/SD_CONFIG_IMPLEMENTATION.md
@@ -0,0 +1,355 @@
+# SD Card Configuration Loader - Manual Implementation Guide
+
+## Overview
+This guide lists all manual edits required to complete the SD Card Configuration Loader implementation. The following files have been automatically created:
+- ✅ `SmartFuseBox/SdCardConfigLoader.h`
+- ✅ `SmartFuseBox/SdCardConfigLoader.cpp`
+- ✅ `SD_CONFIG_README.md`
+
+The following files require manual edits due to whitespace matching issues with the editing tool.
+
+---
+
+## Manual Edits Required
+
+### 1. Shared/SystemDefinitions.h
+**Location**: After line 74 (after `constexpr char ControlPanelTones[] = "C28";`)
+
+**Add**:
+```cpp
+constexpr char ConfigReloadFromSd[] = "C29";
+constexpr char ConfigExportToSd[] = "C30";
+```
+
+---
+
+### 2. Shared/CommandHandlers/ConfigCommandHandler.h
+**Location**: Update the class declaration
+
+**Change**:
+```cpp
+// Forward declarations
+class BluetoothController;
+class ConfigSyncManager;
+
+class ConfigCommandHandler : public BaseCommandHandler
+{
+private:
+ WifiController* _wifiController;
+ ConfigController* _configController;
+ ConfigSyncManager* _configSyncManager;
+
+public:
+ explicit ConfigCommandHandler(WifiController* wifiController, ConfigController* configController);
+
+ void setConfigSyncManager(ConfigSyncManager* syncManager);
+```
+
+**To**:
+```cpp
+// Forward declarations
+class BluetoothController;
+class ConfigSyncManager;
+class SdCardConfigLoader; // ADD THIS LINE
+
+class ConfigCommandHandler : public BaseCommandHandler
+{
+private:
+ WifiController* _wifiController;
+ ConfigController* _configController;
+ ConfigSyncManager* _configSyncManager;
+ SdCardConfigLoader* _sdCardConfigLoader; // ADD THIS LINE
+
+public:
+ explicit ConfigCommandHandler(WifiController* wifiController, ConfigController* configController);
+
+ void setConfigSyncManager(ConfigSyncManager* syncManager);
+ void setSdCardConfigLoader(SdCardConfigLoader* sdCardConfigLoader); // ADD THIS LINE
+```
+
+---
+
+### 3. Shared/CommandHandlers/ConfigCommandHandler.cpp
+
+#### 3a. Add include at top
+**Location**: After `#include "ConfigSyncManager.h"`
+
+**Add**:
+```cpp
+#include "SdCardConfigLoader.h"
+```
+
+#### 3b. Update constructor
+**Location**: Lines 10-14
+
+**Change**:
+```cpp
+ConfigCommandHandler::ConfigCommandHandler(WifiController* wifiController, ConfigController* configController)
+ : _wifiController(wifiController),
+ _configController(configController),
+ _configSyncManager(nullptr)
+{
+}
+```
+
+**To**:
+```cpp
+ConfigCommandHandler::ConfigCommandHandler(WifiController* wifiController, ConfigController* configController)
+ : _wifiController(wifiController),
+ _configController(configController),
+ _configSyncManager(nullptr),
+ _sdCardConfigLoader(nullptr) // ADD THIS LINE
+{
+}
+```
+
+#### 3c. Add setter method
+**Location**: After `setConfigSyncManager` method (around line 19)
+
+**Add**:
+```cpp
+void ConfigCommandHandler::setSdCardConfigLoader(SdCardConfigLoader* sdCardConfigLoader)
+{
+ _sdCardConfigLoader = sdCardConfigLoader;
+}
+```
+
+#### 3d. Add C29 and C30 command handlers
+**Location**: After the C28 (ControlPanelTones) handler and before the `else` clause (around line 635)
+
+**Add**:
+```cpp
+ else if (strcmp(command, "C29") == 0) // ConfigReloadFromSd
+ {
+ // C29 - Reload config from SD card
+ if (_sdCardConfigLoader)
+ {
+ if (_sdCardConfigLoader->reloadConfigFromSd())
+ {
+ result = ConfigResult::Success;
+ }
+ else
+ {
+ sendAckErr(sender, command, F("Failed to reload from SD"));
+ return true;
+ }
+ }
+ else
+ {
+ sendAckErr(sender, command, F("SD config loader not available"));
+ return true;
+ }
+ }
+ else if (strcmp(command, "C30") == 0) // ConfigExportToSd
+ {
+ // C30 - Export current config to SD card
+ if (_sdCardConfigLoader)
+ {
+ if (_sdCardConfigLoader->exportConfigToSd())
+ {
+ result = ConfigResult::Success;
+ }
+ else
+ {
+ sendAckErr(sender, command, F("Failed to export to SD"));
+ return true;
+ }
+ }
+ else
+ {
+ sendAckErr(sender, command, F("SD config loader not available"));
+ return true;
+ }
+ }
+```
+
+#### 3e. Update supportedCommands method
+**Location**: Around line 693
+
+**Change**:
+```cpp
+const char* const* ConfigCommandHandler::supportedCommands(size_t& count) const
+{
+ static const char* cmds[] = {
+ ConfigSaveSettings, ConfigGetSettings, ConfigResetSettings,
+ ConfigRename, ConfigRenameRelay, ConfigMapHomeButton, ConfigSetButtonColor,
+ ConfigBoatType, ConfigSoundRelayId, ConfigSoundStartDelay,
+#if defined(ARDUINO_UNO_R4)
+ ConfigBluetoothEnable, ConfigWifiEnable, ConfigWifiMode, ConfigWifiSSID,
+ ConfigWifiPassword, ConfigWifiPort, ConfigWifiState, ConfigWifiApIpAddress,
+#endif
+ ConfigDefaultRelayState, ConfigLinkRelays,
+ ConfigTimeZoneOffset, ConfigMmsi, ConfigCallSign, ConfigHomePort,
+ ConfigLedColor, ConfigLedBrightness, ConfigLedAutoSwitch, ConfigLedEnable,
+ ControlPanelTones
+ };
+ count = sizeof(cmds) / sizeof(cmds[0]);
+ return cmds;
+}
+```
+
+**To**:
+```cpp
+const char* const* ConfigCommandHandler::supportedCommands(size_t& count) const
+{
+ static const char* cmds[] = {
+ ConfigSaveSettings, ConfigGetSettings, ConfigResetSettings,
+ ConfigRename, ConfigRenameRelay, ConfigMapHomeButton, ConfigSetButtonColor,
+ ConfigBoatType, ConfigSoundRelayId, ConfigSoundStartDelay,
+#if defined(ARDUINO_UNO_R4)
+ ConfigBluetoothEnable, ConfigWifiEnable, ConfigWifiMode, ConfigWifiSSID,
+ ConfigWifiPassword, ConfigWifiPort, ConfigWifiState, ConfigWifiApIpAddress,
+#endif
+ ConfigDefaultRelayState, ConfigLinkRelays,
+ ConfigTimeZoneOffset, ConfigMmsi, ConfigCallSign, ConfigHomePort,
+ ConfigLedColor, ConfigLedBrightness, ConfigLedAutoSwitch, ConfigLedEnable,
+ ControlPanelTones,
+ "C29", "C30" // ADD THIS LINE
+ };
+ count = sizeof(cmds) / sizeof(cmds[0]);
+ return cmds;
+}
+```
+
+---
+
+### 4. SmartFuseBox.ino
+
+#### 4a. Add include
+**Location**: After `#include "SdCardLogger.h"` (around line 54)
+
+**Add**:
+```cpp
+#include "SdCardConfigLoader.h"
+```
+
+#### 4b. Add SD card config loader instantiation
+**Location**: After the SD card logger instantiation (around line 141)
+
+**Add**:
+```cpp
+// SD card config loader
+SdCardConfigLoader sdCardConfigLoader(&commandMgrComputer, &commandMgrLink, &configController, &configSyncManager, SdCardCsPin);
+```
+
+#### 4c. Link config loader to config handler
+**Location**: In setup() function, after `configHandler.setConfigSyncManager(&configSyncManager);` (around line 177)
+
+**Add**:
+```cpp
+ configHandler.setSdCardConfigLoader(&sdCardConfigLoader);
+```
+
+#### 4d. Load SD config at boot
+**Location**: In setup() function, after `sdCardLogger.initialize();` (around line 188)
+
+**Add**:
+```cpp
+ // Load config from SD card if present (this will disable ConfigSyncManager if SD config found)
+ bool sdConfigLoaded = sdCardConfigLoader.loadConfigFromSd();
+```
+
+#### 4e. Conditional config sync
+**Location**: In setup() function, replace the line `configSyncManager.requestSync();` (around line 202)
+
+**Change**:
+```cpp
+ configSyncManager.requestSync();
+```
+
+**To**:
+```cpp
+ // Only request config sync if SD config was not loaded
+ if (!sdConfigLoaded)
+ {
+ configSyncManager.requestSync();
+ }
+```
+
+---
+
+### 5. Commands.md
+
+#### 5a. Add C29 and C30 commands
+**Location**: After C28 in the Configuration Commands table
+
+**Add**:
+```markdown
+| `C29` — Reload Config from SD (SFB) | `C29` | Reload configuration from SD card config.txt file. If successful, applies all settings from SD card, saves to EEPROM, and syncs to control panel via LINK. ConfigSyncManager is disabled when SD config is active. No params. Returns error if SD card not present or config file invalid. |
+| `C30` — Export Config to SD (SFB) | `C30` | Export current in-memory configuration to SD card config.txt file. Creates a new file with all current settings in command format, suitable for editing and reloading. Overwrites existing config.txt if present. No params. Returns error if SD card not present or write fails. |
+```
+
+#### 5b. Update error responses
+**Location**: In the "Common error responses" section (after C28)
+
+**Change**:
+```markdown
+Common error responses you may see: `Missing param`, `Missing params`, `Missing name`, `Empty name`, `Index out of range`, `Button out of range`, `Slot out of range`, `Relay out of range (or 255 to clear)`, `Invalid value (0 or 1)`, `Invalid mode (0=AP, 1=Client)`, `Only available in Client mode`, `Invalid port`, `Invalid offset (-12 to +14)`, `MMSI must be 9 digits`, `Invalid boat type`, `No available link slots`, `EEPROM commit failed`, `Unknown config command`, `Invalid type (0=day, 1=night)`, `Brightness must be 0-100`, `Invalid value (true/false or 0/1)`, `Missing params (t,r,g,b)`, `Missing params (t,b)`, `Missing params (g,w,s)`.
+```
+
+**To**:
+```markdown
+Common error responses you may see: `Missing param`, `Missing params`, `Missing name`, `Empty name`, `Index out of range`, `Button out of range`, `Slot out of range`, `Relay out of range (or 255 to clear)`, `Invalid value (0 or 1)`, `Invalid mode (0=AP, 1=Client)`, `Only available in Client mode`, `Invalid port`, `Invalid offset (-12 to +14)`, `MMSI must be 9 digits`, `Invalid boat type`, `No available link slots`, `EEPROM commit failed`, `Unknown config command`, `Invalid type (0=day, 1=night)`, `Brightness must be 0-100`, `Invalid value (true/false or 0/1)`, `Missing params (t,r,g,b)`, `Missing params (t,b)`, `Missing params (g,w,s)`, `Failed to reload from SD`, `Failed to export to SD`, `SD config loader not available`.
+```
+
+---
+
+## Testing Checklist
+
+After making all manual edits, test the following:
+
+### Compilation
+- [ ] Project compiles without errors
+- [ ] No warnings related to SD config loader
+
+### Boot Sequence (No SD Card)
+- [ ] System boots normally
+- [ ] EEPROM config is loaded
+- [ ] ConfigSyncManager is enabled
+- [ ] Serial shows: "SD card not present or not accessible"
+
+### Boot Sequence (With SD Card + config.txt)
+- [ ] System boots and loads SD config
+- [ ] EEPROM is updated with SD config
+- [ ] ConfigSyncManager is disabled
+- [ ] Control panel receives config via LINK
+- [ ] Serial shows: "SD config loaded: X commands applied, Y errors"
+
+### C29 Command
+- [ ] C29 reloads config from SD card
+- [ ] Changes are applied and saved to EEPROM
+- [ ] Control panel is synced
+- [ ] Returns ACK:C29=ok on success
+
+### C30 Command
+- [ ] C30 exports current config to SD card
+- [ ] config.txt is created/overwritten
+- [ ] File format is correct and parseable
+- [ ] Returns ACK:C30=ok on success
+
+### Error Handling
+- [ ] Parse errors are logged to Serial
+- [ ] Invalid commands don't crash the system
+- [ ] Missing SD card returns appropriate error
+- [ ] Missing config.txt falls back to EEPROM
+
+---
+
+## Summary
+
+**Files Created**:
+- `SmartFuseBox/SdCardConfigLoader.h`
+- `SmartFuseBox/SdCardConfigLoader.cpp`
+- `SD_CONFIG_README.md`
+- `SD_CONFIG_IMPLEMENTATION.md` (this file)
+
+**Files Requiring Manual Edits**:
+1. `Shared/SystemDefinitions.h` - Add C29/C30 constants
+2. `Shared/CommandHandlers/ConfigCommandHandler.h` - Add SD loader member and setter
+3. `Shared/CommandHandlers/ConfigCommandHandler.cpp` - Add C29/C30 handlers
+4. `SmartFuseBox.ino` - Integrate SD loader into boot sequence
+5. `Commands.md` - Document C29/C30 commands
+
+**Total Manual Edits**: 11 locations across 5 files
+
+For detailed usage instructions and examples, see `SD_CONFIG_README.md`.
diff --git a/SD_CONFIG_README.md b/SD_CONFIG_README.md
new file mode 100644
index 0000000..7c70a54
--- /dev/null
+++ b/SD_CONFIG_README.md
@@ -0,0 +1,339 @@
+# SD Card Configuration Loader
+
+## Overview
+The SD Card Configuration Loader provides a robust, plug-and-play configuration management system for the SmartFuseBox. It allows users to preconfigure settings on an SD card and automatically apply them at boot, with synchronization to connected control panels.
+
+## Features
+- ✅ **Plug-and-Play**: Insert SD card with `config.txt` and settings are automatically applied
+- ✅ **EEPROM Persistence**: SD config is saved to EEPROM for retention after card removal
+- ✅ **LINK Synchronization**: Automatically syncs configuration to control panel via LINK
+- ✅ **ConfigSyncManager Integration**: Automatically disables ConfigSyncManager when SD config is present
+- ✅ **Error Logging**: Comprehensive error reporting via Serial
+- ✅ **Command Support**: C29 (reload) and C30 (export) commands for runtime management
+- ✅ **Validation**: Full command validation before applying changes
+- ✅ **Human-Readable Format**: Easy to edit configuration file
+
+## Boot Sequence
+1. **Check SD Card**: System checks for SD card presence
+2. **Load config.txt**: If found, reads and parses all configuration commands
+3. **Validate**: Each command is validated before application
+4. **Apply Changes**: Configuration is applied to in-memory config
+5. **Save to EEPROM**: If changes were made, saves to EEPROM for persistence
+6. **Sync via LINK**: Sends configuration to control panel
+7. **Disable ConfigSyncManager**: Prevents periodic sync requests from control panel
+
+If no SD card or config.txt is present, the system boots normally using EEPROM settings and ConfigSyncManager remains enabled.
+
+## Configuration File Format
+
+### File Location
+- **Filename**: `config.txt`
+- **Location**: Root directory of SD card
+- **Encoding**: Plain text (ASCII/UTF-8)
+
+### File Format
+The configuration file uses the same command format as the serial protocol:
+```
+# Comments start with #
+# Empty lines are ignored
+
+# Boat identification
+C3:Sea Wolf
+
+# Relay names (short|long format)
+C4:0=Nav|Navigation
+C4:1=Bilge|Bilge Pump
+C4:2=Light|Cabin Lights
+C4:3=Pump|Water Pump
+C4:4=Horn|Sound Horn
+C4:5=Fan|Ventilation
+C4:6=Spare|Spare 6
+C4:7=Spare|Spare 7
+
+# Home button mappings (slot=relay)
+C5:0=0
+C5:1=1
+C5:2=2
+C5:3=3
+
+# Button colors (button=color_index)
+C6:0=4
+C6:1=4
+C6:2=4
+C6:3=4
+C6:4=4
+C6:5=4
+C6:6=4
+C6:7=4
+
+# Vessel type (0=Motor, 1=Sail, 2=Fishing, 3=Yacht)
+C7:v=0
+
+# Sound relay (255=unmapped)
+C8:v=255
+
+# Sound delay (milliseconds)
+C9:v=244
+
+# Bluetooth enabled (0=off, 1=on)
+C10:v=0
+
+# WiFi enabled (0=off, 1=on)
+C11:v=1
+
+# WiFi mode (0=AP, 1=Client)
+C12:v=0
+
+# WiFi SSID (no v= prefix)
+C13:SmartFuseBox
+
+# WiFi password (no v= prefix)
+C14:sfb-776064
+
+# WiFi port
+C15:v=80
+
+# WiFi AP IP address (no v= prefix)
+C17:192.168.4.1
+
+# Default relay states (relay=state, 0=off, 1=on)
+C18:0=0
+C18:1=0
+C18:2=0
+C18:3=0
+C18:4=0
+C18:5=0
+C18:6=0
+C18:7=0
+
+# Linked relays (relay1=relay2, 255=unlink)
+C19:255=255
+C19:255=255
+
+# Timezone offset (hours from UTC)
+C20:v=0
+
+# MMSI (9 digits)
+C21:000000000
+
+# Call sign (no v= prefix, can be empty)
+C22:
+
+# Home port (no v= prefix, can be empty)
+C23:
+
+# LED colors (t=type[0=day,1=night];c=colorset[0=good,1=bad];r=red;g=green;b=blue)
+C24:t=0;c=0;r=0;g=80;b=255
+C24:t=0;c=1;r=255;g=140;b=0
+C24:t=1;c=0;r=100;g=0;b=0
+C24:t=1;c=1;r=255;g=50;b=0
+
+# LED brightness (t=type[0=day,1=night];b=brightness[0-100])
+C25:t=0;b=80
+C25:t=1;b=20
+
+# LED auto switch (0=off, 1=on)
+C26:v=1
+
+# LED enable states (g=GPS;w=Warning;s=System)
+C27:g=1;w=1;s=1
+
+# Control panel tones (t=type[0=good,1=bad];h=Hz;d=duration;p=preset;r=repeat)
+C28:t=0;h=1000;d=100;p=1;r=0
+C28:t=1;h=400;d=500;p=5;r=30000
+```
+
+### Command Reference
+See [Commands.md](Commands.md) for detailed documentation on each command format and parameters.
+
+## Usage
+
+### Initial Setup
+1. Create a `config.txt` file with your desired settings
+2. Copy the file to the root directory of a FAT32-formatted SD card
+3. Insert the SD card into the SmartFuseBox
+4. Power on the system
+5. Monitor Serial output for confirmation: `SD_CFG_INFO: SD config loaded: X commands applied, Y errors`
+
+### Editing Configuration
+1. Power off the system
+2. Remove the SD card
+3. Edit `config.txt` on your computer
+4. Insert the SD card back into the SmartFuseBox
+5. Power on (configuration will be reloaded automatically)
+
+**OR** use the C29 command to reload without power cycle:
+```
+C29
+```
+
+### Exporting Current Configuration
+To save your current settings to SD card:
+```
+C30
+```
+This creates/overwrites `config.txt` with all current settings.
+
+### Removing SD Card Configuration
+If you want to go back to control panel-driven configuration:
+1. Power off the system
+2. Remove the SD card (or delete/rename `config.txt`)
+3. Power on the system
+4. ConfigSyncManager will be re-enabled automatically
+5. Control panel can now manage configuration via C1 sync requests
+
+## Commands
+
+### C29 — Reload Config from SD
+**Format**: `C29`
+**Parameters**: None
+**Purpose**: Reload configuration from SD card `config.txt` file
+
+**Behavior**:
+- Checks for SD card presence
+- Reads and parses `config.txt`
+- Applies all valid commands
+- Saves changes to EEPROM
+- Syncs to control panel via LINK
+- Returns `ACK:C29=ok` on success
+
+**Errors**:
+- `Failed to reload from SD` - SD card not present, file not found, or parse errors
+- `SD config loader not available` - SD card logger not initialized
+
+### C30 — Export Config to SD
+**Format**: `C30`
+**Parameters**: None
+**Purpose**: Export current configuration to SD card `config.txt` file
+
+**Behavior**:
+- Checks for SD card presence
+- Creates/overwrites `config.txt`
+- Writes all current settings in command format
+- Includes header comment with timestamp
+- Returns `ACK:C30=ok` on success
+
+**Errors**:
+- `Failed to export to SD` - SD card not present or write failed
+- `SD config loader not available` - SD card logger not initialized
+
+## Error Handling
+
+### Parse Errors
+If the config loader encounters an error parsing a command:
+- Error is logged to Serial: `SD_CFG_ERROR: Command failed: (result=)`
+- The problematic line is logged: `SD_CFG_LINE: `
+- Processing continues with remaining commands
+- Partial configuration may be applied
+
+### SD Card Issues
+If SD card is not present or unreadable:
+- System boots using EEPROM configuration
+- ConfigSyncManager remains enabled
+- Info logged: `SD card not present or not accessible`
+
+### Missing Config File
+If SD card is present but `config.txt` is not found:
+- System boots using EEPROM configuration
+- ConfigSyncManager remains enabled
+- Info logged: `Config file not found on SD card`
+
+## Integration with ConfigSyncManager
+
+The SD Card Config Loader intelligently manages ConfigSyncManager:
+
+**When SD config is loaded**:
+- `ConfigSyncManager.setEnabled(false)` is called
+- Periodic C1 sync requests from control panel are disabled
+- SD card becomes the authoritative configuration source
+- Control panel receives config via LINK but doesn't initiate syncs
+
+**When SD config is NOT loaded**:
+- ConfigSyncManager remains enabled
+- Control panel can request config sync via C1
+- Bidirectional configuration management is active
+
+This prevents conflicts between SD card configuration and control panel-driven configuration.
+
+## Best Practices
+
+### File Management
+- ✅ Keep a backup copy of your `config.txt` on your computer
+- ✅ Use meaningful relay names in your configuration
+- ✅ Add comments to document your configuration choices
+- ✅ Test configuration changes incrementally
+- ✅ Use C30 to export working configurations as templates
+
+### Configuration Workflow
+1. **Initial Setup**: Use C30 to export default config as starting point
+2. **Customize**: Edit exported config on computer
+3. **Test**: Load config with SD card, verify behavior
+4. **Iterate**: Make adjustments, reload with C29
+5. **Deploy**: Copy final config to production SD cards
+
+### Troubleshooting
+- Enable Serial monitor to view detailed loading logs
+- Check for parse errors in Serial output
+- Verify command format matches documentation
+- Ensure SD card is FAT32 formatted
+- Confirm `config.txt` is in root directory (not in subfolder)
+- Use C1 command to verify applied configuration
+
+## Technical Details
+
+### File Reading
+- Line-by-line parsing (max 128 characters per line)
+- Comments (#) and empty lines are skipped
+- Whitespace and newlines are automatically trimmed
+- Commands are validated before application
+
+### Memory Management
+- Minimal memory footprint during parsing
+- No large buffers or caching
+- Immediate application of each command
+- Single-pass file reading
+
+### SD Card Compatibility
+- SdFat library used for robust SD card access
+- Supports FAT16/FAT32 file systems
+- Compatible with standard SD and SDHC cards
+- Chip select pin: D10 (SdCardCsPin)
+
+### Performance
+- Boot time impact: ~1-2 seconds for typical config file
+- C29 reload: ~1-2 seconds
+- C30 export: ~500ms-1 second
+- No impact on runtime performance after boot
+
+## Examples
+
+### Minimal Configuration
+```
+# Minimal config - just boat name and one relay
+C3:My Boat
+C4:0=Bilge|Bilge Pump
+C18:0=0
+```
+
+### Advanced Configuration
+See the full example at the top of this document.
+
+### Multiple Profiles
+Create different config files for different scenarios:
+- `config_default.txt` - Standard configuration
+- `config_winter.txt` - Winter storage settings
+- `config_race.txt` - Racing configuration
+
+Rename the desired profile to `config.txt` before boot.
+
+## Related Documentation
+- [Commands.md](Commands.md) - Complete command reference
+- [CONFIG_SYNC_README.md](CONFIG_SYNC_README.md) - ConfigSyncManager documentation
+- [SdCardLogger.h](SmartFuseBox/SdCardLogger.h) - SD card logging functionality
+
+## Version History
+- v1.0.0 - Initial implementation
+ - SD card config loading at boot
+ - C29 reload command
+ - C30 export command
+ - ConfigSyncManager integration
diff --git a/Shared/CommandHandlers/AckCommandHandler.cpp b/Shared/CommandHandlers/AckCommandHandler.cpp
index 8a47ba7..e1987a4 100644
--- a/Shared/CommandHandlers/AckCommandHandler.cpp
+++ b/Shared/CommandHandlers/AckCommandHandler.cpp
@@ -116,6 +116,8 @@ void AckCommandHandler::setConfigSyncManager(ConfigSyncManager* syncManager, Con
bool AckCommandHandler::processConfigAck(SerialCommandManager* sender, const char* key, const char* value)
{
+ (void)sender;
+
// Handle C1 (ConfigGetSettings) acknowledgement
if (strcmp(key, ConfigGetSettings) == 0 && strcmp(value, AckSuccess) == 0)
{
diff --git a/Shared/CommandHandlers/ConfigCommandHandler.cpp b/Shared/CommandHandlers/ConfigCommandHandler.cpp
index c3d34d6..6c4c356 100644
--- a/Shared/CommandHandlers/ConfigCommandHandler.cpp
+++ b/Shared/CommandHandlers/ConfigCommandHandler.cpp
@@ -1,5 +1,6 @@
#include "ConfigCommandHandler.h"
#include "ConfigSyncManager.h"
+#include "SdCardConfigLoader.h"
#if defined(ARDUINO_UNO_R4)
#include "BluetoothController.h"
@@ -10,7 +11,8 @@
ConfigCommandHandler::ConfigCommandHandler(WifiController* wifiController, ConfigController* configController)
: _wifiController(wifiController),
_configController(configController),
- _configSyncManager(nullptr)
+ _configSyncManager(nullptr),
+ _sdCardConfigLoader(nullptr)
{
}
@@ -634,6 +636,48 @@ bool ConfigCommandHandler::handleCommand(SerialCommandManager* sender, const cha
result = ConfigResult::InvalidParameter;
}
}
+ else if (strcmp(command, ConfigReloadFromSd) == 0)
+ {
+ // C29 - Reload config from SD card
+ if (_sdCardConfigLoader)
+ {
+ if (_sdCardConfigLoader->reloadConfigFromSd())
+ {
+ result = ConfigResult::Success;
+ }
+ else
+ {
+ sendAckErr(sender, command, F("Failed to reload from SD"));
+ return true;
+ }
+ }
+ else
+ {
+ sendAckErr(sender, command, F("SD config loader not available"));
+ return true;
+ }
+ }
+ else if (strcmp(command, ConfigExportToSd) == 0)
+ {
+ // C30 - Export current config to SD card
+ if (_sdCardConfigLoader)
+ {
+ if (_sdCardConfigLoader->exportConfigToSd())
+ {
+ result = ConfigResult::Success;
+ }
+ else
+ {
+ sendAckErr(sender, command, F("Failed to export to SD"));
+ return true;
+ }
+ }
+ else
+ {
+ sendAckErr(sender, command, F("SD config loader not available"));
+ return true;
+ }
+ }
else
{
result = ConfigResult::InvalidCommand;
@@ -677,6 +721,11 @@ bool ConfigCommandHandler::handleCommand(SerialCommandManager* sender, const cha
return true;
}
+void ConfigCommandHandler::setSdCardConfigLoader(SdCardConfigLoader* sdCardConfigLoader)
+{
+ _sdCardConfigLoader = sdCardConfigLoader;
+}
+
const char* const* ConfigCommandHandler::supportedCommands(size_t& count) const
{
static const char* cmds[] = {
@@ -690,7 +739,7 @@ const char* const* ConfigCommandHandler::supportedCommands(size_t& count) const
ConfigDefaultRelayState, ConfigLinkRelays,
ConfigTimeZoneOffset, ConfigMmsi, ConfigCallSign, ConfigHomePort,
ConfigLedColor, ConfigLedBrightness, ConfigLedAutoSwitch, ConfigLedEnable,
- ControlPanelTones
+ ControlPanelTones, ConfigReloadFromSd, ConfigExportToSd
};
count = sizeof(cmds) / sizeof(cmds[0]);
return cmds;
diff --git a/Shared/CommandHandlers/ConfigCommandHandler.h b/Shared/CommandHandlers/ConfigCommandHandler.h
index 7f500a0..f633d39 100644
--- a/Shared/CommandHandlers/ConfigCommandHandler.h
+++ b/Shared/CommandHandlers/ConfigCommandHandler.h
@@ -4,10 +4,13 @@
#include "BaseCommandHandler.h"
#include "ConfigController.h"
#include "WifiController.h"
+#include "ConfigSyncManager.h"
+#include "SDCardConfigLoader.h"
// Forward declarations
class BluetoothController;
class ConfigSyncManager;
+class SdCardConfigLoader;
class ConfigCommandHandler : public BaseCommandHandler
{
@@ -15,11 +18,13 @@ class ConfigCommandHandler : public BaseCommandHandler
WifiController* _wifiController;
ConfigController* _configController;
ConfigSyncManager* _configSyncManager;
+ SdCardConfigLoader* _sdCardConfigLoader;
public:
explicit ConfigCommandHandler(WifiController* wifiController, ConfigController* configController);
void setConfigSyncManager(ConfigSyncManager* syncManager);
+ void setSdCardConfigLoader(SdCardConfigLoader* sdCardConfigLoader);
bool handleCommand(SerialCommandManager* sender, const char* command, const StringKeyValue params[], uint8_t paramCount) override;
const char* const* supportedCommands(size_t& count) const override;
diff --git a/Shared/CommandHandlers/SensorCommandHandler.cpp b/Shared/CommandHandlers/SensorCommandHandler.cpp
index 12b4697..90a032d 100644
--- a/Shared/CommandHandlers/SensorCommandHandler.cpp
+++ b/Shared/CommandHandlers/SensorCommandHandler.cpp
@@ -241,54 +241,148 @@ bool SensorCommandHandler::handleCommand(SerialCommandManager* sender, const cha
// Handle query requests (no parameters) - return current sensor values
if (paramCount == 0)
{
- char buffer[32];
+ char buffer[64];
+ sendDebugMessage(F("Query request - no params"), F("SensorCommandHandler"));
- if (strcmp(command, SensorLightSensor) == 0)
- {
- // S9 query - return current day/night status
- snprintf_P(buffer, sizeof(buffer), PSTR("v=%d"), _isDaytime ? 1 : 0);
- sender->sendCommand(SensorLightSensor, buffer);
- sendAckOk(sender, command);
- return true;
- }
- else if (strcmp(command, SensorTemperature) == 0)
+ if (strcmp(command, SensorTemperature) == 0)
{
+ // S0 query
snprintf_P(buffer, sizeof(buffer), PSTR("v=%.1f"), _lastTemperature);
sender->sendCommand(SensorTemperature, buffer);
sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Temperature"), F("SensorCommandHandler"));
return true;
}
else if (strcmp(command, SensorHumidity) == 0)
{
+ // S1 query
snprintf_P(buffer, sizeof(buffer), PSTR("v=%d"), _lastHumidity);
sender->sendCommand(SensorHumidity, buffer);
sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Humidity"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorBearing) == 0)
+ {
+ // S2 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("v=%.1f"), _lastBearing);
+ sender->sendCommand(SensorBearing, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Bearing"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorDirection) == 0)
+ {
+ // S3 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("v=%s"), _gpsDirection ? _gpsDirection : "N");
+ sender->sendCommand(SensorDirection, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Direction"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorSpeed) == 0)
+ {
+ // S4 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("v=%d"), _lastSpeed);
+ sender->sendCommand(SensorSpeed, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Speed"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorCompassTemp) == 0)
+ {
+ // S5 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("v=%.1f"), _lastCompassTemp);
+ sender->sendCommand(SensorCompassTemp, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Compass Temp"), F("SensorCommandHandler"));
return true;
}
else if (strcmp(command, SensorWaterLevel) == 0)
{
+ // S6 query
snprintf_P(buffer, sizeof(buffer), PSTR("v=%d"), _lastWaterLevel);
sender->sendCommand(SensorWaterLevel, buffer);
sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Water Level"), F("SensorCommandHandler"));
return true;
}
else if (strcmp(command, SensorWaterPumpActive) == 0)
{
+ // S7 query
snprintf_P(buffer, sizeof(buffer), PSTR("v=%d"), _lastWaterPumpActive ? 1 : 0);
sender->sendCommand(SensorWaterPumpActive, buffer);
sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Water Pump"), F("SensorCommandHandler"));
return true;
}
else if (strcmp(command, SensorHornActive) == 0)
{
+ // S8 query
snprintf_P(buffer, sizeof(buffer), PSTR("v=%d"), _lastHornActive ? 1 : 0);
sender->sendCommand(SensorHornActive, buffer);
sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Horn Active"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorLightSensor) == 0)
+ {
+ // S9 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("v=%d"), _isDaytime ? 1 : 0);
+ sender->sendCommand(SensorLightSensor, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned Light Sensor"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorGpsLatLong) == 0)
+ {
+ // S10 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("lat=%.6f&lon=%.6f"), _gpsLatitude, _gpsLongitude);
+ sender->sendCommand(SensorGpsLatLong, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned GPS LatLong"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorGpsAltitude) == 0)
+ {
+ // S11 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("v=%.2f"), _altitude);
+ sender->sendCommand(SensorGpsAltitude, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned GPS Altitude"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorGpsSpeed) == 0)
+ {
+ // S12 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("v=%.2f&course=%.2f&dir=%s"),
+ (double)_lastSpeed, _gpsCourse, _gpsDirection ? _gpsDirection : "N");
+ sender->sendCommand(SensorGpsSpeed, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned GPS Speed"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorGpsSatellites) == 0)
+ {
+ // S13 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("v=%lu"), (unsigned long)_gpsSatellites);
+ sender->sendCommand(SensorGpsSatellites, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned GPS Satellites"), F("SensorCommandHandler"));
+ return true;
+ }
+ else if (strcmp(command, SensorGpsDistance) == 0)
+ {
+ // S14 query
+ snprintf_P(buffer, sizeof(buffer), PSTR("v=%.2f"), _gpsDistance);
+ sender->sendCommand(SensorGpsDistance, buffer);
+ sendAckOk(sender, command);
+ sendDebugMessage(F("Returned GPS Distance"), F("SensorCommandHandler"));
return true;
}
// Unknown query command
- sendDebugMessage(F("No parameters in sensor command"), F("SensorCommandHandler"));
+ sendErrorMessage(F("Unknown sensor query command"), "SensorCommandHandler");
return false;
}
diff --git a/Shared/CommandHandlers/SystemCommandHandler.cpp b/Shared/CommandHandlers/SystemCommandHandler.cpp
index 1b0d6c3..af4e74e 100644
--- a/Shared/CommandHandlers/SystemCommandHandler.cpp
+++ b/Shared/CommandHandlers/SystemCommandHandler.cpp
@@ -18,8 +18,11 @@ SystemCommandHandler::~SystemCommandHandler()
const char* const* SystemCommandHandler::supportedCommands(size_t& count) const
{
- static const char* cmds[] = { SystemHeartbeatCommand, SystemInitialized, SystemFreeMemory, SystemCpuUsage,
- SystemBluetoothStatus, SystemWifiStatus, SystemSetDateTime, SystemGetDateTime };
+ static const char* cmds[] = {
+ SystemHeartbeatCommand, SystemInitialized, SystemFreeMemory, SystemCpuUsage,
+ SystemBluetoothStatus, SystemWifiStatus, SystemSetDateTime, SystemGetDateTime,
+ SystemSdCardPresent, SystemSdCardLogFileSize
+ };
count = sizeof(cmds) / sizeof(cmds[0]);
return cmds;
}
@@ -107,20 +110,13 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha
else if (strcmp(command, SystemSetDateTime) == 0 && paramCount == 1)
{
bool success = false;
-
- // Try ISO 8601 format first (contains 'T' or '-')
- if (strchr(params[0].value, 'T') != nullptr || strchr(params[0].value, '-') != nullptr)
- {
- success = DateTimeManager::setDateTimeISO(params[0].value);
- }
- else
+
+ // Only supports Unix timestamp (all digits)
+ unsigned long timestamp = static_cast(strtoul(params[0].value, nullptr, 0));
+ if (timestamp > 0)
{
- // Try Unix timestamp (all digits)
- unsigned long timestamp = static_cast(strtoul(params[0].value, nullptr, 0));
- if (timestamp > 0) {
- DateTimeManager::setDateTime(timestamp);
- success = true;
- }
+ DateTimeManager::setDateTime(timestamp);
+ success = true;
}
if (success)
@@ -132,6 +128,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha
}
else
{
+ _broadcaster->getComputerSerial()->sendDebug(command, F("Invalid Datetime"));
sendAckErr(sender, command, F("Invalid datetime format"));
}
@@ -153,6 +150,37 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha
return true;
}
+ else if (strcmp(command, SystemSdCardPresent) == 0)
+ {
+ bool present = false;
+
+#if defined(ARDUINO_UNO_R4)
+ if (_sdCardLogger)
+ {
+ present = _sdCardLogger->isSdCardPresent();
+ }
+#endif
+
+ char value = present ? '1' : '0';
+ StringKeyValue param = makeParam(ValueParamName, value);
+ sendAckOk(sender, command, ¶m);
+ }
+ else if (strcmp(command, SystemSdCardLogFileSize) == 0)
+ {
+ uint32_t fileSize = 0;
+
+#if defined(ARDUINO_UNO_R4)
+ if (_sdCardLogger)
+ {
+ fileSize = _sdCardLogger->getCurrentLogFileSize();
+ }
+#endif
+
+ StringKeyValue param;
+ strncpy(param.key, ValueParamName, sizeof(param.key));
+ snprintf_P(param.value, sizeof(param.value), PSTR("%lu"), (unsigned long)fileSize);
+ sendAckOk(sender, command, ¶m);
+ }
else
{
sendAckErr(sender, command, F("Unknown system command"));
@@ -162,6 +190,12 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha
}
#if defined(ARDUINO_UNO_R4)
+
+void SystemCommandHandler::setSdCardLogger(SdCardLogger* sdCardLogger)
+{
+ _sdCardLogger = sdCardLogger;
+}
+
void SystemCommandHandler::setWifiController(WifiController* wifiController)
{
_wifiController = wifiController;
diff --git a/Shared/CommandHandlers/SystemCommandHandler.h b/Shared/CommandHandlers/SystemCommandHandler.h
index 5daefcd..2d573b3 100644
--- a/Shared/CommandHandlers/SystemCommandHandler.h
+++ b/Shared/CommandHandlers/SystemCommandHandler.h
@@ -8,11 +8,18 @@
#include "Local.h"
#if defined(ARDUINO_UNO_R4)
+#include "SdCardLogger.h"
#include "WifiController.h"
#endif
class SystemCommandHandler : public SharedBaseCommandHandler
{
+private:
+#if defined(ARDUINO_UNO_R4)
+ WifiController* _wifiController = nullptr;
+ SdCardLogger* _sdCardLogger = nullptr;
+#endif
+
public:
explicit SystemCommandHandler(BroadcastManager* broadcaster, WarningManager* warningManager);
~SystemCommandHandler();
@@ -21,12 +28,8 @@ class SystemCommandHandler : public SharedBaseCommandHandler
const char* const* supportedCommands(size_t& count) const override;
#if defined(ARDUINO_UNO_R4)
- // Add this method
void setWifiController(WifiController* wifiController);
+ void setSdCardLogger(SdCardLogger* sdCardLogger);
#endif
-private:
-#if defined(ARDUINO_UNO_R4)
- WifiController* _wifiController = nullptr;
-#endif
};
diff --git a/Shared/DateTimeManager.cpp b/Shared/DateTimeManager.cpp
index 7690b71..81af215 100644
--- a/Shared/DateTimeManager.cpp
+++ b/Shared/DateTimeManager.cpp
@@ -7,20 +7,24 @@ unsigned long DateTimeManager::_syncedTimestamp = 0;
unsigned long DateTimeManager::_syncedMillis = 0;
bool DateTimeManager::_isSet = false;
-void DateTimeManager::setDateTime() {
+void DateTimeManager::setDateTime()
+{
// Set to default: January 1, 2025 00:00:00
setDateTime(DefaultTimestamp);
}
-void DateTimeManager::setDateTime(unsigned long unixTimestamp) {
+void DateTimeManager::setDateTime(unsigned long unixTimestamp)
+{
_syncedTimestamp = unixTimestamp;
_syncedMillis = millis();
_isSet = true;
}
-bool DateTimeManager::setDateTimeISO(const char* isoDateTime) {
+bool DateTimeManager::setDateTimeISO(const char* isoDateTime)
+{
// Expected format: YYYY-MM-DDTHH:MM:SS (19 characters minimum)
- if (strlen(isoDateTime) < 19) {
+ if (strlen(isoDateTime) < 19)
+ {
return false;
}
@@ -42,7 +46,8 @@ bool DateTimeManager::setDateTimeISO(const char* isoDateTime) {
day < 1 || day > 31 ||
hour > 23 ||
minute > 59 ||
- second > 59) {
+ second > 59)
+ {
return false;
}
@@ -52,8 +57,10 @@ bool DateTimeManager::setDateTimeISO(const char* isoDateTime) {
return true;
}
-unsigned long DateTimeManager::getCurrentTime() {
- if (!_isSet) {
+unsigned long DateTimeManager::getCurrentTime()
+{
+ if (!_isSet)
+ {
return 0;
}
@@ -61,7 +68,8 @@ unsigned long DateTimeManager::getCurrentTime() {
unsigned long currentMillis = millis();
unsigned long elapsedMillis;
- if (currentMillis >= _syncedMillis) {
+ if (currentMillis >= _syncedMillis)
+ {
elapsedMillis = currentMillis - _syncedMillis;
} else {
// millis() has overflowed (after ~49.7 days)
@@ -73,20 +81,23 @@ unsigned long DateTimeManager::getCurrentTime() {
return _syncedTimestamp + elapsedSeconds;
}
-bool DateTimeManager::isTimeSet() {
+bool DateTimeManager::isTimeSet()
+{
return _isSet;
}
unsigned long DateTimeManager::getSecondsSinceSync()
{
- if (!_isSet) {
+ if (!_isSet)
+ {
return 0;
}
unsigned long currentMillis = millis();
unsigned long elapsedMillis;
- if (currentMillis >= _syncedMillis) {
+ if (currentMillis >= _syncedMillis)
+ {
elapsedMillis = currentMillis - _syncedMillis;
} else {
elapsedMillis = (0xFFFFFFFF - _syncedMillis) + currentMillis + 1;
@@ -95,8 +106,10 @@ unsigned long DateTimeManager::getSecondsSinceSync()
return elapsedMillis / 1000;
}
-uint16_t DateTimeManager::getYear() {
- if (!_isSet) {
+uint16_t DateTimeManager::getYear()
+{
+ if (!_isSet)
+ {
return 0;
}
@@ -106,8 +119,10 @@ uint16_t DateTimeManager::getYear() {
return year;
}
-uint8_t DateTimeManager::getMonth() {
- if (!_isSet) {
+uint8_t DateTimeManager::getMonth()
+{
+ if (!_isSet)
+ {
return 0;
}
@@ -117,8 +132,10 @@ uint8_t DateTimeManager::getMonth() {
return month;
}
-uint8_t DateTimeManager::getDay() {
- if (!_isSet) {
+uint8_t DateTimeManager::getDay()
+{
+ if (!_isSet)
+ {
return 0;
}
@@ -128,8 +145,10 @@ uint8_t DateTimeManager::getDay() {
return day;
}
-uint8_t DateTimeManager::getHour() {
- if (!_isSet) {
+uint8_t DateTimeManager::getHour()
+{
+ if (!_isSet)
+ {
return 0;
}
@@ -139,8 +158,10 @@ uint8_t DateTimeManager::getHour() {
return hour;
}
-uint8_t DateTimeManager::getMinute() {
- if (!_isSet) {
+uint8_t DateTimeManager::getMinute()
+{
+ if (!_isSet)
+ {
return 0;
}
@@ -150,8 +171,10 @@ uint8_t DateTimeManager::getMinute() {
return minute;
}
-uint8_t DateTimeManager::getSecond() {
- if (!_isSet) {
+uint8_t DateTimeManager::getSecond()
+{
+ if (!_isSet)
+ {
return 0;
}
@@ -163,7 +186,8 @@ uint8_t DateTimeManager::getSecond() {
bool DateTimeManager::formatDateTime(char* buffer, const uint8_t bufferLength)
{
- if (!_isSet) {
+ if (!_isSet)
+ {
return false;
}
@@ -212,19 +236,23 @@ unsigned long DateTimeManager::dateTimeToUnix(uint16_t year, uint8_t month, uint
unsigned long days = 0;
// Years
- for (uint16_t y = 1970; y < year; y++) {
+ for (uint16_t y = 1970; y < year; y++)
+ {
days += 365;
// Add leap day if leap year
- if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) {
+ if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0))
+ {
days++;
}
}
// Months
- for (uint8_t m = 1; m < month; m++) {
+ for (uint8_t m = 1; m < month; m++)
+ {
days += daysInMonth[m - 1];
// Add leap day for February in leap year
- if (m == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
+ if (m == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
+ {
days++;
}
}
@@ -242,7 +270,8 @@ unsigned long DateTimeManager::dateTimeToUnix(uint16_t year, uint8_t month, uint
}
void DateTimeManager::unixToDateTime(unsigned long unixTime, uint16_t& year, uint8_t& month, uint8_t& day,
- uint8_t& hour, uint8_t& minute, uint8_t& second) {
+ uint8_t& hour, uint8_t& minute, uint8_t& second)
+{
// Extract time components
second = unixTime % 60;
unixTime /= 60;
@@ -253,13 +282,16 @@ void DateTimeManager::unixToDateTime(unsigned long unixTime, uint16_t& year, uin
// Calculate year
year = 1970;
- while (true) {
+ while (true)
+ {
uint16_t daysInYear = 365;
- if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
+ if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
+ {
daysInYear = 366;
}
- if (days < daysInYear) {
+ if (days < daysInYear)
+ {
break;
}
@@ -272,13 +304,16 @@ void DateTimeManager::unixToDateTime(unsigned long unixTime, uint16_t& year, uin
bool isLeapYear = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
month = 1;
- while (month <= 12) {
+ while (month <= 12)
+ {
uint8_t monthDays = daysInMonth[month - 1];
- if (month == 2 && isLeapYear) {
+ if (month == 2 && isLeapYear)
+ {
monthDays = 29;
}
- if (days < monthDays) {
+ if (days < monthDays)
+ {
break;
}
diff --git a/Shared/Sensors/GpsSensorHandler.h b/Shared/Sensors/GpsSensorHandler.h
index 491bfc7..9ebdd9e 100644
--- a/Shared/Sensors/GpsSensorHandler.h
+++ b/Shared/Sensors/GpsSensorHandler.h
@@ -63,6 +63,7 @@ class GpsSensorHandler : public BaseSensor, public BroadcastLoggerSupport
/**
* @brief Synchronize DateTimeManager with GPS UTC time.
* Converts GPS date/time to Unix timestamp and updates DateTimeManager.
+ * Supports partial updates: if only date or time is valid, uses existing value from DateTimeManager for the missing component.
*/
void syncTimeFromGps()
{
@@ -71,34 +72,71 @@ class GpsSensorHandler : public BaseSensor, public BroadcastLoggerSupport
return;
}
- // Check if GPS date and time are valid
- if (_gps->date.isValid() && _gps->time.isValid())
+ bool hasGpsDate = _gps->date.isValid();
+ bool hasGpsTime = _gps->time.isValid();
+
+ // Need at least one valid component
+ if (!hasGpsDate && !hasGpsTime)
{
- // Extract date/time components
- uint16_t year = _gps->date.year();
- uint8_t month = _gps->date.month();
- uint8_t day = _gps->date.day();
- uint8_t hour = _gps->time.hour();
- uint8_t minute = _gps->time.minute();
- uint8_t second = _gps->time.second();
-
- // Format as ISO 8601 string: YYYY-MM-DDTHH:MM:SS
- char isoDateTime[20];
- snprintf_P(isoDateTime, sizeof(isoDateTime), PSTR("%04d-%02d-%02dT%02d:%02d:%02d"),
- year, month, day, hour, minute, second);
-
- // Update DateTimeManager with GPS time (UTC)
- if (DateTimeManager::setDateTimeISO(isoDateTime))
- {
+ return;
+ }
- // Send F6 command to notify of time update
- StringKeyValue timeParam;
- strncpy(timeParam.key, ValueParamName, sizeof(timeParam.key));
- strncpy(timeParam.value, isoDateTime, sizeof(timeParam.value));
- sendCommand(SystemSetDateTime, &timeParam, 1);
+ // Extract date/time components
+ uint16_t year;
+ uint8_t month;
+ uint8_t day;
+ uint8_t hour;
+ uint8_t minute;
+ uint8_t second;
- _lastTimeSync = millis();
- }
+ // Get date components (from GPS or existing DateTimeManager)
+ if (hasGpsDate)
+ {
+ year = _gps->date.year();
+ month = _gps->date.month();
+ day = _gps->date.day();
+ }
+ else
+ {
+ // Use existing date from DateTimeManager
+ year = DateTimeManager::getYear();
+ month = DateTimeManager::getMonth();
+ day = DateTimeManager::getDay();
+ }
+
+ // Get time components (from GPS or existing DateTimeManager)
+ if (hasGpsTime)
+ {
+ hour = _gps->time.hour();
+ minute = _gps->time.minute();
+ second = _gps->time.second();
+ }
+ else
+ {
+ // Use existing time from DateTimeManager
+ hour = DateTimeManager::getHour();
+ minute = DateTimeManager::getMinute();
+ second = DateTimeManager::getSecond();
+ }
+
+ // Convert to Unix timestamp using ISO format (setDateTimeISO handles the conversion)
+ char isoDateTime[20];
+ snprintf_P(isoDateTime, sizeof(isoDateTime), PSTR("%04d-%02d-%02dT%02d:%02d:%02d"),
+ year, month, day, hour, minute, second);
+
+ // Update DateTimeManager with GPS time (UTC)
+ if (DateTimeManager::setDateTimeISO(isoDateTime))
+ {
+ // Get the Unix timestamp we just set
+ unsigned long unixTime = DateTimeManager::getCurrentTime();
+
+ // Send F6 command with Unix timestamp
+ StringKeyValue timeParam;
+ strncpy(timeParam.key, ValueParamName, sizeof(timeParam.key));
+ snprintf_P(timeParam.value, sizeof(timeParam.value), PSTR("%lu"), unixTime);
+ sendCommand(SystemSetDateTime, &timeParam, 1);
+
+ _lastTimeSync = millis();
}
}
diff --git a/Shared/SystemDefinitions.h b/Shared/SystemDefinitions.h
index 90d7885..8845e37 100644
--- a/Shared/SystemDefinitions.h
+++ b/Shared/SystemDefinitions.h
@@ -17,6 +17,8 @@ constexpr char SystemBluetoothStatus[] = "F4";
constexpr char SystemWifiStatus[] = "F5";
constexpr char SystemSetDateTime[] = "F6";
constexpr char SystemGetDateTime[] = "F7";
+constexpr char SystemSdCardPresent[] = "F8";
+constexpr char SystemSdCardLogFileSize[] = "F9";
constexpr char RelayTurnAllOff[] = "R0";
constexpr char RelayTurnAllOn[] = "R1";
@@ -72,6 +74,8 @@ constexpr char ConfigLedBrightness[] = "C25";
constexpr char ConfigLedAutoSwitch[] = "C26";
constexpr char ConfigLedEnable[] = "C27";
constexpr char ControlPanelTones[] = "C28";
+constexpr char ConfigReloadFromSd[] = "C29";
+constexpr char ConfigExportToSd[] = "C30";
constexpr char WarningsActive[] = "W0";
constexpr char WarningsList[] = "W1";
diff --git a/Shared/SystemFunctions.h b/Shared/SystemFunctions.h
index 5c31376..ca0da87 100644
--- a/Shared/SystemFunctions.h
+++ b/Shared/SystemFunctions.h
@@ -87,6 +87,25 @@ class SystemFunctions {
*/
static bool hasElapsed(unsigned long now, unsigned long previous, unsigned long interval);
+ /**
+ * @brief Reset a serial port by flushing outgoing data and clearing incoming buffer.
+ *
+ * This can help recover from communication issues by ensuring the serial port is in a clean state.
+ *
+ * @param serial Reference to the Stream (e.g., HardwareSerial) to reset
+ */
+ static void resetSerial(Stream& serial)
+ {
+ // Flush outgoing data
+ serial.flush();
+
+ // Clear incoming buffer
+ while (serial.available() > 0)
+ {
+ serial.read();
+ }
+ }
+
/**
* @brief Concatenate multiple strings into a provided buffer.
*
diff --git a/Shared/ToneManager.cpp b/Shared/ToneManager.cpp
index bf11904..5eef3e4 100644
--- a/Shared/ToneManager.cpp
+++ b/Shared/ToneManager.cpp
@@ -18,41 +18,24 @@ void ToneManager::configSet(SoundSignalConfig* config)
void ToneManager::play(ToneType type)
{
- Serial.print(F("[ToneManager::play] Type="));
- Serial.println(type == ToneType::Good ? F("Good") : F("Bad"));
-
stop();
buildSequence(type);
if (_totalSteps > 0)
{
- Serial.print(F("[ToneManager::play] Starting sequence - steps="));
- Serial.println(_totalSteps);
_playing = true;
_currentStep = 0;
_stepStartTime = millis();
startCurrentStep();
}
- else
- {
- Serial.println(F("[ToneManager::play] No steps built"));
- }
}
void ToneManager::stop()
{
- Serial.print(F("[ToneManager::stop] Called - _playing="));
- Serial.println(_playing);
-
if (_playing)
{
- Serial.println(F("[ToneManager::stop] Calling noTone()"));
noTone(_pin);
}
- else
- {
- Serial.println(F("[ToneManager::stop] Skipped noTone() - not playing"));
- }
_playing = false;
_currentStep = 0;
@@ -80,12 +63,10 @@ void ToneManager::update(unsigned long now)
if (_currentStep >= _totalSteps)
{
- Serial.println(F("[ToneManager::update] Sequence complete"));
stop();
return;
}
- Serial.println(F("[ToneManager::update] Advancing to next step"));
_stepStartTime = now;
startCurrentStep();
}
@@ -99,23 +80,13 @@ void ToneManager::startCurrentStep()
{
const ToneStep& step = _steps[_currentStep];
- Serial.print(F("[ToneManager::startCurrentStep] Step "));
- Serial.print(_currentStep);
- Serial.print(F("/"));
- Serial.print(_totalSteps);
- Serial.print(F(" - Hz="));
- Serial.print(step.frequencyHz);
- Serial.print(F(", ms="));
- Serial.println(step.durationMs);
if (step.frequencyHz > 0)
{
- Serial.println(F("[ToneManager::startCurrentStep] Calling tone()"));
tone(_pin, step.frequencyHz, step.durationMs);
}
else
{
- Serial.println(F("[ToneManager::startCurrentStep] Calling noTone() - silence step"));
noTone(_pin);
}
}
diff --git a/Shared/WarningManager.cpp b/Shared/WarningManager.cpp
index 3192d48..b4bbc53 100644
--- a/Shared/WarningManager.cpp
+++ b/Shared/WarningManager.cpp
@@ -108,13 +108,28 @@ void WarningManager::raiseWarning(WarningType type)
return;
uint32_t warningBit = static_cast(type);
+ bool wasActive = (_localWarnings & warningBit) != 0;
_localWarnings |= warningBit;
-
+
+ // Broadcast warning change if it's new
+ if (!wasActive)
+ {
+ broadcastWarningChange(type, true);
+ }
+
// Auto-raise SensorFailure for any sensor-related warning (bit 20+)
if (warningBit & SENSOR_WARNING_MASK) {
- _localWarnings |= static_cast(WarningType::SensorFailure);
+ uint32_t sensorFailureBit = static_cast(WarningType::SensorFailure);
+ bool sensorFailureWasActive = (_localWarnings & sensorFailureBit) != 0;
+ _localWarnings |= sensorFailureBit;
+
+ // Broadcast SensorFailure if it wasn't already active
+ if (!sensorFailureWasActive)
+ {
+ broadcastWarningChange(WarningType::SensorFailure, true);
+ }
}
-
+
#if defined(BOAT_CONTROL_PANEL)
updateLedStatus();
#endif
@@ -126,17 +141,32 @@ void WarningManager::clearWarning(WarningType type)
return;
uint32_t warningBit = static_cast(type);
+ bool wasActive = (_localWarnings & warningBit) != 0;
_localWarnings &= ~warningBit;
-
+
+ // Broadcast warning change if it was active
+ if (wasActive)
+ {
+ broadcastWarningChange(type, false);
+ }
+
// Auto-clear SensorFailure only if no sensor warnings remain (check bits 20+)
if (warningBit & SENSOR_WARNING_MASK) {
// Check if any other sensor warnings are still active (excluding SensorFailure itself)
uint32_t otherSensorWarnings = _localWarnings & SENSOR_WARNING_MASK & ~static_cast(WarningType::SensorFailure);
if (otherSensorWarnings == 0) {
- _localWarnings &= ~static_cast(WarningType::SensorFailure);
+ uint32_t sensorFailureBit = static_cast(WarningType::SensorFailure);
+ bool sensorFailureWasActive = (_localWarnings & sensorFailureBit) != 0;
+ _localWarnings &= ~sensorFailureBit;
+
+ // Broadcast SensorFailure clear if it was active
+ if (sensorFailureWasActive)
+ {
+ broadcastWarningChange(WarningType::SensorFailure, false);
+ }
}
}
-
+
#if defined(BOAT_CONTROL_PANEL)
updateLedStatus();
#endif
@@ -171,7 +201,11 @@ void WarningManager::sendHeartbeat()
{
if (_commandMgr)
{
- _commandMgr->sendCommand(SystemHeartbeatCommand, "");
+ // Include local warnings in heartbeat
+ char params[32];
+ snprintf_P(params, sizeof(params), PSTR("w=%s%lx"),
+ HexPrefix, _localWarnings);
+ _commandMgr->sendCommand(SystemHeartbeatCommand, params);
}
}
@@ -228,12 +262,30 @@ uint32_t WarningManager::getRemoteWarningsMask() const
void WarningManager::updateRemoteWarnings(uint32_t remoteWarningMask)
{
_remoteWarnings = remoteWarningMask;
-
+
#if defined(BOAT_CONTROL_PANEL)
updateLedStatus();
#endif
}
+void WarningManager::broadcastWarningChange(WarningType type, bool isActive)
+{
+ if (!_commandMgr || type == WarningType::None)
+ return;
+
+ // Format warning type as hex string (e.g., "0x800")
+ char warningHex[12];
+ uint32_t warningValue = static_cast(type);
+ snprintf_P(warningHex, sizeof(warningHex), PSTR("%s%lx"), HexPrefix, warningValue);
+
+ // Format as W2:=<0|1>
+ char params[32];
+ snprintf_P(params, sizeof(params), PSTR("%s=%d"), warningHex, isActive ? 1 : 0);
+
+ // Send W2 command via LINK
+ _commandMgr->sendCommand(WarningStatus, params);
+}
+
#if defined(BOAT_CONTROL_PANEL)
void WarningManager::updateLedStatus()
diff --git a/Shared/WarningManager.h b/Shared/WarningManager.h
index 875217e..e4ba6eb 100644
--- a/Shared/WarningManager.h
+++ b/Shared/WarningManager.h
@@ -76,6 +76,14 @@ class WarningManager {
*/
void updateConnection(unsigned long now);
+ /**
+ * @brief Broadcast warning change to connected device via LINK.
+ * Sends W2 command with warning status.
+ * @param type The warning type that changed
+ * @param isActive True if warning is now active, false if cleared
+ */
+ void broadcastWarningChange(WarningType type, bool isActive);
+
#if defined(BOAT_CONTROL_PANEL)
void updateLedStatus();
#endif
diff --git a/Shared/WarningType.h b/Shared/WarningType.h
index cf28f85..a950e5e 100644
--- a/Shared/WarningType.h
+++ b/Shared/WarningType.h
@@ -28,6 +28,8 @@ enum class WarningType : uint32_t {
WifiInvalidConfig = 1UL << 7, // 0x00000080 - WiFi configuration invalid
WeakWifiSignal = 1UL << 8, // 0x00000100 - WiFi signal weak
SyncFailed = 1UL << 9, // 0x00000200 - Configuration sync issue detected
+ SdCardError = 1UL << 10, // 0x00000400 - SD card read/write error
+ SdCardMissing = 1UL << 11, // 0x00000800 - SD card not detected
// Sensor warnings (bits 20+)
SensorFailure = 1UL << 20, // 0x00100000 - Sensor communication failure
@@ -48,8 +50,8 @@ static const char WT_6[] PROGMEM = "Wifi Init Failed";
static const char WT_7[] PROGMEM = "Wifi Invalid Config";
static const char WT_8[] PROGMEM = "Weak Wifi Signal";
static const char WT_9[] PROGMEM = "Synchronization Failed";
-static const char WT_10[] PROGMEM = "";
-static const char WT_11[] PROGMEM = "";
+static const char WT_10[] PROGMEM = "SD Card Error";
+static const char WT_11[] PROGMEM = "SD Card Not Found";
static const char WT_12[] PROGMEM = "";
static const char WT_13[] PROGMEM = "";
static const char WT_14[] PROGMEM = "";
diff --git a/SmartFuseBox/BluetoothSystemService.cpp b/SmartFuseBox/BluetoothSystemService.cpp
index adbf316..090423b 100644
--- a/SmartFuseBox/BluetoothSystemService.cpp
+++ b/SmartFuseBox/BluetoothSystemService.cpp
@@ -28,7 +28,6 @@ bool BluetoothSystemService::begin()
{
if (!_commandHandler)
{
- Serial.println(F("[BluetoothSystemService] Error: Command handler is null"));
return false;
}
@@ -37,7 +36,6 @@ bool BluetoothSystemService::begin()
if (!_service)
{
- Serial.println(F("[BluetoothSystemService] Error: Failed to create service"));
return false;
}
@@ -62,7 +60,6 @@ bool BluetoothSystemService::begin()
// Add service to BLE
BLE.addService(*_service);
- Serial.println(F("[BluetoothSystemService] Service initialized successfully"));
return true;
}
@@ -112,7 +109,6 @@ void BluetoothSystemService::notifyInitialized()
if (_charInitialized)
{
_charInitialized->writeValue((uint8_t)1);
- Serial.println(F("[BluetoothSystemService] Notified clients: System Initialized"));
}
}
diff --git a/SmartFuseBox/ConfigSyncManager.cpp b/SmartFuseBox/ConfigSyncManager.cpp
index 8807ad2..f241e6d 100644
--- a/SmartFuseBox/ConfigSyncManager.cpp
+++ b/SmartFuseBox/ConfigSyncManager.cpp
@@ -149,7 +149,7 @@ void ConfigSyncManager::saveConfigIfChanged()
if (_computerSerial)
{
- _computerSerial->sendError("Config synced and saved", "ConfigSync");
+ _computerSerial->sendDebug("Config synced and saved", "ConfigSync");
}
}
else
diff --git a/SmartFuseBox/LightSensorHandler.h b/SmartFuseBox/LightSensorHandler.h
index e63ef99..89e146c 100644
--- a/SmartFuseBox/LightSensorHandler.h
+++ b/SmartFuseBox/LightSensorHandler.h
@@ -11,6 +11,7 @@ class LightSensorHandler : public BaseSensor, public BroadcastLoggerSupport
SensorCommandHandler* _sensorCommandHandler;
WarningManager* _warningManager;
const uint8_t _sensorPin;
+ const uint8_t _analogPin;
bool _isPinActive;
bool _isDaytime;
protected:
@@ -42,9 +43,9 @@ class LightSensorHandler : public BaseSensor, public BroadcastLoggerSupport
}
public:
LightSensorHandler(MessageBus* messageBus, BroadcastManager* broadcastManager, SensorCommandHandler* sensorCommandHandler,
- WarningManager* warningManager, uint8_t sensorPin)
+ WarningManager* warningManager, uint8_t sensorPin, uint8_t analogPin)
: BroadcastLoggerSupport(broadcastManager), _messageBus(messageBus), _sensorCommandHandler(sensorCommandHandler),
- _warningManager(warningManager), _sensorPin(sensorPin), _isPinActive(false), _isDaytime(true)
+ _warningManager(warningManager), _sensorPin(sensorPin), _analogPin(analogPin), _isPinActive(false), _isDaytime(true)
{
}
diff --git a/SmartFuseBox/SDCardConfigLoader.cpp b/SmartFuseBox/SDCardConfigLoader.cpp
new file mode 100644
index 0000000..1fa56ca
--- /dev/null
+++ b/SmartFuseBox/SDCardConfigLoader.cpp
@@ -0,0 +1,799 @@
+#include "SDCardConfigLoader.h"
+#include "ConfigManager.h"
+#include
+
+SdCardConfigLoader::SdCardConfigLoader(SerialCommandManager* computerSerial,
+ SerialCommandManager* linkSerial,
+ ConfigController* configController,
+ ConfigSyncManager* configSyncManager,
+ uint8_t csPin)
+ : _computerSerial(computerSerial),
+ _linkSerial(linkSerial),
+ _configController(configController),
+ _configSyncManager(configSyncManager),
+ _sdCardLogger(nullptr),
+ _csPin(csPin),
+ _sdConfigPresent(false)
+{
+}
+
+void SdCardConfigLoader::setSdCardLogger(SdCardLogger* sdCardLogger)
+{
+ _sdCardLogger = sdCardLogger;
+}
+
+bool SdCardConfigLoader::checkSdCard()
+{
+ if (!_sd.begin(_csPin, SD_SCK_MHZ(50)))
+ {
+ return false;
+ }
+ return true;
+}
+
+bool SdCardConfigLoader::configFileExists()
+{
+ return _sd.exists(SD_CONFIG_FILENAME);
+}
+
+bool SdCardConfigLoader::applyConfigCommand(const char* line)
+{
+ // Skip empty lines and comments
+ if (line == nullptr || line[0] == '\0' || line[0] == '#' || line[0] == '\r' || line[0] == '\n')
+ {
+ return true;
+ }
+
+ // Create a mutable copy for parsing
+ char buffer[SD_CONFIG_MAX_LINE_LENGTH];
+ strncpy(buffer, line, sizeof(buffer) - 1);
+ buffer[sizeof(buffer) - 1] = '\0';
+
+ // Trim whitespace and newlines
+ char* end = buffer + strlen(buffer) - 1;
+ while (end > buffer && (*end == '\r' || *end == '\n' || *end == ' ' || *end == '\t'))
+ {
+ *end = '\0';
+ end--;
+ }
+
+ // Parse command using SerialCommandManager's parser
+ // Format: "CMD:param1=value1;param2=value2"
+ char* colonPos = strchr(buffer, ':');
+ char command[8] = {0};
+
+ if (colonPos != nullptr)
+ {
+ size_t cmdLen = colonPos - buffer;
+ if (cmdLen >= sizeof(command))
+ {
+ logError("Command too long", line);
+ return false;
+ }
+ strncpy(command, buffer, cmdLen);
+ command[cmdLen] = '\0';
+ }
+ else
+ {
+ // No colon means command with no params (like C0, C1, C2)
+ strncpy(command, buffer, sizeof(command) - 1);
+ }
+
+ // Parse parameters manually since we're simulating command input
+ const char* paramsStr = colonPos ? (colonPos + 1) : "";
+
+ // Use the computer serial's command parser to validate and route the command
+ // We'll construct a synthetic command and feed it through the handler chain
+
+ // Parse parameters into key-value pairs
+ StringKeyValue params[10];
+ uint8_t paramCount = 0;
+
+ if (paramsStr && *paramsStr)
+ {
+ char paramBuffer[SD_CONFIG_MAX_LINE_LENGTH];
+ strncpy(paramBuffer, paramsStr, sizeof(paramBuffer) - 1);
+ paramBuffer[sizeof(paramBuffer) - 1] = '\0';
+
+ // Parse semicolon-separated parameters
+ char* savePtr1 = nullptr;
+ char* param = strtok_r(paramBuffer, ";", &savePtr1);
+
+ while (param && paramCount < 10)
+ {
+ // Each param can be "key=value" or just "value"
+ char* equalPos = strchr(param, '=');
+ if (equalPos)
+ {
+ *equalPos = '\0';
+ strncpy(params[paramCount].key, param, sizeof(params[paramCount].key) - 1);
+ params[paramCount].key[sizeof(params[paramCount].key) - 1] = '\0';
+ strncpy(params[paramCount].value, equalPos + 1, sizeof(params[paramCount].value) - 1);
+ params[paramCount].value[sizeof(params[paramCount].value) - 1] = '\0';
+ }
+ else
+ {
+ // For commands like C13:SSID, the whole thing after : is the value
+ // with an implied key (often "v" or empty)
+ params[paramCount].key[0] = '\0';
+ strncpy(params[paramCount].value, param, sizeof(params[paramCount].value) - 1);
+ params[paramCount].value[sizeof(params[paramCount].value) - 1] = '\0';
+ }
+ paramCount++;
+ param = strtok_r(nullptr, ";", &savePtr1);
+ }
+ }
+
+ // Now apply the command through ConfigController
+ // We bypass the SerialCommandManager and call ConfigController methods directly
+
+ ConfigResult result = ConfigResult::InvalidCommand;
+
+ if (strcmp(command, "C3") == 0 && paramCount >= 1)
+ {
+ result = _configController->rename(params[0].value);
+ }
+ else if (strcmp(command, "C4") == 0 && paramCount >= 1)
+ {
+ uint8_t idx = static_cast(strtoul(params[0].key, nullptr, 0));
+ result = _configController->renameRelay(idx, params[0].value);
+ }
+ else if (strcmp(command, "C5") == 0 && paramCount >= 1)
+ {
+ uint8_t button = static_cast(strtoul(params[0].key, nullptr, 0));
+ uint8_t relay = static_cast(strtoul(params[0].value, nullptr, 0));
+ result = _configController->mapHomeButton(button, relay);
+ }
+ else if (strcmp(command, "C6") == 0 && paramCount >= 1)
+ {
+ uint8_t button = static_cast(strtoul(params[0].key, nullptr, 0));
+ uint8_t color = static_cast(strtoul(params[0].value, nullptr, 0));
+ result = _configController->mapHomeButtonColor(button, color);
+ }
+ else if (strcmp(command, "C7") == 0 && paramCount >= 1)
+ {
+ uint8_t type = atoi(params[0].value);
+ result = _configController->setVesselType(type);
+ }
+ else if (strcmp(command, "C8") == 0 && paramCount >= 1)
+ {
+ uint8_t relay = atoi(params[0].value);
+ result = _configController->setSoundRelayButton(relay);
+ }
+ else if (strcmp(command, "C9") == 0 && paramCount >= 1)
+ {
+ uint16_t delay = atoi(params[0].value);
+ result = _configController->setsoundDelayStart(delay);
+ }
+#if defined(ARDUINO_UNO_R4)
+ else if (strcmp(command, "C10") == 0 && paramCount >= 1)
+ {
+ bool enabled = (atoi(params[0].value) != 0);
+ result = _configController->setBluetoothEnabled(enabled);
+ }
+ else if (strcmp(command, "C11") == 0 && paramCount >= 1)
+ {
+ bool enabled = (atoi(params[0].value) != 0);
+ result = _configController->setWifiEnabled(enabled);
+ }
+ else if (strcmp(command, "C12") == 0 && paramCount >= 1)
+ {
+ uint8_t mode = atoi(params[0].value);
+ result = _configController->setWifiAccessMode(mode);
+ }
+ else if (strcmp(command, "C13") == 0 && paramCount >= 1)
+ {
+ // For C13, the entire string after : is the SSID (no v= prefix in file)
+ // We need to find original param value from line
+ const char* ssid = strchr(line, ':');
+ if (ssid)
+ {
+ ssid++; // Skip the colon
+ result = _configController->setWifiSsid(ssid);
+ }
+ }
+ else if (strcmp(command, "C14") == 0 && paramCount >= 1)
+ {
+ // For C14, the entire string after : is the password
+ const char* password = strchr(line, ':');
+ if (password)
+ {
+ password++; // Skip the colon
+ result = _configController->setWifiPassword(password);
+ }
+ }
+ else if (strcmp(command, "C15") == 0 && paramCount >= 1)
+ {
+ uint16_t port = atoi(params[0].value);
+ result = _configController->setWifiPort(port);
+ }
+ else if (strcmp(command, "C17") == 0 && paramCount >= 1)
+ {
+ // For C17, the entire string after : is the IP address
+ const char* ip = strchr(line, ':');
+ if (ip)
+ {
+ ip++; // Skip the colon
+ result = _configController->setWifiIpAddress(ip);
+ }
+ }
+#endif
+ else if (strcmp(command, "C18") == 0 && paramCount >= 1)
+ {
+ uint8_t relay = static_cast(atoi(params[0].key));
+ bool state = (atoi(params[0].value) != 0);
+ result = _configController->setRelayDefaultState(relay, state);
+ }
+ else if (strcmp(command, "C19") == 0 && paramCount >= 1)
+ {
+ uint8_t relay1 = static_cast(strtoul(params[0].key, nullptr, 0));
+ uint8_t relay2 = static_cast(strtoul(params[0].value, nullptr, 0));
+
+ if (relay2 == 255)
+ {
+ result = _configController->unlinkRelay(relay1);
+ }
+ else
+ {
+ result = _configController->linkRelays(relay1, relay2);
+ }
+ }
+ else if (strcmp(command, "C20") == 0 && paramCount >= 1)
+ {
+ int8_t offset = static_cast(atoi(params[0].value));
+ result = _configController->setTimezoneOffset(offset);
+ }
+ else if (strcmp(command, "C21") == 0 && paramCount >= 1)
+ {
+ const char* mmsi = strchr(line, ':');
+ if (mmsi)
+ {
+ mmsi++; // Skip the colon
+ result = _configController->setMmsi(mmsi);
+ }
+ }
+ else if (strcmp(command, "C22") == 0)
+ {
+ const char* callSign = strchr(line, ':');
+ if (callSign && *(callSign + 1) != '\0')
+ {
+ callSign++; // Skip the colon
+ result = _configController->setCallSign(callSign);
+ }
+ else
+ {
+ result = _configController->setCallSign("");
+ }
+ }
+ else if (strcmp(command, "C23") == 0)
+ {
+ const char* homePort = strchr(line, ':');
+ if (homePort && *(homePort + 1) != '\0')
+ {
+ homePort++; // Skip the colon
+ result = _configController->setHomePort(homePort);
+ }
+ else
+ {
+ result = _configController->setHomePort("");
+ }
+ }
+ else if (strcmp(command, "C24") == 0 && paramCount >= 5)
+ {
+ uint8_t type = 0, colorSet = 0, r = 0, g = 0, b = 0;
+
+ for (uint8_t i = 0; i < paramCount; i++)
+ {
+ if (strcmp(params[i].key, "t") == 0)
+ type = atoi(params[i].value);
+ else if (strcmp(params[i].key, "c") == 0)
+ colorSet = atoi(params[i].value);
+ else if (strcmp(params[i].key, "r") == 0)
+ r = atoi(params[i].value);
+ else if (strcmp(params[i].key, "g") == 0)
+ g = atoi(params[i].value);
+ else if (strcmp(params[i].key, "b") == 0)
+ b = atoi(params[i].value);
+ }
+
+ result = _configController->setLedColor(type, colorSet, r, g, b);
+ }
+ else if (strcmp(command, "C25") == 0 && paramCount >= 2)
+ {
+ uint8_t type = 0, brightness = 0;
+
+ for (uint8_t i = 0; i < paramCount; i++)
+ {
+ if (strcmp(params[i].key, "t") == 0)
+ type = atoi(params[i].value);
+ else if (strcmp(params[i].key, "b") == 0)
+ brightness = atoi(params[i].value);
+ }
+
+ result = _configController->setLedBrightness(type, brightness);
+ }
+ else if (strcmp(command, "C26") == 0 && paramCount >= 1)
+ {
+ bool enabled = (atoi(params[0].value) != 0);
+ result = _configController->setLedAutoSwitch(enabled);
+ }
+ else if (strcmp(command, "C27") == 0 && paramCount >= 3)
+ {
+ bool gps = false, warning = false, system = false;
+
+ for (uint8_t i = 0; i < paramCount; i++)
+ {
+ if (strcmp(params[i].key, "g") == 0)
+ gps = (atoi(params[i].value) != 0);
+ else if (strcmp(params[i].key, "w") == 0)
+ warning = (atoi(params[i].value) != 0);
+ else if (strcmp(params[i].key, "s") == 0)
+ system = (atoi(params[i].value) != 0);
+ }
+
+ result = _configController->setLedEnableStates(gps, warning, system);
+ }
+ else if (strcmp(command, "C28") == 0 && paramCount >= 4)
+ {
+ uint8_t type = 0, preset = 0;
+ uint16_t toneHz = 0, durationMs = 0;
+ uint32_t repeatMs = 0;
+
+ for (uint8_t i = 0; i < paramCount; i++)
+ {
+ if (strcmp(params[i].key, "t") == 0)
+ type = atoi(params[i].value);
+ else if (strcmp(params[i].key, "h") == 0)
+ toneHz = atoi(params[i].value);
+ else if (strcmp(params[i].key, "d") == 0)
+ durationMs = atoi(params[i].value);
+ else if (strcmp(params[i].key, "p") == 0)
+ preset = atoi(params[i].value);
+ else if (strcmp(params[i].key, "r") == 0)
+ repeatMs = strtoul(params[i].value, nullptr, 0);
+ }
+
+ result = _configController->setControlPanelTones(type, preset, toneHz, durationMs, repeatMs);
+ }
+ else
+ {
+ logError("Unknown or invalid command", line);
+ return false;
+ }
+
+ if (result != ConfigResult::Success)
+ {
+ char errorMsg[64];
+ snprintf(errorMsg, sizeof(errorMsg), "Command failed: %s (result=%d)", command, static_cast(result));
+ logError(errorMsg, line);
+ return false;
+ }
+
+ return true;
+}
+
+void SdCardConfigLoader::syncConfigToLink()
+{
+ // Send C1 (get settings) to trigger config broadcast via LINK
+ // This will make the control panel receive the new config
+ if (_linkSerial)
+ {
+ _linkSerial->sendCommand("C1", "");
+ }
+}
+
+void SdCardConfigLoader::logError(const char* message, const char* line)
+{
+ if (_computerSerial)
+ {
+ _computerSerial->sendError("SD_CFG_ERROR", message);
+ if (line)
+ {
+ _computerSerial->sendError("SD_CFG_LINE", line);
+ }
+ }
+}
+
+void SdCardConfigLoader::logInfo(const char* message)
+{
+ if (_computerSerial)
+ {
+ _computerSerial->sendError("SD_CFG_INFO", message);
+ }
+}
+
+bool SdCardConfigLoader::loadConfigFromSd()
+{
+ logInfo("Checking for SD config...");
+
+ // Temporarily release SD card if logger is using it
+ bool loggerWasActive = false;
+ if (_sdCardLogger && _sdCardLogger->isSdCardReady())
+ {
+ logInfo("Releasing SD card from logger...");
+ _sdCardLogger->releaseSDCard();
+ loggerWasActive = true;
+ }
+
+ if (!checkSdCard())
+ {
+ logInfo("SD card not present or not accessible");
+
+ // Reacquire SD card for logger if it was active
+ if (loggerWasActive && _sdCardLogger)
+ {
+ _sdCardLogger->reacquireSDCard();
+ }
+
+ return false;
+ }
+
+ if (!configFileExists())
+ {
+ logInfo("Config file not found on SD card");
+
+ // Reacquire SD card for logger if it was active
+ if (loggerWasActive && _sdCardLogger)
+ {
+ _sdCardLogger->reacquireSDCard();
+ }
+
+ return false;
+ }
+
+ logInfo("Loading config from SD card...");
+
+ FsFile configFile = _sd.open(SD_CONFIG_FILENAME, O_RDONLY);
+ if (!configFile)
+ {
+ logError("Failed to open config file");
+
+ // Reacquire SD card for logger if it was active
+ if (loggerWasActive && _sdCardLogger)
+ {
+ _sdCardLogger->reacquireSDCard();
+ }
+
+ return false;
+ }
+
+ // Read and apply commands line by line
+ char line[SD_CONFIG_MAX_LINE_LENGTH];
+ uint16_t lineNumber = 0;
+ uint16_t successCount = 0;
+ uint16_t errorCount = 0;
+
+ while (configFile.available())
+ {
+ int len = configFile.fgets(line, sizeof(line));
+ lineNumber++;
+
+ if (len > 0)
+ {
+ if (applyConfigCommand(line))
+ {
+ successCount++;
+ }
+ else
+ {
+ errorCount++;
+ }
+ }
+ }
+
+ configFile.close();
+
+ // Save to EEPROM
+ if (successCount > 0)
+ {
+ logInfo("Saving config to EEPROM...");
+ ConfigResult saveResult = _configController->save();
+
+ if (saveResult == ConfigResult::Success)
+ {
+ logInfo("Config saved to EEPROM");
+
+ // Sync to control panel via LINK
+ logInfo("Syncing config to control panel...");
+ syncConfigToLink();
+
+ // Disable ConfigSyncManager since SD config is authoritative
+ if (_configSyncManager)
+ {
+ _configSyncManager->setEnabled(false);
+ logInfo("ConfigSyncManager disabled (SD config active)");
+ }
+
+ _sdConfigPresent = true;
+
+ char summary[64];
+ snprintf(summary, sizeof(summary), "SD config loaded: %u commands applied, %u errors", successCount, errorCount);
+ logInfo(summary);
+
+ // Reacquire SD card for logger if it was active
+ if (loggerWasActive && _sdCardLogger)
+ {
+ logInfo("Reacquiring SD card for logger...");
+ _sdCardLogger->reacquireSDCard();
+ }
+
+ return true;
+ }
+ else
+ {
+ logError("Failed to save config to EEPROM");
+ }
+ }
+
+ // Reacquire SD card for logger if it was active
+ if (loggerWasActive && _sdCardLogger)
+ {
+ _sdCardLogger->reacquireSDCard();
+ }
+
+ return false;
+}
+
+bool SdCardConfigLoader::reloadConfigFromSd()
+{
+ logInfo("Reloading config from SD card...");
+ return loadConfigFromSd();
+}
+
+bool SdCardConfigLoader::exportConfigToSd()
+{
+ logInfo("Exporting config to SD card...");
+
+ // Temporarily release SD card if logger is using it
+ bool loggerWasActive = false;
+ if (_sdCardLogger && _sdCardLogger->isSdCardReady())
+ {
+ logInfo("Releasing SD card from logger...");
+ _sdCardLogger->releaseSDCard();
+ loggerWasActive = true;
+ }
+
+ if (!checkSdCard())
+ {
+ logError("SD card not present or not accessible");
+
+ // Reacquire SD card for logger if it was active
+ if (loggerWasActive && _sdCardLogger)
+ {
+ _sdCardLogger->reacquireSDCard();
+ }
+
+ return false;
+ }
+
+ // Delete existing config file if present
+ if (_sd.exists(SD_CONFIG_FILENAME))
+ {
+ _sd.remove(SD_CONFIG_FILENAME);
+ }
+
+ FsFile configFile = _sd.open(SD_CONFIG_FILENAME, O_WRONLY | O_CREAT);
+ if (!configFile)
+ {
+ logError("Failed to create config file");
+
+ // Reacquire SD card for logger if it was active
+ if (loggerWasActive && _sdCardLogger)
+ {
+ _sdCardLogger->reacquireSDCard();
+ }
+
+ return false;
+ }
+
+ Config* config = _configController->getConfigPtr();
+ if (!config)
+ {
+ configFile.close();
+ logError("Config not available");
+
+ // Reacquire SD card for logger if it was active
+ if (loggerWasActive && _sdCardLogger)
+ {
+ _sdCardLogger->reacquireSDCard();
+ }
+
+ return false;
+ }
+
+ // Write header comment
+ configFile.println("# SmartFuseBox Configuration");
+ configFile.println("# Generated by C30 command");
+ configFile.println();
+
+ // C3 - Boat name
+ if (strlen(config->name) > 0)
+ {
+ configFile.print("C3:");
+ configFile.println(config->name);
+ }
+
+ // C4 - Relay names
+ for (uint8_t i = 0; i < ConfigRelayCount; i++)
+ {
+ configFile.print("C4:");
+ configFile.print(i);
+ configFile.print("=");
+ configFile.print(config->relayShortNames[i]);
+ configFile.print("|");
+ configFile.println(config->relayLongNames[i]);
+ }
+
+ // C5 - Home button mappings
+ for (uint8_t i = 0; i < ConfigHomeButtons; i++)
+ {
+ configFile.print("C5:");
+ configFile.print(i);
+ configFile.print("=");
+ configFile.println(config->homePageMapping[i]);
+ }
+
+ // C6 - Button colors
+ for (uint8_t i = 0; i < ConfigRelayCount; i++)
+ {
+ configFile.print("C6:");
+ configFile.print(i);
+ configFile.print("=");
+ configFile.println(config->buttonImage[i]);
+ }
+
+ // C7 - Vessel type
+ configFile.print("C7:v=");
+ configFile.println(static_cast(config->vesselType));
+
+ // C8 - Sound relay
+ configFile.print("C8:v=");
+ configFile.println(config->hornRelayIndex);
+
+ // C9 - Sound delay
+ configFile.print("C9:v=");
+ configFile.println(config->soundStartDelayMs);
+
+#if defined(ARDUINO_UNO_R4)
+ // C10 - Bluetooth enabled
+ configFile.print("C10:v=");
+ configFile.println(config->bluetoothEnabled ? "1" : "0");
+
+ // C11 - WiFi enabled
+ configFile.print("C11:v=");
+ configFile.println(config->wifiEnabled ? "1" : "0");
+
+ // C12 - WiFi mode
+ configFile.print("C12:v=");
+ configFile.println(config->accessMode);
+
+ // C13 - WiFi SSID
+ configFile.print("C13:");
+ configFile.println(config->apSSID);
+
+ // C14 - WiFi password
+ configFile.print("C14:");
+ configFile.println(config->apPassword);
+
+ // C15 - WiFi port
+ configFile.print("C15:v=");
+ configFile.println(config->wifiPort);
+
+ // C17 - WiFi AP IP
+ configFile.print("C17:");
+ configFile.println(config->apIpAddress);
+#endif
+
+ // C18 - Default relay states
+ for (uint8_t i = 0; i < ConfigRelayCount; i++)
+ {
+ configFile.print("C18:");
+ configFile.print(i);
+ configFile.print("=");
+ configFile.println(config->defaulRelayState[i] ? "1" : "0");
+ }
+
+ // C19 - Linked relays
+ for (uint8_t i = 0; i < ConfigMaxLinkedRelays; i++)
+ {
+ configFile.print("C19:");
+ configFile.print(config->linkedRelays[i][0]);
+ configFile.print("=");
+ configFile.println(config->linkedRelays[i][1]);
+ }
+
+ // C20 - Timezone offset
+ configFile.print("C20:v=");
+ configFile.println(config->timezoneOffset);
+
+ // C21 - MMSI
+ configFile.print("C21:");
+ configFile.println(config->mMSI);
+
+ // C22 - Call sign
+ configFile.print("C22:");
+ configFile.println(config->callSign);
+
+ // C23 - Home port
+ configFile.print("C23:");
+ configFile.println(config->homePort);
+
+ // C24 - LED colors
+ configFile.print("C24:t=0;c=0;r=");
+ configFile.print(config->ledConfig.dayGoodColor[0]);
+ configFile.print(";g=");
+ configFile.print(config->ledConfig.dayGoodColor[1]);
+ configFile.print(";b=");
+ configFile.println(config->ledConfig.dayGoodColor[2]);
+
+ configFile.print("C24:t=0;c=1;r=");
+ configFile.print(config->ledConfig.dayBadColor[0]);
+ configFile.print(";g=");
+ configFile.print(config->ledConfig.dayBadColor[1]);
+ configFile.print(";b=");
+ configFile.println(config->ledConfig.dayBadColor[2]);
+
+ configFile.print("C24:t=1;c=0;r=");
+ configFile.print(config->ledConfig.nightGoodColor[0]);
+ configFile.print(";g=");
+ configFile.print(config->ledConfig.nightGoodColor[1]);
+ configFile.print(";b=");
+ configFile.println(config->ledConfig.nightGoodColor[2]);
+
+ configFile.print("C24:t=1;c=1;r=");
+ configFile.print(config->ledConfig.nightBadColor[0]);
+ configFile.print(";g=");
+ configFile.print(config->ledConfig.nightBadColor[1]);
+ configFile.print(";b=");
+ configFile.println(config->ledConfig.nightBadColor[2]);
+
+ // C25 - LED brightness
+ configFile.print("C25:t=0;b=");
+ configFile.println(config->ledConfig.dayBrightness);
+
+ configFile.print("C25:t=1;b=");
+ configFile.println(config->ledConfig.nightBrightness);
+
+ // C26 - LED auto switch
+ configFile.print("C26:v=");
+ configFile.println(config->ledConfig.autoSwitch ? "1" : "0");
+
+ // C27 - LED enable states
+ configFile.print("C27:g=");
+ configFile.print(config->ledConfig.gpsEnabled ? "1" : "0");
+ configFile.print(";w=");
+ configFile.print(config->ledConfig.warningEnabled ? "1" : "0");
+ configFile.print(";s=");
+ configFile.println(config->ledConfig.systemEnabled ? "1" : "0");
+
+ // C28 - Control panel tones
+ configFile.print("C28:t=0;h=");
+ configFile.print(config->soundConfig.good_toneHz);
+ configFile.print(";d=");
+ configFile.print(config->soundConfig.good_durationMs);
+ configFile.print(";p=");
+ configFile.print(config->soundConfig.goodPreset);
+ configFile.println(";r=0");
+
+ configFile.print("C28:t=1;h=");
+ configFile.print(config->soundConfig.bad_toneHz);
+ configFile.print(";d=");
+ configFile.print(config->soundConfig.bad_durationMs);
+ configFile.print(";p=");
+ configFile.print(config->soundConfig.badPreset);
+ configFile.print(";r=");
+ configFile.println(config->soundConfig.bad_repeatMs);
+
+ configFile.close();
+
+ logInfo("Config exported to SD card");
+
+ // Reacquire SD card for logger if it was active
+ if (loggerWasActive && _sdCardLogger)
+ {
+ logInfo("Reacquiring SD card for logger...");
+ _sdCardLogger->reacquireSDCard();
+ }
+
+ return true;
+}
diff --git a/SmartFuseBox/SDCardConfigLoader.h b/SmartFuseBox/SDCardConfigLoader.h
new file mode 100644
index 0000000..d7d3bd0
--- /dev/null
+++ b/SmartFuseBox/SDCardConfigLoader.h
@@ -0,0 +1,144 @@
+#pragma once
+
+#include
+#include
+#include
+#include "ConfigController.h"
+#include "ConfigSyncManager.h"
+#include "SdCardLogger.h"
+
+constexpr char SD_CONFIG_FILENAME[] = "config.txt";
+constexpr uint16_t SD_CONFIG_MAX_LINE_LENGTH = 128;
+
+/**
+ * @class SdCardConfigLoader
+ * @brief Loads configuration from SD card and applies to ConfigManager
+ *
+ * Boot sequence:
+ * 1. Check for SD card with config.txt
+ * 2. Parse and validate all commands
+ * 3. Compare with current EEPROM config
+ * 4. If different, apply changes and save to EEPROM
+ * 5. Send config via LINK to sync control panel
+ *
+ * Features:
+ * - Read-only SD card config (not auto-updated during runtime)
+ * - Command format validation
+ * - Error logging to Serial
+ * - Integration with ConfigSyncManager (disable if SD config loaded)
+ * - C29: Reload config from SD card
+ * - C30: Export current config to SD card
+ *
+ * Usage:
+ * @code
+ * SdCardConfigLoader loader(&commandMgrComputer, &commandMgrLink,
+ * &configController, &configSyncManager, csPin);
+ *
+ * void setup() {
+ * bool sdConfigLoaded = loader.loadConfigFromSd();
+ * if (sdConfigLoaded) {
+ * // SD config was applied, ConfigSyncManager will be disabled
+ * }
+ * }
+ * @endcode
+ */
+class SdCardConfigLoader
+{
+private:
+ SerialCommandManager* _computerSerial;
+ SerialCommandManager* _linkSerial;
+ ConfigController* _configController;
+ ConfigSyncManager* _configSyncManager;
+ SdCardLogger* _sdCardLogger;
+ uint8_t _csPin;
+ bool _sdConfigPresent;
+
+ // SD card
+ SdFat _sd;
+
+ /**
+ * @brief Check if SD card is accessible
+ * @return true if SD card is present and readable
+ */
+ bool checkSdCard();
+
+ /**
+ * @brief Check if config.txt exists on SD card
+ * @return true if config file exists
+ */
+ bool configFileExists();
+
+ /**
+ * @brief Parse and apply a single config command line
+ * @param line Command line to parse
+ * @return true if command was successfully applied
+ */
+ bool applyConfigCommand(const char* line);
+
+ /**
+ * @brief Send config to LINK serial to sync control panel
+ */
+ void syncConfigToLink();
+
+ /**
+ * @brief Log error message to serial
+ * @param message Error message
+ * @param line Optional line content that caused error
+ */
+ void logError(const char* message, const char* line = nullptr);
+
+ /**
+ * @brief Log info message to serial
+ * @param message Info message
+ */
+ void logInfo(const char* message);
+
+public:
+ /**
+ * @brief Constructor
+ * @param computerSerial Serial manager for computer communication
+ * @param linkSerial Serial manager for LINK communication
+ * @param configController Configuration controller
+ * @param configSyncManager Configuration sync manager (will be disabled if SD config loaded)
+ * @param csPin Chip select pin for SD card
+ */
+ SdCardConfigLoader(SerialCommandManager* computerSerial,
+ SerialCommandManager* linkSerial,
+ ConfigController* configController,
+ ConfigSyncManager* configSyncManager,
+ uint8_t csPin);
+
+ /**
+ * @brief Set the SdCardLogger reference for coordinated SD card access
+ * @param sdCardLogger Pointer to the SdCardLogger instance
+ */
+ void setSdCardLogger(SdCardLogger* sdCardLogger);
+
+ /**
+ * @brief Load configuration from SD card if present
+ *
+ * Reads config.txt, applies all commands, saves to EEPROM if changed,
+ * and syncs to control panel via LINK.
+ *
+ * @return true if SD config was found and applied
+ */
+ bool loadConfigFromSd();
+
+ /**
+ * @brief Reload configuration from SD card (C29 command)
+ * @return true if config was reloaded successfully
+ */
+ bool reloadConfigFromSd();
+
+ /**
+ * @brief Export current configuration to SD card (C30 command)
+ * @return true if config was exported successfully
+ */
+ bool exportConfigToSd();
+
+ /**
+ * @brief Check if SD config was loaded at boot
+ * @return true if SD config is present and was loaded
+ */
+ bool isSdConfigPresent() const { return _sdConfigPresent; }
+};
diff --git a/SmartFuseBox/SdCardLogger.cpp b/SmartFuseBox/SdCardLogger.cpp
new file mode 100644
index 0000000..a10600c
--- /dev/null
+++ b/SmartFuseBox/SdCardLogger.cpp
@@ -0,0 +1,569 @@
+#include "SdCardLogger.h"
+#include "DateTimeManager.h"
+#include "SmartFuseBoxConstants.h"
+#include
+
+SdCardLogger::SdCardLogger(SensorCommandHandler* sensorHandler, WarningManager* warningManager, uint8_t csPin)
+ : _sensorHandler(sensorHandler),
+ _warningManager(warningManager),
+ _csPin(csPin),
+ _initialized(false),
+ _sdCardPresent(false),
+ _bufferHead(0),
+ _bufferTail(0),
+ _bufferCount(0),
+ _currentDay(0),
+ _lastWriteTime(0),
+ _lastFileCheckTime(0),
+ _lastCardPresenceCheck(0),
+ _totalRecordsLogged(0),
+ _recordsDropped(0),
+ _sdCardErrorRaised(false),
+ _sdCardMissingRaised(false),
+ _cachedTotalSize(0),
+ _cachedFreeSpace(0),
+ _initialLogFileSize(0)
+{
+ memset(_currentFileName, 0, sizeof(_currentFileName));
+}
+
+bool SdCardLogger::initialize()
+{
+ if (!initializeSdCard())
+ {
+ // Check if it's a card missing error or other error
+ if (isCardMissingError())
+ {
+ _warningManager->raiseWarning(WarningType::SdCardMissing);
+ _sdCardMissingRaised = true;
+ }
+ else
+ {
+ _warningManager->raiseWarning(WarningType::SdCardError);
+ _sdCardErrorRaised = true;
+ }
+ return false;
+ }
+
+ _initialized = true;
+ _sdCardPresent = true;
+
+ // Cache SD card size info (expensive operations done once at init)
+ uint32_t cardSizeBlocks = _sd.card()->sectorCount();
+ _cachedTotalSize = (uint64_t)cardSizeBlocks * 512ULL;
+
+ uint32_t freeClusterCount = _sd.freeClusterCount();
+ uint32_t sectorsPerCluster = _sd.sectorsPerCluster();
+ _cachedFreeSpace = (uint64_t)freeClusterCount * sectorsPerCluster * 512ULL;
+
+ return true;
+}
+
+bool SdCardLogger::initializeSdCard()
+{
+ // Explicitly set CS pin as output (best practice, though SdFat usually handles this)
+ pinMode(_csPin, OUTPUT);
+ digitalWrite(_csPin, HIGH);
+
+ SPI.begin();
+
+ // Small delay to allow SPI to stabilize
+ delay(10);
+
+ // Initialize SD card with explicit CS pin and slower speed for reliability
+ // SD_SCK_MHZ(4) = 4MHz (try 1, 2, or 4 if having issues)
+ if (!_sd.begin(_csPin, SD_SCK_MHZ(4)))
+ {
+ return false;
+ }
+
+ return true;
+}
+
+void SdCardLogger::update(unsigned long now)
+{
+ // Periodically check for card presence changes (insertion/removal)
+ if (now - _lastCardPresenceCheck >= SD_CARD_PRESENCE_CHECK_MS)
+ {
+ checkCardPresence();
+ _lastCardPresenceCheck = now;
+ }
+
+ if (!_initialized || !_sdCardPresent)
+ {
+ return;
+ }
+
+ // Check for date change periodically
+ if (now - _lastFileCheckTime >= SD_FILE_CHECK_INTERVAL_MS)
+ {
+ checkForDateChange(now);
+ _lastFileCheckTime = now;
+ }
+
+ // Capture sensor snapshot and write to SD every second
+ if (now - _lastWriteTime >= SD_WRITE_INTERVAL_MS)
+ {
+ // Capture current sensor state
+ captureSensorSnapshot();
+
+ // Write buffered records
+ if (!isBufferEmpty())
+ {
+ if (writeRecordsToCard(SD_MAX_WRITES_PER_LOOP))
+ {
+ _lastWriteTime = now;
+
+ // Clear SD card error if it was raised and we can write again
+ if (_sdCardErrorRaised)
+ {
+ _warningManager->clearWarning(WarningType::SdCardError);
+ _sdCardErrorRaised = false;
+ }
+ } else {
+ // Write failed
+ if (!_sdCardErrorRaised)
+ {
+ _warningManager->raiseWarning(WarningType::SdCardError);
+ _sdCardErrorRaised = true;
+ }
+ }
+ }
+ else
+ {
+ _lastWriteTime = now;
+ }
+ }
+}
+
+bool SdCardLogger::writeRecordsToCard(uint8_t maxRecords)
+{
+ uint8_t recordsWritten = 0;
+
+ // Open or create file if needed
+ if (!_currentFile)
+ {
+ if (!openOrCreateFile(millis()))
+ {
+ return false;
+ }
+ }
+
+ // Write up to maxRecords from buffer
+ while (!isBufferEmpty() && recordsWritten < maxRecords)
+ {
+ const SensorSnapshot& snapshot = _buffer[_bufferTail];
+
+ writeSnapshotToCsv(snapshot);
+
+ // Move tail forward (circular)
+ _bufferTail = (_bufferTail + 1) % SD_BUFFER_SIZE;
+ _bufferCount--;
+ _totalRecordsLogged++;
+ recordsWritten++;
+ }
+
+ // Flush to ensure data is written
+ if (recordsWritten > 0)
+ {
+ _currentFile.flush();
+ }
+
+ return true;
+}
+
+void SdCardLogger::writeSnapshotToCsv(const SensorSnapshot& snapshot)
+{
+ if (!_currentFile)
+ {
+ return;
+ }
+
+ // Format: Timestamp,Temp,Humidity,Bearing,CompassTemp,Speed,WaterLevel,WaterPump,GPSLat,GPSLon,Altitude,GPSCourse,GPSSats,GPSDistance,Warnings,Horn
+
+ // DateTime (formatted)
+ char dateTimeBuf[DateTimeBufferLength];
+ DateTimeManager::formatDateTime(dateTimeBuf, sizeof(dateTimeBuf));
+ _currentFile.print(dateTimeBuf);
+ _currentFile.print(',');
+
+ // Temperature
+ if (isnan(snapshot.temperature))
+ _currentFile.print('?');
+ else
+ _currentFile.print(snapshot.temperature, 1);
+ _currentFile.print(',');
+
+ // Humidity
+ _currentFile.print(snapshot.humidity);
+ _currentFile.print(',');
+
+ // Bearing
+ if (isnan(snapshot.bearing))
+ _currentFile.print('?');
+ else
+ _currentFile.print(snapshot.bearing, 1);
+ _currentFile.print(',');
+
+ // CompassTemp
+ if (isnan(snapshot.compassTemp))
+ _currentFile.print('?');
+ else
+ _currentFile.print(snapshot.compassTemp, 1);
+ _currentFile.print(',');
+
+ // Speed
+ _currentFile.print(snapshot.speed);
+ _currentFile.print(',');
+
+ // WaterLevel
+ _currentFile.print(snapshot.waterLevel);
+ _currentFile.print(',');
+
+ // WaterPump (0 or 1)
+ _currentFile.print(snapshot.waterPumpActive ? '1' : '0');
+ _currentFile.print(',');
+
+ // GPSLat
+ if (isnan(snapshot.gpsLat))
+ _currentFile.print('?');
+ else
+ _currentFile.print(snapshot.gpsLat, 6);
+ _currentFile.print(',');
+
+ // GPSLon
+ if (isnan(snapshot.gpsLon))
+ _currentFile.print('?');
+ else
+ _currentFile.print(snapshot.gpsLon, 6);
+ _currentFile.print(',');
+
+ // Altitude
+ if (isnan(snapshot.altitude))
+ _currentFile.print('?');
+ else
+ _currentFile.print(snapshot.altitude, 1);
+ _currentFile.print(',');
+
+ // GPSCourse
+ if (isnan(snapshot.gpsCourse))
+ _currentFile.print('?');
+ else
+ _currentFile.print(snapshot.gpsCourse, 1);
+ _currentFile.print(',');
+
+ // GPSSats
+ _currentFile.print(snapshot.gpsSats);
+ _currentFile.print(',');
+
+ // GPSDistance
+ if (isnan(snapshot.gpsDistance))
+ _currentFile.print('?');
+ else
+ _currentFile.print(snapshot.gpsDistance, 2);
+ _currentFile.print(',');
+
+ // Warnings (hex format with leading zeros to 8 digits)
+ _currentFile.print(F("0x"));
+ char hexBuf[9];
+ snprintf(hexBuf, sizeof(hexBuf), "%08lX", (unsigned long)snapshot.warnings);
+ _currentFile.print(hexBuf);
+ _currentFile.print(',');
+
+ // Horn (0 or 1)
+ _currentFile.print(snapshot.hornActive ? '1' : '0');
+
+ _currentFile.println();
+}
+
+bool SdCardLogger::openOrCreateFile(unsigned long now)
+{
+ // Close existing file if open
+ if (_currentFile)
+ {
+ closeCurrentFile();
+ }
+
+ // Generate filename based on current date
+ updateFileName(now);
+
+ // Check if file exists
+ bool fileExists = _sd.exists(_currentFileName);
+
+ // Open file in append mode (O_RDWR | O_CREAT | O_APPEND)
+ if (!_currentFile.open(_currentFileName, O_RDWR | O_CREAT | O_AT_END))
+ {
+ return false;
+ }
+
+ // Track initial file size for free space calculation
+ _initialLogFileSize = _currentFile.fileSize();
+
+ // Write CSV header if new file
+ if (!fileExists)
+ {
+ _currentFile.println(F("Timestamp,Temp,Humidity,Bearing,CompassTemp,Speed,WaterLevel,WaterPump,GPSLat,GPSLon,Altitude,GPSCourse,GPSSats,GPSDistance,Warnings,Horn"));
+ _currentFile.flush();
+ }
+
+ return true;
+}
+
+void SdCardLogger::closeCurrentFile()
+{
+ if (_currentFile)
+ {
+ _currentFile.flush();
+ _currentFile.close();
+ }
+}
+
+void SdCardLogger::updateFileName(unsigned long now)
+{
+ (void)now; // Unused parameter, but could be used for future enhancements
+ uint16_t year = DateTimeManager::getYear();
+ uint8_t month = DateTimeManager::getMonth();
+ uint8_t day = DateTimeManager::getDay();
+
+ // Format: YYYYMMDD.csv
+ snprintf(_currentFileName, sizeof(_currentFileName),
+ "%04d%02d%02d.csv", year, month, day);
+
+ _currentDay = day;
+}
+
+void SdCardLogger::checkForDateChange(unsigned long now)
+{
+ uint8_t currentDay = DateTimeManager::getDay();
+
+ if (currentDay != _currentDay && _currentDay != 0)
+ {
+ // Date has changed, close current file and open new one
+ closeCurrentFile();
+ openOrCreateFile(now);
+ }
+}
+
+void SdCardLogger::captureSensorSnapshot()
+{
+ if (!_sensorHandler)
+ {
+ return;
+ }
+
+ SensorSnapshot snapshot;
+
+ // Capture all sensor values
+ snapshot.temperature = _sensorHandler->getTemperature();
+ snapshot.humidity = _sensorHandler->getHumidity();
+ snapshot.bearing = _sensorHandler->getBearing();
+ snapshot.compassTemp = _sensorHandler->getCompassTemperature();
+ snapshot.speed = _sensorHandler->getSpeed();
+ snapshot.waterLevel = _sensorHandler->getWaterLevel();
+ snapshot.waterPumpActive = _sensorHandler->getWaterPumpActive();
+ snapshot.gpsLat = _sensorHandler->getGpsLatitude();
+ snapshot.gpsLon = _sensorHandler->getGpsLongitude();
+ snapshot.altitude = _sensorHandler->getGpsAltitude();
+ snapshot.gpsCourse = _sensorHandler->getGpsCourse();
+ snapshot.gpsSats = _sensorHandler->getGpsSatellites();
+ snapshot.gpsDistance = _sensorHandler->getGpsDistance();
+ snapshot.warnings = _warningManager->getActiveWarningsMask();
+ snapshot.hornActive = _sensorHandler->getHornActive();
+
+ addSnapshotToBuffer(snapshot);
+}
+
+void SdCardLogger::addSnapshotToBuffer(const SensorSnapshot& snapshot)
+{
+ if (isBufferFull())
+ {
+ // Buffer is full, drop oldest record (overwrite tail)
+ _recordsDropped++;
+ _bufferTail = (_bufferTail + 1) % SD_BUFFER_SIZE;
+ _bufferCount--;
+ }
+
+ // Add new record at head
+ _buffer[_bufferHead] = snapshot;
+ _bufferHead = (_bufferHead + 1) % SD_BUFFER_SIZE;
+ _bufferCount++;
+}
+
+bool SdCardLogger::isBufferFull() const
+{
+ return _bufferCount >= SD_BUFFER_SIZE;
+}
+
+bool SdCardLogger::isBufferEmpty() const
+{
+ return _bufferCount == 0;
+}
+
+void SdCardLogger::flush()
+{
+ if ( !_initialized)
+ {
+ return;
+ }
+
+ // Write all buffered records (blocking)
+ while (!isBufferEmpty())
+ {
+ writeRecordsToCard(SD_MAX_WRITES_PER_LOOP);
+ }
+
+ closeCurrentFile();
+}
+
+void SdCardLogger::checkCardPresence()
+{
+ // Try to re-initialize SD card with slower speed for reliability
+ bool cardPresent = _sd.begin(_csPin, SD_SCK_MHZ(4));
+
+ // Card state changed from missing to present
+ if (cardPresent && !_sdCardPresent)
+ {
+ _sdCardPresent = true;
+ _initialized = true;
+
+ // Re-cache SD card size info after reinsertion
+ uint32_t cardSizeBlocks = _sd.card()->sectorCount();
+ _cachedTotalSize = (uint64_t)cardSizeBlocks * 512ULL;
+
+ uint32_t freeClusterCount = _sd.freeClusterCount();
+ uint32_t sectorsPerCluster = _sd.sectorsPerCluster();
+ _cachedFreeSpace = (uint64_t)freeClusterCount * sectorsPerCluster * 512ULL;
+
+ // Clear any SD card warnings
+ if (_sdCardMissingRaised)
+ {
+ _warningManager->clearWarning(WarningType::SdCardMissing);
+ _sdCardMissingRaised = false;
+ }
+
+ if (_sdCardErrorRaised)
+ {
+ _warningManager->clearWarning(WarningType::SdCardError);
+ _sdCardErrorRaised = false;
+ }
+ }
+ // Card is not present (either just removed or still missing)
+ else if (!cardPresent)
+ {
+ // Close any open files if card was previously present
+ if (_sdCardPresent)
+ {
+ closeCurrentFile();
+ }
+
+ _sdCardPresent = false;
+ _initialized = false;
+
+ // Determine if card is missing or there's another error
+ if (isCardMissingError())
+ {
+ // Clear error warning if it was raised
+ if (_sdCardErrorRaised)
+ {
+ _warningManager->clearWarning(WarningType::SdCardError);
+ _sdCardErrorRaised = false;
+ }
+
+ // Raise/maintain missing warning
+ if (!_sdCardMissingRaised)
+ {
+ _warningManager->raiseWarning(WarningType::SdCardMissing);
+ _sdCardMissingRaised = true;
+ }
+ }
+ else
+ {
+ // Clear missing warning if it was raised
+ if (_sdCardMissingRaised)
+ {
+ _warningManager->clearWarning(WarningType::SdCardMissing);
+ _sdCardMissingRaised = false;
+ }
+
+ // Raise/maintain error warning for non-missing errors
+ if (!_sdCardErrorRaised)
+ {
+ _warningManager->raiseWarning(WarningType::SdCardError);
+ _sdCardErrorRaised = true;
+ }
+ }
+ }
+ // Card present and state unchanged - no action needed
+}
+
+bool SdCardLogger::isCardMissingError()
+{
+ // Check the SD card error code to determine if card is missing
+ // Common error codes for missing card:
+ // - SD_CARD_ERROR_CMD0: Card not responding to CMD0 (GO_IDLE_STATE)
+ // - SD_CARD_ERROR_ACMD41: Card not responding to ACMD41 (initialization)
+ // - 0xFF or 0x01: Card not present (no response on SPI bus)
+
+ uint8_t errorCode = _sd.card()->errorCode();
+
+ // Error codes that typically indicate missing card:
+ // 0x01 = SD_CARD_ERROR_CMD0 (card not present/responding)
+ // 0x02 = SD_CARD_ERROR_CMD8 (card not responding to voltage check)
+ // 0xFF = No card or no response
+ return (errorCode == 0x01 || errorCode == 0x02 || errorCode == 0xFF);
+}
+
+bool SdCardLogger::isSdCardPresent()
+{
+ return _sdCardPresent;
+}
+
+uint32_t SdCardLogger::getCurrentLogFileSize() const
+{
+ if (!_currentFile || !_currentFile.isOpen())
+ {
+ return 0;
+ }
+
+ return _currentFile.fileSize();
+}
+
+void SdCardLogger::releaseSDCard()
+{
+ if (!_initialized)
+ {
+ return;
+ }
+
+ // Flush any pending writes first
+ flush();
+
+ // Close the current file
+ closeCurrentFile();
+
+ // Mark as not initialized to prevent operations during release
+ _initialized = false;
+}
+
+bool SdCardLogger::reacquireSDCard()
+{
+ // Attempt to reinitialize the SD card
+ if (!initializeSdCard())
+ {
+ return false;
+ }
+
+ _initialized = true;
+ _sdCardPresent = true;
+
+ // Recache SD card size info
+ uint32_t cardSizeBlocks = _sd.card()->sectorCount();
+ _cachedTotalSize = (uint64_t)cardSizeBlocks * 512ULL;
+
+ uint32_t freeClusterCount = _sd.freeClusterCount();
+ uint32_t sectorsPerCluster = _sd.sectorsPerCluster();
+ _cachedFreeSpace = (uint64_t)freeClusterCount * sectorsPerCluster * 512ULL;
+
+ // Note: File will be reopened automatically on next update() call via openOrCreateFile()
+ return true;
+}
diff --git a/SmartFuseBox/SdCardLogger.h b/SmartFuseBox/SdCardLogger.h
new file mode 100644
index 0000000..970a02f
--- /dev/null
+++ b/SmartFuseBox/SdCardLogger.h
@@ -0,0 +1,204 @@
+#pragma once
+
+#include
+#include
+#include
+#include "SensorCommandHandler.h"
+#include "WarningManager.h"
+
+constexpr uint8_t SD_BUFFER_SIZE = 64; // Number of records to buffer
+constexpr uint8_t SD_MAX_WRITES_PER_LOOP = 5; // Max records to write per update() call
+constexpr uint16_t SD_WRITE_INTERVAL_MS = 1000; // Minimum time between write operations
+constexpr uint16_t SD_FILE_CHECK_INTERVAL_MS = 60000; // Check for date change every minute
+constexpr uint16_t SD_CARD_PRESENCE_CHECK_MS = 5000; // Check for card presence every 5 seconds
+
+/**
+ * @struct SensorSnapshot
+ * @brief Snapshot of all sensor values at a point in time
+ */
+struct SensorSnapshot {
+ float temperature;
+ uint8_t humidity;
+ float bearing;
+ float compassTemp;
+ uint8_t speed;
+ uint16_t waterLevel;
+ bool waterPumpActive;
+ double gpsLat;
+ double gpsLon;
+ double altitude;
+ double gpsCourse;
+ uint32_t gpsSats;
+ double gpsDistance;
+ uint32_t warnings;
+ bool hornActive;
+
+ SensorSnapshot()
+ : temperature(NAN), humidity(0), bearing(NAN), compassTemp(NAN),
+ speed(0), waterLevel(0), waterPumpActive(false),
+ gpsLat(NAN), gpsLon(NAN), altitude(NAN), gpsCourse(NAN),
+ gpsSats(0), gpsDistance(NAN), warnings(0), hornActive(false) {}
+};
+
+/**
+ * @class SdCardLogger
+ * @brief Non-blocking SD card logger for sensor data
+ *
+ * Features:
+ * - Subscribes to MessageBus sensor events
+ * - Buffers sensor readings in circular buffer
+ * - Non-blocking writes (max N records per loop iteration)
+ * - Date-based file naming (YYYYMMDD.csv)
+ * - Automatic file rotation at midnight
+ * - Integrates with WarningManager for error reporting
+ * - Configurable via Config struct
+ *
+ * Architecture:
+ * - Sensor events -> MessageBus -> SdCardLogger buffer -> SD card (chunked writes)
+ *
+ * Usage:
+ * @code
+ * SdCardLogger logger(&messageBus, &warningManager);
+ *
+ * void setup() {
+ * Config* config = ConfigManager::getConfigPtr();
+ * if (!logger.initialize(config)) {
+ * // Handle initialization failure
+ * }
+ * }
+ *
+ * void loop() {
+ * unsigned long now = millis();
+ * logger.update(now);
+ * }
+ * @endcode
+ */
+class SdCardLogger
+{
+private:
+ SensorCommandHandler* _sensorHandler;
+ WarningManager* _warningManager;
+ uint8_t _csPin;
+
+ // SD card
+ SdFat _sd; // Main SD card object
+ FsFile _currentFile; // Current log file
+
+ // State
+ bool _initialized;
+ bool _sdCardPresent;
+
+ // Circular buffer
+ SensorSnapshot _buffer[SD_BUFFER_SIZE];
+ uint8_t _bufferHead; // Next position to write
+ uint8_t _bufferTail; // Next position to read
+ uint8_t _bufferCount; // Number of records in buffer
+ char _currentFileName[13]; // "YYYYMMDD.csv\0"
+ uint8_t _currentDay; // Track current day for file rotation
+
+ // Timing
+ unsigned long _lastWriteTime;
+ unsigned long _lastFileCheckTime;
+ unsigned long _lastCardPresenceCheck;
+
+ // Statistics
+ unsigned long _totalRecordsLogged;
+ unsigned long _recordsDropped;
+ bool _sdCardErrorRaised;
+ bool _sdCardMissingRaised;
+
+ // Cached SD card info (to avoid expensive freeClusterCount calls)
+ uint64_t _cachedTotalSize;
+ uint64_t _cachedFreeSpace;
+ uint32_t _initialLogFileSize;
+
+ // Internal methods
+ bool initializeSdCard();
+ bool openOrCreateFile(unsigned long now);
+ void closeCurrentFile();
+ bool writeRecordsToCard(uint8_t maxRecords);
+ void writeSnapshotToCsv(const SensorSnapshot& snapshot);
+ void captureSensorSnapshot();
+ void addSnapshotToBuffer(const SensorSnapshot& snapshot);
+ bool isBufferFull() const;
+ bool isBufferEmpty() const;
+ void updateFileName(unsigned long now);
+ void checkForDateChange(unsigned long now);
+ void checkCardPresence();
+ bool isCardMissingError();
+
+public:
+ /**
+ * @brief Constructor
+ * @param messageBus Pointer to MessageBus for subscribing to events
+ * @param warningManager Pointer to WarningManager for error reporting
+ */
+ SdCardLogger(SensorCommandHandler* sensorHandler, WarningManager* warningManager, uint8_t csPin);
+
+ /**
+ * @brief Initialize SD card logger with configuration
+ * @param config Configuration structure
+ * @return true if initialization successful, false otherwise
+ */
+ bool initialize();
+
+ /**
+ * @brief Update logger - processes buffered writes in non-blocking manner
+ * @param now Current time from millis()
+ */
+ void update(unsigned long now);
+
+ /**
+ * @brief Check if SD card is initialized and working
+ * @return true if SD card is ready, false otherwise
+ */
+ bool isSdCardReady() const { return _initialized && _sdCardPresent; }
+
+ /**
+ * @brief Check if SD card is present (may not be initialized)
+ * @return true if card is present, false otherwise
+ */
+ bool isSdCardPresent();
+
+ /**
+ * @brief Get current log file size in bytes
+ * @return Log file size in bytes, or 0 if file not open
+ */
+ uint32_t getCurrentLogFileSize() const;
+
+ /**
+ * @brief Get total number of records successfully logged
+ * @return Total records logged
+ */
+ unsigned long getTotalRecordsLogged() const { return _totalRecordsLogged; }
+
+ /**
+ * @brief Get number of records dropped due to buffer overflow
+ * @return Number of dropped records
+ */
+ unsigned long getRecordsDropped() const { return _recordsDropped; }
+
+ /**
+ * @brief Get current buffer utilization
+ * @return Number of records currently in buffer
+ */
+ uint8_t getBufferCount() const { return _bufferCount; }
+
+ /**
+ * @brief Flush all buffered records to SD card (blocking)
+ * Use sparingly, typically only during shutdown
+ */
+ void flush();
+
+ /**
+ * @brief Temporarily release SD card access (closes file and deinitializes SD)
+ * Use before operations that need exclusive SD card access (e.g., config reload)
+ */
+ void releaseSDCard();
+
+ /**
+ * @brief Re-initialize SD card after temporary release
+ * @return true if re-initialization successful
+ */
+ bool reacquireSDCard();
+};
diff --git a/SmartFuseBox/SensorDataRecord.h b/SmartFuseBox/SensorDataRecord.h
new file mode 100644
index 0000000..e76a973
--- /dev/null
+++ b/SmartFuseBox/SensorDataRecord.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include
+#include
+
+/**
+ * @enum SensorDataType
+ * @brief Types of sensor data that can be logged
+ */
+enum class SensorDataType : uint8_t {
+ Temperature = 0x01,
+ Humidity = 0x02,
+ WaterLevel = 0x03,
+ LightLevel = 0x04,
+};
+
+/**
+ * @struct SensorDataRecord
+ * @brief Standardized structure for logging sensor data
+ *
+ * This structure holds a single sensor reading with timestamp.
+ * Designed to be compact for efficient memory usage in circular buffer.
+ */
+struct SensorDataRecord {
+ unsigned long timestamp; // millis() when reading was taken
+ SensorDataType sensorType; // Type of sensor data
+ float value1; // Primary value (e.g., temperature, water level)
+ float value2; // Secondary value (e.g., humidity, average water level)
+
+ SensorDataRecord()
+ : timestamp(0), sensorType(SensorDataType::Temperature), value1(0.0f), value2(0.0f) {}
+
+ SensorDataRecord(unsigned long ts, SensorDataType type, float val1, float val2 = 0.0f)
+ : timestamp(ts), sensorType(type), value1(val1), value2(val2) {}
+};
diff --git a/SmartFuseBox/Shared/Sensors/Dht11SensorHandler.h b/SmartFuseBox/Shared/Sensors/Dht11SensorHandler.h
deleted file mode 100644
index e69de29..0000000
diff --git a/SmartFuseBox/SmartFuseBox.ino b/SmartFuseBox/SmartFuseBox.ino
index 508793c..3626440 100644
--- a/SmartFuseBox/SmartFuseBox.ino
+++ b/SmartFuseBox/SmartFuseBox.ino
@@ -2,6 +2,8 @@
#include
#include
#include
+#include
+#include
#include "SystemCpuMonitor.h"
#include "DateTimeManager.h"
@@ -35,13 +37,12 @@
#include "ConfigNetworkHandler.h"
#include "RelayNetworkHandler.h"
#include "SoundNetworkHandler.h"
-#include "WarningNetworkHandler.h"
#include "SystemNetworkHandler.h"
#include "SensorNetworkHandler.h"
+#include "WarningNetworkHandler.h"
#include "ConfigController.h"
#include "ConfigSyncManager.h"
-#include "RelayController.h"
#include "SensorController.h"
#include "SoundController.h"
@@ -50,7 +51,12 @@
#endif
#include "MessageBus.h"
+#include "SensorDataRecord.h"
+#include "SdCardLogger.h"
+#if defined(CARD_CONFIG_LOADER)
+#include "SDCardConfigLoader.h"
+#endif
#define COMPUTER_SERIAL Serial
#define LINK_SERIAL Serial1
@@ -96,7 +102,7 @@ SystemCommandHandler systemCommandHandler(&broadcastManager, &warningManager);
// Sensors
WaterSensorHandler waterSensorHandler(&messageBus, &broadcastManager, &sensorCommandHandler, WaterSensorPin, WaterSensorActivePin);
Dht11SensorHandler dht11SensorHandler(&messageBus, &broadcastManager, &sensorCommandHandler, &warningManager, Dht11SensorPin);
-LightSensorHandler lightSensorHandler(&messageBus, &broadcastManager, &sensorCommandHandler, &warningManager, LightSensorPin);
+LightSensorHandler lightSensorHandler(&messageBus, &broadcastManager, &sensorCommandHandler, &warningManager, LightSensorPin, LightSensorAnalogPin);
BaseSensorHandler* sensorHandlers[] = {
&waterSensorHandler, &dht11SensorHandler, &lightSensorHandler
@@ -131,13 +137,19 @@ WarningNetworkHandler warningNetworkHandler(&warningManager);
SystemNetworkHandler systemNetworkHandler(&wifiController);
SensorNetworkHandler sensorNetworkHandler(&sensorController);
+// SD card logger
+SdCardLogger sdCardLogger(&sensorCommandHandler, &warningManager, SdCardCsPin);
+
+#if defined(CARD_CONFIG_LOADER)
+SdCardConfigLoader sdCardConfigLoader(&commandMgrComputer, &commandMgrLink, &configController, &configSyncManager, SdCardCsPin);
+#endif
void setup()
{
// Serial initialization is performed first to ensure that any logging or error messages
// from DateTimeManager or ConfigManager during initialization are properly output.
SystemFunctions::initializeSerial(COMPUTER_SERIAL, 115200, true);
- SystemFunctions::initializeSerial(LINK_SERIAL, 9600, true);
+ SystemFunctions::initializeSerial(LINK_SERIAL, 19200, true);
DateTimeManager::setDateTime();
@@ -167,14 +179,22 @@ void setup()
ackHandler.setConfigSyncManager(&configSyncManager, &configController);
configHandler.setConfigSyncManager(&configSyncManager);
+#if defined(CARD_CONFIG_LOADER)
+ configHandler.setSdCardConfigLoader(&sdCardConfigLoader);
+#endif
+
Config* config = ConfigManager::getConfigPtr();
configureWifiSupport(config);
configureBluetoothSupport(config);
+ systemCommandHandler.setSdCardLogger(&sdCardLogger);
+ systemNetworkHandler.setSdCardLogger(&sdCardLogger);
soundController.configUpdated(config);
relayHandler.configUpdated(config);
sensorManager.setup();
+ // Initialize SD card logger
+ sdCardLogger.initialize();
#if defined(ARDUINO_UNO_R4) && defined(LED_MANAGER)
ledManager.Initialize();
@@ -189,7 +209,16 @@ void setup()
}
}
- configSyncManager.requestSync();
+#if defined(CARD_CONFIG_LOADER)
+ // Link SD card logger to config loader for coordinated SD card access
+ sdCardConfigLoader.setSdCardLogger(&sdCardLogger);
+
+ bool sdConfigLoaded = sdCardConfigLoader.loadConfigFromSd();
+ if (!sdConfigLoaded)
+ {
+ configSyncManager.requestSync();
+ }
+#endif
// indicate system initialized
commandMgrComputer.sendCommand(SystemInitialized, "");
@@ -230,6 +259,10 @@ void loop()
configSyncManager.update(now);
SystemCpuMonitor::endTask();
+ SystemCpuMonitor::startTask();
+ sdCardLogger.update(now);
+ SystemCpuMonitor::endTask();
+
SystemCpuMonitor::update();
delay(DefaultDelay);
}
@@ -237,11 +270,13 @@ void loop()
void onComputerCommandReceived(SerialCommandManager* mgr)
{
commandMgrComputer.sendError(mgr->getRawMessage(), F("STATCMD"));
+ SystemFunctions::resetSerial(COMPUTER_SERIAL);
}
void onLinkCommandReceived(SerialCommandManager* mgr)
{
commandMgrComputer.sendError(mgr->getRawMessage(), F("STATLNK"));
+ SystemFunctions::resetSerial(LINK_SERIAL);
}
void configureWifiSupport(Config* config)
diff --git a/SmartFuseBox/SmartFuseBox.vcxproj b/SmartFuseBox/SmartFuseBox.vcxproj
index 2490ef6..fceefae 100644
--- a/SmartFuseBox/SmartFuseBox.vcxproj
+++ b/SmartFuseBox/SmartFuseBox.vcxproj
@@ -74,6 +74,8 @@
+
+
@@ -117,7 +119,10 @@
CppCode
true
-
+
+
+
+
@@ -161,7 +166,6 @@
-
@@ -176,7 +180,7 @@
VisualMicroDebugger
- $(ProjectDir)..\SmartFuseBox;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\WiFiS3\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\dht11;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\EEPROM\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\BlockDevices;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\Storage;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\tinyusb;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\api\deprecated;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\api\deprecated-avr-comp;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc\api;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc\instances;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\arm\CMSIS_5\CMSIS\Core\Include;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_gen;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_cfg\fsp_cfg\bsp;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_cfg\fsp_cfg;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_usb_basic\src\driver\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\crypto_procedures\src\sce5\plainkey\private\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\crypto_procedures\src\sce5\plainkey\public\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\common;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1\arm-none-eabi\thumb\v7e-m\fpv4-sp\hard;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1\backward;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\lib\gcc\arm-none-eabi\7.2.1\include;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\lib\gcc\arm-none-eabi\7.2.1\include-fixed;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src\utility;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src
+ $(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\SPI;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SdFat\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\WiFiS3\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\dht11;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\EEPROM\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\BlockDevices;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\Storage;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\tinyusb;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\api\deprecated;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\api\deprecated-avr-comp;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc\api;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc\instances;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\arm\CMSIS_5\CMSIS\Core\Include;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_gen;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_cfg\fsp_cfg\bsp;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_cfg\fsp_cfg;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_usb_basic\src\driver\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\crypto_procedures\src\sce5\plainkey\private\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\crypto_procedures\src\sce5\plainkey\public\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\common;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1\arm-none-eabi\thumb\v7e-m\fpv4-sp\hard;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1\backward;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\lib\gcc\arm-none-eabi\7.2.1\include;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\lib\gcc\arm-none-eabi\7.2.1\include-fixed;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src\utility;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\SPI;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SdFat\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src
$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\bin\arm-none-eabi-g++
$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\bin\arm-none-eabi-g++
false
@@ -203,7 +207,7 @@
- $(ProjectDir)..\SmartFuseBox;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\WiFiS3\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\dht11;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\EEPROM\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\BlockDevices;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\Storage;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\tinyusb;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\api\deprecated;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\api\deprecated-avr-comp;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc\api;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc\instances;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\arm\CMSIS_5\CMSIS\Core\Include;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_gen;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_cfg\fsp_cfg\bsp;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_cfg\fsp_cfg;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_usb_basic\src\driver\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\crypto_procedures\src\sce5\plainkey\private\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\crypto_procedures\src\sce5\plainkey\public\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\common;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1\arm-none-eabi\thumb\v7e-m\fpv4-sp\hard;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1\backward;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\lib\gcc\arm-none-eabi\7.2.1\include;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\lib\gcc\arm-none-eabi\7.2.1\include-fixed;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src\utility;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;%(AdditionalIncludeDirectories)
+ $(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\SPI;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SdFat\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\WiFiS3\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\dht11;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\EEPROM\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\BlockDevices;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\Storage;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\tinyusb;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\api\deprecated;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino\api\deprecated-avr-comp;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\cores\arduino;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc\api;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\inc\instances;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\arm\CMSIS_5\CMSIS\Core\Include;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_gen;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_cfg\fsp_cfg\bsp;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra_cfg\fsp_cfg;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_usb_basic\src\driver\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\crypto_procedures\src\sce5\plainkey\private\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\crypto_procedures\src\sce5\plainkey\public\inc;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce\common;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\variants\UNOWIFIR4\includes\ra\fsp\src\r_sce;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1\arm-none-eabi\thumb\v7e-m\fpv4-sp\hard;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include\c++\7.2.1\backward;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\lib\gcc\arm-none-eabi\7.2.1\include;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\lib\gcc\arm-none-eabi\7.2.1\include-fixed;$(ProjectDir)..\..\..\users\simon\appdata\local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\arm-none-eabi\include;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\ArduinoBLE\src\utility;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SerialCommandManager\src;$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\hardware\renesas_uno\1.5.1\libraries\SPI;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SdFat\src;$(ProjectDir)..\..\..\Users\Simon\Documents\Arduino\libraries\SensorManager\src;%(AdditionalIncludeDirectories)
$(ProjectDir)..\..\..\Users\Simon\AppData\Local\arduino15\packages\arduino\tools\arm-none-eabi-gcc\7-2017q4\bin\arm-none-eabi-g++
c++17
gnu11
diff --git a/SmartFuseBox/SmartFuseBox.vcxproj.filters b/SmartFuseBox/SmartFuseBox.vcxproj.filters
index 7c39dc6..abc04f5 100644
--- a/SmartFuseBox/SmartFuseBox.vcxproj.filters
+++ b/SmartFuseBox/SmartFuseBox.vcxproj.filters
@@ -160,13 +160,13 @@
Source Files\BusinessLogic
-
- Source Files
+
+ Source Files\SharedCode
-
- Source Files\SharedCode\SerialCommandHandlers
+
+ Source Files
-
+
Source Files
@@ -175,9 +175,6 @@
Source Files
-
- Source Files\SharedCode
-
Source Files
@@ -330,9 +327,6 @@
Header Files\SharedCode
-
- Header Files
-
Header Files
@@ -345,6 +339,36 @@
Header Files
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
diff --git a/SmartFuseBox/SmartFuseBox/SdCardConfigLoader.cpp b/SmartFuseBox/SmartFuseBox/SdCardConfigLoader.cpp
new file mode 100644
index 0000000..d941b8d
--- /dev/null
+++ b/SmartFuseBox/SmartFuseBox/SdCardConfigLoader.cpp
@@ -0,0 +1,709 @@
+#include "SdCardConfigLoader.h"
+#include "ConfigManager.h"
+#include
+
+SdCardConfigLoader::SdCardConfigLoader(SerialCommandManager* computerSerial,
+ SerialCommandManager* linkSerial,
+ ConfigController* configController,
+ ConfigSyncManager* configSyncManager,
+ uint8_t csPin)
+ : _computerSerial(computerSerial),
+ _linkSerial(linkSerial),
+ _configController(configController),
+ _configSyncManager(configSyncManager),
+ _csPin(csPin),
+ _sdConfigPresent(false)
+{
+}
+
+bool SdCardConfigLoader::checkSdCard()
+{
+ if (!_sd.begin(_csPin, SD_SCK_MHZ(50)))
+ {
+ return false;
+ }
+ return true;
+}
+
+bool SdCardConfigLoader::configFileExists()
+{
+ return _sd.exists(SD_CONFIG_FILENAME);
+}
+
+bool SdCardConfigLoader::applyConfigCommand(const char* line)
+{
+ // Skip empty lines and comments
+ if (line == nullptr || line[0] == '\0' || line[0] == '#' || line[0] == '\r' || line[0] == '\n')
+ {
+ return true;
+ }
+
+ // Create a mutable copy for parsing
+ char buffer[SD_CONFIG_MAX_LINE_LENGTH];
+ strncpy(buffer, line, sizeof(buffer) - 1);
+ buffer[sizeof(buffer) - 1] = '\0';
+
+ // Trim whitespace and newlines
+ char* end = buffer + strlen(buffer) - 1;
+ while (end > buffer && (*end == '\r' || *end == '\n' || *end == ' ' || *end == '\t'))
+ {
+ *end = '\0';
+ end--;
+ }
+
+ // Parse command using SerialCommandManager's parser
+ // Format: "CMD:param1=value1;param2=value2"
+ char* colonPos = strchr(buffer, ':');
+ char command[8] = {0};
+
+ if (colonPos != nullptr)
+ {
+ size_t cmdLen = colonPos - buffer;
+ if (cmdLen >= sizeof(command))
+ {
+ logError("Command too long", line);
+ return false;
+ }
+ strncpy(command, buffer, cmdLen);
+ command[cmdLen] = '\0';
+ }
+ else
+ {
+ // No colon means command with no params (like C0, C1, C2)
+ strncpy(command, buffer, sizeof(command) - 1);
+ }
+
+ // Parse parameters manually since we're simulating command input
+ const char* paramsStr = colonPos ? (colonPos + 1) : "";
+
+ // Use the computer serial's command parser to validate and route the command
+ // We'll construct a synthetic command and feed it through the handler chain
+
+ // Parse parameters into key-value pairs
+ StringKeyValue params[10];
+ uint8_t paramCount = 0;
+
+ if (paramsStr && *paramsStr)
+ {
+ char paramBuffer[SD_CONFIG_MAX_LINE_LENGTH];
+ strncpy(paramBuffer, paramsStr, sizeof(paramBuffer) - 1);
+ paramBuffer[sizeof(paramBuffer) - 1] = '\0';
+
+ // Parse semicolon-separated parameters
+ char* savePtr1 = nullptr;
+ char* param = strtok_r(paramBuffer, ";", &savePtr1);
+
+ while (param && paramCount < 10)
+ {
+ // Each param can be "key=value" or just "value"
+ char* equalPos = strchr(param, '=');
+ if (equalPos)
+ {
+ *equalPos = '\0';
+ params[paramCount].key = param;
+ params[paramCount].value = equalPos + 1;
+ }
+ else
+ {
+ // For commands like C13:SSID, the whole thing after : is the value
+ // with an implied key (often "v" or empty)
+ params[paramCount].key = "";
+ params[paramCount].value = param;
+ }
+ paramCount++;
+ param = strtok_r(nullptr, ";", &savePtr1);
+ }
+ }
+
+ // Now apply the command through ConfigController
+ // We bypass the SerialCommandManager and call ConfigController methods directly
+
+ ConfigResult result = ConfigResult::InvalidCommand;
+
+ if (strcmp(command, "C3") == 0 && paramCount >= 1)
+ {
+ result = _configController->rename(params[0].value);
+ }
+ else if (strcmp(command, "C4") == 0 && paramCount >= 1)
+ {
+ uint8_t idx = static_cast(strtoul(params[0].key, nullptr, 0));
+ result = _configController->renameRelay(idx, params[0].value);
+ }
+ else if (strcmp(command, "C5") == 0 && paramCount >= 1)
+ {
+ uint8_t button = static_cast(strtoul(params[0].key, nullptr, 0));
+ uint8_t relay = static_cast(strtoul(params[0].value, nullptr, 0));
+ result = _configController->mapHomeButton(button, relay);
+ }
+ else if (strcmp(command, "C6") == 0 && paramCount >= 1)
+ {
+ uint8_t button = static_cast(strtoul(params[0].key, nullptr, 0));
+ uint8_t color = static_cast(strtoul(params[0].value, nullptr, 0));
+ result = _configController->mapHomeButtonColor(button, color);
+ }
+ else if (strcmp(command, "C7") == 0 && paramCount >= 1)
+ {
+ uint8_t type = atoi(params[0].value);
+ result = _configController->setVesselType(type);
+ }
+ else if (strcmp(command, "C8") == 0 && paramCount >= 1)
+ {
+ uint8_t relay = atoi(params[0].value);
+ result = _configController->setSoundRelayButton(relay);
+ }
+ else if (strcmp(command, "C9") == 0 && paramCount >= 1)
+ {
+ uint16_t delay = atoi(params[0].value);
+ result = _configController->setsoundDelayStart(delay);
+ }
+#if defined(ARDUINO_UNO_R4)
+ else if (strcmp(command, "C10") == 0 && paramCount >= 1)
+ {
+ bool enabled = (atoi(params[0].value) != 0);
+ result = _configController->setBluetoothEnabled(enabled);
+ }
+ else if (strcmp(command, "C11") == 0 && paramCount >= 1)
+ {
+ bool enabled = (atoi(params[0].value) != 0);
+ result = _configController->setWifiEnabled(enabled);
+ }
+ else if (strcmp(command, "C12") == 0 && paramCount >= 1)
+ {
+ uint8_t mode = atoi(params[0].value);
+ result = _configController->setWifiAccessMode(mode);
+ }
+ else if (strcmp(command, "C13") == 0 && paramCount >= 1)
+ {
+ // For C13, the entire string after : is the SSID (no v= prefix in file)
+ // We need to find original param value from line
+ const char* ssid = strchr(line, ':');
+ if (ssid)
+ {
+ ssid++; // Skip the colon
+ result = _configController->setWifiSsid(ssid);
+ }
+ }
+ else if (strcmp(command, "C14") == 0 && paramCount >= 1)
+ {
+ // For C14, the entire string after : is the password
+ const char* password = strchr(line, ':');
+ if (password)
+ {
+ password++; // Skip the colon
+ result = _configController->setWifiPassword(password);
+ }
+ }
+ else if (strcmp(command, "C15") == 0 && paramCount >= 1)
+ {
+ uint16_t port = atoi(params[0].value);
+ result = _configController->setWifiPort(port);
+ }
+ else if (strcmp(command, "C17") == 0 && paramCount >= 1)
+ {
+ // For C17, the entire string after : is the IP address
+ const char* ip = strchr(line, ':');
+ if (ip)
+ {
+ ip++; // Skip the colon
+ result = _configController->setWifiIpAddress(ip);
+ }
+ }
+#endif
+ else if (strcmp(command, "C18") == 0 && paramCount >= 1)
+ {
+ uint8_t relay = static_cast(atoi(params[0].key));
+ bool state = (atoi(params[0].value) != 0);
+ result = _configController->setRelayDefaultState(relay, state);
+ }
+ else if (strcmp(command, "C19") == 0 && paramCount >= 1)
+ {
+ uint8_t relay1 = static_cast(strtoul(params[0].key, nullptr, 0));
+ uint8_t relay2 = static_cast(strtoul(params[0].value, nullptr, 0));
+
+ if (relay2 == 255)
+ {
+ result = _configController->unlinkRelay(relay1);
+ }
+ else
+ {
+ result = _configController->linkRelays(relay1, relay2);
+ }
+ }
+ else if (strcmp(command, "C20") == 0 && paramCount >= 1)
+ {
+ int8_t offset = static_cast(atoi(params[0].value));
+ result = _configController->setTimezoneOffset(offset);
+ }
+ else if (strcmp(command, "C21") == 0 && paramCount >= 1)
+ {
+ const char* mmsi = strchr(line, ':');
+ if (mmsi)
+ {
+ mmsi++; // Skip the colon
+ result = _configController->setMmsi(mmsi);
+ }
+ }
+ else if (strcmp(command, "C22") == 0)
+ {
+ const char* callSign = strchr(line, ':');
+ if (callSign && *(callSign + 1) != '\0')
+ {
+ callSign++; // Skip the colon
+ result = _configController->setCallSign(callSign);
+ }
+ else
+ {
+ result = _configController->setCallSign("");
+ }
+ }
+ else if (strcmp(command, "C23") == 0)
+ {
+ const char* homePort = strchr(line, ':');
+ if (homePort && *(homePort + 1) != '\0')
+ {
+ homePort++; // Skip the colon
+ result = _configController->setHomePort(homePort);
+ }
+ else
+ {
+ result = _configController->setHomePort("");
+ }
+ }
+ else if (strcmp(command, "C24") == 0 && paramCount >= 5)
+ {
+ uint8_t type = 0, colorSet = 0, r = 0, g = 0, b = 0;
+
+ for (uint8_t i = 0; i < paramCount; i++)
+ {
+ if (strcmp(params[i].key, "t") == 0)
+ type = atoi(params[i].value);
+ else if (strcmp(params[i].key, "c") == 0)
+ colorSet = atoi(params[i].value);
+ else if (strcmp(params[i].key, "r") == 0)
+ r = atoi(params[i].value);
+ else if (strcmp(params[i].key, "g") == 0)
+ g = atoi(params[i].value);
+ else if (strcmp(params[i].key, "b") == 0)
+ b = atoi(params[i].value);
+ }
+
+ result = _configController->setLedColor(type, colorSet, r, g, b);
+ }
+ else if (strcmp(command, "C25") == 0 && paramCount >= 2)
+ {
+ uint8_t type = 0, brightness = 0;
+
+ for (uint8_t i = 0; i < paramCount; i++)
+ {
+ if (strcmp(params[i].key, "t") == 0)
+ type = atoi(params[i].value);
+ else if (strcmp(params[i].key, "b") == 0)
+ brightness = atoi(params[i].value);
+ }
+
+ result = _configController->setLedBrightness(type, brightness);
+ }
+ else if (strcmp(command, "C26") == 0 && paramCount >= 1)
+ {
+ bool enabled = (atoi(params[0].value) != 0);
+ result = _configController->setLedAutoSwitch(enabled);
+ }
+ else if (strcmp(command, "C27") == 0 && paramCount >= 3)
+ {
+ bool gps = false, warning = false, system = false;
+
+ for (uint8_t i = 0; i < paramCount; i++)
+ {
+ if (strcmp(params[i].key, "g") == 0)
+ gps = (atoi(params[i].value) != 0);
+ else if (strcmp(params[i].key, "w") == 0)
+ warning = (atoi(params[i].value) != 0);
+ else if (strcmp(params[i].key, "s") == 0)
+ system = (atoi(params[i].value) != 0);
+ }
+
+ result = _configController->setLedEnableStates(gps, warning, system);
+ }
+ else if (strcmp(command, "C28") == 0 && paramCount >= 4)
+ {
+ uint8_t type = 0, preset = 0;
+ uint16_t toneHz = 0, durationMs = 0;
+ uint32_t repeatMs = 0;
+
+ for (uint8_t i = 0; i < paramCount; i++)
+ {
+ if (strcmp(params[i].key, "t") == 0)
+ type = atoi(params[i].value);
+ else if (strcmp(params[i].key, "h") == 0)
+ toneHz = atoi(params[i].value);
+ else if (strcmp(params[i].key, "d") == 0)
+ durationMs = atoi(params[i].value);
+ else if (strcmp(params[i].key, "p") == 0)
+ preset = atoi(params[i].value);
+ else if (strcmp(params[i].key, "r") == 0)
+ repeatMs = strtoul(params[i].value, nullptr, 0);
+ }
+
+ result = _configController->setControlPanelTones(type, preset, toneHz, durationMs, repeatMs);
+ }
+ else
+ {
+ logError("Unknown or invalid command", line);
+ return false;
+ }
+
+ if (result != ConfigResult::Success)
+ {
+ char errorMsg[64];
+ snprintf(errorMsg, sizeof(errorMsg), "Command failed: %s (result=%d)", command, static_cast(result));
+ logError(errorMsg, line);
+ return false;
+ }
+
+ return true;
+}
+
+void SdCardConfigLoader::syncConfigToLink()
+{
+ // Send C1 (get settings) to trigger config broadcast via LINK
+ // This will make the control panel receive the new config
+ if (_linkSerial)
+ {
+ _linkSerial->sendCommand("C1", "");
+ }
+}
+
+void SdCardConfigLoader::logError(const char* message, const char* line)
+{
+ if (_computerSerial)
+ {
+ _computerSerial->sendError("SD_CFG_ERROR", message);
+ if (line)
+ {
+ _computerSerial->sendError("SD_CFG_LINE", line);
+ }
+ }
+}
+
+void SdCardConfigLoader::logInfo(const char* message)
+{
+ if (_computerSerial)
+ {
+ _computerSerial->sendError("SD_CFG_INFO", message);
+ }
+}
+
+bool SdCardConfigLoader::loadConfigFromSd()
+{
+ logInfo("Checking for SD config...");
+
+ if (!checkSdCard())
+ {
+ logInfo("SD card not present or not accessible");
+ return false;
+ }
+
+ if (!configFileExists())
+ {
+ logInfo("Config file not found on SD card");
+ return false;
+ }
+
+ logInfo("Loading config from SD card...");
+
+ FsFile configFile = _sd.open(SD_CONFIG_FILENAME, O_RDONLY);
+ if (!configFile)
+ {
+ logError("Failed to open config file");
+ return false;
+ }
+
+ // Read and apply commands line by line
+ char line[SD_CONFIG_MAX_LINE_LENGTH];
+ uint16_t lineNumber = 0;
+ uint16_t successCount = 0;
+ uint16_t errorCount = 0;
+
+ while (configFile.available())
+ {
+ int len = configFile.fgets(line, sizeof(line));
+ lineNumber++;
+
+ if (len > 0)
+ {
+ if (applyConfigCommand(line))
+ {
+ successCount++;
+ }
+ else
+ {
+ errorCount++;
+ }
+ }
+ }
+
+ configFile.close();
+
+ // Save to EEPROM
+ if (successCount > 0)
+ {
+ logInfo("Saving config to EEPROM...");
+ ConfigResult saveResult = _configController->save();
+
+ if (saveResult == ConfigResult::Success)
+ {
+ logInfo("Config saved to EEPROM");
+
+ // Sync to control panel via LINK
+ logInfo("Syncing config to control panel...");
+ syncConfigToLink();
+
+ // Disable ConfigSyncManager since SD config is authoritative
+ if (_configSyncManager)
+ {
+ _configSyncManager->setEnabled(false);
+ logInfo("ConfigSyncManager disabled (SD config active)");
+ }
+
+ _sdConfigPresent = true;
+
+ char summary[64];
+ snprintf(summary, sizeof(summary), "SD config loaded: %u commands applied, %u errors", successCount, errorCount);
+ logInfo(summary);
+
+ return true;
+ }
+ else
+ {
+ logError("Failed to save config to EEPROM");
+ }
+ }
+
+ return false;
+}
+
+bool SdCardConfigLoader::reloadConfigFromSd()
+{
+ logInfo("Reloading config from SD card...");
+ return loadConfigFromSd();
+}
+
+bool SdCardConfigLoader::exportConfigToSd()
+{
+ logInfo("Exporting config to SD card...");
+
+ if (!checkSdCard())
+ {
+ logError("SD card not present or not accessible");
+ return false;
+ }
+
+ // Delete existing config file if present
+ if (_sd.exists(SD_CONFIG_FILENAME))
+ {
+ _sd.remove(SD_CONFIG_FILENAME);
+ }
+
+ FsFile configFile = _sd.open(SD_CONFIG_FILENAME, O_WRONLY | O_CREAT);
+ if (!configFile)
+ {
+ logError("Failed to create config file");
+ return false;
+ }
+
+ Config* config = _configController->getConfigPtr();
+ if (!config)
+ {
+ configFile.close();
+ logError("Config not available");
+ return false;
+ }
+
+ // Write header comment
+ configFile.println("# SmartFuseBox Configuration");
+ configFile.println("# Generated by C30 command");
+ configFile.println();
+
+ // C3 - Boat name
+ if (strlen(config->name) > 0)
+ {
+ configFile.print("C3:");
+ configFile.println(config->name);
+ }
+
+ // C4 - Relay names
+ for (uint8_t i = 0; i < ConfigRelayCount; i++)
+ {
+ configFile.print("C4:");
+ configFile.print(i);
+ configFile.print("=");
+ configFile.print(config->relayShortNames[i]);
+ configFile.print("|");
+ configFile.println(config->relayLongNames[i]);
+ }
+
+ // C5 - Home button mappings
+ for (uint8_t i = 0; i < ConfigHomeButtons; i++)
+ {
+ configFile.print("C5:");
+ configFile.print(i);
+ configFile.print("=");
+ configFile.println(config->homePageMapping[i]);
+ }
+
+ // C6 - Button colors
+ for (uint8_t i = 0; i < ConfigRelayCount; i++)
+ {
+ configFile.print("C6:");
+ configFile.print(i);
+ configFile.print("=");
+ configFile.println(config->buttonImage[i]);
+ }
+
+ // C7 - Vessel type
+ configFile.print("C7:v=");
+ configFile.println(static_cast(config->vesselType));
+
+ // C8 - Sound relay
+ configFile.print("C8:v=");
+ configFile.println(config->hornRelayIndex);
+
+ // C9 - Sound delay
+ configFile.print("C9:v=");
+ configFile.println(config->soundStartDelayMs);
+
+#if defined(ARDUINO_UNO_R4)
+ // C10 - Bluetooth enabled
+ configFile.print("C10:v=");
+ configFile.println(config->bluetoothEnabled ? "1" : "0");
+
+ // C11 - WiFi enabled
+ configFile.print("C11:v=");
+ configFile.println(config->wifiEnabled ? "1" : "0");
+
+ // C12 - WiFi mode
+ configFile.print("C12:v=");
+ configFile.println(config->accessMode);
+
+ // C13 - WiFi SSID
+ configFile.print("C13:");
+ configFile.println(config->apSSID);
+
+ // C14 - WiFi password
+ configFile.print("C14:");
+ configFile.println(config->apPassword);
+
+ // C15 - WiFi port
+ configFile.print("C15:v=");
+ configFile.println(config->wifiPort);
+
+ // C17 - WiFi AP IP
+ configFile.print("C17:");
+ configFile.println(config->apIpAddress);
+#endif
+
+ // C18 - Default relay states
+ for (uint8_t i = 0; i < ConfigRelayCount; i++)
+ {
+ configFile.print("C18:");
+ configFile.print(i);
+ configFile.print("=");
+ configFile.println(config->defaulRelayState[i] ? "1" : "0");
+ }
+
+ // C19 - Linked relays
+ for (uint8_t i = 0; i < ConfigMaxLinkedRelays; i++)
+ {
+ configFile.print("C19:");
+ configFile.print(config->linkedRelays[i][0]);
+ configFile.print("=");
+ configFile.println(config->linkedRelays[i][1]);
+ }
+
+ // C20 - Timezone offset
+ configFile.print("C20:v=");
+ configFile.println(config->timezoneOffset);
+
+ // C21 - MMSI
+ configFile.print("C21:");
+ configFile.println(config->mMSI);
+
+ // C22 - Call sign
+ configFile.print("C22:");
+ configFile.println(config->callSign);
+
+ // C23 - Home port
+ configFile.print("C23:");
+ configFile.println(config->homePort);
+
+ // C24 - LED colors
+ configFile.print("C24:t=0;c=0;r=");
+ configFile.print(config->ledConfig.dayGoodColor[0]);
+ configFile.print(";g=");
+ configFile.print(config->ledConfig.dayGoodColor[1]);
+ configFile.print(";b=");
+ configFile.println(config->ledConfig.dayGoodColor[2]);
+
+ configFile.print("C24:t=0;c=1;r=");
+ configFile.print(config->ledConfig.dayBadColor[0]);
+ configFile.print(";g=");
+ configFile.print(config->ledConfig.dayBadColor[1]);
+ configFile.print(";b=");
+ configFile.println(config->ledConfig.dayBadColor[2]);
+
+ configFile.print("C24:t=1;c=0;r=");
+ configFile.print(config->ledConfig.nightGoodColor[0]);
+ configFile.print(";g=");
+ configFile.print(config->ledConfig.nightGoodColor[1]);
+ configFile.print(";b=");
+ configFile.println(config->ledConfig.nightGoodColor[2]);
+
+ configFile.print("C24:t=1;c=1;r=");
+ configFile.print(config->ledConfig.nightBadColor[0]);
+ configFile.print(";g=");
+ configFile.print(config->ledConfig.nightBadColor[1]);
+ configFile.print(";b=");
+ configFile.println(config->ledConfig.nightBadColor[2]);
+
+ // C25 - LED brightness
+ configFile.print("C25:t=0;b=");
+ configFile.println(config->ledConfig.dayBrightness);
+
+ configFile.print("C25:t=1;b=");
+ configFile.println(config->ledConfig.nightBrightness);
+
+ // C26 - LED auto switch
+ configFile.print("C26:v=");
+ configFile.println(config->ledConfig.autoSwitch ? "1" : "0");
+
+ // C27 - LED enable states
+ configFile.print("C27:g=");
+ configFile.print(config->ledConfig.gpsEnabled ? "1" : "0");
+ configFile.print(";w=");
+ configFile.print(config->ledConfig.warningEnabled ? "1" : "0");
+ configFile.print(";s=");
+ configFile.println(config->ledConfig.systemEnabled ? "1" : "0");
+
+ // C28 - Control panel tones
+ configFile.print("C28:t=0;h=");
+ configFile.print(config->soundConfig.good_toneHz);
+ configFile.print(";d=");
+ configFile.print(config->soundConfig.good_durationMs);
+ configFile.print(";p=");
+ configFile.print(config->soundConfig.goodPreset);
+ configFile.println(";r=0");
+
+ configFile.print("C28:t=1;h=");
+ configFile.print(config->soundConfig.bad_toneHz);
+ configFile.print(";d=");
+ configFile.print(config->soundConfig.bad_durationMs);
+ configFile.print(";p=");
+ configFile.print(config->soundConfig.badPreset);
+ configFile.print(";r=");
+ configFile.println(config->soundConfig.bad_repeatMs);
+
+ configFile.close();
+
+ logInfo("Config exported to SD card");
+ return true;
+}
diff --git a/SmartFuseBox/SmartFuseBox/SdCardConfigLoader.h b/SmartFuseBox/SmartFuseBox/SdCardConfigLoader.h
new file mode 100644
index 0000000..f7abe5c
--- /dev/null
+++ b/SmartFuseBox/SmartFuseBox/SdCardConfigLoader.h
@@ -0,0 +1,136 @@
+#pragma once
+
+#include
+#include
+#include
+#include "ConfigController.h"
+#include "ConfigSyncManager.h"
+
+constexpr char SD_CONFIG_FILENAME[] = "config.txt";
+constexpr uint16_t SD_CONFIG_MAX_LINE_LENGTH = 128;
+
+/**
+ * @class SdCardConfigLoader
+ * @brief Loads configuration from SD card and applies to ConfigManager
+ *
+ * Boot sequence:
+ * 1. Check for SD card with config.txt
+ * 2. Parse and validate all commands
+ * 3. Compare with current EEPROM config
+ * 4. If different, apply changes and save to EEPROM
+ * 5. Send config via LINK to sync control panel
+ *
+ * Features:
+ * - Read-only SD card config (not auto-updated during runtime)
+ * - Command format validation
+ * - Error logging to Serial
+ * - Integration with ConfigSyncManager (disable if SD config loaded)
+ * - C29: Reload config from SD card
+ * - C30: Export current config to SD card
+ *
+ * Usage:
+ * @code
+ * SdCardConfigLoader loader(&commandMgrComputer, &commandMgrLink,
+ * &configController, &configSyncManager, csPin);
+ *
+ * void setup() {
+ * bool sdConfigLoaded = loader.loadConfigFromSd();
+ * if (sdConfigLoaded) {
+ * // SD config was applied, ConfigSyncManager will be disabled
+ * }
+ * }
+ * @endcode
+ */
+class SdCardConfigLoader
+{
+private:
+ SerialCommandManager* _computerSerial;
+ SerialCommandManager* _linkSerial;
+ ConfigController* _configController;
+ ConfigSyncManager* _configSyncManager;
+ uint8_t _csPin;
+ bool _sdConfigPresent;
+
+ // SD card
+ SdFat _sd;
+
+ /**
+ * @brief Check if SD card is accessible
+ * @return true if SD card is present and readable
+ */
+ bool checkSdCard();
+
+ /**
+ * @brief Check if config.txt exists on SD card
+ * @return true if config file exists
+ */
+ bool configFileExists();
+
+ /**
+ * @brief Parse and apply a single config command line
+ * @param line Command line to parse
+ * @return true if command was successfully applied
+ */
+ bool applyConfigCommand(const char* line);
+
+ /**
+ * @brief Send config to LINK serial to sync control panel
+ */
+ void syncConfigToLink();
+
+ /**
+ * @brief Log error message to serial
+ * @param message Error message
+ * @param line Optional line content that caused error
+ */
+ void logError(const char* message, const char* line = nullptr);
+
+ /**
+ * @brief Log info message to serial
+ * @param message Info message
+ */
+ void logInfo(const char* message);
+
+public:
+ /**
+ * @brief Constructor
+ * @param computerSerial Serial manager for computer communication
+ * @param linkSerial Serial manager for LINK communication
+ * @param configController Configuration controller
+ * @param configSyncManager Configuration sync manager (will be disabled if SD config loaded)
+ * @param csPin Chip select pin for SD card
+ */
+ SdCardConfigLoader(SerialCommandManager* computerSerial,
+ SerialCommandManager* linkSerial,
+ ConfigController* configController,
+ ConfigSyncManager* configSyncManager,
+ uint8_t csPin);
+
+ /**
+ * @brief Load configuration from SD card if present
+ *
+ * Reads config.txt, applies all commands, saves to EEPROM if changed,
+ * and syncs to control panel via LINK.
+ *
+ * @return true if SD config was found and applied
+ */
+ bool loadConfigFromSd();
+
+ /**
+ * @brief Reload configuration from SD card (C29 command)
+ * @return true if config was reloaded successfully
+ */
+ bool reloadConfigFromSd();
+
+ /**
+ * @brief Export current configuration to SD card (C30 command)
+ * @return true if config was exported successfully
+ */
+ bool exportConfigToSd();
+
+ /**
+ * @brief Check if SD config was loaded at boot
+ * @return true if SD config is present and was loaded
+ */
+ bool isSdConfigPresent() const { return _sdConfigPresent; }
+};
diff --git a/SmartFuseBox/SmartFuseBoxConstants.h b/SmartFuseBox/SmartFuseBoxConstants.h
index 9b26e2a..6a0053f 100644
--- a/SmartFuseBox/SmartFuseBoxConstants.h
+++ b/SmartFuseBox/SmartFuseBoxConstants.h
@@ -8,9 +8,14 @@ constexpr unsigned long serialInitTimeoutMs = 300;
constexpr uint8_t WaterSensorPin = A0;
+constexpr uint8_t LightSensorAnalogPin = A1;
constexpr uint8_t LightSensorPin = D3;
constexpr uint8_t WaterSensorActivePin = D8;
constexpr uint8_t Dht11SensorPin = D9;
+constexpr uint8_t SdCardCsPin = D10;
+constexpr uint8_t SdCardMosiPin = D11;
+constexpr uint8_t SdCardMisoPin = D12;
+constexpr uint8_t SdCardSckPin = D13;
// Digital pins for relays
diff --git a/SmartFuseBox/SystemNetworkHandler.cpp b/SmartFuseBox/SystemNetworkHandler.cpp
index f59ce86..4b37930 100644
--- a/SmartFuseBox/SystemNetworkHandler.cpp
+++ b/SmartFuseBox/SystemNetworkHandler.cpp
@@ -5,7 +5,8 @@
#include "SystemFunctions.h"
SystemNetworkHandler::SystemNetworkHandler(WifiController* wifiController)
- : _wifiController(wifiController)
+ : _wifiController(wifiController),
+ _sdCardLogger(nullptr)
{
}
@@ -45,7 +46,6 @@ void SystemNetworkHandler::formatStatusJson(char* buffer, size_t size)
wifiEnabled = config->wifiEnabled;
}
- // Get runtime WiFi status if available
if (_wifiController && wifiEnabled && _wifiController->isEnabled())
{
rssi = _wifiController->getServer()->getSignalStrength();
@@ -54,15 +54,26 @@ void SystemNetworkHandler::formatStatusJson(char* buffer, size_t size)
char dateTimeStr[DateTimeBufferLength];
DateTimeManager::formatDateTime(dateTimeStr, sizeof(dateTimeStr));
- // Enhanced JSON formatting with WiFi runtime details
+ bool sdPresent = false;
+ uint32_t logSize = 0;
+
+ if (_sdCardLogger)
+ {
+ sdPresent = _sdCardLogger->isSdCardPresent();
+ logSize = _sdCardLogger->getCurrentLogFileSize();
+ }
+
snprintf(buffer, size,
- "\"system\":{\"mem\":%d,\"cpu\":%d,\"bluetooth\":%d,\"wifi\":%d,\"rssi\":%d,\"time\":\"%s\"}",
+ "\"system\":{\"mem\":%d,\"cpu\":%d,\"bluetooth\":%d,\"wifi\":%d,\"rssi\":%d,\"time\":\"%s\","
+ "\"sd\":{\"present\":%d,\"log\":%lu}}",
SystemFunctions::freeMemory(),
SystemCpuMonitor::getCpuUsage(),
bluetoothEnabled,
wifiEnabled,
rssi,
- dateTimeStr);
+ dateTimeStr,
+ sdPresent,
+ (unsigned long)logSize);
}
void SystemNetworkHandler::formatWifiStatusJson(WiFiClient* client)
diff --git a/SmartFuseBox/SystemNetworkHandler.h b/SmartFuseBox/SystemNetworkHandler.h
index c056fa4..0b9099c 100644
--- a/SmartFuseBox/SystemNetworkHandler.h
+++ b/SmartFuseBox/SystemNetworkHandler.h
@@ -3,16 +3,23 @@
#include "INetworkCommandHandler.h"
#include "SystemDefinitions.h"
#include "WifiController.h"
+#include "SdCardLogger.h"
class SystemNetworkHandler : public INetworkCommandHandler
{
private:
WifiController* _wifiController;
+ SdCardLogger* _sdCardLogger;
public:
explicit SystemNetworkHandler(WifiController* wifiController);
+ void setSdCardLogger(SdCardLogger* sdCardLogger)
+ {
+ _sdCardLogger = sdCardLogger;
+ }
+
const char* getRoute() const override { return "/api/system"; }
void formatWifiStatusJson(WiFiClient* client) override;
diff --git a/SmartFuseBox/__vm/Compile.vmps.xml b/SmartFuseBox/__vm/Compile.vmps.xml
deleted file mode 100644
index 1a81d15..0000000
--- a/SmartFuseBox/__vm/Compile.vmps.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/SmartFuseBox/__vm/Upload.vmps.xml b/SmartFuseBox/__vm/Upload.vmps.xml
deleted file mode 100644
index 1a81d15..0000000
--- a/SmartFuseBox/__vm/Upload.vmps.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file