Skip to content

Instantly share code, notes, and snippets.

@667bdrm
Last active May 10, 2020 19:15
Show Gist options
  • Save 667bdrm/72e18a2d4dca8c19f2c05e755096bfb0 to your computer and use it in GitHub Desktop.
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)
#!/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