#!/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;