#!/usr/common/bin/perl -w
# This script contains subroutines to check printer status
############################################################
# 05/10/2001 rcc2 - Fix printer_idle; Rennovate vendor independence scheme
# 06/24/2000 rcc2 - Convert to DBMS
# 10/11/99 rcc - Add "printer_idle"; Remove "service request" as a "down"
# 09/24/99 rcc - Add "get_pagecount"
# 08/01/99 rcc - Convert NPStat.pl to Perl 5 module npstatlib.pm
# 04/09/99 rcc - Reduce SNMP retries from 5 to 2
# 10/01/98 rcc
############################################################

package npstatlib;

# "use diagnostics" should be commented out for production environment
#use diagnostics;
use strict;

use vars qw{ @ISA @EXPORT };
require Exporter;
@ISA = qw{ Exporter };
@EXPORT = qw{
    get_status
    get_snmp_info
    err2str
    printer_ok
    printer_idle
    get_pagecount
    snmp_get
    $PSTAT_SERVICE_REQ
    $PSTAT_OFFLINE
    $PSTAT_PAPER_JAM
    $PSTAT_DOOR_OPEN
    $PSTAT_TONER_OUT
    $PSTAT_TONER_LOW
    $PSTAT_PAPER_OUT
    $PSTAT_PAPER_LOW
    $PSTAT_IDLE
    $PSTAT_PRINTING
    $PSTAT_WARMING_UP
    $PSTAT_UNREACHABLE
    $PSTAT_MAX
};

# exported
use vars qw{
    $PSTAT_SERVICE_REQ
    $PSTAT_OFFLINE
    $PSTAT_PAPER_JAM
    $PSTAT_DOOR_OPEN
    $PSTAT_TONER_OUT
    $PSTAT_TONER_LOW
    $PSTAT_PAPER_OUT
    $PSTAT_PAPER_LOW
    $PSTAT_IDLE
    $PSTAT_PRINTING
    $PSTAT_WARMING_UP
    $PSTAT_UNREACHABLE
    $PSTAT_MAX
};

# non-exported but global to the package:
use vars qw{
    %statmsg
    %prtstatmsg
    %encoded_oids
    $PAGECOUNT_OID
    $MODEL_OID
    $DEVICE_STATUS_OID
    $PRINTER_STATUS_OID
    $ERROR_STATE_OID
    $MEMORY_SIZE_OID
    $LEX_MODEL_OID
    $LEX_CONSOLE_OID
    $LEX_OPTRA_SERIAL_NO_OID
    $LEX_INT_SERIAL_NO_OID
    $HP_SERIAL_NO_OID
    $HP_5M_SERIAL_NO_OID
};

# Loads modules from source directory if executed in source directory
use lib qw(. /usr/local/netprint/lib);
use npparams;
use SNMP_Session;
use BER;

#### Global data

BEGIN {
    # OIDs for SNMP variables
    # Techniques for figuring out OIDs:
    #  To get OID for printer serial number:
    #   snmpwalk uris1 public .iso.3.6.1 | grep 11CKWB3
    #  To access an OID:
    #   snmpget uris1 public .1.3.6.1.4.1.641.2.1.2.1.6.1
    #  Helpful URL: www.ibr.cs.tu-bs.de/cgi-bin/sbrowser.cgi

    # OIDs common to all printers (I hope)
    $PAGECOUNT_OID     = '1.3.6.1.2.1.43.10.2.1.4.1.1'; # prtMarkerLifeCount.1.1
    $MODEL_OID         = '1.3.6.1.2.1.25.3.2.1.3.1';    # hrDeviceDescr.1
    $DEVICE_STATUS_OID = '1.3.6.1.2.1.25.3.2.1.5.1';    # hrDeviceStatus.1
    $PRINTER_STATUS_OID= '1.3.6.1.2.1.25.3.5.1.1.1';    # hrPrinterStatus.1
    $ERROR_STATE_OID   = '1.3.6.1.2.1.25.3.5.1.2.1';    # hrPrinterDetectedErrorState.1
    $MEMORY_SIZE_OID   = '1.3.6.1.2.1.25.2.2.0';	# hrMemorySize.0

    # Manufacturer or model-specific OIDs
    $LEX_MODEL_OID     = '1.3.6.1.4.1.641.2.1.2.1.2.1'; # prtgenPrinterName.1
    $LEX_CONSOLE_OID   = '1.3.6.1.2.1.43.16.5.1.2.1.1'; # prtConsoleDisplayBufferText.1.1
    $LEX_OPTRA_SERIAL_NO_OID = '1.3.6.1.4.1.641.2.1.2.1.6.1'; # Not in published Lexmark MIB
    $LEX_INT_SERIAL_NO_OID = '1.3.6.1.4.1.641.2.1.2.1.5.1';   # Bogus (not available)
    $HP_SERIAL_NO_OID   = '1.3.6.1.2.1.43.5.1.1.17.1',	# Not in published RFC1759
    $HP_5M_SERIAL_NO_OID= '1.3.6.1.4.1.11.2.3.9.4.2.1.1.3.3.0', # HP private MIB
    # Note:  The bit definitions for the first 8 of the items below are taken
    # from the description of the values for hrPrinterDetectedErrorState in
    # RFC1759.
    $PSTAT_SERVICE_REQ = 1;
    $PSTAT_OFFLINE = 2;
    $PSTAT_PAPER_JAM = 4;
    $PSTAT_DOOR_OPEN = 8;
    $PSTAT_TONER_OUT = 16;
    $PSTAT_TONER_LOW = 32;
    $PSTAT_PAPER_OUT = 64;
    $PSTAT_PAPER_LOW = 128;
    $PSTAT_IDLE = 256;
    $PSTAT_PRINTING = 512;
    $PSTAT_WARMING_UP = 1024;
    $PSTAT_UNREACHABLE = 2048;
    $PSTAT_MAX = 2048;

    %statmsg =
	(
	 $PSTAT_SERVICE_REQ, 'Service Requested',
	 $PSTAT_OFFLINE, 'Offline',
	 $PSTAT_PAPER_JAM, 'Paper Jam',
	 $PSTAT_DOOR_OPEN, 'Door Open',
	 $PSTAT_TONER_OUT, 'Toner Out',
	 $PSTAT_TONER_LOW, 'Toner Low',
	 $PSTAT_PAPER_OUT, 'Paper Out',
	 $PSTAT_PAPER_LOW, 'Paper Low',
	 $PSTAT_IDLE, 'Idle',
	 $PSTAT_PRINTING, 'Printing',
	 $PSTAT_WARMING_UP, 'Warming Up',
	 $PSTAT_UNREACHABLE, 'DISCONNECTED, OFFLINE, OR NOT RESPONDING',
	 );

    %prtstatmsg =
	(
	 3, $PSTAT_IDLE,
	 4, $PSTAT_PRINTING,
	 5, $PSTAT_WARMING_UP,
	 );
}


########################################################
# get_status($ip)
#
# $ip		IP address or DNS name of printer
# $model	Printer model or undef
# Returns:
#  $status	Bit encoded printer status

# Uses SNMP to find the status and returns an errorcode
# which can be deciphered in err2str, which will return an array of
# strings specifying the error(s) occuring.

sub get_status {
    my($ip, $model) = @_;

    # No idea what this is about
#    if ( $ip eq '' ) {
#	return( { 'status' => $PSTAT_IDLE } );
#    }

    my($community) = 'public';

    # The following line can be commented out for debugging purposes.
    $SNMP_Session::suppress_warnings = 1;
    my($session);
    if ( ! ($session = SNMP_Session->open ($ip, $community, 161)) ) {
        warn "Couldn't open SNMP session to $ip: $SNMP_Session::errmsg";
        return($PSTAT_UNREACHABLE);
    }
    $session->set_retries(2);

    # Get printer model if necessary
    if ( ! defined($model) ) {
	$model = snmp_get_model($session);
    }
    if ( ! defined($model) ) {
	return($PSTAT_UNREACHABLE);
    }

    my($error, $prtstatus, $console);
    if ( $model =~ /^Lexmark/ ) {

        # Lexmark printers require looking at the console status too because
	# printer_status doesn't show 'printing' until the paper starts moving.
	($error, $prtstatus, $console) = snmp_get($session, $ERROR_STATE_OID, $PRINTER_STATUS_OID, $LEX_CONSOLE_OID);
	$error = unpack("C", $error);
	if ( ! defined($error) ) {
	    return($PSTAT_UNREACHABLE);
	}
	if ( $console =~ /Ready|Power Saver/ ) {
	    if ( defined($prtstatmsg{$prtstatus}) ) {
		$error |= $prtstatmsg{$prtstatus};
	    }
	}
	else {
	    $error |= $PSTAT_PRINTING;
	}
    }
    elsif ( $model =~ /^HP/ ) {

	# HP printer
	($error, $prtstatus) = snmp_get($session, $ERROR_STATE_OID, $PRINTER_STATUS_OID);
	if ( ! defined($error) ) {
	    return($PSTAT_UNREACHABLE);
	}
	$error = unpack("C", $error);
	if ( defined($prtstatmsg{$prtstatus}) ) {
	    $error |= $prtstatmsg{$prtstatus};
	}
    }
    $session->close;


    return($error);
}


########################################################
# get_snmp_info($ip)
#
# $ip		IP address or DNS name of printer
# $model	Printer model or undef
# Returns:
# $snmpinfo	Hash reference containing keys 'pagecount', 'model', 'status',
#		'device_status', 'printer_status', 'error_state',
#		'memory_size', 'serial_no', 'status', 'real_model'.

sub get_snmp_info {
    my($ip, $model) = @_;

    # No idea what this is about
#    if ( $ip eq '' ) {
#	return( { 'status' => $PSTAT_IDLE } );
#    }

    my($community) = 'public';

    # The following line can be commented out for debugging purposes.
    $SNMP_Session::suppress_warnings = 1;
    my($session);
    if ( ! ($session = SNMP_Session->open ($ip, $community, 161)) ) {
        warn "Couldn't open SNMP session to $ip: $SNMP_Session::errmsg";
        return( { 'status' => $PSTAT_UNREACHABLE } );
    }
    $session->set_retries(2);

    # Get printer model if necessary
    if ( ! defined($model) ) {
	$model = snmp_get_model($session);
    }
    if ( ! defined($model) ) {
	return( { 'status' => $PSTAT_UNREACHABLE } );
    }

    my($count, $devstatus, $error, $prtstatus, $console, $memsize, $serialno, $status, $real_model);
    if ( $model =~ /^Lexmark/ ) {

        # Lexmark printers require looking at the console status too because
	# printer_status doesn't show 'printing' until the paper starts moving.
	my($serial_no_oid) = ($model =~ /^Lexmark Optra/ ? $LEX_OPTRA_SERIAL_NO_OID : $LEX_INT_SERIAL_NO_OID);
	($count, $devstatus, $error, $prtstatus, $console, $memsize, $serialno, $real_model) = snmp_get($session, $PAGECOUNT_OID, $DEVICE_STATUS_OID, $ERROR_STATE_OID, $PRINTER_STATUS_OID, $LEX_CONSOLE_OID, $MEMORY_SIZE_OID, $serial_no_oid, $LEX_MODEL_OID);
	if ( ! defined($count) ) {
	    return( { 'status' => $PSTAT_UNREACHABLE } );
	}
	$error = unpack("C", $error);
	$status = $error;
	if ( $console =~ /Ready|Power Saver/ ) {
	    if ( defined($prtstatmsg{$prtstatus}) ) {
		$status |= $prtstatmsg{$prtstatus};
	    }
	}
	else {
	    $status |= $PSTAT_PRINTING;
	}
    }
    elsif ( $model =~ /^HP/ ) {

	# HP printer
	my($serial_no_oid) = ($model =~ /^HP LaserJet 5M/ ? $HP_5M_SERIAL_NO_OID : $HP_SERIAL_NO_OID);
	($count, $devstatus, $error, $prtstatus, $console, $memsize, $serialno) = snmp_get($session, $PAGECOUNT_OID, $DEVICE_STATUS_OID, $ERROR_STATE_OID, $PRINTER_STATUS_OID, $LEX_CONSOLE_OID, $MEMORY_SIZE_OID, $serial_no_oid);
	if ( ! defined($count) ) {
	    return( { 'status' => $PSTAT_UNREACHABLE } );
	}
	$error = unpack("C", $error);
	$status = $error;
	if ( defined($prtstatmsg{$prtstatus}) ) {
	    $status |= $prtstatmsg{$prtstatus};
	}
	if ( $model eq 'HP LaserJet 5M' ) {
	    $serialno = substr($serialno, 2);
	}
	$real_model = $model;
    }
    $session->close;

    return( { 'pagecount' => $count, 'model' => $model, 'device_status' => $devstatus, 'printer_status' => $prtstatus, 'error_state' => $error, 'memory_size' => $memsize, 'serial_no' => $serialno, 'status' => $status, 'real_model' => $real_model } );
}

#########################################################
# err2str ($code)
#
# Given an errorcode, this sub will convert the
# code to an array of strings, each ending in a newline.

sub err2str {
    my($code) = @_;
    my(@msgs);
    my($i);

    @msgs = ();
    for ( $i = 1; $i <= $PSTAT_MAX; $i <<= 1 ) {
	if ( $i & $code ) {
	    push(@msgs, $statmsg{$i});
	}
    }
    return(@msgs);
}


#########################################################
# printer_ok($code)
#
# Given a printer status code, return TRUE for printer OK, and FALSE
# for printer not functioning.

sub printer_ok {
    my($code) = @_;
    return( ! ($code & ($PSTAT_OFFLINE | $PSTAT_PAPER_JAM | $PSTAT_DOOR_OPEN | $PSTAT_TONER_LOW | $PSTAT_TONER_OUT | $PSTAT_PAPER_OUT | $PSTAT_UNREACHABLE)) );
}


#########################################################
# printer_idle($code)
#
# Given a printer status code, return TRUE for printer idle, and FALSE
# for any other printer status.

sub printer_idle {
    my($code) = @_;
    return( (($code & $PSTAT_IDLE) != 0) and printer_ok($code) );
}


#########################################################
# Get printer page count
#
# $ip		Printer IP address or DNS name
# Returns:
#  $pagecount	Printer page count or -1 if error

sub get_pagecount {
    my($ip) = @_;

    # The following line can be commented out for debugging purposes.
    $SNMP_Session::suppress_warnings = 1;

    my($session);
    unless ( $session = SNMP_Session->open($ip, 'public', 161) ) {
	return(-1);
    }
    my(@snmpvals) = snmp_get($session, $PAGECOUNT_OID);
    $session->close();
    unless ( $#snmpvals >= 0 ) {
        return(-1);             # Will come here if 'noSuchName' error!!
    }
    return($snmpvals[0]);
}

#########################################################
# Get printer model using SNMP
#
# $session	SNMP session ID
# Returns:
#  $model	Printer model or undef if error
sub snmp_get_model {
    my($session) = @_;
    my($model);
    if ( ! (($model) = snmp_get($session, $MODEL_OID)) ) {
	return(undef);
    }
    return(undef) if ! defined($model);
    $model =~ s/\s+$//;
    return($model);
}

#########################################################
# Query the SNMP agent
#
# $session	SNMP session ID
# @oids		Array of OIDs
# Returns:
#  @values	Array of values or undef if error

# Given a session ID and an array of variable nicknames, query the SNMP
# agent and return an array of values.  We are assuming that the values
# come back in the same order as the OIDs.
sub snmp_get {
    my($session, @oids) = @_;

    my(@eoids);
    my($oid);
    foreach $oid ( @oids ) {
#	print "--> $oid\n";
	my($eoid);
	if ( ! defined($eoid = $encoded_oids{$oid}) ) {
	    $eoid = encode_oid(split(/\./, $oid));
	    $encoded_oids{$oid} = $eoid;
	}
	push(@eoids, $eoid);
    }

    my(@values);
    if ( $session->get_request_response(@eoids) ) {
	my($response) = $session->pdu_buffer;
	my($bindings) = $session->decode_get_response($response);
	my($binding);
	while ( $bindings ne '' ) {
	    my($value);
	    ($binding, $bindings) = decode_sequence($bindings);
	    ($oid, $value) = decode_by_template($binding, "%O%@");
	    push(@values, pretty_print($value));
	}
    }
    else {
	return(undef);
    }
    return @values;
}

1;
