diff --git a/.github/workflows/release-workflow.yaml b/.github/workflows/release-workflow.yaml index 45214ad..33fe06e 100644 --- a/.github/workflows/release-workflow.yaml +++ b/.github/workflows/release-workflow.yaml @@ -99,12 +99,37 @@ jobs: commit: ${{ steps.commit.outputs.updated-version-sha || github.sha }} token: ${{ steps.app-token.outputs.token }} - upload: - name: Upload to Particle + upload-to-particle: + name: Upload to Particle Projects needs: release runs-on: ubuntu-latest # Only run if release job has completed and the firmware version was updated if: needs.release.outputs.firmware-version-updated == 'true' + strategy: + matrix: + project: + - name: "GEMS Demo" + product_id_secret: "PARTICLE_GEMS_DEMO_PRODUCT_ID" + - name: "Runk Lab (B SoM)" + product_id_secret: "PARTICLE_RUNCK_LAB_BSOM_PRODUCT_ID" + - name: "WinterTurf - v3 International" + product_id_secret: "PARTICLE_WINTERTURF_INTERNATIONAL_PRODUCT_ID" + - name: "WinterTurf - v3" + product_id_secret: "PARTICLE_WINTERTURF_PRODUCT_ID" + - name: "Plant Pathways" + product_id_secret: "PARTICLE_PLANT_PATHWAYS_PRODUCT_ID" + - name: "LCCMR Irrigation" + product_id_secret: "PARTICLE_LCCMR_IRRIGATION_PRODUCT_ID" + - name: "Roadside Turf" + product_id_secret: "PARTICLE_ROADSIDE_TURF_PRODUCT_ID" + - name: "PepsiCo" + product_id_secret: "PARTICLE_PEPSICO_PRODUCT_ID" + - name: "Stellenbosch" + product_id_secret: "PARTICLE_STELLENBOSCH_PRODUCT_ID" + - name: "Runk Lab (B5 SoM)" + product_id_secret: "PARTICLE_RUNCK_LAB_B5SOM_PRODUCT_ID" + - name: "LCCMR Irrigation Sensing" + product_id_secret: "PARTICLE_LCCMR_IRRIGATION_SENSING_PRODUCT_ID" steps: - name: Checkout code uses: actions/checkout@v4 @@ -121,12 +146,16 @@ jobs: FIRMWARE=$(find ./release -name "*.bin" -type f | head -n 1) echo "firmware-path=$FIRMWARE" >> $GITHUB_OUTPUT - - name: Upload product firmware to Particle + - name: Upload firmware to ${{ matrix.project.name }} uses: particle-iot/firmware-upload-action@v1 with: particle-access-token: ${{ secrets.PARTICLE_ACCESS_TOKEN }} firmware-path: ${{ steps.find_binary.outputs.firmware-path }} firmware-version: ${{ needs.release.outputs.firmware-version }} - product-id: ${{ secrets.PARTICLE_GEMS_DEMO_PRODUCT_ID }} + product-id: ${{ secrets[matrix.project.product_id_secret] }} title: 'Firmware v${{ needs.release.outputs.firmware-version }}' - description: '[Firmware v${{ needs.release.outputs.firmware-version }} GitHub Release](${{ needs.release.outputs.release-url }}' \ No newline at end of file + description: '[Firmware v${{ needs.release.outputs.firmware-version }} GitHub Release](${{ needs.release.outputs.release-url }})' + + - name: Log upload success + run: | + echo "✅ Successfully uploaded firmware v${{ needs.release.outputs.firmware-version }} to ${{ matrix.project.name }}" \ No newline at end of file diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..58c5cf6 --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,292 @@ +### Configuration Management + +#### Loading Configuration + +Configuration is loaded at startup in the following priority order: + +1. **SD Card**: `config.json` file on the SD card +2. **Default**: Built-in default configuration if SD file not found + +#### Updating Configuration + +Configuration can be updated through: + +1. **Cloud Function**: `updateConfig` Particle function + - copy the intended config.json and paste it as and argument into updateConfig + - If successful, the device will restart without returning any value. + - verify that the first metadata packet after reset matches your configuration + - See below for other types of responses. +3. **SD Card**: Replace `config.json` file and restart system + +#### updateConfig Function Details + +The `updateConfig` Particle cloud function accepts a JSON configuration string and applies it to the system. The function provides detailed feedback about the configuration update process. + +##### Usage + +```bash +# Using Particle CLI +particle call updateConfig '{"config":{"system":{"logPeriod":600}}}' + +# Using Particle Console +# Paste the JSON configuration directly into the function argument field + +# Remove configuration (revert to defaults) +particle call updateConfig 'remove' +``` + +##### Response Types + +The `updateConfig` function returns different responses based on the outcome: + +**Success Responses:** +- **Return 1**: Configuration updated successfully, device will restart automatically +- **Return 0**: Configuration removed successfully (when using "remove" command) +- Verify success by checking the first metadata packet after reset matches your configuration +- New configuration UIDs will be available via `getSystemConfig` and `getSensorConfig` + +**Error Responses:** +The function returns specific error codes when configuration updates fail: + +| Error Code | Description | Troubleshooting | +|------------|-------------|-----------------| +| `0` | Success - Configuration removed from SD card | | +| `1` | Success - Configuration updated, system restarting | | +| `-1` | Failed to remove configuration from SD card | | +| `-2` | Invalid configuration format - Missing 'config' element | | +| `-3` | Invalid configuration format - Missing 'system' element | | +| `-4` | Invalid configuration format - Missing 'sensors' element | | +| `-5` | Failed to write test file to SD card | | +| `-6` | Failed to remove current configuration from SD card | | +| `-7` | Failed to write new configuration to SD card | | + +##### Configuration Validation Rules + +The system performs several validation checks: + +1. **JSON Structure Validation** + - Must contain "config" root element (checked by string search) + - Must contain "system" section within config (checked by string search) + - Must contain "sensors" section within config (checked by string search) + - All whitespace, newlines, carriage returns, and tabs are automatically stripped + +2. **SD Card Validation** + - SD card must be accessible for writing + - System tests write capability before attempting configuration update + - Current configuration must be removable before writing new configuration + +3. **Special Commands** + - Use "remove" as the configuration string to delete config.json and revert to defaults + +##### Example Error Scenarios + +###### Missing Configuration Elements +```bash +particle call device_name updateConfig '{"system":{"logPeriod":300}}' # Missing "config" wrapper +# Returns: -2 + +particle call device_name updateConfig '{"config":{"sensors":{"numSoil":3}}}' # Missing "system" section +# Returns: -3 + +particle call device_name updateConfig '{"config":{"system":{"logPeriod":300}}}' # Missing "sensors" section +# Returns: -4 +``` + +###### SD Card Issues +```bash +# If SD card is not available or full +particle call device_name updateConfig '{"config":{"system":{"logPeriod":300},"sensors":{}}}' +# May return: -5, -6, or -7 depending on the specific SD card failure point +``` + +###### Configuration Removal +```bash +particle call device_name updateConfig 'remove' +# Returns: 0 (success) or -1 (failed to remove) +``` + +##### Best Practices + +1. **Validate JSON First**: Use a JSON validator before sending to the device +2. **Check Parameter Ranges**: Verify all values are within acceptable ranges +3. **Plan Hardware Requirements**: Ensure sufficient Talons for sensor configuration +4. **Monitor Device Status**: Watch for restart after successful configuration +5. **Verify Configuration**: Check UIDs after restart to confirm changes applied + +#### Configuration UIDs + +The system generates unique identifiers for configuration tracking: + +- **System Configuration UID**: Changes when system parameters are modified +- **Sensor Configuration UID**: Changes when sensor counts are modified + +These UIDs can be retrieved via cloud functions: +- `getSystemConfig`: Returns system configuration UID +- `getSensorConfig`: Returns sensor configuration UID + +##### UID Encoding Format + +The Configuration UIDs are encoded as 32-bit integers using bit-packing to efficiently store multiple configuration parameters in a single value. + +###### System Configuration UID Encoding + +The System Configuration UID is constructed using the following bit layout: + +``` +Bits: 31-16 15-12 11-10 9-8 7-6 5-4 3-2 1-0 +Field: logPeriod backhaul powerSave logMode numAux numI2C numSDI12 reserved +``` + +| Field | Bits | Description | Range | +|-------|------|-------------|-------| +| `logPeriod` | 31-16 | Logging period in seconds | 0-65535 | +| `backhaulCount` | 15-12 | Number of logs before backhaul | 0-15 | +| `powerSaveMode` | 11-10 | Power management mode | 0-3 | +| `loggingMode` | 9-8 | Logging behavior mode | 0-3 | +| `numAuxTalons` | 7-6 | Number of Auxiliary Talons | 0-3 | +| `numI2CTalons` | 5-4 | Number of I2C Talons | 0-3 | +| `numSDI12Talons` | 3-2 | Number of SDI-12 Talons | 0-3 | +| Reserved | 1-0 | Reserved for future use | 0-3 | + +**Encoding Formula:** +```cpp +int systemUID = (logPeriod << 16) | + (backhaulCount << 12) | + (powerSaveMode << 10) | + (loggingMode << 8) | + (numAuxTalons << 6) | + (numI2CTalons << 4) | + (numSDI12Talons << 2); +``` + +###### Sensor Configuration UID Encoding + +The Sensor Configuration UID uses the following bit layout: + +``` +Bits: 31-28 27-24 23-20 19-16 15-12 11-8 7-4 3-0 +Field: numET numHaar numSoil numApogee numCO2 numO2 numPress reserved +``` + +| Field | Bits | Description | Range | +|-------|------|-------------|-------| +| `numET` | 31-28 | Number of ET sensors (LI-710) | 0-15 | +| `numHaar` | 27-24 | Number of Haar atmospheric sensors | 0-15 | +| `numSoil` | 23-20 | Number of soil sensors (TDR315H) | 0-15 | +| `numApogeeSolar` | 19-16 | Number of Apogee solar sensors | 0-15 | +| `numCO2` | 15-12 | Number of CO2 sensors (Hedorah) | 0-15 | +| `numO2` | 11-8 | Number of O2 sensors (SO421) | 0-15 | +| `numPressure` | 7-4 | Number of pressure sensors | 0-15 | +| Reserved | 3-0 | Reserved for future use | 0-15 | + +**Encoding Formula:** +```cpp +int sensorUID = (numET << 28) | + (numHaar << 24) | + (numSoil << 20) | + (numApogeeSolar << 16) | + (numCO2 << 12) | + (numO2 << 8) | + (numPressure << 4); +``` + +##### UID Decoding Examples + +###### Decoding System Configuration UID + +To extract individual values from a System Configuration UID: + +```cpp +// Example UID: 1234567890 (decimal) = 0x499602D2 (hex) +int systemUID = 1234567890; + +// Extract each field +int logPeriod = (systemUID >> 16) & 0xFFFF; // Bits 31-16 +int backhaulCount = (systemUID >> 12) & 0xF; // Bits 15-12 +int powerSaveMode = (systemUID >> 10) & 0x3; // Bits 11-10 +int loggingMode = (systemUID >> 8) & 0x3; // Bits 9-8 +int numAuxTalons = (systemUID >> 6) & 0x3; // Bits 7-6 +int numI2CTalons = (systemUID >> 4) & 0x3; // Bits 5-4 +int numSDI12Talons = (systemUID >> 2) & 0x3; // Bits 3-2 +``` + +###### Decoding Sensor Configuration UID + +```cpp +// Example UID: 305419896 (decimal) = 0x12345678 (hex) +int sensorUID = 305419896; + +// Extract each field +int numET = (sensorUID >> 28) & 0xF; // Bits 31-28 +int numHaar = (sensorUID >> 24) & 0xF; // Bits 27-24 +int numSoil = (sensorUID >> 20) & 0xF; // Bits 23-20 +int numApogeeSolar = (sensorUID >> 16) & 0xF; // Bits 19-16 +int numCO2 = (sensorUID >> 12) & 0xF; // Bits 15-12 +int numO2 = (sensorUID >> 8) & 0xF; // Bits 11-8 +int numPressure = (sensorUID >> 4) & 0xF; // Bits 7-4 +``` + +A tool has been developed to help parse this UID and make sense of it, found [in RTGS_Lab gems_sensing_db_tools](https://github.com/RTGS-Lab/gems_sensing_db_tools) + +##### Practical Examples + +###### Example 1: Default Configuration +```json +{ + "system": { + "logPeriod": 300, + "backhaulCount": 4, + "powerSaveMode": 1, + "loggingMode": 0, + "numAuxTalons": 1, + "numI2CTalons": 1, + "numSDI12Talons": 1 + } +} +``` + +**System UID Calculation:** +- logPeriod (300) << 16 = 19660800 +- backhaulCount (4) << 12 = 16384 +- powerSaveMode (1) << 10 = 1024 +- loggingMode (0) << 8 = 0 +- numAuxTalons (1) << 6 = 64 +- numI2CTalons (1) << 4 = 16 +- numSDI12Talons (1) << 2 = 4 + +**System UID = 19678292** (decimal) or **0x12C4154** (hex) + +###### Example 2: Sensor Configuration +```json +{ + "sensors": { + "numET": 1, + "numHaar": 2, + "numSoil": 3, + "numApogeeSolar": 1, + "numCO2": 1, + "numO2": 1, + "numPressure": 1 + } +} +``` + +**Sensor UID Calculation:** +- numET (1) << 28 = 268435456 +- numHaar (2) << 24 = 33554432 +- numSoil (3) << 20 = 3145728 +- numApogeeSolar (1) << 16 = 65536 +- numCO2 (1) << 12 = 4096 +- numO2 (1) << 8 = 256 +- numPressure (1) << 4 = 16 + +**Sensor UID = 305205520** (decimal) or **0x12311110** (hex) + +##### UID Usage + +Configuration UIDs are used for: + +1. **Change Detection**: Compare current UID with stored UID to detect configuration changes +2. **Remote Monitoring**: Cloud functions return UIDs for remote configuration verification +3. **Debugging**: Quick identification of active configuration without full JSON parsing +4. **Optimization**: Fast configuration comparison without string operations \ No newline at end of file diff --git a/README.md b/README.md index 4fca4f3..5063adc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,255 @@ -# FlightControl-Demo -Demo for testing drivers for the Flight system +# FlightControl + +Frirmware repository for the Flight data logging system. + +## Overview + +This repository serves as a production and development environment for the Flight data logging system, featuring: + +- **Modular Sensor Architecture**: Dynamic sensor configuration and management +- **Hardware Abstraction**: Platform and hardware dependency injection for testability +- **Configuration Management**: JSON-based system and sensor configuration +- **Comprehensive Testing**: Unit tests with Google Test and Google Mock +- **Multiple Platform Support**: Abstracted platform dependencies for Particle devices + +## Key Features + +- Plug and play sensors +- Support for multiple different protocol configurations including Analog, I2C and SDI12 + +### Configuration Management + +The system supports dynamic configuration through JSON files stored on SD card or applied via cloud functions. + +#### Configuration Structure + +This is the default configuration if there is no config.json file on the SD card. + +```json +{ + "config": { + "system": { + "logPeriod": 300, + "backhaulCount": 4, + "powerSaveMode": 1, + "loggingMode": 0, + "numAuxTalons": 1, + "numI2CTalons": 1, + "numSDI12Talons": 1 + }, + "sensors": { + "numET": 0, + "numHaar": 0, + "numSoil": 3, + "numApogeeSolar": 0, + "numCO2": 0, + "numO2": 0, + "numPressure": 0 + } + } +} +``` + +#### System Configuration Parameters + +| Parameter | Description | Default | Range | +|-----------|-------------|---------|-------| +| `logPeriod` | Logging interval in seconds | 300 | 0-65535 | +| `backhaulCount` | Number of logs before cellular backhaul | 4 | 1-15 | +| `powerSaveMode` | Power management mode | 1 | 0-3 | +| `loggingMode` | Logging behavior mode | 0 | 0-3 | +| `numAuxTalons` | Number of Auxiliary Talons | 1 | 0-3 | +| `numI2CTalons` | Number of I2C Talons | 1 | 0-3 | +| `numSDI12Talons` | Number of SDI-12 Talons | 1 | 0-3 | + +#### Sensor Configuration Parameters + +| Parameter | Description | Default | Range | +|-----------|-------------|---------|-------| +| `numET` | Number of Evapotranspiration sensors (LI-710) | 0 | 0-15 | +| `numHaar` | Number of Haar atmospheric sensors | 0 | 0-15 | +| `numSoil` | Number of soil sensors (TDR315H) | 3 | 0-15 | +| `numApogeeSolar` | Number of Apogee solar radiation sensors | 0 | 0-15 | +| `numCO2` | Number of CO2 sensors (Hedorah) | 0 | 0-15 | +| `numO2` | Number of O2 sensors (SO421) | 0 | 0-15 | +| `numPressure` | Number of pressure sensors (BaroVue10) | 0 | 0-15 | + +#### Power Save Modes + +- **0 - Performance**: No power saving, maximum sensor responsiveness +- **1 - Balanced**: Moderate power saving with good performance +- **2 - Low Power**: Aggressive power saving, longer sensor warm-up times +- **3 - Ultra Low Power**: Maximum power saving, minimal sensor operation + +#### Logging Modes + +- **0 - Standard**: Regular diagnostic intervals with full data logging +- **1 - Performance**: Maximum data throughput, minimal diagnostics +- **2 - Balanced**: Hourly diagnostics with standard data logging +- **3 - No Local**: Cloud-only logging, no SD card storage + +## Hardware Architecture + +### Core Components + +- **Kestrel Logger**: Main data logging board with GPS, accelerometer, RTC, and cellular connectivity +- **Gonk Battery**: Smart battery management system +- **Talons**: Expansion boards for sensor connectivity + - **Auxiliary Talon**: General-purpose analog/digital sensor interface + - **I2C Talon**: I2C sensor interface with power management + - **SDI-12 Talon**: SDI-12 protocol sensor interface + +### Supported Sensors + +#### Environmental Sensors +- **LI-710 (ET)**: Evapotranspiration measurements +- **Haar**: Atmospheric temperature, humidity, pressure +- **TDR315H**: Soil moisture and temperature +- **BaroVue10**: Barometric pressure +- **SO421**: Oxygen concentration +- **SP421**: Solar radiation + +#### Gas Sensors +- **Hedorah**: CO2 concentration with SCD30 sensor +- **T9602**: Humidity and temperature + +## Development Setup + +### Prerequisites + +- CMake 3.14 or higher +- C++17 compatible compiler +- Git with submodule support + +### Building Tests + +```bash +# Clone repository with submodules +git clone --recursive https://github.com/RTGS-Lab/Firmware_-_FlightControl.git +cd Firmware_-_FlightControl + +# Create build directory +mkdir build && cd build + +# Configure and build tests +cmake .. +make + +# Run tests +./test/unit_tests +``` + +### Project Structure + +``` +FlightControl-Demo/ +├── src/ # Source code +│ ├── configuration/ # Configuration management +│ ├── hardware/ # Hardware interface implementations +│ └── platform/ # Platform abstraction implementations +├── test/ # Unit tests +│ ├── mocks/ # Mock implementations +│ └── unit/ # Unit test files +├── lib/ # External libraries (git submodules) +└── docs/ # Documentation +``` + +## Testing Framework + +### Unit Testing + +The project uses Google Test and Google Mock for unit testing: + +- **Platform Abstraction Testing**: Mock implementations for all platform dependencies +- **Hardware Interface Testing**: Mock hardware components for isolated testing +- **Configuration Testing**: Validation of configuration parsing and management +- **Sensor Management Testing**: Dynamic sensor initialization and management + +### Mock Architecture + +Mock implementations are provided for: + +- **Platform Dependencies**: TimeProvider, GPIO, System, Wire, Cloud, Serial +- **Hardware Components**: IO Expanders, Current Sensors, RTC, GPS, Accelerometer +- **Sensor Interfaces**: SDI-12, I2C communication protocols + +### Running Tests + +```bash +# Run all tests +./test/unit_tests + +# Run specific test suites +./test/unit_tests --gtest_filter="ConfigurationManagerTest.*" +./test/unit_tests --gtest_filter="KestrelTest.*" +./test/unit_tests --gtest_filter="SensorManagerTest.*" +``` + +## Configuration Examples + +### Full Environmental Station + +```json +{ + "config": { + "system": { + "logPeriod": 300, + "backhaulCount": 8, + "powerSaveMode": 1, + "loggingMode": 0, + "numAuxTalons": 1, + "numI2CTalons": 1, + "numSDI12Talons": 1 + }, + "sensors": { + "numET": 1, + "numHaar": 1, + "numSoil": 2, + "numApogeeSolar": 0, + "numCO2": 0, + "numO2": 0, + "numPressure": 0 + } + } +} +``` + +## Cloud Functions + +The system exposes several Particle cloud functions: + +- `updateConfig`: Update system configuration +- `getSystemConfig`: Get system configuration UID +- `getSensorConfig`: Get sensor configuration UID +- `nodeID`: Set custom node identifier +- `findSensors`: Trigger sensor auto-detection +- `findTalons`: Trigger Talon auto-detection +- `systemRestart`: Restart the system +- `takeSample`: Force immediate data collection +- `commandExe`: Execute system commands + +## Schema and Error Codes + +- **Data Schema**: SEE SCHEMA.md +- **Error Codes**: SEE ERRORCODES.md +- **Command Execution**: SEE COMMANDEXE.md +- **System and Sensor Configuration**: SEE CONFIGURATION.md + +## Contributing + +1. Fork the repository +2. Create a feature branch (feature/name-of-branch) +3. Add unit tests for new functionality +4. Ensure all tests pass +5. Submit a pull request + +## Related Repositories + +This project depends on several driver libraries available as git submodules: + +- Driver libraries for individual sensors +- Hardware abstraction libraries +- Platform dependency interfaces +- Communication protocol implementations + +For a complete list, see [.gitmodules](.gitmodules). \ No newline at end of file diff --git a/lib/Driver_-_Kestrel-FileHandler b/lib/Driver_-_Kestrel-FileHandler index a284d03..1b5db1a 160000 --- a/lib/Driver_-_Kestrel-FileHandler +++ b/lib/Driver_-_Kestrel-FileHandler @@ -1 +1 @@ -Subproject commit a284d0348dbde5c974b2f826df0a3582381aca67 +Subproject commit 1b5db1ad66baeced2e2c0356a2a5e72ee600478a diff --git a/src/FlightControl_Demo.cpp b/src/FlightControl.cpp similarity index 79% rename from src/FlightControl_Demo.cpp rename to src/FlightControl.cpp index 00cd96c..9bbf512 100644 --- a/src/FlightControl_Demo.cpp +++ b/src/FlightControl.cpp @@ -33,6 +33,12 @@ int takeSample(String dummy); int commandExe(String command); int systemRestart(String resetType); int configurePowerSave(int desiredPowerSaveMode); +int updateConfiguration(String configJson); +int getSystemConfiguration(String dummy); +int getSensorConfiguration(String dummy); +bool loadConfiguration(); +void updateSensorVectors(); +void initializeSensorSystem(); #define WAIT_GPS false #define USE_CELL //System attempts to connect to cell @@ -77,6 +83,11 @@ int configurePowerSave(int desiredPowerSaveMode); #include "hardware/AccelerometerMXC6655.h" #include "hardware/AccelerometerBMA456.h" +#include "configuration/ConfigurationManager.h" +#include "configuration/SensorManager.h" + +int getIndexOfPort(int port); + const String firmwareVersion = "2.9.11"; const String schemaVersion = "2.2.9"; @@ -104,6 +115,8 @@ GpsSFE_UBLOX_GNSS realGps; HumidityTemperatureAdafruit_SHT4X realTempHumidity; AccelerometerMXC6655 realAccel; AccelerometerBMA456 realBackupAccel; +IOExpanderPCAL9535A ioAlpha(0x20); +IOExpanderPCAL9535A ioBeta(0x21); Kestrel logger(realTimeProvider, realGpio, @@ -126,24 +139,6 @@ Kestrel logger(realTimeProvider, true); KestrelFileHandler fileSys(logger); Gonk battery(5); //Instantiate with defaults, manually set to port 5 -AuxTalon aux(0, 0x14); //Instantiate AUX talon with deaults - null port and hardware v1.4 -I2CTalon i2c(0, 0x21); //Instantiate I2C talon with alt - null port and hardware v2.1 -SDI12Talon sdi12(0, 0x14); //Instantiate SDI12 talon with alt - null port and hardware v1.4 -SDI12TalonAdapter realSdi12(sdi12); -IOExpanderPCAL9535A ioAlpha(0x20); -IOExpanderPCAL9535A ioBeta(0x21); - -String globalNodeID = ""; //Store current node ID - -const uint8_t numTalons = 3; //Number must match the number of objects defined in `talonsToTest` array - -Talon* talons[Kestrel::numTalonPorts]; //Create an array of the total possible length -Talon* talonsToTest[numTalons] = { - &aux, - &i2c, - &sdi12 -}; - namespace LogModes { constexpr uint8_t STANDARD = 0; constexpr uint8_t PERFORMANCE = 1; @@ -151,50 +146,24 @@ namespace LogModes { constexpr uint8_t NO_LOCAL = 3; //Same as standard log, but no attempt to log to SD card }; -/////////////////////////// BEGIN USER CONFIG //////////////////////// -//PRODUCT_ID(18596) //Configured based on the target product, comment out if device has no product -PRODUCT_VERSION(34) //Configure based on the firmware version you wish to create, check product firmware page to see what is currently the highest number - -const int backhaulCount = 4; //Number of log events before backhaul is performed -const unsigned long logPeriod = 300; //Number of seconds to wait between logging events -int desiredPowerSaveMode = PowerSaveModes::LOW_POWER; //Specify the power save mode you wish to use: PERFORMANCE, BALANCED, LOW_POWER, ULTRA_LOW_POWER -int loggingMode = LogModes::STANDARD; //Specify the type of logging mode you wish to use: STANDARD, PERFORMANCE, BALANCED, NO_LOCAL - -Haar haar(0, 0, 0x20); //Instantiate Haar sensor with default ports and version v2.0 -// Haar haar1(0, 0, 0x20); //Instantiate Haar sensor with default ports and version v2.0 -// Haar haar2(0, 0, 0x20); //Instantiate Haar sensor with default ports and version v2.0 -SO421 apogeeO2(sdi12, 0, 0); //Instantiate O2 sensor with default ports and unknown version, pass over SDI12 Talon interface -SP421 apogeeSolar(sdi12, 0, 0); //Instantiate solar sensor with default ports and unknown version, pass over SDI12 Talon interface -// TEROS11 soil(sdi12, 0, 0); //Instantiate soil sensor with default ports and unknown version, pass over SDI12 Talon interface -TDR315H soil1(sdi12, 0, 0); //Instantiate soil sensor with default ports and unknown version, pass over SDI12 Talon interface -TDR315H soil2(sdi12, 0, 0); //Instantiate soil sensor with default ports and unknown version, pass over SDI12 Talon interface -TDR315H soil3(sdi12, 0, 0); //Instantiate soil sensor with default ports and unknown version, pass over SDI12 Talon interface -Hedorah gas(0, 0, 0x10); //Instantiate CO2 sensor with default ports and v1.0 hardware -// T9602 humidity(0, 0, 0x00); //Instantiate Telair T9602 with default ports and version v0.0 -LI710 et(realTimeProvider, realSdi12, 0, 0); //Instantiate ET sensor with default ports and unknown version, pass over SDI12 Talon interface -BaroVue10 campPressure(sdi12, 0, 0x00); // Instantiate Barovue10 with default ports and v0.0 hardware - -const uint8_t numSensors = 8; //Number must match the number of objects defined in `sensors` array - -Sensor* const sensors[numSensors] = { - &fileSys, - &aux, - &i2c, - &sdi12, - &battery, - &logger, //Add sensors after this line - &et, - &haar - // &soil1, - // &apogeeSolar, - - // &soil2, - // &soil3, - // &gas, - // &apogeeO2, -}; -/////////////////////////// END USER CONFIG ///////////////////////////////// +PRODUCT_VERSION(34) + +//global variables affected by configuration manager +int backhaulCount; +unsigned long logPeriod; +int desiredPowerSaveMode; +int loggingMode; + +int systemConfigUid = 0; //Used to track the UID of the configuration file +int sensorConfigUid = 0; //Used to track the UID of the sensor configuration file + +String globalNodeID = ""; //Store current node ID +ConfigurationManager configManager; +SensorManager sensorManager(configManager); +std::vector sensors; +std::vector talons; +SDI12TalonAdapter* realSdi12 = nullptr; namespace PinsIO { //For Kestrel v1.1 constexpr uint16_t VUSB = 5; } @@ -245,7 +214,6 @@ String metadata = ""; String data = ""; void setup() { - configurePowerSave(desiredPowerSaveMode); //Setup power mode of the system (Talons and Sensors) System.enableFeature(FEATURE_RESET_INFO); //Allows for Particle to see reason for last reset using System.resetReason(); if(System.resetReason() != RESET_REASON_POWER_DOWN) { //DEBUG! Set safe mode @@ -255,6 +223,9 @@ void setup() { // talons[aux.getTalonPort()] = &aux; //Place talon objects at coresponding positions in array // talons[aux1.getTalonPort()] = &aux1; time_t startTime = millis(); + Particle.function("updateConfig", updateConfiguration); + Particle.function("getSystemConfig", getSystemConfiguration); + Particle.function("getSensorConfig", getSensorConfiguration); Particle.function("nodeID", setNodeID); Particle.function("findSensors", detectSensors); Particle.function("findTalons", detectTalons); @@ -269,6 +240,8 @@ void setup() { bool hasError = false; // logger.begin(Time.now(), hasCriticalError, hasError); //Needs to be called the first time with Particle time since I2C not yet initialized logger.begin(0, hasCriticalError, hasError); //Called with 0 since time collection system has not been initialized + Serial.println("Critial error: " + String(hasCriticalError)); //DEBUG! + Serial.println("Error: " + String(hasError)); //DEBUG! logger.setIndicatorState(IndicatorLight::ALL,IndicatorMode::INIT); bool batState = logger.testForBat(); //Check if a battery is connected logger.enableI2C_OB(false); @@ -277,6 +250,8 @@ void setup() { if(batState) battery.setIndicatorState(GonkIndicatorMode::SOLID); //Turn on charge indication LEDs during setup else battery.setIndicatorState(GonkIndicatorMode::BLINKING); //If battery not switched on, set to blinking fileSys.begin(0, hasCriticalError, hasError); //Initialzie, but do not attempt backhaul + Serial.println("Critial error: " + String(hasCriticalError)); //DEBUG! + Serial.println("Error: " + String(hasError)); //DEBUG! if(hasCriticalError) { Serial.println(getErrorString()); //Report current error codes logger.setIndicatorState(IndicatorLight::STAT,IndicatorMode::ERROR); //Display error state if critical error is reported @@ -312,6 +287,19 @@ void setup() { // logger.enableData(i, false); //Turn off all data by default // } + //load configuration from SD card, default config if not found or not possible + Serial.println("Loading configuration..."); //DEBUG! + loadConfiguration(); + Serial.println("Configuration loaded"); //DEBUG! + + //initilize all sensors + Serial.println("Initializing sensors..."); //DEBUG! + initializeSensorSystem(); + Serial.println("Sensors initialized"); //DEBUG! + + // Apply power save mode + configurePowerSave(desiredPowerSaveMode); //Setup power mode of the system (Talons and Sensors) + detectTalons(); detectSensors(); @@ -668,9 +656,9 @@ String getErrorString() if(globalNodeID != "") errors = errors + "\"Node ID\":\"" + globalNodeID + "\","; //Concatonate node ID else errors = errors + "\"Device ID\":\"" + System.deviceID() + "\","; //If node ID not initialized, use device ID errors = errors + "\"Packet ID\":" + logger.getMessageID() + ","; //Concatonate unique packet hash - errors = errors + "\"NumDevices\":" + String(numSensors) + ","; //Concatonate number of sensors + errors = errors + "\"NumDevices\":" + String(sensors.size()) + ","; //Concatonate number of sensors errors = errors + "\"Devices\":["; - for(int i = 0; i < numSensors; i++) { + for(int i = 0; i < sensors.size(); i++) { if(sensors[i]->totalErrors() > 0) { numErrors = numErrors + sensors[i]->totalErrors(); //Increment the total error count if(!errors.endsWith("[")) errors = errors + ","; //Only append if not first entry @@ -692,27 +680,35 @@ String getDataString() if(globalNodeID != "") leader = leader + "\"Node ID\":\"" + globalNodeID + "\","; //Concatonate node ID else leader = leader + "\"Device ID\":\"" + System.deviceID() + "\","; //If node ID not initialized, use device ID leader = leader + "\"Packet ID\":" + logger.getMessageID() + ","; //Concatonate unique packet hash - leader = leader + "\"NumDevices\":" + String(numSensors) + ","; //Concatonate number of sensors + leader = leader + "\"NumDevices\":" + String(sensors.size()) + ","; //Concatonate number of sensors leader = leader + "\"Devices\":["; const String closer = "]}}"; String output = leader; uint8_t deviceCount = 0; //Used to keep track of how many devices have been appended - for(int i = 0; i < numSensors; i++) { + for(int i = 0; i < sensors.size(); i++) { logger.disableDataAll(); //Turn off data to all ports, then just enable those needed - if(sensors[i]->sensorInterface != BusType::CORE && sensors[i]->getTalonPort() != 0) logger.enablePower(sensors[i]->getTalonPort(), true); //Turn on kestrel port for needed Talon, only if not core system and port is valid - if(sensors[i]->sensorInterface != BusType::CORE && sensors[i]->getTalonPort() != 0) logger.enableData(sensors[i]->getTalonPort(), true); //Turn on kestrel port for needed Talon, only if not core system and port is valid + if(sensors[i]->sensorInterface != BusType::CORE && sensors[i]->getTalonPort() != 0) { + logger.enablePower(sensors[i]->getTalonPort(), true); //Turn on kestrel port for needed Talon, only if not core system and port is valid + Serial.println("Enabled power for Talon: " + String(sensors[i]->getTalonPort())); //DEBUG! + } + if(sensors[i]->sensorInterface != BusType::CORE && sensors[i]->getTalonPort() != 0) { + logger.enableData(sensors[i]->getTalonPort(), true); //Turn on kestrel port for needed Talon, only if not core system and port is valid + Serial.println("Enabled data for Talon: " + String(sensors[i]->getTalonPort())); //DEBUG! + } logger.enableI2C_OB(false); logger.enableI2C_Global(true); bool dummy1; bool dummy2; - if(sensors[i]->getTalonPort() > 0 && talons[sensors[i]->getTalonPort() - 1]) { //DEBUG! REPALCE! + int currentTalonIndex = getIndexOfPort(sensors[i]->getTalonPort()); //Find the talon associated with this sensor + + if((sensors[i]->getTalonPort() > 0) && (currentTalonIndex >= 0)) { //DEBUG! REPALCE! Serial.print("TALON CALL: "); //DEBUG! Serial.println(sensors[i]->getTalonPort()); logger.configTalonSense(); //Setup to allow for current testing // talons[sensors[i]->getTalonPort() - 1]->begin(logger.getTime(), dummy1, dummy2); //DEBUG! Do only if talon is associated with sensor, and object exists - talons[sensors[i]->getTalonPort() - 1]->restart(); //DEBUG! Do only if talon is associated with sensor, and object exists + talons[currentTalonIndex]->restart(); //DEBUG! Do only if talon is associated with sensor, and object exists // logger.enableI2C_OB(false); //Return to isolation mode // logger.enableI2C_Global(true); } @@ -720,13 +716,15 @@ String getDataString() Serial.print("Device "); //DEBUG! Serial.print(i); Serial.println(" is a sensor"); - talons[sensors[i]->getTalonPort() - 1]->disableDataAll(); //Turn off all data ports to start for the given Talon - // talons[sensors[i]->getTalonPort() - 1]->disablePowerAll(); //Turn off all power ports to start for the given Talon - // talons[sensors[i]->getTalonPort() - 1]->enablePower(sensors[i]->getSensorPort(), true); //Turn on power for the given port on the Talon - talons[sensors[i]->getTalonPort() - 1]->enableData(sensors[i]->getSensorPort(), true); //Turn on data for the given port on the Talon - // bool dummy1; - // bool dummy2; - // sensors[i]->begin(Time.now(), dummy1, dummy2); //DEBUG! + if(currentTalonIndex >= 0) { //DEBUG! REPALCE! + talons[currentTalonIndex]->disableDataAll(); //Turn off all data ports to start for the given Talon + // talons[sensors[i]->getTalonPort() - 1]->disablePowerAll(); //Turn off all power ports to start for the given Talon + // talons[sensors[i]->getTalonPort() - 1]->enablePower(sensors[i]->getSensorPort(), true); //Turn on power for the given port on the Talon + talons[currentTalonIndex]->enableData(sensors[i]->getSensorPort(), true); //Turn on data for the given port on the Talon + // bool dummy1; + // bool dummy2; + // sensors[i]->begin(Time.now(), dummy1, dummy2); //DEBUG! + } } // delay(100); //DEBUG! logger.enableI2C_OB(false); @@ -741,7 +739,7 @@ String getDataString() if(deviceCount > 0) output = output + ","; //Add preceeding comma if not the first entry output = output + "{" + val + "}"; //Append result deviceCount++; - // if(i + 1 < numSensors) diagnostic = diagnostic + ","; //Only append if not last entry + // if(i + 1 < sensors.size()) diagnostic = diagnostic + ","; //Only append if not last entry } else { output = output + closer + "\n"; //End this packet @@ -752,14 +750,14 @@ String getDataString() // if(deviceCount > 0) data = data + ","; //Preappend comma only if not first addition // data = data + "{" + val + "}"; // deviceCount++; - // // if(i + 1 < numSensors) metadata = metadata + ","; //Only append if not last entry + // // if(i + 1 < sensors.size()) metadata = metadata + ","; //Only append if not last entry // } Serial.print("Cumulative data string: "); //DEBUG! Serial.println(output); //DEBUG! // data = data + sensors[i]->getData(logger.getTime()); //DEBUG! REPLACE! - // if(i + 1 < numSensors) data = data + ","; //Only append if not last entry - if(sensors[i]->getSensorPort() > 0 && sensors[i]->getTalonPort() > 0) { - talons[sensors[i]->getTalonPort() - 1]->enableData(sensors[i]->getSensorPort(), false); //Turn off data for the given port on the Talon + // if(i + 1 < sensors.size()) data = data + ","; //Only append if not last entry + if((sensors[i]->getSensorPort() > 0) && (sensors[i]->getTalonPort() > 0) && (currentTalonIndex >= 0)) { + talons[currentTalonIndex]->enableData(sensors[i]->getSensorPort(), false); //Turn off data for the given port on the Talon // talons[sensors[i]->getTalonPort() - 1]->enablePower(sensors[i]->getSensorPort(), false); //Turn off power for the given port on the Talon //DEBUG! } } @@ -775,22 +773,25 @@ String getDiagnosticString(uint8_t level) if(globalNodeID != "") leader = leader + "\"Node ID\":\"" + globalNodeID + "\","; //Concatonate node ID else leader = leader + "\"Device ID\":\"" + System.deviceID() + "\","; //If node ID not initialized, use device ID leader = leader + "\"Packet ID\":" + logger.getMessageID() + ","; //Concatonate unique packet hash - leader = leader + "\"NumDevices\":" + String(numSensors) + ",\"Level\":" + String(level) + ",\"Devices\":["; //Concatonate number of sensors and level + leader = leader + "\"NumDevices\":" + String(sensors.size()) + ",\"Level\":" + String(level) + ",\"Devices\":["; //Concatonate number of sensors and level const String closer = "]}}"; String output = leader; uint8_t deviceCount = 0; //Used to keep track of how many devices have been appended - for(int i = 0; i < numSensors; i++) { + for(int i = 0; i < sensors.size(); i++) { logger.disableDataAll(); //Turn off data to all ports, then just enable those needed if(sensors[i]->sensorInterface != BusType::CORE && sensors[i]->getTalonPort() != 0) logger.enablePower(sensors[i]->getTalonPort(), true); //Turn on kestrel port for needed Talon, only if not core system and port is valid if(sensors[i]->sensorInterface != BusType::CORE && sensors[i]->getTalonPort() != 0) logger.enableData(sensors[i]->getTalonPort(), true); //Turn on kestrel port for needed Talon, only if not core system and port is valid logger.enableI2C_OB(false); logger.enableI2C_Global(true); // if(!sensors[i]->isTalon()) { //If sensor is not Talon - if(sensors[i]->getSensorPort() > 0 && sensors[i]->getTalonPort() > 0) { //If a Talon is associated with the sensor, turn that port on - talons[sensors[i]->getTalonPort() - 1]->disableDataAll(); //Turn off all data on Talon + + int currentTalonIndex = getIndexOfPort(sensors[i]->getTalonPort()); //Find the talon associated with this sensor + + if((sensors[i]->getSensorPort() > 0) && (sensors[i]->getTalonPort() > 0) && (currentTalonIndex >= 0)) { //If a Talon is associated with the sensor, turn that port on + talons[currentTalonIndex]->disableDataAll(); //Turn off all data on Talon // talons[sensors[i]->getTalonPort() - 1]->enablePower(sensors[i]->getSensorPort(), true); //Turn on power for the given port on the Talon - talons[sensors[i]->getTalonPort() - 1]->enableData(sensors[i]->getSensorPort(), true); //Turn back on only port used + talons[currentTalonIndex]->enableData(sensors[i]->getSensorPort(), true); //Turn back on only port used } @@ -800,7 +801,7 @@ String getDiagnosticString(uint8_t level) if(deviceCount > 0) output = output + ","; //Add preceeding comma if not the first entry output = output + "{" + diagnostic + "}"; //Append result deviceCount++; - // if(i + 1 < numSensors) diagnostic = diagnostic + ","; //Only append if not last entry + // if(i + 1 < sensors.size()) diagnostic = diagnostic + ","; //Only append if not last entry } else { output = output + closer + "\n"; //End this packet @@ -808,8 +809,8 @@ String getDiagnosticString(uint8_t level) } } - if(sensors[i]->getSensorPort() > 0 && sensors[i]->getTalonPort() > 0) { - talons[sensors[i]->getTalonPort() - 1]->enableData(sensors[i]->getSensorPort(), false); //Turn off data for the given port on the Talon + if((sensors[i]->getSensorPort() > 0) && (sensors[i]->getTalonPort() > 0) && (currentTalonIndex >= 0)) { + talons[currentTalonIndex]->enableData(sensors[i]->getSensorPort(), false); //Turn off data for the given port on the Talon // talons[sensors[i]->getTalonPort() - 1]->enablePower(sensors[i]->getSensorPort(), false); //Turn off power for the given port on the Talon //DEBUG! } @@ -826,7 +827,7 @@ String getMetadataString() if(globalNodeID != "") leader = leader + "\"Node ID\":\"" + globalNodeID + "\","; //Concatonate node ID else leader = leader + "\"Device ID\":\"" + System.deviceID() + "\","; //If node ID not initialized, use device ID leader = leader + "\"Packet ID\":" + logger.getMessageID() + ","; //Concatonate unique packet hash - leader = leader + "\"NumDevices\":" + String(numSensors) + ","; //Concatonate number of sensors + leader = leader + "\"NumDevices\":" + String(sensors.size()) + ","; //Concatonate number of sensors leader = leader + "\"Devices\":["; const String closer = "]}}"; String output = leader; @@ -844,13 +845,16 @@ String getMetadataString() //FIX! Add support for device name uint8_t deviceCount = 0; //Used to keep track of how many devices have been appended - for(int i = 0; i < numSensors; i++) { + for(int i = 0; i < sensors.size(); i++) { logger.disableDataAll(); //Turn off data to all ports, then just enable those needed if(sensors[i]->sensorInterface != BusType::CORE && sensors[i]->getTalonPort() != 0) logger.enableData(sensors[i]->getTalonPort(), true); //Turn on data to required Talon port only if not core and port is valid // if(!sensors[i]->isTalon()) { //If sensor is not Talon - if(sensors[i]->getSensorPort() > 0 && sensors[i]->getTalonPort() > 0) { //If a Talon is associated with the sensor, turn that port on - talons[sensors[i]->getTalonPort() - 1]->disableDataAll(); //Turn off all data on Talon - talons[sensors[i]->getTalonPort() - 1]->enableData(sensors[i]->getSensorPort(), true); //Turn back on only port used + + int currentTalonIndex = getIndexOfPort(sensors[i]->getTalonPort()); //Find the talon associated with this sensor + + if((sensors[i]->getSensorPort() > 0) && (sensors[i]->getTalonPort() > 0) && (currentTalonIndex >= 0)) { //If a Talon is associated with the sensor, turn that port on + talons[currentTalonIndex]->disableDataAll(); //Turn off all data on Talon + talons[currentTalonIndex]->enableData(sensors[i]->getSensorPort(), true); //Turn back on only port used } // logger.enablePower(sensors[i]->getTalon(), true); //Turn on power to port // logger.enableData(sensors[i]->getTalon(), true); //Turn on data to port @@ -862,14 +866,14 @@ String getMetadataString() // if(deviceCount > 0) metadata = metadata + ","; //Preappend comma only if not first addition // metadata = metadata + val; // deviceCount++; - // // if(i + 1 < numSensors) metadata = metadata + ","; //Only append if not last entry + // // if(i + 1 < sensors.size()) metadata = metadata + ","; //Only append if not last entry // } if(!val.equals("")) { //Only append if not empty string if(output.length() - output.lastIndexOf('\n') + val.length() + closer.length() + 1 < Kestrel::MAX_MESSAGE_LENGTH) { //Add +1 to account for comma appending, subtract any previous lines from count if(deviceCount > 0) output = output + ","; //Add preceeding comma if not the first entry output = output + "{" + val + "}"; //Append result deviceCount++; - // if(i + 1 < numSensors) diagnostic = diagnostic + ","; //Only append if not last entry + // if(i + 1 < sensors.size()) diagnostic = diagnostic + ","; //Only append if not last entry } else { output = output + closer + "\n"; //End this packet @@ -890,7 +894,7 @@ String initSensors() if(globalNodeID != "") leader = leader + "\"Node ID\":\"" + globalNodeID + "\","; //Concatonate node ID else leader = leader + "\"Device ID\":\"" + System.deviceID() + "\","; //If node ID not initialized, use device ID leader = leader + "\"Packet ID\":" + logger.getMessageID() + ","; //Concatonate unique packet hash - leader = leader + "\"NumDevices\":" + String(numSensors) + ",\"Devices\":["; //Concatonate number of sensors and level + leader = leader + "\"NumDevices\":" + String(sensors.size()) + ",\"Devices\":["; //Concatonate number of sensors and level String closer = "]}}"; String output = leader; @@ -899,7 +903,7 @@ String initSensors() bool missingSensor = false; // output = output + "\"Devices\":["; uint8_t deviceCount = 0; //Used to keep track of how many devices have been appended - for(int i = 0; i < numSensors; i++) { + for(int i = 0; i < sensors.size(); i++) { logger.disableDataAll(); //Turn off data to all ports, then just enable those needed if(sensors[i]->sensorInterface != BusType::CORE && sensors[i]->getTalonPort() != 0) logger.enableData(sensors[i]->getTalonPort(), true); //Turn on data to required Talon port only if not core and the port is valid logger.enableI2C_OB(false); @@ -909,12 +913,19 @@ String initSensors() // if(!sensors[i]->isTalon()) { //If sensor is not Talon logger.configTalonSense(); //Setup to allow for current testing // if(sensors[i]->getTalonPort() > 0 && talons[sensors[i]->getTalonPort() - 1]) talons[sensors[i]->getTalonPort() - 1]->begin(logger.getTime(), dummy1, dummy2); //DEBUG! Do only if talon is associated with sensor, and object exists //DEBUG! REPLACE! - if(sensors[i]->getTalonPort() > 0 && talons[sensors[i]->getTalonPort() - 1]) talons[sensors[i]->getTalonPort() - 1]->restart(); //DEBUG! Do only if talon is associated with sensor, and object exists //DEBUG! REPLACE! - if(sensors[i]->getSensorPort() > 0 && sensors[i]->getTalonPort() > 0) { //If a Talon is associated with the sensor, turn that port on - talons[sensors[i]->getTalonPort() - 1]->disableDataAll(); //Turn off all data on Talon - // talons[sensors[i]->getTalonPort() - 1]->enablePower(sensors[i]->getSensorPort(), true); //Turn on power for the given port on the Talon - talons[sensors[i]->getTalonPort() - 1]->enableData(sensors[i]->getSensorPort(), true); //Turn back on only port used - + + int currentTalonIndex = getIndexOfPort(sensors[i]->getTalonPort()); + + if(currentTalonIndex >= 0) + { + if(sensors[i]->getTalonPort() > 0) { //DEBUG! REPLACE! + talons[currentTalonIndex]->restart(); //DEBUG! Do only if talon is associated with sensor, and object exists //DEBUG! REPLACE! + } + if(sensors[i]->getSensorPort() > 0 && sensors[i]->getTalonPort() > 0) { //If a Talon is associated with the sensor, turn that port on + talons[currentTalonIndex]->disableDataAll(); //Turn off all data on Talon + // talons[sensors[i]->getTalonPort() - 1]->enablePower(sensors[i]->getSensorPort(), true); //Turn on power for the given port on the Talon + talons[currentTalonIndex]->enableData(sensors[i]->getSensorPort(), true); //Turn back on only port used + } } if(sensors[i]->getTalonPort() == 0 && sensors[i]->sensorInterface != BusType::CORE) { missingSensor = true; //Set flag if any sensors not assigned to Talon and not a core sensor @@ -936,7 +947,7 @@ String initSensors() if(deviceCount > 0) output = output + ","; //Add preceeding comma if not the first entry output = output + "{" + val + "}"; //Append result deviceCount++; - // if(i + 1 < numSensors) diagnostic = diagnostic + ","; //Only append if not last entry + // if(i + 1 < sensors.size()) diagnostic = diagnostic + ","; //Only append if not last entry } else { output = output + closer + "\n"; //End this packet @@ -1075,16 +1086,17 @@ int sleepSensors() { if(powerSaveMode > PowerSaveModes::PERFORMANCE) { //Only turn off is power save requested Serial.println("BEGIN SENSOR SLEEP"); //DEBUG! - for(int s = 0; s < numSensors; s++) { //Iterate over all sensors objects + for(int s = 0; s < sensors.size(); s++) { //Iterate over all sensors objects //If not set to keep power on and Talon is assocated, power down sensor. Ignore if core device, we will handle these seperately - if(sensors[s]->keepPowered == false && sensors[s]->sensorInterface != BusType::CORE && sensors[s]->getTalonPort() > 0 && sensors[s]->getTalonPort() < numTalons) { + if(sensors[s]->keepPowered == false && sensors[s]->sensorInterface != BusType::CORE && sensors[s]->getTalonPort() > 0 && sensors[s]->getTalonPort() < talons.size()) { Serial.print("Power Down Sensor "); //DEBUG! Serial.print(s + 1); Serial.print(","); Serial.println(sensors[s]->getTalonPort()); - talons[sensors[s]->getTalonPort() - 1]->enablePower(sensors[s]->getSensorPort(), false); //Turn off power for any sensor which does not need to be kept powered + int currentTalonIndex = getIndexOfPort(sensors[s]->getTalonPort()); + talons[currentTalonIndex]->enablePower(sensors[s]->getSensorPort(), false); //Turn off power for any sensor which does not need to be kept powered } - else if(sensors[s]->sensorInterface != BusType::CORE && sensors[s]->getTalonPort() > 0 && sensors[s]->getTalonPort() < numTalons){ //If sensor has a position and is not core, but keepPowered is true, run sleep routine + else if(sensors[s]->sensorInterface != BusType::CORE && sensors[s]->getTalonPort() > 0 && sensors[s]->getTalonPort() < talons.size()){ //If sensor has a position and is not core, but keepPowered is true, run sleep routine Serial.print("Sleep Sensor "); //DEBUG! Serial.println(s + 1); sensors[s]->sleep(); //If not powered down, run sleep protocol @@ -1101,7 +1113,7 @@ int sleepSensors() } } - for(int t = 0; t < Kestrel::numTalonPorts; t++) { //Iterate over all talon objects + for(int t = 0; t < talons.size(); t++) { //Iterate over all talon objects if(talons[t] && talons[t]->keepPowered == false) { //If NO sensors on a given Talon require it to be kept powered, shut the whole thing down Serial.print("Power Down Talon "); //DEBUG! Serial.println(talons[t]->getTalonPort()); @@ -1124,14 +1136,14 @@ int wakeSensors() logger.enableI2C_OB(false); logger.disableDataAll(); //Turn off all data to start for(int p = 1; p <= Kestrel::numTalonPorts; p++) logger.enablePower(p, true); //Turn power back on to all Kestrel ports - for(int t = 0; t < Kestrel::numTalonPorts; t++) { + for(int t = 0; t < talons.size(); t++) { if(talons[t] && talons[t]->getTalonPort() != 0) { logger.enableData(talons[t]->getTalonPort(), true); //Turn on data for given port talons[t]->restart(); //Restart all Talons, this turns on all ports it can logger.enableData(talons[t]->getTalonPort(), false); //Turn data back off for given port } } - for(int s = 0; s < numSensors; s++) { + for(int s = 0; s < sensors.size(); s++) { if(sensors[s]->getTalonPort() != 0) { logger.enableData(sensors[s]->getTalonPort(), true); //Turn on data for given port sensors[s]->wake(realTimeProvider); //Wake each sensor @@ -1150,7 +1162,7 @@ int detectTalons(String dummyStr) // bool hasCriticalError = false; // bool hasError = false; - // for(int i = 0; i < numTalons; i++) { //Initialize all Talons //DEBUG! + // for(int i = 0; i < talons.size(); i++) { //Initialize all Talons //DEBUG! // talons[i]->begin(Time.now(), hasCriticalError, hasError); // } // logger.enableI2C_External(false); //Turn off connection to @@ -1172,22 +1184,24 @@ int detectTalons(String dummyStr) if(error == 0) break; //Exit loop once we are able to connect with Talon } quickTalonShutdown(); //Quickly disables power to all ports on I2C or SDI talons, this is a kluge - for(int t = 0; t < numTalons; t++) { //Iterate over all Talon objects - if(talonsToTest[t]->getTalonPort() == 0) { //If port not already specified + for(int t = 0; t < talons.size(); t++) { //Iterate over all Talon objects + if(talons[t]->getTalonPort() == 0) { //If port not already specified Serial.print("New Talon: "); Serial.println(t); // logger.enableAuxPower(false); //Turn aux power off, then configure port to on, then switch aux power back for faster response // logger.enablePower(port, true); //Toggle power just before testing to get result within 10ms // logger.enablePower(port, false); - if(talonsToTest[t]->isPresent()) { //Test if that Talon is present, if it is, configure the port - talonsToTest[t]->setTalonPort(port); - talons[port - 1] = talonsToTest[t]; //Copy test talon object to index location in talons array + if(talons[t]->isPresent()) { //Test if that Talon is present, if it is, configure the port + talons[t]->setTalonPort(port); Serial.print("Talon Port Result "); //DEBUG! Serial.print(t); Serial.print(": "); - Serial.println(talonsToTest[t]->getTalonPort()); + Serial.println(talons[t]->getTalonPort()); break; //Exit the interation after the first one tests positive } + else { + Serial.println("Talon not present"); //DEBUG! + } } } logger.enableData(port, false); //Turn port back off @@ -1197,7 +1211,7 @@ int detectTalons(String dummyStr) // talons[i2c.getTalonPort() - 1] = &i2c; bool dummy; bool dummy1; - for(int i = 0; i < Kestrel::numTalonPorts - 1; i++) { + for(int i = 0; i < talons.size(); i++) { if(talons[i] && talons[i]->getTalonPort() > 0) { Serial.print("BEGIN TALON: "); //DEBUG! Serial.print(talons[i]->getTalonPort()); @@ -1208,38 +1222,39 @@ int detectTalons(String dummyStr) logger.setDirection(talons[i]->getTalonPort(), HIGH); //If the talon is an SDI12 interface type, set port to use serial interface } else if(talons[i]->talonInterface != BusType::CORE) logger.setDirection(talons[i]->getTalonPort(), LOW); //Otherwise set talon to use GPIO interface, unless bus type is core, in which case ignore it - logger.enablePower(i + 1, true); //Turn on specific channel - logger.enableData(i + 1, true); + logger.enablePower(talons[i]->getTalonPort(), true); //Turn on specific channel + logger.enableData(talons[i]->getTalonPort(), true); if(logger.getFault(talons[i]->getTalonPort())) { //Only toggle power if there is a fault on that Talon line - logger.enablePower(i + 1, true); //Toggle power just before testing to get result within 10ms - logger.enablePower(i + 1, false); - logger.enablePower(i + 1, true); + logger.enablePower(talons[i]->getTalonPort(), true); //Toggle power just before testing to get result within 10ms + logger.enablePower(talons[i]->getTalonPort(), false); + logger.enablePower(talons[i]->getTalonPort(), true); } logger.configTalonSense(); //Setup to allow for current testing - // Serial.println("TALON SENSE CONFIG DONE"); //DEBUG! + //Serial.println("TALON SENSE CONFIG DONE"); //DEBUG! // Serial.flush(); //DEBUG! // logger.enableI2C_Global(true); // logger.enableI2C_OB(false); // talons[i]->begin(Time.now(), dummy, dummy1); //If Talon object exists and port has been assigned, initialize it //DEBUG! talons[i]->begin(logger.getTime(), dummy, dummy1); //If Talon object exists and port has been assigned, initialize it //REPLACE getTime! // talons[i]->begin(0, dummy, dummy1); //If Talon object exists and port has been assigned, initialize it //REPLACE getTime! - // Serial.println("TALON BEGIN DONE"); //DEBUG! - // Serial.flush(); //DEBUG! - // delay(10000); //DEBUG! - logger.enableData(i + 1, false); //Turn data back off to prevent conflict - // Serial.println("ENABLE DATA DONE"); //DEBUG! + //Serial.println("TALON BEGIN DONE"); //DEBUG! + //Serial.flush(); //DEBUG! + //delay(10000); //DEBUG! + logger.enableData(talons[i]->getTalonPort(), false); //Turn data back off to prevent conflict + //Serial.println("ENABLE DATA DONE"); //DEBUG! // Serial.flush(); //DEBUG! - // delay(10000); //DEBUG! + //delay(10000); //DEBUG! } } + //Serial.println("TALON DETECTION DONE"); //DEBUG! return 0; //DEBUG! } int detectSensors(String dummyStr) { /////////////// SENSOR AUTO DETECTION ////////////////////// - for(int t = 0; t < Kestrel::numTalonPorts; t++) { //Iterate over each Talon + for(int t = 0; t < talons.size(); t++) { //Iterate over each Talon // Serial.println(talons[t]->talonInterface); //DEBUG! // Serial.print("DETECT ON TALON: "); //DEBUG! // Serial.println(t); @@ -1257,12 +1272,12 @@ int detectSensors(String dummyStr) // delay(5000); // if(talons[t]->talonInterface != BusType::NONE) { // delay(5000); - // Serial.println("TALON NOT NONE"); //DEBUG! + // Serial.println("TALON NOT NONE"); //DEBUG! // Serial.flush(); // } // else { // delay(5000); - // Serial.println("TALON NONE"); //DEBUG! + // Serial.println("TALON NONE"); //DEBUG! // Serial.flush(); // } // delay(10000); //DEBUG! @@ -1274,18 +1289,19 @@ int detectSensors(String dummyStr) talons[t]->disableDataAll(); //Turn off all data ports on Talon for(int p = 1; p <= talons[t]->getNumPorts(); p++) { //Iterate over each port on given Talon // talons[t]->enablePower(p, true); //Turn data and power on for specific channel - talons[t]->enableData(p, true); + Serial.print("Port enable success: "); //DEBUG! + Serial.println(talons[t]->enableData(p, true)); delay(10); //Wait to make sure sensor is responsive after power up command Serial.print("Testing Port: "); //DEBUG! - Serial.print(t + 1); + Serial.print(talons[t]->getTalonPort()); Serial.print(","); Serial.println(p); - for(int s = 0; s < numSensors; s++) { //Iterate over all sensors objects + for(int s = 0; s < sensors.size(); s++) { //Iterate over all sensors objects if(sensors[s]->getTalonPort() == 0 && talons[t]->talonInterface == sensors[s]->sensorInterface) { //If Talon not already specified AND sensor bus is compatible with Talon bus Serial.print("Test Sensor: "); //DEBUG! Serial.println(s); if(sensors[s]->isPresent()) { //Test if that sensor is present, if it is, configure the port - sensors[s]->setTalonPort(t + 1); + sensors[s]->setTalonPort(talons[t]->getTalonPort()); //Set the Talon port for the sensor sensors[s]->setSensorPort(p); if(sensors[s]->keepPowered == true) talons[sensors[s]->getTalonPort() - 1]->keepPowered = true; //If any of the sensors on a Talon require power, set the flag for the Talon Serial.print("Sensor Found:\n\t"); //DEBUG! @@ -1322,6 +1338,76 @@ int setNodeID(String nodeID) } } +int updateConfiguration(String configJson) { + if(configJson == "remove") { + // Remove the configuration file from the SD card + if (!fileSys.removeFileFromSD("config.json")) { + Serial.println("Error: Failed to remove configuration from SD card."); + return -1; // Failed to remove config + } + else { + Serial.println("Configuration removed from SD card."); + } + return 0; // Success + } + + Serial.println("Updating configuration..."); + Serial.println(configJson); + + //remove all whitespace and newlines from the config string + configJson.replace(" ", ""); + configJson.replace("\n", ""); + configJson.replace("\r", ""); + configJson.replace("\t", ""); + + Serial.println(configJson); + + //verify the passed config is valid format + if (configJson.indexOf("\"config\"") == -1) { + Serial.println("Error: Invalid configuration format. Missing 'config' element."); + return -2; // Invalid format + } + if (configJson.indexOf("\"system\"") == -1) { + Serial.println("Error: Invalid configuration format. Missing 'system' element."); + return -3; // Invalid format + } + if (configJson.indexOf("\"sensors\"") == -1) { + Serial.println("Error: Invalid configuration format. Missing 'sensors' element."); + return -4; // Invalid format + } + + // test write to SD card + if (!fileSys.writeToSD("", "config.json")) { + Serial.println("Error: Failed to write to SD card."); + return -5; // Failed to write config + } + + //clear current config.json + if(!fileSys.removeFileFromSD("config.json")) { + Serial.println("Error: Failed to remove current configuration from SD card."); + return -6; // Failed to remove current config + } + + // Write new configuration to SD card + if (!fileSys.writeToSD(configJson.c_str(), "config.json")) { + Serial.println("Error: Failed to write new configuration to SD card."); + return -7; // Failed to write new config + } + + System.reset(); //restart the system to apply new configuration + return 1; //Success +} + +int getSystemConfiguration(String dummy) { + int uid = configManager.updateSystemConfigurationUid(); + return uid; +} + +int getSensorConfiguration(String dummy) { + int uid = configManager.updateSensorConfigurationUid(); + return uid; +} + int takeSample(String dummy) { logger.wake(); //Wake logger in case it was sleeping @@ -1413,12 +1499,92 @@ int systemRestart(String resetType) int configurePowerSave(int desiredPowerSaveMode) { powerSaveMode = desiredPowerSaveMode; //Configure global flag - for(int s = 0; s < numSensors; s++) { //Iterate over all sensors objects + for(int s = 0; s < sensors.size(); s++) { //Iterate over all sensors objects sensors[s]->powerSaveMode = desiredPowerSaveMode; //Set power save mode for all sensors } - for(int t = 0; t < numTalons; t++) { //Iterate over all talon objects - talonsToTest[t]->powerSaveMode = desiredPowerSaveMode; //Set power save mode for all talons + for(int t = 0; t < talons.size(); t++) { //Iterate over all talon objects + talons[t]->powerSaveMode = desiredPowerSaveMode; //Set power save mode for all talons } return 0; //DEBUG! +} + +bool loadConfiguration() { + bool configLoaded = false; + std::string configStr = fileSys.readFromSD("config.json").c_str(); + + if (!configStr.empty()) { + Serial.println("Loading configuration from SD card..."); + configLoaded = configManager.setConfiguration(configStr); + } + + if (!configLoaded) { + Serial.println("Loading default configuration..."); + std::string defaultConfig = configManager.getDefaultConfigurationJson(); + configLoaded = configManager.setConfiguration(defaultConfig); + fileSys.removeFileFromSD("config.json"); + fileSys.writeToSD(defaultConfig.c_str(), "config.json"); + } + + // Set global variables from configuration + logPeriod = configManager.getLogPeriod(); + backhaulCount = configManager.getBackhaulCount(); + desiredPowerSaveMode = configManager.getPowerSaveMode(); + loggingMode = configManager.getLoggingMode(); + + return configLoaded; +} + +void initializeSensorSystem() { + // Create SDI12 adapter if needed + auto& sdi12Talons = sensorManager.getSDI12Talons(); + if (!sdi12Talons.empty() && realSdi12 == nullptr) { + realSdi12 = new SDI12TalonAdapter(*sdi12Talons[0]); + } + + // Initialize sensors + if (realSdi12 != nullptr) { + sensorManager.initializeSensors(realTimeProvider, *realSdi12); + } else { + SDI12Talon dummyTalon(0, 0x14); + SDI12TalonAdapter dummyAdapter(dummyTalon); + sensorManager.initializeSensors(realTimeProvider, dummyAdapter); + } + + updateSensorVectors(); +} + +void updateSensorVectors() { + sensors.clear(); + talons.clear(); + + // Add core sensors + Serial.println("Adding core sensors"); //DEBUG! + sensors.push_back(&fileSys); + sensors.push_back(&battery); + sensors.push_back(&logger); + + // Get vectors from sensor manager + talons = sensorManager.getAllTalons(); + Serial.println("Adding talons"); //DEBUG! + Serial.println(talons.size()); + for (auto* talon : talons) { + sensors.push_back(talon); + } + + auto configuredSensors = sensorManager.getAllSensors(); + Serial.println("Adding sensors"); //DEBUG! + for (auto* sensor : configuredSensors) { + sensors.push_back(sensor); + } + Serial.println(sensors.size()); //DEBUG! +} + +int getIndexOfPort(int port) { + for (int i = 0; i < talons.size(); i++) { + if (talons[i]->getTalonPort() == port) { + return i; + } + } + return -1; } \ No newline at end of file diff --git a/src/configuration/ConfigurationManager.cpp b/src/configuration/ConfigurationManager.cpp new file mode 100644 index 0000000..c6ed5ea --- /dev/null +++ b/src/configuration/ConfigurationManager.cpp @@ -0,0 +1,204 @@ +/** + * @file ConfigurationManager.cpp + * @brief Implementation of ConfigurationManager class + * + * © 2025 Regents of the University of Minnesota. All rights reserved. + */ + + #include "ConfigurationManager.h" + + ConfigurationManager::ConfigurationManager() {}; + + + bool ConfigurationManager::setConfiguration(std::string config) { + // Parse and apply the configuration + return parseConfiguration(config); + } + + std::string ConfigurationManager::getConfiguration() { + std::string config = "{\"config\":{"; + + // System configuration + config += "\"system\":{"; + config += "\"logPeriod\":" + std::to_string(m_logPeriod) + ","; + config += "\"backhaulCount\":" + std::to_string(m_backhaulCount) + ","; + config += "\"powerSaveMode\":" + std::to_string(m_powerSaveMode) + ","; + config += "\"loggingMode\":" + std::to_string(m_loggingMode); + config += "}}"; + + return config; + } + + int ConfigurationManager::updateSystemConfigurationUid() { + int tempUid = m_logPeriod << 16; + tempUid |= m_backhaulCount << 12; + tempUid |= m_powerSaveMode << 10; + tempUid |= m_loggingMode << 8; + tempUid |= m_numAuxTalons << 6; + tempUid |= m_numI2CTalons << 4; + tempUid |= m_numSDI12Talons << 2; + m_SystemConfigUid = tempUid; + return m_SystemConfigUid; + } + + int ConfigurationManager::updateSensorConfigurationUid() { + int tempUid = m_numET << 28; + tempUid |= m_numHaar << 24; + tempUid |= m_numSoil << 20; + tempUid |= m_numApogeeSolar << 16; + tempUid |= m_numCO2 << 12; + tempUid |= m_numO2 << 8; + tempUid |= m_numPressure << 4; + m_SensorConfigUid = tempUid; + return m_SensorConfigUid; + } + + + bool ConfigurationManager::parseConfiguration(const std::string& configStr) { + // Find the system configuration section + size_t systemStart = configStr.find("\"system\":{"); + if (systemStart != std::string::npos) { + size_t systemEnd = findMatchingBracket(configStr, systemStart + 9); + if (systemEnd > 0) { + std::string systemJson = configStr.substr(systemStart + 9, systemEnd - (systemStart + 9)); + + // Parse system settings + m_logPeriod = extractJsonIntField(systemJson, "logPeriod", 300); + m_backhaulCount = extractJsonIntField(systemJson, "backhaulCount", 4); + m_powerSaveMode = extractJsonIntField(systemJson, "powerSaveMode", 1); + m_loggingMode = extractJsonIntField(systemJson, "loggingMode", 0); + m_numAuxTalons = extractJsonIntField(systemJson, "numAuxTalons", 1); + m_numI2CTalons = extractJsonIntField(systemJson, "numI2CTalons", 1); + m_numSDI12Talons = extractJsonIntField(systemJson, "numSDI12Talons", 1); + + updateSystemConfigurationUid(); + } + } + // Find the sensor configuration section + size_t sensorsStart = configStr.find("\"sensors\":{"); + if (sensorsStart != std::string::npos) { + size_t sensorsEnd = findMatchingBracket(configStr, sensorsStart + 10); + if (sensorsEnd > 0) { + std::string sensorsJson = configStr.substr(sensorsStart + 10, sensorsEnd - (sensorsStart + 10)); + + // Parse sensor settings + m_numET = extractJsonIntField(sensorsJson, "numET", 0); + m_numHaar = extractJsonIntField(sensorsJson, "numHaar", 0); + m_numSoil = extractJsonIntField(sensorsJson, "numSoil", 3); + m_numApogeeSolar = extractJsonIntField(sensorsJson, "numApogeeSolar", 0); + m_numCO2 = extractJsonIntField(sensorsJson, "numCO2", 0); + m_numO2 = extractJsonIntField(sensorsJson, "numO2", 0); + m_numPressure = extractJsonIntField(sensorsJson, "numPressure", 0); + + updateSensorConfigurationUid(); + } + } + + return true; + } + + std::string ConfigurationManager::extractJsonField(const std::string& json, const std::string& fieldName) { + std::string searchStr = "\"" + fieldName + "\":"; + size_t fieldStart = json.find(searchStr); + if (fieldStart == std::string::npos) return ""; + + fieldStart += searchStr.length(); + // Skip whitespace + while (fieldStart < json.length() && isspace(json[fieldStart])) { + fieldStart++; + } + + // Check if string value + if (json[fieldStart] == '"') { + size_t valueEnd = json.find('"', fieldStart + 1); + if (valueEnd == std::string::npos) return ""; + return json.substr(fieldStart + 1, valueEnd - fieldStart - 1); + } + + // Must be numeric or boolean value + size_t valueEnd = fieldStart; + while (valueEnd < json.length() && + (isalnum(json[valueEnd]) || json[valueEnd] == '.' || json[valueEnd] == '-')) { + valueEnd++; + } + + return json.substr(fieldStart, valueEnd - fieldStart); + } + + int ConfigurationManager::extractJsonIntField(const std::string& json, const std::string& fieldName, int defaultValue) { + std::string value = extractJsonField(json, fieldName); + if (value.empty()) return defaultValue; + return atoi(value.c_str()); + } + + bool ConfigurationManager::extractJsonBoolField(const std::string& json, const std::string& fieldName, bool defaultValue) { + std::string value = extractJsonField(json, fieldName); + if (value.empty()) return defaultValue; + return (value == "true" || value == "True" || value == "TRUE" || value == "1"); + } + + int ConfigurationManager::findMatchingBracket(const std::string& str, int openPos) { + char open = str[openPos]; + char close; + + if (open == '{') close = '}'; + else if (open == '[') close = ']'; + else if (open == '(') close = ')'; + else return -1; + + int depth = 1; + for (size_t i = openPos + 1; i < str.length(); i++) { + if (str[i] == open) depth++; + else if (str[i] == close) { + depth--; + if (depth == 0) return i; + } + } + + return -1; // No matching bracket found + } + + // ConfigurationManager.cpp - Updated factory methods +std::unique_ptr ConfigurationManager::createAuxTalon() { + return std::make_unique(0, 0x14); // Default port and hardware version +} + +std::unique_ptr ConfigurationManager::createI2CTalon() { + return std::make_unique(0, 0x21); // Default port and hardware version +} + +std::unique_ptr ConfigurationManager::createSDI12Talon() { + return std::make_unique(0, 0x14); // Default port and hardware version +} + +std::unique_ptr ConfigurationManager::createHaarSensor() { + return std::make_unique(0, 0, 0x20); // Default ports and version +} + +std::unique_ptr ConfigurationManager::createO2Sensor(SDI12Talon& talon) { + return std::make_unique(talon, 0, 0); // Default ports and version +} + +std::unique_ptr ConfigurationManager::createSolarSensor(SDI12Talon& talon) { + return std::make_unique(talon, 0, 0); // Default ports and version +} + +std::unique_ptr ConfigurationManager::createSoilSensor(SDI12Talon& talon) { + return std::make_unique(talon, 0, 0); // Default ports and version +} + +std::unique_ptr ConfigurationManager::createCO2Sensor() { + return std::make_unique(0, 0, 0x10); // Default ports and version +} + +std::unique_ptr ConfigurationManager::createHumiditySensor() { + return std::make_unique(0, 0, 0x00); // Default ports and version +} + +std::unique_ptr ConfigurationManager::createETSensor(ITimeProvider& timeProvider, ISDI12Talon& talon) { + return std::make_unique(timeProvider, talon, 0, 0); // Default ports and version +} + +std::unique_ptr ConfigurationManager::createPressureSensor(SDI12Talon& talon) { + return std::make_unique(talon, 0, 0x00); // Default ports and version +} \ No newline at end of file diff --git a/src/configuration/ConfigurationManager.h b/src/configuration/ConfigurationManager.h new file mode 100644 index 0000000..a94ea9b --- /dev/null +++ b/src/configuration/ConfigurationManager.h @@ -0,0 +1,115 @@ +// ConfigurationManager.h - Updated to work with vectors +#ifndef CONFIGURATION_MANAGER_H +#define CONFIGURATION_MANAGER_H + +#include "IConfiguration.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ConfigurationManager : public IConfiguration { +public: + ConfigurationManager(); + ~ConfigurationManager() = default; + + // IConfiguration implementation + bool setConfiguration(std::string config) override; + std::string getConfiguration() override; + //{"config":{"system":{"logPeriod":300,"backhaulCount":4,"powerSaveMode":1,"loggingMode":0,"numAuxTalons":1,"numI2CTalons":1,"numSDI12Talons":1},"sensors":{"numET":0,"numHaar":0,"numSoil":3,"numApogeeSolar":0,"numCO2":0,"numO2":0,"numPressure":0}}} + std::string getDefaultConfigurationJson() const { + return "{\"config\":{" + "\"system\":{" + "\"logPeriod\":300," + "\"backhaulCount\":4," + "\"powerSaveMode\":1," + "\"loggingMode\":0," + "\"numAuxTalons\":1," + "\"numI2CTalons\":1," + "\"numSDI12Talons\":1" + "}," + "\"sensors\":{" + "\"numET\":0," + "\"numHaar\":0," + "\"numSoil\":3," + "\"numApogeeSolar\":0," + "\"numCO2\":0," + "\"numO2\":0," + "\"numPressure\":0" + "}" + "}}"; + } + int updateSystemConfigurationUid() override; + int updateSensorConfigurationUid() override; + + // System configuration getters + unsigned long getLogPeriod() const { return m_logPeriod; } + int getBackhaulCount() const { return m_backhaulCount; } + int getPowerSaveMode() const { return m_powerSaveMode; } + int getLoggingMode() const { return m_loggingMode; } + + // Sensor count getters + int getNumAuxTalons() const { return m_numAuxTalons; } + int getNumI2CTalons() const { return m_numI2CTalons; } + int getNumSDI12Talons() const { return m_numSDI12Talons; } + int getNumSoil() const { return m_numSoil; } + int getNumHaar() const { return m_numHaar; } + int getNumET() const { return m_numET; } + int getNumApogeeSolar() const { return m_numApogeeSolar; } + int getNumCO2() const { return m_numCO2; } + int getNumO2() const { return m_numO2; } + int getNumPressure() const { return m_numPressure; } + + // Static factory methods for creating sensors + static std::unique_ptr createAuxTalon(); + static std::unique_ptr createI2CTalon(); + static std::unique_ptr createSDI12Talon(); + static std::unique_ptr createHaarSensor(); + static std::unique_ptr createO2Sensor(SDI12Talon& talon); + static std::unique_ptr createSolarSensor(SDI12Talon& talon); + static std::unique_ptr createSoilSensor(SDI12Talon& talon); + static std::unique_ptr createCO2Sensor(); + static std::unique_ptr createHumiditySensor(); + static std::unique_ptr createETSensor(class ITimeProvider& timeProvider, class ISDI12Talon& talon); + static std::unique_ptr createPressureSensor(SDI12Talon& talon); + +private: + // System configuration + unsigned long m_logPeriod; + int m_backhaulCount; + int m_powerSaveMode; + int m_loggingMode; + + // Sensor counts + int m_numAuxTalons; + int m_numI2CTalons; + int m_numSDI12Talons; + int m_numET; + int m_numHaar; + int m_numSoil; + int m_numApogeeSolar; + int m_numCO2; + int m_numO2; + int m_numPressure; + + int m_SystemConfigUid; + int m_SensorConfigUid; + + // Internal methods + bool parseConfiguration(const std::string& config); + std::string extractJsonField(const std::string& json, const std::string& fieldName); + int extractJsonIntField(const std::string& json, const std::string& fieldName, int defaultValue); + bool extractJsonBoolField(const std::string& json, const std::string& fieldName, bool defaultValue); + int findMatchingBracket(const std::string& str, int openPos); +}; + +#endif // CONFIGURATION_MANAGER_H \ No newline at end of file diff --git a/src/configuration/IConfiguration.h b/src/configuration/IConfiguration.h new file mode 100644 index 0000000..70cc26d --- /dev/null +++ b/src/configuration/IConfiguration.h @@ -0,0 +1,28 @@ +/** + * @file IConfiguration.h + * @brief Interface for abstracting configuration functionality + * + * Defines a contract for configuring sensors for different applications + * + * © 2025 Regents of the University of Minnesota. All rights reserved. + */ + + #ifndef I_CONFIGURATION_H + #define I_CONFIGURATION_H + + #include + #include + + /** + * @brief Abstract interface for coinfiguration of sensors + */ + class IConfiguration { + public: + virtual ~IConfiguration() = default; + virtual bool setConfiguration(std::string config) = 0; + virtual std::string getConfiguration() = 0; + virtual int updateSystemConfigurationUid() = 0; + virtual int updateSensorConfigurationUid() = 0; +}; + + #endif // I_CONFIGURATION_H \ No newline at end of file diff --git a/src/configuration/SensorManager.cpp b/src/configuration/SensorManager.cpp new file mode 100644 index 0000000..6dfb2a9 --- /dev/null +++ b/src/configuration/SensorManager.cpp @@ -0,0 +1,138 @@ +// SensorManager.cpp - Implementation +#include "SensorManager.h" +#include "ConfigurationManager.h" + +SensorManager::SensorManager(ConfigurationManager& configManager) + : configManager(configManager) { +} + +void SensorManager::initializeSensors(ITimeProvider& timeProvider, ISDI12Talon& sdi12Interface) { + // Clear existing sensors + clearAllSensors(); + + // Create Talons + for (int i = 0; i < configManager.getNumAuxTalons(); i++) { + auxTalons.push_back(ConfigurationManager::createAuxTalon()); + } + + for (int i = 0; i < configManager.getNumI2CTalons(); i++) { + i2cTalons.push_back(ConfigurationManager::createI2CTalon()); + } + + for (int i = 0; i < configManager.getNumSDI12Talons(); i++) { + sdi12Talons.push_back(ConfigurationManager::createSDI12Talon()); + } + + // Create Sensors + for (int i = 0; i < configManager.getNumHaar(); i++) { + haarSensors.push_back(ConfigurationManager::createHaarSensor()); + } + + // For SDI12-based sensors, use the first available SDI12 talon + if (!sdi12Talons.empty()) { + SDI12Talon* firstSDI12Talon = sdi12Talons[0].get(); + + for (int i = 0; i < configManager.getNumO2(); i++) { + apogeeO2Sensors.push_back(ConfigurationManager::createO2Sensor(*firstSDI12Talon)); + } + + for (int i = 0; i < configManager.getNumApogeeSolar(); i++) { + apogeeSolarSensors.push_back(ConfigurationManager::createSolarSensor(*firstSDI12Talon)); + } + + for (int i = 0; i < configManager.getNumSoil(); i++) { + soilSensors.push_back(ConfigurationManager::createSoilSensor(*firstSDI12Talon)); + } + + for (int i = 0; i < configManager.getNumET(); i++) { + etSensors.push_back(ConfigurationManager::createETSensor(timeProvider, sdi12Interface)); + } + + for (int i = 0; i < configManager.getNumPressure(); i++) { + pressureSensors.push_back(ConfigurationManager::createPressureSensor(*firstSDI12Talon)); + } + } + + for (int i = 0; i < configManager.getNumCO2(); i++) { + gasSensors.push_back(ConfigurationManager::createCO2Sensor()); + } +} + +void SensorManager::clearAllSensors() { + auxTalons.clear(); + i2cTalons.clear(); + sdi12Talons.clear(); + haarSensors.clear(); + apogeeO2Sensors.clear(); + apogeeSolarSensors.clear(); + soilSensors.clear(); + gasSensors.clear(); + humiditySensors.clear(); + etSensors.clear(); + pressureSensors.clear(); +} + +std::vector SensorManager::getAllTalons() { + std::vector allTalons; + + for (auto& talon : auxTalons) { + allTalons.push_back(talon.get()); + } + + for (auto& talon : i2cTalons) { + allTalons.push_back(talon.get()); + } + + for (auto& talon : sdi12Talons) { + allTalons.push_back(talon.get()); + } + + return allTalons; +} + +std::vector SensorManager::getAllSensors() { + std::vector allSensors; + + // Add all sensor types to the vector + for (auto& sensor : haarSensors) { + allSensors.push_back(sensor.get()); + } + + for (auto& sensor : apogeeO2Sensors) { + allSensors.push_back(sensor.get()); + } + + for (auto& sensor : apogeeSolarSensors) { + allSensors.push_back(sensor.get()); + } + + for (auto& sensor : soilSensors) { + allSensors.push_back(sensor.get()); + } + + for (auto& sensor : gasSensors) { + allSensors.push_back(sensor.get()); + } + + for (auto& sensor : humiditySensors) { + allSensors.push_back(sensor.get()); + } + + for (auto& sensor : etSensors) { + allSensors.push_back(sensor.get()); + } + + for (auto& sensor : pressureSensors) { + allSensors.push_back(sensor.get()); + } + + return allSensors; +} + +int SensorManager::getTotalSensorCount() const { + // Core sensors (3) + Talons + Other Sensors + return 3 + auxTalons.size() + i2cTalons.size() + sdi12Talons.size() + + haarSensors.size() + apogeeO2Sensors.size() + apogeeSolarSensors.size() + + soilSensors.size() + gasSensors.size() + humiditySensors.size() + + etSensors.size() + pressureSensors.size(); +} \ No newline at end of file diff --git a/src/configuration/SensorManager.h b/src/configuration/SensorManager.h new file mode 100644 index 0000000..1834e83 --- /dev/null +++ b/src/configuration/SensorManager.h @@ -0,0 +1,66 @@ +// SensorManager.h - New class to manage sensor vectors +#ifndef SENSOR_MANAGER_H +#define SENSOR_MANAGER_H + +#include +#include + +// Forward declarations +class Sensor; +class Talon; +class AuxTalon; +class I2CTalon; +class SDI12Talon; +class ConfigurationManager; +class ITimeProvider; +class ISDI12Talon; + +class SensorManager { +public: + SensorManager(ConfigurationManager& configManager); + ~SensorManager() = default; + + // Initialize all sensor vectors based on configuration + void initializeSensors(ITimeProvider& timeProvider, ISDI12Talon& sdi12Interface); + + // Clear all sensors + void clearAllSensors(); + + // Getters for sensor vectors + const std::vector>& getAuxTalons() const { return auxTalons; } + const std::vector>& getI2CTalons() const { return i2cTalons; } + const std::vector>& getSDI12Talons() const { return sdi12Talons; } + + std::vector>& getAuxTalons() { return auxTalons; } + std::vector>& getI2CTalons() { return i2cTalons; } + std::vector>& getSDI12Talons() { return sdi12Talons; } + + // Get all talons as a single vector of base pointers + std::vector getAllTalons(); + + // Get all sensors as a single vector (excluding core sensors) + std::vector getAllSensors(); + + // Get total sensor count (including core sensors) + int getTotalSensorCount() const; + +private: + ConfigurationManager& configManager; + + // Talon vectors + std::vector> auxTalons; + std::vector> i2cTalons; + std::vector> sdi12Talons; + + // Sensor vectors + std::vector> haarSensors; + std::vector> apogeeO2Sensors; + std::vector> apogeeSolarSensors; + std::vector> soilSensors; + std::vector> gasSensors; + std::vector> humiditySensors; + std::vector> etSensors; + std::vector> pressureSensors; +}; + +#endif // SENSOR_MANAGER_H \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 061bba2..12f35af 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -51,6 +51,14 @@ add_executable(unit_tests # Li710 tests unit/Driver_-_Li710/Li710Test.cpp ${CMAKE_SOURCE_DIR}/lib/Driver_-_Li710/src/Li710.cpp + + # ConfigurationManger tests + #unit/ConfigurationManager/ConfigurationManagerTest.cpp + #${CMAKE_SOURCE_DIR}/src/configuration/ConfigurationManager.cpp + + # SensorManager Tests + #unit/SensorManager/SensorManagerTest.cpp + #${CMAKE_SOURCE_DIR}/src/configuration/SensorManager.cpp ) # Link against mocks and GoogleTest diff --git a/test/mocks/MockConfiguration.h b/test/mocks/MockConfiguration.h new file mode 100644 index 0000000..bdab594 --- /dev/null +++ b/test/mocks/MockConfiguration.h @@ -0,0 +1,24 @@ +/** + * @file MockConfiguration.h + * @brief Mock implementation of the IConfiguration interface for testing + * + * © 2025 Regents of the University of Minnesota. All rights reserved. + */ + +#ifndef MOCK_CONFIGURATION_H +#define MOCK_CONFIGURATION_H + +#include +#include +#include "../../src/configuration/IConfiguration.h" + +/** + * @brief Mock implementation of the IConfiguration interface for testing + */ +class MockConfiguration : public IConfiguration { +public: + MOCK_METHOD(bool, setConfiguration, (std::string config), (override)); + MOCK_METHOD(std::string, getConfiguration, (), (override)); +}; + +#endif // MOCK_CONFIGURATION_H \ No newline at end of file diff --git a/test/unit/ConfigurationManager/ConfigurationManagerTest.cpp b/test/unit/ConfigurationManager/ConfigurationManagerTest.cpp new file mode 100644 index 0000000..d468193 --- /dev/null +++ b/test/unit/ConfigurationManager/ConfigurationManagerTest.cpp @@ -0,0 +1,242 @@ +#include +#include +#include "configuration/ConfigurationManager.h" + +class ConfigurationManagerTest : public ::testing::Test { +protected: + ConfigurationManager configManager; + + void SetUp() override { + // Reset to known state before each test + } +}; + +// Test default configuration +TEST_F(ConfigurationManagerTest, DefaultConfiguration) { + // Get default configuration + std::string defaultConfig = configManager.getDefaultConfigurationJson(); + + // Apply default configuration + EXPECT_TRUE(configManager.setConfiguration(defaultConfig)); + + // Check default values + EXPECT_EQ(configManager.getLogPeriod(), 300); // Default log period + EXPECT_EQ(configManager.getBackhaulCount(), 4); // Default backhaul count + EXPECT_EQ(configManager.getPowerSaveMode(), 1); // Default power save mode + EXPECT_EQ(configManager.getLoggingMode(), 0); // Default logging mode + + // Check default talon counts + EXPECT_EQ(configManager.getNumAuxTalons(), 1); + EXPECT_EQ(configManager.getNumI2CTalons(), 1); + EXPECT_EQ(configManager.getNumSDI12Talons(), 1); + + // Check default sensor counts + EXPECT_EQ(configManager.getNumET(), 0); + EXPECT_EQ(configManager.getNumHaar(), 0); + EXPECT_EQ(configManager.getNumSoil(), 3); + EXPECT_EQ(configManager.getNumApogeeSolar(), 0); + EXPECT_EQ(configManager.getNumCO2(), 0); + EXPECT_EQ(configManager.getNumO2(), 0); + EXPECT_EQ(configManager.getNumPressure(), 0); +} + +// Test custom configuration +TEST_F(ConfigurationManagerTest, CustomConfiguration) { + // Create custom configuration + std::string customConfig = + "{\"config\":{" + "\"system\":{" + "\"logPeriod\":600," + "\"backhaulCount\":10," + "\"powerSaveMode\":2," + "\"loggingMode\":1," + "\"numAuxTalons\":2," + "\"numI2CTalons\":2," + "\"numSDI12Talons\":2" + "}," + "\"sensors\":{" + "\"numET\":1," + "\"numHaar\":2," + "\"numSoil\":4," + "\"numApogeeSolar\":1," + "\"numCO2\":1," + "\"numO2\":1," + "\"numPressure\":1" + "}" + "}}"; + + // Apply custom configuration + EXPECT_TRUE(configManager.setConfiguration(customConfig)); + + // Check custom values + EXPECT_EQ(configManager.getLogPeriod(), 600); + EXPECT_EQ(configManager.getBackhaulCount(), 10); + EXPECT_EQ(configManager.getPowerSaveMode(), 2); + EXPECT_EQ(configManager.getLoggingMode(), 1); + + // Check custom talon counts + EXPECT_EQ(configManager.getNumAuxTalons(), 2); + EXPECT_EQ(configManager.getNumI2CTalons(), 2); + EXPECT_EQ(configManager.getNumSDI12Talons(), 2); + + // Check custom sensor counts + EXPECT_EQ(configManager.getNumET(), 1); + EXPECT_EQ(configManager.getNumHaar(), 2); + EXPECT_EQ(configManager.getNumSoil(), 4); + EXPECT_EQ(configManager.getNumApogeeSolar(), 1); + EXPECT_EQ(configManager.getNumCO2(), 1); + EXPECT_EQ(configManager.getNumO2(), 1); + EXPECT_EQ(configManager.getNumPressure(), 1); +} + +// Test invalid configuration handling +TEST_F(ConfigurationManagerTest, InvalidConfiguration) { + // Set default configuration first to have known state + configManager.setConfiguration(configManager.getDefaultConfigurationJson()); + + // Try invalid configuration + std::string invalidConfig = "{\"not_config\":{}}"; + EXPECT_TRUE(configManager.setConfiguration(invalidConfig)); + + // Values should remain unchanged + EXPECT_EQ(configManager.getLogPeriod(), 300); + EXPECT_EQ(configManager.getBackhaulCount(), 4); +} + +// Test partial configuration +TEST_F(ConfigurationManagerTest, PartialConfiguration) { + // Apply default configuration + configManager.setConfiguration(configManager.getDefaultConfigurationJson()); + + // Apply partial configuration (only modifying system) + std::string partialConfig = + "{\"config\":{" + "\"system\":{" + "\"logPeriod\":900," + "\"backhaulCount\":8" + "}" + "}}"; + + EXPECT_TRUE(configManager.setConfiguration(partialConfig)); + + // Check that specified values changed + EXPECT_EQ(configManager.getLogPeriod(), 900); + EXPECT_EQ(configManager.getBackhaulCount(), 8); + + // Check that unspecified values remain at defaults + EXPECT_EQ(configManager.getPowerSaveMode(), 1); + EXPECT_EQ(configManager.getLoggingMode(), 0); + EXPECT_EQ(configManager.getNumSoil(), 3); +} + +// Test configuration UID updates +TEST_F(ConfigurationManagerTest, ConfigurationUIDs) { + // Apply default configuration + configManager.setConfiguration(configManager.getDefaultConfigurationJson()); + + // Get initial UIDs + int initialSystemUID = configManager.updateSystemConfigurationUid(); + int initialSensorUID = configManager.updateSensorConfigurationUid(); + + // Modify system configuration + std::string systemConfig = + "{\"config\":{" + "\"system\":{" + "\"logPeriod\":1200" + "}" + "}}"; + + configManager.setConfiguration(systemConfig); + + // System UID should change, sensor UID should remain the same + int newSystemUID = configManager.updateSystemConfigurationUid(); + int newSensorUID = configManager.updateSensorConfigurationUid(); + + EXPECT_NE(initialSystemUID, newSystemUID); + EXPECT_EQ(initialSensorUID, newSensorUID); + + // Modify sensor configuration + std::string sensorConfig = + "{\"config\":{" + "\"sensors\":{" + "\"numSoil\":5" + "}" + "}}"; + + configManager.setConfiguration(sensorConfig); + + // Sensor UID should change + int finalSensorUID = configManager.updateSensorConfigurationUid(); + EXPECT_NE(newSensorUID, finalSensorUID); +} + +// Test configuration serialization +TEST_F(ConfigurationManagerTest, ConfigurationSerialization) { + // Apply custom configuration + std::string customConfig = + "{\"config\":{" + "\"system\":{" + "\"logPeriod\":600," + "\"backhaulCount\":10," + "\"powerSaveMode\":2," + "\"loggingMode\":1" + "}" + "}}"; + + configManager.setConfiguration(customConfig); + + // Get serialized configuration + std::string serialized = configManager.getConfiguration(); + + // Parse it back + ConfigurationManager newManager; + newManager.setConfiguration(serialized); + + // Values should match + EXPECT_EQ(newManager.getLogPeriod(), 600); + EXPECT_EQ(newManager.getBackhaulCount(), 10); + EXPECT_EQ(newManager.getPowerSaveMode(), 2); + EXPECT_EQ(newManager.getLoggingMode(), 1); +} + +// Test Factory Methods +TEST_F(ConfigurationManagerTest, FactoryMethods) { + // Test that factory methods create non-null objects + auto auxTalon = ConfigurationManager::createAuxTalon(); + EXPECT_NE(auxTalon, nullptr); + + auto i2cTalon = ConfigurationManager::createI2CTalon(); + EXPECT_NE(i2cTalon, nullptr); + + auto sdi12Talon = ConfigurationManager::createSDI12Talon(); + EXPECT_NE(sdi12Talon, nullptr); + + auto haarSensor = ConfigurationManager::createHaarSensor(); + EXPECT_NE(haarSensor, nullptr); + + // For sensors that need talons, we need to pass in real talons + auto sdi12TalonObj = ConfigurationManager::createSDI12Talon(); + + auto o2Sensor = ConfigurationManager::createO2Sensor(*sdi12TalonObj); + EXPECT_NE(o2Sensor, nullptr); + + auto solarSensor = ConfigurationManager::createSolarSensor(*sdi12TalonObj); + EXPECT_NE(solarSensor, nullptr); + + auto soilSensor = ConfigurationManager::createSoilSensor(*sdi12TalonObj); + EXPECT_NE(soilSensor, nullptr); + + auto co2Sensor = ConfigurationManager::createCO2Sensor(); + EXPECT_NE(co2Sensor, nullptr); + + auto humiditySensor = ConfigurationManager::createHumiditySensor(); + EXPECT_NE(humiditySensor, nullptr); + + // For ET sensor, we need real time provider and SDI12 talon + // This would require mocking in a real test + // auto etSensor = ConfigurationManager::createETSensor(timeProvider, sdi12Interface); + // EXPECT_NE(etSensor, nullptr); + + auto pressureSensor = ConfigurationManager::createPressureSensor(*sdi12TalonObj); + EXPECT_NE(pressureSensor, nullptr); +} \ No newline at end of file diff --git a/test/unit/SensorManager/SensorManagerTest.cpp b/test/unit/SensorManager/SensorManagerTest.cpp new file mode 100644 index 0000000..89f4ac0 --- /dev/null +++ b/test/unit/SensorManager/SensorManagerTest.cpp @@ -0,0 +1,187 @@ +#include +#include +#include "configuration/SensorManager.h" +#include "MockConfiguration.h" +#include "MockTimeProvider.h" +#include "MockSDI12Talon.h" + +class SensorManagerTest : public ::testing::Test { +protected: + MockConfiguration mockConfig; + SensorManager sensorManager{mockConfig}; + MockTimeProvider mockTimeProvider; + MockSDI12Talon mockSDI12Talon; + + void SetUp() override { + // Apply a known configuration + std::string testConfig = + "{\"config\":{" + "\"system\":{" + "\"logPeriod\":300," + "\"backhaulCount\":4," + "\"powerSaveMode\":1," + "\"loggingMode\":0," + "\"numAuxTalons\":1," + "\"numI2CTalons\":1," + "\"numSDI12Talons\":1" + "}," + "\"sensors\":{" + "\"numET\":1," + "\"numHaar\":1," + "\"numSoil\":2," + "\"numApogeeSolar\":1," + "\"numCO2\":1," + "\"numO2\":1," + "\"numPressure\":1" + "}" + "}}"; + + configManager.setConfiguration(testConfig); + } +}; + +// Test initialization of sensors based on configuration +TEST_F(SensorManagerTest, InitializeSensors) { + // Initialize sensors + sensorManager.initializeSensors(mockTimeProvider, mockSDI12Talon); + + // Check that talons were created + EXPECT_EQ(sensorManager.getAuxTalons().size(), 1); + EXPECT_EQ(sensorManager.getI2CTalons().size(), 1); + EXPECT_EQ(sensorManager.getSDI12Talons().size(), 1); + + // Check that sensors were created + auto allSensors = sensorManager.getAllSensors(); + // Expected: 1 ET + 1 Haar + 2 Soil + 1 Solar + 1 CO2 + 1 O2 + 1 Pressure = 8 + EXPECT_EQ(allSensors.size(), 8); + + // Check total sensor count (including 3 core sensors) + EXPECT_EQ(sensorManager.getTotalSensorCount(), 11); +} + +// Test clearing all sensors +TEST_F(SensorManagerTest, ClearAllSensors) { + // Initialize sensors + sensorManager.initializeSensors(mockTimeProvider, mockSDI12Talon); + + // Clear all sensors + sensorManager.clearAllSensors(); + + // Check that all collections are empty + EXPECT_EQ(sensorManager.getAuxTalons().size(), 0); + EXPECT_EQ(sensorManager.getI2CTalons().size(), 0); + EXPECT_EQ(sensorManager.getSDI12Talons().size(), 0); + EXPECT_EQ(sensorManager.getAllSensors().size(), 0); + EXPECT_EQ(sensorManager.getTotalSensorCount(), 3); // Core sensors still counted +} + +// Test getting all talons +TEST_F(SensorManagerTest, GetAllTalons) { + // Initialize sensors + sensorManager.initializeSensors(mockTimeProvider, mockSDI12Talon); + + // Get all talons + auto allTalons = sensorManager.getAllTalons(); + + // Expected: 1 AuxTalon + 1 I2CTalon + 1 SDI12Talon = 3 + EXPECT_EQ(allTalons.size(), 3); + + // Check that all talons are non-null + for (auto talon : allTalons) { + EXPECT_NE(talon, nullptr); + } +} + +// Test handling of configuration changes +TEST_F(SensorManagerTest, ConfigurationChanges) { + // Initialize with current configuration + sensorManager.initializeSensors(mockTimeProvider, mockSDI12Talon); + + // Check initial state + EXPECT_EQ(sensorManager.getAuxTalons().size(), 1); + EXPECT_EQ(sensorManager.getAllSensors().size(), 8); + + // Change configuration + std::string newConfig = + "{\"config\":{" + "\"system\":{" + "\"logPeriod\":300," + "\"backhaulCount\":4," + "\"powerSaveMode\":1," + "\"loggingMode\":0," + "\"numAuxTalons\":2," + "\"numI2CTalons\":1," + "\"numSDI12Talons\":1" + "}," + "\"sensors\":{" + "\"numET\":0," + "\"numHaar\":0," + "\"numSoil\":5," + "\"numApogeeSolar\":0," + "\"numCO2\":0," + "\"numO2\":0," + "\"numPressure\":0" + "}" + "}}"; + + configManager.setConfiguration(newConfig); + + // Re-initialize + sensorManager.initializeSensors(mockTimeProvider, mockSDI12Talon); + + // Check that counts have changed + EXPECT_EQ(sensorManager.getAuxTalons().size(), 2); + EXPECT_EQ(sensorManager.getAllSensors().size(), 5); // Only soil sensors now +} + +// Test sensor and talon relationship +TEST_F(SensorManagerTest, SensorTalonRelationship) { + // Initialize with a minimal configuration + std::string minConfig = + "{\"config\":{" + "\"system\":{" + "\"numAuxTalons\":0," + "\"numI2CTalons\":0," + "\"numSDI12Talons\":1" + "}," + "\"sensors\":{" + "\"numET\":0," + "\"numHaar\":0," + "\"numSoil\":1," + "\"numApogeeSolar\":0," + "\"numCO2\":0," + "\"numO2\":0," + "\"numPressure\":0" + "}" + "}}"; + + configManager.setConfiguration(minConfig); + sensorManager.initializeSensors(mockTimeProvider, mockSDI12Talon); + + // Check that we have one SDI12 talon and one soil sensor + EXPECT_EQ(sensorManager.getSDI12Talons().size(), 1); + EXPECT_EQ(sensorManager.getAllSensors().size(), 1); + + // The soil sensor should be created using the first SDI12 talon + // This relationship is maintained internally and can't be easily tested + // without additional modifications to the code to expose this relationship +} + +// Test handling of empty configuration +TEST_F(SensorManagerTest, EmptyConfiguration) { + // Apply an empty configuration (only system structure, no values) + std::string emptyConfig = "{\"config\":{\"system\":{},\"sensors\":{}}}"; + configManager.setConfiguration(emptyConfig); + + // Initialize sensors + sensorManager.initializeSensors(mockTimeProvider, mockSDI12Talon); + + // Should fall back to defaults + EXPECT_EQ(sensorManager.getAuxTalons().size(), 1); + EXPECT_EQ(sensorManager.getI2CTalons().size(), 1); + EXPECT_EQ(sensorManager.getSDI12Talons().size(), 1); + + // Default sensor counts from ConfigurationManager + auto allSensors = sensorManager.getAllSensors(); + EXPECT_EQ(allSensors.size(), 3); // Default 3 soil sensors +} \ No newline at end of file