A private doorbell project to serve an Android app with webcam pictures and microphone recordings when someone presses the button. Information is being spread via MQTT. https://www.taibsu.de
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
heimdall/heimdall.cpp

244 lines
7.1 KiB

#include "heimdall.h"
#include <iomanip>
#include <chrono>
#include "threadLauncher.h"
using namespace std::chrono_literals;
// #define DEBUG_TAI
namespace tai {
heimdall::heimdall(uint8_t gpio_button, uint8_t gpio_opener, std::shared_ptr<tai::config> conf)
: _btn(std::make_unique<GPIO::PushButton>(gpio_button, GPIO::GPIO_PULL::UP))
, _opener(std::make_unique<GPIO::DigitalOut>(gpio_opener))
, _conf(conf)
{
LOG_INF("------------------------------");
LOG_INF("Hello from heimdall.");
if (!_btn) {
LOG_FTL("[HD] Unable to create button object or invalid GPIO #. Exiting.");
exit(-1);
}
registerMqttListener();
// button push function hook
const PictureType pictureType { _conf->options("pictureType") == "motion" ? PictureType::MOTION : PictureType::RASPISTILL };
#ifndef DEBUG_TAI
_btn->f_pushed = [&]() {
if (_btn->is_on()) {
#endif
startMqttAndPlaySound();
takePicture(
pictureType,
_conf->optionsVec("tg_chatIds"),
_conf->options("tg_notify") == "true"
);
#ifndef DEBUG_TAI
}
// we want to wait at least 100ms for the next trigger
LOG_DBG("[HD] Button pushed; sleeping 100ms.");
std::this_thread::sleep_for(100ms);
};
// TODO take care of this
#if 0
_startAudioStreamingServer();
#endif
#endif // DEBUG_TAI
_btn->start();
}
heimdall::~heimdall()
{
// stop event handling
_btn->stop();
LOG_DBG("[HD] Releasing button pointer.");
_btn.release();
/*
LOG_DBG("[HD] Releasing all running threads.");
threadLauncher::inst().closeAllThreads();
*/
LOG_INF("Bye-bye from heimdall.");
}
/**
* @brief Starts MQTT publishing and plays a sound.
*/
void heimdall::startMqttAndPlaySound()
{
if (_conf->options("openOnButtonPush") == "true") {
_opener->off(std::chrono::milliseconds(500));
}
// only used when tg_notify is set to "true" in .cfg file
std::string cmdNotifyOnTelegram {};
// sound playback shell command
std::string cmdPlaySound { "aplay " + _conf->options("soundFilePath") };
if (_conf->options("tg_notify") == "true") {
// Telegram notification
std::string message { _conf->options("tg_notifMessage") };
std::string telegramBotId { _conf->options("tg_botId") };
std::vector<std::string> telegramChatIds { _conf->optionsVec("tg_chatIds") };
for (const std::string& id : telegramChatIds) {
// notification command
cmdNotifyOnTelegram.append(_prepareTelegramMsg(id, message));
// irgendwie so
if (id != telegramChatIds.back()) {
cmdNotifyOnTelegram.append(" && ");
}
}
}
threadLauncher& launcher { threadLauncher::inst() };
#ifndef DEBUG_TAI
LOG_INF("[HD] Button was pushed!");
// execute mqtt publishing async
_mqttClient->send(_conf->options("mqttToken") + _conf->options("mqttMessage"));
LOG_DBG("[HD] Creating thread for sound playback: " + cmdPlaySound);
// execute sound playback async
launcher.go(cmdPlaySound);
#endif
if (_conf->options("tg_notify") == "true") {
LOG_DBG("[HD] Creating thread for Telegram notification: " + cmdNotifyOnTelegram);
launcher.go(cmdNotifyOnTelegram);
}
}
/**
* @brief Triggers a picture taking command using either raspistill or motion and saves it as {pictureName}.jpg
*/
void heimdall::takePicture(PictureType type, std::vector<std::string> const& recipients, bool notifyOnTelegram)
{
std::string pictureName { "door.jpg" };
std::string cmd {};
switch (type) {
case PictureType::RASPISTILL:
{
std::string quality { "80" };
std::string delay { "1s" };
std::string rotation { "270" };
std::string iso { "600" };
std::string width { "600" };
std::string height { "800" };
cmd = "raspistill -q " + quality +
" -t " + delay +
" --rotation " + rotation +
" -ISO " + iso +
" -w " + width +
" -h " + height +
" -o " + pictureName;
break;
}
case PictureType::MOTION:
{
cmd = "curl -o door.jpg " + _conf->options("motion_webcamUrl") + "?api_key=" + _conf->options("motion_apiKey");
}
default:
break;
}
if (notifyOnTelegram) {
/* We are appending the commands to the picture taking process
* since we are using threads but not futures so we don't know
* when the picture taking process is done.
*/
for (auto const& recipient : recipients) {
cmd.append(" && " + _prepareTelegramPhoto(recipient, "./" + pictureName));
}
}
threadLauncher::inst().go(cmd);
}
void heimdall::registerMqttListener()
{
LOG_INF("[HD] Registering MQTT listener for topic " + _conf->options("mqttTopic"));
// create our MQTT client and start subscription
_mqttClient = std::make_shared<mqttClient>(_conf);
_mqttClient->subscribeToKnownTopic();
}
void heimdall::_startAudioStreamingServer()
{
// our streaming server is called "RTSP Simple Server"
// -- see https://github.com/aler9/rtsp-simple-server for information.
std::string cmdStartAudioStream { _conf->options("rtspServerHome") + "rtsp-simple-server" };
/*
* @TODO
* I think it would be better to rework this whole threading with futures so we can
* check the status of the threads we are starting so we don't have to use weird
* and unreliable member variables as indicators.
*/
// start streaming server
if (!_isAudioStreamingServerRunning) {
LOG_DBG("[HD] Creating thread for " + cmdStartAudioStream);
threadLauncher::inst().go(cmdStartAudioStream);
_isAudioStreamingServerRunning = true;
}
}
auto heimdall::_prepareTelegramMsg(std::string const& chat_id, std::string const& message) -> std::string
{
return "curl -s --location --request GET 'https://api.telegram.org/" + _conf->options("tg_botId") + "/sendMessage?chat_id=" + chat_id + "&text=" + _urlEncode(message) + "'";
}
auto heimdall::_prepareTelegramPhoto(std::string const& chat_id, std::string const& photo_path) -> std::string
{
return "curl -s --location --request GET 'https://api.telegram.org/" + _conf->options("tg_botId") + "/sendPhoto?chat_id=" + chat_id + "' --form 'photo=@\"" + photo_path + "\"' > /dev/null";
}
auto heimdall::_urlEncode(std::string const& str) -> std::string
{
using namespace std;
ostringstream escaped;
escaped.fill('0');
escaped << hex;
for (string::const_iterator i = str.begin(), n = str.end(); i != n; ++i) {
string::value_type c = (*i);
// Keep alphanumeric and other accepted characters intact
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
escaped << c;
continue;
}
// Any other characters are percent-encoded
escaped << uppercase;
escaped << '%' << setw(2) << int((unsigned char)c);
escaped << nouppercase;
}
return escaped.str();
}
} // namespace tai