OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
qml_tcp_outbound_socket.hpp
1#pragma once
2#include <ossia/detail/variant.hpp>
3#include <ossia/network/context.hpp>
4#include <ossia/network/sockets/configuration.hpp>
5#include <ossia/network/sockets/line_framing.hpp>
6#include <ossia/network/sockets/no_framing.hpp>
7#include <ossia/network/sockets/size_prefix_framing.hpp>
8#include <ossia/network/sockets/slip_framing.hpp>
9#include <ossia/network/sockets/tcp_socket.hpp>
10
11#include <ossia-qt/protocols/utils.hpp>
12
13#include <QJSValue>
14#include <QObject>
15#include <QQmlEngine>
16
17#include <nano_observer.hpp>
18
19#include <verdigris>
20
21namespace ossia::qt
22{
23class qml_tcp_outbound_socket
24 : public QObject
25 , public Nano::Observer
26{
27 W_OBJECT(qml_tcp_outbound_socket)
28public:
29 using socket_t = boost::asio::ip::tcp::socket;
30 using decoder_type = ossia::slow_variant<
31 ossia::net::no_framing::decoder<socket_t>,
32 ossia::net::slip_decoder<socket_t>,
33 ossia::net::size_prefix_decoder<socket_t>,
34 ossia::net::line_framing_decoder<socket_t>>;
35
36 struct state
37 {
38 ossia::net::tcp_client socket;
39 std::atomic_bool alive{true};
40 ossia::net::framing framing{ossia::net::framing::none};
41 char line_delimiter[8] = {};
42 decoder_type decoder;
43
44 state(
45 const ossia::net::outbound_socket_configuration& conf,
46 boost::asio::io_context& ctx,
47 ossia::net::framing f = ossia::net::framing::none,
48 const std::string& delim = {})
49 : socket{conf, ctx}
50 , decoder{ossia::in_place_index<0>, socket.m_socket}
51 {
52 framing = f;
53 if(!delim.empty())
54 {
55 auto sz = std::min(delim.size(), (size_t)7);
56 std::copy_n(delim.begin(), sz, line_delimiter);
57 }
58
59 switch(f)
60 {
61 default:
62 case ossia::net::framing::none:
63 break;
64 case ossia::net::framing::slip:
65 decoder.template emplace<1>(socket.m_socket);
66 break;
67 case ossia::net::framing::size_prefix:
68 decoder.template emplace<2>(socket.m_socket);
69 break;
70 case ossia::net::framing::line_delimiter:
71 decoder.template emplace<3>(socket.m_socket);
72 {
73 auto& dec = ossia::get<3>(decoder);
74 std::copy_n(line_delimiter, 8, dec.delimiter);
75 }
76 break;
77 }
78 }
79
80 void write_encoded(const char* data, std::size_t sz)
81 {
82 switch(framing)
83 {
84 default:
85 case ossia::net::framing::none:
86 socket.write(data, sz);
87 break;
88 case ossia::net::framing::slip: {
89 ossia::net::slip_encoder<socket_t> enc{socket.m_socket};
90 enc.write(data, sz);
91 break;
92 }
93 case ossia::net::framing::size_prefix: {
94 ossia::net::size_prefix_encoder<socket_t> enc{socket.m_socket};
95 enc.write(data, sz);
96 break;
97 }
98 case ossia::net::framing::line_delimiter: {
99 ossia::net::line_framing_encoder<socket_t> enc{socket.m_socket};
100 std::copy_n(line_delimiter, 8, enc.delimiter);
101 enc.write(data, sz);
102 break;
103 }
104 }
105 }
106 };
107
108 struct receive_callback
109 {
110 std::shared_ptr<state> st;
111 QPointer<qml_tcp_outbound_socket> self;
112 QJSValue* target; // points to onMessage or onBytes on the QObject
113
114 void operator()(const unsigned char* data, std::size_t sz) const
115 {
116 if(!st->alive)
117 return;
118 auto buf = QByteArray((const char*)data, sz);
119 auto cb = target;
120 ossia::qt::run_async(
121 self.get(),
122 [self = self, buf, cb] {
123 if(!self.get())
124 return;
125 if(cb->isCallable())
126 {
127 auto engine = qjsEngine(self.get());
128 if(engine)
129 cb->call({engine->toScriptValue(buf)});
130 }
131 },
132 Qt::AutoConnection);
133 }
134
135 bool validate_stream(boost::system::error_code ec) const
136 {
137 if(ec == boost::asio::error::operation_aborted)
138 return false;
139 if(ec == boost::asio::error::eof)
140 return false;
141 return true;
142 }
143 };
144
145 qml_tcp_outbound_socket() { }
146
147 ~qml_tcp_outbound_socket()
148 {
149 if(m_state)
150 {
151 m_state->alive = false;
152 close();
153 }
154 }
155
156 bool isOpen() const noexcept { return m_state != nullptr; }
157
158 void open(
159 const ossia::net::outbound_socket_configuration& conf,
160 boost::asio::io_context& ctx,
161 ossia::net::framing f = ossia::net::framing::none,
162 const std::string& delim = {})
163 {
164 m_state = std::make_shared<state>(conf, ctx, f, delim);
165
166 try
167 {
168 if(onOpen.isCallable())
169 m_state->socket.on_open.connect<&qml_tcp_outbound_socket::on_open>(this);
170 if(onClose.isCallable())
171 m_state->socket.on_close.connect<&qml_tcp_outbound_socket::on_close>(this);
172 if(onError.isCallable())
173 m_state->socket.on_fail.connect<&qml_tcp_outbound_socket::on_fail>(this);
174 m_state->socket.connect();
175 }
176 catch(const std::exception& e)
177 {
178 if(onError.isCallable())
179 {
180 onError.call({QString::fromStdString(e.what())});
181 }
182 }
183 }
184
185 void write(QByteArray buffer)
186 {
187 if(!m_state)
188 return;
189 auto st = m_state;
190 boost::asio::dispatch(st->socket.m_context, [st, buffer] {
191 if(st->alive)
192 st->write_encoded(buffer.data(), buffer.size());
193 });
194 }
195 W_SLOT(write)
196
197 void close()
198 {
199 if(!m_state)
200 return;
201 auto st = m_state;
202 boost::asio::post(st->socket.m_context, [st] {
203 try
204 {
205 st->socket.m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
206 }
207 catch(...)
208 {
209 }
210 st->socket.m_socket.close();
211 st->socket.on_close();
212 });
213 }
214 W_SLOT(close)
215
216 void on_open()
217 {
218 if(!m_state || !m_state->alive)
219 return;
220
221 // Start receive loop:
222 // - onMessage uses the configured framing decoder
223 // - onBytes always uses no_framing (raw bytes, backward compatible)
224 // - onMessage takes priority if both are set
225 auto st = m_state;
226 auto self = QPointer{this};
227 if(onMessage.isCallable())
228 {
229 ossia::visit(
230 [cb = receive_callback{st, self, &self.data()->onMessage}](
231 auto& decoder) mutable { decoder.receive(std::move(cb)); },
232 st->decoder);
233 }
234 else if(onBytes.isCallable())
235 {
236 // Override decoder to no_framing for raw bytes
237 st->decoder.template emplace<0>(st->socket.m_socket);
238 ossia::get<0>(st->decoder)
239 .receive(receive_callback{st, self, &self.data()->onBytes});
240 }
241
242 ossia::qt::run_async(
243 this, [=, this] { onOpen.call({qjsEngine(this)->newQObject(this)}); },
244 Qt::AutoConnection);
245 }
246 void on_fail()
247 {
248 if(!m_state || !m_state->alive)
249 return;
250 ossia::qt::run_async(this, [=, this] { onError.call(); }, Qt::AutoConnection);
251 }
252 void on_close()
253 {
254 if(!m_state || !m_state->alive)
255 return;
256 ossia::qt::run_async(this, [=, this] { onClose.call(); }, Qt::AutoConnection);
257 }
258
259 void osc(QByteArray address, QJSValueList values)
260 {
261 if(!m_state)
262 return;
263
264 QByteArray packet;
265 buffer_writer bw{packet};
266 using send_visitor = ossia::net::osc_value_send_visitor<
267 ossia::net::full_parameter_data, ossia::net::osc_1_0_policy, buffer_writer>;
268
270 const std::string addr = address.toStdString();
271
272 switch(values.size())
273 {
274 case 0: {
275 ossia::value{ossia::impulse{}}.apply(send_visitor{p, addr, bw});
276 break;
277 }
278 case 1: {
279 auto v = ossia::qt::value_from_js(values[0]);
280 v.apply(send_visitor{p, addr, bw});
281 break;
282 }
283 default: {
284 std::vector<ossia::value> vec;
285 vec.reserve(values.size());
286 for(const auto& v : values)
287 vec.push_back(ossia::qt::value_from_js(v));
288 ossia::value vvec(std::move(vec));
289 vvec.apply(send_visitor{p, addr, bw});
290 }
291 }
292
293 write(packet);
294 }
295 W_SLOT(osc)
296
297 QJSValue onOpen;
298 QJSValue onClose;
299 QJSValue onError;
300 QJSValue onMessage;
301 QJSValue onBytes; // raw bytes, ignores Framing (backward compatible)
302
303private:
304 std::shared_ptr<state> m_state;
305};
306
307}
The value class.
Definition value.hpp:173
Definition qml_device.cpp:43
Definition git_info.h:7
Full information about a parameter.
Definition parameter_data.hpp:61