diff --git a/CMakeLists.txt b/CMakeLists.txt index d75e252..b2f565b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ endforeach() # Boost find_package(Boost 1.74.0 COMPONENTS REQUIRED + program_options serialization system thread diff --git a/functions.cmake b/functions.cmake index 6110fe1..92d0408 100644 --- a/functions.cmake +++ b/functions.cmake @@ -1,24 +1,26 @@ # Create module library function(make_lib module srcs link_libs inc_dirs compile_defs) - add_library(${module} INTERFACE ${srcs}) - target_compile_definitions(${module} INTERFACE ${compile_defs}) - target_link_libraries(${module} INTERFACE ${link_libs}) + add_library(${module} ${srcs}) + ament_target_dependencies(${module} PUBLIC ${ROS_DEPS}) + target_compile_definitions(${module} PUBLIC ${compile_defs}) + target_link_libraries(${module} PUBLIC ${link_libs}) target_include_directories( - ${module} INTERFACE + ${module} PUBLIC ${CMAKE_CURRENT_LIST_DIR}/inc ${CMAKE_SOURCE_DIR}/lib ${inc_dirs} ) add_dependencies(${module} ${AUTOGEN_TARGETS}) + set(${module}_inc_dir ${CMAKE_CURRENT_LIST_DIR}/inc CACHE INTERNAL "${module} header include directory") endfunction() -# Create module ROS executable -function(make_exe module srcs link_libs inc_dirs ${compile_defs}) +# Create project module ROS executable +function(make_exe module srcs link_libs inc_dirs compile_defs) set(bin_module bin_${module}) add_executable(${bin_module} ${srcs}) target_compile_definitions(${bin_module} PUBLIC ${compile_defs}) ament_target_dependencies(${bin_module} PUBLIC ${ROS_DEPS}) - target_link_libraries(${bin_module} PUBLIC ${link_libs}) + target_link_libraries(${bin_module} PUBLIC ${link_libs} boost_program_options) target_include_directories( ${bin_module} PUBLIC ${CMAKE_CURRENT_LIST_DIR}/inc diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 74dee69..d05824d 100755 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(cmn_hdrs) add_subdirectory(protofiles) +add_subdirectory(sailbot_db) # add directories as needed diff --git a/lib/sailbot_db/CMakeLists.txt b/lib/sailbot_db/CMakeLists.txt new file mode 100644 index 0000000..43c9be1 --- /dev/null +++ b/lib/sailbot_db/CMakeLists.txt @@ -0,0 +1,36 @@ +set(module sailbot_db) + +set(link_libs + ${PROTOBUF_LINK_LIBS} + mongo::mongocxx_shared + mongo::bsoncxx_shared +) + +set(inc_dirs + ${PROTOBUF_INCLUDE_PATH} + ${LIBMONGOCXX_INCLUDE_DIRS} + ${LIBBSONCXX_INCLUDE_DIRS} +) + +set(srcs + ${CMAKE_CURRENT_LIST_DIR}/src/sailbot_db.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/util_db.cpp +) + +# make sailbot_db library +make_lib(${module} "${srcs}" "${link_libs}" "${inc_dirs}" "${compile_defs}") + +set(bin_srcs + ${srcs} + ${CMAKE_CURRENT_LIST_DIR}/src/main.cpp +) + +# Make executable +make_exe(${module} "${bin_srcs}" "${link_libs}" "${inc_dirs}" "${compile_defs}") + +# Create unit test +set(test_srcs + ${srcs} + ${CMAKE_CURRENT_LIST_DIR}/test/test_sailbot_db.cpp +) +make_unit_test(${module} "${test_srcs}" "${link_libs}" "${inc_dirs}" "${compile_defs}") diff --git a/projects/remote_transceiver/inc/sailbot_db.h b/lib/sailbot_db/inc/sailbot_db.h similarity index 74% rename from projects/remote_transceiver/inc/sailbot_db.h rename to lib/sailbot_db/inc/sailbot_db.h index 9ea958c..b429d52 100644 --- a/projects/remote_transceiver/inc/sailbot_db.h +++ b/lib/sailbot_db/inc/sailbot_db.h @@ -10,88 +10,8 @@ #include "sensors.pb.h" #include "waypoint.pb.h" -// BSON document formats (from: https://ubcsailbot.atlassian.net/wiki/spaces/prjt22/pages/1907589126/Database+Schemas): - -// GPS -// { -// latitude: decimal, -// longitude: decimal, -// speed: decimal, -// heading: decimal, -// timestamp: -- :: -// } - -// Global Path -// { -// waypoints: [ -// { -// latitude: decimal, -// longitude: decimal -// } -// ], -// timestamp: -- :: -// } - -// Local Path -// { -// waypoints: [ -// { -// latitude: decimal, -// longitude: decimal -// } -// ], -// timestamp: -- :: -// } - -// AIS Ships -// { -// ships: [ -// { -// id: Number, -// latitude: decimal, -// longitude: decimal, -// cog: decimal, -// rot: decimal, -// sog: decimal, -// width: decimal, -// length: decimal -// } -// ], -// timestamp: -- :: -// } - -// Generic Sensors -// { -// genericSensors: [ -// { -// id: integer -// data: long -// } -// ], -// timestamp: -- :: -// } - -// Wind Sensors -// { -// windSensors: [ -// { -// speed: decimal, -// direction: number -// } -// ], -// timestamp: -- :: -// } - -// Batteries -// { -// batteries: [ -// { -// voltage: decimal, -// current: decimal -// } -// ], -// timestamp: -- :: -// } +// >>>>IMPORTANT<<<<< +// BSON document formats from: https://ubcsailbot.atlassian.net/wiki/spaces/prjt22/pages/1907589126/Database+Schemas: const std::string COLLECTION_AIS_SHIPS = "ais_ships"; const std::string COLLECTION_BATTERIES = "batteries"; @@ -125,14 +45,15 @@ class SailbotDB /** * @brief overload stream operator */ - friend std::ostream & operator<<(std::ostream & os, const RcvdMsgInfo & info) - { - os << "Latitude: " << info.lat_ << "\n" - << "Longitude: " << info.lon_ << "\n" - << "Accuracy (km): " << info.cep_ << "\n" - << "Timestamp: " << info.timestamp_; - return os; - } + friend std::ostream & operator<<(std::ostream & os, const RcvdMsgInfo & info); + + /** + * @brief Get a properly formatted timestamp string + * + * @param tm standard C/C++ time structure + * @return tm converted to a timestamp string + */ + static std::string mkTimestamp(const std::tm & tm); }; /** @@ -141,7 +62,7 @@ class SailbotDB * @param db_name name of desired database * @param mongodb_conn_str URL for mongodb database (ex. mongodb://localhost:27017) */ - explicit SailbotDB(const std::string & db_name, const std::string & mongodb_conn_str); + SailbotDB(const std::string & db_name, const std::string & mongodb_conn_str); /** * @brief Format and print a document in the DB diff --git a/lib/sailbot_db/inc/util_db.h b/lib/sailbot_db/inc/util_db.h new file mode 100644 index 0000000..d4d4d4f --- /dev/null +++ b/lib/sailbot_db/inc/util_db.h @@ -0,0 +1,114 @@ +#pragma once + +#include +#include + +#include "sailbot_db.h" +#include "sensors.pb.h" +#include "utils/utils.h" + +class UtilDB : public SailbotDB +{ +public: + static constexpr int NUM_AIS_SHIPS = 15; // arbitrary number + static constexpr int NUM_GENERIC_SENSORS = 5; // arbitrary number + static constexpr int NUM_PATH_WAYPOINTS = 5; // arbitrary number + + /** + * @brief Construct a UtilDB, which has debug utilities for SailbotDB + * + * @param db_name + * @param mongodb_conn_str + * @param rng + */ + UtilDB(const std::string & db_name, const std::string & mongodb_conn_str, std::shared_ptr rng); + + /** + * @brief Delete all documents in all collections + */ + void cleanDB(); + + /** + * @brief Generate random data for all sensors + * + * @return Sensors object + */ + Polaris::Sensors genRandSensors(); + + /** + * @return timestamp for the current time + */ + static std::tm getTimestamp(); + + /** + * @brief Generate random sensors and Iridium msg info + * + * @param tm Timestamp returned by getTimestamp() (with any modifications made to it) + * @return std::pair + */ + std::pair genRandData(const std::tm & tm); + + /** + * @brief Query the database and check that the sensor and message are correct + * + * @param expected_sensors + * @param expected_msg_info + */ + bool verifyDBWrite( + std::span expected_sensors, std::span expected_msg_info); + + /** + * @brief Dump and check all sensors and timestamps from the database + * + * @param tracker FailureTracker that gets if any unexpected results are dumped + * @param expected_num_docs Expected number of documents. tracker is updated if there's a mismatch + * @return std::pair{Vector of dumped Sensors, Vector of dumped timestamps} + */ + std::pair, std::vector> dumpSensors( + utils::FailTracker & tracker, size_t expected_num_docs = 1); + +private: + std::shared_ptr rng_; // random number generator + + /** + * @brief generate random GPS data + * + * @param gps_data GPS data to modify + */ + void genRandGpsData(Polaris::Sensors::Gps & gps_data); + + /** + * @brief generate random ais ships data + * + * @param ais_ship AIS ship data to modify + */ + void genRandAisData(Polaris::Sensors::Ais & ais_ship); + + /** + * @brief generate random generic sensor data + * + * @param generic_sensor Generic sensor data to modify + */ + void genRandGenericSensorData(Polaris::Sensors::Generic & generic_sensor); + + /** + * @brief generate random battery data + * + * @param battery battery data to modify + */ + void genRandBatteryData(Polaris::Sensors::Battery & battery); + + /** + * @brief generate random wind sensors data + * + * @param wind_data Wind sensor data to modify + */ + void genRandWindData(Polaris::Sensors::Wind & wind_data); + + /** + * @brief generate random path data + * + * @param path_data Path data to modify + */ + void genRandPathData(Polaris::Sensors::Path & path_data); +}; diff --git a/lib/sailbot_db/src/main.cpp b/lib/sailbot_db/src/main.cpp new file mode 100644 index 0000000..56249c3 --- /dev/null +++ b/lib/sailbot_db/src/main.cpp @@ -0,0 +1,126 @@ +#include + +#include "util_db.h" + +namespace po = boost::program_options; + +enum class CLIOpt { Help, Clear, Populate, Seed, DumpSensors, DumpGlobalPath, DBName }; + +std::string to_string(CLIOpt c) +{ + switch (c) { + case CLIOpt::Help: + return "help"; + case CLIOpt::Clear: + return "clear"; + case CLIOpt::Populate: + return "populate"; + case CLIOpt::Seed: + return "seed"; + case CLIOpt::DumpSensors: + return "dump-sensors"; + case CLIOpt::DumpGlobalPath: + return "dump-global-path"; + case CLIOpt::DBName: + return "db-name"; + } +}; + +const std::map CLIOptDesc{ + {CLIOpt::Help, "Help message"}, + {CLIOpt::Clear, "Clear the contents of a database collection"}, + {CLIOpt::Populate, "Populate a database collection with random data"}, + {CLIOpt::Seed, "(Optional) Unsigned integer random seed to generate random data used to populate db collection"}, + {CLIOpt::DumpSensors, "Dump latest sensor data in the database collection"}, + {CLIOpt::DumpGlobalPath, "Dump latest Global Path stored in the database collection"}, + {CLIOpt::DBName, "Name of db collection to target"}, +}; + +int main(int argc, char ** argv) +{ + try { + // Formatting is weird, see: https://www.boost.org/doc/libs/1_63_0/doc/html/program_options/tutorial.html + po::options_description o_desc("COMMAND(s)"); + // The ",h" allows for a shortened -h flag + o_desc.add_options()((to_string(CLIOpt::Help) + ",h").c_str(), CLIOptDesc.at(CLIOpt::Help).c_str()); + o_desc.add_options()(to_string(CLIOpt::Clear).c_str(), CLIOptDesc.at(CLIOpt::Clear).c_str()); + o_desc.add_options()(to_string(CLIOpt::Populate).c_str(), CLIOptDesc.at(CLIOpt::Populate).c_str()); + o_desc.add_options()( + to_string(CLIOpt::Seed).c_str(), po::value(), CLIOptDesc.at(CLIOpt::Seed).c_str()); + o_desc.add_options()(to_string(CLIOpt::DumpSensors).c_str(), CLIOptDesc.at(CLIOpt::DumpSensors).c_str()); + o_desc.add_options()(to_string(CLIOpt::DumpGlobalPath).c_str(), CLIOptDesc.at(CLIOpt::DumpGlobalPath).c_str()); + o_desc.add_options()( + to_string(CLIOpt::DBName).c_str(), po::value(), CLIOptDesc.at(CLIOpt::DBName).c_str()); + + // Make DBName a positional argument so we don't have to specify --db-name + po::positional_options_description po_desc; + po_desc.add(to_string(CLIOpt::DBName).c_str(), -1); + + po::variables_map vm; + po::store(po::command_line_parser(argc, argv).options(o_desc).positional(po_desc).run(), vm); + po::notify(vm); + + const std::string usage_instructions = [&o_desc]() { + std::stringstream ss; + ss << "Usage: sailbot_db DB-NAME [COMMAND]\n\n" + // Need to separately print that DB-NAME is a positional argument + << "DB-NAME: " << CLIOptDesc.at(CLIOpt::DBName) << "\n\n" + << o_desc << std::endl; + return ss.str(); + }(); + + if (vm.count(to_string(CLIOpt::Help)) != 0) { + std::cout << usage_instructions << std::endl; + return 0; + } + + if (vm.count(to_string(CLIOpt::DBName)) == 0) { + std::cerr << usage_instructions << std::endl; + return -1; + } + + uint32_t seed; + if (vm.count(to_string(CLIOpt::Seed)) != 0) { + seed = vm[to_string(CLIOpt::Seed)].as(); + } else { + seed = std::random_device()(); // initialize seed with random device value + } + std::mt19937 mt(seed); + + std::string db_name = vm[to_string(CLIOpt::DBName)].as(); + UtilDB db(db_name, MONGODB_CONN_STR, std::make_shared(mt)); + + if (!db.testConnection()) { + std::cerr << "Failed to establish connection to DB \"" << db_name << "\"" << std::endl; + return -1; + } + + if (vm.count(to_string(CLIOpt::Clear)) != 0) { + db.cleanDB(); + } + + if (vm.count(to_string(CLIOpt::Populate)) != 0) { + std::cout << "Populating random sensors with seed: " << seed << std::endl; + auto [rand_sensors, info] = db.genRandData(UtilDB::getTimestamp()); + db.storeNewSensors(rand_sensors, info); + // TODO(hsn200406,vaibhavambastha): Add code to store global path + } + + if (vm.count(to_string(CLIOpt::DumpSensors)) != 0) { + utils::FailTracker t; + auto [sensors_vec, timestamp_vec] = db.dumpSensors(t, 1); + std::cout << "Latest sensors:\n\n" << sensors_vec.at(sensors_vec.size() - 1).DebugString() << std::endl; + std::cout << "Timestamp: " << timestamp_vec.at(sensors_vec.size() - 1) << std::endl; + } + + if (vm.count(to_string(CLIOpt::DumpGlobalPath)) != 0) { + std::cerr << "Dump global path not implemented!" << std::endl; + // TODO(hsn200406,vaibhavambastha): Add code to dump global path + } + + return 0; + } catch (std::exception & e) { + std::cerr << e.what() << std::endl; + return -1; + } +} diff --git a/projects/remote_transceiver/src/sailbot_db.cpp b/lib/sailbot_db/src/sailbot_db.cpp similarity index 88% rename from projects/remote_transceiver/src/sailbot_db.cpp rename to lib/sailbot_db/src/sailbot_db.cpp index b1bb5db..588b5a0 100644 --- a/projects/remote_transceiver/src/sailbot_db.cpp +++ b/lib/sailbot_db/src/sailbot_db.cpp @@ -5,12 +5,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include "sensors.pb.h" #include "waypoint.pb.h" @@ -22,6 +24,27 @@ mongocxx::instance SailbotDB::inst_{}; // staticallly initialize instance // PUBLIC +std::ostream & operator<<(std::ostream & os, const SailbotDB::RcvdMsgInfo & info) +{ + os << "Latitude: " << info.lat_ << "\n" + << "Longitude: " << info.lon_ << "\n" + << "Accuracy (km): " << info.cep_ << "\n" + << "Timestamp: " << info.timestamp_; + return os; +} + +std::string SailbotDB::RcvdMsgInfo::mkTimestamp(const std::tm & tm) +{ + // This is impossible to read. It's reading each field of tm and 0 padding it to 2 digits with either "-" or ":" + // in between each number + std::stringstream tm_ss; + tm_ss << std::setfill('0') << std::setw(2) << tm.tm_year << "-" << std::setfill('0') << std::setw(2) << tm.tm_mon + << "-" << std::setfill('0') << std::setw(2) << tm.tm_mday << " " << std::setfill('0') << std::setw(2) + << tm.tm_hour << ":" << std::setfill('0') << std::setw(2) << tm.tm_min << ":" << std::setfill('0') + << std::setw(2) << tm.tm_sec; + return tm_ss.str(); +} + SailbotDB::SailbotDB(const std::string & db_name, const std::string & mongodb_conn_str) : db_name_(db_name) { mongocxx::uri uri = mongocxx::uri{mongodb_conn_str}; diff --git a/lib/sailbot_db/src/util_db.cpp b/lib/sailbot_db/src/util_db.cpp new file mode 100644 index 0000000..e74e834 --- /dev/null +++ b/lib/sailbot_db/src/util_db.cpp @@ -0,0 +1,399 @@ + +#include "util_db.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cmn_hdrs/shared_constants.h" +#include "utils/utils.h" + +using Polaris::Sensors; + +void UtilDB::cleanDB() +{ + mongocxx::pool::entry entry = pool_->acquire(); + mongocxx::database db = (*entry)[db_name_]; + + mongocxx::collection gps_coll = db[COLLECTION_GPS]; + mongocxx::collection ais_coll = db[COLLECTION_AIS_SHIPS]; + mongocxx::collection generic_coll = db[COLLECTION_DATA_SENSORS]; + mongocxx::collection batteries_coll = db[COLLECTION_BATTERIES]; + mongocxx::collection wind_coll = db[COLLECTION_WIND_SENSORS]; + mongocxx::collection local_path_coll = db[COLLECTION_LOCAL_PATH]; + + gps_coll.delete_many(bsoncxx::builder::basic::make_document()); + ais_coll.delete_many(bsoncxx::builder::basic::make_document()); + generic_coll.delete_many(bsoncxx::builder::basic::make_document()); + batteries_coll.delete_many(bsoncxx::builder::basic::make_document()); + wind_coll.delete_many(bsoncxx::builder::basic::make_document()); + local_path_coll.delete_many(bsoncxx::builder::basic::make_document()); +} + +Sensors UtilDB::genRandSensors() +{ + Sensors sensors; + + // gps + genRandGpsData(*sensors.mutable_gps()); + + // ais ships, TODO(): Polaris should be included as one of the AIS ships + for (int i = 0; i < NUM_AIS_SHIPS; i++) { + genRandAisData(*sensors.add_ais_ships()); + } + + // generic sensors + for (int i = 0; i < NUM_GENERIC_SENSORS; i++) { + genRandGenericSensorData(*sensors.add_data_sensors()); + } + + // batteries + for (int i = 0; i < NUM_BATTERIES; i++) { + genRandBatteryData(*sensors.add_batteries()); + } + + // wind sensors + for (int i = 0; i < NUM_WIND_SENSORS; i++) { + genRandWindData(*sensors.add_wind_sensors()); + } + + // path waypoints + genRandPathData(*sensors.mutable_local_path_data()); + + return sensors; +} + +std::tm UtilDB::getTimestamp() +{ + // Get the current time + std::time_t t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm * tm = std::gmtime(&t); // NOLINT(concurrency-mt-unsafe) + // tm stores years since 1900 by default, the schema expects years since 2000 + tm->tm_year -= 100; // NOLINT(readability-magic-numbers) + return *tm; +} + +std::pair UtilDB::genRandData(const std::tm & tm) +{ + Sensors rand_sensors = genRandSensors(); + + SailbotDB::RcvdMsgInfo rand_info{ + .lat_ = 0, // Not processed yet, so just set to 0 + .lon_ = 0, // Not processed yet, so just set to 0 + .cep_ = 0, // Not processed yet, so just set to 0 + .timestamp_ = SailbotDB::RcvdMsgInfo::mkTimestamp(tm)}; + return {rand_sensors, rand_info}; +} + +bool UtilDB::verifyDBWrite(std::span expected_sensors, std::span expected_msg_info) +{ + utils::FailTracker tracker; + + auto expectEQ = [&tracker](T rcvd, T expected, const std::string & err_msg) -> void { + tracker.track(utils::checkEQ(rcvd, expected, err_msg)); + }; + auto expectFloatEQ = [&tracker](T rcvd, T expected, const std::string & err_msg) -> void { + tracker.track(utils::checkEQ(rcvd, expected, err_msg)); + }; + + expectEQ(expected_sensors.size(), expected_msg_info.size(), "Must have msg info for each set of Sensors"); + size_t num_docs = expected_sensors.size(); + auto [dumped_sensors, dumped_timestamps] = dumpSensors(tracker, num_docs); + + expectEQ(dumped_sensors.size(), num_docs, ""); + expectEQ(dumped_timestamps.size(), num_docs, ""); + + for (size_t i = 0; i < num_docs; i++) { + expectEQ(dumped_timestamps[i], expected_msg_info[i].timestamp_, ""); + + // gps + expectFloatEQ(dumped_sensors[i].gps().latitude(), expected_sensors[i].gps().latitude(), ""); + expectFloatEQ(dumped_sensors[i].gps().longitude(), expected_sensors[i].gps().longitude(), ""); + expectFloatEQ(dumped_sensors[i].gps().speed(), expected_sensors[i].gps().speed(), ""); + expectFloatEQ(dumped_sensors[i].gps().heading(), expected_sensors[i].gps().heading(), ""); + + // ais ships + for (int j = 0; j < NUM_AIS_SHIPS; j++) { + const Sensors::Ais & dumped_ais_ship = dumped_sensors[i].ais_ships(j); + const Sensors::Ais & expected_ais_ship = expected_sensors[i].ais_ships(j); + expectEQ(dumped_ais_ship.id(), expected_ais_ship.id(), ""); + expectFloatEQ(dumped_ais_ship.latitude(), expected_ais_ship.latitude(), ""); + expectFloatEQ(dumped_ais_ship.longitude(), expected_ais_ship.longitude(), ""); + expectFloatEQ(dumped_ais_ship.sog(), expected_ais_ship.sog(), ""); + expectFloatEQ(dumped_ais_ship.cog(), expected_ais_ship.cog(), ""); + expectFloatEQ(dumped_ais_ship.rot(), expected_ais_ship.rot(), ""); + expectFloatEQ(dumped_ais_ship.width(), expected_ais_ship.width(), ""); + expectFloatEQ(dumped_ais_ship.length(), expected_ais_ship.length(), ""); + } + + // generic sensors + for (int j = 0; j < NUM_GENERIC_SENSORS; j++) { + const Sensors::Generic & dumped_data_sensor = dumped_sensors[i].data_sensors(j); + const Sensors::Generic & expected_data_sensor = expected_sensors[i].data_sensors(j); + expectEQ(dumped_data_sensor.id(), expected_data_sensor.id(), ""); + expectEQ(dumped_data_sensor.data(), expected_data_sensor.data(), ""); + } + + // batteries + for (int j = 0; j < NUM_BATTERIES; j++) { + const Sensors::Battery & dumped_battery = dumped_sensors[i].batteries(j); + const Sensors::Battery & expected_battery = expected_sensors[i].batteries(j); + expectFloatEQ(dumped_battery.voltage(), expected_battery.voltage(), ""); + expectFloatEQ(dumped_battery.current(), expected_battery.current(), ""); + } + + // wind sensors + for (int j = 0; j < NUM_WIND_SENSORS; j++) { + const Sensors::Wind & dumped_wind_sensor = dumped_sensors[i].wind_sensors(j); + const Sensors::Wind & expected_wind_sensor = expected_sensors[i].wind_sensors(j); + expectFloatEQ(dumped_wind_sensor.speed(), expected_wind_sensor.speed(), ""); + expectEQ(dumped_wind_sensor.direction(), expected_wind_sensor.direction(), ""); + } + + // path waypoints + for (int j = 0; j < NUM_PATH_WAYPOINTS; j++) { + const Polaris::Waypoint & dumped_path_waypoint = dumped_sensors[i].local_path_data().waypoints(j); + const Polaris::Waypoint & expected_path_waypoint = expected_sensors[i].local_path_data().waypoints(j); + expectFloatEQ(dumped_path_waypoint.latitude(), expected_path_waypoint.latitude(), ""); + expectFloatEQ(dumped_path_waypoint.longitude(), expected_path_waypoint.longitude(), ""); + } + } + return !tracker.failed(); +} + +std::pair, std::vector> UtilDB::dumpSensors( + utils::FailTracker & tracker, size_t num_docs) +{ + auto expectEQ = [&tracker](T rcvd, T expected, const std::string & err_msg) -> void { + tracker.track(utils::checkEQ(rcvd, expected, err_msg)); + }; + + std::vector sensors_vec(num_docs); + std::vector timestamp_vec(num_docs); + mongocxx::pool::entry entry = pool_->acquire(); + mongocxx::database db = (*entry)[db_name_]; + // Set the find options to sort by timestamp + bsoncxx::document::value order = bsoncxx::builder::stream::document{} << "timestamp" << 1 + << bsoncxx::builder::stream::finalize; + mongocxx::options::find opts = mongocxx::options::find{}; + opts.sort(order.view()); + + // gps + mongocxx::collection gps_coll = db[COLLECTION_GPS]; + mongocxx::cursor gps_docs = gps_coll.find({}, opts); + expectEQ( + static_cast(gps_coll.count_documents({})), num_docs, + "Error: TestDB should only have " + std::to_string(num_docs) + " documents per collection"); + + for (auto [i, gps_docs_it] = std::tuple{size_t{0}, gps_docs.begin()}; i < num_docs; i++, gps_docs_it++) { + Sensors & sensors = sensors_vec[i]; + std::string & timestamp = timestamp_vec[i]; + const bsoncxx::document::view gps_doc = *gps_docs_it; + + Sensors::Gps * gps = sensors.mutable_gps(); + gps->set_latitude(static_cast(gps_doc["latitude"].get_double().value)); + gps->set_longitude(static_cast(gps_doc["longitude"].get_double().value)); + gps->set_speed(static_cast(gps_doc["speed"].get_double().value)); + gps->set_heading(static_cast(gps_doc["heading"].get_double().value)); + timestamp = gps_doc["timestamp"].get_utf8().value.to_string(); + } + + // ais ships + mongocxx::collection ais_coll = db[COLLECTION_AIS_SHIPS]; + mongocxx::cursor ais_docs = ais_coll.find({}, opts); + expectEQ( + static_cast(ais_coll.count_documents({})), num_docs, + "Error: TestDB should only have " + std::to_string(num_docs) + " documents per collection"); + + for (auto [i, ais_docs_it] = std::tuple{size_t{0}, ais_docs.begin()}; i < num_docs; i++, ais_docs_it++) { + Sensors & sensors = sensors_vec[i]; + const std::string & timestamp = timestamp_vec[i]; + const bsoncxx::document::view ais_ships_doc = *ais_docs_it; + + for (bsoncxx::array::element ais_ships_doc : ais_ships_doc["ships"].get_array().value) { + Sensors::Ais * ais_ship = sensors.add_ais_ships(); + ais_ship->set_id(static_cast(ais_ships_doc["id"].get_int64().value)); + ais_ship->set_latitude(static_cast(ais_ships_doc["latitude"].get_double().value)); + ais_ship->set_longitude(static_cast(ais_ships_doc["longitude"].get_double().value)); + ais_ship->set_sog(static_cast(ais_ships_doc["sog"].get_double().value)); + ais_ship->set_cog(static_cast(ais_ships_doc["cog"].get_double().value)); + ais_ship->set_rot(static_cast(ais_ships_doc["rot"].get_double().value)); + ais_ship->set_width(static_cast(ais_ships_doc["width"].get_double().value)); + ais_ship->set_length(static_cast(ais_ships_doc["length"].get_double().value)); + } + expectEQ(sensors.ais_ships().size(), NUM_AIS_SHIPS, "Size mismatch when reading AIS ships from DB"); + expectEQ(ais_ships_doc["timestamp"].get_utf8().value.to_string(), timestamp, "Document timestamp mismatch"); + } + + // generic sensor + mongocxx::collection generic_coll = db[COLLECTION_DATA_SENSORS]; + mongocxx::cursor generic_sensor_docs = generic_coll.find({}, opts); + expectEQ( + static_cast(generic_coll.count_documents({})), num_docs, + "Error: TestDB should only have " + std::to_string(num_docs) + " documents per collection"); + + for (auto [i, generic_sensor_docs_it] = std::tuple{size_t{0}, generic_sensor_docs.begin()}; i < num_docs; + i++, generic_sensor_docs_it++) { + Sensors & sensors = sensors_vec[i]; + const std::string & timestamp = timestamp_vec[i]; + const bsoncxx::document::view generic_doc = *generic_sensor_docs_it; + + for (bsoncxx::array::element generic_doc : generic_doc["genericSensors"].get_array().value) { + Sensors::Generic * generic = sensors.add_data_sensors(); + generic->set_id(static_cast(generic_doc["id"].get_int64().value)); + generic->set_data(static_cast(generic_doc["data"].get_int64().value)); + } + expectEQ(generic_doc["timestamp"].get_utf8().value.to_string(), timestamp, "Document timestamp mismatch"); + } + + // battery + mongocxx::collection batteries_coll = db[COLLECTION_BATTERIES]; + mongocxx::cursor batteries_data_docs = batteries_coll.find({}, opts); + expectEQ( + static_cast(batteries_coll.count_documents({})), num_docs, + "Error: TestDB should only have " + std::to_string(num_docs) + " documents per collection"); + + for (auto [i, batteries_doc_it] = std::tuple{size_t{0}, batteries_data_docs.begin()}; i < num_docs; + i++, batteries_doc_it++) { + Sensors & sensors = sensors_vec[i]; + const std::string & timestamp = timestamp_vec[i]; + const bsoncxx::document::view batteries_doc = *batteries_doc_it; + + for (bsoncxx::array::element batteries_doc : batteries_doc["batteries"].get_array().value) { + Sensors::Battery * battery = sensors.add_batteries(); + battery->set_voltage(static_cast(batteries_doc["voltage"].get_double().value)); + battery->set_current(static_cast(batteries_doc["current"].get_double().value)); + } + expectEQ(sensors.batteries().size(), NUM_BATTERIES, "Size mismatch when reading batteries from DB"); + expectEQ(batteries_doc["timestamp"].get_utf8().value.to_string(), timestamp, "Document timestamp mismatch"); + } + + // wind sensor + mongocxx::collection wind_coll = db[COLLECTION_WIND_SENSORS]; + mongocxx::cursor wind_sensors_docs = wind_coll.find({}, opts); + expectEQ( + static_cast(wind_coll.count_documents({})), num_docs, + "Error: TestDB should only have " + std::to_string(num_docs) + " documents per collection"); + + for (auto [i, wind_doc_it] = std::tuple{size_t{0}, wind_sensors_docs.begin()}; i < num_docs; i++, wind_doc_it++) { + Sensors & sensors = sensors_vec[i]; + const std::string & timestamp = timestamp_vec[i]; + const bsoncxx::document::view wind_doc = *wind_doc_it; + for (bsoncxx::array::element wind_doc : wind_doc["windSensors"].get_array().value) { + Sensors::Wind * wind = sensors.add_wind_sensors(); + wind->set_speed(static_cast(wind_doc["speed"].get_double().value)); + wind->set_direction(static_cast(wind_doc["direction"].get_int32().value)); + } + expectEQ(sensors.wind_sensors().size(), NUM_WIND_SENSORS, "Size mismatch when reading batteries from DB"); + expectEQ(wind_doc["timestamp"].get_utf8().value.to_string(), timestamp, "Document timestamp mismatch"); + } + + // local path + mongocxx::collection path_coll = db[COLLECTION_LOCAL_PATH]; + mongocxx::cursor local_path_docs = path_coll.find({}, opts); + expectEQ( + static_cast(path_coll.count_documents({})), num_docs, + "Error: TestDB should only have " + std::to_string(num_docs) + " documents per collection"); + + for (auto [i, path_doc_it] = std::tuple{size_t{0}, local_path_docs.begin()}; i < num_docs; i++, path_doc_it++) { + Sensors & sensors = sensors_vec[i]; + const std::string & timestamp = timestamp_vec[i]; + const bsoncxx::document::view path_doc = *path_doc_it; + for (bsoncxx::array::element path_doc : path_doc["waypoints"].get_array().value) { + Polaris::Waypoint * path = sensors.mutable_local_path_data()->add_waypoints(); + path->set_latitude(static_cast(path_doc["latitude"].get_double().value)); + path->set_longitude(static_cast(path_doc["longitude"].get_double().value)); + } + expectEQ( + sensors.local_path_data().waypoints_size(), NUM_PATH_WAYPOINTS, + "Size mismatch when reading path waypoints from DB"); + expectEQ(path_doc["timestamp"].get_utf8().value.to_string(), timestamp, "Document timestamp mismatch"); + } + + return {sensors_vec, timestamp_vec}; +} + +UtilDB::UtilDB(const std::string & db_name, const std::string & mongodb_conn_str, std::shared_ptr rng) +: SailbotDB(db_name, mongodb_conn_str), rng_(rng) +{ +} + +void UtilDB::genRandGpsData(Sensors::Gps & gps_data) +{ + std::uniform_real_distribution lat_dist(LAT_LBND, LAT_UBND); + std::uniform_real_distribution lon_dist(LON_LBND, LON_UBND); + std::uniform_real_distribution speed_dist(SPEED_LBND, SPEED_UBND); + std::uniform_real_distribution heading_dist(HEADING_LBND, HEADING_UBND); + gps_data.set_latitude(lat_dist(*rng_)); + gps_data.set_longitude(lon_dist(*rng_)); + gps_data.set_speed(speed_dist(*rng_)); + gps_data.set_heading(heading_dist(*rng_)); +} + +void UtilDB::genRandAisData(Sensors::Ais & ais_ship) +{ + std::uniform_int_distribution id_dist(0, UINT32_MAX); + std::uniform_real_distribution lat_dist(LAT_LBND, LAT_UBND); + std::uniform_real_distribution lon_dist(LON_LBND, LON_UBND); + std::uniform_real_distribution speed_dist(SPEED_LBND, SPEED_UBND); + std::uniform_real_distribution heading_dist(HEADING_LBND, HEADING_UBND); + std::uniform_real_distribution rot_dist(ROT_LBND, ROT_UBND); + std::uniform_real_distribution width_dist(SHIP_DIMENSION_LBND, SHIP_DIMENSION_UBND); + std::uniform_real_distribution length_dist(SHIP_DIMENSION_LBND, SHIP_DIMENSION_UBND); + + ais_ship.set_id(id_dist(*rng_)); + ais_ship.set_latitude(lat_dist(*rng_)); + ais_ship.set_longitude(lon_dist(*rng_)); + ais_ship.set_sog(speed_dist(*rng_)); + ais_ship.set_cog(heading_dist(*rng_)); + ais_ship.set_rot(rot_dist(*rng_)); + ais_ship.set_width(width_dist(*rng_)); + ais_ship.set_length(length_dist(*rng_)); +} + +void UtilDB::genRandGenericSensorData(Sensors::Generic & generic_sensor) +{ + std::uniform_int_distribution id_generic(0, UINT8_MAX); + std::uniform_int_distribution data_generic(0, UINT64_MAX); + + generic_sensor.set_id(id_generic(*rng_)); + generic_sensor.set_data(data_generic(*rng_)); +} + +void UtilDB::genRandBatteryData(Sensors::Battery & battery) +{ + std::uniform_real_distribution voltage_battery(BATT_VOLT_LBND, BATT_VOLT_UBND); + std::uniform_real_distribution current_battery(BATT_CURR_LBND, BATT_CURR_UBND); + + battery.set_voltage(voltage_battery(*rng_)); + battery.set_current(current_battery(*rng_)); +} + +void UtilDB::genRandWindData(Sensors::Wind & wind_data) +{ + std::uniform_real_distribution speed_wind(SPEED_LBND, SPEED_UBND); + std::uniform_int_distribution direction_wind(WIND_DIRECTION_LBND, WIND_DIRECTION_UBND); + + wind_data.set_speed(speed_wind(*rng_)); + wind_data.set_direction(direction_wind(*rng_)); +} + +void UtilDB::genRandPathData(Sensors::Path & path_data) +{ + std::uniform_real_distribution latitude_path(LAT_LBND, LAT_UBND); + std::uniform_real_distribution longitude_path(LON_LBND, LON_UBND); + + for (int i = 0; i < NUM_PATH_WAYPOINTS; i++) { + Polaris::Waypoint * waypoint = path_data.add_waypoints(); + waypoint->set_latitude(latitude_path(*rng_)); + waypoint->set_longitude(longitude_path(*rng_)); + } +} diff --git a/lib/sailbot_db/test/test_sailbot_db.cpp b/lib/sailbot_db/test/test_sailbot_db.cpp new file mode 100644 index 0000000..f2fa59e --- /dev/null +++ b/lib/sailbot_db/test/test_sailbot_db.cpp @@ -0,0 +1,40 @@ +#include + +#include "sailbot_db.h" +#include "util_db.h" + +using Polaris::Sensors; + +static std::random_device g_rd = std::random_device(); // random number sampler +static uint32_t g_rand_seed = g_rd(); // seed used for random number generation +static std::mt19937 g_mt(g_rand_seed); // initialize random number generator with seed +static UtilDB g_test_db("test", MONGODB_CONN_STR, std::make_shared(g_mt)); +class TestSailbotDB : public ::testing::Test +{ +protected: + TestSailbotDB() { g_test_db.cleanDB(); } + ~TestSailbotDB() {} +}; + +/** + * @brief Check that MongoDB is running + */ +TEST_F(TestSailbotDB, TestConnection) +{ + ASSERT_TRUE(g_test_db.testConnection()) << "MongoDB not running - remember to connect!"; +} + +/** + * @brief Write random sensor data to the TestDB - read and verify said data + */ +TEST_F(TestSailbotDB, TestStoreSensors) +{ + SCOPED_TRACE("Seed: " + std::to_string(g_rand_seed)); // Print seed on any failure + auto [rand_sensors, rand_info] = g_test_db.genRandData(UtilDB::getTimestamp()); + ASSERT_TRUE(g_test_db.storeNewSensors(rand_sensors, rand_info)); + + std::array expected_sensors = {rand_sensors}; + std::array expected_info = {rand_info}; + + EXPECT_TRUE(g_test_db.verifyDBWrite(expected_sensors, expected_info)); +} diff --git a/lib/utils/utils.h b/lib/utils/utils.h index 16d6817..6239101 100644 --- a/lib/utils/utils.h +++ b/lib/utils/utils.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -7,6 +8,8 @@ // Define a concept for arithmetic types template concept arithmetic = std::integral or std::floating_point; +template +concept not_float = not std::floating_point; namespace utils { @@ -33,4 +36,118 @@ std::optional isOutOfBounds(T val, T lbnd, T ubnd) return std::nullopt; } +/** + * @brief Calculate floating point equality using the GoogleTest default definition + * http://google.github.io/googletest/reference/assertions.html#floating-point + * + * @tparam T A floating point type (float, double, etc) + * @param to_check Value to compare to expected + * @param expected Expected value + * @param err_msg String that gets printed to stderr on failure + * @return true if "to_check" is close enough to "expected", false otherwise + */ +template +bool isFloatEQ(T to_check, T expected, const std::string & err_msg) +{ + constexpr int ALLOWED_ULP_DIFF = 4; + + int diff = boost::math::float_distance(to_check, expected); + if (std::abs(diff) <= ALLOWED_ULP_DIFF) { + return true; + } + if (!err_msg.empty()) { + std::cerr << err_msg << std::endl; + } + return false; +} + +/** + * @brief Calls default isFloatEq(T, T, string) with an empty error string + * + */ +template +bool isFloatEQ(T to_check, T expected) +{ + return isFloatEQ(to_check, expected, ""); +} + +/** + * @brief Check if two non-floating point values are equal and output an error on mismatch + * + * @tparam T non-floating point type + * @param rcvd received value + * @param expected expected value + * @param err_msg error string + * @return true if rcvd and expected match + * @return false otherwise + */ +template +bool checkEQ(T rcvd, T expected, const std::string & err_msg) +{ + if (rcvd != expected) { + std::cerr << "Expected: " << expected << " but received: " << rcvd << std::endl; + std::cerr << err_msg << std::endl; + return false; + } + return true; +}; + +/** + * @brief Check if two floating point values are equal using isFloatEq() and output an error on mismatch + * + * @tparam T floating point values (float, double, etc...) + * @param rcvd received value + * @param expected expected value + * @param err_msg error string + * @return true if rcvd and expected match + * @return false otherwise + */ +template +bool checkEQ(T rcvd, T expected, const std::string & err_msg) +{ + std::stringstream ss; + ss << "Expected: " << expected << " but received: " << rcvd << "\n" << err_msg; + return static_cast(utils::isFloatEQ(rcvd, expected, ss.str())); +}; + +/** + * @brief Simple class to count number of failures + * + */ +class FailTracker +{ +public: + /** + * @brief Update the tracker + * + * @param was_success result of operation to check + */ + void track(bool was_success) + { + if (!was_success) { + fail_count_++; + } + } + + /** + * @brief Reset fail count + * + */ + void reset() { fail_count_ = 0; } + + /** + * @return true if any failures were tracked + * @return false otherwise + */ + bool failed() const { return fail_count_ != 0; } + + /** + * @return number of failures + */ + uint32_t failCount() const { return fail_count_; } + +private: + uint32_t fail_count_ = 0; +}; + } // namespace utils diff --git a/projects/mock_ais/CMakeLists.txt b/projects/mock_ais/CMakeLists.txt index 837c8d7..71978a1 100644 --- a/projects/mock_ais/CMakeLists.txt +++ b/projects/mock_ais/CMakeLists.txt @@ -10,11 +10,9 @@ set(inc_dirs set(compile_defs ) -# Create module library set(srcs ${CMAKE_CURRENT_LIST_DIR}/src/mock_ais.cpp ) -make_lib(${module} "${srcs}" "${link_libs}" "${inc_dirs}" "${compile_defs}") # Create module ROS executable set(bin_srcs diff --git a/projects/remote_transceiver/CMakeLists.txt b/projects/remote_transceiver/CMakeLists.txt index 46cdb25..aeb4c01 100644 --- a/projects/remote_transceiver/CMakeLists.txt +++ b/projects/remote_transceiver/CMakeLists.txt @@ -4,20 +4,21 @@ set(link_libs ${PROTOBUF_LINK_LIBS} mongo::mongocxx_shared mongo::bsoncxx_shared + sailbot_db ) set(inc_dirs ${PROTOBUF_INCLUDE_PATH} ${LIBMONGOCXX_INCLUDE_DIRS} ${LIBBSONCXX_INCLUDE_DIRS} + ${SAILBOT_DB_INC_DIR} ) -set(srcs - ${CMAKE_CURRENT_LIST_DIR}/src/sailbot_db.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/remote_transceiver.cpp +set(compile_defs ) -set(compile_defs +set(srcs + ${CMAKE_CURRENT_LIST_DIR}/src/remote_transceiver.cpp ) set(bin_srcs diff --git a/projects/remote_transceiver/test/test_remote_transceiver.cpp b/projects/remote_transceiver/test/test_remote_transceiver.cpp index c146552..8b043bf 100644 --- a/projects/remote_transceiver/test/test_remote_transceiver.cpp +++ b/projects/remote_transceiver/test/test_remote_transceiver.cpp @@ -4,19 +4,9 @@ #include #include #include -#include -#include -#include -#include -#include #include #include #include -#include -#include -#include -#include -#include #include #include #include @@ -26,12 +16,9 @@ #include "remote_transceiver.h" #include "sailbot_db.h" #include "sensors.pb.h" +#include "util_db.h" #include "waypoint.pb.h" -constexpr int NUM_AIS_SHIPS = 15; // arbitrary number -constexpr int NUM_GENERIC_SENSORS = 5; // arbitrary number -constexpr int NUM_PATH_WAYPOINTS = 5; // arbitrary number - using Polaris::Sensors; using remote_transceiver::HTTPServer; using remote_transceiver::Listener; @@ -39,468 +26,13 @@ using remote_transceiver::TESTING_HOST; using remote_transceiver::TESTING_PORT; namespace http_client = remote_transceiver::http_client; -//Child class of SailbotDB that includes additional database utility functions to help testing -class TestDB : public SailbotDB -{ -public: - static constexpr auto TEST_DB = "test"; - TestDB() : SailbotDB(TEST_DB, MONGODB_CONN_STR) {} - - /** - * @brief Delete all documents in all collections - */ - void cleanDB() - { - mongocxx::pool::entry entry = pool_->acquire(); - mongocxx::database db = (*entry)[db_name_]; - - mongocxx::collection gps_coll = db[COLLECTION_GPS]; - mongocxx::collection ais_coll = db[COLLECTION_AIS_SHIPS]; - mongocxx::collection generic_coll = db[COLLECTION_DATA_SENSORS]; - mongocxx::collection batteries_coll = db[COLLECTION_BATTERIES]; - mongocxx::collection wind_coll = db[COLLECTION_WIND_SENSORS]; - mongocxx::collection local_path_coll = db[COLLECTION_LOCAL_PATH]; - - gps_coll.delete_many(bsoncxx::builder::basic::make_document()); - ais_coll.delete_many(bsoncxx::builder::basic::make_document()); - generic_coll.delete_many(bsoncxx::builder::basic::make_document()); - batteries_coll.delete_many(bsoncxx::builder::basic::make_document()); - wind_coll.delete_many(bsoncxx::builder::basic::make_document()); - local_path_coll.delete_many(bsoncxx::builder::basic::make_document()); - } - - /** - * @brief Retrieve all sensors from the database sorted by timestamp - * - * @param num_docs expected number of documents for each collection, default 1 - * - * @return Vector of sensors objects: gps, ais, generic, batteries, wind, local path - * @return Vector of timestamps - * both vectors will be num_docs in size - */ - std::pair, std::vector> dumpSensors(size_t num_docs = 1) - { - std::vector sensors_vec(num_docs); - std::vector timestamp_vec(num_docs); - mongocxx::pool::entry entry = pool_->acquire(); - mongocxx::database db = (*entry)[db_name_]; - // Set the find options to sort by timestamp - bsoncxx::document::value order = bsoncxx::builder::stream::document{} << "timestamp" << 1 - << bsoncxx::builder::stream::finalize; - mongocxx::options::find opts = mongocxx::options::find{}; - opts.sort(order.view()); - - // gps - mongocxx::collection gps_coll = db[COLLECTION_GPS]; - mongocxx::cursor gps_docs = gps_coll.find({}, opts); - EXPECT_EQ(gps_coll.count_documents({}), num_docs) - << "Error: TestDB should only have " << num_docs << " documents per collection"; - - for (auto [i, gps_docs_it] = std::tuple{size_t{0}, gps_docs.begin()}; i < num_docs; i++, gps_docs_it++) { - Sensors & sensors = sensors_vec[i]; - std::string & timestamp = timestamp_vec[i]; - const bsoncxx::document::view gps_doc = *gps_docs_it; - - Sensors::Gps * gps = sensors.mutable_gps(); - gps->set_latitude(static_cast(gps_doc["latitude"].get_double().value)); - gps->set_longitude(static_cast(gps_doc["longitude"].get_double().value)); - gps->set_speed(static_cast(gps_doc["speed"].get_double().value)); - gps->set_heading(static_cast(gps_doc["heading"].get_double().value)); - timestamp = gps_doc["timestamp"].get_utf8().value.to_string(); - } - - // ais ships - mongocxx::collection ais_coll = db[COLLECTION_AIS_SHIPS]; - mongocxx::cursor ais_docs = ais_coll.find({}, opts); - EXPECT_EQ(ais_coll.count_documents({}), num_docs) - << "Error: TestDB should only have " << num_docs << " documents per collection"; - - for (auto [i, ais_docs_it] = std::tuple{size_t{0}, ais_docs.begin()}; i < num_docs; i++, ais_docs_it++) { - Sensors & sensors = sensors_vec[i]; - const std::string & timestamp = timestamp_vec[i]; - const bsoncxx::document::view ais_ships_doc = *ais_docs_it; - - for (bsoncxx::array::element ais_ships_doc : ais_ships_doc["ships"].get_array().value) { - Sensors::Ais * ais_ship = sensors.add_ais_ships(); - ais_ship->set_id(static_cast(ais_ships_doc["id"].get_int64().value)); - ais_ship->set_latitude(static_cast(ais_ships_doc["latitude"].get_double().value)); - ais_ship->set_longitude(static_cast(ais_ships_doc["longitude"].get_double().value)); - ais_ship->set_sog(static_cast(ais_ships_doc["sog"].get_double().value)); - ais_ship->set_cog(static_cast(ais_ships_doc["cog"].get_double().value)); - ais_ship->set_rot(static_cast(ais_ships_doc["rot"].get_double().value)); - ais_ship->set_width(static_cast(ais_ships_doc["width"].get_double().value)); - ais_ship->set_length(static_cast(ais_ships_doc["length"].get_double().value)); - } - EXPECT_EQ(sensors.ais_ships().size(), NUM_AIS_SHIPS) << "Size mismatch when reading AIS ships from DB"; - EXPECT_EQ(ais_ships_doc["timestamp"].get_utf8().value.to_string(), timestamp) - << "Document timestamp mismatch"; - } - - // generic sensor - mongocxx::collection generic_coll = db[COLLECTION_DATA_SENSORS]; - mongocxx::cursor generic_sensor_docs = generic_coll.find({}, opts); - EXPECT_EQ(generic_coll.count_documents({}), num_docs) - << "Error: TestDB should only have " << num_docs << " documents per collection"; - - for (auto [i, generic_sensor_docs_it] = std::tuple{size_t{0}, generic_sensor_docs.begin()}; i < num_docs; - i++, generic_sensor_docs_it++) { - Sensors & sensors = sensors_vec[i]; - const std::string & timestamp = timestamp_vec[i]; - const bsoncxx::document::view generic_doc = *generic_sensor_docs_it; - - for (bsoncxx::array::element generic_doc : generic_doc["genericSensors"].get_array().value) { - Sensors::Generic * generic = sensors.add_data_sensors(); - generic->set_id(static_cast(generic_doc["id"].get_int64().value)); - generic->set_data(static_cast(generic_doc["data"].get_int64().value)); - } - EXPECT_EQ(generic_doc["timestamp"].get_utf8().value.to_string(), timestamp) - << "Document timestamp mismatch"; - } - - // battery - mongocxx::collection batteries_coll = db[COLLECTION_BATTERIES]; - mongocxx::cursor batteries_data_docs = batteries_coll.find({}, opts); - EXPECT_EQ(batteries_coll.count_documents({}), num_docs) - << "Error: TestDB should only have " << num_docs << " documents per collection"; - - for (auto [i, batteries_doc_it] = std::tuple{size_t{0}, batteries_data_docs.begin()}; i < num_docs; - i++, batteries_doc_it++) { - Sensors & sensors = sensors_vec[i]; - const std::string & timestamp = timestamp_vec[i]; - const bsoncxx::document::view batteries_doc = *batteries_doc_it; - - for (bsoncxx::array::element batteries_doc : batteries_doc["batteries"].get_array().value) { - Sensors::Battery * battery = sensors.add_batteries(); - battery->set_voltage(static_cast(batteries_doc["voltage"].get_double().value)); - battery->set_current(static_cast(batteries_doc["current"].get_double().value)); - } - EXPECT_EQ(sensors.batteries().size(), NUM_BATTERIES) << "Size mismatch when reading batteries from DB"; - EXPECT_EQ(batteries_doc["timestamp"].get_utf8().value.to_string(), timestamp) - << "Document timestamp mismatch"; - } - - // wind sensor - mongocxx::collection wind_coll = db[COLLECTION_WIND_SENSORS]; - mongocxx::cursor wind_sensors_docs = wind_coll.find({}, opts); - EXPECT_EQ(wind_coll.count_documents({}), num_docs) - << "Error: TestDB should only have " << num_docs << " documents per collection"; - - for (auto [i, wind_doc_it] = std::tuple{size_t{0}, wind_sensors_docs.begin()}; i < num_docs; - i++, wind_doc_it++) { - Sensors & sensors = sensors_vec[i]; - const std::string & timestamp = timestamp_vec[i]; - const bsoncxx::document::view wind_doc = *wind_doc_it; - for (bsoncxx::array::element wind_doc : wind_doc["windSensors"].get_array().value) { - Sensors::Wind * wind = sensors.add_wind_sensors(); - wind->set_speed(static_cast(wind_doc["speed"].get_double().value)); - wind->set_direction(static_cast(wind_doc["direction"].get_int32().value)); - } - EXPECT_EQ(sensors.wind_sensors().size(), NUM_WIND_SENSORS) - << "Size mismatch when reading batteries from DB"; - EXPECT_EQ(wind_doc["timestamp"].get_utf8().value.to_string(), timestamp) << "Document timestamp mismatch"; - } - - // local path - mongocxx::collection path_coll = db[COLLECTION_LOCAL_PATH]; - mongocxx::cursor local_path_docs = path_coll.find({}, opts); - EXPECT_EQ(path_coll.count_documents({}), num_docs) - << "Error: TestDB should only have " << num_docs << " documents per collection"; - - for (auto [i, path_doc_it] = std::tuple{size_t{0}, local_path_docs.begin()}; i < num_docs; i++, path_doc_it++) { - Sensors & sensors = sensors_vec[i]; - const std::string & timestamp = timestamp_vec[i]; - const bsoncxx::document::view path_doc = *path_doc_it; - for (bsoncxx::array::element path_doc : path_doc["waypoints"].get_array().value) { - Polaris::Waypoint * path = sensors.mutable_local_path_data()->add_waypoints(); - path->set_latitude(static_cast(path_doc["latitude"].get_double().value)); - path->set_longitude(static_cast(path_doc["longitude"].get_double().value)); - } - EXPECT_EQ(sensors.local_path_data().waypoints_size(), NUM_PATH_WAYPOINTS) - << "Size mismatch when reading path waypoints from DB"; - EXPECT_EQ(path_doc["timestamp"].get_utf8().value.to_string(), timestamp) << "Document timestamp mismatch"; - } - - return {sensors_vec, timestamp_vec}; - } -}; - -static TestDB g_test_db = TestDB(); // initialize the TestDB instance -static std::random_device g_rd = std::random_device(); // random number sampler -static uint32_t g_rand_seed = g_rd(); // seed used for random number generation -static std::mt19937 g_mt(g_rand_seed); // initialize random number generator with seed -// Use a static counter for creating testing timestamps as the internals of the -// remote transceiver do not care about the format. -// The counter is of type char because of how strings are sorted in alphabetical order, -// where a sequence of strings "1", "2", "10" are sorted to "1", "10", "2". -// Incrementing a char prevents this issue for up to 256 numbers as it increments -// '0', '1', ..., '9', '', ... -static char g_doc_num = '0'; - -class TestSailbotDB : public ::testing::Test -{ -protected: - TestSailbotDB() - { - g_test_db.cleanDB(); - g_doc_num = '0'; - } - ~TestSailbotDB() override {} -}; - -/** - * @brief generate random GPS data - * - * @param gps_data pointer to generated gps_data - */ -void * genRandGpsData(Sensors::Gps * gps_data) -{ - std::uniform_real_distribution lat_dist(LAT_LBND, LAT_UBND); - std::uniform_real_distribution lon_dist(LON_LBND, LON_UBND); - std::uniform_real_distribution speed_dist(SPEED_LBND, SPEED_UBND); - std::uniform_real_distribution heading_dist(HEADING_LBND, HEADING_UBND); - gps_data->set_latitude(lat_dist(g_mt)); - gps_data->set_longitude(lon_dist(g_mt)); - gps_data->set_speed(speed_dist(g_mt)); - gps_data->set_heading(heading_dist(g_mt)); - - return gps_data; -} - -/** - * @brief generate random ais ships data - * - * @param ais_ship pointer to generated ais data - */ -void genRandAisData(Sensors::Ais * ais_ship) -{ - std::uniform_int_distribution id_dist(0, UINT32_MAX); - std::uniform_real_distribution lat_dist(LAT_LBND, LAT_UBND); - std::uniform_real_distribution lon_dist(LON_LBND, LON_UBND); - std::uniform_real_distribution speed_dist(SPEED_LBND, SPEED_UBND); - std::uniform_real_distribution heading_dist(HEADING_LBND, HEADING_UBND); - std::uniform_real_distribution rot_dist(ROT_LBND, ROT_UBND); - std::uniform_real_distribution width_dist(SHIP_DIMENSION_LBND, SHIP_DIMENSION_UBND); - std::uniform_real_distribution length_dist(SHIP_DIMENSION_LBND, SHIP_DIMENSION_UBND); - - ais_ship->set_id(id_dist(g_mt)); - ais_ship->set_latitude(lat_dist(g_mt)); - ais_ship->set_longitude(lon_dist(g_mt)); - ais_ship->set_sog(speed_dist(g_mt)); - ais_ship->set_cog(heading_dist(g_mt)); - ais_ship->set_rot(rot_dist(g_mt)); - ais_ship->set_width(width_dist(g_mt)); - ais_ship->set_length(length_dist(g_mt)); -} - -/** - * @brief generate random generic sensor data - * - * @return pointer to generated generic sensor data - */ -void genRandGenericSensorData(Sensors::Generic * generic_sensor) -{ - std::uniform_int_distribution id_generic(0, UINT8_MAX); - std::uniform_int_distribution data_generic(0, UINT64_MAX); - - generic_sensor->set_id(id_generic(g_mt)); - generic_sensor->set_data(data_generic(g_mt)); -} - -/** - * @brief generate random battery data - * - * @return pointer to generated battery data - */ -void genRandBatteriesData(Sensors::Battery * battery) -{ - std::uniform_real_distribution voltage_battery(BATT_VOLT_LBND, BATT_VOLT_UBND); - std::uniform_real_distribution current_battery(BATT_CURR_LBND, BATT_CURR_UBND); - - battery->set_voltage(voltage_battery(g_mt)); - battery->set_current(current_battery(g_mt)); -} - -/** - * @brief generate random wind sensors data - * - * @return pointer to generated wind sensors data - */ -void genRandWindData(Sensors::Wind * wind_data) -{ - std::uniform_real_distribution speed_wind(SPEED_LBND, SPEED_UBND); - std::uniform_int_distribution direction_wind(WIND_DIRECTION_LBND, WIND_DIRECTION_UBND); - - wind_data->set_speed(speed_wind(g_mt)); - wind_data->set_direction(direction_wind(g_mt)); -} - -/** - * @brief generate random path data - * - * @return pointer to generated path data - */ -void genRandPathData(Sensors::Path * path_data) -{ - std::uniform_real_distribution latitude_path(LAT_LBND, LAT_UBND); - std::uniform_real_distribution longitude_path(LON_LBND, LON_UBND); - - for (int i = 0; i < NUM_PATH_WAYPOINTS; i++) { - Polaris::Waypoint * waypoint = path_data->add_waypoints(); - waypoint->set_latitude(latitude_path(g_mt)); - waypoint->set_longitude(longitude_path(g_mt)); - } -} - -/** - * @brief Generate random data for all sensors - * - * @return Sensors object - */ -Sensors genRandSensors() -{ - Sensors sensors; - - // gps - genRandGpsData(sensors.mutable_gps()); - - // ais ships, TODO(): Polaris should be included as one of the AIS ships - for (int i = 0; i < NUM_AIS_SHIPS; i++) { - genRandAisData(sensors.add_ais_ships()); - } - - // generic sensors - for (int i = 0; i < NUM_GENERIC_SENSORS; i++) { - genRandGenericSensorData(sensors.add_data_sensors()); - } - - // batteries - for (int i = 0; i < NUM_BATTERIES; i++) { - genRandBatteriesData(sensors.add_batteries()); - } - - // wind sensors - for (int i = 0; i < NUM_WIND_SENSORS; i++) { - genRandWindData(sensors.add_wind_sensors()); - } - - // path waypoints - genRandPathData(sensors.mutable_local_path_data()); - - return sensors; -} - -/** - * @brief Generate random sensors and Iridium msg info - * - * @return std::pair - */ -std::pair genRandData() -{ - Sensors rand_sensors = genRandSensors(); - SailbotDB::RcvdMsgInfo rand_info{ - .lat_ = 0, // Not processed yet, so just set to 0 - .lon_ = 0, // Not processed yet, so just set to 0 - .cep_ = 0, // Not processed yet, so just set to 0 - .timestamp_ = std::to_string(g_doc_num++)}; // increment counter after converting and storing - return {rand_sensors, rand_info}; -} - -/** - * @brief Query the database and check that the sensor and message are correct - * - * @param expected_sensors - * @param expected_msg_info - */ -void verifyDBWrite(std::span expected_sensors, std::span expected_msg_info) -{ - ASSERT_EQ(expected_sensors.size(), expected_msg_info.size()) << "Must have msg info for each set of Sensors"; - size_t num_docs = expected_sensors.size(); - auto [dumped_sensors, dumped_timestamps] = g_test_db.dumpSensors(num_docs); - - EXPECT_EQ(dumped_sensors.size(), num_docs); - EXPECT_EQ(dumped_timestamps.size(), num_docs); - - for (size_t i = 0; i < num_docs; i++) { - EXPECT_EQ(dumped_timestamps[i], expected_msg_info[i].timestamp_); - - // gps - EXPECT_FLOAT_EQ(dumped_sensors[i].gps().latitude(), expected_sensors[i].gps().latitude()); - EXPECT_FLOAT_EQ(dumped_sensors[i].gps().longitude(), expected_sensors[i].gps().longitude()); - EXPECT_FLOAT_EQ(dumped_sensors[i].gps().speed(), expected_sensors[i].gps().speed()); - EXPECT_FLOAT_EQ(dumped_sensors[i].gps().heading(), expected_sensors[i].gps().heading()); - - // ais ships - for (int j = 0; j < NUM_AIS_SHIPS; j++) { - const Sensors::Ais & dumped_ais_ship = dumped_sensors[i].ais_ships(j); - const Sensors::Ais & expected_ais_ship = expected_sensors[i].ais_ships(j); - EXPECT_EQ(dumped_ais_ship.id(), expected_ais_ship.id()); - EXPECT_FLOAT_EQ(dumped_ais_ship.latitude(), expected_ais_ship.latitude()); - EXPECT_FLOAT_EQ(dumped_ais_ship.longitude(), expected_ais_ship.longitude()); - EXPECT_FLOAT_EQ(dumped_ais_ship.sog(), expected_ais_ship.sog()); - EXPECT_FLOAT_EQ(dumped_ais_ship.cog(), expected_ais_ship.cog()); - EXPECT_FLOAT_EQ(dumped_ais_ship.rot(), expected_ais_ship.rot()); - EXPECT_FLOAT_EQ(dumped_ais_ship.width(), expected_ais_ship.width()); - EXPECT_FLOAT_EQ(dumped_ais_ship.length(), expected_ais_ship.length()); - } - - // generic sensors - for (int j = 0; j < NUM_GENERIC_SENSORS; j++) { - const Sensors::Generic & dumped_data_sensor = dumped_sensors[i].data_sensors(j); - const Sensors::Generic & expected_data_sensor = expected_sensors[i].data_sensors(j); - EXPECT_EQ(dumped_data_sensor.id(), expected_data_sensor.id()); - EXPECT_EQ(dumped_data_sensor.data(), expected_data_sensor.data()); - } - - // batteries - for (int j = 0; j < NUM_BATTERIES; j++) { - const Sensors::Battery & dumped_battery = dumped_sensors[i].batteries(j); - const Sensors::Battery & expected_battery = expected_sensors[i].batteries(j); - EXPECT_EQ(dumped_battery.voltage(), expected_battery.voltage()); - EXPECT_EQ(dumped_battery.current(), expected_battery.current()); - } - - // wind sensors - for (int j = 0; j < NUM_WIND_SENSORS; j++) { - const Sensors::Wind & dumped_wind_sensor = dumped_sensors[i].wind_sensors(j); - const Sensors::Wind & expected_wind_sensor = expected_sensors[i].wind_sensors(j); - EXPECT_EQ(dumped_wind_sensor.speed(), expected_wind_sensor.speed()); - EXPECT_EQ(dumped_wind_sensor.direction(), expected_wind_sensor.direction()); - } - - // path waypoints - for (int j = 0; j < NUM_PATH_WAYPOINTS; j++) { - const Polaris::Waypoint & dumped_path_waypoint = dumped_sensors[i].local_path_data().waypoints(j); - const Polaris::Waypoint & expected_path_waypoint = expected_sensors[i].local_path_data().waypoints(j); - EXPECT_EQ(dumped_path_waypoint.latitude(), expected_path_waypoint.latitude()); - EXPECT_EQ(dumped_path_waypoint.longitude(), expected_path_waypoint.longitude()); - } - } -} - -/** - * @brief Check that MongoDB is running - */ -TEST_F(TestSailbotDB, TestConnection) -{ - ASSERT_TRUE(g_test_db.testConnection()) << "MongoDB not running - remember to connect!"; -} - -/** - * @brief Write random sensor data to the TestDB - read and verify said data - */ -TEST_F(TestSailbotDB, TestStoreSensors) -{ - SCOPED_TRACE("Seed: " + std::to_string(g_rand_seed)); // Print seed on any failure - auto [rand_sensors, rand_info] = genRandData(); - ASSERT_TRUE(g_test_db.storeNewSensors(rand_sensors, rand_info)); - - std::array expected_sensors = {rand_sensors}; - std::array expected_info = {rand_info}; +static const std::string test_db_name = "test"; +static std::random_device g_rd = std::random_device(); // random number sampler +static uint32_t g_rand_seed = g_rd(); // seed used for random number generation +static std::mt19937 g_mt(g_rand_seed); // initialize random number generator with seed +static UtilDB g_test_db(test_db_name, MONGODB_CONN_STR, std::make_shared(g_mt)); - verifyDBWrite(expected_sensors, expected_info); -} - -class TestHTTP : public TestSailbotDB +class TestRemoteTransceiver : public ::testing::Test { protected: static constexpr int NUM_THREADS = 4; @@ -516,7 +48,8 @@ class TestHTTP : public TestSailbotDB static void SetUpTestSuite() { std::make_shared( - TestHTTP::io_, tcp::endpoint{TestHTTP::addr_, TESTING_PORT}, std::move(TestHTTP::server_db_)) + TestRemoteTransceiver::io_, tcp::endpoint{TestRemoteTransceiver::addr_, TESTING_PORT}, + std::move(TestRemoteTransceiver::server_db_)) ->run(); for (std::thread & io_thread : io_threads_) { @@ -532,25 +65,22 @@ class TestHTTP : public TestSailbotDB } } - TestHTTP() - { - // Automatically calls TestSailbotDB's constructor to setup tests - } + TestRemoteTransceiver() { g_test_db.cleanDB(); } - ~TestHTTP() override {} + ~TestRemoteTransceiver() override {} }; // Initialize static objects -bio::io_context TestHTTP::io_{TestHTTP::NUM_THREADS}; -std::vector TestHTTP::io_threads_ = std::vector(NUM_THREADS); -SailbotDB TestHTTP::server_db_ = TestDB(); -bio::ip::address TestHTTP::addr_ = bio::ip::make_address(TESTING_HOST); +bio::io_context TestRemoteTransceiver::io_{TestRemoteTransceiver::NUM_THREADS}; +std::vector TestRemoteTransceiver::io_threads_ = std::vector(NUM_THREADS); +SailbotDB TestRemoteTransceiver::server_db_ = SailbotDB(test_db_name, MONGODB_CONN_STR); +bio::ip::address TestRemoteTransceiver::addr_ = bio::ip::make_address(TESTING_HOST); /** * @brief Test HTTP GET request sending and handling. Currently just retrieves a placeholder string. * */ -TEST_F(TestHTTP, TestGet) +TEST_F(TestRemoteTransceiver, TestGet) { auto [status, result] = http_client::get({TESTING_HOST, std::to_string(TESTING_PORT), remote_transceiver::targets::ROOT}); @@ -578,10 +108,10 @@ std::string createPostBody(remote_transceiver::MOMsgParams::Params params) * @brief Test that we can POST sensor data to the server * */ -TEST_F(TestHTTP, TestPostSensors) +TEST_F(TestRemoteTransceiver, TestPostSensors) { SCOPED_TRACE("Seed: " + std::to_string(g_rand_seed)); // Print seed on any failure - auto [rand_sensors, rand_info] = genRandData(); + auto [rand_sensors, rand_info] = g_test_db.genRandData(UtilDB::getTimestamp()); std::string rand_sensors_str; ASSERT_TRUE(rand_sensors.SerializeToString(&rand_sensors_str)); @@ -606,27 +136,31 @@ TEST_F(TestHTTP, TestPostSensors) std::array expected_sensors = {rand_sensors}; std::array expected_info = {rand_info}; - verifyDBWrite(expected_sensors, expected_info); + EXPECT_TRUE(g_test_db.verifyDBWrite(expected_sensors, expected_info)); } /** * @brief Test that the server can multiple POST requests at once * */ -TEST_F(TestHTTP, TestPostSensorsMult) +TEST_F(TestRemoteTransceiver, TestPostSensorsMult) { SCOPED_TRACE("Seed: " + std::to_string(g_rand_seed)); // Print seed on any failure - constexpr int NUM_REQS = 50; - std::array queries; - std::array req_threads; - std::array res_statuses; - std::array expected_sensors; + constexpr int NUM_REQS = 50; // Keep this number under 60 to simplify timestamp logic + std::array queries; + std::array req_threads; + std::array res_statuses; + std::array expected_sensors; std::array expected_info; + std::tm tm = UtilDB::getTimestamp(); // Prepare all queries for (int i = 0; i < NUM_REQS; i++) { - auto [rand_sensors, rand_info] = genRandData(); + // Timestamps are only granular to the second, so if we want to maintain document ordering by time + // without adding a lot of 1 second delays, then the time must be modified + tm.tm_sec = i; + auto [rand_sensors, rand_info] = g_test_db.genRandData(tm); expected_sensors[i] = rand_sensors; expected_info[i] = rand_info; std::string rand_sensors_str; @@ -663,5 +197,5 @@ TEST_F(TestHTTP, TestPostSensorsMult) std::this_thread::sleep_for(WAIT_AFTER_RES); // Check that DB is updated properly for all requests - verifyDBWrite(expected_sensors, expected_info); + EXPECT_TRUE(g_test_db.verifyDBWrite(expected_sensors, expected_info)); } diff --git a/scripts/README.md b/scripts/README.md index ed1aab5..5dc3c5a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -8,6 +8,19 @@ Given an input text file where each line is the name of a ROS topic, generates a C++ header file matching those names. +## Sailbot DB + +```shell +./sailbot_db [COMMAND] +./sailbot_db --help +``` + +Wrapper for the [SailbotDB Utility DB tool](../lib/sailbot_db/src/main.cpp). + +- Requires network_systems to be built +- Run with `--help` for full details on how to run +- Can clear, populate, and dump data from a DB + ## Run Virtual Iridium ```shell diff --git a/scripts/sailbot_db b/scripts/sailbot_db new file mode 100755 index 0000000..854efb9 --- /dev/null +++ b/scripts/sailbot_db @@ -0,0 +1,9 @@ +#!/bin/bash + +EXE=$ROS_WORKSPACE/build/network_systems/lib/sailbot_db/sailbot_db + +if [ -f $EXE ]; then + $EXE "$@" +else + echo "$EXE not found! Did you build network_systems?" +fi