File Coverage

blib/lib/Ekahau/Base.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Ekahau::Base;
2             our $VERSION = '0.001';
3              
4             # Written by Scott Gifford
5             # Copyright (C) 2004 The Regents of the University of Michigan.
6             # See the file LICENSE included with the distribution for license
7             # information.
8              
9 6     6   64773 use warnings;
  6         14  
  6         364  
10 6     6   33 use strict;
  6         11  
  6         203  
11 6     6   18363 use bytes; # Avoid Unicode crap
  6         205  
  6         37  
12              
13 6     6   202 use base 'Ekahau::ErrHandler';
  6         17  
  6         4564  
14              
15             our $_global_last_error;
16              
17 6     6   54 use constant DEFAULT_PORT => 8548;
  6         22  
  6         348  
18 6     6   35 use constant DEFAULT_HOST => 'localhost';
  6         11  
  6         289  
19 6     6   14140 use constant READ_BLOCKSIZE => 8192;
  6         16  
  6         463  
20              
21             =head1 NAME
22              
23             Ekahau::Base - Low-level interface to Ekahau location sensing system
24              
25             =head1 SYNOPSIS
26              
27             The C class provides a low-level interface to the Ekahau
28             location sensing system's YAX protocol. In general you don't want to
29             use this class directly; instead the subclasses L and
30             L provide a nicer interface.
31              
32             =head1 DESCRIPTION
33              
34             This class implements methods for querying the Ekahau Positioning
35             Engine, and processing the responses. Each object represents a
36             connection to the Ekahau server. Some methods send queries to the
37             server, while others receive responses. Continuous queries generate
38             data until they are asked to stop, so the protocol is not strictly
39             request-response. To deal with this, queries can have a "tag"
40             associated with them, which allows the response to that specific
41             command to be identified.
42              
43             =cut
44              
45 6     6   4587 use Ekahau::Response;
  6         21  
  6         256  
46 6     6   5344 use Ekahau::License;
  0            
  0            
47              
48             use IO::Socket::INET;
49             use IO::Select;
50              
51             =head2 Constructor
52              
53             =head3 new ( [ %params ] )
54              
55             The C constructor creates a new Ekahau object. It takes a series
56             of parameters as arguments, in the C Value> style. The
57             following parameters are recognized:
58              
59             =over 4
60              
61             =item Timeout
62              
63             The maximum length of time to wait for a response or connection.
64              
65             =item PeerAddr
66              
67             The name or IP address of the Ekahau server you'd like to communicate
68             with. This is passed along to the L module, and you
69             can use the alias C if you prefer. It defaults to C.
70              
71             =item PeerPort
72              
73             The TCP port where the Ekahau server you'd like to communicate with is
74             running. It defaults to C<8548>.
75              
76             =item Password
77              
78             The password to talk to the Ekahau server. The default password is
79             C, which is what the server will use if you haven't configured
80             a password.
81              
82             =item LicenseFile
83              
84             The XML file containing your Ekahau license. If you don't specify a
85             C, and anonymous connection will be used, which may be
86             limited by the software.
87              
88             =back
89              
90             =cut
91              
92             sub new
93             {
94             my $class = shift;
95             my(%p) = @_;
96            
97             my $self = {};
98             bless $self,$class;
99             $self->{_errhandler} = Ekahau::ErrHandler->errhandler_new($class,%p);
100            
101             $self->{tag} = 0;
102             $self->{_readbuf} = "";
103             $self->{_timeout}=$p{Timeout}||$p{timeout};
104              
105             $self->_connect(%p)
106             or return undef;
107             $self->_start(%p)
108             or return undef;
109              
110             $self->errhandler_constructed();
111             }
112              
113             sub ERROBJ
114             {
115             my $self = shift;
116             $self->{_errhandler};
117             }
118              
119             # Connect to the TCP socket
120             sub _connect
121             {
122             my $self = shift;
123             my(%p)=@_;
124             my $sock;
125              
126             if ($p{Socket})
127             {
128             $sock = $p{Socket};
129             }
130             else
131             {
132             # For IO::Socket::INET
133             if ($p{timeout} && !$p{Timeout})
134             {
135             $p{Timeout}=$p{timeout};
136             }
137             elsif ($self->{_timeout})
138             {
139             $p{Timeout} = $self->{_timeout};
140             }
141            
142             if (!$p{PeerPort}) { $p{PeerPort} = DEFAULT_PORT };
143             if (!$p{PeerAddr} and !$p{PeerHost}) { $p{PeerAddr} = DEFAULT_HOST };
144            
145             warn "DEBUG Connecting to $p{PeerAddr}:$p{PeerPort}...\n"
146             if ($ENV{VERBOSE});
147             $sock = IO::Socket::INET->new(%p,
148             Proto => 'tcp')
149             or return $self->reterr("Couldn't create IO::Socket::INET - $!");
150             }
151              
152             $self->{_sock} = $sock;
153             binmode $self->{_sock};
154             $self->{_sock}->autoflush(1);
155             $self->{_socksel} = IO::Select->new($self->{_sock})
156             or return $self->reterr("Couldn't create IO::Select - $!");
157              
158             warn "DEBUG connected.\n"
159             if ($ENV{VERBOSE});
160              
161             1;
162             }
163              
164             # Start the YAX protocol, and authenticate with our license
165             # or anonymously
166             sub _start
167             {
168             my $self = shift;
169             my $talkresp;
170             my(%p)=@_;
171              
172             $p{Password} ||= $p{password};
173             if (!defined($p{Password})) { $p{Password}="Llama" };
174              
175             my $hello_resp = $self->nextresponse;
176              
177             my $talk_str = '';
178             my($lic,$randstr);
179             if ($p{LicenseFile})
180             {
181             # Make up a random string real quick.
182             # This isn't cryptographically secure, but who cares?
183             $randstr = sprintf "%02x"x8, map { int(rand(256)) } 1..8;
184             # Read in the license file
185             eval {
186             $lic = Ekahau::License->new(LicenseFile => $p{LicenseFile})
187             or return $self->reterr("Error processing LicenseFile '$p{LicenseFile}': " . Ekahau::License->lasterr);
188             };
189             $@ and return $self->reterr("Error creating Ekahau::License object - $@");
190              
191             $self->command(['HELLO',1,'"'.$randstr.'"',$lic->hello_str])
192             or return undef;
193             $talk_str = $lic->talk_str(Password => $p{Password}, HelloStr => $hello_resp->{args}[1])
194             or return $self->reterr("Error getting talk string from LicenseFile '$p{LicenseFile}': ".$lic->lasterr);
195             }
196             else
197             {
198             # No license file, log in anonymously
199             $self->command(['HELLO',1,'""',"password=$p{Password}"])
200             or return undef;
201             }
202             $self->command(['TALK','yax',1,'yax1','MD5','"'.$talk_str.'"'])
203             or return undef;
204             $talkresp = $self->nextresponse
205             or return undef;
206             if ($talkresp->error)
207             {
208             return $self->reterr("Couldn't initiate session with Ekahau: ".$talkresp->error_description)
209             }
210             elsif ($talkresp->{cmd} ne 'TALK')
211             {
212             return $self->reterr("Couldn't initiate session with Ekahau: Unexpected response $talkresp->{string}");
213             }
214              
215             if ($talkresp->{args}[0] !~ /^"?yax"?$/i)
216             {
217             return $self->reterr("Server is speaking unknown protocol '$talkresp->{args}[0]'");
218             }
219             if ($talkresp->{args}[3] !~ /^"?MD5"?/i)
220             {
221             return $self->reterr("Server is using unknown checksum '$talkresp->{args}[3]'");
222             }
223            
224             if ($p{LicenseFile})
225             {
226             my $server_talk_str = $lic->talk_str(Password => $p{Password}, HelloStr => $randstr)
227             or $self->reterr("Error getting server talk string from LicenseFile: ".$lic->lasterr);
228             if ($server_talk_str ne $talkresp->{args}[4])
229             {
230             return $self->reterr("Server gave invalid checksum");
231             }
232             }
233             1;
234             }
235              
236             # Read a response, taking it from the read buffer if a full response
237             # is available, and otherwise reading from the network.
238             sub _readresponse
239             {
240             my $self = shift;
241             my $r;
242              
243             while (1)
244             {
245             if ($r = $self->readpending) { last };
246             if ($self->can_read($self->{_timeout}))
247             {
248             $self->readsome();
249             }
250             else
251             {
252             return '';
253             }
254             }
255             $r;
256             }
257              
258             sub _set_errhandler
259             {
260             my $self = shift;
261             my($eh)=@_;
262             if ($eh)
263             {
264             $self->{_lasterror}=$eh;
265             }
266             else
267             {
268             $self->{_lasterror} = \$_global_last_error
269             }
270             $self->reterr('no error yet');
271             1;
272             }
273              
274             =head2 Methods
275              
276             =head3 close ( )
277              
278             Properly shut down the connection to the Ekahau engine, by sending a
279             C command then closing the socket.
280              
281             =cut
282              
283             sub close
284             {
285             my $self = shift;
286             $self->command('CLOSE')
287             or return undef;
288             # It's the same as an abort from here on out.
289             $self->abort;
290             }
291              
292             =head3 abort ( )
293              
294             Abort the connection to the Ekahau engine, by closing the socket.
295              
296             =cut
297              
298             sub abort
299             {
300             my $self = shift;
301              
302             my $close_ok = 1;
303              
304             $close_ok = CORE::close($self->{_sock});
305             undef $self->{_sock};
306             undef $self->{_socksel};
307            
308             $close_ok or return $self->reterr("Error closing socket: $!\n");
309             1;
310             }
311              
312             =head3 readsome ( )
313              
314             Read some data from the network into the read buffer. This is the
315             buffer where L gets pending events from. This call
316             blocks, so if you don't want to wait for events, you should either
317             C
318             call the L method to determine if data is available to
319             read.
320              
321             =cut
322              
323             sub readsome
324             {
325             my $self = shift;
326             my $sock = $self->{_sock};
327            
328             sysread($sock,$self->{_readbuf},READ_BLOCKSIZE,length($self->{_readbuf}))
329             or return $self->reterr("Error reading from socket: $!\n");
330             }
331              
332             =head3 getpending ( )
333              
334             Returns the next pending event, or C if no events are pending.
335             The event returned is an L object.
336              
337             Pending events come from the buffer filled by L.
338              
339             =cut
340              
341             sub getpending
342             {
343             my $self = shift;
344             my $resp_txt = $self->_readpending()
345             or return undef;
346             return Ekahau::Response->parsenew($resp_txt);
347             }
348              
349             sub _readpending
350             {
351             my $self = shift;
352              
353             if ($self->{_readbuf} =~ /^(\s*<.*?(?)>\s*)/s)
354             {
355             my $msg = $1;
356             # Is this an object with a size parameter?
357             if ($self->{_readbuf} =~ /^\s*<[^>]*\x0asize=(\d+)[^>]*\x0adata=/sg)
358             {
359             my $data_len = $1;
360             my $data_start = pos($self->{_readbuf});
361              
362             if ((length($self->{_readbuf})-$data_start) < $data_len)
363             {
364             # We don't have the whole thing.
365             # This is just a warning.
366             return $self->reterr('incomplete data response');
367             }
368             else
369             {
370             $msg = substr($self->{_readbuf}, 0,$data_start + $data_len + 3);
371             }
372             }
373             warn "READ: '$msg'\n"
374             if ($ENV{VERBOSE});
375             substr($self->{_readbuf},0,length($msg))='';
376             # Preserve taintedness with substr(X,0,0)
377             return $msg.substr($self->{_readbuf},0,0);
378             }
379             return $self->reterr('no complete response so far');
380             }
381              
382             sub nextresponse
383             {
384             my $self = shift;
385              
386             # Wait until we get something, or the timeout expires.
387             my $started = time;
388             while(1)
389             {
390             if (my $resp = $self->getpending)
391             {
392             return $resp;
393             }
394             # See if we timed out.
395             $self->can_read($self->{_timeout}? $self->{_timeout} : 0)
396             or return undef;
397            
398             $self->readsome()
399             or return undef;
400             }
401              
402             }
403              
404             =head3 can_read ( $timeout )
405              
406             Returns true if the network socket becomes readable within C<$timeout>
407             seconds; otherwise returns false.
408              
409             =cut
410              
411             sub can_read
412             {
413             my $self = shift;
414              
415             $self->{_socksel}->can_read($_[0]||$self->{_timeout})
416             or return $self->reterr("socket read timed out (probably)");
417             }
418              
419             =head3 select_handles
420              
421             Returns a list of filehandles suitable for use with C
422             you're multiplexing I/O from this module and other sources, you can
423             select these filehandles for readability, then call the L
424             method to read the available data, and finally call L in
425             a loop to get all of the pending events. Note that these handles
426             become selectable for read only when there is data on the network; if
427             multiple events come in at once (which is common), the handle will
428             become selectable once, and you'll have to retreive all of the events
429             with L; it won't be selectable again until there is more
430             data to read.
431              
432             =cut
433              
434             sub select_handles
435             {
436             my $self = shift;
437              
438             ($self->{_sock});
439             }
440              
441             =head3 request_device_list ( [ $props ] )
442              
443             Requests a list of all devices connected to the system. Returns the
444             command tag that was sent (which can be used to identify the
445             response).
446              
447             An optional hash reference can be supplied with a list of properties.
448             The special property C will be used to set the command tag if
449             given (otherwise a tag will be generated). Other properties will be
450             sent along in the Ekahau request. Properties currently recognized
451             are:
452              
453             =over 4
454              
455             =item NETWORK.MAC
456              
457             The MAC address of the device you'd like to look for, in
458             colon-seperated format. For example:
459              
460             'NETWORK.MAC' => '00:E0:63:82:65:76'
461              
462             =item NETWORK.IP-ADDRESS
463              
464              
465             The IP address of the device you'd like to look for, in dotted-quad
466             format. For example:
467              
468             'NETWORK.IP-ADDRESS' => '10.0.0.1'
469              
470             =back
471              
472              
473             =cut
474              
475             sub request_device_list
476             {
477             my $self = shift;
478             my %p = ref $_[0] ? %{ (shift) } : ();
479             my $tag = delete $p{Tag} || ++$self->{tag};
480            
481             $self->command('GET_DEVICE_LIST',\%p,$tag)
482             or return undef;
483             $tag;
484             }
485              
486             =head3 request_device_properties ( [ $props ], $device_id )
487              
488             Request the property list for device C<$device_id>.
489              
490             The first parameter can be a hash reference containing additional
491             request properties to be sent, but none are documented by Ekahau for
492             this command. The one exception is the special property C, which
493             will be used to set the command tag if given (otherwise a tag will be
494             generated).
495              
496             =cut
497              
498             sub request_device_properties
499             {
500             my $self = shift;
501             my %p = ref $_[0] ? %{ (shift) } : ();
502             my $tag = delete $p{Tag} || ++$self->{tag};
503             my($dev)=@_;
504              
505             $self->command(['GET_DEVICE_PROPERTIES', $dev],\%p,$tag)
506             or return undef;
507             $tag;
508             }
509              
510             =head3 request_location_context ( [ $props ], $area_id )
511              
512             Request information about logical area C<$location_id>.
513              
514             The first parameter can be a hash reference containing additional
515             request properties to be sent, but none are documented by Ekahau for
516             this command. The one exception is the special property C, which
517             will be used to set the command tag if given (otherwise a tag will be
518             generated).
519              
520             =cut
521              
522             sub request_location_context
523             {
524             my $self = shift;
525             my %p = ref $_[0] ? %{ (shift) } : ();
526             my $tag = delete $p{Tag} || ++$self->{tag};
527             my($c)=@_;
528              
529             $self->command(['GET_CONTEXT', $c],{},$tag)
530             or return undef;
531             $tag;
532             }
533              
534             =head3 request_map_image ( [ $props ], $area_id )
535              
536             Request a map of logical area C<$area_id>. Returns an
537             L object.
538              
539             =cut
540              
541             sub request_map_image
542             {
543             my $self = shift;
544             my %p = ref $_[0] ? %{ (shift) } : ();
545             my $tag = delete $p{Tag} || ++$self->{tag};
546             my($c)=@_;
547              
548             $self->command(['GET_MAP', $c],{},$tag)
549             or return undef;
550             $tag;
551             }
552              
553             =head3 request_all_areas ( )
554              
555             Request information about all logical areas known to the Ekahau
556             engine.
557              
558             =cut
559              
560             sub request_all_areas
561             {
562             my $self = shift;
563             my %p = ref $_[0] ? %{ (shift) } : ();
564             my $tag = delete $p{Tag} || ++$self->{tag};
565              
566             $self->command(['GET_LOGICAL_AREAS'],{},$tag)
567             or return undef;
568             $tag;
569             }
570              
571             =head3 start_location_track ( [ $properties ], $device_id )
572              
573             Ask the Ekahau engine to start sending location information about
574             device C<$device_id>. You can get responses with L.
575              
576             An optional hash reference can be supplied with a list of properties.
577             The special property C will be used to set the command tag if
578             given (otherwise a tag will be generated). Other properties will be
579             sent along in the Ekahau request. Properties currently recognized
580             are:
581              
582             =over 4
583              
584             =item EPE.WLAN_SCAN_INTERVAL
585              
586             Interval at which wireless LAN devices should scan. See documentation
587             for more information.
588              
589             =item EPE.WLAN_SCAN_MODE
590              
591             Wireless LAN scan mode. See documentation for more information.
592              
593             =item EPE.SNAP_TO_RAIL
594              
595             Set to the string C to have all locations correspond to
596             positions on tracking rails, or C to allow any location.
597              
598             =item EPE.EXPECTED_ERROR
599              
600             Set to the string C if you would like an expected error
601             estimate, or C to avoid this calculation.
602              
603             =item EPE.POSITIONING_MODE
604              
605             Set to 1 for realtime positioning, or 2 for more accurate
606             positioining.
607              
608             =item EPE.LOCATION_UPDATE_INTERVAL
609              
610             How often you'd like an update on the device's position.
611              
612             =back
613              
614             =cut
615              
616             sub start_location_track
617             {
618             my $self = shift;
619             my %p = ref $_[0] ? %{ (shift) } : ();
620             my $tag = delete $p{Tag} || ++$self->{tag};
621             my($dev) = @_;
622              
623             $self->command(['START_LOCATION_TRACK',$dev],\%p,$tag)
624             or return undef;
625             $tag;
626             }
627              
628             =head3 request_stop_location_track ( $device_id )
629              
630             Ask the Ekahau engine to stop sending location information about
631             device C<$device_id>.
632              
633             =cut
634              
635             sub request_stop_location_track
636             {
637             my $self = shift;
638             my %p = ref $_[0] ? %{ (shift) } : ();
639             my $tag = delete $p{Tag} || ++$self->{tag};
640             my($dev) = @_;
641              
642             $self->command(['STOP_LOCATION_TRACK',$dev],\%p,$tag)
643             or return undef;
644             $tag;
645             }
646              
647             =head3 stop_location_track ( $device_id )
648              
649             Alias for C.
650              
651             =cut
652              
653             sub stop_location_track
654             {
655             my $self = shift;
656             $self->request_stop_location_track(@_);
657             }
658              
659             =head3 start_area_track ( [ $properties ], $device_id )
660              
661             Ask the Ekahau engine to start sending area information about
662             device C<$device_id>. You can get responses with L.
663              
664             An optional hash reference can be supplied with a list of properties.
665             The special property C will be used to set the command tag if
666             given (otherwise a tag will be generated). Other properties will be
667             sent along in the Ekahau request. This command recognizes all of the parameters used by L, and also these:
668              
669             =over 4
670              
671             =item EPE.NUMBER_OF_AREAS
672              
673             How many areas you'd like returned with each area response. Each will
674             come with a probability that the user is in that area.
675              
676             =back
677              
678             =cut
679              
680             sub start_area_track
681             {
682             my $self = shift;
683             my %p = ref $_[0] ? %{ (shift) } : ();
684             my $dev = shift;
685             my $tag = delete $p{Tag} || ++$self->{tag};
686              
687             $self->command(['START_AREA_TRACK',$dev], \%p, $tag)
688             or return undef;
689             }
690              
691             =head3 request_stop_area_track ( $device_id )
692              
693             Ask the Ekahau engine to stop sending area information about
694             device C<$device_id>.
695              
696             =cut
697              
698             sub request_stop_area_track
699             {
700             my $self = shift;
701             my %p = ref $_[0] ? %{ (shift) } : ();
702             my $tag = delete $p{Tag} || ++$self->{tag};
703             my($dev) = @_;
704              
705             $self->command(['STOP_AREA_TRACK',$dev],\%p,$tag)
706             or return undef;
707             $tag;
708             }
709              
710             =head3 stop_area_track ( $device_id )
711              
712             Alias for C.
713              
714             =cut
715              
716             sub stop_area_track
717             {
718             my $self = shift;
719             $self->request_stop_area_track(@_);
720             }
721              
722              
723             =head3 command ( $cmd, $props, $tag )
724              
725             This is a fairly low-level routine, and shouldn't be needed in normal
726             use. It is the only way to send an arbitrary command to the YAX
727             engine, however, so it is available and documented.
728              
729             YAX commands look like this:
730              
731             <#$tag command arguments
732             property1=value1
733             property2=value2
734             ...
735             >
736              
737             For clarity, we'll call the string sent at the very beginning of first
738             line command the I, the next whitespace-seperated word the
739             I, and the remainder of the first line a space-seperated list
740             called I. Additional information on other lines we'll call
741             I.
742              
743             C<$cmd> is a list reference containing the command and arguments to
744             send. It can also be a string, which is the same as specifying a list
745             with just that string.
746              
747             C<$props> is a hash reference containing the properties to be sent
748             with the command. If it is empty or C, no properties are sent.
749              
750             C<$tag> is the command's tag, which allows the response to be picked
751             out of the data coming back from the server.
752              
753             Here are some examples:
754              
755             $self->command(['GET_DEVICE_PROPERTIES',1], {}, 'A1');
756             $self->command('GET_DEVICE_LIST',{'NETWORK.IP-ADDRESS' => '10.1.1.1'}, 'B2');
757              
758             =cut
759              
760             sub command
761             {
762             my $self = shift;
763             my($cmd,$props,$tag)=@_;
764             my $data;
765              
766             my @args;
767              
768             if ($cmd and ref($cmd) eq 'ARRAY')
769             {
770             $cmd=join(' ',map { (!defined($_) or $_ eq '') ? '""' : $_ } @$cmd);
771             }
772             if ($props and ref($props) eq 'HASH')
773             {
774             $cmd .= "\x0d\x0a";
775             while (my($key,$val)=each(%$props))
776             {
777             if ($key eq 'data')
778             {
779             # Data blob
780             $data = $val;
781             $cmd .= "size=".length($$data)."\x0d\x0a";
782             }
783             elsif (ref($val) and ref($val) eq 'ARRAY')
784             {
785             foreach my $prop2 (@$val)
786             {
787             $cmd .= $key ."\x0d\x0a";
788             $cmd .= "$_=$prop2->{$_}\x0d\x0a"
789             foreach keys %$prop2;
790             }
791             }
792             else
793             {
794             $cmd .= "$key=$val\x0d\x0a";
795             }
796             }
797             }
798             if ($data)
799             {
800             $cmd .= 'data='.$$data."\x0d\x0a";
801             }
802             $self->_sendcmd($cmd, $tag);
803             }
804              
805             sub _sendcmd
806             {
807             my $self = shift;
808             my($params,$tag) = @_;
809              
810             if (defined($tag))
811             {
812             $tag = "#$tag ";
813             }
814             else
815             {
816             $tag = '';
817             }
818             my $cmd = "<$tag$params>\x0d\x0a";
819             $self->_write($cmd);
820             }
821              
822             sub _write
823             {
824             my $self = shift;
825             my $sock = $self->{_sock};
826              
827             warn "SENT: ",join("",@_),"\n"
828             if ($ENV{VERBOSE});
829             print $sock @_
830             or return $self->reterr("socket write error: $!\n");
831             }
832              
833             =head2 lasterr ( )
834              
835             Returns the last error generated by this object, or when called as a
836             class method the last constructor error that prevented an object from
837             being created. The return value is a string describing the error,
838             suitable for display to the ser.
839              
840             =head2 Destructors
841              
842             =head3 DESTROY ( )
843              
844             When an C object is destroyed, its connection is closed
845             using the L method.
846              
847             =cut
848              
849             sub DESTROY
850             {
851             my $self = shift;
852             $self->close
853             if ($self->{_sock});
854             }
855              
856             1;
857              
858             =head2 Error Handling
859              
860             Constructors and most methods return I on error. To find out
861             details about the error, you can call the L method, which
862             will return a string. If the error happened in the constructor and so
863             you don't have an object to call a method on, call it as a class
864             method:
865              
866             my $errstr = Ekahau::Base->lasterr;
867              
868             =head1 AUTHOR
869              
870             Scott Gifford Egifford@umich.eduE, Esgifford@suspectclass.comE
871              
872             Copyright (C) 2005 The Regents of the University of Michigan.
873              
874             See the file LICENSE included with the distribution for license
875             information.
876              
877              
878             =head1 SEE ALSO
879              
880             L, I
881             Engine User Guide>, L, L, L,
882             L, L, L.
883              
884             =cut
885              
886             1;