File Coverage

blib/lib/Net/Elexol/EtherIO24.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             # Module for manipulating Elexol Ether I/O 24 units
2             #
3             # Copyright (c) 2005 Chris Luke . All rights
4             # reserved. This program is free software; you can redistribute it
5             # and/or modify it under the same terms as Perl itself.
6             #
7             # Feel free to use, modify and redistribute it as long as
8             # you retain the correct attribution.
9             #
10              
11             package Net::Elexol::EtherIO24;
12              
13             require 5.8.0;
14              
15 1     1   26858 use warnings;
  1         2  
  1         39  
16 1     1   7 use strict;
  1         2  
  1         37  
17              
18 1     1   4062 use threads;
  0            
  0            
19             use threads::shared;
20              
21             use Socket;
22             use IO::Socket::INET;
23             use IO::Select;
24             use Time::HiRes;
25              
26              
27             =head1 NAME
28              
29             Net::Elexol::EtherIO24 - Threaded object interface for manipulating Elexol Ether I/O 24 units with Perl
30              
31             =cut
32              
33             our $VERSION = '0.22';
34              
35             =head1 VERSION
36              
37             Version 0.22.
38              
39             Requires Perl 5.8.0.
40              
41             =cut
42              
43             # =============================================================================
44              
45             =head1 SYNOPSIS
46              
47             use Net::Elexol::EtherIO24;
48              
49             Net::Elexol::EtherIO24->debug(1);
50             my $eio = Net::Elexol::EtherIO24->new(target_addr=>$addr, threaded=>1);
51              
52             for my $line (0..23) {
53             print "line $line dir: ".$eio->get_line_dir($line)." ".
54             "line $line val: ".$eio->get_line($line)."\n";
55             }
56              
57             $eio->close;
58              
59             =head1 DESCRIPTION
60              
61             The Ether I/O 24 manufactured by Elexol is an inexpensive and simple to
62             use and operate device designed for remote control or remote sensing.
63             It has 24 digital lines that are each programmable for input or output and a
64             variety of other things.
65              
66             The control protocol is relatively simplistic and UDP based. This Perl
67             module attempts to abstract this protocol and add other features
68             along the way. In particular, programmers are encouraged to investigate
69             setting direct_writes => 0 and direct_reads => 0 in the constructor
70             for network efficiency (since these are not yet the defaults).
71              
72             It is thread savvy and will use threads unless told not to. It might perform
73             adequately without threads, but various functionality would be reduced as
74             a result. In particular, the module functions in a nice asynchronous
75             way when it can use threads. Threads support requires Perl 5.8.
76             This module may not function correctly, or even compile, with an older Perl.
77             Your Perl will require Threads to be enabled at compile-time, even if you
78             don't use Threads with this module.
79              
80             It uses C for network I/O and C for timing.
81             It was developed using Perl on a FreeBSD and a Linux system, but has
82             been known to function using Perl with Cygwin or ActivePerl on Windows.
83              
84             =cut
85              
86             # =============================================================================
87              
88             my $_debug = 0;
89             my $_error = 0;
90              
91             share($_debug);
92             share($_error);
93              
94             sub _dbg($$) {
95             my $self = shift;
96             my $line = shift;
97             my $debug = shift;
98             return if(!$self->{'debug'});
99             $debug = 0 if(!$debug);
100             return if($self->{'debug'} < $debug);
101             my $pfx = $self->{'debug_prefix'};
102             $pfx = 'eio' if(!$pfx);
103             $pfx .= ':'.threads->self->tid() if($self->{'threaded'});
104             print STDERR $pfx.': '.$line."\n";
105             }
106              
107              
108             # =============================================================================
109              
110             =head1 CONSTRUCTOR
111              
112             =over 4
113              
114             =item I
115              
116             Creates a new C object complete with associated
117             socket and any necessary threads (if enabled). Returns undef if
118             this is not possible, whereupon the application can check
119             C<< Net::Elexol::EtherIO24->error() >> for any relevant error string.
120              
121             Arguments are given in C<< key => value >> form from these candidates:
122              
123             =over 4
124              
125             =item I
126              
127             Address or hostname of the device to communicate with. B.
128              
129             =item I
130              
131             UDP port number to communicate on. Defaults to '2424'.
132              
133             =item I
134              
135             Indicates that the current status and configuration of ports on
136             the device should be immediately fetched. Defaults to '1', which
137             enables this feature.
138              
139             =item I
140              
141             Indicates that the initial state should be immediately sent to the
142             device. This would set all lines to inputs with no pullups and
143             threshold set to TTL levels. If at some point we add a way to pre-set
144             this status, then this feature might become useful!
145              
146             =item I
147              
148             Enables Perl ithreads and creates a thread to listen to replies from the
149             EtherIO24 unit. This currently requires Perl 5.8 to function. For the
150             most part, client applications do not need to be thread aware, but some
151             functionality may change this assumption. Defaults to '1'.
152              
153             =item I, I, I
154              
155             These values control various timers and are unlikely to need any
156             tweaking. However: recv_timeout is the time recv_result will hang around
157             waiting for an answer. service_recv_timeout is the timer used in the
158             packet receiver thread, when threading, to wait for packets before
159             seeing if there's anything else to do. service_status_fetch is how
160             often in that same thread we fire off a call to the status_fetch
161             method, just to keep our status up-to-date, just in case. These
162             timers are all integer seconds.
163              
164             I defaults to '1.0', I to '1.0' and
165             I to '60.0'.
166              
167             =item I, I
168              
169             These values, which default to '1' (on) control whether the various
170             line_ methods directly query/update the Elexol device or whether they
171             cache data and send/fetch the data to/fom the Elexol device periodically.
172              
173             The latter method (a setting of '0') can cause less network traffic if you
174             are constantly polling the device at the expense of a marginally longer interval
175             before the device is polled. However, you must call the I
176             method in order to push out writes quickly, or especially if you are not
177             using threads.
178              
179             By default, if the I method is called any pending writes are sent
180             (See I below).
181              
182             If data is received that would overwrite a pending write then any pending
183             writes are sent.
184              
185             =item I
186              
187             The interval, in seconds, between background writes to the Elexol device.
188             When not using I this is the interval at which updates are
189             sent. Defaults to '0.1' (200ms).
190              
191             =item I
192              
193             The interval, in fractional seconds, after which a cached read value from
194             the Elexol device is considered invalid and must be refetched if that
195             line group is queried. Defaults to '0.5' (500ms).
196              
197             =item I
198              
199             Defaults to '0'. Forces any "write" functions to "read" the current status
200             first. However, if I is '0', it will used the cached value
201             if it has not yet expired.
202              
203             It should be noted that this is very risky since collisions will occur if
204             two such agents attempt to write to the same group of lines at
205             approximately the same time.
206              
207             =item I, I
208              
209             Controls debugging output. Default value of I<$debug> is inherited from the parent
210             object (if you set it with C before cloning
211             it).
212              
213             I<$debug_prefix> is displayed at the start of all debug output and defaults to
214             'eio24'. You can set this, for example, if you have more than one EtherIO24 object
215             so you can differentiate the debugging output of each.
216              
217             See also the C method. Also note that when using threads, the thread ID
218             that produced the debugging output is included after the prefix.
219              
220             =item I
221              
222             Defaults to '1', on. Determines whether the I method is
223             called at I to flush any pending writes.
224              
225             =item I
226              
227             Number of attempts to read an eeprom location, if it times out. Defaults to '2'.
228              
229             =item I
230              
231             By default this is not defined. The developer can pass in a reference to a
232             subroutine that will be called after new status information is received from
233             the Elexol device.
234              
235             Such new status includes both the response to a status query, or unsolicited
236             updates from the autoscan feature.
237              
238             The subroutine is passed four parameters:
239              
240             $fn($data, $key, $new_value, $old_value)
241              
242             I<$data> is the $data hash used to store information by the object, and which
243             can optionally be passed in at object creation time (see below).
244              
245             I<$key> is the index into $data that was updated with new status information.
246             This will be in the form "TYPE GROUP" where TYPE is one of "status", "dir",
247             "pullup", "thresh" and GROUP is one of "A", "B", or "C".
248              
249             I<$new_value> is the value just received.
250              
251             I<$old_value> is the previous value.
252              
253             =item I
254              
255             If true then we will attempt to wakeup the module at initialisation. See the
256             'wakeup' method for details.
257              
258             =item I
259              
260             Various state information is contained within a hash. If not given, one
261             is created and used anonymously. However, the application can pass in
262             a reference to a hash here, for instance to identify this object to an
263             async callback subroutine.
264              
265             Many of the items in this hash are C when we are threading.
266              
267             Developers should prefix their own elements in this hash with a '_'
268             character to ensure uniqueness from those added by this module.
269              
270             =back
271              
272             =back
273              
274             =cut
275              
276             sub new {
277             my $proto = shift;
278             my %arg = @_;
279              
280             my $class = ref($proto) || $proto;
281             my $self = {};
282              
283             if(!$arg{'target_addr'}) {
284             $_error = "No target_addr specified";
285             return undef;
286             }
287              
288             # It's worth noting that when threading, our backround servicing
289             # thread only sees the values of these things as they were at the
290             # time the thread started. Anything that changes needs to go into
291             # $self->{'data'} and be share()'ed. The best place to initialise
292             # such a thing is in init_state() further down.
293              
294             $self->{'debug'} = $_debug;
295             $self->{'debug_prefix'} = 'eio24';
296             $self->{'target_port'} = '2424';
297             $self->{'prefetch_status'} = 1;
298             $self->{'presend_status'} = 0;
299             $self->{'threaded'} = 1;
300             $self->{'recv_timeout'} = 1.0;
301             $self->{'service_recv_timeout'} = 1.0;
302             $self->{'service_status_fetch'} = 60;
303             $self->{'direct_writes'} = 1;
304             $self->{'direct_reads'} = 1;
305             $self->{'indirect_write_interval'} = 0.1;
306             $self->{'indirect_read_interval'} = 0.5;
307             $self->{'read_before_write'} = 0;
308             $self->{'flush_writes_at_close'} = 1;
309             $self->{'eeprom_read_retries'} = 2;
310             $self->{'async_status_sub'} = undef;
311              
312             $self->{'socket'} = undef;
313             $self->{'thread_indirect'} = undef;
314             $self->{'thread_status'} = undef;
315             $self->{'thread_recv'} = undef;
316              
317             foreach my $field (('debug', 'debug_prefix',
318             'target_addr', 'target_port',
319             'prefetch_status', 'presend_status', 'threaded', 'recv_timeout',
320             'service_recv_timeout', 'service_status_fetch',
321             'direct_writes', 'direct_reads',
322             'indirect_write_interval', 'indirect_read_interval',
323             'read_before_write', 'flush_writes_at_close', 'eeprom_read_retries',
324             'async_status_sub', 'wakeup', )) {
325             $self->{$field} = $arg{$field} if(defined($arg{$field}));
326             }
327              
328             # Bless me...
329             bless($self, $class);
330              
331             # Things relating to the state of the IO24 module
332             $self->{'data'} = {};
333             if($arg{'data'}) {
334             $self->{'data'} = $arg{'data'};
335             }
336              
337             _init_state($self);
338              
339             $self->wakeup if($arg{'wakeup'});
340              
341             $self->{'socket'} = IO::Socket::INET->new(
342             PeerAddr => $self->{'target_addr'},
343             PeerPort => $self->{'target_port'},
344             Proto => 'udp',
345             ReuseAddr => 1,
346             );
347             if(!$self->{'socket'}) {
348             $_error = "Net::Elexol::EtherIO24->new can't create socket: $@\n";
349             return undef;
350             }
351              
352             if($self->{'threaded'}) {
353             $self->_dbg("we're going to be using threads, starting service threads...", 1);
354             $self->{'thread_indirect'} = threads->new(\&_service_indirect, $self);
355             $self->{'thread_status'} = threads->new(\&_service_status, $self);
356             $self->{'thread_recv'} = threads->new(\&_service_recv, $self);
357             }
358              
359             if($self->{'prefetch_status'}) {
360             if(!$self->status_fetch) {
361             $self->close;
362             $_error .= ' while prefetching status';
363             return undef;
364             }
365             if(!$self->eeprom_fetch) {
366             $self->close;
367             $_error .= ' while prefetching eeprom contents';
368             return undef;
369             }
370             }
371             $self->status_send() if($self->{'presend_status'});
372             $self->{'parent'} = 1;
373              
374             return $self;
375             }
376              
377             # Until we can reliably detect the one and only useful call to this,
378             # we need to comment out DESTROY. It's called too many times when a
379             # thread ends and runtime values like 'running' don't seem to keep up!
380             # Net effect: Applications MUST call 'close'.
381              
382             #DESTROY {
383             # my $self = shift;
384             # $self->close if($self->{'parent'});
385             # $self->SUPER::DESTROY if($self->can("SUPER::DESTROY"));
386             #}
387              
388             =head1 METHODS
389              
390             =over 4
391              
392             =item I
393              
394             Closes network resources and waits (briefly) for any running threads to end. Should
395             be called when the host application is ending or when the object is no longer needed.
396              
397             The object destructor will attempt to call this function when the world ends, but Perl
398             might not be patient enough to wait for threads to end by that time.
399              
400             =cut
401              
402             sub close {
403             my $self = shift;
404              
405             return if(!$self->{'parent'});
406             return if(!$self->{'data'}->{'running'});
407              
408             $self->_dbg("close called, shutting down...", 1);
409              
410             $self->indirect_write_send if($self->{'flush_writes_at_close'}); # flush anything pending
411              
412             { lock($self->{'data'}->{'running'}); $self->{'data'}->{'running'} = 0; } # should signal threads to exit
413              
414             if($self->{'threaded'}) {
415             foreach my $tname (('indirect', 'status', 'recv')) {
416             my $t = $self->{'thread_'.$tname};
417             if($t) {
418             $self->_dbg("waiting for thread '$tname' (id ".$t->tid().") to stop", 1);
419             $t->join;
420             $self->{'thread_'.$tname} = undef;
421             }
422             }
423             }
424              
425             if($self->{'socket'}) {
426             $self->{'socket'}->close;
427             $self->{'socket'} = undef;
428             }
429             }
430              
431             =item I
432              
433             Send a handful of UDP packets to the device to 'wake it up'. The intention
434             is to trigger MAC address resolution before we send any real packets to it.
435              
436             This method creates and closes its own socket so it does not interfere with
437             other threads.
438              
439             It is called at initialisation if you pass 'wakeup' into the constructor.
440              
441             =cut
442              
443             sub wakeup {
444             my $self = shift;
445             my %arg = @_;
446              
447             $self->_dbg("Attempting to wakeup module at ".$self->{'target_addr'}.":".$self->{'target_port'}, 1);
448              
449             my $s = IO::Socket::INET->new(
450             PeerAddr => $self->{'target_addr'},
451             PeerPort => $self->{'target_port'},
452             Proto => 'udp',
453             ReuseAddr => 1,
454             );
455             if(!$s) {
456             $_error = "Net::Elexol::EtherIO24->wakeup can't create socket for wakeup: $@\n";
457             return undef;
458             }
459              
460             # Send a couple of simple packets to the device.
461             $s->send('IO24');
462             $s->send('IO24');
463             $s->send('IO24');
464             $s->send('IO24');
465              
466             # Wait briefly - enough time for MAC resolution to occur.
467             Time::HiRes::usleep(250000);
468              
469             # TODO: Detect if the module was woken up or not. :)
470              
471             # Move on.
472             $s->close;
473              
474             return 1;
475             }
476              
477             sub _service_indirect {
478             my $self = shift;
479            
480             $self->_dbg("service_indirect starting up", 1);
481              
482             my $indirect_time = Time::HiRes::time() + $self->{'indirect_write_interval'};
483              
484             while($self->{'data'}->{'running'}) {
485             if($self->{'indirect_write_interval'} && $indirect_time < Time::HiRes::time()) {
486             $indirect_time = Time::HiRes::time() + $self->{'indirect_write_interval'};
487             $self->indirect_write_send;
488             } else {
489             Time::HiRes::usleep(1000000);
490             }
491             }
492              
493             $self->_dbg("service_indirect shutting down", 1);
494             }
495              
496             sub _service_status {
497             my $self = shift;
498              
499             $self->_dbg("service_status starting up", 1);
500              
501             my $status_time = Time::HiRes::time() + $self->{'service_status_fetch'};
502              
503             while($self->{'data'}->{'running'}) {
504             if($self->{'service_status_fetch'} && $status_time < Time::HiRes::time()) {
505             $status_time = Time::HiRes::time() + $self->{'service_status_fetch'};
506             # 0=don't recv_result, which would cause a deadlock
507             $self->status_fetch(0);
508             } else {
509             Time::HiRes::usleep(1000000);
510             }
511              
512             }
513              
514             $self->_dbg("service_status shutting down", 1);
515             }
516              
517             sub _service_recv {
518             my $self = shift;
519              
520             $self->_dbg("service_recv starting up", 1);
521              
522             while($self->{'data'}->{'running'}) {
523             $self->recv_command;
524             }
525              
526             $self->_dbg("service_recv shutting down", 1);
527             }
528              
529             =item I
530              
531             The higher $level is, the more debugging is output. "3" is currently the useful
532             limit, though "4" will enable hex-dumps of all data sent and received.
533              
534             Can be called on the parent to set default debugging level, and on each object
535             to control that objects debug level.
536              
537             =cut
538              
539             sub debug {
540             my $self = shift;
541             if(@_) {
542             $_debug = shift;
543             $self->{'debug'} = $_debug if(ref($self));
544             }
545             return $_debug;
546             }
547              
548             =item I
549              
550             Returns a string description of the last error, or 0 if no error recorded.
551              
552             Note that this value is global - it is shared between all EtherIO24 objects
553             so that you can return an error should C fail to construct a new object.
554              
555             =cut
556              
557             sub error {
558             my $self = shift;
559             return $_error;
560             }
561              
562             =item I
563              
564             Returns a string containing a HEX and ASCII dump of the packet in I<$packet>.
565              
566             This is used in the send/receive routines if a high enough debug level is set and
567             is provided here in case someone else finds it useful.
568              
569             I<$offset> is optional and specifies the offset in the packet to start at, defaults to 0.
570              
571             I<$increment> is optional and specifies how many items to display per line, defaults to 16.
572              
573             =cut
574              
575             sub dump_packet {
576             my $self = shift;
577             my $packet = shift;
578             my $offset = shift;
579             my $incr = shift;
580              
581             my $string = "";
582              
583             $offset = 0 if(!defined($offset));
584             $incr = 16 if(!defined($incr));
585              
586             while($offset < length($packet)) {
587             my $l = substr($packet, $offset, $incr);
588             my $hexstr = join(' ', map { sprintf "%02.2x", $_ } unpack("C*", $l));
589             my $ascstr = $l;
590             $ascstr =~ s/[^A-Za-z0-9,:;\-=_+<>?\/\\{}[\]'"`]/./g;
591            
592             my $hexlen = ($incr*3)-1;
593             $string .= sprintf("%04.4d %-${hexlen}.${hexlen}s %s\n", $offset, $hexstr, $ascstr);
594              
595             $offset += $incr;
596             }
597             return $string;
598             }
599              
600             sub _dbg_packet {
601             my $self = shift;
602             my $packet = shift;
603             my $debug = shift;
604             my $offset = shift;
605             my $incr = shift;
606              
607             return if($self->{'debug'} < $debug);
608             my $string = $self->dump_packet($packet, $offset, $incr);
609             foreach my $line (split(/\n/, $string)) {
610             $self->_dbg($line, $debug);
611             }
612             }
613              
614             # =============================================================================
615              
616             my $status_commands = {
617             'status A' => 'a',
618             'status B' => 'b',
619             'status C' => 'c',
620             'dir A' => '!a',
621             'dir B' => '!b',
622             'dir C' => '!c',
623             'pullup A' => '@a',
624             'pullup B' => '@b',
625             'pullup C' => '@c',
626             'thresh A' => '#a',
627             'thresh B' => '#b',
628             'thresh C' => '#c',
629             'schmitt A' => '$a',
630             'schmitt B' => '$b',
631             'schmitt C' => '$c',
632             };
633              
634             # reverse mapping of status commands
635             my $status_map = {};
636             foreach my $key (keys %$status_commands) {
637             $status_map->{$status_commands->{$key}} = $key;
638             }
639              
640             my $set_commands = {
641             'status A' => 'A',
642             'status B' => 'B',
643             'status C' => 'C',
644             'dir A' => '!A',
645             'dir B' => '!B',
646             'dir C' => '!C',
647             'pullup A' => '@A',
648             'pullup B' => '@B',
649             'pullup C' => '@C',
650             'thresh A' => '#A',
651             'thresh B' => '#B',
652             'thresh C' => '#C',
653             'schmitt A' => '$A',
654             'schmitt B' => '$B',
655             'schmitt C' => '$C',
656             };
657              
658             # reverse mapping of set command
659             my $set_map = {};
660             foreach my $key (keys %$set_commands) {
661             $set_map->{$set_commands->{$key}} = $key;
662             }
663              
664             # What a status query results in
665             my $cmd_map = {
666             'a' => 'A',
667             '!a' => '!A',
668             '@a' => '@A',
669             '#a' => '#A',
670             '$a' => '$A',
671             'b' => 'B',
672             '!b' => '!B',
673             '@b' => '@B',
674             '#b' => '#B',
675             '$b' => '$B',
676             'c' => 'C',
677             '!c' => '!C',
678             '@c' => '@C',
679             '#c' => '#C',
680             '$c' => '$C',
681             'IO24' => 'IO24',
682             '%' => '%',
683             '`' => '\'',
684             '*' => ' ',
685             '\'R' => 'R',
686             };
687              
688             my $cmd_rev_map = {};
689             foreach my $key (keys %$cmd_map) {
690             $cmd_rev_map->{$cmd_map->{$key}} = $key;
691             }
692              
693             my $send_commands = {
694             'IO24' => {
695             length => 4,
696             'desc' => 'ID units',
697             },
698              
699             'A' => {
700             'length' => 2,
701             'desc' => 'Wr Port A',
702             'type' => 'hex_byte',
703             },
704             'B' => {
705             'length' => 2,
706             'desc' => 'Wr Port C',
707             'type' => 'hex_byte',
708             },
709             'C' => {
710             'length' => 2,
711             'desc' => 'Wr Port C',
712             'type' => 'hex_byte',
713             },
714              
715             'a' => {
716             'length' => 1,
717             'desc' => 'Rd Port A',
718             },
719             'b' => {
720             'length' => 1,
721             'desc' => 'Rd Port B',
722             },
723             'c' => {
724             'length' => 1,
725             'desc' => 'Rd Port C',
726             },
727              
728             '!A' => {
729             'length' => 3,
730             'desc' => 'Wr Dir A',
731             'type' => 'hex_byte',
732             },
733             '!B' => {
734             'length' => 3,
735             'desc' => 'Wr Dir B',
736             'type' => 'hex_byte',
737             },
738             '!C' => {
739             'length' => 3,
740             'desc' => 'Wr Dir C',
741             'type' => 'hex_byte',
742             },
743              
744             '!a' => {
745             'length' => 2,
746             'desc' => 'Rd Dir A',
747             },
748             '!b' => {
749             'length' => 2,
750             'desc' => 'Rd Dir B',
751             },
752             '!c' => {
753             'length' => 2,
754             'desc' => 'Rd Dir C',
755             },
756              
757             '@A' => {
758             'length' => 3,
759             'desc' => 'Wr Pullup A',
760             'type' => 'hex_byte',
761             },
762             '@B' => {
763             'length' => 3,
764             'desc' => 'Wr Pullup B',
765             'type' => 'hex_byte',
766             },
767             '@C' => {
768             'length' => 3,
769             'desc' => 'Wr Pullup C',
770             'type' => 'hex_byte',
771             },
772              
773             '#A' => {
774             'length' => 3,
775             'desc' => 'Wr Thresh A',
776             'type' => 'hex_byte',
777             },
778             '#B' => {
779             'length' => 3,
780             'desc' => 'Wr Thresh B',
781             'type' => 'hex_byte',
782             },
783             '#C' => {
784             'length' => 3,
785             'desc' => 'Wr Thresh C',
786             'type' => 'hex_byte',
787             },
788              
789             '$A' => {
790             'length' => 3,
791             'desc' => 'Wr Schmitt A',
792             'type' => 'hex_byte',
793             },
794             '$B' => {
795             'length' => 3,
796             'desc' => 'Wr Schmitt B',
797             'type' => 'hex_byte',
798             },
799             '$C' => {
800             'length' => 3,
801             'desc' => 'Wr Schmitt C',
802             'type' => 'hex_byte',
803             },
804              
805             '@a' => {
806             'length' => 2,
807             'desc' => 'Rd Pullup a',
808             },
809             '@b' => {
810             'length' => 2,
811             'desc' => 'Rd Pullup b',
812             },
813             '@c' => {
814             'length' => 2,
815             'desc' => 'Rd Pullup c',
816             },
817              
818             '#a' => {
819             'length' => 2,
820             'desc' => 'Rd Thresh a',
821             },
822             '#b' => {
823             'length' => 2,
824             'desc' => 'Rd Thresh b',
825             },
826             '#c' => {
827             'length' => 2,
828             'desc' => 'Rd Thresh c',
829             },
830              
831             '$a' => {
832             'length' => 2,
833             'desc' => 'Rd Schmitt a',
834             },
835             '$b' => {
836             'length' => 2,
837             'desc' => 'Rd Schmitt b',
838             },
839             '$c' => {
840             'length' => 2,
841             'desc' => 'Rd Schmitt c',
842             },
843              
844             '\'R' => {
845             'length' => 5,
846             'desc' => 'Rd EEPROM word',
847             'type' => 'eeprom',
848             'nobundle' => 1,
849             },
850             '\'W' => {
851             'length' => 5,
852             'desc' => 'Wr EEPROM word',
853             'type' => 'eeprom',
854             'nobundle' => 1,
855             },
856             '\'E' => {
857             'length' => 5,
858             'desc' => 'Erase EEPROM word',
859             'type' => 'eeprom',
860             'nobundle' => 1,
861             },
862             '\'0' => {
863             'length' => 5,
864             'desc' => 'Write disable EEPROM',
865             'type' => 'hex_byte',
866             'nobundle' => 1,
867             },
868             '\'1' => {
869             'length' => 5,
870             'desc' => 'Write enable EEPROM',
871             'type' => 'hex_byte',
872             'nobundle' => 1,
873             },
874             '\'@' => {
875             'length' => 5,
876             'desc' => 'Reset module',
877             'type' => 'eeprom',
878             'nobundle' => 1,
879             },
880              
881             '`' => {
882             'length' => 2,
883             'desc' => 'Echo byte',
884             'type' => 'hex_byte',
885             },
886             '*' => {
887             'length' => 1,
888             'desc' => 'Echo a space',
889             },
890             '%' => {
891             'length' => 1,
892             'desc' => 'Read host data',
893             },
894             };
895              
896             my $recv_commands = {
897             'IO24' => {
898             length => 12,
899             'desc' => 'ID units',
900             'type' => 'io24',
901             },
902              
903             'A' => {
904             'length' => 2,
905             'desc' => 'Wr Port A',
906             'type' => 'hex_byte',
907             },
908             'B' => {
909             'length' => 2,
910             'desc' => 'Wr Port C',
911             'type' => 'hex_byte',
912             },
913             'C' => {
914             'length' => 2,
915             'desc' => 'Wr Port C',
916             'type' => 'hex_byte',
917             },
918              
919             '!A' => {
920             'length' => 3,
921             'desc' => 'Wr Dir A',
922             'type' => 'hex_byte',
923             },
924             '!B' => {
925             'length' => 3,
926             'desc' => 'Wr Dir B',
927             'type' => 'hex_byte',
928             },
929             '!C' => {
930             'length' => 3,
931             'desc' => 'Wr Dir C',
932             'type' => 'hex_byte',
933             },
934              
935             '@A' => {
936             'length' => 3,
937             'desc' => 'Wr Pullup A',
938             'type' => 'hex_byte',
939             },
940             '@B' => {
941             'length' => 3,
942             'desc' => 'Wr Pullup B',
943             'type' => 'hex_byte',
944             },
945             '@C' => {
946             'length' => 3,
947             'desc' => 'Wr Pullup C',
948             'type' => 'hex_byte',
949             },
950              
951             '#A' => {
952             'length' => 3,
953             'desc' => 'Wr Thresh A',
954             'type' => 'hex_byte',
955             },
956             '#B' => {
957             'length' => 3,
958             'desc' => 'Wr Thresh B',
959             'type' => 'hex_byte',
960             },
961             '#C' => {
962             'length' => 3,
963             'desc' => 'Wr Thresh C',
964             'type' => 'hex_byte',
965             },
966              
967             '$A' => {
968             'length' => 3,
969             'desc' => 'Wr Schmitt A',
970             'type' => 'hex_byte',
971             },
972             '$B' => {
973             'length' => 3,
974             'desc' => 'Wr Schmitt B',
975             'type' => 'hex_byte',
976             },
977             '$C' => {
978             'length' => 3,
979             'desc' => 'Wr Schmitt C',
980             'type' => 'hex_byte',
981             },
982              
983             'R' => {
984             'length' => 4,
985             'desc' => 'Rd EEPROM word',
986             'type' => 'eeprom_recv',
987             },
988              
989             '\'' => {
990             'length' => 2,
991             'desc' => 'Echo byte',
992             'type' => 'hex_byte',
993             },
994             ' ' => {
995             'length' => 1,
996             'desc' => 'Echo a space',
997             },
998             '%' => {
999             'length' => 16,
1000             'desc' => 'Read host data',
1001             'type' => 'host_data',
1002             },
1003             };
1004              
1005             # =============================================================================
1006              
1007             sub _init_state {
1008             my $self = shift;
1009             my $data = $self->{'data'};
1010              
1011             $data->{'running'} = 1;
1012             share($data->{'running'});
1013             $data->{'running'} = 1;
1014              
1015             foreach my $key (keys %$status_commands) {
1016             $data->{$key} = 0;
1017             share($data->{$key});
1018             $data->{'changed '.$key} = 0;
1019             share($data->{'changed '.$key});
1020             $data->{'ts '.$key} = 0;
1021             share($data->{'ts '.$key});
1022             }
1023             foreach my $addr (0..63) {
1024             $data->{'rcvd eeprom '.$addr} = 0;
1025             share($data->{'rcvd eeprom '.$addr});
1026             $data->{'eeprom '.$addr} = 0;
1027             share($data->{'eeprom '.$addr});
1028             }
1029             foreach my $cmd (keys %$cmd_map) {
1030             $data->{'rcvd '.$cmd_map->{$cmd}} = 1;
1031             share($data->{'rcvd '.$cmd_map->{$cmd}});
1032             $data->{'rcvdcmd '.$cmd_map->{$cmd}} = 0;
1033             share($data->{'rcvdcmd '.$cmd_map->{$cmd}});
1034             }
1035              
1036             foreach my $var ((
1037             'last_status_fetch',
1038             'last_status_send',
1039             'last_eeprom_fetch')) {
1040             $data->{$var} = 0;
1041             share($data->{$var});
1042             }
1043             }
1044              
1045             =item I
1046              
1047             Resets the timestamps on all cached data forcing the next read
1048             to query the Elexol device.
1049              
1050             =cut
1051              
1052             sub clear_cache {
1053             my $self = shift;
1054             my $data = $self->{'data'};
1055              
1056             foreach my $key (keys %$status_commands) {
1057             $data->{'ts '.$key} = 0;
1058             }
1059             }
1060              
1061             =item I
1062              
1063             Fetch the contents of the eeprom. If recv is 1 then it will wait
1064             for the results to arrive, otherwise it returns immediately.
1065              
1066             Additionally gets all the reserved and user space words if $fetchall is 1.
1067              
1068             Returns 1 on success, 0 on failure.
1069              
1070             =cut
1071              
1072             sub eeprom_fetch {
1073             my $self = shift;
1074             my $recv = shift || 1;
1075             my $fetchall = shift || 0;
1076              
1077             my $last = 24;
1078             $last = 63 if($fetchall);
1079              
1080             foreach my $addr (0..$last) {
1081             if(!$self->read_eeprom($addr)) {
1082             return 0;
1083             }
1084             }
1085             $self->{'data'}->{'last_eeprom_fetch'} = time();
1086             return 1;
1087             }
1088              
1089             =item I
1090              
1091             Query the EtherIO24 for its current status. This will store
1092             the current status of all I/O lines and their programmed
1093             settings. If you don't intend to reset these settings then
1094             this is important in order for the Net::Elexol::EtherIO24 to be
1095             able to manipulate I/O lines on a per-bit basis.
1096              
1097             If recv is 1 then it will wait
1098             for the results to arrive, otherwise it returns immediately.
1099              
1100             Returns 1 on success, 0 on failure.
1101              
1102             =cut
1103              
1104             sub status_fetch {
1105             my $self = shift;
1106             my $recv = shift;
1107              
1108             # Issue "read" commands and issue a read_command.
1109             # We can ignore the response since status-responses
1110             # will get updated in our state automagically.
1111             my $cmd;
1112             foreach my $key (sort keys %$status_commands) {
1113             $cmd .= $status_commands->{$key};
1114             }
1115             if(!send_command($self, $cmd)) {
1116             $self->_dbg("WARNING: Unable to send status request.", 0);
1117             return 0;
1118             } else {
1119             if($recv && !recv_result($self, $cmd)) {
1120             $self->_dbg("WARNING: Error receiving status reply ($_error)", 0);
1121             return 0;
1122             }
1123             }
1124             $self->{'data'}->{'last_status_fetch'} = time();
1125             return 1;
1126             }
1127              
1128             =item I
1129              
1130             Send all current status to the EtherIO24.
1131              
1132             Returns 1 on success, 0 on failure.
1133              
1134             =cut
1135              
1136             sub status_send {
1137             my $self = shift;
1138              
1139             # Issue a series of write commands.
1140             my $cmd = '';
1141             foreach my $key (sort keys %$set_commands) {
1142             $cmd .= $set_commands->{$key}.pack("C", $self->{$key});
1143             }
1144             if(!send_command($self, $cmd)) {
1145             $self->_dbg("WARNING: Unable to send status.", 0);
1146             return 0;
1147             }
1148             $self->{'data'}->{'last_status_send'} = time();
1149             }
1150              
1151              
1152             =item I
1153              
1154             This method performs various background tasks such as sending any updates to
1155             the Elexol device that are pending and retrieving status from the device.
1156              
1157             It should be called periodically (often) if you are not using threads; otherwise
1158             it is not necessary (but not harmful) to call this.
1159              
1160             =cut
1161              
1162             sub indirect_write_send {
1163             my $self = shift;
1164              
1165             my $data = $self->{'data'};
1166              
1167             # Send out any pending writes.
1168              
1169             $self->_dbg("indirect_write_send: checking for pending writes", 5);
1170              
1171             foreach my $key (sort keys %$status_commands) { # sorting this means "dir" is written before "status" - important
1172             if($data->{'changed '.$key}) {
1173             $self->_dbg("indirect_write_send: \"$key\" is pending write...", 4);
1174             send_command($self, $set_commands->{$key}.pack("C", $data->{$key}));
1175             $data->{'changed '.$key} = 0;
1176             }
1177             }
1178             }
1179              
1180             # =============================================================================
1181              
1182             sub _decode_cmd {
1183             my $cmd = shift;
1184             my $len = shift;
1185             my $type = shift;
1186              
1187             my $txt = '';
1188             $type = 0 if(!$type);
1189             if($type eq 'hex_byte') {
1190             # dump non-cmd chars as hex bytes
1191             foreach my $i ($len..length($cmd)-1) {
1192             $txt .= sprintf("%02.2x ", unpack("x$i C1", $cmd));
1193             }
1194             } elsif($type eq 'eeprom') {
1195             my($addr, $msb, $lsb) = unpack("x2 CCC", $cmd);
1196             $txt = sprintf("addr: %d (0x%02.2x) val: %02.2x %02.2x",
1197             $addr, $addr, $msb, $lsb);
1198             } elsif($type eq 'eeprom_recv') {
1199             my($addr, $msb, $lsb) = unpack("x CCC", $cmd);
1200             $txt = sprintf("addr: %d (0x%02.2x) val: %02.2x %02.2x",
1201             $addr, $addr, $msb, $lsb);
1202             } elsif($type eq 'host_data') {
1203             $txt .= sprintf("Serial: %02.2x%02.2x%02.2x ".
1204             "IP: %d.%d.%d.%d ".
1205             "MAC: %02.2x:%02.2x:%02.2x:%02.2x:%02.2x:%02.2x",
1206             unpack("x$len CCCCCCCCCCCCC", $cmd));
1207             } elsif($type eq 'io24') {
1208             $txt .= sprintf("MAC: %02.2x:%02.2x:02.2x:02.2x:02.2x:02.2x ".
1209             "Fw: %02.2x.$02.2x",
1210             unpack("x$len CCCCCCCC", $cmd));
1211             }
1212              
1213             return $txt;
1214             }
1215              
1216             sub _find_cmd {
1217             my $cmd = shift;
1218             my $cmds = shift;
1219              
1220             foreach my $len (1..length($cmd)) {
1221             my $c = substr($cmd, 0, $len);
1222             if($cmds->{$c}) {
1223             return $c;
1224             }
1225             }
1226             return 0;
1227             }
1228              
1229             =item I
1230              
1231             Not normally called directly.
1232              
1233             Verify commands to be sent. Returns 1 if the command(s) is(are) valid. Returns
1234             0 if any command is invalid. Will search the entire string given for multiple commands.
1235              
1236             Will perform various processing tasks on the command, such as resetting the
1237             "status received" flags for any status fields referenced by the command.
1238              
1239             =cut
1240              
1241             sub verify_send_command {
1242             my $self = shift;
1243             my $cmd = shift;
1244              
1245             my $data = $self->{'data'};
1246             my $start = 0;
1247             my $ok = 1;
1248             while($start < length($cmd) && $ok) {
1249             my $c = _find_cmd(substr($cmd, $start, 6), $send_commands);
1250             if($c) {
1251             # found it!
1252             my $len = length($c);
1253             my $chk = substr($cmd, $start, $send_commands->{$c}->{'length'});
1254             if($self->{'debug'}>1) {
1255             my $type = $send_commands->{$c}->{'type'};
1256             my $txt = _decode_cmd($chk, $len, $type);
1257             $self->_dbg("verify_send_command: cmd \"$c\" -> \"".
1258             $send_commands->{$c}->{'desc'}."\"".
1259             ($txt ne ''?": $txt":""), 1);
1260             }
1261             # Hmm, what was this block of code for? It seems to slow things down and occasionaly hang the whole thing!
1262             # #if($set_map->{$c} && $self->{'threaded'}) {
1263             # if($cmd_map->{$c} && $self->{'threaded'}) {
1264             # # block waiting for any outstanding status queries to return
1265             # # to avoid a race condition
1266             # #my $f = 'rcvd '.$c;
1267             # my $f = 'rcvd '.$cmd_map->{$c};
1268             # my $timeout = time() + $self->{'recv_timeout'};
1269             # lock($data->{$f});
1270             # while(!$data->{$f}) {
1271             # $self->_dbg("verify_send_command: flag snd check data->{$f} = ".$data->{$f}, 2);
1272             # last if(!cond_timedwait($data->{$f}, $timeout));
1273             # }
1274             # $self->_dbg("verify_send_command: flag snd result data->{$f} = ".$data->{$f}, 2);
1275             # if(!$data->{$f}) {
1276             # $_error = 'Timeout waiting for outstanding status reply '.
1277             # 'while trying to send new status';
1278             # $ok = 0;
1279             # last;
1280             # }
1281             #
1282             # }
1283             if($cmd_map->{$c}) { # reset "received" status for query commands
1284             my $f = 'rcvd '.$cmd_map->{$c};
1285             if($cmd_map->{$c} eq 'R') { # save eeprom data
1286             # eepromness
1287             my($addr, $msb, $lsb) = unpack("x$len CCC", $chk);
1288             $f = 'rcvd eeprom '.$addr;
1289             }
1290             lock($data->{$f});
1291             $data->{$f} = 0;
1292             $self->_dbg("verify_send_command: flag send data->{$f} set to 0", 2);
1293             }
1294             $start += $send_commands->{$c}->{'length'};
1295             } else {
1296             $self->_dbg("verify_send_command: cmd unknown: \"".substr($cmd, $start, 2)."\"", 1);
1297             $ok = 0;
1298             last;
1299             }
1300             }
1301              
1302             return $ok;
1303             }
1304              
1305             =item I
1306              
1307             Not normally called directly.
1308              
1309             Verify a command is valid and if so, return how many bytes of it form
1310             that valid command or 0 if invalid.
1311              
1312             Will perform various tasks on the command, such as updating the stored
1313             status of things referenced in the command. When threading, it also
1314             sends a signal to indicate that new status has arrived, if relevant.
1315              
1316             =cut
1317              
1318             sub verify_recv_command {
1319             my $self = shift;
1320             my $cmd = shift;
1321              
1322             my $data = $self->{'data'};
1323             my $c = _find_cmd(substr($cmd, 0, 6), $recv_commands);
1324             if($c) {
1325             # found it!
1326             my $len = length($c);
1327             my $chk = substr($cmd, 0, $recv_commands->{$c}->{'length'});
1328             if(1 || $self->{'debug'}>1) { # !!! For some reason, not doing this causes a deadlock
1329             my $type = $recv_commands->{$c}->{'type'};
1330             my $txt = _decode_cmd($chk, $len, $type);
1331             $type = 0 if(!$type);
1332             $self->_dbg("verify_recv_command: cmd \"$c\" -> \"".
1333             $recv_commands->{$c}->{'desc'}."\"".
1334             ($txt ne ''?": $txt":""), 1);
1335             }
1336             # flag received status
1337             if($c ne 'R') { # only if not an eeprom
1338             my $f = 'rcvd '.$c;
1339             lock($data->{$f});
1340             $data->{$f} = 1;
1341             $self->_dbg("verify_recv_command: flag rcvd data->{$f} = 1", 2);
1342             $data->{'rcvdcmd '.$c} = $chk; # store whole rcvd cmd too
1343             cond_signal($data->{$f});
1344             }
1345              
1346             if(defined($set_map->{$c})) { # save new status
1347             my $k = $set_map->{$c};
1348             if($data->{'changed '.$k}) {
1349             # we have a pending write on the same value - flush all pending writes!
1350             # This does mean that, at least temporarily, we might not reflect our
1351             # written state correctly, but it will recover on a subsequent read.
1352             # We don't update the timestamp in this case, to encourage a faster
1353             # refresh.
1354             $self->indirect_write_send;
1355             } else {
1356             $data->{'ts '.$k} = Time::HiRes::time();
1357             }
1358             $data->{'prev '.$k} = $data->{$k};
1359             $data->{$k} = unpack("x$len C", $cmd);
1360             $self->_dbg("verify_recv_command: set_map \"$c\" ($k) = ".
1361             sprintf("%02.2x", $data->{$k}), 2);
1362             my $fn = $self->{'async_status_sub'};
1363             if(defined($fn) && ref($fn) eq 'CODE') {
1364             # call the handler
1365             $self->_dbg("verify_recv_command: calling async handler", 2);
1366             &$fn($data, $k, $data->{$k}, $data->{'prev '.$k});
1367             }
1368             } elsif($c eq 'R') { # save eeprom data
1369             # eepromness
1370             my($addr, $msb, $lsb) = unpack("x$len CCC", $chk);
1371             $data->{'eeprom '.$addr} = ($msb * 256) + $lsb;
1372             $self->_dbg("verify_recv_command: eeprom \"$c\" addr $addr = ".
1373             sprintf("%02.2x %02.2x", $msb, $lsb), 2);
1374              
1375             my $f = 'rcvd eeprom '.$addr;
1376             lock($data->{$f});
1377             $data->{$f} = 1;
1378             $self->_dbg("verify_recv_command: flag rcvd data->{$f} set to 1", 2);
1379             $data->{'rcvdcmd '.$c} = $chk; # store whole rcvd cmd too
1380             cond_signal($data->{$f});
1381             }
1382             return $recv_commands->{$c}->{'length'};
1383             }
1384              
1385             $self->_dbg("verify_recv_command: cmd unknown: \"".substr($cmd, 0, 2)."\"", 0);
1386              
1387             return 0;
1388             }
1389              
1390             # =============================================================================
1391              
1392             =item I
1393              
1394             Send a packet to the EtherIO24 unit. Passes it through verify_send_command
1395             and then to send_pkt.
1396              
1397             Returns 0 on failure, or the result of send_pkt otherwise.
1398              
1399             =cut
1400              
1401             sub send_command {
1402             my $self = shift;
1403             my $cmd = shift;
1404              
1405             return 0 if(!verify_send_command($self, $cmd));
1406              
1407             return send_pkt($self, $cmd);
1408             }
1409              
1410              
1411             =item I
1412              
1413             Not normally called directly. See C instead.
1414              
1415             Wait for a packet from the EtherIO24 unit. Returns an array of received commands
1416             upto any point where an invalid command was found in the input. Is NOT
1417             thread-friendly unless used in a particular way!
1418              
1419             Received packets are passed through verify_recv_command to parse into
1420             commands and perform any automatic processing on them.
1421              
1422             =cut
1423              
1424             sub recv_command {
1425             my $self = shift;
1426              
1427             my $data = $self->{'data'};
1428             my $cmds = $self->recv_pkt;
1429             if(!$cmds) {
1430             return 0;
1431             }
1432              
1433             my @cmds = ();
1434             while(length($cmds)) {
1435             my $len = verify_recv_command($self, $cmds);
1436             if(!$len) {
1437             $self->_dbg("recv_command encountered invalid command. Returning ".
1438             scalar(@cmds)." commands to caller.", 0);
1439             last;
1440             }
1441             push(@cmds, substr($cmds, 0, $len));
1442             $cmds = substr($cmds, $len);
1443             }
1444             $_error = 0;
1445             return @cmds;
1446             }
1447              
1448             =item I
1449              
1450             Wait for results to arrive. May happen sync or async. Is thread-friendly.
1451              
1452             Need to give $cmd for threaded operation so it knows what result in particular to wait for.
1453             Returns undef if there was a problem doing this, such as a timeout waiting for the reply.
1454              
1455             Replies with the reply command as received, except when it's an eeprom command and threads
1456             are being used. In this case the actual command returned is an indeterminate recent eeprom
1457             related reply.
1458              
1459             =cut
1460              
1461             sub recv_result {
1462             my $self = shift;
1463             my $cmd = shift; # what we wait for
1464              
1465             my $data = $self->{'data'};
1466             if($self->{'threaded'}) {
1467             # wait for our result to arrive
1468             my $c = _find_cmd($cmd, $send_commands);
1469             $c = _find_cmd($cmd, $recv_commands) if(!$c);
1470             $c = $cmd if(!$c);
1471              
1472             if($cmd_map->{$c}) {
1473             $c = $cmd_map->{$c};
1474             }
1475             my $f = 'rcvd '.$c;
1476             if($c eq 'R') {
1477             my $len = length(_find_cmd($cmd, $send_commands));
1478             my($addr, $msb, $lsb) = unpack("x$len CCC", $cmd);
1479             $f = 'rcvd eeprom '.$addr;
1480             }
1481              
1482             my $timeout = time() + $self->{'recv_timeout'};
1483             lock($data->{$f});
1484             while(!$data->{$f}) {
1485             $self->_dbg("recv_result: flag check data->{$f} = ".$data->{$f}, 2);
1486             last if(!cond_timedwait($data->{$f}, $timeout));
1487             }
1488             $self->_dbg("recv_result: flag result data->{$f} = ".$data->{$f}, 2);
1489             if(!$data->{$f}) {
1490             $_error = 'Timeout waiting for reply';
1491             return undef;
1492             }
1493             if(!defined($data->{'rcvdcmd '.$c})) {
1494             # eek, strange error
1495             $_error = 'Data not delivered to main thread.';
1496             return 0;
1497             }
1498             $_error = 0;
1499             return $data->{'rcvdcmd '.$c};
1500             }
1501              
1502             # Go-do in realtime
1503             return recv_command($self);
1504             }
1505              
1506             # =============================================================================
1507              
1508             =item I
1509              
1510             Reads the given eeprom locations. Always waits for the answer.
1511              
1512             Returns the count of locations sucessfuly read or 0 on error.
1513              
1514             =cut
1515              
1516             sub read_eeprom {
1517             my $self = shift;
1518              
1519             my $count = 0;
1520             while(@_) {
1521             my $addr = shift;
1522             my $cmd = "'R".pack("CCC", $addr, 0, 0);
1523             my $retries = $self->{'eeprom_read_retries'};
1524             while($retries) {
1525             if(!send_command($self, $cmd)) {
1526             $self->_dbg("WARNING: Unable to send eeprom read request for location $addr.", 0);
1527             next;
1528             } else {
1529             if(!recv_result($self, $cmd)) {
1530             $retries--;
1531             if(!$retries) {
1532             $self->_dbg("ERROR: Timeout waiting for eeprom reply for location $addr.", 0);
1533             } else {
1534             $self->_dbg("WARNING: Timeout waiting for eeprom reply for location $addr. Retrying.", 1);
1535             }
1536             next;
1537             } else {
1538             $count++;
1539             last;
1540             }
1541             }
1542             }
1543             }
1544             return $count;
1545             }
1546              
1547             =item I
1548              
1549             Write the contents of our local eeprom cache for the given index(es) to the
1550             Elexol device.
1551              
1552             It includes a 100ms delay after each write in order to let the eeprom settle.
1553              
1554             =cut
1555              
1556             sub write_eeprom {
1557             my $self = shift;
1558             my $data = $self->{'data'};
1559              
1560             while(@_) {
1561             my $index = shift;
1562              
1563             my $lsb = $data->{'eeprom '.$index} & 0xff;
1564             my $msb = ($data->{'eeprom '.$index} >> 8) & 0xff;
1565              
1566             $self->send_command("'W".pack('C*', $index, $msb, $lsb));
1567              
1568             Time::HiRes::usleep(100000); # let it settle
1569             }
1570             }
1571              
1572             =item I
1573              
1574             Enables or disables the "write" flag for the eeprom on the Elexol device.
1575              
1576             =cut
1577              
1578             sub eeprom_write_enable {
1579             my $self = shift;
1580             my $enable = shift;
1581              
1582             if($enable) {
1583             $self->_dbg("Sending eeprom write enable...", 2);
1584             return $self->send_command("'1" . pack("C*", 0x00, 0xaa, 0x55)); # write enable
1585             } else {
1586             $self->_dbg("Sending eeprom write disable...", 2);
1587             return $self->send_command("'0" . pack("C*", 0x00, 0x00, 0x00)); # write disable
1588             }
1589             }
1590              
1591             # =============================================================================
1592              
1593             =item I
1594              
1595             Send a packet over the socket. Not normally called directly.
1596              
1597             =cut
1598              
1599             sub send_pkt {
1600             my $self = shift;
1601             my $pkt = shift;
1602              
1603             my $socket = $self->{'socket'};
1604             $self->_dbg("send_pkt: Sending ".length($pkt)." bytes", 1);
1605             $self->_dbg_packet($pkt, 3);
1606             my $ret = $socket->send($pkt);
1607             if(!defined($ret) || $ret<=0) {
1608             $self->_dbg("send_pkt: Unable to send packet: $!", 0);
1609             return 0;
1610             }
1611             return 1;
1612             }
1613              
1614             =item I
1615              
1616             Wait for a packet to come in. Not normally called directly.
1617              
1618             =cut
1619              
1620             sub recv_pkt {
1621             my $self = shift;
1622              
1623             my $data = $self->{'data'};
1624             my $socket = $self->{'socket'};
1625              
1626             # see if anything waits for us
1627             my @ready = ();
1628             my $timeout = $self->{'service_recv_timeout'};
1629             my $sel = new IO::Select($socket);
1630             @ready = $sel->can_read($timeout);
1631              
1632             foreach my $fh (@ready) {
1633             if($fh = $socket) {
1634             # get packet
1635             my $pkt;
1636             if(!defined($socket->recv($pkt, 8192))) {
1637             $_error = "Unable to receive packet: $!";
1638             $self->_dbg("recv_pkt: Unable to receive packet: $!", 0);
1639             return 0;
1640             }
1641             $self->_dbg("recv_pkt: Received ".length($pkt)." bytes", 1);
1642             $self->_dbg_packet($pkt, 3);
1643             $_error = 0;
1644             return $pkt;
1645             } else {
1646             # some other socket issue perhaps
1647             }
1648             }
1649             return 0;
1650             }
1651              
1652             # =============================================================================
1653              
1654             sub _getgrp {
1655             my $line = shift;
1656              
1657             my $grp;
1658             $grp = "A" if($line >= 0 && $line < 8);
1659             $grp = "B" if($line >= 8 && $line < 16);
1660             $grp = "C" if($line >= 16 && $line < 24);
1661             my $bit = $line % 8;
1662              
1663             return ($grp, $bit, (1 << $bit));
1664             }
1665              
1666             # =============================================================================
1667              
1668             =item I
1669              
1670             Restarts the module. Needed to make any eeprom changes take affect.
1671              
1672             =cut
1673              
1674             sub reboot {
1675             my $self = shift;
1676              
1677             $self->indirect_write_send;
1678              
1679             return $self->send_command("'@".pack('C*', 0x00, 0xaa, 0x55));
1680             }
1681              
1682             sub _chkts {
1683             my $self = shift;
1684             my $item = shift;
1685              
1686             # Check the timestamp for an item and if in need of a refresh, go
1687             # refresh it and wait for the result.
1688              
1689             my $data = $self->{'data'};
1690              
1691             my $ts = $data->{'ts '.$item} + $self->{'indirect_read_interval'};
1692             my $now = Time::HiRes::time();
1693             if($self->{'direct_reads'} || ($ts < $now)) {
1694             if($self->{'debug'}>3) {
1695             if($self->{'direct_reads'}) {
1696             $self->_dbg("_chts: direct_reads, fetching data...", 3);
1697             } else {
1698             $self->_dbg("_chkts: ts for '$item' (ts=$ts now=$now iv=".$self->{'indirect_read_interval'}.") expired, fetching...", 3);
1699             }
1700             }
1701             my $cmd = $status_commands->{$item};
1702             send_command($self, $cmd);
1703             recv_result($self, $cmd);
1704             return 1;
1705             }
1706             return 0;
1707             }
1708              
1709             =item I
1710              
1711             Sets the line to boolean val and sends to EtherIO module.
1712              
1713             Ignored if line is (believed to be) an input.
1714              
1715             =cut
1716              
1717             sub set_line {
1718             my $self = shift;
1719             my $line = shift;
1720             my $val = shift;
1721              
1722             return undef if(!defined($line));
1723             return undef if(!defined($val));
1724             return undef if($line < 0 || $line > 23);
1725              
1726             my $data = $self->{'data'};
1727              
1728             my ($linegrp, $bitno, $bitval) = _getgrp($line);
1729              
1730             my $var;
1731             $var = "dir ".$linegrp;
1732             if(($data->{$var} & $bitval)) {
1733             $self->_dbg("set_line: line $line ignored, is input", 1);
1734             return 0;
1735             }
1736              
1737             $var = "status ".$linegrp;
1738             $self->_chkts($var) if($self->{'read_before_write'}); # read (possibly cached) data if we read_before_write
1739             if($val) {
1740             $self->_dbg("set_line: line $line set to ON", 1);
1741             $data->{$var} |= $bitval;
1742             } else {
1743             $self->_dbg("set_line: line $line set to OFF", 1);
1744             $data->{$var} &= ~$bitval;
1745             }
1746              
1747             if($self->{'direct_writes'}) {
1748             return send_command($self, $set_commands->{$var}.pack("C", $data->{$var}));
1749             } else {
1750             $data->{'changed '.$var} = 1;
1751             return 1;
1752             }
1753             }
1754              
1755             =item I
1756              
1757             Returns live boolean value of line.
1758              
1759             =cut
1760              
1761             sub get_line_live {
1762             my $self = shift;
1763             my $line = shift;
1764              
1765             return undef if(!defined($line));
1766             return undef if($line < 0 || $line > 23);
1767              
1768             my $data = $self->{'data'};
1769              
1770             my ($linegrp, $bitno, $bitval) = _getgrp($line);
1771              
1772             # get live value
1773             my $var;
1774             $var = "status ".$linegrp;
1775             send_command($self, $status_commands->{$var});
1776             recv_result($self, $status_commands->{$var});
1777              
1778             $var = "status ".$linegrp;
1779             my $val = (($data->{$var} & $bitval) != 0) + 0;
1780             $self->_dbg("get_line_live: line $line = ".($val?"ON":"OFF"), 1);
1781             return $val;
1782             }
1783              
1784             =item I
1785              
1786             Returns the value of the specified I/O line.
1787              
1788             If using direct_reads then this method always queries the device. Otherwise
1789             this method uses the cached value, unless expired (See I
1790             constructor parameter) whereupon it will query the device.
1791              
1792             =cut
1793              
1794             sub get_line {
1795             my $self = shift;
1796             my $line = shift;
1797              
1798             return undef if(!defined($line));
1799             return undef if($line < 0 || $line > 23);
1800              
1801             my $data = $self->{'data'};
1802              
1803             my ($linegrp, $bitno, $bitval) = _getgrp($line);
1804              
1805             my $var = "status ".$linegrp;
1806             $self->_chkts($var); # check timestamp
1807             my $val = (($data->{$var} & $bitval) != 0) + 0;
1808             $self->_dbg("get_line: line $line = ".($val?"ON":"OFF"), 1);
1809             return $val;
1810             }
1811              
1812             =item I
1813              
1814             Set line direction. 0 = output, 1 = input.
1815              
1816             =cut
1817              
1818             sub set_line_dir {
1819             my $self = shift;
1820             my $line = shift;
1821             my $dir = shift;
1822              
1823             return undef if(!defined($line));
1824             return undef if($line < 0 || $line > 23);
1825              
1826             my $data = $self->{'data'};
1827              
1828             my ($linegrp, $bitno, $bitval) = _getgrp($line);
1829              
1830             my $var;
1831             $var = "dir ".$linegrp;
1832             $self->_chkts($var) if($self->{'read_before_write'}); # read (possibly cached) data if we read_before_write
1833             if($dir) {
1834             $self->_dbg("set_line_dir: line $line set to ON", 1);
1835             $data->{$var} |= $bitval;
1836             } else {
1837             $self->_dbg("set_line_dir: line $line set to OFF", 1);
1838             $data->{$var} &= ~$bitval;
1839             }
1840              
1841             if($self->{'direct_writes'}) {
1842             return send_command($self, $set_commands->{$var}.pack("C", $data->{$var}));
1843             } else {
1844             $data->{'changed '.$var} = 1;
1845             return 1;
1846             }
1847             }
1848              
1849             =item I
1850              
1851             Returns direction setting for $line. 0 = output, 1 = input.
1852              
1853             See I for direct_reads and cachine heuristics.
1854              
1855             =cut
1856              
1857             sub get_line_dir {
1858             my $self = shift;
1859             my $line = shift;
1860              
1861             return undef if(!defined($line));
1862             return undef if($line < 0 || $line > 23);
1863              
1864             my $data = $self->{'data'};
1865              
1866             my ($linegrp, $bitno, $bitval) = _getgrp($line);
1867              
1868             my $var;
1869             $var = "dir ".$linegrp;
1870             $self->_chkts($var); # check timestamp
1871             my $val = (($data->{$var} & $bitval) != 0) + 0;
1872             $self->_dbg("get_line_dir: line $line = ".($val?"IN":"OUT"), 1);
1873             return $val;
1874             }
1875              
1876             =item I
1877              
1878             Set input line pullup. 0 = pullup off, 1 = pullup on.
1879              
1880             =cut
1881              
1882             sub set_line_pullup {
1883             my $self = shift;
1884             my $line = shift;
1885             my $pullup = shift;
1886              
1887             return undef if(!defined($line));
1888             return undef if($line < 0 || $line > 23);
1889              
1890             my $data = $self->{'data'};
1891              
1892             my ($linegrp, $bitno, $bitval) = _getgrp($line);
1893              
1894             my $var;
1895             $var = "pullup ".$linegrp;
1896             $self->_chkts($var) if($self->{'read_before_write'}); # read (possibly cached) data if we read_before_write
1897             if($pullup) {
1898             $self->_dbg("set_line_pullup: line $line set to pullup ON", 1);
1899             $data->{$var} |= $bitval;
1900             } else {
1901             $self->_dbg("set_line_pullup: line $line set to pullup OFF", 1);
1902             $data->{$var} &= ~$bitval;
1903             }
1904              
1905             if($self->{'direct_writes'}) {
1906             return send_command($self, $set_commands->{$var}.pack("C", $data->{$var}));
1907             } else {
1908             $data->{'changed '.$var} = 1;
1909             return 1;
1910             }
1911             }
1912              
1913             =item I
1914              
1915             Returns pullup setting for $line. 0 = pullup off, 1 = pullup on.
1916              
1917             See I for direct_reads and cachine heuristics.
1918              
1919             =cut
1920              
1921             sub get_line_pullup {
1922             my $self = shift;
1923             my $line = shift;
1924              
1925             return undef if(!defined($line));
1926             return undef if($line < 0 || $line > 23);
1927              
1928             my $data = $self->{'data'};
1929              
1930             my ($linegrp, $bitno, $bitval) = _getgrp($line);
1931              
1932             my $var;
1933             $var = "pullup ".$linegrp;
1934             $self->_chkts($var); # check timestamp
1935             my $val = (($data->{$var} & $bitval) != 0) + 0;
1936             $self->_dbg("get_line_pullup: line $line = ".($val?"pullup ON":"pullup OFF"), 1);
1937             return $val;
1938             }
1939              
1940             =item I
1941              
1942             Set line threshhold. 0 = 2.5v (TTL), 1 = 1.4v (CMOS).
1943              
1944             =cut
1945              
1946             sub set_line_thresh {
1947             my $self = shift;
1948             my $line = shift;
1949             my $thresh = shift;
1950              
1951             return undef if(!defined($line));
1952             return undef if($line < 0 || $line > 23);
1953              
1954             my $data = $self->{'data'};
1955              
1956             my ($linegrp, $bitno, $bitval) = _getgrp($line);
1957              
1958             my $var;
1959             $var = "thresh ".$linegrp;
1960             $self->_chkts($var) if($self->{'read_before_write'}); # read (possibly cached) data if we read_before_write
1961             if($thresh) {
1962             $self->_dbg("set_line_thresh: line $line set to 1.4v (CMOS)", 1);
1963             $data->{$var} |= $bitval;
1964             } else {
1965             $self->_dbg("set_line_thresh: line $line set to 2.5v (TTL)", 1);
1966             $data->{$var} &= ~$bitval;
1967             }
1968              
1969             if($self->{'direct_writes'}) {
1970             return send_command($self, $set_commands->{$var}.pack("C", $data->{$var}));
1971             } else {
1972             $data->{'changed '.$var} = 1;
1973             return 1;
1974             }
1975             }
1976              
1977             =item I
1978              
1979             Returns threshold setting for $line. 0 = 2.5v (TTL), 1 = 1.4v (CMOS).
1980              
1981             See I for direct_reads and cachine heuristics.
1982              
1983             =cut
1984              
1985             sub get_line_thresh {
1986             my $self = shift;
1987             my $line = shift;
1988              
1989             return undef if(!defined($line));
1990             return undef if($line < 0 || $line > 23);
1991              
1992             my $data = $self->{'data'};
1993              
1994             my ($linegrp, $bitno, $bitval) = _getgrp($line);
1995              
1996             my $var;
1997             $var = "thresh ".$linegrp;
1998             $self->_chkts($var); # check timestamp
1999             my $val = (($data->{$var} & $bitval) != 0) + 0;
2000             $self->_dbg("get_line_thresh: line $line = ".($val?"1.4v (CMOS)":"2.5v (TTL)"), 1);
2001             return $val;
2002             }
2003              
2004             =item I
2005              
2006             Set line Schmitt trigger. 0 = off, 1 = on.
2007              
2008             =cut
2009              
2010             sub set_line_schmitt {
2011             my $self = shift;
2012             my $line = shift;
2013             my $schmitt = shift;
2014              
2015             return undef if(!defined($line));
2016             return undef if($line < 0 || $line > 23);
2017              
2018             my $data = $self->{'data'};
2019              
2020             my ($linegrp, $bitno, $bitval) = _getgrp($line);
2021              
2022             my $var;
2023             $var = "schmitt ".$linegrp;
2024             $self->_chkts($var) if($self->{'read_before_write'}); # read (possibly cached) data if we read_before_write
2025             if($schmitt) {
2026             $self->_dbg("set_line_schmitt: line $line set to ON", 1);
2027             $data->{$var} |= $bitval;
2028             } else {
2029             $self->_dbg("set_line_schmitt: line $line set to OFF", 1);
2030             $data->{$var} &= ~$bitval;
2031             }
2032              
2033             if($self->{'direct_writes'}) {
2034             return send_command($self, $set_commands->{$var}.pack("C", $data->{$var}));
2035             } else {
2036             $data->{'changed '.$var} = 1;
2037             return 1;
2038             }
2039             }
2040              
2041             =item I
2042              
2043             Returns schmitt setting for $line. 0 = off, 1 = on.
2044              
2045             See I for direct_reads and cachine heuristics.
2046              
2047             =cut
2048              
2049             sub get_line_schmitt {
2050             my $self = shift;
2051             my $line = shift;
2052              
2053             return undef if(!defined($line));
2054             return undef if($line < 0 || $line > 23);
2055              
2056             my $data = $self->{'data'};
2057              
2058             my ($linegrp, $bitno, $bitval) = _getgrp($line);
2059              
2060             my $var;
2061             $var = "schmitt ".$linegrp;
2062             $self->_chkts($var); # check timestamp
2063             my $val = (($data->{$var} & $bitval) != 0) + 0;
2064             $self->_dbg("get_line_schmitt: line $line = ".($val?"IN":"OUT"), 1);
2065             return $val;
2066             }
2067              
2068             =item I
2069              
2070             Programs an IP address to use for autoscan functions.
2071              
2072             $addr is an ASCII string representation of an IP address.
2073             $port is a numeric UDP port number.
2074              
2075             If not specified, it will attempt to determine the current
2076             IP address and port of the open UDP socket. This may not be
2077             wholly portable and your mileage may vary. Unix-like platforms
2078             should fare best.
2079              
2080             If $addr is a numeric 0 then the autoscan function will be disabled
2081             on the module.
2082              
2083             Changes made by this function require a module restart to take effect.
2084              
2085             Before making changes to the eeprom, this method always reads in the
2086             current value first.
2087              
2088             =cut
2089              
2090             sub set_autoscan_addr {
2091             my $self = shift;
2092             my $addr = shift;
2093             my $port = shift;
2094              
2095             # Bit 2 of eeprom word 5 controls autoscan enable
2096             # Words 22,23 are the autoscan ip addr
2097             # Word 24 is the autoscan udp port
2098              
2099             my $data = $self->{'data'};
2100              
2101             if(defined($addr) && $addr eq '0') {
2102             $self->read_eeprom(5); # refresh, just in case
2103             $data->{'eeprom 5'} |= 4; # add bit 4 to disable autoscan
2104             $self->write_eeprom(5);
2105              
2106             Time::HiRes::usleep(500000); # let the eeprom settle
2107              
2108             $self->read_eeprom(5); # refresh, one more time
2109              
2110             } else {
2111              
2112             my $sockaddr = $self->{'socket'}->sockname;
2113             my @s = sockaddr_in($sockaddr);
2114             if(!$addr) {
2115             $addr = inet_ntoa($s[1]);
2116             }
2117             if(!$port) {
2118             $port = $s[0];
2119             }
2120              
2121             $self->_dbg("set_autoscan_addr: set addr to $addr:$port", 1);
2122              
2123              
2124             $self->eeprom_write_enable(1);
2125              
2126             my @a = split(/\./, $addr, 4);
2127              
2128             $data->{'eeprom 22'} = ($a[1] << 8) | $a[0];
2129             $data->{'eeprom 23'} = ($a[3] << 8) | $a[2];
2130             $data->{'eeprom 24'} = $port;
2131              
2132             my ($w22, $w23, $w24) = ($data->{'eeprom 22'}, $data->{'eeprom 23'}, $data->{'eeprom 24'}); # keep a copy
2133              
2134             $data->{'eeprom 18'} = 4; # 125 scans per second (1000 / 4)
2135              
2136             $self->write_eeprom(18, 22, 23, 24); # write these values out
2137              
2138             $self->read_eeprom(22, 23, 24); # read it back in
2139              
2140             # TODO: verify the written values made it in.
2141              
2142             $self->read_eeprom(5); # refresh, just in case
2143             $data->{'eeprom 5'} &= ~4; # subtract bit 4 to enable autoscan
2144             $self->write_eeprom(5);
2145              
2146             $self->read_eeprom(5); # refresh, one more time
2147              
2148             # TODO: verify the flag was set
2149              
2150             $self->eeprom_write_enable(0);
2151             }
2152             }
2153              
2154             =item I $state, ...)>
2155              
2156             Sets the autoscan state of the given lines to the given state.
2157              
2158             Where $state = 1, the module will send status changes for $line.
2159              
2160             Changes made by this function require a module restart to take effect.
2161              
2162             =cut
2163              
2164             sub set_autoscan_lines {
2165             my $self = shift;
2166              
2167             return 0 if(!@_);
2168              
2169             my %args = @_;
2170              
2171             my $data = $self->{'data'};
2172              
2173             $self->read_eeprom(16, 17); # get a fresh copy
2174             foreach my $line (keys %args) {
2175             my $state = $args{$line};
2176              
2177             my $bit = $line % 16;
2178             my $mask = 1 << $bit;
2179              
2180             my $addr;
2181             $addr = 16 if($line >= 0 && $line <= 15);
2182             $addr = 17 if($line >= 16 && $line <= 23);
2183              
2184             my $val = $data->{'eeprom '.$addr};
2185             if($state) {
2186             $val &= ~$mask;
2187             } else {
2188             $val |= $mask;
2189             }
2190             $data->{'eeprom '.$addr} = $val;
2191             }
2192             $self->write_eeprom(16, 17); # save new version
2193              
2194             return 1;
2195             }
2196              
2197             =item I
2198              
2199             If $state is 1, uses the current status to set the startup status. Details such
2200             as line direction, trigger levels, etc are programmed into the
2201             eeprom. Status from the module is fetched prior to setting
2202             the startup status if not pre-fetched at object creation.
2203              
2204             Otherwise, disables startup port status setting.
2205              
2206             Changes made by this function require a module restart to take effect.
2207              
2208             =cut
2209              
2210             sub set_startup_status {
2211             my $self = shift;
2212             my $state = shift || 1;
2213              
2214             my $data = $self->{'data'};
2215              
2216             if(!$self->{'prefetch_status'}) {
2217             $self->status_fetch(1); # 1=wait for response
2218             $self->eeprom_fetch(1); # 1=wait for response
2219             }
2220              
2221             my $fields = {
2222             8 => [ 'status A', 'dir A' ],
2223             9 => [ 'pullup A', 'thresh A' ],
2224             10 => [ 'dir B', 'schmitt A' ],
2225             11 => [ 'thresh B', 'status B' ],
2226             12 => [ 'schmitt B', 'pullup B' ],
2227             13 => [ 'status C', 'dir C' ],
2228             14 => [ 'pullup C', 'thresh C' ],
2229             15 => [ 0, 'schmitt C' ],
2230             };
2231              
2232              
2233             $self->read_eeprom(5); # ensure a fresh copy
2234              
2235             if($state) {
2236             $data->{'eeprom 5'} &= ~2; # subtract bit 1 (value 2) to enable port preset
2237              
2238             foreach my $field (sort { $a <=> $b } keys %$fields) {
2239             my $arr = $fields->{$field};
2240             $data->{'eeprom '.$field} = ((@$arr[0]?$data->{@$arr[0]}:0) * 256) + (@$arr[1]?$data->{@$arr[1]}:0);
2241             }
2242             } else {
2243             $data->{'eeprom 5'} |= 2; # add bit 1 (value 2) to enable port preset
2244              
2245             foreach my $field (sort { $a <=> $b } keys %$fields) {
2246             $data->{'eeprom '.$field} = 0xffff;
2247             }
2248             }
2249              
2250             $self->eeprom_write_enable(1);
2251             $self->write_eeprom(5, sort keys %$fields);
2252             $self->eeprom_write_enable(0);
2253              
2254             return 1;
2255             }
2256              
2257             =back
2258              
2259             =head1 NOTE
2260              
2261             The author is not in any way affiliated with Elexol, the manufacturer of the device
2262             this Perl module is designed to operate. This module has been developed using only
2263             data available in the public domain.
2264              
2265             =head1 SEE ALSO
2266              
2267             L, L, L
2268              
2269             =head1 AUTHOR
2270              
2271             Chris Luke C<< >>
2272              
2273             =head1 BUGS
2274              
2275             Please report any bugs or feature requests to
2276             C, or through the web interface at
2277             L.
2278             I will be notified, and then you'll automatically be notified of progress on
2279             your bug as I make changes.
2280              
2281             =head1 ACKNOWLEDGEMENTS
2282              
2283             =head1 COPYRIGHT & LICENSE
2284              
2285             Copyright 2005..2008 Chris Luke, all rights reserved.
2286              
2287             This program is free software; you can redistribute it and/or modify it
2288             under the same terms as Perl itself.
2289              
2290             =cut
2291              
2292             1; # End of Net::Elexol::EtherIO24
2293