src/statusbarserver.cpp \
src/applicationlauncher.cpp \
src/mastervolume.cpp \
- src/homescreenhandler.cpp
+ src/homescreenhandler.cpp \
+ src/aglsocketwrapper.cpp \
+ src/chromecontroller.cpp
HEADERS += \
src/statusbarmodel.h \
src/statusbarserver.h \
src/applicationlauncher.h \
src/mastervolume.h \
- src/homescreenhandler.h
+ src/homescreenhandler.h \
+ src/aglsocketwrapper.h \
+ src/chromecontroller.h \
+ src/constants.h
OTHER_FILES += \
README.md
qml/images/Shortcut/shortcut.qrc \
qml/images/Status/status.qrc \
qml/images/images.qrc \
- qml/qml.qrc
+ qml/qml.qrc \
+ qml/images/SpeechChrome/speechchrome.qrc
\ No newline at end of file
StackView {
id: root
- width: 1080
- height: 215
+ width: parent.width
+ height: parent.height
initialItem: blank
import MasterVolume 1.0
Image {
- width: 1080
- height: 215
+ width: parent.width
+ height: parent.height
source: './images/Utility_Logo_Background-01.svg'
property bool displayVolume: false;
}
Image {
- id: logo_image
+ id: logo_image
anchors.centerIn: parent
source: './images/Utility_Logo_Grey-01.svg'
}
Timer {
id: volume_timer
- interval: 5000; running: false; repeat: false
+ interval: 3000; running: false; repeat: false
onTriggered: displayVolume = false
}
PropertyChanges { target: master_volume; opacity: 1.0 }
PropertyChanges { target: slider; enabled: true }
PropertyChanges { target: logo_image; opacity: 0.0 }
+ PropertyChanges { target: speech_chrome; visible: false }
},
State { when: !displayVolume;
PropertyChanges { target: master_volume; opacity: 0.0 }
PropertyChanges { target: slider; enabled: false }
PropertyChanges { target: logo_image; opacity: 1.0 }
+ PropertyChanges { target: speech_chrome; visible: speech_chrome.agentPresent }
}
]
}
}
}
+
+ SpeechChrome {
+ id: speech_chrome
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: parent.height
+ }
}
--- /dev/null
+import QtQuick 2.0
+import SpeechChrome 1.0
+
+Item {
+ id: root
+
+ clip: true
+
+ property bool agentPresent: speechChromeController.agentPresent
+
+ visible: agentPresent
+
+ Image {
+ id: chromeBarImage
+
+ anchors.top: parent.top
+ source: "./images/SpeechChrome/bar.png"
+
+ Behavior on x {
+ NumberAnimation { duration: 250 }
+ }
+ Behavior on opacity {
+ NumberAnimation { duration: 250 }
+ }
+ }
+
+ Image {
+ id: pushToTalk
+
+ height: parent.height * 0.80
+ width: height
+
+ anchors.left: parent.left
+ anchors.leftMargin: parent.width / 128
+ anchors.verticalCenter: parent.verticalCenter
+ source: "./images/SpeechChrome/push_to_talk.svg"
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: speechChromeController.pushToTalk()
+ }
+
+ Behavior on opacity {
+ NumberAnimation { duration: 250 }
+ }
+ }
+
+ states: [
+ State {
+ name: "Idle"
+ when: speechChromeController.chromeState == SpeechChromeController.Idle
+ PropertyChanges {
+ target: chromeBarImage
+ opacity: 0.0
+ x: 0
+ }
+ PropertyChanges {
+ target: pushToTalk
+ opacity: 1.0
+ enabled: true
+ }
+ },
+ State {
+ name: "Listening"
+ when: speechChromeController.chromeState == SpeechChromeController.Listening
+ PropertyChanges {
+ target: chromeBarImage
+ opacity: 1.0
+ x: 0
+ }
+ PropertyChanges {
+ target: pushToTalk
+ opacity: 0.0
+ enabled: false
+ }
+ },
+ State {
+ name: "Thinking"
+ when: speechChromeController.chromeState == SpeechChromeController.Thinking
+ PropertyChanges {
+ target: chromeBarImage
+ opacity: 1.0
+ x: root.width - chromeBarImage.width
+ }
+ PropertyChanges {
+ target: pushToTalk
+ opacity: 0.0
+ enabled: false
+ }
+ },
+ State {
+ name: "Speaking"
+ when: speechChromeController.chromeState == SpeechChromeController.Speaking
+ PropertyChanges {
+ target: chromeBarImage
+ opacity: 1.0
+ x: (root.width - chromeBarImage.width) * 0.5
+ }
+ PropertyChanges {
+ target: pushToTalk
+ opacity: 0.0
+ enabled: false
+ }
+ },
+ State {
+ name: "MicrophoneOff"
+ when: speechChromeController.chromeState == SpeechChromeController.MicrophoneOff
+ PropertyChanges {
+ target: chromeBarImage
+ opacity: 0.0
+ x: 0
+ }
+ PropertyChanges {
+ target: pushToTalk
+ opacity: 1.0
+ enabled: true
+ }
+ }
+ ]
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg
+ xmlns:i="&#38;ns_ai;"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="Radio_Inactive"
+ x="0px"
+ y="0px"
+ viewBox="0 0 280 280"
+ xml:space="preserve"
+ inkscape:version="0.92.4 (unknown)"
+ sodipodi:docname="mic2.svg"
+ width="280"
+ height="280"><metadata
+ id="metadata5319"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs5317" /><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1910"
+ inkscape:window-height="899"
+ id="namedview5315"
+ showgrid="false"
+ inkscape:zoom="2"
+ inkscape:cx="166.88636"
+ inkscape:cy="140"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="Radio_Inactive" /><style
+ type="text/css"
+ id="style5192">
+ .st0{fill:#FFFFFF;}
+ .st1{font-family:'Roboto-Regular';}
+ .st2{font-size:25px;}
+ .st3{letter-spacing:6;}
+ .st4{fill:url(#SVGID_1_);}
+ .st5{fill:url(#SVGID_2_);}
+ .st6{fill:url(#SVGID_3_);}
+ .st7{fill:url(#SVGID_4_);}
+ .st8{fill:url(#SVGID_5_);}
+ .st9{fill:url(#SVGID_6_);}
+ .st10{fill:url(#SVGID_7_);}
+ .st11{fill:url(#SVGID_8_);}
+ .st12{fill:url(#SVGID_9_);}
+ .st13{fill:url(#SVGID_10_);}
+ .st14{fill:url(#SVGID_11_);}
+ .st15{fill:url(#SVGID_12_);}
+ .st16{fill:url(#SVGID_13_);}
+</style><switch
+ id="switch5194"
+ transform="matrix(1.3307804,0,0,1.3314313,-72.924861,-37.945792)"><g
+ i:extraneous="self"
+ id="g5196"><g
+ id="g5198"><linearGradient
+ id="SVGID_1_"
+ gradientUnits="userSpaceOnUse"
+ x1="4.0481"
+ y1="287.94919"
+ x2="320.4859"
+ y2="-15.4029"
+ gradientTransform="matrix(1,0.00546456,-0.00546456,1,-2.0192,-3.0212)"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5201" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5203" /></linearGradient><path
+ class="st4"
+ d="m 160,238.8 c -0.2,0 -0.4,0 -0.6,0 C 101.4,238.5 54.5,191.1 54.8,133.1 55.2,75.3 102.3,28.5 160,28.5 c 0.2,0 0.4,0 0.6,0 58,0.3 104.9,47.7 104.6,105.7 v 0 C 264.8,192 217.7,238.8 160,238.8 Z m 0,-206.6 c -55.7,0 -101.2,45.2 -101.5,100.9 -0.3,55.9 45,101.7 100.9,102 0.2,0 0.4,0 0.6,0 55.7,0 101.2,-45.2 101.5,-100.9 0.3,-55.9 -45,-101.7 -100.9,-102 -0.2,0 -0.4,0 -0.6,0 z"
+ id="path5205"
+ style="fill:url(#SVGID_1_)"
+ inkscape:connector-curvature="0" /><g
+ id="g5207"><linearGradient
+ id="SVGID_2_"
+ gradientUnits="userSpaceOnUse"
+ x1="-11.0561"
+ y1="273.63409"
+ x2="354.8013"
+ y2="-51.979"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5210" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5212" /></linearGradient><path
+ class="st5"
+ d="m 168.2,162.4 -1.2,-3.5 c 7.9,-2.6 13.3,-9.6 13.3,-17.3 v -40.5 c 0,-10.2 -9.1,-18.4 -20.2,-18.4 -11.1,0 -20.2,8.3 -20.2,18.4 v 40.5 c 0,7.7 5.3,14.6 13.2,17.3 l -1.2,3.5 c -9.4,-3.2 -15.7,-11.5 -15.7,-20.8 v -40.5 c 0,-12.2 10.7,-22.1 23.9,-22.1 13.2,0 23.9,9.9 23.9,22.1 v 40.5 c 0,9.3 -6.4,17.6 -15.8,20.8 z"
+ id="path5214"
+ style="fill:url(#SVGID_2_)"
+ inkscape:connector-curvature="0" /></g><g
+ id="g5216"><linearGradient
+ id="SVGID_3_"
+ gradientUnits="userSpaceOnUse"
+ x1="3.6219001"
+ y1="290.12631"
+ x2="369.4794"
+ y2="-35.486801"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5219" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5221" /></linearGradient><path
+ class="st6"
+ d="m 160,172.9 c -18.3,0 -33.1,-12.2 -33.1,-27.3 h 3.7 c 0,13 13.2,23.6 29.5,23.6 16.3,0 29.5,-10.6 29.5,-23.6 h 3.7 c -0.2,15.1 -15,27.3 -33.3,27.3 z"
+ id="path5223"
+ style="fill:url(#SVGID_3_)"
+ inkscape:connector-curvature="0" /></g><g
+ id="g5225"><linearGradient
+ id="SVGID_4_"
+ gradientUnits="userSpaceOnUse"
+ x1="19.325199"
+ y1="307.77039"
+ x2="385.18259"
+ y2="-17.8428"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5228" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5230" /></linearGradient><rect
+ x="158.2"
+ y="178.5"
+ class="st7"
+ width="3.7"
+ height="8"
+ id="rect5232"
+ style="fill:url(#SVGID_4_)" /></g><g
+ id="g5234"><linearGradient
+ id="SVGID_5_"
+ gradientUnits="userSpaceOnUse"
+ x1="-22.1502"
+ y1="261.16879"
+ x2="343.70721"
+ y2="-64.444397"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5237" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5239" /></linearGradient><rect
+ x="138.10001"
+ y="110.3"
+ class="st8"
+ width="14.3"
+ height="3.7"
+ id="rect5241"
+ style="fill:url(#SVGID_5_)" /></g><g
+ id="g5243"><linearGradient
+ id="SVGID_6_"
+ gradientUnits="userSpaceOnUse"
+ x1="-27.6269"
+ y1="255.0152"
+ x2="338.23059"
+ y2="-70.5979"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5246" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5248" /></linearGradient><rect
+ x="138.10001"
+ y="99.300003"
+ class="st9"
+ width="14.3"
+ height="3.7"
+ id="rect5250"
+ style="fill:url(#SVGID_6_)" /></g><g
+ id="g5252"><linearGradient
+ id="SVGID_7_"
+ gradientUnits="userSpaceOnUse"
+ x1="-16.6164"
+ y1="267.3866"
+ x2="349.241"
+ y2="-58.226601"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5255" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5257" /></linearGradient><rect
+ x="138.10001"
+ y="121.4"
+ class="st10"
+ width="14.3"
+ height="3.7"
+ id="rect5259"
+ style="fill:url(#SVGID_7_)" /></g><g
+ id="g5261"><linearGradient
+ id="SVGID_8_"
+ gradientUnits="userSpaceOnUse"
+ x1="-11.1393"
+ y1="273.54059"
+ x2="354.71811"
+ y2="-52.072498"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5264" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5266" /></linearGradient><rect
+ x="138.10001"
+ y="132.5"
+ class="st11"
+ width="14.3"
+ height="3.7"
+ id="rect5268"
+ style="fill:url(#SVGID_8_)" /></g><g
+ id="g5270"><linearGradient
+ id="SVGID_9_"
+ gradientUnits="userSpaceOnUse"
+ x1="-9.1322002"
+ y1="275.7959"
+ x2="356.72531"
+ y2="-49.817299"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5273" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5275" /></linearGradient><rect
+ x="167.60001"
+ y="110.3"
+ class="st12"
+ width="14.3"
+ height="3.7"
+ id="rect5277"
+ style="fill:url(#SVGID_9_)" /></g><g
+ id="g5279"><linearGradient
+ id="SVGID_10_"
+ gradientUnits="userSpaceOnUse"
+ x1="-14.6088"
+ y1="269.6423"
+ x2="351.2486"
+ y2="-55.970798"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5282" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5284" /></linearGradient><rect
+ x="167.60001"
+ y="99.300003"
+ class="st13"
+ width="14.3"
+ height="3.7"
+ id="rect5286"
+ style="fill:url(#SVGID_10_)" /></g><g
+ id="g5288"><linearGradient
+ id="SVGID_11_"
+ gradientUnits="userSpaceOnUse"
+ x1="-3.5984001"
+ y1="282.01361"
+ x2="362.25909"
+ y2="-43.599499"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5291" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5293" /></linearGradient><rect
+ x="167.60001"
+ y="121.4"
+ class="st14"
+ width="14.3"
+ height="3.7"
+ id="rect5295"
+ style="fill:url(#SVGID_11_)" /></g><g
+ id="g5297"><linearGradient
+ id="SVGID_12_"
+ gradientUnits="userSpaceOnUse"
+ x1="1.8788"
+ y1="288.16769"
+ x2="367.73621"
+ y2="-37.445499"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5300" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5302" /></linearGradient><rect
+ x="167.60001"
+ y="132.5"
+ class="st15"
+ width="14.3"
+ height="3.7"
+ id="rect5304"
+ style="fill:url(#SVGID_12_)" /></g><g
+ id="g5306"><linearGradient
+ id="SVGID_13_"
+ gradientUnits="userSpaceOnUse"
+ x1="24.376101"
+ y1="313.44559"
+ x2="390.23361"
+ y2="-12.1676"><stop
+ offset="0"
+ style="stop-color:#00ADDC"
+ id="stop5309" /><stop
+ offset="1"
+ style="stop-color:#6BFBFF"
+ id="stop5311" /></linearGradient><path
+ class="st16"
+ d="m 182.1,195 h -3.7 c 0,-4.6 -2.3,-5.4 -8.8,-5.4 h -19.2 c -6.5,0 -8.8,0.8 -8.8,5.4 h -3.7 c 0,-9.1 7.8,-9.1 12.5,-9.1 h 19.2 c 4.7,0 12.5,0 12.5,9.1 z"
+ id="path5313"
+ style="fill:url(#SVGID_13_)"
+ inkscape:connector-curvature="0" /></g></g></g></switch></svg>
\ No newline at end of file
--- /dev/null
+<RCC>
+ <qresource prefix="/images/SpeechChrome">
+ <file>bar.png</file>
+ <file>push_to_talk.svg</file>
+ </qresource>
+</RCC>
}
}
- Timer {
+ Timer {
id:notificationTimer
interval: 3000
running: false
<file>StatusArea.qml</file>
<file>TopArea.qml</file>
<file>IconItem.qml</file>
+ <file>SpeechChrome.qml</file>
</qresource>
</RCC>
--- /dev/null
+#include "aglsocketwrapper.h"
+#include "constants.h"
+
+#include <QWebSocket>
+#include <QUuid>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+namespace {
+enum MessageTypes {
+ Call = 2,
+ Success = 3,
+ Error = 4,
+ Event = 5
+};
+}
+
+AglSocketWrapper::AglSocketWrapper(QObject *parent) :
+ QObject(parent)
+ , m_socket(new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this))
+{
+ connect(m_socket, &QWebSocket::connected, this, &AglSocketWrapper::connected);
+ connect(m_socket, &QWebSocket::disconnected, this, &AglSocketWrapper::disconnected);
+ connect(m_socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
+ [](QAbstractSocket::SocketError error) -> void {
+ qWarning() << "AglSocketWrapper internal socket error" << error;
+ });
+ connect(m_socket, &QWebSocket::textMessageReceived,
+ this, [this](const QString &msg) -> void {
+ const QJsonDocument doc = QJsonDocument::fromJson(msg.toUtf8());
+ if (doc.isArray()) {
+ const QJsonArray msgArray = doc.array();
+ if (msgArray.count() >= 3) {
+ const int msgType = msgArray.at(0).toInt();
+ switch (msgType) {
+ case Success:
+ case Error: {
+ auto callbackIt = m_callbacks.find( msgArray.at(1).toString());
+ if (callbackIt != m_callbacks.constEnd()) {
+ (*callbackIt)(msgType == Success, msgArray.at(2));
+ m_callbacks.erase(callbackIt);
+ }
+ }
+ break;
+ case Event: {
+ const QJsonObject eventObj = msgArray.at(2).toObject();
+ emit eventReceived(msgArray.at(1).toString(), eventObj.value(vshl::DATA_TAG));
+ }
+ break;
+ default:
+ break;
+ }
+ return;
+ }
+ }
+ qWarning() << "Unsupported message format:" << msg;
+ });
+}
+
+void AglSocketWrapper::open(const QUrl &url)
+{
+ m_socket->open(url);
+}
+
+void AglSocketWrapper::close()
+{
+ m_socket->close();
+}
+
+void AglSocketWrapper::apiCall(const QString &api, const QString &verb, const QJsonValue &args,
+ AglSocketWrapper::ApiCallback callback)
+{
+ const QString id = QUuid::createUuid().toString();
+ if (callback)
+ m_callbacks.insert(id, callback);
+
+ QJsonArray callData;
+ callData.append(Call);
+ callData.append(id);
+ callData.append(api + QLatin1String("/") + verb);
+ callData.append(args);
+
+ const QString msg = QLatin1String(QJsonDocument(callData).toJson(QJsonDocument::Compact));
+ m_socket->sendTextMessage(msg);
+
+ qDebug() << Q_FUNC_INFO << "Data sent:" << msg;
+}
--- /dev/null
+#ifndef AGLSOCKETWRAPPER_H
+#define AGLSOCKETWRAPPER_H
+
+#include <QUrl>
+#include <QMap>
+#include <QObject>
+#include <QJsonValue>
+
+#include <functional>
+
+class QWebSocket;
+class AglSocketWrapper : public QObject
+{
+ Q_OBJECT
+public:
+ explicit AglSocketWrapper(QObject *parent = nullptr);
+
+ void open(const QUrl &url);
+ void close();
+
+ using ApiCallback = std::function<void(bool, const QJsonValue&)>;
+ void apiCall(const QString &api, const QString &verb, const QJsonValue &args = QJsonValue(),
+ ApiCallback callback = nullptr);
+
+signals:
+ void connected();
+ void disconnected();
+ void eventReceived(const QString &eventName, const QJsonValue &data);
+
+private:
+ QWebSocket *m_socket;
+ QMap<QString, ApiCallback> m_callbacks;
+};
+
+#endif // AGLSOCKETWRAPPER_H
--- /dev/null
+#include "chromecontroller.h"
+#include "aglsocketwrapper.h"
+#include "constants.h"
+
+#include <QTimer>
+#include <QDebug>
+#include <QJsonDocument>
+
+ChromeController::ChromeController(const QUrl &bindingUrl, QObject *parent) :
+ QObject(parent)
+ , m_aglSocket(new AglSocketWrapper(this))
+{
+ //Alexa voice agent subscription----------------------------------------------------------------
+ {
+ connect(m_aglSocket, &AglSocketWrapper::connected,
+ this, [this]() -> void {
+ m_aglSocket->apiCall(vshl::API, vshl::VOICE_AGENT_ENUMERATION_VERB, QJsonValue(),
+ [this](bool result, const QJsonValue &data) -> void {
+ qDebug() << (vshl::API + QLatin1String(":") + vshl::VOICE_AGENT_ENUMERATION_VERB)
+ << "result: " << result << " val: " << data;
+ if (!result) {
+ qWarning() << "Failed to enumerate voice agents";
+ return;
+ }
+
+ QJsonObject dataObj = data.toObject();
+ auto objIt = dataObj.find(vshl::RESPONSE_TAG);
+ if (objIt == dataObj.constEnd()) {
+ qWarning() << "Voice agent enumeration response tag missing."
+ << dataObj;
+ return;
+ }
+
+ // Get default voice agent
+ dataObj = objIt.value().toObject();
+ QJsonObject responseObj = dataObj;
+ objIt = dataObj.find(vshl::DEFAULT_TAG);
+ if (objIt == dataObj.constEnd()) {
+ qWarning() << "Voice agent enumeration default agent tag missing."
+ << dataObj;
+ return;
+ }
+ QString agentId = objIt.value().toString();
+ if (agentId.isEmpty()) {
+ qWarning() << "Default voice agent not found";
+ return;
+ }
+ qDebug() << (vshl::API + QLatin1String(":") + vshl::VOICE_AGENT_ENUMERATION_VERB) << "default: " << agentId;
+
+ objIt = dataObj.find(vshl::AGENTS_TAG);
+ if (objIt == dataObj.constEnd()) {
+ qWarning() << "Voice agent enumeration agents tag missing."
+ << dataObj;
+ return;
+ }
+
+ // Sanity check that the default agent is actually listed
+ bool agentFound = false;
+ const QJsonArray agents = objIt.value().toArray();
+ for (const QJsonValue &agent : agents) {
+ const QJsonObject agentObj = agent.toObject();
+ auto agentIt = agentObj.find(vshl::ID_TAG);
+ if (agentIt == agentObj.constEnd())
+ continue;
+ if (agentId.compare(agentIt.value().toString()) == 0) {
+ agentFound = true;
+ break;
+ }
+ }
+ if (!agentFound) {
+ qWarning() << "Default voice agent configuration not found";
+ return;
+ }
+ m_agentPresent = true;
+ emit agentPresentChanged();
+
+ //Voice agent subscription------------------------------------------------------
+ {
+ m_voiceAgentId = agentId;
+ const QJsonObject args {
+ { vshl::VOICE_AGENT_ID_ARG, agentId },
+ { vshl::VOICE_AGENT_EVENTS_ARG, vshl::VOICE_AGENT_EVENTS_ARRAY }
+ };
+ m_aglSocket->apiCall(vshl::API, vshl::SUBSCRIBE_VERB, args,
+ [](bool result, const QJsonValue &data) -> void {
+ qDebug() << (vshl::API + QLatin1String(":") + vshl::SUBSCRIBE_VERB)
+ << "result: " << result << " val: " << data;
+ });
+ }
+ //------------------------------------------------------------------------------
+ });
+ });
+ }
+ //----------------------------------------------------------------------------------------------<
+
+ //Socket connection management------------------------------------------------------------------
+ {
+ auto connectToBinding = [bindingUrl, this]() -> void {
+ m_aglSocket->open(bindingUrl);
+ qDebug() << "Connecting to:" << bindingUrl;
+ };
+ connect(m_aglSocket, &AglSocketWrapper::disconnected, this, [connectToBinding]() -> void {
+ QTimer::singleShot(2500, connectToBinding);
+ });
+ connectToBinding();
+ }
+ //----------------------------------------------------------------------------------------------
+
+ //Speech chrome state change event handling-----------------------------------------------------
+ {
+ connect(m_aglSocket, &AglSocketWrapper::eventReceived,
+ this, [this](const QString &eventName, const QJsonValue &data) -> void {
+ if (eventName.compare(vshl::VOICE_DIALOG_STATE_EVENT + m_voiceAgentId) == 0) {
+ const QJsonObject dataObj = QJsonDocument::fromJson(data.toString().toUtf8()).object();
+ auto objIt = dataObj.find(vshl::STATE_TAG);
+ if (objIt == dataObj.constEnd()) {
+ qWarning() << "Voice dialog state event state missing.";
+ return;
+ }
+ const QString stateStr = objIt.value().toString();
+ if (stateStr.compare(vshl::VOICE_DIALOG_IDLE) == 0) {
+ setChromeState(Idle);
+ } else if (stateStr.compare(vshl::VOICE_DIALOG_LISTENING) == 0) {
+ setChromeState(Listening);
+ } else if (stateStr.compare(vshl::VOICE_DIALOG_THINKING) == 0) {
+ setChromeState(Thinking);
+ } else if (stateStr.compare(vshl::VOICE_DIALOG_SPEAKING) == 0) {
+ setChromeState(Speaking);
+ } else if (stateStr.compare(vshl::VOICE_DIALOG_MICROPHONEOFF) == 0) {
+ setChromeState(MicrophoneOff);
+ }
+ }
+ });
+ }
+ //----------------------------------------------------------------------------------------------
+}
+
+void ChromeController::pushToTalk()
+{
+ m_aglSocket->apiCall(vshl::API, vshl::TAP_TO_TALK_VERB, QJsonValue(),
+ [](bool result, const QJsonValue &data) -> void {
+ qDebug() << (vshl::API + QLatin1String(":") + vshl::TAP_TO_TALK_VERB)
+ << "result: " << result << " val: " << data;
+ });
+}
+
+void ChromeController::setChromeState(ChromeController::ChromeState state)
+{
+ const char* ChromeStateNames[MicrophoneOff + 1] = { "Idle", "Listening", "Thinking", "Speaking", "MicrophoneOff" };
+
+ if (m_chromeState != state) {
+ m_chromeState = state;
+ emit chromeStateChanged();
+ if(state <= MicrophoneOff)
+ qDebug() << "new state = " << ChromeStateNames[state];
+ else
+ qDebug() << "new state = " << state;
+ }
+}
--- /dev/null
+#pragma once
+
+#include <QObject>
+#include <QUrl>
+
+class AglSocketWrapper;
+class ChromeController : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(bool agentPresent READ agentPresent NOTIFY agentPresentChanged)
+ Q_PROPERTY(int chromeState READ chromeState NOTIFY chromeStateChanged)
+
+public:
+ enum ChromeState {
+ Idle = 0,
+ Listening,
+ Thinking,
+ Speaking,
+ MicrophoneOff
+ };
+ Q_ENUM(ChromeState)
+
+ explicit ChromeController(const QUrl &bindingUrl, QObject *parent = nullptr);
+ bool agentPresent() const { return m_agentPresent; }
+ int chromeState() const { return m_chromeState; }
+
+public slots:
+ void pushToTalk();
+
+signals:
+ void agentPresentChanged();
+ void chromeStateChanged();
+
+private:
+ void setChromeState(ChromeState state);
+
+ AglSocketWrapper *m_aglSocket;
+ QString m_voiceAgentId;
+ bool m_agentPresent = false;
+ ChromeState m_chromeState = Idle;
+};
--- /dev/null
+#ifndef CONSTANTS_H
+#define CONSTANTS_H
+
+#include <QString>
+#include <QJsonArray>
+#include <QJsonObject>
+
+namespace vshl {
+const QString API = QLatin1String("vshl-core");
+const QString VOICE_AGENT_ENUMERATION_VERB = QLatin1String("enumerateVoiceAgents");
+const QString SUBSCRIBE_VERB = QLatin1String("subscribe");
+const QString TAP_TO_TALK_VERB = QLatin1String("startListening");
+
+const QString ALEXA_AGENT_NAME = QLatin1String("Alexa");
+
+const QString DATA_TAG = QLatin1String("data");
+const QString RESPONSE_TAG = QLatin1String("response");
+const QString AGENTS_TAG = QLatin1String("agents");
+const QString DEFAULT_TAG = QLatin1String("default");
+const QString NAME_TAG = QLatin1String("name");
+const QString ID_TAG = QLatin1String("id");
+const QString STATE_TAG = QLatin1String("state");
+
+const QString VOICE_AGENT_ID_ARG = QLatin1String("va_id");
+const QString VOICE_AGENT_EVENTS_ARG = QLatin1String("events");
+const QString VOICE_AGENT_ACTIONS_ARG = QLatin1String("actions");
+
+const QJsonArray VOICE_AGENT_EVENTS_ARRAY = {
+ QLatin1String("voice_authstate_event"),
+ QLatin1String("voice_dialogstate_event"),
+ QLatin1String("voice_connectionstate_event")
+};
+
+const QString VOICE_DIALOG_STATE_EVENT = QLatin1String("vshl-core/voice_dialogstate_event#");
+const QString VOICE_DIALOG_IDLE = QLatin1String("IDLE");
+const QString VOICE_DIALOG_LISTENING = QLatin1String("LISTENING");
+const QString VOICE_DIALOG_THINKING = QLatin1String("THINKING");
+const QString VOICE_DIALOG_SPEAKING = QLatin1String("SPEAKING");
+const QString VOICE_DIALOG_MICROPHONEOFF = QLatin1String("MICROPHONEOFF");
+}
+
+#endif // CONSTANTS_H
#include "mastervolume.h"
#include "homescreenhandler.h"
#include "hmi-debug.h"
+#include "chromecontroller.h"
// XXX: We want this DBus connection to be shared across the different
// QML objects, is there another way to do this, a nice way, perhaps?
// qmlRegisterType<ApplicationLauncher>("HomeScreen", 1, 0, "ApplicationLauncher");
qmlRegisterType<StatusBarModel>("HomeScreen", 1, 0, "StatusBarModel");
qmlRegisterType<MasterVolume>("MasterVolume", 1, 0, "MasterVolume");
+ qmlRegisterUncreatableType<ChromeController>("SpeechChrome", 1, 0, "SpeechChromeController",
+ QLatin1String("SpeechChromeController is uncreatable."));
ApplicationLauncher *launcher = new ApplicationLauncher();
QLibWindowmanager* layoutHandler = new QLibWindowmanager();
engine.rootContext()->setContextProperty("launcher", launcher);
engine.rootContext()->setContextProperty("weather", new Weather(bindingAddress));
engine.rootContext()->setContextProperty("bluetooth", new Bluetooth(bindingAddress, engine.rootContext()));
+ engine.rootContext()->setContextProperty("speechChromeController", new ChromeController(bindingAddress, &engine));
engine.rootContext()->setContextProperty("screenInfo", &screenInfo);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
<param name="Bluetooth-Manager" value="ws" />
<param name="windowmanager" value="ws" />
<param name="audiomixer" value="ws" />
+ <param name="vshl-core" value="ws" />
</feature>
<feature name="urn:AGL:widget:required-permission">
<param name="urn:AGL:permission::public:no-htdocs" value="required" />