Home Bio Projects Blog CV

Trident (Part 1)

August 5, 2021

Django

JavaScript

Leaflet

Irregular Leaflet #1

A few years ago, Andrew Graham asked me to help him develop a digital dashboard for his motorcycles (check out his company, Trident Cycles) that would record his laps around the track during races. I spent a summer toiling over various Arduino components trying to build out a prototype but was not particularly successful. After building relevant skills during the development of other recent projects, I decided to try my hand again at working on a functional prototype. This post documents my thought process and findings while working through Trident, a web-based motorcycle trip recorder.

Concept

I wanted to design Trident similarly to a preexisting and popular workout application, Strava. Users will be able to record trips, view recordings that display post-processed visualizations generated from the data, and share those recordings with others. I’m hoping to keep the scope of the project much smaller than a true clone of Strava, which includes user interactions (follows, likes, and comments). Instead, I have decided to focus on the data processing in order to pull the most interesting information from the combination of phone sensors. At the moment, these sensors include:

Individually, the sensors have their strengths and weaknesses. The GPS provides absolute positioning and speed but is limited to a slow refresh rate of 1Hz. This greatly limits its effectiveness when tracking a motorcycle through a turn during a race. In contrast, the IMU has a very quick refresh rate but only provides relative measurements. By combining these sensors, I hope to generate an expected position that fills in the gaps between the observed positions.

But before I can get to working through the processing steps, I needed to build a stable method for creating, storing, and viewing trip recordings.

Mock Ups

Trident - Mock Ups

Trident - Mock Ups

For the mockups, I created a loading screen with the Trident Cycles logo, a recordings screen where you can view your previous trips, and a detail view for each recording which will display your specific stats and visualizations for the trip. I went with a bold color palette that I feel fits the motorcycle racing theme. I wanted each view to contain some unifying features, such as the header and footer menu, that are common with many mobile applications. These mockups were created in Adobe XD, which I felt really helped to quickly prototype the look of the different views without the need for diving into the code. These gave a good starting point for the look that I wanted.

Database Thoughts

Sticking with my strengths, I developed this web application using Django. As we get into more in-depth data analysis, I think that the Python backend will be very beneficial. I stuck with the default SQLite database; this may have been a mistake, but I’ll talk about that later in this post. I first determined what a recording would look like within the database. Each recording has some general information (name, user, date, and description), summary statistics that are generated by the backend (maximum speed, average speed, duration, coordinate path, etc…), and the raw time series data. Storing the time series data within the database seemed very wasteful, as I didn’t think that there would be a need to query this data across multiple recordings at the same time. I instead decided to store it as CSV file, which was linked to the recording model as a FileField. This means that there will be a CSV per recording, stored within a directory alongside one another. I had originally envisioned a way to mark recordings as public or private, but this method of storing the CSV does seemingly complicate that feature because, by default, these files are visible to every user if you know the associated URL. This will be something that I revisit once the build is a bit more stable.

Where’s The Code?

Dashboard

The dashboard section is where all of the recording actually occurs. I am not a strong JavaScript programmer, but I had worked through much of this code on a previous version of the project. It utilizes the Geolocation and DeviceMotion/DeviceOrientation APIs to monitor the mobile device sensors. Each one of these has to be manually enable with a button click, which calls the enableSensors() function. The screen waits for the sensors to be fully functioning before removing the overlay and passing the user to the dashboard.

main.js

function enableSensors () {
    setInterval(function() {
        if (document.getElementById("connectedGPS").style.color == "white" && document.getElementById("connectedGyro").style.color == "white" && document.getElementById("connectedAccel").style.color == "white") {
            document.getElementById("enableSensorsOverlay").style.display = "none";
        }
    }, 100);
    enableLocation();
    enableOrientation();
    enableMotion();
}

main.js

var timeseries = [
    ["time", "duration", "speed", "lat", "lon", "llaccuracy", "alt", "altaccuracy", "alpha", "beta", "gamma", "accelX", "accelY", "accelZ"]
];

var currentStats = {
    time: "",
    duration: "",
    speed: "",
    lat: "",
    lon: "",
    llaccuracy: "",
    alt: "",
    altaccuracy: "",
    alpha: "",
    beta: "",
    gamma: "",
    accelX: "",
    accelY: "",
    accelZ: "",
};

‘timeseries’ is a an array of arrays, which is appended as the recording runs. With each time step, a new array is appended to ‘timeseries’ consisting of that time step’s observed measurements. More measurements and sensors could be added by including more columns in ‘timeseries’. ‘currentStats’ is a variable that you will see frequently throughout the code; it is a dictionary which stores the current measurement values. This is necessary as the sensors are updating at different rates, so the values must be stored between updates.

main.js (Geolocation)

function enableLocation () {
    if (navigator.geolocation) {
        var options = {
            enableHighAccuracy: true,
            timeout: 5000,
            maximumAge: 0
        };
        navigator.geolocation.watchPosition(geolocation_success, geolocation_error, options);
    }
}

function geolocation_success (position) {
    document.getElementById("sensors").innerHTML = "Connecting...";
    document.getElementById("sensors").style.backgroundColor = "red";
    document.getElementById("sensors").disabled = true;
    if (position.coords.speed == null) {
        document.getElementById("speed").innerHTML = 0;
    } else {
        document.getElementById("connectedGPS").style.color = "white";
        var mph = position.coords.speed * 2.23694;
        document.getElementById("speed").innerHTML = (mph).toFixed(0);

        currentStats.time = position.timestamp;
        currentStats.speed = mph;
        currentStats.lat = position.coords.latitude;
        currentStats.lon = position.coords.longitude;
        currentStats.llaccuracy = position.coords.accuracy;
        currentStats.alt = position.coords.altitude;
        currentStats.altaccuracy = position.coords.altitudeAccuracy;
    }
}

function geolocation_error (err) {
    console.error("Geolocation Unavailable!");
}

The Geolocation API measures your position, altitude, and speed. This occurs once per second. The code from the documentation worked right out of the box, with an added conversion from meters per second to miles per hour.

main.js (Orientation)

function enableOrientation () {
    if (typeof DeviceOrientationEvent !== "undefined" && typeof DeviceOrientationEvent.requestPermission !== "undefined") {
        DeviceOrientationEvent.requestPermission().then(response => {
            if (response == "granted") {
                document.getElementById("connectedGyro").style.color = "white";
                window.addEventListener("deviceorientation", (event) => {
                    currentStats.alpha = event.alpha;
                    currentStats.beta = -event.beta;
                    currentStats.gamma = event.gamma;
                })
            }
        })
    } else {
        console.error("Orientation Unavailable!");
    }
}

Orientation refers to the tilt of the mobile device. This is measured across the three axes. I flipped the sign of the beta axis (top to the bottom of the screen) because this felt more intuitive for when you are on the bike.

main.js (Acceleration)

function enableMotion () {
    if (typeof DeviceMotionEvent !== "undefined" && typeof DeviceMotionEvent.requestPermission !== "undefined") {
        DeviceMotionEvent.requestPermission().then(response => {
            if (response == "granted") {
                document.getElementById("connectedAccel").style.color = "white";
                window.addEventListener("devicemotion", (event) => {
                    currentStats.accelX = event.acceleration.x;
                    currentStats.accelY = event.acceleration.y;
                    currentStats.accelZ = event.acceleration.z;
                })
            }
        })
    } else {
        console.error("Motion Unavailable!");
    }
}

Lastly, the motion events measure the accelerations in the three axes. This combined with th orientation will be used to smooth the GPS signal. All of these functions are called in a loop as part of the makeRecording() function.

main.js

function makeRecording () {
    if (record == 0) {
        record = 1;
        timeseries = [["time", "duration", "speed", "lat", "lon", "llaccuracy", "alt", "altaccuracy",  "alpha", "beta", "gamma", "accelX", "accelY", "accelZ"]];
        document.getElementById("record").innerHTML = "Stop Recording";
        document.getElementById("record").style.backgroundColor = "red";
        startRecordingTime = Date.now();
        durationLoop = setInterval(function() {
            currentStats.duration = Date.now() - startRecordingTime;
            stopRecordingTime = calculateDuration(duration=currentStats.duration);
            document.getElementById("recordingTimer").innerHTML = stopRecordingTime;
            timeseries.push([
                currentStats.time,
                currentStats.duration,
                currentStats.speed,
                currentStats.lat,
                currentStats.lon,
                currentStats.llaccuracy,
                currentStats.alt,
                currentStats.altaccuracy,
                currentStats.alpha,
                currentStats.beta,
                currentStats.gamma,
                currentStats.accelX,
                currentStats.accelY,
                currentStats.accelZ,
            ]);
        }, 50);
    } else if (record == 1) {
        record = 0;
        clearInterval(durationLoop);
        document.getElementById("record").innerHTML = "Record";
        document.getElementById("record").style.backgroundColor = "grey";
        document.getElementById("saveRecordingOverlay").style.display = "flex";
        document.getElementById("overlayTimer").innerHTML = stopRecordingTime;
        document.getElementById("recordingArray").value = timeseries;
    }
}

When the user starts the recording, the sensors will be read at a regular interval, adding measurements to the timeseries array. When the recording is finished, the ‘saveRecordingOverlay” is displayed, allowing users to add details to the recording such as name, description, etc. When saved, a new row is added to the Recording model, but what exactly is stored for each recording?

Models

A database in Django consists of tables, or models, which store related data. For instance, you may have a model called ‘animals’ an object per species broken out into fields, such as scientific name, common name, and number of legs.

Pomona, California

|

kitchensjn@gmail.com

|

(804) 572-3197