DevIo.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. ##############################################
  2. # $Id: DevIo.pm 17702 2018-11-07 19:02:28Z rudolfkoenig $
  3. package main;
  4. sub DevIo_CloseDev($@);
  5. sub DevIo_Disconnected($);
  6. sub DevIo_Expect($$$);
  7. sub DevIo_OpenDev($$$;$);
  8. sub DevIo_SetHwHandshake($);
  9. sub DevIo_SimpleRead($);
  10. sub DevIo_SimpleReadWithTimeout($$);
  11. sub DevIo_SimpleWrite($$$;$);
  12. sub DevIo_TimeoutRead($$);
  13. sub
  14. DevIo_setStates($$)
  15. {
  16. my ($hash, $val) = @_;
  17. $hash->{STATE} = $val;
  18. setReadingsVal($hash, "state", $val, TimeNow());
  19. }
  20. ########################
  21. # Try to read once from the device.
  22. # "private" function
  23. sub
  24. DevIo_DoSimpleRead($)
  25. {
  26. my ($hash) = @_;
  27. my ($buf, $res);
  28. if($hash->{USBDev}) {
  29. $buf = $hash->{USBDev}->input();
  30. } elsif($hash->{DIODev}) {
  31. $res = sysread($hash->{DIODev}, $buf, 4096);
  32. $buf = undef if(!defined($res));
  33. } elsif($hash->{TCPDev}) {
  34. $res = sysread($hash->{TCPDev}, $buf, 4096);
  35. $buf = "" if(!defined($res));
  36. } elsif($hash->{IODev}) {
  37. if($hash->{IOReadFn}) {
  38. $buf = CallFn($hash->{IODev}{NAME},"IOReadFn",$hash);
  39. } else {
  40. $buf = $hash->{IODevRxBuffer};
  41. $hash->{IODevRxBuffer} = "";
  42. $buf = "" if(!defined($buf));
  43. }
  44. }
  45. return $buf;
  46. }
  47. ########################
  48. # This is the function to read data, to be called in ReadFn.
  49. # If there is no data, sets the device to disconnected, which results in
  50. # polling via ReadyFn, trying to open it.
  51. sub
  52. DevIo_SimpleRead($)
  53. {
  54. my ($hash) = @_;
  55. my $buf = DevIo_DoSimpleRead($hash);
  56. ###########
  57. # Lets' try again: Some drivers return len(0) on the first read...
  58. if(defined($buf) && length($buf) == 0) {
  59. $buf = DevIo_SimpleReadWithTimeout($hash, 0.01); # Forum #57806
  60. }
  61. if(!defined($buf) || length($buf) == 0) {
  62. DevIo_Disconnected($hash);
  63. return undef;
  64. }
  65. return $buf;
  66. }
  67. ########################
  68. # wait at most timeout seconds until the file handle gets ready
  69. # for reading; returns undef on timeout
  70. # NOTE1: FHEM can be blocked for $timeout seconds, DO NOT USE IT!
  71. # NOTE2: This works on Windows only for TCP connections
  72. sub
  73. DevIo_SimpleReadWithTimeout($$)
  74. {
  75. my ($hash, $timeout) = @_;
  76. my $rin = "";
  77. vec($rin, $hash->{FD}, 1) = 1;
  78. my $nfound = select($rin, undef, undef, $timeout);
  79. return DevIo_DoSimpleRead($hash) if($nfound> 0);
  80. return undef;
  81. }
  82. ########################
  83. # Read until the timeout occures
  84. # NOTE1: FHEM WILL be blocked for $timeout seconds, DO NOT USE IT!
  85. # NOTE2: This works on Windows only for TCP connections
  86. sub
  87. DevIo_TimeoutRead($$)
  88. {
  89. my ($hash, $timeout) = @_;
  90. my $answer = "";
  91. for(;;) {
  92. my $rin = "";
  93. vec($rin, $hash->{FD}, 1) = 1;
  94. my $nfound = select($rin, undef, undef, $timeout);
  95. last if($nfound <= 0);
  96. my $r = DevIo_DoSimpleRead($hash);
  97. last if(!defined($r) || ($r == "" && $hash->{TCPDev}));
  98. $answer .= $r;
  99. }
  100. return $answer;
  101. }
  102. ########################
  103. # Function to write data
  104. sub
  105. DevIo_SimpleWrite($$$;$)
  106. {
  107. my ($hash, $msg, $type, $addnl) = @_; # Type: 0:binary, 1:hex, 2:ASCII
  108. return if(!$hash);
  109. my $name = $hash->{NAME};
  110. Log3 ($name, 5, $type ? "SW: $msg" : "SW: ".unpack("H*",$msg));
  111. $msg = pack('H*', $msg) if($type && $type == 1);
  112. $msg .= "\n" if($addnl);
  113. if($hash->{USBDev}){
  114. $hash->{USBDev}->write($msg);
  115. } elsif($hash->{TCPDev}) {
  116. syswrite($hash->{TCPDev}, $msg);
  117. } elsif($hash->{DIODev}) {
  118. syswrite($hash->{DIODev}, $msg);
  119. } elsif($hash->{IODev}) {
  120. CallFn($hash->{IODev}{NAME},"IOWriteFn",$hash,$msg);
  121. }
  122. select(undef, undef, undef, 0.001);
  123. }
  124. ########################
  125. # Write something, then read something
  126. # reopen device if timeout occurs and write again, then read again
  127. # NOTE1: FHEM can be blocked for $timeout seconds, DO NOT USE IT!
  128. sub
  129. DevIo_Expect($$$)
  130. {
  131. my ($hash, $msg, $timeout) = @_;
  132. my $name= $hash->{NAME};
  133. my $state= $hash->{STATE};
  134. if($state ne "opened") {
  135. Log3 $name, 2, "Attempt to write to $state device.";
  136. return undef;
  137. }
  138. # write something
  139. return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));
  140. # read answer
  141. my $answer= DevIo_SimpleReadWithTimeout($hash, $timeout);
  142. return $answer unless($answer eq "");
  143. # the device has failed to deliver a result
  144. DevIo_setStates($hash, "failed");
  145. DoTrigger($name, "FAILED");
  146. # reopen device
  147. # unclear how to know whether the following succeeded
  148. Log3 $name, 2, "$name: first attempt to read timed out, ".
  149. "trying to close and open the device.";
  150. # The next two lines are required to avoid a deadlock when the remote end
  151. # closes the connection upon DevIo_OpenDev, as e.g. netcat -l <port> does.
  152. DevIo_CloseDev($hash);
  153. DevIo_OpenDev($hash, 0, undef); # where to get the initfn from?
  154. # write something again
  155. return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));
  156. # read answer again
  157. $answer= DevIo_SimpleReadWithTimeout($hash, $timeout);
  158. # success
  159. if($answer ne "") {
  160. DevIo_setStates($hash, "opened");
  161. DoTrigger($name, "CONNECTED");
  162. return $answer;
  163. }
  164. # ultimate failure
  165. Log3 $name, 2,
  166. "$name: second attempt to read timed out, this is an unrecoverable error.";
  167. DoTrigger($name, "DISCONNECTED");
  168. return undef; # undef means ultimate failure
  169. }
  170. ########################
  171. # Open a device for reading/writing data.
  172. # Possible values for $hash->{DeviceName}:
  173. # - device@baud[78][NEO][012] => open device, set serial-line parameters
  174. # - hostname:port => TCP/IP client connection
  175. # - device@directio => open device without additional "magic"
  176. # - UNIX:(SEQPACKET|STREAM):filename => Open filename as a UNIX socket
  177. # - FHEM:DEVIO:IoDev[:IoPort] => Cascade I/O over another FHEM Device
  178. #
  179. # callback is only meaningful for TCP/IP (in which case a nonblocking connect
  180. # is executed) every cases. It will be called with $hash and a (potential)
  181. # error message. If $hash->{SSL} is set, SSL encryption is activated.
  182. sub
  183. DevIo_OpenDev($$$;$)
  184. {
  185. my ($hash, $reopen, $initfn, $callback) = @_;
  186. my $dev = $hash->{DeviceName};
  187. my $name = $hash->{NAME};
  188. my $po;
  189. my $baudrate;
  190. ($dev, $baudrate) = split("@", $dev);
  191. my ($databits, $parity, $stopbits) = (8, 'none', 1);
  192. my $nextOpenDelay = ($hash->{nextOpenDelay} ? $hash->{nextOpenDelay} : 60);
  193. # Call the callback if specified, simply return in other cases
  194. my $doCb = sub ($) {
  195. my ($r) = @_;
  196. Log3 $name, 1, "$name: Can't connect to $dev: $r" if(!$reopen && $r);
  197. $callback->($hash,$r) if($callback);
  198. return $r;
  199. };
  200. # Call initFn
  201. # if fails: disconnect, schedule the next polltime for reopen
  202. # if ok: log message, trigger CONNECTED on reopen
  203. my $doTailWork = sub {
  204. DevIo_setStates($hash, "opened");
  205. my $ret;
  206. if($initfn) {
  207. my $hadFD = defined($hash->{FD});
  208. $ret = &$initfn($hash);
  209. if($ret) {
  210. if($hadFD && !defined($hash->{FD})) { # Forum #54732 / ser2net
  211. DevIo_Disconnected($hash);
  212. $hash->{NEXT_OPEN} = time() + $nextOpenDelay;
  213. } else {
  214. DevIo_CloseDev($hash);
  215. Log3 $name, 1, "Cannot init $dev, ignoring it ($name)";
  216. }
  217. }
  218. }
  219. if(!$ret) {
  220. my $l = $hash->{devioLoglevel}; # Forum #61970
  221. if($reopen) {
  222. Log3 $name, ($l ? $l:1), "$dev reappeared ($name)";
  223. } else {
  224. Log3 $name, ($l ? $l:3), "$name device opened" if(!$hash->{DevioText});
  225. }
  226. }
  227. DoTrigger($name, "CONNECTED") if($reopen && !$ret);
  228. return undef;
  229. };
  230. if($baudrate =~ m/(\d+)(,([78])(,([NEO])(,([012]))?)?)?/) {
  231. $baudrate = $1 if(defined($1));
  232. $databits = $3 if(defined($3));
  233. $parity = 'odd' if(defined($5) && $5 eq 'O');
  234. $parity = 'even' if(defined($5) && $5 eq 'E');
  235. $stopbits = $7 if(defined($7));
  236. }
  237. if($hash->{DevIoJustClosed}) {
  238. delete $hash->{DevIoJustClosed};
  239. return &$doCb(undef);
  240. }
  241. $hash->{PARTIAL} = "";
  242. Log3 $name, 3, ($hash->{DevioText} ? $hash->{DevioText} : "Opening").
  243. " $name device $dev" if(!$reopen);
  244. if($dev =~ m/^UNIX:(SEQPACKET|STREAM):(.*)$/) { # FBAHA
  245. my ($type, $fname) = ($1, $2);
  246. my $conn;
  247. eval {
  248. require IO::Socket::UNIX;
  249. $conn = IO::Socket::UNIX->new(
  250. Type=>($type eq "STREAM" ? SOCK_STREAM:SOCK_SEQPACKET), Peer=>$fname);
  251. };
  252. if($@) {
  253. Log3 $name, 1, $@;
  254. return &$doCb($@);
  255. }
  256. if(!$conn) {
  257. Log3 $name, 1, "$name: Can't connect to $dev: $!" if(!$reopen);
  258. $readyfnlist{"$name.$dev"} = $hash;
  259. DevIo_setStates($hash, "disconnected");
  260. return &$doCb("");
  261. }
  262. $hash->{TCPDev} = $conn;
  263. $hash->{FD} = $conn->fileno();
  264. delete($readyfnlist{"$name.$dev"});
  265. $selectlist{"$name.$dev"} = $hash;
  266. } elsif($dev =~ m/^FHEM:DEVIO:(.*)(:(.*))/) { # Forum #46276
  267. my ($devName, $devPort) = ($1, $3);
  268. AssignIoPort($hash, $devName);
  269. if (defined($hash->{IODev})) {
  270. ($dev, $baudrate) = split("@", $hash->{DeviceName});
  271. $hash->{IODevPort} = $devPort if (defined($devPort));
  272. $hash->{IODevParameters} = $baudrate if (defined($baudrate));
  273. if (!CallFn($devName, "IOOpenFn", $hash)) {
  274. Log3 $name, 1, "$name: Can't open $dev!";
  275. DevIo_setStates($hash, "disconnected");
  276. return &$doCb("");
  277. }
  278. } else {
  279. DevIo_setStates($hash, "disconnected");
  280. return &$doCb("");
  281. }
  282. } elsif($dev =~ m/^(.+):([0-9]+)$/) { # host:port
  283. # This part is called every time the timeout (5sec) is expired _OR_
  284. # somebody is communicating over another TCP connection. As the connect
  285. # for non-existent devices has a delay of 3 sec, we are sitting all the
  286. # time in this connect. NEXT_OPEN tries to avoid this problem.
  287. if($hash->{NEXT_OPEN} && time() < $hash->{NEXT_OPEN}) {
  288. return &$doCb(undef); # Forum 53309
  289. }
  290. delete($readyfnlist{"$name.$dev"});
  291. my $timeout = $hash->{TIMEOUT} ? $hash->{TIMEOUT} : 3;
  292. # Do common TCP/IP "afterwork":
  293. # if connected: set keepalive, fill selectlist, FD, TCPDev.
  294. # if not: report the error and schedule reconnect
  295. my $doTcpTail = sub($) {
  296. my ($conn) = @_;
  297. if($conn) {
  298. delete($hash->{NEXT_OPEN});
  299. $conn->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1) if(defined($conn));
  300. } else {
  301. Log3 $name, 1, "$name: Can't connect to $dev: $!" if(!$reopen && $!);
  302. $readyfnlist{"$name.$dev"} = $hash;
  303. DevIo_setStates($hash, "disconnected");
  304. $hash->{NEXT_OPEN} = time() + $nextOpenDelay;
  305. return 0;
  306. }
  307. $hash->{TCPDev} = $conn;
  308. $hash->{FD} = $conn->fileno();
  309. $hash->{CD} = $conn;
  310. $selectlist{"$name.$dev"} = $hash;
  311. return 1;
  312. };
  313. if($callback) { # reuse the nonblocking connect from HttpUtils.
  314. use HttpUtils;
  315. my $err = HttpUtils_Connect({ # Nonblocking
  316. timeout => $timeout,
  317. url => $hash->{SSL} ? "https://$dev/" : "http://$dev/",
  318. NAME => $hash->{NAME},
  319. noConn2 => 1,
  320. callback=> sub() {
  321. my ($h, $err, undef) = @_;
  322. &$doTcpTail($err ? undef : $h->{conn});
  323. return &$doCb($err ? $err : &$doTailWork());
  324. }
  325. });
  326. return &$doCb($err) if($err);
  327. return undef; # no double callback: connect is running in bg now
  328. } else { # blocking connect
  329. my $conn = $haveInet6 ?
  330. IO::Socket::INET6->new(PeerAddr => $dev, Timeout => $timeout) :
  331. IO::Socket::INET ->new(PeerAddr => $dev, Timeout => $timeout);
  332. return "" if(!&$doTcpTail($conn)); # no callback: no doCb
  333. }
  334. } elsif($baudrate && lc($baudrate) eq "directio") { # w/o Device::SerialPort
  335. if(!open($po, "+<$dev")) {
  336. return &$doCb(undef) if($reopen);
  337. Log3 $name, 1, "$name: Can't open $dev: $!";
  338. $readyfnlist{"$name.$dev"} = $hash;
  339. DevIo_setStates($hash, "disconnected");
  340. return &$doCb("");
  341. }
  342. $hash->{DIODev} = $po;
  343. if( $^O =~ /Win/ ) {
  344. $readyfnlist{"$name.$dev"} = $hash;
  345. } else {
  346. $hash->{FD} = fileno($po);
  347. delete($readyfnlist{"$name.$dev"});
  348. $selectlist{"$name.$dev"} = $hash;
  349. }
  350. } else { # USB/Serial device
  351. if ($^O=~/Win/) {
  352. eval {
  353. require Win32::SerialPort;
  354. $po = new Win32::SerialPort ($dev);
  355. }
  356. } else {
  357. eval {
  358. require Device::SerialPort;
  359. $po = new Device::SerialPort ($dev);
  360. }
  361. }
  362. if($@) {
  363. Log3 $name, 1, $@;
  364. return &$doCb($@);
  365. }
  366. if(!$po) {
  367. return &$doCb(undef) if($reopen);
  368. Log3 $name, 1, "$name: Can't open $dev: $!";
  369. $readyfnlist{"$name.$dev"} = $hash;
  370. DevIo_setStates($hash, "disconnected");
  371. return &$doCb("");
  372. }
  373. $hash->{USBDev} = $po;
  374. if( $^O =~ /Win/ ) {
  375. $readyfnlist{"$name.$dev"} = $hash;
  376. } else {
  377. $hash->{FD} = $po->FILENO;
  378. delete($readyfnlist{"$name.$dev"});
  379. $selectlist{"$name.$dev"} = $hash;
  380. }
  381. if($baudrate) {
  382. $po->reset_error();
  383. my $p = ($parity eq "none" ? "N" : ($parity eq "odd" ? "O" : "E"));
  384. Log3 $name, 3, "Setting $name serial parameters to ".
  385. "$baudrate,$databits,$p,$stopbits" if(!$hash->{DevioText});
  386. $po->baudrate($baudrate);
  387. $po->databits($databits);
  388. $po->parity($parity);
  389. $po->stopbits($stopbits);
  390. $po->handshake('none');
  391. # This part is for some Linux kernel versions whih has strange default
  392. # settings. Device::SerialPort is nice: if the flag is not defined for
  393. # your OS then it will be ignored.
  394. $po->stty_icanon(0);
  395. #$po->stty_parmrk(0); # The debian standard install does not have it
  396. $po->stty_icrnl(0);
  397. $po->stty_echoe(0);
  398. $po->stty_echok(0);
  399. $po->stty_echoctl(0);
  400. # Needed for some strange distros
  401. $po->stty_echo(0);
  402. $po->stty_icanon(0);
  403. $po->stty_isig(0);
  404. $po->stty_opost(0);
  405. $po->stty_icrnl(0);
  406. }
  407. $po->write_settings;
  408. }
  409. return &$doCb(&$doTailWork());
  410. }
  411. sub
  412. DevIo_SetHwHandshake($)
  413. {
  414. my ($hash) = @_;
  415. $hash->{USBDev}->can_dtrdsr();
  416. $hash->{USBDev}->can_rtscts();
  417. }
  418. ########################
  419. # close the device, remove it from selectlist,
  420. # delete DevIo specific internals from $hash
  421. sub
  422. DevIo_CloseDev($@)
  423. {
  424. my ($hash,$isFork) = @_;
  425. my $name = $hash->{NAME};
  426. my $dev = $hash->{DeviceName};
  427. return if(!$dev);
  428. if($hash->{TCPDev}) {
  429. $hash->{TCPDev}->close();
  430. delete($hash->{TCPDev});
  431. } elsif($hash->{USBDev}) {
  432. if($isFork) { # SerialPort close resets the serial parameters.
  433. POSIX::close($hash->{USBDev}{FD});
  434. } else {
  435. $hash->{USBDev}->close() ;
  436. }
  437. delete($hash->{USBDev});
  438. } elsif($hash->{DIODev}) {
  439. close($hash->{DIODev});
  440. delete($hash->{DIODev});
  441. } elsif($hash->{IODev}) {
  442. eval {
  443. CallFn($hash->{IODev}{NAME}, "IOCloseFn", $hash);
  444. }; # ignore closing errors (e.g. caused by fork)
  445. delete($hash->{IODevParameters});
  446. delete($hash->{IODevPort});
  447. delete($hash->{IODevRxBuffer});
  448. delete($hash->{IODev});
  449. }
  450. ($dev, undef) = split("@", $dev); # Remove the baudrate
  451. delete($selectlist{"$name.$dev"});
  452. delete($readyfnlist{"$name.$dev"});
  453. delete($hash->{FD});
  454. delete($hash->{EXCEPT_FD});
  455. delete($hash->{PARTIAL});
  456. delete($hash->{NEXT_OPEN});
  457. }
  458. sub
  459. DevIo_IsOpen($)
  460. {
  461. my ($hash) = @_;
  462. return ($hash->{TCPDev} ||
  463. $hash->{USBDev} ||
  464. $hash->{DIODev} ||
  465. $hash->{IODevPort});
  466. }
  467. # Close the device, schedule the reopen via ReadyFn, trigger DISCONNECTED
  468. sub
  469. DevIo_Disconnected($)
  470. {
  471. my $hash = shift;
  472. my $dev = $hash->{DeviceName};
  473. my $name = $hash->{NAME};
  474. my $baudrate;
  475. ($dev, $baudrate) = split("@", $dev);
  476. return if(!defined($hash->{FD})); # Already deleted or RFR
  477. my $l = $hash->{devioLoglevel}; # Forum #61970
  478. Log3 $name, ($l ? $l:1), "$dev disconnected, waiting to reappear ($name)";
  479. DevIo_CloseDev($hash);
  480. $readyfnlist{"$name.$dev"} = $hash; # Start polling
  481. DevIo_setStates($hash, "disconnected");
  482. $hash->{DevIoJustClosed} = 1; # Avoid a direct reopen
  483. DoTrigger($name, "DISCONNECTED");
  484. }
  485. 1;