########################################################################################
#
# OWX_SER.pm
#
# FHEM module providing hardware dependent functions for the serial (USB) interface of OWX
#
# Prof. Dr. Peter A. Henning
# Norbert Truchsess
#
# $Id: OWX_SER.pm 6398 2014-08-12 21:22:18Z ntruchsess $
#
########################################################################################
#
# Provides the following methods for OWX
#
# Alarms
# Complex
# Define
# Discover
# Init
# Reset
# Verify
#
########################################################################################

package OWX_SER;

use strict;
use warnings;

use vars qw/@ISA/;

require "$main::attr{global}{modpath}/FHEM/DevIo.pm";

use Time::HiRes qw( gettimeofday );
use ProtoThreads;
no warnings 'deprecated';

########################################################################################
# 
# Constructor
#
########################################################################################

sub new() {
  my $class = shift;
  my $self = {
    interface => "serial",
    #-- module version
    version => 5.2,
    alarmdevs => [],
    devs => [],
    fams => [],
  };
  return bless $self,$class;
}

sub poll() {
  my ( $self ) = @_;
  my $hash = $self->{hash};
  if(defined($hash->{FD})) {
    my ($rout, $rin) = ('', '');
    vec($rin, $hash->{FD}, 1) = 1;
    select($rout=$rin, undef, undef, 0.1);
    my $mfound = vec($rout, $hash->{FD}, 1);
    if ($mfound) {
      if ($self->read()) {
        return 1;
      } else {
        main::OWX_ASYNC_Disconnect($hash);
      }
    }
  }
  return undef;
}

########################################################################################
# 
# Public methods
#
########################################################################################
#
# Define - Implements Define method
# 
# Parameter def = definition string
#
# Return undef if ok, otherwise error message
#
########################################################################################

sub Define ($$) {
  my ($self,$hash,$def) = @_;
  my @a = split("[ \t][ \t]*", $def);

  $self->{name} = $hash->{NAME};

  #-- check syntax
  if(int(@a) < 3){
    return "OWX_SER: Syntax error - must be define <name> OWX <serial-device>"
  }
  my $dev = $a[2];

  my $device;
  #-- network attached serial:
  if ( $dev =~ m/^(.+):([0-9]+)$/ ) {
    $hash->{DeviceName} = $dev;
    $device = $dev;
  } else {
    #-- when the specified device name contains @<digits> already, use it as supplied
    if ( $dev !~ m/\@\d*/ ){
      $hash->{DeviceName} = $dev."\@9600";
    }
    my $baudrate;
    ($device,$baudrate) = split('@',$dev);
    $self->{baud} = $baudrate ? $baudrate : 9600;
  }
  #-- let fhem.pl MAIN call OWX_Ready when setup is done.
  $main::readyfnlist{"$hash->{NAME}.$device"} = $hash;
  $self->{hash} = $hash;
  return undef;
}

########################################################################################
#
# Alarms - Find devices on the 1-Wire bus, which have the alarm flag set
#
# Return number of alarmed devices
#
########################################################################################

sub get_pt_alarms() {
  my ($self) = @_;
  my $pt_next;
  return PT_THREAD(sub {
    my ($thread) = @_;
    PT_BEGIN($thread);
    $thread->{alarmdevs} = [];
    #-- Discover all alarmed devices on the 1-Wire bus
    $self->first($thread);
    do {
      $pt_next = $self->pt_next($thread,"alarms");
      PT_WAIT_THREAD($pt_next);
      die $pt_next->PT_CAUSE() if ($pt_next->PT_STATE() == PT_ERROR || $pt_next->PT_STATE() == PT_CANCELED);
      $self->next_response($thread,"alarms");
    } while( $thread->{LastDeviceFlag}==0 );
    main::Log3($self->{name},5, " Alarms = ".join(' ',@{$thread->{alarmdevs}}));
    PT_EXIT($thread->{alarmdevs});
    PT_END;
  });
}

########################################################################################
#
# Discover - Find devices on the 1-Wire bus
#
# Parameter hash = hash of bus master
#
# Return 1, if alarmed devices found, 0 otherwise.
#
########################################################################################

sub get_pt_discover() {
  my ($self) = @_;
  my $pt_next;
  return PT_THREAD(sub {
    my ($thread) = @_;
    PT_BEGIN($thread);
    #-- Discover all alarmed devices on the 1-Wire bus
    $self->first($thread);
    do {
      $pt_next = $self->pt_next($thread,"discover");
      PT_WAIT_THREAD($pt_next);
      die $pt_next->PT_CAUSE() if ($pt_next->PT_STATE() == PT_ERROR || $pt_next->PT_STATE() == PT_CANCELED);
      $self->next_response($thread,"discover");
    } while( $thread->{LastDeviceFlag}==0 );
    PT_EXIT($thread->{devs});
    PT_END;
  });
}

########################################################################################
# 
# Init - Initialize the 1-wire device
#
# Parameter hash = hash of bus master
#
# Return 1 or Errormessage : not OK
#        0 or undef : OK
#
########################################################################################

sub initialize() {
  my ($self) = @_;
  my ($i,$j,$k,$l,$res,$ret,$ress);
  #-- Second step in case of serial device: open the serial device to test it
  my $hash = $self->{hash};
  my $msg = "OWX_SER: Serial device $hash->{DeviceName}";
  main::DevIo_OpenDev($hash,$self->{reopen},undef);
  return undef unless $hash->{STATE} eq "opened";

  #-- Third step detect busmaster on serial interface

  my $name = $self->{name};
  my $ress0 = "OWX_SER::Detect 1-Wire bus $name: interface ";
  $ress     = $ress0;

  my $interface;
  
  require "$main::attr{global}{modpath}/FHEM/OWX_DS2480.pm";
  my $ds2480 = OWX_DS2480->new($self);

  if (defined (my $hwdevice = $hash->{USBDev})) {
    #force master reset in DS2480
    $hwdevice->reset_error();
    $hwdevice->purge_all;
    $hwdevice->baudrate(4800);
    $hwdevice->write_settings;
    $hwdevice->write("\x00");
    select(undef,undef,undef,0.5);
    #-- timing byte for DS2480
    $ds2480->start_query();
    $hwdevice->baudrate(9600);
    $hwdevice->write_settings;
    $ds2480->query("\xC1",0);
    $hwdevice->baudrate($self->{baud});
    $hwdevice->write_settings;
  } else {
    #-- for serial over network we cannot reset but just send the timing byte
    $ds2480->start_query();
    $ds2480->query("\xC1",0);
  }

  #-- Max 4 tries to detect an interface
  for($l=0;$l<100;$l++) {
    #-- write 1-Wire bus (Fig. 2 of Maxim AN192)
    $ds2480->start_query();
    $ds2480->query("\x17\x45\x5B\x0F\x91",5);
    my $until = gettimeofday + main::AttrVal($hash->{NAME},"timeout",1);
    eval {
      do {
        $ds2480->poll();
      } until ($ds2480->response_ready() or gettimeofday >= $until);
    };
    $res = $ds2480->{string_in};
    #-- process 4/5-byte string for detection
    if( !defined($res)){
      $res="";
      $ret=1;
    }elsif( ($res eq "\x16\x44\x5A\x00\x90") || ($res eq "\x16\x44\x5A\x00\x93")){
      $ress .= "master DS2480 detected for the first time";
      $interface="DS2480";
      $ret=0;
    } elsif( $res eq "\x17\x45\x5B\x0F\x91"){
      $ress .= "master DS2480 re-detected";
      $interface="DS2480";
      $ret=0;
    } elsif( ($res eq "\x17\x0A\x5B\x0F\x02") || ($res eq "\x00\x17\x0A\x5B\x0F\x02") || ($res eq "\x30\xf8\x00") || ($res eq "\x06\x00\x09\x07\x80") || ($res eq "\x17\x41\xAB\x20\xFC")){
      $ress .= "passive DS9097 detected";
      $interface="DS9097";
      $ret=0;
    } else {
      $ret=1;
    }
    last 
      if( $ret==0 );
    $ress .= "not found, answer was ";
    for($i=0;$i<length($res);$i++){
      $j=int(ord(substr($res,$i,1))/16);
      $k=ord(substr($res,$i,1))%16;
      $ress.=sprintf "0x%1x%1x ",$j,$k;
    }
    main::Log3($hash->{NAME},4, $ress);
    $ress = $ress0;
    #-- sleeping for some time
    select(undef,undef,undef,0.5);
  }
  if( $ret == 1 ){
    $interface=undef;
    $ress .= "not detected, answer was ";
    for($i=0;$i<length($res);$i++){
      $j=int(ord(substr($res,$i,1))/16);
      $k=ord(substr($res,$i,1))%16;
      $ress.=sprintf "0x%1x%1x ",$j,$k;
    }
  }
  $self->{interface} = $interface;
  main::Log3($hash->{NAME},3, $ress);
  if ($interface eq "DS2480") {
    return $ds2480;
  } elsif ($interface eq "DS9097") {
    require "$main::attr{global}{modpath}/FHEM/OWX_DS9097.pm";
    return OWX_DS9097->new($self);
  } else {
    die $ress;
  }
}

sub exit($) {
  my ($self) = @_;
  main::DevIo_Disconnected($self->{hash});
  $self->{interface} = "serial";
  $self->{reopen} = 1;
}

########################################################################################
#
# Verify - Verify a particular device on the 1-Wire bus
#
# Parameter hash = hash of bus master, dev =  8 Byte ROM ID of device to be tested
#
# Return 1 : device found
#        0 : device not
#
########################################################################################

sub get_pt_verify($) {
  my ($self,$dev) = @_;
  my $pt_next;
  return PT_THREAD(sub {
    my ($thread) = @_;
    my $i;
    PT_BEGIN($thread);  
    #-- from search string to byte id
    my $devs=$dev;
    $devs=~s/\.//g;
    for($i=0;$i<8;$i++){
      $thread->{ROM_ID}->[$i]=hex(substr($devs,2*$i,2));
    }
    #-- reset the search state
    $thread->{LastDiscrepancy} = 65;
    $thread->{LastDeviceFlag} = 0;
    $thread->{LastFamilyDiscrepancy} = 0;
    
    #-- now do the search
    $pt_next = $self->pt_next($thread,"verify");
    PT_WAIT_THREAD($pt_next);
    die $pt_next->PT_CAUSE() if ($pt_next->PT_STATE() == PT_ERROR || $pt_next->PT_STATE() == PT_CANCELED);
    $self->next_response($thread,"verify");
    my $dev2=sprintf("%02X.%02X%02X%02X%02X%02X%02X.%02X",@{$thread->{ROM_ID}});
    #-- reset the search state
    $thread->{LastDiscrepancy} = 0;
    $thread->{LastDeviceFlag} = 0;
    $thread->{LastFamilyDiscrepancy} = 0;
    #-- check result
    if ($dev eq $dev2){
      PT_EXIT(1);
    }else{
      PT_EXIT(0);
    }
    PT_END;
  });
};

#######################################################################################
#
# First - Find the 'first' devices on the 1-Wire bus
#
# Parameter hash = hash of bus master, mode
#
# Return 1 : device found, ROM number pushed to list
#        0 : no device present
#
########################################################################################

sub first($) {
  my ($self,$thread) = @_;
  #-- reset the search state
  $thread->{LastDiscrepancy} = 0;
  $thread->{LastDeviceFlag} = 0;
  $thread->{LastFamilyDiscrepancy} = 0;
  $thread->{ROM_ID} = [0,0,0,0,0,0,0,0];
}

sub next_response($) {
  my ($self,$thread,$mode) = @_;

  #-- character version of device ROM_ID, first byte = family 
  my $dev=sprintf("%02X.%02X%02X%02X%02X%02X%02X.%02X",@{$thread->{ROM_ID}});

  #-- mode was to verify presence of a device
  if ($mode eq "verify") {
    main::Log3($self->{name},5, "OWX_SER::Search: device verified $dev");
  #--check if we really found a device
  } elsif( main::OWX_CRC($thread->{ROM_ID})!= 0) {
    #-- reset the search
    main::Log3($self->{name},1, "OWX_SER::Search CRC failed : $dev");
    $thread->{LastDiscrepancy} = 0;
    $thread->{LastDeviceFlag} = 0;
    $thread->{LastFamilyDiscrepancy} = 0;
    die "OWX_SER::Search CRC failed : $dev";
  }

  #--
  if( $thread->{LastDiscrepancy}==$thread->{LastFamilyDiscrepancy} ){
      $thread->{LastFamilyDiscrepancy}=0;
  }

  #-- mode was to discover devices
  if( $mode eq "discover" ) {
    #-- check families
    my $famfnd=0;
    foreach (@{$self->{fams}}){
      if( substr($dev,0,2) eq $_ ){
        #-- if present, set the fam found flag
        $famfnd=1;
        last;
      }
    }
    push(@{$self->{fams}},substr($dev,0,2)) if( !$famfnd );
    foreach (@{$thread->{devs}}){
      if( $dev eq $_ ){        
        #-- if present, set the last device found flag
        $thread->{LastDeviceFlag}=1;
        last;
      }
    }
    if( $thread->{LastDeviceFlag}!=1 ){
      #-- push to list
      push(@{$thread->{devs}},$dev);
      main::Log3($self->{name},5, "OWX_SER::Search: new device found $dev");
    }
  #-- mode was to discover alarm devices 
  } elsif ( $mode eq "alarms" ) {
    for(my $i=0;$i<@{$thread->{alarmdevs}};$i++){
      if( $dev eq ${$thread->{alarmdevs}}[$i] ){        
        #-- if present, set the last device found flag
        $thread->{LastDeviceFlag}=1;
        last;
      }
    }
    if( $thread->{LastDeviceFlag}!=1 ){
    #--push to list
      push(@{$thread->{alarmdevs}},$dev);
      main::Log3($self->{name},5, "OWX_SER::Search: new alarm device found $dev");
    }
  }
  return 1;
}

1;
