Last active
May 10, 2020 19:15
-
-
Save 667bdrm/72e18a2d4dca8c19f2c05e755096bfb0 to your computer and use it in GitHub Desktop.
PTZ control tool and generic SDK for Huamai PTZ camera 5508N-W-IR (eye10000.com)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
# PTZ control tool and generic SDK for Huamai PTZ camera 5508N-W-IR (eye10000.com) | |
# http://www.aliexpress.com/item/New-IR-wireless-ip-camera-with-P2P-H-264-PTZ-WIFI-SD-Slot-Network-camera-Free/782606869.html | |
# | |
# Future releases will be available at https://gitlab.com/667bdrm/huamaictl | |
# | |
# Usage: | |
# | |
# ipcptz.pl --host <ip> --port <port> --authserial <serial> --authtime <time> --authtoken <token> --direction <up|down|left|right|patrol|zoomin|zoomout> --speed <speed> | |
# | |
# authtime and authtoken must be grabbed from the original Windows desktop application traffic (<Authentication></Authentication> and <Time></Time>) | |
# Don't know right now how to calculate authtoken (custom MD5 hash based on "2huamai8D" salt), but constant time+token pair works perfect. See hm::CHmRequest_Certification::BuildMD5() | |
package IPPTZcam; | |
use constant { | |
CMD_STREAMINFO => 0x0101, | |
CMD_CLOSEVIDEO => 0x0103, | |
CMD_OPENAUDIO => 0x0201, | |
CMD_CLOSEAUDIO => 0x0203, | |
CMD_OPENTALK => 0x0301, | |
CMD_RECVTALK => 0x0302, | |
CMD_CLOSETALK => 0x0303, | |
CMD_PTZ => 0x0401, | |
CMD_SETPARAMCFG => 0x0501, | |
CMD_DEVINFO => 0x0502, # GetParamCFG | |
CMD_VIDEO => 0x0504, | |
CMD_AUTH => 0x060d, # Certification | |
CMD_REMOTE_RESTARTCAMERA => 0x060e, | |
CMD_CLOSECONNECT => 0x060f, | |
CMD_TIME => 0x0610, | |
CMD_REMOTE_QUERYRECORD => 0x0701, | |
CMD_REMOTE_PLAYRECORD => 0x0702, | |
CMD_REMOTE_STOPRECORD => 0x0705, | |
CMD_REMOTE_DELETERECORD => 0x0706, | |
CMD_REMOTE_PAUSERECORD => 0x0707, | |
CMD_REMOTE_RESUMERECORD => 0x0708, | |
CMD_REMOTE_STEPRECORD => 0x0709, | |
CMD_REMOTE_SAVEPIC => 0x0801, | |
CMD_REMOTE_QUERYPIC => 0x0802, | |
CMD_REMOTE_DOWNLOADPIC => 0x0803, | |
CMD_REMOTE_CANCELDOWNLOAD => 0x0805, | |
CMD_REMOTE_DELETEPIC => 0x0806, | |
CMD_REMOTE_STARTRECORD => 0x0900, | |
CMD_REMOTE_CLOSERECORD => 0x0901, | |
CMD_HEARTBEAT => 0x0a01, | |
CMD_ONLINE => 0x0b01, | |
CMD_OPENARMOR => 0x1201, | |
CMD_CLOSEARMOR => 0x1202, | |
CMD_SENSORINFO => 0x1203, # GetArmorStatus | |
}; | |
use Module::Load::Conditional qw[can_load check_install requires]; | |
my $use_list = { | |
'IO::Socket' => undef, | |
'IO::Socket::INET' => undef, | |
'Time::Local' => undef, | |
'Data::Dumper' => undef, | |
}; | |
if (!can_load( modules => $use_list, autoload => true )) { | |
die('Failed to load required modules: ' . join(', ', keys %{$use_list})); | |
} | |
sub new { | |
my $classname = shift; | |
my $self = {}; | |
bless($self, $classname); | |
$self->_init(@_); | |
return $self; | |
} | |
sub DESTROY { | |
my $self = shift; | |
} | |
sub disconnect { | |
my $self = shift; | |
$self->{socket}->close(); | |
} | |
sub _init { | |
my $self = shift; | |
$self->{host} = ""; | |
$self->{port} = 8100; | |
$self->{serial} = ""; | |
$self->{token} = ""; | |
$self->{time} = ""; | |
$self->{socket} = undef; | |
if (@_) { | |
my %extra = @_; | |
@$self{keys %extra} = values %extra; | |
} | |
print "p=".$self->{serial}; | |
} | |
sub BuildPacket { | |
my $self = shift; | |
my ($type, $msg) = @_; | |
my @pkt_prefix_1; | |
my @pkt_prefix_2; | |
my $pkt_type; | |
print $msg; | |
print length($msg); | |
my $msglen = length($msg); | |
#@pkt_prefix_1 = (0x00, 0x00, 0x04, 0x01); # signature | |
$pkt_type = $type; | |
#my $prefix1 = pack('c*', @pkt_prefix_1); | |
my $prefix1 = pack('N', $type); | |
my $pkt_prefix_data = $prefix1 . pack('N', $msglen). pack('N', 0x00) . $msg; | |
my $pkt_data = $pkt_prefix_data; | |
return $pkt_data; | |
} | |
sub GetReplyHead { | |
my $self = shift; | |
my $data; | |
my @reply_head_array; | |
# head_flag, version, reserved | |
$self->{socket}->recv($data, 4); | |
my @header = unpack('C*', $data); | |
my ($byte0, $byte1, $byte2, $byte3) = (@header)[0,1,2,3]; | |
$self->{socket}->recv($data, 4); | |
my $size = unpack('N', $data); | |
# int sid, int seq | |
$self->{socket}->recv($data, 4); | |
my $word3 = unpack('N', $data); | |
my $reply_head = { | |
byte0 => $version, | |
byte1 => $sid, | |
byte2 => $seq, | |
word3 => $word3, | |
Content_Length => $size, | |
}; | |
printf("reply: byte0=0x%x byte1=0x%x byte2=0x%x byte3=0x%x size = %d word3=0x%x\n", $header[0], $header[1], $header[2], $header[3], $size, $word3); | |
return $reply_head; | |
} | |
sub GetReplyData { | |
my $self = shift; | |
my $reply = $_[0]; | |
my $length = $reply->{'Content_Length'}; | |
my $out; | |
for (my $downloaded=0; $downloaded < $length; $downloaded++) { | |
$self->{socket}->recv($data, 1); | |
$out .= $data; | |
} | |
return $out; | |
} | |
sub PrepareGenericCommandHead { | |
my $self = shift; | |
my $msgid = $_[0]; | |
my $parameters = $_[1]; | |
my $data; | |
my $pkt = $parameters; | |
my $cmd_data = $self->BuildPacket($msgid, $pkt); | |
$self->{socket}->send($cmd_data); | |
my $reply_head = $self->GetReplyHead(); | |
return $reply_head; | |
} | |
sub PrepareGenericCommand { | |
my $self = shift; | |
my $msgid = $_[0]; | |
my $parameters = $_[1]; | |
my $reply_head = $self->PrepareGenericCommandHead($msgid, $parameters); | |
my $out = $self->GetReplyData($reply_head); | |
if ($out) { | |
return $out; | |
} | |
return undef; | |
} | |
sub PrepareGenericDownloadCommand { | |
my $self = shift; | |
my $msgid = $_[0]; | |
my $parameters = $_[1]; | |
my $file = $_[2]; | |
my $reply_head = $self->PrepareGenericCommandHead($msgid, $parameters); | |
my $out = $self->GetReplyData($reply_head); | |
open(OUT, ">$file"); | |
print OUT $out; | |
close(OUT); | |
return 1; | |
} | |
sub CmdLogin { | |
my $self = shift; | |
my ($direction, $speed) = @_; | |
my $data; | |
my $pkt = { | |
}; | |
print Dumper $pkt; | |
my $msg = $self->BuildAuthMsg(); | |
my $cmd_data = $self->BuildPacket(CMD_AUTH, $msg); | |
$self->{socket}->send($cmd_data); | |
my $reply_head = $self->GetReplyHead(); | |
my $out = $self->GetReplyData($reply_head); | |
if ($out) { | |
return $out; | |
} | |
return undef; | |
} | |
sub CmdHeartBeat { | |
my $self = shift; | |
my $data; | |
my $pkt = { | |
}; | |
print Dumper $pkt; | |
my $msg = undef; | |
my $cmd_data = $self->BuildPacket(CMD_HEARTBEAT, $msg); | |
$self->{socket}->send($cmd_data); | |
my $reply_head = $self->GetReplyHead(); | |
my $out = $self->GetReplyData($reply_head); | |
if ($out) { | |
return $out; | |
} | |
return undef; | |
} | |
sub CmdPTZ { | |
my $self = shift; | |
my ($direction, $speed) = @_; | |
my $data; | |
my $pkt = { | |
}; | |
print Dumper $pkt; | |
my $msg = $self->BuildPTZMsg($direction, $speed); | |
my $cmd_data = $self->BuildPacket(CMD_PTZ, $msg); | |
$self->{socket}->send($cmd_data); | |
my $reply_head = $self->GetReplyHead(); | |
my $out = $self->GetReplyData($reply_head); | |
if ($out) { | |
return $out; | |
} | |
return undef; | |
} | |
sub CmdSensorInfo { | |
my $self = shift; | |
return $self->PrepareGenericCommand(CMD_SENSORINFO, undef); | |
} | |
sub CmdStreamInfo { | |
my $self = shift; | |
my $msg = <<END_MESSAGE; | |
<Channel>0</Channel> | |
<StreamType>1</StreamType> | |
<VideoType>1</VideoType> | |
END_MESSAGE | |
$replyhead = $self->PrepareGenericCommand(CMD_STREAMINFO, $self->WrapMessage($msg)); | |
if ($replyhead) { | |
$rh2 = $self->GetReplyHead(); | |
my $out = $self->GetReplyData($rh2); | |
open(OUT, ">stream.dat"); | |
print OUT $out; | |
close(OUT); | |
} | |
return $replyhead; | |
} | |
sub CmdDeviceInfo { | |
my $self = shift; | |
my $msg = $self->WrapMessage('<Target Name="DevBase" />'); | |
return $self->PrepareGenericCommand(CMD_DEVINFO, $msg); | |
} | |
sub CmdSdcardInfo { | |
my $self = shift; | |
my $msg = $self->WrapMessage('<Target Name="SdcardInfo" />'); | |
return $self->PrepareGenericCommand(CMD_DEVINFO, $msg); | |
} | |
sub CmdImageConfig { | |
my $self = shift; | |
my $channel = $_[0]; | |
my $msg = $self->WrapMessage('<Target Name="ImageConfig" /><Channel>' . $channel . '</Channel>'); | |
return $self->PrepareGenericCommand(CMD_DEVINFO, $msg); | |
} | |
sub CmdVideo { | |
my $self = shift; | |
my $channel = $_[0]; | |
my $msg = $self->WrapMessage('<Target Name="video" /><Channel>' . $channel . '</Channel>'); | |
return $self->PrepareGenericCommand(CMD_VIDEO, $msg); | |
} | |
sub CmdTime { | |
my $self = shift; | |
my $channel = $_[0]; | |
my $msg = $self->WrapMessage('<Time>' . time() . '</Time>'); | |
return $self->PrepareGenericCommand(CMD_TIME, $msg); | |
} | |
sub WrapMessage { | |
my $self = shift; | |
my $msg = $_[0]; | |
my $out = <<END_MESSAGE; | |
<?xml version="1.0" encoding="utf-8" ?> | |
<Message> | |
$msg | |
</Message> | |
END_MESSAGE | |
return $out; | |
} | |
sub BuildPTZMsg { | |
my $self = shift; | |
my ($direction, $speed) = @_; | |
my $msg = <<END_MESSAGE; | |
<Channel>0</Channel> | |
<Dir>$direction</Dir> | |
<Speed>$speed</Speed> | |
<Name /> | |
END_MESSAGE | |
return $self->WrapMessage($msg); | |
} | |
sub BuildAuthMsg { | |
my $self = shift; | |
my $msg = <<END_MESSAGE; | |
<Authentication>$cfgAuthToken</Authentication> | |
<Time>$cfgAuthTime</Time> | |
<Type>1</Type> | |
<Sn>$cfgAuthSn</Sn> | |
END_MESSAGE | |
return $self->WrapMessage($msg); | |
} | |
package main; | |
use IO::Socket; | |
use IO::Socket::INET; | |
use Time::Local; | |
use Getopt::Long; | |
use Pod::Usage; | |
use Data::Dumper; | |
my $cfgFile = ""; | |
my $cfgAuthSn = ""; | |
my $cfgAuthToken = ""; | |
my $cfgAuthTime = ""; | |
my $cfgHost = ""; | |
my $cfgPort = 8100; | |
my $cfgCmd = undef; | |
my $cfgSpeed = 6; | |
my $cfgDirection = undef; | |
my $help = 0; | |
my $result = GetOptions ( | |
"help|h" => \$help, | |
"outputfile|of|o=s" => \$cfgFile, | |
"user|u=s" => \$cfgUser, | |
"pass|p=s" => \$cfgPass, | |
"host|hst=s" => \$cfgHost, | |
"port|prt=s" => \$cfgPort, | |
"command|cmd|c=s" => \$cfgCmd, | |
"direction|d=s" => \$cfgDirection, | |
"speed|s=s" => \$cfgSpeed, | |
"authserial|sn=s" => \$cfgAuthSn, | |
"authtime|tm=s" => \$cfgAuthTime, | |
"authtoken|token=s" => $cfgAuthToken, | |
); | |
pod2usage(1) if ($help); | |
if (!($cfgHost or $cfgPort)) { | |
print STDERR "You must set user, host and port!\n"; | |
exit(0); | |
} | |
my $socket = IO::Socket::INET->new( | |
PeerAddr => $cfgHost, | |
PeerPort => $cfgPort, | |
Proto => 'tcp', | |
Timeout => 10000, | |
Type => SOCK_STREAM, | |
Blocking => 1 | |
) or die "Error at line " . __LINE__. ": $!\n"; | |
print "Setting clock for: host = $cfgHost port = $cfgPort\n"; | |
my $dvr = IPPTZcam->new(host => $cfgHost, port => $cfgPort, serial => $cfgAuthSn, token => $cfgAuthToken, 'time' => $cfgAuthTime, socket => $socket); | |
my $savePath = '/tmp'; | |
my $decoded = $dvr->CmdLogin(); | |
print $decoded; | |
# 1 - up | |
# 2 - left | |
# 3 - down | |
# 4 - right | |
# 5 - patrol | |
my $dir; | |
if ($cfgDirection eq "up") { | |
$dir = 1; | |
} elsif ($cfgDirection eq "left") { | |
$dir = 2; | |
} elsif ($cfgDirection eq "down") { | |
$dir = 3; | |
} elsif ($cfgDirection eq "right") { | |
$dir = 4; | |
} elsif ($cfgDirection eq "patrol") { | |
$dir = 5; | |
} elsif ($cfgDirection eq "zoomout") { | |
$dir = 9; | |
} elsif ($cfgDirection eq "zoomin") { | |
$dir = 10; | |
} | |
my $decoded = $dvr->CmdPTZ($dir, $cfgSpeed); | |
#my $decoded = $dvr->CmdSensorInfo(); | |
#my $decoded = $dvr->CmdStreamInfo(); | |
#my $decoded = $dvr->CmdDeviceInfo(); | |
#my $decoded = $dvr->CmdSdcardInfo(); | |
#my $decoded = $dvr->CmdImageConfig(0); | |
#my $decoded = $dvr->CmdVideo(0); | |
#my $decoded = $dvr->CmdTime(); #fixme | |
print $decoded; | |
$dvr->disconnect(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment