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.

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