5 use Data::Dump qw(dump);
7 # Table of CRC values for high order byte
8 use constant CRC_HI => [
9 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
10 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
11 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1,
12 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
13 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
14 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
15 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1,
16 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
17 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
18 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
19 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
20 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
21 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
22 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
23 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
24 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
25 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
26 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
27 0x00, 0xC1, 0x81, 0x40
30 # Table of CRC values for low order byte
31 use constant CRC_LO => [
32 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5,
33 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B,
34 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE,
35 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6,
36 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
37 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D,
38 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8,
39 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C,
40 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21,
41 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
42 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A,
43 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA,
44 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7,
45 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51,
46 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
47 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98,
48 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D,
49 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83,
50 0x41, 0x81, 0x80, 0x40
58 my @bytes = split(//, $msg);
60 my $data = unpack('C*', $_);
61 my $crcIdx = $crcl ^ $data;
62 $crcl = $crch ^ &CRC_HI->[$crcIdx];
63 $crch = &CRC_LO->[$crcIdx];
66 # Pack the trailer - CRC has low byte first
67 my $trlr = pack('CC', $crcl, $crch);
76 my @a = split(/\s*\t\s*/, $_,7);
77 my $id = hex( $a[0] );
78 my $pack_fmt = $a[2]; $pack_fmt =~ s/\s+.*$//;
81 pack_fmt => $pack_fmt,
90 warn "## protocol = ",dump( $protocol );
95 #Publish/at-most-once,retain Inclinometer/ZCT330Ex_SWP_N_YK/869858035167585/up
97 5a 0c 08 29 00 01 04 55 79 fb 71 02 01 10 03 04 Z..)...Uy.q.....
98 00 00 df c1 04 04 75 93 6c c1 0c 02 49 0a 0d 02 ......u.l...I...
99 61 01 11 01 01 17 01 12 18 01 01 19 01 03 48 5d a.............H]
102 #Publish/at-most-once,retain Inclinometer/ZCT330Ex_SWP_N_YK/869858031634109/up
104 5a 0c 07 29 00 01 04 00 00 00 00 02 01 10 03 04 Z..)............
105 df 4f ad 3f 04 04 d1 22 4b c0 0c 02 c2 14 0d 02 .O.?..."K.......
106 41 01 11 01 01 17 01 16 18 01 01 19 01 00 a8 b9 A...............
108 #Publish/at-most-once,retain Inclinometer/ZCT330Ex_SWP_N_YK/864823041482779/up
110 5a 0c 08 29 00 01 04 00 00 00 00 02 01 10 03 04 Z..)............
111 db f9 3e 3f 04 04 b2 9d 6f be 0c 02 b7 10 0d 02 ..>?....o.......
112 48 01 11 01 01 17 01 1a 18 01 01 19 01 03 7d 2c H.............},
114 #Publish/at-most-once,retain Inclinometer/ZCT330Ex_SWP_N_YK/864823041482498/up
116 5a 0c 08 29 00 01 04 e3 b1 a0 77 02 01 10 03 04 Z..)......w.....
117 02 2b 87 be 04 04 27 31 e8 c1 0c 02 bd 0b 0d 02 .+....'1........
118 45 01 11 01 01 17 01 16 18 01 01 19 01 02 2a 69 E.............*i
121 my $raw_nr = shift @ARGV || 0;
122 my $raw = $raw[$raw_nr];
123 $raw =~ s/ \S{16}\n//gs;
127 warn "up/down: $up_down raw = ",dump($raw);
129 my $bin = join('', map { chr(hex($_)) } split(/\s+/,$raw));
131 warn "bin = ", dump($bin);
133 my $cksum = substr($bin, -2, 2);
135 if ( my $crc = modbus_crc16( substr($bin,0,-2) ) ) {
136 if ( $crc ne $cksum ) {
137 warn "CRC error, got ",unpack('H*',$crc), " expected ", unpack('H*',$cksum), "\n";
139 warn "CRC OK ", unpack('H*',$crc), "\n";
143 my ( $header, $ver, $function_code, $len ) = unpack("CCCv", $bin);
145 my $data = substr($bin,5,-2);
147 warn "header = $header ver = $ver function_code = $function_code len = $len == ",length($data), " data = ",dump($data), " cksum = ", unpack('H*',$cksum);
149 warn "header corrupt: $header" if $header != 0x5a;
150 warn "invalid length $len got ",length($data) if length($data) != $len;
152 my $function_code_upstream = {
153 0x07 => 'Upstream Heartbeat Frame',
154 0x08 => 'Upstream alarm frame',
155 0x03 => 'Upstream Read Parameter Frame',
156 0x06 => 'Upstream return write parameter frame',
159 my $function_code_downstream = {
160 0x03 => 'Downstream read parameter frame',
161 0x06 => 'Downstream write parameter frame',
164 print "Function code: $function_code ",
165 $up_down eq 'up' ? $function_code_upstream->{ $function_code } : $function_code_downstream->{ $function_code }, "\n";
171 my $hex = unpack('H*', $data);
172 $hex =~ s/(..)/$1 /g;
173 warn "XXX data = $hex\n";
175 my $data_id = unpack( 'C', substr($data,0,1) );
176 my $data_id_desc = $protocol->{$data_id}->{description} || die "can't find description for data_id $data_id";
177 my $pack_fmt = $protocol->{$data_id}->{pack_fmt} || die "can't find pack_fmt for data_id $data_id";
181 if ( $data_id == 0x00 ) {
182 warn "FIXME seq number?";
184 } elsif ( $up_down eq 'down' && $function_code == 0x03 ) {
187 $data_range = substr($data,0,1);
188 $data = substr($data,1);
190 } elsif ( $up_down eq 'up' && ( $function_code == 0x07 || $function_code == 0x08 || $function_code == 0x03 ) ) {
193 my $data_len = unpack('C', substr($data,1,1));
194 $data_range = substr($data,2, $data_len);
196 $data = substr($data,2 + $data_len);
198 } elsif ( $up_down eq 'down' && $function_code == 0x06 ) {
201 my $data_len = unpack('C', substr($data,1,1));
202 $data_range = substr($data,2, $data_len);
204 $data = substr($data,2 + $data_len);
206 } elsif ( $up_down eq 'up' && $function_code == 0x06 ) {
209 # FIXME check first byte?
210 my $data_range = substr($data,1,1);
212 $data = substr($data,2);
215 warn "ERROR unknown function_code = $function_code";
218 my $v = unpack($pack_fmt, $data_range);
220 if ( $data_id == 0x0c ) {
222 } elsif ( $data_id == 0x0d ) {
226 my $description = $protocol->{$data_id}->{description} || die "can't find description for data_id $data_id";
227 print "$up_down | $data_id | $description | hex:", unpack('H*', $data_range), " fmt:$pack_fmt [v=$v] range:",$protocol->{$data_id}->{range}, " remark:", $protocol->{$data_id}->{remark},"\n";
231 # Valid data ID and parameter range supported by the product
232 # DATA ID ID Description Data Type( Data Length) R/W Range Default Remark
235 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.
236 0x01 PN l DWord(4) R / / /
237 0x02 Model C Byte(1) R 32 32 Inner number:32
238 0x03 X axis angle f Float(4) R ‐90°~90° / X axis angle
239 0x04 Y axis angle f Float(4) R ‐90°~90° / Y axis angle
240 0x09 X axis relative angle f Float(4) R ‐90°~90° 0 Return the X angle value according to the set relative zero
241 0x0A Y axis relative angle f Float(4) R ‐90°~90° 0 Return the Y angle value according to the set relative zero
242 0x0C Sensor temperature s Word(2) R ‐32768~32767 / signed,sensor temperature =Data/100, unit ℃
243 0x0D Power source voltage S Word(2) R 0~65535 / Voltage= Data/100, unit V
244 0x11 Arming/disarming C Byte(1) R/W 0~255 1 0 means disarming, non‐zero means arming
245 0x12 Alarm delay time C Byte(1) R/W 3~255 20 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.
247 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.
248 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.
249 0x17 Signal strength C Byte(1) R 10~34 / A larger value indicates a stronger signal
250 0x18 Sensor operating mode C Byte(1) R/W 0 0 The sensor can only work in absolute measurement mode
251 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
252 0x1A SIM card ID Q QWord(8) R 0~18446744073709551615 / Take the first 19 digits, the last digit is discarded
253 0x21 Heartbeat interval l DWord(4) R/W 60~131071 86400 Interval at which the device periodically uploads data to the server
254 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.
255 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 /
256 0x24 Backup server enable C Byte(1) R/W 0~255 0 0 means off, non‐zero means on
257 0x33 DNS IP address CCCC 4*Byte(1) R/W / 208.67.222.222 /
258 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
259 0x35 MQTT‐ClientID a* 32*Byte(1) R/W / IMEI number of the device Length <=32, subject to MQTT related specifications
260 0x36 MQTT‐Username a* 32*Byte(1) R/W / empty Length <=32, subject to MQTT related specifications
262 0x37 MQTT‐Password a* 32*Byte(1) R/W / empty Length <=32, subject to MQTT related specifications
263 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
264 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
265 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.
266 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.
267 0x3D Protocol type C Byte(1) R/W 0~255 CTIOT 0:CTIOT 1:MQTT Other values are invalid.
268 0x44 Alarm angle f Float(4) R/W ‐90°~90° 3° X/Y axis alarm angle is consistent