diff --git a/src/software/thunderscope/BUILD b/src/software/thunderscope/BUILD index ae5b6bdcc8..e0a7391396 100644 --- a/src/software/thunderscope/BUILD +++ b/src/software/thunderscope/BUILD @@ -6,7 +6,6 @@ package(default_visibility = ["//visibility:public"]) compile_pip_requirements( name = "requirements", src = "requirements.in", - requirements_darwin = "requirements_lock.darwin.txt", requirements_txt = "requirements_lock.txt", ) diff --git a/src/software/thunderscope/constants.py b/src/software/thunderscope/constants.py index b52fb9c8af..295c21c5be 100644 --- a/src/software/thunderscope/constants.py +++ b/src/software/thunderscope/constants.py @@ -327,15 +327,6 @@ class TrailValues: class DiagnosticsConstants: """Constants for Robot Diagnostics""" - # Device names of the controllers supported for controlling robots - SUPPORTED_CONTROLLERS = { - "Microsoft Xbox One X pad", - "Microsoft X-Box One S pad", - "Microsoft X-Box 360 pad", - "Microsoft Xbox 360 pad", - "Generic X-Box pad", - } - BUTTON_PRESSED_THRESHOLD = 0.5 DEADZONE_PERCENTAGE = 0.20 diff --git a/src/software/thunderscope/requirements.in b/src/software/thunderscope/requirements.in index 8c97f43533..ae5dea9fc3 100644 --- a/src/software/thunderscope/requirements.in +++ b/src/software/thunderscope/requirements.in @@ -1,9 +1,9 @@ colorama==0.4.6 netifaces==0.11.0 -evdev==1.7.0; sys_platform == "linux" numpy==1.26.4 protobuf==6.31.1 pyserial==3.5 +pygame==2.6.1 pyqtgraph==0.13.7 pyqtdarktheme-fork==2.3.2 PyQt6-Qt6==6.8.1 diff --git a/src/software/thunderscope/requirements_lock.darwin.txt b/src/software/thunderscope/requirements_lock.darwin.txt deleted file mode 100644 index 11cd4b3925..0000000000 --- a/src/software/thunderscope/requirements_lock.darwin.txt +++ /dev/null @@ -1,136 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# bazel run //software/thunderscope:requirements.update -# -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via -r software/thunderscope/requirements.in -darkdetect==0.7.1 \ - --hash=sha256:3efe69f8ecd5f1b7f4fbb0d1d93f656b0e493c45cc49222380ffe2a529cbc866 \ - --hash=sha256:47be3cf5134432ddb616bbffc927237718407914993c82809983e7ccebf49013 - # via pyqtdarktheme-fork -netifaces==0.11.0 \ - --hash=sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32 \ - --hash=sha256:08e3f102a59f9eaef70948340aeb6c89bd09734e0dca0f3b82720305729f63ea \ - --hash=sha256:0f6133ac02521270d9f7c490f0c8c60638ff4aec8338efeff10a1b51506abe85 \ - --hash=sha256:18917fbbdcb2d4f897153c5ddbb56b31fa6dd7c3fa9608b7e3c3a663df8206b5 \ - --hash=sha256:2479bb4bb50968089a7c045f24d120f37026d7e802ec134c4490eae994c729b5 \ - --hash=sha256:2650beee182fed66617e18474b943e72e52f10a24dc8cac1db36c41ee9c041b7 \ - --hash=sha256:28f4bf3a1361ab3ed93c5ef360c8b7d4a4ae060176a3529e72e5e4ffc4afd8b0 \ - --hash=sha256:3ecb3f37c31d5d51d2a4d935cfa81c9bc956687c6f5237021b36d6fdc2815b2c \ - --hash=sha256:469fc61034f3daf095e02f9f1bbac07927b826c76b745207287bc594884cfd05 \ - --hash=sha256:48324183af7f1bc44f5f197f3dad54a809ad1ef0c78baee2c88f16a5de02c4c9 \ - --hash=sha256:50721858c935a76b83dd0dd1ab472cad0a3ef540a1408057624604002fcfb45b \ - --hash=sha256:54ff6624eb95b8a07e79aa8817288659af174e954cca24cdb0daeeddfc03c4ff \ - --hash=sha256:5be83986100ed1fdfa78f11ccff9e4757297735ac17391b95e17e74335c2047d \ - --hash=sha256:5f9ca13babe4d845e400921973f6165a4c2f9f3379c7abfc7478160e25d196a4 \ - --hash=sha256:73ff21559675150d31deea8f1f8d7e9a9a7e4688732a94d71327082f517fc6b4 \ - --hash=sha256:7dbb71ea26d304e78ccccf6faccef71bb27ea35e259fb883cfd7fd7b4f17ecb1 \ - --hash=sha256:815eafdf8b8f2e61370afc6add6194bd5a7252ae44c667e96c4c1ecf418811e4 \ - --hash=sha256:841aa21110a20dc1621e3dd9f922c64ca64dd1eb213c47267a2c324d823f6c8f \ - --hash=sha256:84e4d2e6973eccc52778735befc01638498781ce0e39aa2044ccfd2385c03246 \ - --hash=sha256:8f7da24eab0d4184715d96208b38d373fd15c37b0dafb74756c638bd619ba150 \ - --hash=sha256:96c0fe9696398253f93482c84814f0e7290eee0bfec11563bd07d80d701280c3 \ - --hash=sha256:aab1dbfdc55086c789f0eb37affccf47b895b98d490738b81f3b2360100426be \ - --hash=sha256:c03fb2d4ef4e393f2e6ffc6376410a22a3544f164b336b3a355226653e5efd89 \ - --hash=sha256:c37a1ca83825bc6f54dddf5277e9c65dec2f1b4d0ba44b8fd42bc30c91aa6ea1 \ - --hash=sha256:c92ff9ac7c2282009fe0dcb67ee3cd17978cffbe0c8f4b471c00fe4325c9b4d4 \ - --hash=sha256:c9a3a47cd3aaeb71e93e681d9816c56406ed755b9442e981b07e3618fb71d2ac \ - --hash=sha256:cb925e1ca024d6f9b4f9b01d83215fd00fe69d095d0255ff3f64bffda74025c8 \ - --hash=sha256:d07b01c51b0b6ceb0f09fc48ec58debd99d2c8430b09e56651addeaf5de48048 \ - --hash=sha256:e76c7f351e0444721e85f975ae92718e21c1f361bda946d60a214061de1f00a1 \ - --hash=sha256:eb4813b77d5df99903af4757ce980a98c4d702bbcb81f32a0b305a1537bdf0b1 - # via -r software/thunderscope/requirements.in -numpy==1.26.4 \ - --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ - --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ - --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ - --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ - --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ - --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ - --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ - --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ - --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ - --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ - --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ - --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ - --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ - --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ - --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ - --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ - --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ - --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ - --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ - --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ - --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ - --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ - --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ - --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ - --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ - --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ - --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ - --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ - --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ - --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ - --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ - --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ - --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ - --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ - --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ - --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f - # via - # -r software/thunderscope/requirements.in - # pyqtgraph -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f - # via qtpy -protobuf==6.31.1 \ - --hash=sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16 \ - --hash=sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447 \ - --hash=sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6 \ - --hash=sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402 \ - --hash=sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e \ - --hash=sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9 \ - --hash=sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9 \ - --hash=sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39 \ - --hash=sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a - # via -r software/thunderscope/requirements.in -pyqt-toast-notification==1.3.2 \ - --hash=sha256:135736ec0f16bff41104dee3c60ac318e5d55ae3378bf26892c6d08c36088ae6 \ - --hash=sha256:82688101202737736d51ab6c74a573b32266ecb7c8b0002f913407bd369737d9 - # via -r software/thunderscope/requirements.in -pyqt6-qt6==6.8.1 \ - --hash=sha256:006d786693d0511fbcf184a862edbd339c6ed1bb3bd9de363d73a19ed4b23dff \ - --hash=sha256:08065d595f1e6fc2dde9f4450eeff89082f4bad26f600a8e9b9cc5966716bfcf \ - --hash=sha256:1eb8460a1fdb38d0b2458c2974c01d471c1e59e4eb19ea63fc447aaba3ad530e \ - --hash=sha256:20843cb86bd94942d1cd99e39bf1aeabb875b241a35a8ab273e4bbbfa63776db \ - --hash=sha256:9f3790c4ce4dc576e48b8718d55fb8743057e6cbd53a6ca1dd253ffbac9b7287 \ - --hash=sha256:a8bc2ed4ee5e7c6ff4dd1c7db0b27705d151fee5dc232bbd1bf17618f937f515 \ - --hash=sha256:d6ca5d2b9d2ec0ee4a814b2175f641a5c4299cb80b45e0f5f8356632663f89b3 - # via -r software/thunderscope/requirements.in -pyqtdarktheme-fork==2.3.2 \ - --hash=sha256:3ea94fed5df262d960378409357c63032639f749794d766f41a45ad8558b2523 \ - --hash=sha256:d96ee64f0884678fad9b6bc352d5e37d84ca786fa60ed32ffaa7e6c6bc67e964 - # via -r software/thunderscope/requirements.in -pyqtgraph==0.13.7 \ - --hash=sha256:64f84f1935c6996d0e09b1ee66fe478a7771e3ca6f3aaa05f00f6e068321d9e3 \ - --hash=sha256:7754edbefb6c367fa0dfb176e2d0610da3ada20aa7a5318516c74af5fb72bf7a - # via -r software/thunderscope/requirements.in -pyserial==3.5 \ - --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \ - --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0 - # via -r software/thunderscope/requirements.in -qtawesome==1.4.0 \ - --hash=sha256:783e414d1317f3e978bf67ea8e8a1b1498bad9dbd305dec814027e3b50521be6 \ - --hash=sha256:a4d689fa071c595aa6184171ce1f0f847677cb8d2db45382c43129f1d72a3d93 - # via -r software/thunderscope/requirements.in -qtpy==2.4.2 \ - --hash=sha256:5a696b1dd7a354cb330657da1d17c20c2190c72d4888ba923f8461da67aa1a1c \ - --hash=sha256:9d6ec91a587cc1495eaebd23130f7619afa5cdd34a277acb87735b4ad7c65156 - # via - # pyqt-toast-notification - # qtawesome diff --git a/src/software/thunderscope/requirements_lock.txt b/src/software/thunderscope/requirements_lock.txt index e0e951fc21..b13d47dc07 100644 --- a/src/software/thunderscope/requirements_lock.txt +++ b/src/software/thunderscope/requirements_lock.txt @@ -12,9 +12,6 @@ darkdetect==0.7.1 \ --hash=sha256:3efe69f8ecd5f1b7f4fbb0d1d93f656b0e493c45cc49222380ffe2a529cbc866 \ --hash=sha256:47be3cf5134432ddb616bbffc927237718407914993c82809983e7ccebf49013 # via pyqtdarktheme-fork -evdev==1.7.0 ; sys_platform == "linux" \ - --hash=sha256:95bd2a1e0c6ce2cd7a2ecc6e6cd9736ff794b3ad5cb54d81d8cbc2e414d0b870 - # via -r software/thunderscope/requirements.in netifaces==0.11.0 \ --hash=sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32 \ --hash=sha256:08e3f102a59f9eaef70948340aeb6c89bd09734e0dca0f3b82720305729f63ea \ @@ -102,6 +99,68 @@ protobuf==6.31.1 \ --hash=sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39 \ --hash=sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a # via -r software/thunderscope/requirements.in +pygame==2.6.1 \ + --hash=sha256:00827aba089355925902d533f9c41e79a799641f03746c50a374dc5c3362e43d \ + --hash=sha256:10e3d2a55f001f6c0a6eb44aa79ea7607091c9352b946692acedb2ac1482f1c9 \ + --hash=sha256:1206125f14cae22c44565c9d333607f1d9f59487b1f1432945dfc809aeaa3e88 \ + --hash=sha256:14f9dda45469b254c0f15edaaeaa85d2cc072ff6a83584a265f5d684c7f7efd8 \ + --hash=sha256:15efaa11a80a65dd589a95bebe812fa5bfc7e14946b638a424c5bd9ac6cca1a4 \ + --hash=sha256:163e66de169bd5670c86e27d0b74aad0d2d745e3b63cf4e7eb5b2bff1231ca8d \ + --hash=sha256:173badf82fa198e6888017bea40f511cb28e69ecdd5a72b214e81e4dcd66c3b1 \ + --hash=sha256:17498a2b043bc0e795faedef1b081199c688890200aef34991c1941caa2d2c89 \ + --hash=sha256:20349195326a5e82a16e351ed93465a7845a7e2a9af55b7bc1b2110ea3e344e1 \ + --hash=sha256:21160d9093533eb831f1b708e630706e5ac16b30750571ec27bc3b8364814f38 \ + --hash=sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b \ + --hash=sha256:28b43190436037e428a5be28fc80cf6615304fd528009f2c688cc828f4ff104b \ + --hash=sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171 \ + --hash=sha256:2a615d78b2364e86f541458ff41c2a46181b9a1e9eabd97b389282fdf04efbb3 \ + --hash=sha256:325a84d072d52e3c2921eff02f87c6a74b7e77d71db3bdf53801c6c975f1b6c4 \ + --hash=sha256:33006f784e1c7d7e466fcb61d5489da59cc5f7eb098712f792a225df1d4e229d \ + --hash=sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65 \ + --hash=sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e \ + --hash=sha256:3bede70ec708057e305815d6546012669226d1d80566785feca9b044216062e7 \ + --hash=sha256:481cfe1bdbb7fe00acc5950c494c26f00240888619bdc396fc8c39a734797432 \ + --hash=sha256:4a8ea113b1bf627322a025a1a5a87e3818a7f55ab3a4077ff1ae5c8c60576614 \ + --hash=sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b \ + --hash=sha256:4ee7f2771f588c966fa2fa8b829be26698c9b4836f82ede5e4edc1a68594942e \ + --hash=sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f \ + --hash=sha256:56ffca6059b165bbf64f4b4be23b8068f6a0e220780e4f96ec0bb5ac3c63ec39 \ + --hash=sha256:5d09fd950725d187aa5207c0cb8eb9ab0d2f8ce9ab8d189c30eeb470e71b617e \ + --hash=sha256:6582aa71a681e02e55d43150a9ab41394e6bf4d783d2962a10aea58f424be060 \ + --hash=sha256:7103c60939bbc1e05cfc7ba3f1d2ad3bbf103b7828b82a7166a9ab6f51950146 \ + --hash=sha256:7bffdd3eaf394d9645331d1c3a5df9d782ebcc3c5a78f3b657c7879a828dd111 \ + --hash=sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c \ + --hash=sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a \ + --hash=sha256:816e85000c5d8b02a42b9834f761a5925ef3377d2924e3a7c4c143d2990ce5b8 \ + --hash=sha256:818b4eaec9c4acb6ac64805d4ca8edd4062bebca77bd815c18739fe2842c97e9 \ + --hash=sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e \ + --hash=sha256:8a78fd030d98faab4a8e27878536fdff7518d3e062a72761c552f624ebba5a5f \ + --hash=sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299 \ + --hash=sha256:94afd1177680d92f9214c54966ad3517d18210c4fbc5d84a0192d218e93647e0 \ + --hash=sha256:97ac4e13847b6b293ecaffa5ffce9886c98d09c03309406931cc592f0cea6366 \ + --hash=sha256:9beeb647e555afb5657111fa83acb74b99ad88761108eaea66472e8b8547b55b \ + --hash=sha256:9dd5c054d4bd875a8caf978b82672f02bec332f52a833a76899220c460bb4b58 \ + --hash=sha256:a1bf7ab5311bbced70320f1a56701650b4c18231343ae5af42111eea91e0949a \ + --hash=sha256:a4b8f04fceddd9a3ac30778d11f0254f59efcd1c382d5801271113cea8b4f2f3 \ + --hash=sha256:a620883d589926f157b8f1d1f543183ac52e5c30507dea445e3927ae0bee1c54 \ + --hash=sha256:ac3f033d2be4a9e23660a96afe2986df3a6916227538a6a0061bc218c5088507 \ + --hash=sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2 \ + --hash=sha256:b46e68cd168f44d0224c670bb72186688fc692d7079715f79d04096757d703d0 \ + --hash=sha256:b7f9f8e6f76de36f4725175d686601214af362a4f30614b4dae2240198e72e6f \ + --hash=sha256:bbb7167c92103a2091366e9af26d4914ba3776666e8677d3c93551353fffa626 \ + --hash=sha256:c0b11356ac96261162d54a2c2b41a41978f00525631b01ec9c4fe26b01c66595 \ + --hash=sha256:c31dbdb5d0217f32764797d21c2752e258e5fb7e895326538d82b5f75a0cd856 \ + --hash=sha256:c47a6938de93fa610accd4969e638c2aebcb29b2fca518a84c3a39d91ab47116 \ + --hash=sha256:c8040ea2ab18c6b255af706ec01355c8a6b08dc48d77fd4ee783f8fc46a843bf \ + --hash=sha256:ce8cc108b92de9b149b344ad2e25eedbe773af0dc41dfb24d1f07f679b558c60 \ + --hash=sha256:d1a7f2b66ac2e4c9583b6d4c6d6f346fb10a3392c04163f537061f86a448ed5c \ + --hash=sha256:d29eb9a93f12aa3d997b6e3c447ac85b2a4b142ab2548441523a8fcf5e216042 \ + --hash=sha256:da3ad64d685f84a34ebe5daacb39fff14f1251acb34c098d760d63fee768f50c \ + --hash=sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c \ + --hash=sha256:f3935459109da4bb0b3901da9904f0a3e52028a3332a355d298b1673a334cf21 \ + --hash=sha256:f84f15d146d6aa93254008a626c56ef96fed276006202881a47b29757f0cd65a \ + --hash=sha256:fb6e8d0547f30ddc845f4fd1e33070ef548233ad0dbf21f7ecea768883d1bbdc + # via -r software/thunderscope/requirements.in pyqt-toast-notification==1.3.2 \ --hash=sha256:135736ec0f16bff41104dee3c60ac318e5d55ae3378bf26892c6d08c36088ae6 \ --hash=sha256:82688101202737736d51ab6c74a573b32266ecb7c8b0002f913407bd369737d9 diff --git a/src/software/thunderscope/robot_diagnostics/BUILD b/src/software/thunderscope/robot_diagnostics/BUILD index aee3a437d1..d8d6510aec 100644 --- a/src/software/thunderscope/robot_diagnostics/BUILD +++ b/src/software/thunderscope/robot_diagnostics/BUILD @@ -23,24 +23,41 @@ py_library( ], ) +py_library( + name = "keyboard_controller", + srcs = ["keyboard_controller.py"], + deps = [ + ":controller_base", + "//software/thunderscope:constants", + requirement("pyqtgraph"), + ], +) + py_library( name = "handheld_controller", srcs = ["handheld_controller.py"], + deps = [ + ":controller_base", + "//software/thunderscope:constants", + requirement("pygame"), + ], +) + +py_library( + name = "controller_base", + srcs = ["controller_base.py"], ) py_library( name = "handheld_controller_widget", srcs = ["handheld_controller_widget.py"], deps = [ + ":controller_base", ":handheld_controller", + ":keyboard_controller", "//software/thunderscope:constants", requirement("pyqtgraph"), - ] + select({ - # TODO: remove this selection when we replace evdev to - # other macos supported libs. - "@platforms//os:linux": [requirement("evdev")], - "//conditions:default": [], - }), + ], ) py_library( diff --git a/src/software/thunderscope/robot_diagnostics/controller_base.py b/src/software/thunderscope/robot_diagnostics/controller_base.py new file mode 100644 index 0000000000..e2b64ba7a7 --- /dev/null +++ b/src/software/thunderscope/robot_diagnostics/controller_base.py @@ -0,0 +1,56 @@ +from abc import ABC, abstractmethod + + +class IControllerBase(ABC): + """Abstract base class for controller input sources.""" + + @abstractmethod + def name(self) -> str: + """Get the display name of the input source.""" + + @abstractmethod + def connected(self) -> bool: + """Return true if the input source is active and available.""" + + @abstractmethod + def close(self) -> None: + """Release any resources held by the input source.""" + + def update(self) -> None: + """Refresh controller input state. Called once per frame before reading inputs. + Override for polled backends; push-based controllers can leave this as a no-op. + """ + + @abstractmethod + def get_move_velocity(self) -> tuple[float, float, float]: + """Return (x, y, angular) velocity, each normalized to [-1, 1] with deadzone applied. + Positive x = forward, positive y = strafe left, positive angular = CCW. + """ + + @abstractmethod + def get_speed_factor(self) -> float: + """Return 1.0 normally, or SPEED_SLOWDOWN_FACTOR when slowdown input is active.""" + + @abstractmethod + def is_dribbler_held(self) -> bool: + """Return True if the dribbler engage input is active.""" + + @abstractmethod + def get_kick_power_step(self) -> int: + """Return -1, 0, or +1 for kick/chip power step direction. + Non-zero only once per new input (edge-detected). + """ + + @abstractmethod + def get_dribbler_step(self) -> int: + """Return -1, 0, or +1 for dribbler RPM step direction. + Non-zero only once per new input (edge-detected). + """ + + @abstractmethod + def is_kick_fired(self) -> bool: + """Return True once per kick button press (rising edge only).""" + + @abstractmethod + def is_chip_fired(self) -> bool: + """Return True once per chip button press (rising edge only).""" diff --git a/src/software/thunderscope/robot_diagnostics/handheld_controller.py b/src/software/thunderscope/robot_diagnostics/handheld_controller.py index d3a39a3ab0..f760467a13 100644 --- a/src/software/thunderscope/robot_diagnostics/handheld_controller.py +++ b/src/software/thunderscope/robot_diagnostics/handheld_controller.py @@ -1,91 +1,174 @@ -import numpy +import os -# TODO: remove the try-catch when we rewrite this with macOS-compatible lib -try: - from evdev import InputDevice, ecodes -except ImportError: - pass +# Suppress the pygame "Hello from the pygame community" banner on import +os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1" -from threading import Thread +import pygame +from pygame._sdl2 import controller +from software.thunderscope.constants import DiagnosticsConstants +from software.thunderscope.robot_diagnostics.controller_base import IControllerBase -class HandheldController: - """Represents a handheld game controller or input device that can be used - to manually control our robots. + +class HandheldController(IControllerBase): + """Represents a handheld game controller (e.g. an Xbox gamepad) that can be + used to manually control our robots. + + Backed by SDL's game-controller API (via pygame), which recognizes controllers + through its controller database and exposes a standardized button/axis layout. + Controllers are read in poll mode — call update() each frame before reading inputs. """ - def __init__(self, path: str): - """Initialize a HandheldController that reads input events from - the specified device path. + # Raw SDL axis values span the signed 16-bit range. + _AXIS_MAX_VALUE = 32767 + + def __init__(self, controller_index: int): + """Open the game controller at the given SDL device index. + + :param controller_index: the SDL device index of the controller + """ + self.controller = controller.Controller(controller_index) + + self._last_dpad_x = 0 + self._last_dpad_y = 0 + self._last_btn_a = False + self._last_btn_b = False + + @classmethod + def detect(cls) -> "HandheldController | None": + """Scan the currently connected devices for a supported game controller. - :param path: the device input path + :return: a HandheldController for the first recognized controller, or + None if none is connected """ - self.input_device = InputDevice(path) - self.input_values: dict[int, float] = {} + controller.init() + # Poll mode: refresh state via update() instead of the SDL event queue, + # so no display/event subsystem is required. + controller.set_eventstate(False) + controller.update() - capabilities = self.input_device.capabilities() - self.supported_keys = set(capabilities[ecodes.EV_KEY]) - self.abs_info = dict(capabilities[ecodes.EV_ABS]) + for index in range(controller.get_count()): + if controller.is_controller(index): + return cls(index) - self.thread = Thread(target=self.__read_input, daemon=True) - self.thread.start() + return None def name(self) -> str: """Get the device name of the controller. :return: the device name """ - return self.input_device.name + return self.controller.name def connected(self) -> bool: """Check whether the controller is currently connected. :return: true if the controller is connected, false otherwise """ - return self.input_device is not None + return self.controller.attached() - def key_down(self, key_code: int) -> bool: - """Check whether the given key on the controller is currently pressed down. + def update(self) -> None: + """Refresh the controller's input state from SDL. - :param key_code: the EV_KEY code of the key to check - :return: true if the key is currently pressed down, false otherwise + Must be called once per frame before reading any input values. """ - if key_code not in self.supported_keys: - return False + controller.update() - return bool(self.input_values.get(key_code, 0)) + def close(self) -> None: + """Close the connection to the controller.""" + self.controller.quit() - def abs_value(self, abs_code: int) -> float: - """Get the current value of an absolute axis input on the controller, - normalized to a range of [-1, 1]. + def get_move_velocity(self) -> tuple[float, float, float]: + """Return (x, y, angular) velocity normalized to [-1, 1] with deadzone applied. - :param abs_code: the EV_ABS code of the axis to check - :return: the value of the axis input, normalized to [-1, 1] + :return: (vx, vy, vrot) where positive x = forward, positive y = strafe left, + positive angular = CCW """ - if abs_code not in self.abs_info: - return 0 - value_min = self.abs_info[abs_code].min - value_max = self.abs_info[abs_code].max - value = self.input_values.get(abs_code, 0) + def with_deadzone(v: float) -> float: + return v if abs(v) >= DiagnosticsConstants.DEADZONE_PERCENTAGE else 0.0 - if value >= 0: - return numpy.interp(value, (0, value_max), (0, 1)) - else: - return numpy.interp(value, (value_min, 0), (-1, 0)) + # Negate: SDL stick-up is negative LEFTY, but semantic "forward" = +vx. + vx = -with_deadzone(self._get_abs_normalized(pygame.CONTROLLER_AXIS_LEFTY)) + vy = -with_deadzone(self._get_abs_normalized(pygame.CONTROLLER_AXIS_LEFTX)) + vrot = -with_deadzone(self._get_abs_normalized(pygame.CONTROLLER_AXIS_RIGHTX)) + return vx, vy, vrot - def close(self) -> None: - """Close the connection to the controller.""" - if self.input_device is not None: - self.input_values.clear() - self.input_device.close() - self.input_device = None - - def __read_input(self) -> None: - """Endless loop that reads input events from the controller.""" - try: - for event in self.input_device.read_loop(): - if event.type == ecodes.EV_KEY or event.type == ecodes.EV_ABS: - self.input_values[event.code] = event.value - except: - self.close() + def get_speed_factor(self) -> float: + """Return 1.0 normally, or SPEED_SLOWDOWN_FACTOR when the left trigger is held. + + :return: the speed scaling factor + """ + trigger = self._get_abs_normalized(pygame.CONTROLLER_AXIS_TRIGGERLEFT) + return ( + DiagnosticsConstants.SPEED_SLOWDOWN_FACTOR + if trigger > DiagnosticsConstants.BUTTON_PRESSED_THRESHOLD + else 1.0 + ) + + def is_dribbler_held(self) -> bool: + """Return True if the right trigger is held past the button threshold. + + :return: true if the dribbler engage input is active + """ + return ( + self._get_abs_normalized(pygame.CONTROLLER_AXIS_TRIGGERRIGHT) + > DiagnosticsConstants.BUTTON_PRESSED_THRESHOLD + ) + + def get_kick_power_step(self) -> int: + """Return the kick/chip power step direction from the d-pad x buttons. + Non-zero only on a new press (edge-detected). + + :return: -1, 0, or +1 + """ + current = int( + self.controller.get_button(pygame.CONTROLLER_BUTTON_DPAD_RIGHT) + ) - int(self.controller.get_button(pygame.CONTROLLER_BUTTON_DPAD_LEFT)) + step = current if current != self._last_dpad_x else 0 + self._last_dpad_x = current + return step + + def get_dribbler_step(self) -> int: + """Return the dribbler RPM step direction from the d-pad y buttons. + Non-zero only on a new press (edge-detected). Positive = increase RPM. + + :return: -1, 0, or +1 + """ + current = int( + self.controller.get_button(pygame.CONTROLLER_BUTTON_DPAD_DOWN) + ) - int(self.controller.get_button(pygame.CONTROLLER_BUTTON_DPAD_UP)) + # Negate: down = +1 raw → decrease dribbler → -1; up = -1 raw → increase → +1. + step = -current if current != self._last_dpad_y else 0 + self._last_dpad_y = current + return step + + def is_kick_fired(self) -> bool: + """Return True once on the rising edge of the kick button (A). + + :return: true if the kick button was just pressed + """ + current = bool(self.controller.get_button(pygame.CONTROLLER_BUTTON_A)) + fired = current and not self._last_btn_a + self._last_btn_a = current + return fired + + def is_chip_fired(self) -> bool: + """Return True once on the rising edge of the chip button (B). + + :return: true if the chip button was just pressed + """ + current = bool(self.controller.get_button(pygame.CONTROLLER_BUTTON_B)) + fired = current and not self._last_btn_b + self._last_btn_b = current + return fired + + def _get_abs_normalized(self, axis: int) -> float: + """Return the current value of an axis, normalized to [-1, 1]. + + :param axis: the pygame.CONTROLLER_AXIS_* code of the axis to read + :return: normalized axis value in [-1, 1] + """ + return max( + -1.0, min(1.0, self.controller.get_axis(axis) / self._AXIS_MAX_VALUE) + ) diff --git a/src/software/thunderscope/robot_diagnostics/handheld_controller_widget.py b/src/software/thunderscope/robot_diagnostics/handheld_controller_widget.py index 127402fa85..41b1f658d1 100644 --- a/src/software/thunderscope/robot_diagnostics/handheld_controller_widget.py +++ b/src/software/thunderscope/robot_diagnostics/handheld_controller_widget.py @@ -1,23 +1,19 @@ import numpy -# TODO: remove the try-catch when we rewrite this with macOS-compatible lib -try: - import evdev - from evdev import ecodes -except ImportError: - pass - import proto.import_all_protos as protos -from pyqtgraph.Qt import QtWidgets -from pyqtgraph.Qt import QtCore +from pyqtgraph.Qt import QtCore, QtWidgets import software.python_bindings as tbots_cpp from software.thunderscope.constants import DiagnosticsConstants +from software.thunderscope.robot_diagnostics.controller_base import IControllerBase from software.thunderscope.robot_diagnostics.handheld_controller import ( HandheldController, ) +from software.thunderscope.robot_diagnostics.keyboard_controller import ( + KeyboardController, +) class HandheldControllerWidget(QtWidgets.QWidget): @@ -38,12 +34,7 @@ def __init__(self) -> None: self.constants = tbots_cpp.createRobotConstants() - self.handheld_controller: HandheldController | None = None - - self.last_d_pad_axis_x_value = 0 - self.last_d_pad_axis_y_value = 0 - self.last_btn_a_value = False - self.last_btn_b_value = False + self.handheld_controller: IControllerBase | None = None self.motor_control = protos.MotorControl() self.dribbler_speed = 0 @@ -57,18 +48,20 @@ def __init__(self) -> None: self.detect_controller() def detect_controller(self) -> None: - """Scan through the list of currently connected devices for a supported - handheld controller and, if one is found, set it as the device to accept + """Scan the currently connected devices for a supported handheld + controller and, if one is found, set it as the device to accept controller inputs from. """ - self.handheld_controller = None - - for path in evdev.list_devices(): - device = evdev.InputDevice(path) - if device.name in DiagnosticsConstants.SUPPORTED_CONTROLLERS: - self.handheld_controller = HandheldController(path) - break + if self.handheld_controller is not None: + self.handheld_controller.close() + self.handheld_controller = HandheldController.detect() + self.__update_controller_status() + def use_keyboard_controller(self) -> None: + """Switch to keyboard input, closing any active controller first.""" + if self.handheld_controller is not None: + self.handheld_controller.close() + self.handheld_controller = KeyboardController() self.__update_controller_status() def controller_input_enabled(self) -> bool: @@ -82,7 +75,10 @@ def refresh(self) -> None: if self.handheld_controller is None: return + self.handheld_controller.update() + if not self.handheld_controller.connected(): + self.handheld_controller.close() self.handheld_controller = None self.__update_controller_status() return @@ -104,15 +100,19 @@ def __create_widgets(self) -> QtWidgets.QGroupBox: self.detect_controller_button = QtWidgets.QPushButton("Detect Controller") self.detect_controller_button.clicked.connect(self.detect_controller) + self.use_keyboard_button = QtWidgets.QPushButton("Use Keyboard") + self.use_keyboard_button.clicked.connect(self.use_keyboard_controller) + self.enable_input_checkbox = QtWidgets.QCheckBox("Enable Input") self.enable_input_checkbox.setChecked(False) self.enable_input_checkbox.setEnabled(True) grid_layout = QtWidgets.QGridLayout() - grid_layout.addWidget(self.controller_status_label, 0, 0, 2, 1) + grid_layout.addWidget(self.controller_status_label, 0, 0, 3, 1) grid_layout.addWidget(self.detect_controller_button, 0, 1) + grid_layout.addWidget(self.use_keyboard_button, 1, 1) grid_layout.addWidget( - self.enable_input_checkbox, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter + self.enable_input_checkbox, 2, 1, QtCore.Qt.AlignmentFlag.AlignHCenter ) grid_layout.setColumnStretch(0, 4) grid_layout.setColumnStretch(1, 1) @@ -124,84 +124,54 @@ def __create_widgets(self) -> QtWidgets.QGroupBox: return box def __read_controller_inputs(self) -> None: - """Read and interpret the current controller input values.""" - - def with_deadzone(abs_value: float) -> float: - return ( - abs_value - if abs(abs_value) >= DiagnosticsConstants.DEADZONE_PERCENTAGE - else 0 - ) - - move_axis_x = with_deadzone(self.handheld_controller.abs_value(ecodes.ABS_Y)) - move_axis_y = with_deadzone(self.handheld_controller.abs_value(ecodes.ABS_X)) - move_axis_rot = with_deadzone(self.handheld_controller.abs_value(ecodes.ABS_RX)) - - left_trigger_value = self.handheld_controller.abs_value(ecodes.ABS_Z) - speed_factor = ( - DiagnosticsConstants.SPEED_SLOWDOWN_FACTOR - if left_trigger_value > DiagnosticsConstants.BUTTON_PRESSED_THRESHOLD - else 1 - ) + """Read controller state and update motor control proto.""" + vx, vy, vrot = self.handheld_controller.get_move_velocity() + speed = self.handheld_controller.get_speed_factor() self.motor_control.direct_velocity_control.velocity.x_component_meters = ( - -move_axis_x * self.constants.robot_max_speed_m_per_s * speed_factor + vx * self.constants.robot_max_speed_m_per_s * speed ) self.motor_control.direct_velocity_control.velocity.y_component_meters = ( - -move_axis_y * self.constants.robot_max_speed_m_per_s * speed_factor + vy * self.constants.robot_max_speed_m_per_s * speed ) self.motor_control.direct_velocity_control.angular_velocity.radians_per_second = ( - -move_axis_rot * self.constants.robot_max_ang_speed_rad_per_s * speed_factor + vrot * self.constants.robot_max_ang_speed_rad_per_s * speed ) - d_pad_axis_x = self.handheld_controller.abs_value(ecodes.ABS_HAT0X) - d_pad_axis_y = self.handheld_controller.abs_value(ecodes.ABS_HAT0Y) - - if d_pad_axis_x != self.last_d_pad_axis_x_value: - self.last_d_pad_axis_x_value = d_pad_axis_x + kick_step = self.handheld_controller.get_kick_power_step() + if kick_step: self.kick_power = numpy.clip( - a=self.kick_power - + int(d_pad_axis_x) * DiagnosticsConstants.KICK_POWER_STEPPER, + a=self.kick_power + kick_step * DiagnosticsConstants.KICK_POWER_STEPPER, a_min=DiagnosticsConstants.MIN_KICK_POWER, a_max=DiagnosticsConstants.MAX_KICK_POWER, ) self.chip_distance = numpy.clip( a=self.chip_distance - + d_pad_axis_x * DiagnosticsConstants.CHIP_DISTANCE_STEPPER, + + kick_step * DiagnosticsConstants.CHIP_DISTANCE_STEPPER, a_min=DiagnosticsConstants.MIN_CHIP_POWER, a_max=DiagnosticsConstants.MAX_CHIP_POWER, ) - if d_pad_axis_y != self.last_d_pad_axis_y_value: - self.last_d_pad_axis_y_value = d_pad_axis_y + dribbler_step = self.handheld_controller.get_dribbler_step() + if dribbler_step: self.dribbler_speed = int( numpy.clip( a=self.dribbler_speed - - d_pad_axis_y * DiagnosticsConstants.DRIBBLER_RPM_STEPPER, + + dribbler_step * DiagnosticsConstants.DRIBBLER_RPM_STEPPER, a_min=self.constants.indefinite_dribbler_speed_rpm, a_max=-self.constants.indefinite_dribbler_speed_rpm, ) ) - right_trigger_value = self.handheld_controller.abs_value(ecodes.ABS_RZ) self.motor_control.dribbler_speed_rpm = ( - self.dribbler_speed - if right_trigger_value > DiagnosticsConstants.BUTTON_PRESSED_THRESHOLD - else 0 + self.dribbler_speed if self.handheld_controller.is_dribbler_held() else 0 ) - btn_a = self.handheld_controller.key_down(ecodes.BTN_A) - btn_b = self.handheld_controller.key_down(ecodes.BTN_B) - - if btn_a != self.last_btn_a_value: - self.last_btn_a_value = btn_a - if btn_a: - self.kick_button_pressed.emit() + if self.handheld_controller.is_kick_fired(): + self.kick_button_pressed.emit() - if btn_b != self.last_btn_b_value: - self.last_btn_b_value = btn_b - if btn_b: - self.chip_button_pressed.emit() + if self.handheld_controller.is_chip_fired(): + self.chip_button_pressed.emit() def __update_controller_status(self) -> None: """Update the widget to display the current controller connection status.""" diff --git a/src/software/thunderscope/robot_diagnostics/keyboard_controller.py b/src/software/thunderscope/robot_diagnostics/keyboard_controller.py new file mode 100644 index 0000000000..7fc622ab0e --- /dev/null +++ b/src/software/thunderscope/robot_diagnostics/keyboard_controller.py @@ -0,0 +1,127 @@ +from abc import ABCMeta + +from pyqtgraph.Qt.QtCore import Qt, QObject, QEvent +from pyqtgraph.Qt.QtWidgets import QApplication + +from software.thunderscope.constants import DiagnosticsConstants +from software.thunderscope.robot_diagnostics.controller_base import IControllerBase + + +class _QABCMeta(type(QObject), ABCMeta): + pass + + +class KeyboardController(QObject, IControllerBase, metaclass=_QABCMeta): + """Keyboard input source. + + Installs a QApplication-level event filter so key events are captured + regardless of which widget currently has focus. + + Analog axes (move velocity, speed factor, dribbler hold) are driven by + held keys. Stepped inputs (kick/chip power, dribbler RPM) and one-shot + actions (kick, chip) are set as pending flags in the event filter and + consumed on the first read. + """ + + def __init__(self) -> None: + super().__init__() + self._held_keys: set[Qt.Key] = set() + self._active = True + self._pending_kick_power_step = 0 + self._pending_dribbler_step = 0 + self._pending_kick = False + self._pending_chip = False + QApplication.instance().installEventFilter(self) + + def name(self) -> str: + return "Keyboard" + + def connected(self) -> bool: + return self._active + + def close(self) -> None: + self._active = False + self._held_keys.clear() + app = QApplication.instance() + if app is not None: + app.removeEventFilter(self) + + def get_move_velocity(self) -> tuple[float, float, float]: + """Return (vx, vy, vrot) from held movement keys. + + :return: (vx, vy, vrot) where positive x = forward (W), positive y = strafe left (A), + positive angular = CCW (Q) + """ + + def axis(neg: Qt.Key, pos: Qt.Key) -> float: + if neg in self._held_keys: + return -1.0 + if pos in self._held_keys: + return 1.0 + return 0.0 + + return ( + axis(Qt.Key.Key_S, Qt.Key.Key_W), + axis(Qt.Key.Key_D, Qt.Key.Key_A), + axis(Qt.Key.Key_E, Qt.Key.Key_Q), + ) + + def get_speed_factor(self) -> float: + """Return SPEED_SLOWDOWN_FACTOR if Shift is held, otherwise 1.0.""" + return ( + DiagnosticsConstants.SPEED_SLOWDOWN_FACTOR + if Qt.Key.Key_Shift in self._held_keys + else 1.0 + ) + + def is_dribbler_held(self) -> bool: + """Return True if R is held.""" + return Qt.Key.Key_R in self._held_keys + + def get_kick_power_step(self) -> int: + """Consume and return the pending kick/chip power step (-1, 0, or +1). + Left arrow = -1, Right arrow = +1. + """ + step = self._pending_kick_power_step + self._pending_kick_power_step = 0 + return step + + def get_dribbler_step(self) -> int: + """Consume and return the pending dribbler RPM step (-1, 0, or +1). + Up arrow = +1 (increase), Down arrow = -1 (decrease). + """ + step = self._pending_dribbler_step + self._pending_dribbler_step = 0 + return step + + def is_kick_fired(self) -> bool: + """Consume and return whether a kick was pending (X key).""" + fired = self._pending_kick + self._pending_kick = False + return fired + + def is_chip_fired(self) -> bool: + """Consume and return whether a chip was pending (C key).""" + fired = self._pending_chip + self._pending_chip = False + return fired + + def eventFilter(self, obj: QObject, event: QEvent) -> bool: + if event.type() == QEvent.Type.KeyPress and not event.isAutoRepeat(): + key = Qt.Key(event.key()) + self._held_keys.add(key) + if key == Qt.Key.Key_Left: + self._pending_kick_power_step = -1 + elif key == Qt.Key.Key_Right: + self._pending_kick_power_step = 1 + elif key == Qt.Key.Key_Up: + self._pending_dribbler_step = 1 + elif key == Qt.Key.Key_Down: + self._pending_dribbler_step = -1 + elif key == Qt.Key.Key_X: + self._pending_kick = True + elif key == Qt.Key.Key_C: + self._pending_chip = True + elif event.type() == QEvent.Type.KeyRelease and not event.isAutoRepeat(): + self._held_keys.discard(Qt.Key(event.key())) + return False