DevIo.pm 14 KB

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