diff --git a/lib/Driver_-_SDI12AnalogMux/library.json b/lib/Driver_-_SDI12AnalogMux/library.json new file mode 100644 index 0000000..795730a --- /dev/null +++ b/lib/Driver_-_SDI12AnalogMux/library.json @@ -0,0 +1,11 @@ +{ + "name": "SDI12AnalogMux", + "keywords": "sdi12, temperature, rtd, pt100, sensor, analog, multiplexer", + "description": "Driver for Pico-based SDI-12 PT100 analog multiplexer temperature sensor with 7 RTD channels plus internal temperature monitoring.", + "version": "1.0.0", + "frameworks": "arduino", + "dependencies": { + "Driver_-_Sensor": "*", + "Driver_-_Talon-SDI12": "*" + } +} diff --git a/lib/Driver_-_SDI12AnalogMux/src/SDI12AnalogMux.cpp b/lib/Driver_-_SDI12AnalogMux/src/SDI12AnalogMux.cpp new file mode 100644 index 0000000..1f0e403 --- /dev/null +++ b/lib/Driver_-_SDI12AnalogMux/src/SDI12AnalogMux.cpp @@ -0,0 +1,239 @@ +//© 2025 Regents of the University of Minnesota. All rights reserved. + +#include + +SDI12AnalogMux::SDI12AnalogMux(SDI12Talon& talon_, uint8_t talonPort_, uint8_t sensorPort_, uint8_t version): talon(talon_) +{ + //Only update values if they are in range, otherwise stick with default values + if(talonPort_ > 0) talonPort = talonPort_ - 1; + else talonPort = 255; //Reset to null default if not in range + if(sensorPort_ > 0) sensorPort = sensorPort_ - 1; + else sensorPort = 255; //Reset to null default if not in range + sensorInterface = BusType::SDI12; +} + +String SDI12AnalogMux::begin(time_t time, bool &criticalFault, bool &fault) +{ + return ""; //No special initialization required +} + +String SDI12AnalogMux::selfDiagnostic(uint8_t diagnosticLevel, time_t time) +{ + if(getSensorPort() == 0) throwError(FIND_FAIL); //If no port found, report failure + else if(isPresent() == false) throwError(DETECT_FAIL); //If sensor port is good, but fail to detect sensor, throw error + String output = "\"SDI12AnalogMux\":{"; + if(diagnosticLevel == 0) { + //TBD + } + + if(diagnosticLevel <= 1) { + //TBD + } + + if(diagnosticLevel <= 2) { + //TBD + } + + if(diagnosticLevel <= 3) { + //TBD + } + + if(diagnosticLevel <= 4) { + //TBD + } + + if(diagnosticLevel <= 5) { + if(getSensorPort() != 0 && isPresent() == true) { //Test as normal + String adr = talon.sendCommand("?!"); + int adrVal = adr.toInt(); + output = output + "\"Adr\":"; + if(adr.equals("") || (!adr.equals("0") && adrVal == 0)) output = output + "null"; //If no return, report null + else output = output + adr; //Otherwise report the read value + output = output + ","; + } + else output = output + "\"Adr\":null,"; //Else append null string + + } + return output + "\"Pos\":[" + getTalonPortString() + "," + getSensorPortString() + "]}"; //Write position in logical form - Return completed closed output +} + +String SDI12AnalogMux::getMetadata() +{ + uint8_t adr = (talon.sendCommand("?!")).toInt(); //Get address of local device + String id = talon.command("I", adr); + Serial.println(id); //DEBUG! + String sdi12Version; + String mfg; + String model; + String senseVersion; + String sn; + if((id.substring(0, 1)).toInt() != adr) { //If address returned is not the same as the address read, throw error + Serial.println("ADDRESS MISMATCH!"); //DEBUG! + //Throw error! + sdi12Version = "null"; + mfg = "null"; + model = "null"; + senseVersion = "null"; + sn = "null"; + } + else { //Standard across SDI-12 devices + sdi12Version = (id.substring(1,3)).trim(); //Grab SDI-12 version code + mfg = (id.substring(3, 11)).trim(); //Grab manufacturer + model = (id.substring(11,17)).trim(); //Grab sensor model name + senseVersion = (id.substring(17,20)).trim(); //Grab version number + sn = (id.substring(20,33)).trim(); //Grab the serial number + } + String metadata = "\"SDI12AnalogMux\":{"; + metadata = metadata + "\"Hardware\":\"" + senseVersion + "\","; //Report sensor version pulled from SDI-12 system + metadata = metadata + "\"Firmware\":\"" + FIRMWARE_VERSION + "\","; //Static firmware version + metadata = metadata + "\"SDI12_Ver\":\"" + sdi12Version.substring(0,1) + "." + sdi12Version.substring(1,2) + "\","; + metadata = metadata + "\"ADR\":" + String(adr) + ","; + metadata = metadata + "\"Mfg\":\"" + mfg + "\","; + metadata = metadata + "\"Model\":\"" + model + "\","; + metadata = metadata + "\"SN\":\"" + sn + "\","; + metadata = metadata + "\"Pos\":[" + getTalonPortString() + "," + getSensorPortString() + "]"; //Concatenate position + metadata = metadata + "}"; //CLOSE + return metadata; +} + +String SDI12AnalogMux::getData(time_t time) +{ + String output = "\"SDI12AnalogMux\":{"; //OPEN JSON BLOB + bool readDone = false; + delay(2500); //Give sensor time to initialize (ADC + RTD config + SDI-12 setup) + + // Arrays to store temperature readings for all 9 channels + float temperatures[9] = {-9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999, -9999}; //Initialize with error values + bool channelSuccess[9] = {false, false, false, false, false, false, false, false, false}; + + if(getSensorPort() != 0) { //Check for valid port + int adr = -1; + + // First, get the sensor address (only need to do this once) + for(int retry = 0; retry < talon.retryCount; retry++) { + if(!isPresent()) continue; //If presence check fails, try again + adr = talon.getAddress(); + if(adr >= 0) break; //Address found successfully + } + + if(adr >= 0) { + // Read all 9 temperature channels (M1-M9) + for(int channel = 1; channel <= 9; channel++) { + for(int retry = 0; retry < talon.retryCount; retry++) { + // Send measurement command (aM1! through aM9!) + int waitTime = talon.startMeasurmentIndex(channel, adr); + if(waitTime < 0) { + continue; //If wait time invalid, try again + } + + // Wait for measurement to complete (sensor returns 0 seconds, but add small buffer) + delay(waitTime*1000 + 100); //Wait requested time plus 100ms buffer + + // Retrieve data with D0 command + String data = talon.command("D0", adr); + + // Parse the temperature value + if(parseTemperature(data, temperatures[channel-1])) { + channelSuccess[channel-1] = true; + readDone = true; + break; //Successfully read this channel, move to next + } + } + } + } + + if(readDone == false) throwError(talon.SDI12_READ_FAIL); //Throw error if no channels read successfully + } + else throwError(FIND_FAIL); + + // Build JSON output with all 9 channels + output = output + appendData(temperatures[0], "RTD1_Temp", 2, true); + output = output + appendData(temperatures[1], "RTD2_Temp", 2, true); + output = output + appendData(temperatures[2], "RTD3_Temp", 2, true); + output = output + appendData(temperatures[3], "RTD4_Temp", 2, true); + output = output + appendData(temperatures[4], "RTD5_Temp", 2, true); + output = output + appendData(temperatures[5], "RTD6_Temp", 2, true); + output = output + appendData(temperatures[6], "RTD7_Temp", 2, true); + output = output + appendData(temperatures[7], "Pico_Temp", 2, true); + output = output + appendData(temperatures[8], "ADC_Temp", 2, false); //Last entry, no trailing comma + + output = output + ",\"Pos\":[" + getTalonPortString() + "," + getSensorPortString() + "]"; //Concatenate position + output = output + "}"; //CLOSE JSON BLOB + Serial.println(output); //DEBUG! + return output; +} + +bool SDI12AnalogMux::isPresent() +{ + uint8_t adr = (talon.sendCommand("?!")).toInt(); + + String id = talon.command("I", adr); + id.remove(0, 1); //Trim address character from start + Serial.print("SDI12 Address: "); //DEBUG! + Serial.print(adr); + Serial.print(","); + Serial.println(id); + // Check for either "GEMS" (vendor) or "GORGON" (model) in identification string + if(id.indexOf("GEMS") > 0 || id.indexOf("GORGON") > 0) return true; + else return false; +} + +String SDI12AnalogMux::appendData(float data, String label, uint8_t precision, bool appendComma) +{ + String val = ""; + if(data == -9999 || data == 9999999) val = "\"" + label + "\":null"; //Append null if value is error indicator + else val = "\"" + label + "\":" + String(data, precision); //Otherwise, append as normal using fixed specified precision + + if(appendComma) return val + ","; + else return val; +} + +bool SDI12AnalogMux::parseTemperature(String input, float &temperature) +{ + // Expected format: "a±xx.xx" where a is address + // Example: "0+25.43\r\n" + + if(input.length() < 3) return false; //String too short to be valid + + // Remove address character from start + input.remove(0, 1); + input.trim(); //Remove any trailing whitespace/CR/LF + + // Check if string starts with + or - sign + if(input.charAt(0) != '+' && input.charAt(0) != '-') return false; + + // Parse the float value + temperature = input.toFloat(); + + // Basic sanity check: PT100 sensors typically measure -200°C to +850°C + // But for this application, reasonable range is likely -50 to +150°C + if(temperature < -200.0 || temperature > 1000.0) { + temperature = -9999; //Set to error value + return false; + } + + return true; +} + +String SDI12AnalogMux::getErrors() +{ + String output = "\"SDI12AnalogMux\":{"; // OPEN JSON BLOB + output = output + "\"CODES\":["; //Open codes pair + + for(int i = 0; i < min(MAX_NUM_ERRORS, numErrors); i++) { //Iterate over used element of array without exceeding bounds + output = output + "\"0x" + String(errors[i], HEX) + "\","; //Add each error code + errors[i] = 0; //Clear errors as they are read + } + if(output.substring(output.length() - 1).equals(",")) { + output = output.substring(0, output.length() - 1); //Trim trailing ',' + } + output = output + "],"; //close codes pair + output = output + "\"OW\":"; //Open state pair + if(numErrors > MAX_NUM_ERRORS) output = output + "1,"; //If overwritten, indicate the overwrite is true + else output = output + "0,"; //Otherwise set it as clear + output = output + "\"NUM\":" + String(numErrors) + ","; //Append number of errors + output = output + "\"Pos\":[" + getTalonPortString() + "," + getSensorPortString() + "]"; //Concatenate position + output = output + "}"; //CLOSE JSON BLOB + numErrors = 0; //Clear error count + return output; +} diff --git a/lib/Driver_-_SDI12AnalogMux/src/SDI12AnalogMux.h b/lib/Driver_-_SDI12AnalogMux/src/SDI12AnalogMux.h new file mode 100644 index 0000000..a51728c --- /dev/null +++ b/lib/Driver_-_SDI12AnalogMux/src/SDI12AnalogMux.h @@ -0,0 +1,34 @@ +//© 2025 Regents of the University of Minnesota. All rights reserved. + +#ifndef SDI12ANALOGMUX_h +#define SDI12ANALOGMUX_h + +#include +#include + +class SDI12AnalogMux: public Sensor +{ + constexpr static int DEFAULT_PORT = 2; /// #include #include +#include #include #include #include @@ -1272,12 +1273,14 @@ int detectTalons(String dummyStr) // 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! + 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(">>> FlightControl: Talon begin() returned"); //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 + //delay(10000); //DEBUG! + logger.enableData(talons[i]->getTalonPort(), false); //Turn data back off to prevent conflict + Serial.println(">>> FlightControl: Disabled data, exiting detectTalons loop"); //DEBUG! //Serial.println("ENABLE DATA DONE"); //DEBUG! // Serial.flush(); //DEBUG! //delay(10000); //DEBUG! @@ -1289,6 +1292,7 @@ int detectTalons(String dummyStr) int detectSensors(String dummyStr) { + Serial.println(">>> FlightControl: detectSensors() START - Power should still be ON from detectTalons"); //DEBUG! /////////////// SENSOR AUTO DETECTION ////////////////////// for(int t = 0; t < talons.size(); t++) { //Iterate over each Talon // Serial.println(talons[t]->talonInterface); //DEBUG! diff --git a/src/configuration/ConfigurationManager.cpp b/src/configuration/ConfigurationManager.cpp index dab33b9..36db466 100644 --- a/src/configuration/ConfigurationManager.cpp +++ b/src/configuration/ConfigurationManager.cpp @@ -57,9 +57,10 @@ config += "\"numApogeeSolar\":" + std::to_string(m_numApogeeSolar) + ","; config += "\"numCO2\":" + std::to_string(m_numCO2) + ","; config += "\"numO2\":" + std::to_string(m_numO2) + ","; - config += "\"numPressure\":" + std::to_string(m_numPressure); + config += "\"numPressure\":" + std::to_string(m_numPressure) + ","; + config += "\"numAnalogMux\":" + std::to_string(m_numAnalogMux); config += "}}}"; - + return config; } @@ -83,6 +84,7 @@ tempUid |= m_numCO2 << 12; tempUid |= m_numO2 << 8; tempUid |= m_numPressure << 4; + tempUid |= m_numAnalogMux; m_SensorConfigUid = tempUid; return m_SensorConfigUid; } @@ -129,7 +131,8 @@ m_numCO2 = extractJsonIntField(sensorsJson, "numCO2", 0); m_numO2 = extractJsonIntField(sensorsJson, "numO2", 0); m_numPressure = extractJsonIntField(sensorsJson, "numPressure", 0); - + m_numAnalogMux = extractJsonIntField(sensorsJson, "numAnalogMux", 0); + updateSensorConfigurationUid(); } } @@ -243,6 +246,10 @@ std::unique_ptr ConfigurationManager::createPressureSensor(SDI12Talon return std::make_unique(talon, 0, 0x00); // Default ports and version } +std::unique_ptr ConfigurationManager::createAnalogMuxSensor(SDI12Talon& talon) { + return std::make_unique(talon, 0, 0, 0x00); // Default ports and version +} + // EEPROM backup functionality bool ConfigurationManager::saveConfigToEEPROM() { // Write system and sensor UIDs (they encode all the config values) @@ -285,7 +292,8 @@ bool ConfigurationManager::loadConfigFromEEPROM() { m_numCO2 = (sensorUid >> 12) & 0xF; m_numO2 = (sensorUid >> 8) & 0xF; m_numPressure = (sensorUid >> 4) & 0xF; - + m_numAnalogMux = sensorUid & 0xF; + // Store the UIDs m_SystemConfigUid = systemUid; m_SensorConfigUid = sensorUid; diff --git a/src/configuration/ConfigurationManager.h b/src/configuration/ConfigurationManager.h index 28fcd5d..8d2ec01 100644 --- a/src/configuration/ConfigurationManager.h +++ b/src/configuration/ConfigurationManager.h @@ -16,6 +16,7 @@ #include #include #include +#include class ConfigurationManager : public IConfiguration { public: @@ -44,7 +45,8 @@ class ConfigurationManager : public IConfiguration { "\"numApogeeSolar\":0," "\"numCO2\":0," "\"numO2\":0," - "\"numPressure\":0" + "\"numPressure\":0," + "\"numAnalogMux\":0" "}" "}}"; } @@ -73,7 +75,8 @@ class ConfigurationManager : public IConfiguration { int getNumCO2() const { return m_numCO2; } int getNumO2() const { return m_numO2; } int getNumPressure() const { return m_numPressure; } - + int getNumAnalogMux() const { return m_numAnalogMux; } + // Static factory methods for creating sensors static std::unique_ptr createAuxTalon(); static std::unique_ptr createI2CTalon(); @@ -86,7 +89,8 @@ class ConfigurationManager : public IConfiguration { static std::unique_ptr createHumiditySensor(); static std::unique_ptr createETSensor(class ITimeProvider& timeProvider, class ISDI12Talon& talon); static std::unique_ptr createPressureSensor(SDI12Talon& talon); - + static std::unique_ptr createAnalogMuxSensor(SDI12Talon& talon); + private: // EEPROM addresses for configuration backup static const int EEPROM_CONFIG_START = 16; // Start at address 16 (after accel offsets at 0-11) @@ -112,6 +116,7 @@ class ConfigurationManager : public IConfiguration { int m_numCO2; int m_numO2; int m_numPressure; + int m_numAnalogMux; int m_SystemConfigUid; int m_SensorConfigUid; diff --git a/src/configuration/SensorManager.cpp b/src/configuration/SensorManager.cpp index bd999bb..30693fb 100644 --- a/src/configuration/SensorManager.cpp +++ b/src/configuration/SensorManager.cpp @@ -61,8 +61,12 @@ void SensorManager::initializeSensorsOnly(ITimeProvider& timeProvider, ISDI12Tal for (int i = 0; i < configManager.getNumPressure(); i++) { pressureSensors.push_back(ConfigurationManager::createPressureSensor(*firstSDI12Talon)); } + + for (int i = 0; i < configManager.getNumAnalogMux(); i++) { + analogMuxSensors.push_back(ConfigurationManager::createAnalogMuxSensor(*firstSDI12Talon)); + } } - + for (int i = 0; i < configManager.getNumCO2(); i++) { gasSensors.push_back(ConfigurationManager::createCO2Sensor()); } @@ -80,6 +84,7 @@ void SensorManager::clearAllSensors() { humiditySensors.clear(); etSensors.clear(); pressureSensors.clear(); + analogMuxSensors.clear(); } std::vector SensorManager::getAllTalons() { @@ -135,7 +140,11 @@ std::vector SensorManager::getAllSensors() { for (auto& sensor : pressureSensors) { allSensors.push_back(sensor.get()); } - + + for (auto& sensor : analogMuxSensors) { + allSensors.push_back(sensor.get()); + } + return allSensors; } @@ -144,5 +153,5 @@ int SensorManager::getTotalSensorCount() const { return 3 + auxTalons.size() + i2cTalons.size() + sdi12Talons.size() + haarSensors.size() + apogeeO2Sensors.size() + apogeeSolarSensors.size() + soilSensors.size() + gasSensors.size() + humiditySensors.size() + - etSensors.size() + pressureSensors.size(); + etSensors.size() + pressureSensors.size() + analogMuxSensors.size(); } \ No newline at end of file diff --git a/src/configuration/SensorManager.h b/src/configuration/SensorManager.h index 3aff736..620cbb5 100644 --- a/src/configuration/SensorManager.h +++ b/src/configuration/SensorManager.h @@ -67,6 +67,7 @@ class SensorManager { std::vector> humiditySensors; std::vector> etSensors; std::vector> pressureSensors; + std::vector> analogMuxSensors; }; #endif // SENSOR_MANAGER_H \ No newline at end of file