From 2ca532122d60cff4dbdc7f24fbc5783bcc5ad68d Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Tue, 12 Apr 2016 20:06:47 +0200 Subject: Soil Moisture: Adding support for controlling lights. Bluetooth: refectorying, trying to be more c++ idiomatic and modern. SM/Diller: adding bluetooth to Diller bridge. --- apps/sm-diller.cpp | 423 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 apps/sm-diller.cpp (limited to 'apps/sm-diller.cpp') diff --git a/apps/sm-diller.cpp b/apps/sm-diller.cpp new file mode 100644 index 0000000..2b1d63d --- /dev/null +++ b/apps/sm-diller.cpp @@ -0,0 +1,423 @@ +#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); +} -- cgit v1.2.3