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/encoding.hpp>
6#include <ossia/network/sockets/cobs_framing.hpp>
7#include <ossia/network/sockets/fixed_length_framing.hpp>
8#include <ossia/network/sockets/line_framing.hpp>
9#include <ossia/network/sockets/no_framing.hpp>
10#include <ossia/network/sockets/size_prefix_framing.hpp>
11#include <ossia/network/sockets/slip_framing.hpp>
12#include <ossia/network/sockets/stx_etx_framing.hpp>
13#include <ossia/network/sockets/tcp_socket.hpp>
14#include <ossia/network/sockets/var_size_prefix_framing.hpp>
15
16#include <ossia-qt/protocols/utils.hpp>
17
18#include <QJSValue>
19#include <QObject>
20#include <QQmlEngine>
21
22#include <nano_observer.hpp>
23
24#include <mutex>
25
26#include <verdigris>
27
28namespace ossia::qt
29{
30class qml_tcp_outbound_socket : public QObject
31{
32 W_OBJECT(qml_tcp_outbound_socket)
33public:
34 using socket_t = boost::asio::ip::tcp::socket;
35 using decoder_type = ossia::slow_variant<
36 ossia::net::no_framing::decoder<socket_t>,
37 ossia::net::slip_decoder<socket_t>,
38 ossia::net::size_prefix_decoder<socket_t>,
39 ossia::net::line_framing_decoder<socket_t>,
40 ossia::net::cobs_decoder<socket_t>,
41 ossia::net::stx_etx_framing::decoder<socket_t>,
42 ossia::net::size_prefix_1byte_framing::decoder<socket_t>,
43 ossia::net::size_prefix_2byte_be_framing::decoder<socket_t>,
44 ossia::net::size_prefix_2byte_le_framing::decoder<socket_t>,
45 ossia::net::size_prefix_4byte_le_framing::decoder<socket_t>,
46 ossia::net::fixed_length_decoder<socket_t>>;
47
56 struct state : Nano::Observer
57 {
58 ossia::net::tcp_client socket;
59 std::atomic_bool alive{true};
60 // on_close is emitted at most once, be it on EOF or on an explicit close;
61 // both emitters run on the io_context thread.
62 std::atomic_bool closed{false};
63 ossia::net::framing framing{ossia::net::framing::none};
64 ossia::net::encoding enc{ossia::net::encoding::none};
65 char line_delimiter[8] = {};
66 decoder_type decoder;
67
68 state(
69 const ossia::net::outbound_socket_configuration& conf,
70 boost::asio::io_context& ctx,
71 ossia::net::framing f = ossia::net::framing::none,
72 const std::string& delim = {},
73 ossia::net::encoding e = ossia::net::encoding::none)
74 : socket{conf, ctx}
75 , decoder{ossia::in_place_index<0>, socket.m_socket}
76 {
77 framing = f;
78 enc = e;
79 if(!delim.empty())
80 {
81 auto sz = std::min(delim.size(), (size_t)7);
82 std::copy_n(delim.begin(), sz, line_delimiter);
83 }
84
85 switch(f)
86 {
87 default:
88 case ossia::net::framing::none:
89 break;
90 case ossia::net::framing::slip:
91 decoder.template emplace<1>(socket.m_socket);
92 break;
93 case ossia::net::framing::size_prefix:
94 decoder.template emplace<2>(socket.m_socket);
95 break;
96 case ossia::net::framing::line_delimiter:
97 decoder.template emplace<3>(socket.m_socket);
98 {
99 auto& dec = ossia::get<3>(decoder);
100 std::copy_n(line_delimiter, 8, dec.delimiter);
101 }
102 break;
103 case ossia::net::framing::cobs:
104 decoder.template emplace<4>(socket.m_socket);
105 break;
106 case ossia::net::framing::stx_etx:
107 decoder.template emplace<5>(socket.m_socket);
108 break;
109 case ossia::net::framing::size_prefix_1byte:
110 decoder.template emplace<6>(socket.m_socket);
111 break;
112 case ossia::net::framing::size_prefix_2byte_be:
113 decoder.template emplace<7>(socket.m_socket);
114 break;
115 case ossia::net::framing::size_prefix_2byte_le:
116 decoder.template emplace<8>(socket.m_socket);
117 break;
118 case ossia::net::framing::size_prefix_4byte_le:
119 decoder.template emplace<9>(socket.m_socket);
120 break;
121 case ossia::net::framing::fixed_length:
122 decoder.template emplace<10>(socket.m_socket);
123 if(!delim.empty())
124 ossia::get<10>(decoder).frame_size = std::stoul(delim);
125 break;
126 }
127 }
128
129 void write_encoded(const char* data, std::size_t sz)
130 {
131 switch(framing)
132 {
133 default:
134 case ossia::net::framing::none:
135 socket.write(data, sz);
136 break;
137 case ossia::net::framing::slip:
138 ossia::net::slip_encoder<socket_t>{socket.m_socket}.write(data, sz);
139 break;
140 case ossia::net::framing::size_prefix:
141 ossia::net::size_prefix_encoder<socket_t>{socket.m_socket}.write(data, sz);
142 break;
143 case ossia::net::framing::line_delimiter: {
144 ossia::net::line_framing_encoder<socket_t> enc{socket.m_socket};
145 std::copy_n(line_delimiter, 8, enc.delimiter);
146 enc.write(data, sz);
147 break;
148 }
149 case ossia::net::framing::cobs:
150 ossia::net::cobs_encoder<socket_t>{socket.m_socket}.write(data, sz);
151 break;
152 case ossia::net::framing::stx_etx:
153 ossia::net::stx_etx_framing::encoder<socket_t>{socket.m_socket}.write(data, sz);
154 break;
155 case ossia::net::framing::size_prefix_1byte:
156 ossia::net::size_prefix_1byte_framing::encoder<socket_t>{socket.m_socket}.write(
157 data, sz);
158 break;
159 case ossia::net::framing::size_prefix_2byte_be:
160 ossia::net::size_prefix_2byte_be_framing::encoder<socket_t>{socket.m_socket}
161 .write(data, sz);
162 break;
163 case ossia::net::framing::size_prefix_2byte_le:
164 ossia::net::size_prefix_2byte_le_framing::encoder<socket_t>{socket.m_socket}
165 .write(data, sz);
166 break;
167 case ossia::net::framing::size_prefix_4byte_le:
168 ossia::net::size_prefix_4byte_le_framing::encoder<socket_t>{socket.m_socket}
169 .write(data, sz);
170 break;
171 case ossia::net::framing::fixed_length:
172 ossia::net::fixed_length_encoder<socket_t>{socket.m_socket}.write(data, sz);
173 break;
174 }
175 }
176
180 std::mutex qt_mutex;
181 qml_tcp_outbound_socket* self{};
182
183 void detach()
184 {
185 std::lock_guard g{qt_mutex};
186 self = nullptr;
187 }
188
189 template <typename F>
190 void post_to_qt(F&& f)
191 {
192 std::lock_guard g{qt_mutex};
193 if(self)
194 ossia::qt::run_async(self, std::forward<F>(f), Qt::AutoConnection);
195 }
196
198 void fire_open();
199 void fire_fail();
200 void fire_close();
201
204 {
205 if(alive && !closed.exchange(true))
206 socket.on_close();
207 }
208 };
209
210 struct receive_callback
211 {
212 std::shared_ptr<state> st;
213 QPointer<qml_tcp_outbound_socket> self;
214 // Points to onMessage or onBytes on the QObject; null when no callback is
215 // set, the read loop then only observing the remote closing. Dereferenced
216 // on the Qt thread only: a QJSValue must not be touched from asio.
217 QJSValue* target;
218
219 void operator()(const unsigned char* data, std::size_t sz) const
220 {
221 if(!st->alive || !target)
222 return;
223 auto buf = apply_decoding(st->enc, data, sz);
224 // A frame that decodes to nothing is encoding metadata, not a message:
225 // the EOF record of Intel HEX / S-record firmware streams carries no
226 // payload. Unencoded frames are passed on as they are, empty or not.
227 if(buf.isEmpty() && st->enc != ossia::net::encoding::none)
228 return;
229 auto cb = target;
230 st->post_to_qt([self = self, buf, cb] {
231 if(!self.get())
232 return;
233 if(cb->isCallable())
234 {
235 auto engine = qjsEngine(self.get());
236 if(engine)
237 cb->call({engine->toScriptValue(buf)});
238 }
239 });
240 }
241
242 bool validate_stream(boost::system::error_code ec) const
243 {
244 if(!ec)
245 return true;
246 if(ec == boost::asio::error::operation_aborted)
247 return false;
248
249 // Any other error ends this stream -- eof on a graceful close,
250 // connection_reset on an RST -- and stops the read loop, so this is the
251 // only place where the close can be reported.
252 st->notify_closed();
253 return false;
254 }
255 };
256
257 qml_tcp_outbound_socket() { }
258
259 ~qml_tcp_outbound_socket()
260 {
261 if(m_state)
262 {
263 m_state->alive = false;
264 // No asio -> Qt call may start from here on: close() below still posts a
265 // shutdown, but its close notification is suppressed by `alive`.
266 m_state->detach();
267 close();
268 }
269 }
270
271 bool isOpen() const noexcept { return m_state != nullptr; }
272
273 void open(
274 const ossia::net::outbound_socket_configuration& conf,
275 boost::asio::io_context& ctx,
276 ossia::net::framing f = ossia::net::framing::none,
277 const std::string& delim = {},
278 ossia::net::encoding e = ossia::net::encoding::none)
279 {
280 m_state = std::make_shared<state>(conf, ctx, f, delim, e);
281 m_state->self = this;
282
283 try
284 {
285 if(onOpen.isCallable())
286 m_state->socket.on_open.connect<&state::fire_open>(m_state.get());
287 if(onClose.isCallable())
288 m_state->socket.on_close.connect<&state::fire_close>(m_state.get());
289 if(onError.isCallable())
290 m_state->socket.on_fail.connect<&state::fire_fail>(m_state.get());
291 m_state->socket.connect();
292 }
293 catch(const std::exception& e)
294 {
295 if(onError.isCallable())
296 {
297 onError.call({QString::fromStdString(e.what())});
298 }
299 }
300 }
301
302 void write(QByteArray buffer)
303 {
304 if(!m_state)
305 return;
306 auto st = m_state;
307 if(st->enc != ossia::net::encoding::none)
308 buffer = apply_encoding(st->enc, buffer);
309 boost::asio::dispatch(st->socket.m_context, [st, buffer = std::move(buffer)] {
310 if(st->alive)
311 st->write_encoded(buffer.data(), buffer.size());
312 });
313 }
314 W_SLOT(write)
315
316 void close()
317 {
318 if(!m_state)
319 return;
320 auto st = m_state;
321 boost::asio::post(st->socket.m_context, [st] {
322 try
323 {
324 st->socket.m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
325 }
326 catch(...)
327 {
328 }
329 st->socket.m_socket.close();
330 // The destructor comes through here too: notify_closed() checks `alive`,
331 // so it cannot schedule a script call into an object that is gone.
332 st->notify_closed();
333 });
334 }
335 W_SLOT(close)
336
337 void on_open()
338 {
339 if(!m_state || !m_state->alive)
340 return;
341
342 // Start receive loop:
343 // - onMessage uses the configured framing decoder
344 // - onBytes always uses no_framing (raw bytes, backward compatible)
345 // - onMessage takes priority if both are set
346 auto st = m_state;
347 auto self = QPointer{this};
348 // The read loop is always armed: it is what observes the remote closing.
349 // With no callback the cheapest decoder does, as nothing is dispatched.
350 if(onMessage.isCallable())
351 {
352 ossia::visit(
353 [cb = receive_callback{st, self, &self.data()->onMessage}](
354 auto& decoder) mutable { decoder.receive(std::move(cb)); },
355 st->decoder);
356 }
357 else
358 {
359 QJSValue* target = onBytes.isCallable() ? &self.data()->onBytes : nullptr;
360 st->decoder.template emplace<0>(st->socket.m_socket);
361 ossia::get<0>(st->decoder).receive(receive_callback{st, self, target});
362 }
363
364 ossia::qt::run_async(
365 this, [=, this] { onOpen.call({qjsEngine(this)->newQObject(this)}); },
366 Qt::AutoConnection);
367 }
368 void on_fail()
369 {
370 if(!m_state || !m_state->alive)
371 return;
372 ossia::qt::run_async(this, [=, this] { onError.call(); }, Qt::AutoConnection);
373 }
374 void on_close()
375 {
376 if(!m_state || !m_state->alive)
377 return;
378 ossia::qt::run_async(this, [=, this] { onClose.call(); }, Qt::AutoConnection);
379 }
380
381 void osc(QByteArray address, QJSValueList values)
382 {
383 if(!m_state)
384 return;
385
386 QByteArray packet;
387 buffer_writer bw{packet};
388 using send_visitor = ossia::net::osc_value_send_visitor<
389 ossia::net::full_parameter_data, ossia::net::osc_1_0_policy, buffer_writer>;
390
392 const std::string addr = address.toStdString();
393
394 switch(values.size())
395 {
396 case 0: {
397 ossia::value{ossia::impulse{}}.apply(send_visitor{p, addr, bw});
398 break;
399 }
400 case 1: {
401 auto v = ossia::qt::value_from_js(values[0]);
402 v.apply(send_visitor{p, addr, bw});
403 break;
404 }
405 default: {
406 std::vector<ossia::value> vec;
407 vec.reserve(values.size());
408 for(const auto& v : values)
409 vec.push_back(ossia::qt::value_from_js(v));
410 ossia::value vvec(std::move(vec));
411 vvec.apply(send_visitor{p, addr, bw});
412 }
413 }
414
415 write(packet);
416 }
417 W_SLOT(osc)
418
419 QJSValue onOpen;
420 QJSValue onClose;
421 QJSValue onError;
422 QJSValue onMessage;
423 QJSValue onBytes; // raw bytes, ignores Framing (backward compatible)
424
425private:
426 std::shared_ptr<state> m_state;
427};
428
429inline void qml_tcp_outbound_socket::state::fire_open()
430{
431 std::lock_guard g{qt_mutex};
432 if(self)
433 self->on_open();
434}
435
436inline void qml_tcp_outbound_socket::state::fire_fail()
437{
438 std::lock_guard g{qt_mutex};
439 if(self)
440 self->on_fail();
441}
442
443inline void qml_tcp_outbound_socket::state::fire_close()
444{
445 std::lock_guard g{qt_mutex};
446 if(self)
447 self->on_close();
448}
449
450}
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
Definition qml_tcp_outbound_socket.hpp:57
void fire_open()
Nano slots, always on the asio thread.
Definition qml_tcp_outbound_socket.hpp:429
std::mutex qt_mutex
Definition qml_tcp_outbound_socket.hpp:180
void notify_closed()
Single-shot close notification, whoever observed it first.
Definition qml_tcp_outbound_socket.hpp:203