Пример #1
0
def get_pid_analysis_plots(ulog, px4_ulog, db_data, link_to_main_plots):
    """
    get all bokeh plots shown on the PID analysis page
    :return: list of bokeh plots
    """
    def _resample(time_array, data, desired_time):
        """ resample data at a given time to a vector of desired_time """
        data_f = interp1d(time_array, data, fill_value='extrapolate')
        return data_f(desired_time)

    page_intro = """
<p>
This page shows step response plots for the PID controller. The step
response is an objective measure to evaluate the performance of a PID
controller, i.e. if the tuning gains are appropriate. In particular, the
following metrics can be read from the plots: response time, overshoot and
settling time.
</p>
<p>
The step response plots are based on <a href="https://github.com/Plasmatree/PID-Analyzer">
PID-Analyzer</a>, originally written for Betaflight by Florian Melsheimer.
Documentation with some examples can be found <a
href="https://github.com/Plasmatree/PID-Analyzer/wiki/Influence-of-parameters">here</a>.
</p>
<p>
The analysis may take a while...
</p>
    """
    curdoc().template_variables['title_html'] = get_heading_html(
        ulog, px4_ulog, db_data, None, [('Open Main Plots', link_to_main_plots)],
        'PID Analysis') + page_intro

    plots = []
    data = ulog.data_list
    flight_mode_changes = get_flight_mode_changes(ulog)
    x_range_offset = (ulog.last_timestamp - ulog.start_timestamp) * 0.05
    x_range = Range1d(ulog.start_timestamp - x_range_offset, ulog.last_timestamp + x_range_offset)

    # COMPATIBILITY support for old logs
    if any(elem.name == 'vehicle_angular_velocity' for elem in data):
        rate_topic_name = 'vehicle_angular_velocity'
        rate_field_names = ['xyz[0]', 'xyz[1]', 'xyz[2]']
    else: # old
        rate_topic_name = 'rate_ctrl_status'
        rate_field_names = ['rollspeed', 'pitchspeed', 'yawspeed']

    # required PID response data
    pid_analysis_error = False
    try:
        # Rate
        rate_data = ulog.get_dataset(rate_topic_name)
        gyro_time = rate_data.data['timestamp']

        vehicle_rates_setpoint = ulog.get_dataset('vehicle_rates_setpoint')
        actuator_controls_0 = ulog.get_dataset('actuator_controls_0')
        throttle = _resample(actuator_controls_0.data['timestamp'],
                             actuator_controls_0.data['control[3]'] * 100, gyro_time)
        time_seconds = gyro_time / 1e6
    except (KeyError, IndexError, ValueError) as error:
        print(type(error), ":", error)
        pid_analysis_error = True
        div = Div(text="<p><b>Error</b>: missing topics or data for PID analysis "
                  "(required topics: vehicle_angular_velocity, vehicle_rates_setpoint, "
                  "vehicle_attitude, vehicle_attitude_setpoint and "
                  "actuator_controls_0).</p>", width=int(plot_width*0.9))
        plots.append(column(div, width=int(plot_width*0.9)))

    has_attitude = True
    try:
        # Attitude (optional)
        vehicle_attitude = ulog.get_dataset('vehicle_attitude')
        attitude_time = vehicle_attitude.data['timestamp']
        vehicle_attitude_setpoint = ulog.get_dataset('vehicle_attitude_setpoint')
    except (KeyError, IndexError, ValueError) as error:
        print(type(error), ":", error)
        has_attitude = False

    for index, axis in enumerate(['roll', 'pitch', 'yaw']):
        axis_name = axis.capitalize()
        # rate
        data_plot = DataPlot(data, plot_config, 'actuator_controls_0',
                             y_axis_label='[deg/s]', title=axis_name+' Angular Rate',
                             plot_height='small',
                             x_range=x_range)

        thrust_max = 200
        actuator_controls = data_plot.dataset
        if actuator_controls is None: # do not show the rate plot if actuator_controls is missing
            continue
        time_controls = actuator_controls.data['timestamp']
        thrust = actuator_controls.data['control[3]'] * thrust_max
        # downsample if necessary
        max_num_data_points = 4.0*plot_config['plot_width']
        if len(time_controls) > max_num_data_points:
            step_size = int(len(time_controls) / max_num_data_points)
            time_controls = time_controls[::step_size]
            thrust = thrust[::step_size]
        if len(time_controls) > 0:
            # make sure the polygon reaches down to 0
            thrust = np.insert(thrust, [0, len(thrust)], [0, 0])
            time_controls = np.insert(time_controls, [0, len(time_controls)],
                                      [time_controls[0], time_controls[-1]])

        p = data_plot.bokeh_plot
        p.patch(time_controls, thrust, line_width=0, fill_color='#555555',
                fill_alpha=0.4, alpha=0, legend_label='Thrust [0, {:}]'.format(thrust_max))

        data_plot.change_dataset(rate_topic_name)
        data_plot.add_graph([lambda data: ("rate"+str(index),
                                           np.rad2deg(data[rate_field_names[index]]))],
                            colors3[0:1], [axis_name+' Rate Estimated'], mark_nan=True)
        data_plot.change_dataset('vehicle_rates_setpoint')
        data_plot.add_graph([lambda data: (axis, np.rad2deg(data[axis]))],
                            colors3[1:2], [axis_name+' Rate Setpoint'],
                            mark_nan=True, use_step_lines=True)
        axis_letter = axis[0].upper()
        rate_int_limit = '(*100)'
        # this param is MC/VTOL only (it will not exist on FW)
        rate_int_limit_param = 'MC_' + axis_letter + 'R_INT_LIM'
        if rate_int_limit_param in ulog.initial_parameters:
            rate_int_limit = '[-{0:.0f}, {0:.0f}]'.format(
                ulog.initial_parameters[rate_int_limit_param]*100)
        data_plot.change_dataset('rate_ctrl_status')
        data_plot.add_graph([lambda data: (axis, data[axis+'speed_integ']*100)],
                            colors3[2:3], [axis_name+' Rate Integral '+rate_int_limit])
        plot_flight_modes_background(data_plot, flight_mode_changes)

        if data_plot.finalize() is not None: plots.append(data_plot.bokeh_plot)

        # PID response
        if not pid_analysis_error:
            try:
                gyro_rate = np.rad2deg(rate_data.data[rate_field_names[index]])
                setpoint = _resample(vehicle_rates_setpoint.data['timestamp'],
                                     np.rad2deg(vehicle_rates_setpoint.data[axis]),
                                     gyro_time)
                trace = Trace(axis, time_seconds, gyro_rate, setpoint, throttle)
                plots.append(plot_pid_response(trace, ulog.data_list, plot_config).bokeh_plot)
            except Exception as e:
                print(type(e), axis, ":", e)
                div = Div(text="<p><b>Error</b>: PID analysis failed. Possible "
                          "error causes are: logged data rate is too low, or there "
                          "is not enough motion for the analysis.</p>",
                          width=int(plot_width*0.9))
                plots.insert(0, column(div, width=int(plot_width*0.9)))
                pid_analysis_error = True

    # attitude
    if not pid_analysis_error and has_attitude:
        throttle = _resample(actuator_controls_0.data['timestamp'],
                             actuator_controls_0.data['control[3]'] * 100, attitude_time)
        time_seconds = attitude_time / 1e6
    # don't plot yaw, as yaw is mostly controlled directly by rate
    for index, axis in enumerate(['roll', 'pitch']):
        axis_name = axis.capitalize()

        # PID response
        if not pid_analysis_error and has_attitude:
            try:
                attitude_estimated = np.rad2deg(vehicle_attitude.data[axis])
                setpoint = _resample(vehicle_attitude_setpoint.data['timestamp'],
                                     np.rad2deg(vehicle_attitude_setpoint.data[axis+'_d']),
                                     attitude_time)
                trace = Trace(axis, time_seconds, attitude_estimated, setpoint, throttle)
                plots.append(plot_pid_response(trace, ulog.data_list, plot_config,
                                               'Angle').bokeh_plot)
            except Exception as e:
                print(type(e), axis, ":", e)
                div = Div(text="<p><b>Error</b>: Attitude PID analysis failed. Possible "
                          "error causes are: logged data rate is too low/data missing, "
                          "or there is not enough motion for the analysis.</p>",
                          width=int(plot_width*0.9))
                plots.insert(0, column(div, width=int(plot_width*0.9)))
                pid_analysis_error = True

    return plots
Пример #2
0
def generate_plots(ulog, px4_ulog, db_data, vehicle_data, link_to_3d_page):
    """ create a list of bokeh plots (and widgets) to show """

    plots = []
    data = ulog.data_list

    # COMPATIBILITY support for old logs
    if any(elem.name == 'vehicle_air_data'
           or elem.name == 'vehicle_magnetometer' for elem in data):
        baro_alt_meter_topic = 'vehicle_air_data'
        magnetometer_ga_topic = 'vehicle_magnetometer'
    else:  # old
        baro_alt_meter_topic = 'sensor_combined'
        magnetometer_ga_topic = 'sensor_combined'
    for topic in data:
        if topic.name == 'system_power':
            # COMPATIBILITY: rename fields to new format
            if 'voltage5V_v' in topic.data:  # old (prior to PX4/Firmware:213aa93)
                topic.data['voltage5v_v'] = topic.data.pop('voltage5V_v')
            if 'voltage3V3_v' in topic.data:  # old (prior to PX4/Firmware:213aa93)
                topic.data['voltage3v3_v'] = topic.data.pop('voltage3V3_v')

    # initialize flight mode changes
    flight_mode_changes = get_flight_mode_changes(ulog)

    # VTOL state changes
    vtol_states = None
    try:
        cur_dataset = ulog.get_dataset('vehicle_status')
        if np.amax(cur_dataset.data['is_vtol']) == 1:
            vtol_states = cur_dataset.list_value_changes('in_transition_mode')
            # find mode after transitions (states: 1=transition, 2=FW, 3=MC)
            for i in range(len(vtol_states)):
                if vtol_states[i][1] == 0:
                    t = vtol_states[i][0]
                    idx = np.argmax(cur_dataset.data['timestamp'] >= t) + 1
                    vtol_states[i] = (t, 2 +
                                      cur_dataset.data['is_rotary_wing'][idx])
            vtol_states.append((ulog.last_timestamp, -1))
    except (KeyError, IndexError) as error:
        vtol_states = None

    # Heading
    curdoc().template_variables['title_html'] = get_heading_html(
        ulog, px4_ulog, db_data, link_to_3d_page)

    # info text on top (logging duration, max speed, ...)
    curdoc().template_variables['info_table_html'] = \
        get_info_table_html(ulog, px4_ulog, db_data, vehicle_data, vtol_states)

    curdoc().template_variables['error_labels_html'] = get_error_labels_html()

    hardfault_html = get_hardfault_html(ulog)
    if hardfault_html is not None:
        curdoc().template_variables['hardfault_html'] = hardfault_html

# FIXME: for now, we use Google maps directly without bokeh, because it's not working reliably
# GPS map
#    gps_plots = []
#    gps_titles = []
#    plot = plot_map(ulog, plot_config, map_type='google', api_key =
#            get_google_maps_api_key(), setpoints=False)
#    if plot is not None:
#        gps_plots.append(plot)
#        gps_titles.append('GPS Map: Satellite')
#
#    plot = plot_map(ulog, plot_config, map_type='plain', setpoints=True)
#    if plot is not None:
#        gps_plots.append(plot)
#        gps_titles.append('GPS Map: Plain')
#
#    data_plot = DataPlot2D(data, plot_config, 'vehicle_local_position',
#        x_axis_label = '[m]', y_axis_label='[m]', plot_height='large')
#    data_plot.add_graph('y', 'x', colors2[0], 'Estimated')
#    data_plot.change_dataset('vehicle_local_position_setpoint')
#    data_plot.add_graph('y', 'x', colors2[1], 'Setpoint')
#    if data_plot.finalize() is not None:
#        gps_plots.append(data_plot.bokeh_plot)
#        gps_titles.append('Local Position')
#
#    if len(gps_plots) >= 2:
#        tabs = []
#        for i in range(len(gps_plots)):
#            tabs.append(Panel(child=gps_plots[i], title=gps_titles[i]))
#        gps_plot_height=plot_config['plot_height']['large'] + 30
#        plots.append(Tabs(tabs=tabs, width=plot_width, height=gps_plot_height))
#    elif len(gps_plots) == 1:
#        plots.extend(gps_plots)

    if is_running_locally():
        # show the google maps plot via Bokeh, since the one in the html
        # template does not work locally (we disable it further down)
        map_plot = plot_map(ulog,
                            plot_config,
                            map_type='google',
                            api_key=get_google_maps_api_key(),
                            setpoints=False)
        if map_plot is not None:
            plots.append(map_plot)

    # Position plot
    data_plot = DataPlot2D(data,
                           plot_config,
                           'vehicle_local_position',
                           x_axis_label='[m]',
                           y_axis_label='[m]',
                           plot_height='large')
    data_plot.add_graph('y',
                        'x',
                        colors2[0],
                        'Estimated',
                        check_if_all_zero=True)
    if not data_plot.had_error:  # vehicle_local_position is required
        data_plot.change_dataset('vehicle_local_position_setpoint')
        data_plot.add_graph('y', 'x', colors2[1], 'Setpoint')
        # groundtruth (SITL only)
        data_plot.change_dataset('vehicle_groundtruth')
        data_plot.add_graph('y', 'x', color_gray, 'Groundtruth')
        # GPS + position setpoints
        plot_map(ulog,
                 plot_config,
                 map_type='plain',
                 setpoints=True,
                 bokeh_plot=data_plot.bokeh_plot)
        if data_plot.finalize() is not None:
            plots.append(data_plot.bokeh_plot)
            if not is_running_locally(
            ):  # do not enable Google Map if running locally
                curdoc().template_variables['has_position_data'] = True

    # initialize parameter changes
    changed_params = None
    if not 'replay' in ulog.msg_info_dict:  # replay can have many param changes
        if len(ulog.changed_parameters) > 0:
            changed_params = ulog.changed_parameters
            plots.append(None)  # save space for the param change button

    ### Add all data plots ###

    x_range_offset = (ulog.last_timestamp - ulog.start_timestamp) * 0.05
    x_range = Range1d(ulog.start_timestamp - x_range_offset,
                      ulog.last_timestamp + x_range_offset)

    # Altitude estimate
    data_plot = DataPlot(data,
                         plot_config,
                         'vehicle_gps_position',
                         y_axis_label='[m]',
                         title='Altitude Estimate',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph([lambda data: ('alt', data['alt'] * 0.001)],
                        colors8[0:1], ['GPS Altitude'])
    data_plot.change_dataset(baro_alt_meter_topic)
    data_plot.add_graph(['baro_alt_meter'], colors8[1:2],
                        ['Barometer Altitude'])
    data_plot.change_dataset('vehicle_global_position')
    data_plot.add_graph(['alt'], colors8[2:3], ['Fused Altitude Estimation'])
    data_plot.change_dataset('position_setpoint_triplet')
    data_plot.add_circle(['current.alt'],
                         [plot_config['mission_setpoint_color']],
                         ['Altitude Setpoint'])
    data_plot.change_dataset('actuator_controls_0')
    data_plot.add_graph([lambda data: ('thrust', data['control[3]'] * 100)],
                        colors8[6:7], ['Thrust [0, 100]'])
    plot_flight_modes_background(data_plot, flight_mode_changes, vtol_states)

    if data_plot.finalize() is not None: plots.append(data_plot)

    # Roll/Pitch/Yaw angle & angular rate
    for axis in ['roll', 'pitch', 'yaw']:

        # angle
        axis_name = axis.capitalize()
        data_plot = DataPlot(data,
                             plot_config,
                             'vehicle_attitude',
                             y_axis_label='[deg]',
                             title=axis_name + ' Angle',
                             plot_height='small',
                             changed_params=changed_params,
                             x_range=x_range)
        data_plot.add_graph([lambda data: (axis, np.rad2deg(data[axis]))],
                            colors2[0:1], [axis_name + ' Estimated'],
                            mark_nan=True)
        data_plot.change_dataset('vehicle_attitude_setpoint')
        data_plot.add_graph(
            [lambda data: (axis + '_d', np.rad2deg(data[axis + '_d']))],
            colors2[1:2], [axis_name + ' Setpoint'],
            mark_nan=True,
            use_step_lines=True)
        data_plot.change_dataset('vehicle_groundtruth')
        data_plot.add_graph([lambda data: (axis, np.rad2deg(data[axis]))],
                            [color_gray], [axis_name + ' Groundtruth'])
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        if data_plot.finalize() is not None: plots.append(data_plot)

        # rate
        data_plot = DataPlot(data,
                             plot_config,
                             'vehicle_attitude',
                             y_axis_label='[deg/s]',
                             title=axis_name + ' Angular Rate',
                             plot_height='small',
                             changed_params=changed_params,
                             x_range=x_range)
        data_plot.add_graph(
            [lambda data: (axis + 'speed', np.rad2deg(data[axis + 'speed']))],
            colors3[0:1], [axis_name + ' Rate Estimated'],
            mark_nan=True)
        data_plot.change_dataset('vehicle_rates_setpoint')
        data_plot.add_graph([lambda data: (axis, np.rad2deg(data[axis]))],
                            colors3[1:2], [axis_name + ' Rate Setpoint'],
                            mark_nan=True,
                            use_step_lines=True)
        axis_letter = axis[0].upper()
        rate_int_limit = '(*100)'
        # this param is MC/VTOL only (it will not exist on FW)
        rate_int_limit_param = 'MC_' + axis_letter + 'R_INT_LIM'
        if rate_int_limit_param in ulog.initial_parameters:
            rate_int_limit = '[-{0:.0f}, {0:.0f}]'.format(
                ulog.initial_parameters[rate_int_limit_param] * 100)
        data_plot.change_dataset('rate_ctrl_status')
        data_plot.add_graph(
            [lambda data: (axis, data[axis + 'speed_integ'] * 100)],
            colors3[2:3], [axis_name + ' Rate Integral ' + rate_int_limit])
        data_plot.change_dataset('vehicle_groundtruth')
        data_plot.add_graph(
            [lambda data: (axis + 'speed', np.rad2deg(data[axis + 'speed']))],
            [color_gray], [axis_name + ' Rate Groundtruth'])
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        if data_plot.finalize() is not None: plots.append(data_plot)

    # Local position
    for axis in ['x', 'y', 'z']:
        data_plot = DataPlot(data,
                             plot_config,
                             'vehicle_local_position',
                             y_axis_label='[m]',
                             title='Local Position ' + axis.upper(),
                             plot_height='small',
                             changed_params=changed_params,
                             x_range=x_range)
        data_plot.add_graph([axis],
                            colors2[0:1], [axis.upper() + ' Estimated'],
                            mark_nan=True)
        data_plot.change_dataset('vehicle_local_position_setpoint')
        data_plot.add_graph([axis],
                            colors2[1:2], [axis.upper() + ' Setpoint'],
                            mark_nan=True,
                            use_step_lines=True)
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        if data_plot.finalize() is not None: plots.append(data_plot)

    # Velocity
    data_plot = DataPlot(data,
                         plot_config,
                         'vehicle_local_position',
                         y_axis_label='[m/s]',
                         title='Velocity',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph(['vx', 'vy', 'vz'], colors8[0:3], ['X', 'Y', 'Z'])
    data_plot.change_dataset('vehicle_local_position_setpoint')
    data_plot.add_graph(['vx', 'vy', 'vz'],
                        [colors8[5], colors8[4], colors8[6]],
                        ['X Setpoint', 'Y Setpoint', 'Z Setpoint'],
                        use_step_lines=True)
    plot_flight_modes_background(data_plot, flight_mode_changes, vtol_states)

    if data_plot.finalize() is not None: plots.append(data_plot)

    # Visual Odometry (only if topic found)
    if any(elem.name == 'vehicle_visual_odometry' for elem in data):
        # Vision position
        data_plot = DataPlot(data,
                             plot_config,
                             'vehicle_visual_odometry',
                             y_axis_label='[m]',
                             title='Visual Odometry Position',
                             plot_height='small',
                             changed_params=changed_params,
                             x_range=x_range)
        data_plot.add_graph(['x', 'y', 'z'],
                            colors3, ['X', 'Y', 'Z'],
                            mark_nan=True)
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        data_plot.change_dataset('vehicle_groundtruth')
        data_plot.add_graph(
            ['x', 'y', 'z'], colors8[2:5],
            ['Groundtruth X', 'Groundtruth Y', 'Groundtruth Z'])

        if data_plot.finalize() is not None: plots.append(data_plot)

        # Vision velocity
        data_plot = DataPlot(data,
                             plot_config,
                             'vehicle_visual_odometry',
                             y_axis_label='[m]',
                             title='Visual Odometry Velocity',
                             plot_height='small',
                             changed_params=changed_params,
                             x_range=x_range)
        data_plot.add_graph(['vx', 'vy', 'vz'],
                            colors3, ['X', 'Y', 'Z'],
                            mark_nan=True)
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        data_plot.change_dataset('vehicle_groundtruth')
        data_plot.add_graph(
            ['vx', 'vy', 'vz'], colors8[2:5],
            ['Groundtruth VX', 'Groundtruth VY', 'Groundtruth VZ'])
        if data_plot.finalize() is not None: plots.append(data_plot)

        # Vision attitude
        data_plot = DataPlot(data,
                             plot_config,
                             'vehicle_visual_odometry',
                             y_axis_label='[deg]',
                             title='Visual Odometry Attitude',
                             plot_height='small',
                             changed_params=changed_params,
                             x_range=x_range)
        data_plot.add_graph([
            lambda data: ('roll', np.rad2deg(data['roll'])), lambda data:
            ('pitch', np.rad2deg(data['pitch'])), lambda data:
            ('yaw', np.rad2deg(data['yaw']))
        ],
                            colors3, ['Roll', 'Pitch', 'Yaw'],
                            mark_nan=True)
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        data_plot.change_dataset('vehicle_groundtruth')
        data_plot.add_graph(
            [
                lambda data: ('roll', np.rad2deg(data['roll'])), lambda data:
                ('pitch', np.rad2deg(data['pitch'])), lambda data:
                ('yaw', np.rad2deg(data['yaw']))
            ], colors8[2:5],
            ['Roll Groundtruth', 'Pitch Groundtruth', 'Yaw Groundtruth'])

        # Vision attitude rate
        data_plot = DataPlot(data,
                             plot_config,
                             'vehicle_visual_odometry',
                             y_axis_label='[deg]',
                             title='Visual Odometry Attitude Rate',
                             plot_height='small',
                             changed_params=changed_params,
                             x_range=x_range)
        data_plot.add_graph([
            lambda data:
            ('rollspeed', np.rad2deg(data['rollspeed'])), lambda data:
            ('pitchspeed', np.rad2deg(data['pitchspeed'])), lambda data:
            ('yawspeed', np.rad2deg(data['yawspeed']))
        ],
                            colors3, ['Roll Rate', 'Pitch Rate', 'Yaw Rate'],
                            mark_nan=True)
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        data_plot.change_dataset('vehicle_groundtruth')
        data_plot.add_graph([
            lambda data:
            ('rollspeed', np.rad2deg(data['rollspeed'])), lambda data:
            ('pitchspeed', np.rad2deg(data['pitchspeed'])), lambda data:
            ('yawspeed', np.rad2deg(data['yawspeed']))
        ], colors8[2:5], [
            'Roll Rate Groundtruth', 'Pitch Rate Groundtruth',
            'Yaw Rate Groundtruth'
        ])

        if data_plot.finalize() is not None: plots.append(data_plot)

    # Airspeed vs Ground speed: but only if there's valid airspeed data
    try:
        cur_dataset = ulog.get_dataset('airspeed')
        if np.amax(cur_dataset.data['indicated_airspeed_m_s']) > 0.1:
            data_plot = DataPlot(data,
                                 plot_config,
                                 'vehicle_global_position',
                                 y_axis_label='[m/s]',
                                 title='Airspeed',
                                 plot_height='small',
                                 changed_params=changed_params,
                                 x_range=x_range)
            data_plot.add_graph([
                lambda data: ('groundspeed_estimated',
                              np.sqrt(data['vel_n']**2 + data['vel_e']**2))
            ], colors3[2:3], ['Ground Speed Estimated'])
            data_plot.change_dataset('airspeed')
            data_plot.add_graph(['indicated_airspeed_m_s'], colors2[0:1],
                                ['Airspeed Indicated'])

            plot_flight_modes_background(data_plot, flight_mode_changes,
                                         vtol_states)

            if data_plot.finalize() is not None: plots.append(data_plot)
    except (KeyError, IndexError) as error:
        pass

    # manual control inputs
    # prefer the manual_control_setpoint topic. Old logs do not contain it
    if any(elem.name == 'manual_control_setpoint' for elem in data):
        data_plot = DataPlot(data,
                             plot_config,
                             'manual_control_setpoint',
                             title='Manual Control Inputs (Radio or Joystick)',
                             plot_height='small',
                             y_range=Range1d(-1.1, 1.1),
                             changed_params=changed_params,
                             x_range=x_range)
        data_plot.add_graph([
            'y', 'x', 'r', 'z', lambda data:
            ('mode_slot', data['mode_slot'] / 6), 'aux1', 'aux2', lambda data:
            ('kill_switch', data['kill_switch'] == 1)
        ], colors8, [
            'Y / Roll', 'X / Pitch', 'Yaw', 'Throttle [0, 1]', 'Flight Mode',
            'Aux1', 'Aux2', 'Kill Switch'
        ])
        # TODO: add RTL switch and others? Look at params which functions are mapped?
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        if data_plot.finalize() is not None: plots.append(data_plot)

    else:  # it's an old log (COMPATIBILITY)
        data_plot = DataPlot(data,
                             plot_config,
                             'rc_channels',
                             title='Raw Radio Control Inputs',
                             plot_height='small',
                             y_range=Range1d(-1.1, 1.1),
                             changed_params=changed_params,
                             x_range=x_range)
        num_rc_channels = 8
        if data_plot.dataset:
            max_channels = np.amax(data_plot.dataset.data['channel_count'])
            if max_channels < num_rc_channels: num_rc_channels = max_channels
        legends = []
        for i in range(num_rc_channels):
            channel_names = px4_ulog.get_configured_rc_input_names(i)
            if channel_names is None:
                legends.append('Channel ' + str(i))
            else:
                legends.append('Channel ' + str(i) + ' (' +
                               ', '.join(channel_names) + ')')
        data_plot.add_graph(
            ['channels[' + str(i) + ']' for i in range(num_rc_channels)],
            colors8[0:num_rc_channels],
            legends,
            mark_nan=True)
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        if data_plot.finalize() is not None: plots.append(data_plot)

    # actuator controls 0
    data_plot = DataPlot(data,
                         plot_config,
                         'actuator_controls_0',
                         y_start=0,
                         title='Actuator Controls 0',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph(
        ['control[0]', 'control[1]', 'control[2]', 'control[3]'],
        colors8[0:4], ['Roll', 'Pitch', 'Yaw', 'Thrust'],
        mark_nan=True)
    plot_flight_modes_background(data_plot, flight_mode_changes, vtol_states)
    if data_plot.finalize() is not None: plots.append(data_plot)

    # actuator controls (Main) FFT (for filter & output noise analysis)
    data_plot = DataPlotFFT(data,
                            plot_config,
                            'actuator_controls_0',
                            title='Actuator Controls FFT')
    data_plot.add_graph(['control[0]', 'control[1]', 'control[2]'], colors3,
                        ['Roll', 'Pitch', 'Yaw'])
    if not data_plot.had_error:
        if 'MC_DTERM_CUTOFF' in ulog.initial_parameters:
            data_plot.mark_frequency(
                ulog.initial_parameters['MC_DTERM_CUTOFF'], 'MC_DTERM_CUTOFF')
        if 'IMU_GYRO_CUTOFF' in ulog.initial_parameters:
            data_plot.mark_frequency(
                ulog.initial_parameters['IMU_GYRO_CUTOFF'], 'IMU_GYRO_CUTOFF',
                20)

    if data_plot.finalize() is not None: plots.append(data_plot)

    # actuator controls 1
    # (only present on VTOL, Fixed-wing config)
    data_plot = DataPlot(data,
                         plot_config,
                         'actuator_controls_1',
                         y_start=0,
                         title='Actuator Controls 1 (VTOL in Fixed-Wing mode)',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph(
        ['control[0]', 'control[1]', 'control[2]', 'control[3]'],
        colors8[0:4], ['Roll', 'Pitch', 'Yaw', 'Thrust'],
        mark_nan=True)
    plot_flight_modes_background(data_plot, flight_mode_changes, vtol_states)
    if data_plot.finalize() is not None: plots.append(data_plot)

    # actuator outputs 0: Main
    data_plot = DataPlot(data,
                         plot_config,
                         'actuator_outputs',
                         y_start=0,
                         title='Actuator Outputs (Main)',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    num_actuator_outputs = 8
    if data_plot.dataset:
        max_outputs = np.amax(data_plot.dataset.data['noutputs'])
        if max_outputs < num_actuator_outputs:
            num_actuator_outputs = max_outputs
    data_plot.add_graph(
        ['output[' + str(i) + ']' for i in range(num_actuator_outputs)],
        colors8[0:num_actuator_outputs],
        ['Output ' + str(i) for i in range(num_actuator_outputs)],
        mark_nan=True)
    plot_flight_modes_background(data_plot, flight_mode_changes, vtol_states)

    if data_plot.finalize() is not None: plots.append(data_plot)

    # actuator outputs 1: AUX
    data_plot = DataPlot(data,
                         plot_config,
                         'actuator_outputs',
                         y_start=0,
                         title='Actuator Outputs (AUX)',
                         plot_height='small',
                         changed_params=changed_params,
                         topic_instance=1,
                         x_range=x_range)
    num_actuator_outputs = 8
    # only plot if at least one of the outputs is not constant
    all_constant = True
    if data_plot.dataset:
        max_outputs = np.amax(data_plot.dataset.data['noutputs'])
        if max_outputs < num_actuator_outputs:
            num_actuator_outputs = max_outputs

        for i in range(num_actuator_outputs):
            output_data = data_plot.dataset.data['output[' + str(i) + ']']
            if not np.all(output_data == output_data[0]):
                all_constant = False
    if not all_constant:
        data_plot.add_graph(
            ['output[' + str(i) + ']' for i in range(num_actuator_outputs)],
            colors8[0:num_actuator_outputs],
            ['Output ' + str(i) for i in range(num_actuator_outputs)],
            mark_nan=True)
        plot_flight_modes_background(data_plot, flight_mode_changes,
                                     vtol_states)

        if data_plot.finalize() is not None: plots.append(data_plot)

    # raw acceleration
    data_plot = DataPlot(data,
                         plot_config,
                         'sensor_combined',
                         y_axis_label='[m/s^2]',
                         title='Raw Acceleration',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph([
        'accelerometer_m_s2[0]', 'accelerometer_m_s2[1]',
        'accelerometer_m_s2[2]'
    ], colors3, ['X', 'Y', 'Z'])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # raw angular speed
    data_plot = DataPlot(data,
                         plot_config,
                         'sensor_combined',
                         y_axis_label='[deg/s]',
                         title='Raw Angular Speed (Gyroscope)',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph([
        lambda data:
        ('gyro_rad[0]', np.rad2deg(data['gyro_rad[0]'])), lambda data:
        ('gyro_rad[1]', np.rad2deg(data['gyro_rad[1]'])), lambda data:
        ('gyro_rad[2]', np.rad2deg(data['gyro_rad[2]']))
    ], colors3, ['X', 'Y', 'Z'])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # magnetic field strength
    data_plot = DataPlot(data,
                         plot_config,
                         magnetometer_ga_topic,
                         y_axis_label='[gauss]',
                         title='Raw Magnetic Field Strength',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph(
        ['magnetometer_ga[0]', 'magnetometer_ga[1]', 'magnetometer_ga[2]'],
        colors3, ['X', 'Y', 'Z'])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # distance sensor
    data_plot = DataPlot(data,
                         plot_config,
                         'distance_sensor',
                         y_start=0,
                         y_axis_label='[m]',
                         title='Distance Sensor',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph(['current_distance', 'covariance'], colors3[0:2],
                        ['Distance', 'Covariance'])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # gps uncertainty
    # the accuracy values can be really large if there is no fix, so we limit the
    # y axis range to some sane values
    data_plot = DataPlot(data,
                         plot_config,
                         'vehicle_gps_position',
                         title='GPS Uncertainty',
                         y_range=Range1d(0, 40),
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph(
        ['eph', 'epv', 'satellites_used', 'fix_type'], colors8[::2], [
            'Horizontal position accuracy [m]',
            'Vertical position accuracy [m]', 'Num Satellites used', 'GPS Fix'
        ])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # gps noise & jamming
    data_plot = DataPlot(data,
                         plot_config,
                         'vehicle_gps_position',
                         y_start=0,
                         title='GPS Noise & Jamming',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph(['noise_per_ms', 'jamming_indicator'], colors3[0:2],
                        ['Noise per ms', 'Jamming Indicator'])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # thrust and magnetic field
    data_plot = DataPlot(data,
                         plot_config,
                         magnetometer_ga_topic,
                         y_start=0,
                         title='Thrust and Magnetic Field',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph([
        lambda data:
        ('len_mag',
         np.sqrt(data['magnetometer_ga[0]']**2 + data['magnetometer_ga[1]']**2
                 + data['magnetometer_ga[2]']**2))
    ], colors2[0:1], ['Norm of Magnetic Field'])
    data_plot.change_dataset('actuator_controls_0')
    data_plot.add_graph([lambda data: ('thrust', data['control[3]'])],
                        colors2[1:2], ['Thrust'])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # Acceleration Spectrogram
    data_plot = DataPlotSpec(data,
                             plot_config,
                             'sensor_combined',
                             y_axis_label='[Hz]',
                             title='Acceleration Power Spectral Density',
                             plot_height='small',
                             x_range=x_range)
    data_plot.add_graph([
        'accelerometer_m_s2[0]', 'accelerometer_m_s2[1]',
        'accelerometer_m_s2[2]'
    ], ['X', 'Y', 'Z'])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # power
    data_plot = DataPlot(data,
                         plot_config,
                         'battery_status',
                         y_start=0,
                         title='Power',
                         plot_height='small',
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph([
        'voltage_v', 'voltage_filtered_v', 'current_a', lambda data:
        ('discharged_mah', data['discharged_mah'] / 100), lambda data:
        ('remaining', data['remaining'] * 10)
    ], colors8[::2] + colors8[1:2], [
        'Battery Voltage [V]', 'Battery Voltage filtered [V]',
        'Battery Current [A]', 'Discharged Amount [mAh / 100]',
        'Battery remaining [0=empty, 10=full]'
    ])
    data_plot.change_dataset('system_power')
    if data_plot.dataset:
        if 'voltage5v_v' in data_plot.dataset.data and \
                        np.amax(data_plot.dataset.data['voltage5v_v']) > 0.0001:
            data_plot.add_graph(['voltage5v_v'], colors8[7:8], ['5 V'])
        if 'voltage3v3_v' in data_plot.dataset.data and \
                        np.amax(data_plot.dataset.data['voltage3v3_v']) > 0.0001:
            data_plot.add_graph(['voltage3v3_v'], colors8[5:6], ['3.3 V'])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # estimator watchdog
    try:
        data_plot = DataPlot(data,
                             plot_config,
                             'estimator_status',
                             y_start=0,
                             title='Estimator Watchdog',
                             plot_height='small',
                             changed_params=changed_params,
                             x_range=x_range)
        estimator_status = ulog.get_dataset('estimator_status').data
        plot_data = []
        plot_labels = []
        input_data = [
            ('NaN Flags', estimator_status['nan_flags']),
            ('Health Flags (vel, pos, hgt)', estimator_status['health_flags']),
            ('Timeout Flags (vel, pos, hgt)',
             estimator_status['timeout_flags']),
            ('Velocity Check Bit',
             (estimator_status['innovation_check_flags']) & 0x1),
            ('Horizontal Position Check Bit',
             (estimator_status['innovation_check_flags'] >> 1) & 1),
            ('Vertical Position Check Bit',
             (estimator_status['innovation_check_flags'] >> 2) & 1),
            ('Mag X, Y, Z Check Bits',
             (estimator_status['innovation_check_flags'] >> 3) & 0x7),
            ('Yaw Check Bit',
             (estimator_status['innovation_check_flags'] >> 6) & 1),
            ('Airspeed Check Bit',
             (estimator_status['innovation_check_flags'] >> 7) & 1),
            ('Synthetic Sideslip Check Bit',
             (estimator_status['innovation_check_flags'] >> 8) & 1),
            ('Height to Ground Check Bit',
             (estimator_status['innovation_check_flags'] >> 9) & 1),
            ('Optical Flow X, Y Check Bits',
             (estimator_status['innovation_check_flags'] >> 10) & 0x3),
        ]
        # filter: show only the flags that have non-zero samples
        for cur_label, cur_data in input_data:
            if np.amax(cur_data) > 0.1:
                data_label = 'flags_' + str(
                    len(plot_data))  # just some unique string
                plot_data.append(lambda d, data=cur_data, label=data_label:
                                 (label, data))
                plot_labels.append(cur_label)
                if len(plot_data) >= 8:  # cannot add more than that
                    break

        if len(plot_data) == 0:
            # add the plot even in the absence of any problem, so that the user
            # can validate that (otherwise it's ambiguous: it could be that the
            # estimator_status topic is not logged)
            plot_data = [lambda d: ('flags', input_data[0][1])]
            plot_labels = [input_data[0][0]]
        data_plot.add_graph(plot_data, colors8[0:len(plot_data)], plot_labels)
        if data_plot.finalize() is not None: plots.append(data_plot)
    except (KeyError, IndexError) as error:
        print('Error in estimator plot: ' + str(error))

    # RC Quality
    data_plot = DataPlot(data,
                         plot_config,
                         'input_rc',
                         title='RC Quality',
                         plot_height='small',
                         y_range=Range1d(0, 1),
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph([lambda data: ('rssi', data['rssi'] / 100), 'rc_lost'],
                        colors3[0:2], ['RSSI [0, 1]', 'RC Lost (Indicator)'])
    data_plot.change_dataset('vehicle_status')
    data_plot.add_graph(['rc_signal_lost'], colors3[2:3],
                        ['RC Lost (Detected)'])
    if data_plot.finalize() is not None: plots.append(data_plot)

    # cpu load
    data_plot = DataPlot(data,
                         plot_config,
                         'cpuload',
                         title='CPU & RAM',
                         plot_height='small',
                         y_range=Range1d(0, 1),
                         changed_params=changed_params,
                         x_range=x_range)
    data_plot.add_graph(['ram_usage', 'load'], [colors3[1], colors3[2]],
                        ['RAM Usage', 'CPU Load'])
    data_plot.add_span('load', line_color=colors3[2])
    data_plot.add_span('ram_usage', line_color=colors3[1])
    plot_flight_modes_background(data_plot, flight_mode_changes, vtol_states)
    if data_plot.finalize() is not None: plots.append(data_plot)

    # sampling: time difference
    try:
        data_plot = DataPlot(data,
                             plot_config,
                             'sensor_combined',
                             y_range=Range1d(0, 25e3),
                             y_axis_label='[us]',
                             title='Sampling Regularity of Sensor Data',
                             plot_height='small',
                             changed_params=changed_params,
                             x_range=x_range)
        sensor_combined = ulog.get_dataset('sensor_combined').data
        sampling_diff = np.diff(sensor_combined['timestamp'])
        min_sampling_diff = np.amin(sampling_diff)

        plot_dropouts(data_plot.bokeh_plot, ulog.dropouts, min_sampling_diff)

        data_plot.add_graph(
            [lambda data: ('timediff', np.append(sampling_diff, 0))],
            [colors3[2]], ['delta t (between 2 logged samples)'])
        data_plot.change_dataset('estimator_status')
        data_plot.add_graph(
            [lambda data: ('time_slip', data['time_slip'] * 1e6)],
            [colors3[1]], ['Estimator time slip (cumulative)'])
        if data_plot.finalize() is not None: plots.append(data_plot)
    except:
        pass

    # exchange all DataPlot's with the bokeh_plot and handle parameter changes

    param_changes_button = Button(label="Hide Parameter Changes", width=170)
    param_change_labels = []

    # FIXME: this should be a CustomJS callback, not on the server. However this
    # did not work for me.
    def param_changes_button_clicked():
        """ callback to show/hide parameter changes """
        for label in param_change_labels:
            if label.visible:
                param_changes_button.label = 'Show Parameter Changes'
                label.visible = False
                label.text_alpha = 0  # label.visible does not work, so we use this instead
            else:
                param_changes_button.label = 'Hide Parameter Changes'
                label.visible = True
                label.text_alpha = 1

    param_changes_button.on_click(param_changes_button_clicked)

    jinja_plot_data = []
    for i in range(len(plots)):
        if plots[i] is None:
            plots[i] = widgetbox(param_changes_button,
                                 width=int(plot_width * 0.99))
        if isinstance(plots[i], DataPlot):
            if plots[i].param_change_label is not None:
                param_change_labels.append(plots[i].param_change_label)

            plot_title = plots[i].title
            plots[i] = plots[i].bokeh_plot

            fragment = 'Nav-'+plot_title.replace(' ', '-') \
                .replace('&', '_').replace('(', '').replace(')', '')
            jinja_plot_data.append({
                'model_id': plots[i].ref['id'],
                'fragment': fragment,
                'title': plot_title
            })

    # changed parameters
    plots.append(get_changed_parameters(ulog.initial_parameters, plot_width))

    # information about which messages are contained in the log
    # TODO: need to load all topics for this (-> log loading will take longer)
    #       but if we load all topics and the log contains some (external) topics
    #       with buggy timestamps, it will affect the plotting.
    #    data_list_sorted = sorted(ulog.data_list, key=lambda d: d.name + str(d.multi_id))
    #    table_text = []
    #    for d in data_list_sorted:
    #        message_size = sum([ULog.get_field_size(f.type_str) for f in d.field_data])
    #        num_data_points = len(d.data['timestamp'])
    #        table_text.append((d.name, str(d.multi_id), str(message_size), str(num_data_points),
    #           str(message_size * num_data_points)))
    #    topics_info = '<table><tr><th>Name</th><th>Topic instance</th><th>Message Size</th>' \
    #            '<th>Number of data points</th><th>Total bytes</th></tr>' + ''.join(
    #            ['<tr><td>'+'</td><td>'.join(list(x))+'</td></tr>' for x in table_text]) + '</table>'
    #    topics_div = Div(text=topics_info, width=int(plot_width*0.9))
    #    plots.append(widgetbox(topics_div, width=int(plot_width*0.9)))

    # log messages
    plots.append(get_logged_messages(ulog.logged_messages, plot_width))

    # perf & top output
    top_data = ''
    perf_data = ''
    for state in ['pre', 'post']:
        if 'perf_top_' + state + 'flight' in ulog.msg_info_multiple_dict:
            current_top_data = ulog.msg_info_multiple_dict['perf_top_' +
                                                           state + 'flight'][0]
            flight_data = escape('\n'.join(current_top_data))
            top_data += '<p>' + state.capitalize(
            ) + ' Flight:<br/><pre>' + flight_data + '</pre></p>'
        if 'perf_counter_' + state + 'flight' in ulog.msg_info_multiple_dict:
            current_perf_data = ulog.msg_info_multiple_dict['perf_counter_' +
                                                            state +
                                                            'flight'][0]
            flight_data = escape('\n'.join(current_perf_data))
            perf_data += '<p>' + state.capitalize(
            ) + ' Flight:<br/><pre>' + flight_data + '</pre></p>'

    additional_data_html = ''
    if len(top_data) > 0:
        additional_data_html += '<h5>Processes</h5>' + top_data
    if len(perf_data) > 0:
        additional_data_html += '<h5>Performance Counters</h5>' + perf_data
    if len(additional_data_html) > 0:
        # hide by default & use a button to expand
        additional_data_html = '''
<button class="btn btn-secondary" data-toggle="collapse" style="min-width:0;"
 data-target="#show-additional-data">Show additional Data</button>
<div id="show-additional-data" class="collapse">
{:}
</div>
'''.format(additional_data_html)
        additional_data_div = Div(text=additional_data_html,
                                  width=int(plot_width * 0.9))
        plots.append(
            widgetbox(additional_data_div, width=int(plot_width * 0.9)))

    curdoc().template_variables['plots'] = jinja_plot_data

    return plots