Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

572 lines
18 KiB

  1. /*
  2. PZEM004T V3 Sensor
  3. Adapted for ESPurna based on:
  4. - https://github.com/mandulaj/PZEM-004T-v30 by Jakub Mandula
  5. - https://innovatorsguru.com/wp-content/uploads/2019/06/PZEM-004T-V3.0-Datasheet-User-Manual.pdf
  6. - http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
  7. Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
  8. */
  9. #include "BaseEmonSensor.h"
  10. #include "../debug.h"
  11. #include "../utils.h"
  12. #include "../terminal.h"
  13. #include <cstdint>
  14. #include <array>
  15. #define PZEM_DEBUG_MSG_P(...) if (_debug) DEBUG_MSG_P(__VA_ARGS__)
  16. class PZEM004TV30Sensor : public BaseEmonSensor {
  17. private:
  18. PZEM004TV30Sensor() : BaseEmonSensor(0) {
  19. _sensor_id = SENSOR_PZEM004TV30_ID;
  20. _error = SENSOR_ERROR_OK;
  21. _count = 6;
  22. }
  23. ~PZEM004TV30Sensor() {
  24. PZEM004TV30Sensor::instance = nullptr;
  25. }
  26. public:
  27. static PZEM004TV30Sensor* instance;
  28. static PZEM004TV30Sensor* create() {
  29. if (PZEM004TV30Sensor::instance) return PZEM004TV30Sensor::instance;
  30. PZEM004TV30Sensor::instance = new PZEM004TV30Sensor();
  31. return PZEM004TV30Sensor::instance;
  32. }
  33. static constexpr unsigned long Baudrate = 9600u;
  34. // per MODBUS application protocol specification
  35. // > 4.1 Protocol description
  36. // > ...
  37. // > The size of the MODBUS PDU is limited by the size constraint inherited from the first
  38. // > MODBUS implementation on Serial Line network (max. RS485 ADU = 256 bytes).
  39. // > Therefore:
  40. // > MODBUS PDU for serial line communication = 256 - Server address (1 byte) - CRC (2
  41. // > bytes) = 253 bytes.
  42. // However, we only ever expect very small payloads. Maximum being 10 registers at the same time.
  43. static constexpr size_t BufferSize = 25u;
  44. // stock address, cannot be used with multiple devices on the line
  45. static constexpr uint8_t DefaultAddress = 0xf8;
  46. // XXX: pzem manual does not specify anything, these are arbitrary values (ms)
  47. static constexpr unsigned long DefaultReadTimeout = 200u;
  48. static constexpr unsigned long DefaultUpdateInterval = 200u;
  49. // Device uses Modbus-RTU protocol and implements the following function codes:
  50. // - 0x03 (Read Holding Register) (NOT IMPLEMENTED)
  51. // - 0x04 (Read Input Register) (measurements readout)
  52. // - 0x06 (Write Single Register) (set device address, set alarm is NOT IMPLEMENTED)
  53. // - 0x41 (Calibration) (NOT IMPLEMENTED)
  54. // - 0x42 (Reset energy) (can only reset to 0)
  55. static constexpr uint8_t ReadInputCode = 0x04;
  56. static constexpr uint8_t WriteCode = 0x06;
  57. static constexpr uint8_t ResetEnergyCode = 0x42;
  58. static constexpr uint8_t ErrorMask = 0x80;
  59. // We **can** reset PZEM energy, unlike the original PZEM004T
  60. // However, we can't set it to a specific value, we can only start from 0
  61. void resetEnergy(unsigned char, sensor::Energy) override {}
  62. void resetEnergy() override {
  63. _reset_energy = true;
  64. }
  65. void resetEnergy(unsigned char) override {
  66. _reset_energy = true;
  67. }
  68. double getEnergy(unsigned char index) override {
  69. return _energy;
  70. }
  71. sensor::Energy totalEnergy(unsigned char index) override {
  72. return getEnergy(index);
  73. }
  74. size_t countDevices() override {
  75. return 1;
  76. }
  77. // ---------------------------------------------------------------------
  78. using buffer_type = std::array<uint8_t, BufferSize>;
  79. // - PZEM manual "2.7 CRC check":
  80. // > CRC check use 16bits format, occupy two bytes, the generator polynomial is X16 + X15 + X2 +1,
  81. // > the polynomial value used for calculation is 0xA001.
  82. // - Note that we use a simple function instead of a table to save space and RAM.
  83. static uint16_t crc16modbus(uint8_t* data, size_t size) {
  84. auto crc16_update = [](uint16_t crc, uint8_t value) {
  85. crc ^= static_cast<uint16_t>(value);
  86. for (size_t index = 0; index < 8; ++index) {
  87. if (crc & 1) {
  88. crc = (crc >> 1) ^ 0xa001;
  89. } else {
  90. crc = (crc >> 1);
  91. }
  92. }
  93. return crc;
  94. };
  95. uint16_t crc = 0xffff;
  96. for (size_t index = 0; index < size; ++index) {
  97. crc = crc16_update(crc, data[index]);
  98. }
  99. return crc;
  100. }
  101. struct adu_builder {
  102. adu_builder(uint8_t device_address, uint8_t fcode) :
  103. buffer({device_address, fcode}),
  104. size(2)
  105. {}
  106. adu_builder& add(uint8_t value) {
  107. if (!locked && (size < buffer.size())) {
  108. buffer[size] = value;
  109. size += 1;
  110. }
  111. return *this;
  112. }
  113. adu_builder& add(uint16_t value) {
  114. if (!locked && ((size + 1) < buffer.size())) {
  115. buffer[size] = static_cast<uint8_t>((value >> 8) & 0xff);
  116. buffer[size + 1] = static_cast<uint8_t>(value & 0xff);
  117. size += 2;
  118. }
  119. return *this;
  120. }
  121. // Note that CRC order is reversed in comparison to every other value
  122. adu_builder& end() {
  123. static_assert(BufferSize >= 4, "Cannot fit the minimal request");
  124. if (!locked) {
  125. uint16_t value = crc16modbus(buffer.data(), size);
  126. buffer[size] = static_cast<uint8_t>(value & 0xff);
  127. buffer[size + 1] = static_cast<uint8_t>((value >> 8) & 0xff);
  128. size += 2;
  129. locked = true;
  130. }
  131. return *this;
  132. }
  133. buffer_type buffer;
  134. size_t size { 0 };
  135. bool locked { false };
  136. };
  137. void modbusDebugBuffer(const String& message, buffer_type& buffer, size_t size) {
  138. hexEncode(buffer.data(), size, _debug_buffer, sizeof(_debug_buffer));
  139. PZEM_DEBUG_MSG_P(PSTR("[PZEM004TV3] %s: %s (%u bytes)\n"), message.c_str(), _debug_buffer, size);
  140. }
  141. static size_t modbusExpect(const adu_builder& builder) {
  142. if (!builder.locked) {
  143. return 0;
  144. }
  145. switch (builder.buffer[1]) {
  146. case ReadInputCode:
  147. if (builder.size >= 6) {
  148. return 3 + (2 * ((builder.buffer[4] << 8) | (builder.buffer[5]))) + 2;
  149. }
  150. return 0;
  151. case WriteCode:
  152. return builder.size;
  153. case ResetEnergyCode:
  154. return builder.size;
  155. default:
  156. return 0;
  157. }
  158. }
  159. template <typename Callback>
  160. void modbusProcess(const adu_builder& builder, Callback callback) {
  161. if (!builder.locked) {
  162. return;
  163. }
  164. _stream->write(builder.buffer.data(), builder.size);
  165. size_t expect = modbusExpect(builder);
  166. if (!expect) {
  167. return;
  168. }
  169. uint8_t code = builder.buffer[1];
  170. uint8_t error_code = ErrorMask | code;
  171. size_t bytes = 0;
  172. buffer_type buffer;
  173. // In case we need multiple devices, we need to manually set each one with an unique address **and** also provide
  174. // a way to distinguish between bus messages based on addresses received. Multiple instances **could** work,
  175. // based on the idea that we never receive replies from unknown addresses i.e. we never NOT read responses fully
  176. // and leave something in the serial buffers.
  177. // TODO: testing is much easier, b/c we can just grab any modbus simulator and set up multiple devices
  178. auto ts = millis();
  179. while ((bytes < expect) && (millis() - ts <= _read_timeout)) {
  180. int c = _stream->read();
  181. if (c < 0) {
  182. continue;
  183. }
  184. if ((0 == bytes) && (_address != c)) {
  185. continue;
  186. }
  187. if (1 == bytes) {
  188. if (error_code == c) {
  189. expect = 5;
  190. } else if (code != c) {
  191. bytes = 0;
  192. continue;
  193. }
  194. }
  195. buffer[bytes++] = static_cast<uint8_t>(c);
  196. }
  197. if (bytes && _debug) {
  198. modbusDebugBuffer(F("Received"), buffer, bytes);
  199. }
  200. if (bytes != expect) {
  201. PZEM_DEBUG_MSG_P(PSTR("[PZEM004TV3] ERROR: Expected %u bytes, got %u\n"), expect, bytes);
  202. _error = SENSOR_ERROR_OTHER; // TODO: more error codes
  203. return;
  204. }
  205. uint16_t received_crc = static_cast<uint16_t>(buffer[bytes - 1] << 8) | static_cast<uint16_t>(buffer[bytes - 2]);
  206. uint16_t crc = crc16modbus(buffer.data(), bytes - 2);
  207. if (received_crc != crc) {
  208. PZEM_DEBUG_MSG_P(PSTR("[PZEM004TV3] ERROR: CRC invalid: expected %04X expected, received %04X\n"), crc, received_crc);
  209. _error = SENSOR_ERROR_CRC;
  210. return;
  211. }
  212. if (buffer[1] & ErrorMask) {
  213. PZEM_DEBUG_MSG_P(PSTR("[PZEM004TV3] ERROR: %s (0x%02X)\n"),
  214. errorToString(buffer[2]).c_str(), buffer[2]);
  215. return;
  216. }
  217. callback(std::move(buffer), bytes);
  218. }
  219. // Energy reset is a 'custom' function, and it does not take any function params
  220. bool modbusResetEnergy() {
  221. auto request = adu_builder(_address, ResetEnergyCode)
  222. .end();
  223. // quoting pzem user manual: "Set up correctly, the slave return to the data which is sent from the master.",
  224. bool result = false;
  225. modbusProcess(request, [&](buffer_type&& buffer, size_t size) {
  226. result = std::equal(request.buffer.begin(), request.buffer.begin() + size, buffer.begin());
  227. });
  228. return result;
  229. }
  230. // Address setter is only needed when we are using multiple devices.
  231. // Note that we would no longer be able to receive replies without changing _address member too
  232. bool modbusChangeAddress(uint8_t to) {
  233. if (_address == to) {
  234. return true;
  235. }
  236. auto request = adu_builder(_address, WriteCode)
  237. .add(static_cast<uint16_t>(2))
  238. .add(static_cast<uint16_t>(to))
  239. .end();
  240. // same as for resetEnergy, we receive echo
  241. bool result = false;
  242. modbusProcess(request, [&](buffer_type&& buffer, size_t size) {
  243. result = std::equal(request.buffer.begin(), request.buffer.begin() + size, buffer.begin());
  244. });
  245. return result;
  246. }
  247. // For more, see MODBUS application protocol specification, 7 MODBUS Exception Responses
  248. String errorToString(uint8_t error) {
  249. const __FlashStringHelper *ptr = nullptr;
  250. switch (error) {
  251. case 0x01:
  252. ptr = F("Illegal function");
  253. break;
  254. case 0x02:
  255. ptr = F("Illegal data address");
  256. break;
  257. case 0x03:
  258. ptr = F("Illegal data value");
  259. break;
  260. case 0x04:
  261. ptr = F("Device failure");
  262. break;
  263. case 0x05:
  264. ptr = F("Acknowledged");
  265. break;
  266. case 0x06:
  267. ptr = F("Busy");
  268. break;
  269. case 0x08:
  270. ptr = F("Memory parity error");
  271. break;
  272. default:
  273. ptr = F("Unknown");
  274. break;
  275. }
  276. return ptr;
  277. }
  278. // Quoting the README.md of the original library repo and datasheet, we have:
  279. // (name, measuring range, resolution, accuracy)
  280. // 1. Voltage 80~260V 0.1V 0.5%
  281. // 2. Current 0~10A or 0~100A* 0.01A or 0.02A* 0.5%
  282. // 3. Active power 0~2.3kW or 0~23kW* 0.1W 0.5%
  283. // 4. Active energy 0~9999.99kWh 1Wh 0.5%
  284. // 5. Frequency 45~65Hz 0.1Hz 0.5%
  285. // 6. Power factor 0.00~1.00 0.01 1%
  286. void parseMeasurements(buffer_type&& buffer, size_t size) {
  287. if (25 != size) {
  288. PZEM_DEBUG_MSG_P(PSTR("[PZEM004TV3] Expected measurements ADU size to be at least 25 bytes, but got only %u\n"), size);
  289. return;
  290. }
  291. auto it = buffer.begin() + 3;
  292. auto end = buffer.end();
  293. auto take_2 = [&]() -> double {
  294. double value = 0.0;
  295. if (std::distance(it, end) >= 2) {
  296. value = (static_cast<uint32_t>(*(it)) << 8)
  297. | static_cast<uint32_t>(*(it + 1));
  298. it += 2;
  299. }
  300. return value;
  301. };
  302. auto take_4 = [&]() -> double {
  303. double value = 0.0;
  304. if (std::distance(it, end) >= 4) {
  305. value = (
  306. ((static_cast<uint32_t>(*(it + 2)) << 24)
  307. | (static_cast<uint32_t>(*(it + 3)) << 16))
  308. | ((static_cast<uint32_t>(*it) << 8)
  309. | static_cast<uint32_t>(*(it + 1))));
  310. it += 4;
  311. }
  312. return value;
  313. };
  314. // - Voltage: 2 bytes, in 0.1V (we return V)
  315. _voltage = take_2();
  316. _voltage /= 10.0;
  317. // - Current: 4 bytes, in 0.001A (we return A)
  318. _current = take_4();
  319. _current /= 1000.0;
  320. // - Power: 4 bytes, in 0.1W (we return W)
  321. _power = take_4();
  322. _power /= 10.0;
  323. // - Energy: 4 bytes, in Wh (we return kWh)
  324. _energy = take_4();
  325. _energy /= 1000.0;
  326. // - Frequency: 2 bytes, in 0.1Hz (we return Hz)
  327. _frequency = take_2();
  328. _frequency /= 10.0;
  329. // - Power Factor: 2 bytes in 0.01 (we return %)
  330. _power_factor = take_2();
  331. // - Alarms: 2 bytes, (NOT IMPLEMENTED)
  332. // XXX: it seems it can only be either 0xffff or 0 for ON and OFF respectively
  333. // XXX: what this does, exactly?
  334. _alarm = (0xff == *it) && (0xff == *(it + 1));
  335. }
  336. // Reading measurements is a standard modbus function:
  337. // - addr, 0x04, rhigh, rlow, rnumhigh, rnumlow, crchigh, crclow
  338. // ReadInput reply can be one of:
  339. // - addr, 0x04, nbytes, rndatahigh, rndatalow, rndata..., crchigh, crclow (on success)
  340. // - addr, 0x84, error_code, crchigh, crclow (on error. modbus rtu sets high bit to 1 i.e. 0b00000100 becomes 0b10000100)
  341. void modbusReadValues() {
  342. _error = SENSOR_ERROR_OK;
  343. auto request = adu_builder(_address, ReadInputCode)
  344. .add(static_cast<uint16_t>(0))
  345. .add(static_cast<uint16_t>(10))
  346. .end();
  347. modbusProcess(request, [this](buffer_type&& buffer, size_t size) {
  348. parseMeasurements(std::move(buffer), size);
  349. });
  350. }
  351. void flush() {
  352. while (_stream->read() >= 0) {
  353. }
  354. }
  355. // ---------------------------------------------------------------------
  356. // Note that the device (aka slave) address needs be changed first via
  357. // - some external tool. For example, using USB2TTL adapter and a PC app
  358. // - `pzem.address` with **only** one device on the line
  359. // (because we would change all 0xf8-addressed devices at the same time)
  360. void setAddress(uint8_t address) {
  361. _address = address;
  362. }
  363. void setDebug(bool debug) {
  364. _debug = debug;
  365. }
  366. void setStream(Stream* stream) {
  367. _stream = stream;
  368. _stream->setTimeout(_read_timeout);
  369. }
  370. void setReadTimeout(unsigned long value) {
  371. _read_timeout = value;
  372. }
  373. void setUpdateInterval(unsigned long value) {
  374. _update_interval = value;
  375. }
  376. template <typename T>
  377. void setDescription(T&& description) {
  378. _description = std::forward<T>(description);
  379. }
  380. // ---------------------------------------------------------------------
  381. void begin() override {
  382. _ready = (_stream != nullptr);
  383. _last_reading = millis() - _update_interval;
  384. #if TERMINAL_SUPPORT
  385. terminalRegisterCommand(F("PZ.ADDRESS"), [](const terminal::CommandContext& ctx) {
  386. if (ctx.argc != 2) {
  387. terminalError(ctx.output, F("PZ.ADDRESS <ADDRESS>"));
  388. return;
  389. }
  390. uint8_t updated = settings::internal::convert<uint8_t>(ctx.argv[1]);
  391. PZEM004TV30Sensor::instance->flush();
  392. if (PZEM004TV30Sensor::instance->modbusChangeAddress(updated)) {
  393. PZEM004TV30Sensor::instance->setAddress(updated);
  394. setSetting("pzemv30Addr", updated);
  395. terminalOK(ctx.output);
  396. return;
  397. }
  398. terminalError(ctx.output, F("Could not change the address"));
  399. });
  400. #endif
  401. }
  402. String description() override {
  403. static const String base(F("PZEM004T V3.0"));
  404. return base + " @ " + _description + ", 0x" + String(_address, 16);
  405. }
  406. String description(unsigned char) override {
  407. return description();
  408. }
  409. String address(unsigned char) override {
  410. return String(_address, 16);
  411. }
  412. unsigned char type(unsigned char index) override {
  413. switch (index) {
  414. case 0: return MAGNITUDE_VOLTAGE;
  415. case 1: return MAGNITUDE_CURRENT;
  416. case 2: return MAGNITUDE_POWER_ACTIVE;
  417. case 3: return MAGNITUDE_ENERGY;
  418. case 4: return MAGNITUDE_FREQUENCY;
  419. case 5: return MAGNITUDE_POWER_FACTOR;
  420. }
  421. return MAGNITUDE_NONE;
  422. }
  423. double value(unsigned char index) override {
  424. switch (index) {
  425. case 0: return _voltage;
  426. case 1: return _current;
  427. case 2: return _power;
  428. case 3: return _energy;
  429. case 4: return _frequency;
  430. case 5: return _power_factor;
  431. }
  432. return 0.0;
  433. }
  434. void pre() override {
  435. flush();
  436. if (_reset_energy) {
  437. if (!modbusResetEnergy()) {
  438. PZEM_DEBUG_MSG_P(PSTR("[PZEM004TV3] Energy reset failed\n"));
  439. }
  440. _reset_energy = false;
  441. flush();
  442. }
  443. if (millis() - _last_reading >= _update_interval) {
  444. modbusReadValues();
  445. _last_reading = millis();
  446. }
  447. }
  448. private:
  449. String _description;
  450. bool _debug { false };
  451. char _debug_buffer[(BufferSize * 2) + 1];
  452. Stream* _stream { nullptr };
  453. uint8_t _address { DefaultAddress };
  454. bool _reset_energy { false };
  455. unsigned long _read_timeout { DefaultReadTimeout };
  456. unsigned long _update_interval { DefaultUpdateInterval };
  457. unsigned long _last_reading { 0 };
  458. double _voltage { 0.0 };
  459. double _current { 0.0 };
  460. double _power { 0.0 };
  461. double _energy { 0.0 };
  462. double _frequency { 0.0 };
  463. double _power_factor { 0.0 };
  464. bool _alarm { false };
  465. };
  466. PZEM004TV30Sensor* PZEM004TV30Sensor::instance = nullptr;
  467. #undef PZEM_DEBUG_MSG_P