File Coverage

blib/lib/Device/Regulator/Plasmatronic.pm
Criterion Covered Total %
statement 24 194 12.3
branch 0 50 0.0
condition 0 15 0.0
subroutine 8 28 28.5
pod 1 12 8.3
total 33 299 11.0


line stmt bran cond sub pod time code
1             package Device::Regulator::Plasmatronic;
2 1     1   5 use strict;
  1         2  
  1         28  
3 1     1   976 use IO::File;
  1         11210  
  1         178  
4 1     1   1219 use IO::Select;
  1         1704  
  1         49  
5 1     1   7 use Carp;
  1         2  
  1         65  
6 1     1   5 use vars qw($AUTOLOAD);
  1         2  
  1         38  
7 1     1   1113 use Time::HiRes qw(usleep);
  1         1957  
  1         5  
8 1     1   1114 use POSIX;
  1         7526  
  1         8  
9 1     1   3184 use Fcntl;
  1         2  
  1         2612  
10              
11             our $VERSION = "0.03";
12              
13             my $TEMP_DELAY = 100; # Microsecond (I think...)
14             my $TEMP_TIMEOUT = 2; # Max time for an entry (seconds)
15              
16             =head1 NAME
17              
18             Plasmatronics - Plasmatronics PL regulator controller
19              
20             =head1 SYNOPSIS
21              
22             use Device::Regulator::Plasmatronic;
23             my $r = Device::Regulator::Plasmatronic;
24             print "Current state of charge = " . $r->pl_dsoc . "\n";
25              
26             =head1 DESCRIPTION
27              
28             This is an interface library via the serial port to a Plasmatronics Regulator.
29             (http://www.plasmatronics.com.au/)
30              
31             =head1 MAJOR LIMITATIONS
32              
33             =head2 Serial Port
34              
35             I have to replace the serial port driver - currently I use the unix only version,
36             but I have written
37              
38             =head2 Hard Coded Multiplier
39              
40             The multiplier used for voltages etc is hard coded (currently 4 = 48 Volt system).
41             This can be read from the system, so I will have to do that as part of the
42             initialisation.
43              
44             =head2 Combined Values
45              
46             Load and other things combine values from multiple locations to allow for larger
47             numbers. I know that I have got this wrong in a number of places. Work to be done
48             to test these for large numbers (eg: > 25 Amps etc).
49              
50             =head1 FUTURE
51              
52             =head2 Fix Limitations
53              
54             As above, look at each limitation and try and fix it up.
55              
56             =head2 CGI Scripts
57              
58             Write a number of example CGI scripts
59              
60             =head2 Graphing
61              
62             Include a graph of the history, or even daily history of the system.
63              
64             =head2 Learning Kit
65              
66             Put together the whole kit of files above so that it can be used in learning
67             environments etc to demonstrate logging, power use etc.
68              
69             =head2 Power Control Link
70              
71             My house has most lights and equipment controlled by the computer, which means
72             combined with current load we get a really good idea how much power is used
73             when things are switched on. This also means we can work out how much power is
74             used by which piece of equipment (over time), and monitor the standard load (eg:
75             what is on all the time like the Fridge).
76              
77             =head1 TOOLS
78              
79             I have documented here the tools that come with this, although they are not part
80             of this library, it is a convenient place to put them.
81              
82             =head2 plbackup / plrestore
83              
84             This allows you to backup all the data currently in the regulator.
85             This is very handy if you want to work on the regulator which involves disconnecting
86             the power. You then loose all the data for the current day. This allows you to
87             keep that information, not even loosing any data (except for the period it is off).
88              
89             =head2 plhistory
90              
91             Display the history.
92              
93             =head2 plload
94              
95             A simple example of some load variables displayed. A good one to look at on how
96             you would write your own code.
97              
98             =head2 pllogger
99              
100             This writes all daily entries to a log file, good for long term logging accross
101             long periods. You could adapt this to log any data in the system at any interval.
102              
103             =head2 plloopback
104              
105             Just test the loopback. You can run this to make sure the unit and code is working.
106             Handy to put in a test script, you could for example trigger an alarm if the
107             systems goes down.
108              
109             =head2 plread / plwrite
110              
111             Read and write to any variable in the system. Your raw access tool.
112              
113             =head2 pltest
114              
115             Another test code - not really necessary but I use it mostly to generally test
116             my changes.
117              
118             =head2 pltime
119              
120             Read and write the time on the system. You can setup a job to set the time
121             correctly from your server on regular basis, or call it after a plrestore.
122              
123             =head1 EXAMPLE CODE
124              
125             =head2 Initialisation
126              
127             my $pl = Plasmatronics->new();
128             if ($pl->pl_loopback) {
129             print "Cool\n";
130             } else {
131             print "Not so cool\n";
132             }
133              
134             =head2 Read
135              
136             # Init above
137             print "Current load = " . $pl->pl_load . "\n";
138              
139             =head2 Write
140              
141             # Change the hour
142             print "New hour = " . $pl->pl_hour(15) . "\n";
143              
144             =head2 Full Example Used for remote display
145              
146             # This example could be used for an app, cgi or remote display
147              
148             use Plasmatronics;
149              
150             my $pl = Plasmatronics->new() || die "Can't connect to PL";
151              
152             my $soc = $pl->pl_dsoc;
153             print $soc . "%\n";
154             my $load = $pl->pl_load;
155             my $charge = $pl->pl_charge;
156             print "OUT $load, IN $charge\n";
157              
158             =head1 DATA FILE
159              
160             The data file (plasmatronics.dat) contains all the clever information.
161              
162             So why a data file and not hard coded. Well theoretically I want to be able
163             to write alternate versions of this software in other languages (eg: Java,
164             Python, or even a windows DLL/OLE). By keeping any of the non language
165             specific intelligence in the data file, this can be shared, it is also
166             a much neater way of doing development.
167              
168             =head2 Parameters
169              
170             - Short Name
171             - Number
172             - Full description
173             - Divide by (number)
174             - Multiply by (number or BM)
175             - Unit
176             - ShiftLeft by (other name or NA)
177             - Write flag
178             - Non NV Backup/Restore (should it be backed up)
179              
180             The combination of these allows us to do most of the intelligent calculations
181             in the data file.
182              
183             =head2 Mapping to methods
184              
185             Each of the short names maps to the equivellent method starting with 'pl_'.
186             The nice part about this is it means you can write code with that name that
187             is used in place of the generic code. This is kind of useful when you want
188             to do more complicated calculations which can not be covered in the data file.
189              
190             =head1 METHODS
191              
192             Here are the methods...
193              
194             =cut
195              
196             # ==============================================================================
197             # Configuration
198             # ==============================================================================
199             # TODO: Check if these change per model.
200             my $commands = {
201             readproc => 20, # Read from processor location
202             readnvram => 72, # Read from NV Ram
203             writeproc => 152, # Write to processor location
204             writenvram => 202, # Write to NV Ram
205             loopback => 187
206             };
207              
208             # ==============================================================================
209             # INITIALISATION
210             # ==============================================================================
211             # XXX: How to work out the device?
212             # - Arguments
213             # - Configuration file
214             # - Default
215             # TODO: Change the default port (via serial port driver) to windows version on
216             # windows, etc.
217             sub new {
218 0     0 0   my ($class, %args) = @_;
219 0   0       my $this = bless {}, ref($class) || $class;
220 0   0       $this->{PORT}{NAME} = $args{port} || "/dev/plasmatronic";
221 0   0       $this->{PORT}{TYPE} = $args{type} || "FILE";
222 0           $this->_port_init;
223 0           $this->_read_dat;
224 0           return $this;
225             }
226              
227             # Read in plasmatronics.dat
228             sub _read_dat {
229 0     0     my ($this) = @_;
230 0           my %h = ();
231             # XXX: How do you get the location of this file?
232             # (temp symlink from etc !!!)
233 0           close IN;
234 0           foreach my $f ('plasmatronic.dat', '/etc/plasmatronic.dat'){
235 0 0         next if (! -f $f);
236 0 0         open (IN, $f) || die "Can't open found file $f";
237             }
238             # XXX: Check it is open?
239 0           while () {
240 0           chomp;
241 0 0         if (! /^#/) {
242 0           my @arr = split(/ *\t+ */, $_);
243             # Convert hex values
244 0 0         if (substr($arr[1], 0,1) eq "h") {
245 0           $arr[1] = hex(substr($arr[1], 1,2));
246             }
247 0           $h{$arr[0]}{number} = $arr[1];
248 0           $h{$arr[0]}{note} = $arr[2];
249 0           $h{$arr[0]}{divider} = $arr[3];
250 0           $h{$arr[0]}{multiplier} = $arr[4];
251 0           $h{$arr[0]}{unit} = $arr[5];
252 0           $h{$arr[0]}{shiftleft} = $arr[6];
253 0           $h{$arr[0]}{write} = $arr[7];
254 0           $h{$arr[0]}{backup} = $arr[8];
255             }
256             }
257 0           close IN;
258 0           $this->{DAT} = \%h;
259             }
260              
261             # Serial port
262             sub _port_init {
263 0     0     my ($this) = @_;
264              
265 0 0         if ($this->{PORT}{TYPE} eq "FILE") {
266             # XXX - Errors here ?
267 0           $this->{PORT}{REF} = new IO::File "+< " . $this->{PORT}{NAME};
268              
269 0           my $DisplayFD = fileno ($this->{PORT}{REF}) ;
270              
271 0           my $DisplayTermios = POSIX::Termios->new () ;
272 0           $DisplayTermios->getattr ($DisplayFD) ;
273              
274 0           $DisplayTermios->setispeed (B9600) ; # serial input speed (19200bps)
275 0           $DisplayTermios->setospeed (B9600) ; # serial output speed (19200bps)
276              
277 0           my $CFlag = $DisplayTermios->getcflag () ;
278 0           my $LFlag = $DisplayTermios->getlflag () ;
279 0           my $OFlag = $DisplayTermios->getoflag () ;
280 0           my $IFlag = $DisplayTermios->getiflag () ;
281              
282 0           $IFlag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON) ; # raw IO
283 0           $OFlag &= ~(OPOST) ;
284 0           $LFlag &= ~(ECHO|ECHONL|ICANON|ISIG) ;
285 0           $CFlag &= ~(CSIZE|PARENB|HUPCL) ;
286 0           $CFlag |= (CREAD|CS8|CLOCAL) ;
287              
288 0           $DisplayTermios->setcflag ($CFlag) ; # update serial settings
289 0           $DisplayTermios->setlflag ($LFlag) ;
290 0           $DisplayTermios->setoflag ($OFlag) ;
291 0           $DisplayTermios->setiflag ($IFlag) ;
292 0           $DisplayTermios->setattr ($DisplayFD, TCSANOW) ; # update serial device
293              
294             } else {
295             # Serial device
296             # XXX: The device driver should know the lock file, why does it
297             # insist on each bit of code calcuating the code !!!
298 0           my $lock = $this->{PORT}{NAME};
299 0           $lock =~ s/\/dev\///;
300 0           $lock = "/var/lock/LCK..$lock";
301             # 1 = quiet
302 0           eval q{use Device::SerialPort;};
303 0 0         die "Failed to load Device::SerialPort - $@" if ($@);
304 0   0       $this->{PORT}{REF} = new Device::SerialPort ($this->{PORT}{NAME}, 0, $lock)
305             || die "Can't open " . $this->{PORT}{REF} . ": $!\n";
306 0           $this->_port->baudrate(9600);
307 0           $this->_port->parity("none");
308 0           $this->_port->databits(8);
309 0           $this->_port->stopbits(1);
310             }
311             # XXX Check this works for Device::SerialPort too.
312 0           $this->{SELECT} = new IO::Select;
313 0           $this->_select->add($this->_port());
314             }
315              
316             sub _port {
317 0     0     my ($this) = @_;
318 0           return $this->{PORT}{REF};
319             }
320              
321             sub _select {
322 0     0     my ($this) = @_;
323 0           return $this->{SELECT};
324             }
325              
326             # ==============================================================================
327             # COMMANDS
328             # ==============================================================================
329             # Match a list
330             sub list {
331 0     0 0   my ($this, $match) = @_;
332 0           my @ret = ();
333 0           MATCH: foreach my $key (sort {$a cmp $b} keys %{$this->{DAT}}) {
  0            
  0            
334 0 0         if (defined($match)) {
335 0           warn "Doing matches";
336 0           foreach my $m (keys %{$match}) {
  0            
337 0           warn "\tMatching on $m";
338 0           warn "\t\tDAT = " . $this->{DAT}{$key}{$m};
339 0           warn "\t\tMATCH = " . $match->{$m};
340 0 0         if ($match->{$m} ne $this->{DAT}{$key}{$m}) {
341 0           warn "\t\tNO MATCH for $m";
342 0           next MATCH;
343             }
344             }
345             }
346 0           warn "ADDING $key";
347 0           push @ret, $key;
348             }
349 0 0         if (wantarray()) {
350 0           return @ret;
351             } else {
352 0           return \@ret;
353             }
354             }
355              
356             sub pl_loopback {
357 0     0 0   my ($this) = @_;
358 0           $this->_write(
359             $commands->{'loopback'},
360             0,
361             0,
362             255 - $commands->{'loopback'}
363             );
364 0           my $buf = $this->_read(1);
365 0           return (ord($buf) == 128);
366             }
367              
368             =head2 data
369              
370             Read or write to a processor or nvram location.
371              
372             Returned values are always adjusted to make your life easier, but that is not
373             so easy for writting, so that has not yet been implemented.
374              
375             =cut
376              
377             sub data {
378 0     0 1   my ($this, $name, $value) = @_;
379 0           $this->initparams;
380 0 0         if (!defined($this->{DAT}{$name})) {
381 0           carp "Invalid data requested - " . $name;
382             }
383              
384             # Write
385 0 0         if (defined($value)) {
386 0 0         if (! $this->{DAT}{$name}{write}) {
387 0           croak "Trying to write to a read only value - " . $name;
388             }
389 0           my $cmd = $commands->{'writeproc'};
390 0 0         if ($name =~ /^nv/) {
391 0           $cmd = $commands->{'writenvram'};
392             }
393             $this->_write(
394 0           $cmd,
395             $this->{DAT}{$name}{number},
396             $value,
397             255 - $cmd
398             );
399             }
400              
401             # Proc or NV read
402 0           my $cmd = $commands->{'readproc'};
403 0 0         if ($name =~ /^nv/) {
404 0           $cmd = $commands->{'readnvram'};
405             }
406              
407             # Send command
408             $this->_write(
409 0           $cmd,
410             $this->{DAT}{$name}{number},
411             0,
412             255 - $cmd
413             );
414              
415             # Get results
416 0 0         my $buf = $this->_read(2)
417             or return undef;
418 0           my $out = ord(substr($buf, 1, 1));
419              
420             # Check value
421 0 0 0       if (defined($value) && ($out != $value)) {
422 0           warn "Received value was not what was written";
423             }
424              
425             # Shift left by?
426             # Not quite right.
427             # 8 bits = * by 10
428             # 32 bits = *
429 0 0         if ($this->{DAT}{$name}{shiftleft} ne "NA") {
430 0           my $sl = $this->data($this->{DAT}{$name}{shiftleft});
431 0           $out = $out << $sl;
432             }
433              
434             # Multiplier and Divider
435 0 0         if ($this->{DAT}{$name}{multiplier} eq "BM") {
436 0           $out = $out * $this->{PARAMS}{MULTIPLIER};
437             } else {
438 0           $out = $out * $this->{DAT}{$name}{multiplier};
439             }
440 0           $out = $out / $this->{DAT}{$name}{divider};
441 0           return $out;
442             }
443              
444             sub exists {
445 0     0 0   my ($this, $name) = @_;
446 0           return exists($this->{DAT}{$name});
447             }
448              
449             sub unit {
450 0     0 0   my ($this, $name) = @_;
451 0           return $this->{DAT}{$name}{unit};
452             }
453              
454             sub note {
455 0     0 0   my ($this, $name) = @_;
456 0           return $this->{DAT}{$name}{note};
457             }
458              
459             # Autoload for all methods (all others)
460             sub AUTOLOAD {
461 0     0     my ($this, $val) = @_;
462 0 0 0       if ($AUTOLOAD =~ /::pl_(.*)$/ && $this->exists($1)) {
463 0           return $this->data($1, $val);
464             } else {
465 0           carp "Invalid method called - $AUTOLOAD";
466             }
467             }
468              
469 0     0     sub DESTROY {
470             }
471              
472             # ==============================================================================
473             # SPECIAL HELPERS (not defined by autoloader, usually because to complicated)
474             # ==============================================================================
475             sub pl_out {
476 0     0 0   my ($this) = @_;
477             # Need high byte too
478 0           return $this->data('leahl') + $this->data('liahl');
479             }
480             sub pl_in {
481 0     0 0   my ($this) = @_;
482 0           return $this->data('ciahl') + $this->data('ceahl');
483             }
484             sub pl_load {
485 0     0 0   my ($this) = @_;
486 0           return $this->data('lint') + $this->data('lext');
487             }
488             sub pl_charge {
489 0     0 0   my ($this) = @_;
490 0           return $this->data('cint') + $this->data('cext');
491             }
492              
493              
494             # ==============================================================================
495             # INITIALISATION INTERNALLY (batv divider etc.)
496             # ==============================================================================
497             sub initparams {
498 0     0 0   my ($this) = @_;
499             # XXX: Get this somehow, only if we don't already have it
500 0           $this->{PARAMS}{MULTIPLIER} = "4";
501             }
502              
503             # ==============================================================================
504             # READ and WRITE to Serial Port
505             # ==============================================================================
506             # XXX: Arbitrary sleeps to cope with no flow control. Parameterise and
507             # otherwise work out better ways to deal with.
508             sub _write {
509 0     0     my ($this, @arr) = @_;
510 0           my $out = "";
511 0 0         if ($this->{PORT}{TYPE} eq "FILE") {
512 0           foreach my $bit (@arr) {
513 0           usleep $TEMP_DELAY;
514             # $out .= chr($bit);
515 0           my @ready = $this->_select->can_write($TEMP_TIMEOUT);
516 0 0         if (scalar(@ready) < 1) {
517 0           croak "Timeout on write";
518             }
519 0           $this->_port->syswrite(chr($bit), 1);
520             }
521             # return $this->_port->syswrite($out, length($out));
522             } else {
523 0           my @ready = $this->_select->can_write($TEMP_TIMEOUT);
524 0 0         if (scalar(@ready) < 1) {
525 0           croak "Timeout on write";
526             }
527 0           return $this->_port->write($out);
528             }
529             }
530              
531             sub _read {
532 0     0     my ($this, $len) = @_;
533 0           my $buf = "";
534 0           my $tmp = "";
535 0           eval {
536             # local $SIG{__DIE__} = sub {die $_[0];};
537             # local $SIG{ALRM} = sub {die "timeout";};
538 0           for (my $i = 0; $i < $len; $i++) {
539 0           usleep $TEMP_DELAY;
540 0           my @ready = $this->_select->can_read($TEMP_TIMEOUT);
541 0 0         if (scalar(@ready) < 1) {
542 0           croak "Timeout on read";
543             }
544 0 0         if ($this->{PORT}{TYPE} eq "FILE") {
545 0           my $num = $this->_port->sysread($tmp, 1);
546 0           $buf .= $tmp;
547             } else {
548 0           my ($num, $tmp) = $this->_port->read(1);
549 0           $buf .= $tmp;
550             }
551             }
552             };
553 0 0         if ($@) {
554 0           print STDERR "Failed read (request $len)\n";
555 0           return undef;
556             }
557 0           return $buf;
558             }
559              
560             # ==============================================================================
561             # END
562             # ==============================================================================
563              
564             1;
565