#!/usr/bin/perl
#  $Id$
# -----------------------------------------------------------------------------
#  Purpose:
#    - probe the camera
#    - pre-tune camera befor for retriever (RC and Audio)
# -----------------------------------------------------------------------------
#  Call:
#    probe DEVID=123   
#    probe DEVID=123 PROBE=FAST  
#    probe DEVID=123 DEVIP=192.168.17.177 [ ... ]
#    probe DEVIP=192.168.17.177 USRNAME=admin PASSWD=pass PROBE=DEFINE
# -----------------------------------------------------------------------------
#  Does:
#   0. can be called without DEVID argument. 
#      In this case DEVIP,USRNAME,PASSWD should be provided in command line
#   1. load $APL_CONF/<DEVID>/conf if DEVID is provided
#   2. combine conf and args into one hash
#   3. connect to camera over http and read MODELID and FIRMWARE
#   4. report MODELID|FIRMWARE|STATUS to $APL_CONF/<DEVID>/conf.probe and stdout
#   5. if PROBE=DEFINE then get
#         IMAGESIZE_LIST,MEDIA_FORMAT_LIST
#         AUDIO_LIST,AUDIO_FORMAT_LIST
#         SNAPSHOT (picture)
#   6. set RC camera attributes    if mpeg4 | h264
#   7. set AUDIO camera attributes if AUDIO is on 
#   8. example output:
#       AUDIO_SET=OK
#       FIRMWARE=5.02
#       MODELID=Q1755
#       RC_SET=OK
#       STATUS=OK
#       -------------
#       AUDIO_SET=NONE
#       FIRMWARE=5.02
#       MODELID=Q1755
#       RC_SET=NONE
#       STATUS=OK
#       SNAPSHOT=/tmp/probe/192.168.17.177-12367576123.jpg
#       IMAGESIZE_LIST=640x480,480x360,320x240,240x180,176x144,160x120
#       MEDIA_FORMAT_LIST=mjpeg,h264
#       AUDIO_LIST=off,on
#       AUDIO_FORMAT_LIST=g711,g726,aac
#   9. sample errors: 
#       STATUS=ERROR: PCE-0001 [101] configuration is not found
#       STATUS=ERROR: PCE-0002 [101] USRNAME and PASSWD should provided
#       STATUS=ERROR: PCE-0003 [101] DEVIP is not defined
#       STATUS=ERROR: PCE-0500 [101] Device does not respond (http://207.107.163.123:80)
#       STATUS=ERROR: PCE-0401 [101] Authorization error 
#       STATUS=ERROR: PCE-0403 [101] Forbidden 
#       STATUS=ERROR: PCE-0030 [101] Cannot get MODELID
#       note: [101] is DEVID
#  10. warnings:
#       STATUS=WARNING: PCW-0001 [101] MODELID does not match configuration
#       STATUS=WARNING: PCW-0002 [101] FIRMWARE does not match configuration
#       
# -----------------------------------------------------------------------------
#  Note:
#   1. CAMERAMODEL file is obsolite. ptz_axisv2.pl & ptz_udp.pl has to be modified
#   2. Script mast be in the directory .../camera/<BRAND>/bin/
# -----------------------------------------------------------------------------
#  Author: teetov, 03/22/10
#  Edited by: Alexey Tsibulnik
#  QA by:
#  Copyright: videoNEXT Network Solutions, Inc, 2010
# -----------------------------------------------------------------------------
#
use strict;
use warnings;

use Socket qw(:DEFAULT);
use SKM::DB;
use Errno 'EADDRINUSE';
use Data::Dumper;
use NextCAM::Conf;
use Node::Conf;
use Device::Conf;

my $CTRL_PORT = 7000;
my $TIMEOUT = 2;
my $RESTART_TIMEOUT=8;
my $IP = get_own_ip();
my $UDP_PORT_BASE = 54000;
my $MAX_PORT = 65535;

# OpCodes
my $OPC_QUERY_AUTH    = 0x26;
my $OPC_QUERY_VIDEO   = 0x11;
my $OPC_START_VIDEO   = 0x12;
my $OPC_STOP_VIDEO    = 0x13;
my $OPC_VERSION_VIDEO = 0x23;
my $OPC_RESET         = 0x07;
my $OPC_API_GOP_CONTROL = 0x16;

# Resolution mapping
my %ResMap = (
	'D1'    => 0,
	'2/3D1' => 1,
	'1/2D1' => 2,
	'SIF'   => 3
);
my %Result;
my $Conf; # Camera configuration
#my %CamCfg = ();
my $RadCnt = 0; # Number of Radiant cameras in the system
my $PrefPort;
my @VS; # Video settings
my $Err =  ""; # Last error text

# VIDEO SETTINGS STRUCT
#
# typedef struct
# {
#    int     nIpAddr;		//32 bits
#    int     nUdpPort;		//lower 16 bits
#    int     bUseRTP;		//off=0, on=1
#    int     bDoPacing;		//0
#    int     bDoSync;		//0
#    int     bUseKeepAlive;	//0
#    int     nStreamBitrate;	//range 1200000-7500000 [15000000 AVN220]
#    int     eVideoInput;	//Svideo=0, Comp=1
#    int     eTvFormat;		//NTSC=0, PAL=1
#    int     eVbr;			//0
#    int     eVideoResolution;	//D1=0,2/3D1=1,1/2D1=2,SIF=3
#    int     eAudioBitRate;	//Off=0,256k=1,384k=2
#    int     eStreamType;	//Transport Stream=2
#    int     eErrorCorrection;	//off=0, on=1 (only with RTP)
#    int     nFecBurstSize;	//range 1-3
#    int     nFecNumBursts;	//range 3-10
# } SessionSettings;

sub errdie
{
	$Err = shift;
	die $Err;
}

sub hexbytes
{
	my $data = shift;
	
	my $bits = unpack('H*', $data);
	$bits=~s/([0-9aAbBcCdDeEfF]{2})/$1 /g;
	return $bits;
}

sub hdr
{
	return pack('C', shift) . "\x00" x 3;
}

sub resp_ok
{
	my ($opcode, $data) = @_;
	
	my $fb = unpack('C', substr($data, 0, 1));
	return undef if ($fb & 0x3f) != $opcode;
	return $fb & 0x80 ? 0 : $fb & 0x40 ? 1 : undef;
}

sub get_own_ip
{
	my $dbm;
	my $ip = eval {
		my $dbm = DBMaster({PrintError=>0,RaiseError=>1});
		my $ra = $dbm->selectall_arrayref(
			"SELECT val AS ip FROM _objs o 
			    INNER JOIN _obj_attr oa ON o.obj=oa.obj
			WHERE otype='D' and subtype='N' 
			    and o.name=? and attr='IP'",
			undef,
			UNI(),
		);
		$ra->[0][0];
	};
	my $err = $@;
	eval { $dbm->disconnect } if $dbm;
	die "Unable to get own IP: $@\n" if $err;
	
	return $ip;
}

sub udp_port_search
{
	my $protocol = getprotobyname('udp');
	my $port = $PrefPort || $UDP_PORT_BASE;
	my $ok = 0;
	
	socket(SOCK, AF_INET, SOCK_DGRAM, $protocol)
		or return undef;
	while ($port <= $MAX_PORT && !$ok) {
		my $local_addr = sockaddr_in($port, INADDR_ANY);
		bind(SOCK, $local_addr) or do {
			$port+=(2*$RadCnt),next if $! == EADDRINUSE;
			last;
		};
		$ok = 1;
	}
	close SOCK;
	
	return $ok ? $port : 0;
}

sub send_msg
{
	my ($req,$timeout) = @_;
        $timeout=$TIMEOUT if not defined $timeout;
	my $data;
	
	my $protocol = getprotobyname('udp');
	socket(SOCK, AF_INET, SOCK_DGRAM, $protocol)
		or errdie "socket() failed: $!";
	my $dest_addr = sockaddr_in($CTRL_PORT, inet_aton($Conf->{DEVIP}));
	
	send(SOCK, $req, 0, $dest_addr) 
		or errdie "send() failed: $!";
	eval {
		local $SIG{ALRM} = sub{ die 'TIMEOUT' };
		alarm $timeout;
		recv(SOCK, $data, length($req), 0)
			or errdie "recv() failed: $!";
	};
	close(SOCK);
        alarm 0;                        # disable alarm
	errdie "timeout on recv" if $@;
	
	return $data;
}

sub cmd_query_auth
{
	my $req = hdr($OPC_QUERY_AUTH);
	$req .= $Conf->{USRNAME} . "\x00" x (32 - length($Conf->{USRNAME}));
	$req .= $Conf->{PASSWD}  . "\x00" x (32 - length($Conf->{PASSWD}));
	
	my $data = eval { send_msg($req) };
	return undef if $@;
	
	return resp_ok($OPC_QUERY_AUTH, $data);
}

sub cmd_query_video
{
	my $req = hdr($OPC_QUERY_VIDEO);
	$req .= "\x00" x 64;
	
	my $data = eval { send_msg($req) };
	return undef if $@;
	
	my $status = resp_ok($OPC_QUERY_VIDEO, $data);
	
	my @vs = unpack('N16', substr($data, 4, 64));
	return [ @vs ];
}

sub cmd_start_video
{
	my $req = hdr($OPC_START_VIDEO);
	
	# Login info
	$req .= $Conf->{USRNAME} . "\x00" x (32 - length($Conf->{USRNAME}));
	$req .= $Conf->{PASSWD}  . "\x00" x (32 - length($Conf->{PASSWD}));
	# Get UDP port for incoming stream
	#
	my $udp_port = udp_port_search;
	return undef unless $udp_port;
	$Result{RTP_UNICAST_PORT} = $udp_port;
	
	# Fill required fields in Video Settings struct
	#
	$VS[0]  = unpack('N', inet_aton($IP));
	$VS[1]  = $udp_port;
	$VS[2]  = 1; # Use RTP
	$VS[6]  = $Conf->{RC_TARGETBITRATE} * 1024; # Bitrate
	$VS[7]  = $Conf->{VIDEO_INPUT} eq 'Svideo' ? 0 : 1;
	$VS[8]  = 0; # NTSC
	$VS[10] = $ResMap{$Conf->{IMAGESIZE}}; # Resolution
	$VS[11] = 0; # No audio
	$VS[12] = 0; # Elementary stream
	$VS[13] = 0; # No error correction
	
	$req .= pack('N16', @VS); # Convert to network byte order
	
	my $data = eval { send_msg($req) };
	return undef if $@;
	return resp_ok($OPC_START_VIDEO, $data);
}

sub cmd_stop_video
{
	my $req = hdr($OPC_STOP_VIDEO);
	# Login info
	$req .= $Conf->{USRNAME} . "\x00" x (32 - length($Conf->{USRNAME}));
	$req .= $Conf->{PASSWD}  . "\x00" x (32 - length($Conf->{PASSWD}));
	
	my $data = eval { send_msg($req) };
	return undef if $@;
	return resp_ok($OPC_STOP_VIDEO, $data);
}

sub cmd_version_video
{
	my $req = "\x23\x34\x00\x00"; # Special format
	$req .= "\x00" x 80;
	my $data = eval { send_msg($req) };
	unless ($@) {
		my $version = substr($data, 4);
		$Result{FIRMWARE} = $1 if $version=~/^(.+?),/;
	}
}

sub cmd_reset
{
        my $req = pack('C',$OPC_RESET)."\x00\x01\x00"; # CMD_RESET (0x07): reset and reboot
        $req .= $Conf->{USRNAME} . "\x00" x (32 - length($Conf->{USRNAME}));
        $req .= $Conf->{PASSWD}  . "\x00" x (32 - length($Conf->{PASSWD}));
        my $start=time;
        my $data = eval { send_msg($req,$RESTART_TIMEOUT) };
        my $spent=time-$start;
        sleep($RESTART_TIMEOUT-$spent) if $RESTART_TIMEOUT>$spent;    # sleep the rest of timeout
        return undef if $@;
        # Wait for encoder to boot
        sleep 1 until defined cmd_query_video;
        return resp_ok($OPC_RESET, $data);
}

sub cmd_api_gop_control
{
	my $gop_length = $Conf->{RC_GOP_LENGTH};
	my $gop_distance = $Conf->{RC_GOP_DISTANCE};
	# Normalize RC_GOP_* parameter values
	$gop_length = 1 if $gop_length < 1;
	$gop_length = 19 if $gop_length > 19;
	$gop_distance = 0 if $gop_distance < 0;
	$gop_distance = 3 if $gop_distance > 3;
	# Send request
	my $req = pack('C',$OPC_API_GOP_CONTROL)."\x01\x00\x00";
	$req .= $Conf->{USRNAME} . "\x00" x (32 - length($Conf->{USRNAME}));
        $req .= $Conf->{PASSWD}  . "\x00" x (32 - length($Conf->{PASSWD}));
        $req .= pack('C',$gop_distance); # GOP distance
        $req .= pack('C',$gop_length); # GOP length
        my $data = eval { send_msg($req,$RESTART_TIMEOUT) };
        return undef if $@;
        return resp_ok($OPC_API_GOP_CONTROL, $data);
}

sub init
{
	# --------- check required parameters: DEVID,USRNAME,PASSWD,
	$Conf = ProbeInit();
	ProbeErr("PCE-0001","configuration is not found") if not defined $Conf->{DEVID}; 
	ProbeErr("PCE-0003","DEVIP is not defined")       if not defined $Conf->{DEVIP};
	ProbeErr("PCE-0002","USRNAME and PASSWD should be provided") 
                 if not defined $Conf->{USRNAME} or not defined $Conf->{PASSWD};
	
	if($Conf->{DEVID}) {
		my %cfgs = GetCfgs('DEVICETYPE' => 'CAMERA');
	    
	        # Find preferred RTP port for this camera
	        #
	        $PrefPort = $UDP_PORT_BASE;
	        my $found = 0;
	        foreach my $dev (sort keys %cfgs) {
    			next if $cfgs{$dev}{CAMERAMODEL} ne 'Radiant';
		        $found = 1 if $dev eq $Conf->{DEVID};
		        $PrefPort += 2 unless $found;
		        $RadCnt++;
		}
	    
		die "No Radiant cameras in the system!\n" unless $RadCnt;
	}
	
	%Result = (MODELID=>'0',FIRMWARE=>'0.0',STATUS=>'OK',RC_SET=>'NONE',AUDIO_SET=>'NONE');
}

sub define
{
	my $raVS = cmd_query_video;
	
	ProbeErr("PCE-0500","Device does not respond",$Err) 
		unless defined $raVS; # not a Radiant camera
	
	$Result{MODELID} = 'V4400';
	@VS = @$raVS;
	
	# Verify login and pass
	my $auth_ok = 1; # AT: work around #cmd_query_auth;
	ProbeErr("PCE-0401","Authorization error") 
		unless $auth_ok;
	
	# Check version
	cmd_version_video;
	
	return 1;
}

sub configure
{
        # we recommend that the first call to the encoder be an Encoder Reset Command.
        # This will bring the encoder back to the known state and clear the Lock state if set with one API call. 
        # This call only requires 6.8 seconds to execute so it should not be an issue.

        cmd_reset if $Conf->{PROBE} ne 'DEFINE';   # cmd reset will allow 9 seconds timeout

	# Must stop video stream before updating camera settings
	#
	cmd_stop_video;
	
	# set the GOP structure
	#
	cmd_api_gop_control if $Conf->{PROBE} ne 'DEFINE';
	
	# Try once more if first request failed
	#
	cmd_start_video or do { sleep 1; cmd_start_video };
	
	$Result{RC_SET} = "OK";
}

sub main
{
	# Process command line args and/or configuration file
	#
	init;
	
	# Define camera MODELID and FIRMWARE
	#
	define;
	
	# Interim report & exit 
	#
	ProbeResult(\%Result)  if $Conf->{PROBE} =~ /^(DEFINE|FAST)$/;
	ProbeResult(\%Result)  if $Conf->{DEVID} == 0;
	
	# Set RC (rate control) parameters + qality + imagesize
	#
	configure;
	
	# Final report
	#
	ProbeResult(\%Result);
}

main;
