OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
qml_serial_socket.hpp
1#pragma once
2#include <ossia/detail/variant.hpp>
3#include <ossia/network/context.hpp>
4#include <ossia/network/sockets/cobs_framing.hpp>
5#include <ossia/network/sockets/configuration.hpp>
6#include <ossia/network/sockets/encoding.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/serial_socket.hpp>
11#include <ossia/network/sockets/size_prefix_framing.hpp>
12#include <ossia/network/sockets/slip_framing.hpp>
13#include <ossia/network/sockets/stx_etx_framing.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 <algorithm>
25#include <mutex>
26#include <verdigris>
27
28namespace ossia::qt
29{
30
31class qml_serial_socket : public QObject
32{
33 W_OBJECT(qml_serial_socket)
34public:
35 // Unlike the TCP/UDP sockets, where the decoder is a separate object that can
36 // be swapped after construction, a serial_socket owns its encoder and decoder:
37 // the framing is therefore part of the socket's type and is chosen when the
38 // port is opened.
39 using socket_type = ossia::slow_variant<
40 ossia::net::serial_socket<ossia::net::no_framing>,
41 ossia::net::serial_socket<ossia::net::size_prefix_framing>,
42 ossia::net::serial_socket<ossia::net::slip_framing>,
43 ossia::net::serial_socket<ossia::net::line_framing>,
44 ossia::net::serial_socket<ossia::net::cobs_framing>,
45 ossia::net::serial_socket<ossia::net::stx_etx_framing>,
46 ossia::net::serial_socket<ossia::net::size_prefix_1byte_framing>,
47 ossia::net::serial_socket<ossia::net::size_prefix_2byte_be_framing>,
48 ossia::net::serial_socket<ossia::net::size_prefix_2byte_le_framing>,
49 ossia::net::serial_socket<ossia::net::size_prefix_4byte_le_framing>,
50 ossia::net::serial_socket<ossia::net::fixed_length_framing>>;
51
58 struct state : Nano::Observer
59 {
60 socket_type socket;
61 std::atomic_bool alive{true};
62 ossia::net::encoding enc{ossia::net::encoding::none};
63
66 std::mutex qt_mutex;
67 qml_serial_socket* self{};
68
69 void detach()
70 {
71 std::lock_guard g{qt_mutex};
72 self = nullptr;
73 }
74
75 template <typename F>
76 void post_to_qt(F&& f)
77 {
78 std::lock_guard g{qt_mutex};
79 if(self)
80 ossia::qt::run_async(self, std::forward<F>(f), Qt::AutoConnection);
81 }
82
84 void fire_open();
85 void fire_fail();
86 void fire_close();
87
88 state(
89 const ossia::net::serial_configuration& conf, boost::asio::io_context& ctx,
90 ossia::net::framing f, const std::string& delim, std::size_t frame_size,
91 ossia::net::encoding e)
92 // The alternatives hold references into their own serial_port, so the
93 // variant must be built in place and never moved afterwards.
94 : socket{make_socket(conf, ctx, f)}
95 , enc{e}
96 {
97 switch(f)
98 {
99 case ossia::net::framing::line_delimiter: {
100 char delimiter[8] = {};
101 std::copy_n(delim.begin(), std::min(delim.size(), std::size_t(7)), delimiter);
102
103 auto& sock = ossia::get<3>(socket);
104 std::copy_n(delimiter, 8, sock.m_encoder.delimiter);
105 std::copy_n(delimiter, 8, sock.m_decoder.delimiter);
106 break;
107 }
108 case ossia::net::framing::fixed_length:
109 if(frame_size > 0)
110 ossia::get<10>(socket).m_decoder.frame_size = frame_size;
111 break;
112 default:
113 break;
114 }
115 }
116
117 static socket_type make_socket(
118 const ossia::net::serial_configuration& conf, boost::asio::io_context& ctx,
119 ossia::net::framing f)
120 {
121 switch(f)
122 {
123 default:
124 case ossia::net::framing::none:
125 return socket_type{ossia::in_place_index<0>, conf, ctx};
126 case ossia::net::framing::size_prefix:
127 return socket_type{ossia::in_place_index<1>, conf, ctx};
128 case ossia::net::framing::slip:
129 return socket_type{ossia::in_place_index<2>, conf, ctx};
130 case ossia::net::framing::line_delimiter:
131 return socket_type{ossia::in_place_index<3>, conf, ctx};
132 case ossia::net::framing::cobs:
133 return socket_type{ossia::in_place_index<4>, conf, ctx};
134 case ossia::net::framing::stx_etx:
135 return socket_type{ossia::in_place_index<5>, conf, ctx};
136 case ossia::net::framing::size_prefix_1byte:
137 return socket_type{ossia::in_place_index<6>, conf, ctx};
138 case ossia::net::framing::size_prefix_2byte_be:
139 return socket_type{ossia::in_place_index<7>, conf, ctx};
140 case ossia::net::framing::size_prefix_2byte_le:
141 return socket_type{ossia::in_place_index<8>, conf, ctx};
142 case ossia::net::framing::size_prefix_4byte_le:
143 return socket_type{ossia::in_place_index<9>, conf, ctx};
144 case ossia::net::framing::fixed_length:
145 return socket_type{ossia::in_place_index<10>, conf, ctx};
146 }
147 }
148 };
149
150 struct receive_callback
151 {
152 std::shared_ptr<state> st;
153 QPointer<qml_serial_socket> self;
154 // Points to onMessage or onBytes on the QObject; null when no callback is
155 // set, the read loop then only keeping the port's error and close
156 // notifications flowing. Dereferenced on the Qt thread only: a QJSValue
157 // must not be touched from asio.
158 QJSValue* target;
159
160 void operator()(const unsigned char* data, std::size_t sz) const
161 {
162 if(!st->alive || !target)
163 return;
164 auto buf = apply_decoding(st->enc, data, sz);
165 auto cb = target;
166 st->post_to_qt([self = self, buf, cb] {
167 if(!self.get())
168 return;
169 if(cb->isCallable())
170 {
171 if(auto engine = qjsEngine(self.get()))
172 cb->call({engine->toScriptValue(buf)});
173 }
174 });
175 }
176 };
177
178 qml_serial_socket() { }
179
180 ~qml_serial_socket()
181 {
182 if(m_state)
183 {
184 m_state->alive = false;
185 // No asio -> Qt call may start from here on.
186 m_state->detach();
187 close();
188 }
189 }
190
191 bool isOpen() const noexcept { return m_state != nullptr; }
192
193 void open(
194 const ossia::net::serial_configuration& conf, boost::asio::io_context& ctx,
195 ossia::net::framing f = ossia::net::framing::none, const std::string& delim = {},
196 std::size_t frame_size = 0, ossia::net::encoding e = ossia::net::encoding::none)
197 {
198 // As on the other sockets, onBytes means "raw bytes, whatever the framing".
199 // The framing cannot be changed once the port is open, so the choice has to
200 // be made here rather than when the receive loop starts.
201 if(!onMessage.isCallable() && onBytes.isCallable())
202 f = ossia::net::framing::none;
203
204 m_state = std::make_shared<state>(conf, ctx, f, delim, frame_size, e);
205 m_state->self = this;
206
207 ossia::visit([st = m_state.get()](auto& sock) {
208 sock.on_open.template connect<&state::fire_open>(st);
209 sock.on_close.template connect<&state::fire_close>(st);
210 sock.on_fail.template connect<&state::fire_fail>(st);
211 }, m_state->socket);
212
213 try
214 {
215 ossia::visit([](auto& sock) { sock.connect(); }, m_state->socket);
216 }
217 catch(...)
218 {
219 // Opening a port that does not exist is the common failure here; leave
220 // the object unusable and let the caller report it.
221 m_state->alive = false;
222 m_state->detach();
223 m_state.reset();
224 throw;
225 }
226 }
227
228 void write(QByteArray buffer)
229 {
230 if(!m_state)
231 return;
232 auto st = m_state;
233 if(st->enc != ossia::net::encoding::none)
234 buffer = apply_encoding(st->enc, buffer);
235 boost::asio::dispatch(context(), [st, buffer = std::move(buffer)] {
236 if(st->alive)
237 ossia::visit(
238 [&](auto& sock) { sock.write(buffer.data(), buffer.size()); }, st->socket);
239 });
240 }
241 W_SLOT(write)
242
243 void close()
244 {
245 if(!m_state)
246 return;
247 auto st = m_state;
248 boost::asio::dispatch(
249 context(), [st] { ossia::visit([](auto& sock) { sock.close(); }, st->socket); });
250 }
251 W_SLOT(close)
252
253 void osc(QByteArray address, QJSValueList values)
254 {
255 if(!m_state)
256 return;
257
258 QByteArray packet;
259 buffer_writer bw{packet};
260 using send_visitor = ossia::net::osc_value_send_visitor<
261 ossia::net::full_parameter_data, ossia::net::osc_1_0_policy, buffer_writer>;
262
264 const std::string addr = address.toStdString();
265
266 switch(values.size())
267 {
268 case 0: {
269 ossia::value{ossia::impulse{}}.apply(send_visitor{p, addr, bw});
270 break;
271 }
272 case 1: {
273 auto v = ossia::qt::value_from_js(values[0]);
274 v.apply(send_visitor{p, addr, bw});
275 break;
276 }
277 default: {
278 std::vector<ossia::value> vec;
279 vec.reserve(values.size());
280 for(const auto& v : values)
281 vec.push_back(ossia::qt::value_from_js(v));
282 ossia::value vvec(std::move(vec));
283 vvec.apply(send_visitor{p, addr, bw});
284 }
285 }
286
287 write(packet);
288 }
289 W_SLOT(osc)
290
291 void on_open()
292 {
293 if(!m_state || !m_state->alive)
294 return;
295
296 auto st = m_state;
297 auto self = QPointer{this};
298 // The read loop is always armed: it is what makes the port's own stream
299 // notifications (errors, close) observable, even with no callback set.
300 // Which decoder runs was decided in open(): a serial_socket owns its
301 // framing.
302 QJSValue* target = onMessage.isCallable() ? &self.data()->onMessage
303 : onBytes.isCallable() ? &self.data()->onBytes
304 : nullptr;
305 ossia::visit([cb = receive_callback{st, self, target}](auto& sock) mutable {
306 sock.receive(std::move(cb));
307 }, st->socket);
308
309 if(onOpen.isCallable())
310 ossia::qt::run_async(this, [=, this] {
311 onOpen.call({qjsEngine(this)->newQObject(this)});
312 }, Qt::AutoConnection);
313 }
314
315 void on_fail()
316 {
317 if(!m_state || !m_state->alive)
318 return;
319 if(onError.isCallable())
320 ossia::qt::run_async(this, [=, this] { onError.call(); }, Qt::AutoConnection);
321 }
322
323 void on_close()
324 {
325 if(!m_state || !m_state->alive)
326 return;
327 if(onClose.isCallable())
328 ossia::qt::run_async(this, [=, this] { onClose.call(); }, Qt::AutoConnection);
329 }
330
331 QJSValue onOpen;
332 QJSValue onClose;
333 QJSValue onError;
334 QJSValue onMessage;
335 QJSValue onBytes; // raw bytes, ignores Framing (as on the other sockets)
336
337private:
338 boost::asio::io_context& context() const noexcept
339 {
340 return ossia::visit([](auto& sock) -> boost::asio::io_context& {
341 return sock.m_context;
342 }, m_state->socket);
343 }
344
345 std::shared_ptr<state> m_state;
346};
347
349{
350 std::lock_guard g{qt_mutex};
351 if(self)
352 self->on_open();
353}
354
355inline void qml_serial_socket::state::fire_fail()
356{
357 std::lock_guard g{qt_mutex};
358 if(self)
359 self->on_fail();
360}
361
362inline void qml_serial_socket::state::fire_close()
363{
364 std::lock_guard g{qt_mutex};
365 if(self)
366 self->on_close();
367}
368
369}
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_serial_socket.hpp:59
std::mutex qt_mutex
Definition qml_serial_socket.hpp:66
void fire_open()
Nano slots, always on the asio thread.
Definition qml_serial_socket.hpp:348