summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorLoïc Collignon <loic.collignon@iot.bzh>2018-03-13 08:56:37 +0100
committerLoïc Collignon <loic.collignon@iot.bzh>2018-06-14 11:36:47 +0200
commit6bf2ccbd72176a8cbdfb3cdb2c15ee1c2db594b8 (patch)
tree52d9432e8f74507e4aaf477ca5605de6812bdecf /app
parent47695d79a938eb52c116062c218147049b994246 (diff)
make use of alsacore and hal bindings to control audio volume
Change-Id: Ib7e90a7d2a148a067566bc04929fda445b46ab45 Signed-off-by: Loïc Collignon <loic.collignon@iot.bzh>
Diffstat (limited to 'app')
-rw-r--r--app/Mixer.qml161
-rw-r--r--app/app.pro12
-rw-r--r--app/main.cpp26
-rw-r--r--app/mixer.cpp209
-rw-r--r--app/mixer.h74
-rw-r--r--app/paclient.cpp329
-rw-r--r--app/paclient.h97
-rw-r--r--app/pacontrolmodel.cpp208
-rw-r--r--app/pacontrolmodel.h92
-rw-r--r--app/qafbwsclient.cpp136
-rw-r--r--app/qafbwsclient.h50
-rw-r--r--app/qafbwsmsg.cpp44
-rw-r--r--app/qafbwsmsg.h33
13 files changed, 653 insertions, 818 deletions
diff --git a/app/Mixer.qml b/app/Mixer.qml
index 96875e0..2b6d5cc 100644
--- a/app/Mixer.qml
+++ b/app/Mixer.qml
@@ -18,11 +18,21 @@ import QtQuick 2.6
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.0
import AGL.Demo.Controls 1.0
-import PaControlModel 1.0
+import Mixer 1.0
ApplicationWindow {
id: root
+ Mixer {
+ id: mixer
+ Component.objectName: {
+ mixer.open(bindingAddress)
+ }
+ onMasterVolumeChanged: sliderMasterVolume.value = masterVolume
+ onPcmVolumeChanged: sliderPcmVolume.value = pcmVolume
+ onMicrophoneVolumeChanged: sliderMicrophoneVolume.value = microphoneVolume
+ }
+
Label {
id: title
font.pixelSize: 48
@@ -30,69 +40,92 @@ ApplicationWindow {
anchors.horizontalCenter: parent.horizontalCenter
}
- Component {
- id: ctldesc
- Label {
- font.pixelSize: 32
- width: listView.width
- wrapMode: Text.WordWrap
- property var typeString: {modelType ? "Output" : "Input"}
- text: "[" + typeString + " #" + modelCIndex + "]: " + modelDesc
- }
- }
+ ColumnLayout {
+ anchors.margins: 80
+ anchors.top: title.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
- Component {
- id: empty
- Item {
- }
- }
+ ComboBox {
+ id: soundCardSelector
+ anchors.left: parent.left
+ anchors.right: parent.right
+ model: mixer.hals
+ onCurrentIndexChanged: mixer.ActiveHal = currentIndex
+ }
- ListView {
- id: listView
- anchors.left: parent.left
- anchors.top: title.bottom
- anchors.margins: 80
- anchors.fill: parent
- model: PaControlModel { objectName: "pacm" }
- delegate: ColumnLayout {
- width: parent.width
- spacing: 40
- Connections {
- target: listView.model
- onDataChanged: slider.value = volume
- }
- Loader {
- property int modelType: type
- property int modelCIndex: cindex
- property string modelDesc: name
- sourceComponent: (channel == 0) ? ctldesc : empty
- }
- RowLayout {
- Layout.minimumHeight: 75
- Label {
- font.pixelSize: 24
- text: cdesc
- Layout.minimumWidth: 150
- }
- Label {
- font.pixelSize: 24
- text: "0 %"
- }
- Slider {
- id: slider
- Layout.fillWidth: true
- from: 0
- to: 65536
- stepSize: 256
- snapMode: Slider.SnapOnRelease
- onValueChanged: volume = value
- Component.onCompleted: value = volume
- }
- Label {
- font.pixelSize: 24
- text: "100 %"
- }
- }
- }
- }
+ RowLayout {
+ Layout.minimumHeight: 75
+ Label {
+ font.pixelSize: 24
+ text: "Master"
+ Layout.minimumWidth: 150
+ }
+ Label {
+ id: textMasterVolume
+ font.pixelSize: 24
+ text: "0 %"
+ }
+ Slider {
+ id: sliderMasterVolume
+ Layout.fillWidth: true
+ from: 0
+ to: 100
+ stepSize: 1
+ snapMode: Slider.SnapOnRelease
+ onValueChanged: {
+ textMasterVolume.text = value + " %"
+ mixer.masterVolume = value
+ }
+ }
+ }
+/*
+ RowLayout {
+ Layout.minimumHeight: 75
+ Label {
+ font.pixelSize: 24
+ text: "PCM"
+ Layout.minimumWidth: 150
+ }
+ Label {
+ font.pixelSize: 24
+ text: "0 %"
+ }
+ Slider {
+ id: sliderPcmVolume
+ Layout.fillWidth: true
+ from: 0
+ to: 100
+ stepSize: 1
+ snapMode: Slider.SnapOnRelease
+ onValueChanged: mixer.pcmVolume = value
+ }
+ }
+*/
+ RowLayout {
+ Layout.minimumHeight: 75
+ Label {
+ font.pixelSize: 24
+ text: "Microphone"
+ Layout.minimumWidth: 150
+ }
+ Label {
+ id: textMicrophoneVolume
+ font.pixelSize: 24
+ text: "0 %"
+ }
+ Slider {
+ id: sliderMicrophoneVolume
+ Layout.fillWidth: true
+ from: 0
+ to: 100
+ stepSize: 1
+ snapMode: Slider.SnapOnRelease
+ onValueChanged: {
+ textMicrophoneVolume.text = value + " %"
+ mixer.microphoneVolume = value
+ }
+ }
+ }
+ }
}
diff --git a/app/app.pro b/app/app.pro
index a33fc0d..239dc8f 100644
--- a/app/app.pro
+++ b/app/app.pro
@@ -1,13 +1,15 @@
TARGET = mixer
-QT = quickcontrols2
+QT = quickcontrols2 websockets core
HEADERS += \
- pacontrolmodel.h \
- paclient.h
+ mixer.h \
+ qafbwsmsg.h \
+ qafbwsclient.h
SOURCES = main.cpp \
- pacontrolmodel.cpp \
- paclient.cpp
+ mixer.cpp \
+ qafbwsclient.cpp \
+ qafbwsmsg.cpp
CONFIG += link_pkgconfig
PKGCONFIG += libhomescreen qlibwindowmanager libpulse
diff --git a/app/main.cpp b/app/main.cpp
index bfce498..a0a5667 100644
--- a/app/main.cpp
+++ b/app/main.cpp
@@ -15,9 +15,6 @@
* limitations under the License.
*/
-#include "paclient.h"
-#include "pacontrolmodel.h"
-
#include <QtCore/QDebug>
#include <QtCore/QCommandLineParser>
#include <QtCore/QUrlQuery>
@@ -34,6 +31,7 @@
#include <QQuickWindow>
#include <libhomescreen.hpp>
#include <qlibwindowmanager.h>
+#include "mixer.h"
int main(int argc, char *argv[])
{
@@ -51,14 +49,7 @@ int main(int argc, char *argv[])
parser.process(app);
QStringList positionalArguments = parser.positionalArguments();
- // Fire up PA client QThread
- QThread* pat = new QThread;
- PaClient* client = new PaClient();
- client->moveToThread(pat);
- pat->start();
-
- // Register the PA Control Model
- qmlRegisterType<PaControlModel>("PaControlModel", 1, 0, "PaControlModel");
+ qmlRegisterType<Mixer>("Mixer", 1, 0, "Mixer");
QQmlApplicationEngine engine;
if (positionalArguments.length() == 2) {
@@ -88,7 +79,7 @@ int main(int argc, char *argv[])
exit(EXIT_FAILURE);
}
// Create an event callbnewack against an event type. Here a lambda is called when SyncDraw event occurs
- qwm->set_event_handler(QLibWindowmanager::Event_SyncDraw, [qwm, myname](json_object *object) {
+ qwm->set_event_handler(QLibWindowmanager::Event_SyncDraw, [qwm, myname](json_object*) {
fprintf(stderr, "Surface got syncDraw!\n");
qwm->endDraw(myname);
});
@@ -116,17 +107,6 @@ int main(int argc, char *argv[])
QQuickWindow *window = qobject_cast<QQuickWindow *>(mobjs.first());
QObject::connect(window, SIGNAL(frameSwapped()), qwm, SLOT(slotActivateSurface()));
-
- PaControlModel *pacm = mobjs.first()->findChild<PaControlModel *>("pacm");
- QObject::connect(client, SIGNAL(controlAdded(int, QString, QString, int, int, const char *, int)),
- pacm, SLOT(addOneControl(int, QString, QString, int, int, const char *, int)));
- QObject::connect(client, SIGNAL(volumeExternallyChanged(uint32_t, uint32_t, uint32_t, uint32_t)),
- pacm, SLOT(changeExternalVolume(uint32_t, uint32_t, uint32_t, uint32_t)));
- QObject::connect(pacm, SIGNAL(volumeChanged(uint32_t, uint32_t, uint32_t, uint32_t)),
- client, SLOT(setVolume(uint32_t, uint32_t, uint32_t, uint32_t)));
-
- // Initalize PA client
- client->init();
}
return app.exec();
}
diff --git a/app/mixer.cpp b/app/mixer.cpp
new file mode 100644
index 0000000..8c2d2af
--- /dev/null
+++ b/app/mixer.cpp
@@ -0,0 +1,209 @@
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QtDebug>
+#include "mixer.h"
+
+Mixer::Mixer(QObject* parent)
+ : QObject(parent)
+ , m_masterVolume{0}
+ , m_pcmVolume{0}
+ , m_microphoneVolume{0}
+{
+ connect(&m_client, SIGNAL(connected()), this, SLOT(onClientConnected()));
+}
+
+QVariantList Mixer::hals() const
+{
+ return m_hallist;
+}
+
+int Mixer::masterVolume() const
+{
+ return m_masterVolume;
+}
+
+int Mixer::pcmVolume() const
+{
+ return m_pcmVolume;
+}
+
+int Mixer::microphoneVolume() const
+{
+ return m_microphoneVolume;
+}
+
+void Mixer::setMasterVolume(int v)
+{
+ qDebug() << "Mixer::setMasterVolume(" << v << ")";
+ if (v != m_masterVolume)
+ {
+ m_masterVolumePending = v;
+ QJsonObject arg;
+ arg["label"] = "Master_Playback_Volume";
+ arg["val"] = static_cast<int>(v);
+ m_setMasterVolume = m_client.call(m_activeHal, "ctlset", arg);
+ connect(m_setMasterVolume.data(), SIGNAL(closed()), this, SLOT(onSetMasterVolume()));
+ }
+}
+
+void Mixer::setPcmVolume(int v)
+{
+ qDebug() << "Mixer::setPcmVolume(" << v << ")";
+ if (v != m_pcmVolume)
+ {
+ m_pcmVolumePending = v;
+ QJsonObject arg;
+ arg["label"] = "PCM_Playback_Volume";
+ arg["val"] = static_cast<int>(v);
+ m_setPcmVolume = m_client.call(m_activeHal, "ctlset", arg);
+ connect(m_setPcmVolume.data(), SIGNAL(closed()), this, SLOT(onSetPcmVolume()));
+ }
+}
+
+void Mixer::setMicrophoneVolume(int v)
+{
+ qDebug() << "Mixer::setMicrophoneVolume(" << v << ")";
+ if (v != m_microphoneVolume)
+ {
+ m_microphoneVolumePending = v;
+ QJsonObject arg;
+ arg["label"] = "Capture_Volume";
+ arg["val"] = static_cast<int>(v);
+ m_setMicrophoneVolume = m_client.call(m_activeHal, "ctlset", arg);
+ connect(m_setMicrophoneVolume.data(), SIGNAL(closed()), this, SLOT(onSetMicrophoneVolume()));
+ }
+}
+
+QString Mixer::activeHal() const
+{
+ return m_activeHal;
+}
+
+void Mixer::setActiveHal(QString h)
+{
+ if (h != m_activeHal)
+ {
+ m_activeHal = h;
+ qDebug() << "Mixer::setActiveHal: " << h;
+ // Get volumes for this card
+
+ QJsonObject arg;
+
+ arg["label"] = "Master_Playback_Volume";
+ m_getMasterVolume = m_client.call(m_activeHal, "ctlget", arg);
+ connect(m_getMasterVolume.data(), SIGNAL(closed()), this, SLOT(onGetMasterVolume()));
+
+ arg["label"] = "PCM_Playback_Volume";
+ m_getPcmVolume = m_client.call(m_activeHal, "ctlget", arg);
+ connect(m_getPcmVolume.data(), SIGNAL(closed()), this, SLOT(onGetPcmVolume()));
+
+ arg["label"] = "Capture_Volume";
+ m_getMicrophoneVolume = m_client.call(m_activeHal, "ctlget", arg);
+ connect(m_getMicrophoneVolume.data(), SIGNAL(closed()), this, SLOT(onGetMicrophoneVolume()));
+
+ emit activeHalChanged();
+ }
+}
+
+void Mixer::open(const QUrl &url)
+{
+ m_client.open(url);
+}
+
+void Mixer::onClientConnected()
+{
+ // Call HAL to populate list
+ m_alsacoreHallist = m_client.call("alsacore", "hallist");
+ connect(m_alsacoreHallist.data(), SIGNAL(closed()), this, SLOT(onHalListClosed()));
+}
+
+void Mixer::onHalListClosed()
+{
+ qDebug() << "Mixer::onHalListClosed";
+ if(m_alsacoreHallist->messageType() == AfMsgType::RetOk)
+ {
+ m_hallist.clear();
+
+ QJsonArray cards = m_alsacoreHallist->value().toObject()["response"].toArray();
+ qDebug() << "Mixer::onHalListClosed - founds: " << cards;
+ foreach (const QJsonValue& card, cards)
+ {
+ QJsonObject c = card.toObject();
+ QVariant v(c["api"].toString());
+ m_hallist.append(v);
+ qDebug() << "Mixer::onHalListClosed - added this HAL: " << v;
+ }
+
+ m_alsacoreHallist.clear();
+
+ setActiveHal(m_hallist[0].toString());
+
+ emit halsChanged();
+ }
+}
+
+void Mixer::onGetMasterVolume()
+{
+ qDebug() << "Mixer::onGetMasterVolume()";
+ disconnect(m_getMasterVolume.data(), SIGNAL(closed()), this, SLOT(onGetMasterVolume()));
+ if (m_getMasterVolume->messageType() == AfMsgType::RetOk && m_getMasterVolume->value().isObject())
+ {
+ setMasterVolume(m_getMasterVolume->value().toObject()["response"].toObject()["val"].toArray()[0].toInt());
+ }
+}
+
+void Mixer::onGetPcmVolume()
+{
+ qDebug() << "Mixer::onGetPcmVolume()";
+ disconnect(m_getPcmVolume.data(), SIGNAL(closed()), this, SLOT(onGetPcmVolume()));
+ if (m_getPcmVolume->messageType() == AfMsgType::RetOk && m_getPcmVolume->value().isObject())
+ {
+ setPcmVolume(m_getPcmVolume->value().toObject()["response"].toObject()["val"].toArray()[0].toInt());
+ }
+}
+
+void Mixer::onGetMicrophoneVolume()
+{
+ qDebug() << "Mixer::onGetMicrophoneVolume()";
+ disconnect(m_getMicrophoneVolume.data(), SIGNAL(closed()), this, SLOT(onGetMicrophoneVolume()));
+ if (m_getMicrophoneVolume->messageType() == AfMsgType::RetOk && m_getMicrophoneVolume->value().isObject())
+ {
+ setMicrophoneVolume(m_getMicrophoneVolume->value().toObject()["response"].toObject()["val"].toArray()[0].toInt());
+ }
+}
+
+void Mixer::onSetMasterVolume()
+{
+ qDebug() << "Mixer::onSetMasterVolume()";
+ disconnect(m_setMasterVolume.data(), SIGNAL(closed()), this, SLOT(onSetMasterVolume()));
+ if (m_setMasterVolume->messageType() == AfMsgType::RetOk && m_setMasterVolume->value().isObject())
+ {
+ m_masterVolume = m_masterVolumePending;
+ emit masterVolumeChanged();
+ }
+}
+
+void Mixer::onSetPcmVolume()
+{
+ qDebug() << "Mixer::onSetPcmVolume()";
+ disconnect(m_setPcmVolume.data(), SIGNAL(closed()), this, SLOT(onSetPcmVolume()));
+ if (m_setPcmVolume->messageType() == AfMsgType::RetOk && m_setPcmVolume->value().isObject())
+ {
+ m_pcmVolume = m_pcmVolumePending;
+ emit pcmVolumeChanged();
+ }
+}
+
+void Mixer::onSetMicrophoneVolume()
+{
+ qDebug() << "Mixer::onSetMicrophoneVolume()";
+ disconnect(m_setMicrophoneVolume.data(), SIGNAL(closed()), this, SLOT(onSetMicrophoneVolume()));
+ if (m_setMicrophoneVolume->messageType() == AfMsgType::RetOk && m_setMicrophoneVolume->value().isObject())
+ {
+ m_microphoneVolume = m_microphoneVolumePending;
+ qDebug() << "Mixer::onSetMicrophoneVolume() - ";
+ emit microphoneVolumeChanged();
+ }
+}
+
+
diff --git a/app/mixer.h b/app/mixer.h
new file mode 100644
index 0000000..2c25562
--- /dev/null
+++ b/app/mixer.h
@@ -0,0 +1,74 @@
+#ifndef MIXER_H
+#define MIXER_H
+
+#include <QObject>
+#include <QSharedPointer>
+#include <QVariantList>
+#include "qafbwsclient.h"
+#include "qafbwsmsg.h"
+
+class Mixer
+ : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QVariantList hals READ hals NOTIFY halsChanged)
+ Q_PROPERTY(QString activeHal READ activeHal WRITE setActiveHal NOTIFY activeHalChanged)
+
+ Q_PROPERTY(int masterVolume READ masterVolume WRITE setMasterVolume NOTIFY masterVolumeChanged)
+ Q_PROPERTY(int pcmVolume READ pcmVolume WRITE setPcmVolume NOTIFY pcmVolumeChanged)
+ Q_PROPERTY(int microphoneVolume READ microphoneVolume WRITE setMicrophoneVolume NOTIFY microphoneVolumeChanged)
+
+public:
+ explicit Mixer(QObject* parent = nullptr);
+ Mixer(const Mixer&) = delete;
+
+ Q_INVOKABLE void open(const QUrl& url);
+ Q_INVOKABLE QVariantList hals() const;
+ Q_INVOKABLE QString activeHal() const;
+ Q_INVOKABLE void setActiveHal(QString h);
+
+ Q_INVOKABLE int masterVolume() const;
+ Q_INVOKABLE int pcmVolume() const;
+ Q_INVOKABLE int microphoneVolume() const;
+
+ Q_INVOKABLE void setMasterVolume(int v);
+ Q_INVOKABLE void setPcmVolume(int v);
+ Q_INVOKABLE void setMicrophoneVolume(int v);
+
+signals:
+ void halsChanged();
+ void activeHalChanged();
+ void masterVolumeChanged();
+ void pcmVolumeChanged();
+ void microphoneVolumeChanged();
+
+private slots:
+ void onClientConnected();
+ void onHalListClosed();
+ void onGetMasterVolume();
+ void onGetPcmVolume();
+ void onGetMicrophoneVolume();
+ void onSetMasterVolume();
+ void onSetPcmVolume();
+ void onSetMicrophoneVolume();
+
+private:
+ int m_masterVolume;
+ int m_pcmVolume;
+ int m_microphoneVolume;
+ int m_masterVolumePending;
+ int m_pcmVolumePending;
+ int m_microphoneVolumePending;
+ QAfbWsClient m_client;
+ QVariantList m_hallist;
+ QString m_activeHal;
+ QSharedPointer<QAfbWsMsg> m_alsacoreHallist;
+ QSharedPointer<QAfbWsMsg> m_getMasterVolume;
+ QSharedPointer<QAfbWsMsg> m_getPcmVolume;
+ QSharedPointer<QAfbWsMsg> m_getMicrophoneVolume;
+ QSharedPointer<QAfbWsMsg> m_setMasterVolume;
+ QSharedPointer<QAfbWsMsg> m_setPcmVolume;
+ QSharedPointer<QAfbWsMsg> m_setMicrophoneVolume;
+};
+
+#endif // MIXER_H
diff --git a/app/paclient.cpp b/app/paclient.cpp
deleted file mode 100644
index bd53cde..0000000
--- a/app/paclient.cpp
+++ /dev/null
@@ -1,329 +0,0 @@
-/*
- * Copyright (C) 2016,2017 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 "paclient.h"
-
-#include <QtCore/QDebug>
-
-PaClient::PaClient()
- : m_init(false), m_ml(nullptr), m_mlapi(nullptr), m_ctx(nullptr)
-{
-}
-
-PaClient::~PaClient()
-{
- if (m_init)
- close();
-}
-
-void PaClient::close()
-{
- if (!m_init) return;
- pa_threaded_mainloop_stop(m_ml);
- pa_threaded_mainloop_free(m_ml);
- m_init = false;
-}
-
-static void set_sink_volume_cb(pa_context *c, int success, void *data)
-{
- Q_UNUSED(data);
-
- if (!success)
- qWarning() << "PaClient: set sink volume: " <<
- pa_strerror(pa_context_errno(c));
-}
-
-static void set_source_volume_cb(pa_context *c, int success, void *data)
-{
- Q_UNUSED(data);
-
- if (!success)
- qWarning() << "PaClient: set source volume: " <<
- pa_strerror(pa_context_errno(c));
-}
-
-void PaClient::setVolume(uint32_t type, uint32_t index, uint32_t channel, uint32_t volume)
-{
- pa_operation *o;
- pa_context *c = context();
- pa_cvolume *cvolume = NULL;
-
- if (type == C_SINK) {
- cvolume = m_sink_states.value(index);
- cvolume->values[channel] = volume;
- if (!(o = pa_context_set_sink_volume_by_index(c, index, cvolume, set_sink_volume_cb, NULL))) {
- qWarning() << "PaClient: set sink #" << index <<
- " channel #" << channel <<
- " volume: " << pa_strerror(pa_context_errno(c));
- return;
- }
- pa_operation_unref(o);
- } else if (type == C_SOURCE) {
- cvolume = m_source_states.value(index);
- cvolume->values[channel] = volume;
- if (!(o = pa_context_set_source_volume_by_index(c, index, cvolume, set_source_volume_cb, NULL))) {
- qWarning() << "PaClient: set source #" << index <<
- " channel #" << channel <<
- " volume: " << pa_strerror(pa_context_errno(c));
- return;
- }
- pa_operation_unref(o);
- }
-}
-
-void get_source_list_cb(pa_context *c,
- const pa_source_info *i,
- int eol,
- void *data)
-{
- int chan;
-
- PaClient *self = reinterpret_cast<PaClient*>(data);
-
- if (eol < 0) {
- qWarning() << "PaClient: get source list: " <<
- pa_strerror(pa_context_errno(c));
-
- self->close();
- return;
- }
-
- if (!eol) {
- self->addOneControlState(C_SOURCE, i->index, &i->volume);
- for (chan = 0; chan < i->channel_map.channels; chan++) {
- // NOTE: hide input control
- if (QString(i->name).endsWith("monitor"))
- continue;
-
- emit self->controlAdded(i->index, QString(i->name), QString(i->description),
- C_SOURCE, chan, channel_position_string[i->channel_map.map[chan]],
- i->volume.values[chan]);
- }
- }
-}
-
-void get_sink_list_cb(pa_context *c,
- const pa_sink_info *i,
- int eol,
- void *data)
-{
- PaClient *self = reinterpret_cast<PaClient*>(data);
- int chan;
-
- if(eol < 0) {
- qWarning() << "PaClient: get sink list: " <<
- pa_strerror(pa_context_errno(c));
- self->close();
- return;
- }
-
- if(!eol) {
- self->addOneControlState(C_SINK, i->index, &i->volume);
- for (chan = 0; chan < i->channel_map.channels; chan++) {
- emit self->controlAdded(i->index, QString(i->name), QString(i->description),
- C_SINK, chan, channel_position_string[i->channel_map.map[chan]],
- i->volume.values[chan]);
- }
- }
-}
-
-void get_sink_info_change_cb(pa_context *c,
- const pa_sink_info *i,
- int eol,
- void *data)
-{
- Q_UNUSED(c);
- Q_ASSERT(i);
- Q_ASSERT(data);
-
- if (eol) return;
-
- for (int chan = 0; chan < i->channel_map.channels; chan++) {
- PaClient *self = reinterpret_cast<PaClient*>(data);
- QHash<int, pa_cvolume *> states = self->sink_states();
- pa_cvolume *cvolume = states.value(i->index);
- // Check each channel for volume change
- if (cvolume->values[chan] != i->volume.values[chan]) {
- // On change, update cache and signal
- cvolume->values[chan] = i->volume.values[chan];
- emit self->volumeExternallyChanged(C_SINK, i->index, chan, i->volume.values[chan]);
- }
- }
-}
-
-void get_source_info_change_cb(pa_context *c,
- const pa_source_info *i,
- int eol,
- void *data)
-{
- Q_UNUSED(c);
- Q_ASSERT(i);
- Q_ASSERT(data);
-
- if (eol) return;
-
- for (int chan = 0; chan < i->channel_map.channels; chan++) {
- PaClient *self = reinterpret_cast<PaClient*>(data);
- QHash<int, pa_cvolume *> states = self->source_states();
- pa_cvolume *cvolume = states.value(i->index);
- // Check each channel for volume change
- if (cvolume->values[chan] != i->volume.values[chan]) {
- // On change, update cache and signal
- cvolume->values[chan] = i->volume.values[chan];
- emit self->volumeExternallyChanged(C_SOURCE, i->index, chan, i->volume.values[chan]);
- }
- }
-}
-
-
-void subscribe_cb(pa_context *c,
- pa_subscription_event_type_t type,
- uint32_t index,
- void *data)
-{
- pa_operation *o;
-
- if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) != PA_SUBSCRIPTION_EVENT_CHANGE) {
- qWarning("PaClient: unhandled subscribe event operation");
- return;
- }
-
- switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
- case PA_SUBSCRIPTION_EVENT_SINK:
- if (!(o = pa_context_get_sink_info_by_index(c, index, get_sink_info_change_cb, data))) {
- qWarning() << "PaClient: get sink info by index: " <<
- pa_strerror(pa_context_errno(c));
- return;
- }
- break;
- case PA_SUBSCRIPTION_EVENT_SOURCE:
- if (!(o = pa_context_get_source_info_by_index(c, index, get_source_info_change_cb, data))) {
- qWarning() << "PaClient: get source info by index: " <<
- pa_strerror(pa_context_errno(c));
- return;
- }
- break;
- default:
- qWarning("PaClient: unhandled subscribe event facility");
- }
-}
-
-void context_state_cb(pa_context *c, void *data)
-{
- pa_operation *o;
- PaClient *self = reinterpret_cast<PaClient*>(data);
-
- switch (pa_context_get_state(c)) {
- case PA_CONTEXT_CONNECTING:
- case PA_CONTEXT_AUTHORIZING:
- case PA_CONTEXT_SETTING_NAME:
- break;
- case PA_CONTEXT_READY:
- // Fetch the controls of interest
- if (!(o = pa_context_get_source_info_list(c, get_source_list_cb, data))) {
- qWarning() << "PaClient: get source info list: " <<
- pa_strerror(pa_context_errno(c));
- return;
- }
- pa_operation_unref(o);
- if (!(o = pa_context_get_sink_info_list(c, &get_sink_list_cb, data))) {
- qWarning() << "PaClient: get sink info list: " <<
- pa_strerror(pa_context_errno(c));
- return;
- }
- pa_operation_unref(o);
- pa_context_set_subscribe_callback(c, subscribe_cb, data);
- if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE), NULL, NULL))) {
- qWarning() << "PaClient: subscribe: " <<
- pa_strerror(pa_context_errno(c));
- return;
- }
- break;
- case PA_CONTEXT_TERMINATED:
- self->close();
- break;
-
- case PA_CONTEXT_FAILED:
- default:
- qCritical() << "PaClient: connection failed: " <<
- pa_strerror(pa_context_errno(c));
- self->close();
- break;
- }
-}
-
-void PaClient::init()
-{
- m_ml = pa_threaded_mainloop_new();
- if (!m_ml) {
- qCritical("PaClient: failed to create mainloop");
- return;
- }
-
- pa_threaded_mainloop_set_name(m_ml, "PaClient mainloop");
-
- m_mlapi = pa_threaded_mainloop_get_api(m_ml);
-
- lock();
-
- m_ctx = pa_context_new(m_mlapi, "Mixer");
- if (!m_ctx) {
- qCritical("PaClient: failed to create context");
- pa_threaded_mainloop_free(m_ml);
- return;
- }
- pa_context_set_state_callback(m_ctx, context_state_cb, this);
-
- if (pa_context_connect(m_ctx, 0, (pa_context_flags_t)0, 0) < 0) {
- qCritical("PaClient: failed to connect");
- pa_context_unref(m_ctx);
- pa_threaded_mainloop_free(m_ml);
- return;
- }
-
- if (pa_threaded_mainloop_start(m_ml) != 0) {
- qCritical("PaClient: failed to start mainloop");
- pa_context_unref(m_ctx);
- pa_threaded_mainloop_free(m_ml);
- return;
- }
-
- unlock();
-
- m_init = true;
-}
-
-void PaClient::addOneControlState(int type, int index, const pa_cvolume *cvolume)
-{
- pa_cvolume *cvolume_new = new pa_cvolume;
- cvolume_new->channels = cvolume->channels;
- for (int i = 0; i < cvolume->channels; i++)
- cvolume_new->values[i] = cvolume->values[i];
- if (type == C_SINK)
- m_sink_states.insert(index, cvolume_new);
- else if (type == C_SOURCE)
- m_source_states.insert(index, cvolume_new);
-}
-
-QHash<int, pa_cvolume *> PaClient::sink_states(void)
-{
- return m_sink_states;
-}
-
-QHash<int, pa_cvolume *> PaClient::source_states(void)
-{
- return m_source_states;
-}
diff --git a/app/paclient.h b/app/paclient.h
deleted file mode 100644
index 73137f2..0000000
--- a/app/paclient.h
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2016,2017 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 <pulse/pulseaudio.h>
-
-#include <QtCore/QHash>
-#include <QtCore/QObject>
-
-const char * const channel_position_string[] =
-{
- "Mono",
- "Front Left",
- "Front Right",
- "Center",
- "Rear Center",
- "Rear Left",
- "Rear Right",
- "LFE",
- "Left Center",
- "Right Center",
- "Side Left",
- "Side Right",
-};
-
-enum control_type
-{
- C_SOURCE,
- C_SINK
-};
-
-typedef struct
-{
- uint32_t type;
- uint32_t index;
- pa_cvolume cvolume;
-} CState;
-
-class PaClient : public QObject
-{
- Q_OBJECT
- public:
- PaClient();
- ~PaClient();
-
- void init();
- void close();
-
- inline pa_context *context() const
- {
- return m_ctx;
- }
-
- inline void lock()
- {
- pa_threaded_mainloop_lock(m_ml);
- }
-
- inline void unlock()
- {
- pa_threaded_mainloop_unlock(m_ml);
- }
-
- void addOneControlState(int type, int index, const pa_cvolume *cvolume);
-
- QHash<int, pa_cvolume *> sink_states();
- QHash<int, pa_cvolume *> source_states();
-
- public slots:
- void setVolume(uint32_t type, uint32_t index, uint32_t channel, uint32_t volume);
-
- signals:
- void controlAdded(int cindex, QString name, QString desc, int type, int channel, const char *cdesc, int volume);
- void volumeExternallyChanged(uint32_t type, uint32_t cindex, uint32_t channel, uint32_t volume);
-
- private:
- bool m_init;
- pa_threaded_mainloop *m_ml;
- pa_mainloop_api *m_mlapi;
- pa_context *m_ctx;
- QHash<int, pa_cvolume *> m_sink_states;
- QHash<int, pa_cvolume *> m_source_states;
-
- public slots:
-};
diff --git a/app/pacontrolmodel.cpp b/app/pacontrolmodel.cpp
deleted file mode 100644
index 9489052..0000000
--- a/app/pacontrolmodel.cpp
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (C) 2016,2017 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 "pacontrolmodel.h"
-
-PaControl::PaControl(const quint32 &cindex, const QString &name, const QString &desc, const quint32 &type, const quint32 &channel, const QString &cdesc, const quint32 &volume)
- : m_cindex(cindex), m_name(name), m_desc(desc), m_type(type), m_channel(channel), m_cdesc(cdesc), m_volume(volume)
-{
-}
-
-quint32 PaControl::cindex() const
-{
- return m_cindex;
-}
-
-QString PaControl::name() const
-{
- QStringList list = m_name.split(".");
-
- return list.at(1);
-}
-
-QString PaControl::desc() const
-{
- return m_desc;
-}
-
-quint32 PaControl::type() const
-{
- return m_type;
-}
-
-quint32 PaControl::channel() const
-{
- return m_channel;
-}
-
-QString PaControl::cdesc() const
-{
- return m_cdesc;
-}
-
-
-quint32 PaControl::volume() const
-{
- return m_volume;
-}
-
-// FIXME: Not all of these should be editable roles
-void PaControl::setCIndex(const QVariant &cindex)
-{
- m_cindex = cindex.toUInt();
-}
-
-void PaControl::setName(const QVariant &name)
-{
- m_name = name.toString();
-}
-
-void PaControl::setDesc(const QVariant &desc)
-{
- m_desc = desc.toString();
-}
-
-void PaControl::setType(const QVariant &type)
-{
- m_type = type.toUInt();
-}
-
-void PaControl::setChannel(const QVariant &channel)
-{
- m_channel = channel.toUInt();
-}
-
-void PaControl::setCDesc(const QVariant &cdesc)
-{
- m_cdesc = cdesc.toString();
-}
-
-void PaControl::setVolume(PaControlModel *pacm, const QVariant &volume)
-{
- if (volume != m_volume) {
- m_volume = volume.toUInt();
- if (pacm)
- emit pacm->volumeChanged(type(), cindex(), channel(), m_volume);
- }
-}
-
-PaControlModel::PaControlModel(QObject *parent)
- : QAbstractListModel(parent)
-{
-}
-
-void PaControlModel::addControl(const PaControl &control)
-{
- beginInsertRows(QModelIndex(), rowCount(), rowCount());
- m_controls << control;
- endInsertRows();
-}
-
-void PaControlModel::addOneControl(int cindex, QString name, QString desc, int type, int channel, const char *cdesc, int volume)
-{
- addControl(PaControl(cindex, name, desc, type, channel, cdesc, volume));
-}
-
-void PaControlModel::changeExternalVolume(uint32_t type, uint32_t cindex, uint32_t channel, uint32_t volume)
-{
- QList<PaControl>::iterator i;
- int row;
-
- for (i = m_controls.begin(), row = 0; i < m_controls.end(); ++i, ++row) {
- if ((i->type() == type) &&
- (i->cindex() == cindex) &&
- (i->channel() == channel)) {
- break;
- }
- }
-
- i->setVolume(NULL, QVariant(volume));
- QModelIndex qmindex = index(row);
- QVector<int> roles;
- roles.push_back(VolumeRole);
- emit dataChanged(qmindex, qmindex, roles);
-}
-
-int PaControlModel::rowCount(const QModelIndex & parent) const {
- Q_UNUSED(parent);
- return m_controls.count();
-}
-
-bool PaControlModel::setData(const QModelIndex &index, const QVariant &value, int role) {
- if (index.row() < 0 || index.row() >= m_controls.count())
- return false;
- PaControl &control = m_controls[index.row()];
- if (role == CIndexRole)
- control.setCIndex(value);
- else if (role == NameRole)
- control.setName(value);
- else if (role == DescRole)
- control.setDesc(value);
- else if (role == TypeRole)
- control.setType(value);
- else if (role == ChannelRole)
- control.setChannel(value);
- else if (role == CDescRole)
- control.setCDesc(value);
- else if (role == VolumeRole)
- control.setVolume(this, value);
- QVector<int> roles;
- roles.push_back(role);
- emit dataChanged(index, index, roles);
- return true;
-}
-
-QVariant PaControlModel::data(const QModelIndex & index, int role) const {
- if (index.row() < 0 || index.row() >= m_controls.count())
- return QVariant();
-
- const PaControl &control = m_controls[index.row()];
- if (role == CIndexRole)
- return control.cindex();
- else if (role == NameRole)
- return control.name();
- else if (role == DescRole)
- return control.desc();
- else if (role == TypeRole)
- return control.type();
- else if (role == ChannelRole)
- return control.channel();
- else if (role == CDescRole)
- return control.cdesc();
- else if (role == VolumeRole)
- return control.volume();
- return QVariant();
-}
-
-Qt::ItemFlags PaControlModel::flags(const QModelIndex &index) const
-{
- if (!index.isValid())
- return Qt::ItemIsEnabled;
-
- return QAbstractListModel::flags(index) | Qt::ItemIsEditable;
-}
-
-QHash<int, QByteArray> PaControlModel::roleNames() const {
- QHash<int, QByteArray> roles;
- roles[CIndexRole] = "cindex";
- roles[NameRole] = "name";
- roles[DescRole] = "desc";
- roles[TypeRole] = "type";
- roles[ChannelRole] = "channel";
- roles[CDescRole] = "cdesc";
- roles[VolumeRole] = "volume";
- return roles;
-}
diff --git a/app/pacontrolmodel.h b/app/pacontrolmodel.h
deleted file mode 100644
index 81eb70b..0000000
--- a/app/pacontrolmodel.h
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2016,2017 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 <pulse/pulseaudio.h>
-
-#include <QtCore/QAbstractListModel>
-#include <QtCore/QList>
-
-class PaControlModel;
-
-class PaControl
-{
- public:
- PaControl(const quint32 &index, const QString &name, const QString &desc, const quint32 &type, const quint32 &channel, const QString &cdesc, const quint32 &volume);
-
- quint32 cindex() const;
- QString name() const;
- QString desc() const;
- quint32 type() const;
- quint32 channel() const;
- QString cdesc() const;
- quint32 volume() const;
- void setCIndex(const QVariant&);
- void setName(const QVariant&);
- void setDesc(const QVariant&);
- void setType(const QVariant&);
- void setChannel(const QVariant&);
- void setCDesc(const QVariant&);
- void setVolume(PaControlModel *, const QVariant&);
-
- private:
- quint32 m_cindex;
- QString m_name;
- QString m_desc;
- quint32 m_type;
- quint32 m_channel;
- QString m_cdesc;
- quint32 m_volume;
-};
-
-class PaControlModel : public QAbstractListModel
-{
- Q_OBJECT
- public:
- enum PaControlRoles {
- CIndexRole = Qt::UserRole + 1,
- NameRole,
- DescRole,
- TypeRole,
- ChannelRole,
- CDescRole,
- VolumeRole
- };
-
- PaControlModel(QObject *parent = 0);
-
- void addControl(const PaControl &control);
-
- int rowCount(const QModelIndex &parent = QModelIndex()) const;
-
- QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
-
- bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
-
- Qt::ItemFlags flags(const QModelIndex &index) const;
-
- public slots:
- void addOneControl(int cindex, QString name, QString desc, int type, int channel, const char *cdesc, int volume);
- void changeExternalVolume(uint32_t type, uint32_t cindex, uint32_t chan, uint32_t volume);
-
- signals:
- void volumeChanged(uint32_t type, uint32_t index, uint32_t channel, uint32_t volume);
-
- protected:
- QHash<int, QByteArray> roleNames() const;
- private:
- QList<PaControl> m_controls;
- pa_context *pa_ctx;
-};
diff --git a/app/qafbwsclient.cpp b/app/qafbwsclient.cpp
new file mode 100644
index 0000000..2816907
--- /dev/null
+++ b/app/qafbwsclient.cpp
@@ -0,0 +1,136 @@
+#include "qafbwsclient.h"
+#include "qafbwsmsg.h"
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QtDebug>
+
+QAfbWsClient::QAfbWsClient(QObject* parent)
+ : QObject{parent}
+ , m_nextCallId{0}
+{
+ connect(&m_socket, SIGNAL(connected()), this, SLOT(onSocketConnected()));
+ connect(&m_socket, SIGNAL(disconnected()), this, SLOT(onSocketDisconnected()));
+ connect(&m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError)));
+}
+
+QUrl QAfbWsClient::url() const
+{
+ return m_socket.requestUrl();
+}
+
+QSharedPointer<QAfbWsMsg> QAfbWsClient::call(const QString& api, const QString& verb, const QJsonValue& args)
+{
+ while(m_calls.constFind(m_nextCallId) != m_calls.cend()) m_nextCallId++; // Make sure that callId is not currently used
+
+ qDebug() << "QAfbWsClient::call(" << api << ", " << verb << ", " << args << ")";
+ QJsonArray arr;
+ arr.append(static_cast<int>(AfMsgType::Call));
+ arr.append(m_nextCallId);
+ arr.append(api + "/" + verb);
+ arr.append(args);
+
+ QJsonDocument doc;
+ doc.setArray(arr);
+
+ QSharedPointer<QAfbWsMsg> msg(new QAfbWsMsg(m_nextCallId, api, verb));
+ m_calls[m_nextCallId] = msg;
+
+ // TODO: handle failure of sendTextMessage
+ m_socket.sendTextMessage(doc.toJson(QJsonDocument::Compact));
+ qDebug() << "m_socket.sendTextMessage(" << doc.toJson(QJsonDocument::Compact) << ")";
+
+ m_nextCallId++;
+
+ return msg;
+}
+
+void QAfbWsClient::onSocketConnected()
+{
+ qDebug() << "QAfbWsClient::onSocketConnected()";
+ connect(&m_socket, SIGNAL(textMessageReceived(QString)), this, SLOT(onSocketTextMessageReceived(QString)));
+ emit connected();
+}
+
+void QAfbWsClient::onSocketDisconnected()
+{
+ qDebug() << "QAfbWsClient::onSocketDisconnected()";
+ m_nextCallId = 0;
+ disconnect(&m_socket, SIGNAL(textMessageReceived(QString)), this, SLOT(onSocketTextMessageReceived(QString)));
+ emit disconnected();
+}
+
+void QAfbWsClient::onSocketError(QAbstractSocket::SocketError err)
+{
+ qDebug() << "QAfbWsClient::onSocketError(" << err << ")";
+ emit error(err, m_socket.errorString());
+}
+
+void QAfbWsClient::onSocketTextMessageReceived(QString msg)
+{
+ qDebug() << "QAfbWsClient::onSocketTextMessageReceived(" << msg << ")";
+
+ if (msg.size() == 0)
+ {
+ qCritical() << "QAfbWsClient::onSocketTextMessageReceived: received an empty message.";
+ return;
+ }
+
+ QJsonDocument doc = QJsonDocument::fromJson(msg.toUtf8());
+ if (doc.isEmpty() || doc.isNull() || !doc.isArray())
+ {
+ qCritical() << "QAfbWsClient::onSocketTextMessageReceived: received an invalid message.";
+ return;
+ }
+
+ QJsonArray arr = doc.array();
+ AfMsgType msgType = static_cast<AfMsgType>(arr[0].toInt());
+ int callId = arr[1].isString() ? arr[1].toString().toInt() : arr[1].toInt();
+ QString api;
+ QJsonValue value;
+
+ switch(msgType)
+ {
+ case AfMsgType::Call:
+ qCritical() << "QAfbWsClient::onSocketTextMessageReceived: Client received a call, which should not happen.";
+ break;
+
+ case AfMsgType::Event:
+ qDebug() << "QAfbWsClient::onSocketTextMessageReceived: Client received an event.";
+ // TODO: handle events
+ value = arr[3];
+ break;
+
+ case AfMsgType::RetOk:
+ case AfMsgType::RetErr:
+ {
+ value = arr[2];
+ QMap<int, QSharedPointer<QAfbWsMsg>>::const_iterator it = m_calls.find(callId);
+ if (it == m_calls.end())
+ {
+ qCritical() << "QAfbWsClient::onSocketTextMessageReceived: received a response to a not found query.";
+ return;
+ }
+
+ QSharedPointer<QAfbWsMsg> m = *it;
+ m_calls.remove(callId);
+ m->close(msgType, value);
+ }
+ break;
+
+ default:
+ qCritical() << "QAfbWsClient::onSocketTextMessageReceived: Client received an unsupported message.";
+ break;
+ }
+}
+
+void QAfbWsClient::open(const QUrl& url)
+{
+ qDebug() << "QAfbWsClient::open(" << url << ")";
+ m_socket.open(url);
+}
+
+void QAfbWsClient::close()
+{
+ qDebug() << "QAfbWsClient::close()";
+ m_socket.close();
+}
diff --git a/app/qafbwsclient.h b/app/qafbwsclient.h
new file mode 100644
index 0000000..2576ecb
--- /dev/null
+++ b/app/qafbwsclient.h
@@ -0,0 +1,50 @@
+#ifndef QAFBWSCLIENT_H
+#define QAFBWSCLIENT_H
+
+#include <QObject>
+#include <QMap>
+#include <QSharedPointer>
+#include <QWebSocket>
+#include <QJsonValue>
+
+enum class AfMsgType
+ : int
+{
+ Call = 2,
+ RetOk = 3,
+ RetErr = 4,
+ Event = 5
+};
+
+class QAfbWsMsg;
+
+class QAfbWsClient
+ : public QObject
+{
+ Q_OBJECT
+public:
+ explicit QAfbWsClient(QObject* parent = nullptr);
+
+ void open(const QUrl& url);
+ void close();
+ QUrl url() const;
+ QSharedPointer<QAfbWsMsg> call(const QString &api, const QString &verb, const QJsonValue &args = QJsonValue());
+
+signals:
+ void connected();
+ void disconnected();
+ void error(QAbstractSocket::SocketError err, QString errStr);
+
+private slots:
+ void onSocketConnected();
+ void onSocketDisconnected();
+ void onSocketError(QAbstractSocket::SocketError err);
+ void onSocketTextMessageReceived(QString msg);
+
+private:
+ int m_nextCallId;
+ QWebSocket m_socket;
+ QMap<int, QSharedPointer<QAfbWsMsg>> m_calls;
+};
+
+#endif // QAFBWSCLIENT_H
diff --git a/app/qafbwsmsg.cpp b/app/qafbwsmsg.cpp
new file mode 100644
index 0000000..3f07cb0
--- /dev/null
+++ b/app/qafbwsmsg.cpp
@@ -0,0 +1,44 @@
+#include "qafbwsmsg.h"
+
+QAfbWsMsg::QAfbWsMsg(int callId, const QString& api, const QString& verb, QObject* parent)
+ : QObject{parent}
+ , m_callId{callId}
+{
+ m_api = api;
+ m_verb = verb;
+}
+
+void QAfbWsMsg::close(AfMsgType type, const QJsonValue& result)
+{
+ m_type = type;
+ m_value = result;
+
+ qDebug() << "QAfbWsMsg::close: type=" << static_cast<int>(m_type) << ", api=" << m_api << ", verb=" << m_verb << ", value=" << m_value;
+
+ emit closed();
+}
+
+int QAfbWsMsg::callId() const
+{
+ return m_callId;
+}
+
+AfMsgType QAfbWsMsg::messageType() const
+{
+ return m_type;
+}
+
+QString QAfbWsMsg::api() const
+{
+ return m_api;
+}
+
+QString QAfbWsMsg::verb() const
+{
+ return m_verb;
+}
+
+QJsonValue QAfbWsMsg::value() const
+{
+ return m_value;
+}
diff --git a/app/qafbwsmsg.h b/app/qafbwsmsg.h
new file mode 100644
index 0000000..6acce59
--- /dev/null
+++ b/app/qafbwsmsg.h
@@ -0,0 +1,33 @@
+#ifndef QAFBWSMSG_H
+#define QAFBWSMSG_H
+
+#include <QObject>
+#include <QJsonValue>
+#include "qafbwsclient.h"
+
+class QAfbWsMsg
+ : public QObject
+{
+ Q_OBJECT
+public:
+ explicit QAfbWsMsg(int callId = 0, const QString& api = "", const QString& verb = "", QObject* parent = nullptr);
+ void close(AfMsgType type, const QJsonValue& result);
+
+ int callId() const;
+ AfMsgType messageType() const;
+ QString api() const;
+ QString verb() const;
+ QJsonValue value() const;
+
+signals:
+ void closed();
+
+private:
+ int m_callId;
+ AfMsgType m_type;
+ QString m_api;
+ QString m_verb;
+ QJsonValue m_value;
+};
+
+#endif // QAFBWSMSG_H