34_panStamp.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. # $Id: 34_panStamp.pm 12056 2016-08-22 19:30:31Z justme1968 $
  2. package main;
  3. use strict;
  4. use warnings;
  5. use Time::HiRes qw(gettimeofday);
  6. sub panStamp_Attr(@);
  7. sub panStamp_Clear($);
  8. sub panStamp_HandleWriteQueue($);
  9. sub panStamp_Parse($$$$);
  10. sub panStamp_Read($);
  11. sub panStamp_ReadAnswer($$$$);
  12. sub panStamp_Ready($);
  13. sub panStamp_Write($$$);
  14. sub panStamp_SimpleWrite(@);
  15. my $clientsPanStamp = ":SWAP:";
  16. my %matchListSWAP = (
  17. "1:SWAP" => "^.*",
  18. );
  19. sub
  20. panStamp_Initialize($)
  21. {
  22. my ($hash) = @_;
  23. require "$attr{global}{modpath}/FHEM/DevIo.pm";
  24. # Provider
  25. $hash->{ReadFn} = "panStamp_Read";
  26. $hash->{WriteFn} = "panStamp_Write";
  27. $hash->{ReadyFn} = "panStamp_Ready";
  28. # Normal devices
  29. $hash->{DefFn} = "panStamp_Define";
  30. $hash->{FingerprintFn} = "panStamp_Fingerprint";
  31. $hash->{UndefFn} = "panStamp_Undef";
  32. #$hash->{GetFn} = "panStamp_Get";
  33. $hash->{SetFn} = "panStamp_Set";
  34. #$hash->{AttrFn} = "panStamp_Attr";
  35. $hash->{AttrList}= "dummy:1,0";
  36. $hash->{ShutdownFn} = "panStamp_Shutdown";
  37. }
  38. sub
  39. panStamp_Fingerprint($$)
  40. {
  41. }
  42. #####################################
  43. sub
  44. panStamp_Define($$)
  45. {
  46. my ($hash, $def) = @_;
  47. my @a = split("[ \t][ \t]*", $def);
  48. if(@a < 3 || @a > 6) {
  49. my $msg = "wrong syntax: define <name> panStamp {none | devicename[\@baudrate] ".
  50. "| devicename\@directio | hostname:port} [<address> [<channel> [<syncword>]]]";
  51. Log3 undef, 2, $msg;
  52. return $msg;
  53. }
  54. my $address = $a[3];
  55. $address = "01" if( !defined($address) );
  56. my $channel = $a[4];
  57. $channel = "00" if( !defined($channel) );
  58. my $syncword = $a[5];
  59. $syncword = 'B547' if( !defined($syncword) );
  60. return "$address is not a 1 byte hex value" if( $address !~ /^[\da-f]{2}$/i );
  61. return "$address is not an allowed address" if( $address eq "00" );
  62. return "$channel is not a 1 byte hex value" if( $channel !~ /^[\da-f]{2}$/i );
  63. return "$syncword is not a 2 byte hex value" if( $syncword !~ /^[\da-f]{4}$/i );
  64. DevIo_CloseDev($hash);
  65. $hash->{Clients} = $clientsPanStamp;
  66. $hash->{MatchList} = \%matchListSWAP;
  67. my $name = $a[0];
  68. my $dev = $a[2];
  69. if($dev eq "none") {
  70. Log3 $name, 1, "$name device is none, commands will be echoed only";
  71. $attr{$name}{dummy} = 1;
  72. return undef;
  73. }
  74. $dev .= "\@38400" if( $dev !~ m/\@/ && $dev !~ m/:/ );
  75. $hash->{address} = uc($address);
  76. $hash->{channel} = uc($channel);
  77. $hash->{syncword} = uc($syncword);
  78. $hash->{nonce} = 0;
  79. $hash->{DeviceName} = $dev;
  80. my $ret = DevIo_OpenDev($hash, 0, "panStamp_DoInit");
  81. return $ret;
  82. }
  83. #####################################
  84. sub
  85. panStamp_Undef($$)
  86. {
  87. my ($hash, $arg) = @_;
  88. my $name = $hash->{NAME};
  89. foreach my $d (sort keys %defs) {
  90. if(defined($defs{$d}) &&
  91. defined($defs{$d}{IODev}) &&
  92. $defs{$d}{IODev} == $hash)
  93. {
  94. my $lev = ($reread_active ? 4 : 2);
  95. Log3 $name, $lev, "deleting port for $d";
  96. delete $defs{$d}{IODev};
  97. }
  98. }
  99. panStamp_Shutdown($hash);
  100. DevIo_CloseDev($hash);
  101. return undef;
  102. }
  103. #####################################
  104. sub
  105. panStamp_Shutdown($)
  106. {
  107. my ($hash) = @_;
  108. ###panStamp_SimpleWrite($hash, "X00");
  109. return undef;
  110. }
  111. #####################################
  112. sub
  113. panStamp_Set($@)
  114. {
  115. my ($hash, @a) = @_;
  116. my $name = shift @a;
  117. my $cmd = shift @a;
  118. my $arg = join("", @a);
  119. my $list = "discover raw";
  120. return $list if( $cmd eq '?' );
  121. if($cmd eq "raw") {
  122. return "\"set panStamp $cmd\" needs exactly one parameter" if(@_ != 4);
  123. return "Expecting a even length hex number" if((length($arg)&1) == 1 || $arg !~ m/^[\dA-F]{12,}$/ );
  124. Log3 $name, 4, "set $name $cmd $arg";
  125. panStamp_SimpleWrite($hash, $arg);
  126. } elsif($cmd eq "discover") {
  127. Log3 $name, 4, "set $name $cmd";
  128. panStamp_SimpleWrite($hash, "00".$hash->{address}."0000010000" );
  129. } else {
  130. return "Unknown argument $cmd, choose one of ".$list;
  131. }
  132. return undef;
  133. }
  134. #####################################
  135. sub
  136. panStamp_Get($@)
  137. {
  138. my ($hash, @a) = @_;
  139. my $name = $hash->{NAME};
  140. return "No $a[1] for dummies" if(IsDummy($name));
  141. #$hash->{READINGS}{$a[1]}{VAL} = $msg;
  142. $hash->{READINGS}{$a[1]}{TIME} = TimeNow();
  143. #return "$a[0] $a[1] => $msg";
  144. }
  145. sub
  146. panStamp_Clear($)
  147. {
  148. my $hash = shift;
  149. # Clear the pipe
  150. $hash->{RA_Timeout} = 0.1;
  151. for(;;) {
  152. my ($err, undef) = panStamp_ReadAnswer($hash, "Clear", 0, undef);
  153. last if($err && $err =~ m/^Timeout/);
  154. }
  155. delete($hash->{RA_Timeout});
  156. }
  157. #####################################
  158. sub
  159. panStamp_DoInit($)
  160. {
  161. my $hash = shift;
  162. my $name = $hash->{NAME};
  163. my $err;
  164. my $msg = undef;
  165. my $val;
  166. panStamp_Clear($hash);
  167. panStamp_ReadAnswer($hash, "ready?", 0, undef);
  168. panStamp_SimpleWrite($hash, "+++", 1 );
  169. sleep 2;
  170. panStamp_ReadAnswer($hash, "cmd mode?", 0, undef);
  171. panStamp_SimpleWrite($hash, "ATHV?" );
  172. ($err, $val) = panStamp_ReadAnswer($hash, "HW Version", 0, undef);
  173. return "$name: $err" if($err && ($err !~ m/Timeout/));
  174. $hash->{HWVersion} = $val;
  175. panStamp_SimpleWrite($hash, "ATFV?" );
  176. ($err, $val) = panStamp_ReadAnswer($hash, "FW Version", 0, undef);
  177. return "$name: $err" if($err && ($err !~ m/Timeout/));
  178. $hash->{FWVersion} = $val;
  179. panStamp_SimpleWrite($hash, "ATSW=$hash->{syncword}" );
  180. ($err, $val) = panStamp_ReadAnswer($hash, "sync word", 0, undef);
  181. return "$name: $err" if($err && ($err !~ m/Timeout/));
  182. panStamp_SimpleWrite($hash, "ATSW?" );
  183. ($err, $val) = panStamp_ReadAnswer($hash, "sync word", 0, undef);
  184. return "$name: $err" if($err && ($err !~ m/Timeout/));
  185. $hash->{syncword} = sprintf( "%04s", $val );
  186. panStamp_SimpleWrite($hash, "ATCH=$hash->{channel}" );
  187. ($err, $val) = panStamp_ReadAnswer($hash, "channel", 0, undef);
  188. return "$name: $err" if($err && ($err !~ m/Timeout/));
  189. panStamp_SimpleWrite($hash, "ATCH?" );
  190. ($err, $val) = panStamp_ReadAnswer($hash, "channel", 0, undef);
  191. return "$name: $err" if($err && ($err !~ m/Timeout/));
  192. $hash->{channel} = sprintf( "%02s", $val);
  193. panStamp_SimpleWrite($hash, "ATDA=$hash->{address}" );
  194. ($err, $val) = panStamp_ReadAnswer($hash, "address", 0, undef);
  195. return "$name: $err" if($err && ($err !~ m/Timeout/));
  196. panStamp_SimpleWrite($hash, "ATDA?" );
  197. ($err, $val) = panStamp_ReadAnswer($hash, "address", 0, undef);
  198. return "$name: $err" if($err && ($err !~ m/Timeout/));
  199. $hash->{address} = sprintf( "%02s", $val);
  200. panStamp_SimpleWrite($hash, "ATO" );
  201. panStamp_ReadAnswer($hash, "data mode?", 0, undef);
  202. panStamp_SimpleWrite($hash, "00".$hash->{address}."0000010000" );
  203. readingsSingleUpdate($hash, "state", "initialized", 1);
  204. # Reset the counter
  205. delete($hash->{XMIT_TIME});
  206. delete($hash->{NR_CMD_LAST_H});
  207. return undef;
  208. }
  209. #####################################
  210. # This is a direct read for commands like get
  211. # Anydata is used by read file to get the filesize
  212. sub
  213. panStamp_ReadAnswer($$$$)
  214. {
  215. my ($hash, $arg, $anydata, $regexp) = @_;
  216. my $type = $hash->{TYPE};
  217. return ("No FD", undef)
  218. if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD})));
  219. my ($mpandata, $rin) = ("", '');
  220. my $buf;
  221. my $to = 3; # 3 seconds timeout
  222. $to = $hash->{RA_Timeout} if($hash->{RA_Timeout}); # ...or less
  223. for(;;) {
  224. if($^O =~ m/Win/ && $hash->{USBDev}) {
  225. $hash->{USBDev}->read_const_time($to*1000); # set timeout (ms)
  226. # Read anstatt input sonst funzt read_const_time nicht.
  227. $buf = $hash->{USBDev}->read(999);
  228. return ("Timeout reading answer for get $arg", undef)
  229. if(length($buf) == 0);
  230. } else {
  231. return ("Device lost when reading answer for get $arg", undef)
  232. if(!$hash->{FD});
  233. vec($rin, $hash->{FD}, 1) = 1;
  234. my $nfound = select($rin, undef, undef, $to);
  235. if($nfound < 0) {
  236. next if ($! == EAGAIN() || $! == EINTR() || $! == 0);
  237. my $err = $!;
  238. DevIo_Disconnected($hash);
  239. return("panStamp_ReadAnswer $arg: $err", undef);
  240. }
  241. return ("Timeout reading answer for get $arg", undef)
  242. if($nfound == 0);
  243. $buf = DevIo_SimpleRead($hash);
  244. return ("No data", undef) if(!defined($buf));
  245. }
  246. if($buf) {
  247. Log3 $hash->{NAME}, 5, "panStamp/RAW (ReadAnswer): $buf";
  248. $mpandata .= $buf;
  249. }
  250. chop($mpandata);
  251. chop($mpandata);
  252. return (undef, $mpandata)
  253. }
  254. }
  255. #####################################
  256. # Check if the 1% limit is reached and trigger notifies
  257. sub
  258. panStamp_XmitLimitCheck($$)
  259. {
  260. my ($hash,$fn) = @_;
  261. my $now = time();
  262. if(!$hash->{XMIT_TIME}) {
  263. $hash->{XMIT_TIME}[0] = $now;
  264. $hash->{NR_CMD_LAST_H} = 1;
  265. return;
  266. }
  267. my $nowM1h = $now-3600;
  268. my @b = grep { $_ > $nowM1h } @{$hash->{XMIT_TIME}};
  269. if(@b > 163) { # 163 comes from fs20. todo: verify if correct for panstamp modulation
  270. my $name = $hash->{NAME};
  271. Log3 $name, 2, "panStamp TRANSMIT LIMIT EXCEEDED";
  272. DoTrigger($name, "TRANSMIT LIMIT EXCEEDED");
  273. } else {
  274. push(@b, $now);
  275. }
  276. $hash->{XMIT_TIME} = \@b;
  277. $hash->{NR_CMD_LAST_H} = int(@b);
  278. }
  279. #####################################
  280. sub
  281. panStamp_Write($$$)
  282. {
  283. my ($hash,$addr,$msg) = @_;
  284. my $name = $hash->{NAME};
  285. Log3 $name, 5, "$name sending $msg";
  286. my $bstring = $addr.$hash->{address}.$msg;
  287. panStamp_AddQueue($hash, $bstring);
  288. #panStamp_SimpleWrite($hash, $bstring);
  289. }
  290. sub
  291. panStamp_SendFromQueue($$)
  292. {
  293. my ($hash, $bstring) = @_;
  294. my $name = $hash->{NAME};
  295. my $to = 0.05;
  296. if($bstring ne "") {
  297. my $sp = AttrVal($name, "sendpool", undef);
  298. if($sp) { # Is one of the panStamp-fellows sending data?
  299. my @fellows = split(",", $sp);
  300. foreach my $f (@fellows) {
  301. if($f ne $name &&
  302. $defs{$f} &&
  303. $defs{$f}{QUEUE} &&
  304. $defs{$f}{QUEUE}->[0] ne "")
  305. {
  306. unshift(@{$hash->{QUEUE}}, "");
  307. InternalTimer(gettimeofday()+$to, "panStamp_HandleWriteQueue", $hash, 0);
  308. return;
  309. }
  310. }
  311. }
  312. panStamp_XmitLimitCheck($hash,$bstring);
  313. panStamp_SimpleWrite($hash, $bstring);
  314. }
  315. InternalTimer(gettimeofday()+$to, "panStamp_HandleWriteQueue", $hash, 0);
  316. }
  317. sub
  318. panStamp_AddQueue($$)
  319. {
  320. my ($hash, $bstring) = @_;
  321. if(!$hash->{QUEUE}) {
  322. $hash->{QUEUE} = [ $bstring ];
  323. panStamp_SendFromQueue($hash, $bstring);
  324. } else {
  325. push(@{$hash->{QUEUE}}, $bstring);
  326. }
  327. }
  328. #####################################
  329. sub
  330. panStamp_HandleWriteQueue($)
  331. {
  332. my $hash = shift;
  333. my $arr = $hash->{QUEUE};
  334. if(defined($arr) && @{$arr} > 0) {
  335. shift(@{$arr});
  336. if(@{$arr} == 0) {
  337. delete($hash->{QUEUE});
  338. return;
  339. }
  340. my $bstring = $arr->[0];
  341. if($bstring eq "") {
  342. panStamp_HandleWriteQueue($hash);
  343. } else {
  344. panStamp_SendFromQueue($hash, $bstring);
  345. }
  346. }
  347. }
  348. #####################################
  349. # called from the global loop, when the select for hash->{FD} reports data
  350. sub
  351. panStamp_Read($)
  352. {
  353. my ($hash) = @_;
  354. my $buf = DevIo_SimpleRead($hash);
  355. return "" if(!defined($buf));
  356. my $name = $hash->{NAME};
  357. my $pandata = $hash->{PARTIAL};
  358. Log3 $name, 5, "panStamp/RAW: $pandata/$buf";
  359. $pandata .= $buf;
  360. while($pandata =~ m/\n/) {
  361. my $rmsg;
  362. ($rmsg,$pandata) = split("\n", $pandata, 2);
  363. $rmsg =~ s/\r//;
  364. panStamp_Parse($hash, $hash, $name, $rmsg) if($rmsg);
  365. }
  366. $hash->{PARTIAL} = $pandata;
  367. }
  368. sub
  369. panStamp_Parse($$$$)
  370. {
  371. my ($hash, $iohash, $name, $rmsg) = @_;
  372. my $dmsg = $rmsg;
  373. my $l = length($dmsg);
  374. my $rssi = hex(substr($dmsg, 1, 2));
  375. $rssi = ($rssi>=128 ? (($rssi-256)/2-74) : ($rssi/2-74));
  376. my $lqi = hex(substr($dmsg, 3, 2));
  377. $dmsg = substr($dmsg, 6, $l-6);
  378. Log3 $name, 5, "$name: $dmsg $rssi $lqi";
  379. next if(!$dmsg || length($dmsg) < 1); # Bogus messages
  380. $hash->{"${name}_MSGCNT"}++;
  381. $hash->{"${name}_TIME"} = TimeNow();
  382. readingsSingleUpdate($hash, "state", $hash->{READINGS}{state}{VAL}, 0);
  383. $hash->{RAWMSG} = $rmsg;
  384. my %addvals = (RAWMSG => $rmsg);
  385. if(defined($rssi)) {
  386. $hash->{RSSI} = $rssi;
  387. $addvals{RSSI} = $rssi;
  388. }
  389. if(defined($lqi)) {
  390. $hash->{LQI} = $lqi;
  391. $addvals{LQI} = $lqi;
  392. }
  393. Dispatch($hash, $dmsg, \%addvals);
  394. }
  395. #####################################
  396. sub
  397. panStamp_Ready($)
  398. {
  399. my ($hash) = @_;
  400. return DevIo_OpenDev($hash, 1, "panStamp_DoInit")
  401. if($hash->{STATE} eq "disconnected");
  402. # This is relevant for windows/USB only
  403. my $po = $hash->{USBDev};
  404. my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags);
  405. if($po) {
  406. ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status;
  407. }
  408. return ($InBytes && $InBytes>0);
  409. }
  410. ########################
  411. sub
  412. panStamp_SimpleWrite(@)
  413. {
  414. my ($hash, $msg, $nocr) = @_;
  415. return if(!$hash);
  416. my $name = $hash->{NAME};
  417. Log3 $name, 5, "SW: $msg";
  418. $msg .= "\r" unless($nocr);
  419. $hash->{USBDev}->write($msg) if($hash->{USBDev});
  420. syswrite($hash->{TCPDev}, $msg) if($hash->{TCPDev});
  421. syswrite($hash->{DIODev}, $msg) if($hash->{DIODev});
  422. # Some linux installations are broken with 0.001, T01 returns no answer
  423. select(undef, undef, undef, 0.01);
  424. }
  425. sub
  426. panStamp_Attr(@)
  427. {
  428. my @a = @_;
  429. return undef;
  430. }
  431. 1;
  432. =pod
  433. =item summary interface to a panStamp based SWAP network
  434. =item summary_DE Anbindung von panStamp basierten SWAP Netwerken
  435. =begin html
  436. <a name="panStamp"></a>
  437. <h3>panStamp</h3>
  438. <ul>
  439. The panStamp is a family of RF devices sold by <a href="http://www.panstamp.com">panstamp.com</a>.
  440. It is possible to attach more than one device in order to get better
  441. reception, fhem will filter out duplicate messages.<br><br>
  442. This module provides the IODevice for the <a href="#SWAP">SWAP</a> modules that implement the SWAP protocoll
  443. to communicate with the individual moths in a panStamp network.<br><br>
  444. Note: currently only panSticks are know to work. The panStamp shield for a Rasperry Pi is untested.
  445. <br><br>
  446. Note: this module may require the Device::SerialPort or Win32::SerialPort
  447. module if you attach the device via USB and the OS sets strange default
  448. parameters for serial devices.
  449. <br><br>
  450. <a name="panStamp_Define"></a>
  451. <b>Define</b>
  452. <ul>
  453. <code>define &lt;name&gt; panStamp &lt;device&gt; [&lt;address&gt; [&lt;channel&gt; [&lt;syncword&gt;]]]</code> <br>
  454. <br>
  455. USB-connected devices:<br><ul>
  456. &lt;device&gt; specifies the serial port to communicate with the panStamp.
  457. The name of the serial-device depends on your distribution, under
  458. linux the cdc_acm kernel module is responsible, and usually a
  459. /dev/ttyACM0 device will be created. If your distribution does not have a
  460. cdc_acm module, you can force usbserial to handle the panStamp by the
  461. following command:<ul>modprobe usbserial vendor=0x0403
  462. product=0x6001</ul>In this case the device is most probably
  463. /dev/ttyUSB0.<br><br>
  464. You can also specify a baudrate if the device name contains the @
  465. character, e.g.: /dev/ttyACM0@38400<br><br>
  466. If the baudrate is "directio" (e.g.: /dev/ttyACM0@directio), then the
  467. perl module Device::SerialPort is not needed, and fhem opens the device
  468. with simple file io. This might work if the operating system uses sane
  469. defaults for the serial parameters, e.g. some Linux distributions and
  470. OSX. <br><br>
  471. </ul>
  472. <br>
  473. The address is a 2 digit hex number to identify the moth in the panStamp network. The default is 01.<br>
  474. The channel is a 2 digit hex number to define the channel. the default is 00.<br>
  475. The syncword is a 4 digit hex number to identify the panStamp network. The default is B547.<br><br>
  476. Uppon initialization a broadcast message is send to the panStamp network to try to
  477. autodetect and autocreate all listening SWAP devices (i.e. all devices not in power down mode).
  478. </ul>
  479. <br>
  480. <a name="panStamp_Set"></a>
  481. <b>Set</b>
  482. <ul>
  483. <li>raw data<br>
  484. send raw data to the panStamp to be transmitted over the RF link.
  485. </li><br>
  486. </ul>
  487. <a name="panStamp_Get"></a>
  488. <b>Get</b>
  489. <ul>
  490. </ul>
  491. <a name="panStamp_Attr"></a>
  492. <b>Attributes</b>
  493. <ul>
  494. </ul>
  495. <br>
  496. </ul>
  497. =end html
  498. =cut