class ModelCollector(object): """A controller that fetches data for responders.""" (EMPTYING_QUEUE, DEVICE_INFO, MODEL_DESCRIPTION, SUPPORTED_PARAMS, SOFTWARE_VERSION_LABEL, PERSONALITIES, SENSORS, MANUFACTURER_PIDS, LANGUAGE, LANGUAGES, SLOT_INFO, SLOT_DESCRIPTION, SLOT_DEFAULT_VALUE) = xrange(13) def __init__(self, wrapper, pid_store): self.wrapper = wrapper self.pid_store = pid_store self.client = self.wrapper.Client() self.rdm_api = RDMAPI(self.client, self.pid_store) def Run(self, universe, skip_queued_messages): """Run the collector. Args: universe: The universe to collect skip_queued_messages: """ self.universe = universe self.skip_queued_messages = skip_queued_messages self._ResetData() self.client.RunRDMDiscovery(self.universe, True, self._HandleUIDList) self.wrapper.Run() # strip various info that is redundant for model_list in self.data.values(): for model in model_list: for key in [ 'language', 'current_personality', 'personality_count', 'sensor_count' ]: if key in model: del model[key] return self.data def _ResetData(self): """Reset the internal data structures.""" self.uids = [] self.uid = None # the current uid we're fetching from self.outstanding_pid = None self.work_state = None self.manufacturer_pids = [] self.slots = [] self.personalities = [] self.sensors = [] # keyed by manufacturer id self.data = {} def _GetDevice(self): return self.data[self.uid.manufacturer_id][-1] def _GetVersion(self): this_device = self._GetDevice() software_versions = this_device['software_versions'] return software_versions[software_versions.keys()[0]] def _GetCurrentPersonality(self): this_device = self._GetDevice() this_version = self._GetVersion() personalities = this_version['personalities'] return personalities[(this_device['current_personality'] - 1)] def _GetSlotData(self, slot): this_personality = self._GetCurrentPersonality() return this_personality.setdefault('slots', {}).setdefault(slot, {}) def _GetLanguage(self): this_device = self._GetDevice() return this_device.get('language', DEFAULT_LANGUAGE) def _CheckPidSupported(self, pid): this_version = self._GetVersion() if pid.value in this_version['supported_parameters']: return True else: return False def _GetPid(self, pid): self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._RDMRequestComplete) logging.debug('Sent %s request' % pid) self.outstanding_pid = pid def _HandleUIDList(self, state, uids): """Called when the UID list arrives.""" if not state.Succeeded(): raise DiscoveryException(state.message) found_uids = set() for uid in uids: found_uids.add(uid) logging.debug(uid) self.uids = list(found_uids) self._FetchNextUID() def _HandleResponse(self, unpacked_data): logging.debug(unpacked_data) if self.work_state == self.DEVICE_INFO: self._HandleDeviceInfo(unpacked_data) elif self.work_state == self.MODEL_DESCRIPTION: self._HandleDeviceModelDescription(unpacked_data) elif self.work_state == self.SUPPORTED_PARAMS: self._HandleSupportedParams(unpacked_data) elif self.work_state == self.SOFTWARE_VERSION_LABEL: self._HandleSoftwareVersionLabel(unpacked_data) elif self.work_state == self.PERSONALITIES: self._HandlePersonalityData(unpacked_data) elif self.work_state == self.SENSORS: self._HandleSensorData(unpacked_data) elif self.work_state == self.MANUFACTURER_PIDS: self._HandleManufacturerPids(unpacked_data) elif self.work_state == self.LANGUAGE: self._HandleLanguage(unpacked_data) elif self.work_state == self.LANGUAGES: self._HandleLanguages(unpacked_data) elif self.work_state == self.SLOT_INFO: self._HandleSlotInfo(unpacked_data) elif self.work_state == self.SLOT_DESCRIPTION: self._HandleSlotDescription(unpacked_data) elif self.work_state == self.SLOT_DEFAULT_VALUE: self._HandleSlotDefaultValue(unpacked_data) def _HandleDeviceInfo(self, data): """Called when we get a DEVICE_INFO response.""" this_device = self._GetDevice() fields = [ 'device_model', 'product_category', 'current_personality', 'personality_count', 'sensor_count', 'sub_device_count' ] for field in fields: this_device[field] = data[field] this_device['software_versions'][data['software_version']] = { 'languages': [], 'personalities': [], 'manufacturer_pids': [], 'supported_parameters': [], 'sensors': [], } self.personalities = list(xrange(1, data['personality_count'] + 1)) self.sensors = list(xrange(0, data['sensor_count'])) self._NextState() def _HandleDeviceModelDescription(self, data): """Called when we get a DEVICE_MODEL_DESCRIPTION response.""" this_device = self._GetDevice() this_device['model_description'] = data['description'] self._NextState() def _HandleSupportedParams(self, data): """Called when we get a SUPPORTED_PARAMETERS response.""" this_version = self._GetVersion() for param_info in data['params']: this_version['supported_parameters'].append(param_info['param_id']) if (param_info['param_id'] >= ola.RDMConstants.RDM_MANUFACTURER_PID_MIN and param_info['param_id'] <= ola.RDMConstants.RDM_MANUFACTURER_PID_MAX): self.manufacturer_pids.append(param_info['param_id']) self._NextState() def _HandleSoftwareVersionLabel(self, data): """Called when we get a SOFTWARE_VERSION_LABEL response.""" this_version = self._GetVersion() this_version['label'] = data['label'] self._NextState() def _HandlePersonalityData(self, data): """Called when we get a DMX_PERSONALITY_DESCRIPTION response.""" this_version = self._GetVersion() this_version['personalities'].append({ 'description': data['name'], 'index': data['personality'], 'slot_count': data['slots_required'], }) self._FetchNextPersonality() def _HandleSensorData(self, data): """Called when we get a SENSOR_DEFINITION response.""" this_version = self._GetVersion() this_version['sensors'].append({ 'index': data['sensor_number'], 'description': data['name'], 'type': data['type'], 'supports_recording': data['supports_recording'], }) self._FetchNextSensor() def _HandleManufacturerPids(self, data): """Called when we get a PARAMETER_DESCRIPTION response.""" this_version = self._GetVersion() this_version['manufacturer_pids'].append({ 'pid': data['pid'], 'description': data['description'], 'command_class': data['command_class'], 'data_type': data['data_type'], 'unit': data['unit'], 'prefix': data['prefix'], 'min_value': data['min_value'], 'default_value': data['default_value'], 'max_value': data['max_value'], }) self._FetchNextManufacturerPid() def _HandleLanguage(self, data): """Called when we get a LANGUAGE response.""" this_device = self._GetDevice() this_device['language'] = data['language'] self._NextState() def _HandleLanguages(self, data): """Called when we get a LANGUAGE_CAPABILITIES response.""" this_version = self._GetVersion() for language in data['languages']: this_version['languages'].append(language['language']) self._NextState() def _HandleSlotInfo(self, data): """Called when we get a SLOT_INFO response.""" for slot in data['slots']: this_slot_data = self._GetSlotData(slot['slot_offset']) this_slot_data['label_id'] = slot['slot_label_id'] this_slot_data['type'] = slot['slot_type'] self.slots.append(slot['slot_offset']) self._NextState() def _HandleSlotDescription(self, data): """Called when we get a SLOT_DESCRIPTION response.""" if data is not None: # Got valid data, not a nack this_slot_data = self._GetSlotData(data['slot_number']) this_slot_data.setdefault('name', {})[self._GetLanguage()] = data['name'] self._FetchNextSlotDescription() def _HandleSlotDefaultValue(self, data): """Called when we get a DEFAULT_SLOT_VALUE response.""" for slot in data['slot_values']: this_slot_data = self._GetSlotData(slot['slot_offset']) this_slot_data['default_value'] = slot['default_slot_value'] self._NextState() def _NextState(self): """Move to the next state of information fetching.""" if self.work_state == self.EMPTYING_QUEUE: # fetch device info pid = self.pid_store.GetName('DEVICE_INFO') self._GetPid(pid) self.work_state = self.DEVICE_INFO elif self.work_state == self.DEVICE_INFO: # fetch device model description pid = self.pid_store.GetName('DEVICE_MODEL_DESCRIPTION') self._GetPid(pid) self.work_state = self.MODEL_DESCRIPTION elif self.work_state == self.MODEL_DESCRIPTION: # fetch supported params pid = self.pid_store.GetName('SUPPORTED_PARAMETERS') self._GetPid(pid) self.work_state = self.SUPPORTED_PARAMS elif self.work_state == self.SUPPORTED_PARAMS: # fetch software version label pid = self.pid_store.GetName('SOFTWARE_VERSION_LABEL') self._GetPid(pid) self.work_state = self.SOFTWARE_VERSION_LABEL elif self.work_state == self.SOFTWARE_VERSION_LABEL: self.work_state = self.PERSONALITIES self._FetchNextPersonality() elif self.work_state == self.PERSONALITIES: self.work_state = self.SENSORS self._FetchNextSensor() elif self.work_state == self.SENSORS: self.work_state = self.MANUFACTURER_PIDS self._FetchNextManufacturerPid() elif self.work_state == self.MANUFACTURER_PIDS: self.work_state = self.LANGUAGE pid = self.pid_store.GetName('LANGUAGE') if self._CheckPidSupported(pid): self._GetPid(pid) else: logging.debug( "Skipping pid %s as it's not supported on this device" % pid) self._NextState() elif self.work_state == self.LANGUAGE: # fetch language capabilities self.work_state = self.LANGUAGES pid = self.pid_store.GetName('LANGUAGE_CAPABILITIES') if self._CheckPidSupported(pid): self._GetPid(pid) else: logging.debug( "Skipping pid %s as it's not supported on this device" % pid) self._NextState() elif self.work_state == self.LANGUAGES: # fetch slot info self.work_state = self.SLOT_INFO pid = self.pid_store.GetName('SLOT_INFO') if self._CheckPidSupported(pid): self._GetPid(pid) else: logging.debug( "Skipping pid %s as it's not supported on this device" % pid) self._NextState() elif self.work_state == self.SLOT_INFO: self.work_state = self.SLOT_DESCRIPTION self._FetchNextSlotDescription() elif self.work_state == self.SLOT_DESCRIPTION: # fetch slot default value self.work_state = self.SLOT_DEFAULT_VALUE pid = self.pid_store.GetName('DEFAULT_SLOT_VALUE') if self._CheckPidSupported(pid): self._GetPid(pid) else: logging.debug( "Skipping pid %s as it's not supported on this device" % pid) self._NextState() else: # this one is done, onto the next UID self._FetchNextUID() def _FetchNextUID(self): """Start fetching the info for the next UID.""" if not self.uids: self.wrapper.Stop() return self.uid = self.uids.pop() self.personalities = [] self.sensors = [] logging.debug('Fetching data for %s' % self.uid) devices = self.data.setdefault(self.uid.manufacturer_id, []) devices.append({ 'software_versions': {}, }) self.work_state = self.EMPTYING_QUEUE if self.skip_queued_messages: # proceed to the fetch now self._NextState() else: self.queued_message_failures = 0 self._FetchQueuedMessages() def _FetchNextPersonality(self): """Fetch the info for the next personality, or proceed to the next state if there are none left. """ pid = self.pid_store.GetName('DMX_PERSONALITY_DESCRIPTION') if self._CheckPidSupported(pid): if self.personalities: personality_index = self.personalities.pop(0) self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._RDMRequestComplete, [personality_index]) logging.debug('Sent DMX_PERSONALITY_DESCRIPTION request') self.outstanding_pid = pid else: self._NextState() else: if self.personalities: # If we have personalities but no description, we still need the basic # data structure to add the other info to this_version = self._GetVersion() for personality_index in self.personalities: this_version['personalities'].append({ 'index': personality_index, }) logging.debug( "Skipping pid %s as it's not supported on this device" % pid) self._NextState() def _FetchNextSensor(self): """Fetch the info for the next sensor, or proceed to the next state if there are none left. """ if self.sensors: sensor_index = self.sensors.pop(0) pid = self.pid_store.GetName('SENSOR_DEFINITION') self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._RDMRequestComplete, [sensor_index]) logging.debug('Sent SENSOR_DEFINITION request') self.outstanding_pid = pid else: self._NextState() def _FetchNextManufacturerPid(self): """Fetch the info for the next manufacturer PID, or proceed to the next state if there are none left. """ if self.manufacturer_pids: manufacturer_pid = self.manufacturer_pids.pop(0) pid = self.pid_store.GetName('PARAMETER_DESCRIPTION') self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._RDMRequestComplete, [manufacturer_pid]) logging.debug('Sent PARAMETER_DESCRIPTION request') self.outstanding_pid = pid else: self._NextState() def _FetchNextSlotDescription(self): """Fetch the description for the next slot, or proceed to the next state if there are none left. """ pid = self.pid_store.GetName('SLOT_DESCRIPTION') if self._CheckPidSupported(pid): if self.slots: slot = self.slots.pop(0) self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._RDMRequestComplete, [slot]) logging.debug('Sent SLOT_DESCRIPTION request for slot %d' % slot) self.outstanding_pid = pid else: self._NextState() else: logging.debug( "Skipping pid %s as it's not supported on this device" % pid) self._NextState() def _FetchQueuedMessages(self): """Fetch messages until the queue is empty.""" pid = self.pid_store.GetName('QUEUED_MESSAGE') self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._QueuedMessageComplete, ['advisory']) logging.debug('Sent GET QUEUED_MESSAGE') def _RDMRequestComplete(self, response, unpacked_data, unpack_exception): if not self._CheckForAckOrNack(response): return # at this stage the response is either a ack or nack # We have to allow nacks from SLOT_DESCRIPTION, as it may not have a # description for every slot if (response.response_type == OlaClient.RDM_NACK_REASON and response.pid != self.pid_store.GetName('SLOT_DESCRIPTION').value): print('Got nack with reason for pid %s: %s' % (response.pid, response.nack_reason)) self._NextState() elif unpack_exception: print unpack_exception self.wrapper.Stop() else: self._HandleResponse(unpacked_data) def _QueuedMessageComplete(self, response, unpacked_data, unpack_exception): """Called when a queued message is returned.""" if not self._CheckForAckOrNack(response): return if response.response_type == OlaClient.RDM_NACK_REASON: if (self.outstanding_pid and response.command_class == OlaClient.RDM_GET_RESPONSE and response.pid == self.outstanding_pid.value): # we found what we were looking for logging.debug( 'Received matching queued message, response was NACK') self.outstanding_pid = None self._NextState() elif (response.nack_reason == RDMNack.NR_UNKNOWN_PID and response.pid == self.pid_store.GetName('QUEUED_MESSAGE').value): logging.debug('Device doesn\'t support queued messages') self._NextState() else: print 'Got nack for 0x%04hx with reason: %s' % ( response.pid, response.nack_reason) elif unpack_exception: print 'Invalid Param data: %s' % unpack_exception self.queued_message_failures += 1 if self.queued_message_failures >= 10: # declare this bad and move on self._NextState() else: self._FetchQueuedMessages() else: status_messages_pid = self.pid_store.GetName('STATUS_MESSAGES') queued_message_pid = self.pid_store.GetName('QUEUED_MESSAGE') if (response.pid == status_messages_pid.value and unpacked_data.get('messages', []) == []): logging.debug('Got back empty list of STATUS_MESSAGES') self._NextState() return elif response.pid == queued_message_pid.value: logging.debug( 'Got back QUEUED_MESSAGE, this is a bad responder') self._NextState() return logging.debug('Got pid 0x%hx' % response.pid) if self.outstanding_pid and response.pid == self.outstanding_pid.value: self._HandleResponse(unpacked_data) else: self._FetchQueuedMessages() def _CheckForAckOrNack(self, response): """Check for all the different error conditions. Returns: True if this response was an ACK or NACK, False for all other cases. """ if not response.status.Succeeded(): print response.status.message self.wrapper.Stop() return False if response.response_code != OlaClient.RDM_COMPLETED_OK: print response.ResponseCodeAsString() self.wrapper.Stop() return False if response.response_type == OlaClient.RDM_ACK_TIMER: # schedule the fetch logging.debug('Got ack timer for %d ms' % response.ack_timer) self.wrapper.AddEvent(response.ack_timer, self._FetchQueuedMessages) return False return True
class ConfigReader(object): """A controller that fetches data for responders.""" (EMPTYING_QUEUE, DMX_START_ADDRESS, DEVICE_LABEL, PERSONALITY) = range(4) def __init__(self, wrapper, pid_store): self.wrapper = wrapper self.pid_store = pid_store self.client = self.wrapper.Client() self.rdm_api = RDMAPI(self.client, self.pid_store) def Run(self, universe, skip_queued_messages): """Run the collector. Args: universe: The universe to collect skip_queued_messages: """ self.universe = universe self.skip_queued_messages = skip_queued_messages self._ResetData() self.client.RunRDMDiscovery(self.universe, True, self._HandleUIDList) self.wrapper.Run() return self.data def _ResetData(self): """Reset the internal data structures.""" self.uids = [] self.uid = None # the current uid we're fetching from self.outstanding_pid = None self.work_state = None # uid: {'start_address': '', 'personality': '', 'label': ''} self.data = {} def _GetPid(self, pid): self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._RDMRequestComplete) logging.debug('Sent %s request' % pid) self.outstanding_pid = pid def _HandleUIDList(self, state, uids): """Called when the UID list arrives.""" if not state.Succeeded(): raise DiscoveryException(state.message) found_uids = set() for uid in uids: found_uids.add(uid) logging.debug(uid) self.uids = list(found_uids) self._FetchNextUID() def _HandleResponse(self, unpacked_data): logging.debug(unpacked_data) if self.work_state == self.DMX_START_ADDRESS: self._HandleStartAddress(unpacked_data) elif self.work_state == self.DEVICE_LABEL: self._HandleDeviceLabel(unpacked_data) elif self.work_state == self.PERSONALITY: self._HandlePersonality(unpacked_data) def _HandleStartAddress(self, data): """Called when we get a DMX_START_ADDRESS response.""" this_device = self.data.setdefault(str(self.uid), {}) this_device['dmx_start_address'] = data['dmx_address'] self._NextState() def _HandleDeviceLabel(self, data): """Called when we get a DEVICE_LABEL response.""" this_device = self.data.setdefault(str(self.uid), {}) this_device['label'] = data['label'] self._NextState() def _HandlePersonality(self, data): """Called when we get a DMX_PERSONALITY response.""" this_device = self.data.setdefault(str(self.uid), {}) this_device['personality'] = data['current_personality'] self._NextState() def _NextState(self): """Move to the next state of information fetching.""" if self.work_state == self.EMPTYING_QUEUE: # fetch start address pid = self.pid_store.GetName('DMX_START_ADDRESS') self._GetPid(pid) self.work_state = self.DMX_START_ADDRESS elif self.work_state == self.DMX_START_ADDRESS: # fetch device label pid = self.pid_store.GetName('DEVICE_LABEL') self._GetPid(pid) self.work_state = self.DEVICE_LABEL elif self.work_state == self.DEVICE_LABEL: # fetch personality pid = self.pid_store.GetName('DMX_PERSONALITY') self._GetPid(pid) self.work_state = self.PERSONALITY else: # this one is done, onto the next UID self._FetchNextUID() def _FetchNextUID(self): """Start fetching the info for the next UID.""" if not self.uids: self.wrapper.Stop() return self.uid = self.uids.pop() self.personalities = [] self.sensors = [] logging.debug('Fetching data for %s' % self.uid) self.work_state = self.EMPTYING_QUEUE if self.skip_queued_messages: # proceed to the fetch now self._NextState() else: self._FetchQueuedMessages() def _FetchNextPersonality(self): """Fetch the info for the next personality, or proceed to the next state if there are none left. """ if self.personalities: personality_index = self.personalities.pop(0) pid = self.pid_store.GetName('DMX_PERSONALITY_DESCRIPTION') self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._RDMRequestComplete, [personality_index]) logging.debug('Sent DMX_PERSONALITY_DESCRIPTION request') self.outstanding_pid = pid else: self._NextState() def _FetchNextSensor(self): """Fetch the info for the next sensor, or proceed to the next state if there are none left. """ if self.sensors: sensor_index = self.sensors.pop(0) pid = self.pid_store.GetName('SENSOR_DEFINITION') self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._RDMRequestComplete, [sensor_index]) logging.debug('Sent SENSOR_DEFINITION request') self.outstanding_pid = pid else: self._NextState() def _FetchQueuedMessages(self): """Fetch messages until the queue is empty.""" pid = self.pid_store.GetName('QUEUED_MESSAGE') self.rdm_api.Get(self.universe, self.uid, PidStore.ROOT_DEVICE, pid, self._QueuedMessageComplete, ['advisory']) logging.debug('Sent GET QUEUED_MESSAGE') def _QueuedMessageFound(self): if self.work_state == self.EMPTYING_QUEUE: self._FetchQueuedMessages() def _RDMRequestComplete(self, response, unpacked_data, unpack_exception): if not self._CheckForAckOrNack(response): return # at this stage the response is either a ack or nack if response.response_type == OlaClient.RDM_NACK_REASON: print('Got nack with reason for PID %d: %s' % (response.pid, response.nack_reason)) self._NextState() elif unpack_exception: print(unpack_exception) self.wrapper.Stop() else: self._HandleResponse(unpacked_data) def _QueuedMessageComplete(self, response, unpacked_data, unpack_exception): """Called when a queued message is returned.""" if not self._CheckForAckOrNack(response): return if response.response_type == OlaClient.RDM_NACK_REASON: if (self.outstanding_pid and response.command_class == OlaClient.RDM_GET_RESPONSE and response.pid == self.outstanding_pid.value): # we found what we were looking for print("found, but nacked") self.outstanding_pid = None self._NextState() elif (response.nack_reason == RDMNack.NR_UNKNOWN_PID and response.pid == self.pid_store.GetName('QUEUED_MESSAGE').value): logging.debug('Device doesn\'t support queued messages') self._NextState() else: print('Got nack for 0x%04hx with reason: %s' % (response.pid, response.nack_reason)) elif unpack_exception: print('Invalid Param data: %s' % unpack_exception) else: status_messages_pid = self.pid_store.GetName('STATUS_MESSAGES') queued_message_pid = self.pid_store.GetName('QUEUED_MESSAGE') if (response.pid == status_messages_pid.value and unpacked_data.get('messages', []) == []): logging.debug('Got back empty list of STATUS_MESSAGES') self._NextState() return elif response.pid == queued_message_pid.value: logging.debug( 'Got back QUEUED_MESSAGE, this is a bad responder') self._NextState() return logging.debug('Got pid 0x%hx' % response.pid) if self.outstanding_pid and response.pid == self.outstanding_pid.value: self._HandleResponse(unpacked_data) else: self._FetchQueuedMessages() def _CheckForAckOrNack(self, response): """Check for all the different error conditions. Returns: True if this response was an ACK or NACK, False for all other cases. """ if not response.status.Succeeded(): print(response.status.message) self.wrapper.Stop() return False if response.response_code != OlaClient.RDM_COMPLETED_OK: print(response.ResponseCodeAsString()) self.wrapper.Stop() return False if response.response_type == OlaClient.RDM_ACK_TIMER: # schedule the fetch logging.debug('Got ack timer for %d ms' % response.ack_timer) self.wrapper.AddEvent(response.ack_timer, self._FetchQueuedMessages) return False return True
def testGetWithResponse(self): """uses client to send an RDM get with mocked olad. Regression test that confirms sent message is correct and sends fixed response message.""" sockets = socket.socketpair() wrapper = ClientWrapper(sockets[0]) pid_store = PidStore.GetStore(pid_store_path) client = wrapper.Client() rdm_api = RDMAPI(client, pid_store) class results: got_request = False got_response = False def DataCallback(self): # request and response for # ola_rdm_get.py -u 1 --uid 7a70:ffffff00 device_info # against olad dummy plugin # enable logging in rpc/StreamRpcChannel.py data = sockets[1].recv(4096) expected = binascii.unhexlify( "29000010080110001a0a52444d436f6d6d616e6422170801120908f0f4011500" "ffffff180020602a0030003800") self.assertEqual(data, expected, msg="Regression check failed. If protocol change " "was intended set expected to: " + str(binascii.hexlify(data))) results.got_request = True response = binascii.unhexlify( "3f0000100802100022390800100018002213010000017fff0000000300050204" "00010000032860300038004a0908f0f4011500ffffff520908f0f40115ac1100" "02580a") sent_bytes = sockets[1].send(response) self.assertEqual(sent_bytes, len(response)) def ResponseCallback(self, response, data, unpack_exception): results.got_response = True self.assertEqual(response.response_type, client.RDM_ACK) self.assertEqual(response.pid, 0x60) self.assertEqual(data["dmx_footprint"], 5) self.assertEqual(data["software_version"], 3) self.assertEqual(data["personality_count"], 4) self.assertEqual(data["device_model"], 1) self.assertEqual(data["current_personality"], 2) self.assertEqual(data["protocol_major"], 1) self.assertEqual(data["protocol_minor"], 0) self.assertEqual(data["product_category"], 32767) self.assertEqual(data["dmx_start_address"], 1) self.assertEqual(data["sub_device_count"], 0) self.assertEqual(data["sensor_count"], 3) wrapper.AddEvent(0, wrapper.Stop) wrapper._ss.AddReadDescriptor(sockets[1], lambda: DataCallback(self)) uid = UID.FromString("7a70:ffffff00") pid = pid_store.GetName("DEVICE_INFO") rdm_api.Get(1, uid, 0, pid, lambda x, y, z: ResponseCallback(self, x, y, z)) wrapper.Run() sockets[0].close() sockets[1].close() self.assertTrue(results.got_request) self.assertTrue(results.got_response)
class InteractiveModeController(cmd.Cmd): """Interactive mode!""" def __init__(self, universe, uid, sub_device, pid_location): """Create a new InteractiveModeController. Args: universe: uid: sub_device: pid_location: """ cmd.Cmd.__init__(self) self._universe = universe self._uid = uid self._sub_device = sub_device self.pid_store = PidStore.GetStore(pid_location) self.wrapper = ClientWrapper() self.client = self.wrapper.Client() self.rdm_api = RDMAPI(self.client, self.pid_store) self._uids = [] self._response_printer = ResponsePrinter() # tuple of (sub_device, command_class, pid) self._outstanding_request = None self.prompt = '> ' def emptyline(self): pass def do_exit(self, s): """Exit the interpreter.""" return True def do_EOF(self, s): print('') return self.do_exit('') def do_uid(self, line): """Sets the active UID.""" args = line.split() if len(args) != 1: print('*** Requires a single UID argument') return uid = UID.FromString(args[0]) if uid is None: print('*** Invalid UID') return if uid not in self._uids: print('*** UID not found') return self._uid = uid print('Fetching queued messages...') self._FetchQueuedMessages() self.wrapper.Run() def complete_uid(self, text, line, start_index, end_index): tokens = line.split() if len(tokens) > 1 and text == '': return [] uids = [str(uid) for uid in self._uids if str(uid).startswith(text)] return uids def do_subdevice(self, line): """Sets the sub device.""" args = line.split() if len(args) != 1: print('*** Requires a single int argument') return try: sub_device = int(args[0]) except ValueError: print('*** Requires a single int argument') return if sub_device < 0 or sub_device > PidStore.ALL_SUB_DEVICES: print('*** Argument must be between 0 and 0x%hx' % PidStore.ALL_SUB_DEVICES) return self._sub_device = sub_device def do_print(self, line): """Prints the current universe, UID and sub device.""" print(textwrap.dedent("""\ Universe: %d UID: %s Sub Device: %d""" % ( self._universe, self._uid, self._sub_device))) def do_uids(self, line): """List the UIDs for this universe.""" self.client.FetchUIDList(self._universe, self._DisplayUids) self.wrapper.Run() def _DisplayUids(self, state, uids): self._uids = [] if state.Succeeded(): self._UpdateUids(uids) for uid in uids: print(str(uid)) self.wrapper.Stop() def do_full_discovery(self, line): """Run full RDM discovery for this universe.""" self.client.RunRDMDiscovery(self._universe, True, self._DiscoveryDone) self.wrapper.Run() def do_incremental_discovery(self, line): """Run incremental RDM discovery for this universe.""" self.client.RunRDMDiscovery(self._universe, False, self._DiscoveryDone) self.wrapper.Run() def _DiscoveryDone(self, state, uids): if state.Succeeded(): self._UpdateUids(uids) self.wrapper.Stop() def _UpdateUids(self, uids): self._uids = [] for uid in uids: self._uids.append(uid) def do_list(self, line): """List the pids available.""" names = [] for pid in self.pid_store.Pids(): names.append('%s (0x%04hx)' % (pid.name.lower(), pid.value)) if self._uid: for pid in self.pid_store.ManufacturerPids(self._uid.manufacturer_id): names.append('%s (0x%04hx)' % (pid.name.lower(), pid.value)) names.sort() print('\n'.join(names)) def do_queued(self, line): """Fetch all the queued messages.""" self._FetchQueuedMessages() self.wrapper.Run() def do_get(self, line): """Send a GET command.""" self.GetOrSet(PidStore.RDM_GET, line) def complete_get(self, text, line, start_index, end_index): return self.CompleteGetOrSet(PidStore.RDM_GET, text, line) def do_set(self, line): """Send a SET command.""" self.GetOrSet(PidStore.RDM_SET, line) def complete_set(self, text, line, start_index, end_index): return self.CompleteGetOrSet(PidStore.RDM_SET, text, line) def CompleteGetOrSet(self, request_type, text, line): if len(line.split(' ')) > 2: return [] pids = [pid for pid in self.pid_store.Pids() if pid.name.lower().startswith(text)] if self._uid: for pid in self.pid_store.ManufacturerPids(self._uid.manufacturer_id): if pid.name.lower().startswith(text): pids.append(pid) # now check if this type of request is supported pid_names = sorted([pid.name.lower() for pid in pids if pid.RequestSupported(request_type)]) return pid_names def GetOrSet(self, request_type, line): if self._uid is None: print('*** No UID selected, use the uid command') return args = line.split() command = 'get' if request_type == PidStore.RDM_SET: command = 'set' if len(args) < 1: print('%s <pid> [args]' % command) return pid = None try: pid = self.pid_store.GetPid(int(args[0], 0), self._uid.manufacturer_id) except ValueError: pid = self.pid_store.GetName(args[0].upper(), self._uid.manufacturer_id) if pid is None: print('*** Unknown pid %s' % args[0]) return if not pid.RequestSupported(request_type): print('*** PID does not support command') return if request_type == PidStore.RDM_SET: method = self.rdm_api.Set else: method = self.rdm_api.Get try: if method(self._universe, self._uid, self._sub_device, pid, self._RDMRequestComplete, args[1:]): self._outstanding_request = (self._sub_device, request_type, pid) self.wrapper.Run() except PidStore.ArgsValidationError as e: args, help_string = pid.GetRequestDescription(request_type) print('Usage: %s %s %s' % (command, pid.name.lower(), args)) print(help_string) print('') print('*** %s' % e) return def _FetchQueuedMessages(self): """Fetch messages until the queue is empty.""" pid = self.pid_store.GetName('QUEUED_MESSAGE', self._uid.manufacturer_id) self.rdm_api.Get(self._universe, self._uid, PidStore.ROOT_DEVICE, pid, self._QueuedMessageComplete, ['error']) def _RDMRequestComplete(self, response, unpacked_data, unpack_exception): if not self._CheckForAckOrNack(response): return # at this stage the response is either a ack or nack self._outstanding_request = None self.wrapper.Stop() if response.response_type == OlaClient.RDM_NACK_REASON: print('Got nack with reason: %s' % response.nack_reason) elif unpack_exception: print(unpack_exception) else: self._response_printer.PrintResponse(self._uid, response.pid, unpacked_data) if response.queued_messages: print('%d queued messages remain' % response.queued_messages) def _QueuedMessageComplete(self, response, unpacked_data, unpack_exception): if not self._CheckForAckOrNack(response): return if response.response_type == OlaClient.RDM_NACK_REASON: if (self._outstanding_request and response.sub_device == self._outstanding_request[0] and response.command_class == self._outstanding_request[1] and response.pid == self._outstanding_request[2].value): # we found what we were looking for self._outstanding_request = None self.wrapper.StopIfNoEvents() elif (response.nack_reason == RDMNack.NR_UNKNOWN_PID and response.pid == self.pid_store.GetName('QUEUED_MESSAGE').value): print('Device doesn\'t support queued messages') self.wrapper.StopIfNoEvents() else: print('Got nack for 0x%04hx with reason: %s' % ( response.pid, response.nack_reason)) elif unpack_exception: print('Invalid Param data: %s' % unpack_exception) else: status_messages_pid = self.pid_store.GetName('STATUS_MESSAGES') if (response.pid == status_messages_pid.value and unpacked_data.get('messages', []) == []): self.wrapper.StopIfNoEvents() return self._response_printer.PrintResponse(self._uid, response.pid, unpacked_data) if response.queued_messages: self._FetchQueuedMessages() else: self.wrapper.StopIfNoEvents() def _CheckForAckOrNack(self, response): """Check for all the different error conditions. Returns: True if this response was an ACK or NACK, False for all other cases. """ if not response.status.Succeeded(): print(response.status.message) self.wrapper.Stop() return False if response.response_code != OlaClient.RDM_COMPLETED_OK: print(response.ResponseCodeAsString()) self.wrapper.Stop() return False if response.response_type == OlaClient.RDM_ACK_TIMER: # schedule the fetch self.wrapper.AddEvent(response.ack_timer, self._FetchQueuedMessages) return False return True
def testGetParamsWithResponse(self): """uses client to send an RDM get with mocked olad. Regression test that confirms sent message is correct and sends fixed response message.""" sockets = socket.socketpair() wrapper = ClientWrapper(sockets[0]) pid_store = PidStore.GetStore(pid_store_path) client = wrapper.Client() rdm_api = RDMAPI(client, pid_store) class results: got_request = False got_response = False def DataCallback(self): # request and response for # ola_rdm_get.py -u 1 --uid 7a70:ffffff00 DMX_PERSONALITY_DESCRIPTION 2 # against olad dummy plugin # enable logging in rpc/StreamRpcChannel.py data = sockets[1].recv(4096) expected = binascii.unhexlify( "2b000010080110001a0a52444d436f6d6d616e6422190801120908f0f4011500" "ffffff180020e1012a010230003800") self.assertEqual(data, expected, msg="Regression check failed. If protocol change " "was intended set expected to: " + str(binascii.hexlify(data))) results.got_request = True response = binascii.unhexlify( "3d0000100802100022370800100018002210020005506572736f6e616c697479" "203228e101300038004a0908f0f4011500ffffff520908f0f40115ac107de058" "29") sent_bytes = sockets[1].send(response) self.assertEqual(sent_bytes, len(response)) def ResponseCallback(self, response, data, unpack_exception): results.got_response = True self.assertEqual(response.response_type, client.RDM_ACK) self.assertEqual(response.pid, 0xe1) self.assertEqual(data['personality'], 2) self.assertEqual(data['slots_required'], 5) self.assertEqual(data['name'], "Personality 2") wrapper.AddEvent(0, wrapper.Stop) wrapper._ss.AddReadDescriptor(sockets[1], lambda: DataCallback(self)) uid = UID.FromString("7a70:ffffff00") pid = pid_store.GetName("DMX_PERSONALITY_DESCRIPTION") rdm_api.Get(1, uid, 0, pid, lambda x, y, z: ResponseCallback(self, x, y, z), args=["2"]) wrapper.Run() sockets[0].close() sockets[1].close() self.assertTrue(results.got_request) self.assertTrue(results.got_response)