diff --git a/.gitignore b/.gitignore
index 0af181c..0a3fc02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
# Godot 4+ specific ignores
.godot/
/android/
+*.blend?
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..d56d911
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,9 @@
+
+
+Copyright 2025 Hartmut Seichter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Notes.md b/Notes.md
deleted file mode 100644
index a90c629..0000000
--- a/Notes.md
+++ /dev/null
@@ -1,18 +0,0 @@
-# Resources
-
-- [vrpn-rs description of VRPN protocol](https://github.com/vrpn/vrpn-rs/blob/main/Protocol.md)
-
-
-# Design
-
-Allow for a session based design - data needs to be collected per block
-and immediatly made available. Minimal or no internal state keeping!
-
-- first collect names and ids of message_types and senders (stored in session)
-- register listeners in session
-- listeners inject captured data with signals
-
-
-- keep naming "classic" ??? ... above session is a tracker with a sender with sensors
-
-
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b5ddebe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,23 @@
+# gdvrpn
+
+A native GDScript implementation of the [VRPN](https://vrpn.github.io/) client protocol.
+
+# TOC
+
+# Background
+
+Various VR tools even today in use are providing an interface for tracking data by implementing a VPRN server. Binding a the actual VRPN library into an extension is proven and working but is quite a burden if the main aim of usage is receiving tracking data. Hence, this project was born to allow for easy integration of VRPN tracking in Godot.
+
+This project does not implement any server components and is only tested with a limited number of devices in our lab.
+
+# Install
+
+Install the project from the AssetLib in Godot or directly from the repository.
+
+# Usage
+
+TBW
+
+# License
+
+This project is licensed under the terms of the [MIT License](https://opensource.org/license/mit)
diff --git a/Root.tscn b/Root.tscn
deleted file mode 100644
index ddc3977..0000000
--- a/Root.tscn
+++ /dev/null
@@ -1,12 +0,0 @@
-[gd_scene load_steps=2 format=3 uid="uid://bj5ykdjle10tt"]
-
-[ext_resource type="Script" uid="uid://vnywsf0rn1ax" path="res://SocketClient.gd" id="1_gxo8o"]
-
-[node name="Node3D" type="Node3D"]
-script = ExtResource("1_gxo8o")
-vrpn_server = "212.201.64.122"
-
-[connection signal="connected" from="." to="." method="_on_connected"]
-[connection signal="data" from="." to="." method="_on_data"]
-[connection signal="disconnected" from="." to="." method="_on_disconnected"]
-[connection signal="error" from="." to="." method="_on_error"]
diff --git a/SocketClient.gd b/SocketClient.gd
deleted file mode 100644
index d75c83d..0000000
--- a/SocketClient.gd
+++ /dev/null
@@ -1,82 +0,0 @@
-extends Node
-
-signal connected(s:StreamPeerTCP)
-signal data(data:Array)
-signal disconnected
-signal error
-
-
-@onready var _stream: StreamPeerTCP = StreamPeerTCP.new()
-
-@export var vrpn_server : String = "localhost"
-@export var vrpn_port : int = 3883
-
-func _ready() -> void:
- self.connect_to_host(vrpn_server,vrpn_port)
-
-func _process(delta: float) -> void:
- var old_status = _stream.get_status()
- _stream.poll()
- var new_status = _stream.get_status()
- if old_status != new_status:
- match new_status:
- _stream.STATUS_NONE:
- emit_signal("disconnected")
- _stream.STATUS_CONNECTING:
- print("Connecting.")
- _stream.STATUS_CONNECTED:
- print("Connected.")
- emit_signal("connected",_stream.poll()
-)
- _stream.STATUS_ERROR:
- print("Error with socket stream.")
- emit_signal("error")
-
- if new_status == _stream.STATUS_CONNECTED:
- var available_bytes: int = _stream.get_available_bytes()
- if available_bytes > 0:
- var res = _stream.get_partial_data(available_bytes)
- if res[0] != OK:
- emit_signal("error")
- else:
- emit_signal("data", res[1])
-
-func connect_to_host(host: String, port: int) -> void:
- print("Connecting to %s:%d" % [host, port])
- if _stream.connect_to_host(host, port) != OK:
- print("Error connecting to host.")
- emit_signal("error")
-
-func send(data: PackedByteArray) -> bool:
- if _stream.get_status() != _stream.STATUS_CONNECTED:
- print("Error: Stream is not currently connected.")
- return false
- var error: int = _stream.put_data(data)
- if error != OK:
- print("Error writing to stream: ", error)
- return false
- return true
-
-
-func _on_data(data : Array):
- var bytes = PackedByteArray(data)
- var as_cookie = bytes.get_string_from_ascii()
-
- # Cookie Hack!
- if as_cookie.begins_with(VRPN.magic_cookie_start): #
- # kaboom we just send back the same cookie :)
- self.send(bytes)
- else:
- VRPN.marshall_block(bytes)
-
-
-
-func _on_connected(s : StreamPeerTCP):
- print("Connected to",s.get_connected_host()) # Replace with function body.
-
-func _on_disconnected():
- print("Disconnected") # Replace with function body.
-
-func _on_error():
- print("Error") # Replace with function body.
-
diff --git a/SocketClient.gd.uid b/SocketClient.gd.uid
deleted file mode 100644
index 2e69597..0000000
--- a/SocketClient.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://vnywsf0rn1ax
diff --git a/VRPN.gd b/VRPN.gd
deleted file mode 100644
index 4426d80..0000000
--- a/VRPN.gd
+++ /dev/null
@@ -1,77 +0,0 @@
-extends Node
-
-class_name VRPN
-
-const magic_cookie_start : String = "vrpn: ver."
-
-
-static func marshall_block(data : PackedByteArray) -> void:
-
- # need to fix that
- var block_offset : int = 0
- var header_size = aligned_size(20) # kinda redundant as we take the seq number as well
-
- while data.size() > block_offset:
- # reader for stream
- var header := StreamPeerBuffer.new()
- # get block addresses
- header.data_array = data.slice(block_offset,block_offset+header_size)
- # make sure we read as big endian
- header.big_endian = true
-
- # read header
- var length := header.get_32() as int # length of message
- var time_sec := header.get_32() as int # datetime sec
- var time_msec := header.get_32() as int # datetime micro sec
- var sender_id := header.get_32() as int # sender id
- var message_type := header.get_32() as int # type of message (payload)
- var sequence_num := header.get_32() as int # inofficial sequence number (padding)
-
- if true:
- print("length '%d'" % length)
- print("time_sec '%d'" % time_sec)
- print("time_msec '%d'" % time_msec)
- print("sender_id '%d'" % sender_id)
- print("message_type '%d'" % message_type)
- print("sequence_num '%d'" % sequence_num)
-
- # print
- print("block_offset:{0} header_size:{1} length:{2}".format([block_offset,header_size,length]))
-
- marshall_body(data.slice(block_offset+header_size,block_offset+length),message_type)
-
- # next datablock
- block_offset += aligned_size(length)
-
-
-static func marshall_body(data : PackedByteArray,message_type : int):
- var body := StreamPeerBuffer.new()
- body.data_array = data
- body.big_endian = true
- # sender description
- match message_type:
- -1,-2:
- # get length of string
- var body_length = body.get_32()
- # get string
- var sender_name = body.get_string(body_length)
- print("sender name is %s:" % sender_name)
- #print(body.data_array)
- 4: # quat pos
- # get id
- var sensor_id = body.get_32()
- var padding = body.get_32()
- var pos = Vector3(body.get_double(),body.get_double(),body.get_double())
- # VRPN quaternions are w,xyz
- var quat_w = body.get_double()
- var quat_x = body.get_double()
- var quat_y = body.get_double()
- var quat_z = body.get_double()
- var quat = Quaternion(quat_x,quat_y,quat_z,quat_w)
- print("sensor id {0} {1} {2}".format([sensor_id,pos,quat]))
- _:
- print("unhandled message type {0}".format([message_type]))
-
-
-static func aligned_size(actual_size : int, alignment : int = 8) -> int:
- return (actual_size + alignment - 1) & ~(alignment - 1)
diff --git a/addons/vrpn/assets/axis.blend b/addons/vrpn/assets/axis.blend
new file mode 100644
index 0000000..a54953e
Binary files /dev/null and b/addons/vrpn/assets/axis.blend differ
diff --git a/addons/vrpn/assets/axis.blend.import b/addons/vrpn/assets/axis.blend.import
new file mode 100644
index 0000000..393c9be
--- /dev/null
+++ b/addons/vrpn/assets/axis.blend.import
@@ -0,0 +1,62 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://b426fy7d6jw2d"
+path="res://.godot/imported/axis.blend-64f5b0cb53b3b69c5a0ce687708e9ae2.scn"
+
+[deps]
+
+source_file="res://addons/vrpn/assets/axis.blend"
+dest_files=["res://.godot/imported/axis.blend-64f5b0cb53b3b69c5a0ce687708e9ae2.scn"]
+
+[params]
+
+nodes/root_type=""
+nodes/root_name=""
+nodes/apply_root_scale=true
+nodes/root_scale=1.0
+nodes/import_as_skeleton_bones=false
+nodes/use_node_type_suffixes=true
+meshes/ensure_tangents=true
+meshes/generate_lods=true
+meshes/create_shadow_meshes=true
+meshes/light_baking=1
+meshes/lightmap_texel_size=0.2
+meshes/force_disable_compression=false
+skins/use_named_skins=true
+animation/import=true
+animation/fps=30
+animation/trimming=false
+animation/remove_immutable_tracks=true
+animation/import_rest_as_RESET=false
+import_script/path=""
+_subresources={
+"nodes": {
+"PATH:Camera": {
+"import/skip_import": true
+},
+"PATH:Light": {
+"import/skip_import": true
+}
+}
+}
+blender/nodes/visible=0
+blender/nodes/active_collection_only=false
+blender/nodes/punctual_lights=true
+blender/nodes/cameras=true
+blender/nodes/custom_properties=true
+blender/nodes/modifiers=1
+blender/meshes/colors=false
+blender/meshes/uvs=true
+blender/meshes/normals=true
+blender/meshes/export_geometry_nodes_instances=false
+blender/meshes/tangents=true
+blender/meshes/skins=2
+blender/meshes/export_bones_deforming_mesh_only=false
+blender/materials/unpack_enabled=true
+blender/materials/export_materials=1
+blender/animation/limit_playback=true
+blender/animation/always_sample=true
+blender/animation/group_tracks=true
diff --git a/addons/vrpn/docs/Notes.md b/addons/vrpn/docs/Notes.md
new file mode 100644
index 0000000..9854ae2
--- /dev/null
+++ b/addons/vrpn/docs/Notes.md
@@ -0,0 +1,24 @@
+# Resources
+
+Reference for implementing this library are from here [vrpn-rs description of VRPN protocol](https://github.com/vrpn/vrpn-rs/blob/main/Protocol.md)
+Unfortunately, there are some mistakes in the document, most notably it presumes that
+Quaternion in VRPN (quat library) are w,x,y,z - which is incorrect.
+
+# Design
+
+Allow for a session based design - data needs to be collected per block
+and immediatly made available. Minimal or no internal state keeping!
+
+- first collect names and ids of message_types and senders (stored in session)
+- register listeners in session
+- listeners inject captured data with signals
+
+- keep naming "classic" ??? ... above session is a tracker with a sender with sensors
+
+# Todo
+- [x] Testset for Quaternion rotations
+- [x] Axis as subscene
+
+# Internal Notes
+
+- Graphics Interaction Lab OptiTrack system is at 212.201.64.122
diff --git a/addons/vrpn/examples/optitrack.tscn b/addons/vrpn/examples/optitrack.tscn
new file mode 100644
index 0000000..819a329
--- /dev/null
+++ b/addons/vrpn/examples/optitrack.tscn
@@ -0,0 +1,47 @@
+[gd_scene load_steps=5 format=3 uid="uid://en7tpf1d6yak"]
+
+[ext_resource type="Script" uid="uid://dmq3i7qmo1qe0" path="res://addons/vrpn/scripts/VRPN.gd" id="1_jrm7s"]
+[ext_resource type="Script" uid="uid://dpj1wrvfsiq4v" path="res://addons/vrpn/scripts/VRPN_Receiver.gd" id="2_fp2uy"]
+[ext_resource type="PackedScene" uid="uid://b426fy7d6jw2d" path="res://addons/vrpn/assets/axis.blend" id="3_73ywu"]
+
+[sub_resource type="PlaneMesh" id="PlaneMesh_24d08"]
+size = Vector2(6, 2)
+
+[node name="Node3D" type="Node3D"]
+
+[node name="Camera3D" type="Camera3D" parent="."]
+transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0, 0.355791, -1.59348)
+current = true
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(-0.878275, 0.266876, -0.396749, -7.71365e-11, 0.829749, 0.558137, 0.478155, 0.490197, -0.728748, 0, 0.631436, 0)
+shadow_enabled = true
+
+[node name="Root" type="Node3D" parent="."]
+
+[node name="VRPN" type="Node3D" parent="Root" node_paths=PackedStringArray("tracker_receivers")]
+script = ExtResource("1_jrm7s")
+tracker_receivers = [NodePath("RB1"), NodePath("RB2")]
+vrpn_server = "212.201.64.122"
+
+[node name="RB1" type="Node3D" parent="Root/VRPN"]
+script = ExtResource("2_fp2uy")
+tracker_name = "RB1"
+
+[node name="axis" parent="Root/VRPN/RB1" instance=ExtResource("3_73ywu")]
+transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
+
+[node name="RB2" type="Node3D" parent="Root/VRPN"]
+script = ExtResource("2_fp2uy")
+tracker_name = "RB2"
+
+[node name="axis" parent="Root/VRPN/RB2" instance=ExtResource("3_73ywu")]
+transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
+
+[node name="Floor" type="MeshInstance3D" parent="Root"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.704947, 0)
+mesh = SubResource("PlaneMesh_24d08")
+
+[node name="Wall" type="MeshInstance3D" parent="Root"]
+transform = Transform3D(-1, 8.74228e-08, -3.82137e-15, 0, -4.37114e-08, -1, -8.74228e-08, -1, 4.37114e-08, 0, 0.268571, 1.06259)
+mesh = SubResource("PlaneMesh_24d08")
diff --git a/addons/vrpn/examples/spin_tracker.tscn b/addons/vrpn/examples/spin_tracker.tscn
new file mode 100644
index 0000000..cb3e429
--- /dev/null
+++ b/addons/vrpn/examples/spin_tracker.tscn
@@ -0,0 +1,70 @@
+[gd_scene load_steps=5 format=3 uid="uid://bj5ykdjle10tt"]
+
+[ext_resource type="Script" uid="uid://dmq3i7qmo1qe0" path="res://addons/vrpn/scripts/VRPN.gd" id="2_24d08"]
+[ext_resource type="Script" uid="uid://dpj1wrvfsiq4v" path="res://addons/vrpn/scripts/VRPN_Receiver.gd" id="2_170dk"]
+[ext_resource type="PackedScene" uid="uid://b426fy7d6jw2d" path="res://addons/vrpn/assets/axis.blend" id="3_170dk"]
+
+[sub_resource type="PlaneMesh" id="PlaneMesh_24d08"]
+size = Vector2(6, 2)
+
+[node name="Node3D" type="Node3D"]
+
+[node name="Camera3D" type="Camera3D" parent="."]
+transform = Transform3D(1, 0, -1.74846e-07, 0, 1, 0, 1.74846e-07, 0, 1, 0, 0.933082, 2.80616)
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(-0.999686, -0.0139775, 0.0207795, 0.0125216, 0.439603, 0.898105, -0.021688, 0.898083, -0.43929, 0, 1.59521, 0)
+shadow_enabled = true
+
+[node name="Root" type="Node3D" parent="."]
+
+[node name="VRPN" type="Node3D" parent="Root" node_paths=PackedStringArray("tracker_receivers")]
+script = ExtResource("2_24d08")
+tracker_receivers = [NodePath("../SpinTracker/Offset0/Tracker0"), NodePath("../SpinTracker/Tracker1"), NodePath("../SpinTracker/Offset2/Tracker2")]
+
+[node name="Floor" type="MeshInstance3D" parent="Root"]
+mesh = SubResource("PlaneMesh_24d08")
+
+[node name="SpinTracker" type="Node3D" parent="Root"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.764802, 0)
+
+[node name="Tracker1" type="Node3D" parent="Root/SpinTracker"]
+script = ExtResource("2_170dk")
+tracker_name = "Tracker1"
+tracker_use_position = false
+
+[node name="Axis" parent="Root/SpinTracker/Tracker1" instance=ExtResource("3_170dk")]
+transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
+
+[node name="Label-Y-Axis" type="Label3D" parent="Root/SpinTracker/Tracker1"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.759683, 0)
+text = "Y-Axis"
+
+[node name="Offset2" type="Node3D" parent="Root/SpinTracker"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0, 0)
+
+[node name="Tracker2" type="Node3D" parent="Root/SpinTracker/Offset2"]
+script = ExtResource("2_170dk")
+tracker_name = "Tracker2"
+tracker_use_position = false
+
+[node name="Axis" parent="Root/SpinTracker/Offset2/Tracker2" instance=ExtResource("3_170dk")]
+transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
+
+[node name="Label-Z-Axis" type="Label3D" parent="Root/SpinTracker/Offset2/Tracker2"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.764205, 0)
+text = "Z-Axis"
+
+[node name="Offset0" type="Node3D" parent="Root/SpinTracker"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0, 0)
+
+[node name="Tracker0" type="Node3D" parent="Root/SpinTracker/Offset0"]
+script = ExtResource("2_170dk")
+tracker_use_position = false
+
+[node name="axis" parent="Root/SpinTracker/Offset0/Tracker0" instance=ExtResource("3_170dk")]
+transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
+
+[node name="Label3D" type="Label3D" parent="Root/SpinTracker/Offset0/Tracker0"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.769847, 0)
+text = "X-Axis"
diff --git a/icon.svg b/addons/vrpn/icons/icon.svg
similarity index 100%
rename from icon.svg
rename to addons/vrpn/icons/icon.svg
diff --git a/icon.svg.import b/addons/vrpn/icons/icon.svg.import
similarity index 75%
rename from icon.svg.import
rename to addons/vrpn/icons/icon.svg.import
index 25183c1..3f3be88 100644
--- a/icon.svg.import
+++ b/addons/vrpn/icons/icon.svg.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://cdprcmtx102rp"
-path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+path="res://.godot/imported/icon.svg-bbc889e9147eb1676401b9ec3d05066e.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://icon.svg"
-dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+source_file="res://addons/vrpn/icons/icon.svg"
+dest_files=["res://.godot/imported/icon.svg-bbc889e9147eb1676401b9ec3d05066e.ctex"]
[params]
diff --git a/addons/vrpn/scripts/VRPN.gd b/addons/vrpn/scripts/VRPN.gd
new file mode 100644
index 0000000..eecd554
--- /dev/null
+++ b/addons/vrpn/scripts/VRPN.gd
@@ -0,0 +1,193 @@
+extends Node
+class_name VRPN
+
+# tracking associated data
+enum TrackingData { POS_QUAT, VELOCITY, ACCELERATION }
+
+# magic cookie
+const magic_cookie_start : String = "vrpn: ver."
+# kinda redundant as we take the seq number as well
+static var header_size : int = aligned_size(20)
+
+
+var sensors : Dictionary[int,String] = {}
+var messages : Dictionary[int,String] = {}
+
+signal connected(s:StreamPeerTCP)
+signal data(data:Array)
+signal disconnected
+signal error(msg:String)
+
+
+@onready var _stream: StreamPeerTCP = StreamPeerTCP.new()
+
+@export var tracker_receivers : Array[VRPN_Receiver] = []
+
+@export var vrpn_server : String = "127.0.0.1"
+@export var vrpn_port : int = 3883
+
+func _ready() -> void:
+
+ self.connect_to_host(vrpn_server,vrpn_port)
+
+ if not connected.has_connections():
+ connected.connect(self._on_connected)
+ if not data.has_connections():
+ data.connect(self._on_data)
+ if not disconnected.has_connections():
+ disconnected.connect(self._on_disconnected)
+ if not error.has_connections():
+ error.connect(self._on_error)
+
+func _process(delta: float) -> void:
+ # store old state
+ var old_status = _stream.get_status()
+ # poll
+ _stream.poll()
+ # receive all errors
+ var new_status = _stream.get_status()
+ # process if something changed
+ if old_status != new_status:
+ match new_status:
+ _stream.STATUS_NONE:
+ emit_signal("disconnected")
+ _stream.STATUS_CONNECTING:
+ pass
+ _stream.STATUS_CONNECTED:
+ emit_signal("connected",_stream)
+ _stream.STATUS_ERROR:
+ emit_signal("error","error with socket stream from {0}:{1}".format([_stream.get_connected_host(),_stream.get_connected_port()]))
+
+ if new_status == _stream.STATUS_CONNECTED:
+ var available_bytes: int = _stream.get_available_bytes()
+ if available_bytes > 0:
+ var res = _stream.get_partial_data(available_bytes)
+ if res[0] != OK:
+ emit_signal("error","error receiving data {0}".format([res[0]]))
+ else:
+ emit_signal("data", res[1])
+
+func connect_to_host(host: String, port: int) -> void:
+ if _stream.connect_to_host(host, port) != OK:
+ emit_signal("error","error connecting to host '{0}:{1}'".format([host,port]))
+
+func send(data: PackedByteArray) -> bool:
+ if _stream.get_status() != _stream.STATUS_CONNECTED:
+ emit_signal("error","stream not connected!")
+ return false
+ var res: int = _stream.put_data(data)
+ if res != OK:
+ emit_signal("error","error writing to stream {0}!".format([res]))
+ return false
+ return true
+
+
+func _on_data(data : Array):
+ var bytes = PackedByteArray(data)
+ var as_cookie = bytes.get_string_from_ascii()
+
+ # Cookie Hack!
+ if as_cookie.begins_with(VRPN.magic_cookie_start): #
+ # kaboom we just send back the same cookie :)
+ self.send(bytes)
+ else:
+ VRPN.marshall_block(bytes,self)
+
+
+func _on_connected(s : StreamPeerTCP):
+ print("Connected to",s.get_connected_host()) # Replace with function body.
+
+func _on_disconnected():
+ push_warning("Disconnected") # Replace with function body.
+
+func _on_error(msg:String):
+ push_warning(msg) # Replace with function body.
+
+
+static func marshall_block(data : PackedByteArray,session : VRPN) -> void:
+
+ # need to fix that
+ var block_offset : int = 0
+
+ while data.size() > block_offset:
+ # reader for stream
+ var header := StreamPeerBuffer.new()
+ # get block addresses
+ header.data_array = data.slice(block_offset,block_offset+header_size)
+ # make sure we read as big endian
+ header.big_endian = true
+
+ # read header
+ var length := header.get_32() as int # length of message
+ var time_sec := header.get_32() as int # datetime sec
+ var time_msec := header.get_32() as int # datetime micro sec
+ var sender_id := header.get_32() as int # sender id (tracker or interfaces)
+ var message_type := header.get_32() as int # type of message (payload)
+ var sequence_num := header.get_32() as int # inofficial sequence number (padding)
+
+ # for debugging
+ #print("length '%d'" % length)
+ #print("time_sec '%d'" % time_sec)
+ #print("time_msec '%d'" % time_msec)
+ #print("sender_id '%d'" % sender_id)
+ #print("message_type '%d'" % message_type)
+ #print("sequence_num '%d'" % sequence_num)
+
+ marshall_body(data.slice(block_offset+header_size,block_offset+length),message_type,sender_id,session)
+
+ # next datablock
+ block_offset += aligned_size(length)
+
+static func decode_string(stream : StreamPeerBuffer) -> String:
+ var len = stream.get_32()
+ return stream.get_string(len)
+
+
+static func marshall_body(data : PackedByteArray,message_type : int, sender_id: int, session : VRPN):
+ var body := StreamPeerBuffer.new()
+ body.data_array = data
+ body.big_endian = true
+
+ # only take message_type directly for negative (-1,-2)
+ # messages that provide dynamic descriptors
+ if message_type < 0:
+ # message and sender descriptions
+ match message_type:
+ -1: # sensor names
+ var name = decode_string(body)
+ print("sensor name is '%s' with '%d" % [name,sender_id])
+ session.sensors[sender_id] = name
+ -2: # message names
+ var name = decode_string(body)
+ print("message name is '%s' for message_type '%d'" % [name,sender_id])
+ session.messages[sender_id] = name
+ return
+
+ # now we use the string identifiers
+ # because they are supposedly dynamically assigned
+ match session.messages[message_type]:
+ 'vrpn_Tracker Pos_Quat': # quat pos
+ # get id
+ var sensor_id = body.get_32()
+ var padding = body.get_32() # padding
+ # position
+ var pos = Vector3(body.get_double(),body.get_double(),body.get_double())
+ # VRPN quat layout and Godot Quaternion c'tor identical with x,y,z,w
+ var quat = Quaternion(body.get_double(),body.get_double(),body.get_double(),body.get_double()).normalized()
+ # submit to listener
+ for r in session.tracker_receivers:
+ r._on_pos_quat({
+ "tracker" : session.sensors[sender_id],
+ "sensor" : sensor_id,
+ "position" : pos,
+ "rotation" : quat
+ })
+ 'vrpn_Tracker Velocity':
+ pass
+ _:
+ pass
+ #print("unhandled message type {0}".format([message_type]))
+
+
+static func aligned_size(actual_size : int, alignment : int = 8) -> int:
+ return (actual_size + alignment - 1) & ~(alignment - 1)
diff --git a/VRPN.gd.uid b/addons/vrpn/scripts/VRPN.gd.uid
similarity index 100%
rename from VRPN.gd.uid
rename to addons/vrpn/scripts/VRPN.gd.uid
diff --git a/addons/vrpn/scripts/VRPN_Receiver.gd b/addons/vrpn/scripts/VRPN_Receiver.gd
new file mode 100644
index 0000000..a9654e3
--- /dev/null
+++ b/addons/vrpn/scripts/VRPN_Receiver.gd
@@ -0,0 +1,18 @@
+extends Node3D
+
+class_name VRPN_Receiver
+
+@export var tracker_name : String = "Tracker0"
+@export var tracker_sensor : int = 0
+@export var tracker_use_position : bool = true
+@export var tracker_use_rotation : bool = true
+
+
+func _on_pos_quat(tracker_data : Dictionary):
+
+ if tracker_data['tracker'] == tracker_name and tracker_data['sensor'] == tracker_sensor:
+ if tracker_use_position:
+ self.global_position = tracker_data['position']
+ if tracker_use_rotation:
+ var rotation := tracker_data['rotation'] as Quaternion
+ self.global_basis = Basis(rotation)
diff --git a/addons/vrpn/scripts/VRPN_Receiver.gd.uid b/addons/vrpn/scripts/VRPN_Receiver.gd.uid
new file mode 100644
index 0000000..fcbe47b
--- /dev/null
+++ b/addons/vrpn/scripts/VRPN_Receiver.gd.uid
@@ -0,0 +1 @@
+uid://dpj1wrvfsiq4v
diff --git a/vrpn.test.cfg b/addons/vrpn/tests/vrpn.test.cfg
similarity index 99%
rename from vrpn.test.cfg
rename to addons/vrpn/tests/vrpn.test.cfg
index 51f8da3..caec7db 100644
--- a/vrpn.test.cfg
+++ b/addons/vrpn/tests/vrpn.test.cfg
@@ -40,7 +40,9 @@
# float z_of_axis_to_spin_around
# float rotation_rate_around_axis_in_Hz
-vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
+vrpn_Tracker_Spin Tracker0 1 200.0 1.0 0.0 0.0 0.1
+vrpn_Tracker_Spin Tracker1 1 200.0 0.0 1.0 0.0 0.2
+vrpn_Tracker_Spin Tracker2 1 200.0 0.0 0.0 1.0 0.3
################################################################################
# WintrackerIII from VR SPace
@@ -66,7 +68,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
# int number_of_buttons
# float rate_at_which_the_buttons_toggle (transitions/second)
-#vrpn_Button_Example Button0 2 2.0
+vrpn_Button_Example Button0 2 2.0
################################################################################
# Example Dial server. This is a "device" that reports constant rotations for
@@ -1533,19 +1535,19 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
##############################################################################
# PhaseSpace Impulse system. This device type is not compiled by default, since
-# it uses a proprietary library. However, you can get it to compile by
+# it uses a proprietary library. However, you can get it to compile by
# defining VRPN_INCLUDE_PHASESPACE and including the appropriate libraries
# and headers when compiling (See vrpn_Configure.h).
-#
+#
# Arguments:
# char tracker_name[] # Tracker0, Tracker1, ... etc
# [Additional lines specifying tracker configuration]
-#
+#
# The section following the Tracker declaration is a tag delimited set of
# lines which specify system configuration and vrpn sensors. The section begins
# with an tag and ends with a tag. Each tag must be on a separate
# line. (see below)
-#
+#
# Each line in the specification section is a white-space separated set of
# key-value pairs. At most one sensor is defined per line. The sensor and type
# keys are mandatory. Other keys are are required based on the type.
@@ -1553,8 +1555,8 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
# Comments can be embedded with the '#' character.
#
# Example:
-#
-# vrpn_Tracker_PhaseSpace Tracker0
+#
+# vrpn_Tracker_PhaseSpace Tracker0
#
# device="192.168.1.1"
# frequency=960
@@ -1577,7 +1579,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
# ======================
#
# device
-# A string specifying the IP address of the Impulse server to connect to.
+# A string specifying the IP address of the Impulse server to connect to.
#
# frequency
# A floating-point number specifying the system streaming frequency.
@@ -1592,7 +1594,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
# An integer. Set to zero to disable. Specifying 1 for drop_frames will
# cause the server to drop frames in order to get the most recent data on
# every run through the mainloop. This may be desirable for most VR
-# applications and for slower machines.
+# applications and for slower machines.
#
# debug
# An integer. Zero to disable. Specifying 1 for debug will cause the server
@@ -1607,7 +1609,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
#
# type
# A string which specifies what type the sensor is. Required if sensor is
-# specified. The following types are supported:
+# specified. The following types are supported:
# point
# rigid
# rigid_body (deprecated)
@@ -1619,23 +1621,23 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
# led
# An integer which specifies the led id of a sensor.
# Required if type is "point".
-#
+#
# pos
# An optional comma-separated list of three floating-point numbers specifying
# the 3D position of a marker on a rigid body. No spaces. Valid if type is
# "point". Specifying positions is only valid if the tracker number is set
# to a rigid body. The units MUST be in millimeters, regardless of scale
# setting.
-#
+#
# init
# An optional comma-separated list of four floating-point numbers specifying
# kalman parameters for a rigid body. Valid if type is "rigid_body".
#
#
-# For support, questions, comments, or bug reports please send emails
+# For support, questions, comments, or bug reports please send emails
# to: support@phasespace.com
-#
-#vrpn_Tracker_PhaseSpace Tracker0
+#
+#vrpn_Tracker_PhaseSpace Tracker0
#
#device="192.168.1.230"
#frequency=480
@@ -2454,8 +2456,8 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
#vrpn_Tracker_FilterOneEuro Filter0 *Tracker0 2 1.15 1.0 1.2 1.5 5.0 1.2
################################################################################
-# Sensics zSight HMD with built-in tracker. This is an inertial tracker that
-# gives orientation information, but no position (the tracker reports
+# Sensics zSight HMD with built-in tracker. This is an inertial tracker that
+# gives orientation information, but no position (the tracker reports
# (0, 0, 0) for position).
#
# Arguments:
@@ -2531,19 +2533,19 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
# To access Polhemus G4 on Windows using the Polhemus PDI library,
# use vrpn_Tracker_G4.
#
-# The vrpn_Tracker_G4 tracker definition requires the tracker name and
+# The vrpn_Tracker_G4 tracker definition requires the tracker name and
# the vrpn server name for the tracker, followed by an optional Server Poll rate,
# and on the next line, the file path to the .g4c configuration file:
#
# vrpn_Tracker_G4 G4 \
# C:\filepath\source_config_file.g4c
#
-# The Server Poll Rate is optional.
-# If it is not specified, the VRPN server will poll for new data at a rate of 120 frames per second.
+# The Server Poll Rate is optional.
+# If it is not specified, the VRPN server will poll for new data at a rate of 120 frames per second.
# (120 frames per second is the default output rate of G4 hardware.)
#
# If you wish to poll at a slower rate than the tracker, you may specify any number. For example, if
-# you wish to poll 20 times per second, then specify a poll rate of 20.
+# you wish to poll 20 times per second, then specify a poll rate of 20.
#
# The '\' at the end of the first line, after the server name, is optional and
# will be disregarded. A '\' on a subsequent line means that further commands
@@ -2774,30 +2776,30 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
# To access Polhemus FasTrak on Windows using the Polhemus PDI library,
# use vrpn_Tracker_FastrakPDI.
#
-# The vrpn_Tracker_FastrakPDI tracker definition requires the tracker name and
+# The vrpn_Tracker_FastrakPDI tracker definition requires the tracker name and
# the vrpn server name for the tracker, followed by an optional Server Poll rate:
#
# vrpn_Tracker_FastrakPDI myFastrak4 30\ <-valid
# vrpn_Tracker_FastrakPDI myFastrak2 60\ <-valid
# vrpn_Tracker_FastrakPDI myFastrak1 \ <-valid
#
-# The Server Poll Rate is optional.
-# If it is not specified, the VRPN server will poll for new data at a rate of 120 frames per second.
+# The Server Poll Rate is optional.
+# If it is not specified, the VRPN server will poll for new data at a rate of 120 frames per second.
# (120 frames per second is the default output rate of FasTrak hardware with ONE sensor connected.)
-#
+#
# For FasTrak trackers the update rate depends on the number of sensors connected to the device:
# Number of Sensors Update Rate
# ----------------- -----------
# 1 120 frames/sec
-# 2 60 frames/sec
+# 2 60 frames/sec
# 3 40 frames/sec
# 4 30 frames/sec
#
-# If you wish to poll at the same rate as the tracker output, then you must specify the poll rate
+# If you wish to poll at the same rate as the tracker output, then you must specify the poll rate
# to match the update rate in the table above.
#
# If you wish to poll at a slower rate than the tracker, you may specify any number. For example, if
-# you wish to poll 20 times per second, then specify and update rate of 20.
+# you wish to poll 20 times per second, then specify and update rate of 20.
#
# The '\' at the end of the first line, after the server name, is optional and will be disregarded.
# a '\' on a subsequent line means that further commands are to be input. The format is to have
@@ -2870,7 +2872,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
# -collects a second single pno to confirm this removal (P) and
# -finally returns to binary mode before passing control to VRPN (F1<>).
# The trackers name is TrackerJoe.
-# The server poll rate is 120 Hz.
+# The server poll rate is 120 Hz.
# vrpn_Tracker_FastrakPDI TrackerJoe 120\
# PDIStylus 1\
@@ -2892,7 +2894,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
# To access Polhemus Liberty or Patriot on Windows using the Polhemus PDI library,
# use vrpn_Tracker_LibertyPDI.
#
-# The vrpn_Tracker_LibertyPDI tracker definition requires the tracker name and
+# The vrpn_Tracker_LibertyPDI tracker definition requires the tracker name and
# the vrpn server name for the tracker, followed by an optional Server Poll rate:
#
# vrpn_Tracker_LibertyPDI MyLiberty 240\ <-valid
@@ -2901,16 +2903,16 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
#
# Note that "Tracker_LibertyPDI" is used for both Polhemus Patriot and Liberty tracker hardware!
#
-# The Server Poll Rate is optional.
-# If it is not specified, the VRPN server will poll for new data at a rate of 60 frames per second.
+# The Server Poll Rate is optional.
+# If it is not specified, the VRPN server will poll for new data at a rate of 60 frames per second.
# (60 frames per second is the default output rate of Patriot tracker hardware.)
-#
+#
# For Liberty trackers, the default rate is 240 frames per second. If you wish for the server to poll
# at this rate, then you must specify 240!
#
# If you wish to poll at a slower rate than the tracker, you may specify any number. For example, if
-# you wish to poll 20 times per second, then specify and update rate of 20.
-#
+# you wish to poll 20 times per second, then specify and update rate of 20.
+#
# The '\' at the end of the first line, after the server name, is optional and will be disregarded.
# a '\' on a subsequent line means that further commands are to be input. The format is to have
# one command per line. Each line that isn't the final line must end in a '\'. The final line should not
@@ -2929,7 +2931,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
#
# Note: The R command is used to set the Liberty tracker update rate. This command has no effect on Patriot.
# For Liberty, if you use the R command to change the update rate, remember to change the Server Poll Rate
-# specification in the tracker definition (like "MyPolhemus" example above).
+# specification in the tracker definition (like "MyPolhemus" example above).
# Command Syntax:
# Every command, except P (gather single pno frame), requires a carriage return on the end. Carriage
@@ -3003,7 +3005,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
#-----------------------------------------------------------------------------
# Finally the default config below:
# -May be used for a Liberty or a Patriot tracker
-# -Polls the tracker 60 times per second
+# -Polls the tracker 60 times per second
# -Sets tracker output to ASCII
# -Queries the tracker for WhoAmI information
# -Sets the output back to Binary
@@ -3159,7 +3161,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
#vrpn_Tracker_DeadReckoning_Rotation Tracker1 *Tracker0 2 0.0333
################################################################################
-# OSVR Hacker Dev Kit inertial measurement unit. This is an inertial tracker that
+# OSVR Hacker Dev Kit inertial measurement unit. This is an inertial tracker that
# gives orientation information, but no position. Position values are always
# 0. Version 1 of this device sends only poses. Version 2 also sends velocity
# reports.
@@ -3170,7 +3172,7 @@ vrpn_Tracker_Spin Tracker0 1 200.0 0.0 1.0 0.0 0.1
#vrpn_Tracker_OSVRHackerDevKit Tracker0
################################################################################
-# Oculus Rift DK1 and DK2. There are two versions of the DK2 driver, which use the
+# Oculus Rift DK1 and DK2. There are two versions of the DK2 driver, which use the
# same hardware but in two different modes.
#
# vrpn_Oculus_DK2_inertial: Oculus DK2 inertial measurement unit only. This
diff --git a/project.godot b/project.godot
index 70fb73a..19b923a 100644
--- a/project.godot
+++ b/project.godot
@@ -13,4 +13,4 @@ config_version=5
config/name="uvrpn"
run/main_scene="uid://bj5ykdjle10tt"
config/features=PackedStringArray("4.4", "Forward Plus")
-config/icon="res://icon.svg"
+config/icon="uid://cdprcmtx102rp"
diff --git a/uvrpn.gd b/uvrpn.gd
deleted file mode 100644
index 4267cde..0000000
--- a/uvrpn.gd
+++ /dev/null
@@ -1,50 +0,0 @@
-extends Node
-
-@export var url : String = "localhost"
-@export var port : int = 3883
-
-var vrpn_cookie : String = "vrpn: ver. 07.35"
-
-#var server : UDPServer = null
-@onready var socket : StreamPeerTCP = StreamPeerTCP.new()
-
-func _ready() -> void:
- #if StreamPeerTCP.STATUS_NONE == socket.get_status():
- if socket.connect_to_host(url,port) == OK:
- print("Socket connected ...")
- socket.set_no_delay(true)
- send_data(socket,vrpn_cookie.to_utf8_buffer())
- #server = UDPServer.new()
- #server.listen(3883)
- else:
- print("Error connecting to server")
- #else:
- #print("Stream not ready")
-
-
-func _process(delta: float) -> void:
- if socket:
- socket.poll()
- ##return
- ##if server:
- ##server.poll()
- ##if server.is_connection_available():
- ##var peer = server.take_connection()
- ##var packet = peer.get_packet()
- ##print("Accepted peer: %s:%s" % [peer.get_packet_ip(), peer.get_packet_port()])
- ##print("Received data: %s" % [packet.get_string_from_utf8()])
- ### Reply so it knows we received the message.
- ##peer.put_packet(packet)
-
-
-static func send_data(socket : StreamPeerTCP, data: PackedByteArray) -> bool:
- print(socket.get_status())
- if socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
- if socket.put_data(data) == OK:
- return true
- else:
- print("Error writing data ...")
- return false
- else:
- print("Error connecting")
- return false
diff --git a/uvrpn.gd.uid b/uvrpn.gd.uid
deleted file mode 100644
index 9ba71af..0000000
--- a/uvrpn.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ca5psnjx63ua8