diff --git a/code/espurna/config/sensors.h b/code/espurna/config/sensors.h index b9f49480dc..727183e7be 100644 --- a/code/espurna/config/sensors.h +++ b/code/espurna/config/sensors.h @@ -257,8 +257,13 @@ #define DALLAS_PIN 14 #endif -#define DALLAS_RESOLUTION 9 // Not used atm -#define DALLAS_READ_INTERVAL 2000 // Force sensor read & cache every 2 seconds +#ifndef DALLAS_PARASITE +#define DALLAS_PARASITE 1 // Use parasite power mode by default (set to 0 to use normally powered sensors) +#endif + +#ifndef DALLAS_RESOLUTION +#define DALLAS_RESOLUTION 0 // Forcibly set resolution of the sensor after booting. Valid values are 9, 10, 11 and 12. Set to 0 to keep the default resolution +#endif //------------------------------------------------------------------------------ // DHTXX temperature/humidity sensor @@ -1488,7 +1493,13 @@ HDC1080_SUPPORT \ ) #undef I2C_SUPPORT -#define I2C_SUPPORT 1 +#define I2C_SUPPORT 1 +#endif + +// OneWire support when sensor needs it +#if DALLAS_SUPPORT +#undef ONE_WIRE_SUPPORT +#define ONE_WIRE_SUPPORT 1 #endif // Can't have ADC reading something else diff --git a/code/espurna/config/types.h b/code/espurna/config/types.h index 5891394953..b4f1c7a3f6 100644 --- a/code/espurna/config/types.h +++ b/code/espurna/config/types.h @@ -405,9 +405,10 @@ #define SENSOR_ERROR_NOT_READY 10 // Device is not ready / available / disconnected #define SENSOR_ERROR_CONFIG 11 // Configuration values were invalid #define SENSOR_ERROR_SUPPORT 12 // Not supported +#define SENSOR_ERROR_NOT_FOUND 13 // Not found #define SENSOR_ERROR_OTHER 99 // Any other error -#define SENSOR_ERROR_MAX 13 +#define SENSOR_ERROR_MAX 14 //------------------------------------------------------------------------------ // OTA Client (not related to the Web OTA support) diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp index c5929072cb..dc08f33023 100644 --- a/code/espurna/sensor.cpp +++ b/code/espurna/sensor.cpp @@ -280,6 +280,9 @@ String error(unsigned char error) { case SENSOR_ERROR_SUPPORT: result = PSTR("Not Supported"); break; + case SENSOR_ERROR_NOT_FOUND: + result = PSTR("Not found"); + break; case SENSOR_ERROR_OTHER: default: result = PSTR("Other / Unknown Error"); @@ -1743,7 +1746,6 @@ double process(const Magnitude& magnitude, double value) { } // namespace namespace internal { -namespace { std::vector magnitudes; bool real_time { sensor::build::realTimeValues() }; @@ -1752,7 +1754,6 @@ using ReadHandlers = std::forward_list; ReadHandlers read_handlers; ReadHandlers report_handlers; -} // namespace } // namespace internal size_t count(unsigned char type) { @@ -1784,6 +1785,15 @@ const Magnitude* find(unsigned char type, unsigned char index) { return out; } +Unit units(size_t index) { + return internal::magnitudes[index].units; +} + +unsigned char error(size_t index) { + const auto& magnitude = internal::magnitudes[index]; + return magnitude.sensor->error(); +} + Magnitude& get(size_t index) { return internal::magnitudes[index]; } @@ -2086,9 +2096,103 @@ void load() { #if DALLAS_SUPPORT { - auto* sensor = new DallasSensor(); - sensor->setGPIO(DALLAS_PIN); - add(sensor); + PROGMEM_STRING(Pin, "dallasPin"); + const auto pin = getSetting(Pin, uint8_t{ DALLAS_PIN }); + + PROGMEM_STRING(Parasite, "dallasParasite"); + const auto parasite = getSetting(Parasite, DALLAS_PARASITE == 1); + + PROGMEM_STRING(Resolution, "dallasResolution"); + const auto resolution = getSetting(Resolution, uint8_t{ DALLAS_RESOLUTION }); + + using namespace espurna::driver; + auto port = std::make_shared(); + + // TODO hybrid mode with an extra pull-up pin? + // TODO parasite *can* be detected for DS18X, see + // 'DS18B20 .pdf / ROM Commands / Read Power Supply (0xB4)' + // > During the read time slot, parasite powered DS18B20s will + // > pull the bus low, and externally powered DS18B20s will + // > let the bus remain high. + // (but, not every DS clone properly implements it) + auto error = port->attach(pin, parasite); + if (onewire::Error::Ok == error) { + using namespace espurna::sensor::driver; + + const auto devices = port->devices(); + DEBUG_MSG_P(PSTR("[DALLAS] Found %zu device(s) on GPIO%hhu\n"), + devices.size(), pin); + + std::vector filtered; + filtered.reserve(devices.size()); + + for (auto& device : devices) { + filtered.push_back(&device); + } + + // Currently known sensor types + using Temperature = dallas::temperature::Sensor; + using Digital = dallas::temperature::Sensor; + + const auto unknown = std::remove_if( + filtered.begin(), filtered.end(), + [](const onewire::Device* device) { + if (!Temperature::match(*device) + && !Digital::match(*device)) + { + DEBUG_MSG_P(PSTR("[DALLAS] Unknown device %s\n"), + hexEncode(device->address).c_str()); + return true; + } + + return false; + }); + filtered.erase(unknown, filtered.end()); + + if (!filtered.size()) { + error = onewire::Error::NotFound; + goto dallas_end; + } + + // Push digital sensors first, temperature sensors last + // Making sure temperature sensor always becomes port handler + std::sort( + filtered.begin(), filtered.end(), + [](const onewire::Device* lhs, const onewire::Device*) { + return Digital::match(*lhs); + }); + + // TODO per-sensor resolution matters much? + dallas::temperature::Sensor::setResolution(resolution); + + dallas::internal::Sensor* ptr = nullptr; + for (const auto* device : filtered) { + if (Temperature::match(*device)) { + ptr = new Temperature(port, *device); + } else if (Digital::match(*device)) { + ptr = new Digital(port, *device); + } else { + error = onewire::Error::NotFound; + goto dallas_end; + } + + sensor::add(ptr); + } + + // Since sensor reading order is constant, make sure the + // last sensor is handling everything related to the wire. + // (also note 'Digital' being pushed to the front above) + DEBUG_MSG_P(PSTR("[DALLAS] %s is port handler\n"), + hexEncode(ptr->getDeviceAddress()).c_str()); + ptr->setPortHandler(); + } + +dallas_end: + if (onewire::Error::Ok != error) { + DEBUG_MSG_P(PSTR("[DALLAS] Could not initialize the sensor - %.*s\n"), + espurna::driver::onewire::error(error).length(), + espurna::driver::onewire::error(error).data()); + } } #endif @@ -3440,10 +3544,10 @@ void magnitudes(JsonObject& root) { magnitude::process(magnitude, magnitude.last))); }}, {STRING_VIEW("units"), [](JsonArray& out, size_t index) { - out.add(static_cast(magnitude::get(index).units)); + out.add(static_cast(magnitude::units(index))); }}, {STRING_VIEW("error"), [](JsonArray& out, size_t index) { - out.add(magnitude::get(index).sensor->error()); + out.add(magnitude::error(index)); }}, }); } @@ -3869,8 +3973,13 @@ bool init() { if (out) { internal::state = State::Ready; - DEBUG_MSG_P(PSTR("[SENSOR] Finished initialization for %zu sensor(s) and %zu magnitude(s)\n"), - sensor::count(), magnitude::count()); + + if (sensor::count()) { + DEBUG_MSG_P(PSTR("[SENSOR] Finished initialization for %zu sensor(s) and %zu magnitude(s)\n"), + sensor::count(), magnitude::count()); + } else { + DEBUG_MSG_P(PSTR("[SENSOR] Finished initialization\n")); + } } return out; @@ -3917,10 +4026,6 @@ void tick() { void pre() { for (auto sensor : internal::sensors) { sensor->pre(); - if (!sensor->status()) { - DEBUG_MSG_P(PSTR("[SENSOR] Could not read from %s (%s)\n"), - sensor->description().c_str(), error(sensor->error()).c_str()); - } } } @@ -3930,6 +4035,18 @@ void post() { } } +void error() { +#if DEBUG_SUPPORT + for (auto sensor : internal::sensors) { + if (SENSOR_ERROR_OK != sensor->error()) { + DEBUG_MSG_P(PSTR("[SENSOR] Could not read from %s - %s\n"), + sensor->description().c_str(), + error(sensor->error()).c_str()); + } + } +#endif +} + void reset_init(duration::Seconds init_interval) { internal::init_interval = init_interval; } @@ -3992,26 +4109,28 @@ void loop() { sensor::tick(); if (ready_to_read()) { - // Pre-read hook, called every reading - sensor::pre(); - // XXX: Filter out certain magnitude types when relay is turned OFF #if RELAY_SUPPORT && SENSOR_POWER_CHECK_STATUS const bool relay_off = (relayCount() == 1) && (relayStatus(0) == 0); #endif + // Pre-read hook, called every reading + sensor::pre(); + + // Notify about sensor errors that may have been updated by pre() + sensor::error(); + auto value = sensor::ReadValue{}; for (size_t index = 0; index < magnitude::count(); ++index) { auto& magnitude = magnitude::get(index); - if (!magnitude.sensor->status()) { + + // Do not read anything from a failed sensor + if (SENSOR_ERROR_OK != magnitude.sensor->error()) { continue; } - // ------------------------------------------------------------- // RAW value, returned from the sensor - // ------------------------------------------------------------- - value.raw = magnitude.sensor->value(magnitude.slot); // But, completely remove spurious values if relay is OFF @@ -4040,17 +4159,10 @@ void loop() { magnitude.last = value.raw; magnitude.filter->update(value.raw); - // ------------------------------------------------------------- // Procesing (units and decimals) - // ------------------------------------------------------------- - value.processed = magnitude::process(magnitude, value.raw); magnitude::read(magnitude::value(magnitude, value.processed)); - // ------------------------------------------------------------------- - // Reporting - // ------------------------------------------------------------------- - // Initial status or after report counter overflows bool report { ready_to_report() }; diff --git a/code/espurna/sensors/BaseSensor.h b/code/espurna/sensors/BaseSensor.h index 2f4d337a3e..1acbb74bc8 100644 --- a/code/espurna/sensors/BaseSensor.h +++ b/code/espurna/sensors/BaseSensor.h @@ -257,12 +257,7 @@ class BaseSensor { // Current value for slot # index virtual double value(unsigned char index) = 0; - // Return status (true if no errors) - bool status() const { - return 0 == _error; - } - - // Return ready status (true for ready) + // Return ready status (true if ready to be read) bool ready() const { return _ready; } @@ -273,7 +268,7 @@ class BaseSensor { } protected: - int _error = 0; + int _error = SENSOR_ERROR_OK; bool _dirty = true; bool _ready = false; }; diff --git a/code/espurna/sensors/DallasSensor.h b/code/espurna/sensors/DallasSensor.h index 5087f21b29..1d55293cee 100644 --- a/code/espurna/sensors/DallasSensor.h +++ b/code/espurna/sensors/DallasSensor.h @@ -1,530 +1,681 @@ // ----------------------------------------------------------------------------- // Dallas OneWire Sensor -// Uses OneWire library // Copyright (C) 2017-2019 by Xose Pérez +// Copyright (C) 2024 by Maxim Prokhorov // ----------------------------------------------------------------------------- +#pragma once + #if SENSOR_SUPPORT && DALLAS_SUPPORT -#pragma once +#include "BaseSensor.h" +#include "driver_onewire.h" -#include +namespace espurna { +namespace sensor { +namespace driver { +namespace dallas { +namespace { -#include -#include +namespace temperature { -#include "BaseSensor.h" +class Sensor; -#define DS_CHIP_DS18S20 0x10 -#define DS_CHIP_DS2406 0x12 -#define DS_CHIP_DS1822 0x22 -#define DS_CHIP_DS18B20 0x28 -#define DS_CHIP_DS1825 0x3B +} // namespace temperature -#define DS_PARASITE 1 -#define DS_DISCONNECTED -127 +namespace digital { -#define DS_CMD_START_CONVERSION 0x44 -#define DS_CMD_READ_SCRATCHPAD 0xBE +class Sensor; -#define DS18x20_ADDR_LEN 8 -#define DS18x20_SCRATCHPAD_LEN 9 +} // namespace digital -// ====== DS2406 specific constants ======= +constexpr auto MinimalConversionTime = duration::Milliseconds{ 95 }; -#define DS2406_CHANNEL_ACCESS 0xF5; +constexpr auto MaximumConversionTime = duration::Milliseconds{ 750 }; -// CHANNEL CONTROL BYTE -// 7 6 5 4 3 2 1 0 -// ALR IM TOG IC CHS1 CHS0 CRC1 CRC0 -// 0 1 0 0 0 1 0 1 0x45 +constexpr bool validResolution(uint8_t resolution) { + return (resolution >= 9) && (resolution <= 12); +} -// CHS1 CHS0 Description -// 0 0 (not allowed) -// 0 1 channel A only -// 1 0 channel B only -// 1 1 both channels interleaved +constexpr duration::Milliseconds resolutionConversionTime(uint8_t resolution) { + return (9 == resolution) ? (duration::Milliseconds{ 95 }) : // - Tconv / 8 + // 93.75ms per datasheet + (10 == resolution) ? (duration::Milliseconds{ 190 }) : // - Tconv / 4 + // 187.5ms per datasheet + (11 == resolution) ? (duration::Milliseconds{ 375 }) : // - Tconv / 2 + (duration::Milliseconds{ 750 }); // - Tconv default +} -// TOG IM CHANNELS EFFECT -// 0 0 one channel Write all bits to the selected channel -// 0 1 one channel Read all bits from the selected channel -// 1 0 one channel Write 8 bits, read 8 bits, write, read, etc. to/from the selected channel -// 1 1 one channel Read 8 bits, write 8 bits, read, write, etc. from/to the selected channel -// 0 0 two channels Repeat: four times (write A, write B) -// 0 1 two channels Repeat: four times (read A, read B) -// 1 0 two channels Four times: (write A, write B), four times: (readA, read B), write, read, etc. -// 1 1 two channels Four times: (read A, read B), four times: (write A, write B), read, write, etc. +namespace internal { -// CRC1 CRC0 Description -// 0 0 CRC disabled (no CRC at all) -// 0 1 CRC after every byte -// 1 0 CRC after 8 bytes -// 1 1 CRC after 32 bytes -#define DS2406_CHANNEL_CONTROL_BYTE 0x45; -#define DS2406_STATE_BUF_LEN 7 +class Sensor : public BaseSensor { +public: + using Address = espurna::driver::onewire::Address; + using Device = espurna::driver::onewire::Device; -class DallasSensor : public BaseSensor { + using PortPtr = espurna::driver::onewire::PortPtr; - private: + Sensor(PortPtr port, Device device) : + _port(port), + _device(device) + {} - using Address = std::array; - using Data = std::array; + void setPortHandler() { + _port_handler = true; + } - struct Device { - Address address{}; - Data data{}; - uint8_t error{ SENSOR_ERROR_OK }; - double value{ 0.0 }; - }; + Address getDeviceAddress() const { + return _device.address; + } - public: +protected: + static duration::Milliseconds _conversionTime; - // --------------------------------------------------------------------- + bool _port_handler { false }; + int _read_error { SENSOR_ERROR_OK }; - void setGPIO(unsigned char gpio) { - _dirty = _gpio != gpio; - _gpio = gpio; - } + PortPtr _port; + Device _device{}; +}; - // --------------------------------------------------------------------- +} // namespace internal - unsigned char getGPIO() const { - return _gpio; - } +namespace temperature { +namespace command { - // --------------------------------------------------------------------- - // Sensor API - // --------------------------------------------------------------------- +constexpr uint8_t ReadScratchpad { 0xBE }; +constexpr uint8_t StartConversion { 0x44 }; +constexpr uint8_t WriteScratchpad { 0x4E }; - unsigned char id() const override { - return SENSOR_DALLAS_ID; - } +} // namespace command - unsigned char count() const override { - return _devices.size(); - } +namespace chip { - // Initialization method, must be idempotent - void begin() override { +constexpr uint8_t DS18S20 { 0x10 }; +constexpr uint8_t DS1822 { 0x22 }; +constexpr uint8_t DS18B20 { 0x28 }; +constexpr uint8_t DS1825 { 0x3B }; - if (!_dirty) return; +} // namespace chip - // Manage GPIO lock - if (_previous != GPIO_NONE) { - gpioUnlock(_previous); - } +constexpr int16_t Disconnected { -127 }; - _previous = GPIO_NONE; - if (!gpioLock(_gpio)) { - _error = SENSOR_ERROR_GPIO_USED; - return; - } +class Sensor : public internal::Sensor { +public: + using Data = std::array; - // OneWire - if (_wire) { - _wire.reset(nullptr); - } + using internal::Sensor::Sensor; - _wire = std::make_unique(_gpio); + static bool match(uint8_t id) { + return (id == chip::DS18S20) + || (id == chip::DS18B20) + || (id == chip::DS1822) + || (id == chip::DS1825); + } - // Search devices - loadDevices(); + static bool match(const Device& device) { + return match(chip(device)); + } - // If no devices found check again pulling up the line - if (!_devices.size()) { - pinMode(_gpio, INPUT_PULLUP); - loadDevices(); - } + static unsigned char chip(const Device& device) { + return device.address[0]; + } - // Check connection - if (_devices.size() == 0) { - gpioUnlock(_gpio); - } else { - _previous = _gpio; - } + static void setResolution(uint8_t resolution) { + _override_resolution = resolution; + } - _last_reading = TimeSource::now(); - _ready = true; - _dirty = false; + uint8_t getResolution() { + if (chip(_device) == chip::DS18S20) { + return 9; + } + Data data; + auto err = _readScratchpad(_device, data); + if (err != SENSOR_ERROR_OK) { + return 0; } - // Loop-like method, call it in your main loop - void tick() override { + return _resultGeneric(data).resolution; + } - const auto now = TimeSource::now(); - if (now - _last_reading < ReadInterval) { - return; - } + // --------------------------------------------------------------------- + // Sensor API + // --------------------------------------------------------------------- - _last_reading = now; + unsigned char id() const override { + return SENSOR_DALLAS_ID; + } - // Every second we either start a conversion or read the scratchpad - if (_conversion) { - _startConversion(); - } else { - _readScratchpad(); - } + unsigned char count() const override { + return 1; + } - _conversion = !_conversion; - } + void begin() override { + _ready = true; + _dirty = false; - // Descriptive name of the sensor - String description() const override { - char buffer[20]; - snprintf_P(buffer, sizeof(buffer), - PSTR("Dallas @ GPIO%hhu"), _gpio); - return String(buffer); + _updateResolution(_override_resolution); + if (_port_handler) { + DEBUG_MSG_P(PSTR("[DALLAS] Conversion time is %u (ms)\n"), + _conversion_time.count()); + _startPortConversion(); + } + } + + void notify() override { + _read_error = _readScratchpad(); + } + + // Descriptive name of the sensor + String description() const override { + return _description(); + } + + // Address of the device + String address(unsigned char) const override { + return _address(); + } + + // Type for slot # index + unsigned char type(unsigned char index) const override { + if (index == 0) { + return MAGNITUDE_TEMPERATURE; } - // Address of the device - String address(unsigned char index) const override { - String out; - if (index < _devices.size()) { - out = hexEncode(_devices[index].address); - } + return MAGNITUDE_NONE; + } - return out; + // Number of decimals for a magnitude (or -1 for default) + signed char decimals(espurna::sensor::Unit unit) const override { + // Smallest increment is 0.0625 °C + if (unit == espurna::sensor::Unit::Celcius) { + return 2; } - // Descriptive name of the slot # index - String description(unsigned char index) const override { - String out; - if (index < _devices.size()) { - char buffer[64]{}; - snprintf_P(buffer, sizeof(buffer), - PSTR("%s (%s) @ GPIO%hhu"), - chipAsString(index).c_str(), - hexEncode(_devices[index].address).c_str(), _gpio); - - out = buffer; - } + return -1; + } - return out; - } + // Pre-read hook (usually to populate registers with up-to-date data) + void pre() override { + _error = _read_error; - // Type for slot # index - unsigned char type(unsigned char index) const override { - if (index < _devices.size()) { - if (chip(index) == DS_CHIP_DS2406) { - return MAGNITUDE_DIGITAL; - } else { - return MAGNITUDE_TEMPERATURE; - } + if (_error == SENSOR_ERROR_OK) { + const auto result = + (chip::DS18S20 == chip(_device)) + ? _resultDs18s20(_data) + : _resultGeneric(_data); + + if (result.raw == Disconnected) { + _error = SENSOR_ERROR_OUT_OF_RANGE; } - return MAGNITUDE_NONE; + _value = result.value; } - // Number of decimals for a magnitude (or -1 for default) - signed char decimals(espurna::sensor::Unit unit) const override { - // Smallest increment is 0.0625 °C - if (unit == espurna::sensor::Unit::Celcius) { - return 2; - } - - // In case we have DS2406, it is a digital sensor and there are no decimal places - return 0; + if (_port_handler) { + _startPortConversion(); } + } - // Pre-read hook (usually to populate registers with up-to-date data) - void pre() override { - _error = SENSOR_ERROR_OK; - - for (auto& device : _devices) { - const auto chip_id = chip(device); - - switch (chip_id) { - case DS_CHIP_DS2406: - device.value = _valueDs2406(device.data); - break; - - case DS_CHIP_DS18S20: - device.value = _valueDs18s20(device.data); - break; - - default: - device.value = _valueGeneric(device.data); - break; - } - - if ((chip_id != DS_CHIP_DS2406) && (device.value == DS_DISCONNECTED)) { - device.error = SENSOR_ERROR_OUT_OF_RANGE; - } - - if (device.error != SENSOR_ERROR_OK) { - DEBUG_MSG_P(PSTR("[DALLAS] %s @ GPIO%hhu (#%zu) reading failed\n"), - _chipIdToString(chip(device)).c_str(), _gpio, index); - _error = device.error; - return; - } - } + // Current value for slot # index + double value(unsigned char index) override { + if (index == 0) { + return _value; } - // Current value for slot # index - double value(unsigned char index) override { - if (index <= _devices.size()) { - return _devices[index].value; - } + return 0.0; + } + +private: + struct Result { + int16_t raw; + double value; + uint8_t resolution; + }; + + static Result makeResult(int16_t raw, uint8_t resolution) { + // clear undefined bits for the set resolution + switch (resolution) { + case 9: + raw = raw & ~7; + break; + + case 10: + raw = raw & ~3; + break; + + case 11: + raw = raw & ~1; + break; + + case 12: + break; + + } - return 0.0; + return Result{ + .raw = raw, + .value = double(raw) / 16.0, + .resolution = resolution, + }; + } + + // byte 0: temperature LSB + // byte 1: temperature MSB + // byte 2: high alarm temp + // byte 3: low alarm temp + // byte 4: DS18B20 & DS1822: configuration register + // byte 5: internal use & crc + // byte 6: DS18B20 & DS1822: store for crc + // byte 7: DS18B20 & DS1822: store for crc + // byte 8: SCRATCHPAD_CRC + static Result _resultGeneric(const Data& data) { + int16_t raw = (data[1] << 8) | data[0]; + + uint8_t resolution = (data[4] & 0b1100000) >> 5; + resolution += 9; + + return makeResult(raw, resolution); + } + + // byte 0: temperature LSB + // byte 1: temperature MSB + // byte 2: high alarm temp + // byte 3: low alarm temp + // byte 4: store for crc + // byte 5: internal use & crc + // byte 6: COUNT_REMAIN + // byte 7: COUNT_PER_C + // byte 8: SCRATCHPAD_CRC + static Result _resultDs18s20(const Data& data) { + int16_t raw = (data[1] << 8) | data[0]; + + // 9 bit resolution by default, but + // "count remain" gives full 12 bit resolution + uint8_t resolution = 12; + raw = raw << 3; + + if (data[7] == 0x10) { + raw = (raw & 0xFFF0) + 12 - data[6]; } - protected: - - // --------------------------------------------------------------------- - // Protected - // --------------------------------------------------------------------- - - // byte 0: temperature LSB - // byte 1: temperature MSB - // byte 2: high alarm temp - // byte 3: low alarm temp - // byte 4: DS18S20: store for crc - // DS18B20 & DS1822: configuration register - // byte 5: internal use & crc - // byte 6: DS18S20: COUNT_REMAIN - // DS18B20 & DS1822: store for crc - // byte 7: DS18S20: COUNT_PER_C - // DS18B20 & DS1822: store for crc - // byte 8: SCRATCHPAD_CRC - static double _valueGeneric(const Data& data) { - int16_t raw = (data[1] << 8) | data[0]; - - const uint8_t res = ((data[4] & 0x60) >> 5) & 0b11; - switch (res) { - // 9 bit res, 93.75 ms - case 0x00: - raw = raw & ~7; - break; - - // 10 bit res, 187.5 ms - case 0x20: - raw = raw & ~3; - break; - - // 11 bit res, 375 ms - case 0x40: - raw = raw & ~1; - break; - - // 12 bit res, 750 ms - default: - break; + return makeResult(raw, resolution); + } - } + int _readScratchpad(Device& device, Data& out) { + auto ok = _port->request( + device.address, command::ReadScratchpad, + espurna::make_span(out)); - double out = raw; - raw /= 16.0; + if (!ok) { + return SENSOR_ERROR_TIMEOUT; + } - return out; + ok = espurna::driver::onewire::check_crc8( + espurna::make_span(std::cref(out).get())); + if (!ok) { + return SENSOR_ERROR_CRC; } - // See _valueGeneric(const Data&) for register info - static double _valueDs18s20(const Data& data) { - int16_t raw = (data[1] << 8) | data[0]; + return SENSOR_ERROR_OK; + } + + int _readScratchpad() { + return _readScratchpad(_device, _data); + } + + // ask specific device to perform temperature conversion + void _startConversion(const Device& device) { + _port->write(device.address, command::StartConversion); + } + + // same as above, but 'skip ROM' allows to select everything on the wire + void _startConversion() { + _port->write(command::StartConversion); + } + + // when instance is controlling the port, schedule the next conversion + void _startPortConversion() { + _read_error = SENSOR_ERROR_NOT_READY; + _startConversion(); + notify_after( + _conversion_time, + [](const BaseSensor* sensor) { + return SENSOR_DALLAS_ID == sensor->id(); + }); + } + + // Make a fast read to determine sensor resolution. + // If override resolution was set, change cfg byte and write back. + // (probably does not work very well in parasite mode?) + void _updateResolution(uint8_t resolution) { + // Impossible to change, fixed to 9bit + if (chip::DS18S20 == chip(_device)) { + _maxConversionTime(MinimalConversionTime); + return; + } - // 9 bit resolution default - raw = raw << 3; + Data data; + auto err = _readScratchpad(_device, data); + if (err != SENSOR_ERROR_OK) { + _maxConversionTime(MaximumConversionTime); + return; + } - // "count remain" gives full 12 bit resolution - if (data[7] == 0x10) { - raw = (raw & 0xFFF0) + 12 - data[6]; - } + auto result = _resultGeneric(data); + if (!validResolution(result.resolution)) { + _maxConversionTime(MaximumConversionTime); + return; + } - double out = raw; - out /= 16.0; + _maxConversionTime( + resolutionConversionTime(result.resolution)); - return out; + // If resolution change doesn't do anything, keep the default value + if (!validResolution(resolution)) { + return; } - // 3 cmd bytes, 1 channel info byte, 1 0x00, 2 CRC16 - // CHANNEL INFO BYTE - // Bit 7 : Supply Indication 0 = no supply - // Bit 6 : Number of Channels 0 = channel A only - // Bit 5 : PIO-B Activity Latch - // Bit 4 : PIO-A Activity Latch - // Bit 3 : PIO B Sensed Level - // Bit 2 : PIO A Sensed Level - // Bit 1 : PIO-B Channel Flip-Flop Q - // Bit 0 : PIO-A Channel Flip-Flop Q - static double _valueDs2406(const Data& data) { - return ((data[3] & 0x04) != 0) ? 1.0 : 0.0; + if (result.resolution == resolution) { + return; } - bool _readDs2406(Device& device) { - device.error = SENSOR_ERROR_OK; - if (_wire->reset() == 0) { - device.error = SENSOR_ERROR_TIMEOUT; - return false; - } + std::array upd; + upd[0] = command::WriteScratchpad; + upd[1] = _data[2]; + upd[2] = _data[3]; + + uint8_t cfg = data[4]; + cfg &= 0b110011111; + cfg |= ((resolution - 9) << 5) & 0b1100000; + + upd[3] = cfg; + + _port->write(_device.address, upd); + _port->reset(); + + _maxConversionTime( + resolutionConversionTime(resolution)); + } + + String _address() const { + return hexEncode(_device.address); + } + + static espurna::StringView _chipIdToStringView(unsigned char id) { + espurna::StringView out; + + switch (id) { + case chip::DS18S20: + out = STRING_VIEW("DS18S20"); + break; + case chip::DS18B20: + out = STRING_VIEW("DS18B20"); + break; + case chip::DS1822: + out = STRING_VIEW("DS1822"); + break; + case chip::DS1825: + out = STRING_VIEW("DS1825"); + break; + default: + out = STRING_VIEW("Unknown"); + break; + } - _wire->select(device.address.data()); + return out; + } - std::array data; - data[0] = DS2406_CHANNEL_ACCESS; - data[1] = DS2406_CHANNEL_CONTROL_BYTE; - data[2] = 0xFF; + static String _chipIdToString(unsigned char id) { + return _chipIdToStringView(id).toString(); + } - _wire->write_bytes(data.data(), 3); + String _description() const { + char buffer[24]; + snprintf_P(buffer, sizeof(buffer), + PSTR("%s @ GPIO%hhu"), + _chipIdToString(chip(_device)).c_str(), + _port->pin()); + return String(buffer); + } - // 3 cmd bytes, 1 channel info byte, 1 0x00, 2 CRC16 - _wire->read_bytes(data.data(), data.size()); + void _maxConversionTime(duration::Milliseconds duration) { + _conversion_time = std::max(_conversion_time, duration); + } - // Read scratchpad - if (_wire->reset() == 0) { - device.error = SENSOR_ERROR_TIMEOUT; - return false; - } + static uint8_t _override_resolution; + static duration::Milliseconds _conversion_time; - if (!OneWire::check_crc16(data.data(), 5, &data[5])) { - device.error = SENSOR_ERROR_CRC; - } + Data _data{}; - static_assert(data.size() <= decltype(Device::data){}.size(), ""); - std::copy(data.begin(), data.end(), device.data.begin()); + double _value{}; +}; - return device.error == SENSOR_ERROR_OK; - } +uint8_t Sensor::_override_resolution { DALLAS_RESOLUTION }; +duration::Milliseconds Sensor::_conversion_time { MinimalConversionTime }; - bool _readGeneric(Device& device) { - device.error = SENSOR_ERROR_OK; - if (_wire->reset() == 0) { - device.error = SENSOR_ERROR_TIMEOUT; - return false; - } +} // namespace temperature - _wire->select(device.address.data()); - _wire->write(DS_CMD_READ_SCRATCHPAD); +// CHANNEL CONTROL BYTE +// 7 6 5 4 3 2 1 0 +// ALR IM TOG IC CHS1 CHS0 CRC1 CRC0 +// 0 1 0 0 0 1 0 1 0x45 - Data data{}; - _wire->read_bytes(data.data(), data.size()); +// CHS1 CHS0 Description +// 0 0 (not allowed) +// 0 1 channel A only +// 1 0 channel B only +// 1 1 both channels interleaved - if (_wire->reset() == 0) { - device.error = SENSOR_ERROR_TIMEOUT; - return false; - } +// TOG IM CHANNELS EFFECT +// 0 0 one channel Write all bits to the selected channel +// 0 1 one channel Read all bits from the selected channel +// 1 0 one channel Write 8 bits, read 8 bits, write, read, etc. to/from the selected channel +// 1 1 one channel Read 8 bits, write 8 bits, read, write, etc. from/to the selected channel +// 0 0 two channels Repeat: four times (write A, write B) +// 0 1 two channels Repeat: four times (read A, read B) +// 1 0 two channels Four times: (write A, write B), four times: (readA, read B), write, read, etc. +// 1 1 two channels Four times: (read A, read B), four times: (write A, write B), read, write, etc. - if (OneWire::crc8(data.data(), data.size() - 1) != data.back()) { - device.error = SENSOR_ERROR_CRC; - } +// CRC1 CRC0 Description +// 0 0 CRC disabled (no CRC at all) +// 0 1 CRC after every byte +// 1 0 CRC after 8 bytes +// 1 1 CRC after 32 bytes - device.data = data; +namespace digital { +namespace chip { - return device.error == SENSOR_ERROR_OK; - } +constexpr uint8_t DS2406 { 0x12 }; - bool _readScratchpad() { - for (size_t index = 0; index < _devices.size(); ++index) { - auto& device = _devices[index]; +} // namespace chip - auto status = - (device.address[0] == DS_CHIP_DS2406) - ? _readDs2406(device) - : _readGeneric(device); +constexpr uint8_t ChannelControlByte { 0x45 }; +constexpr uint8_t ChannelAccess { 0xF5 }; - if (!status) { - return false; - } - } +class Sensor : public internal::Sensor { +public: + using Data = std::array; - return true; - } + using internal::Sensor::Sensor; - void _startConversion() { - _wire->reset(); - _wire->skip(); - _wire->write(DS_CMD_START_CONVERSION, DS_PARASITE); - } + static bool match(unsigned char id) { + return (id == chip::DS2406); + } - static bool validateID(unsigned char id) { - return (id == DS_CHIP_DS18S20) - || (id == DS_CHIP_DS18B20) - || (id == DS_CHIP_DS1822) - || (id == DS_CHIP_DS1825) - || (id == DS_CHIP_DS2406); - } + static bool match(const Device& device) { + return match(chip(device)); + } - static espurna::StringView _chipIdToStringView(unsigned char id) { - espurna::StringView out; - - switch (id) { - case DS_CHIP_DS18S20: - out = STRING_VIEW("DS18S20"); - break; - case DS_CHIP_DS18B20: - out = STRING_VIEW("DS18B20"); - break; - case DS_CHIP_DS1822: - out = STRING_VIEW("DS1822"); - break; - case DS_CHIP_DS1825: - out = STRING_VIEW("DS1825"); - break; - case DS_CHIP_DS2406: - out = STRING_VIEW("DS2406"); - break; - default: - out = STRING_VIEW("Unknown"); - break; - } + static unsigned char chip(const Device& device) { + return device.address[0]; + } - return out; - } + // --------------------------------------------------------------------- + // Sensor API + // --------------------------------------------------------------------- - static String _chipIdToString(unsigned char id) { - return _chipIdToStringView(id).toString(); - } + unsigned char id() const override { + return SENSOR_DALLAS_ID; + } - String chipAsString(unsigned char index) const { - return _chipIdToString(chip(index)); - } + unsigned char count() const override { + return 1; + } - static unsigned char chip(const Device& device) { - return device.address[0]; + void begin() override { + _ready = true; + _dirty = false; + + if (_port_handler) { + _startPortRead(); + } + } + + void notify() override { + _read_error = _read(); + } + + // Descriptive name of the sensor + String description() const override { + return _description(); + } + + // Address of the device + String address(unsigned char) const override { + return _address(); + } + + // Type for slot # index + unsigned char type(unsigned char index) const override { + if (index == 0) { + return MAGNITUDE_DIGITAL; } - unsigned char chip(unsigned char index) const { - if (index < _devices.size()) { - return chip(_devices[index]); - } + return MAGNITUDE_NONE; + } - return 0; - } + // Number of decimals for a magnitude (or -1 for default) + signed char decimals(espurna::sensor::Unit unit) const override { + return 0; + } - void loadDevices() { - Address address; + // Pre-read hook (usually to populate registers with up-to-date data) + void pre() override { + _error = _read_error; - _wire->reset(); - _wire->reset_search(); + if (_error == SENSOR_ERROR_OK) { + _value = _valueFromData(_data); + } - while (_wire->search(address.data())) { - if (_wire->crc8(address.data(), address.size() - 1) != address.back()) { - continue; - } + if (_port_handler) { + _startPortRead(); + } + } - if (!validateID(address.front())) { - continue; - } + // Current value for slot # index + double value(unsigned char index) override { + if (index == 0) { + return _value; + } - Device out; - out.address = address; - _devices.emplace_back(std::move(out)); - } + return 0.0; + } + +private: + // 3 cmd bytes, 1 channel info byte, 1 0x00, 2 CRC16 + // CHANNEL INFO BYTE + // Bit 7 : Supply Indication 0 = no supply + // Bit 6 : Number of Channels 0 = channel A only + // Bit 5 : PIO-B Activity Latch + // Bit 4 : PIO-A Activity Latch + // Bit 3 : PIO B Sensed Level + // Bit 2 : PIO A Sensed Level + // Bit 1 : PIO-B Channel Flip-Flop Q + // Bit 0 : PIO-A Channel Flip-Flop Q + static double _valueFromData(const Data& data) { + return ((data[3] & 0x04) != 0) ? 1.0 : 0.0; + } + + int _read(Device& device, Data& out) { + Data data{}; + data[0] = ChannelAccess; + data[1] = ChannelControlByte; + data[2] = 0xFF; + + auto ok = _port->request( + device.address, std::cref(data).get(), data); + if (!ok) { + return SENSOR_ERROR_TIMEOUT; } - using TimeSource = espurna::time::CoreClock; - TimeSource::time_point _last_reading; + ok = espurna::driver::onewire::check_crc16( + espurna::make_span(std::cref(data).get())); + if (!ok) { + return SENSOR_ERROR_CRC; + } - static constexpr auto ReadInterval = TimeSource::duration { DALLAS_READ_INTERVAL }; + std::copy(data.begin(), data.end(), out.begin()); + + return SENSOR_ERROR_OK; + } + + void _startPortRead() { + _read_error = SENSOR_ERROR_NOT_READY; + notify_now( + [](const BaseSensor* sensor) { + return SENSOR_DALLAS_ID == sensor->id(); + }); + } + + int _read() { + return _read(_device, _data); + } + + String _description() const { + char buffer[24]; + snprintf_P(buffer, sizeof(buffer), + PSTR("%s @ GPIO%hhu"), + _chipIdToString(chip(_device)).c_str(), + _port->pin()); + return String(buffer); + } + + String _address() const { + return hexEncode(_device.address); + } + + static espurna::StringView _chipIdToStringView(unsigned char id) { + espurna::StringView out; + + switch (id) { + case chip::DS2406: + out = STRING_VIEW("DS2406"); + break; + + default: + out = STRING_VIEW("Unknown"); + break; + } - std::vector _devices; + return out; + } - bool _conversion = true; - unsigned char _gpio = GPIO_NONE; - unsigned char _previous = GPIO_NONE; - std::unique_ptr _wire; + static String _chipIdToString(unsigned char id) { + return _chipIdToStringView(id).toString(); + } + Data _data{}; + double _value{}; }; +} // namespace digital + +} // namespace +} // namespace dallas +} // namespace driver +} // namespace sensor +} // namespace espurna + #endif // SENSOR_SUPPORT && DALLAS_SUPPORT