7 our @ISA = qw(Exporter);
8 our @EXPORT = qw( modbus_crc16 $protocol read_parameter_frame write_parameter_frame $function_code_description hex_dump protocol_decode );
10 use Data::Dump qw(dump);
12 my $debug = $ENV{DEBUG} || 0;
14 # Table of CRC values for high order byte
15 use constant CRC_HI => [
16 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
17 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
18 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1,
19 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
20 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
21 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
22 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1,
23 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
24 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
25 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
26 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
27 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
28 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
29 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
30 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
31 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
32 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
33 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
34 0x00, 0xC1, 0x81, 0x40
37 # Table of CRC values for low order byte
38 use constant CRC_LO => [
39 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5,
40 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B,
41 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE,
42 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6,
43 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
44 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D,
45 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8,
46 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C,
47 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21,
48 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
49 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A,
50 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA,
51 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7,
52 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51,
53 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
54 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98,
55 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D,
56 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83,
57 0x41, 0x81, 0x80, 0x40
65 my @bytes = split(//, $msg);
67 my $data = unpack('C*', $_);
68 my $crcIdx = $crcl ^ $data;
69 $crcl = $crch ^ &CRC_HI->[$crcIdx];
70 $crch = &CRC_LO->[$crcIdx];
73 # Pack the trailer - CRC has low byte first
74 my $trlr = pack('CC', $crcl, $crch);
82 next if m{^\s*$}; # skip empty lines
83 my @a = split(/\s*\t\s*/, $_,7);
84 my $id = hex( $a[0] );
85 my $pack_fmt = $a[2]; $pack_fmt =~ s/\s+.*$//;
88 pack_fmt => $pack_fmt,
97 sub read_parameter_frame {
98 my $params = join('', @_);
102 . "\x0B" # protocol version
103 . "\x03" # function code
104 . pack("v", length($params)) # data length
109 return $frame . modbus_crc16($frame);
112 sub write_parameter_frame {
113 my $params = join('', @_);
117 . "\x0B" # protocol version
118 . "\x06" # function code
119 . pack("v", length($params)) # data length
124 return $frame . modbus_crc16($frame);
127 our $function_code_description = {
129 0x07 => 'Upstream Heartbeat Frame',
130 0x08 => 'Upstream alarm frame',
131 0x03 => 'Upstream Read Parameter Frame',
132 0x06 => 'Upstream return write parameter frame',
135 0x03 => 'Downstream read parameter frame',
136 0x06 => 'Downstream write parameter frame',
142 my $hex = unpack('H*', $bin);
143 $hex =~ s{(..)}{$1 }g;
147 sub protocol_decode {
148 my ( $up_down, $bin ) = @_;
150 $SIG{__WARN__} = sub {
151 return unless $debug;
160 my $cksum = substr($bin, -2, 2);
162 if ( my $crc = modbus_crc16( substr($bin,0,-2) ) ) {
163 if ( $crc ne $cksum ) {
164 $hash->{error}->{crc} = "got " . unpack('H*',$crc) . " expected " . unpack('H*',$cksum);
168 my ( $header, $ver, $function_code, $len ) = unpack("CCCv", $bin);
170 $hash->{function_code} = $function_code;
174 my $data = substr($bin,5,-2);
176 warn "header = $header 0x", hex_dump($header)," ver = $ver function_code = $function_code len = $len == ",length($data), " data = ",hex_dump($data), " cksum = ", unpack('H*',$cksum);
178 if ( $header != 0x5a ) {
179 $hash->{error}->{header} = "$header expected 0x5a";
182 my $length_data = length($data);
183 if ( $length_data != $len ) {
184 $hash->{error}->{length} = "$length_data expected $len";
190 warn "XXX data = ", hex_dump($data);
192 my $data_id = unpack( 'C', substr($data,0,1) );
194 if ( ! exists( $protocol->{$data_id} ) ) {
195 my $len = unpack('C', substr($data,1,1));
196 push @{ $hash->{error}->{data_id} }, sprintf "data_id %d 0x%2x len %d [%s]", $data_id, $data_id, $len, unpack('H*', substr($data,2,$len));
197 $data = substr($data,2 + $len);
200 my $pack_fmt = $protocol->{$data_id}->{pack_fmt} || die "can't find pack_fmt for data_id $data_id";
205 if ( $data_id == 0x00 ) {
208 $data_range = substr($data,2,4);
209 $data = substr($data,6);
211 } elsif ( $up_down eq 'down' && $function_code == 0x03 ) {
213 # 0x03 => 'Downstream read parameter frame',
215 $data_range = substr($data,0,1);
216 $data = substr($data,1);
218 } elsif ( $up_down eq 'up' && ( $function_code == 0x07 || $function_code == 0x08 || $function_code == 0x03 ) ) {
220 # 0x07 => 'Upstream Heartbeat Frame',
221 # 0x08 => 'Upstream alarm frame',
222 # 0x03 => 'Upstream Read Parameter Frame',
224 $data_len = unpack('C', substr($data,1,1));
225 $data_range = substr($data,2, $data_len);
227 $data = substr($data,2 + $data_len);
229 } elsif ( $up_down eq 'down' && $function_code == 0x06 ) {
231 # 0x06 => 'Downstream write parameter frame',
233 $data_len = unpack('C', substr($data,1,1));
234 $data_range = substr($data,2, $data_len);
236 $data = substr($data,2 + $data_len);
238 } elsif ( $up_down eq 'up' && $function_code == 0x06 ) {
240 # 0x06 => 'Upstream return write parameter frame',
242 $data_len = unpack('C', substr($data,1,1));
243 $data_range = substr($data,2, $data_len);
244 if ( $data_len == 1 ) {
245 # XXX return is OK/not OK
249 $data = substr($data,2 + $data_len);
252 $hash->{error}->{function_code_unknown} = $function_code;
253 print STDERR "ERROR unknown function_code = $function_code\n";
257 my @v = unpack($pack_fmt, $data_range);
259 my $v = join(' ', @v);
261 if ( $data_id == 0x0c ) {
263 } elsif ( $data_id == 0x0d ) {
267 push @{ $hash->{data_id_order} }, $data_id;
268 $hash->{data_id}->{$data_id} = $v;
269 $hash->{data_len}->{$data_id} = $data_len;
270 $hash->{data_range}->{$data_id} = $data_range;
282 # Valid data ID and parameter range supported by the product
283 # DATA ID ID Description Data Type(Data Length) R/W Range Default Remark
286 0x00 Seq # L DWord(4) R / 0 The platform can carry the ID when the platform downstream reads and sets the device parameters, and the device returns the same data. Please refer to the example for use.Each seq # refer to one command and its response.
287 0x01 PN L DWord(4) R / / /
288 0x02 Model C Byte(1) R 32 32 Inner number:32
289 0x03 X axis angle f Float(4) R -90-90 / X axis angle
290 0x04 Y axis angle f Float(4) R -90-90 / Y axis angle
291 0x09 X axis relative angle f Float(4) R -90-90 0 Return the X angle value according to the set relative zero
292 0x0A Y axis relative angle f Float(4) R -90-90 0 Return the Y angle value according to the set relative zero
293 0x0C Sensor temperature s Word(2) R -32768-32767 / signed,sensor temperature =Data/100, unit celsius
294 0x0D Power source voltage S Word(2) R 0~65535 / Voltage= Data/100, unit V
295 0x0E Heartbeat interval l DWord(4) R/W 60~2160000 86400 ZCT330E Interval at which the device periodically uploads data to the server
296 0x0F Failure interval l DWord(4) R/W 60~2160000 3600 ZCT330E Interval at which the device failure is retransmitted
297 0x11 Arming/disarming C Byte(1) R/W 0~255 1 0 means disarming, non zero means arming
298 0x12 Alarm delay time C Byte(1) R/W 3~255 20 ZCT330M The unit is 0.1 second, which means that the product responds to the alarm only after the alarm has exceeded the alarm angle for a certain period of time. (mot used by ZCT330E, only ZCT330M)
300 0x13 Restore factory setting C Byte(1) R/W 0~255 0 0: Do nothing Non zero: Restore the non network related parameters of the sensor.
301 0x14 Server IP&port CCCCs 4*Byte(1)+Word(2) R/W / CTIOT:117.60.157.137,5683 MQTT:0.0.0.0,0 The server address should be IP, not the domain name; using the domain name may cause the connection server to be unstable and cause data loss. After the function is set, content of 0x34 will be cleared, if 0x14 and 0x34 is set at the same time, priority is given to 0x14 (passive upload)
302 0x17 Signal strength C Byte(1) R 10~34 / A larger value indicates a stronger signal
303 0x18 Sensor operating mode C Byte(1) R/W 0 0 0: absolute angle, 1: relative angle, 2: vibration, ZCT330Mx sensor can only work in absolute measurement mode, ZCT330Ex supports all modes
304 0x19 Alarm axis C Byte(1) R 0~3 / 0: no alarm; 1: X axis alarm 2: Y axis alarm; 3: X/Y axis alarm at the same time
305 0x1A SIM card ID Q QWord(8) R 0~18446744073709551615 / Take the first 19 digits, the last digit is discarded
306 0x1B Alarm angle f Float(4) R/W 0.06-30.00 5.0 ZCT330E relative mode, passive upload
307 0x1C Alarm trigger time L Dword(4) R/W 2-20 2 ZCT330E relative mode, passive upload
308 0x1D Static angle f Float(4) R/W 0.06-30.00 2.5 ZCT330E relative mode, passive upload
309 0x1E Static trigger time L Dword(4) R/W 2-20 2 ZCT330E relative mode, passive upload
310 0x1F Acquisition interval L Dword(4) R/W 1-2160000 60 ZCT330E absolute mode, passive upload
311 0x20 Failure retransmission L Dword(4) R/W 20-2160000 3600 ZCT330E absolute mode, passive upload
312 0x21 Heartbeat interval L DWord(4) R/W 60~131071 86400 Interval at which the device periodically uploads data to the server
313 0x22 IMEI number of the device Q QWord(8) R 0~18446744073709551615 / Refers to the IMEI of the NB-IOT network module in the product.
314 0x23 Backup server IP&port CCCCs 4*Byte(1)+Word(2) R/W / CTIOT:117.60.157.137,5683 MQTT:0.0.0.0,0 / if set, clears 0x3b, if 0x23 and 0x3b are set at same time, 0x23 has priority
315 0x24 Backup server enable C Byte(1) R/W 0~255 0 0 means off, non-zero means on
316 0x33 DNS IP address CCCC 4*Byte(1) R/W / 208.67.222.222 /
317 0x34 Domain name and port a* 64*Byte(1) R/W / mqtt.zc-sensor.com,1883 Supports CTIOT and MQTT protocols. Priority IP in the case of IP (ID number 0x14); domain name and port should be separated with comma, length <=64, cleared by setting 0x14
318 0x35 MQTT-ClientID a* 32*Byte(1) R/W / IMEI number of the device Length <=32, subject to MQTT related specifications
319 0x36 MQTT-Username a* 32*Byte(1) R/W / empty Length <=32, subject to MQTT related specifications
321 0x37 MQTT-Password a* 32*Byte(1) R/W / empty Length <=32, subject to MQTT related specifications
322 0x38 MQTT-published topic name a* 128*Byte(1) R/W / Inclinometer/ZCT330Mx_SWP_N_YK/IMEI/up Length <=128, subject to MQTT related specifications
323 0x39 MQTT-subscribed topic name a* 128*Byte(1) R/W / Inclinometer/ZCT330Mx_SWP_N_YK/IMEI/down Length <=128, subject to MQTT related specifications
324 0x3A Set relative zero command C Byte(1) R/W 0~255 0 0: absolute angle mode 1: Set the current position to zero, relative angle mode (0x09, 0x0A content will be set to the current angle), Other values are invalid.
325 0x3B Backup server domain name and port a* 64*Byte(1) R/W / mqtt.zc-sensor.com,1883 Supports CTIOT and MQTT protocols. In the case of backup IP, the IP is preferentially backed up; the domain name and port are distinguished by commas, and the length is <=64. , will be cleared if 0x23 is set
326 0x3D Protocol type C Byte(1) R/W 0~1 CTIOT 0:CTIOT 1:MQTT Other values are invalid.
327 0x3E Alarm angle f Float(4) R/W 0.06-30.00 2.0 ZCT330E vibration mode, passive upload
328 0x3F Alarm trigger time L Dword(4) R/W 80-20000 240 ZCT330E vibration mode, passive upload (trigger time in ms)
329 0x40 Static angle f Float(4) R/W 0.06-30.00 1.0 ZCT330E vibration mode, passive upload
330 0x41 Static trigger time L Dword(4) R/W 80-20000 240 ZCT330E vibration mode, passive upload (trigger time in ms)
331 0x42 Heartbeat interval L DWord(4) R/W 60~2160000 86400 ZCT330E (unit: s) vibration mode, Interval at which the device periodically uploads data to the server
332 0x43 Failure retransmission L Dword(4) R/W 20-2160000 3600 ZCT330E (unit: s) vibration mode, passive upload
333 0x44 Alarm angle f Float(4) R/W ZCT330M:‐90°~90°,ZCT330E:0.06-30.00 ZCT330M:3°,ZCT330E:5.0 X/Y axis alarm angle is consistent, ZCT330E in absolue mode, passsive upload
334 0x45 Alarm trigger time L Dword(4) R/W 0-3600 0 ZCT330E Sensor waiting time for the server to issue a command (s), passive upload