Ejemplo n.º 1
0
    def get(self, *args, **kwargs):
        """ GET request callback """

        # load the log file
        log_id = self.get_argument('log')
        if not validate_log_id(log_id):
            raise tornado.web.HTTPError(400, 'Invalid Parameter')
        log_file_name = get_log_filename(log_id)
        ulog = load_ulog_file(log_file_name)

        # extract the necessary information from the log

        try:
            # required topics: none of these are optional
            gps_pos = ulog.get_dataset('vehicle_gps_position').data
            vehicle_global_position = ulog.get_dataset(
                'vehicle_global_position').data
            attitude = ulog.get_dataset('vehicle_attitude').data
        except (KeyError, IndexError, ValueError) as error:
            raise CustomHTTPError(
                400, 'The log does not contain all required topics<br />'
                '(vehicle_gps_position, vehicle_global_position, '
                'vehicle_attitude)') from error

        # manual control setpoint is optional
        manual_control_setpoint = None
        try:
            manual_control_setpoint = ulog.get_dataset(
                'manual_control_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            pass

        # Get the takeoff location. We use the first position with a valid fix,
        # and assume that the vehicle is not in the air already at that point
        takeoff_index = 0
        gps_indices = np.nonzero(gps_pos['fix_type'] > 2)
        if len(gps_indices[0]) > 0:
            takeoff_index = gps_indices[0][0]
        takeoff_altitude = '{:.3f}' \
            .format(gps_pos['alt'][takeoff_index] * 1.e-3)
        takeoff_latitude = '{:.10f}'.format(gps_pos['lat'][takeoff_index] *
                                            1.e-7)
        takeoff_longitude = '{:.10f}'.format(gps_pos['lon'][takeoff_index] *
                                             1.e-7)

        # calculate UTC time offset (assume there's no drift over the entire log)
        utc_offset = int(gps_pos['time_utc_usec'][takeoff_index]) - \
                int(gps_pos['timestamp'][takeoff_index])

        # flight modes
        flight_mode_changes = get_flight_mode_changes(ulog)
        flight_modes_str = '[ '
        for t, mode in flight_mode_changes:
            t += utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(
                t / 1.e6).replace(tzinfo=datetime.timezone.utc)
            if mode in flight_modes_table:
                mode_name, color = flight_modes_table[mode]
            else:
                mode_name = ''
                color = '#ffffff'
            flight_modes_str += '["{:}", "{:}"], ' \
                .format(utctimestamp.isoformat(), mode_name)
        flight_modes_str += ' ]'

        # manual control setpoints (stick input)
        manual_control_setpoints_str = '[ '
        if manual_control_setpoint:
            for i in range(len(manual_control_setpoint['timestamp'])):
                manual_x = manual_control_setpoint['x'][i]
                manual_y = manual_control_setpoint['y'][i]
                manual_z = manual_control_setpoint['z'][i]
                manual_r = manual_control_setpoint['r'][i]
                t = manual_control_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(
                    t / 1.e6).replace(tzinfo=datetime.timezone.utc)
                manual_control_setpoints_str += '["{:}", {:.3f}, {:.3f}, {:.3f}, {:.3f}], ' \
                    .format(utctimestamp.isoformat(), manual_x, manual_y, manual_z, manual_r)
        manual_control_setpoints_str += ' ]'

        # position
        # Note: alt_ellipsoid from gps_pos would be the better match for
        # altitude, but it's not always available. And since we add an offset
        # (to match the takeoff location with the ground altitude) it does not
        # matter as much.
        position_data = '[ '
        # TODO: use vehicle_global_position? If so, then:
        # - altitude requires an offset (to match the GPS data)
        # - it's worse for some logs where the estimation is bad -> acro flights
        #   (-> add both: user-selectable between GPS & estimated trajectory?)
        for i in range(len(gps_pos['timestamp'])):
            lon = gps_pos['lon'][i] * 1.e-7
            lat = gps_pos['lat'][i] * 1.e-7
            alt = gps_pos['alt'][i] * 1.e-3
            t = gps_pos['timestamp'][i] + utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(
                t / 1.e6).replace(tzinfo=datetime.timezone.utc)
            if i == 0:
                start_timestamp = utctimestamp
            end_timestamp = utctimestamp
            position_data += '["{:}", {:.10f}, {:.10f}, {:.3f}], ' \
                .format(utctimestamp.isoformat(), lon, lat, alt)
        position_data += ' ]'

        start_timestamp_str = '"{:}"'.format(start_timestamp.isoformat())
        boot_timestamp = datetime.datetime.utcfromtimestamp(
            utc_offset / 1.e6).replace(tzinfo=datetime.timezone.utc)
        boot_timestamp_str = '"{:}"'.format(boot_timestamp.isoformat())
        end_timestamp_str = '"{:}"'.format(end_timestamp.isoformat())

        # orientation as quaternion
        attitude_data = '[ '
        for i in range(len(attitude['timestamp'])):
            att_qw = attitude['q[0]'][i]
            att_qx = attitude['q[1]'][i]
            att_qy = attitude['q[2]'][i]
            att_qz = attitude['q[3]'][i]
            t = attitude['timestamp'][i] + utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(
                t / 1.e6).replace(tzinfo=datetime.timezone.utc)
            # Cesium uses (x, y, z, w)
            attitude_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                .format(utctimestamp.isoformat(), att_qx, att_qy, att_qz, att_qw)
        attitude_data += ' ]'

        # handle different vehicle types
        # the model_scale_factor should scale the different models to make them
        # equal in size (in proportion)
        mav_type = ulog.initial_parameters.get('MAV_TYPE', None)
        if mav_type == 1:  # fixed wing
            model_scale_factor = 0.06
            model_uri = 'plot_app/static/cesium/SampleData/models/CesiumAir/Cesium_Air.glb'
        elif mav_type == 2:  # quad
            model_scale_factor = 1
            model_uri = 'plot_app/static/cesium/models/iris/iris.glb'
        elif mav_type == 22:  # delta-quad
            # TODO: use the delta-quad model
            model_scale_factor = 0.06
            model_uri = 'plot_app/static/cesium/SampleData/models/CesiumAir/Cesium_Air.glb'
        else:  # TODO: handle more types
            model_scale_factor = 1
            model_uri = 'plot_app/static/cesium/models/iris/iris.glb'

        template = get_jinja_env().get_template(THREED_TEMPLATE)
        self.write(
            template.render(
                flight_modes=flight_modes_str,
                manual_control_setpoints=manual_control_setpoints_str,
                takeoff_altitude=takeoff_altitude,
                takeoff_longitude=takeoff_longitude,
                takeoff_latitude=takeoff_latitude,
                position_data=position_data,
                start_timestamp=start_timestamp_str,
                boot_timestamp=boot_timestamp_str,
                end_timestamp=end_timestamp_str,
                attitude_data=attitude_data,
                model_scale_factor=model_scale_factor,
                model_uri=model_uri,
                log_id=log_id,
                bing_api_key=get_bing_maps_api_key(),
                cesium_api_key=get_cesium_api_key()))
Ejemplo n.º 2
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
Ejemplo n.º 3
0
    def get(self, *args, **kwargs):
        """ GET request callback """

        # load the log file
        log_id = self.get_argument('log')
        if not validate_log_id(log_id):
            raise tornado.web.HTTPError(400, 'Invalid Parameter')
        log_file_name = get_log_filename(log_id)
        ulog = load_ulog_file(log_file_name)

        # extract the necessary information from the log

        try:
            # required topics: none of these are optional
            gps_pos = ulog.get_dataset('vehicle_gps_position').data
            vehicle_global_position = ulog.get_dataset('vehicle_global_position').data
            attitude = ulog.get_dataset('vehicle_attitude').data
        except (KeyError, IndexError, ValueError) as error:
            raise CustomHTTPError(
                400,
                'The log does not contain all required topics<br />'
                '(vehicle_gps_position, vehicle_global_position, '
                'vehicle_attitude)')

        # These are optional data streams. Most of them are added
        # for charting/streaming 2D plots on the 3D viewer.
        manual_control_setpoint = None
        vehicle_local_position = None
        vehicle_local_position_setpoint = None
        vehicle_attitude_setpoint = None
        vehicle_rates_setpoint = None
        actuator_outputs = None
        sensor_combined = None
        actuator_controls_0 = None

        # Exception handling is done on each dataset separately to find
        # source of the missing stream.

        # Exception: manual_control_setpoint
        try:
            manual_control_setpoint = ulog.get_dataset('manual_control_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            print("Manual control setpoint not found")

        # Exception: vehicle_local_position
        try:
            vehicle_local_position = ulog.get_dataset('vehicle_local_position').data
        except (KeyError, IndexError, ValueError) as error:
            print("Vehicle local position not found")

        # Exception: vehicle_local_position_setpoint
        try:
            vehicle_local_position_setpoint = ulog.get_dataset('vehicle_local_position_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            print("Vehicle local position setpoint not found")   

        # Exception: vehicle_attitude_setpoint
        try:
            vehicle_attitude_setpoint = ulog.get_dataset('vehicle_attitude_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            print("Vehicle attitude setpoint not found")                       

        # Exception: vehicle_rates_setpoint
        try:
            vehicle_rates_setpoint = ulog.get_dataset('vehicle_rates_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            print("Vehicle rates setpoint not found")  

        # Exception: actuator_outputs
        try:
            actuator_outputs = ulog.get_dataset('actuator_outputs').data
        except (KeyError, IndexError, ValueError) as error:
            print("Actuator output not found")                  

        # Exception: sensor_combined
        try:
            sensor_combined = ulog.get_dataset('sensor_combined').data
        except (KeyError, IndexError, ValueError) as error:
            print("Sensor combined not found")      

        # Exception: actuator_controls_0
        try:
            actuator_controls_0 = ulog.get_dataset('actuator_controls_0').data
        except (KeyError, IndexError, ValueError) as error:
            print("Actuator Controls 0 not found")   

        # Get the takeoff location. We use the first position with a valid fix,
        # and assume that the vehicle is not in the air already at that point
        takeoff_index = 0
        gps_indices = np.nonzero(gps_pos['fix_type'] > 2)
        if len(gps_indices[0]) > 0:
            takeoff_index = gps_indices[0][0]
        takeoff_altitude = '{:.3f}' \
            .format(gps_pos['alt'][takeoff_index] * 1.e-3)
        takeoff_latitude = '{:.10f}'.format(gps_pos['lat'][takeoff_index] * 1.e-7)
        takeoff_longitude = '{:.10f}'.format(gps_pos['lon'][takeoff_index] * 1.e-7)


        # calculate UTC time offset (assume there's no drift over the entire log)
        utc_offset = int(gps_pos['time_utc_usec'][takeoff_index]) - \
                int(gps_pos['timestamp'][takeoff_index])

        # flight modes
        flight_mode_changes = get_flight_mode_changes(ulog)
        flight_modes_str = '[ '
        for t, mode in flight_mode_changes:
            t += utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                tzinfo=datetime.timezone.utc)
            if mode in flight_modes_table:
                mode_name, color = flight_modes_table[mode]
            else:
                mode_name = ''
                color = '#ffffff'
            flight_modes_str += '["{:}", "{:}"], ' \
                .format(utctimestamp.isoformat(), mode_name)
        flight_modes_str += ' ]'

        # manual control setpoints (stick input)
        manual_control_setpoints_str = '[ '
        if manual_control_setpoint:
            for i in range(len(manual_control_setpoint['timestamp'])):
                manual_x = manual_control_setpoint['x'][i]
                manual_y = manual_control_setpoint['y'][i]
                manual_z = manual_control_setpoint['z'][i]
                manual_r = manual_control_setpoint['r'][i]
                t = manual_control_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                manual_control_setpoints_str += '["{:}", {:.3f}, {:.3f}, {:.3f}, {:.3f}], ' \
                    .format(utctimestamp.isoformat(), manual_x, manual_y, manual_z, manual_r)
        manual_control_setpoints_str += ' ]'


        # position
        # Note: alt_ellipsoid from gps_pos would be the better match for
        # altitude, but it's not always available. And since we add an offset
        # (to match the takeoff location with the ground altitude) it does not
        # matter as much.
        position_data = '[ '
        # TODO: use vehicle_global_position? If so, then:
        # - altitude requires an offset (to match the GPS data)
        # - it's worse for some logs where the estimation is bad -> acro flights
        #   (-> add both: user-selectable between GPS & estimated trajectory?)
        for i in range(len(gps_pos['timestamp'])):
            lon = gps_pos['lon'][i] * 1.e-7
            lat = gps_pos['lat'][i] * 1.e-7
            alt = gps_pos['alt'][i] * 1.e-3
            t = gps_pos['timestamp'][i] + utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                tzinfo=datetime.timezone.utc)
            if i == 0:
                start_timestamp = utctimestamp
            end_timestamp = utctimestamp
            position_data += '["{:}", {:.10f}, {:.10f}, {:.3f}], ' \
                .format(utctimestamp.isoformat(), lon, lat, alt)
        position_data += ' ]'

        start_timestamp_str = '"{:}"'.format(start_timestamp.isoformat())
        boot_timestamp = datetime.datetime.utcfromtimestamp(utc_offset/1.e6).replace(
            tzinfo=datetime.timezone.utc)
        boot_timestamp_str = '"{:}"'.format(boot_timestamp.isoformat())
        end_timestamp_str = '"{:}"'.format(end_timestamp.isoformat())

        # orientation as quaternion
        attitude_data = '[ '
        for i in range(len(attitude['timestamp'])):
            att_qw = attitude['q[0]'][i]
            att_qx = attitude['q[1]'][i]
            att_qy = attitude['q[2]'][i]
            att_qz = attitude['q[3]'][i]
            rollSpeed = attitude['rollspeed'][i]
            pitchSpeed = attitude['pitchspeed'][i]
            yawSpeed = attitude['yawspeed'][i]
            t = attitude['timestamp'][i] + utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                tzinfo=datetime.timezone.utc)
            # Cesium uses (x, y, z, w)
            attitude_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                .format(utctimestamp.isoformat(), att_qx, att_qy, att_qz, att_qw, rollSpeed, pitchSpeed, yawSpeed)
        attitude_data += ' ]'

        # Optional data stream serialization starts here.
        # The code checks None condition to decide whether
        # to serialize or not.

        # Attitude setpoint data
        vehicle_rates_setpoint_data = '[ '
        if vehicle_rates_setpoint is not None:
            for i in range(len(vehicle_rates_setpoint['timestamp'])):
                rollRateSP = vehicle_rates_setpoint['roll'][i]
                pitchRateSP = vehicle_rates_setpoint['pitch'][i]
                yawRateSp = vehicle_rates_setpoint['yaw'][i]
                t = vehicle_rates_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                vehicle_rates_setpoint_data += '["{:}", {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), rollRateSP, pitchRateSP, yawRateSp)
        vehicle_rates_setpoint_data += ' ]'        

        # Sensor combined data. Includes things like raw gyro, raw accelleration.
        sensor_combined_data = '[ '
        if sensor_combined is not None:
            for i in range(len(sensor_combined['timestamp'])):
                rawRoll = sensor_combined['gyro_rad[0]'][i]
                rawPitch = sensor_combined['gyro_rad[1]'][i]
                rawYaw = sensor_combined['gyro_rad[2]'][i]
                rawXAcc = sensor_combined['accelerometer_m_s2[0]'][i]
                rawYAcc = sensor_combined['accelerometer_m_s2[1]'][i]
                rawZAcc = sensor_combined['accelerometer_m_s2[2]'][i]            
                t = sensor_combined['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                sensor_combined_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), rawRoll, rawPitch, rawYaw, rawXAcc, rawYAcc, rawZAcc)
        sensor_combined_data += ' ]'         

        # Attitude setpoint
        vehicle_attitude_setpoint_data = '[ '
        if vehicle_attitude_setpoint is not None:
            for i in range(len(vehicle_attitude_setpoint['timestamp'])):
                rollSP = vehicle_attitude_setpoint['roll_body'][i]
                pitchSP = vehicle_attitude_setpoint['pitch_body'][i]
                yawSP = vehicle_attitude_setpoint['yaw_body'][i]

                t = vehicle_attitude_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                vehicle_attitude_setpoint_data += '["{:}", {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), rollSP, pitchSP, yawSP)
        vehicle_attitude_setpoint_data += ' ]'    

        # Local Position
        vehicle_local_position_data = '[ '
        if vehicle_local_position is not None:
            for i in range(len(vehicle_local_position['timestamp'])):
                xPos = vehicle_local_position['x'][i]
                yPos = vehicle_local_position['y'][i]
                zPos = vehicle_local_position['z'][i]
                xVel = vehicle_local_position['vx'][i]
                yVel = vehicle_local_position['vy'][i]
                zVel = vehicle_local_position['vz'][i]            

                t = vehicle_local_position['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                vehicle_local_position_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), xPos, yPos, zPos, xVel, yVel, zVel)
        vehicle_local_position_data += ' ]'   

        # Local Position Setpoint
        vehicle_local_position_setpoint_data = '[ '
        if vehicle_local_position_setpoint is not None:
            for i in range(len(vehicle_local_position_setpoint['timestamp'])):
                xPosSP = vehicle_local_position_setpoint['x'][i]
                yPosSP = vehicle_local_position_setpoint['y'][i]
                zPosSP = vehicle_local_position_setpoint['z'][i]
                xVelSP = vehicle_local_position_setpoint['vx'][i]
                yVelSP = vehicle_local_position_setpoint['vy'][i]
                zVelSP = vehicle_local_position_setpoint['vz'][i]

                t = vehicle_local_position_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                vehicle_local_position_setpoint_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), xPosSP, yPosSP, zPosSP, xVelSP, yVelSP, zVelSP)
        vehicle_local_position_setpoint_data += ' ]'       

        # Actuator Outputs. This can handle airframes up to 8 actuation outputs (i.e. motors).
        # Tons of formatting things ...

        actuator_outputs_data = '[ '
        if actuator_outputs is not None:
            num_actuator_outputs = 8
            max_outputs = np.amax(actuator_outputs['noutputs'])
            if max_outputs < num_actuator_outputs: num_actuator_outputs = max_outputs

            
            for i in range(len(actuator_outputs['timestamp'])):

                t = actuator_outputs['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)            

                actuatorList = []
                actuatorList.append(utctimestamp.isoformat())
                actuatorDictionary={}
                formatStringLoop = ''
                for x in range(max_outputs):
                    actuatorDictionary["actuator_outputs_{0}".format(x)]=actuator_outputs['output['+str(x)+']'][i]
                    formatStringLoop += ', {:.6f}'
                    actuatorList.append(actuatorDictionary["actuator_outputs_{0}".format(x)])
                formatStringLoop += '], '   
                formatString = '["{:}"' + formatStringLoop

                actuator_outputs_data += formatString.format(*actuatorList)
        actuator_outputs_data += ' ]'          

        # Actuator controls
        actuator_controls_0_data = '[ '
        if actuator_controls_0 is not None:
            for i in range(len(actuator_controls_0['timestamp'])):
                cont0 = actuator_controls_0['control[0]'][i]
                cont1 = actuator_controls_0['control[1]'][i]
                cont2 = actuator_controls_0['control[2]'][i]
                cont3 = actuator_controls_0['control[3]'][i]

                t = actuator_controls_0['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                actuator_controls_0_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), cont0, cont1, cont2, cont3)
        actuator_controls_0_data += ' ]'     

        # handle different vehicle types
        # the model_scale_factor should scale the different models to make them
        # equal in size (in proportion)
        mav_type = ulog.initial_parameters.get('MAV_TYPE', None)
        if mav_type == 1: # fixed wing
            model_scale_factor = 0.06
            model_uri = 'plot_app/static/cesium/SampleData/models/CesiumAir/Cesium_Air.glb'
        elif mav_type == 2: # quad
            model_scale_factor = 1
            model_uri = 'plot_app/static/cesium/models/iris/iris.glb'
        elif mav_type == 22: # delta-quad
            # TODO: use the delta-quad model
            model_scale_factor = 0.06
            model_uri = 'plot_app/static/cesium/SampleData/models/CesiumAir/Cesium_Air.glb'
        else: # TODO: handle more types
            model_scale_factor = 1
            model_uri = 'plot_app/static/cesium/models/iris/iris.glb'

        template = get_jinja_env().get_template(THREED_TEMPLATE)
        self.write(template.render(
            flight_modes=flight_modes_str,
            manual_control_setpoints=manual_control_setpoints_str,
            takeoff_altitude=takeoff_altitude,
            takeoff_longitude=takeoff_longitude,
            takeoff_latitude=takeoff_latitude,
            position_data=position_data,
            start_timestamp=start_timestamp_str,
            boot_timestamp=boot_timestamp_str,
            end_timestamp=end_timestamp_str,
            attitude_data=attitude_data,
            vehicle_attitude_setpoint_data = vehicle_attitude_setpoint_data,
            vehicle_local_position_data = vehicle_local_position_data,
            vehicle_local_position_setpoint_data = vehicle_local_position_setpoint_data,
            actuator_outputs_data = actuator_outputs_data,
            vehicle_rates_setpoint_data = vehicle_rates_setpoint_data,
            sensor_combined_data = sensor_combined_data,
            actuator_controls_0_data = actuator_controls_0_data,
            model_scale_factor=model_scale_factor,
            model_uri=model_uri,
            log_id=log_id,
            bing_api_key=get_bing_maps_api_key(),
            cesium_api_key=get_cesium_api_key()))