##############################################
# $Id: DevIo.pm 27247 2023-02-18 21:22:32Z rudolfkoenig $
package main;

use strict;

sub DevIo_CloseDev($@);
sub DevIo_DecodeWS($$);
sub DevIo_Disconnected($);
sub DevIo_Expect($$$);
sub DevIo_OpenDev($$$;$);
sub DevIo_SetHwHandshake($);
sub DevIo_SimpleRead($);
sub DevIo_SimpleReadWithTimeout($$);
sub DevIo_SimpleWrite($$$;$);
sub DevIo_TimeoutRead($$;$$);

sub
DevIo_setStates($$)
{
  my ($hash, $val) = @_;
  setReadingsVal($hash, "state", $val, TimeNow());
  if($hash->{devioNoSTATE}) {
    evalStateFormat($hash);
  } else {
    $hash->{STATE} = $val;
  }
}

sub
DevIo_getState($)
{
  my ($hash) = @_;
  return ReadingsVal($hash->{NAME}, "state", "disconnected")
}

########################
# Try to read once from the device.
# "private" function
sub
DevIo_DoSimpleRead($)
{
  my ($hash) = @_;
  my ($buf, $res);

  if($hash->{USBDev}) {
    $buf = $hash->{USBDev}->input();

  } elsif($hash->{DIODev}) {
    $res = sysread($hash->{DIODev}, $buf, 4096);
    $buf = undef if(!defined($res));

  } elsif($hash->{TCPDev}) {
    $res = sysread($hash->{TCPDev}, $buf, 4096);
    $buf = "" if(!defined($res));

  } elsif($hash->{IODev}) {

    if($hash->{IOReadFn}) {
      $buf = CallFn($hash->{IODev}{NAME},"IOReadFn",$hash);

    } else {
      $buf = $hash->{IODevRxBuffer};
      $hash->{IODevRxBuffer} = "";
      $buf = "" if(!defined($buf));
    }

  }
  return $buf;
}

########################
# This is the function to read data, to be called in ReadFn.
# If there is no data, sets the device to disconnected, which results in
# polling via ReadyFn, trying to open it.
sub
DevIo_SimpleRead($)
{
  my ($hash) = @_;
  my $buf = DevIo_DoSimpleRead($hash);

  if(length($buf) == 0 && $! == EWOULDBLOCK && $hash->{SSL} && $hash->{TCPDev}) {
    my $es = $hash->{TCPDev}->errstr;
    $hash->{wantWrite} = 1 if($es == &IO::Socket::SSL::SSL_WANT_WRITE);
    $hash->{wantRead}  = 1 if($es == &IO::Socket::SSL::SSL_WANT_READ);
    return "";
  }

  ###########
  # Lets' try again: Some drivers return len(0) on the first read...
  if(defined($buf) && length($buf) == 0) {
    $buf = DevIo_SimpleReadWithTimeout($hash, 0.01); # Forum #57806
  }

  if(!defined($buf) || length($buf) == 0) {
    DevIo_Disconnected($hash);
    return undef;
  }

  return DevIo_DecodeWS($hash, $buf) if($hash->{WEBSOCKET});
  return $buf;
}

########################
# wait at most timeout seconds until the file handle gets ready
# for reading; returns undef on timeout
# NOTE1: FHEM can be blocked for $timeout seconds, DO NOT USE IT!
# NOTE2: This works on Windows only for TCP connections
sub
DevIo_SimpleReadWithTimeout($$)
{
  my ($hash, $timeout) = @_;

  my $rin = "";
  vec($rin, $hash->{FD}, 1) = 1;
  my $nfound = select($rin, undef, undef, $timeout);
  return DevIo_DoSimpleRead($hash) if($nfound> 0);
  return undef;
}

########################
# Read until the timeout occures
# NOTE1: FHEM WILL be blocked for $timeout seconds, DO NOT USE IT!
# NOTE2: This works on Windows only for TCP connections
sub
DevIo_TimeoutRead($$;$$)
{
  my ($hash, $timeout, $maxlen, $regexp) = @_;

  my $answer = "";
  $timeout = 1 if(!$timeout);
  $maxlen = 1024 if(!$maxlen);    # Avoid endless loop
  for(;;) {
    my $rin = "";
    vec($rin, $hash->{FD}, 1) = 1;
    my $nfound = select($rin, undef, undef, $timeout);
    last if($nfound <= 0);      # timeout
    my $r = DevIo_DoSimpleRead($hash);
    last if(!defined($r) || ($r eq "" && $hash->{TCPDev}));
    $answer .= $r;
    last if(length($answer) >= $maxlen || ($regexp && $answer =~ m/$regexp/));
  }
  return $answer;
}

sub
DevIo_MaskWS($$)
{
  my ($opcode,$msg) = @_;
  $opcode = pack("C",$opcode|0x80);

  my $len = length($msg);
  my $lb;
  if($len < 126) {
    $lb = chr(0x80+$len);
  } else {
    if ($len < 65536) {
      $lb = chr(0xFE).pack('n',$len);
    } else {
      $lb = chr(0xFF).chr(0x00).chr(0x00).chr(0x00).chr(0x00).pack('N',$len);
    }
  }

  my $mask = pack("L",rand(2**32));
  my @m = unpack("C*", $mask);
  my $idx = 0;
  $msg = pack("C*", map { $_ ^ $m[$idx++ % 4] } unpack("C*", $msg));
  return $opcode.$lb.$mask.$msg;
}

my %wsCloseCode = (
  1000=>"normal",
  1001=>"going away",
  1002=>"protocol error",
  1003=>"cannot accept datatype",
  1007=>"inconsistent data",
  1008=>"policy violation",
  1009=>"too big",
  1010=>"missing extension",
  1011=>"unexpected condition"
);

sub
DevIo_Ping($;$)
{
  my ($hash,$msg) = @_;
  $msg="" if(!defined($msg));
  syswrite($hash->{TCPDev}, DevIo_MaskWS(0x9, $msg)) if($hash->{WEBSOCKET});
}

sub
DevIo_DecodeWS($$)
{
  my ($hash, $buf) = @_;
  # https://tools.ietf.org/html/rfc6455
  $hash->{".WSBUF"} = "" if(!defined($hash->{".WSBUF"}));
  $hash->{".WSBUF"} .= $buf;
  my $data = $hash->{".WSBUF"};
  return "" if(length($data) < 2);

  my $fin  = (ord(substr($data,0,1)) & 0x80)?1:0;
  my $op   = (ord(substr($data,0,1)) & 0x0F);
  my $mask = (ord(substr($data,1,1)) & 0x80)?1:0;
  my $len  = (ord(substr($data,1,1)) & 0x7F);
  my $i = 2;

  if( $len == 126 ) {
    return "" if(length($data) < 4);
    $len = unpack('n', substr($data, $i, 2));
    $i += 2;
  } elsif( $len == 127 ) {
    return "" if(length($data) < 10);
    $len = unpack( 'Q>', substr($hash->{".WSBUF"},$i,8) );
    $i += 8;
  }

  my @m;
  if($mask) {
    return "" if(length($data) < $i+4);
    @m = unpack("C*", substr($data,$i,4));
    $i += 4;
  }
  return "" if(length($data) < $i+$len);

  $hash->{".WSBUF"} = substr($data, $i+$len);
  $data = substr($data, $i, $len);
  if($mask) {
    my $idx = 0;
    $data = pack("C*", map { $_ ^ $m[$idx++ % 4] } unpack("C*", $data));
  }

  # $op: 0=>Continuation, 1=>Text, 2=>Binary, 8=>Close, 9=>Ping, 10=>Pong
  Log3 $hash, 5, "Websocket msg: OP:$op LEN:$len MASK:$mask FIN:$fin";
  if($op == 1) {              # Text
    $data = Encode::decode('UTF-8', $data) if($unicodeEncoding);

  } elsif($op == 8) {         # Close
    my $clCode = unpack("n", substr($data,0,2));
    $clCode = "$clCode ($wsCloseCode{$clCode})" if($wsCloseCode{$clCode});
    $clCode .= " ".substr($data, 2) if($len > 2);
    Log3 $hash, 5, "Websocket close, reason: $clCode";
    syswrite($hash->{TCPDev}, DevIo_MaskWS(0x8,$data));
    DevIo_CloseDev($hash);
    return undef;

  } elsif($op == 9) {   # Ping
    syswrite($hash->{TCPDev}, DevIo_MaskWS(0xA, $data)); # Pong
    Log3 $hash, 5, "Websocket ping: $data" if($data);
    return DevIo_DecodeWS($hash, "");

  } elsif($op == 10) {   # Pong
    Log3 $hash, 5, "Websocket pong: $data" if($data);
    return ""

  }

  if(length($hash->{".WSBUF"})) { # There is more data to digest
    my $nd = DevIo_DecodeWS($hash, "");
    $data .= $nd if(defined($nd));
  }

  return $data;
}

########################
# Function to write data
sub
DevIo_SimpleWrite($$$;$)
{
  my ($hash, $msg, $type, $addnl) = @_; # Type: 0:binary, 1:hex, 2:ASCII
  return if(!$hash);

  my $name = $hash->{NAME};
  Log3 $name, 5, "DevIo_SimpleWrite $name: ".($type ? $msg : unpack("H*",$msg));

  $msg = pack('H*', $msg) if($type && $type == 1);
  $msg .= "\n" if($addnl);
  if($hash->{USBDev}){
    $hash->{USBDev}->write($msg);

  } elsif($hash->{TCPDev}) {
    if($hash->{WEBSOCKET}) {
      $msg = Encode::encode('UTF-8', $msg)
        if($unicodeEncoding && !$hash->{binary});
      $msg = DevIo_MaskWS($hash->{binary} ? 0x2:0x1, $msg);
    }
    syswrite($hash->{TCPDev}, $msg);

  } elsif($hash->{DIODev}) { 
    syswrite($hash->{DIODev}, $msg);

  } elsif($hash->{IODev}) { 
    CallFn($hash->{IODev}{NAME},"IOWriteFn",$hash,$msg);

  }
  select(undef, undef, undef, 0.001);
}

########################
# Write something, then read something
# reopen device if timeout occurs and write again, then read again
# NOTE1: FHEM can be blocked for $timeout seconds, DO NOT USE IT!
sub
DevIo_Expect($$$)
{
  my ($hash, $msg, $timeout) = @_;
  my $name= $hash->{NAME};
  
  my $state= DevIo_getState($hash);
  if($state ne "opened") {
    Log3 $name, 2, "Attempt to write to $state device.";
    return undef;
  }
  # write something
  return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));
  # read answer
  my $answer= DevIo_SimpleReadWithTimeout($hash, $timeout);
  return $answer unless($answer eq "");
    # the device has failed to deliver a result
  DevIo_setStates($hash, "failed");
  DoTrigger($name, "FAILED");

  # reopen device
  # unclear how to know whether the following succeeded
  Log3 $name, 2, "$name: first attempt to read timed out, ".
                        "trying to close and open the device.";

  # The next two lines are required to avoid a deadlock when the remote end
  # closes the connection upon DevIo_OpenDev, as e.g. netcat -l <port> does.
  DevIo_CloseDev($hash);
  DevIo_OpenDev($hash, 0, undef); # where to get the initfn from? 

  # write something again
  return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));

  # read answer again
  $answer= DevIo_SimpleReadWithTimeout($hash, $timeout);

  # success
  if($answer ne "") {
    DevIo_setStates($hash, "opened");
    DoTrigger($name, "CONNECTED");
    return $answer;
  }

  # ultimate failure
  Log3 $name, 2,
    "$name: second attempt to read timed out, this is an unrecoverable error.";
  DoTrigger($name, "DISCONNECTED");
  return undef; # undef means ultimate failure
}


########################
# Open a device for reading/writing data.
# Possible values for $hash->{DeviceName}:
# - device@baud[78][NEO][012] => open device, set serial-line parameters
# - hostname:port => TCP/IP client connection (set $hash->{SSL}=>1 for TLS)
# - ws:hostname:port => websocket connection (wss: sets $hash->{SSL}=1)
# - device@directio => open device without additional "magic"
# - UNIX:(SEQPACKET|STREAM):filename => Open filename as a UNIX socket
# - FHEM:DEVIO:IoDev[:IoPort] => Cascade I/O over another FHEM Device
#
# callback is only meaningful for TCP/IP, in which case a nonblocking connect
# is executed. It will be called with $hash and a (potential) error message.
# If # $hash->{SSL} is set, SSL encryption is activated.

sub
DevIo_OpenDev($$$;$)
{
  my ($hash, $reopen, $initfn, $callback) = @_;
  my $dev = $hash->{DeviceName};
  my $name = $hash->{NAME};
  my $po;
  my $baudrate;
  ($dev, $baudrate) = split("@", $dev);
  my ($databits, $parity, $stopbits) = (8, 'none', 1);
  my $nextOpenDelay = ($hash->{nextOpenDelay} ? $hash->{nextOpenDelay} : 60);

  # Call the callback if specified, simply return in other cases
  my $doCb = sub ($) {
    my ($r) = @_;
    Log3 $name, 1, "$name: Can't connect to $dev: $r" if(!$reopen && $r);
    no strict "refs";
    $callback->($hash,$r) if($callback);
    use strict "refs";
    return ($callback ? undef : $r);
  };

  # Call initFn
  # if fails: disconnect, schedule the next polltime for reopen
  # if ok: log message, trigger CONNECTED on reopen
  my $doTailWork = sub {
    DevIo_setStates($hash, "opened");

    my $ret;
    if($initfn) {
      my $hadFD = defined($hash->{FD});
      no strict "refs";
      $ret = &$initfn($hash);
      use strict "refs";
      if($ret) {
        if($hadFD && !defined($hash->{FD})) { # Forum #54732 / ser2net
          DevIo_Disconnected($hash);
          $hash->{NEXT_OPEN} = gettimeofday() + $nextOpenDelay;

        } else {
          DevIo_CloseDev($hash);
          Log3 $name, 1, "Cannot init $dev, ignoring it ($name)";
        }
      }
    }

    if(!$ret) {
      my $l = $hash->{devioLoglevel}; # Forum #61970
      if($reopen) {
        Log3 $name, ($l ? $l:1), "$dev reappeared ($name)";
      } else {
        Log3 $name, ($l ? $l:3), "$name device opened" if(!$hash->{DevioText});
      }
    }

    DoTrigger($name, "CONNECTED") if(!$ret);
    return undef;
  };
  
  $baudrate = "" if(!defined($baudrate));
  if($baudrate =~ m/(\d+)(,([78])(,([NEO])(,([012]))?)?)?/) {
    $baudrate = $1 if(defined($1));
    $databits = $3 if(defined($3));
    $parity = 'odd'  if(defined($5) && $5 eq 'O');
    $parity = 'even' if(defined($5) && $5 eq 'E');
    $stopbits = $7 if(defined($7));
  }

  if($hash->{DevIoJustClosed}) {
    delete $hash->{DevIoJustClosed};
    return &$doCb(undef);
  }

  $hash->{PARTIAL} = "";
  my $l = $hash->{devioLoglevel}; # Forum #61970
  Log3 $name, ($l ? $l:3), ($hash->{DevioText} ? $hash->{DevioText} : "Opening").
       " $name device ". (AttrVal($name,"privacy",0) ? "(private)" : $dev)
       if(!$reopen);

  if($dev =~ m/^UNIX:(SEQPACKET|STREAM):(.*)$/) { # FBAHA
    my ($type, $fname) = ($1, $2);
    my $conn;
    eval {
      require IO::Socket::UNIX;
      $conn = IO::Socket::UNIX->new(
        Type=>($type eq "STREAM" ? SOCK_STREAM:SOCK_SEQPACKET), Peer=>$fname);
    };
    if($@) {
      Log3 $name, 1, $@;
      return &$doCb($@);
    }

    if(!$conn) {
      Log3 $name, 1, "$name: Can't connect to $dev: $!" if(!$reopen);
      $readyfnlist{"$name.$dev"} = $hash;
      DevIo_setStates($hash, "disconnected");
      return &$doCb("");
    }
    $hash->{TCPDev} = $conn;
    $hash->{FD} = $conn->fileno();
    delete($readyfnlist{"$name.$dev"});
    $selectlist{"$name.$dev"} = $hash;

  } elsif($dev =~ m/^FHEM:DEVIO:(.*)(:(.*))/) {      # Forum #46276
    my ($devName, $devPort) = ($1, $3);
    AssignIoPort($hash, $devName);
    if (defined($hash->{IODev})) {
      ($dev, $baudrate) = split("@", $hash->{DeviceName});
      $hash->{IODevPort} = $devPort if (defined($devPort));
      $hash->{IODevParameters} = $baudrate if (defined($baudrate));
      if (!CallFn($devName, "IOOpenFn", $hash)) {
        Log3 $name, 1, "$name: Can't open $dev!";
        DevIo_setStates($hash, "disconnected");
        return &$doCb("");
      }
    } else {
      DevIo_setStates($hash, "disconnected");
      return &$doCb("");
    }

  } elsif($dev =~ m,^(ws:|wss:)?([^/:]+):([0-9]+)(.*?)$,) {# TCP or websocket
   
    my ($proto, $host, $port, $path) = ($1 ? $1 : "", $2, $3, $4);
    my $hp = "$host:$port";
    if($proto eq "wss:")  {
      $hash->{SSL} = 1;
      $proto = "ws:";
    }
    if($proto eq "ws:")  {
      require MIME::Base64;
      return &$doCb('websocket is only supported with callback') if(!$callback);
    }
    $path = "/" if(!defined($path) || $path eq "");

    # This part is called every time the timeout (5sec) is expired _OR_
    # somebody is communicating over another TCP connection. As the connect
    # for non-existent devices has a delay of 3 sec, we are sitting all the
    # time in this connect. NEXT_OPEN tries to avoid this problem.
    if($hash->{NEXT_OPEN} && gettimeofday() < $hash->{NEXT_OPEN}) {
      return &$doCb(undef); # Forum 53309
    }

    delete($readyfnlist{"$name.$dev"});
    my $timeout = $hash->{TIMEOUT} ? $hash->{TIMEOUT} : 3;

    # Do common TCP/IP "afterwork":
    # if connected: set keepalive, fill selectlist, FD, TCPDev.
    # if not: report the error and schedule reconnect
    my $doTcpTail = sub($) {
      my ($conn) = @_;
      if($conn) {
        delete($hash->{NEXT_OPEN});
        $conn->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1) if(defined($conn));

      } else {
        Log3 $name, 1, "$name: Can't connect to $dev: $!" if(!$reopen && $!);
        $readyfnlist{"$name.$dev"} = $hash;
        DevIo_setStates($hash, "disconnected");
        DoTrigger($name, "DISCONNECTED") if(!$reopen);
        $hash->{NEXT_OPEN} = gettimeofday() + $nextOpenDelay;
        return 0;
      }

      $hash->{WEBSOCKET} = 1 if($proto eq "ws:");
      $hash->{TCPDev} = $conn;
      $hash->{FD} = $conn->fileno();
      $hash->{CD} = $conn;
      $selectlist{"$name.$dev"} = $hash;
      return 1;
    };

    if($callback) { # reuse the nonblocking connect from HttpUtils.
      use HttpUtils;

      my %header = ();
      if($proto eq "ws:") {
        %header = (
          "Connection" => "Upgrade",
          "Upgrade" => "websocket",
          "Sec-WebSocket-Key"=>encode_base64(pack("H*",createUniqueId()),""),
          "Sec-WebSocket-Version" => 13
        );
      }
      map { $header{$_} = $hash->{header}{$_} } keys %{$hash->{header}}
        if($hash->{header});

      my $err = HttpUtils_Connect({     # Nonblocking
        timeout => $timeout,
        url     => $hash->{SSL} ? "https://$hp$path" : "http://$hp$path",
        NAME    => $hash->{NAME},
        sslargs => $hash->{sslargs} ? $hash->{sslargs} : {},
        noConn2 => $proto eq "ws:" ? 0 : 1,
        keepalive=>$proto eq "ws:" ? 1 : 0,
        httpversion=>$proto eq "ws:" ? "1.1" : "1.0",
        header  => \%header,
        sslargs => $hash->{sslargs},
        callback=> sub() {
          my ($h, $err, undef) = @_;
          $err = "HTTP CODE $h->{code}"
                if($proto eq "ws:" && !$err && $h->{code} != 101);
          &$doTcpTail($err ? undef : $h->{conn});
          return &$doCb($err ? $err : &$doTailWork());
        }
      });
      return &$doCb($err) if($err);
      return undef;     # no double callback: connect is running in bg now

    } else {    # blocking connect
      my $conn = $haveInet6 ? 
          IO::Socket::INET6->new(PeerAddr => $hp, Timeout => $timeout) :
          IO::Socket::INET ->new(PeerAddr => $hp, Timeout => $timeout);
      return "" if(!&$doTcpTail($conn)); # no callback: no doCb
    }

  } elsif($baudrate && lc($baudrate) eq "directio") { # w/o Device::SerialPort

    if(!open($po, "+<$dev")) {
      return &$doCb(undef) if($reopen);
      Log3 $name, 1, "$name: Can't open $dev: $!";
      $readyfnlist{"$name.$dev"} = $hash;
      DevIo_setStates($hash, "disconnected");
      return &$doCb("");
    }

    $hash->{DIODev} = $po;

    if( $^O =~ /Win/ ) {
      $readyfnlist{"$name.$dev"} = $hash;
    } else {
      $hash->{FD} = fileno($po);
      delete($readyfnlist{"$name.$dev"});
      $selectlist{"$name.$dev"} = $hash;
    }


  } else {                              # USB/Serial device

    if ($^O=~/Win/) {
     eval {
       require Win32::SerialPort;
       $po = new Win32::SerialPort ($dev);
     }
    } else  {
     eval {
       require Device::SerialPort;
       $po = new Device::SerialPort ($dev);
     }
    }
    if($@) {
      Log3 $name,  1, $@;
      return &$doCb($@);
    }

    if(!$po) {
      return &$doCb(undef) if($reopen);
      Log3 $name, 1, "$name: Can't open $dev: $!";
      $readyfnlist{"$name.$dev"} = $hash;
      DevIo_setStates($hash, "disconnected");
      return &$doCb("");
    }
    $hash->{USBDev} = $po;
    if( $^O =~ /Win/ ) {
      $readyfnlist{"$name.$dev"} = $hash;
    } else {
      $hash->{FD} = $po->FILENO;
      delete($readyfnlist{"$name.$dev"});
      $selectlist{"$name.$dev"} = $hash;
    }

    if($baudrate) {
      $po->reset_error();
      my $p = ($parity eq "none" ? "N" : ($parity eq "odd" ? "O" : "E"));
      Log3 $name, 3, "Setting $name serial parameters to ".
                    "$baudrate,$databits,$p,$stopbits" if(!$hash->{DevioText});
      $po->baudrate($baudrate);
      $po->databits($databits);
      $po->parity($parity);
      $po->stopbits($stopbits);
      $po->handshake('none');

      # This part is for some Linux kernel versions whih has strange default
      # settings.  Device::SerialPort is nice: if the flag is not defined for
      # your OS then it will be ignored.

      $po->stty_icanon(0);
      #$po->stty_parmrk(0); # The debian standard install does not have it
      $po->stty_icrnl(0);
      $po->stty_echoe(0);
      $po->stty_echok(0);
      $po->stty_echoctl(0);

      # Needed for some strange distros
      $po->stty_echo(0);
      $po->stty_icanon(0);
      $po->stty_isig(0);
      $po->stty_opost(0);
      $po->stty_icrnl(0);
    }

    $po->write_settings;
  }

  return &$doCb(&$doTailWork());
}

sub
DevIo_SetHwHandshake($)
{
  my ($hash) = @_;
  $hash->{USBDev}->can_dtrdsr();
  $hash->{USBDev}->can_rtscts();
}

########################
# close the device, remove it from selectlist, 
# delete DevIo specific internals from $hash
sub
DevIo_CloseDev($@)
{
  my ($hash,$isFork) = @_;
  my $name = $hash->{NAME};
  my $dev = $hash->{DeviceName};

  return if(!$dev);
  
  if($hash->{TCPDev}) {
    if($isFork && $hash->{SSL}) { # Forum #94219
      $hash->{TCPDev}->close(SSL_no_shutdown => 1);
    } else {
      $hash->{TCPDev}->close();
    }
    delete($hash->{TCPDev});
    delete($hash->{".WSBUF"});
    delete($hash->{WEBSOCKET});

  } elsif($hash->{USBDev}) {
    if($isFork) { # SerialPort close resets the serial parameters.
      POSIX::close($hash->{USBDev}{FD});
    } else {
      $hash->{USBDev}->close() ;
    }
    delete($hash->{USBDev});

  } elsif($hash->{DIODev}) {
    close($hash->{DIODev});
    delete($hash->{DIODev});

  } elsif($hash->{IODev}) {
    eval {
      CallFn($hash->{IODev}{NAME}, "IOCloseFn", $hash);
    }; # ignore closing errors (e.g. caused by fork)
    delete($hash->{IODevParameters});
    delete($hash->{IODevPort});
    delete($hash->{IODevRxBuffer});
    delete($hash->{IODev});
    
  }
  ($dev, undef) = split("@", $dev); # Remove the baudrate
  delete($selectlist{"$name.$dev"});
  delete($readyfnlist{"$name.$dev"});
  delete($hash->{FD});
  delete($hash->{EXCEPT_FD});
  delete($hash->{PARTIAL});
  delete($hash->{NEXT_OPEN});
}

sub
DevIo_IsOpen($)
{
  my ($hash) = @_;
  return ($hash->{TCPDev} || 
          $hash->{USBDev} || 
          $hash->{DIODev} || 
          $hash->{IODevPort});
}


# Close the device, schedule the reopen via ReadyFn, trigger DISCONNECTED
sub
DevIo_Disconnected($)
{
  my $hash = shift;
  my $dev = $hash->{DeviceName};
  my $name = $hash->{NAME};
  my $baudrate;
  ($dev, $baudrate) = split("@", $dev);

  return if(!defined($hash->{FD}));                 # Already deleted or RFR

  my $l = $hash->{devioLoglevel}; # Forum #61970
  Log3 $name, ($l ? $l:1), "$dev disconnected, waiting to reappear ($name)";
  DevIo_CloseDev($hash);
  $readyfnlist{"$name.$dev"} = $hash;               # Start polling
  DevIo_setStates($hash, "disconnected");
  $hash->{DevIoJustClosed} = 1;                     # Avoid a direct reopen

  DoTrigger($name, "DISCONNECTED");
}

1;
