top of page

Software

Public·131 members

Bittle + XBOX Controller + Serial Port

I extended the code available and described here to also be able to use the Bittle connected to the serial port (as in my bittle box was no bluetooth/wifi adapter). I added some messages on the screen, I thought this might come in handy...


You need to: pip install pyBittle pygame


ree

ree

bittle_xbox_controller.py:


"""This program allow to control Bittle using Xbox controller.
"""

import math
import pyBittle
import pygame
import sys
import time

__initialauthor__ = "EnriqueMoran"


# Define some colors.
BLACK = pygame.Color('black')
WHITE = pygame.Color('white')

BUTTONS_MAP = {
    0: pyBittle.Command.BALANCE,   # A
    1: pyBittle.Command.REST,      # B
    2: pyBittle.Command.GREETING,  # X
    3: pyBittle.Command.SIT,       # Y
    4: pyBittle.Command.STEP,      # LB
    5: pyBittle.Command.GYRO       # RB
}


class TextPrint(object):
    def __init__(self):
        self.reset()
        self.font = pygame.font.Font(None, 20)

    def tprint(self, window, textString):
        textBitmap = self.font.render(textString, True, BLACK)
        window.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


class Controller():

    def __init__(self, window_size=(1, 1),
                 connect_wifi=False, connect_bluetooth=False,
                 connect_com=False,
                 ip_addr=None, device_name=None, bt_port=None):
        self.window_size = window_size
        self.window = None
        self.joystick = None
        self.n_axes = 0
        self.n_buttons = 0
        self.n_hats = 0

        self.bittle = None
        self.connect_wifi = connect_wifi
        self.connect_bluetooth = connect_bluetooth
        self.connect_com = connect_com
        self.ip_addr = ip_addr
        self.device_name = device_name
        self.bt_port = bt_port
        self.direction = pyBittle.Command.BALANCE

    def initialize(self):
        # os.environ["DISPLAY"] = ":0"
        # os.environ["SDL_VIDEODRIVER"] = "dummy"  # Hide window
        pygame.init()
        controller_found = False
        self.window = pygame.display.set_mode(self.window_size)
        pygame.display.set_caption("Bittle controller")
        self.clock = pygame.time.Clock()
        pygame.joystick.init()

        self.textPrint = TextPrint()

        self.bittle = pyBittle.Bittle()
        if self.connect_wifi:
            self.bittle.wifiManager.ip = self.ip_addr
        elif self.connect_bluetooth:
            self.isconnected = self.bittle.connect_bluetooth()
        elif self.connect_com:
            self.isconnected = self.bittle.connect_serial(True)
        try:
            self.joystick = pygame.joystick.Joystick(0)
            self.joystick.init()
            self.n_axes = self.joystick.get_numaxes()
            self.n_buttons = self.joystick.get_numbuttons()
            self.n_hats = self.joystick.get_numhats()
            print(f"Joystick found: {self.joystick.get_name()}")
            controller_found = True
        except:
            print("No controller found!")
        return controller_found

    def read_inputs(self):
        new_direction = self.direction
        x_axis_value = 0
        y_axis_value = 0
        for i in range(self.n_axes):
            axis_value = self.joystick.get_axis(i)
            if i == 0:  # Horizontal
                x_axis_value = axis_value
            elif i == 1:  # Vertical
                y_axis_value = -axis_value  # Set FORWARD as positive value

        if abs(x_axis_value) > 0.8 or abs(y_axis_value) > 0.8:
            angle = self.get_angle(x_axis_value, y_axis_value)
            if angle >= 337.5 or angle < 22.5:
                new_direction = pyBittle.Direction.FORWARD
            elif angle >= 22.5 and angle < 67.5:
                new_direction = pyBittle.Direction.FORWARDRIGHT
            elif angle >= 67.5 and angle < 112.5:
                new_direction = pyBittle.Direction.FORWARDRIGHT
            elif angle >= 112.5 and angle < 157.5:
                new_direction = pyBittle.Direction.BACKWARDRIGHT
            elif angle >= 157.5 and angle < 202.5:
                new_direction = pyBittle.Direction.BACKWARD
            elif angle >= 202.5 and angle < 247.5:
                new_direction = pyBittle.Direction.BACKWARDLEFT
            elif angle >= 247.5 and angle < 292.5:
                new_direction = pyBittle.Direction.FORWARDLEFT
            elif angle >= 292.5 and angle < 337.5:
                new_direction = pyBittle.Direction.FORWARDLEFT
        elif abs(x_axis_value) < 0.2 or abs(y_axis_value) < 0.2:
            new_direction = pyBittle.Command.BALANCE  # Stop

        if self.direction != new_direction:
            self.direction = new_direction
            self.send_direction(self.direction)

        self.textPrint.tprint(self.window, "Direction: {}".format(self.direction))

        for i in range(self.n_buttons):
            button = self.joystick.get_button(i)
            if button == 1:
                try:
                    command = BUTTONS_MAP[i]
                    self.send_command(command)
                    self.textPrint.tprint(self.window, "Command: {}".format(command))
                except Exception as e:
                    print(e)

        for i in range(self.n_hats):
            gait = None
            hat = self.joystick.get_hat(i)
            if hat == (-1, 0):  # Left pad
                gait = pyBittle.Gait.CRAWL
            elif hat == (1, 0):  # Right pad
                gait = pyBittle.Gait.TROT
            elif hat == (0, -1):  # Down pad
                gait = pyBittle.Gait.WALK
            elif hat == (0, 1):  # Up pad
                gait = pyBittle.Gait.RUN
            if gait:
                self.bittle.gait = gait
                print(f"New gait selected: {gait}")
                time.sleep(0.2)
            self.textPrint.tprint(self.window, "Gait: {}".format(self.bittle.gait))

    def run(self):
        initialized = self.initialize()
        if initialized:
            running = True
            clock = pygame.time.Clock()

            while running:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        running = False
                    elif event.type == pygame.JOYBUTTONDOWN:
                        print("Joystick button pressed.")
                    elif event.type == pygame.JOYBUTTONUP:
                        print("Joystick button released.")

                self.window.fill(WHITE)
                self.textPrint.reset()

                if self.connect_wifi:
                    self.textPrint.tprint(self.window, "WIFI Connnection")
                elif self.connect_bluetooth:
                    self.textPrint.tprint(self.window, "Bluetooth Connnection")
                else:
                    self.textPrint.tprint(self.window, "COM Connnection")

                if not self.connect_wifi:
                    self.textPrint.tprint(self.window, "Connnected: {}".format(self.isconnected))

                # Get count of joysticks.
                joystick_count = pygame.joystick.get_count()

                self.textPrint.tprint(self.window, "Number of joysticks: {}".format(joystick_count))
                self.textPrint.indent()

                # For each joystick:
                for i in range(joystick_count):
                    joystick = pygame.joystick.Joystick(i)
                    joystick.init()

                    try:
                        jid = joystick.get_instance_id()
                    except AttributeError:
                        # get_instance_id() is an SDL2 method
                        jid = joystick.get_id()
                    self.textPrint.tprint(self.window, "Joystick {}".format(jid))
                    self.textPrint.indent()

                    # Get the name from the OS for the controller/joystick.
                    name = joystick.get_name()
                    self.textPrint.tprint(self.window, "Joystick name: {}".format(name))

                    try:
                        guid = joystick.get_guid()
                    except AttributeError:
                        # get_guid() is an SDL2 method
                        pass
                    else:
                        self.textPrint.tprint(self.window, "GUID: {}".format(guid))

                    # Usually axis run in pairs, up/down for one, and left/right for
                    # the other.
                    axes = joystick.get_numaxes()
                    self.textPrint.tprint(self.window, "Number of axes: {}".format(axes))
                    self.textPrint.indent()

                    for i in range(axes):
                        axis = joystick.get_axis(i)
                        self.textPrint.tprint(self.window, "Axis {} value: {:>6.3f}".format(i, axis))
                    self.textPrint.unindent()

                    buttons = joystick.get_numbuttons()
                    self.textPrint.tprint(self.window, "Number of buttons: {}".format(buttons))
                    self.textPrint.indent()

                    for i in range(buttons):
                        button = joystick.get_button(i)
                        if(button == 1):
                            self.textPrint.tprint(self.window,
                                                  "Button {:>2} value: {}".format(i, button))
                    self.textPrint.unindent()

                    hats = joystick.get_numhats()
                    self.textPrint.tprint(self.window, "Number of hats: {}".format(hats))
                    self.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)
                        self.textPrint.tprint(self.window, "Hat {} value: {}".format(i, str(hat)))
                    self.textPrint.unindent()

                    self.textPrint.unindent()

                self.read_inputs()
                pygame.display.flip()
                self.clock.tick(20)
        pygame.quit()

    def send_command(self, command):
        if self.connect_wifi:
            if self.bittle.has_wifi_connection():
                self.bittle.send_command_wifi(command)
        elif self.connect_bluetooth:
            self.bittle.send_command_bluetooth(command)
        elif self.connect_com:
            self.bittle.send_command_serial(command)
        print(f"Action: {command} sent")
        time.sleep(0.5)  # Let Bittle rest to prevent damage
        return True

    def send_direction(self, direction):
        if self.connect_wifi:
            if self.bittle.has_wifi_connection():
                if direction == pyBittle.Command.BALANCE:
                    self.bittle.send_command_wifi(direction)
                else:
                    self.bittle.send_movement_wifi(direction)
        elif self.connect_bluetooth:
            if direction == pyBittle.Command.BALANCE:
                self.bittle.send_command_bluetooth(direction)
            else:
                self.bittle.send_movement_bluetooth(direction)
        elif self.connect_com:
            if direction == pyBittle.Command.BALANCE:
                self.bittle.send_command_serial(direction)
            else:
                self.bittle.send_movement_serial(direction)
        print(f"Direction: {direction} sent")
        # Let Bittle rest to prevent damage, modify this under your own risk
        time.sleep(0.5)
        return True

    def get_angle(self, xPercent, yPercent):
        """Returns joystick angle.
        """
        angle_deg = 0
        if xPercent > 0.9 and yPercent == 0:
            angle_deg = 90
        elif xPercent < -0.9 and yPercent == 0:
            angle_deg = 270
        elif xPercent == 0 and yPercent > 0.9:
            angle_deg = 1
        elif xPercent == 0 and yPercent < -0.9:
            angle_deg = 180
        elif (xPercent > 0 and yPercent > 0 and (xPercent > 0.2 or
              yPercent > 0.2)):
            angle_rad = math.atan2(xPercent, yPercent)
            angle_deg = angle_rad * 180 / math.pi
        elif (xPercent > 0 and yPercent < 0 and (xPercent > 0.2 or
              yPercent < -0.2)):
            angle_rad = math.atan2(xPercent, yPercent)
            angle_deg = angle_rad * 180 / math.pi
        elif (xPercent < 0 and yPercent < 0 and (xPercent < -0.2 or
              yPercent < -0.2)):
            angle_rad = math.atan2(xPercent, yPercent)
            angle_deg = angle_rad * 180 / math.pi
            angle_deg += 360
        elif (xPercent < 0 and yPercent > 0 and (xPercent < -0.2 or
              yPercent > 0.2)):
            angle_rad = math.atan2(xPercent, yPercent)
            angle_deg = angle_rad * 180 / math.pi
            angle_deg += 360
        return angle_deg


if __name__ == '__main__':
    connect_wifi = False  # Set to True to connect through WiFi
    connect_bluetooth = False  # Set to True to connect through Bluetooth
    connect_com = True  # Set to True to connect through COM port
    ip_addr = '192.168.1.138'  # Here goes your Bittle's IP address
    controller = Controller(connect_wifi=connect_wifi,
                            connect_bluetooth=connect_bluetooth,
                            connect_com=connect_com,
                            window_size=(500, 400),
                            ip_addr=ip_addr)
    if not connect_wifi and not connect_bluetooth and not connect_com:
        print("No connection method selected.")
        input("Press any key to exit.")
        sys.exit()
    try:
        controller.run()
    except KeyboardInterrupt:
        if controller.connect_bluetooth:
            print("Closing Bluetooth connection with Bittle")
            try:
                controller.bittle.send_command_bluetooth(pyBittle.Command.REST)
                controller.bittle.disconnect_bluetooth()
                print("Connection Closed")
            except:
                pass
        input("Press any key to exit.")
        sys.exit()
348 Views
Struzck
Struzck
May 25, 2021

I haven't had much time since pyBittle v1.1.3 was released (which adds the possibility to control Bittle through serial), once I have some free time this week I will update the projects that make uses of pyBittle

bottom of page