From 7e5bfe891c3a61f8a14872818431d4faccde3438 Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Tue, 2 Dec 2025 17:37:11 +0100 Subject: [PATCH] progress on single shared light --- main.gd | 25 +++++++++++++---- main.tscn | 18 +----------- project.godot | 13 +++++++++ rpi/gdmath.py | 11 +++++++- rpi/lights.py | 2 ++ rpi/main.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ rpi/readangle.py | 14 ++++++++++ rpi/stepper.py | 20 ++++++++++++- 8 files changed, 151 insertions(+), 25 deletions(-) create mode 100644 rpi/main.py create mode 100644 rpi/readangle.py diff --git a/main.gd b/main.gd index bcb39fb..4e2a9ad 100644 --- a/main.gd +++ b/main.gd @@ -2,18 +2,21 @@ extends Node3D @onready var lights: Lights = %Lights @onready var slider: HSlider = %HSlider -@onready var networked_lights: Lights = %NetworkedLights @export var light_rotation: float = 0.0 +@export var light_delta: float = 0.0 @export var color_gradient: Gradient +var remote_light_rotation: float = 0.0 +var remote_light_delta: float = 0.0 + var peer: PacketPeerUDP func _ready(): slider.value_changed.connect(set_local_light_rotation) peer = PacketPeerUDP.new() peer.bind(4433) - peer.set_dest_address("rpi", 4444) + peer.set_dest_address("rpi1", 4444) func send_data(data: Dictionary) -> void: @@ -46,11 +49,21 @@ func set_local_light_rotation(value: float) -> void: send_data({"value": value}) return + error_correction = 0. + lights.expected_rotation_delta = diff + light_delta = diff light_rotation = value send_data({"value": value, "delta": diff}) func _process(_delta): + var joystick_x = Input.get_joy_axis(0, JOY_AXIS_LEFT_X) + var joystick_y = Input.get_joy_axis(0, JOY_AXIS_LEFT_Y) + var joystick = Vector2(joystick_x, joystick_y) + if joystick.length() > 0.2: + var angle = (atan2(joystick_y, joystick_x) / TAU) + 0.25 + angle = wrapf(angle, 0, 1) + set_local_light_rotation(angle) if peer.get_available_packet_count() > 0: var array_bytes = peer.get_packet() var packet_string = array_bytes.get_string_from_ascii() @@ -58,8 +71,8 @@ func _process(_delta): if json != null: var value = json.value var delta = json.delta - networked_lights.expected_rotation_delta = delta + # networked_lights.expected_rotation_delta = delta - var polarized_value := pingpong(value * 4, 1) - networked_lights.light_intensity = 1.0 - polarized_value - networked_lights.light_color = color_gradient.sample(value) \ No newline at end of file + # var polarized_value := pingpong(value * 4, 1) + # networked_lights.light_intensity = 1.0 - polarized_value + # networked_lights.light_color = color_gradient.sample(value) \ No newline at end of file diff --git a/main.tscn b/main.tscn index 27951da..78f3e6f 100644 --- a/main.tscn +++ b/main.tscn @@ -32,7 +32,6 @@ color_gradient = SubResource("Gradient_h2yge") [node name="Plane" type="MeshInstance3D" parent="."] unique_name_in_owner = true -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 0) mesh = SubResource("CylinderMesh_0xm2m") surface_material_override/0 = SubResource("StandardMaterial3D_7dm0k") @@ -45,26 +44,11 @@ light_count = 20 light_radius = 0.7 light_template = ExtResource("2_0xm2m") -[node name="NetworkedPlane" type="MeshInstance3D" parent="."] -unique_name_in_owner = true -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0) -mesh = SubResource("CylinderMesh_0xm2m") -surface_material_override/0 = SubResource("StandardMaterial3D_7dm0k") - -[node name="NetworkedLights" type="Marker3D" parent="NetworkedPlane"] -unique_name_in_owner = true -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) -gizmo_extents = 0.7 -script = ExtResource("1_ig7tw") -light_count = 20 -light_radius = 0.7 -light_template = ExtResource("2_0xm2m") - [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = SubResource("Environment_0xm2m") [node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, 2, 0) +transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, 1.4683123, 0) [node name="HSlider" type="HSlider" parent="."] unique_name_in_owner = true diff --git a/project.godot b/project.godot index 90c95c6..b1b09fc 100644 --- a/project.godot +++ b/project.godot @@ -15,6 +15,19 @@ run/main_scene="uid://bfysabo18nycq" config/features=PackedStringArray("4.5", "Forward Plus") config/icon="res://icon.svg" +[input] + +forward={ +"deadzone": 0.2, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) +] +} +right={ +"deadzone": 0.2, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) +] +} + [rendering] anti_aliasing/quality/msaa_2d=2 diff --git a/rpi/gdmath.py b/rpi/gdmath.py index ecbb82f..afb1dd9 100644 --- a/rpi/gdmath.py +++ b/rpi/gdmath.py @@ -30,4 +30,13 @@ def sample_color_gradient(t): color_a = colors_rgb[i] color_b = colors_rgb[i + 1] return tuple(int(lerp(color_a[j], color_b[j], local_t)) for j in range(3)) - return colors_rgb[-1] \ No newline at end of file + return colors_rgb[-1] +def shortest_diff(old, new): + diff = old - new + wrapped_diff = (new + 1) - old + if abs(wrapped_diff) < abs(diff): + diff = -wrapped_diff + wrapped_diff = (old + 1) - new + if abs(wrapped_diff) < abs(diff): + diff = wrapped_diff + return diff \ No newline at end of file diff --git a/rpi/lights.py b/rpi/lights.py index 1802f3a..6212e15 100644 --- a/rpi/lights.py +++ b/rpi/lights.py @@ -23,3 +23,5 @@ class Lights: self.pixels[i] = new_color self.pixels.show() self.expected_rotation_delta = lerp(self.expected_rotation_delta, 0, delta * LIGHT_SLOWDOWN_SPEED) + +lights = Lights(pixels) \ No newline at end of file diff --git a/rpi/main.py b/rpi/main.py new file mode 100644 index 0000000..b8d2d08 --- /dev/null +++ b/rpi/main.py @@ -0,0 +1,73 @@ +import socket +import time +import json +from lights import lights +from stepper import stepper +import gdmath +from readangle import read_angle_f +import threading + +UDP_HOST = "steamdeck" +UDP_PORT_LOCAL = 4444 +UDP_PORT_NETWORKED = 4433 + +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.bind(("", UDP_PORT_LOCAL)) +sock.setblocking(0) + +def send_json(data): + message = json.dumps(data).encode('utf-8') + sock.sendto(message, (UDP_HOST, UDP_PORT_NETWORKED)) + +def receive_json(): + try: + data, _ = sock.recvfrom(1024) + return json.loads(data.decode('utf-8')) + except BlockingIOError: + return None + +local_rotation = 0 +local_delta = 0 +remote_rotation = 0 +remote_delta = 0 + +def read_thread_fn(): + global local_rotation, local_delta + while True: + old_rotation = local_rotation + local_rotation = read_angle_f() + local_delta = gdmath.shortest_diff(old_rotation, local_rotation) + send_json({"value": local_rotation, "delta": local_delta}) + +def lights_fn(): + global local_rotation, local_delta, remote_rotation, remote_delta + last_time = time.time() + delta = 1/60 + while True: + lights.color = gdmath.sample_color_gradient(local_rotation + remote_rotation) + lights.expected_rotation_delta = local_delta + lights.process(delta) + current_time = time.time() + time.sleep(1/60) + delta = current_time - last_time + last_time = current_time + +def network_recv_fn(): + global remote_rotation, remote_delta + while True: + data = receive_json() + if data: + remote_rotation = data["value"] + remote_delta = data["delta"] + time.sleep(0.01) + +read_thread = threading.Thread(target=read_thread_fn) +read_thread.start() +lights_thread = threading.Thread(target=lights_fn) +lights_thread.start() +network_thread = threading.Thread(target=network_recv_fn) +network_thread.start() + +read_thread.join() +lights_thread.join() +network_thread.join() \ No newline at end of file diff --git a/rpi/readangle.py b/rpi/readangle.py new file mode 100644 index 0000000..699ab62 --- /dev/null +++ b/rpi/readangle.py @@ -0,0 +1,14 @@ +import smbus2 + +# Define I2C address and bus +AS5600_ADDR = 0x36 +ANGLE_REG = 0x0E + +bus = smbus2.SMBus(1) + +def read_angle_f(): + # Read two bytes from the angle register + raw_data = bus.read_i2c_block_data(AS5600_ADDR, ANGLE_REG, 2) + angle = (raw_data[0] << 8) | raw_data[1] # Combine MSB and LSB + angle = angle & 0x0FFF # Mask to 12 bits + return (angle / 4096.0) # convert to 0-1 diff --git a/rpi/stepper.py b/rpi/stepper.py index a7ca79d..3302032 100644 --- a/rpi/stepper.py +++ b/rpi/stepper.py @@ -47,4 +47,22 @@ class StepperMotor: else: self.single_step_back() def pos(self): - return self.current_step % self.step_count \ No newline at end of file + return self.current_step % self.step_count + def fpos(self): + return (self.current_step % self.step_count) / self.step_count + + def single_step_towards(self, target_pos): + current_pos = self.pos() + # Determine shortest direction + # includes wrap-around (it's a circular motion motor) + diff = (target_pos - current_pos + self.step_count) % self.step_count + if diff > self.step_count / 2: + self.single_step_back() + else: + self.single_step() + def angle_to_pos(self, angle): + return int((angle % 360) / 360 * self.step_count) + def fpos_to_pos(self, fpos): + return int((fpos % 1) * self.step_count) + +stepper = StepperMotor() \ No newline at end of file