diff --git a/code/espurna/config/arduino.h b/code/espurna/config/arduino.h index 500e689b..3dda836d 100644 --- a/code/espurna/config/arduino.h +++ b/code/espurna/config/arduino.h @@ -176,3 +176,4 @@ //#define TMP3X_SUPPORT 1 //#define V9261F_SUPPORT 1 //#define VL53L1X_SUPPORT 1 +//#define EZOPH_SUPPORT 1 diff --git a/code/espurna/config/progmem.h b/code/espurna/config/progmem.h index 9202bf77..d468a8d2 100644 --- a/code/espurna/config/progmem.h +++ b/code/espurna/config/progmem.h @@ -232,6 +232,9 @@ PROGMEM const char espurna_sensors[] = #if VL53L1X_SUPPORT "VL53L1X " #endif + #if EZOPH_SUPPORT + "EZOPH " + #endif ""; @@ -246,7 +249,7 @@ PROGMEM const unsigned char magnitude_decimals[] = { 3, 0, 4, 4, // Geiger Counter decimals 0, - 0, 0, 0 // NO2, CO, Ohms + 0, 0, 0, 3 // NO2, CO, Ohms, pH }; PROGMEM const char magnitude_unknown_topic[] = "unknown"; @@ -280,6 +283,7 @@ PROGMEM const char magnitude_count_topic[] = "count"; PROGMEM const char magnitude_no2_topic[] = "no2"; PROGMEM const char magnitude_co_topic[] = "co"; PROGMEM const char magnitude_resistance_topic[] = "resistance"; +PROGMEM const char magnitude_ph_topic[] = "ph"; PROGMEM const char* const magnitude_topics[] = { magnitude_unknown_topic, magnitude_temperature_topic, magnitude_humidity_topic, @@ -293,7 +297,7 @@ PROGMEM const char* const magnitude_topics[] = { magnitude_distance_topic, magnitude_hcho_topic, magnitude_geiger_cpm_topic, magnitude_geiger_sv_topic, magnitude_count_topic, - magnitude_no2_topic, magnitude_co_topic, magnitude_resistance_topic + magnitude_no2_topic, magnitude_co_topic, magnitude_resistance_topic, magnitude_ph_topic }; PROGMEM const char magnitude_empty[] = ""; @@ -330,7 +334,8 @@ PROGMEM const char* const magnitude_units[] = { magnitude_geiger_cpm, magnitude_geiger_sv, // Geiger counter units magnitude_empty, // magnitude_ppm, magnitude_ppm, // NO2 & CO2 - magnitude_resistance + magnitude_resistance, + magnitude_empty // pH }; #endif diff --git a/code/espurna/config/sensors.h b/code/espurna/config/sensors.h index 02f0322e..bab3a643 100644 --- a/code/espurna/config/sensors.h +++ b/code/espurna/config/sensors.h @@ -780,6 +780,25 @@ #define VEML6075_DYNAMIC_MODE VEML6075::DYNAMIC_NORMAL // The dynamic mode can either be normal or high. In high #endif // dynamic mode, the resolution increases by about two // times. +// EZOPH pH meter +// Enable support by passing EZOPH_SUPPORT=1 build flag +//------------------------------------------------------------------------------ + +#ifndef EZOPH_SUPPORT +#define EZOPH_SUPPORT 0 +#endif + +#ifndef EZOPH_RX_PIN +#define EZOPH_RX_PIN 13 // Software serial RX GPIO +#endif + +#ifndef EZOPH_TX_PIN +#define EZOPH_TX_PIN 15 // Software serial TX GPIO +#endif + +#ifndef EZOPH_SYNC_INTERVAL +#define EZOPH_SYNC_INTERVAL 1000 // Amount of time (in ms) sync new readings. +#endif // ============================================================================= // Sensor helpers configuration - can't move to dependencies.h @@ -818,7 +837,8 @@ TMP3X_SUPPORT || \ V9261F_SUPPORT || \ VEML6075_SUPPORT || \ - VL53L1X_SUPPORT \ + VL53L1X_SUPPORT || \ + EZOPH_SUPPORT \ ) #endif @@ -988,4 +1008,8 @@ #include "../sensors/VL53L1XSensor.h" #endif +#if EZOPH_SUPPORT + #include "../sensors/EZOPHSensor.h" +#endif + #endif // SENSOR_SUPPORT diff --git a/code/espurna/config/types.h b/code/espurna/config/types.h index 41d5b74f..ff76bd65 100644 --- a/code/espurna/config/types.h +++ b/code/espurna/config/types.h @@ -282,6 +282,7 @@ #define SENSOR_PULSEMETER_ID 30 #define SENSOR_VEML6075_ID 31 #define SENSOR_VL53L1X_ID 32 +#define SENSOR_EZOPH_ID 33 //-------------------------------------------------------------------------------- // Magnitudes @@ -319,5 +320,6 @@ #define MAGNITUDE_NO2 28 #define MAGNITUDE_CO 29 #define MAGNITUDE_RESISTANCE 30 +#define MAGNITUDE_PH 31 -#define MAGNITUDE_MAX 31 +#define MAGNITUDE_MAX 32 diff --git a/code/espurna/sensor.ino b/code/espurna/sensor.ino index d38d37de..2144767c 100644 --- a/code/espurna/sensor.ino +++ b/code/espurna/sensor.ino @@ -738,6 +738,15 @@ void _sensorLoad() { _sensors.push_back(sensor); } #endif + + #if EZOPH_SUPPORT + { + EZOPHSensor * sensor = new EZOPHSensor(); + sensor->setRX(EZOPH_RX_PIN); + sensor->setTX(EZOPH_TX_PIN); + _sensors.push_back(sensor); + } + #endif } void _sensorCallback(unsigned char i, unsigned char type, double value) { diff --git a/code/espurna/sensors/EZOPHSensor.h b/code/espurna/sensors/EZOPHSensor.h new file mode 100644 index 00000000..d6dbaf0c --- /dev/null +++ b/code/espurna/sensors/EZOPHSensor.h @@ -0,0 +1,212 @@ +// ----------------------------------------------------------------------------- +// EZO™ pH Circuit from Atlas Scientific +// +// Uses SoftwareSerial library +// Copyright (C) 2018 by Rui Marinho +// ----------------------------------------------------------------------------- + +#if SENSOR_SUPPORT && EZOPH_SUPPORT + +#pragma once + +#include "Arduino.h" +#include "BaseSensor.h" +#include + +class EZOPHSensor : public BaseSensor { + + public: + + // --------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------- + + EZOPHSensor(): BaseSensor() { + _count = 1; + _sensor_id = SENSOR_EZOPH_ID; + } + + ~EZOPHSensor() { + if (_serial) delete _serial; + } + + // --------------------------------------------------------------------- + + void setRX(unsigned char pin_rx) { + if (_pin_rx == pin_rx) return; + _pin_rx = pin_rx; + _dirty = true; + } + + void setTX(unsigned char pin_tx) { + if (_pin_tx == pin_tx) return; + _pin_tx = pin_tx; + _dirty = true; + } + + // --------------------------------------------------------------------- + + unsigned char getRX() { + return _pin_rx; + } + + unsigned char getTX() { + return _pin_tx; + } + + // --------------------------------------------------------------------- + // Sensor API + // --------------------------------------------------------------------- + + // Initialization method, must be idempotent + void begin() { + if (!_dirty) return; + + if (_serial) delete _serial; + + _serial = new SoftwareSerial(_pin_rx, _pin_tx); + _serial->enableIntTx(false); + _serial->begin(9600); + + _ready = true; + _dirty = false; + } + + // Descriptive name of the sensor + String description() { + char buffer[28]; + snprintf(buffer, sizeof(buffer), "EZOPH @ SwSerial(%u,%u)", _pin_rx, _pin_tx); + return String(buffer); + } + + // Descriptive name of the slot # index + String slot(unsigned char index) { + return description(); + }; + + // Address of the sensor (it could be the GPIO or I2C address) + String address(unsigned char index) { + char buffer[6]; + snprintf(buffer, sizeof(buffer), "%u:%u", _pin_rx, _pin_tx); + return String(buffer); + } + + // Type for slot # index + unsigned char type(unsigned char index) { + if (index == 0) return MAGNITUDE_PH; + return MAGNITUDE_NONE; + } + + void tick() { + _setup(); + _read(); + } + + // Current value for slot # index + double value(unsigned char index) { + if (index == 0) return _ph; + return 0; + } + + + + protected: + + // --------------------------------------------------------------------- + // Protected + // --------------------------------------------------------------------- + + void _setup() { + if (_sync_responded) { + return; + } + + _error = SENSOR_ERROR_WARM_UP; + + String sync_serial = ""; + sync_serial.reserve(30); + + if (!_sync_requested) { + _serial->write(67); // C + _serial->write(44); // , + _serial->write(63); // ? + _serial->write(13); // \r + _serial->flush(); + + _sync_requested = true; + } + + while ((_serial->available() > 0)) { + char sync_char = (char)_serial->read(); + sync_serial += sync_char; + + if (sync_char == '\r') { + break; + } + } + + if (sync_serial.startsWith("?C,")) { + _sync_interval = sync_serial.substring(sync_serial.indexOf(",") + 1).toInt() * 1000; + + if (_sync_interval == 0) { + _error = SENSOR_ERROR_OTHER; + return; + } + } + + if (sync_serial.startsWith("*OK")) { + _sync_responded = true; + } + + if (!_sync_responded) { + return; + } + + _error = SENSOR_ERROR_OK; + } + + void _read() { + if (_error != SENSOR_ERROR_OK) { + return; + } + + if (millis() - _ts <= _sync_interval) { + return; + } + + _ts = millis(); + + String ph_serial = ""; + ph_serial.reserve(30); + + while ((_serial->available() > 0)) { + char ph_char = (char)_serial->read(); + ph_serial += ph_char; + + if (ph_char == '\r') { + break; + } + } + + if (ph_serial == "*ER") { + _error = SENSOR_ERROR_OTHER; + return; + } + + _ph = ph_serial.toFloat(); + + _error = SENSOR_ERROR_OK; + } + + bool _sync_requested = false; + bool _sync_responded = false; + unsigned long _sync_interval = 100000; // Maximum continuous reading interval allowed is 99000 milliseconds. + unsigned long _ts = 0; + double _ph = 0; + unsigned int _pin_rx; + unsigned int _pin_tx; + SoftwareSerial * _serial = NULL; + +}; + +#endif // SENSOR_SUPPORT && EZOPH_SUPPORT diff --git a/code/html/custom.js b/code/html/custom.js index 3a9d6644..20270bcf 100644 --- a/code/html/custom.js +++ b/code/html/custom.js @@ -47,7 +47,8 @@ function sensorName(id) { "Events", "PMSX003", "BMX280", "MHZ19", "SI7021", "SHT3X I2C", "BH1750", "PZEM004T", "AM2320 I2C", "GUVAS12SD", "TMP3X", "Sonar", "SenseAir", "GeigerTicks", "GeigerCPM", - "NTC", "SDS011", "MICS2710", "MICS5525", "VL53L1X", "VEML6075" + "NTC", "SDS011", "MICS2710", "MICS5525", "VL53L1X", "VEML6075", + "EZOPH" ]; if (1 <= id && id <= names.length) { return names[id - 1]; @@ -64,7 +65,7 @@ function magnitudeType(type) { "PM1.0", "PM2.5", "PM10", "CO2", "Lux", "UVA", "UVB", "UV Index", "Distance" , "HCHO", "Local Dose Rate", "Local Dose Rate", "Count", - "NO2", "CO", "Resistance" + "NO2", "CO", "Resistance", "pH" ]; if (1 <= type && type <= types.length) { return types[type - 1];