diff --git a/wb-mqtt-serial/Makefile b/wb-mqtt-serial/Makefile index 7052e65..8066aed 100644 --- a/wb-mqtt-serial/Makefile +++ b/wb-mqtt-serial/Makefile @@ -46,7 +46,8 @@ SERIAL_SRCS=register.cpp \ modbus_device.cpp \ em_device.cpp \ milur_device.cpp \ - mercury230_device.cpp + mercury230_device.cpp \ + pulsar_device.cpp SERIAL_OBJS=$(SERIAL_SRCS:.cpp=.o) TEST_SRCS= \ $(TEST_DIR)/testlog.o \ @@ -59,6 +60,7 @@ TEST_SRCS= \ $(TEST_DIR)/em_test.o \ $(TEST_DIR)/em_integration.o \ $(TEST_DIR)/ivtm_test.o \ + $(TEST_DIR)/pulsar_test.o \ $(TEST_DIR)/fake_mqtt.o \ $(TEST_DIR)/fake_serial_port.o \ $(TEST_DIR)/pty_based_fake_serial.o \ diff --git a/wb-mqtt-serial/config-pulsar.json b/wb-mqtt-serial/config-pulsar.json new file mode 100644 index 0000000..5383c25 --- /dev/null +++ b/wb-mqtt-serial/config-pulsar.json @@ -0,0 +1,22 @@ +{ + "debug": true, + "ports": [ + { + "path" : "/tmp/ttyNSC1", + "protocol": "pulsar", + "baud_rate": 9600, + "parity": "N", + "data_bits": 8, + "stop_bits": 1, + "enabled": true, + "response_timeout_ms": 500, + "poll_interval" : 100, + "devices": [ + { + "slave_id": 1, + "device_type": "Pulsar" + } + ] + } + ] +} diff --git a/wb-mqtt-serial/debian/changelog b/wb-mqtt-serial/debian/changelog index 0674c59..be7f814 100644 --- a/wb-mqtt-serial/debian/changelog +++ b/wb-mqtt-serial/debian/changelog @@ -1,3 +1,10 @@ +wb-mqtt-serial (1.16) stable; urgency=medium + + * adds support for Pulsar and Pulsar-M water meters + * adds support for Pulsar compact heat meter + + -- Nikita Maslov Mon, 04 Jul 2016 15:08:58 +0300 + wb-mqtt-serial (1.15.2) stable; urgency=medium * adds schema for WB-MSGR diff --git a/wb-mqtt-serial/pulsar_device.cpp b/wb-mqtt-serial/pulsar_device.cpp new file mode 100644 index 0000000..32d975f --- /dev/null +++ b/wb-mqtt-serial/pulsar_device.cpp @@ -0,0 +1,232 @@ +/* vim: set ts=4 sw=4: */ + +#include +#include + +#include "pulsar_device.h" + +/* FIXME: move this to configuration file! */ +namespace { + const int FrameTimeout = 300; +} + +REGISTER_PROTOCOL("pulsar", TPulsarDevice, TRegisterTypes({ + { TPulsarDevice::REG_DEFAULT, "default", "value", Double, true }, + { TPulsarDevice::REG_SYSTIME, "systime", "value", U64, true } +})); + +TPulsarDevice::TPulsarDevice(PDeviceConfig device_config, PAbstractSerialPort port) + : TSerialDevice(device_config, port) + , RequestID(0) {} + +uint16_t TPulsarDevice::CalculateCRC16(const uint8_t *buffer, size_t size) +{ + uint16_t w; + uint16_t shift_cnt, f; + const uint8_t *ptrByte; + + uint16_t byte_cnt = size; + ptrByte = buffer; + w = (uint16_t) 0xFFFF; + + for (; byte_cnt > 0; byte_cnt--) { + w = (uint16_t) (w ^ (uint16_t) (*ptrByte++)); + for (shift_cnt = 0; shift_cnt < 8; shift_cnt++) { + f = (uint8_t) ((w) & 0x01); + w >>= 1; + if (f) + w = (uint16_t) (w ^ 0xa001); + } + } + + return w; +} + +void TPulsarDevice::WriteBCD(uint64_t value, uint8_t *buffer, size_t size, bool big_endian) +{ + for (size_t i = 0; i < size; i++) { + // form byte from the end of value + uint8_t byte = value % 10; + value /= 10; + byte |= (value % 10) << 4; + value /= 10; + + buffer[big_endian ? size - i - 1 : i] = byte; + } +} + +void TPulsarDevice::WriteHex(uint64_t value, uint8_t *buffer, size_t size, bool big_endian) +{ + for (size_t i = 0; i < size; i++) { + buffer[big_endian ? size - i - 1 : i] = value & 0xFF; + value >>= 8; + } +} + +uint64_t TPulsarDevice::ReadBCD(const uint8_t *buffer, size_t size, bool big_endian) +{ + uint64_t result = 0; + + for (size_t i = 0; i < size; i++) { + result *= 100; + + uint8_t bcd_byte = buffer[big_endian ? i : size - i - 1]; + uint8_t dec_byte = (bcd_byte & 0x0F) + 10 * ((bcd_byte >> 4) & 0x0F); + + result += dec_byte; + } + + return result; +} + +uint64_t TPulsarDevice::ReadHex(const uint8_t *buffer, size_t size, bool big_endian) +{ + uint64_t result = 0; + + for (size_t i = 0; i < size; i++) { + result <<= 8; + result |= buffer[big_endian ? i : size - i - 1]; + } + + return result; +} + +void TPulsarDevice::WriteDataRequest(uint32_t addr, uint32_t mask, uint16_t id) +{ + Port()->CheckPortOpen(); + + uint8_t buf[14]; + + /* header = device address in big-endian BCD */ + WriteBCD(addr, buf, sizeof (uint32_t), true); + + /* data request => F == 1, L == 14 */ + buf[4] = 1; + buf[5] = 14; + + /* data mask in little-endian */ + WriteHex(mask, &buf[6], sizeof (uint32_t), false); + + /* request ID */ + WriteHex(id, &buf[10], sizeof (uint16_t), false); + + /* CRC16 */ + uint16_t crc = CalculateCRC16(buf, 12); + WriteHex(crc, &buf[12], sizeof (uint16_t), false); + + Port()->WriteBytes(buf, 14); +} + +void TPulsarDevice::WriteSysTimeRequest(uint32_t addr, uint16_t id) +{ + Port()->CheckPortOpen(); + + uint8_t buf[10]; + + /* header = device address in big-endian BCD */ + WriteBCD(addr, buf, sizeof (uint32_t), true); + + /* sys time request => F == 4, L == 10 */ + buf[4] = 1; + buf[5] = 10; + + /* request ID */ + WriteHex(id, &buf[6], sizeof (uint16_t), false); + + /* CRC16 */ + uint16_t crc = CalculateCRC16(buf, 8); + WriteHex(crc, &buf[8], sizeof (uint16_t), false); + + Port()->WriteBytes(buf, 10); +} + +void TPulsarDevice::ReadResponse(uint32_t addr, uint8_t *payload, size_t size, uint16_t id) +{ + const int exp_size = size + 10; /* payload size + service bytes */ + uint8_t response[exp_size]; + + int nread = Port()->ReadFrame(response, exp_size, std::chrono::milliseconds(FrameTimeout), + [] (uint8_t *buf, int size) { + return size >= 6 && size == buf[5]; + }); + + /* check size */ + if (nread < 6) + throw TSerialDeviceTransientErrorException("frame is too short"); + + if (nread != exp_size) + throw TSerialDeviceTransientErrorException("unexpected end of frame"); + + if (exp_size != response[5]) + throw TSerialDeviceTransientErrorException("unexpected frame length"); + + /* check CRC16 */ + uint16_t crc_recv = ReadHex(&response[nread - 2], sizeof (uint16_t), false); + if (crc_recv != CalculateCRC16(response, nread - 2)) + throw TSerialDeviceTransientErrorException("CRC mismatch"); + + /* check address */ + uint32_t addr_recv = ReadBCD(response, sizeof (uint32_t), true); + if (addr_recv != addr) + throw TSerialDeviceTransientErrorException("slave address mismatch"); + + /* check request ID */ + uint16_t id_recv = ReadHex(&response[nread - 4], sizeof (uint16_t), false); + if (id_recv != id) + throw TSerialDeviceTransientErrorException("request ID mismatch"); + + /* copy payload data to external buffer */ + memcpy(payload, response + 6, size); +} + +uint64_t TPulsarDevice::ReadDataRegister(PRegister reg) +{ + // raw payload data + uint8_t payload[sizeof (uint64_t)]; + + // form register mask from address + uint32_t mask = 1 << reg->Address; // TODO: register range or something like this + + // send data request and receive response + WriteDataRequest(reg->Slave->Id, mask, RequestID); + ReadResponse(reg->Slave->Id, payload, reg->ByteWidth(), RequestID); + + ++RequestID; + + // decode little-endian double64_t value + return ReadHex(payload, reg->ByteWidth(), false); +} + +uint64_t TPulsarDevice::ReadSysTimeRegister(PRegister reg) +{ + // raw payload data + uint8_t payload[6]; + + // send system time request and receive response + WriteSysTimeRequest(reg->Slave->Id, RequestID); + ReadResponse(reg->Slave->Id, payload, sizeof (payload), RequestID); + + ++RequestID; + + // decode little-endian double64_t value + return ReadHex(payload, sizeof (payload), false); +} + +uint64_t TPulsarDevice::ReadRegister(PRegister reg) +{ + Port()->SkipNoise(); + + switch (reg->Type) { + case REG_DEFAULT: + return ReadDataRegister(reg); + case REG_SYSTIME: // TODO: think about return value + return ReadSysTimeRegister(reg); + default: + throw TSerialDeviceException("Pulsar protocol: wrong register type"); + } +} + +void TPulsarDevice::WriteRegister(PRegister reg, uint64_t value) +{ + throw TSerialDeviceException("Pulsar protocol: writing to registers is not supported"); +} diff --git a/wb-mqtt-serial/pulsar_device.h b/wb-mqtt-serial/pulsar_device.h new file mode 100644 index 0000000..5684a84 --- /dev/null +++ b/wb-mqtt-serial/pulsar_device.h @@ -0,0 +1,42 @@ +/* vim: set ts=4 sw=4: */ + +#pragma once + +#include +#include "serial_device.h" +#include + + +class TPulsarDevice: public TSerialDevice { +public: + + enum RegisterType { + REG_DEFAULT, + REG_SYSTIME + }; + + TPulsarDevice(PDeviceConfig device_config, PAbstractSerialPort port); + uint64_t ReadRegister(PRegister reg); + void WriteRegister(PRegister reg, uint64_t value); + +private: + void WriteBCD(uint64_t data, uint8_t *buffer, size_t size, bool big_endian = true); + void WriteHex(uint64_t data, uint8_t *buffer, size_t size, bool big_endian = true); + + uint64_t ReadBCD(const uint8_t *data, size_t size, bool big_endian = true); + uint64_t ReadHex(const uint8_t *data, size_t size, bool big_endian = true); + + uint16_t CalculateCRC16(const uint8_t *buffer, size_t size); + + void WriteDataRequest(uint32_t addr, uint32_t mask, uint16_t id); + void WriteSysTimeRequest(uint32_t addr, uint16_t id); + + void ReadResponse(uint32_t addr, uint8_t *payload, size_t size, uint16_t id); + + uint64_t ReadDataRegister(PRegister reg); + uint64_t ReadSysTimeRegister(PRegister reg); + + uint16_t RequestID; +}; + +typedef std::shared_ptr PPulsarDevice; diff --git a/wb-mqtt-serial/test/TPulsarDeviceTest.PulsarHeatMeterFloatQuery.dat b/wb-mqtt-serial/test/TPulsarDeviceTest.PulsarHeatMeterFloatQuery.dat new file mode 100644 index 0000000..818e741 --- /dev/null +++ b/wb-mqtt-serial/test/TPulsarDeviceTest.PulsarHeatMeterFloatQuery.dat @@ -0,0 +1,5 @@ +Open() +SkipNoise() +>> 00 10 70 80 01 0E 04 00 00 00 00 00 7C A7 +<< 00 10 70 80 01 0E 5A B3 C5 41 00 00 18 DB +Close() diff --git a/wb-mqtt-serial/test/pulsar_test.cpp b/wb-mqtt-serial/test/pulsar_test.cpp new file mode 100644 index 0000000..6410ef7 --- /dev/null +++ b/wb-mqtt-serial/test/pulsar_test.cpp @@ -0,0 +1,48 @@ +#include "testlog.h" +#include "fake_serial_port.h" +#include "pulsar_device.h" + +namespace { + PSlaveEntry HeatMeter = TSlaveEntry::Intern("pulsar", 107080); + + PRegister Heat_TempIn = TRegister::Intern(HeatMeter, 0, 2, Float); + PRegister Heat_TempOut = TRegister::Intern(HeatMeter, 0, 3, Float); + // TODO: time register +}; + +class TPulsarDeviceTest: public TSerialDeviceTest +{ +protected: + void SetUp(); + PPulsarDevice Dev; +}; + +void TPulsarDeviceTest::SetUp() +{ + TSerialDeviceTest::SetUp(); + + // Create device with fixed Slave ID + Dev = std::make_shared( + std::make_shared("pulsar-heat", 102030, "pulsar"), + SerialPort); + SerialPort->Open(); +} + +TEST_F(TPulsarDeviceTest, PulsarHeatMeterFloatQuery) +{ + // >> 00 10 70 80 01 0e 04 00 00 00 00 00 7C A7 + // << 00 10 70 80 01 0E 5A B3 C5 41 00 00 18 DB + // temperature == 24.71257 + + SerialPort->Expect( + { + 0x00, 0x10, 0x70, 0x80, 0x01, 0x0e, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0xa7 + }, + { + 0x00, 0x10, 0x70, 0x80, 0x01, 0x0e, 0x5a, 0xb3, 0xc5, 0x41, 0x00, 0x00, 0x18, 0xdb + }); + + ASSERT_EQ(0x41C5B35A, Dev->ReadRegister(Heat_TempIn)); + + SerialPort->Close(); +} diff --git a/wb-mqtt-serial/wb-mqtt-serial-templates/config-pulsar-heat.json b/wb-mqtt-serial/wb-mqtt-serial-templates/config-pulsar-heat.json new file mode 100644 index 0000000..584b8ac --- /dev/null +++ b/wb-mqtt-serial/wb-mqtt-serial-templates/config-pulsar-heat.json @@ -0,0 +1,87 @@ +{ + "device_type": "pulsar-heat", + "device": { + "name": "Pulsar Heat Meter", + "id": "pulsar-heat", + "protocol": "pulsar", + "channels": [ + { + "name": "Input temperature", + "reg_type": "default", + "address": 2, + "format": "float", + "type": "temperature" + }, + { + "name": "Output temperature", + "reg_type": "default", + "address": 3, + "format": "float", + "type": "temperature" + }, + { + "name": "Delta temperature", + "reg_type": "default", + "address": 4, + "format": "float", + "type": "temperature" + }, + { + "name": "Heat power", + "reg_type": "default", + "address": 5, + "format": "float", + "type": "heat_power" + }, + { + "name": "Heat energy", + "reg_type": "default", + "address": 6, + "format": "float", + "type": "heat_energy" + }, + { + "name": "Water consumption", + "reg_type": "default", + "address": 7, + "format": "float", + "type": "water_consumption" + }, + { + "name": "Water flow", + "reg_type": "default", + "address": 8, + "format": "float", + "type": "water_flow" + }, + { + "name": "Pulse 1", + "reg_type": "default", + "address": 9, + "format": "float", + "type": "water_consumption" + }, + { + "name": "Pulse 2", + "reg_type": "default", + "address": 10, + "format": "float", + "type": "water_consumption" + }, + { + "name": "Device temperature", + "reg_type": "default", + "address": 11, + "format": "float", + "type": "temperature" + }, + { + "name": "Status", + "reg_type": "default", + "address": 12, + "format": "u32", + "type": "value" + } + ] + } +} diff --git a/wb-mqtt-serial/wb-mqtt-serial-templates/config-pulsar-m-water.json b/wb-mqtt-serial/wb-mqtt-serial-templates/config-pulsar-m-water.json new file mode 100644 index 0000000..6c2c992 --- /dev/null +++ b/wb-mqtt-serial/wb-mqtt-serial-templates/config-pulsar-m-water.json @@ -0,0 +1,17 @@ +{ + "device_type": "pulsar-m-water", + "device": { + "name": "Pulsar-M Water Meter", + "id": "pulsar-m-water", + "protocol": "pulsar", + "channels": [ + { + "name": "Consumption", + "reg_type": "default", + "address": "0", + "format": "s32", + "type": "water_consumption" + } + ] + } +} diff --git a/wb-mqtt-serial/wb-mqtt-serial-templates/config-pulsar-water.json b/wb-mqtt-serial/wb-mqtt-serial-templates/config-pulsar-water.json new file mode 100644 index 0000000..f68adec --- /dev/null +++ b/wb-mqtt-serial/wb-mqtt-serial-templates/config-pulsar-water.json @@ -0,0 +1,17 @@ +{ + "device_type": "pulsar-water", + "device": { + "name": "Pulsar Water Meter", + "id": "pulsar-water", + "protocol": "pulsar", + "channels": [ + { + "name": "Consumption", + "reg_type": "default", + "address": "0", + "format": "float", + "type": "water_consumption" + } + ] + } +} diff --git a/wb-mqtt-serial/wb-mqtt-serial.schema.json b/wb-mqtt-serial/wb-mqtt-serial.schema.json index 485c32e..2f4c0c6 100644 --- a/wb-mqtt-serial/wb-mqtt-serial.schema.json +++ b/wb-mqtt-serial/wb-mqtt-serial.schema.json @@ -358,6 +358,7 @@ "atmospheric_pressure", "rainfall", "wind_speed", "power", "power_consumption", "voltage", "water_flow", "water_consumption", "resistance", "concentration", + "heat_energy", "heat_power", // FIXME: "dimmer", "lux" and "pressure" aren't present in // conventions but is present in config-test.json in // wb-mqtt-serial