summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/BodyTemplateDialog.qml164
-rw-r--r--app/CMakeLists.txt66
-rw-r--r--app/Main.qml121
-rw-r--r--app/WeatherTemplateDialog.qml239
-rw-r--r--app/afbclient.cpp245
-rw-r--r--app/afbclient.h67
-rw-r--r--app/alexa-viewer.qrc7
-rw-r--r--app/main.cpp207
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();
+}