Skip to content

Commit

Permalink
v9261f: lazy parsing
Browse files Browse the repository at this point in the history
ref. #2483, #2546 and #2554
instead of waiting for our time window, attempt to read & parse everything
discard unknown data more frequently, and attempt to store more data as well
  • Loading branch information
mcspr committed Dec 4, 2022
1 parent ae0ae0b commit bea8508
Showing 1 changed file with 215 additions and 106 deletions.
321 changes: 215 additions & 106 deletions code/espurna/sensors/V9261FSensor.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@
#include "../libs/fs_math.h"

class V9261FSensor : public BaseEmonSensor {
private:
static constexpr uint16_t AddressPowerActive = 0x119;
static constexpr uint16_t AddressPowerReactive = 0x11a;
static constexpr uint16_t AddressVoltage = 0x11b;
static constexpr uint16_t AddressCurrent = 0x11c;

static constexpr uint8_t ControlDirectionMask = 0b1111;
static constexpr uint8_t ControlRead = 0b1;
static constexpr uint8_t ControlWrite = 0b10;

static constexpr uint8_t ControlAddressMask = 0b11110000;

static constexpr uint8_t HeadByte = 0b11111110;

public:

Expand Down Expand Up @@ -59,7 +72,8 @@ class V9261FSensor : public BaseEmonSensor {

// Descriptive name of the sensor
String description() const override {
return F("V9261F");
STRING_VIEW_INLINE(Name, "V9261F");
return Name.toString();
}

// Address of the sensor (it could be the GPIO or I2C address)
Expand All @@ -69,7 +83,8 @@ class V9261FSensor : public BaseEmonSensor {

// Loop-like method, call it in your main loop
void tick() override {
_read();
_read_some();
_process();
}

// Type for slot # index
Expand Down Expand Up @@ -141,125 +156,171 @@ class V9261FSensor : public BaseEmonSensor {
// Protected
// ---------------------------------------------------------------------

void _read() {
// Current approach is to just listen for the incoming data
// We never send anything (and usually disable TX on the port)
void _read_some() {
const auto result = _serial->available();
if (result <= 0) {
return;
}

// we are seeing the data request
if (_state == 0) {
const auto available = _serial->available();
if (available <= 0) {
if (_found && (TimeSource::now() - _timestamp > SyncInterval)) {
_index = 0;
_state = 1;
}
return;
}
const size_t available = result;
if (available >= (_buffer.size() - _size)) {
_size = 0;
return;
}

consumeAvailable(*_serial);
_found = true;
_timestamp = TimeSource::now();
const auto read = _serial->readBytes(
_buffer.begin() + _size,
std::min(static_cast<size_t>(available), _buffer.size() - _size));
if (read < 0) {
return;
}

_size += static_cast<size_t>(result);
}

// ...which we just skip...
} else if (_state == 1) {
void _process() {
const auto begin = _buffer.begin();
const auto end = begin + _size;

_index += consumeAvailable(*_serial);
if (_index++ >= 7) {
_index = 0;
_state = 2;
switch (_status) {
// Every read or write frame starts with specific byte
case Status::Idle:
{
if (!_size) {
break;
}

// ...until we receive response...
} else if (_state == 2) {
auto it = std::find(begin, end, HeadByte);
if (it == end) {
break;
}

const auto available = _serial->available();
if (available <= 0) {
return;
if (it != _buffer.begin()) {
std::copy_backward(it, end, _buffer.begin());
}

_index += _serial->readBytes(&_data[_index], std::min(
static_cast<size_t>(available), sizeof(_data)));
if (_index >= 19) {
_timestamp = TimeSource::now();
_state = 3;
_status = Status::ExpectWriteResponse;
_timestamp = TimeSource::now();
break;
}

// 7.3 - communication protocol
// write operation ack from sensor
case Status::ExpectWriteResponse:
case Status::ExpectReadWriteRequest:
case Status::ExpectDataResponse:
{
const size_t expect = expectedLength(_status);
if (_size < expect) {
break;
}

// validate received data and wait for the next request -> response
// FE1104 25F2420069C1BCFF20670C38C05E4101 B6
// ^^^^^^ - HEAD byte, mask, number of values
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - u32 4 times
// ^^ - CRC byte
} else if (_state == 3) {

if (_checksum(&_data[0], &_data[19]) == _data[19]) {

_active = (double) (
(_data[3]) +
(_data[4] << 8) +
(_data[5] << 16) +
(_data[6] << 24)
) / _power_active_ratio;

// With known ratio, could also use this
// _reactive = (double) (
// (_data[7]) +
// (_data[8] << 8) +
// (_data[9] << 16) +
// (_data[10] << 24);

_voltage = (double) (
(_data[11]) +
(_data[12] << 8) +
(_data[13] << 16) +
(_data[14] << 24)
) / _voltage_ratio;

_current = (double) (
(_data[15]) +
(_data[16] << 8) +
(_data[17] << 16) +
(_data[18] << 24)
) / _current_ratio;

if (_active < 0) _active = 0;
if (_voltage < 0) _voltage = 0;
if (_current < 0) _current = 0;

_apparent = _voltage * _current;
_factor = ((_voltage > 0) && (_current > 0))
? (100 * _active / _voltage / _current)
: 100;

if (_apparent > _active) {
_reactive = fs_sqrt(_apparent * _apparent - _active * _active);
} else {
_reactive = 0;
}
const auto checksum = _checksum(
_buffer.begin(), _buffer.begin() + expect - 1);

const auto now = TimeSource::now();
if (_reading) {
using namespace espurna::sensor;
const auto elapsed = std::chrono::duration_cast<espurna::duration::Seconds>(now - _last_reading);
_energy[0] += WattSeconds(Watts{_active}, elapsed);
if (_buffer[expect - 1] == checksum) {
if (_status != Status::ExpectDataResponse) {
consumeFirst(expect);
break;
}
_status = Status::Update;
break;
}

_reading = true;
_last_reading = now;
_status = nextStatus(_status);
break;
}

case Status::Update:
{
if (_size < 20) {
break;
}

_timestamp = TimeSource::now();
_index = 0;
_state = 4;
Data data;
std::copy(
_buffer.begin() + 3,
_buffer.begin() + 19,
data.begin());
_decode(data);

// ... by consuming everything until our clock runs out
} else if (_state == 4) {
consumeFirst(20);

consumeAvailable(*_serial);
if (TimeSource::now() - _timestamp > SyncInterval) {
_state = 1;
}
_status = Status::Idle;
break;
}

}

if ((_status != Status::Idle) && ((TimeSource::now() - _timestamp) > SyncInterval)) {
consumeFirst(_size);
_status = Status::Idle;
}
}

using Data = std::array<uint8_t, 16>;

void _decode(Data data) {
// 0x0119
_active = (double) (
(data[0]) +
(data[1] << 8) +
(data[2] << 16) +
(data[3] << 24)
) / _power_active_ratio;

// TODO with a known ratio, consider parsing
// 0x011a
// _reactive = (double) (
// (data[4]) +
// (data[5] << 8) +
// (data[6] << 16) +
// (data[7] << 24);

// 0x011b
_voltage = (double) (
(data[8]) +
(data[9] << 8) +
(data[10] << 16) +
(data[11] << 24)
) / _voltage_ratio;

// 0x011c
_current = (double) (
(data[12]) +
(data[13] << 8) +
(data[14] << 16) +
(data[15] << 24)
) / _current_ratio;

// note: discard negative values
_active = std::max(_active, 0.0);
_reactive = std::max(_reactive, 0.0);
_voltage = std::max(_voltage, 0.0);
_current = std::max(_current, 0.0);

_apparent = _voltage * _current;
_factor = ((_voltage > 0) && (_current > 0))
? (100 * _active / _voltage / _current)
: 100;

if (_apparent > _active) {
_reactive = fs_sqrt(_apparent * _apparent - _active * _active);
} else {
_reactive = 0;
}

const auto now = TimeSource::now();
if (_reading) {
using namespace espurna::sensor;
const auto elapsed = std::chrono::duration_cast<espurna::duration::Seconds>(now - _last_reading);
_energy[0] += WattSeconds(Watts{_active}, elapsed);
}

_reading = true;
_last_reading = now;
}

static uint8_t _checksum(const uint8_t* begin, const uint8_t* end) {
Expand All @@ -273,8 +334,6 @@ class V9261FSensor : public BaseEmonSensor {

// ---------------------------------------------------------------------

Stream* _serial { nullptr };

using TimeSource = espurna::time::CoreClock;
static constexpr auto SyncInterval = TimeSource::duration { V9261F_SYNC_INTERVAL };

Expand All @@ -287,16 +346,66 @@ class V9261FSensor : public BaseEmonSensor {

double _factor { 0 };

bool _reading { false };
TimeSource::time_point _last_reading;
TimeSource::time_point _timestamp;

int _state { 0 };
bool _found { false };
bool _reading { false };
enum class Status {
Idle,
ExpectWriteResponse,
ExpectReadWriteRequest,
ExpectDataResponse,
Update,
};

uint8_t _data[24] {0};
size_t _index { 0 };
static Status nextStatus(Status status) {
switch (status) {
case Status::Idle:
break;
case Status::ExpectWriteResponse:
return Status::ExpectReadWriteRequest;
case Status::ExpectReadWriteRequest:
return Status::ExpectDataResponse;
case Status::ExpectDataResponse:
case Status::Update:
break;
}

return Status::Idle;
}

static size_t expectedLength(Status status) {
switch (status) {
case Status::Idle:
break;
case Status::ExpectWriteResponse:
return 4;
case Status::ExpectReadWriteRequest:
return 8;
case Status::ExpectDataResponse:
return 20;
case Status::Update:
break;
}

return 0;
}

void consumeFirst(size_t size) {
std::copy_backward(
_buffer.begin() + size,
_buffer.begin() + _size,
_buffer.begin());
_size -= size;
}

Status _status { Status::Idle };

using Buffer = std::array<uint8_t, 48>;
Buffer _buffer;
size_t _size { 0 };

Stream* _serial { nullptr };
};

#if __cplusplus < 201703L
Expand Down

0 comments on commit bea8508

Please sign in to comment.