From eaac73ab2e116423e813484fa02715a1dfe08782 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Mon, 13 Nov 2023 08:13:07 +0100 Subject: [PATCH] feat: several enhancements * feat: added gdUnit4 addon * feat: added support for parsing OSC timetags to Unix time * feat: added more static types to function signatures * feat: OSCReceiver server handles up to `max_packets_per_poll` (default `10`) packets per poll interval. * feat: more than one callback can be registered and dispatched to for a single OSC Address * fix: correct padding for OSC strings * docs: added docstrings for exported variables Signed-off-by: Christopher Arndt --- .gitmodules | 3 +++ addons/gdUnit4 | 1 + gdUnit4 | 1 + osc_receiver.gd | 51 ++++++++++++++++++++++++++------------ osc_sender.gd | 11 ++++----- project.godot | 4 +++ test/osc_sender_test.gd | 55 +++++++++++++++++++++++++++++++++++++++++ ui.gd | 12 +++++++++ 8 files changed, 116 insertions(+), 22 deletions(-) create mode 100644 .gitmodules create mode 120000 addons/gdUnit4 create mode 160000 gdUnit4 create mode 100644 test/osc_sender_test.gd diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..83a2c98 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "gdUnit4"] + path = gdUnit4 + url = https://github.com/MikeSchulze/gdUnit4 diff --git a/addons/gdUnit4 b/addons/gdUnit4 new file mode 120000 index 0000000..9b73094 --- /dev/null +++ b/addons/gdUnit4 @@ -0,0 +1 @@ +../gdUnit4/addons/gdUnit4 \ No newline at end of file diff --git a/gdUnit4 b/gdUnit4 new file mode 160000 index 0000000..3fb08ac --- /dev/null +++ b/gdUnit4 @@ -0,0 +1 @@ +Subproject commit 3fb08ac9ef5f45eedfe896dc7826aff756d18f10 diff --git a/osc_receiver.gd b/osc_receiver.gd index e73353c..52fb68d 100644 --- a/osc_receiver.gd +++ b/osc_receiver.gd @@ -6,11 +6,16 @@ class_name OSCReceiver @export var default_address:String = "*" @export var default_port: int = 9001 @export var poll_interval: float = 0.05 +@export var max_packets_per_poll: int = 10 @export var debug: bool = false var _server: UDPServer var _observers: Dictionary var _timer: Timer - +# UNIX_EPOCH = 1970-01-01 00:00:00 +# NTP_EPOCH = 1900-01-01 00:00:00 +# NTP_DELTA = UNIX_EPOCH - NTP_EPOCH in seconds +const NTP_DELTA = 2208988800 +const ISIZE = 4294967296 # 2**32 func _init(): _observers = {} @@ -26,7 +31,7 @@ func _exit_tree(): stop_server() -func register_callback(osc_address, arg_types, callback) -> void: +func register_callback(osc_address: String, arg_types: String, callback: Callable) -> void: if not _observers.has([osc_address, arg_types]): _observers[[osc_address, arg_types]] = [] @@ -64,11 +69,14 @@ func _poll() -> void: _server.poll() - if _server.is_connection_available(): + var num_packets = 0 + + while num_packets < max_packets_per_poll and _server.is_connection_available(): var peer: PacketPeerUDP = _server.take_connection() var packet = peer.get_packet() var sender_ip = peer.get_packet_ip() var sender_port = peer.get_packet_port() + num_packets += 1 _debug("Accepted peer: %s:%s" % [sender_ip, sender_port]) @@ -80,7 +88,11 @@ func _poll() -> void: _debug("OSC address: %s" % address) _debug("OSC arg types: %s" % types) - if _observers.has([address, types]): + var callbacks = _observers.get([address, types], []) + # Get callbacks registered with types wild-card. + callbacks.append_array(_observers.get([address, "*"], [])) + + if callbacks: result = _parse_osc_values(packet.slice(offset), types) if result[0] != OK: @@ -95,17 +107,17 @@ func _poll() -> void: "types": types } - for callback in _observers[[address, types]]: + for callback in callbacks: callback.call(msg_info, result[1]) func _parse_osc_addr_and_types(packet: PackedByteArray) -> Array: var asep = packet.find(0) - var address = packet.slice(0, asep).get_string_from_ascii() - + var address = packet.slice(0, asep).get_string_from_ascii() + var toffset = asep + (4 - asep % 4) assert(char(packet.decode_u8(toffset)) == ",") var tsep = packet.find(0, toffset) - var types = packet.slice(toffset + 1, tsep).get_string_from_ascii() + var types = packet.slice(toffset + 1, tsep).get_string_from_ascii() return [address, types, tsep + (4 - tsep % 4)] @@ -145,7 +157,7 @@ func _parse_osc_values(packet: PackedByteArray, types: String) -> Array: "b": var count = stream.get_u32() result = stream.get_data(count) - + if result[0] == OK: values.append(result[1]) @@ -155,13 +167,9 @@ func _parse_osc_values(packet: PackedByteArray, types: String) -> Array: _debug("Could not read OSC blob argument.") return [ERR_PARSE_ERROR] "t": - result = stream.get_data(8) - - if result[0] == OK: - values.append(result[1]) - else: - _debug("Could not read OSC timetag argument.") - return [ERR_PARSE_ERROR] + var sec = stream.get_u32() + var frac = stream.get_u32() + values.append(to_time(sec, frac)) "m", "r": values.append([ stream.get_u8(), @@ -180,3 +188,14 @@ func _parse_osc_values(packet: PackedByteArray, types: String) -> Array: return [ERR_INVALID_DATA] return [OK, values] + + +## Return seconds and fractional part of NTP timestamp as 2-item array. +func to_frac(timestamp) -> Array: + var sec = int(timestamp) + return [sec, int(abs(timestamp - sec) * ISIZE)] + + +## Return NTP timestamp from integer seconds and fractional part. +func to_time(sec: int, frac: int) -> float: + return sec + float(frac) / ISIZE diff --git a/osc_sender.gd b/osc_sender.gd index 935ff1e..bbf4867 100644 --- a/osc_sender.gd +++ b/osc_sender.gd @@ -11,10 +11,8 @@ var _port: int var _socket = PacketPeerUDP.new() -func _init(): - _address = default_address - _port = default_port - _socket.set_dest_address(_address, _port) +func _init(dest = [default_address, default_port]): + set_destination(dest) func _debug(msg) -> void: @@ -35,7 +33,7 @@ func set_destination(dest) -> void: _port = dest[1] -func create_message(osc_address: String, arg_types: String, values: Array) -> Array: +func create_message(osc_address: String, arg_types: String = "", values: Array = []) -> Array: if not osc_address.begins_with("/"): return [ERR_INVALID_DATA] @@ -82,7 +80,7 @@ func create_message(osc_address: String, arg_types: String, values: Array) -> Ar return [OK, buf.get_data_array()] -func send_osc(osc_address: String, arg_types: String, values: Array, dest = null) -> Error: +func send_osc(osc_address: String, arg_types: String = "", values: Array = [], dest = null) -> Error: var res = create_message(osc_address, arg_types, values) if res[0] != OK: @@ -98,6 +96,7 @@ func send_osc(osc_address: String, arg_types: String, values: Array, dest = null func _pack_string(buf: StreamPeerBuffer, s: String) -> void: ## Pack a string into a binary OSC buffer buf.put_data(s.to_ascii_buffer()) + buf.put_u8(0) # pad to next 32-bit offset while buf.get_position() % 4: diff --git a/project.godot b/project.godot index bc89283..dce6973 100644 --- a/project.godot +++ b/project.godot @@ -26,6 +26,10 @@ window/size/viewport_height=800 window/stretch/mode="canvas_items" window/handheld/orientation=1 +[editor_plugins] + +enabled=PackedStringArray("res://addons/gdUnit4/plugin.cfg") + [input_devices] pointing/emulate_touch_from_mouse=true diff --git a/test/osc_sender_test.gd b/test/osc_sender_test.gd new file mode 100644 index 0000000..9663626 --- /dev/null +++ b/test/osc_sender_test.gd @@ -0,0 +1,55 @@ +# GdUnit4 TestSuite +class_name OscSenderTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite for +const __source = 'res://osc_sender.gd' + + +func test_create_message_minimal() -> void: + var sender = OSCSender.new() + var result = sender.create_message("/") + assert_bool(result[0] == OK).is_true() + assert_int(result[1].size()).is_equal(8) + assert_str(result[1].get_string_from_ascii()).is_equal("/") + assert_str(result[1].slice(4).get_string_from_ascii()).is_equal(",") + assert_array(Array(result[1])).is_equal([47, 0, 0 , 0, 44, 0, 0, 0]) + sender.free() + +func test_address_padding0() -> void: + var sender = OSCSender.new() + var result = sender.create_message("/f12") + assert_bool(result[0] == OK).is_true() + assert_int(result[1].size()).is_equal(8) + assert_str(result[1].get_string_from_ascii()).is_equal("/12") + assert_array(Array(result[1])).is_equal([47, 49, 50 , 0, 44, 0, 0, 0]) + sender.free() + +func test_address_padding1() -> void: + var sender = OSCSender.new() + var result = sender.create_message("/f1") + assert_bool(result[0] == OK).is_true() + assert_int(result[1].size()).is_equal(8) + assert_str(result[1].get_string_from_ascii()).is_equal("/1") + assert_array(Array(result[1])).is_equal([47, 49, 0 , 0, 44, 0, 0, 0]) + sender.free() + +func test_address_padding2() -> void: + var sender = OSCSender.new() + var result = sender.create_message("/1234") + assert_bool(result[0] == OK).is_true() + assert_int(result[1].size()).is_equal(12) + assert_str(result[1].get_string_from_ascii()).is_equal("/1234") + assert_array(Array(result[1].slice(0, 8))).is_equal([47, 49, 50 , 51, 52, 0, 0, 0]) + sender.free() + +func test_address_padding3() -> void: + var sender = OSCSender.new() + var result = sender.create_message("/123") + assert_bool(result[0] == OK).is_true() + assert_int(result[1].size()).is_equal(12) + assert_str(result[1].get_string_from_ascii()).is_equal("/123") + assert_array(Array(result[1].slice(0, 8))).is_equal([47, 49, 50 , 51, 0, 0, 0, 0]) + sender.free() diff --git a/ui.gd b/ui.gd index e27cad9..d8e5809 100644 --- a/ui.gd +++ b/ui.gd @@ -6,10 +6,22 @@ var osc_server: OSCReceiver const OSCSender = preload("res://osc_sender.gd") var osc_client: OSCSender +## The network address the OSCReceiver UDP server listens on. +## +## A string containing a hostname or IP address or a special wildcard. +## +## '*' (default) will make the server listen on all IPv4 and IPv6 interfaces +## '0.0.0.0' means all IPv4 and '::1' all IPv6 interfaces. @export var osc_server_address:String = "*" +## The port the OSCReceiver UDP server listens on. @export var osc_server_port: int = 9001 +## The address the OSCSender UDP client will send packets too. +## A string containing a hostname or IP address. @export var osc_dest_address:String = "127.0.0.1" +## The port the OSCSender UDP client will send packets too. @export var osc_dest_port: int = 9000 +## Setting this to true enables logging of received OSC message information +## and UI control value changes. @export var debug: bool = false