swbt-python 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

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.
swbt/__init__.py CHANGED
@@ -4,11 +4,14 @@ from swbt.adapter_discovery import AdapterInfo, list_adapters
4
4
  from swbt.diagnostics import DiagnosticsConfig, GamepadStatus
5
5
  from swbt.errors import (
6
6
  AdapterDiscoveryError,
7
+ AdapterIdentityRecoveryRequired,
7
8
  ClosedError,
8
9
  ConnectionFailedError,
9
10
  ConnectionTimeoutError,
10
11
  InvalidInputError,
11
12
  InvalidKeyStoreError,
13
+ InvalidProfileError,
14
+ ProfileControllerMismatchError,
12
15
  SwbtError,
13
16
  TransportOpenError,
14
17
  UnsupportedInputError,
@@ -30,6 +33,7 @@ from swbt.protocol.profiles.base import ControllerColors
30
33
 
31
34
  __all__ = (
32
35
  "AdapterDiscoveryError",
36
+ "AdapterIdentityRecoveryRequired",
33
37
  "AdapterInfo",
34
38
  "Button",
35
39
  "ClosedError",
@@ -47,10 +51,12 @@ __all__ = (
47
51
  "InputState",
48
52
  "InvalidInputError",
49
53
  "InvalidKeyStoreError",
54
+ "InvalidProfileError",
50
55
  "JoyConL",
51
56
  "JoyConR",
52
57
  "PeriodicSwitchGamepad",
53
58
  "ProController",
59
+ "ProfileControllerMismatchError",
54
60
  "Stick",
55
61
  "SwbtError",
56
62
  "SwitchGamepad",
swbt/_testing/gamepad.py CHANGED
@@ -19,7 +19,7 @@ def make_pro_controller(
19
19
  *,
20
20
  transport: HidDeviceTransport,
21
21
  adapter: str | None = None,
22
- key_store_path: str | None = None,
22
+ profile_path: str | None = None,
23
23
  report_period_us: int | None = None,
24
24
  controller_colors: ControllerColors | None = None,
25
25
  diagnostics: DiagnosticsConfig | None = None,
@@ -28,7 +28,7 @@ def make_pro_controller(
28
28
  return ProController._from_config(
29
29
  _SwitchGamepadConfig(
30
30
  adapter=adapter,
31
- key_store_path=key_store_path,
31
+ profile_path=profile_path,
32
32
  report_period_us=report_period_us,
33
33
  controller_colors=controller_colors,
34
34
  ),
@@ -41,7 +41,7 @@ def make_joycon_l(
41
41
  *,
42
42
  transport: HidDeviceTransport,
43
43
  adapter: str | None = None,
44
- key_store_path: str | None = None,
44
+ profile_path: str | None = None,
45
45
  report_period_us: int | None = None,
46
46
  controller_colors: ControllerColors | None = None,
47
47
  diagnostics: DiagnosticsConfig | None = None,
@@ -50,7 +50,7 @@ def make_joycon_l(
50
50
  return JoyConL._from_config(
51
51
  _SwitchGamepadConfig(
52
52
  adapter=adapter,
53
- key_store_path=key_store_path,
53
+ profile_path=profile_path,
54
54
  report_period_us=report_period_us,
55
55
  controller_colors=controller_colors,
56
56
  profile=JoyConLeftProfile(),
@@ -64,7 +64,7 @@ def make_joycon_r(
64
64
  *,
65
65
  transport: HidDeviceTransport,
66
66
  adapter: str | None = None,
67
- key_store_path: str | None = None,
67
+ profile_path: str | None = None,
68
68
  report_period_us: int | None = None,
69
69
  controller_colors: ControllerColors | None = None,
70
70
  diagnostics: DiagnosticsConfig | None = None,
@@ -73,7 +73,7 @@ def make_joycon_r(
73
73
  return JoyConR._from_config(
74
74
  _SwitchGamepadConfig(
75
75
  adapter=adapter,
76
- key_store_path=key_store_path,
76
+ profile_path=profile_path,
77
77
  report_period_us=report_period_us,
78
78
  controller_colors=controller_colors,
79
79
  profile=JoyConRightProfile(),
@@ -87,7 +87,7 @@ def make_direct_pro_controller(
87
87
  *,
88
88
  transport: HidDeviceTransport,
89
89
  adapter: str | None = None,
90
- key_store_path: str | None = None,
90
+ profile_path: str | None = None,
91
91
  controller_colors: ControllerColors | None = None,
92
92
  diagnostics: DiagnosticsConfig | None = None,
93
93
  ) -> DirectProController:
@@ -95,7 +95,7 @@ def make_direct_pro_controller(
95
95
  return DirectProController._from_config(
96
96
  _SwitchGamepadConfig(
97
97
  adapter=adapter,
98
- key_store_path=key_store_path,
98
+ profile_path=profile_path,
99
99
  controller_colors=controller_colors,
100
100
  ),
101
101
  diagnostics=diagnostics,
@@ -107,7 +107,7 @@ def make_direct_joycon_l(
107
107
  *,
108
108
  transport: HidDeviceTransport,
109
109
  adapter: str | None = None,
110
- key_store_path: str | None = None,
110
+ profile_path: str | None = None,
111
111
  controller_colors: ControllerColors | None = None,
112
112
  diagnostics: DiagnosticsConfig | None = None,
113
113
  ) -> DirectJoyConL:
@@ -115,7 +115,7 @@ def make_direct_joycon_l(
115
115
  return DirectJoyConL._from_config(
116
116
  _SwitchGamepadConfig(
117
117
  adapter=adapter,
118
- key_store_path=key_store_path,
118
+ profile_path=profile_path,
119
119
  controller_colors=controller_colors,
120
120
  profile=JoyConLeftProfile(),
121
121
  ),
@@ -128,7 +128,7 @@ def make_direct_joycon_r(
128
128
  *,
129
129
  transport: HidDeviceTransport,
130
130
  adapter: str | None = None,
131
- key_store_path: str | None = None,
131
+ profile_path: str | None = None,
132
132
  controller_colors: ControllerColors | None = None,
133
133
  diagnostics: DiagnosticsConfig | None = None,
134
134
  ) -> DirectJoyConR:
@@ -136,7 +136,7 @@ def make_direct_joycon_r(
136
136
  return DirectJoyConR._from_config(
137
137
  _SwitchGamepadConfig(
138
138
  adapter=adapter,
139
- key_store_path=key_store_path,
139
+ profile_path=profile_path,
140
140
  controller_colors=controller_colors,
141
141
  profile=JoyConRightProfile(),
142
142
  ),
swbt/diagnostics.py CHANGED
@@ -112,7 +112,7 @@ class DiagnosticsRecorder:
112
112
  return diagnostics_event
113
113
 
114
114
  def record_report_tx(self, *, report_id: int, reason: str) -> DiagnosticsEvent:
115
- """Record one sent report and increment its counter."""
115
+ """Record one transport-accepted report and increment its counter."""
116
116
  counter = self._report_counters.get(report_id, 0) + 1
117
117
  self._report_counters[report_id] = counter
118
118
  return self.record_event(
@@ -139,9 +139,7 @@ class DiagnosticsRecorder:
139
139
  self,
140
140
  *,
141
141
  adapter: str,
142
- key_store_exists: bool | None = None,
143
- key_store_path: str | None = None,
144
- key_store_previous_exists: bool | None = None,
142
+ profile_path: str | None = None,
145
143
  ) -> DiagnosticsEvent:
146
144
  """Record environment metadata for one diagnostics run."""
147
145
  fields: dict[str, object] = {
@@ -150,12 +148,8 @@ class DiagnosticsRecorder:
150
148
  "package_version": self._package_version(),
151
149
  "python_version": platform.python_version(),
152
150
  }
153
- if key_store_path is not None:
154
- fields["key_store_path"] = key_store_path
155
- if key_store_exists is not None:
156
- fields["key_store_exists"] = key_store_exists
157
- if key_store_previous_exists is not None:
158
- fields["key_store_previous_exists"] = key_store_previous_exists
151
+ if profile_path is not None:
152
+ fields["profile_path"] = profile_path
159
153
  return self.record_event(
160
154
  "run_metadata",
161
155
  **fields,
swbt/errors.py CHANGED
@@ -63,6 +63,43 @@ class InvalidKeyStoreError(SwbtError):
63
63
  """Raised when a key store has an unsupported or invalid shape."""
64
64
 
65
65
 
66
+ class InvalidProfileError(SwbtError):
67
+ """Raised when a pairing profile is invalid or unsupported."""
68
+
69
+
70
+ class ProfileControllerMismatchError(InvalidProfileError):
71
+ """Raised when a profile belongs to a different concrete controller kind."""
72
+
73
+ def __init__(
74
+ self,
75
+ *,
76
+ expected_controller_kind: str,
77
+ actual_controller_kind: str,
78
+ ) -> None:
79
+ """Initialize expected and actual profile controller kinds."""
80
+ message = (
81
+ "profile controller_kind mismatch: "
82
+ f"expected {expected_controller_kind!r}, got {actual_controller_kind!r}"
83
+ )
84
+ super().__init__(message)
85
+ self.expected_controller_kind = expected_controller_kind
86
+ self.actual_controller_kind = actual_controller_kind
87
+
88
+
89
+ class AdapterIdentityRecoveryRequired(SwbtError): # noqa: N818
90
+ """Raised when a volatile write may have changed the adapter identity."""
91
+
92
+ def __init__(self, *, target_address: str, stage: str) -> None:
93
+ """Initialize the target and failure stage requiring physical recovery."""
94
+ message = (
95
+ "adapter identity preparation became uncertain after write started; "
96
+ "unplug and reconnect the USB dongle before retrying"
97
+ )
98
+ super().__init__(message)
99
+ self.target_address = target_address
100
+ self.stage = stage
101
+
102
+
66
103
  class ProtocolError(SwbtError):
67
104
  """Raised when protocol bytes cannot be parsed or produced."""
68
105
 
swbt/gamepad/_config.py CHANGED
@@ -13,7 +13,7 @@ class _SwitchGamepadConfig:
13
13
 
14
14
  Attributes:
15
15
  adapter: Bumble adapter moniker, such as ``"usb:0"``.
16
- key_store_path: Path used by the default transport to persist pairing keys.
16
+ profile_path: Path to a swbt-owned pairing profile.
17
17
  profile: Fixed controller identity and protocol profile.
18
18
  report_period_us: Periodic input report interval in microseconds.
19
19
  device_name: HID device name advertised to the host.
@@ -21,7 +21,7 @@ class _SwitchGamepadConfig:
21
21
  """
22
22
 
23
23
  adapter: str | None = None
24
- key_store_path: str | None = None
24
+ profile_path: str | None = None
25
25
  profile: ControllerProfile = field(default_factory=default_controller_profile)
26
26
  report_period_us: int | None = None
27
27
  device_name: str | None = None
@@ -60,14 +60,14 @@ class _ControllerSpec:
60
60
  self,
61
61
  *,
62
62
  adapter: str | None,
63
- key_store_path: str | None,
64
63
  report_period_us: int | None,
65
64
  controller_colors: ControllerColors | None,
65
+ profile_path: str | None = None,
66
66
  ) -> _SwitchGamepadConfig:
67
67
  """Create internal construction config from public constructor options."""
68
68
  return _SwitchGamepadConfig(
69
69
  adapter=adapter,
70
- key_store_path=key_store_path,
70
+ profile_path=profile_path,
71
71
  profile=self.profile,
72
72
  report_period_us=report_period_us,
73
73
  controller_colors=controller_colors,
@@ -79,7 +79,7 @@ class _RuntimeConfig:
79
79
  """Normalized internal configuration for ControllerRuntime."""
80
80
 
81
81
  adapter: str | None
82
- key_store_path: str | None
82
+ profile_path: str | None
83
83
  profile: ControllerProfile
84
84
  report_period_us: int
85
85
  device_name: str
@@ -93,7 +93,7 @@ class _RuntimeConfig:
93
93
  raise InvalidInputError(msg)
94
94
  return cls(
95
95
  adapter=config.adapter,
96
- key_store_path=config.key_store_path,
96
+ profile_path=config.profile_path,
97
97
  profile=config.profile,
98
98
  report_period_us=config.report_period_us,
99
99
  device_name=config.device_name,
@@ -58,7 +58,7 @@ class ConnectionWorkflow:
58
58
  diagnostics: DiagnosticsRecorder
59
59
  ensure_open: EnsureOpen
60
60
  get_transport: TransportProvider
61
- key_store_path: str | None
61
+ profile_path: str | None
62
62
  pair: PairWithTimeout
63
63
  set_connection_state: StateSetter
64
64
  transport_was_injected: bool
@@ -71,10 +71,10 @@ class ConnectionWorkflow:
71
71
  """Try active reconnect with exactly one bonded peer."""
72
72
  await self.ensure_open()
73
73
  transport = self._transport()
74
- if self.key_store_path is None and not self.transport_was_injected:
74
+ if self.profile_path is None and not self.transport_was_injected:
75
75
  self.diagnostics.record_event(
76
- "reconnect_key_store_unavailable",
77
- reason="key_store_path_none",
76
+ "reconnect_profile_unavailable",
77
+ reason="profile_path_none",
78
78
  route="active_reconnect",
79
79
  )
80
80
  peers = await transport.list_bonded_peers()
swbt/gamepad/core.py CHANGED
@@ -17,6 +17,10 @@ from swbt.protocol.profiles.base import ControllerColors
17
17
  from swbt.protocol.profiles.joycon import JoyConLeftProfile, JoyConRightProfile
18
18
  from swbt.protocol.profiles.pro_controller import default_controller_profile
19
19
  from swbt.state_store import InputStateStore
20
+ from swbt.transport._pairing_profile import (
21
+ LocalAddress,
22
+ PairingProfile,
23
+ )
20
24
  from swbt.transport.base import HidDeviceTransport
21
25
 
22
26
 
@@ -34,7 +38,6 @@ class _RuntimeBackedGamepad:
34
38
  self,
35
39
  *,
36
40
  adapter: str | None = None,
37
- key_store_path: str | None = None,
38
41
  report_period_us: int | None = None,
39
42
  controller_colors: ControllerColors | None = None,
40
43
  diagnostics: DiagnosticsConfig | None = None,
@@ -43,7 +46,6 @@ class _RuntimeBackedGamepad:
43
46
 
44
47
  Args:
45
48
  adapter: Bumble adapter moniker used for the Bluetooth backend.
46
- key_store_path: Optional path used by the Bluetooth backend to persist keys.
47
49
  report_period_us: Optional periodic input report interval in microseconds.
48
50
  controller_colors: Optional fixed controller body, button, and grip colors.
49
51
  diagnostics: Optional diagnostics configuration for trace output.
@@ -53,7 +55,6 @@ class _RuntimeBackedGamepad:
53
55
  """
54
56
  config = self._controller_spec.build_config(
55
57
  adapter=adapter,
56
- key_store_path=key_store_path,
57
58
  report_period_us=report_period_us,
58
59
  controller_colors=controller_colors,
59
60
  )
@@ -350,6 +351,39 @@ class _PeriodicRuntimeBackedGamepad(_RuntimeBackedGamepad, PeriodicSwitchGamepad
350
351
  """
351
352
  await self._runtime.apply(state)
352
353
 
354
+ @classmethod
355
+ async def _create_pairing_profile(
356
+ cls,
357
+ *,
358
+ adapter: str,
359
+ profile_path: str,
360
+ local_address: str | None,
361
+ pair_timeout: float | None,
362
+ report_period_us: int | None,
363
+ controller_colors: ControllerColors | None,
364
+ diagnostics: DiagnosticsConfig | None,
365
+ ) -> Self:
366
+ """Create, pair, and clean up a concrete periodic controller profile."""
367
+ target = None if local_address is None else LocalAddress.parse(local_address)
368
+ PairingProfile.create_new(
369
+ profile_path,
370
+ target,
371
+ controller_kind=cls._controller_spec.profile.kind,
372
+ )
373
+ gamepad = cls(
374
+ adapter=adapter,
375
+ profile_path=profile_path, # ty: ignore[unknown-argument]
376
+ report_period_us=report_period_us,
377
+ controller_colors=controller_colors,
378
+ diagnostics=diagnostics,
379
+ )
380
+ try:
381
+ await gamepad.pair(timeout=pair_timeout)
382
+ except BaseException:
383
+ await gamepad.close(neutral=False)
384
+ raise
385
+ return gamepad
386
+
353
387
 
354
388
  class _DirectRuntimeBackedGamepad(_RuntimeBackedGamepad, DirectSwitchGamepad):
355
389
  """Runtime-backed gamepad with caller-owned input transmission."""
@@ -360,7 +394,7 @@ class _DirectRuntimeBackedGamepad(_RuntimeBackedGamepad, DirectSwitchGamepad):
360
394
  self,
361
395
  *,
362
396
  adapter: str | None = None,
363
- key_store_path: str | None = None,
397
+ profile_path: str | None = None,
364
398
  controller_colors: ControllerColors | None = None,
365
399
  diagnostics: DiagnosticsConfig | None = None,
366
400
  ) -> None:
@@ -368,7 +402,7 @@ class _DirectRuntimeBackedGamepad(_RuntimeBackedGamepad, DirectSwitchGamepad):
368
402
 
369
403
  Args:
370
404
  adapter: Bumble adapter moniker used for the Bluetooth backend.
371
- key_store_path: Optional path used by the Bluetooth backend to persist keys.
405
+ profile_path: Optional swbt-owned pairing profile path.
372
406
  controller_colors: Optional fixed controller body, button, and grip colors.
373
407
  diagnostics: Optional diagnostics configuration for trace output.
374
408
 
@@ -377,12 +411,63 @@ class _DirectRuntimeBackedGamepad(_RuntimeBackedGamepad, DirectSwitchGamepad):
377
411
  """
378
412
  config = self._controller_spec.build_config(
379
413
  adapter=adapter,
380
- key_store_path=key_store_path,
414
+ profile_path=profile_path,
381
415
  report_period_us=None,
382
416
  controller_colors=controller_colors,
383
417
  )
384
418
  self._init_from_config(config, diagnostics=diagnostics, transport=None)
385
419
 
420
+ @classmethod
421
+ async def create_profile(
422
+ cls,
423
+ *,
424
+ adapter: str,
425
+ profile_path: str,
426
+ local_address: str | None = None,
427
+ pair_timeout: float | None = None,
428
+ controller_colors: ControllerColors | None = None,
429
+ diagnostics: DiagnosticsConfig | None = None,
430
+ ) -> Self:
431
+ """Create a new direct pairing profile and pair it.
432
+
433
+ Args:
434
+ adapter: Bumble adapter moniker. An explicit local address may prepare
435
+ volatile adapter identity state.
436
+ profile_path: New path for the swbt-owned profile JSON.
437
+ local_address: Optional individual locally administered Bluetooth address.
438
+ ``None`` uses the adapter's current default address without rewriting it.
439
+ pair_timeout: Maximum seconds to wait for the initial pairing connection.
440
+ controller_colors: Optional fixed controller body, button, and grip colors.
441
+ diagnostics: Optional diagnostics configuration for trace output.
442
+
443
+ Returns:
444
+ The paired direct controller. The caller owns its lifetime.
445
+
446
+ Raises:
447
+ ValueError: ``local_address`` is invalid.
448
+ FileExistsError: ``profile_path`` already exists.
449
+ Exception: Profile preparation or pairing failed. The created profile remains
450
+ available for a later retry.
451
+ """
452
+ target = None if local_address is None else LocalAddress.parse(local_address)
453
+ PairingProfile.create_new(
454
+ profile_path,
455
+ target,
456
+ controller_kind=cls._controller_spec.profile.kind,
457
+ )
458
+ gamepad = cls(
459
+ adapter=adapter,
460
+ profile_path=profile_path,
461
+ controller_colors=controller_colors,
462
+ diagnostics=diagnostics,
463
+ )
464
+ try:
465
+ await gamepad.pair(timeout=pair_timeout)
466
+ except BaseException:
467
+ await gamepad.close(neutral=False)
468
+ raise
469
+ return gamepad
470
+
386
471
  async def send(self, state: InputState) -> None:
387
472
  """Send one complete input state and commit it after transmission.
388
473
 
@@ -397,6 +482,79 @@ class ProController(_PeriodicRuntimeBackedGamepad):
397
482
 
398
483
  _controller_spec = _ControllerSpec(profile=default_controller_profile())
399
484
 
485
+ def __init__(
486
+ self,
487
+ *,
488
+ adapter: str | None = None,
489
+ profile_path: str | None = None,
490
+ report_period_us: int | None = None,
491
+ controller_colors: ControllerColors | None = None,
492
+ diagnostics: DiagnosticsConfig | None = None,
493
+ ) -> None:
494
+ """Create a Pro Controller-compatible gamepad.
495
+
496
+ Args:
497
+ adapter: Bumble adapter moniker used for the Bluetooth backend.
498
+ profile_path: Optional swbt-owned pairing profile path.
499
+ report_period_us: Optional periodic input report interval in microseconds.
500
+ controller_colors: Optional fixed controller body, button, and grip colors.
501
+ diagnostics: Optional diagnostics configuration for trace output.
502
+
503
+ Raises:
504
+ InvalidInputError: adapter is omitted or report_period_us is not positive.
505
+ """
506
+ config = self._controller_spec.build_config(
507
+ adapter=adapter,
508
+ profile_path=profile_path,
509
+ report_period_us=report_period_us,
510
+ controller_colors=controller_colors,
511
+ )
512
+ self._init_from_config(config, diagnostics=diagnostics, transport=None)
513
+
514
+ @classmethod
515
+ async def create_profile(
516
+ cls,
517
+ *,
518
+ adapter: str,
519
+ profile_path: str,
520
+ local_address: str | None = None,
521
+ pair_timeout: float | None = None,
522
+ report_period_us: int | None = None,
523
+ controller_colors: ControllerColors | None = None,
524
+ diagnostics: DiagnosticsConfig | None = None,
525
+ ) -> Self:
526
+ """Create a new pairing profile and pair it.
527
+
528
+ Args:
529
+ adapter: Bumble adapter moniker. An explicit local address may prepare
530
+ volatile adapter identity state.
531
+ profile_path: New path for the swbt-owned profile JSON.
532
+ local_address: Optional individual locally administered Bluetooth address.
533
+ ``None`` uses the adapter's current default address without rewriting it.
534
+ pair_timeout: Maximum seconds to wait for the initial pairing connection.
535
+ report_period_us: Optional periodic input report interval in microseconds.
536
+ controller_colors: Optional fixed controller body, button, and grip colors.
537
+ diagnostics: Optional diagnostics configuration for trace output.
538
+
539
+ Returns:
540
+ ProController: The paired controller. The caller owns its lifetime.
541
+
542
+ Raises:
543
+ ValueError: ``local_address`` is invalid.
544
+ FileExistsError: ``profile_path`` already exists.
545
+ Exception: Profile preparation or pairing failed. The created profile remains
546
+ available for a later retry.
547
+ """
548
+ return await cls._create_pairing_profile(
549
+ adapter=adapter,
550
+ profile_path=profile_path,
551
+ local_address=local_address,
552
+ pair_timeout=pair_timeout,
553
+ report_period_us=report_period_us,
554
+ controller_colors=controller_colors,
555
+ diagnostics=diagnostics,
556
+ )
557
+
400
558
 
401
559
  class JoyConL(_PeriodicRuntimeBackedGamepad):
402
560
  """Runtime-backed Joy-Con L-compatible gamepad."""
@@ -407,7 +565,7 @@ class JoyConL(_PeriodicRuntimeBackedGamepad):
407
565
  self,
408
566
  *,
409
567
  adapter: str | None = None,
410
- key_store_path: str | None = None,
568
+ profile_path: str | None = None,
411
569
  report_period_us: int | None = None,
412
570
  controller_colors: ControllerColors | None = None,
413
571
  diagnostics: DiagnosticsConfig | None = None,
@@ -416,7 +574,7 @@ class JoyConL(_PeriodicRuntimeBackedGamepad):
416
574
 
417
575
  Args:
418
576
  adapter: Bumble adapter moniker used for the Bluetooth backend.
419
- key_store_path: Optional path used by the Bluetooth backend to persist keys.
577
+ profile_path: Optional swbt-owned pairing profile path.
420
578
  report_period_us: Optional periodic input report interval in microseconds.
421
579
  controller_colors: Optional fixed controller body, button, and grip colors.
422
580
  diagnostics: Optional diagnostics configuration for trace output.
@@ -426,12 +584,56 @@ class JoyConL(_PeriodicRuntimeBackedGamepad):
426
584
  """
427
585
  config = self._controller_spec.build_config(
428
586
  adapter=adapter,
429
- key_store_path=key_store_path,
587
+ profile_path=profile_path,
430
588
  report_period_us=report_period_us,
431
589
  controller_colors=controller_colors,
432
590
  )
433
591
  self._init_from_config(config, diagnostics=diagnostics, transport=None)
434
592
 
593
+ @classmethod
594
+ async def create_profile(
595
+ cls,
596
+ *,
597
+ adapter: str,
598
+ profile_path: str,
599
+ local_address: str | None = None,
600
+ pair_timeout: float | None = None,
601
+ report_period_us: int | None = None,
602
+ controller_colors: ControllerColors | None = None,
603
+ diagnostics: DiagnosticsConfig | None = None,
604
+ ) -> Self:
605
+ """Create a new Joy-Con L pairing profile and pair it.
606
+
607
+ Args:
608
+ adapter: Bumble adapter moniker. An explicit local address may prepare
609
+ volatile adapter identity state.
610
+ profile_path: New path for the swbt-owned profile JSON.
611
+ local_address: Optional individual locally administered Bluetooth address.
612
+ ``None`` uses the adapter's current default address without rewriting it.
613
+ pair_timeout: Maximum seconds to wait for the initial pairing connection.
614
+ report_period_us: Optional periodic input report interval in microseconds.
615
+ controller_colors: Optional fixed controller body, button, and grip colors.
616
+ diagnostics: Optional diagnostics configuration for trace output.
617
+
618
+ Returns:
619
+ JoyConL: The paired controller. The caller owns its lifetime.
620
+
621
+ Raises:
622
+ ValueError: ``local_address`` is invalid.
623
+ FileExistsError: ``profile_path`` already exists.
624
+ Exception: Profile preparation or pairing failed. The created profile remains
625
+ available for a later retry.
626
+ """
627
+ return await cls._create_pairing_profile(
628
+ adapter=adapter,
629
+ profile_path=profile_path,
630
+ local_address=local_address,
631
+ pair_timeout=pair_timeout,
632
+ report_period_us=report_period_us,
633
+ controller_colors=controller_colors,
634
+ diagnostics=diagnostics,
635
+ )
636
+
435
637
 
436
638
  class JoyConR(_PeriodicRuntimeBackedGamepad):
437
639
  """Runtime-backed Joy-Con R-compatible gamepad."""
@@ -442,7 +644,7 @@ class JoyConR(_PeriodicRuntimeBackedGamepad):
442
644
  self,
443
645
  *,
444
646
  adapter: str | None = None,
445
- key_store_path: str | None = None,
647
+ profile_path: str | None = None,
446
648
  report_period_us: int | None = None,
447
649
  controller_colors: ControllerColors | None = None,
448
650
  diagnostics: DiagnosticsConfig | None = None,
@@ -451,7 +653,7 @@ class JoyConR(_PeriodicRuntimeBackedGamepad):
451
653
 
452
654
  Args:
453
655
  adapter: Bumble adapter moniker used for the Bluetooth backend.
454
- key_store_path: Optional path used by the Bluetooth backend to persist keys.
656
+ profile_path: Optional swbt-owned pairing profile path.
455
657
  report_period_us: Optional periodic input report interval in microseconds.
456
658
  controller_colors: Optional fixed controller body, button, and grip colors.
457
659
  diagnostics: Optional diagnostics configuration for trace output.
@@ -461,12 +663,56 @@ class JoyConR(_PeriodicRuntimeBackedGamepad):
461
663
  """
462
664
  config = self._controller_spec.build_config(
463
665
  adapter=adapter,
464
- key_store_path=key_store_path,
666
+ profile_path=profile_path,
465
667
  report_period_us=report_period_us,
466
668
  controller_colors=controller_colors,
467
669
  )
468
670
  self._init_from_config(config, diagnostics=diagnostics, transport=None)
469
671
 
672
+ @classmethod
673
+ async def create_profile(
674
+ cls,
675
+ *,
676
+ adapter: str,
677
+ profile_path: str,
678
+ local_address: str | None = None,
679
+ pair_timeout: float | None = None,
680
+ report_period_us: int | None = None,
681
+ controller_colors: ControllerColors | None = None,
682
+ diagnostics: DiagnosticsConfig | None = None,
683
+ ) -> Self:
684
+ """Create a new Joy-Con R pairing profile and pair it.
685
+
686
+ Args:
687
+ adapter: Bumble adapter moniker. An explicit local address may prepare
688
+ volatile adapter identity state.
689
+ profile_path: New path for the swbt-owned profile JSON.
690
+ local_address: Optional individual locally administered Bluetooth address.
691
+ ``None`` uses the adapter's current default address without rewriting it.
692
+ pair_timeout: Maximum seconds to wait for the initial pairing connection.
693
+ report_period_us: Optional periodic input report interval in microseconds.
694
+ controller_colors: Optional fixed controller body, button, and grip colors.
695
+ diagnostics: Optional diagnostics configuration for trace output.
696
+
697
+ Returns:
698
+ JoyConR: The paired controller. The caller owns its lifetime.
699
+
700
+ Raises:
701
+ ValueError: ``local_address`` is invalid.
702
+ FileExistsError: ``profile_path`` already exists.
703
+ Exception: Profile preparation or pairing failed. The created profile remains
704
+ available for a later retry.
705
+ """
706
+ return await cls._create_pairing_profile(
707
+ adapter=adapter,
708
+ profile_path=profile_path,
709
+ local_address=local_address,
710
+ pair_timeout=pair_timeout,
711
+ report_period_us=report_period_us,
712
+ controller_colors=controller_colors,
713
+ diagnostics=diagnostics,
714
+ )
715
+
470
716
 
471
717
  class DirectProController(_DirectRuntimeBackedGamepad):
472
718
  """Direct-reporting Pro Controller-compatible gamepad."""