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
20Q_LOGGING_CATEGORY(osc, "overte.osc")
21
22enum OSCTag: char {
23 Int = 'i',
24 Float = 'f',
25 String = 's',
26 Blob = 'b',
27};
28
29static const QRegularExpression invalidCharacters = QRegularExpression("([ #*,/?\\[\\]{}])");
30static 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
38OSCScriptingInterface::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
53OSCScriptingInterface::~OSCScriptingInterface() {
54 _socket->close();
55 qCInfo(osc) << "Closed listen socket";
56}
57
58void OSCScriptingInterface::receivePacket() {
59 // TODO
60 emit messageReceived("/test", "", QVariantList());
61}
62
63void 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}