support sent files, less verbose debug output
[zc] / Protocol.pm
1 package Protocol;
2 use warnings;
3 use strict;
4
5 require Exporter;
6
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 );
9
10 use Data::Dump qw(dump);
11
12 my $debug = $ENV{DEBUG} || 0;
13
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
35 ];
36  
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
58 ];
59
60 sub modbus_crc16 {
61         my ($msg) = @_;
62
63         my $crcl  = 0xFF;
64         my $crch  = 0xFF;
65         my @bytes = split(//, $msg);
66         for (@bytes) {
67                 my $data = unpack('C*', $_);
68                 my $crcIdx = $crcl ^ $data;
69                 $crcl = $crch ^ &CRC_HI->[$crcIdx];
70                 $crch = &CRC_LO->[$crcIdx];
71         }
72  
73         # Pack the trailer - CRC has low byte first
74         my $trlr = pack('CC', $crcl, $crch);
75
76         return $trlr;
77 }
78
79 our $protocol;
80 while(<DATA>) {
81         chomp;
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+.*$//;
86         $protocol->{$id} = {
87                 description => $a[1],
88                 pack_fmt => $pack_fmt,
89                 r_w => $a[3],
90                 range => $a[4],
91                 default => $a[5],
92                 remark => $a[6],
93                 line => [ @a ],
94         };
95 }
96
97 sub read_parameter_frame {
98         my $params = join('', @_);
99
100         my $frame
101                 = "\x5A" # header
102                 . "\x0B" # protocol version
103                 . "\x03" # function code
104                 . pack("v", length($params)) # data length
105         ;
106
107         $frame .= $params;
108
109         return $frame . modbus_crc16($frame);
110 }
111
112 sub write_parameter_frame {
113         my $params = join('', @_);
114
115         my $frame
116                 = "\x5A" # header
117                 . "\x0B" # protocol version
118                 . "\x06" # function code
119                 . pack("v", length($params)) # data length
120         ;
121
122         $frame .= $params;
123
124         return $frame . modbus_crc16($frame);
125 }
126
127 our $function_code_description = {
128         up => {
129                 0x07 => 'Upstream Heartbeat Frame',
130                 0x08 => 'Upstream alarm frame',
131                 0x03 => 'Upstream Read Parameter Frame',
132                 0x06 => 'Upstream return write parameter frame',
133         },
134         down => {
135                 0x03 => 'Downstream read parameter frame',
136                 0x06 => 'Downstream write parameter frame',
137         }
138 };
139
140 sub hex_dump {
141         my $bin = shift;
142         my $hex = unpack('H*', $bin);
143         $hex =~ s{(..)}{$1 }g;
144         return $hex;
145 }
146
147 sub protocol_decode {
148         my ( $up_down, $bin ) = @_;
149
150         $SIG{__WARN__} = sub {
151                 return unless $debug;
152                 print STDERR @_;
153         };
154
155         my $hash = {
156                 up_down => $up_down,
157                 bin => $bin,
158         };
159
160         my $cksum = substr($bin, -2, 2);
161
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);
165                 }
166         }
167
168         my ( $header, $ver, $function_code, $len ) = unpack("CCCv", $bin);
169
170         $hash->{function_code} = $function_code;
171         $hash->{ver} = $ver;
172         $hash->{len} = $len;
173
174         my $data = substr($bin,5,-2);
175
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);
177
178         if ( $header != 0x5a ) {
179                 $hash->{error}->{header} = "$header expected 0x5a";
180         }
181
182         my $length_data = length($data);
183         if ( $length_data != $len ) {
184                 $hash->{error}->{length} = "$length_data expected $len";
185         }
186
187
188         while ( $data ) {
189
190                 warn "XXX data = ", hex_dump($data);
191
192                 my $data_id = unpack( 'C', substr($data,0,1) );
193
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);
198                         next;
199                 }
200                 my $pack_fmt = $protocol->{$data_id}->{pack_fmt} || die "can't find pack_fmt for data_id $data_id";
201
202                 my $data_range;
203                 my $data_len;
204
205                 if ( $data_id == 0x00 ) {
206                         # sequence number
207
208                         $data_range = substr($data,2,4);
209                         $data = substr($data,6);
210
211                 } elsif ( $up_down eq 'down' && $function_code == 0x03 ) {
212                         # type A
213                         # 0x03 => 'Downstream read parameter frame',
214
215                         $data_range = substr($data,0,1);
216                         $data = substr($data,1);
217
218                 } elsif ( $up_down eq 'up'   && ( $function_code == 0x07 || $function_code == 0x08 || $function_code == 0x03 ) ) {
219                         # type B
220                         # 0x07 => 'Upstream Heartbeat Frame',
221                         # 0x08 => 'Upstream alarm frame',
222                         # 0x03 => 'Upstream Read Parameter Frame',
223
224                         $data_len = unpack('C', substr($data,1,1));
225                         $data_range = substr($data,2, $data_len);
226
227                         $data = substr($data,2 + $data_len);
228
229                 } elsif ( $up_down eq 'down' && $function_code == 0x06 ) {
230                         # type B
231                         # 0x06 => 'Downstream write parameter frame',
232
233                         $data_len = unpack('C', substr($data,1,1));
234                         $data_range = substr($data,2, $data_len);
235
236                         $data = substr($data,2 + $data_len);
237
238                 } elsif ( $up_down eq 'up' && $function_code == 0x06 ) {
239                         # type C
240                         # 0x06 => 'Upstream return write parameter frame',
241
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
246                                 $pack_fmt = 'C';
247                         }
248
249                         $data = substr($data,2 + $data_len);
250
251                 } else {
252                         $hash->{error}->{function_code_unknown} = $function_code;
253                         print STDERR "ERROR unknown function_code = $function_code\n";
254                         return $hash;
255                 }
256
257                 my @v = unpack($pack_fmt, $data_range);
258
259                 my $v = join(' ', @v);
260
261                 if ( $data_id == 0x0c ) {
262                         $v = $v / 100;
263                 } elsif ( $data_id == 0x0d ) {
264                         $v = $v / 100;
265                 }
266
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;
271
272         }
273
274         return $hash;
275
276 } # protocol_decode
277
278
279 1;
280
281 # 6. Protocol Format 
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 
284
285 __DATA__
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) 
299
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 
320
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
335