From 6c382d7259a427fc52ea280f7868484c5c5dc6b1 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Sat, 10 Feb 2024 22:11:40 +0300 Subject: [PATCH] dallas: per-device sensor instance ref. #2543 search the bus preemptively and create sensor instances based on that more clean-up for error handling, allow specific device to fail independently most of the sensor was re-written - general one-wire operations are part of the system driver now - split class definitions for digital and temperature sensors - system timer instead of polling, enforce ordering by forcing specific instance to handle conversion request, while notifying every other ones - synchronize readings and conversion requests with the sensor reading interval, instead of relying on internal polling. now, it happens right after begin() and pre() instead of previosly (almost) random tick() calls - auto-detect conversion time based on device resolution / resolution setting initialization also adds runtime settings (read on boot) - `dallasPin` to set current one-wire pin - `dallasParasite` to change from non-powered mode (default is still on) - `dallasResolution` to force sensor to be operated using 9...12bit res (0 to keep default one) --- code/espurna/config/sensors.h | 17 +- code/espurna/config/types.h | 3 +- code/espurna/sensor.cpp | 166 ++++- code/espurna/sensors/BaseSensor.h | 9 +- code/espurna/sensors/DallasSensor.h | 963 ++++++++++++++++------------ 5 files changed, 714 insertions(+), 444 deletions(-) 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