From 729507d04006fa372f7d62d45c9d8cb6d5de9311 Mon Sep 17 00:00:00 2001 From: anders617 Date: Sat, 10 Oct 2020 16:48:59 -0400 Subject: [PATCH] Refactor and better logging --- .gitignore | 1 + Makefile | 2 +- src/BTScanner.cpp | 2 - src/BTScanner.h | 32 +++++++ src/GoveeData.h | 22 +++++ src/GoveeEventHandler.cpp | 96 +++++++++++++++++++++ src/GoveeEventHandler.h | 44 ++++++++++ src/Util.cpp | 3 +- src/Util.h | 5 ++ src/main.cpp | 172 ++++++++++++++------------------------ 10 files changed, 264 insertions(+), 115 deletions(-) create mode 100644 src/GoveeData.h create mode 100644 src/GoveeEventHandler.cpp create mode 100644 src/GoveeEventHandler.h diff --git a/.gitignore b/.gitignore index 0d0a51d..5c6f806 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # production /build +*.out # misc .vscode diff --git a/Makefile b/Makefile index ffd5cbb..06db813 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ CXX := -c++ -CXXFLAGS := -pedantic-errors -Wall -Wextra -Werror -Wno-psabi -lbluetooth -std=c++17 -laws-cpp-sdk-core -laws-cpp-sdk-kinesis -lpthread +CXXFLAGS := -pedantic-errors -Wall -Wextra -Werror -Wno-psabi -lbluetooth -std=c++2a -laws-cpp-sdk-core -laws-cpp-sdk-kinesis -lpthread LDFLAGS := -L/usr/lib -lstdc++ -lm BUILD := ./build OBJ_DIR := $(BUILD)/objects diff --git a/src/BTScanner.cpp b/src/BTScanner.cpp index 6fbfb30..ba823f4 100644 --- a/src/BTScanner.cpp +++ b/src/BTScanner.cpp @@ -2,8 +2,6 @@ #include -#include "Util.h" - const int ON = 1; const int OFF = 0; diff --git a/src/BTScanner.h b/src/BTScanner.h index 1090faa..16bbd55 100644 --- a/src/BTScanner.h +++ b/src/BTScanner.h @@ -15,6 +15,8 @@ #include #include +#include "Util.h" + class BTScanner { public: // Scan from device with addr "XX:XX:XX:XX:XX:XX" @@ -27,6 +29,8 @@ class BTScanner { bool scan(std::function handle_message, std::chrono::seconds::rep scan_duration); + template + bool scan(T &event_handler, std::chrono::seconds::rep scan_duration); void stop_scanning(); private: @@ -39,4 +43,32 @@ class BTScanner { struct hci_filter original_filter; }; +template +bool BTScanner::scan(T &event_handler, std::chrono::seconds::rep scan_duration) { + bool error = false; + scanning = true; + auto scanStartTime = std::chrono::steady_clock::now(); + // Enable scanning + CHECK(hci_le_set_scan_enable(device_handle, 0x01, 1, 1000), + "Failed to enable scan"); + while (scanning && !error) { + if (std::chrono::duration_cast( + std::chrono::steady_clock::now() - scanStartTime) + .count() > scan_duration) { + break; + } + auto [len, buf] = read_device(); + if (scanning && len != -1) { + evt_le_meta_event *meta = + (evt_le_meta_event *)(buf.get() + (1 + HCI_EVENT_HDR_SIZE)); + len -= (1 + HCI_EVENT_HDR_SIZE); + event_handler.parse(meta); + } + } + // Disable scanning + CHECK(hci_le_set_scan_enable(device_handle, 0x00, 1, 1000), + "Failed to disable scan"); + return false; +} + #endif diff --git a/src/GoveeData.h b/src/GoveeData.h new file mode 100644 index 0000000..0c3f447 --- /dev/null +++ b/src/GoveeData.h @@ -0,0 +1,22 @@ +#ifndef GOVEE_DATA_H +#define GOVEE_DATA_H + +#include + +struct GoveeData { + long long int timestamp; + std::string name; + float temp, humidity; + int battery; +}; + +std::string to_json(const GoveeData &data) { + std::stringstream s; + s << "{\"timestamp\":" << data.timestamp << ",\"temp\":" << data.temp + << ",\"humidity\":" << data.humidity << ",\"battery\":" << data.battery + << ",\"name\":\"" << data.name << "\"" + << "}"; + return s.str(); +} + +#endif diff --git a/src/GoveeEventHandler.cpp b/src/GoveeEventHandler.cpp new file mode 100644 index 0000000..e7018ca --- /dev/null +++ b/src/GoveeEventHandler.cpp @@ -0,0 +1,96 @@ +#include "GoveeEventHandler.h" + +#include "Util.h" + +int GoveeEventParser::add_name_handler(NameEventHandler name_handler) { + int id = next_handler_id++; + name_handlers[id] = name_handler; + return next_handler_id; +} + +int GoveeEventParser::add_data_handler(DataEventHandler data_handler) { + int id = next_handler_id++; + data_handlers[id] = data_handler; + return next_handler_id; +} + +void GoveeEventParser::remove_name_handler(int id) { + name_handlers.erase(id); +} + +void GoveeEventParser::remove_data_handler(int id) { + data_handlers.erase(id); +} + +std::optional> GoveeEventParser::read_data(const uint8_t *data, const size_t data_len) { + if (data_len != 9 && data_len != 10) return std::nullopt; + if ((data[1] == 0x88) && (data[2] == 0xEC)) { + float temp = -1, humidity = -1; + int battery = -1; + if (data_len == 9) { + // This data came from https://github.com/Thrilleratplay/GoveeWatcher + // 88ec00 03519e 6400 Temp: 21.7502°C Temp: 71.1504°F Humidity: 50.2% + // 1 2 3 4 5 6 7 8 + int temp_humidity = int(data[4]) << 16 | int(data[5]) << 8 | int(data[6]); + int humidity_component = temp_humidity % 1000; + int temp_component = temp_humidity - humidity_component; + temp = ((float(temp_component) / 10000.0) * 9.0 / 5.0) + 32.0; + humidity = float(humidity_component % 1000) / 10.0; + battery = int(data[7]); + } else if (data_len == 10) { + Log("Data length 10"); + // This data came from + // https://github.com/neilsheps/GoveeTemperatureAndHumidity 88ec00 dd07 + // 9113 64 02 1 2 3 4 5 6 7 8 9 + int iTemp = int(data[5]) << 8 | int(data[4]); + int iHumidity = int(data[7]) << 8 | int(data[6]); + temp = ((float(iTemp) / 100.0) * 9.0 / 5.0) + 32.0; + humidity = float(iHumidity) / 100.0; + battery = int(data[8]); + } + return std::make_tuple(temp, humidity, battery); + } + return std::nullopt; +} + +void GoveeEventParser::parse(evt_le_meta_event *meta) { + if (meta->subevent != EVT_LE_ADVERTISING_REPORT) + return; + le_advertising_info *info = (le_advertising_info *)(meta->data + 1); + if (!info->length) + return; + int current_offset = 0; + bool data_error = false; + while (!data_error && current_offset < info->length) { + size_t data_len = info->data[current_offset]; + if (data_len + 1 > info->length) { + Log("EIR data length is longer than EIR packet length. %d + 1 %d", + data_len, info->length); + data_error = true; + } else { + // Bluetooth Extended Inquiry Response + // I'm paying attention to only three types of EIR, Short Name, + // Complete Name, and Manufacturer Specific Data The names are how I + // learn which Bluetooth Addresses I'm going to listen to + char addr[19] = {0}; + ba2str(&info->bdaddr, addr); + std::string_view strAddr(addr); + if ((info->data + current_offset + 1)[0] == EIR_NAME_SHORT || + (info->data + current_offset + 1)[0] == EIR_NAME_COMPLETE) { + std::string name((char *)&((info->data + current_offset + 1)[1]), data_len-1); + for (const auto &[id, name_handler] : name_handlers) { + name_handler(strAddr, name); + } + } else if ((info->data + current_offset + 1)[0] == EIR_MANUFACTURE_SPECIFIC) { + if (auto new_data = + read_data((info->data + current_offset + 1), data_len)) { + const auto [temp, humidity, battery] = *new_data; + for (const auto &[id, data_handler] : data_handlers) { + data_handler(strAddr, temp, humidity, battery); + } + } + } + current_offset += data_len + 1; + } + } +} diff --git a/src/GoveeEventHandler.h b/src/GoveeEventHandler.h new file mode 100644 index 0000000..40f7bfc --- /dev/null +++ b/src/GoveeEventHandler.h @@ -0,0 +1,44 @@ +#ifndef GOVEE_EVENT_HANDLER_H +#define GOVEE_EVENT_HANDLER_H + +#include +#include +#include + +#include +#include + +/** + * Class for parsing bluetooth evt_le_meta_event messages. + * + * Name handlers are called when a message advertising the name for a device is parsed. + * + * Data handlers are called when a message containing temp/humidity/battery for a device is parsed. +*/ +class GoveeEventParser { + public: + // Associate `name` with the bluetooth `addr` + using NameEventHandler = std::function; + // New data from the given `addr` + using DataEventHandler = std::function; + + GoveeEventParser() : next_handler_id(1) {} + + // Returns the id of the handler which can be used when calling remove_name_handler + int add_name_handler(NameEventHandler name_handler); + // Returns the id of the handler which can be used when calling remove_name_handler + int add_data_handler(DataEventHandler data_handler); + + void remove_name_handler(int id); + void remove_data_handler(int id); + + void parse(evt_le_meta_event *meta); + private: + std::optional> read_data(const uint8_t *data, const size_t data_len); + + int next_handler_id; + std::unordered_map name_handlers; + std::unordered_map data_handlers; +}; + +#endif diff --git a/src/Util.cpp b/src/Util.cpp index 5106c70..e03f3d7 100644 --- a/src/Util.cpp +++ b/src/Util.cpp @@ -5,10 +5,11 @@ void Log(const char *format, ...) { printf("[%lld] ", - std::chrono::steady_clock::now().time_since_epoch().count()); + std::chrono::system_clock::now().time_since_epoch().count()); va_list arglist; va_start(arglist, format); vprintf(format, arglist); va_end(arglist); printf("\n"); + fflush(stdout); } diff --git a/src/Util.h b/src/Util.h index 3e8577b..f189476 100644 --- a/src/Util.h +++ b/src/Util.h @@ -5,6 +5,11 @@ #include #include +constexpr uint8_t EIR_FLAGS = 0X01; +constexpr uint8_t EIR_NAME_SHORT = 0x08; +constexpr uint8_t EIR_NAME_COMPLETE = 0x09; +constexpr uint8_t EIR_MANUFACTURE_SPECIFIC = 0xFF; + void Log(const char *format, ...); struct Defer { diff --git a/src/main.cpp b/src/main.cpp index c094f28..43bdc26 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -16,39 +17,29 @@ #include "BTScanner.h" #include "Util.h" +#include "GoveeEventHandler.h" +#include "GoveeData.h" -#define EIR_FLAGS 0X01 -#define EIR_NAME_SHORT 0x08 -#define EIR_NAME_COMPLETE 0x09 -#define EIR_MANUFACTURE_SPECIFIC 0xFF - -struct GoveeData { - long long int timestamp; - std::string name; - float temp, humidity; - int battery; -}; - -std::string to_json(const GoveeData &data) { - std::stringstream s; - s << "{\"timestamp\":" << data.timestamp << ",\"temp\":" << data.temp - << ",\"humidity\":" << data.humidity << ",\"battery\":" << data.battery - << ",\"name\":\"" << data.name << "\"" - << "}"; - return s.str(); -} +// How often data is retrieved from the sensors +const std::chrono::seconds::rep UPDATE_PERIOD = 60; +// How long to scan for devices during each update const std::chrono::seconds::rep SCAN_DURATION = 10; + +// bt addr -> device name +std::unordered_map address_to_name; +// bt addr -> latest data +std::unordered_map govee_data; + +// Bluetooth scanner BTScanner scanner; -std::map addressToName; -std::map govee_data; std::atomic running = true; -std::vector awsThreads; +// Uploads data in json format to AWS Kinesis void put_temperatures( std::shared_ptr kinesis_client) { - for (auto pair : govee_data) { - std::string json = to_json(pair.second); + for (const auto &[addr, data] : govee_data) { + std::string json = to_json(data); auto result = kinesis_client->PutRecord( Aws::Kinesis::Model::PutRecordRequest() .WithStreamName(Aws::String("govee-data")) @@ -56,102 +47,52 @@ void put_temperatures( reinterpret_cast(json.data()), json.length())) .WithPartitionKey("testing")); if (!result.IsSuccess()) { - Log("Put Failed"); + Log("[%s] Put Failed", addr.c_str()); Log("%d", result.GetError().GetErrorType()); Log(result.GetError().GetMessage().c_str()); } else { - Log("Put success for %s", addressToName[pair.first].c_str()); + Log("[%s] Put success for %s", addr.c_str(), data.name.c_str()); } } } -bool is_govee_name(const std::string &name) { +// Used to make sure we only store names of govee devices +bool is_govee_name(std::string_view name) { return name.compare(0, 7, "GVH5075") == 0 || name.compare(0, 11, "Govee_H5074") == 0; } -std::optional read_msg(const uint8_t *data, const size_t data_len, - const le_advertising_info *info) { - (void)info; - if ((data[1] == 0x88) && (data[2] == 0xEC)) { - float temp = -1, humidity = -1; - int battery = -1; - if (data_len == 9) { - // This data came from https://github.com/Thrilleratplay/GoveeWatcher - // 88ec00 03519e 6400 Temp: 21.7502°C Temp: 71.1504°F Humidity: 50.2% - // 1 2 3 4 5 6 7 8 - int iTemp = int(data[4]) << 16 | int(data[5]) << 8 | int(data[6]); - temp = ((float(iTemp) / 10000.0) * 9.0 / 5.0) + 32.0; - humidity = float(iTemp % 1000) / 10.0; - battery = int(data[7]); - } else if (data_len == 10) { - // This data came from - // https://github.com/neilsheps/GoveeTemperatureAndHumidity 88ec00 dd07 - // 9113 64 02 1 2 3 4 5 6 7 8 9 - int iTemp = int(data[5]) << 8 | int(data[4]); - int iHumidity = int(data[7]) << 8 | int(data[6]); - temp = ((float(iTemp) / 100.0) * 9.0 / 5.0) + 32.0; - humidity = float(iHumidity) / 100.0; - battery = int(data[8]); - } - Log("temp: %f humidity: %f battery: %d", temp, humidity, battery); - - char addr[19] = {0}; - ba2str(&info->bdaddr, addr); - std::string strAddr(addr); - GoveeData data = { - std::chrono::system_clock::now().time_since_epoch().count(), - addressToName[strAddr], temp, humidity, battery}; - return data; + +// Event Handlers + +void store_name(std::string_view addr, std::string_view name) { + std::string key(addr); + if (is_govee_name(name)) { + address_to_name[key] = name; } - return std::nullopt; } -void process_event(evt_le_meta_event *meta) { - if (meta->subevent != EVT_LE_ADVERTISING_REPORT) - return; - le_advertising_info *info = (le_advertising_info *)(meta->data + 1); - if (!info->length) - return; - int current_offset = 0; - bool data_error = false; - while (!data_error && current_offset < info->length) { - size_t data_len = info->data[current_offset]; - if (data_len + 1 > info->length) { - Log("EIR data length is longer than EIR packet length. %d + 1 %d", - data_len, info->length); - data_error = true; - } else { - // Bluetooth Extended Inquiry Response - // I'm paying attention to only three types of EIR, Short Name, - // Complete Name, and Manufacturer Specific Data The names are how I - // learn which Bluetooth Addresses I'm going to listen to - char addr[19] = {0}; - ba2str(&info->bdaddr, addr); - std::string strAddr(addr); - bool is_govee_device = - (addressToName.end() != addressToName.find(strAddr)); - if ((info->data + current_offset + 1)[0] == EIR_NAME_SHORT || - (info->data + current_offset + 1)[0] == EIR_NAME_COMPLETE) { - std::string name((char *)&((info->data + current_offset + 1)[1]), - data_len - 1); - if (is_govee_name(name)) { - addressToName[strAddr] = name; - } - Log("[%s] Name: %s", addr, name.c_str()); - } else if (is_govee_device) { - if ((info->data + current_offset + 1)[0] == EIR_MANUFACTURE_SPECIFIC) { - if (auto new_data = - read_msg((info->data + current_offset + 1), data_len, info)) { - govee_data[strAddr] = *new_data; - } - } - } - current_offset += data_len + 1; - } +void log_name(std::string_view addr, std::string_view name) { + Log("[%s] Name=%s", addr.data(), name.data()); +} + +void store_data(std::string_view addr, float temp, float humidity, int battery) { + std::string key(addr); + if (address_to_name.count(key)) { + govee_data[key] = { + std::chrono::system_clock::now().time_since_epoch().count(), + address_to_name[key], + temp, + humidity, + battery + }; } } +void log_data(std::string_view addr, float temp, float humidity, int battery) { + Log("[%s] Data={temp: %f, humidity: %f, battery: %d}", addr.data(), temp, humidity, battery); +} + // This ensures we shutdown properly by disabling bluetooth scan etc. before // exiting void signal_handler(int signal) { @@ -163,23 +104,32 @@ void signal_handler(int signal) { int main() { std::signal(SIGINT, signal_handler); + // Setup AWS Aws::SDKOptions options; options.loggingOptions.logLevel = Aws::Utils::Logging::LogLevel::Info; Aws::InitAPI(options); Defer shutdownAws([=] { Aws::ShutdownAPI(options); }); auto kinesis_client = Aws::MakeShared("test"); + std::vector awsThreads; + + // Govee event parser + GoveeEventParser govee_event_parser; + + // Add event handlers to log/store info + govee_event_parser.add_name_handler(store_name); + govee_event_parser.add_name_handler(log_name); + govee_event_parser.add_data_handler(store_data); + govee_event_parser.add_data_handler(log_data); + while (running) { // Scan for temperatures - scanner.scan( - [](evt_le_meta_event *event) { - process_event(event); - return false; - }, - SCAN_DURATION); + scanner.scan(govee_event_parser, SCAN_DURATION); // Upload temperatures awsThreads.emplace_back(put_temperatures, kinesis_client); - for (int i = 0; i < 60 - SCAN_DURATION; i++) { + + // Wait until next update + for (int i = 0; i < UPDATE_PERIOD - SCAN_DURATION; i++) { if (!running) break; sleep(1);