standardbots 2.0.0.dev1744991399__tar.gz → 2.0.0.dev1744991699__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of standardbots might be problematic. Click here for more details.

Files changed (18) hide show
  1. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/PKG-INFO +1 -1
  2. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/README.md +42 -8
  3. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/setup.py +1 -1
  4. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/standardbots/auto_generated/apis.py +547 -0
  5. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/standardbots/auto_generated/models.py +1099 -10
  6. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/standardbots.egg-info/PKG-INFO +1 -1
  7. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/standardbots.egg-info/SOURCES.txt +1 -0
  8. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/tests/fixtures/client_fixt.py +0 -2
  9. standardbots-2.0.0.dev1744991699/tests/fixtures/robot_fixt.py +31 -0
  10. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/tests/fixtures/routines_fixt.py +23 -0
  11. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/tests/test_apis.py +131 -46
  12. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/setup.cfg +0 -0
  13. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/standardbots/__init__.py +0 -0
  14. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/standardbots/auto_generated/__init__.py +0 -0
  15. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/standardbots.egg-info/dependency_links.txt +0 -0
  16. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/standardbots.egg-info/requires.txt +0 -0
  17. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/standardbots.egg-info/top_level.txt +0 -0
  18. {standardbots-2.0.0.dev1744991399 → standardbots-2.0.0.dev1744991699}/tests/fixtures/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: standardbots
3
- Version: 2.0.0.dev1744991399
3
+ Version: 2.0.0.dev1744991699
4
4
  Summary: Standard Bots RO1 Robotics API
5
5
  Home-page:
6
6
  Author: Standard Bots Support
@@ -24,6 +24,8 @@ To run a script move into the `sdks/python` folder and run `python playground/fi
24
24
 
25
25
  You may now use the `botctl` commands to setup and run tests.
26
26
 
27
+ Set up the testing environment on the computer where you are running the tests:
28
+
27
29
  ```bash
28
30
  # Setup testing environment
29
31
  botctl publicapi test:setup --help
@@ -32,6 +34,11 @@ botctl publicapi test:setup --help
32
34
  botctl publicapi test --help
33
35
  ```
34
36
 
37
+ Set up the bot environment on the robot:
38
+
39
+ ```bash
40
+ botctl publicapi test:setup-bot
41
+ ```
35
42
 
36
43
  ## Setup
37
44
 
@@ -99,11 +106,8 @@ At start of testing, robot should:
99
106
  The basic idea here is:
100
107
 
101
108
  - These special tests will not be run by default.
102
- - You may pass a flag (e.g. `--routine-running`) when the bot is in the correct state to run the tests.
109
+ - You may pass a flag (e.g. `--camera-disconnected`) when the bot is in the correct state to run the tests.
103
110
  - Markers usage:
104
- - When `--routine-running` flag is passed:
105
- 1. Tests with the flag are run.
106
- 2. Tests without the flag are not run.
107
111
  - When `--gripper=<type>` flag is passed(`type` is value from `GripperKindEnum` enum):
108
112
  1. Tests expect that this specific gripper is connected
109
113
  2. Tests without the flag are not run.
@@ -113,12 +117,12 @@ The basic idea here is:
113
117
 
114
118
  We use [pytest markers](https://docs.pytest.org/en/7.1.x/example/markers.html) to do this.
115
119
 
116
- #### Routine running
120
+ #### Camera disconnected
117
121
 
118
- The special sample routine ("Test Public API") should be running prior to running these tests. Then do:
122
+ The wrist camera should be disabled. Then run:
119
123
 
120
124
  ```bash
121
- python3 -m pytest ./tests --cov=standardbots --routine-running
125
+ python3 -m pytest ./tests --cov=standardbots --camera-disconnected
122
126
  ```
123
127
 
124
128
  #### E-stop
@@ -162,5 +166,35 @@ To test custom sensors:
162
166
  Then run:
163
167
 
164
168
  ```bash
165
- python3 -m pytest ./tests --cov=standardbots --custom-sensors
169
+ python3 -m pytest ./tests --cov=standardbots --gripper=custom_sensors
170
+ ```
171
+
172
+ ## Database backups
173
+
174
+ > See command: `botctl publicapi test:setup-bot`.
175
+
176
+ We now have a common robot database state that can be used for testing. While this isn't necessary for use, it does provide a common state for testing.
177
+
178
+ ### How to create a new backup
179
+
180
+ In the place where you are running the stack and want to create the backup (e.g. on the control box):
181
+
182
+ ```bash
183
+ DB_USER=sb
184
+ DB_NAME=sb
185
+ BACKUP_FILE=$HOME/db-backup-publicapi-test.sql
186
+
187
+ docker exec -it postgres-bot pg_dump -U $DB_USER -d $DB_NAME -F c -f /tmp/backup.sql
188
+
189
+ docker cp postgres-bot:/tmp/backup.sql $BACKUP_FILE
190
+ ```
191
+
192
+ If you need to download to the actual development environment:
193
+
194
+ ```bash
195
+ CB_FILE=/home/control-box-bot/db-backup-publicapi-test.sql
196
+ VM_FILE=~/sb/golang/apps/botctl/commands/publicapi/db-backup-publicapi-test.sql
197
+
198
+ # Move directly to VM:
199
+ scp cb2047:$CB_FILE $VM_FILE
166
200
  ```
@@ -13,7 +13,7 @@
13
13
  from setuptools import setup, find_packages # noqa: H301
14
14
 
15
15
  NAME = "standardbots"
16
- VERSION = "2.0.0-dev1744991399"
16
+ VERSION = "2.0.0-dev1744991699"
17
17
  # To install the library, run the following
18
18
  #
19
19
  # python setup.py install
@@ -354,6 +354,368 @@ class Default:
354
354
  if parsed is None and (is_user_error or is_unavailable):
355
355
  parsed = models.parse_error_response(json.loads(response.data))
356
356
 
357
+ return Response(
358
+ parsed,
359
+ response.status,
360
+ response
361
+ )
362
+ except urllib3.exceptions.MaxRetryError:
363
+ return Response(
364
+ models.ErrorResponse(
365
+ error=models.ErrorEnum.InternalServerError,
366
+ message="Connection Refused"
367
+ ),
368
+ 503,
369
+ None
370
+ )
371
+ class Payload:
372
+ def __init__(self, request_manager: RequestManager):
373
+ self._request_manager = request_manager
374
+
375
+
376
+ def set_payload(
377
+ self,
378
+ body: models.PayloadStateRequest,
379
+ ) -> Response[
380
+ None,
381
+ None
382
+ ]:
383
+ """
384
+ Set a value for the mass being carried by the robot&#x27;s end-effector.
385
+
386
+ """
387
+ path = "/api/v1/payload"
388
+ try:
389
+ response = self._request_manager.request(
390
+ "POST",
391
+ path,
392
+ headers=self._request_manager.json_headers(),
393
+ body=json.dumps(models.serialize_payload_state_request(body)),
394
+ )
395
+ parsed = None
396
+
397
+ is_user_error = response.status >= 400 and response.status <= 500
398
+ is_unavailable = response.status == 503
399
+ if parsed is None and (is_user_error or is_unavailable):
400
+ parsed = models.parse_error_response(json.loads(response.data))
401
+
402
+ return Response(
403
+ parsed,
404
+ response.status,
405
+ response
406
+ )
407
+ except urllib3.exceptions.MaxRetryError:
408
+ return Response(
409
+ models.ErrorResponse(
410
+ error=models.ErrorEnum.InternalServerError,
411
+ message="Connection Refused"
412
+ ),
413
+ 503,
414
+ None
415
+ )
416
+ def get_payload(
417
+ self,
418
+ ) -> Response[
419
+ Union[
420
+ models.PayloadStateResponse,
421
+ models.ErrorResponse,
422
+ None
423
+ ],
424
+ models.PayloadStateResponse
425
+ ]:
426
+ """
427
+ Get the current mass value being carried by the robot&#x27;s end-effector.
428
+
429
+ """
430
+ path = "/api/v1/payload"
431
+ try:
432
+ response = self._request_manager.request(
433
+ "GET",
434
+ path,
435
+ headers=self._request_manager.json_headers(),
436
+ )
437
+ parsed = None
438
+ if response.status == 200:
439
+ parsed = models.parse_payload_state_response(json.loads(response.data))
440
+
441
+ is_user_error = response.status >= 400 and response.status <= 500
442
+ is_unavailable = response.status == 503
443
+ if parsed is None and (is_user_error or is_unavailable):
444
+ parsed = models.parse_error_response(json.loads(response.data))
445
+
446
+ return Response(
447
+ parsed,
448
+ response.status,
449
+ response
450
+ )
451
+ except urllib3.exceptions.MaxRetryError:
452
+ return Response(
453
+ models.ErrorResponse(
454
+ error=models.ErrorEnum.InternalServerError,
455
+ message="Connection Refused"
456
+ ),
457
+ 503,
458
+ None
459
+ )
460
+ class Recorder:
461
+ def __init__(self, request_manager: RequestManager):
462
+ self._request_manager = request_manager
463
+
464
+
465
+ def get_recorder_state(
466
+ self,
467
+ ) -> Response[
468
+ Union[
469
+ models.RecorderState,
470
+ models.ErrorResponse,
471
+ None
472
+ ],
473
+ models.RecorderState
474
+ ]:
475
+ """
476
+ Get the state of the recorder
477
+ """
478
+ path = "/api/v1/recorder/state"
479
+ try:
480
+ response = self._request_manager.request(
481
+ "GET",
482
+ path,
483
+ headers=self._request_manager.json_headers(),
484
+ )
485
+ parsed = None
486
+ if response.status == 200:
487
+ parsed = models.parse_recorder_state(json.loads(response.data))
488
+
489
+ is_user_error = response.status >= 400 and response.status <= 500
490
+ is_unavailable = response.status == 503
491
+ if parsed is None and (is_user_error or is_unavailable):
492
+ parsed = models.parse_error_response(json.loads(response.data))
493
+
494
+ return Response(
495
+ parsed,
496
+ response.status,
497
+ response
498
+ )
499
+ except urllib3.exceptions.MaxRetryError:
500
+ return Response(
501
+ models.ErrorResponse(
502
+ error=models.ErrorEnum.InternalServerError,
503
+ message="Connection Refused"
504
+ ),
505
+ 503,
506
+ None
507
+ )
508
+ def update_recording(
509
+ self,
510
+ body: models.RecorderConfig,
511
+ ) -> Response[
512
+ Union[
513
+ models.UpdateRecordingResponse,
514
+ models.ErrorResponse,
515
+ None
516
+ ],
517
+ models.UpdateRecordingResponse
518
+ ]:
519
+ """
520
+ Update recording configuration
521
+ """
522
+ path = "/api/v1/recorder/update"
523
+ try:
524
+ response = self._request_manager.request(
525
+ "POST",
526
+ path,
527
+ headers=self._request_manager.json_headers(),
528
+ body=json.dumps(models.serialize_recorder_config(body)),
529
+ )
530
+ parsed = None
531
+ if response.status == 200:
532
+ parsed = models.parse_update_recording_response(json.loads(response.data))
533
+
534
+ is_user_error = response.status >= 400 and response.status <= 500
535
+ is_unavailable = response.status == 503
536
+ if parsed is None and (is_user_error or is_unavailable):
537
+ parsed = models.parse_error_response(json.loads(response.data))
538
+
539
+ return Response(
540
+ parsed,
541
+ response.status,
542
+ response
543
+ )
544
+ except urllib3.exceptions.MaxRetryError:
545
+ return Response(
546
+ models.ErrorResponse(
547
+ error=models.ErrorEnum.InternalServerError,
548
+ message="Connection Refused"
549
+ ),
550
+ 503,
551
+ None
552
+ )
553
+ def enable_recorder_bot(
554
+ self,
555
+ body: models.ToggleRecorderBotRequest,
556
+ ) -> Response[
557
+ Union[
558
+ models.UpdateRecordingResponse,
559
+ models.ErrorResponse,
560
+ None
561
+ ],
562
+ models.UpdateRecordingResponse
563
+ ]:
564
+ """
565
+ Enable or disable a secondary bot
566
+ """
567
+ path = "/api/v1/recorder/set-bot-enabled"
568
+ try:
569
+ response = self._request_manager.request(
570
+ "POST",
571
+ path,
572
+ headers=self._request_manager.json_headers(),
573
+ body=json.dumps(models.serialize_toggle_recorder_bot_request(body)),
574
+ )
575
+ parsed = None
576
+ if response.status == 200:
577
+ parsed = models.parse_update_recording_response(json.loads(response.data))
578
+
579
+ is_user_error = response.status >= 400 and response.status <= 500
580
+ is_unavailable = response.status == 503
581
+ if parsed is None and (is_user_error or is_unavailable):
582
+ parsed = models.parse_error_response(json.loads(response.data))
583
+
584
+ return Response(
585
+ parsed,
586
+ response.status,
587
+ response
588
+ )
589
+ except urllib3.exceptions.MaxRetryError:
590
+ return Response(
591
+ models.ErrorResponse(
592
+ error=models.ErrorEnum.InternalServerError,
593
+ message="Connection Refused"
594
+ ),
595
+ 503,
596
+ None
597
+ )
598
+ def start_recording(
599
+ self,
600
+ body: models.StartRecordingRequest,
601
+ ) -> Response[
602
+ Union[
603
+ models.StartRecordingResponse,
604
+ models.ErrorResponse,
605
+ None
606
+ ],
607
+ models.StartRecordingResponse
608
+ ]:
609
+ """
610
+ Start recording movement and camera data
611
+ """
612
+ path = "/api/v1/recorder/start"
613
+ try:
614
+ response = self._request_manager.request(
615
+ "POST",
616
+ path,
617
+ headers=self._request_manager.json_headers(),
618
+ body=json.dumps(models.serialize_start_recording_request(body)),
619
+ )
620
+ parsed = None
621
+ if response.status == 200:
622
+ parsed = models.parse_start_recording_response(json.loads(response.data))
623
+
624
+ is_user_error = response.status >= 400 and response.status <= 500
625
+ is_unavailable = response.status == 503
626
+ if parsed is None and (is_user_error or is_unavailable):
627
+ parsed = models.parse_error_response(json.loads(response.data))
628
+
629
+ return Response(
630
+ parsed,
631
+ response.status,
632
+ response
633
+ )
634
+ except urllib3.exceptions.MaxRetryError:
635
+ return Response(
636
+ models.ErrorResponse(
637
+ error=models.ErrorEnum.InternalServerError,
638
+ message="Connection Refused"
639
+ ),
640
+ 503,
641
+ None
642
+ )
643
+ def stop_recording(
644
+ self,
645
+ body: models.StopRecordingRequest,
646
+ ) -> Response[
647
+ Union[
648
+ models.StopRecordingResponse,
649
+ models.ErrorResponse,
650
+ None
651
+ ],
652
+ models.StopRecordingResponse
653
+ ]:
654
+ """
655
+ Stop recording movement and camera data
656
+ """
657
+ path = "/api/v1/recorder/stop"
658
+ try:
659
+ response = self._request_manager.request(
660
+ "POST",
661
+ path,
662
+ headers=self._request_manager.json_headers(),
663
+ body=json.dumps(models.serialize_stop_recording_request(body)),
664
+ )
665
+ parsed = None
666
+ if response.status == 200:
667
+ parsed = models.parse_stop_recording_response(json.loads(response.data))
668
+
669
+ is_user_error = response.status >= 400 and response.status <= 500
670
+ is_unavailable = response.status == 503
671
+ if parsed is None and (is_user_error or is_unavailable):
672
+ parsed = models.parse_error_response(json.loads(response.data))
673
+
674
+ return Response(
675
+ parsed,
676
+ response.status,
677
+ response
678
+ )
679
+ except urllib3.exceptions.MaxRetryError:
680
+ return Response(
681
+ models.ErrorResponse(
682
+ error=models.ErrorEnum.InternalServerError,
683
+ message="Connection Refused"
684
+ ),
685
+ 503,
686
+ None
687
+ )
688
+ def save_recording(
689
+ self,
690
+ body: models.SaveRecordingRequest,
691
+ ) -> Response[
692
+ Union[
693
+ models.SaveRecordingResponse,
694
+ models.ErrorResponse,
695
+ None
696
+ ],
697
+ models.SaveRecordingResponse
698
+ ]:
699
+ """
700
+ Save recording to marvin app
701
+ """
702
+ path = "/api/v1/recorder/save"
703
+ try:
704
+ response = self._request_manager.request(
705
+ "POST",
706
+ path,
707
+ headers=self._request_manager.json_headers(),
708
+ body=json.dumps(models.serialize_save_recording_request(body)),
709
+ )
710
+ parsed = None
711
+ if response.status == 200:
712
+ parsed = models.parse_save_recording_response(json.loads(response.data))
713
+
714
+ is_user_error = response.status >= 400 and response.status <= 500
715
+ is_unavailable = response.status == 503
716
+ if parsed is None and (is_user_error or is_unavailable):
717
+ parsed = models.parse_error_response(json.loads(response.data))
718
+
357
719
  return Response(
358
720
  parsed,
359
721
  response.status,
@@ -470,18 +832,203 @@ class Default:
470
832
  503,
471
833
  None
472
834
  )
835
+ class Teleop:
836
+ def __init__(self, request_manager: RequestManager):
837
+ self._request_manager = request_manager
838
+
839
+
840
+ def get_state(
841
+ self,
842
+ ) -> Response[
843
+ Union[
844
+ models.TeleopState,
845
+ models.ErrorResponse,
846
+ None
847
+ ],
848
+ models.TeleopState
849
+ ]:
850
+ """
851
+ Get the state of the teleop
852
+ """
853
+ path = "/api/v1/teleop/state"
854
+ try:
855
+ response = self._request_manager.request(
856
+ "GET",
857
+ path,
858
+ headers=self._request_manager.json_headers(),
859
+ )
860
+ parsed = None
861
+ if response.status == 200:
862
+ parsed = models.parse_teleop_state(json.loads(response.data))
863
+
864
+ is_user_error = response.status >= 400 and response.status <= 500
865
+ is_unavailable = response.status == 503
866
+ if parsed is None and (is_user_error or is_unavailable):
867
+ parsed = models.parse_error_response(json.loads(response.data))
868
+
869
+ return Response(
870
+ parsed,
871
+ response.status,
872
+ response
873
+ )
874
+ except urllib3.exceptions.MaxRetryError:
875
+ return Response(
876
+ models.ErrorResponse(
877
+ error=models.ErrorEnum.InternalServerError,
878
+ message="Connection Refused"
879
+ ),
880
+ 503,
881
+ None
882
+ )
883
+ def enable_bot(
884
+ self,
885
+ body: models.ToggleTeleopBotRequest,
886
+ ) -> Response[
887
+ Union[
888
+ models.TeleopState,
889
+ models.ErrorResponse,
890
+ None
891
+ ],
892
+ models.TeleopState
893
+ ]:
894
+ """
895
+ Enable or disable a secondary bot
896
+ """
897
+ path = "/api/v1/teleop/set-bot-enabled"
898
+ try:
899
+ response = self._request_manager.request(
900
+ "POST",
901
+ path,
902
+ headers=self._request_manager.json_headers(),
903
+ body=json.dumps(models.serialize_toggle_teleop_bot_request(body)),
904
+ )
905
+ parsed = None
906
+ if response.status == 200:
907
+ parsed = models.parse_teleop_state(json.loads(response.data))
908
+
909
+ is_user_error = response.status >= 400 and response.status <= 500
910
+ is_unavailable = response.status == 503
911
+ if parsed is None and (is_user_error or is_unavailable):
912
+ parsed = models.parse_error_response(json.loads(response.data))
913
+
914
+ return Response(
915
+ parsed,
916
+ response.status,
917
+ response
918
+ )
919
+ except urllib3.exceptions.MaxRetryError:
920
+ return Response(
921
+ models.ErrorResponse(
922
+ error=models.ErrorEnum.InternalServerError,
923
+ message="Connection Refused"
924
+ ),
925
+ 503,
926
+ None
927
+ )
928
+ def start_teleop(
929
+ self,
930
+ ) -> Response[
931
+ Union[
932
+ models.StartTeleopResponse,
933
+ models.ErrorResponse,
934
+ None
935
+ ],
936
+ models.StartTeleopResponse
937
+ ]:
938
+ """
939
+ Start teleoperation
940
+ """
941
+ path = "/api/v1/teleop/start"
942
+ try:
943
+ response = self._request_manager.request(
944
+ "POST",
945
+ path,
946
+ headers=self._request_manager.json_headers(),
947
+ )
948
+ parsed = None
949
+ if response.status == 200:
950
+ parsed = models.parse_start_teleop_response(json.loads(response.data))
951
+
952
+ is_user_error = response.status >= 400 and response.status <= 500
953
+ is_unavailable = response.status == 503
954
+ if parsed is None and (is_user_error or is_unavailable):
955
+ parsed = models.parse_error_response(json.loads(response.data))
956
+
957
+ return Response(
958
+ parsed,
959
+ response.status,
960
+ response
961
+ )
962
+ except urllib3.exceptions.MaxRetryError:
963
+ return Response(
964
+ models.ErrorResponse(
965
+ error=models.ErrorEnum.InternalServerError,
966
+ message="Connection Refused"
967
+ ),
968
+ 503,
969
+ None
970
+ )
971
+ def stop_teleop(
972
+ self,
973
+ ) -> Response[
974
+ Union[
975
+ models.StopTeleopResponse,
976
+ models.ErrorResponse,
977
+ None
978
+ ],
979
+ models.StopTeleopResponse
980
+ ]:
981
+ """
982
+ Stop recording movement and camera data
983
+ """
984
+ path = "/api/v1/teleop/stop"
985
+ try:
986
+ response = self._request_manager.request(
987
+ "POST",
988
+ path,
989
+ headers=self._request_manager.json_headers(),
990
+ )
991
+ parsed = None
992
+ if response.status == 200:
993
+ parsed = models.parse_stop_teleop_response(json.loads(response.data))
994
+
995
+ is_user_error = response.status >= 400 and response.status <= 500
996
+ is_unavailable = response.status == 503
997
+ if parsed is None and (is_user_error or is_unavailable):
998
+ parsed = models.parse_error_response(json.loads(response.data))
999
+
1000
+ return Response(
1001
+ parsed,
1002
+ response.status,
1003
+ response
1004
+ )
1005
+ except urllib3.exceptions.MaxRetryError:
1006
+ return Response(
1007
+ models.ErrorResponse(
1008
+ error=models.ErrorEnum.InternalServerError,
1009
+ message="Connection Refused"
1010
+ ),
1011
+ 503,
1012
+ None
1013
+ )
473
1014
 
474
1015
  calibration: Calibration
475
1016
  equipment: Equipment
1017
+ payload: Payload
1018
+ recorder: Recorder
476
1019
  sensors: Sensors
477
1020
  space: Space
1021
+ teleop: Teleop
478
1022
 
479
1023
  def __init__(self, request_manager: RequestManager):
480
1024
  self._request_manager = request_manager
481
1025
  self.calibration = Default.Calibration(request_manager)
482
1026
  self.equipment = Default.Equipment(request_manager)
1027
+ self.payload = Default.Payload(request_manager)
1028
+ self.recorder = Default.Recorder(request_manager)
483
1029
  self.sensors = Default.Sensors(request_manager)
484
1030
  self.space = Default.Space(request_manager)
1031
+ self.teleop = Default.Teleop(request_manager)
485
1032
 
486
1033
  class Movement:
487
1034
  _request_manager: RequestManager