Loftek

From ZoneMinder Wiki
Jump to navigationJump to search

Loftek Sentinel

Loftek ptz.jpg

This is an outdoor PTZ IP Camera with 3x Optical Zoom. The commands to operate this camera are very similar to a Foscam camera [2] except with a different set of features. It has been observed that companies such as Hootoo and Apexis offer cameras that appear identical to this Loftek model. The following instructions will likely work for those models as well.




Loftek Nexus 543

This is an outdoor fixed-mount camera with wifi capabilities. This camera can be configured by following the instructions under "Create the Monitor", but not the instructions under "PTZ Control".

Use of this camera with Zoneminder will cause the syslog to be flooded with "Corrupt JPEG data" messages. This is annoying, but the messages are apparently harmless, as the images are captured correctly and without any visible corruption.

Create the Monitor

From the Zoneminder console, click "Add New Monitor" and enter the following:

General
Name: {give the camera a name}
Source Type: Remote
Function: {select an appropriate function from the dropdown}
Enabled: Tick
Maximum FPS: Empty
Alarm Maximum FPS: Empty
Source
Remote Protocol: HTTP
Remote Method: Simple
Remote Host Name: x.x.x.x {The IP of the camera}
Remote Host Port: 80
Remote Host Path: /videostream.cgi?user=admin&pwd={password}&resolution=32&rate=11
Remote Image Colours: 24 bit colour
Capture Width: 640
Capture Height: 480

This will give you a 640x480 video stream running at ~5 fps. Refer to the table on the Foscam page [1] for other frame rate settings. Unlike the Foscam's , I have not run into any issues running at 640x80.

PTZ Control

I used a number of sources to create the following control file [2][3][4].

Here are the supported functions:

  1. Camera reset
  2. Pan left/right
  3. Tilt up/down
  4. Emulated diagonal movement
  5. Zoom in/out
  6. Brightness (Iris)
  7. Contrast (White)
  8. Can set & recall up to 8 presets
  9. Presets 9 & 10 respectively start and stop a patrol

Zoneminder must be taught how to control the Loftek camera. This is done with a PTZ control file. First, copy and paste the following code into a text file called "LoftekSentinel.pm". Save the file into the same folder as the file called "PanasonicIP.pm".


# ==========================================================================
#
# ZoneMinder Loftek Sentinel IP Control Protocol Module
# Copyright (C) 2001-2008 Philip Coombes
# Original modification for use with Foscam FI8908W IP Camera by Dave Harris
#    Updated by Ivan Francolin Martinez
# Converted for use with Loftek Sentinal IP Camera by Andrew Bauer (knnniggett@users.sourceforge.net)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ==========================================================================
#
# This module contains the implementation of the Loftek Sentinel IP camera control
# protocol
#
package ZoneMinder::Control::LoftekSentinel;

use 5.006;
use strict;
use warnings;

require ZoneMinder::Base;
require ZoneMinder::Control;

our @ISA = qw(ZoneMinder::Control);

our $VERSION = $ZoneMinder::Base::VERSION;

our %CamParams = ();

# ==========================================================================
#
# Loftek Sentinel IP Control Protocol
# 
# On ControlAddress use the format :
#   USERNAME:PASSWORD@ADDRESS:PORT
#   eg : admin:@10.1.2.1:80
#        zoneminder:zonepass@10.0.100.1:40000
#
# ==========================================================================

# Change "ZoneMinder::Logger" to "ZoneMinder::Debug" for zoneminder < 1.25.0
use ZoneMinder::Logger qw(:all);
use ZoneMinder::Config qw(:all);

use Time::HiRes qw( usleep );

sub new
{ 

    my $class = shift;
    my $id = shift;
    my $self = ZoneMinder::Control->new( $id );
    my $logindetails = "";
    bless( $self, $class );
    srand( time() );
    return $self;
}

our $AUTOLOAD;

sub AUTOLOAD
{
    my $self = shift;
    my $class = ref( ) || croak( "$self not object" );
    my $name = $AUTOLOAD;
    $name =~ s/.*://;
    if ( exists($self->{$name}) )
    {
        return( $self->{$name} );
    }
        Fatal( "Can't access $name member of object of class $class" );
    }

sub open
{
    my $self = shift;

    $self->loadMonitor();

    use LWP::UserAgent;
    $self->{ua} = LWP::UserAgent->new;
    $self->{ua}->agent( "ZoneMinder Control Agent/".ZM_VERSION );

    $self->{state} = 'open';
}

sub close
{ 
    my $self = shift;
    $self->{state} = 'closed';
}

sub printMsg
{
    my $self = shift;
    my $msg = shift;
    my $msg_len = length($msg);

    Debug( $msg."[".$msg_len."]" );
}

sub sendCmd
{
    my $self = shift;
    my $cmd = shift;
    my $result = undef;
    printMsg( $cmd, "Tx" );

    my $req = HTTP::Request->new( GET=>"http://".$self->{Monitor}->{ControlAddress}."/$cmd" );
    my $res = $self->{ua}->request($req);

    if ( $res->is_success )
    {
        $result = !undef;
    }
    else
    {
        Error( "Error check failed:'".$res->status_line()."'" );
    }

    return( $result );
}

sub getCamParams
{
    my $self = shift;

    my $req = HTTP::Request->new( GET=>"http://".$self->{Monitor}->{ControlAddress}."/get_camera_params.cgi" );
    my $res = $self->{ua}->request($req);

    if ( $res->is_success ) 
    {
        # Parse results setting values in %FCParams
        my $content = $res->decoded_content;

        while ($content =~ s/var\s+([^=]+)=([^;]+);//ms) {
            $CamParams{$1} = $2;
        }
    } 
    else
    {
        Error( "Error check failed:'".$res->status_line()."'" );
    }
}

#autoStop
#This makes use of the ZoneMinder Auto Stop Timeout on the Control Tab
sub autoStop
{
    my $self = shift;
    my $stop_command = shift;
    my $autostop = shift;
    if( $stop_command && $autostop)
    {
        Debug( "Auto Stop" );
        usleep( $autostop );
        my $cmd = "decoder_control.cgi?command=".$stop_command;
        $self->sendCmd( $cmd );
    }

}

# Reset the Camera
sub reset
{
    my $self = shift;
    Debug( "Camera Reset" );
    my $cmd = "reboot.cgi?";
    $self->sendCmd( $cmd );
}

#Up Arrow
sub moveConUp
{
    my $self = shift;
    my $stop_command = "1";
    Debug( "Move Up" );
    my $cmd = "decoder_control.cgi?command=0";
    $self->sendCmd( $cmd );
    $self->autoStop( $stop_command, $self->{Monitor}->{AutoStopTimeout} );
}

#Down Arrow
sub moveConDown
{
    my $self = shift;
    my $stop_command = "3";
    Debug( "Move Down" );
    my $cmd = "decoder_control.cgi?command=2";
    $self->sendCmd( $cmd );
    $self->autoStop( $stop_command, $self->{Monitor}->{AutoStopTimeout} );
}

#Left Arrow
sub moveConLeft
{
    my $self = shift;
    my $stop_command = "5";
    Debug( "Move Left" );
    my $cmd = "decoder_control.cgi?command=4";
    $self->sendCmd( $cmd );
    $self->autoStop( $stop_command, $self->{Monitor}->{AutoStopTimeout} );
}

#Right Arrow
sub moveConRight
{
    my $self = shift;
    my $stop_command = "7";
    Debug( "Move Right" );
    my $cmd = "decoder_control.cgi?command=6";
    $self->sendCmd( $cmd );
    $self->autoStop( $stop_command, $self->{Monitor}->{AutoStopTimeout} );
}

#Zoom In
sub zoomConTele
{
    my $self = shift;
    my $stop_command = "17";
    Debug( "Zoom Tele" );
    my $cmd = "decoder_control.cgi?command=18";
    $self->sendCmd( $cmd );
    $self->autoStop( $stop_command, $self->{Monitor}->{AutoStopTimeout} );
}

#Zoom Out
sub zoomConWide
{
    my $self = shift;
    my $stop_command = "19";
    Debug( "Zoom Wide" );
    my $cmd = "decoder_control.cgi?command=16";
    $self->sendCmd( $cmd );
    $self->autoStop( $stop_command, $self->{Monitor}->{AutoStopTimeout} );
}

#Diagonally Up Right Arrow
#This camera does not have builtin diagonal commands so we emulate them
sub moveConUpRight
{
    my $self = shift;
    Debug( "Move Diagonally Up Right" );
    $self->moveConUp( );
    $self->moveConRight( );
}

#Diagonally Down Right Arrow
#This camera does not have builtin diagonal commands so we emulate them
sub moveConDownRight
{
    my $self = shift;
    Debug( "Move Diagonally Down Right" );
    $self->moveConDown( );
    $self->moveConRight( );
}

#Diagonally Up Left Arrow
#This camera does not have builtin diagonal commands so we emulate them
sub moveConUpLeft
{
    my $self = shift;
    Debug( "Move Diagonally Up Left" );
    $self->moveConUp( );
    $self->moveConLeft( );
}

#Diagonally Down Left Arrow
#This camera does not have builtin diagonal commands so we emulate them
sub moveConDownLeft
{
    my $self = shift;
    Debug( "Move Diagonally Down Left" );
    $self->moveConDown( );
    $self->moveConLeft( );
}

#Stop
sub moveStop
{
    my $self = shift;
    Debug( "Move Stop" );
    my $cmd = "decoder_control.cgi?command=1";
    $self->sendCmd( $cmd );
}

#Set Camera Preset
#Presets must be translated into values internal to the camera
#Those values are: 30,32,34,36,38,40,42,44 for presets 1-8 respectively
sub presetSet
{
    my $self = shift;
    my $params = shift;
    my $preset = $self->getParam( $params, 'preset' );
    Debug( "Set Preset $preset" );

    if (( $preset >= 1 ) && ( $preset <= 8 )) {
        my $cmd = "decoder_control.cgi?command=".(($preset*2) + 28);
        $self->sendCmd( $cmd );
    }
}

#Recall Camera Preset
#Presets must be translated into values internal to the camera
#Those values are: 31,33,35,37,39,41,43,45 for presets 1-8 respectively
sub presetGoto
{
    my $self = shift;
    my $params = shift;
    my $preset = $self->getParam( $params, 'preset' );
    Debug( "Goto Preset $preset" );

    if (( $preset >= 1 ) && ( $preset <= 8 )) {
        my $cmd = "decoder_control.cgi?command=".(($preset*2) + 29);
        $self->sendCmd( $cmd );
    }

    if ( $preset == 9 ) {
        $self->horizontalPatrol();
    }

    if ( $preset == 10 ) {
        $self->horizontalPatrolStop();
    }
}

#Horizontal Patrol - Vertical Patrols are not supported
sub horizontalPatrol
{
    my $self = shift;
    Debug( "Horizontal Patrol" );
    my $cmd = "decoder_control.cgi?command=20";
    $self->sendCmd( $cmd );
}

#Horizontal Patrol Stop
sub horizontalPatrolStop
{
    my $self = shift;
    Debug( "Horizontal Patrol Stop" );
    my $cmd = "decoder_control.cgi?command=21";
    $self->sendCmd( $cmd );
}

# Increase Brightness
sub irisAbsOpen
{
    my $self = shift;
    my $params = shift;
    $self->getCamParams() unless($CamParams{'brightness'});
    my $step = $self->getParam( $params, 'step' );

    $CamParams{'brightness'} += $step;
    $CamParams{'brightness'} = 255 if ($CamParams{'brightness'} > 255);
    Debug( "Iris $CamParams{'brightness'}" );
    my $cmd = "camera_control.cgi?param=1&value=".$CamParams{'brightness'};
    $self->sendCmd( $cmd );
}

# Decrease Brightness
sub irisAbsClose
{
    my $self = shift;
    my $params = shift;
    $self->getCamParams() unless($CamParams{'brightness'});
    my $step = $self->getParam( $params, 'step' );

    $CamParams{'brightness'} -= $step;
    $CamParams{'brightness'} = 0 if ($CamParams{'brightness'} < 0);
    Debug( "Iris $CamParams{'brightness'}" );
    my $cmd = "camera_control.cgi?param=1&value=".$CamParams{'brightness'};
    $self->sendCmd( $cmd );
}

# Increase Contrast
sub whiteAbsIn
{
    my $self = shift;
    my $params = shift;
    $self->getCamParams() unless($CamParams{'contrast'});
    my $step = $self->getParam( $params, 'step' );

    $CamParams{'contrast'} += $step;
    $CamParams{'contrast'} = 6 if ($CamParams{'contrast'} > 6);
    Debug( "Iris $CamParams{'contrast'}" );
    my $cmd = "camera_control.cgi?param=2&value=".$CamParams{'contrast'};
    $self->sendCmd( $cmd );
}

# Decrease Contrast
sub whiteAbsOut
{
    my $self = shift;
    my $params = shift;
    $self->getCamParams() unless($CamParams{'contrast'});
    my $step = $self->getParam( $params, 'step' );

    $CamParams{'contrast'} -= $step;
    $CamParams{'contrast'} = 0 if ($CamParams{'contrast'} < 0);
    Debug( "Iris $CamParams{'contrast'}" );
    my $cmd = "camera_control.cgi?param=2&value=".$CamParams{'contrast'};
    $self->sendCmd( $cmd );
}

1;

Once the PTZ control file has been saved, open the Monitor created previously and click on the "Control" tab. Next click on "Edit" and then the "Add New Control" button. Enter the following information at each tab:

Main
Name: Loftek Sentinel
Type: Remote
Protocol: LoftekSentinel (notice no space)
Can reset: Tick
Move
Can Move: Tick
Can Move Diagonally: Tick
Can Move Continuous: Tick
Pan
Can Pan: Tick
Tilt
Can Tilt: Tick
Zoom
Can Zoom: Tick
Can Zoom Continuous: Tick
White
Can White Balance: Tick
Can White Balance Absolute: Tick
Max White Bal. Range: 6
Min White Bal. Step: 1
Max White Bal. Step: 1
Iris
Can Iris: Tick
Can Iris Absolute: Tick
Max Iris Range: 255
Min Iris Step: 16
Max Iris Step: 16
Presets
Has Presets: Tick
Num Presets: 10
Can Set Presets: Tick

When finished, click the "Save" button.

Now go back to the Monitor configuration window, click on the "Control" tab and enter the following:

Control
Controllable: Tick
Control Type: Loftek Sentinel
Control Device: empty
Control Address: admin:{password}@{camera ip address}
Auto Stop Timeout: 0.50 (or whatever works best for you)

When finished, click the "Save" button.

At this point you should have a working PTZ camera. Try it out and verify everything works as expected.

Notes & Troubleshooting

  1. The PTZ Control File is compatible with Zoneminder 1.25.0. If using an older version, you may have to change the line that reads "use ZoneMinder::Logger qw(:all);" to "use ZoneMinder::Debug qw(:all);" .
  2. If the Control tab is not visible in the Monitor configuration window then verify that OPT_CONTROL is enabled under the Options -> System Tab.
  3. TODO: Add more notes

References

[1] Foscam Forum Thread

[2] Foscam Wiki Page

[3] Dave Harris Blog Site

[4] IP Cam CGI SDK