-
-
Notifications
You must be signed in to change notification settings - Fork 7
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
Comments
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) |
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) |
Here is the output of the script (running on stock mindstorm firmware, ble xbox controller): Originally posted by @mwinkler in #262 (comment) |
Originally posted by @westurner in #262 (comment) |
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 |
In MicroPython, |
Oh, that's a pity, so nothing new there. |
No, there haven't been any changes in that regard. |
All is not lost though, we're always working on Bluetooth --- @dlech did make significant progress on Bluetooth, just in a different way 😉 |
@laurensvalk thanks for the update! It seems to me that the whole |
Yes, it's definitely a step in the right direction. Indeed, the scanning procedure has been prepared for more generic use in the future. |
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. |
@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. |
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). 😄 |
That's promising and interesting to hear @laurensvalk, thanks, and keep us posted about the developments! |
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? |
Sorry, that must have happened accidentally as I was moving other linked issues around. Rest assured, this is still on our list 😄 |
Okay, thanks! |
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.
So this happened... because the internal preview automatically closed this issue! 🎉 Go check it out! 🎉 |
FWIU, the 8bitdo USB adapters remap all of the supported controllers to an
Xbox controller.
8BitDo controllers and arcade sticks
- Xbox Series X | S, Xbox One Bluetooth controllers
- PS5, PS4, PS4 Pro, PS3 controllers
- Switch Pro, Switch Joy-Con, Wii U Pro, Wiimote
Special Features
- Vibration support on X-input mode ³
- 6-axis motion on Switch
X-input, D-input, Mac mode, Switch mode
Upgradable firmware
- Virtually lag-free
Steam Input also remaps to what looks like an Xbox controller FWICS.
How much work is there to add additional BLE GATT HIDs so that other
controllers work?
…On Fri, Feb 16, 2024, 7:25 AM laurensvalk ***@***.***> wrote:
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 <https://github.com/mozts2005> in #1024
(comment)
<#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! 🎉
[image: preview] <https://www.youtube.com/watch?v=fxInp9cutNg>
—
Reply to this email directly, view it on GitHub
<#1024 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAMNSYPM4SKF7AQFLIP7QTYT5F2LAVCNFSM6AAAAAAWQGT2TKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSNBYGI4TINJZGU>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
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. |
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. |
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. |
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.. |
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). |
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) |
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? |
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
|
FUN I tried to get all arguments for the rumble call into one constant. # 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:
What obvious notation do I miss? def do_rumble(power, duration=250, delay=0, count=1):
controller.rumble(power, duration, delay, count) |
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.
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? |
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
Great, thanks! I agree that some defaults could be useful. 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.
You need to write |
OK, (thanks Victor)
No suggestions from me, Laurens. Just enjoying the new possibilities. |
One more question about the rumble method. ZIG = [100, 100, 0, 0]
controller.rumble(ZIG, 400, 200, 5) |
That's a good question, and I think it could be useful to have a |
Either will work.
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 😄 :
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. |
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:
Conclude that (all?) rumbles are ignored and the program just runs on. |
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. |
That is not the case, I think. The xbox button stays lit until the program ends. |
Laurens, make a separate (discuss) item of this rumble testing? |
Victor, does this help you? Bert |
Thanks, yes, found it. 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. |
I have been testing on an Inventor hub, it works okay but here are my findings. Setting 250 ms duration feels like a good default for me. However, I am getting a lot of connectivity errors:
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. |
Victor, currently I (also) use a spikehub.
In my case the controller always switches off at the end of the program.
Happens here from time to time. [EDIT] |
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. |
"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
controller.rumble(ACT2, 10, 100, 5)
controller.rumble(ACT2, 50, 50, 3)
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) |
See #1509 If you find a way to consistently reproduce it, that would be very helpful. |
The controls do appear to be additive:
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 |
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. |
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
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 |
Looks good! I guess the optional parameters unfold by the arrow on the right and it is possible to add any combination. |
This is a spot for discussing gamepad connections, split off from #262
The text was updated successfully, but these errors were encountered: