RemoteRF 2.2.4__tar.gz → 2.3.0__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.
Files changed (96) hide show
  1. {remoterf-2.2.4/src/remoterf.egg-info → remoterf-2.3.0}/PKG-INFO +1 -1
  2. {remoterf-2.2.4 → remoterf-2.3.0}/pyproject.toml +1 -1
  3. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/app.py +144 -33
  4. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/dynamic_v2_transport.py +36 -10
  5. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/grpc_acc.py +8 -6
  6. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/grpc_client.py +252 -6
  7. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/deployment/backend.py +19 -0
  8. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/deployment/homes.py +1 -1
  9. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/deployment/http.py +2 -1
  10. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/dynamic_device.py +5 -3
  11. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/dynamic_v2.py +6 -3
  12. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/remoterf_cli.py +4 -4
  13. {remoterf-2.2.4 → remoterf-2.3.0/src/remoterf.egg-info}/PKG-INFO +1 -1
  14. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf.egg-info/SOURCES.txt +1 -0
  15. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_cli_login_fresh.py +19 -19
  16. remoterf-2.3.0/tests/test_device_routes.py +661 -0
  17. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_homes.py +3 -3
  18. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_native_devices.py +7 -0
  19. {remoterf-2.2.4 → remoterf-2.3.0}/LICENSE +0 -0
  20. {remoterf-2.2.4 → remoterf-2.3.0}/README.md +0 -0
  21. {remoterf-2.2.4 → remoterf-2.3.0}/setup.cfg +0 -0
  22. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/__init__.py +0 -0
  23. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/__init__.py +0 -0
  24. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/grpc/__init__.py +0 -0
  25. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/grpc/deployment_capabilities.proto +0 -0
  26. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/grpc/deployment_capabilities_pb2.py +0 -0
  27. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  28. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/grpc/grpc.proto +0 -0
  29. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/grpc/grpc_pb2.py +0 -0
  30. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/grpc/grpc_pb2_grpc.py +0 -0
  31. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/grpc/v2_codec.py +0 -0
  32. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/tos_notice.txt +0 -0
  33. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/utils/__init__.py +0 -0
  34. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/utils/ansi_codes.py +0 -0
  35. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/utils/api_token.py +0 -0
  36. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/utils/banner.py +0 -0
  37. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/utils/list_string.py +0 -0
  38. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/common/utils/process_arg.py +0 -0
  39. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/config/__init__.py +0 -0
  40. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/config/cert_fetcher.py +0 -0
  41. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/config/config.py +0 -0
  42. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/__init__.py +0 -0
  43. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/acc_login.py +0 -0
  44. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/banner_note.txt +0 -0
  45. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/direct_path.py +0 -0
  46. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/secure_channel.py +0 -0
  47. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/core/v2_errors.py +0 -0
  48. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/deployment/__init__.py +0 -0
  49. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/deployment/direct.py +0 -0
  50. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/deployment/state.py +0 -0
  51. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/__init__.py +0 -0
  52. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/_virtual.py +0 -0
  53. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/adalm_pluto/__init__.py +0 -0
  54. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
  55. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/hackrf/__init__.py +0 -0
  56. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/hackrf/hackrf_remote.py +0 -0
  57. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/rtl_sdr/__init__.py +0 -0
  58. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
  59. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/sim_sdr/__init__.py +0 -0
  60. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/sim_sdr/sim_sdr_remote.py +0 -0
  61. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/support/__init__.py +0 -0
  62. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/support/ti_mmwave.py +0 -0
  63. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/support/uhd.py +0 -0
  64. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/support/uhd_dsp.py +0 -0
  65. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/support/uhd_v2.py +0 -0
  66. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/ti_mmwave/__init__.py +0 -0
  67. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
  68. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/usrp/__init__.py +0 -0
  69. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/usrp/usrp_remote.py +0 -0
  70. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/drivers/usrp/usrp_remote.pyi +0 -0
  71. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf/version.py +0 -0
  72. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf.egg-info/dependency_links.txt +0 -0
  73. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf.egg-info/entry_points.txt +0 -0
  74. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf.egg-info/requires.txt +0 -0
  75. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf.egg-info/top_level.txt +0 -0
  76. {remoterf-2.2.4 → remoterf-2.3.0}/src/remoterf.py +0 -0
  77. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_banner.py +0 -0
  78. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_config_address.py +0 -0
  79. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_deployment_accounts.py +0 -0
  80. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_direct_path_fixed.py +0 -0
  81. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_direct_path_partner.py +0 -0
  82. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_direct_path_race.py +0 -0
  83. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_dynamic_device_codegen.py +0 -0
  84. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_dynamic_v2.py +0 -0
  85. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_federated_devices.py +0 -0
  86. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_fetch_idl_fallback.py +0 -0
  87. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_finish_prompt.py +0 -0
  88. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_gate_i_direct_path.py +0 -0
  89. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_grpc_client_target.py +0 -0
  90. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_native_account_credentials.py +0 -0
  91. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_partner_dynamic_v2.py +0 -0
  92. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_pluto_virtual.py +0 -0
  93. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_process_arg.py +0 -0
  94. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_remoterf_cli.py +0 -0
  95. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_ti_mmwave.py +0 -0
  96. {remoterf-2.2.4 → remoterf-2.3.0}/tests/test_upgrade_rescue.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.2.4
3
+ Version: 2.3.0
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
5
  Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoterf"
7
- version = "2.2.4"
7
+ version = "2.3.0"
8
8
  description = "A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts."
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -21,6 +21,7 @@ import getpass
21
21
  import os
22
22
  import re
23
23
  import datetime
24
+ import functools
24
25
  import time
25
26
  import ast
26
27
  from pathlib import Path
@@ -123,6 +124,73 @@ home_name = None
123
124
  # The route it was reached over (None on a bare direct profile or an HTTPS home).
124
125
  current_route = None
125
126
 
127
+ # What each generated driver package exposes, for the usage lines after a reservation.
128
+ DRIVER_CALLS = {"pluto": "adi.Pluto", "adalm_pluto": "adi.Pluto", "usrp": "uhd.usrp.MultiUSRP"}
129
+ _HANDLE_TAG = re.compile(r"^(?P<name>.*) \[(?P<handle>\S+)\]$") # handles have no whitespace
130
+ _NO_HANDLE = "This device has no handle"
131
+ _UNROUTABLE = "Its handle {} can't be used from here: {}"
132
+
133
+
134
+ def _token_lab():
135
+ """The lab `<token>@<lab>` routes to from here: this shell's home, else the
136
+ direct profile's (a shell started by --register has no home yet)."""
137
+ from .grpc_client import _active_home_name
138
+
139
+ return home_name or _active_home_name()
140
+
141
+
142
+ def _routable(token):
143
+ """The token as a driver takes it: @<this lab>, so it is never sent elsewhere."""
144
+ lab = _token_lab()
145
+ return f"{token}@{lab}" if lab else f"{token}"
146
+
147
+
148
+ def _name_and_handle(name):
149
+ """A listed device name, 'Pluto SDR [PlutoA]' -> ('Pluto SDR', 'PlutoA')."""
150
+ m = _HANDLE_TAG.match(name)
151
+ return (m["name"], m["handle"]) if m else (name, None)
152
+
153
+
154
+ def _print_reserved(where, when, token, handle, logged_in_to, install, no_handle=_NO_HANDLE):
155
+ """The token and, when the device has one, its handle (both @<deployment>).
156
+ Printed before the driver install, which can fail or stall; the usage
157
+ lines follow it, once the device type is known. `no_handle`: why no
158
+ handle is shown."""
159
+ printf("Reservation successful on ", (Sty.BOLD, Sty.GREEN), where, Sty.MAGENTA, " for ", Sty.DEFAULT, f"{when}.", Sty.CYAN)
160
+ printf("", Sty.DEFAULT)
161
+ width = len("Device handle") if handle else len("Token")
162
+ printf(f" {'Token':<{width}} ", Sty.BOLD, token, (Sty.BOLD, Sty.GREEN))
163
+ if handle:
164
+ printf(" Device handle ", Sty.BOLD, handle, (Sty.BOLD, Sty.GREEN))
165
+ printf("", Sty.DEFAULT)
166
+ if handle:
167
+ printf(" Token works anywhere, no login needed. Keep it safe: it can't be shown again.", Sty.DEFAULT)
168
+ printf(f" Handle works while you're logged in to {logged_in_to} and this reservation is active.", Sty.DEFAULT)
169
+ else:
170
+ printf(" Works anywhere, no login needed. Keep it safe: it can't be shown again.", Sty.DEFAULT)
171
+ printf(f" ({no_handle}. Lost the token? Cancel and reserve again.)", Sty.DEFAULT)
172
+ printf("", Sty.DEFAULT)
173
+ call = DRIVER_CALLS.get(install())
174
+ if call:
175
+ printf("Use either one as the token:" if handle else "Use it as the token:", Sty.DEFAULT)
176
+ for text in (token, handle) if handle else (token,):
177
+ printf(f' sdr = {call}(token="{text}")', Sty.CYAN)
178
+
179
+
180
+ def _print_native_reserved(device_id, label, when, token):
181
+ """`label` as the server lists the device: 'Pluto SDR [PlutoA] (online)',
182
+ or a partner lab's 'Pluto SDR @mit (online)' (partner handles: none yet)."""
183
+ partner = _PARTNER_LABEL.match(label)
184
+ local = None if partner else _LOCAL_LABEL.match(label)
185
+ name, handle = (partner["name"], None) if partner else _name_and_handle(local["name"] if local else label)
186
+ home = _token_lab()
187
+ lab = partner["lab"] if partner else home
188
+ name = handle or name or f"device {device_id}"
189
+ _print_reserved(f"{name} @{lab}" if lab else name, when, _routable(token),
190
+ f"{handle}@{home}" if handle and home else None, home,
191
+ lambda: account.install_reserved_driver(token, int(device_id)),
192
+ _UNROUTABLE.format(handle, "this lab has no saved name") if handle else _NO_HANDLE)
193
+
126
194
 
127
195
  def _print_connected(name, home, route):
128
196
  """One line, same voice as the banner: what we reached and how."""
@@ -258,7 +326,7 @@ def _account_command(command):
258
326
  from ..deployment import homes
259
327
  typed = pending_username or _ask('Username or Email' if account.is_https_home else 'Username')
260
328
  pending_username = None
261
- # `ethan@ucla` typed at ucla is `ethan` there.
329
+ # `alice@ucla` typed at ucla is `alice` there.
262
330
  name, where = homes.split_account(typed)
263
331
  here = homes.GLOBAL_ACCOUNTS if account.is_https_home else home_name
264
332
  account.username = name if where == here else typed
@@ -450,6 +518,10 @@ def _render_destinations(entries):
450
518
  print()
451
519
 
452
520
 
521
+ def _handle_tag(dev):
522
+ return f" [{dev['handle']}]" if dev.get("handle") else ""
523
+
524
+
453
525
  def _federated_devices():
454
526
  data = account.get_devices()
455
527
  _render_destinations(data["destinations"])
@@ -466,7 +538,7 @@ def _federated_devices():
466
538
  deployment = dev["deployment_name"]
467
539
  printf(f" @{deployment}", (Sty.BOLD, Sty.MAGENTA))
468
540
  state = "online" if dev["online"] else "offline"
469
- printf(" Device Name: ", Sty.GRAY, f"{dev['display_name']}", Sty.DEFAULT, f" ({dev['device_type'] or 'unknown'}, {state})", Sty.GRAY)
541
+ printf(" Device Name: ", Sty.GRAY, f"{dev['display_name']}{_handle_tag(dev)}", Sty.DEFAULT, f" ({dev['device_type'] or 'unknown'}, {state})", Sty.GRAY)
470
542
  if data.get("incomplete"):
471
543
  printf(f"{data['omitted_count']} more deployment(s) were not queried.", Sty.WARNING)
472
544
  return data
@@ -546,12 +618,38 @@ def _federated_reservations_for_range(start_day, end_day):
546
618
  return res_dict
547
619
 
548
620
 
549
- def _federated_reserve_slot(device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str):
550
- handle = account.reserve_device(device_id, chosen_slot[0], chosen_slot[1])
551
- # Worded as a lab account's reservation; the handle goes where a token goes.
552
- printf("Reservation successful on device ", (Sty.BOLD, Sty.GREEN), f"{str(device_id).rsplit(':', 1)[-1]}", Sty.MAGENTA, " for ", Sty.DEFAULT, f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.", Sty.CYAN)
553
- printf("Your Token -> ", Sty.BOLD, f"{handle}", (Sty.BOLD, Sty.GREEN))
554
- account.install_reserved_driver(handle, device_id)
621
+ def _global_slug(device_id, deployment_name):
622
+ """The lab's name when it is its own Global slug (checked by discovery),
623
+ else None: then the bare token, which works as is."""
624
+ from .grpc_client import _deployment_id
625
+ from ..deployment.homes import NAME_RE
626
+
627
+ name = str(deployment_name or "").lower()
628
+ try:
629
+ if NAME_RE.match(name) and _deployment_id(name) == str(device_id).split(":")[0]:
630
+ return name
631
+ except RuntimeError:
632
+ pass
633
+ return None
634
+
635
+
636
+ def _federated_reserve_slot(device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str,
637
+ deployment_name=None, device_name=None, device_handle=None):
638
+ token = account.reserve_device(device_id, chosen_slot[0], chosen_slot[1])
639
+ when = f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}"
640
+ install = lambda: account.install_reserved_driver(token, device_id)
641
+ if not token.startswith("rfg_"):
642
+ # An older Global: the HOME reference goes where a token goes.
643
+ printf("Reservation successful on device ", (Sty.BOLD, Sty.GREEN), f"{str(device_id).rsplit(':', 1)[-1]}", Sty.MAGENTA, " for ", Sty.DEFAULT, f"{when}.", Sty.CYAN)
644
+ printf("Your Token -> ", Sty.BOLD, token, (Sty.BOLD, Sty.GREEN))
645
+ install()
646
+ return
647
+ slug = _global_slug(device_id, deployment_name)
648
+ name = device_handle or device_name or str(device_id).rsplit(":", 1)[-1]
649
+ lab = slug or deployment_name
650
+ _print_reserved(f"{name} @{lab}" if lab else name, when, f"{token}@{slug}" if slug else token,
651
+ f"{device_handle}@{slug}" if device_handle and slug else None, "RemoteRF Global", install,
652
+ _UNROUTABLE.format(device_handle, "this lab's Global name is unknown") if device_handle else _NO_HANDLE)
555
653
 
556
654
 
557
655
  def _federated_reserve():
@@ -572,7 +670,7 @@ def _federated_reserve():
572
670
  printf(
573
671
  f"{idx}.", Sty.CYAN,
574
672
  " Name: ", Sty.DEFAULT,
575
- f"{dev['display_name']}", Sty.GRAY,
673
+ f"{dev['display_name']}{_handle_tag(dev)}", Sty.GRAY,
576
674
  " Duration: ", Sty.DEFAULT,
577
675
  f"{dev['block_min']} min", Sty.GREEN,
578
676
  )
@@ -594,7 +692,9 @@ def _federated_reserve():
594
692
  f" is < 10 minutes (max_sec={dev['max_reservation_time_sec'] or 0}). Cannot create valid reservations.", Sty.DEFAULT,
595
693
  )
596
694
  return
597
- _pick_slot_and_reserve(dev["device_id"], dev["display_name"], dev["block_min"], _federated_reservations_for_range, _federated_reserve_slot,
695
+ reserve_slot = functools.partial(_federated_reserve_slot, deployment_name=dev["deployment_name"],
696
+ device_name=dev["display_name"], device_handle=dev.get("handle"))
697
+ _pick_slot_and_reserve(dev["device_id"], dev["display_name"], dev["block_min"], _federated_reservations_for_range, reserve_slot,
598
698
  device_label=f"{dev['display_name']} @{dev['deployment_name']}",
599
699
  max_days=-(-int(dev.get("max_horizon_sec") or 0) // 86400) or None)
600
700
 
@@ -747,7 +847,8 @@ def cancel_my_reservation():
747
847
  print("Aborting. A non integer key was given.")
748
848
 
749
849
  _PARTNER_LABEL = re.compile(r"^(?P<name>.*) @(?P<lab>.+?) \((?P<state>[^)]*)\)$")
750
- _LOCAL_LABEL = re.compile(r"^(?P<name>.*) \((?P<state>[^)]*)\)$")
850
+ # The state always starts with online/offline; a PoE fault after it can hold brackets.
851
+ _LOCAL_LABEL = re.compile(r"^(?P<name>.*?) \((?P<state>(?:online|offline)\b.*)\)$")
751
852
 
752
853
 
753
854
  def _native_device_rows():
@@ -808,7 +909,7 @@ def _print_native_devices(rows, *, numbered=False):
808
909
  if row["lab"] != lab:
809
910
  lab = row["lab"]
810
911
  printf(f" @{lab}", (Sty.BOLD, Sty.MAGENTA))
811
- prefix = (f"{index}.", Sty.CYAN, " ") if numbered else ()
912
+ prefix = (f"{index}. ", Sty.CYAN) if numbered else ()
812
913
  printf(*prefix, " Device Name: " if not numbered else " Device Name: ", Sty.GRAY,
813
914
  row["name"], Sty.DEFAULT, f" ({row['state']})" if row["state"] else "", Sty.GRAY)
814
915
 
@@ -852,11 +953,18 @@ def reserve():
852
953
  if not pick.isdigit() or not 1 <= int(pick) <= len(rows):
853
954
  printf("Pick a number from the list.", Sty.WARNING)
854
955
  return
855
- token = account.reserve_device(rows[int(pick) - 1]["id"], get_datetime("Reserve Start Time"), get_datetime("Reserve End Time"))
956
+ row = rows[int(pick) - 1]
957
+ start, end = get_datetime("Reserve Start Time"), get_datetime("Reserve End Time")
958
+ token = account.reserve_device(row["id"], start, end)
856
959
  if token != '':
857
- printf(f"Reservation successful. Your Token -> ", Sty.BOLD, f"{token}", Sty.BG_GREEN)
858
- printf(f"Please keep this token safe, as it is not saved on the server and cannot be retrieved again. If you lose it, cancel your reservation and make a new one. ", Sty.DEFAULT)
859
- account.install_reserved_driver(token, rows[int(pick) - 1]["id"])
960
+ name, handle = (row["name"], None) if row["away"] else _name_and_handle(row["name"])
961
+ home = _token_lab()
962
+ lab = row["lab"] if row["away"] else home or row["lab"]
963
+ _print_reserved(f"{handle or name} @{lab}" if lab else handle or name,
964
+ f"{start:%Y-%m-%d %I:%M %p}-{end:%I:%M %p}", _routable(token),
965
+ f"{handle}@{home}" if handle and home else None, home,
966
+ lambda: account.install_reserved_driver(token, row["id"]),
967
+ _UNROUTABLE.format(handle, "this lab has no saved name") if handle else _NO_HANDLE)
860
968
  except Exception as e:
861
969
  printf(f"Error: {e}", Sty.BRIGHT_RED)
862
970
 
@@ -1293,11 +1401,9 @@ def interactive_reserve_next_days(block_minutes=60):
1293
1401
  # --- 6) Reserve the chosen slot on the chosen device ---
1294
1402
  token = account.reserve_device(int(chosen_device_id), chosen_slot[0], chosen_slot[1])
1295
1403
  if token:
1296
- printf("Reservation successful on device ", (Sty.BOLD, Sty.GREEN), f"{chosen_device_id}", Sty.MAGENTA, " for ", Sty.DEFAULT, f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.", Sty.CYAN)
1297
- printf("Your Token -> ", Sty.BOLD, f"{token}", (Sty.BOLD, Sty.GREEN))
1298
- printf("Please keep this token safe, as it is not saved on the server and cannot be retrieved again. If you lose it, cancel your reservation and make a new one.", Sty.DEFAULT)
1299
- account.install_reserved_driver(token, int(chosen_device_id))
1300
-
1404
+ _print_native_reserved(chosen_device_id, str(unmap_arg(data.results[chosen_device_id])),
1405
+ f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}", token)
1406
+
1301
1407
  except Exception as e:
1302
1408
  print(f"Error: {e}")
1303
1409
 
@@ -1413,7 +1519,7 @@ def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes,
1413
1519
  slot_end_str = chosen_slot[1].strftime("%I:%M %p")
1414
1520
 
1415
1521
  confirmation = session.prompt(stylize(
1416
- "Reservation successful on ", Sty.DEFAULT,
1522
+ "Reserve ", Sty.DEFAULT,
1417
1523
  f"{chosen_day.strftime('%Y-%m-%d')}", (Sty.BOLD, Sty.BLUE),
1418
1524
  " from ", Sty.DEFAULT,
1419
1525
  f"{slot_start_str}", Sty.CYAN,
@@ -1421,7 +1527,7 @@ def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes,
1421
1527
  f"{slot_end_str}", Sty.CYAN,
1422
1528
  " for ", Sty.DEFAULT,
1423
1529
  f"{chosen_device_name}", Sty.GRAY,
1424
- ". \nConfirm reservation? (y/n): ", (Sty.BOLD, Sty.GREEN),
1530
+ "?\nConfirm reservation? (y/n): ", (Sty.BOLD, Sty.GREEN),
1425
1531
  )).strip().lower()
1426
1532
 
1427
1533
  if confirmation != "y":
@@ -1434,13 +1540,11 @@ def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes,
1434
1540
  do_reserve(chosen_device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str)
1435
1541
 
1436
1542
 
1437
- def _native_reserve_slot(chosen_device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str):
1543
+ def _native_reserve_slot(chosen_device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str, device_label=""):
1438
1544
  token = account.reserve_device(int(chosen_device_id), chosen_slot[0], chosen_slot[1])
1439
1545
  if token:
1440
- printf("Reservation successful on device ", (Sty.BOLD, Sty.GREEN), f"{chosen_device_id}", Sty.MAGENTA, " for ", Sty.DEFAULT, f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.", Sty.CYAN)
1441
- printf("Your Token -> ", Sty.BOLD, f"{token}", (Sty.BOLD, Sty.GREEN))
1442
- printf("Please keep this token safe, as it is not saved on the server and cannot be retrieved again. If you lose it, cancel your reservation and make a new one.", Sty.DEFAULT)
1443
- account.install_reserved_driver(token, int(chosen_device_id))
1546
+ _print_native_reserved(chosen_device_id, device_label,
1547
+ f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}", token)
1444
1548
 
1445
1549
 
1446
1550
  def interactive_reserve_next_days_auto():
@@ -1578,19 +1682,23 @@ def interactive_reserve_next_days_auto():
1578
1682
 
1579
1683
  chosen_device_key = sorted_device_ids[sel_i]
1580
1684
  chosen_device_id = str(chosen_device_key)
1581
- chosen_device_name = unmap_arg(dev_resp.results[chosen_device_key])
1685
+ chosen_device_name = str(unmap_arg(dev_resp.results[chosen_device_key]))
1686
+ # 'Pluto SDR [PlutoA] (online)' -> 'Pluto SDR [PlutoA]', 'Pluto SDR @mit (online)' -> 'Pluto SDR @mit'
1687
+ shown = _LOCAL_LABEL.match(chosen_device_name)
1688
+ shown = shown["name"] if shown else chosen_device_name
1582
1689
  block_minutes = int(block_by_dev.get(chosen_device_id, 30))
1583
1690
 
1584
1691
  if block_minutes < 10:
1585
1692
  printf(
1586
1693
  "Your max reservation duration for device ", Sty.DEFAULT,
1587
- f"{chosen_device_id}", Sty.MAGENTA,
1694
+ shown, Sty.MAGENTA,
1588
1695
  f" is < 10 minutes (max_sec={max_by_dev.get(chosen_device_id, 0)}). Cannot create valid reservations.", Sty.DEFAULT,
1589
1696
  )
1590
1697
  return
1591
1698
 
1592
- _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes, fetch_reservations_for_range, _native_reserve_slot,
1593
- max_days=days_by_dev.get(chosen_device_id) or None)
1699
+ _pick_slot_and_reserve(chosen_device_id, shown, block_minutes, fetch_reservations_for_range,
1700
+ functools.partial(_native_reserve_slot, device_label=chosen_device_name),
1701
+ device_label=shown, max_days=days_by_dev.get(chosen_device_id) or None)
1594
1702
 
1595
1703
  except Exception as e:
1596
1704
  print(f"Error: {e}")
@@ -1628,9 +1736,12 @@ def run(backend=None, *, register=False, enrollment_code=None, server_label=None
1628
1736
  # Continue below the banner rather than wiping the screen: with a
1629
1737
  # stored login the wipe came a split second after everything appeared.
1630
1738
  session_line()
1739
+ from ..deployment import homes
1740
+ # The prompt is the account's full name: alice@ucla, alice@global.
1741
+ where = homes.GLOBAL_ACCOUNTS if account.is_https_home else (home_name or 'remoterf')
1631
1742
  while True:
1632
1743
  try:
1633
- inpu = session.prompt(stylize(f'{account.username}@remoterf: ', Sty.BOLD))
1744
+ inpu = session.prompt(stylize(f'{account.username}@{where}: ', Sty.BOLD))
1634
1745
  if inpu in ('register', 'verify', 'login', 'forgot-password', 'reset-password'):
1635
1746
  authenticated = _account_command(inpu)
1636
1747
  if inpu == 'reset-password' and not welcome(show_banner=False):
@@ -82,8 +82,9 @@ class _DevicePlaneStub:
82
82
  call on that token (the direct path to the owner when it is up, else the
83
83
  HOME, which relays it). The owner runs it through its own servicers."""
84
84
 
85
- def __init__(self, token: str):
85
+ def __init__(self, token: str, label: str = "partner device: "):
86
86
  self._token = str(token)
87
+ self._label = label
87
88
 
88
89
  def _rpc(self, operation, request, response_type):
89
90
  from ..common.utils import map_arg, unmap_arg
@@ -97,7 +98,7 @@ class _DevicePlaneStub:
97
98
  except RuntimeError as exc:
98
99
  # A refusal the HOME or the owner carried in "a": no live
99
100
  # reservation, a revoked agreement, a device no longer visible.
100
- raise RemoteRFReservationError(f"partner device: {exc}") from exc
101
+ raise RemoteRFReservationError(f"{self._label}{exc}") from exc
101
102
  wire = unmap_arg(response.results["wire"]) if "wire" in response.results else None
102
103
  if not isinstance(wire, bytes):
103
104
  raise RemoteRFProtocolError("partner device plane returned no Dynamic v2 reply")
@@ -131,6 +132,7 @@ class DynamicV2Transport:
131
132
  *,
132
133
  control_stub=None,
133
134
  sample_stub=None,
135
+ token=None,
134
136
  control_timeout_sec: float = 30.0,
135
137
  open_timeout_sec: float = 120.0,
136
138
  stream_timeout_margin_sec: float = 5.0,
@@ -156,11 +158,18 @@ class DynamicV2Transport:
156
158
  )
157
159
  self._stream_poisoned = False
158
160
  self._stream_failure_details = {}
161
+ self._renewed = False
159
162
  if control_stub is None or sample_stub is None:
160
- from .grpc_client import get_active_channel
161
- channel = get_active_channel()
162
- control_stub = control_stub or grpc_pb2_grpc.DynamicControlV2Stub(channel)
163
- sample_stub = sample_stub or grpc_pb2_grpc.SampleDataV1Stub(channel)
163
+ from .grpc_client import device_route, get_active_channel
164
+ route = device_route(token) if token is not None else None
165
+ if route is not None and route.kind == "global":
166
+ # No device channel through Global: each request is a relayed RPC.
167
+ plane = _DevicePlaneStub(token, label="")
168
+ control_stub, sample_stub = control_stub or plane, sample_stub or plane
169
+ else:
170
+ channel = route.connection.channel if route and route.connection else get_active_channel()
171
+ control_stub = control_stub or grpc_pb2_grpc.DynamicControlV2Stub(channel)
172
+ sample_stub = sample_stub or grpc_pb2_grpc.SampleDataV1Stub(channel)
164
173
  self.control = control_stub
165
174
  self.samples = sample_stub
166
175
 
@@ -168,17 +177,26 @@ class DynamicV2Transport:
168
177
  """Carry this transport over the device plane when ``token`` drives a
169
178
  partner lab's device (named by the HOME's refusal, or already known)."""
170
179
  from . import direct_path
180
+ from .grpc_client import wire_token
171
181
 
172
182
  if isinstance(self.control, _DevicePlaneStub):
173
183
  return False
174
- owner = owner or direct_path.partner_of(str(token))
184
+ owner = owner or direct_path.partner_of(wire_token(str(token)))
175
185
  if not owner:
176
186
  return False
177
187
  # Marked, the token's device calls take the direct path to the owner.
178
- direct_path.mark_partner(str(token), str(owner))
188
+ direct_path.mark_partner(wire_token(str(token)), str(owner))
179
189
  self.control = self.samples = _DevicePlaneStub(token)
180
190
  return True
181
191
 
192
+ def _inner_token(self, token: str) -> str:
193
+ # Over the device plane the owner substitutes its own bridge token, so
194
+ # no bearer secret ever rides inside a frame.
195
+ if isinstance(self.control, _DevicePlaneStub):
196
+ return ""
197
+ from .grpc_client import wire_token
198
+ return wire_token(str(token))
199
+
182
200
  def _checked(self, response, token: str) -> bool:
183
201
  """raise_for_envelope, except for the refusal of a partner's token:
184
202
  True when the call should be repeated over the device plane."""
@@ -188,6 +206,14 @@ class DynamicV2Transport:
188
206
  if self._to_owner(token, exc.details.get("federated")):
189
207
  return True
190
208
  raise
209
+ except RemoteRFReservationError as exc:
210
+ # A dead bridge token behind x@lab: ask the lab again, once.
211
+ from .grpc_client import device_route, renewed_route
212
+
213
+ if self._renewed or renewed_route(str(token), device_route(str(token)), str(exc)) is None:
214
+ raise
215
+ self._renewed = True
216
+ return True
191
217
  return False
192
218
 
193
219
  def _call(self, fn, request, *, timeout: float | None = None):
@@ -229,7 +255,7 @@ class DynamicV2Transport:
229
255
  response = self._call(
230
256
  self.control.GetSchema,
231
257
  grpc_pb2.GetSchemaRequest(
232
- token=str(token),
258
+ token=self._inner_token(token),
233
259
  schema_versions=[SCHEMA_VERSION],
234
260
  ),
235
261
  )
@@ -254,7 +280,7 @@ class DynamicV2Transport:
254
280
  response = self._call(
255
281
  self.control.OpenSession,
256
282
  grpc_pb2.OpenSessionRequest(
257
- token=str(token),
283
+ token=self._inner_token(token),
258
284
  requested_schema_hash=str(schema_hash),
259
285
  control_protocol_version=CONTROL_PROTOCOL_VERSION,
260
286
  streaming_protocol_version=STREAMING_PROTOCOL_VERSION,
@@ -101,8 +101,9 @@ class RemoteRFAccount:
101
101
  if self.is_https_home:
102
102
  with _busy("Reserving…"):
103
103
  reservation = self.backend.reserve(device_id, int(start_time.timestamp()), int(end_time.timestamp()))
104
- # The HOME reference is the driver handle: adi.Pluto("<deployment>:<id>")
105
- return reservation['device_id']
104
+ # Global's device API token (rfg_...), else (an older Global) the
105
+ # HOME reference, which also drives: adi.Pluto("<deployment>:<id>")
106
+ return reservation.get('token') or reservation['device_id']
106
107
  username, credential_secret = self._rpc_credentials()
107
108
  with _busy("Reserving…"):
108
109
  response = self._call(function_name="ACC:reserve_device", args={"un":map_arg(username), "pw":map_arg(credential_secret), "dd":map_arg(device_id), "st":map_arg(int(start_time.timestamp())), "et":map_arg(int(end_time.timestamp()))})
@@ -114,13 +115,14 @@ class RemoteRFAccount:
114
115
 
115
116
  def install_reserved_driver(self, handle, device_id):
116
117
  """Separate from reserve_device: the token is shown before this network
117
- step runs, so a failed or stuck driver fetch cannot lose it."""
118
+ step runs, so a failed or stuck driver fetch cannot lose it. Returns
119
+ the installed device type, or None."""
118
120
  try:
119
121
  from ..drivers.dynamic_device import install_driver
120
122
  if self.is_https_home:
121
- install_driver(device_id=handle)
122
- else:
123
- install_driver(token=handle, device_id=device_id)
123
+ # By the ref, which needs no live slot (an rfg_ token does).
124
+ return install_driver(device_id=device_id).name
125
+ return install_driver(token=handle, device_id=device_id).name
124
126
  except Exception as e:
125
127
  print(f"Warning: could not install driver for device {device_id}: {e}")
126
128