def setUp(self):
        self.host = 'foobar.baz'
        self.port = 8080
        self.device_id = 'DFDS44'

        self.meta = ThreadSafeDict()
        self.meta['foo'] = mock.Mock(units="F", sampleRate="10", min=0, max=100)
        self.meta['bar'] = mock.Mock(units="C", sampleRate="1", min=-100, max=200)
        self.meta['baz'] = mock.Mock(units="C", sampleRate="1", min=-100, max=200)

        # Mock's constructor treats 'name' in a special way, it won't return a string
        self.meta['foo'].configure_mock(name='foo')
        self.meta['bar'].configure_mock(name='bar')
        self.meta['baz'].configure_mock(name='baz')

        self.data_bus = None
        self.telemetry_connection = None

        self.status_function = mock.Mock()
        self.data_bus = mock.Mock()
        self.telemetry_connection = TelemetryConnection(self.host, self.port, self.device_id, self.meta, self.data_bus, self.status_function)

        # We mock these functions because we don't want to really connect to anything
        # This is a unit test, not integration!
        self.telemetry_connection.create_socket = mock.Mock()
        self.telemetry_connection.connect = mock.Mock()
        self.telemetry_connection.push = mock.Mock()
class TelemetryConnectionTest(unittest.TestCase):

    def setUp(self):
        self.host = 'foobar.baz'
        self.port = 8080
        self.device_id = 'DFDS44'

        self.meta = ThreadSafeDict()
        self.meta['foo'] = mock.Mock(units="F", sampleRate="10", min=0, max=100)
        self.meta['bar'] = mock.Mock(units="C", sampleRate="1", min=-100, max=200)
        self.meta['baz'] = mock.Mock(units="C", sampleRate="1", min=-100, max=200)

        # Mock's constructor treats 'name' in a special way, it won't return a string
        self.meta['foo'].configure_mock(name='foo')
        self.meta['bar'].configure_mock(name='bar')
        self.meta['baz'].configure_mock(name='baz')

        self.data_bus = None
        self.telemetry_connection = None

        self.status_function = mock.Mock()
        self.data_bus = mock.Mock()
        self.telemetry_connection = TelemetryConnection(self.host, self.port, self.device_id, self.meta, self.data_bus, self.status_function)

        # We mock these functions because we don't want to really connect to anything
        # This is a unit test, not integration!
        self.telemetry_connection.create_socket = mock.Mock()
        self.telemetry_connection.connect = mock.Mock()
        self.telemetry_connection.push = mock.Mock()

    def test_run(self, asyncore_loop_mock):
        self.telemetry_connection.run()

        self.telemetry_connection.create_socket.assert_called_with(socket.AF_INET, socket.SOCK_STREAM)
        self.telemetry_connection.connect.assert_called_with((self.host, self.port))

        self.assertTrue(asyncore_loop_mock.called)
        self.assertTrue(self.data_bus.add_sample_listener.called)
        self.assertTrue(self.data_bus.addMetaListener.called)

    def test_sends_auth(self, asyncore_loop_mock):
        self.telemetry_connection.run()

        self.telemetry_connection.handle_connect()

        self.telemetry_connection.push.assert_called_with('{"cmd":{"schemaVer":2,"auth":{"deviceId":"' + self.device_id + '"}}}' + "\n")

    def test_sends_authorized_status(self, asyncore_loop_mock):
        self.telemetry_connection.run()
        self.telemetry_connection.handle_connect()

        args, kwargs = self.status_function.call_args
        status, message, status_code = args
        self.assertEqual(status_code, TelemetryConnection.STATUS_CONNECTED)

        self.telemetry_connection.collect_incoming_data('{"status":"ok"}')
        self.telemetry_connection.found_terminator()

        args, kwargs = self.status_function.call_args
        status, message, status_code = args

        self.assertEqual(status_code, TelemetryConnection.STATUS_STREAMING)

    def test_sends_error_authenticating_status(self, asyncore_loop_mock):

        self.telemetry_connection.run()
        self.telemetry_connection.handle_connect()

        self.telemetry_connection.collect_incoming_data('{"status":"error","message":"invalid serial number"}')
        self.telemetry_connection.found_terminator()

        args, kwargs = self.status_function.call_args
        status, message, status_code = args

        self.assertEqual(status_code, TelemetryConnection.ERROR_AUTHENTICATING)

    def test_sends_unknown_error(self, asyncore_loop_mock):
        self.telemetry_connection.run()
        self.telemetry_connection.handle_connect()

        self.telemetry_connection.collect_incoming_data('{"foo":"bar"}')
        self.telemetry_connection.found_terminator()

        args, kwargs = self.status_function.call_args
        status, message, status_code = args

        self.assertEqual(status_code, TelemetryConnection.ERROR_UNKNOWN_MESSAGE)

    def test_sends_meta(self, asyncore_loop_mock):
        self.telemetry_connection.run()
        self.telemetry_connection.handle_connect()

        args, kwargs = self.status_function.call_args
        status, message, status_code = args
        self.assertEqual(status_code, TelemetryConnection.STATUS_CONNECTED)

        self.telemetry_connection.collect_incoming_data('{"status":"ok"}')
        self.telemetry_connection.found_terminator()

        args, kwargs = self.telemetry_connection.push.call_args
        message, = args

        meta_json = json.loads(message)

        self.assertTrue("s" in meta_json, "Sends 's'")
        self.assertTrue("meta" in meta_json["s"], "Sends meta property")

        for name, info in self.meta.iteritems():
            # Verify all the channels were sent with correct info
            channel_info = next((item for item in meta_json["s"]["meta"] if item["nm"] == name), None)

            self.assertIsNotNone(channel_info, "Channel found")

            self.assertEqual(channel_info["ut"], info.units, "Units are sent")
            self.assertEqual(channel_info["sr"], info.sampleRate, "Sample rate is sent")
            self.assertEqual(channel_info["min"], info.min, "Min is sent")
            self.assertEqual(channel_info["max"], info.max, "Max is sent")

    def test_sends_line_break(self, asyncore_loop_mock):

        self.telemetry_connection.send_msg("foo")
        args, kwargs = self.telemetry_connection.push.call_args
        message, = args

        self.assertEqual(message, "foo\n", "Newline added to end of message")

    @patch('threading.Timer')
    def test_sends_bitmask(self, timer_mock, asyncore_loop_mock):
        sample = ThreadSafeDict()
        sample['bar'] = 3.0

        bitmask = ''

        # Compute our own bitmask to verify
        for channel_name, value in self.meta.iteritems():
            if channel_name == 'bar':
                bitmask = "1" + bitmask
            else:
                bitmask = "0" + bitmask

        bitmask = int(bitmask, 2)

        self.telemetry_connection._on_sample(sample)
        self.telemetry_connection._send_sample()

        args, kwargs = self.telemetry_connection.push.call_args
        message, = args

        message_json = json.loads(message)

        self.assertEqual(bitmask, message_json["s"]["d"][len(message_json["s"]["d"])-1])

    @patch('threading.Timer')
    def test_sends_multiple_bitmasks(self, timer_mock, asyncore_loop_mock):
        sample = ThreadSafeDict()
        meta = ThreadSafeDict()

        for i in range(1, 40):
            meta['sensor' + str(i)] = mock.Mock(units="F", sampleRate="10",
                                              min=random.randint(-100, 0),
                                              max=random.randint(0, 100))
            meta['sensor' + str(i)].configure_mock(name='sensor' + str(i))

        # Manually doing this to aid debugging if this test breaks
        sample['sensor2'] = random.randint(-50, 50)
        sample['sensor3'] = random.randint(-50, 50)
        sample['sensor4'] = random.randint(-50, 50)
        sample['sensor5'] = random.randint(-50, 50)
        sample['sensor6'] = random.randint(-50, 50)
        sample['sensor7'] = random.randint(-50, 50)
        sample['sensor8'] = random.randint(-50, 50)
        sample['sensor10'] = random.randint(-50, 50)
        sample['sensor11'] = random.randint(-50, 50)
        sample['sensor12'] = random.randint(-50, 50)
        sample['sensor21'] = random.randint(-50, 50)
        sample['sensor25'] = random.randint(-50, 50)
        sample['sensor26'] = random.randint(-50, 50)
        sample['sensor29'] = random.randint(-50, 50)
        sample['sensor33'] = random.randint(-50, 50)
        sample['sensor35'] = random.randint(-50, 50)
        sample['sensor37'] = random.randint(-50, 50)
        sample['sensor38'] = random.randint(-50, 50)
        sample['sensor39'] = random.randint(-50, 50)


        self.telemetry_connection.authorized = True
        self.telemetry_connection._on_meta(meta)

        self.telemetry_connection._on_sample(sample)
        self.telemetry_connection._send_sample()

        args, kwargs = self.telemetry_connection.push.call_args
        message, = args

        message_json = json.loads(message)

        self.assertEqual(19, message_json["s"]["d"][len(message_json["s"]["d"])-1])
        self.assertEqual(1479378921, message_json["s"]["d"][len(message_json["s"]["d"])-2])

    @patch('threading.Timer')
    def resends_meta(self, timer_mock, asyncore_loop_mock):
        meta = {}

        for i in range(1, 5):
            meta['sensor'+str(i)] = mock.Mock(units="F", sampleRate="10",
                                              min=random.randint(-100,0),
                                              max=random.randint(0,100))
            meta['sensor'+str(i)].configure_mock(name='sensor'+str(i))

        self.telemetry_connection._on_meta(meta)

        args, kwargs = self.telemetry_connection.push.call_args
        message, = args

        message_json = json.loads(message)

        self.assertEqual(len(message_json["s"]["meta"]), 5)
Exemplo n.º 3
0
class DataBus(object):
    """Central hub for current sample data. Receives data from DataBusPump
    Also contains the periodic updater for listeners. Updates occur in the UI thread via Clock.schedule_interval
    Architecture:    
    (DATA SOURCE) => DataBus => (LISTENERS)
    
    Typical use:
    (CHANNEL LISTENERS) => DataBus.addChannelListener()  -- listeners receive updates with a particular channel's value
    (META LISTENERS) => DataBus.addMetaListener() -- Listeners receive updates with meta data

    Note: DataBus must be started via start_update before any data flows
    """
    channel_metas = ThreadSafeDict()
    channel_data = ThreadSafeDict()
    sample = None
    channel_listeners = {}
    meta_listeners = []
    meta_updated = False
    data_filters = []
    sample_listeners = []
    _polling = False
    rcp_meta_read = False

    def __init__(self, **kwargs):
        super(DataBus, self).__init__(**kwargs)

    def start_update(self, interval=DEFAULT_DATABUS_UPDATE_INTERVAL):
        if self._polling:
            return

        Clock.schedule_interval(self.notify_listeners, interval)
        self._polling = True

    def stop_update(self):
        Clock.unschedule(self.notify_listeners)
        self._polling = False

    def _update_datafilter_meta(self, datafilter):
        channel_metas = self.channel_metas
        metas = datafilter.get_channel_meta(channel_metas)
        with self.channel_metas as cm:
            for channel, meta in metas.iteritems():
                cm[channel] = meta

    def update_channel_meta(self, metas):
        """update channel metadata information
        This should be called when the channel information has changed
        """
        # update channel metadata
        with self.channel_metas as cm, self.channel_data as cd:
            # clear our list of channel data values, in case channels
            # were removed on this metadata update
            cd.clear()

            # clear and reload our channel metas
            cm.clear()
            for meta in metas.channel_metas:
                cm[meta.name] = meta

            # add channel meta for existing filters
            for f in self.data_filters:
                self._update_datafilter_meta(f)

        self.meta_updated = True
        self.rcp_meta_read = True

    def addSampleListener(self, callback):
        self.sample_listeners.append(callback)

    def update_samples(self, sample):
        """Update channel data with new samples
        """
        with self.channel_data as cd:
            for sample_item in sample.samples:
                channel = sample_item.channelMeta.name
                value = sample_item.value
                cd[channel] = value

            # apply filters to updated data
            for f in self.data_filters:
                f.filter(cd)

    def notify_listeners(self, dt):

        if self.meta_updated:
            with self.channel_metas as cm:
                self.notify_meta_listeners(cm)
                self.meta_updated = False

        with self.channel_data as cd:
            for channel, value in cd.iteritems():
                self.notify_channel_listeners(channel, value)

            for listener in self.sample_listeners:
                listener(cd)

    def notify_channel_listeners(self, channel, value):
        listeners = self.channel_listeners.get(str(channel))
        if listeners:
            for listener in listeners:
                listener(value)

    def notify_meta_listeners(self, channelMeta):
        for listener in self.meta_listeners:
            listener(channelMeta)

    def addChannelListener(self, channel, callback):
        listeners = self.channel_listeners.get(channel)
        if listeners == None:
            listeners = [callback]
            self.channel_listeners[channel] = listeners
        else:
            listeners.append(callback)

    def removeChannelListener(self, channel, callback):
        try:
            listeners = self.channel_listeners.get(channel)
            if listeners:
                listeners.remove(callback)
        except:
            pass

    def remove_sample_listener(self, listener):
        '''
        Remove the specified sample listener
        :param listener
        :type object / callback function
        '''
        try:
            self.sample_listeners.remove(listener)
        except Exception as e:
            Logger.debug('Could not remove sample listener {}: {}'.format(
                listener, e))

    def remove_meta_listener(self, listener):
        '''
        Remove the specified meta listener
        :param listener
        :type object / callback function
        '''
        try:
            self.meta_listeners.remove(listener)
        except Exception as e:
            Logger.debug('Could not remove meta listener {}: {}'.format(
                listener, e))

    def add_sample_listener(self, callback):
        self.sample_listeners.append(callback)

    def addMetaListener(self, callback):
        self.meta_listeners.append(callback)

    def add_data_filter(self, datafilter):
        self.data_filters.append(datafilter)
        self._update_datafilter_meta(datafilter)

    def getMeta(self):
        return self.channel_metas

    def getData(self, channel):
        return self.channel_data[channel]