Skip to content

Instantly share code, notes, and snippets.

@depau
Last active November 20, 2023 19:28
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save depau/904ce14b04d935b6f9829cdf2cda64f3 to your computer and use it in GitHub Desktop.
Save depau/904ce14b04d935b6f9829cdf2cda64f3 to your computer and use it in GitHub Desktop.
Valetudo Gamepad Control

PyGame-based Valetudo Gamepad Remote Control

Uses PyGame, so it should work with all gamepads that work in actual games.

Nasty code ;)

It requires the gamepad to have at least one analog stick.

Modify the script and set DRY_RUN to True to avoid contacting Valetudo during gamepad testing.

Find the horizontal and vertical axis for your gamepad and adjust the global variables.

MOVEMENT_TIME and ROTATION_TIME indicate how long in seconds it takes for the vacuum to do a full step when going straight and when rotating. The included values are for viomi.

The stop method in vale.py is specifically for Viomi vacuums (I forgot to implement it in the capability). If you're using another vacuum, replace it with something appropriate or just return so it does nothing.

import os
import time
import pygame
import vale
DRY_RUN = False
VERT_AXIS = 1
HORZ_AXIS = 0
VERT_AXIS_DIR = -1
HORZ_AXIS_DIR = 1
CENTER_THRESH_VERT = 0.5
CENTER_THRESH_HORZ_MOVING = 0.2
CENTER_THRESH_HORZ_STOPPED = 0.5
ROTATION_TIME = 0.5
MOVEMENT_TIME = 1.5
rc = vale.RemoteControl(os.environ.get("VALE_URL"))
if not DRY_RUN:
rc.enter()
# Define some colors.
BLACK = pygame.Color('black')
WHITE = pygame.Color('white')
RED = pygame.Color('red')
last_cmd_time = time.time()
last_cmd = "stop"
JS_PREVIEW_RADIUS = 150
JS_PREVIEW_CENTER = [300 + JS_PREVIEW_RADIUS, 300 + JS_PREVIEW_RADIUS]
# This is a simple class that will help us print to the screen.
# It has nothing to do with the joysticks, just outputting the
# information.
class TextPrint(object):
def __init__(self):
self.reset()
self.font = pygame.font.Font(None, 20)
def tprint(self, screen, textString):
textBitmap = self.font.render(textString, True, BLACK)
screen.blit(textBitmap, (self.x, self.y))
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
try:
pygame.init()
# Set the width and height of the screen (width, height).
screen = pygame.display.set_mode((650, 650))
pygame.display.set_caption("Whack a Robot")
# Loop until the user clicks the close button.
done = False
# Used to manage how fast the screen updates.
clock = pygame.time.Clock()
# Initialize the joysticks.
pygame.joystick.init()
# Get ready to print.
textPrint = TextPrint()
# -------- Main Program Loop -----------
while not done:
#
# EVENT PROCESSING STEP
#
# Possible joystick actions: JOYAXISMOTION, JOYBALLMOTION, JOYBUTTONDOWN,
# JOYBUTTONUP, JOYHATMOTION
for event in pygame.event.get(): # User did something.
if event.type == pygame.QUIT: # If user clicked close.
done = True # Flag that we are done so we exit this loop.
elif event.type == pygame.JOYBUTTONDOWN:
print("Joystick button pressed.")
elif event.type == pygame.JOYBUTTONUP:
print("Joystick button released.")
#
# DRAWING STEP
#
# First, clear the screen to white. Don't put other drawing commands
# above this, or they will be erased with this command.
screen.fill(WHITE)
textPrint.reset()
# Get count of joysticks.
joystick_count = pygame.joystick.get_count()
textPrint.tprint(screen, "Number of joysticks: {}".format(joystick_count))
textPrint.indent()
joystick = pygame.joystick.Joystick(0)
joystick.init()
# Print JS info
try:
jid = joystick.get_instance_id()
except AttributeError:
# get_instance_id() is an SDL2 method
jid = joystick.get_id()
textPrint.tprint(screen, "Joystick {}".format(jid))
textPrint.indent()
# Get the name from the OS for the controller/joystick.
name = joystick.get_name()
textPrint.tprint(screen, "Joystick name: {}".format(name))
try:
guid = joystick.get_guid()
except AttributeError:
# get_guid() is an SDL2 method
pass
else:
textPrint.tprint(screen, "GUID: {}".format(guid))
# Usually axis run in pairs, up/down for one, and left/right for
# the other.
axes = joystick.get_numaxes()
textPrint.tprint(screen, "Number of axes: {}".format(axes))
textPrint.indent()
for i in range(axes):
axis = joystick.get_axis(i)
textPrint.tprint(screen, "Axis {} value: {:>6.3f}".format(i, axis))
textPrint.unindent()
buttons = joystick.get_numbuttons()
textPrint.tprint(screen, "Number of buttons: {}".format(buttons))
textPrint.indent()
for i in range(buttons):
button = joystick.get_button(i)
textPrint.tprint(screen,
"Button {:>2} value: {}".format(i, button))
textPrint.unindent()
hats = joystick.get_numhats()
textPrint.tprint(screen, "Number of hats: {}".format(hats))
textPrint.indent()
# Hat position. All or nothing for direction, not a float like
# get_axis(). Position is a tuple of int values (x, y).
for i in range(hats):
hat = joystick.get_hat(i)
textPrint.tprint(screen, "Hat {} value: {}".format(i, str(hat)))
textPrint.unindent()
textPrint.unindent()
# End print JS info
horz = joystick.get_axis(HORZ_AXIS) * HORZ_AXIS_DIR
if last_cmd == 'stop' or last_cmd == 'straight' and last_cmd_time + MOVEMENT_TIME < time.time() or last_cmd == 'turn' and last_cmd_time + ROTATION_TIME < time.time():
thresh = CENTER_THRESH_HORZ_STOPPED
else:
thresh = CENTER_THRESH_HORZ_MOVING
if abs(horz) < thresh:
horz = 0
vert = joystick.get_axis(VERT_AXIS) * VERT_AXIS_DIR
if abs(vert) < CENTER_THRESH_VERT:
vert = 0
# JS crosshair
pygame.draw.circle(screen, BLACK, JS_PREVIEW_CENTER, JS_PREVIEW_RADIUS, 1)
pygame.draw.line(screen, BLACK, [JS_PREVIEW_CENTER[0], JS_PREVIEW_CENTER[1] - JS_PREVIEW_RADIUS],
[JS_PREVIEW_CENTER[0], JS_PREVIEW_CENTER[1] + JS_PREVIEW_RADIUS])
pygame.draw.line(screen, BLACK, [JS_PREVIEW_CENTER[0] - JS_PREVIEW_RADIUS, JS_PREVIEW_CENTER[1]],
[JS_PREVIEW_CENTER[0] + JS_PREVIEW_RADIUS, JS_PREVIEW_CENTER[1]])
pyg_horz = horz * HORZ_AXIS_DIR
pyg_vert = vert * VERT_AXIS_DIR
pygame.draw.line(screen, RED, JS_PREVIEW_CENTER,
[JS_PREVIEW_CENTER[0] + JS_PREVIEW_RADIUS * pyg_horz,
JS_PREVIEW_CENTER[1] + JS_PREVIEW_RADIUS * pyg_vert], 2)
if not DRY_RUN:
if horz == vert == 0 and last_cmd != "stop":
# rc.exit()
# rc.enter()
rc.stop()
last_cmd = 'stop'
last_cmd_time = time.time()
elif horz == 0 and vert != 0:
timeout = 0
if last_cmd == "straight":
timeout = MOVEMENT_TIME
if last_cmd_time + timeout < time.time():
rc.move("forward" if vert > 0 else "backward")
last_cmd = 'straight'
last_cmd_time = time.time()
elif vert == 0 and horz != 0:
timeout = 0
if last_cmd == "turn":
timeout = ROTATION_TIME
if last_cmd_time + timeout < time.time():
rc.move("rotate_clockwise" if horz > 0 else "rotate_counterclockwise")
last_cmd = 'turn'
last_cmd_time = time.time()
elif horz != 0 and vert != 0:
# Sorta PWM rotation
next_cmd = "forward" if vert > 0 else "backward"
turn_timeout = ROTATION_TIME * abs(horz)
straight_timeout = ROTATION_TIME * (1 - abs(horz))
if last_cmd == "stop":
timeout = 0
elif last_cmd == "straight":
timeout = turn_timeout
next_cmd = "rotate_clockwise" if horz > 0 else "rotate_counterclockwise"
else:
timeout = straight_timeout
next_cmd = "forward" if vert > 0 else "backward"
if last_cmd_time + timeout < time.time():
rc.move(next_cmd)
last_cmd = 'straight' if 'rotate' not in next_cmd else 'turn'
last_cmd_time = time.time()
#
# ALL CODE TO DRAW SHOULD GO ABOVE THIS COMMENT
#
# Go ahead and update the screen with what we've drawn.
pygame.display.flip()
# Limit to 20 frames per second.
clock.tick(20)
finally:
if not DRY_RUN:
rc.exit()
# Close the window and quit.
# If you forget this line, the program will 'hang'
# on exit if running from IDLE.
pygame.quit()
import requests
class RemoteControl:
def __init__(self, robot_url: str):
self.robot_url = robot_url
self.manual_ctl = '/api/v2/robot/capabilities/ManualControlCapability'
def enter(self):
r = requests.put(self.robot_url + self.manual_ctl, json={'action': 'enable'})
print("enter", r)
def exit(self):
r = requests.put(self.robot_url + self.manual_ctl, json={'action': 'disable'})
print("exit", r)
def move(self, direction: str):
r = requests.put(self.robot_url + self.manual_ctl, json={'action': 'move', 'movementCommand': direction})
print(direction, r)
def stop(self):
r = requests.put(self.robot_url + "/api/v2/robot/capabilities/RawCommandCapability", json={'method': 'set_direction', 'args': [5]})
print("stop", r)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment