A Brief Recap
Previously, in Part 1 of this blog series, I explained how I was hired by Dr. Lawlor at UAF to work as an assistant on his arctic mining robot.
Choosing Data Visualization
Once I had a chance to catch my bearings in this new environment, we discussed what part of the project I should undertake. There were several that interested me, but the task that stood out was robot-data visualization. This fit with my long-term goal of becoming a data scientist with a storytelling emphasis.
The Robot Generates Data, but Nothing Saves It
By the time I was hired, the robot was already mostly completed. The robot was capable of driving around the room, extending its mechanical arm, and spinning up its mining tools to dig through concrete and permafrost.
I don’t have a photo of the most recent robot iteration, but if I am able to take one at a later time I will post it here.
The photo below is of a previous version of the robot; our new robot is similar in nature.
There are sensors in several places on the robot that control the various motors that make the robot move. These sensors are capable of gathering data.
The code that Dr. Lawlor wrote to manage the motors and sensors was already reading the data. This allowed for basic decision making, either by the robot itself through automation, or even by the driver.
On the other hand, the data was not saved anywhere. Instead, it disappeared the moment after it was read.
From here, this story is going to become technical in nature. Following along in complete detail may not be possible unless you are familiar with at least one programming language.
Capturing the Data and Inserting It Into a Database
As I began working through the project, Dr. Lawlor assisted me in understanding the robot’s custom C++ classes.
With a growing understanding of the robot’s capabilities, I was able to write the following C++ application.
The code below is broken into three separate files.
The first one is the header file lunacapture.hpp
. Here all of the custom functions I wrote, as well pre-built libraries I used, have an initial declaration. This allows the computer to know what to expect as it compiles all of my code. (Here is a link to the file on Github.com.)
/*
* lunacapture.hpp
* Aurora Robotics
* University of Alaska - Fairbanks
* Project by Bryan Beus
* Under supervision of Dr. Orion Lawlor
* Declaration file for LunaCapture
* Captures data from mining Robot "Excahauler"
*/
#ifndef LUNACAPTURE_HPP
#define LUNACAPTURE_HPP
#include <chrono> // system_clock::time_point(), system_clock::now()
#include <string> // string()
#include <sstream> // stringstream()
#include <ctime> // put_time(), locattime()
#include <iostream> // cout, cin, endl
#include <iomanip> // setprecision
using std::string;
using std::istringstream;
using std::stringstream;
using std::cin;
using std::cout;
using std::endl;
using std::ofstream;
using std::to_string;
// Capture the current time and return in string format
uint capture_epoch();
// Find and replace within string (function copied from stackoverflow: https://tinyurl.com/48fvpu6n via Czarek Tomcza)
std::string ReplaceString(std::string subject, const std::string& search, const std::string& replace);
#endif
The next file is lunacapture.cpp
. This files contains the actual custom functions. There are only two at the time of this blog post, but there may be a few more on the way. (A link to the file on Github.com.)
/*
* lunacapture.hpp
* Aurora Robotics
* University of Alaska - Fairbanks
* Project by Bryan Beus
* Under supervision of Dr. Orion Lawlor
* Declaration file for LunaCapture
* Captures data from mining Robot "Excahauler"
*/
#include <chrono> // system_clock::time_point(), system_clock::now()
#include <string> // string()
#include <sstream> // stringstream()
#include <ctime> // put_time(), locattime()
#include <iostream> // cout, cin, endl
#include <iomanip> // setprecision
#include "lunacapture.hpp" // Include lunacapture header file
using std::string;
using std::istringstream;
using std::stringstream;
using std::cin;
using std::cout;
using std::endl;
using std::ofstream;
using std::to_string;
// Capture current epoch time
uint capture_epoch() {
// Capture current time
const auto timestamp = std::chrono::high_resolution_clock::now();
const auto epoch_time = std::chrono::duration_cast<std::chrono::milliseconds>(timestamp.time_since_epoch());
// Convert to uint
uint result = epoch_time.count();
return result;
}
// Find and replace within string (function copied from stackoverflow: https://tinyurl.com/48fvpu6n via Czarek Tomcza)
std::string ReplaceString(std::string subject, const std::string& search, const std::string& replace) {
// Set initial position at 0
size_t pos = 0;
// Search through subject and replace wherever char is found
while ((pos = subject.find(search, pos)) != std::string::npos) {
subject.replace(pos, search.length(), replace);
pos += replace.length();
}
return subject;
}
The final file, main.cpp
, contains most of the work. This part of the software links to a library that Dr. Lawlor wrote which allows me to reach out to the robot and retrieve data. From there, I use a library called libpqxx
to connect to a PostgreSQL database on my machine. With the connection established and having retrieved data from the robot, I then insert the data into the database as a JSON object.
#include "aurora/lunatic.h"
#include "nlohmann/json.hpp" // json input/output
#include <chrono> // system_clock::time_point(), system_clock::now()
#include <string> // string()
#include <sstream> // stringstream()
#include <ctime> // put_time(), locattime()
#include <iostream> // cout, cin, endl
#include <pqxx/pqxx> // postgresql database library
#include <iomanip> // setprecision
#include <cmath> // round()
#include "lunacapture.hpp"
using std::string;
using std::istringstream;
using std::stringstream;
using std::cin;
using std::cout;
using std::endl;
using std::ofstream;
using std::to_string;
using json = nlohmann::json;
// Error message for loss of a previously established database connection
const string& db_disconnect_msg = "Connection to the database is lost.";
int main() {
// Establish connection to postgresql database
// TO-DO Establish credentials
// TO DO: Read file .dbpass and create string
pqxx::connection psql_conn(" \
dbname=test_cpp \
user=postgres \
password=asdf \
hostaddr=127.0.0.1 \
port=5432 \
target_session_attrs=read-write"
);
if (!psql_conn.is_open()) {
cout << "Connection to database could not be established." << endl;
return 0;
}
// Create the database table, if it does not already exist
try {
if (psql_conn.is_open()) {
pqxx::work w(psql_conn);
w.exec("\
CREATE TABLE IF NOT EXISTS test_conn ( \
id SERIAL NOT NULL PRIMARY KEY, \
robot_json JSON NOT NULL, \
created_at TIMESTAMP NOT NULL DEFAULT NOW()\
);"
);
w.commit();
} else {
cout << db_disconnect_msg << endl;
}
} catch (const std::exception& e) {
cout << e.what() << endl;
return 0;
}
// From robot_base.h (included in lunatic.h), obtain info from class robot_base
// Initialize data capture for the drive encoders
MAKE_exchange_drive_encoders();
aurora::drive_encoders last;
last.left = last.right = 0.0f;
// Initialize data capture for the backend state
MAKE_exchange_backend_state();
aurora::backend_state state;
MAKE_exchange_nanoslot();
nanoslot_exchange nano;
// // Prepare the default psql transactions
// prepare_transactions(psql_conn);
while (true) {
// Craft the json output
json output_json;
// TO DO Simplify code by putting all of the below variable/json building into a separate function
// and returning the result as final string or json value
// Capture epoch time
output_json["epoch_time"] = capture_epoch();
// Update the backend data
state = exchange_backend_state.read();
nano = exchange_nanoslot.read();
// Calculate amount of change in drive encoders
// Drive_encoder data are of type float and provide total distance driven by each side of robot
aurora::drive_encoders cur = exchange_drive_encoders.read();
aurora::drive_encoders change = cur - last;
// Capture drive encoder change data
output_json["drive_encoder_left"] = change.left;
output_json["drive_encoder_right"] = change.right;
// Output tool vibration on each axis
output_json["vibe"] = length(nano.slot_A1.state.tool.vibe);
// List of joints, in sequential order, from base to furthest point of arm
// Vars are all of type float
// These are all angles reconstructed from the inertial measurement units (IMUs)
output_json["fork"] = std::round(state.joint.angle.fork * 100) / 100;
output_json["dump"] = std::round(state.joint.angle.dump * 100) / 100;
output_json["boom"] = std::round(state.joint.angle.boom * 100) / 100;
output_json["stick"] = std::round(state.joint.angle.stick * 100) / 100;
output_json["tilt"] = std::round(state.joint.angle.tilt * 100) / 100;
output_json["spin"] = std::round(state.joint.angle.spin * 100) / 100;
// This is the power being sent to the motor, not necessarily position
// Vars are all of type float
// Values run from -1 to +1, and indicate full backward to full forward
// The first two indicate power to the drive motors
output_json["power_left"] = std::round(state.power.left * 100) / 100;
output_json["power_right"] = std::round(state.power.right * 100) / 100;
// These variables indicate power to the joints
output_json["power_fork"] = std::round(state.power.fork * 100) / 100;
output_json["power_dump"] = std::round(state.power.dump * 100) / 100;
output_json["power_boom"] = std::round(state.power.boom * 100) / 100;
output_json["power_stick"] = std::round(state.power.stick * 100) / 100;
output_json["power_tilt"] = std::round(state.power.tilt * 100) / 100;
output_json["power_spin"] = std::round(state.power.spin * 100) / 100;
// This var represents power level sent to tool
output_json["power_tool"] = std::round(state.power.tool * 100) / 100;
// The state.state variable is one int
output_json["state_state"] = state.state;
// state.sensor is obsolete and therefore currently omitted
// Later will include info such as battery voltage
// The state.loc variables represent an estimate of location
// Values are of type float
// Variables (x, y) are in meters
output_json["loc_x"] = state.loc.x;
output_json["loc_y"] = state.loc.y;
// Variable (angle) is in degrees
output_json["loc_angle"] = std::round(state.loc.angle * 100) / 100;
// Test that json is formatted properly:
cout << output_json.dump() << endl;
cout << endl;
stringstream output_assembled;
output_assembled << "INSERT INTO test_conn ";
output_assembled << " ( robot_json ) VALUES ('";
output_assembled << output_json.dump();
output_assembled << "');";
string output = output_assembled.str();
// TO DO Prep insert command for database
try {
pqxx::work w(psql_conn);
w.exec(output);
w.commit();
// TO DO: Correct functions
// pqxx::work t(psql_conn);
//
// pqxx::result res = execute_insert(t, "test_conn", "robot_json", output_json.dump());
//
// t.commit();
} catch (const std::exception& e) {
cout << e.what() << endl;
return 0;
}
// Reset
last=cur;
aurora::data_exchange_sleep(500);
}
}
In the above version, the cycle to retrieve and insert data repeats every 500 milliseconds. I can change that, if needed, to gather data either more or less frequently.
In the next blog post, we will go over the process of visualizing the data using Python and Jupyter Notebook.
4 comments
Comments are closed.