File Coverage

blib/lib/Device/WS2500PC.pm
Criterion Covered Total %
statement 21 371 5.6
branch 0 236 0.0
condition 0 72 0.0
subroutine 7 21 33.3
pod 0 14 0.0
total 28 714 3.9


line stmt bran cond sub pod time code
1             package Device::WS2500PC;
2              
3              
4              
5             # # ****************************************************************************
6             # # *** ws2500PC, (c) 2004 by Magnus Schmidt, ws2500@27b-6.de ***
7             # # *** Library for interfacing the serial port of the WS2500PC Adapter ***
8             # # *** Produced by German Distributor ELV ***
9             # # ****************************************************************************
10             # # *** This program is free software; you can redistribute it and/or modify ***
11             # # *** it under the terms of the GNU General Public License as published by ***
12             # # *** the Free Software Foundation; either version 2 of the License, or ***
13             # # *** (at your option) any later version. ***
14             # # ****************************************************************************
15             # # *** History: 0.99 Initial release ***
16             # # *** 0.99a Bugfix in distribution ***
17             # # *** 0.99b Bugfix for reading other sensors than temp1-temp8 ***
18             # # *** ws2500_GetDatasetBulk() added ***
19             # # ****************************************************************************
20              
21              
22              
23             # ********************************************************
24             # *** Imports
25             # ********************************************************
26 1     1   13346 use strict;
  1         3  
  1         420  
27 1     1   8 use warnings;
  1         2  
  1         40  
28 1     1   6 use Carp;
  1         7  
  1         99  
29 1     1   1830 use Device::SerialPort qw(:PARAM :STAT 0.07);
  1         61263  
  1         396  
30 1     1   1216 use Time::HiRes qw (sleep);
  1         18469  
  1         9  
31 1     1   1401 use Time::Local qw(timelocal);
  1         1922  
  1         69  
32              
33              
34              
35             # ********************************************************
36             # *** Package Definition
37             # ********************************************************
38             require Exporter;
39 1     1   7 use vars qw (@EXPORT @EXPORT_OK @ISA);
  1         2  
  1         5973  
40             @ISA = qw (Exporter);
41             @EXPORT = qw (ws2500_GetTime ws2500_GetStatus ws2500_GetDataset ws2500_NextDataset);
42             @EXPORT_OK = qw (ws2500_FirstDataset ws2500_SetDebug ws2500_InterfaceInit ws2500_GetDatasetBulk);
43            
44              
45              
46             # ********************************************************
47             # *** Prototypes and global variables
48             # ********************************************************
49             sub printhex ($);
50             sub send_Command;
51             sub read_Response ($;$);
52             sub init_Interface ($);
53             sub close_Interface ();
54             sub ws2500_GetTime ($;$);
55             sub ws2500_GetStatus ($;$);
56             sub ws2500_GetDataset;
57             sub ws2500_GetDatasetBulk ($;$;$);
58             sub ws2500_NextDataset;
59             sub ws2500_FirstDataset ($);
60             sub ws2500_SetDebug ($);
61             sub ws2500_InterfaceTest ($);
62             sub ws2500_InterfaceInit ($;$);
63              
64             our %data;
65             %data = ('debug'=>0, 'maxrepeat'=>10,
66             'commands'=>{'ACTIVATE'=>'0', 'DCF'=>'1', 'NEXTSET'=>'2', 'FIRSTSET'=>'3', 'GETSET'=>'4', 'STATUS'=>'5',
67             'INTERFACETEST'=>'CTST', 'INTERFACEINIT'=>'D'},
68             'markers'=>{'SOH'=>"\x01", 'STX'=>"\x02", 'ETX'=>"\x03", 'EOT'=>"\x04",
69             'ENQ'=>"\x05", 'ACK'=>"\x06",
70             'DLE'=>"\x10", 'DC2'=>"\x12", 'DC3'=>"\x13",
71             'NAK'=>"\x15"});
72             our $VERSION = "0.99";
73              
74              
75              
76             # ********************************************************
77             # *** Internal package routines
78             # ********************************************************
79              
80             # Returns a string in the form 2A E3
81             # The special markers used in this interface (like STX=02) are replaced by
82             # the proper identifier. Only used by the debug messages.
83             # Params: data The message to print
84             # Return: string A string in the format described above
85             sub printhex ($) {
86 0     0 0   my $data = shift;
87 0           my $result = '';
88              
89 0 0         return "" if $data eq '';
90              
91 0           for (my $x=0;$x
92 0           my $char = substr($data,$x,1);
93 0           my $printed = 0;
94              
95 0           foreach (keys %{$data{'markers'}}) {
  0            
96 0 0 0       if ($char eq $data{'markers'}->{$_} and !$printed) {
97 0           $result.=sprintf("<%s> ",$_);
98 0           $printed=1;
99             }
100             }
101 0 0         $result.=sprintf("%02X ",ord($char)) unless $printed;
102             }
103              
104 0           return $result;
105             }
106              
107             # Sends a command to the interface
108             # This subroutine only encodes and sends a message, it does not care wether
109             # the sent message has been received/acknowledged or not
110             # Params: token A command from $data{'commands'}
111             # param An optional parameter containing additional data
112             # Return: 1 Always true
113             sub send_Command {
114 0     0 0   my $token = shift;
115 0           my ($checksum,$message,$command,$param);
116            
117             # Is this a valid command, when not die as this is an internal error
118 0 0         die "Unknown command '$token'" unless exists $data{'commands'}->{$token};
119 0           $param='';
120 0 0         $param = shift if scalar @_;
121 0           $command = $data{'commands'}->{$token}.$param;
122              
123             # Checksum is negative sum of command value, Bit 7 always set
124 0           foreach (split //, $command) { $checksum+=ord($_); }
  0            
125 0           $checksum = (0x100-($checksum & 0xFF)) | 0x80;
126            
127             # Build message and write to port
128 0           $message = $data{'markers'}->{'SOH'}.$command.chr($checksum).$data{'markers'}->{'EOT'};
129 0 0         print "Sending '$token': ".(printhex($message))."\n" if $data{'debug'};
130 0           $data{'port'}->write ($message);
131             # Bad hack, we have to wait until the command is processed
132             # Otherwise we will read only partial data
133 0           sleep (0.03);
134              
135 0           return 1;
136             }
137              
138             # Reads a response from the interface
139             # This routine reads a message from the interface, decodes it and does all integrity checking
140             # Params: bytes_expected The number of *message* bytes expected, -1 if not known
141             # response A hash-reference which will be filled with the reponse
142             # Return: 1 Always true
143             # The filled in hash reference has the following keys:
144             # {ok} 1 if the response has been valid and passed all checks, 0 upon failure
145             # {raw} Actual data received from the interface
146             # {message} The actual message, already decoded without any headers
147             # {datalength} The lenght in bytes of the message
148             # {checksum} The checksum of the message
149             sub read_Response ($;$) {
150 0     0 0   my $bytes_expected = shift;
151 0           my $response = shift;
152            
153 0 0         print "Reading Response ... \n" if $data{'debug'};
154            
155             # Read data
156             # As we do not know how many bytes we expect (due to special char encoding)
157             # we poll as long we receive any data in a reasonable interval -> again a bad hack
158 0           $$response{'raw'}='';
159 0           while (my $received=$data{'port'}->read (100)) {
160 0           $$response{'raw'}.=$received;
161 0           sleep (0.01);
162             }
163              
164             # Did we receive a message with a least 5 bytes (shortest possible message)
165 0 0         if (length($$response{'raw'})>=5) {
166 0           $$response{'ok'} = 1;
167             # First decode any message sequences for STX/ETX/ENQ
168 0           $$response{'message'} = '';
169 0           for (my $x=1;$x<=length($$response{'raw'})-2;$x++) {
170 0           my $char1 = substr($$response{'raw'},$x,1);
171 0           my $char2 = substr($$response{'raw'},$x+1,1);
172 0 0         if ($char1 eq $data{'markers'}->{'ENQ'}) {
173 0 0         if ($char2 eq $data{'markers'}->{'DC2'}) { $char1 = $data{'markers'}->{'STX'} }
  0 0          
    0          
174 0           elsif ($char2 eq $data{'markers'}->{'DC3'}) { $char1 = $data{'markers'}->{'ETX'} }
175 0           elsif ($char2 eq $data{'markers'}->{'NAK'}) { $char1 = $data{'markers'}->{'ENQ'} }
176             else {
177 0           $$response{'ok'} = 0;
178 0 0         print "ERROR: Unknown encoding char ".(ord($char2))."\n" if $data{'debug'};
179             };
180 0           $x++;
181             };
182             # WTF ? This isn't documented anywhere ?
183 0 0 0       if (ord($char1)==0xff and ord($char2)==0xff) {
184 0           $x++;
185             }
186 0           $$response{'message_all'}.= $char1;
187             }
188 0           $$response{'message'} = substr($$response{'message_all'},1,ord(substr($$response{'message_all'},0,1)));
189             # Check if the received frame is consistent
190 0           $$response{'datalength'} = ord(substr($$response{'message_all'},0,1));
191 0           $$response{'checksum'} = ord(substr($$response{'message_all'},length($$response{'message_all'})-1,1));
192             # Did we receive enough data
193 0 0 0       if ($bytes_expected!=-1 and $$response{'datalength'}!=$bytes_expected and $$response{'ok'}) {
      0        
194 0           $$response{'ok'} = 0;
195 0 0         print "ERROR: Expected datalength is not correct\n" if $data{'debug'};
196             };
197             # Are the start and end markers ok ?
198 0 0 0       if (substr($$response{'raw'},0,1) ne $data{'markers'}->{'STX'} and $$response{'ok'}) {
199 0           $$response{'ok'} = 0;
200 0 0         print "ERROR: Start marker not found\n" if $data{'debug'};
201             }
202 0 0 0       if (substr($$response{'raw'},length($$response{'raw'})-1,1) ne $data{'markers'}->{'ETX'} and $$response{'ok'}) {
203 0           $$response{'ok'} = 0;
204 0 0         print "ERROR: End marker not found\n" if $data{'debug'};
205             }
206             # Check for a error message from the interface
207 0 0 0       if ($$response{'message'} eq $data{'markers'}->{'NAK'} and $$response{'datalength'}==1 and $$response{'ok'}) {
      0        
208 0           $$response{'ok'} = 0;
209 0 0         print "ERROR: NAK received from interface\n" if $data{'debug'};
210             }
211             # Calculate and check checksum
212 0 0         if ($$response{'ok'}) {
213 0           my $calc_checksum=0;
214 0           for (my $x=0;$x<$$response{'datalength'};$x++) {
215 0           $calc_checksum+=ord(substr($$response{'message'},$x,1));
216             }
217             # Add first to bytes of raw message to checksum
218 0           $calc_checksum+=ord($data{'markers'}->{'STX'}) + $$response{'datalength'} + $$response{'checksum'};
219 0 0         if (($calc_checksum & 0xFF)!= 0) {
220 0           $$response{'ok'} = 0;
221 0 0         print "ERROR: Checksum not correct\n" if $data{'debug'};
222             }
223             }
224             } else {
225 0           $$response{'ok'} = 0;
226 0 0         print "ERROR: Message received is too short\n" if $data{'debug'};
227             }
228              
229 0 0         print "Response status is: $$response{'ok'}, Message: ".(printhex($$response{'raw'}))."\n" if $data{'debug'};
230              
231 0           return 1;
232             }
233              
234             # Tries to initialize the interface
235             # The interface must be sent an initialization request. The interface will go offline
236             # after 71ms when no data is sent.
237             # Timing is crucial, probably on slow systems this may fail. The initialization request
238             # is sent up to 100 times, until a valid reponse is received.
239             # Params: port The interface to use, e.g. /dev/ttyS0
240             # Return: 0|1 1 upon success, 0 upon failure
241             sub init_Interface ($) {
242 0     0 0   my $interface = shift;
243 0           my ($port,$x);
244              
245              
246             # Setup interface with needed specs
247 0 0         print "Opening port '$interface'\n" if $data{'debug'};
248 0 0         $port = new Device::SerialPort ($interface) or croak "Can't open interface '$interface'\n";
249 0 0         $port->baudrate (19200) or croak "Cannot set baudrate";
250 0 0         $port->parity ("even") or croak "Cannot set parity";
251 0           $port->parity_enable(1);
252 0 0         $port->databits (8) or croak "Cannot set databits";
253 0 0         $port->stopbits (2) or croak "Cannot set stopbits";
254              
255             # Activate interface
256             # Sequence taken from Rainer Krienke's ws2500 program
257 0 0         print "Trying to activate interface\n" if $data{'debug'};
258 0 0         $port->dtr_active(0) or croak "Cannot set dtr_active off";
259 0 0         $port->rts_active(1) or croak "Cannot set rtr_active on";
260 0           sleep (0.09);
261 0 0         $port->dtr_active(1) or croak "Cannot set dtr_active on";
262 0 0         $port->rts_active(0) or croak "Cannot set rts_active off";
263 0           sleep (0.02);
264              
265             # Save for global usage
266 0           $data{'port'} = $port;
267              
268             # Send activation data set
269             # Repeat as often as needed until interface responses
270 0           for ($x=0;$x<100;$x++) {
271 0           my %response;
272              
273 0           send_Command ('ACTIVATE');
274 0           read_Response (1,\%response);
275              
276 0 0 0       last if $response{'ok'} and $response{'message'} eq $data{'markers'}->{'ACK'};
277             }
278              
279 0 0         print "Status of interface initialization: ".($x!=100?'Success':'Failure')."\n" if $data{'debug'};
    0          
280 0 0         return 0 if $x == 100;
281 0           return 1;
282              
283             }
284              
285             # Closes the interface
286             # Params: port The port which has been used, e.g. /dev/ttyS0
287             # Return: 1 Alway true
288             sub close_Interface () {
289 0 0   0 0   print "Closing interface\n" if $data{'debug'};
290              
291 0 0         $data{'port'}->close() or croak "Cannot close interface";
292              
293 0           return 1;
294             }
295              
296              
297              
298             # ********************************************************
299             # *** Main package routines
300             # ********************************************************
301              
302             # Reads the received DCF from the interface
303             # Params: ,[]
304             # Device: The port the interface is connected to, e.g. /dev/ttyS0
305             # DCF-Handling: The interface signals if the internal received time
306             # is available (in sync) or not. When DCF-Handling is
307             # set to 1, the routine will return 0 upon DCF failure.
308             # Optional paramater. When not set the signaled error
309             # is ignorred.
310             # Return: Unix-Timestamp representing the received time, 0 upon failure
311             sub ws2500_GetTime ($;$) {
312 0     0 0   my %response;
313 0           my $dcf_handling=0;
314 0           my $port = shift;
315 0 0         $dcf_handling = shift if scalar @_;
316 0           my ($hour,$minute,$second,$day,$month,$year,$dcfok);
317              
318             # Send command
319 0 0         print "Starting Request: Read DCF Clock\n" if $data{'debug'};
320 0 0         return 0 unless init_Interface ($port);
321              
322             # Try ten times to read interface
323 0           for (my $x=0;$x<$data{'maxrepeat'};$x++) {
324 0           send_Command ('DCF');
325 0           read_Response (6,\%response);
326              
327             # Read data
328 0 0         if ($response{'ok'}) {
329 0           $hour = sprintf ("%x",ord(substr($response{'message'},0,1)));
330 0           $minute = sprintf ("%x",ord(substr($response{'message'},1,1)));
331 0           $second = ord(substr($response{'message'},2,1));
332 0           $day = sprintf ("%x",ord(substr($response{'message'},3,1)));
333             # BCD, second nibble
334 0           $month = ord(substr($response{'message'},4,1)) & 0xF;
335             # Get bit 7
336 0           $dcfok = (ord(substr($response{'message'},4,1)) & 0x80) >> 7;
337 0 0 0       return 0 if $dcf_handling and !$dcfok;
338             # Offset +2000, bad hack, but who cares ;-)
339 0           $year = sprintf ("%x",ord(substr($response{'message'},5,1)))+2000;
340             }
341              
342 0 0         last if $response{'ok'};
343             }
344              
345             # Finish
346 0           close_Interface;
347 0 0         return 0 unless $response{'ok'};
348              
349 0           return timelocal ($second,$minute,$hour,$day,$month-1,$year);
350             }
351              
352             # Reads the status of the interface
353             # A detailed hash reference is returned, containing all status data received.
354             # Params: port The interface to connect to, e.g. /dev/ttyS0
355             # result A hash reference which will be filled the status data.
356             # For information about the hash structure see below
357             # The filled in hash structure contains following data:
358             # {sensors}->{} Status about all sensors. Name is 'temp1'...'temp8',
359             # 'rain', 'wind', 'light' or 'inside'
360             # {sensors}->{}->{status'} Either 'OK', or 'n/a' when this sensor does not exit
361             # {sensors}->{}->{dropouts'} The Number of dropouts (not received sensor data)
362             # {sensors}->{address} The address of the sensor
363             # {interface}->{'interval'} The interval in minutes the interface records data
364             # {interface}->{'language'} Language ('English' or 'German'), don't know what this means
365             # {interface}->{'sync_dcf'} Boolean, contains whether the DCF-clock is in sync
366             # {interface}->{'with_dcf'} Boolean, true if DCF is available
367             # {interface}->{'protocol'} The uses protocol version for the sensors, either '1.1' or '1.2'
368             # {interface}->{'type'} Interface type. Either 'PC_WS2500' or 'WS2500'
369             # {interface}->{'version'} Hardware version of the interface (?)
370             sub ws2500_GetStatus ($;$) {
371 0     0 0   my $port = shift;
372 0           my $result = shift;
373 0           my %response;
374             my $time;
375              
376             # Request the status data
377 0 0         print "Starting Request: Read Status\n" if $data{'debug'};
378 0 0         return 0 unless init_Interface ($port);
379              
380             # Try ten times to read interface
381 0           $$result{'valid'} = 0;
382 0           for (my $x=0;$x<$data{'maxrepeat'};$x++) {
383 0           send_Command ('STATUS');
384 0           read_Response (17,\%response);
385              
386 0 0         if ($response{'ok'}) {
387             # Status of sensors
388 0           my $count=0;
389 0           foreach my $sensor (qw (temp1 temp2 temp3 temp4 temp5 temp6 temp7 temp8 rain wind light inside)) {
390 0           my $status = ord(substr($response{'message'},$count,1));
391 0           my $dropouts=0;
392 0 0         if ( $status<16) { $status='n/a'; }
  0 0          
393 0           elsif ( $status==16) { $status='OK'; }
394 0           else { $dropouts=$status+16; $status='OK'; }
  0            
395 0           $$result{'sensors'}->{$sensor}->{'status'} = $status;
396 0           $$result{'sensors'}->{$sensor}->{'dropouts'} = $dropouts;
397 0 0         $$result{'sensors'}->{$sensor}->{'address'} = $1 if $sensor=~ /^temp(\d+)$/;
398 0           $count++;
399             }
400             # Some misc data
401 0           $$result{'interface'}->{'interval'} = ord(substr($response{'message'},12,1));
402 0 0         $$result{'interface'}->{'language'} = (ord(substr($response{'message'},13,1)) & 0x1)?'English':'German';
403 0 0         $$result{'interface'}->{'sync_dcf'} = (ord(substr($response{'message'},13,1)) & 0x2)?1:0;
404 0 0         $$result{'interface'}->{'with_dcf'} = (ord(substr($response{'message'},13,1)) & 0x4)?1:0;
405 0 0         $$result{'interface'}->{'protocol'} = (ord(substr($response{'message'},13,1)) & 0x8)?'1.1':'1.2';
406 0 0         $$result{'interface'}->{'type'} = (ord(substr($response{'message'},13,1)) & 0x10)?'PC_WS2500':'WS2500';
407 0           $$result{'interface'}->{'version'} = int(sprintf("%x",ord(substr($response{'message'},14,1))))/10;
408             # Some addresses
409 0           $$result{'sensors'}->{'rain'}->{'address'} = ord(substr($response{'message'}, 15,1)) & 0x7;
410 0           $$result{'sensors'}->{'wind'}->{'address'} = (ord(substr($response{'message'},15,1)) & 0x70) >> 4;
411 0           $$result{'sensors'}->{'light'}->{'address'} = ord(substr($response{'message'}, 16,1)) & 0x7;
412 0           $$result{'sensors'}->{'inside'}->{'address'} = (ord(substr($response{'message'},16,1)) & 0x70) >> 4;
413              
414 0           $$result{'valid'} = 1;
415             }
416              
417 0 0         last if $response{'ok'};
418             }
419              
420             # Finish
421 0           close_Interface;
422 0 0         return 0 unless $$result{'valid'};
423 0           return 1;
424             }
425              
426             # Request next dataset
427             # Normally when a dataset is requested from the interface, the internal pointer
428             # does not increase. Use this function to advance to the next dataset, if any.
429             # Params: port The port to connect to, e.g. '/dev/ttyS0'
430             # special When special is set to 'isopen' the interface will not be
431             # opened and will not be closed, for bulk data retrieval
432             # Return: 0/1/-1 0 Error during communication
433             # 1 Success
434             # -1 No next dataset available
435             sub ws2500_NextDataset {
436 0     0 0   my $port = shift;
437 0           my %response;
438 0           my $valid = 0;
439 0           my $special = '';
440 0 0         $special = shift if scalar @_;
441              
442 0 0         if ($special eq '') {
443 0 0         return 0 unless init_Interface ($port);
444             }
445              
446             # Having a loop here is a bad thing
447 0           for (my $x=0;$x<$data{'maxrepeat'};$x++) {
448 0           send_Command ('NEXTSET');
449 0           read_Response (1,\%response);
450 0 0         if ($response{'ok'}) {
451 0           $valid=1;
452 0           last;
453             }
454             }
455 0 0         close_Interface if $special eq '';
456              
457 0 0         return 0 unless $valid;
458 0 0         return 0 unless $response{'ok'};
459 0 0         return 1 if $response{'message'} eq $data{'markers'}->{'ACK'};
460 0 0         return -1 if $response{'message'} eq $data{'markers'}->{'DLE'};
461              
462             # Weird ... we should never have reached this point
463 0           return 0;
464             }
465              
466             # Reset pointer to first dataset
467             # Puts the dataset on the oldest record available. All data will be new.
468             # Params: port The port to connect to, e.g. '/dev/ttyS0'
469             # Return: 0/1 0 Error during communication
470             # 1 Success
471             sub ws2500_FirstDataset ($) {
472 0     0 0   my $port = shift;
473 0           my %response;
474 0           my $valid = 0;
475              
476 0 0         return 0 unless init_Interface ($port);
477 0           for (my $x=0;$x<$data{'maxrepeat'};$x++) {
478 0           send_Command ('FIRSTSET');
479 0           read_Response (1,\%response);
480 0 0 0       if ($response{'ok'} and $response{'message'} eq $data{'markers'}->{'ACK'}) {
481 0           $valid=1;
482 0           last;
483             }
484             }
485 0           close_Interface;
486              
487 0 0         return 1 if $valid;
488 0           return 0;
489             }
490              
491             # Read a dataset from the interface
492             # This function reads the current dataset, to which the internal pointer is set.
493             # Params: The device to read from, e.g. /dev/ttyS0
494             # A hash reference where the dataset will be stored in.
495             # See below for hash structure
496             # The can be either 'current' or 'next':
497             # 'current': Get the current dataset, but do not increase to
498             # next pointer
499             # 'next' : Get the current dataset. After the has been successfully
500             # read, advance the internal pointer to the next dataset
501             # Return: 1 Communication successfull (This does not mean that a dataset has been read)
502             # 0 Cummunication error, the hash-reference does not contain any valid data
503             #
504             # The hash-reference has the following structure:
505             # {valid} This hash contains valid data, when set to 1
506             # {interface}->{timestamp} The current DCF-time
507             # {interface} See status hash returned by ws2500_GetStatus
508             # {sensors} See status hash returned by ws2500_GetStatus
509             # {dataset}->{status} Either 'dataset' for a valid dataset, or 'nonew' when no dataset is available
510             # {dataset}->{block} Block number of dataset
511             # {dataset}->{timestamp} Timestamp of dataset
512             # {dataset}->{tempX} Temperature sensors, X is 1 to 8
513             # {dataset}->{tempX}->{'status'} 1 if this sensor contains valid data, 'n/a' when not available
514             # {dataset}->{tempX}->{'new'} New flag is set
515             # {dataset}->{tempX}->{'temperature'} Temperature in Celcius
516             # {dataset}->{tempX}->{'humidity'} Humidity in %, 'n/a' if this sensor is missing
517             # {dataset}->{wind}->{'status'} 1 if this sensor contains valid data, 'n/a' when not available
518             # {dataset}->{wind}->{'new'} The new flag is set
519             # {dataset}->{wind}->{'speed'} Wind speed in km/h
520             # {dataset}->{wind}->{'direction'} Direction in degree
521             # {dataset}->{wind}->{'accuracy'} Average devivation for direction in degree
522             # {dataset}->{inside}->{'status'} 1 if this sensor contains valid data, 'n/a' when not available
523             # {dataset}->{inside}->{'new'} New flag is set
524             # {dataset}->{inside}->{'temperature'} Temperature in Celcius
525             # {dataset}->{inside}->{'humidity'} Humidity in %, 'n/a' if this sensor is missing
526             # {dataset}->{inside}->{'pressure'} Pressure in hPa
527             # {dataset}->{rain}->{'status'} 1 if this sensor contains valid data, 'n/a' when not available
528             # {dataset}->{rain}->{'new'} New flag is set
529             # {dataset}->{rain}->{'counter_ml'} Current counter
530             # {dataset}->{rain}->{'counter_ml'} Current rain counter in ml, delta to previous call is the rainfall
531             # {dataset}->{light}->{'status'} 1 if this sensor contains valid data, 'n/a' when not available
532             # {dataset}->{light}->{'new'} New flag is set
533             # {dataset}->{light}->{'duration'} Counter in minutes with brightness > 20.000 Lux
534             # {dataset}->{light}->{'brightness'} Sun brightness in Lux
535             # {dataset}->{light}->{'sunflag'} Sunflag is set, undocumented
536             sub ws2500_GetDataset {
537 0     0 0   my $port = shift;
538 0           my $result = shift;
539 0           my $type = shift;
540 0           my %response;
541 0           my $doinit = '';
542 0 0         $doinit = shift if scalar @_;
543            
544 0 0         print "Starting Request: Read Dataset\n" if $data{'debug'};
545            
546 0 0 0       if ($doinit eq '' or $doinit eq 'noclose') {
547             # First get the time for reference
548 0           $$result{'interface'}->{'timestamp'} = ws2500_GetTime ($port);
549 0 0         return 0 if $$result{'interface'}->{'timestamp'}<=0;
550              
551             # Now the status, so we know which sensor is active
552 0 0         return 0 unless ws2500_GetStatus ($port,$result);
553              
554             # Start up the interface to get the data
555 0 0         return 0 unless init_Interface ($port);
556             }
557              
558             # Try several times to read interface, until we get a valid response
559 0           $$result{'valid'}=0;
560 0           for (my $x=0;$x<$data{'maxrepeat'};$x++) {
561 0           send_Command ('GETSET');
562 0           read_Response (-1,\%response);
563              
564 0 0         if ($response{'ok'}) {
565 0 0         unless ($response{'message'} eq $data{'markers'}->{'DLE'}) {
566             # New dataset available
567             # Prepare the message so we can access it more easy
568 0           my @data = (split //, $response{'message'});
569 0           $$result{'dataset'}->{'block'} = ord($data[0]) + ord($data[1])*0x100;
570 0           $$result{'dataset'}->{'timestamp'} = $$result{'interface'}->{'timestamp'}-
571             ((ord($data[2])+ord($data[3])*0x100)*60);
572             # We only have the age in minutes, so cut down to zero seconds
573 0           $$result{'dataset'}->{'timestamp'} = int($$result{'dataset'}->{'timestamp'}/60)*60;
574 0           my $nibble=0;
575 0           foreach my $sensor (qw (temp1 temp2 temp3 temp4 temp5 temp6 temp7 temp8)) {
576 0           my %temp;
577 0 0         if ($$result{'sensors'}->{$sensor}->{'status'} ne 'n/a') {
578 0           my $sign = +1;
579 0           for (my $y=0;$y<5;$y++) {
580 0 0         if ($nibble % 2) { $temp{$y}=(ord($data[int($nibble/2)+4]) & 0xF0) >> 4; }
  0            
581 0           else { $temp{$y}=ord($data[int($nibble/2)+4]) & 0xF; }
582 0           $nibble++;
583             } # for
584              
585             # First the temperature
586             # Test for plus/minus
587 0 0         $sign=-1 if $temp{'2'} & 0x8;
588             # Mask the sign bit
589 0           $temp{'2'}=$temp{'2'} & 0x7;
590 0           $$result{'dataset'}->{$sensor}->{'temperature'} = ($temp{'0'}/10 + $temp{'1'} + $temp{2}*10)*$sign;
591 0           $$result{'dataset'}->{$sensor}->{'status'} = 'ok';
592              
593             # Now the humidity
594             # Is the new flag set
595 0           $$result{'dataset'}->{$sensor}->{'new'} = ($temp{'4'} & 0x8) >> 3;
596             # Mask the new flag
597 0           $temp{'4'}=$temp{'4'} & 0x7;
598 0 0         if ($temp{'3'}<=9) {
599 0           $$result{'dataset'}->{$sensor}->{'humidity'} = ($temp{'3'} + $temp{'4'}*10)+20;
600             } else {
601 0           $$result{'dataset'}->{$sensor}->{'humidity'} = 'n/a';
602             }
603             } else {
604             # This sensor is not available
605 0           $$result{'dataset'}->{$sensor}->{'status'} = 'n/a';
606 0           $nibble+=5;
607             }
608              
609            
610             } # foreach temperature
611              
612 0           my $of=3;
613             # Wind direction
614 0 0         if ($$result{'sensors'}->{'wind'}->{'status'} ne 'n/a') {
615 0           $$result{'dataset'}->{'wind'}->{'speed'} = ((ord($data[$of+21]) & 0xF)/10)+
616             ((ord($data[$of+21]) & 0xF0) >> 4)+
617             ((ord($data[$of+22]) & 0xF)*10);
618 0           $$result{'dataset'}->{'wind'}->{'direction'} = (((ord($data[$of+22]) & 0xF0) >> 4)*10)+
619             ((ord($data[$of+23]) & 0x3)*100);
620 0 0         $$result{'dataset'}->{'wind'}->{'direction'}+=5 if ord($data[$of+23]) & 0x10;
621 0           my $accuracy = (ord($data[$of+23]) & 0xC) >> 2;
622 0 0         $$result{'dataset'}->{'wind'}->{'accuracy'}=0 if $accuracy==0;
623 0 0         $$result{'dataset'}->{'wind'}->{'accuracy'}=22.5 if $accuracy==1;
624 0 0         $$result{'dataset'}->{'wind'}->{'accuracy'}=45 if $accuracy==2;
625 0 0         $$result{'dataset'}->{'wind'}->{'accuracy'}=67.5 if $accuracy==3;
626 0           $$result{'dataset'}->{'wind'}->{'new'} = (ord($data[$of+23]) & 0x8) >> 3;
627 0           $$result{'dataset'}->{'wind'}->{'status'} = 'ok';
628             } else {
629 0           $$result{'dataset'}->{'wind'}->{'status'} = 'n/a';
630             }
631              
632             # Inside sensor
633 0 0         if ($$result{'sensors'}->{'inside'}->{'status'} ne 'n/a') {
634 0           $$result{'dataset'}->{'inside'}->{'pressure'} = (ord($data[$of+24]) & 0xF)+
635             (((ord($data[$of+24]) & 0xF0)>>4)*10)+
636             ((ord($data[$of+25]) & 0xF)*100);
637 0           my $sign=1;
638 0 0         $sign=-1 if ord($data[$of+26]) & 0x80;
639 0           $data[$of+26]=chr(ord($data[$of+26]) & 0x7F);
640 0           $$result{'dataset'}->{'inside'}->{'temperature'} = ((((ord($data[$of+25]) & 0xF0)>>4)/10)+
641             (ord($data[$of+26]) & 0xF)+
642             (((ord($data[$of+26]) & 0xF0)>>4)*10))*$sign;
643 0 0         if ((ord($data[$of+27]) & 0xF)<=9) {
644 0           $$result{'dataset'}->{'inside'}->{'humidity'} = (ord($data[$of+27]) & 0xF)+
645             (((ord($data[$of+27]) & 0x70)>>4)*10)+
646             20;
647             } else {
648 0           $$result{'dataset'}->{'inside'}->{'humidity'} = 'n/a';
649             }
650 0           $$result{'dataset'}->{'inside'}->{'new'} = (ord($data[$of+27]) & 0x80) >> 7;
651 0           $$result{'dataset'}->{'inside'}->{'status'} = 'ok';
652             } else {
653 0           $$result{'dataset'}->{'inside'}->{'status'} = 'n/a';
654             }
655            
656             # Rain sensor
657 0 0         if ($$result{'sensors'}->{'rain'}->{'status'} ne 'n/a') {
658 0           $$result{'dataset'}->{'rain'}->{'counter'} = ord($data[$of+28])+
659             (ord($data[$of+29]) & 0x7)*0x100;
660 0           $$result{'dataset'}->{'rain'}->{'counter_ml'} = $$result{'dataset'}->{'rain'}->{'counter'}*370;
661 0           $$result{'dataset'}->{'rain'}->{'status'} = 'ok';
662             } else {
663 0           $$result{'dataset'}->{'rain'}->{'status'} = 'n/a';
664             }
665              
666             # Light sensor
667 0 0         if ($$result{'sensors'}->{'light'}->{'status'} ne 'n/a') {
668 0           $$result{'dataset'}->{'light'}->{'duration'} = ((ord($data[$of+29]) & 0xF0)>>4)+
669             ((ord($data[$of+30]) & 0xF)*0x10)+
670             (((ord($data[$of+30]) & 0xF0)>>4)*0x100);
671 0           $$result{'dataset'}->{'light'}->{'brightness'} = ((ord($data[$of+31]) & 0xF)+
672             (((ord($data[$of+31]) & 0xF0)>>4)*10)+
673             ((ord($data[$of+32]) & 0xF)*100))*
674             (10**((ord($data[$of+32]) & 0x30)>>4));
675 0           $$result{'dataset'}->{'light'}->{'sun_flag'} = (ord($data[$of+32]) & 0x40) >> 6;
676 0           $$result{'dataset'}->{'light'}->{'new'} = (ord($data[$of+32]) & 0x80) >> 7;
677 0           $$result{'dataset'}->{'light'}->{'status'} = 'ok';
678             } else {
679 0           $$result{'dataset'}->{'light'}->{'status'} = 'n/a';
680             }
681              
682 0           $$result{'dataset'}->{'status'} = 'dataset';
683             } else {
684             # No new dataset available
685 0           $$result{'dataset'}->{'status'} = 'nonew';
686             }
687            
688 0           $$result{'valid'} = 1;
689             }
690 0 0         last if $$result{'valid'};
691             }
692 0 0         close_Interface if $doinit eq '';
693              
694             # Upon request advance to next dataset
695 0 0 0       if ($type eq 'next' and $$result{'valid'} and $$result{'dataset'}->{'status'} eq 'dataset') {
      0        
696 0 0         if ($doinit eq '') {
697 0           ws2500_NextDataset ($port);
698             } else {
699 0           ws2500_NextDataset ($port,'isopen');
700             }
701             }
702              
703             # Finish
704 0 0         return 0 unless $$result{'valid'};
705 0           return 1;
706             }
707              
708             # Get bulk dataset data
709             # Whereas the normal Getdataset function initializes and closes the interface for each
710             # dataset, this function opens the communication only once, and serveral dataset are
711             # then transferred in a batch. This greatly improves the performance
712             # Params: port The port to use, e.g. '/dev/ttyS0'
713             # result The result hash reference, see below
714             # bulkcount The number of datasets to retrieve in one run
715             # Return: 1 Always true
716             # The result hash has the following structure:
717             # {valid} If this bulkdata is valid
718             # {bulkcount} The actual number of retrieved datasets
719             # {bulk} An array. Each element contains a dataset hash reference
720             # See the ws2500_GetDataset function for the structure
721             # {interface} See ws2500_GetDataset function
722             # {sensors} See ws2500_GetDataset function
723             sub ws2500_GetDatasetBulk ($;$;$) {
724 0     0 0   my $port = shift;
725 0           my $result = shift;
726 0           my $bulkcount = shift;
727 0           my @bulkdata;
728             my %firstdataset;
729              
730 0           for (my $x=0;$x<$bulkcount;$x++) {
731 0 0         if ($x==0) {
732             # Request first dataset
733             # As we supply the 'noclose' param the connection to the interface stays
734             # open an we can request additional datasets without reestablishing the connection
735 0           my $res = ws2500_GetDataset ($port,\%firstdataset,'next','noclose');
736             # Check for errors
737 0 0 0       if ($res and $firstdataset{'valid'} and $firstdataset{'dataset'}->{'status'} eq 'dataset') {
      0        
738 0           push @bulkdata, $firstdataset{'dataset'};
739             } else {
740 0           last;
741             }
742             } else {
743             # Further datasets, use the firstdataset as base
744 0           my %result = %firstdataset;
745 0           delete $result{'dataset'};
746 0           my $res = ws2500_GetDataset ($port,\%result,'next','noinit');
747             # Check for errors
748 0 0 0       if ($res and $result{'valid'} and $result{'dataset'}->{'status'} eq 'dataset') {
      0        
749 0           push @bulkdata, $result{'dataset'};
750             } else {
751 0           $firstdataset{'valid'} = $result{'valid'};
752 0           last;
753             }
754             }
755             }
756             # Prepare the result
757 0           $$result{'valid'} = $firstdataset{'valid'};
758 0           $$result{'interface'} = $firstdataset{'interface'};
759 0           $$result{'sensors'} = $firstdataset{'sensors'};
760             # Save the bulkdata
761 0           $$result{'bulk'} = \@bulkdata;
762 0           $$result{'bulkcount'} = scalar @bulkdata;
763              
764 0           close_Interface;
765              
766 0           return 1;
767             }
768              
769              
770             # Test Interface
771             # This function does not work and is not properly documented. See inline comments below
772             # Params: port The port to use, e.g. /dev/ttyS0
773             # Return: 0 Always false, as it does not work
774             sub ws2500_InterfaceTest ($) {
775 0     0 0   my $port = shift;
776 0           my %response;
777 0           my $valid = 0;
778              
779 0           return 0;
780              
781             # This doesn't seem to work. Acoording to the docu we have to send either
782             # 'C' or 'CTST'. However both variants fail, and there is either no data
783             # received at all, or gibberish. Furthermore the interface is not reset.
784             # If anyone has a clear documentation how to activate this (and what to
785             # to with it), please send them.
786             # return 0 unless init_Interface ($port);
787             # for (my $x=0;$x<$data{'maxrepeat'};$x++) {
788             # send_Command ('INTERFACETEST');
789             # sleep (0.04);
790             # read_Response (1,\%response);
791             # if ($response{'ok'} and $response{'message'} eq $data{'markers'}->{'ACK'}) {
792             # $valid=1;
793             # last;
794             # }
795             # }
796             # close_Interface;
797             #
798             # return 1 if $valid;
799             # return 0;
800             }
801              
802             # Initialize the interface we new data
803             # Params: port The port to sent the data, e.g. /dev/ttyS0
804             # data A hash-reference containing the configuration, see below
805             # Return: 0|1 True upon success, else False
806             # The configuration-hash must contain following keys:
807             # {first} Minutes to wait after init to resume normal operation, 0..63 minutes
808             # {interval} The interval in minutes to record data, 2..63 minutes
809             # {addr-rain} The address of the rain sensor, 0..7
810             # {addr-wind} The address of the wind sensor, 0..7
811             # {addr-inside} The address of the inside sensor, 0..7
812             # {addr-light} The address of the light sensor, 0..7
813             # {version} The protocal version to use: 1 (V1.1) or 2 (V1.2)
814             sub ws2500_InterfaceInit ($;$) {
815 0     0 0   my $port = shift;
816 0           my $data = shift;
817 0           my %response;
818 0           my $valid = 0;
819 0           my $message;
820              
821             # {'first'=>12,'interval'=>3,'addr-rain'=>7,'addr-wind'=>7,'addr-inside'=>7,'addr-ligth'=>7,'version'});
822              
823             # Prepare the message (4 Bytes)
824             # First some checks if the data is correct
825 0           foreach my $token (qw (first interval addr-rain addr-wind addr-inside addr-light version)) {
826 0 0         croak "Token '$token' missing in configuration hash" unless exists $$data{$token};
827 0 0         croak "Token '$token' is not a number ('$$data{'$token'}') " unless $$data{$token}=~ /^\d+$/;
828             }
829             # Some sanity checks
830 0 0 0       croak "First interval 'first' must be between 0 and 63" if $$data{'first'}<0 or $$data{'first'}>63;
831 0 0 0       croak "Recording interval 'interval' must be between 2 and 63" if $$data{'interval'}<2 or $$data{'interval'}>63;
832 0           foreach my $token (qw (addr-rain addr-wind addr-inside addr-light)) {
833 0 0 0       croak "Sensor address for '$token' must be between 0 and 7" if $$data{$token}<0 or $$data{$token}>7;
834             }
835 0 0 0       croak "Version must be either 1 (V1.1) or 2 (V1.2)" if $$data{'version'}<1 or $$data{'version'}>2;
836              
837             # Put everything together
838 0           my $addr1 = $$data{'addr-rain'} + ($$data{'addr-wind'} << 4) + 0x80;
839 0 0         $addr1|=0x8 if $$data{'version'}==1;
840 0           my $addr2 = $$data{'addr-light'} + ($$data{'addr-inside'} << 4) + 0x80;
841             # Now build the message
842 0           $message = chr($$data{'first'}).chr($$data{'interval'}).chr($addr1).chr($addr2);
843              
844             # Send the command
845 0 0         return 0 unless init_Interface ($port);
846 0           for (my $x=0;$x<$data{'maxrepeat'};$x++) {
847 0           send_Command ('INTERFACEINIT',$message);
848 0           read_Response (1,\%response);
849 0 0 0       if ($response{'ok'} and $response{'message'} eq $data{'markers'}->{'ACK'}) {
850 0           $valid=1;
851 0           last;
852             }
853             }
854 0           close_Interface;
855              
856 0 0         return 1 if $valid;
857 0           return 0;
858             }
859              
860             # Enables debug
861             # When debug is enabled, a lot of information is printed to STDOUT
862             # Params: debug 1 to enable debug, 0 to disable (default)
863             # Return: 1 Always true
864             sub ws2500_SetDebug ($) {
865 0     0 0   my $debug = shift;
866              
867 0 0 0       croak "Debug must be called with 0 or 1 as argument" if $debug>1 or $debug<0;
868              
869 0           $data{'debug'} = $debug;
870              
871 0           return 1;
872             }
873              
874              
875              
876             1;
877