OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
qml_can_socket.hpp
1#pragma once
2#include <ossia/network/context.hpp>
3#include <ossia/network/sockets/can_socket.hpp>
4
5#if defined(__linux__)
6#include <ossia-qt/protocols/utils.hpp>
7
8#include <boost/asio/dispatch.hpp>
9
10#include <QByteArray>
11#include <QJSValue>
12#include <QObject>
13#include <QQmlEngine>
14#include <QVariant>
15
16#include <nano_observer.hpp>
17
18#include <verdigris>
19
20namespace ossia::qt
21{
22/*
23 var ifaces = Protocols.canInterfaces();
24
25 var bus = Protocols.can({
26 Transport: {
27 Interface: "vcan0",
28 FD: false, // CAN FD: payloads of up to 64 bytes
29 Loopback: true, // let the other local sockets see our frames
30 ReceiveOwnMessages: false,
31 ErrorFrames: false // deliver the kernel's bus error reports
32 },
33 // Omitting the mask means "exactly this id"; omitting Filters means
34 // "every frame". Filters are per-socket, so several buses may share one
35 // interface with different filters.
36 Filters: [ { id: 0x123, mask: 0x7FF } ],
37 onOpen: function(socket) { console.log("bus up"); },
38 onMessage: function(frame) {
39 console.log(frame.id.toString(16), frame.extended, frame.rtr, frame.fd, frame.bytes);
40 },
41 // Also called when a write fails, e.g. ENOBUFS when the interface's
42 // transmit queue is full -- that frame was not sent.
43 onError: function(e) { console.log("CAN error:", e); },
44 onClose: function() { }
45 });
46
47 bus.write({ id: 0x123, bytes: [1, 2, 3, 4] });
48 bus.write({ id: 0x18DAF110, extended: true, bytes: [0x02, 0x10, 0x01] });
49 bus.write({ id: 0x456, fd: true, brs: true, bytes: [ ... up to 64 ... ] });
50 // A remote transmission request carries no data, only a requested length:
51 bus.write({ id: 0x321, rtr: true, length: 8 });
52*/
53class qml_can_socket
54 : public QObject
55 , public Nano::Observer
56{
57 W_OBJECT(qml_can_socket)
58public:
59 struct state
60 {
61 ossia::net::can_socket socket;
62 std::atomic_bool alive{true};
63
64 state(const ossia::net::can_configuration& conf, boost::asio::io_context& ctx)
65 : socket{conf, ctx}
66 {
67 }
68 };
69
71 struct receive_callback
72 {
73 std::shared_ptr<state> st;
74 QPointer<qml_can_socket> self;
75
76 void operator()(const ossia::net::can_message& msg) const
77 {
78 if(!st->alive)
79 return;
80
81 QVariantMap frame;
82 frame["id"] = QVariant::fromValue(msg.id);
83 frame["extended"] = msg.extended;
84 frame["rtr"] = msg.remote;
85 frame["fd"] = msg.fd;
86 frame["error"] = msg.error;
87 frame["brs"] = msg.bitrate_switch;
88 frame["esi"] = msg.error_state;
89
90 // Reaches the script as an ArrayBuffer, like the serial protocol's
91 // payloads: `new Uint8Array(frame.bytes)`. RTR frames carry no data, so
92 // leave it empty rather than exposing `size` zeroes.
93 frame["bytes"] = msg.remote ? QByteArray{}
94 : QByteArray{
95 reinterpret_cast<const char*>(msg.data),
96 qsizetype(msg.size)};
97 frame["length"] = int(msg.size);
98
99 ossia::qt::run_async(self.get(), [self = self, frame] {
100 if(!self.get())
101 return;
102 if(self->onMessage.isCallable())
103 if(auto engine = qjsEngine(self.get()))
104 self->onMessage.call({engine->toScriptValue(frame)});
105 }, Qt::AutoConnection);
106 }
107 };
108
109 qml_can_socket() { }
110
111 ~qml_can_socket()
112 {
113 if(m_state)
114 {
115 m_state->alive = false;
116 close();
117 }
118 }
119
120 bool isOpen() const noexcept { return m_state != nullptr; }
121
122 void open(const ossia::net::can_configuration& conf, boost::asio::io_context& ctx)
123 {
124 m_state = std::make_shared<state>(conf, ctx);
125
126 auto& sock = m_state->socket;
127 sock.on_open.connect<&qml_can_socket::on_open>(this);
128 sock.on_close.connect<&qml_can_socket::on_close>(this);
129 sock.on_fail.connect<&qml_can_socket::on_fail>(this);
130 sock.on_write_error.connect<&qml_can_socket::on_write_error>(this);
131
132 try
133 {
134 sock.connect();
135 }
136 catch(...)
137 {
138 // Typically: the interface does not exist, or is not up. Leave the object
139 // unusable and let the caller report it.
140 m_state->alive = false;
141 m_state.reset();
142 throw;
143 }
144 }
145
147 void write(QVariant frame)
148 {
149 if(!m_state)
150 return;
151
152 const auto map = frame.toMap();
153 ossia::net::can_message msg;
154 msg.id = map["id"].toUInt();
155 msg.extended = map["extended"].toBool();
156 msg.remote = map["rtr"].toBool();
157 msg.fd = map["fd"].toBool();
158 msg.bitrate_switch = map["brs"].toBool();
159
160 const auto max = msg.fd ? CANFD_MAX_DLEN : CAN_MAX_DLEN;
161 int n = 0;
162 const auto bytes = map["bytes"];
163 // QML gives us either a JS array of numbers or, if the user built one, a
164 // string / ArrayBuffer that Qt converts to a QByteArray.
165 if(bytes.canConvert<QVariantList>() && bytes.typeId() != QMetaType::QByteArray)
166 {
167 for(const auto& b : bytes.toList())
168 {
169 if(n >= max)
170 break;
171 msg.data[n++] = uint8_t(b.toUInt() & 0xFF);
172 }
173 }
174 else
175 {
176 const auto arr = bytes.toByteArray();
177 for(char b : arr)
178 {
179 if(n >= max)
180 break;
181 msg.data[n++] = uint8_t(b);
182 }
183 }
184
185 // For an RTR frame there is no payload, so `length` carries the requested
186 // data length instead.
187 if(auto len = map["length"]; len.isValid())
188 msg.size = uint8_t(std::min(len.toInt(), max));
189 else
190 msg.size = uint8_t(n);
191
192 auto st = m_state;
193 boost::asio::dispatch(context(), [st, msg] {
194 if(st->alive)
195 st->socket.write(msg);
196 });
197 }
198 W_SLOT(write)
199
200 void close()
201 {
202 if(!m_state)
203 return;
204 auto st = m_state;
205 boost::asio::dispatch(context(), [st] { st->socket.close(); });
206 }
207 W_SLOT(close)
208
209 void on_open()
210 {
211 if(!m_state || !m_state->alive)
212 return;
213
214 auto self = QPointer{this};
215 if(onMessage.isCallable())
216 m_state->socket.receive(receive_callback{m_state, self});
217
218 if(onOpen.isCallable())
219 ossia::qt::run_async(this, [=, this] {
220 onOpen.call({qjsEngine(this)->newQObject(this)});
221 }, Qt::AutoConnection);
222 }
223
224 void on_fail()
225 {
226 if(!m_state || !m_state->alive)
227 return;
228 if(onError.isCallable())
229 ossia::qt::run_async(this, [=, this] {
230 onError.call({QStringLiteral("CAN socket failed")});
231 }, Qt::AutoConnection);
232 }
233
238 void on_write_error(boost::system::error_code ec)
239 {
240 if(!m_state || !m_state->alive)
241 return;
242 if(!onError.isCallable())
243 return;
244
245 const auto err = QString::fromStdString(ec.message());
246 ossia::qt::run_async(this, [self = QPointer{this}, err] {
247 if(self && self->onError.isCallable())
248 self->onError.call({QStringLiteral("write: ") + err});
249 }, Qt::AutoConnection);
250 }
251
252 void on_close()
253 {
254 if(!m_state || !m_state->alive)
255 return;
256 if(onClose.isCallable())
257 ossia::qt::run_async(this, [=, this] { onClose.call(); }, Qt::AutoConnection);
258 }
259
260 QJSValue onOpen;
261 QJSValue onClose;
262 QJSValue onError;
263 QJSValue onMessage;
264
265private:
266 boost::asio::io_context& context() const noexcept { return m_state->socket.m_context; }
267
268 std::shared_ptr<state> m_state;
269};
270
271}
272#endif
Definition qml_device.cpp:43
OSSIA_INLINE constexpr auto max(const T a, const U b) noexcept -> typename std::conditional<(sizeof(T) > sizeof(U)), T, U >::type
max function tailored for values
Definition math.hpp:96