merge conflict and readme update
old code was uploaded in the initial upload changed everything with working programm and updated the readme
This commit is contained in:
+170
-63
@@ -1,93 +1,200 @@
|
|||||||
import pygame
|
import pygame
|
||||||
import math
|
import math
|
||||||
import time
|
import GlobalVariables as gv
|
||||||
|
|
||||||
import DataTypes as dt
|
def controller():
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
def normalize(x, y, deadzone=0.15):
|
# This is a simple class that will help us print to the screen.
|
||||||
if abs(x) < deadzone:
|
# It has nothing to do with the joysticks, just outputting the
|
||||||
x = 0.0
|
# information.
|
||||||
if abs(y) < deadzone:
|
class TextPrint:
|
||||||
y = 0.0
|
def __init__(self):
|
||||||
|
self.reset()
|
||||||
|
self.font = pygame.font.Font(None, 25)
|
||||||
|
|
||||||
mag = math.hypot(x, y)
|
def tprint(self, screen, text):
|
||||||
if mag > 1.0:
|
text_bitmap = self.font.render(text, True, (0, 0, 0))
|
||||||
x /= mag
|
screen.blit(text_bitmap, (self.x, self.y))
|
||||||
y /= mag
|
self.y += self.line_height
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.x = 10
|
||||||
|
self.y = 10
|
||||||
|
self.line_height = 15
|
||||||
|
|
||||||
|
def indent(self):
|
||||||
|
self.x += 10
|
||||||
|
|
||||||
|
def unindent(self):
|
||||||
|
self.x -= 10
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
def normalize_controller_input(x, y):
|
||||||
|
# Berechne die Laenge des Vektors
|
||||||
|
magnitude = math.sqrt(x**2 + y**2)
|
||||||
|
|
||||||
|
# Wenn die Laenge groesser als 1 ist, normalisiere sie
|
||||||
|
if magnitude > 1.0:
|
||||||
|
x /= magnitude
|
||||||
|
y /= magnitude
|
||||||
|
|
||||||
return x, y
|
return x, y
|
||||||
|
|
||||||
|
# Set the width and height of the screen (width, height), and name the window.
|
||||||
|
screen = pygame.display.set_mode((500, 700))
|
||||||
|
pygame.display.set_caption("Controller Inputs")
|
||||||
|
|
||||||
def controller_loop(shared_intent: dt.ControlIntent, lock):
|
# Used to manage how fast the screen updates.
|
||||||
pygame.init()
|
|
||||||
pygame.joystick.init()
|
|
||||||
|
|
||||||
joysticks = {}
|
|
||||||
prev_buttons = {}
|
|
||||||
|
|
||||||
clock = pygame.time.Clock()
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
while True:
|
# Get ready to print.
|
||||||
# 🔑 REQUIRED: keeps joystick state updating
|
text_print = TextPrint()
|
||||||
pygame.event.pump()
|
|
||||||
|
|
||||||
|
# This dict can be left as-is, since pygame will generate a
|
||||||
|
# pygame.JOYDEVICEADDED event for every joystick connected
|
||||||
|
# at the start of the program.
|
||||||
|
joysticks = {}
|
||||||
|
|
||||||
|
done = False
|
||||||
|
while not done:
|
||||||
|
# Event processing step.
|
||||||
|
# Possible joystick events: JOYAXISMOTION, JOYBALLMOTION, JOYBUTTONDOWN,
|
||||||
|
# JOYBUTTONUP, JOYHATMOTION, JOYDEVICEADDED, JOYDEVICEREMOVED
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
done = True # Flag that we are done so we exit this loop.
|
||||||
|
|
||||||
|
if event.type == pygame.JOYBUTTONDOWN:
|
||||||
|
print("Joystick button pressed.")
|
||||||
|
if event.button == 3 and gv.emote is None:
|
||||||
|
gv.emote = "wave"
|
||||||
|
if event.button == 2 and gv.emote is None:
|
||||||
|
gv.robotCommunication.send_text("Huhrensohn")
|
||||||
|
if event.button == 0:
|
||||||
|
joystick = joysticks[event.instance_id]
|
||||||
|
gv
|
||||||
|
if joystick.rumble(0, 0.7, 500):
|
||||||
|
print(f"Rumble effect played on joystick {event.instance_id}")
|
||||||
|
|
||||||
|
if event.type == pygame.JOYBUTTONUP:
|
||||||
|
print("Joystick button released.")
|
||||||
|
|
||||||
|
# Handle hotplugging
|
||||||
if event.type == pygame.JOYDEVICEADDED:
|
if event.type == pygame.JOYDEVICEADDED:
|
||||||
|
# This event will be generated when the program starts for every
|
||||||
|
# joystick, filling up the list without needing to create them manually.
|
||||||
joy = pygame.joystick.Joystick(event.device_index)
|
joy = pygame.joystick.Joystick(event.device_index)
|
||||||
joysticks[joy.get_instance_id()] = joy
|
joysticks[joy.get_instance_id()] = joy
|
||||||
prev_buttons[joy.get_instance_id()] = [0] * joy.get_numbuttons()
|
print(f"Joystick {joy.get_instance_id()} connencted")
|
||||||
print(f"[Controller] Joystick connected: {joy.get_name()}")
|
|
||||||
|
|
||||||
elif event.type == pygame.JOYDEVICEREMOVED:
|
if event.type == pygame.JOYDEVICEREMOVED:
|
||||||
joysticks.pop(event.instance_id, None)
|
del joysticks[event.instance_id]
|
||||||
prev_buttons.pop(event.instance_id, None)
|
print(f"Joystick {event.instance_id} disconnected")
|
||||||
print("[Controller] Joystick disconnected")
|
|
||||||
|
|
||||||
if not joysticks:
|
# Drawing step
|
||||||
time.sleep(0.1)
|
# First, clear the screen to white. Don't put other drawing commands
|
||||||
continue
|
# above this, or they will be erased with this command.
|
||||||
|
screen.fill((255, 255, 255))
|
||||||
|
text_print.reset()
|
||||||
|
|
||||||
# Use first joystick
|
# Get count of joysticks.
|
||||||
joy = next(iter(joysticks.values()))
|
joystick_count = pygame.joystick.get_count()
|
||||||
jid = joy.get_instance_id()
|
|
||||||
|
|
||||||
# ---- AXES ----
|
text_print.tprint(screen, f"Number of joysticks: {joystick_count}")
|
||||||
forward = -joy.get_axis(1) # usually inverted
|
text_print.indent()
|
||||||
sideways = joy.get_axis(0)
|
|
||||||
|
|
||||||
forward, sideways = normalize(forward, sideways)
|
# For each joystick:
|
||||||
|
for joystick in joysticks.values():
|
||||||
|
jid = joystick.get_instance_id()
|
||||||
|
|
||||||
walk = abs(forward) > 0 or abs(sideways) > 0
|
text_print.tprint(screen, f"Joystick {jid}")
|
||||||
|
text_print.indent()
|
||||||
|
|
||||||
# ---- BUTTONS ----
|
# Get the name from the OS for the controller/joystick.
|
||||||
buttons = joy.get_numbuttons()
|
name = joystick.get_name()
|
||||||
current_buttons = [joy.get_button(i) for i in range(buttons)]
|
text_print.tprint(screen, f"Joystick name: {name}")
|
||||||
|
|
||||||
emote = None
|
guid = joystick.get_guid()
|
||||||
quit_flag = False
|
text_print.tprint(screen, f"GUID: {guid}")
|
||||||
|
|
||||||
|
power_level = joystick.get_power_level()
|
||||||
|
text_print.tprint(screen, f"Joystick's power level: {power_level}")
|
||||||
|
|
||||||
|
# Usually axis run in pairs, up/down for one, and left/right for
|
||||||
|
# the other. Triggers count as axes.
|
||||||
|
axes = joystick.get_numaxes()
|
||||||
|
text_print.tprint(screen, f"Number of axes: {axes}")
|
||||||
|
text_print.indent()
|
||||||
|
|
||||||
|
for i in range(axes):
|
||||||
|
axis = joystick.get_axis(i)
|
||||||
|
gv.vector_dirmov = [axis]
|
||||||
|
text_print.tprint(screen, f"Axis {i} value: {axis:>6.3f}")
|
||||||
|
text_print.unindent()
|
||||||
|
|
||||||
|
# Get Movement Direction Vector
|
||||||
|
# Left stick (translation)
|
||||||
|
vx = joystick.get_axis(1) # forward/back
|
||||||
|
vy = joystick.get_axis(0) # strafe
|
||||||
|
|
||||||
|
vx, vy = normalize_controller_input(vx, vy)
|
||||||
|
|
||||||
|
# Right stick X (rotation) — adjust axis index if needed
|
||||||
|
omega = joystick.get_axis(2)
|
||||||
|
|
||||||
|
# Deadzone for rotation
|
||||||
|
if abs(omega) < 0.2:
|
||||||
|
omega = 0.0
|
||||||
|
|
||||||
|
# Apply axis snapping (only for translation)
|
||||||
|
if 0.8 < vx and -0.2 < vy < 0.2:
|
||||||
|
vx, vy = 1.0, 0.0
|
||||||
|
elif vx < -0.8 and -0.2 < vy < 0.2:
|
||||||
|
vx, vy = -1.0, 0.0
|
||||||
|
elif -0.2 < vx < 0.2 and 0.8 < vy:
|
||||||
|
vx, vy = 0.0, 1.0
|
||||||
|
elif -0.2 < vx < 0.2 and vy < -0.8:
|
||||||
|
vx, vy = 0.0, -1.0
|
||||||
|
|
||||||
|
# Final movement vector
|
||||||
|
gv.vector_dirmov = [vx, vy, omega]
|
||||||
|
|
||||||
|
# Robot state
|
||||||
|
if abs(vx) < 0.2 and abs(vy) < 0.2 and abs(omega) < 0.2:
|
||||||
|
gv.robot_state = "idle"
|
||||||
|
else:
|
||||||
|
gv.robot_state = "walking"
|
||||||
|
|
||||||
|
buttons = joystick.get_numbuttons()
|
||||||
|
text_print.tprint(screen, f"Number of buttons: {buttons}")
|
||||||
|
text_print.indent()
|
||||||
|
|
||||||
for i in range(buttons):
|
for i in range(buttons):
|
||||||
if current_buttons[i] and not prev_buttons[jid][i]:
|
button = joystick.get_button(i)
|
||||||
# Button DOWN (edge)
|
text_print.tprint(screen, f"Button {i:>2} value: {button}")
|
||||||
if i == 0:
|
text_print.unindent()
|
||||||
emote = "HELLO"
|
|
||||||
elif i == 1:
|
|
||||||
emote = "SAD"
|
|
||||||
elif i == 9:
|
|
||||||
quit_flag = True
|
|
||||||
|
|
||||||
prev_buttons[jid] = current_buttons
|
hats = joystick.get_numhats()
|
||||||
|
text_print.tprint(screen, f"Number of hats: {hats}")
|
||||||
|
text_print.indent()
|
||||||
|
|
||||||
# ---- UPDATE INTENT (atomic) ----
|
# Hat position. All or nothing for direction, not a float like
|
||||||
with lock:
|
# get_axis(). Position is a tuple of int values (x, y).
|
||||||
shared_intent.move_x = forward
|
for i in range(hats):
|
||||||
shared_intent.move_y = sideways
|
hat = joystick.get_hat(i)
|
||||||
shared_intent.walk = walk
|
text_print.tprint(screen, f"Hat {i} value: {str(hat)}")
|
||||||
|
text_print.unindent()
|
||||||
|
|
||||||
if emote:
|
text_print.unindent()
|
||||||
shared_intent.emote = emote
|
|
||||||
|
|
||||||
if quit_flag:
|
# Go ahead and update the screen with what we've drawn.
|
||||||
shared_intent.quit = True
|
pygame.display.flip()
|
||||||
|
|
||||||
clock.tick(60)
|
# Limit to 30 frames per second.
|
||||||
|
clock.tick(30)
|
||||||
|
|
||||||
|
main()
|
||||||
|
pygame.quit()
|
||||||
+4
-13
@@ -19,8 +19,6 @@ class PosArray:
|
|||||||
def __getitem__(self, key): return self.data[key]
|
def __getitem__(self, key): return self.data[key]
|
||||||
def __iter__(self): return iter(self.data)
|
def __iter__(self): return iter(self.data)
|
||||||
def __repr__(self): return f"PosArray(\n{self.data}\n)"
|
def __repr__(self): return f"PosArray(\n{self.data}\n)"
|
||||||
def copy(self):
|
|
||||||
return PosArray(self.data.copy())
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -39,8 +37,6 @@ class DegArray:
|
|||||||
def __getitem__(self, key): return self.data[key]
|
def __getitem__(self, key): return self.data[key]
|
||||||
def __iter__(self): return iter(self.data)
|
def __iter__(self): return iter(self.data)
|
||||||
def __repr__(self): return f"PosArray(\n{self.data}\n)"
|
def __repr__(self): return f"PosArray(\n{self.data}\n)"
|
||||||
def copy(self):
|
|
||||||
return DegArray(self.data.copy())
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RadArray:
|
class RadArray:
|
||||||
@@ -57,8 +53,7 @@ class RadArray:
|
|||||||
def __getitem__(self, key): return self.data[key]
|
def __getitem__(self, key): return self.data[key]
|
||||||
def __iter__(self): return iter(self.data)
|
def __iter__(self): return iter(self.data)
|
||||||
def __repr__(self): return f"PosArray(\n{self.data}\n)"
|
def __repr__(self): return f"PosArray(\n{self.data}\n)"
|
||||||
def copy(self):
|
|
||||||
return RadArray(self.data.copy())
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RobotCommand:
|
class RobotCommand:
|
||||||
@@ -68,10 +63,6 @@ class RobotCommand:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ControlIntent:
|
class ControlIntent:
|
||||||
move_x: float = 0.0
|
move_vector: Vector2
|
||||||
move_y: float = 0.0
|
turn: float
|
||||||
turn: float = 0.0
|
emote: str | None
|
||||||
emote: str | None = None
|
|
||||||
walk: bool = False
|
|
||||||
quit: bool = False
|
|
||||||
|
|
||||||
+74
-149
@@ -8,139 +8,113 @@ from queue import Queue
|
|||||||
import config as cfg
|
import config as cfg
|
||||||
import DataTypes as dt
|
import DataTypes as dt
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Protocol definitions (SHARED with ESP32)
|
# Protocol definitions (MATCHES THE ULTIMATE .INO)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
PACKET_VERSION = 1
|
PACKET_VERSION = 1
|
||||||
|
|
||||||
# Packet IDs
|
|
||||||
PKT_COMMAND = 1
|
PKT_COMMAND = 1
|
||||||
PKT_STATUS = 2
|
PKT_CONFIG = 2
|
||||||
PKT_DEBUG = 3
|
PKT_DISPLAY = 3
|
||||||
|
|
||||||
# Field Types
|
FT_ARRAY_F32 = 1
|
||||||
FT_ARRAY_F32 = 1 # float32 array
|
FT_STRING = 2
|
||||||
FT_STRING = 2 # utf-8 string
|
|
||||||
FT_UINT8 = 3
|
FT_UINT8 = 3
|
||||||
FT_FLOAT32 = 4
|
FT_CONFIG_VAL = 4
|
||||||
|
|
||||||
# Header: packet_id | version | payload_len | timestamp_ms
|
HEADER_FMT = "<B B H I" # ID, Ver, Len, Timestamp
|
||||||
HEADER_FMT = "<B B H I"
|
|
||||||
HEADER_SIZE = struct.calcsize(HEADER_FMT)
|
|
||||||
|
|
||||||
# TLV field: type | length | value
|
|
||||||
TLV_FMT = "<B H"
|
TLV_FMT = "<B H"
|
||||||
TLV_SIZE = struct.calcsize(TLV_FMT)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# ESP32 Communication Thread
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
class ESP32Communication(Thread):
|
class ESP32Communication(Thread):
|
||||||
def __init__(self, host=cfg.esp32_ip, port=cfg.esp32_port, timeout=cfg.timeout):
|
def __init__(self, host=cfg.esp32_ip, port=cfg.esp32_port):
|
||||||
super().__init__(daemon=True)
|
super().__init__(daemon=True)
|
||||||
|
self.addr = (host, port)
|
||||||
|
|
||||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
# UDP Socket - Zero Lag
|
||||||
self.sock.settimeout(timeout)
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|
||||||
print(f"Connecting to ESP32 at {host}:{port} ...")
|
|
||||||
self.sock.connect((host, port))
|
|
||||||
print("Connected to ESP32!")
|
|
||||||
|
|
||||||
self.tx_queue = Queue()
|
self.tx_queue = Queue()
|
||||||
self.rx_queue = Queue()
|
|
||||||
self.running = Event()
|
self.running = Event()
|
||||||
self.running.set()
|
self.running.set()
|
||||||
|
self.last_motion_data = None
|
||||||
|
print(f"Communication Initialized: Target {host}:{port}")
|
||||||
|
|
||||||
# --------------------------------------------------------
|
|
||||||
# Thread loop
|
|
||||||
# --------------------------------------------------------
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.sock.setblocking(False)
|
# --- THE FIX: Send Configs immediately upon starting the thread ---
|
||||||
|
self.sync_robot_configs()
|
||||||
|
|
||||||
while self.running.is_set():
|
while self.running.is_set():
|
||||||
self._handle_tx()
|
if not self.tx_queue.empty():
|
||||||
self._handle_rx()
|
data = self.tx_queue.get()
|
||||||
time.sleep(0.002)
|
try:
|
||||||
|
self.sock.sendto(data, self.addr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"UDP Send Error: {e}")
|
||||||
|
|
||||||
# --------------------------------------------------------
|
# Tiny sleep to prevent 100% CPU usage
|
||||||
# Public API
|
time.sleep(0.001)
|
||||||
# --------------------------------------------------------
|
|
||||||
def send_command(self, radial_array :dt.RadArray, state=0, name=""):
|
def sync_robot_configs(self):
|
||||||
"""
|
"""
|
||||||
Example command packet:
|
Sends the parameters from your config.py to the ESP32.
|
||||||
- 3x6 float array
|
This tells the ESP32 how to map degrees to pulses and
|
||||||
- uint8 state
|
how fast the Python loop is running for smoothing.
|
||||||
- optional string
|
|
||||||
"""
|
"""
|
||||||
data = np.degrees(radial_array).astype(np.uint8)
|
print("Sending Config Sync to Robot...")
|
||||||
print(data)
|
|
||||||
fields = [
|
# Calculate interval in ms based on your FPS (tickpersec)
|
||||||
self._tlv_array(data),
|
interval_ms = int(1000 / cfg.standard_tickpersec)
|
||||||
self._tlv_uint8(state)
|
|
||||||
|
# Config IDs: 1=Min, 2=Max, 3=Interval
|
||||||
|
configs = [
|
||||||
|
(1, 125), # servoMin (Default for 50Hz)
|
||||||
|
(2, 625), # servoMax (Default for 50Hz)
|
||||||
|
(3, interval_ms)
|
||||||
]
|
]
|
||||||
|
|
||||||
if name:
|
fields = []
|
||||||
fields.append(self._tlv_string(name))
|
for c_id, val in configs:
|
||||||
|
# Payload: 1 byte for ID, 2 bytes for Value (Little Endian)
|
||||||
|
payload = struct.pack("<B h", c_id, val)
|
||||||
|
fields.append(struct.pack(TLV_FMT, FT_CONFIG_VAL, len(payload)) + payload)
|
||||||
|
|
||||||
|
packet = self._build_packet(PKT_CONFIG, fields)
|
||||||
|
self.tx_queue.put(packet)
|
||||||
|
|
||||||
|
def send_motion(self, radial_array: dt.RadArray):
|
||||||
|
"""Sends motion only if the data has changed."""
|
||||||
|
# 1. Convert to degrees/uint8 as you did before
|
||||||
|
new_data = np.degrees(radial_array.data).astype(np.uint8)
|
||||||
|
|
||||||
|
# 2. Check if it's different from the last sent packet
|
||||||
|
if self.last_motion_data is not None:
|
||||||
|
if np.array_equal(new_data, self.last_motion_data):
|
||||||
|
return # Skip sending if data is identical
|
||||||
|
|
||||||
|
# 3. Update the tracker and send
|
||||||
|
self.last_motion_data = new_data.copy()
|
||||||
|
|
||||||
|
fields = [self._tlv(FT_ARRAY_F32, new_data.tobytes())]
|
||||||
packet = self._build_packet(PKT_COMMAND, fields)
|
packet = self._build_packet(PKT_COMMAND, fields)
|
||||||
self.tx_queue.put(packet)
|
self.tx_queue.put(packet)
|
||||||
|
|
||||||
def receive(self):
|
def send_text(self, text=""):
|
||||||
"""
|
"""Sends a dedicated display packet independent of motion."""
|
||||||
Non-blocking receive.
|
# Convert string to bytes
|
||||||
Returns (packet_id, timestamp, fields) or None
|
text_bytes = text.encode("utf-8")
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self.rx_queue.get_nowait()
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def stop(self):
|
# Wrap in TLV format (Type 2 = FT_STRING)
|
||||||
self.running.clear()
|
field = self._tlv(FT_STRING, text_bytes)
|
||||||
self.join()
|
|
||||||
self.sock.close()
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
# Build the packet using the PKT_DISPLAY (ID 3)
|
||||||
# TX / RX handlers
|
packet = self._build_packet(PKT_DISPLAY, [field])
|
||||||
# --------------------------------------------------------
|
|
||||||
def _handle_tx(self):
|
|
||||||
if not self.tx_queue.empty():
|
|
||||||
data = self.tx_queue.get()
|
|
||||||
self.sock.sendall(data)
|
|
||||||
|
|
||||||
def _handle_rx(self):
|
self.tx_queue.put(packet)
|
||||||
try:
|
|
||||||
header = self._recv_exact(HEADER_SIZE)
|
|
||||||
if not header:
|
|
||||||
return
|
|
||||||
|
|
||||||
packet_id, version, payload_len, timestamp = struct.unpack(HEADER_FMT, header)
|
def _tlv(self, ftype, data):
|
||||||
|
return struct.pack(TLV_FMT, ftype, len(data)) + data
|
||||||
|
|
||||||
if payload_len == 0:
|
|
||||||
# Nothing to parse
|
|
||||||
self.rx_queue.put((packet_id, timestamp, []))
|
|
||||||
return
|
|
||||||
|
|
||||||
payload = self._recv_exact(payload_len)
|
|
||||||
if payload is None:
|
|
||||||
return # incomplete, skip
|
|
||||||
|
|
||||||
fields = self._parse_tlv(payload)
|
|
||||||
self.rx_queue.put((packet_id, timestamp, fields))
|
|
||||||
|
|
||||||
except BlockingIOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
|
||||||
# Packet construction
|
|
||||||
# --------------------------------------------------------
|
|
||||||
def _build_packet(self, packet_id, fields):
|
def _build_packet(self, packet_id, fields):
|
||||||
payload = b"".join(fields)
|
payload = b"".join(fields)
|
||||||
|
|
||||||
header = struct.pack(
|
header = struct.pack(
|
||||||
HEADER_FMT,
|
HEADER_FMT,
|
||||||
packet_id,
|
packet_id,
|
||||||
@@ -148,57 +122,8 @@ class ESP32Communication(Thread):
|
|||||||
len(payload),
|
len(payload),
|
||||||
int(time.time() * 1000) & 0xFFFFFFFF
|
int(time.time() * 1000) & 0xFFFFFFFF
|
||||||
)
|
)
|
||||||
|
|
||||||
return header + payload
|
return header + payload
|
||||||
|
|
||||||
# --------------------------------------------------------
|
def stop(self):
|
||||||
# TLV helpers
|
self.running.clear()
|
||||||
# --------------------------------------------------------
|
self.sock.close()
|
||||||
def _tlv(self, ftype, data):
|
|
||||||
return struct.pack(TLV_FMT, ftype, len(data)) + data
|
|
||||||
|
|
||||||
def _tlv_array(self, array):
|
|
||||||
# Convert to uint8 degrees before sending
|
|
||||||
arr_uint8 = array.astype(np.uint8)
|
|
||||||
return self._tlv(FT_ARRAY_F32, arr_uint8.tobytes())
|
|
||||||
|
|
||||||
def _tlv_string(self, text):
|
|
||||||
return self._tlv(FT_STRING, text.encode("utf-8"))
|
|
||||||
|
|
||||||
def _tlv_uint8(self, value):
|
|
||||||
return self._tlv(FT_UINT8, struct.pack("<B", value))
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
|
||||||
# TLV parsing
|
|
||||||
# --------------------------------------------------------
|
|
||||||
def _parse_tlv(self, payload):
|
|
||||||
pos = 0
|
|
||||||
fields = []
|
|
||||||
|
|
||||||
while pos < len(payload):
|
|
||||||
ftype, flen = struct.unpack(
|
|
||||||
TLV_FMT, payload[pos:pos + TLV_SIZE]
|
|
||||||
)
|
|
||||||
pos += TLV_SIZE
|
|
||||||
|
|
||||||
data = payload[pos:pos + flen]
|
|
||||||
pos += flen
|
|
||||||
|
|
||||||
fields.append((ftype, data))
|
|
||||||
|
|
||||||
return fields
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
|
||||||
# Socket helper
|
|
||||||
# --------------------------------------------------------
|
|
||||||
def _recv_exact(self, size):
|
|
||||||
data = b""
|
|
||||||
while len(data) < size:
|
|
||||||
try:
|
|
||||||
chunk = self.sock.recv(size - len(data))
|
|
||||||
if not chunk:
|
|
||||||
return None
|
|
||||||
data += chunk
|
|
||||||
except BlockingIOError:
|
|
||||||
return None
|
|
||||||
return data
|
|
||||||
+20
-17
@@ -9,6 +9,9 @@ import config as cfg
|
|||||||
# Globale Variablen
|
# Globale Variablen
|
||||||
robotCommunication = None
|
robotCommunication = None
|
||||||
shared_sim = None
|
shared_sim = None
|
||||||
|
emote = None
|
||||||
|
display_text = ""
|
||||||
|
|
||||||
if cfg.sim:
|
if cfg.sim:
|
||||||
shared_sim = Simulation()
|
shared_sim = Simulation()
|
||||||
else:
|
else:
|
||||||
@@ -17,7 +20,7 @@ else:
|
|||||||
else:
|
else:
|
||||||
robotCommunication = ESP32Communication()
|
robotCommunication = ESP32Communication()
|
||||||
|
|
||||||
vector_dirmov = [0, 0] # Direction Movement
|
vector_dirmov = [0, 0, 0] # Direction Movement [vx, vy, omega]
|
||||||
current_rad: dt.RadArray # Current Rad
|
current_rad: dt.RadArray # Current Rad
|
||||||
current_pos: dt.PosArray # Current Position
|
current_pos: dt.PosArray # Current Position
|
||||||
|
|
||||||
@@ -43,26 +46,26 @@ control_pause: bool = False
|
|||||||
# [90, 150, 65],
|
# [90, 150, 65],
|
||||||
# ]
|
# ]
|
||||||
#)
|
#)
|
||||||
init_deg: dt.DegArray = dt.DegArray(
|
|
||||||
[
|
|
||||||
[90, 30, 95],
|
|
||||||
[90, 30, 95],
|
|
||||||
[90, 30, 95],
|
|
||||||
[90, 150, 85],
|
|
||||||
[90, 150, 85],
|
|
||||||
[90, 150, 85],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
#init_deg: dt.DegArray = dt.DegArray(
|
#init_deg: dt.DegArray = dt.DegArray(
|
||||||
# [
|
# [
|
||||||
# [90, 45, 140],
|
# [90, 30, 95],
|
||||||
# [90, 45, 140],
|
# [90, 30, 95],
|
||||||
# [90, 45, 140],
|
# [90, 30, 95],
|
||||||
# [90, 135, 40],
|
# [90, 150, 85],
|
||||||
# [90, 135, 40],
|
# [90, 150, 85],
|
||||||
# [90, 135, 40],
|
# [90, 150, 85],
|
||||||
# ]
|
# ]
|
||||||
#)
|
#)
|
||||||
|
init_deg: dt.DegArray = dt.DegArray(
|
||||||
|
[
|
||||||
|
[90, 45, 140],
|
||||||
|
[90, 45, 140],
|
||||||
|
[90, 45, 140],
|
||||||
|
[90, 135, 40],
|
||||||
|
[90, 135, 40],
|
||||||
|
[90, 135, 40],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
init90_deg: dt.DegArray = dt.DegArray(
|
init90_deg: dt.DegArray = dt.DegArray(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,67 +1,103 @@
|
|||||||
|
|
||||||
# JackBot — Hexapod Control & Simulation
|
# JackBot — Hexapod Control & Simulation
|
||||||
|
|
||||||
**Purpose**
|
JackBot is a Python project for controlling and simulating a six-legged hexapod robot.
|
||||||
- **Overview**: JackBot is a Python project for controlling and simulating a six-legged (hexapod) robot. It computes foot trajectories, runs inverse kinematics against a URDF model, and sends joint commands to either a hardware controller (ESP32 / Arduino) or a PyBullet simulation.
|
It uses IKPy for inverse kinematics, PyBullet for optional simulation, and can send joint commands to ESP32 or Arduino hardware.
|
||||||
|
|
||||||
**Setup Dependencies and Virtual Enviroment**
|
## Requirements
|
||||||
- **Python packages**: At minimum the project uses `numpy`, `pygame`, `ikpy`, `pybullet`, `pyserial`, and `matplotlib`.
|
|
||||||
- **Install** (recommended inside a venv):
|
|
||||||
|
|
||||||
Linux / macOS (Bash):
|
- Python 3.12 recommended on Windows
|
||||||
|
- Python 3.11 / 3.12 is safest for `pygame` compatibility
|
||||||
|
- Required Python packages:
|
||||||
|
- `numpy`
|
||||||
|
- `pygame`
|
||||||
|
- `ikpy`
|
||||||
|
- `pybullet`
|
||||||
|
- `pyserial`
|
||||||
|
- `matplotlib`
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Linux / macOS (Bash)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip setuptools wheel
|
||||||
pip install numpy pygame ikpy pybullet pyserial matplotlib
|
pip install numpy pygame ikpy pybullet pyserial matplotlib
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows (PowerShell):
|
### Windows (PowerShell)
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
.\.venv\Scripts\Activate.ps1
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
python -m pip install --upgrade pip setuptools wheel
|
||||||
pip install numpy pygame ikpy pybullet pyserial matplotlib
|
pip install numpy pygame ikpy pybullet pyserial matplotlib
|
||||||
```
|
```
|
||||||
|
|
||||||
If PowerShell blocks activation, run:
|
If PowerShell blocks activation:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned
|
Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned
|
||||||
.\.venv\Scripts\Activate.ps1
|
.\.venv\Scripts\Activate.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
**Quick Start**
|
## Run
|
||||||
- **Simulation**: enable the simulator in `config.py` by setting `sim = True`, then run:
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
or on Linux/macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 main.py
|
python3 main.py
|
||||||
```
|
```
|
||||||
- **Hardware**: set `sim = False` in `config.py` and choose `arduinoConnection = True` or provide your ESP32 settings. Verify `port`, `baudrate`, `esp32_ip`, and `esp32_port` in `config.py`.
|
|
||||||
|
|
||||||
**Project structure (key files)**
|
## Configuration
|
||||||
- **Entry point**: [main.py](main.py) — starts the controller and state loop.
|
|
||||||
- **Controller**: [Controller.py](Controller.py) — joystick input and `ControlIntent` updates.
|
|
||||||
- **Kinematics**: [kinematics.py](kinematics.py) — URDF-based forward/inverse kinematics using IKPy.
|
|
||||||
- **Simulation**: [simulation.py](simulation.py) — PyBullet visualization and joint control.
|
|
||||||
- **Hardware comms**: [EspCommunication.py](EspCommunication.py) and [ArduinoCommunication.py](ArduinoCommunication.py) — send commands to ESP32 or Arduino.
|
|
||||||
- **Runtime globals**: [GlobalVariables.py](GlobalVariables.py) — central runtime objects and flags.
|
|
||||||
- **Types**: [DataTypes.py](DataTypes.py) — `PosArray`, `DegArray`, `RadArray`, `ControlIntent`.
|
|
||||||
- **States**: [RobotState/idle.py](RobotState/idle.py) and [RobotState/walking.py](RobotState/walking.py) — state machine implementations.
|
|
||||||
- **Config**: [config.py](config.py) — ports, flags (`sim`, `arduinoConnection`), URDF path, timing values.
|
|
||||||
|
|
||||||
**How it works**
|
Edit `config.py` before running:
|
||||||
- `main.py` creates a `ControlIntent` and `RobotContext`, starts the controller thread, and runs a state loop. The `WalkingState` computes per-leg foot paths (Bezier/linear), converts positions to joint angles with `kinematics.ikpyInverse()`, then sends commands to hardware or updates the simulator.
|
|
||||||
|
|
||||||
**Configuration notes**
|
- `sim = True` to enable PyBullet simulation
|
||||||
- **URDF**: the robot description file is `JackBotUrdf.urdf`; ensure the path in `config.py` (`urdf_path`) is correct.
|
- `sim = False` to use hardware control
|
||||||
- **Timing**: step timing and tick rate are configured in `config.py` (`standard_duration`, `standard_tickpersec`).
|
- `arduinoConnection = True` to use Arduino
|
||||||
|
- `arduinoConnection = False` to use ESP32
|
||||||
|
- `port` and `baudrate` for Arduino
|
||||||
|
- `esp32_ip` and `esp32_port` for ESP32
|
||||||
|
- `urdf_path = "JackBotUrdf.urdf"`
|
||||||
|
|
||||||
**Suggested next steps**
|
## Project structure
|
||||||
- Add a `requirements.txt` or `pyproject.toml` for reproducible installs.
|
|
||||||
- Document hardware wiring and expected serial/ESP packet formats if you plan to use hardware.
|
|
||||||
- Provide an example `config.py` for common setups (sim vs hardware).
|
|
||||||
|
|
||||||
**License & Contributing**
|
- `main.py` — main application entry point
|
||||||
- No license file included. Add `LICENSE` if you want to publish or share.
|
- `Controller.py` — Pygame-based controller and input display
|
||||||
- Contributions: open an issue or PR with improvements; add tests for kinematics if possible.
|
- `kinematics.py` — IKPy forward/inverse kinematics
|
||||||
|
- `simulation.py` — PyBullet simulation wrapper
|
||||||
|
- `GlobalVariables.py` — shared runtime state and comms
|
||||||
|
- `DataTypes.py` — typed arrays and control intent
|
||||||
|
- `RobotState/idle.py` — idle robot state
|
||||||
|
- `RobotState/walking.py` — walking robot state
|
||||||
|
- `EspCommunication.py` — ESP32 communication
|
||||||
|
- `ArduinoCommunication.py` — Arduino communication
|
||||||
|
- `JackBotUrdf.urdf` — robot model file
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `GlobalVariables.py` initializes either `Simulation()` or the selected hardware comm class.
|
||||||
|
- `DataTypes.py` declares `PosArray`, `DegArray`, `RadArray`, `RobotCommand`, and `ControlIntent`.
|
||||||
|
- `Controller.py` updates `gv.vector_dirmov` and `gv.robot_state` from joystick input.
|
||||||
|
- `kinematics.py` loads leg chains from `JackBotUrdf.urdf` and computes IK.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- If `pygame` installation fails on Windows, use Python 3.12 and upgrade `pip setuptools wheel` first.
|
||||||
|
- If `python` is not found on Windows, install Python and enable "Add Python to PATH".
|
||||||
|
- If the program crashes on startup, verify `JackBotUrdf.urdf` path and `config.py` settings.
|
||||||
|
|
||||||
|
## Suggested improvements
|
||||||
|
|
||||||
|
- Add a `requirements.txt` or `pyproject.toml`.
|
||||||
|
- Add a `LICENSE` file before sharing the project.
|
||||||
|
- Document hardware wiring and packet formats for ESP32/Arduino.
|
||||||
+319
@@ -0,0 +1,319 @@
|
|||||||
|
import GlobalVariables as gv
|
||||||
|
import config as cfg
|
||||||
|
import DataTypes as dt
|
||||||
|
import kinematics as kin
|
||||||
|
import numpy as np
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Leg_Pair 1 {leg[num] = 0,2,4}
|
||||||
|
# Leg Pair 2 {leg[num] = 1,3,5}
|
||||||
|
|
||||||
|
def emitCalculation(target_rad: dt.RadArray):
|
||||||
|
if gv.shared_sim != None:
|
||||||
|
gv.shared_sim.updatePos(target_rad)
|
||||||
|
gv.shared_sim.step()
|
||||||
|
if gv.robotCommunication != None:
|
||||||
|
gv.robotCommunication.send_motion(target_rad)
|
||||||
|
gv.current_rad = target_rad
|
||||||
|
|
||||||
|
def initPos():
|
||||||
|
# Init Position and gv.current_rad
|
||||||
|
gv.current_rad = gv.init_deg.to_rad()
|
||||||
|
gv.current_pos = gv.center_points
|
||||||
|
target_rad: dt.RadArray = kin.ikpyInverse(gv.center_points)
|
||||||
|
emitCalculation(target_rad)
|
||||||
|
|
||||||
|
|
||||||
|
def walking(duration=cfg.standard_duration, tickpersec=cfg.standard_tickpersec, curve_height=cfg.step_height):
|
||||||
|
# init Values
|
||||||
|
ticks = int(duration * tickpersec)
|
||||||
|
tick_duration = 1 / tickpersec
|
||||||
|
tick_pos: dt.PosArray # aktuelle XYZ-Positionen
|
||||||
|
current_pos_copy: dt.PosArray = gv.current_pos
|
||||||
|
vx, vy, omega = gv.vector_dirmov
|
||||||
|
|
||||||
|
# Apply config scaling
|
||||||
|
vx *= cfg.translation_gain
|
||||||
|
vy *= cfg.translation_gain
|
||||||
|
omega *= cfg.rotation_gain
|
||||||
|
|
||||||
|
target_pos_temp = []
|
||||||
|
|
||||||
|
for i in range(6):
|
||||||
|
cx, cy, cz = gv.center_points[i]
|
||||||
|
|
||||||
|
# relative position (assuming body center = 0,0)
|
||||||
|
rx = cx
|
||||||
|
ry = cy
|
||||||
|
|
||||||
|
# rotation component
|
||||||
|
v_rot_x = -omega * ry
|
||||||
|
v_rot_y = omega * rx
|
||||||
|
|
||||||
|
# combine translation + rotation
|
||||||
|
v_x = vx + v_rot_x
|
||||||
|
v_y = vy + v_rot_y
|
||||||
|
|
||||||
|
# normalize combined vector (important!)
|
||||||
|
length = (v_x**2 + v_y**2) ** 0.5
|
||||||
|
if length > 1.0:
|
||||||
|
v_x /= length
|
||||||
|
v_y /= length
|
||||||
|
|
||||||
|
target_pos_temp.append([
|
||||||
|
cx + v_x * cfg.step_length,
|
||||||
|
cy + v_y * cfg.step_length,
|
||||||
|
cz
|
||||||
|
])
|
||||||
|
target_pos: dt.PosArray = target_pos_temp
|
||||||
|
|
||||||
|
def interpolate(t, p0, p1, p2):
|
||||||
|
return (1 - t) ** 2 * p0 + 2 * (1 - t) * t * p1 + t**2 * p2
|
||||||
|
|
||||||
|
# Smooth Step
|
||||||
|
for tick in range(ticks + 1):
|
||||||
|
loop_start = time.perf_counter()
|
||||||
|
t = tick / ticks
|
||||||
|
tick_pos_temp = []
|
||||||
|
|
||||||
|
for leg_id in range(6):
|
||||||
|
if gv.leg_state[leg_id] == "drag":
|
||||||
|
# lineares Gleiten in die Center-Position
|
||||||
|
tick_pos_temp.append(current_pos_copy[leg_id] + (gv.center_points[leg_id] - current_pos_copy[leg_id]) * t)
|
||||||
|
|
||||||
|
elif gv.leg_state[leg_id] == "step":
|
||||||
|
# Mittlerer Kontrollpunkt für Bezier-Kurve
|
||||||
|
mid_point = [
|
||||||
|
(current_pos_copy[leg_id][0] + target_pos[leg_id][0]) / 2,
|
||||||
|
(current_pos_copy[leg_id][1] + target_pos[leg_id][1]) / 2,
|
||||||
|
max(current_pos_copy[leg_id][2], target_pos[leg_id][2])
|
||||||
|
+ cfg.step_height,
|
||||||
|
]
|
||||||
|
x = interpolate(
|
||||||
|
t, current_pos_copy[leg_id][0], mid_point[0], target_pos[leg_id][0]
|
||||||
|
)
|
||||||
|
y = interpolate(
|
||||||
|
t, current_pos_copy[leg_id][1], mid_point[1], target_pos[leg_id][1]
|
||||||
|
)
|
||||||
|
z = interpolate(
|
||||||
|
t, current_pos_copy[leg_id][2], mid_point[2], target_pos[leg_id][2]
|
||||||
|
)
|
||||||
|
tick_pos_temp.append([x, y, z])
|
||||||
|
|
||||||
|
tick_pos = dt.PosArray(tick_pos_temp)
|
||||||
|
# IK mit letzter Winkelstellung als Startpunkt
|
||||||
|
emitCalculation(kin.ikpyInverse(tick_pos))
|
||||||
|
print("Tick Position: \n", tick_pos)
|
||||||
|
if (cfg.sim == True):
|
||||||
|
gv.shared_sim.step()
|
||||||
|
|
||||||
|
# Timing anpassen, damit Loop gleichmäßig bleibt
|
||||||
|
elapsed = time.perf_counter() - loop_start
|
||||||
|
sleep_time = tick_duration - elapsed
|
||||||
|
if sleep_time > 0:
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
# Endposition sichern
|
||||||
|
#emitCalculation(kin.ikpyInverse(target_pos))
|
||||||
|
#print("END TargetPos:\n", target_pos, "\n\n\n")
|
||||||
|
|
||||||
|
# Update current pos
|
||||||
|
gv.current_pos = tick_pos
|
||||||
|
gv.current_rad = kin.ikpyInverse(tick_pos)
|
||||||
|
#emitCalculation(kin.ikpyInverse(tick_pos))
|
||||||
|
|
||||||
|
# Gait-Zustand wechseln
|
||||||
|
if gv.leg_state[0] == "step":
|
||||||
|
gv.leg_state = np.array(["drag", "step", "drag", "step", "drag", "step"])
|
||||||
|
else:
|
||||||
|
gv.leg_state = np.array(["step", "drag", "step", "drag", "step", "drag"])
|
||||||
|
|
||||||
|
def walking_four(duration=cfg.standard_duration, tickpersec=cfg.standard_tickpersec, curve_height=cfg.step_height):
|
||||||
|
# init Values
|
||||||
|
ticks = duration * tickpersec
|
||||||
|
tick_duration = 1 / tickpersec
|
||||||
|
tick_pos: dt.PosArray # aktuelle XYZ-Positionen
|
||||||
|
current_rad_copy: dt.RadArray = gv.current_rad
|
||||||
|
current_pos_copy: dt.PosArray = gv.current_pos
|
||||||
|
dirmov_copy = gv.vector_dirmov
|
||||||
|
|
||||||
|
# Zielpunkte für jede Beinspitze berechnen
|
||||||
|
target_pos_temp = []
|
||||||
|
for i in range(6):
|
||||||
|
target_pos_temp.append(
|
||||||
|
[
|
||||||
|
gv.center_points[i][0] + dirmov_copy[0] * cfg.step_length,
|
||||||
|
gv.center_points[i][1] + dirmov_copy[1] * cfg.step_length,
|
||||||
|
gv.center_points[i][2]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
target_pos: dt.PosArray = target_pos_temp
|
||||||
|
|
||||||
|
def interpolate(t, p0, p1, p2):
|
||||||
|
return (1 - t) ** 2 * p0 + 2 * (1 - t) * t * p1 + t**2 * p2
|
||||||
|
|
||||||
|
# Smooth Step
|
||||||
|
for tick in range(int(ticks) + 1):
|
||||||
|
loop_start = time.perf_counter()
|
||||||
|
t = tick / ticks
|
||||||
|
tick_pos_temp = []
|
||||||
|
|
||||||
|
for leg_id in range(6):
|
||||||
|
if gv.leg_state[leg_id] == "drag":
|
||||||
|
# lineares Gleiten in die Center-Position
|
||||||
|
tick_pos_temp.append(current_pos_copy[leg_id] + (gv.center_points[leg_id] - current_pos_copy[leg_id]) * t)
|
||||||
|
|
||||||
|
elif gv.leg_state[leg_id] == "step":
|
||||||
|
# Mittlerer Kontrollpunkt für Bezier-Kurve
|
||||||
|
mid_point = [
|
||||||
|
(current_pos_copy[leg_id][0] + target_pos[leg_id][0]) / 2,
|
||||||
|
(current_pos_copy[leg_id][1] + target_pos[leg_id][1]) / 2,
|
||||||
|
max(current_pos_copy[leg_id][2], target_pos[leg_id][2])
|
||||||
|
+ cfg.step_height,
|
||||||
|
]
|
||||||
|
x = interpolate(
|
||||||
|
t, current_pos_copy[leg_id][0], mid_point[0], target_pos[leg_id][0]
|
||||||
|
)
|
||||||
|
y = interpolate(
|
||||||
|
t, current_pos_copy[leg_id][1], mid_point[1], target_pos[leg_id][1]
|
||||||
|
)
|
||||||
|
z = interpolate(
|
||||||
|
t, current_pos_copy[leg_id][2], mid_point[2], target_pos[leg_id][2]
|
||||||
|
)
|
||||||
|
tick_pos_temp.append([x, y, z])
|
||||||
|
|
||||||
|
tick_pos = dt.PosArray(tick_pos_temp)
|
||||||
|
# IK mit letzter Winkelstellung als Startpunkt
|
||||||
|
emitCalculation(kin.ikpyInverse(tick_pos))
|
||||||
|
print("Tick Position: \n", tick_pos)
|
||||||
|
|
||||||
|
# Timing anpassen, damit Loop gleichmäßig bleibt
|
||||||
|
elapsed = time.perf_counter() - loop_start
|
||||||
|
sleep_time = tick_duration - elapsed
|
||||||
|
if sleep_time > 0:
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
# Endposition sichern
|
||||||
|
#emitCalculation(kin.ikpyInverse(target_pos))
|
||||||
|
print("END TargetPos:\n", target_pos, "\n\n\n")
|
||||||
|
|
||||||
|
# Gait-Zustand wechseln
|
||||||
|
if gv.leg_state[0] == "step":
|
||||||
|
gv.leg_state = np.array(["drag", "drag", "step", "drag", "drag", "drag"])
|
||||||
|
elif gv.leg_state[2] == "step":
|
||||||
|
gv.leg_state = np.array(["drag", "drag", "drag", "step", "drag", "drag"])
|
||||||
|
elif gv.leg_state[3] == "step":
|
||||||
|
gv.leg_state = np.array(["drag", "drag", "drag", "drag", "drag", "step"])
|
||||||
|
elif gv.leg_state[5] == "step":
|
||||||
|
gv.leg_state = np.array(["step", "drag", "drag", "drag", "drag", "drag"])
|
||||||
|
|
||||||
|
def wave_emote(cycles=3, duration=1.5, tickpersec=20,
|
||||||
|
height=40, amplitude=25, inward_offset=10):
|
||||||
|
"""
|
||||||
|
Greeting wave using front leg (leg 0)
|
||||||
|
|
||||||
|
Motion:
|
||||||
|
- Z: lifts leg up
|
||||||
|
- Y: waves left/right
|
||||||
|
- X: slightly pulled inward to avoid IK limits
|
||||||
|
"""
|
||||||
|
|
||||||
|
ticks = int(duration * tickpersec)
|
||||||
|
tick_duration = 1 / tickpersec
|
||||||
|
|
||||||
|
# Safe copy
|
||||||
|
base_pos = dt.PosArray(np.copy(gv.current_pos.data))
|
||||||
|
|
||||||
|
leg_id = 0 # front leg
|
||||||
|
|
||||||
|
for cycle in range(cycles):
|
||||||
|
for tick in range(ticks):
|
||||||
|
loop_start = time.perf_counter()
|
||||||
|
|
||||||
|
t = tick / ticks
|
||||||
|
tick_pos = np.copy(base_pos.data)
|
||||||
|
|
||||||
|
cx, cy, cz = base_pos[leg_id]
|
||||||
|
|
||||||
|
# smooth outward-only wave
|
||||||
|
y_wave = math.sin(4 * math.pi * t)
|
||||||
|
y_offset = amplitude * (0.5 * (y_wave + 1))
|
||||||
|
|
||||||
|
# vertical lift
|
||||||
|
z_offset = height * math.sin(math.pi * t)
|
||||||
|
|
||||||
|
# clamp sideways motion
|
||||||
|
max_y_dev = 30
|
||||||
|
new_y = cy + y_offset
|
||||||
|
new_y = max(cy - max_y_dev, min(cy + max_y_dev, new_y))
|
||||||
|
|
||||||
|
tick_pos[leg_id] = [
|
||||||
|
cx + 10, # small forward bias (IMPORTANT)
|
||||||
|
new_y,
|
||||||
|
cz + z_offset
|
||||||
|
]
|
||||||
|
|
||||||
|
tick_pos = dt.PosArray(tick_pos)
|
||||||
|
|
||||||
|
emitCalculation(kin.ikpyInverse(tick_pos))
|
||||||
|
|
||||||
|
# Timing
|
||||||
|
elapsed = time.perf_counter() - loop_start
|
||||||
|
sleep_time = tick_duration - elapsed
|
||||||
|
if sleep_time > 0:
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
# Return to base pose
|
||||||
|
emitCalculation(kin.ikpyInverse(base_pos))
|
||||||
|
gv.current_pos = base_pos
|
||||||
|
|
||||||
|
def laola_wave_emote(cycles=3, duration=1.5, tickpersec=cfg.standard_tickpersec, height=10, amplitude=30):
|
||||||
|
ticks = int(duration * tickpersec)
|
||||||
|
tick_duration = 1 / tickpersec
|
||||||
|
|
||||||
|
# Safe copy of base position
|
||||||
|
base_pos = dt.PosArray(np.copy(gv.current_pos.data))
|
||||||
|
|
||||||
|
# Legs that perform the wave
|
||||||
|
wave_legs = [1, 4]
|
||||||
|
|
||||||
|
for cycle in range(cycles):
|
||||||
|
for tick in range(ticks):
|
||||||
|
loop_start = time.perf_counter()
|
||||||
|
t = tick / ticks # normalized 0 → 1
|
||||||
|
tick_pos = np.copy(base_pos.data)
|
||||||
|
|
||||||
|
for leg_id in wave_legs:
|
||||||
|
cx, cy, cz = base_pos[leg_id]
|
||||||
|
|
||||||
|
# Phase shift for Laola wave
|
||||||
|
phase = 0 if leg_id == 1 else math.pi
|
||||||
|
|
||||||
|
# Sideways motion (Y) — reduced amplitude to avoid overextension
|
||||||
|
y_offset = amplitude * math.sin(2 * math.pi * t + phase)
|
||||||
|
|
||||||
|
# Vertical motion (Z) — full up/down oscillation
|
||||||
|
z_offset = height * math.sin(2 * math.pi * t + phase)
|
||||||
|
|
||||||
|
# Apply movement in YZ plane, X stays fixed
|
||||||
|
tick_pos[leg_id] = [
|
||||||
|
cx,
|
||||||
|
cy + y_offset,
|
||||||
|
cz + z_offset
|
||||||
|
]
|
||||||
|
|
||||||
|
tick_pos = dt.PosArray(tick_pos)
|
||||||
|
|
||||||
|
# IK + send command
|
||||||
|
emitCalculation(kin.ikpyInverse(tick_pos))
|
||||||
|
|
||||||
|
# Timing control
|
||||||
|
elapsed = time.perf_counter() - loop_start
|
||||||
|
sleep_time = tick_duration - elapsed
|
||||||
|
if sleep_time > 0:
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
# Return to original stance
|
||||||
|
emitCalculation(kin.ikpyInverse(base_pos))
|
||||||
|
gv.current_pos = base_pos
|
||||||
@@ -16,8 +16,10 @@ urdf_path = "JackBotUrdf.urdf"
|
|||||||
|
|
||||||
# Dimensions in m
|
# Dimensions in m
|
||||||
#robot_height = -0.1
|
#robot_height = -0.1
|
||||||
step_height = 0.05
|
step_height = 0.03
|
||||||
step_length = 0.08
|
step_length = 0.04
|
||||||
|
translation_gain = 1.0
|
||||||
|
rotation_gain = 2.0
|
||||||
# Leglength in m
|
# Leglength in m
|
||||||
# L1 = 0.068
|
# L1 = 0.068
|
||||||
# L2 = 0.0602
|
# L2 = 0.0602
|
||||||
@@ -25,5 +27,5 @@ step_length = 0.08
|
|||||||
|
|
||||||
# standard_tickpersec = # 20 Fluessige Bewegung
|
# standard_tickpersec = # 20 Fluessige Bewegung
|
||||||
standard_tickpersec:float = 25 # [ticks/s]
|
standard_tickpersec:float = 25 # [ticks/s]
|
||||||
standard_duration:float = 0.4 # [s],ip
|
standard_duration:float = 0.8 # [s]
|
||||||
standard_tickduration:float = 1 / standard_tickpersec
|
standard_tickduration:float = 1 / standard_tickpersec
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from ikpy.chain import Chain
|
from ikpy.chain import Chain
|
||||||
from ikpy.link import OriginLink, URDFLink
|
from ikpy.link import OriginLink, URDFLink
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
|
|||||||
@@ -1,62 +1,60 @@
|
|||||||
|
from threading import Thread
|
||||||
|
from multiprocessing import Process, Manager, Queue
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Selfmade Libraries
|
||||||
|
import Controller as ctr
|
||||||
|
import RobotState as rs
|
||||||
|
|
||||||
# Global Variables
|
# Global Variables
|
||||||
import GlobalVariables as gv
|
import GlobalVariables as gv
|
||||||
|
|
||||||
# main.py
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from DataTypes import ControlIntent
|
def robot_control():
|
||||||
from RobotState.RobotState import RobotContext
|
# Connection
|
||||||
from Controller import controller_loop
|
if gv.robotCommunication != None:
|
||||||
from RobotState.idle import IdleState
|
gv.robotCommunication.start()
|
||||||
from RobotState.walking import WalkingState
|
|
||||||
|
|
||||||
import DataTypes as dt
|
# Start Position
|
||||||
|
rs.initPos()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
def main():
|
############################## Main Loop ##############################
|
||||||
intent = ControlIntent()
|
tick = 0.05
|
||||||
lock = threading.Lock()
|
next_time = time.time()
|
||||||
ctx = RobotContext()
|
|
||||||
|
|
||||||
ctx.center_points = dt.PosArray([
|
while gv.control_pause == False:
|
||||||
[50, 50, -80],
|
next_time += tick
|
||||||
[50,-50, -80],
|
|
||||||
[0, 70, -80],
|
|
||||||
[0,-70, -80],
|
|
||||||
[-50,50, -80],
|
|
||||||
[-50,-50,-80]
|
|
||||||
])
|
|
||||||
ctx.current_pos = ctx.center_points.copy()
|
|
||||||
|
|
||||||
controller_thread = threading.Thread(
|
if gv.emote == "wave":
|
||||||
target=controller_loop,
|
rs.initPos()
|
||||||
args=(intent, lock),
|
time.sleep(0.3)
|
||||||
daemon=True
|
rs.wave_emote()
|
||||||
)
|
gv.emote = None
|
||||||
controller_thread.start()
|
gv.robot_state = "idle"
|
||||||
|
continue
|
||||||
|
|
||||||
states = {
|
# Robot
|
||||||
"idle": IdleState(),
|
if gv.robot_state == "idle":
|
||||||
"walking": WalkingState()
|
rs.initPos()
|
||||||
}
|
time.sleep(0.2)
|
||||||
|
elif gv.robot_state == "walking":
|
||||||
|
rs.walking()
|
||||||
|
|
||||||
current = states["idle"]
|
# Simulation
|
||||||
current.on_enter(ctx)
|
if gv.shared_sim != None:
|
||||||
|
gv.shared_sim.step()
|
||||||
|
|
||||||
last = time.perf_counter()
|
sleep_time = next_time - time.time()
|
||||||
|
if sleep_time > 0:
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
while not intent.quit:
|
|
||||||
now = time.perf_counter()
|
|
||||||
dt_s = now - last
|
|
||||||
last = now
|
|
||||||
|
|
||||||
next_state = current.update(ctx, intent, dt_s)
|
|
||||||
if next_state:
|
|
||||||
current.on_exit(ctx)
|
|
||||||
current = states[next_state]
|
|
||||||
current.on_enter(ctx)
|
|
||||||
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
controller_queue = Queue()
|
||||||
|
robot_queue = Queue()
|
||||||
|
|
||||||
|
controller_thread = Thread(target=ctr.controller)
|
||||||
|
controller_thread.start()
|
||||||
|
robot_control_thread = Thread(target=robot_control)
|
||||||
|
robot_control_thread.start()
|
||||||
Reference in New Issue
Block a user