What is a Readable/Modern Way to Parse a Bit-Based Error Code?

Question:

I’m tasked with reading error codes from printers via snmp. Luckily, I have a working bash script to guide me through this arcane task. I’m writing some python to do some different work from the existing script. The existing code seems to work, but it’s crazy ugly, and I’m hoping for a more python-y way to parse bits.

First, to explain my reading of the current code. The error code returned by an snmpwalk query on hrPrinterDetectedErrorState is encoded as an octet string, often with quotes around it. So the quotes are stripped along with whitespace and newlines. The error code can be up to four bytes, but a null byte is generally sent for the second pair if zero, so a pair of zeros is added in that case. Then the error code is converted to hex.

The existing bash script:

parseErrorState()
{
  setUpErrorCodes
  # pull the error state w/o quotes
  errorState=`snmpwalk -Oqvx -c public -v $snmpV $printerIP hrPrinterDetectedErrorState | grep -v "End of MIB" | tr -d '"'`
  # remove spaces
  errorCode=$(echo $errorState | tr -d [:space:])
  errorString=""

  # if we don't have two hex bytes, append a byte of zeros
  if [[ ${#errorCode} == 2 ]]
  then
    errorCode=$errorCode"00"
  fi

  # do hex conversion
 let errorCode=0x$errorCode

 if (( $errorCode & $overduePreventMaint ))
 then
   errorString=$errorString"Overdue Preventative Maintenance; "
 fi
 if (( $errorCode & $inputTrayEmpty ))
 then
   errorString=$errorString"Input Tray Empty; "
 fi
 if (( $errorCode & $outputFull ))
 then
   errorString=$errorString"Output Full; "
 fi
 if (( $errorCode & $outputNearFull ))
 then
 ... and about 12 more if-thens...

That series of if-thens bit-wise compares the errorCode with each of these and adds a relevant string to the output.

setUpErrorCodes()
  {
  lowPaper=32768
  noPaper=16384
  lowToner=8192
  noToner=4096
  doorOpen=2048
  jammed=1024
  offline=512
  serviceRequested=256

  inputTrayMissing=128
  outputTrayMissing=64
  markerSupplyMissing=32
  outputNearFull=16
  outputFull=8
  inputTrayEmpty=4
  overduePreventMaint=2
  }

My python version uses subprocess for the snmpwalk and does the formatting more or less as above. Then:

# A dictionary of the errors and bit places
errors = {
    16: "low paper",
    15: "no paper",
    ...etc

# a still very ugly bit parse starting with a hex str like '2A00':
b = bin(int(hexstr, 16))[2:] # hex to int, int to bin, cut off the '0b'
binstr = str(b)
length = len(binstr)
indices = []
for i, '1' in enumerate(binstr):
     # find negative index of all '1's and multiply by -1
     # a hack to get around the binary string not containing leading zeros
     indices.append((i-length)*-1)

Then just compare the indices to the errors dictionary and you’re done.

Anyway, very ugly, probably pretty inefficient. What is a more python-y, high-level, readable way to accomplish the same?

Asked By: anthropomo

||

Answers:

Given that you can parse your string as such:

flags = int(hexstr, 16)

I think you’re looking for

flag_indices = []
for flag in errors:
    if flags & (2**flag):
        flag_indices.append(flag)

Instead of building the list and indexing it, you can directly do

(flags >> flag) & 1

on the integer which will be a damn lot faster.

Answered By: Veedrac

You can get a list of set flags with a simple loop:

errors = {
    15: "low paper",
    14: "no paper",
    ...
}
flags = int(hexstr, 16)
flag_indices = []
for i in range(max(errors)):
    if flags & 1:
        flag_indices.append(i)
    flags >>= 1
Answered By: Sven Marnach

I’ve done it in powershell with a flags enum. Then if I cast the resulting integer to the flags enum type, it spits out all the bits that match as strings. How to read SNMP OID Output (bits) (hrPrinterDetectedErrorState)

[hrPrinterDetectedErrorState]37124

inputTrayEmpty, serviceRequested, noToner, lowPaper

Here’s a quick example using only the first byte, which is the most common. Snmpwalk isn’t helpful with interpreting in this case.

[flags()] Enum hrPrinterDetectedErrorState {
  LowPaper            = 0x0080
  NoPaper             = 0x0040
  LowToner            = 0x0020
  NoToner             = 0x0010
  DoorOpen            = 0x0008
  Jammed              = 0x0004
  Offline             = 0x0002
  ServiceRequested    = 0x0001 }


[hrPrinterDetectedErrorState]8

DoorOpen


[hrPrinterDetectedErrorState]0x10

NoToner


[hrPrinterDetectedErrorState][int][char]'@' # 0x40
 
NoPaper

Python version:

from enum import IntFlag

class hrPrinterDetectedErrorState(IntFlag):
  LowPaper            = 0x0080
  NoPaper             = 0x0040
  LowToner            = 0x0020
  NoToner             = 0x0010
  DoorOpen            = 0x0008
  Jammed              = 0x0004
  Offline             = 0x0002
  ServiceRequested    = 0x0001

print(hrPrinterDetectedErrorState(8))
print(hrPrinterDetectedErrorState(0x10))
print(hrPrinterDetectedErrorState(0x18))
print(hrPrinterDetectedErrorState(ord('@')))
hrPrinterDetectedErrorState.DoorOpen
hrPrinterDetectedErrorState.NoToner
hrPrinterDetectedErrorState.NoToner|DoorOpen
hrPrinterDetectedErrorState.NoPaper

With two bytes, the question is whether to put them into a 16-bit integer in big endian/network byte order (which would match how it’s documented) or little endian/host byte order (windows, linux), and then have the bit flags match it. 90% of the time I only see one byte. I have one printer that sends four bytes. The weird printers tend to be plotters.

Answered By: js2010