#include "ble/Bluetooth.h" #include "SoilMoisture.h" #include "diller_util.h" #include "trygvis/sensor.h" #include "trygvis/sensor/io.h" #include "apps.h" #include "mqtt_support.h" #include #include #include #include #include #include #include #include namespace trygvis { namespace apps { // I'm lazy using namespace std; using namespace std::chrono; using namespace trygvis::apps; using namespace trygvis::bluetooth; using namespace trygvis::sensor; using namespace trygvis::sensor::io; using namespace trygvis::mqtt_support; using namespace trygvis::diller; static struct { string server; string client_id; int sleep_time; } opts; struct device_command { virtual ~device_command() { } }; struct set_light final : public virtual device_command { set_light(uint8_t light, uint8_t value) : light(light), value(value) { } virtual ~set_light() { } const uint8_t light; const uint8_t value; }; struct diller_command { diller_command() { cerr << "diller_command" << endl; } virtual ~diller_command() { cerr << "~diller_command" << endl; } }; struct publish_soil_moisture_value final : public virtual diller_command { publish_soil_moisture_value(uint8_t sensor, int value) : sensor(sensor), value(value) { cerr << "publish_soil_moisture_value, this=" << (this) << endl; } virtual ~publish_soil_moisture_value() { cerr << "~publish_soil_moisture_value, this=" << (this) << endl; } const int sensor; const int value; }; struct publish_soil_moisture_sensor_name final : public virtual diller_command { publish_soil_moisture_sensor_name(uint8_t sensor, string name) : sensor(sensor), name(name) { } virtual ~publish_soil_moisture_sensor_name() { } const int sensor; const string name; }; atomic loop{true}; mutex main_mutex; condition_variable main_cv; std::list> to_device; std::list> to_diller; class diller_mqtt_client : public mqtt_client { public: diller_mqtt_client(const Mac mac, const string &host, const int port, const int keep_alive, const o &client_id, const bool clean_session) : mqtt_client(host, port, keep_alive, client_id, clean_session), mac(mac), light_path(diller_topic_for_property_value(mac.str(), "light")) { } void run() { connect(); while (loop) { poll(); std::unique_lock lock(main_mutex); auto res = main_cv.wait_for(lock, 1s); if (res == cv_status::no_timeout) { if (!to_diller.empty()) { diller_command &command = *to_diller.front(); if (typeid(command) == typeid(struct publish_soil_moisture_value &)) { auto cmd = dynamic_cast(command); LOG4CPLUS_DEBUG(logger, "Publishing soil moisture value: sensor=" << cmd.sensor << ", value=" << cmd.value); int mid; const string topic = diller_topic_for_property_value(mac.str(), "soil-moisture-" + std::to_string(cmd.sensor)); auto qos = 0; publish(&mid, topic, qos, false, std::to_string(cmd.value)); LOG4CPLUS_DEBUG(logger, "Published as " << mid); } else if (typeid(command) == typeid(struct publish_soil_moisture_sensor_name &)) { auto cmd = dynamic_cast(command); LOG4CPLUS_DEBUG(logger, "Publishing soil moisture sensor name: sensor=" << cmd.sensor << ", name=" << cmd.name); int mid; const string topic = diller_topic_for_property_name(mac.str(), "soil-moisture-" + std::to_string(cmd.sensor)); auto qos = 0; publish(&mid, topic, qos, true, cmd.name); LOG4CPLUS_DEBUG(logger, "Published as " << mid); } to_diller.pop_front(); } } } LOG4CPLUS_INFO(logger, "Stopping"); disconnect(); LOG4CPLUS_INFO(logger, "Stopped"); } protected: void on_connect(int rc) override { if (rc == MOSQ_ERR_SUCCESS) { LOG4CPLUS_INFO(logger, "Subscribing to " << light_path); subscribe(nullptr, light_path, 0); } } void on_message(const struct mosquitto_message *message) override { LOG4CPLUS_INFO(logger, "got message: " << message->topic); auto segments = mqtt_tokenize_topic(message->topic); // /diller/aa:bb:cc:dd:ee:ff/property/light/value if (segments.size() != 5 || segments[0] != "diller" || // $mac segments[2] != "property" || // $property segments[4] != "value") { LOG4CPLUS_INFO(logger, "Unknown message topic: " << message->topic); return; } if (message->payloadlen != 1) { LOG4CPLUS_WARN(logger, "Unknown message payload, expected exactly one byte, got " << message->payloadlen << " bytes on topic " << message->topic); return; } auto property_name = segments[3]; if (property_name == "light") { std::unique_lock lock(main_mutex); uint8_t value = static_cast(message->payload)[0]; const int light = 0; LOG4CPLUS_INFO(logger, "Pushing command set_light(light=" << light << ", value=" << static_cast(value) << ")"); to_device.push_back(make_unique(light, value)); main_cv.notify_all(); } else { LOG4CPLUS_INFO(logger, "Unknown property name " << property_name); } } private: const Mac mac; const string light_path; Logger logger = Logger::getInstance(LOG4CPLUS_TEXT("diller_mqtt_client")); }; void mqtt_thread(Mac &mac) { Logger logger = Logger::getInstance(LOG4CPLUS_TEXT("mqtt_thread")); try { diller_mqtt_client mqtt_client(mac, opts.server, 1883, 60, o(opts.client_id), true); mqtt_client.run(); } catch (std::runtime_error &ex) { LOG4CPLUS_WARN(logger, "std::exception: " << ex.what()); } catch (std::exception &ex) { LOG4CPLUS_WARN(logger, "std::exception: " << ex.what()); } } class sm_diller : public app { public: sm_diller() : app("sm-diller") { } ~sm_diller() = default; std::chrono::duration sleep_time; vector sensorIndexes; vector> sensors; void add_options(po::options_description_easy_init &options) override { auto opt_server = po::value(&opts.server)->required(); auto opt_client_id = po::value(&opts.client_id)->default_value("sm-diller-" + get_hostname()); auto default_sleep = po::value<>(&opts.sleep_time)->default_value(10); options("server", opt_server, "MQTT server"); options("client-id", opt_client_id, "MQTT client id"); options("device", po::value()->required(), "MAC of device to poll"); options("sensor", po::value<>(&sensorIndexes)->multitoken(), "Sensor to poll, defaults to all"); options("sleep", default_sleep, "How long to sleep in seconds between each poll. If not given, it will exit after first poll"); } int main(app_execution &execution) override { BluetoothSystem bluetoothSystem; auto desc = execution.desc; auto vm = execution.vm; std::thread mqtt_t; int ret; try { if (!vm.count("device")) { cerr << "Missing required option: device" << endl; cerr << desc << "\n"; return EXIT_FAILURE; } auto mac_string = vm["device"].as(); Mac mac = Mac::parseMac(mac_string); auto adapter = bluetoothSystem.getAdapter("0"); auto device = adapter->getDevice(mac); sleep_time = std::chrono::seconds(opts.sleep_time); mqtt_t = std::thread(mqtt_thread, std::ref(mac)); do { try { LOG4CPLUS_INFO(execution.logger, "Connecting to device: " << device->getMac().str()); auto gatt = device->connectGatt(); withConnection(execution.logger, gatt); } catch (BluetoothException &e) { LOG4CPLUS_ERROR(execution.logger, "Bluetooth error: " << e.what()); } catch (runtime_error &e) { LOG4CPLUS_ERROR(execution.logger, "Exception: " << e.what()); } LOG4CPLUS_DEBUG(execution.logger, "Sleeping for " << std::to_string(sleep_time.count())); this_thread::sleep_until(system_clock::now() + sleep_time); } while (loop); unique_lock lock(main_mutex); main_cv.notify_all(); ret = EXIT_SUCCESS; } catch (std::runtime_error &ex) { LOG4CPLUS_WARN(execution.logger, "std::runtime_error: " << ex.what()); ret = EXIT_FAILURE; } catch (std::exception &ex) { LOG4CPLUS_WARN(execution.logger, "std::exception: " << ex.what()); ret = EXIT_FAILURE; } if (mqtt_t.joinable()) { mqtt_t.join(); } return ret; } void read_sensors(SoilMoisture &soilMoisture, string mac) { auto epoch = system_clock::now().time_since_epoch(); auto timestamp = duration_cast(epoch).count(); auto tempO = soilMoisture.readTemperature(); if (tempO) { tempO.value(); } for (auto s : sensors) { auto sensor = s.first; auto name = s.second; uint16_t value = soilMoisture.getValue(sensor); { unique_lock lock(main_mutex); to_diller.push_back(make_unique(sensor, value)); } } main_cv.notify_all(); } void withConnection(const Logger &logger, shared_ptr gatt) { SoilMoisture soilMoisture = SoilMoisture::create(gatt); if (sensors.empty()) { const int sensorCount = soilMoisture.getSensorCount(); if (sensorCount == 0) { throw runtime_error("Sensor count is 0"); } // If the user didn't specify any sensors, add all. if (sensors.size() == 0) { for (uint8_t i = 0; i < sensorCount; i++) { sensorIndexes.push_back(i); } } for_each(begin(sensorIndexes), end(sensorIndexes), [&](uint8_t i) { if (i >= sensorCount) { // Ignore invalid sensors return; } auto name = soilMoisture.getName(i); sensors.push_back(make_pair(i, name)); unique_lock lock(main_mutex); to_diller.push_back(make_unique(i, name)); }); main_cv.notify_all(); } auto mac = gatt->getDevice().getMac().str(); if (!loop) { read_sensors(soilMoisture, mac); } else { std::chrono::time_point target_time; target_time = system_clock::now(); do { target_time = target_time + seconds(sleep_time); read_sensors(soilMoisture, mac); do { unique_lock lock(main_mutex); auto res = main_cv.wait_until(lock, target_time); if (res == cv_status::no_timeout) { LOG4CPLUS_DEBUG(logger, "no_timeout, to_device.size()=" << to_device.size()); } if (!to_device.empty()) { device_command &command = *std::move(to_device.front()); to_device.pop_front(); if (typeid(command) == typeid(set_light &)) { auto sl = dynamic_cast(command); LOG4CPLUS_DEBUG(logger, "performing Set light: light=" << static_cast(sl.light) << ", value=" << static_cast(sl.value)); try { soilMoisture.setLight(sl.light, sl.value); } catch (std::runtime_error &e) { LOG4CPLUS_WARN(logger, "Could not execute setLight, light=" << static_cast(sl.light) << ", value=" << static_cast(sl.value) << ", error: " << e.what()); } } } } while (target_time > system_clock::now()); } while (loop); } } }; } } int main(int argc, const char *argv[]) { using app_t = trygvis::apps::sm_diller; return real_main(new app_t(), argc, argv); }