docs: def handleMessage(self, lmsg):
          """Implements the LLRP client state machine."""
          logger.debug('LLRPMessage received in state %s: %s', self.state, lmsg)
          msgName = lmsg.getName()
          lmsg.proto = self
          lmsg.peername = self.peername
  
          # call per-message callbacks
          logger.debug('starting message callbacks for %s', msgName)
          for fn in self._message_callbacks[msgName]:
              fn(lmsg)
          logger.debug('done with message callbacks for %s', msgName)
  
          # keepalives can occur at any time
          if msgName == 'KEEPALIVE':
              self.send_KEEPALIVE_ACK()
              return
  
          if msgName == 'RO_ACCESS_REPORT' and \
                  self.state != LLRPClient.STATE_INVENTORYING:
              logger.debug('ignoring RO_ACCESS_REPORT because not inventorying')
              return
  
          if msgName == 'READER_EVENT_NOTIFICATION' and \
                  self.state >= LLRPClient.STATE_CONNECTED:
              logger.debug('Got reader event notification')
              return
  
          logger.debug('in handleMessage(%s), there are %d Deferreds',
                       msgName, len(self._deferreds[msgName]))
  
          #######
          # LLRP client state machine follows.  Beware: gets thorny.  Note the
          # order of the LLRPClient.STATE_* fields.
          #######
  
          # in DISCONNECTED, CONNECTING, and CONNECTED states, expect only
          # READER_EVENT_NOTIFICATION messages.
          if self.state in (LLRPClient.STATE_DISCONNECTED,
                            LLRPClient.STATE_CONNECTING,
                            LLRPClient.STATE_CONNECTED):
              if msgName != 'READER_EVENT_NOTIFICATION':
                  logger.error('unexpected message %s while connecting', msgName)
                  return
  
              if not lmsg.isSuccess():
                  rend = lmsg.msgdict[msgName]['ReaderEventNotificationData']
                  try:
                      status = rend['ConnectionAttemptEvent']['Status']
                  except KeyError:
                      status = '(unknown status)'
                  logger.fatal('Could not start session on reader: %s', status)
                  return
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
              # a Deferred to call when we get GET_READER_CAPABILITIES_RESPONSE
              d = defer.Deferred()
              d.addCallback(self._setState_wrapper, LLRPClient.STATE_CONNECTED)
              d.addErrback(self.panic, 'GET_READER_CAPABILITIES failed')
  
              if (self.impinj_search_mode or
                      self.impinj_tag_content_selector or
                      self.impinj_extended_configuration or
                      self.impinj_fixed_frequency_param):
                  caps = defer.Deferred()
                  caps.addCallback(self.send_GET_READER_CAPABILITIES,
                                   onCompletion=d)
                  caps.addErrback(self.panic, 'ENABLE_IMPINJ_EXTENSIONS failed')
                  self.send_ENABLE_IMPINJ_EXTENSIONS(onCompletion=caps)
              else:
                  self.send_GET_READER_CAPABILITIES(self, onCompletion=d)
  
          elif self.state == LLRPClient.STATE_SENT_ENABLE_IMPINJ_EXTENSIONS:
              logger.debug(lmsg)
              if msgName != 'CUSTOM_MESSAGE':
                  logger.error('unexpected response %s while enabling Impinj'
                               'extensions', msgName)
                  return
  
              if not lmsg.isSuccess():
                  status = lmsg.msgdict[msgName]['LLRPStatus']['StatusCode']
                  err = lmsg.msgdict[msgName]['LLRPStatus']['ErrorDescription']
                  logger.fatal('Error %s enabling Impinj extensions: %s',
                               status, err)
                  return
              logger.debug('Successfully enabled Impinj extensions')
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
          # in state SENT_GET_CAPABILITIES, expect GET_CAPABILITIES_RESPONSE;
          # respond to this message by advancing to state CONNECTED.
          elif self.state == LLRPClient.STATE_SENT_GET_CAPABILITIES:
              if msgName != 'GET_READER_CAPABILITIES_RESPONSE':
                  logger.error('unexpected response %s getting capabilities',
                               msgName)
                  return
  
              if not lmsg.isSuccess():
                  status = lmsg.msgdict[msgName]['LLRPStatus']['StatusCode']
                  err = lmsg.msgdict[msgName]['LLRPStatus']['ErrorDescription']
                  logger.fatal('Error %s getting capabilities: %s', status, err)
                  return
  
              self.capabilities = \
                  lmsg.msgdict['GET_READER_CAPABILITIES_RESPONSE']
              logger.debug('Capabilities: %s', pprint.pformat(self.capabilities))
              try:
                  self.parseCapabilities(self.capabilities)
              except LLRPError as err:
                  logger.exception('Capabilities mismatch')
                  raise err
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
              d = defer.Deferred()
              d.addCallback(self._setState_wrapper,
                            LLRPClient.STATE_SENT_GET_CONFIG)
              d.addErrback(self.panic, 'GET_READER_CONFIG failed')
              self.send_GET_READER_CONFIG(onCompletion=d)
  
          elif self.state == LLRPClient.STATE_SENT_GET_CONFIG:
              if msgName not in ('GET_READER_CONFIG_RESPONSE',
                                 'DELETE_ACCESSSPEC_RESPONSE',
                                 'DELETE_ROSPEC_RESPONSE'):
                  logger.error('unexpected response %s getting config',
                               msgName)
                  return
  
              if not lmsg.isSuccess():
                  status = lmsg.msgdict[msgName]['LLRPStatus']['StatusCode']
                  err = lmsg.msgdict[msgName]['LLRPStatus']['ErrorDescription']
                  logger.fatal('Error %s getting reader config: %s', status, err)
                  return
  
              if msgName == 'GET_READER_CONFIG_RESPONSE':
                  config = lmsg.msgdict['GET_READER_CONFIG_RESPONSE']
                  self.configuration = self.parseReaderConfig(config)
                  logger.debug('Reader configuration: %s', self.configuration)
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
              d = defer.Deferred()
              d.addCallback(self._setState_wrapper,
                            LLRPClient.STATE_SENT_SET_CONFIG)
              d.addErrback(self.panic, 'SET_READER_CONFIG failed')
              self.send_ENABLE_EVENTS_AND_REPORTS()
              self.send_SET_READER_CONFIG(onCompletion=d)
  
          elif self.state == LLRPClient.STATE_SENT_SET_CONFIG:
              if msgName not in ('SET_READER_CONFIG_RESPONSE',
                                 'GET_READER_CONFIG_RESPONSE',
                                 'DELETE_ACCESSSPEC_RESPONSE'):
                  logger.error('unexpected response %s setting config',
                               msgName)
                  return
  
              if not lmsg.isSuccess():
                  status = lmsg.msgdict[msgName]['LLRPStatus']['StatusCode']
                  err = lmsg.msgdict[msgName]['LLRPStatus']['ErrorDescription']
                  logger.fatal('Error %s setting reader config: %s', status, err)
                  return
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
              if self.reset_on_connect:
                  d = self.stopPolitely(disconnect=False)
                  if self.start_inventory:
                      d.addCallback(self.startInventory)
              elif self.start_inventory:
                  self.startInventory()
  
          # in state SENT_ADD_ROSPEC, expect only ADD_ROSPEC_RESPONSE; respond to
          # favorable ADD_ROSPEC_RESPONSE by enabling the added ROSpec and
          # advancing to state SENT_ENABLE_ROSPEC.
          elif self.state == LLRPClient.STATE_SENT_ADD_ROSPEC:
              if msgName != 'ADD_ROSPEC_RESPONSE':
                  logger.error('unexpected response %s when adding ROSpec',
                               msgName)
                  return
  
              if not lmsg.isSuccess():
                  status = lmsg.msgdict[msgName]['LLRPStatus']['StatusCode']
                  err = lmsg.msgdict[msgName]['LLRPStatus']['ErrorDescription']
                  logger.fatal('Error %s adding ROSpec: %s', status, err)
                  return
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
          # in state SENT_ENABLE_ROSPEC, expect only ENABLE_ROSPEC_RESPONSE;
          # respond to favorable ENABLE_ROSPEC_RESPONSE by starting the enabled
          # ROSpec and advancing to state SENT_START_ROSPEC.
          elif self.state == LLRPClient.STATE_SENT_ENABLE_ROSPEC:
              if msgName != 'ENABLE_ROSPEC_RESPONSE':
                  logger.error('unexpected response %s when enabling ROSpec',
                               msgName)
                  return
  
              if not lmsg.isSuccess():
                  status = lmsg.msgdict[msgName]['LLRPStatus']['StatusCode']
                  err = lmsg.msgdict[msgName]['LLRPStatus']['ErrorDescription']
                  logger.fatal('Error %s enabling ROSpec: %s', status, err)
                  return
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
          # in state PAUSING, we have sent a DISABLE_ROSPEC, so expect only
          # DISABLE_ROSPEC_RESPONSE.  advance to state PAUSED.
          elif self.state == LLRPClient.STATE_PAUSING:
              if msgName != 'DISABLE_ROSPEC_RESPONSE':
                  logger.error('unexpected response %s '
                               ' when disabling ROSpec', msgName)
  
              if not lmsg.isSuccess():
                  status = lmsg.msgdict[msgName]['LLRPStatus']['StatusCode']
                  err = lmsg.msgdict[msgName]['LLRPStatus']['ErrorDescription']
                  logger.error('DISABLE_ROSPEC failed with status %s: %s',
                               status, err)
                  logger.warn('Error %s disabling ROSpec: %s', status, err)
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
          # in state SENT_START_ROSPEC, expect only START_ROSPEC_RESPONSE;
          # respond to favorable START_ROSPEC_RESPONSE by advancing to state
          # INVENTORYING.
          elif self.state == LLRPClient.STATE_SENT_START_ROSPEC:
              if msgName == 'RO_ACCESS_REPORT':
                  return
              if msgName == 'READER_EVENT_NOTIFICATION':
                  return
              if msgName != 'START_ROSPEC_RESPONSE':
                  logger.error('unexpected response %s when starting ROSpec',
                               msgName)
  
              if not lmsg.isSuccess():
                  status = lmsg.msgdict[msgName]['LLRPStatus']['StatusCode']
                  err = lmsg.msgdict[msgName]['LLRPStatus']['ErrorDescription']
                  logger.error('START_ROSPEC failed with status %s: %s',
                               status, err)
                  logger.fatal('Error %s starting ROSpec: %s', status, err)
                  return
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
          elif self.state == LLRPClient.STATE_INVENTORYING:
              if msgName not in ('RO_ACCESS_REPORT',
                                 'READER_EVENT_NOTIFICATION',
                                 'ADD_ACCESSSPEC_RESPONSE',
                                 'ENABLE_ACCESSSPEC_RESPONSE',
                                 'DISABLE_ACCESSSPEC_RESPONSE',
                                 'DELETE_ACCESSSPEC_RESPONSE'):
                  logger.error('unexpected message %s while inventorying',
                               msgName)
                  return
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
          elif self.state == LLRPClient.STATE_SENT_DELETE_ACCESSSPEC:
              if msgName != 'DELETE_ACCESSSPEC_RESPONSE':
                  logger.error('unexpected response %s when deleting AccessSpec',
                               msgName)
  
              self.processDeferreds(msgName, lmsg.isSuccess())
  
          elif self.state == LLRPClient.STATE_SENT_DELETE_ROSPEC:
              if msgName != 'DELETE_ROSPEC_RESPONSE':
                  logger.error('unexpected response %s when deleting ROSpec',
                               msgName)
  
              if lmsg.isSuccess():
                  if self.disconnecting:
                      self.setState(LLRPClient.STATE_DISCONNECTED)
                  else:
                      self.setState(LLRPClient.STATE_CONNECTED)
  
              else:
                  status = lmsg.msgdict[msgName]['LLRPStatus']['StatusCode']
                  err = lmsg.msgdict[msgName]['LLRPStatus']['ErrorDescription']
                  logger.error('DELETE_ROSPEC failed with status %s: %s',
                               status, err)
  
              self.processDeferreds(msgName, lmsg.isSuccess())
              if self.disconnecting:
                  logger.info('disconnecting')
                  self.transport.loseConnection()
  
          else:
              logger.warn('message %s received in unknown state!', msgName)
  
          if self._deferreds[msgName]:
              logger.error('there should NOT be Deferreds left for %s,'
                           ' but there are!', msgName)