diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/BodyTemplateDialog.qml | 164 | ||||
-rw-r--r-- | app/CMakeLists.txt | 66 | ||||
-rw-r--r-- | app/Main.qml | 121 | ||||
-rw-r--r-- | app/WeatherTemplateDialog.qml | 239 | ||||
-rw-r--r-- | app/afbclient.cpp | 245 | ||||
-rw-r--r-- | app/afbclient.h | 67 | ||||
-rw-r--r-- | app/alexa-viewer.qrc | 7 | ||||
-rw-r--r-- | app/main.cpp | 207 |
8 files changed, 1116 insertions, 0 deletions
diff --git a/app/BodyTemplateDialog.qml b/app/BodyTemplateDialog.qml new file mode 100644 index 0000000..4da85d9 --- /dev/null +++ b/app/BodyTemplateDialog.qml @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2020 Konsulko Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.0 + +Rectangle { + id: body_main + width: 1000 + height: 1000 + radius: 2 + + color: "black" + + ColumnLayout { + id: body_col + anchors.fill: parent.fill + + Label { + id: body_title + Layout.fillWidth: true + Layout.fillHeight: false + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + + text: bodyTemplate.title + color: "white" + font.pixelSize: 32 + font.bold: true + maximumLineCount: 1 + wrapMode: Text.Wrap + elide: Text.ElideRight + horizontalAlignment: Label.AlignLeft + verticalAlignment: Label.AlignVCenter + } + + Label { + id: body_subtitle + Layout.fillWidth: true + Layout.fillHeight: false + Layout.topMargin: 0 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + + text: bodyTemplate.subtitle + visible: bodyTemplate.subtitle != "" + color: "white" + font.pixelSize: 22 + font.bold: false + maximumLineCount: 1 + wrapMode: Text.Wrap + elide: Text.ElideRight + horizontalAlignment: Label.AlignLeft + verticalAlignment: Label.AlignVCenter + } + + RowLayout { + id: body_row + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + spacing: 20 + + Text { + id: body_textContent + Layout.fillWidth: true + Layout.fillHeight: true + + text: bodyTemplate.textContent + color: "white" + font.pixelSize: 32 + font.bold: false + wrapMode: Text.Wrap + verticalAlignment: Text.AlignTop + maximumLineCount: 21 + + states: [ + State { + name: "BodyTemplate2" + when: bodyTemplate.imageContentSource != "" + PropertyChanges { + target: body_textContent + Layout.maximumWidth: (body_main.width - 3 * parent.spacing) / 2 + Layout.preferredWidth: (body_main.width - 3 * parent.spacing) / 2 + } + }, + State { + name: "BodyTemplate1" + when: bodyTemplate.imageContentSource == "" + PropertyChanges { + target: body_textContent + Layout.maximumWidth: body_main.width - 2 * parent.spacing + Layout.preferredWidth: body_main.width - 2 * parent.spacing + } + } + ] + } + + Image { + id: body_imageContent + Layout.fillWidth: true + Layout.fillHeight: false + Layout.maximumWidth: (body_main.width - 3 * parent.spacing) / 2 + Layout.preferredWidth: (body_main.width - 3 * parent.spacing) / 2 + Layout.alignment: Qt.AlignTop + + source: bodyTemplate.imageContentSource + visible: bodyTemplate.imageContentSource != "" + fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignTop + } + } + } + + Button { + id: body_close + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 20 + + text: "Close" + + onReleased: { + body_close.highlighted = false + clear() + hide() + } + onPressed: { + body_close.highlighted = true + } + onCanceled: { + body_close.highlighted = false + } + } + + // Functions + + function clear() { + bodyTemplate.visible = false + + bodyTemplate.title = "" + bodyTemplate.subtitle = "" + bodyTemplate.textContent = "" + bodyTemplate.imageContentSource = "" + } +} diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..836d473 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,66 @@ +########################################################################### +# Copyright 2018-2020 Konsulko Group +# +# Author: Scott Murray <scott.murray@konsulko.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################### + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(OE_QMAKE_PATH_EXTERNAL_HOST_BINS $ENV{OE_QMAKE_PATH_HOST_BINS}) + +find_package(Qt5 COMPONENTS Core Gui QuickControls2 QuickWidgets REQUIRED) +find_package(PkgConfig REQUIRED) + +qt5_add_resources(RESOURCES alexa-viewer.qrc) + +PROJECT_TARGET_ADD(alexa-viewer) + +add_executable(${TARGET_NAME} + main.cpp + afbclient.cpp + ${RESOURCES} +) + +pkg_check_modules(QLIBWINDOWMGR REQUIRED qlibwindowmanager) +pkg_check_modules(QLIBHOMESCREEN REQUIRED qlibhomescreen) +pkg_check_modules(QTAPPFW REQUIRED qtappfw) +pkg_check_modules(LIBAFBWSC REQUIRED libafbwsc) + +include_directories( + ${QTAPPFW_INCLUDE_DIRS} + ${LIBAFBWSC_INCLUDE_DIRS} +) + +set_target_properties(${TARGET_NAME} PROPERTIES + LABELS "EXECUTABLE" + PREFIX "" + COMPILE_FLAGS "${QLIBWINDOWMGR_FLAGS} ${QLIBHOMESCREEN} ${QTAPPFW_FLAGS} ${EXTRAS_CFLAGS} -DFOR_AFB_BINDING" + LINK_FLAGS "${BINDINGS_LINK_FLAG}" + LINK_LIBRARIES "${EXTRAS_LIBRARIES}" + OUTPUT_NAME "${TARGET_NAME}" +) + +target_link_libraries(${TARGET_NAME} + Qt5::QuickControls2 + Qt5::QuickWidgets + ${QLIBWINDOWMGR_LIBRARIES} + ${QLIBHOMESCREEN_LIBRARIES} + ${QTAPPFW_LIBRARIES} + ${LIBAFBWSC_LIBRARIES} + -lpthread +) diff --git a/app/Main.qml b/app/Main.qml new file mode 100644 index 0000000..7060b9b --- /dev/null +++ b/app/Main.qml @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2020 Konsulko Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import QtQuick 2.2 +import QtQuick.Window 2.1 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Extras 1.4 + +Window { + id: root + width: 1080 + height: 1488 + color: '#00000000' + + visible: true + flags: Qt.FramelessWindowHint + + BodyTemplateDialog { + id: bodyTemplate + anchors.centerIn: parent + visible: true + + property string title: "" + property string subtitle: "" + + property string textContent: "" + property url imageContentSource: "" + } + + WeatherTemplateDialog { + id: weatherTemplate + anchors.centerIn: parent + visible: false + + property string title: "" + property string subtitle: "" + + property string currentTemperature: "" + property url currentWeatherIconSource: "" + property string lowTemperature: "" + property url lowTemperatureArrowSource: "" + property string highTemperature: "" + property url highTemperatureArrowSource: "" + } + + Connections { + target: GuiMetadata + + onRenderTemplate: { + console.log("Received renderTemplate, type = " + GuiMetadata.type) + if(GuiMetadata.type == "BodyTemplate1" || GuiMetadata.type == "BodyTemplate2") { + // Normally setting the target to visible would be after changes to the + // content, but I was seeing better behavior during testing by doing it + // here. Further investigation is required, and likely hooking up + // loading indication on the Images in the template dialogs. + weatherTemplate.visible = false + bodyTemplate.visible = true + + bodyTemplate.title = GuiMetadata.title + bodyTemplate.subtitle = GuiMetadata.subtitle + bodyTemplate.textContent = GuiMetadata.bodyText + + if(GuiMetadata.type == "BodyTemplate1") { + bodyTemplate.imageContentSource = "" + } else { + bodyTemplate.imageContentSource = GuiMetadata.bodyImageSmallUrl + } + } else if(GuiMetadata.type == "WeatherTemplate") { + bodyTemplate.visible = false + weatherTemplate.visible = true + + weatherTemplate.title = GuiMetadata.title + weatherTemplate.subtitle = GuiMetadata.subtitle + + weatherTemplate.currentTemperature = GuiMetadata.weatherCurrentTemperature + weatherTemplate.currentWeatherIconSource = GuiMetadata.weatherCurrentWeatherIconMediumDarkBgUrl + weatherTemplate.lowTemperature = GuiMetadata.weatherLowTemperature + weatherTemplate.lowTemperatureArrowSource = GuiMetadata.weatherLowTemperatureArrowMediumDarkBgUrl + weatherTemplate.highTemperature = GuiMetadata.weatherHighTemperature + weatherTemplate.highTemperatureArrowSource = GuiMetadata.weatherHighTemperatureArrowMediumDarkBgUrl + } else { + // Should not happen, but just in case + bodyTemplate.title = "Unsupported Template" + bodyTemplate.subtitle = "" + bodyTemplate.textContent = "The display template for this response is currently unsupported." + bodyTemplate.imageContentSource = "" + + weatherTemplate.visible = false + bodyTemplate.visible = false + } + } + + onClearTemplate: { + console.log("Received clearTemplate!") + bodyTemplate.clear() + weatherTemplate.clear() + hide() + } + } + + // Functions + + function hide() { + console.log("hiding window!") + homescreen.hideWindow("alexa-viewer") + } +} diff --git a/app/WeatherTemplateDialog.qml b/app/WeatherTemplateDialog.qml new file mode 100644 index 0000000..6205695 --- /dev/null +++ b/app/WeatherTemplateDialog.qml @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2020 Konsulko Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.0 + +Rectangle { + id: weather_main + width: 1000 + height: 1000 + radius: 2 + + color: "black" + + ColumnLayout { + id: weather_col + anchors.fill: parent.fill + + Label { + id: weather_title + Layout.fillWidth: true + Layout.fillHeight: false + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + + text: weatherTemplate.title + color: "white" + font.pixelSize: 48 + font.bold: true + maximumLineCount: 1 + wrapMode: Text.Wrap + elide: Text.ElideRight + horizontalAlignment: Label.AlignLeft + verticalAlignment: Label.AlignVCenter + } + + Label { + id: weather_subtitle + Layout.fillWidth: true + Layout.fillHeight: false + Layout.topMargin: 0 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + + text: weatherTemplate.subtitle + visible: weatherTemplate.subtitle != "" + color: "white" + font.pixelSize: 32 + font.bold: false + maximumLineCount: 1 + wrapMode: Text.Wrap + elide: Text.ElideRight + horizontalAlignment: Label.AlignLeft + verticalAlignment: Label.AlignVCenter + } + + RowLayout { + id: weather_row + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.topMargin: 200 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + spacing: 0 + + Image { + id: weather_currentWeatherIcon_imageContent + Layout.fillWidth: true + Layout.fillHeight: true + Layout.maximumWidth: (weather_main.width - 3 * parent.spacing) / 3 + Layout.preferredWidth: (weather_main.width - 3 * parent.spacing) / 3 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + source: weatherTemplate.currentWeatherIconSource + fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter + } + + Text { + id: weather_currentWeather_textContent + Layout.fillWidth: true + Layout.fillHeight: false + Layout.maximumWidth: (weather_main.width - 3 * parent.spacing) / 3 + Layout.preferredWidth: (weather_main.width - 3 * parent.spacing) / 3 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + text: weatherTemplate.currentTemperature + color: "white" + font.pixelSize: 150 + font.bold: false + wrapMode: Text.Wrap + verticalAlignment: Text.AlignTop + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 1 + } + + ColumnLayout { + id: weather_low_high_col + Layout.fillWidth: true + Layout.fillHeight: false + Layout.maximumWidth: (weather_main.width - 3 * parent.spacing) / 3 + Layout.preferredWidth: (weather_main.width - 3 * parent.spacing) / 3 + + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + RowLayout { + id: weather_high_row + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + spacing: 20 + + Image { + id: weather_highTempArrow_imageContent + Layout.fillWidth: false + Layout.fillHeight: false + Layout.maximumWidth: (weather_main.width - 3 * parent.spacing) / 6 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + source: weatherTemplate.highTemperatureArrowSource + fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter + } + + Text { + id: weather_highTemp_textContent + Layout.fillWidth: false + Layout.fillHeight: false + Layout.maximumWidth: (weather_main.width - 3 * parent.spacing) / 6 + Layout.preferredWidth: (weather_main.width - 3 * parent.spacing) / 6 + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + text: weatherTemplate.highTemperature + color: "white" + font.pixelSize: 60 + font.bold: false + wrapMode: Text.Wrap + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + maximumLineCount: 1 + } + } + + RowLayout { + id: weather_low_row + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + spacing: 20 + + Image { + id: weather_lowTempArrow_imageContent + Layout.fillWidth: false + Layout.fillHeight: false + Layout.maximumWidth: (weather_main.width - 3 * parent.spacing) / 6 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + source: weatherTemplate.lowTemperatureArrowSource + fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter + } + + Text { + id: weather_lowTemp_textContent + Layout.fillWidth: false + Layout.fillHeight: false + Layout.maximumWidth: (weather_main.width - 3 * parent.spacing) / 6 + Layout.preferredWidth: (weather_main.width - 3 * parent.spacing) / 6 + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + text: weatherTemplate.lowTemperature + color: "white" + font.pixelSize: 60 + font.bold: false + wrapMode: Text.Wrap + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + maximumLineCount: 1 + } + } + } + } + } + + Button { + id: weather_close + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 20 + + text: "Close" + + onReleased: { + weather_close.highlighted = false + clear() + hide() + } + onPressed: { + weather_close.highlighted = true + } + onCanceled: { + weather_close.highlighted = false + } + } + + // Functions + + function clear() { + weatherTemplate.visible = false + + weatherTemplate.title = "" + weatherTemplate.subtitle = "" + weatherTemplate.currentTemperature = "" + weatherTemplate.currentWeatherIconSource = "" + weatherTemplate.lowTemperature = "" + weatherTemplate.lowTemperatureArrowSource = "" + weatherTemplate.highTemperature = "" + weatherTemplate.highTemperatureArrowSource = "" + } +} diff --git a/app/afbclient.cpp b/app/afbclient.cpp new file mode 100644 index 0000000..4b136cb --- /dev/null +++ b/app/afbclient.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2019,2020 Konsulko Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "afbclient.h" +#include <string> +#include <cstring> +#include <iostream> +#include <mutex> +#include <condition_variable> + +#undef DEBUG +//#define DEBUG + +struct call_data +{ + bool sync; + std::mutex mutex; + std::condition_variable cv; + bool ready; + std::function<void(json_object*)> cb; + json_object *resp; +}; + +static void on_hangup_cb(void *closure, struct afb_wsj1 *wsj) +{ +} + +static void on_call_cb(void *closure, const char *api, const char *verb, struct afb_wsj1_msg *msg) +{ +} + +static void on_reply_cb(void *closure, struct afb_wsj1_msg *msg) +{ + call_data *data = (call_data*) closure; + struct json_object* reply; + + if(!data) + goto reply_done; + + reply = afb_wsj1_msg_object_j(msg); + if(reply) { +#ifdef DEBUG + std::cerr << __FUNCTION__ << ": reply = " << \ + json_object_to_json_string_ext(reply, JSON_C_TO_STRING_SPACED | JSON_C_TO_STRING_PRETTY) << \ + std::endl; +#endif + if(data->sync) { + data->resp = reply; + + // Increase reference count since we are going to use + // reply after this callback returns, caller must do a + // put. + json_object_get(reply); + } else if(data->cb != nullptr) { + data->cb(reply); + } + } +reply_done: + if(data->sync) { + // Signal reply is done + { + std::lock_guard<std::mutex> lk(data->mutex); + data->ready = true; + } + data->cv.notify_one(); + } +} + +// +// on_event_cb is inline in afbclient.h +// + +static void *afb_loop_thread(struct sd_event* loop) +{ + for(;;) + sd_event_run(loop, 30000000); +} + +AfbClient::AfbClient(const int port, const std::string &token) +{ + std::string uri; + + if(sd_event_new(&m_afb_loop) < 0) { + std::cerr << __FUNCTION__ << ": Failed to create event loop" << std::endl; + return; + } + + // Initialize interface for websocket + m_itf.on_hangup = on_hangup_cb; + m_itf.on_call = on_call_cb; + m_itf.on_event = on_event_cb; + + uri = "ws://localhost:" + std::to_string(port) + "/api?token=" + token; +#ifdef DEBUG + std::cerr << "Using URI: " << uri << std::endl; +#endif + m_ws = afb_ws_client_connect_wsj1(m_afb_loop, uri.c_str(), &m_itf, this); + if(m_ws) { + m_afb_thread = std::thread(afb_loop_thread, m_afb_loop); + } else { + std::cerr << __FUNCTION__ << ": Failed to create websocket connection" << std::endl; + goto error; + } + + m_valid = true; + return; +error: + if(m_afb_loop) { + sd_event_unref(m_afb_loop); + m_afb_loop = nullptr; + } + return; +} + +AfbClient::~AfbClient(void) +{ + sd_event_unref(m_afb_loop); + afb_wsj1_unref(m_ws); +} + +int AfbClient::call(const std::string &api, const std::string &verb, struct json_object *arg, reply_callback_fn cb) +{ + if(!m_valid) + return -1; + + call_data data; + data.sync = false; + data.cb = cb; + int rc = afb_wsj1_call_j(m_ws, api.c_str(), verb.c_str(), arg, on_reply_cb, (void*) &data); + if(rc < 0) { + std::cerr << __FUNCTION__ << \ + ": Failed to call " << \ + api.c_str() << \ + "/" << \ + verb.c_str() << \ + std::endl; + } + return rc; +} + +int AfbClient::call_sync(const std::string &api, const std::string &verb, struct json_object *arg, struct json_object **resp) +{ + if(!m_valid) + return -1; + + call_data data; + data.sync = true; + data.ready = false; + data.cb = nullptr; + int rc = afb_wsj1_call_j(m_ws, api.c_str(), verb.c_str(), arg, on_reply_cb, (void*) &data); + if(rc >= 0) { + // Wait for response + std::unique_lock<std::mutex> lk(data.mutex); + data.cv.wait(lk, [&]{ return data.ready; }); + + if(resp) + *resp = data.resp; + else + json_object_put(data.resp); + } else { + std::cerr << __FUNCTION__ << \ + ": Failed to call " << \ + api.c_str() << \ + "/" << \ + verb.c_str() << \ + std::endl; + } + return rc; +} + +// subscribe call for simple forms like { "event": "foo" } or { "signal": "foo" } +int AfbClient::subscribe(const std::string &api, const std::string &event, const std::string &eventValueString) +{ + if(!m_valid) + return -1; + + struct json_object *j_obj = json_object_new_object(); + json_object_object_add(j_obj, eventValueString.c_str(), json_object_new_string(event.c_str())); + return call_sync(api, std::string("subscribe"), j_obj); +} + +// subscribe call allowing passing in complete json_object for argument, for subscribing with e.g. arrays +int AfbClient::subscribe(const std::string &api, json_object *j_obj, const std::string &subscribeValueString) +{ + if(!m_valid || j_obj == nullptr) + return -1; + + return call_sync(api, subscribeValueString, j_obj); +} + +// unsubscribe call for simple forms like { "event": "foo" } or { "signal": "foo" } +int AfbClient::unsubscribe(const std::string &api, const std::string &event, const std::string &eventValueString) +{ + if(!m_valid) + return -1; + + struct json_object *j_obj = json_object_new_object(); + json_object_object_add(j_obj, eventValueString.c_str(), json_object_new_string(event.c_str())); + return call_sync(api, std::string("unsubscribe"), j_obj); +} + +// unsubscribe call allowing passing in complete json_object for argument, for unsubscribing with e.g. arrays +int AfbClient::unsubscribe(const std::string &api, json_object *j_obj, const std::string &unsubscribeValueString) +{ + if(!m_valid || j_obj == nullptr) + return -1; + + return call_sync(api, unsubscribeValueString, j_obj); +} + +void AfbClient::set_event_callback(event_callback_fn cb, void *closure) +{ + m_event_cb = cb; + m_event_cb_closure = closure; +} + +void AfbClient::on_event(const char* event, struct afb_wsj1_msg *msg) +{ + if(m_event_cb == nullptr) + return; + + struct json_object *contents = afb_wsj1_msg_object_j(msg); +#ifdef DEBUG + std::cerr << __FUNCTION__ << ": contents = " << \ + json_object_to_json_string_ext(contents, JSON_C_TO_STRING_SPACED | JSON_C_TO_STRING_PRETTY) << \ + std::endl; +#endif + struct json_object *data; + if(json_object_object_get_ex(contents, "data", &data)) { + m_event_cb(event, data, m_event_cb_closure); + } +} diff --git a/app/afbclient.h b/app/afbclient.h new file mode 100644 index 0000000..596a43f --- /dev/null +++ b/app/afbclient.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019,2020 Konsulko Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AFBCLIENT_H +#define AFBCLIENT_H + +#include <string> +#include <thread> +#include <map> +#include <functional> +#include <systemd/sd-event.h> +#include <json-c/json.h> + +extern "C" +{ +#include <afb/afb-wsj1.h> +#include <afb/afb-ws-client.h> +} + +class AfbClient +{ +public: + AfbClient(int port, const std::string &token); + ~AfbClient(); + + using reply_callback_fn = std::function<void(json_object*)>; + using event_callback_fn = std::function<void(const char*, json_object*, void*)>; + + int call(const std::string &api, const std::string &verb, struct json_object* arg, reply_callback_fn cb = nullptr); + int call_sync(const std::string &api, const std::string &verb, struct json_object* arg, struct json_object **resp = NULL); + int subscribe(const std::string &api, const std::string &event, const std::string &eventValueString = "event"); + int subscribe(const std::string &api, json_object *j_obj, const std::string &subscribeValueString = "subscribe"); + int unsubscribe(const std::string &api, const std::string &eventString, const std::string &eventValueString = "event"); + int unsubscribe(const std::string &api, json_object *j_obj, const std::string &unsubscribeValueString = "unsubscribe"); + void set_event_callback(event_callback_fn cb, void *closure); + + static void on_event_cb(void *closure, const char* event, struct afb_wsj1_msg *msg) { + if(closure) + static_cast<AfbClient*>(closure)->on_event(event, msg); + } + +private: + struct afb_wsj1 *m_ws = nullptr; + struct afb_wsj1_itf m_itf; + std::thread m_afb_thread; + sd_event *m_afb_loop = nullptr; + bool m_valid = false; + event_callback_fn m_event_cb = nullptr; + void *m_event_cb_closure = nullptr; + + void on_event(const char* event, struct afb_wsj1_msg *msg); +}; + +#endif // AFBCLIENT_H diff --git a/app/alexa-viewer.qrc b/app/alexa-viewer.qrc new file mode 100644 index 0000000..e039215 --- /dev/null +++ b/app/alexa-viewer.qrc @@ -0,0 +1,7 @@ +<RCC> + <qresource prefix="/"> + <file>Main.qml</file> + <file>BodyTemplateDialog.qml</file> + <file>WeatherTemplateDialog.qml</file> + </qresource> +</RCC> diff --git a/app/main.cpp b/app/main.cpp new file mode 100644 index 0000000..669341e --- /dev/null +++ b/app/main.cpp @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2016 The Qt Company Ltd. + * Copyright (C) 2020 Konsulko Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QtCore/QDebug> +#include <QtCore/QCommandLineParser> +#include <QtCore/QUrlQuery> +#include <QtGui/QGuiApplication> +#include <QtQml/QQmlApplicationEngine> +#include <QtQml/QQmlContext> +#include <QtQml/qqml.h> +#include <QQuickWindow> +#include <QtQuickControls2/QQuickStyle> + +#include <json-c/json.h> +#include <qlibwindowmanager.h> +#include <qlibhomescreen.h> +#include <guimetadata.h> +#include "afbclient.h" +#include <iostream> + +// Disable window activation at launch by default, but keep option +// for potential debug use. +#define HIDE_AT_LAUNCH + +bool check_template_supported(json_object *data) +{ + json_object *jtype = NULL; + json_object_object_get_ex(data, "type", &jtype); + if(!jtype) { + qWarning("render_template event missing type element"); + return false; + } + const char *type_value = json_object_get_string(jtype); + if(!type_value) { + qWarning("render_template event type element not parsed"); + return false; + } + // We only handle BodyTemplate[12] and WeatherTemplate, ignore + // others + if(!(strcmp(type_value, "BodyTemplate1") && + strcmp(type_value, "BodyTemplate2") && + strcmp(type_value, "WeatherTemplate"))) + return true; + + return false; +} + +void async_event_cb(const char *event, json_object *data, void *closure) +{ + if(!data) + return; + + if(!strcmp(event, "vshl-capabilities/setDestination")) { + // Slight hack here, there's currently no convenient place to hook up raising + // the navigation app when a route is set by Alexa, so do so here for now. + if(closure != nullptr) { + static_cast<QLibHomeScreen*>(closure)->showWindow("navigation", "normal"); + } + } else if(!strcmp(event, "vshl-capabilities/render_template")) { + // Raise ourselves, the UI code will receive the event as well and render it + if(closure != nullptr) { + if(!check_template_supported(data)) { + qDebug() << "Unsupported template type, ignoring!"; + return; + } + static_cast<QLibHomeScreen*>(closure)->showWindow("alexa-viewer", "on_screen"); + } + } else if(!strcmp(event, "vshl-capabilities/clear_template")) { + // Hide ourselves + if(closure != nullptr) { + static_cast<QLibHomeScreen*>(closure)->hideWindow("alexa-viewer"); + } + } +} + +void subscribe_async_events(AfbClient &client) +{ + const char *vshl_capabilities_nav_events[] = { + "setDestination", + NULL, + }; + const char **tmp = vshl_capabilities_nav_events; + json_object *args = json_object_new_object(); + json_object *actions = json_object_new_array(); + while (*tmp) { + json_object_array_add(actions, json_object_new_string(*tmp++)); + } + json_object_object_add(args, "actions", actions); + if(json_object_array_length(actions)) { + client.subscribe("vshl-capabilities", args, "navigation/subscribe"); + } else { + json_object_put(args); + } + + // NOTE: Not subscribing to "clear_template", as it will be passed to + // the app QML to handle by the libqtappfw wrapper. + const char *vshl_capabilities_guimetadata_events[] = { + "render_template", + NULL, + }; + tmp = vshl_capabilities_guimetadata_events; + args = json_object_new_object(); + actions = json_object_new_array(); + while (*tmp) { + json_object_array_add(actions, json_object_new_string(*tmp++)); + } + json_object_object_add(args, "actions", actions); + if(json_object_array_length(actions)) { + client.subscribe("vshl-capabilities", args, "guimetadata/subscribe"); + } else { + json_object_put(args); + } +} + +int main(int argc, char *argv[]) +{ + QString graphic_role = QString("on_screen"); + + QGuiApplication app(argc, argv); + + QCommandLineParser parser; + parser.addPositionalArgument("port", app.translate("main", "port for binding")); + parser.addPositionalArgument("secret", app.translate("main", "secret for binding")); + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(app); + QStringList positionalArguments = parser.positionalArguments(); + QUrl bindingAddress; + + int port = 0; + QString token; + if (positionalArguments.length() == 2) { + port = positionalArguments.takeFirst().toInt(); + token = positionalArguments.takeFirst(); + bindingAddress.setScheme(QStringLiteral("ws")); + bindingAddress.setHost(QStringLiteral("localhost")); + bindingAddress.setPort(port); + bindingAddress.setPath(QStringLiteral("/api")); + QUrlQuery query; + query.addQueryItem(QStringLiteral("token"), token); + bindingAddress.setQuery(query); + } + + // QLibWM + QLibWindowmanager* qwmHandler = new QLibWindowmanager(); + int res; + if((res = qwmHandler->init(port, token)) != 0){ + qCritical("init qlibwm err(%d)", res); + return -1; + } + if((res = qwmHandler->requestSurface(graphic_role)) != 0) { + qCritical("requestSurface error(%d)", res); + return -1; + } + qwmHandler->set_event_handler(QLibWindowmanager::Event_SyncDraw, + [qwmHandler, &graphic_role](json_object *object) { + qwmHandler->endDraw(graphic_role); + }); + + // QLibHS + QLibHomeScreen* qhsHandler = new QLibHomeScreen(); + qhsHandler->init(port, token.toStdString().c_str()); + qhsHandler->set_event_handler(QLibHomeScreen::Event_ShowWindow, + [qwmHandler, &graphic_role](json_object *object){ + qDebug("Surface %s got showWindow\n", graphic_role.toStdString().c_str()); + qwmHandler->activateWindow(graphic_role, "on_screen"); + }); + qhsHandler->set_event_handler(QLibHomeScreen::Event_HideWindow, + [qwmHandler, &graphic_role](json_object *object){ + qDebug("Surface %s got hideWindow\n", graphic_role.toStdString().c_str()); + qwmHandler->deactivateWindow(graphic_role); + }); + + // Load qml + QQmlApplicationEngine engine; + QQmlContext *context = engine.rootContext(); + + context->setContextProperty("homescreen", qhsHandler); + context->setContextProperty("GuiMetadata", new GuiMetadata(bindingAddress, context)); + engine.load(QUrl(QStringLiteral("qrc:/Main.qml"))); + +#ifndef HIDE_AT_LAUNCH + QObject *root = engine.rootObjects().first(); + QQuickWindow *window = qobject_cast<QQuickWindow *>(root); + QObject::connect(window, SIGNAL(frameSwapped()), qwmHandler, SLOT(slotActivateWindow())); +#endif + // Create app framework client to handle events when window is not visible + AfbClient client(port, token.toStdString()); + client.set_event_callback(async_event_cb, (void*) qhsHandler); + subscribe_async_events(client); + + return app.exec(); +} |