Files
dPWR/lib/NC800.pm
Philip Smart e3d165b5ae Initial upload
2019-11-10 23:44:13 +00:00

746 lines
23 KiB
Perl

#########################################################################################################
##
## Name: NC800.pm
## Created: September 2015
## Author(s): Philip Smart
## Description: A perl module which forms part of the dPWR program.
## This module provides all the Private and Public API calls which interface the
## NC800 Ethernet based Relay Board to the dPWR program.
##
## Credits:
## Copyright: (c) 2015-2019 Philip Smart <philip.smart@net2net.org>
##
## History: September 2015 - Initial module written.
##
#########################################################################################################
## This source file 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 3 of the License, or
## (at your option) any later version.
##
## This source file 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, see <http://www.gnu.org/licenses/>.
#########################################################################################################
package NC800;
require 5.8.0;
use strict;
use IO::File;
use File::Temp qw(tempfile);
use Switch;
use POSIX qw(SIGALRM SIGTERM sigaction);
use HTTP::Lite;
use IO::Socket;
use Net::Ping;
use vars qw(@ISA @EXPORT $VERSION);
$VERSION = 1.00;
@ISA = qw(Exporter);
@EXPORT = qw(Init,
Terminate,
GetRelayState,
RelaySet,
MainLoop
); # Symbols to autoexport (:DEFAULT tag)
#################################################################################
# NC800 Package.
#
# Description: This package contains all the functions which control the NC800
# TCP power relay board.
#
# Members Functions:
# ExpandFileName - PRIVATE
# LogFileWriter - PUBLIC
#
# Members Variables:
# - PRIVATE
# - PUBLIC
# Comments:
#
#################################################################################
{
# Module wide constants
#
my $MODEM_RELAY = 1;
my $SERVER_RESET_RELAY = 2;
my $SERVER_POWER_RELAY = 3;
my $RELAY_4 = 4;
my $RELAY_5 = 5;
my $RELAY_6 = 6;
my $RELAY_7 = 7;
my $RELAY_8 = 8;
# Map to map an external identifier to an internal relay value for active relays.
#
my %reverse_RELAY_MAP = ( $MODEM_RELAY => "MODEM",
$SERVER_RESET_RELAY => "SERVER_RESET",
$SERVER_POWER_RELAY => "SERVER_POWER"
);
our %RELAY_MAP = ( $reverse_RELAY_MAP{$MODEM_RELAY} => $MODEM_RELAY,
$reverse_RELAY_MAP{$SERVER_RESET_RELAY} => $SERVER_RESET_RELAY,
$reverse_RELAY_MAP{$SERVER_POWER_RELAY} => $SERVER_POWER_RELAY
);
our %RELAY_STATE_MAP = ( "0" => 0,
"1" => 1,
"OFF" => 0,
"ON" => 1,
"off" => 0,
"on" => 1
);
my %CURRENT_RELAY_STATE = ( $MODEM_RELAY => 0,
$SERVER_RESET_RELAY => 0,
$SERVER_POWER_RELAY => 0
);
# Public variables (our)
#
our $NC800_IP = "";
our $NC800_PORT = 0;
our $NC800_PWD = "";
our $NC800_MAX_HTTP_RETRIES = 0;
our $NC800_LOGFILE = "";
our $NC800_MODEM_PING_ADDR1 = "";
our $NC800_MODEM_PING_ADDR2 = "";
our $NC800_MODEM_MAXPING = "";
our $NC800_MODEM_PINGTIME = 0; # Period between Ping checks.
our $NC800_MODEM_RESETTIME = 0; # Period that Modem is held in reset (power off) state.
our $NC800_MODEM_CHECK_WAIT_TIME = 0; # Period of time to wait after modem reset before making checks.
our $NC800_SERVER_PING_ADDR1 = "";
our $NC800_SERVER_PING_ADDR2 = "";
our $NC800_SERVER_MAXPING = 0;
our $NC800_SERVER_PINGTIME = 0; # Period between Ping checks.
our $NC800_SERVER_RESETTIME = 0; # Period that Server is held in reset (reset button pressed).
our $NC800_SERVER_CHECK_WAIT_TIME = 0; # Period of time to wait after a reset or powercycle before making new checks.
our $NC800_SERVER_MAXRESET = 0; # Number of consecutive reset attempts before a power cycle.
our $NC800_SERVER_HOLDOFF_TIME = 0; # Period of time in seconds needed for Server to register a Power OFF request.
our $NC800_SERVER_HOLDON_TIME = 0; # Period of time in seconds needed for Server to register a Power ON request.
our $NC800_SERVER_POWEROFF_TIME = 0; # Period of time in seconds that the server is held in Power OFF state.
# Private variables (my)
#
my $CURRENT_TIME = 0;
my $MODEM_PING_COUNTER = 0;
my $MODEM_1_PING_TIMER = -1;
my $MODEM_2_PING_TIMER = -1;
my $MODEM_RESET_TIMER = -1;
my $MODEM_CHECK_WAIT_TIME = 0;
my $SERVER_PING_COUNTER = 0;
my $SERVER_1_PING_TIMER = -1;
my $SERVER_2_PING_TIMER = -1;
my $SERVER_RESET_TIMER = -1;
my $SERVER_RESET_COUNTER = 0;
my $SERVER_CHECK_WAIT_TIME = 0;
# Method to log information from this module, seperate from callers logging.
#
sub log
{
my ($error, $msg) = @_;
my $date = scalar localtime;
my ($dow, $mon, $dt, $tm, $yr) = ($date =~ m/(...) (...) (..) (..:..:..) (....)/);
$dt += 0;
$dt = substr("0$dt", length("0$dt") - 2, 2);
$date = "$dt/$mon/$yr:$tm";
if (open(LG_HDL, ">>$NC800_LOGFILE")) {
print LG_HDL <<"EOF";
[$date]:$error:$msg
EOF
close(LG_HDL);
}
}
# Method to run a detached system command.
#
sub forkrun {
my ($cmd)=@_;
my $pid;
if( $pid = fork )
{
return $pid;
}
elsif( defined $pid )
{
exec "$cmd";
exit 0;
}
else {
NC800::log(1, "Failed to run command:$cmd");
return 0;
}
}
# Method to ping a remote address to see if it is alive.
#
sub pingIP
{
my ($ipToPing) = @_;
my $handle;
my $result;
# Create Ping handle, ping address given and return result.
#
$handle = Net::Ping->new();
$result = $handle->ping($ipToPing);
$handle->close();
return $result;
}
# Method to expand a filename macros.
#
sub expandFileName
{
my $filename = shift;
my $year = "";
my ($sec, $min, $hour, $day, $month, $lyear, $dummy, $dummy, $dummy) = localtime();
# Convert integers into formatted strings.
#
$sec = sprintf("%02d", $sec);
$min = sprintf("%02d", $min);
$hour = sprintf("%02d", $hour);
$day = sprintf("%02d", $day);
$month = sprintf("%02d", $month + 1);
$lyear = sprintf("%04d", $lyear + 1900);
$year = substr($lyear, -1, 2);
# Expand macros in filename.
#
$filename =~ s/<DD>/$day/;
$filename =~ s/<DAY>/$day/;
$filename =~ s/<MM>/$month/;
$filename =~ s/<MONTH>/$month/;
$filename =~ s/<YY>/$year/;
$filename =~ s/<YYYY>/$lyear/;
$filename =~ s/<HH>/$hour/;
$filename =~ s/<HOUR>/$hour/;
$filename =~ s/<MIN>/$min/;
$filename =~ s/<SS>/$sec/;
$filename =~ s/<SEC>/$sec/;
# Return finished filename.
#
return($filename);
}
# Method to test if an IP address is valid.
#
sub testIP
{
my ($remote_ip, $remote_port) = @_;
my $local_ip = "0.0.0.0";
my $result = 0;
my $remote = IO::Socket::INET->new(
Proto => "tcp",
LocalAddr => "$local_ip",
PeerAddr => "$remote_ip",
PeerPort => "$remote_port",
Reuse => 1
) or $result = 1;
return $result;
}
# Method to GET a URL, but ignoring the response as it is not needed.
#
sub httpGet
{
my ($url) = shift;
my $idx;
my $result;
my $http;
# Initialise a Lite connection.
#
$http = HTTP::Lite->new;
# Retry a set number of times
for($idx = 1; $idx < $NC800_MAX_HTTP_RETRIES; $idx++)
{
$result = $http->request($url);
if($result == 200)
{
undef $http;
return;
}
}
NC800::log(1, "Failed to open url ($url).");
undef $http;
}
# External method to get the current state of a given relay.
#
sub GetRelayState
{
my $relay = shift;
my $state;
# Map given value to relay known internally.
#
$relay = $RELAY_MAP{$relay};
$state = $CURRENT_RELAY_STATE{$relay};
NC800::log(1, "$relay:$state");
return($state);
}
# External method to change the state of a relay, using numbers 0..7 or name.
#
sub RelaySet
{
my $relay = shift;
my $state = shift;
my $testOnly = shift;
my $result = 0;
# Map given value to relay known internally.
#
$relay = $RELAY_MAP{$relay};
$state = $RELAY_STATE_MAP{$state};
if($relay eq "")
{
$result = 1;
}
elsif($state eq "")
{
$result = 2;
}
else
{
# Only switch ON/OFF if correct value given, else return fail.
#
if($state == 1)
{
if($testOnly == 0)
{
NC800::relayON($relay);
NC800::log(1, "Relay ($relay) has been turned on by external request.");
}
}
elsif($state == 0)
{
if($testOnly == 0)
{
NC800::relayOFF($relay);
NC800::log(1, "Relay ($relay) has been turned off by external request.");
}
}
else {
$result = 2;
}
}
return $result;
}
# Method to turn on a relay. A number 1..8 is given which is sent to the NC-800 in correct format.
#
sub relayON
{
my $url;
my $relay = shift;
# Sanity check.
if( $reverse_RELAY_MAP{$relay} eq "" ) { return; }
# Build and send HTTP request to NC-800.
$url = sprintf("http://%s/%s/6/?1%d&", $NC800_IP, $NC800_PWD, $relay);
httpGet $url;
# Update state.
$CURRENT_RELAY_STATE{$relay} = 1;
}
# Method to turn off a relay. A number 1..8 is given which is sent to the NC-800 in correct format.
#
sub relayOFF
{
my $url;
my $relay = shift;
# Sanity check.
if( $reverse_RELAY_MAP{$relay} eq "" ) { return; }
# Build and send HTTP request to NC-800.
$url = sprintf("http://%s/%s/6/?0%d&", $NC800_IP, $NC800_PWD, $relay);
httpGet $url;
# Update state.
$CURRENT_RELAY_STATE{$relay} = 0;
}
# Method to turn off the server by pressing the 'OFF' button via a relay.
#
sub serverOFF
{
# Switch Power Relay ON, which has the effect of bridging the power switch on the Server.
#
NC800::relayON($SERVER_POWER_RELAY);
# Wait for the programmed time as this is necessary for the Server power supply to register an OFF request.
#
sleep($NC800_SERVER_HOLDOFF_TIME);
# Switch Power Relay OFF, which has the effect of removing the bridge on the power switch.
#
NC800::relayOFF($SERVER_POWER_RELAY);
}
# Method to turn on the server by pressing the 'ON' button via a relay.
#
sub serverON
{
# Switch Power Relay ON, which has the effect of bridging the power switch on the Server.
#
NC800::relayON($SERVER_POWER_RELAY);
# Wait for the programmed time as this is necessary for the Server power supply to register an ON request.
#
sleep($NC800_SERVER_HOLDON_TIME);
# Switch Power Relay OFF, which has the effect of removing the bridge on the power switch.
#
NC800::relayOFF($SERVER_POWER_RELAY);
}
# Method to verify if the ADSL modem is working by pinging external addresses.
# Return 1 if the modem is ok, 0 if not.
#
sub checkModem
{
# If we reach the limit, return 0 to indicate modem failed.
#
if( $MODEM_PING_COUNTER >= $NC800_MODEM_MAXPING )
{
return 0;
}
# Try address 1.
#
if($MODEM_1_PING_TIMER < $CURRENT_TIME)
{
if(NC800::pingIP($NC800_MODEM_PING_ADDR1) == 1)
{
$MODEM_PING_COUNTER = 0;
} else
{
$MODEM_PING_COUNTER = $MODEM_PING_COUNTER + 1;
}
# Adjust the timer for next occurence.
#
$MODEM_1_PING_TIMER = $CURRENT_TIME + $NC800_MODEM_PINGTIME;
}
# Then address 2.
#
if($MODEM_2_PING_TIMER < $CURRENT_TIME)
{
if(NC800::pingIP($NC800_MODEM_PING_ADDR2) == 1)
{
$MODEM_PING_COUNTER = 0;
} else
{
$MODEM_PING_COUNTER = $MODEM_PING_COUNTER + 1;
}
# Adjust the timer for next occurence.
#
$MODEM_2_PING_TIMER = $CURRENT_TIME + $NC800_MODEM_PINGTIME;
}
if( $MODEM_PING_COUNTER >= $NC800_MODEM_MAXPING )
{
NC800::log(1, "Modem not responding to pings, tried $MODEM_PING_COUNTER times.");
}
return 1;
}
# Reset the modem by disconnecting the power, setting a timer and waiting for it to
# expire before re-applying power. Once the power has been re-applied, return a positive
# value to indicate the routine has finished.
#
sub resetModem
{
if( $MODEM_RESET_TIMER != -1 && $MODEM_RESET_TIMER < $CURRENT_TIME )
{
# Turn the relay back off which has the effect of turning on the modem.
#
NC800::relayOFF($MODEM_RELAY);
$MODEM_RESET_TIMER = -1;
# Log event.
#
NC800::log(1, "Turned Modem Relay OFF - ie. Applied power.");
return 1;
}
# If the timer is running but not expired, just exit.
#
if( $MODEM_RESET_TIMER != -1 )
{
return 0;
}
# Turn on the relay, ie. switch power off to the modem.
#
NC800::relayON($MODEM_RELAY);
# Log event.
#
NC800::log(1, "Turned Modem Relay ON - ie. Disconnected power.");
# Set the timer running, basically we need the modem to be switched off for this amount of time.
#
$MODEM_RESET_TIMER = $CURRENT_TIME + $NC800_MODEM_RESETTIME;
return 0;
}
# Method to verify if the SERVER is working by pinging its ethernet addresses.
# Return 1 if the SERVER is ok, 0 if not.
#
sub checkSERVER
{
# If we reach the limit, return 0 to indicate modem failed.
#
if( $SERVER_PING_COUNTER >= $NC800_SERVER_MAXPING )
{
return 0;
}
# Try address 1.
#
if($SERVER_1_PING_TIMER < $CURRENT_TIME)
{
if(NC800::pingIP($NC800_SERVER_PING_ADDR1) == 1)
{
$SERVER_PING_COUNTER = 0;
$SERVER_RESET_COUNTER = 0;
} else
{
$SERVER_PING_COUNTER = $SERVER_PING_COUNTER + 1;
}
# Adjust the timer for next occurence.
#
$SERVER_1_PING_TIMER = $CURRENT_TIME + $NC800_SERVER_PINGTIME;
}
# Then address 2.
#
if($SERVER_2_PING_TIMER < $CURRENT_TIME)
{
if(NC800::pingIP($NC800_SERVER_PING_ADDR2) == 1)
{
$SERVER_PING_COUNTER = 0;
$SERVER_RESET_COUNTER = 0;
} else
{
$SERVER_PING_COUNTER = $SERVER_PING_COUNTER + 1;
}
# Adjust the timer for next occurence.
#
$SERVER_2_PING_TIMER = $CURRENT_TIME + $NC800_SERVER_PINGTIME;
}
if( $SERVER_PING_COUNTER >= $NC800_SERVER_MAXPING )
{
# Log event.
#
NC800::log(1, "Server not responding to pings, tried $SERVER_PING_COUNTER times.");
}
return 1;
}
# Reset the SERVER by closing the relay connected to the reset switch, then setting a timer and waiting for it to
# expire before opening the relay to allow the SERVER to start. Once the reset has finished, return a positive
# value to indicate the routine has finished.
#
sub resetSERVER
{
if( $SERVER_RESET_TIMER != -1 && $SERVER_RESET_TIMER < $CURRENT_TIME )
{
# Turn the relay off which has the effect of allowing the SERVER to start.
#
NC800::relayOFF($SERVER_RESET_RELAY);
$SERVER_RESET_TIMER = -1;
# Log event.
#
NC800::log(1, "Turned Server Reset Relay OFF - ie. Released RESET button.");
return 1;
}
# If the timer is running but not expired, just exit.
#
if( $SERVER_RESET_TIMER != -1 )
{
return 0;
}
# Turn on the relay, ie. switch power off to the modem.
#
NC800::relayON($SERVER_RESET_RELAY);
# Set the timer running, basically we need the modem to be switched off for this amount of time.
#
$SERVER_RESET_TIMER = $CURRENT_TIME + $NC800_SERVER_RESETTIME;
# Log event.
#
NC800::log(1, "Turned Server Reset Relay ON - ie. Pressed RESET button.");
return 0;
}
# Function to initialise the module.
#
sub Init
{
my $relay;
# Setup initial timer values.
#
$CURRENT_TIME=time();
$MODEM_1_PING_TIMER = $CURRENT_TIME + $NC800_MODEM_PINGTIME;
$MODEM_2_PING_TIMER = $CURRENT_TIME + $NC800_MODEM_PINGTIME;
$SERVER_1_PING_TIMER = $CURRENT_TIME + $NC800_SERVER_PINGTIME;
$SERVER_2_PING_TIMER = $CURRENT_TIME + $NC800_SERVER_PINGTIME;
# Verify that we can connect to the NC800, if we cannot, then log and exit.
#
if(testIP($NC800_IP, $NC800_PORT) == 1)
{
NC800::log(1, "Cannot connect to IP ($NC800_IP), PORT ($NC800_PORT).");
return 1;
}
# Turn off all relays - default for a reset on the NC-800.
#
foreach $relay (keys %CURRENT_RELAY_STATE)
{
if($NC800::CURRENT_RELAY_STATE{$relay} == 1)
{
NC800::relayON($relay);
} else
{
NC800::relayOFF($relay);
}
}
# Start message.
#
NC800::log(0, "NC800 online.");
return 0;
}
# Method to tidy up the NC800 prior to exit.
#
sub Terminate
{
# Turn off all relays - default for a reset on the NC-800.
#
my $idx;
for($idx=1; $idx < 9; $idx++)
{
NC800::relayOFF($idx);
}
# End message.
#
NC800::log(0, "NC800 offline.");
}
# Main loop which executes all required NC800 logic.
#
sub MainLoop
{
$CURRENT_TIME=time();
# If the Modem has been reset, we wait a period of time before continuing
# on with further checks.
#
if( $CURRENT_TIME < $MODEM_CHECK_WAIT_TIME )
{
;
}
else
{
# Has the modem failed? If so, reset it via oneshot power cycle.
#
if( NC800::checkModem() == 0 )
{
# Request the modem to be reset, when it has, the method will return 1.
#
if( NC800::resetModem() == 1 )
{
$MODEM_PING_COUNTER=0;
$MODEM_CHECK_WAIT_TIME = $NC800_MODEM_CHECK_WAIT_TIME + $CURRENT_TIME;
}
}
}
# If the server has been reset or powercycled, we wait a period of time before continuing
# on with further checks.
#
if( $CURRENT_TIME < $SERVER_CHECK_WAIT_TIME )
{
;
}
else
{
# Has the SERVER failed? If so, try a reset and if this doesnt work, cycle the power.
#
if( NC800::checkSERVER() == 0 )
{
# If the Reset Counter has reached the maximum, we need to power cycle the server.
# This code has been written intentionally as 'Blocking' as it only happens rarely.
#
if( $SERVER_RESET_COUNTER >= $NC800_SERVER_MAXRESET )
{
# First turn server off.
#
NC800::serverOFF();
# Wait a period of time to allow the power supply to fully reset.
#
sleep($NC800_SERVER_POWEROFF_TIME);
# Turn on the server and continue.
#
NC800::serverON();
$SERVER_RESET_COUNTER = 0;
$SERVER_PING_COUNTER = 0;
$SERVER_CHECK_WAIT_TIME = $NC800_SERVER_CHECK_WAIT_TIME + $CURRENT_TIME;
# Log event.
#
NC800::log(1, "Power Cycled the Server.");
}
else
{
# Request the SERVER to be reset, when it has, the method will return 1.
#
if( NC800::resetSERVER() == 1 )
{
$SERVER_PING_COUNTER=0;
$SERVER_RESET_COUNTER = $SERVER_RESET_COUNTER + 1;
}
}
}
}
}
}
1;