History

Add unfinished OSCScriptingInterface

Ada committed 11 months ago

Commit 2faebb91 parent 0e1eb0b2 files · +237 −0Browse files

Showing the changes to interface/src/scripting/OSCScriptingInterface.cpp (1 of 2 files). Show all

Unified · Split

interface/src/scripting/OSCScriptingInterface.cpp+174−0
@@ -0,0 +1,174 @@
1+//
2+// OSCScriptingInterface.cpp
3+// interface/src/scripting
4+//
5+// Created by Ada <ada@thingvellir.net> on 2025-12-13
6+// Copyright 2025 Overte e.V.
7+//
8+// Distributed under the Apache License, Version 2.0.
9+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
10+//
11+
12+#include <QLoggingCategory>
13+#include <QRegularExpression>
14+#include <QtEndian>
15+
16+#include "ScriptEngine.h"
17+#include "OSCScriptingInterface.h"
18+
19+
20+Q_LOGGING_CATEGORY(osc, "overte.osc")
21+
22+enum OSCTag: char {
23+ Int = 'i',
24+ Float = 'f',
25+ String = 's',
26+ Blob = 'b',
27+};
28+
29+static const QRegularExpression invalidCharacters = QRegularExpression("([ #*,/?\\[\\]{}])");
30+static const QMap<QChar, QVariant::Type> typeNameMap = {
31+ { OSCTag::Int, QVariant::Int },
32+ { OSCTag::Float, QVariant::Double },
33+ { OSCTag::String, QVariant::String },
34+ { OSCTag::Blob, QVariant::ByteArray },
35+};
36+
37+
38+OSCScriptingInterface::OSCScriptingInterface(ScriptManager* parent) :
39+ _scriptManager(parent),
40+ _receivePort("osc/receivePort", 9000),
41+ _receiveHost("osc/receiveHost", "127.0.0.1"),
42+ _sendPort("osc/sendPort", 9001),
43+ _sendHost("osc/sendHost", "127.0.0.1"),
44+ _socket(std::make_shared<QUdpSocket>(this))
45+{
46+ _socket->bind(QHostAddress(_receiveHost.get()), _receivePort.get());
47+
48+ connect(_socket.get(), &QUdpSocket::readyRead, this, &OSCScriptingInterface::receivePacket);
49+
50+ qCInfo(osc) << "Listening on" << _socket->localAddress();
51+}
52+
53+OSCScriptingInterface::~OSCScriptingInterface() {
54+ _socket->close();
55+ qCInfo(osc) << "Closed listen socket";
56+}
57+
58+void OSCScriptingInterface::receivePacket() {
59+ // TODO
60+ emit messageReceived("/test", "", QVariantList());
61+}
62+
63+void OSCScriptingInterface::sendMessage(const QString& address, const QString &argumentTypes, const QVariantList& arguments) {
64+ Q_ASSERT(_socket);
65+
66+ auto scriptEngine = _scriptManager->engine();
67+
68+ if (address.length() < 2 || address[0] != '/') {
69+ scriptEngine->raiseException("address must be at least two characters and start with '/'");
70+ return;
71+ }
72+
73+ if (auto match = invalidCharacters.match(address); match.hasMatch()) {
74+ scriptEngine->raiseException(QString("address contains invalid character '%1'").arg(match.captured()));
75+ return;
76+ }
77+
78+ if (argumentTypes.length() != arguments.length()) {
79+ scriptEngine->raiseException(QString("argumentTypes.length (%1) does not match arguments.length (%2)").arg(argumentTypes.length()).arg(argumentTypes.length()));
80+ return;
81+ }
82+
83+ QByteArray bytes = address.toUtf8();
84+
85+ // pad to 4-byte boundary with at least one zero for the address string
86+ bytes.append((bytes.length() + 1) % 4, 0);
87+
88+ // comma at the start of the argument list, even if empty
89+ bytes.append(',');
90+
91+ QByteArray bodyBytes;
92+
93+ for (int i = 0; i < arguments.length(); i++) {
94+ auto arg = arguments[i];
95+
96+ if (!typeNameMap.contains(argumentTypes[i])) {
97+ scriptEngine->raiseException(QString("Unknown type tag '%1'").arg(argumentTypes[i]));
98+ return;
99+ }
100+
101+ auto expectedType = typeNameMap[argumentTypes[i]];
102+ if (expectedType != arg.type()) {
103+ scriptEngine->raiseException(QString("Specified argument type '%1' does not match value of type '%2'").arg(QVariant::typeToName(expectedType)).arg(arg.typeName()));
104+ return;
105+ }
106+
107+ switch (arg.type()) {
108+ case QVariant::Int: {
109+ bytes.append(OSCTag::Int);
110+
111+ auto* ptr = bodyBytes.data() + bodyBytes.length();
112+ bodyBytes.append(4, 0);
113+
114+ qToBigEndian(static_cast<qint32>(arg.toInt()), ptr);
115+ } break;
116+
117+ case QVariant::Double: {
118+ bytes.append(OSCTag::Float);
119+
120+ auto* ptr = bodyBytes.data() + bodyBytes.length();
121+ bodyBytes.append(4, 0);
122+
123+ // FIXME: use std::bit_cast instead of this hack once c++20 is in
124+ auto tmp = arg.toFloat();
125+ qToBigEndian(*reinterpret_cast<quint32*>(&tmp), ptr);
126+ } break;
127+
128+ case QVariant::String: {
129+ bytes.append(OSCTag::String);
130+
131+ auto stringArg = arg.toString();
132+
133+ if (stringArg.contains(QChar(0))) {
134+ scriptEngine->raiseException("String contains null (0x00) character");
135+ return;
136+ }
137+
138+ bodyBytes.append(stringArg.toUtf8());
139+ // pad to 4-byte boundary with at least one zero for the string
140+ bodyBytes.append((bodyBytes.length() + 1) % 4, 0);
141+ } break;
142+
143+ case QVariant::ByteArray: {
144+ bytes.append(OSCTag::Blob);
145+
146+ auto bytesArg = arg.toByteArray();
147+
148+ // write length prefix
149+ auto* ptr = bodyBytes.data() + bodyBytes.length();
150+ bodyBytes.append(4, 0);
151+ qToBigEndian(bytesArg.length(), ptr);
152+
153+ // write body
154+ bodyBytes.append(bytesArg);
155+
156+ // pad to 4-byte boundary with at least one zero for the string
157+ bodyBytes.append((bodyBytes.length() + 1) % 4, 0);
158+ } break;
159+
160+ // unreachable
161+ default:
162+ Q_ASSERT(false);
163+ break;
164+ }
165+ }
166+
167+ // pad to 4-byte boundary with at least one zero for the argument list string
168+ bytes.append((bytes.length() + 1) % 4, 0);
169+
170+ // pack the header + body data together for the datagram
171+ bytes.append(bodyBytes);
172+
173+ _socket->writeDatagram(bytes, QHostAddress(_sendHost.get()), _sendPort.get());
174+}