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?
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.
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
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.
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?
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.
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
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.