Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Gamepad controller investigation #1024

Open
laurensvalk opened this issue Apr 2, 2023 · 57 comments
Open

[Feature] Gamepad controller investigation #1024

laurensvalk opened this issue Apr 2, 2023 · 57 comments
Labels
enhancement New feature or request topic: bluetooth Issues involving bluetooth

Comments

@laurensvalk
Copy link
Member

This is a spot for discussing gamepad connections, split off from #262

@laurensvalk laurensvalk added enhancement New feature or request topic: bluetooth Issues involving bluetooth labels Apr 2, 2023
@laurensvalk
Copy link
Member Author

          > ### 4. BLE HID Peripherals

We may support connecting to generic HID devices over BLE (and possibly classic). Users could build on this to add specific mappings for certain devices like popular gaming consoles.

image

From #191 (comment) :

import binascii

import ubluetooth

from micropython import const

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_IRQ_GATTS_READ_REQUEST = const(4)
_IRQ_SCAN_RESULT = const(5)
_IRQ_SCAN_DONE = const(6)
_IRQ_PERIPHERAL_CONNECT = const(7)
_IRQ_PERIPHERAL_DISCONNECT = const(8)
_IRQ_GATTC_SERVICE_RESULT = const(9)
_IRQ_GATTC_SERVICE_DONE = const(10)
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
_IRQ_GATTC_READ_RESULT = const(15)
_IRQ_GATTC_READ_DONE = const(16)
_IRQ_GATTC_WRITE_DONE = const(17)
_IRQ_GATTC_NOTIFY = const(18)
_IRQ_GATTC_INDICATE = const(19)
_IRQ_GATTS_INDICATE_DONE = const(20)
_IRQ_MTU_EXCHANGED = const(21)
_IRQ_L2CAP_ACCEPT = const(22)
_IRQ_L2CAP_CONNECT = const(23)
_IRQ_L2CAP_DISCONNECT = const(24)
_IRQ_L2CAP_RECV = const(25)
_IRQ_L2CAP_SEND_READY = const(26)
_IRQ_CONNECTION_UPDATE = const(27)
_IRQ_ENCRYPTION_UPDATE = const(28)
_IRQ_GET_SECRET = const(29)
_IRQ_SET_SECRET = const(30)

_ADV_IND = const(0x00)
_ADV_DIRECT_IND = const(0x01)
_ADV_SCAN_IND = const(0x02)
_ADV_NONCONN_IND = const(0x03)

_UART_SERVICE_UUID = ubluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_RX_CHAR_UUID = ubluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX_CHAR_UUID = ubluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")


class BLESimpleCentral:
    def __init__(self, ble):
        self.gamepad_name = None
        self.gamepad_addr_type = None
        self.gamepad_addr = None

        self._ble = ble
        self._ble.active(True)
        self._ble.irq(self.bt_irq)

    def bt_irq(self, event, data):
        if event == _IRQ_SCAN_RESULT:
            # A single scan result.
            addr_type, addr, adv_type, rssi, adv_data = data
            if find_adv_name(adv_data) is None:
                return

            print('Address-Type: ', addr_type)
            print('Address: ', [hex(n) for n in addr])
            print('ADV-Type: ', adv_type)
            print('RSSI in dBm: ', rssi)
            print('ADV-Data: ', [hex(n) for n in adv_data])
            print('ADV-Data (Name): ', find_adv_name(adv_data))  # Xbox Wireless Controller
            print()

            if find_adv_name(adv_data) == "Xbox Wireless Controller":
                self.gamepad_name = find_adv_name(adv_data)
                self.gamepad_addr_type = addr_type
                self.gamepad_addr = addr
                self._ble.gap_scan(None)
                self.connect()
        elif event == _IRQ_SCAN_DONE:
            # Scan duration finished or manually stopped.
            pass
        elif event == _IRQ_PERIPHERAL_CONNECT:
            # A successful gap_connect().
            conn_handle, addr_type, addr = data
            print("connect")
            print(data)
            print('Address-Type: ', addr_type)
            print('Address: ', [hex(n) for n in addr])
            print()
        elif event == _IRQ_PERIPHERAL_DISCONNECT:
            # Connected peripheral has disconnected.
            conn_handle, addr_type, addr = data
            print("disconnect")
            print(data)
            print('Address-Type: ', addr_type)
            print('Address: ', [hex(n) for n in addr])
            print()

    # Find a device advertising the environmental sensor service.
    def scan(self):
        self._ble.gap_scan(5000, 100000, 25000, True)

    def connect(self):
        print(self.gamepad_addr)
        self._ble.gap_connect(self.gamepad_addr_type, self.gamepad_addr)


def find_adv(adv_type, data):
    i = 0
    while i + 1 < len(data):
        ad_structure_len = data[i]
        ad_structure_type = data[i + 1]
        ad_structure_payload = data[i + 2: i + ad_structure_len + 1]
        if ad_structure_type == adv_type:
            return ad_structure_payload
        i += ad_structure_len + 1
    return None


def find_adv_name(data):
    n = find_adv(9, data)
    if n:
        return str(n, 'UTF-8')  # Text
    return None


def demo():
    ble = ubluetooth.BLE()
    central = BLESimpleCentral(ble)

    central.scan()


def demo_connect():
    ble = ubluetooth.BLE()
    central = BLESimpleCentral(ble)

    central.connect()


if __name__ == "__main__":
    demo()

@ALL @mwinkler @CubeLegend ( #140 (comment))

Originally posted by @westurner in #262 (comment)

@laurensvalk
Copy link
Member Author

          @westurner This seems to be especially for XBOX ONE controllers. I'm not 100% sure about the low level stuff, but as far as I know, XBOX controllers use XINPUT while most other controllers use DINPUT (DINPUT beeing HID based?). It won't work with the stadia controller as that one uses DINPUT and it won't work with a PS4 controller because those use BTC and not BLE.

I think that there is a button standard for XINPUT but not really for DINPUT. Of course, one could focus on the most popular gamepads. I don't think that there are many gamepads out there that use BLE.

Originally posted by @Tcm0 in #262 (comment)

@laurensvalk
Copy link
Member Author

          @westurner 

Here is the output of the script (running on stock mindstorm firmware, ble xbox controller):
image
So connection seems to work

Originally posted by @mwinkler in #262 (comment)

@laurensvalk
Copy link
Member Author

          - [ ] https://github.com/pybricks/support/issues/995
DirectInput (DINPUT), XInput (XINPUT), xlib

Originally posted by @westurner in #262 (comment)

@laurensvalk
Copy link
Member Author

          Does there need to be a *gamepad* Bluetooth HID in the pybricks firmware?
  • https://github.com/lemmingDev/ESP32-BLE-Gamepad
  • https://github.com/DJm00n/ControllersInfo -- HID profiles for Xbox 360, Xbox One, PS4, PS4, Stadia, and Switch
    • "HID over GATT Profile (HOGP) 1.0" https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=245141 :

      1 Introduction
      The HID over GATT profile defines the procedures and features to be used by Bluetooth
      low energy HID Devices using GATT and Bluetooth HID Hosts using GATT.
      This profile is an adaptation of the USB HID specification [2] to operate over a Bluetooth low energy wireless link.
      This profile shall operate over an LE transport only. For BR/EDR, the Bluetooth Human Interface Device Profile Specification [9] shall be used.
      1.1 Profile Dependencies
      This profile requires the Generic Attribute Profile (GATT), the Battery Service, the Device Information Service, and the Scan Parameters Profile.
      This specification can be used with Bluetooth Core Specification Version 4.0 [3] or later.

Originally posted by @westurner in #262 (comment)

@gyenesvi
Copy link

gyenesvi commented Jul 3, 2023

Nice to see this issue split off, and some investigation going on, still interested in this one.

@westurner, how far did you get with this? I remember concluding with the PyBricks guys (in a March maybe) that if some basic BT connectivity functionality would be cleaned up in the PyBricks code (along the lines of the code that connects to the lego remote), then we might be able to build on that to experiment with HID gamepad controllers.

In the above example ubluetooth is a micropython module, right? Is that available on PyBricks? Could it be? I remember us discussing with @dlech that the bluetooth module is not available and would be problematic, but maybe ubluetooth could be, so that we can build on it? What's the essential difference?

@dlech
Copy link
Member

dlech commented Jul 3, 2023

In MicroPython, ubluetooth is an alias for bluetooth so they are both names for the same module.

@gyenesvi
Copy link

gyenesvi commented Jul 3, 2023

Oh, that's a pity, so nothing new there.
Has there been any restructuring lately about BLE device connectivity code to make it more generally applicable for other purposes than connecting the lego remote?

@dlech
Copy link
Member

dlech commented Jul 3, 2023

No, there haven't been any changes in that regard.

@laurensvalk
Copy link
Member Author

All is not lost though, we're always working on Bluetooth --- @dlech did make significant progress on Bluetooth, just in a different way 😉

@gyenesvi
Copy link

gyenesvi commented Jul 4, 2023

@laurensvalk thanks for the update! It seems to me that the whole ble member is recently added in v3.3, right? Is that something that could gain more functionality in the future, such as some generic scan / connect methods to connect to peripheral devices?

@laurensvalk
Copy link
Member Author

Yes, it's definitely a step in the right direction. Indeed, the scanning procedure has been prepared for more generic use in the future.

@mozts2005
Copy link

mozts2005 commented Sep 10, 2023

If someone can sort this out and support both Xbox series, and PlayStation 5 controllers at minimum. I would make a donation to both pybricks and the individual developer of $500.00 each. Must be completed no later then end of Q1 2024.

@gyenesvi
Copy link

@mozts2005 that's a generous offer, thanks! @laurensvalk, any chance of giving this some priority? As I said earlier, if the base BLE connectivity code would be there in some generic reusable form, I'd be happy to help with sorting out higher level HID processing part and @westurner might also be interested in helping in. And I'd not be doing it for the money, would even be okay with donating my part (if any) to PyBricks developers.

I don't know if you guys follow this Eurobricks thread about improving Powered Up stuff, but one of the top things that many people complain about is the lack of proportional physical remote, or any kind of 3rd party alternative that can be used without a smart device, so I'd argue that there is more interest than visible from these discussions here on Github.
https://www.eurobricks.com/forum/index.php?/forums/topic/195601-lets-fix-powered-up/

@laurensvalk
Copy link
Member Author

Thanks @mozts2005! Let's see if we can make this happen. Would you mind dropping a note at team@pybricks.com? I'd like to confirm a few details to be sure. Thanks!

Thanks also @gyenesvi for your kind follow-up. We are indeed aware of community comments about this. To drop a bit of a hint, we are actively working on a huge update behind the scenes. It will address another big request from the community (perhaps even more popular than Bluetooth). 😄

@gyenesvi
Copy link

That's promising and interesting to hear @laurensvalk, thanks, and keep us posted about the developments!

@gyenesvi
Copy link

gyenesvi commented Feb 2, 2024

What's the resolution of this? I see it marked completed, but with what action? Is the investigation completed, or is the feature actually implemented?

@laurensvalk
Copy link
Member Author

Sorry, that must have happened accidentally as I was moving other linked issues around. Rest assured, this is still on our list 😄

@laurensvalk laurensvalk reopened this Feb 2, 2024
@gyenesvi
Copy link

gyenesvi commented Feb 2, 2024

Okay, thanks!

@laurensvalk
Copy link
Member Author

If someone can sort this out and support both Xbox series, and PlayStation 5 controllers at minimum. I would make a donation to both pybricks and the individual developer of $500.00 each. Must be completed no later then end of Q1 2024.

Originally posted by @mozts2005 in #1024 (comment)

I hope the current version meets your needs partially! No matter what you decide, I hope you enjoy the new update 🙂

All Xbox series controllers since 2016 are supported. With Technic Hub, Spike Prime Hub, Spike Essential Hub, and Mindstorms Robot Inventor Hub.

Playstation 5 may have to wait until (if) Sony enables BLE in a future update.


Sorry, that must have happened accidentally as I was moving other linked issues around. Rest assured, this is still on our list 😄

So this happened... because the internal preview automatically closed this issue!

🎉 Go check it out! 🎉

preview

@westurner
Copy link

westurner commented Feb 16, 2024 via email

@laurensvalk
Copy link
Member Author

We've generalized the BLE code considerably, so I'm quite open to adding a few more gamepads. My preference would be gamepads that are somewhat mainstream and easy enough to get worldwide.

We had some serious challenges getting the Xbox Controller pairing/bonding process to work with the Technic Hub, and it could still be better. So having HID over BLE is no guarantee, but certainly worth a try.

@Tcm0
Copy link

Tcm0 commented Feb 16, 2024

This is a really nice addition to pybricks. Is rumble supported? I think that it would be funny as a "beeping" for driving backwards or as a distance sensor indication that some object is close to the vehicle. Or maybe for some interactive games that use the hubs.

Regarding future supported controllers: one that's probably widely available is the amazon luna controller. It's often on sale for about half the price. Other ones like the google stadia controller or the steam controller are discontinued. Another BLE controller that's pretty new and will be available for a while is the 8bitdo neogeo wireless controller.

@laurensvalk
Copy link
Member Author

Is rumble supported?

Not yet, but it could be. We have also generalized the internal "bluetooth write" function. We'd just need to figure out the right combination of commands to send. Same thing for the light, which should be full RGB.

@Tcm0
Copy link

Tcm0 commented Feb 16, 2024

Is rumble supported?

Not yet, but it could be. We have also generalized the internal "bluetooth write" function. We'd just need to figure out the right combination of commands to send. Same thing for the light, which should be full RGB.

I think that the light is RGB for the elite 2 controller but not for the other xbox controllers. I'm not sure tho.
Some of them also have multiple rumble motors in different positions. This might be a bit too much, but in theory it should be possible to control the motors independently

@gyenesvi
Copy link

Would you mind adding your findings to #1462?

Sure, I will do that, good to see that I can experiment with staying connected. Will post the results.

I'll create some issues with proposals for the rest then!

One thing I wonder though is how to use the absolute angle of the motor. There is only an 'angle' query method for the motor class as I see, and that one gets the accumulated position, is that right? Is there no way to directly get the absolute position? The only way I see is resetting the accumulated position to the absolute position, and then querying that..

@laurensvalk
Copy link
Member Author

Since support for other controllers comes up frequently, here is a good overview of BLE models.

The list isn't very long, so we'll probably just keep using Xbox.

image

@Tcm0
Copy link

Tcm0 commented Mar 4, 2024

Since support for other controllers comes up frequently, here is a good overview of BLE models.

The list isn't very long, so we'll probably just keep using Xbox.

It's not complete tho. As far as I know, other BLE controllers are Amazon Luna, 8Bitdo NeoGeo (the other 8Bitdo Controllers are BTC) and multiple mobile ones (Flydigi Apex 2, Steelseries Stratus+, Ipega 9078, Ipega 9083S, Nacon MG-X Pro, Razer Jaiju Mobile, PowerA Moga XP5-A, FlyDigi FeiZhi APEX, Saitake 7007f, asus rog kunai 4, Razer Junglecat, Mad Catz C.T.R.L.R. and more).
I think that xbox controllers are by far the most common ones in that list. But there might be cheaper ones.

@laurensvalk
Copy link
Member Author

laurensvalk commented Mar 18, 2024

Good news, we can support the rumble/vibration function too!

The following example roughly covers all the functionality we can get. Suggestions for specific APIs or alternate commands are welcome.

from pybricks.iodevices import XboxController
from pybricks.tools import wait

# Set up all devices.
controller = XboxController()

# All actuators at 100% power for 250ms, delay 150ms, do this 3 times:
controller.rumble(power=100, duration=250, delay=150, count=3)

wait(2000)

# Left handle 10%, right handle 15%, left trigger 100%, right trigger 0%, do it once.
controller.rumble(power=[10, 15, 100, 0], duration=100, delay=0, count=1)

wait(2000)

@gyenesvi
Copy link

That's a nice addition! Only wonder if there's a more intuitive/readable way to provide ramble power in an individual way for handles/triggers. Maybe a dictionary to name them explicitly?
Also, the parameter 'number' sounds a bit too generic to me, could mean anything. I'd use 'count' or 'times', they sound more intuitive to me, but that may only be my preference.

@laurensvalk
Copy link
Member Author

laurensvalk commented Mar 19, 2024

Only wonder if there's a more intuitive/readable way to provide ramble power in an individual way for handles/triggers. Maybe a dictionary to name them explicitly?

Using a tuple for this sort of thing is common throughout the API, e.g. light brightness for the 4 lights on the ultrasonic sensor, and so on. We usually try to strike a balance between verbosity and ease of use. Tuples have other advantages too, since you can do something like:

# Your own feedback constants
COLLISION = [100, 100, 0, 0]
SPEED_FEEDBACK = [0, 0, 50, 50]

And then use these in different parts of your program with power=SPEED_FEEDBACK instead of repeating left_handle_power=0, right_handle_power=0, left_trigger_power=50, right_trigger_power=50 everywhere.

number is now updated to count.

@BertLindeman
Copy link

Tuples have other advantages too, since you can do something like:

Your own feedback constants
COLLISION = [100, 100, 0, 0]
SPEED_FEEDBACK = [0, 0, 50, 50]

number is now updated to count.

FUN

I tried to get all arguments for the rumble call into one constant.
But I did not succeed.
An example:

# use a constant tuple plus arguments: WORKS
controller.rumble(COLLISION, 250, 150, 3)
wait(2000)

# This one DOES NOT WORK
COLLISION_PLUS_3 = (COLLISION, 250, 150, 3)
controller.rumble(COLLISION_PLUS_3)

The second one complains:

Traceback (most recent call last):
  File "xbox_rumble_issue_1024.py", line 64, in <module>
TypeError: 'duration' argument required

What obvious notation do I miss?
A simple user function could provide defaults like

def do_rumble(power, duration=250, delay=0, count=1):
    controller.rumble(power, duration, delay, count)

@laurensvalk
Copy link
Member Author

I tried toI tried to get all arguments for the rumble call into one constant. get all arguments for the rumble call into one constant.

Why would you like to do that? There is a way to do that in Python by unpacking kwargs with *, but this is unrelated to this rumble API.

A simple user function could provide defaults like

I considered leaving a few values at default, but I'm not sure what those should be in "most" cases.

Any suggestions? duration and delay at 150ms? count at 1 or two?

@gyenesvi
Copy link

Using a tuple for this sort of thing is common throughout the API

Sure, if that's the convention then it's fine by me. By the way, that is a list not a tuple, but I guess they are interchangeable here, and in most places in the API?

My other suggestion would have been to add further keyword arguments like left_handle_power as you mention, that would be None by default and would override the power for that one handle if it is provided. This way either or both notations could be used, whichever the user likes. But that might be a bit over-engineered..

number is now updated to count

Great, thanks!

I agree that some defaults could be useful. count should definitely be 1, and in that case delay is irrelevant. Maybe duration and delay both around 250ms, but I have not tested this yet, don't know how it feels.

BTW, where can I access the latest build? I mean not just this one, but in general, how do I find it? I tried yesterday but could not decide which one it is.

What obvious notation do I miss?

You need to write controller.rumble(*COLLISION_PLUS_3), but not sure this approach helps with readability..

@BertLindeman
Copy link

Why would you like to do that? There is a way to do that in Python by unpacking kwargs with *, but this is unrelated to this rumble API.

OK, (thanks Victor)

I considered leaving a few values at default, but I'm not sure what those should be in "most" cases.

Any suggestions? duration and delay at 150ms? count at 1 or two?

No suggestions from me, Laurens. Just enjoying the new possibilities.

@BertLindeman
Copy link

One more question about the rumble method.
Does it wait for all iterations (counts) to be complete?
If I do a ruble with count=5 at the end of a program, I feel only two.
With a wait(5000) behind it I notice all 5 of them.
An example:

ZIG = [100, 100, 0, 0]
controller.rumble(ZIG, 400, 200, 5)

@gyenesvi
Copy link

Does it wait for all iterations (counts) to be complete?

That's a good question, and I think it could be useful to have a wait parameter, like other methods. Could it? Otherwise, it may be harder to guess how much waiting is actually required to be inserted afterwards to get just enough to move on with the program after all the rumbles have been done.

@laurensvalk
Copy link
Member Author

that is a list not a tuple

Either will work.

This way either or both notations could be used, whichever the user likes.

As with most things, it's usually better if there is just one way to do one thing. I think you answered it very well 😄 :

But that might be a bit over-engineered..


Does it wait for all iterations (counts) to be complete?

I've been wondering about this, but I haven't decided what it should do.

I'll also have to check what the controller does if you send something before the previous one completes.

@BertLindeman
Copy link

I'll also have to check what the controller does if you send something before the previous one completes.

Did a few more after each other:

print("do (ZIG, bla bla)", 1)
controller.rumble(ZIG, 400, 200, 25)
print("do (ZIG, bla bla)", 2)
controller.rumble(ZIG, 400, 200, 25)
print("do (ZIG, bla bla)", 3)
controller.rumble(ZIG, 400, 200, 25)
print("do (ZIG, bla bla)", 4)
controller.rumble(ZIG, 400, 200, 25)
print("do (ZIG, bla bla)", 5)
controller.rumble(ZIG, 400, 200, 25)
print("do (ZIG, bla bla) ready")

Results in one time two rumbles and the prints come out:

do (ZIG, bla bla) 1
do (ZIG, bla bla) 2
do (ZIG, bla bla) 3
do (ZIG, bla bla) 4
do (ZIG, bla bla) 5
do (ZIG, bla bla) ready

Conclude that (all?) rumbles are ignored and the program just runs on.
Do you want more testing like this?

@laurensvalk
Copy link
Member Author

I will have a detailed look at it later, but you're probably seeing that the controller disconnects/turns off when the program ends 😉

You might also want to use the four actuators separately to see if the commands are additive, ignored, or overriding the previous one.

@BertLindeman
Copy link

I will have a detailed look at it later, but you're probably seeing that the controller disconnects/turns off when the program ends 😉

That is not the case, I think. The xbox button stays lit until the program ends.
Feels like the following rumbles are just skipped. Will test with the separate actuators.

@BertLindeman
Copy link

Laurens, make a separate (discuss) item of this rumble testing?
This item is already long as it is.

@BertLindeman
Copy link

BTW, where can I access the latest build? I mean not just this one, but in general, how do I find it? I tried yesterday but could not decide which one it is.

Victor, does this help you?
Go to pybricks-micropython and to the actions tab.
There on the left side choose "Build" to see the build tasks on the right side.
You can take the build of your choice there.
Within the build task, browse down to Artifacts and download the firmware for your hub(s)

Bert

@gyenesvi
Copy link

Victor, does this help you?

Thanks, yes, found it. However, I don't see the artifact for the Inventor hub. Is that the same as for the Prime hub?

@BertLindeman
Copy link

However, I don't see the artifact for the Inventor hub. Is that the same as for the Prime hub?

They are identical for 99.99999 % Only LEGO apps show the difference.

@gyenesvi
Copy link

I have been testing on an Inventor hub, it works okay but here are my findings.

Setting power=100 does not make all actuators rumble (as said above), only the handles, but not the triggers. But that's okay that way I guess for a default, because the trigger rumble feels a bit too much for the base case.

250 ms duration feels like a good default for me.

However, I am getting a lot of connectivity errors:

  • "Connected, but not allowed to read buttons. Is the controller in pairing mode?": At first I thought it's because my controller was getting connected to my phone, which it was paired with before. I made the phone forget it, the situation got better, but still happens sometimes.
  • "OSError: [Errno 19] ENODEV": First it seems to connect, but then it quickly looses the controller connection. Happened many times after restarting the program
  • "RuntimeError: Unknown error": I realized that if I stop the program and restart it and turn on the controller then this can happen
  • Sometimes the hub looses connection to computer altogether, while the hub starts connecting to the controller and seems to hang in there (it does not get out of the init method of the controller)

Often I tried without turning the controller off and on again as it was still blinking. Maybe when I turn it off completely and then on again it becomes more stable, errors happen less often, but still happen.

Don't know if this is because now I am testing with an Inventor hub instead of a technic one (because I wanted to see things printed) and that one stays connected to the computer. Or maybe the same problems would arise on the Technic hub but I would not see the errors being printed.. I could test again when that connectivity issue with the Technic hub is resolved, maybe that solves some of these as well.

@BertLindeman
Copy link

BertLindeman commented Mar 19, 2024

Victor, currently I (also) use a spikehub.

Often I tried without turning the controller off and on again as it was still blinking. Maybe when I turn it off completely and then on again it becomes more stable, errors happen less often, but still happen.

In my case the controller always switches off at the end of the program.
Maybe you have a long way at the end of the program, or do you stop the program crtl-C in the log window?
In the crtl-C case you go into the REPL so the controller knows the program is still running (has a connection) and the controller stays ON.

"RuntimeError: Unknown error": I realized that if I stop the program and restart it and turn on the controller then this can happen

Happens here from time to time.

[EDIT]
Once I was wanted at the door and left the test situation unattended.
When I came back the hub seemed running, was disconnected from the pc.
The controller was OFF.
So I needed to stop the hub by long pressing the center button.
Resulted if fast blinking of the hub.
So needed to remove the battery.
Tried to recreate without success.

@gyenesvi
Copy link

Maybe you have a long way at the end of the program, or do you stop the program crtl-C in the log window?

Yeah, my test program often contains an infinite loop for the controller, so I have to stop it with the stop button in the browser. I think the controller stays on if the program ends with an error, and then it fails with higher probability the next time when trying to connect then if the program is stopped properly, the controller shuts down and needs to be powered up again.

@BertLindeman
Copy link

"Short" conclusions about controller rumble actions.

To diminish typing I use this variables to indicate the actuators:

# Left handle, right handle, left trigger, right trigger
ACT0 = [100, 0, 0, 0]  # Left handle
ACT1 = [0, 100, 0, 0]  # right handle
ACT2 = [0, 0, 100, 0]  # left trigger
ACT3 = [0, 0, 0, 100]  # right trigger
  1. If there are more than one xbox.rumble(blabla) commands:
    the actator(s) start turning after the last rumble command.

  2. Rumble commands update the settings if the list of actuator percentages are equal.
    Example: This will make left trigger actuator turn 3 times for 50ms with delay 50ms

    controller.rumble(ACT2, 10, 100, 5)
    controller.rumble(ACT2, 50, 50, 3)
  1. Rumble commands add settings for more actuators if the list of actuators are different.
    Example: This will make all 4 actuators rumble. ACT0 a bit more times than the others.
    controller.rumble(ACT0, 80, 100, 10000)
    controller.rumble(ACT1, 90, 100, 1000)
    controller.rumble(ACT2, 90, 100, 100)
    controller.rumble(ACT3, 50, 100, 10)

Did not (yet) look into how the power settings of an actuator work.

xbox_rumble_test program
# pylint: disable=R0801  # no test for duplicate lines

import usys as sys

from pybricks.iodevices import XboxController
from pybricks.tools import wait


# Left handle, right handle, left trigger, right trigger
ACT0 = [100, 0, 0, 0]  # Left handle
ACT1 = [0, 100, 0, 0]  # right handle
ACT2 = [0, 0, 100, 0]  # left trigger
ACT3 = [0, 0, 0, 100]  # right trigger

# Set up all devices.
controller = XboxController()

#                       duration, delay, count

print("1 test ALL actuators with waiting for a button pressed")
print("   Expect all 4 act vibrate 4 times")
print("   Result: ONLY left and right handle vibrate")
while True:
    controller.rumble(75, 100, 50, 4)
    print(end=":")
    if controller.buttons.pressed():
        # clear button buffer:
        while controller.buttons.pressed():
            wait(5)
        break
    wait(1500)
wait(1000)

print("\n2 Test 1 actuator. Waiting for a button pressed. Which act? 2 or 3 - 3  or 5 counts")
print("   Expect left trigger 3 vibs and right trigger 3")
while True:
    controller.rumble(ACT2, 10, 100, 5)
    controller.rumble(ACT2, 50, 50, 3)
    print(end="L")
    wait(1500)

    controller.rumble(ACT3, 50, 50, 3)
    controller.rumble(ACT3, 10, 100, 5)
    print(end="R")
    if controller.buttons.pressed():
        # clear button buffer:
        while controller.buttons.pressed():
            wait(5)
        break
    wait(1500)
wait(5000)

print("\n3 Test 1 actuator with deminishing delay and see where it starts vibrating")
print("    expect left handle to rumble during the loop sometimes and after buttonpress 3 vibs")
breakout = False
while True:
    for i in range(100, 0, -5):
        controller.rumble(ACT0, 50, i, 3)
        print(i, end=" ")
        if controller.buttons.pressed():
            # clear button buffer:
            while controller.buttons.pressed():
                wait(5)
            breakout = True
            break
    if breakout:
        break
wait(5000)


print("\n4 Test 1 actuator with small delay and see where it starts vibrating")
print("    Expect hardly any vibs in the loop and after that 3 vibs of act0")
while True:
    for i in range(41, 39, -1):
        controller.rumble(ACT0, 50, i, 3)
        print(i, end="_")
    if controller.buttons.pressed():
        # clear button buffer:
        while controller.buttons.pressed():
            wait(5)
        break
wait(5000)

print("\n5 Test 1 actuator overwrite")
while True:
    for __i in range(5):
        dur = __i * 100
        waitms = __i + 40
        count = __i
        controller.rumble(ACT0, dur, waitms, count)
    # controller.rumble(ACT0, __i*100, __i*50, __i)
    # controller.rumble(ACT0, __i*100, __i*50, __i)
    # controller.rumble(ACT0, __i*100, __i*50, __i)
    # controller.rumble(ACT0, __i*100, __i*50, __i)

    # the same actuator list overwrites the previous one
    # different actuator list are added
    print(end="=")
    if controller.buttons.pressed():
        # clear button buffer:
        while controller.buttons.pressed():
            wait(5)
        break

# clear button buffer:
while controller.buttons.pressed():
    wait(5)
mycounter = 0
print("\n6 Test 4 actuators sequential")
print("    Expect all 4 vibrate but act0 much more counts")
while True:
    mycounter += 1
    controller.rumble(ACT0, 80, 100, 10000)
    controller.rumble(ACT1, 90, 100, 1000)
    controller.rumble(ACT2, 90, 100, 100)
    controller.rumble(ACT3, 50, 100, 10)

    # controller.rumble(ACT0, 1000, 50, 4)
    # controller.rumble(ACT1, 1000, 50, 4)
    # controller.rumble(ACT2, 500, 250, 2)
    # controller.rumble(ACT3, 500, 250, 2)
    # the same actuator list overwrites the previous one
    # different actuator list are added
    print(end=".")
    if controller.buttons.pressed():
        print("\nThis last test did", mycounter, "loops")
        break

print("\nTests ended\nwaiting 60 seconds for potential vibrations")
wait(60000)

raise SystemExit(0)

print("do (ACT1 right handle bla bla)")
controller.rumble(ACT1, 500, 250, 3)
wait(2000)
print("do (ACT2 left trigger bla bla)")
controller.rumble(ACT2, 500, 250, 3)
wait(2000)
print("do (ACT3 right trigger bla bla)")
controller.rumble(ACT3, 500, 250, 3)
wait(2000)
print("do (ACTn, bla bla) ready")

print("do (ACT0 left handle bla bla)")
controller.rumble(ACT0, 10, 50, 10)
print("do (ACT1 right handle bla bla)")
controller.rumble(ACT1, 200, 50, 10)
print("do (ACT2 left trigger bla bla)")
controller.rumble(ACT2, 300, 50, 9)
print("do (ACT3 right trigger bla bla)")
controller.rumble(ACT3, 500, 50, 5)
print("do (ACTn, bla bla) ready")
wait(20000)

@laurensvalk
Copy link
Member Author

"RuntimeError: Unknown error": I realized that if I stop the program and restart it and turn on the controller then this can happen

Happens here from time to time.

See #1509

If you find a way to consistently reproduce it, that would be very helpful.

@laurensvalk
Copy link
Member Author

The controls do appear to be additive:

>>> controller.rumble(power=[100, 00, 0, 0], duration=250, delay=00, count=100)
>>> wait(2000)
>>> controller.rumble(power=[0, 00, 100,0 ], duration=250, delay=00, count=100)

Now both the left + left trigger are active.

If you give a new command for an actuator that is already active, the latest command "wins". So if you used count=100 and then count=1, it will stop soon.

@laurensvalk
Copy link
Member Author

I propose these defaults:

def rumble(power=100, duration=200, delay=100, count=1)
    ...

So you can just do:

controller.rumble()
controller.rumble(count=3)

And so on.

@gyenesvi
Copy link

The controls do appear to be additive:

That sounds like a useful thing, as different parts may be triggered by different events, and adding them up this way can make it easy.

But sometimes we just want sequential execution of multiple rumble commands, so what do you think @laurensvalk about that wait parameter to make that use case easy as well?

I propose these defaults:

It makes sense to have everything defaulted like that, and then it's easy to use and parameterize only what you want. I'll test with that timing later to see how it feels, yesterday I tested with duration=250, delay=150, that was also fine.

@laurensvalk
Copy link
Member Author

laurensvalk commented Mar 21, 2024

Upcoming rumble blocks!

image

@gyenesvi
Copy link

Looks good! I guess the optional parameters unfold by the arrow on the right and it is possible to add any combination.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request topic: bluetooth Issues involving bluetooth
Projects
None yet
Development

No branches or pull requests

7 participants