File Coverage

blib/lib/ControlX10/CM11.pm
Criterion Covered Total %
statement 147 184 79.8
branch 71 120 59.1
condition 20 33 60.6
subroutine 11 11 100.0
pod 4 9 44.4
total 253 357 70.8


line stmt bran cond sub pod time code
1             package ControlX10::CM11;
2             #-----------------------------------------------------------------------------
3             #
4             # An X10 ActiveHome interface, used by Misterhouse ( http://misterhouse.net )
5             #
6             # Uses the Windows or Posix SerialPort.pm functions by Bill Birthisel,
7             # available on CPAN
8             #
9             #-----------------------------------------------------------------------------
10 1     1   4079 use strict;
  1         2  
  1         42  
11 1     1   5 use vars qw($VERSION $DEBUG @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         2  
  1         6087  
12              
13             require Exporter;
14              
15             @ISA = qw(Exporter);
16             @EXPORT= qw( send_cm11 receive_cm11 read_cm11 dim_decode_cm11 );
17             @EXPORT_OK= qw();
18             %EXPORT_TAGS = (FUNC => [qw( send_cm11 receive_cm11
19             read_cm11 dim_decode_cm11 )]);
20              
21             Exporter::export_ok_tags('FUNC');
22              
23             $EXPORT_TAGS{ALL} = \@EXPORT_OK;
24              
25             #### Package variable declarations ####
26              
27             ($VERSION) = q$Revision: 2.09 $ =~ /: (\S+)/; # Note: cvs version reset when we moved to sourceforge
28             $DEBUG = 0;
29             my $Last_Dcode;
30              
31             sub send_cm11 {
32 78 50   78 0 221 return unless ( 2 == @_ );
33 78         145 return ControlX10::CM11::send ( @_ );
34             }
35              
36             sub receive_cm11 {
37 1 50   1 0 6 return unless ( 1 == @_ );
38 1         4 return ControlX10::CM11::receive_buffer ( shift );
39             }
40              
41             sub read_cm11 {
42 1 50   1 0 72 return unless ( 2 == @_ );
43 1         6 return ControlX10::CM11::read ( @_ );
44             }
45              
46             sub dim_decode_cm11 {
47 11 50   11 0 26 return unless ( 1 == @_ );
48 11         90 return ControlX10::CM11::dim_level_decode ( shift );
49             }
50              
51             # These tables are used in sending data
52             my %table_hcodes = qw(A 0110 B 1110 C 0010 D 1010 E 0001 F 1001 G 0101 H 1101
53             I 0111 J 1111 K 0011 L 1011 M 0000 N 1000 O 0100 P 1100);
54             my %table_dcodes = qw(1 0110 2 1110 3 0010 4 1010 5 0001 6 1001 7 0101 8 1101
55             9 0111 10 1111 11 0011 12 1011 13 0000 14 1000 15 0100 16 1100
56             A 1111 B 0011 C 1011 D 0000 E 1000 F 0100 G 1100);
57             my %table_fcodes = qw(J 0010 K 0011 M 0100 L 0101 O 0001 P 0000
58             ALL_OFF 0000 ALL_ON 0001 ON 0010 OFF 0011 DIM 0100 BRIGHT 0101
59             -10 0100 -20 0100 -30 0100 -40 0100
60             -15 0100 -25 0100 -35 0100 -45 0100 -5 0100
61             -50 0100 -60 0100 -70 0100 -80 0100 -90 0100
62             -55 0100 -65 0100 -75 0100 -85 0100 -95 0100
63             +10 0101 +20 0101 +30 0101 +40 0101
64             +15 0101 +25 0101 +35 0101 +45 0101 +5 0101
65             +50 0101 +60 0101 +70 0101 +80 0101 +90 0101
66             +55 0101 +65 0101 +75 0101 +85 0101 +95 0101
67             ALL_LIGHTS_OFF 0110 EXTENDED_CODE 0111 HAIL_REQUEST 1000 HAIL_ACK 1001
68             PRESET_DIM1 1010 PRESET_DIM2 1011 EXTENDED_DATA 1100
69             STATUS_ON 1101 STATUS_OFF 1110 STATUS 1111);
70              
71              
72             # These tables are used in receiving data
73             my %table_hcodes2 = qw(0110 A 1110 B 0010 C 1010 D 0001 E 1001 F 0101 G 1101 H
74             0111 I 1111 J 0011 K 1011 L 0000 M 1000 N 0100 O 1100 P);
75             my %table_dcodes2 = qw(0110 1 1110 2 0010 3 1010 4 0001 5 1001 6 0101 7 1101 8
76             0111 9 1111 A 0011 B 1011 C 0000 D 1000 E 0100 F 1100 G);
77             my %table_fcodes2 = qw(0010 J 0011 K 0100 L 0101 M 0001 O 0000 P
78             0111 Z 1010 PRESET_DIM1 1011 PRESET_DIM2
79             1101 STATUS_ON 1110 STATUS_OFF 1111 STATUS);
80              
81              
82             sub receive_buffer {
83 1     1 1 1 my ($serial_port) = @_;
84              
85 1 50       5 if (exists $main::config_parms{debug}) {
86 1 50       6 $DEBUG = ($main::config_parms{debug} eq 'X10') ? 1 : 0;
87             }
88              
89 1         3 my $pc_ready = pack('C', 0xc3);
90 1 50       4 print "Bad cm11 pc_ready transmition\n" unless 1 == $serial_port->write($pc_ready);
91              
92             # Lets not wait for data (use no_block option), or we loop too long and mh slows way down
93              
94             # let the 0xc3 ack take hold ... emperically derived ... 1/2 misses at 20 ms
95 1         40234 select undef, undef, undef, 40 / 1000;
96              
97              
98 1         8 my $data;
99 1 50       7 return undef unless $data = &read($serial_port, 1);
100             # my $data = &read($serial_port);
101              
102 1         47 my @bytes = split //, $data;
103              
104 1         62 my $length = shift @bytes;
105 1         3 my $mask = shift @bytes;
106              
107 1         4 $length = unpack('C', $length);
108 1         3 $mask = unpack('B8', $mask);
109 1         5 my $data_h = unpack('H*', $data);
110 1 50       4 print "receive buffer length=$length, mask=$mask, data_h=$data_h.\n" if $DEBUG;
111              
112 1         2 my ($house, $function, $device, $i, $extended_count);
113              
114 1         2 undef $data;
115 1         3 foreach my $byte (@bytes) {
116             # Send extended data into MH as untranslated hex.
117 4 50       10 if ($extended_count) {
118 0         0 $data .= unpack('H*', $byte);
119 0         0 --$extended_count;
120 0         0 ++$i;
121             }
122             else {
123 4         9 my $bits = unpack('B8', $byte);
124 4         7 my $house_bits = substr($bits, 0, 4);
125 4         8 my $code_bits = substr($bits, 4, 4);
126 4 50       11 print "CM11 error, not a valid house code: $house_bits\n" unless $house = $table_hcodes2{$house_bits};
127 4 100       14 if (substr($mask, -(++$i), 1)) {
128 1 50       14 print "CM11 error, not a valid function code: $code_bits at byte $i value $bits\n" unless $function = $table_fcodes2{$code_bits};
129             # print "function=$house$function\n";
130              
131             # Add device code back in, since this is not included in status :(
132 1 50       4 $function = $Last_Dcode . $function if $function =~ /^STATUS/;
133             # Handle Vehicle Interface RF Receiver extended code - assume length of 3 for extended
134 1 50       5 $extended_count = 3 if ($function eq 'Z');
135             ## 2.08, but 'Z' not numeric ##
136             ## $extended_count = 3 if ($function == 'Z');
137              
138 1         2 $data .= $house . $function;
139 1 50       4 print "CM11 db: data=$data\n" if $DEBUG;
140             }
141             else {
142 3 50       12 print "CM11 error, not a valid device code: $code_bits\n" unless $device = $table_dcodes2{$code_bits};
143             # print "device=$house$device\n";
144 3         9 $data .= $house . $device;
145             }
146             }
147             # print "byte=$byte, $bits\n";
148             }
149 1         10 return $data;
150             }
151              
152             sub format_data {
153 80     80 0 94 my ($house_code) = @_;
154              
155 80 100       147 print "CM11 send data=$house_code\n" if $DEBUG;
156              
157 80         76 my ($house, $code, $house_bits, $header, $code_bits, $function, $dim_level);
158 0         0 my ($extended, $extended_string, $extended_checksum);
159              
160 80         398 ($house, $code) = $house_code =~ /(\S)(\S+)/;
161 80         133 $house = uc($house);
162 80         93 $code = uc($code);
163              
164 80 100       179 unless ($house_bits = $table_hcodes{$house}) {
165 1         5 print "CM11 error, invalid house code: $house\n";
166 1         3 return;
167             }
168              
169             # $code can be
170             # 1-9,A-G for Device code
171             # d_xyz. for Extended code xyz for device d
172             # xyz for Function codes, including +-## for bright/dim
173              
174             # Test for extended code
175             # - format is D&P## where D is device code and ## is the preset dim level
176 79 100       437 if (my($dcode, $extended_data) = $code =~ /(\S)&P(\d+)/) {
    100          
    100          
177 3 50       9 unless ($code_bits = $table_dcodes{$dcode}) {
178 0         0 print "CM11 error, invalid device in extended code. device=$dcode code=$code\n";
179 0         0 return;
180             }
181 3 100 66     20 unless (($extended_data >= 0) && ($extended_data < 65)) {
182 1         6 print "CM11 error, invalid extended code. code=$code\n";
183 1         2 return;
184             }
185 2         4 $code_bits = '0111'; # Extended code
186             # print "db cb=$code_bits\n";
187 2         3 $function = '1'; # Extended transmitions are a function
188 2         2 $extended = '1';
189 2         4 $dim_level = 0; # Dim level is not applicable to extended transmitions.
190              
191             # Hard codeded preset for now ...
192              
193 2         3 my $extended_junk = '00000101'; # This is not documented, so I have no idea why it is needed!
194             # Emperically derived by looking at ActiveHome errata.
195              
196 2         2 my $extended_code = '00110001'; # Type=3 => Control Modules Func=1 => Preset Receiver
197              
198             # Convert from bit to string
199 2         5 my $b3 = pack('B8', $extended_junk);
200 2         5 my $b4 = pack('C1', $extended_data);
201 2         4 my $b5 = pack('B8', $extended_code);
202 2         4 $extended_string = $b3 . $b4 . $b5;
203 2         4 my $b3c = unpack('C', $b3);
204 2         9 my $b4c = unpack('C', $b4);
205 2         3 my $b5c = unpack('C', $b5);
206 2         3 $extended_checksum = $b3c + $b4c + $b5c;
207 2 50       7 if ($DEBUG) {
208 0         0 printf "CM11 ed=%d, b345=0x%0.2x,0x%0.2x,0x%0.2x ex=%s cs=0x%0.2x\n",
209             $extended_data, $b3c, $b4c, $b5c, $extended_string,
210             $extended_checksum;
211             }
212             }
213             # Test for device code
214             elsif ($code_bits = $table_dcodes{$code}) {
215 18         19 $function = '0';
216 18         20 $extended = '0';
217 18         15 $dim_level = 0;
218 18         21 $Last_Dcode = $code; # This is desperate :)
219             }
220             # Test for function code
221             elsif ($code_bits = $table_fcodes{$code}) {
222 48         62 $function = '1';
223 48         55 $extended = '0';
224 48 100 100     471 if ($code eq 'DIM' or $code eq 'M' or $code eq 'BRIGHT' or $code eq 'L') {
    100 100        
      100        
225 5         8 $dim_level = 50;
226             }
227             elsif ($code =~ /^[+-]\d\d$/) {
228 23         45 $dim_level = abs($code);
229             }
230             else {
231 20         25 $dim_level = 0;
232             }
233             }
234             else {
235 10         53 print "CM11 error, invalid cm11 x10 code: $code\n";
236 10         22 return;
237             }
238              
239 68         130 my $dim = int($dim_level * 22 / 100); # 22 levels = 100%
240 68         243 $header = substr(unpack('B8', pack('C', $dim)), 3);
241              
242 68         98 $header .= '1'; # Bit 2 is always set to a 1 to ensure synchronization
243 68         87 $header .= $function; # 0 for address, 1 for function
244 68         64 $header .= $extended; # 0 for standard, 1 for extended transmition
245            
246             # Convert from bit to string
247 68         114 my $b1 = pack('B8', $header);
248 68         159 my $b2 = pack('B8', $house_bits . $code_bits);
249              
250             # Calculate checksum
251 68         114 my $b1d = unpack('C', $b1);
252 68         98 my $b2d = unpack('C', $b2);
253 68         104 my $checksum = ($b1d + $b2d) & 0xff;
254              
255 68         81 my $data = $b1 . $b2;
256              
257 68 100       278 if ($extended) {
258 2         3 $data .= $extended_string;
259 2         4 $checksum = ($checksum + $extended_checksum) & 0xff;
260             }
261              
262 68 100       125 printf("CM11 dim=$dim header=$header hb=$house_bits cb=$code_bits " .
263             "bd=0x%0.2x,0x%0.2x checksum=0x%0.2x\n",
264             $b1d, $b2d, $checksum) if $DEBUG;
265              
266 68         222 return $data, $checksum;
267            
268             }
269              
270             sub send {
271 80     80 1 122 my ($serial_port, $house_code) = @_;
272              
273 80 50       178 if (exists $main::config_parms{debug}) {
274 80 100       371 $DEBUG = ($main::config_parms{debug} eq 'X10') ? 1 : 0;
275             # &Win32::SerialPort::debug(1) if $DEBUG;
276             }
277              
278 80         125 my ($data_snd, $checksum) = &format_data($house_code);
279 80 100       189 return unless $data_snd;
280              
281 68         70 my $retry_cnt = 0;
282 68 100       126 RETRY:
283             print "CM11 send: ", unpack('H*', $data_snd), "\n" if $DEBUG;
284              
285 68 50       222 print "Bad cm11 data send transmition\n" unless length($data_snd) == $serial_port->write($data_snd);
286              
287             # Note: Skip the power fail check, because we the
288             # checksum might be the power fail flag (0xa5)
289 68         1547 my $data_rcv = &read($serial_port, 0, 1);
290              
291 68         114 my $data_d = unpack('C', $data_rcv);
292              
293             # Unrelated incoming data ... process and re-start
294             # Note: Some checksums will be 0x5a or 0xa5 ... skip this test if so
295 68 50 66     310 if (($data_d == 0x5a or $data_d == 0xa5) and !($checksum == 0x5a or $checksum == 0xa5)) {
      33        
      66        
296 0         0 print "Data received while xmiting data ... will receive and retry\n";
297 0         0 &receive_buffer($serial_port);
298 0 0       0 goto RETRY if $retry_cnt++ < 3;
299             }
300              
301 68 50       120 if ($checksum != $data_d) {
302 0         0 print "Bad checksum in cm11 send: cs1=$checksum cs2=$data_d. Will retry\n";
303 0 0       0 goto RETRY if $retry_cnt++ < 3;
304             }
305              
306 68 100       132 print "CM11 ack\n" if $DEBUG;
307 68         83 my $pc_ok = pack('C', 0x00);
308 68 50       150 print "Bad cm11 acknowledge send transmition\n" unless 1 == $serial_port->write($pc_ok);
309              
310 68         1086 $data_rcv = &read($serial_port);
311 68         132 $data_d = unpack('C', $data_rcv);
312              
313 68 50 0     115 if ($data_d == 0x55) {
    0          
314 68 100       119 print "CM11 done\n" if $DEBUG;
315             }
316             # Unrelated incoming data ... process
317             elsif ($data_d == 0x5a or $data_d == 0xa5) {
318 0         0 print "Data received while xmiting data ... receive and retry\n";
319 0         0 &receive_buffer($serial_port);
320 0 0       0 goto RETRY if $retry_cnt++ < 3;
321             }
322              
323 68 50       167 if (exists $main::config_parms{debug}) {
324             # &Win32::SerialPort::debug(0) if $DEBUG;
325             }
326              
327 68         319 return $data_d;
328             }
329              
330             sub read {
331 138     138 1 210 my ($serial_port, $no_block, $no_power_fail_check) = @_;
332 138         142 my $data;
333             # Note ... for dim commands > 20, this will time out after 20*40=1 seconds
334             # No harm done, but we would rather not wait :)
335 138 100       228 my $tries = ($no_block) ? 1 : 20;
336              
337 138 50       298 if (exists $main::config_parms{debug}) {
338 138 100       346 $DEBUG = ($main::config_parms{debug} eq 'X10') ? 1 : 0;
339             }
340              
341 138         288 while ($tries--) {
342 138 100 66     329 print "." if $DEBUG and !$no_block;
343 138 50       413 if ($data = $serial_port->input) {
344 138         2142 my $data_d = unpack('C', $data);
345             # printf("rcv data=%s, %x.\n", $data, $data_d);
346             # my $pc_ready = pack('C', 0xc3);
347             # $serial_data = "$pc_ready";
348             # print "serial1 out=$serial_data results=", $serial_port->write($serial_data), ".\n" if $DEBUG;
349 138 100       280 printf("\nCM11 data=%s hex=%0.2lx\n", $data_d, $data_d) if $DEBUG;
350              
351             # If we received the power-fail string (0xa5), reset with a blank macro command
352             # - Protocol.txt says to send macros string, but that did not work.
353 138 50 66     342 if ($data_d == 165 and !$no_power_fail_check) {
354              
355             #55 to 48 timer download header (0x9b)
356             #47 to 40 Current time (seconds)
357             #39 to 32 Current time (minutes ranging from 0 to 119)
358             #31 to 23 Current time (hours/2, ranging from 0 to 11)
359             #23 to 16 Current year day (bits 0 to 7)
360             #15 Current year day (bit 8)
361             #14 to 8 Day mask (SMTWTFS)
362             #7 to 4 Monitored house code
363             #3 Reserved
364             #2 Battery timer clear flag
365             #1 Monitored status clear flag
366             #0 Timer purge flag
367              
368 0         0 my ($Second, $Minute, $Hour, $Mday, $Month, $Year, $Wday, $Yday) = localtime time;
369 0         0 my $localtime = localtime time;
370 0         0 $Wday = 2 ** (7 - $Wday);
371 0 0       0 if ($Yday > 255) {
372 0         0 $Yday -= 256;
373 0         0 $Wday *= 2;
374             }
375 0         0 my $power_reset = pack('C7', 0x9b,
376             $Second,
377             $Minute,
378             $Hour,
379             $Yday,
380             $Wday,
381             0x03); # Not sure what is best here. x10d.c did this.
382            
383 0         0 print "\nCM11 power fail detected. Resetting the CM11 clock with:\n $localtime\n";
384 0         0 my $results = $serial_port->write($power_reset);
385 0         0 select undef, undef, undef, 50 / 1000;
386 0         0 my $checksum = $serial_port->input; # Receive, but ignore, checksum
387 0 0       0 if ($DEBUG) {
388 0         0 printf "\npower_reset: %s %s %s %s %s %s %s\n",
389             unpack ('H2H2H2H2H2H2H2', $power_reset);
390 0         0 print " sent $results bytes\n";
391 0         0 printf " checksum = %x\n", ord($checksum);
392             }
393 0         0 my $pc_ok = pack('C', 0x00);
394 0 0       0 print "Bad cm11 checksum acknowledge\n" unless 1 == $serial_port->write($pc_ok);
395             # undef $data;
396             }
397              
398 138         334 return $data;
399             }
400             # If we do not do this, we may get endless error messages.
401             else {
402 0         0 $serial_port->reset_error;
403             }
404            
405 0 0       0 if ($tries) {
406 0         0 select undef, undef, undef, 50 / 1000;
407             }
408             }
409              
410 0 0 0     0 print "No data received from cm11\n" if ($DEBUG and !$no_block);
411 0         0 return undef;
412             }
413              
414             sub dim_level_decode {
415 11     11 1 16 my ($code) = @_;
416              
417 11         148 my %table_hcodes = qw(A 0110 B 1110 C 0010 D 1010 E 0001 F 1001 G 0101 H 1101
418             I 0111 J 1111 K 0011 L 1011 M 0000 N 1000 O 0100 P 1100);
419 11         89 my %table_dcodes = qw(1 0110 2 1110 3 0010 4 1010 5 0001 6 1001 7 0101 8 1101
420             9 0111 10 1111 11 0011 12 1011 13 0000 14 1000 15 0100 16 1100
421             A 1111 B 0011 C 1011 D 0000 E 1000 F 0100 G 1100);
422              
423 11 50       27 if (exists $main::config_parms{debug}) {
424 11 50       25 $DEBUG = ($main::config_parms{debug} eq 'X10') ? 1 : 0;
425             }
426              
427             # Convert bit string to decimal
428 11         27 my $level_b = $table_hcodes{substr($code, 0, 1)} . $table_dcodes{substr($code, 1, 1)};
429 11         36 my $level_d = unpack('C', pack('B8', $level_b));
430             # Varies from 36 to 201, by 11, then to 210 as a max.
431             # 16 different values. Round to nearest 5%, max of 95.
432 11         38 my $level_p = int(100 * $level_d / 211); # Do not allow 100% ... not a valid state?
433             ## print "CM11 debug1: levelb=$level_b level_p=$level_p\n" if $DEBUG;
434 11         19 $level_p = $level_p - ($level_p % 5);
435 11 50       21 print "CM11 debug: dim_code=$code leveld=$level_d level_p=$level_p\n" if $DEBUG;
436 11         91 return $level_p;
437             }
438              
439             return 1; # for require
440             __END__