RemoteRF 2.2.5__tar.gz → 2.3.1__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.5/src/remoterf.egg-info → remoterf-2.3.1}/PKG-INFO +1 -1
  2. {remoterf-2.2.5 → remoterf-2.3.1}/pyproject.toml +1 -1
  3. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/app.py +149 -32
  4. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/dynamic_v2_transport.py +36 -10
  5. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/grpc_acc.py +8 -6
  6. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/grpc_client.py +260 -6
  7. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/deployment/backend.py +19 -0
  8. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/deployment/http.py +2 -1
  9. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/dynamic_device.py +5 -3
  10. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/dynamic_v2.py +6 -3
  11. {remoterf-2.2.5 → remoterf-2.3.1/src/remoterf.egg-info}/PKG-INFO +1 -1
  12. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf.egg-info/SOURCES.txt +1 -0
  13. remoterf-2.3.1/tests/test_device_routes.py +671 -0
  14. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_native_devices.py +7 -0
  15. {remoterf-2.2.5 → remoterf-2.3.1}/LICENSE +0 -0
  16. {remoterf-2.2.5 → remoterf-2.3.1}/README.md +0 -0
  17. {remoterf-2.2.5 → remoterf-2.3.1}/setup.cfg +0 -0
  18. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/__init__.py +0 -0
  19. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/__init__.py +0 -0
  20. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/grpc/__init__.py +0 -0
  21. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/grpc/deployment_capabilities.proto +0 -0
  22. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/grpc/deployment_capabilities_pb2.py +0 -0
  23. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  24. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/grpc/grpc.proto +0 -0
  25. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/grpc/grpc_pb2.py +0 -0
  26. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/grpc/grpc_pb2_grpc.py +0 -0
  27. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/grpc/v2_codec.py +0 -0
  28. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/tos_notice.txt +0 -0
  29. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/utils/__init__.py +0 -0
  30. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/utils/ansi_codes.py +0 -0
  31. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/utils/api_token.py +0 -0
  32. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/utils/banner.py +0 -0
  33. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/utils/list_string.py +0 -0
  34. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/common/utils/process_arg.py +0 -0
  35. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/config/__init__.py +0 -0
  36. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/config/cert_fetcher.py +0 -0
  37. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/config/config.py +0 -0
  38. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/__init__.py +0 -0
  39. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/acc_login.py +0 -0
  40. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/banner_note.txt +0 -0
  41. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/direct_path.py +0 -0
  42. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/secure_channel.py +0 -0
  43. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/core/v2_errors.py +0 -0
  44. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/deployment/__init__.py +0 -0
  45. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/deployment/direct.py +0 -0
  46. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/deployment/homes.py +0 -0
  47. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/deployment/state.py +0 -0
  48. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/__init__.py +0 -0
  49. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/_virtual.py +0 -0
  50. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/adalm_pluto/__init__.py +0 -0
  51. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
  52. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/hackrf/__init__.py +0 -0
  53. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/hackrf/hackrf_remote.py +0 -0
  54. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/rtl_sdr/__init__.py +0 -0
  55. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
  56. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/sim_sdr/__init__.py +0 -0
  57. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/sim_sdr/sim_sdr_remote.py +0 -0
  58. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/support/__init__.py +0 -0
  59. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/support/ti_mmwave.py +0 -0
  60. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/support/uhd.py +0 -0
  61. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/support/uhd_dsp.py +0 -0
  62. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/support/uhd_v2.py +0 -0
  63. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/ti_mmwave/__init__.py +0 -0
  64. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
  65. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/usrp/__init__.py +0 -0
  66. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/usrp/usrp_remote.py +0 -0
  67. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/drivers/usrp/usrp_remote.pyi +0 -0
  68. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/remoterf_cli.py +0 -0
  69. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf/version.py +0 -0
  70. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf.egg-info/dependency_links.txt +0 -0
  71. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf.egg-info/entry_points.txt +0 -0
  72. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf.egg-info/requires.txt +0 -0
  73. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf.egg-info/top_level.txt +0 -0
  74. {remoterf-2.2.5 → remoterf-2.3.1}/src/remoterf.py +0 -0
  75. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_banner.py +0 -0
  76. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_cli_login_fresh.py +0 -0
  77. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_config_address.py +0 -0
  78. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_deployment_accounts.py +0 -0
  79. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_direct_path_fixed.py +0 -0
  80. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_direct_path_partner.py +0 -0
  81. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_direct_path_race.py +0 -0
  82. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_dynamic_device_codegen.py +0 -0
  83. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_dynamic_v2.py +0 -0
  84. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_federated_devices.py +0 -0
  85. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_fetch_idl_fallback.py +0 -0
  86. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_finish_prompt.py +0 -0
  87. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_gate_i_direct_path.py +0 -0
  88. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_grpc_client_target.py +0 -0
  89. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_homes.py +0 -0
  90. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_native_account_credentials.py +0 -0
  91. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_partner_dynamic_v2.py +0 -0
  92. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_pluto_virtual.py +0 -0
  93. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_process_arg.py +0 -0
  94. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_remoterf_cli.py +0 -0
  95. {remoterf-2.2.5 → remoterf-2.3.1}/tests/test_ti_mmwave.py +0 -0
  96. {remoterf-2.2.5 → remoterf-2.3.1}/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.5
3
+ Version: 2.3.1
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.5"
7
+ version = "2.3.1"
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"
@@ -13,7 +13,7 @@
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
15
 
16
- from remoterf.core.grpc_client import addr as server_addr, handle_admin_command
16
+ from remoterf.core.grpc_client import addr as server_addr, handle_admin_command, _key
17
17
  from . import *
18
18
  from ..common.utils import *
19
19
 
@@ -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,80 @@ 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 -> (name, handle). The handle is the name keyed,
150
+ 'Pluto SDR' -> 'Pluto_SDR' (None when the lab can't address it), unless a
151
+ 1.2.0 lab tags it: 'Pluto SDR [PlutoA]'."""
152
+ m = _HANDLE_TAG.match(name)
153
+ if m:
154
+ return m["name"], m["handle"]
155
+ h = _key(name)
156
+ # The lab's is_device_handle (core 0.19.0; this client pins 0.16.0).
157
+ ok = h and not h.startswith(("rfg_", "bridge-")) and not any(c in "@:" or c.isspace() or not c.isprintable() for c in h)
158
+ return name, h if ok else None
159
+
160
+
161
+ def _print_reserved(where, when, token, handle, logged_in_to, install, no_handle=_NO_HANDLE):
162
+ """The token and, when the device has one, its handle (both @<deployment>).
163
+ Printed before the driver install, which can fail or stall; the usage
164
+ lines follow it, once the device type is known. `no_handle`: why no
165
+ handle is shown."""
166
+ printf("Reservation successful on ", (Sty.BOLD, Sty.GREEN), where, Sty.MAGENTA, " for ", Sty.DEFAULT, f"{when}.", Sty.CYAN)
167
+ printf("", Sty.DEFAULT)
168
+ width = len("Device handle") if handle else len("Token")
169
+ printf(f" {'Token':<{width}} ", Sty.BOLD, token, (Sty.BOLD, Sty.GREEN))
170
+ if handle:
171
+ printf(" Device handle ", Sty.BOLD, handle, (Sty.BOLD, Sty.GREEN))
172
+ printf("", Sty.DEFAULT)
173
+ if handle:
174
+ printf(" Token works anywhere, no login needed. Keep it safe: it can't be shown again.", Sty.DEFAULT)
175
+ printf(f" Handle works while you're logged in to {logged_in_to} and this reservation is active.", Sty.DEFAULT)
176
+ else:
177
+ printf(" Works anywhere, no login needed. Keep it safe: it can't be shown again.", Sty.DEFAULT)
178
+ printf(f" ({no_handle}. Lost the token? Cancel and reserve again.)", Sty.DEFAULT)
179
+ printf("", Sty.DEFAULT)
180
+ call = DRIVER_CALLS.get(install())
181
+ if call:
182
+ printf("Use either one as the token:" if handle else "Use it as the token:", Sty.DEFAULT)
183
+ for text in (token, handle) if handle else (token,):
184
+ printf(f' sdr = {call}(token="{text}")', Sty.CYAN)
185
+
186
+
187
+ def _print_native_reserved(device_id, label, when, token):
188
+ """`label` as the server lists the device: 'Pluto SDR [PlutoA] (online)',
189
+ or a partner lab's 'Pluto SDR @mit (online)' (partner handles: none yet)."""
190
+ partner = _PARTNER_LABEL.match(label)
191
+ local = None if partner else _LOCAL_LABEL.match(label)
192
+ name, handle = (partner["name"], None) if partner else _name_and_handle(local["name"] if local else label)
193
+ home = _token_lab()
194
+ lab = partner["lab"] if partner else home
195
+ name = handle or name or f"device {device_id}"
196
+ _print_reserved(f"{name} @{lab}" if lab else name, when, _routable(token),
197
+ f"{handle}@{home}" if handle and home else None, home,
198
+ lambda: account.install_reserved_driver(token, int(device_id)),
199
+ _UNROUTABLE.format(handle, "this lab has no saved name") if handle else _NO_HANDLE)
200
+
126
201
 
127
202
  def _print_connected(name, home, route):
128
203
  """One line, same voice as the banner: what we reached and how."""
@@ -450,6 +525,12 @@ def _render_destinations(entries):
450
525
  print()
451
526
 
452
527
 
528
+ def _handle_tag(dev):
529
+ """' [handle]' only when it isn't just the name."""
530
+ h = dev.get("handle")
531
+ return f" [{h}]" if h and _key(dev.get("display_name") or "") != _key(h) else ""
532
+
533
+
453
534
  def _federated_devices():
454
535
  data = account.get_devices()
455
536
  _render_destinations(data["destinations"])
@@ -466,7 +547,7 @@ def _federated_devices():
466
547
  deployment = dev["deployment_name"]
467
548
  printf(f" @{deployment}", (Sty.BOLD, Sty.MAGENTA))
468
549
  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)
550
+ printf(" Device Name: ", Sty.GRAY, f"{dev['display_name']}{_handle_tag(dev)}", Sty.DEFAULT, f" ({dev['device_type'] or 'unknown'}, {state})", Sty.GRAY)
470
551
  if data.get("incomplete"):
471
552
  printf(f"{data['omitted_count']} more deployment(s) were not queried.", Sty.WARNING)
472
553
  return data
@@ -546,12 +627,38 @@ def _federated_reservations_for_range(start_day, end_day):
546
627
  return res_dict
547
628
 
548
629
 
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)
630
+ def _global_slug(device_id, deployment_name):
631
+ """The lab's name when it is its own Global slug (checked by discovery),
632
+ else None: then the bare token, which works as is."""
633
+ from .grpc_client import _deployment_id
634
+ from ..deployment.homes import NAME_RE
635
+
636
+ name = str(deployment_name or "").lower()
637
+ try:
638
+ if NAME_RE.match(name) and _deployment_id(name) == str(device_id).split(":")[0]:
639
+ return name
640
+ except RuntimeError:
641
+ pass
642
+ return None
643
+
644
+
645
+ def _federated_reserve_slot(device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str,
646
+ deployment_name=None, device_name=None, device_handle=None):
647
+ token = account.reserve_device(device_id, chosen_slot[0], chosen_slot[1])
648
+ when = f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}"
649
+ install = lambda: account.install_reserved_driver(token, device_id)
650
+ if not token.startswith("rfg_"):
651
+ # An older Global: the HOME reference goes where a token goes.
652
+ 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)
653
+ printf("Your Token -> ", Sty.BOLD, token, (Sty.BOLD, Sty.GREEN))
654
+ install()
655
+ return
656
+ slug = _global_slug(device_id, deployment_name)
657
+ name = device_handle or device_name or str(device_id).rsplit(":", 1)[-1]
658
+ lab = slug or deployment_name
659
+ _print_reserved(f"{name} @{lab}" if lab else name, when, f"{token}@{slug}" if slug else token,
660
+ f"{device_handle}@{slug}" if device_handle and slug else None, "RemoteRF Global", install,
661
+ _UNROUTABLE.format(device_handle, "this lab's Global name is unknown") if device_handle else _NO_HANDLE)
555
662
 
556
663
 
557
664
  def _federated_reserve():
@@ -572,7 +679,7 @@ def _federated_reserve():
572
679
  printf(
573
680
  f"{idx}.", Sty.CYAN,
574
681
  " Name: ", Sty.DEFAULT,
575
- f"{dev['display_name']}", Sty.GRAY,
682
+ f"{dev['display_name']}{_handle_tag(dev)}", Sty.GRAY,
576
683
  " Duration: ", Sty.DEFAULT,
577
684
  f"{dev['block_min']} min", Sty.GREEN,
578
685
  )
@@ -594,7 +701,9 @@ def _federated_reserve():
594
701
  f" is < 10 minutes (max_sec={dev['max_reservation_time_sec'] or 0}). Cannot create valid reservations.", Sty.DEFAULT,
595
702
  )
596
703
  return
597
- _pick_slot_and_reserve(dev["device_id"], dev["display_name"], dev["block_min"], _federated_reservations_for_range, _federated_reserve_slot,
704
+ reserve_slot = functools.partial(_federated_reserve_slot, deployment_name=dev["deployment_name"],
705
+ device_name=dev["display_name"], device_handle=dev.get("handle"))
706
+ _pick_slot_and_reserve(dev["device_id"], dev["display_name"], dev["block_min"], _federated_reservations_for_range, reserve_slot,
598
707
  device_label=f"{dev['display_name']} @{dev['deployment_name']}",
599
708
  max_days=-(-int(dev.get("max_horizon_sec") or 0) // 86400) or None)
600
709
 
@@ -747,7 +856,8 @@ def cancel_my_reservation():
747
856
  print("Aborting. A non integer key was given.")
748
857
 
749
858
  _PARTNER_LABEL = re.compile(r"^(?P<name>.*) @(?P<lab>.+?) \((?P<state>[^)]*)\)$")
750
- _LOCAL_LABEL = re.compile(r"^(?P<name>.*) \((?P<state>[^)]*)\)$")
859
+ # The state always starts with online/offline; a PoE fault after it can hold brackets.
860
+ _LOCAL_LABEL = re.compile(r"^(?P<name>.*?) \((?P<state>(?:online|offline)\b.*)\)$")
751
861
 
752
862
 
753
863
  def _native_device_rows():
@@ -808,7 +918,7 @@ def _print_native_devices(rows, *, numbered=False):
808
918
  if row["lab"] != lab:
809
919
  lab = row["lab"]
810
920
  printf(f" @{lab}", (Sty.BOLD, Sty.MAGENTA))
811
- prefix = (f"{index}.", Sty.CYAN, " ") if numbered else ()
921
+ prefix = (f"{index}. ", Sty.CYAN) if numbered else ()
812
922
  printf(*prefix, " Device Name: " if not numbered else " Device Name: ", Sty.GRAY,
813
923
  row["name"], Sty.DEFAULT, f" ({row['state']})" if row["state"] else "", Sty.GRAY)
814
924
 
@@ -852,11 +962,18 @@ def reserve():
852
962
  if not pick.isdigit() or not 1 <= int(pick) <= len(rows):
853
963
  printf("Pick a number from the list.", Sty.WARNING)
854
964
  return
855
- token = account.reserve_device(rows[int(pick) - 1]["id"], get_datetime("Reserve Start Time"), get_datetime("Reserve End Time"))
965
+ row = rows[int(pick) - 1]
966
+ start, end = get_datetime("Reserve Start Time"), get_datetime("Reserve End Time")
967
+ token = account.reserve_device(row["id"], start, end)
856
968
  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"])
969
+ name, handle = (row["name"], None) if row["away"] else _name_and_handle(row["name"])
970
+ home = _token_lab()
971
+ lab = row["lab"] if row["away"] else home or row["lab"]
972
+ _print_reserved(f"{handle or name} @{lab}" if lab else handle or name,
973
+ f"{start:%Y-%m-%d %I:%M %p}-{end:%I:%M %p}", _routable(token),
974
+ f"{handle}@{home}" if handle and home else None, home,
975
+ lambda: account.install_reserved_driver(token, row["id"]),
976
+ _UNROUTABLE.format(handle, "this lab has no saved name") if handle else _NO_HANDLE)
860
977
  except Exception as e:
861
978
  printf(f"Error: {e}", Sty.BRIGHT_RED)
862
979
 
@@ -1293,11 +1410,9 @@ def interactive_reserve_next_days(block_minutes=60):
1293
1410
  # --- 6) Reserve the chosen slot on the chosen device ---
1294
1411
  token = account.reserve_device(int(chosen_device_id), chosen_slot[0], chosen_slot[1])
1295
1412
  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
-
1413
+ _print_native_reserved(chosen_device_id, str(unmap_arg(data.results[chosen_device_id])),
1414
+ f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}", token)
1415
+
1301
1416
  except Exception as e:
1302
1417
  print(f"Error: {e}")
1303
1418
 
@@ -1413,7 +1528,7 @@ def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes,
1413
1528
  slot_end_str = chosen_slot[1].strftime("%I:%M %p")
1414
1529
 
1415
1530
  confirmation = session.prompt(stylize(
1416
- "Reservation successful on ", Sty.DEFAULT,
1531
+ "Reserve ", Sty.DEFAULT,
1417
1532
  f"{chosen_day.strftime('%Y-%m-%d')}", (Sty.BOLD, Sty.BLUE),
1418
1533
  " from ", Sty.DEFAULT,
1419
1534
  f"{slot_start_str}", Sty.CYAN,
@@ -1421,7 +1536,7 @@ def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes,
1421
1536
  f"{slot_end_str}", Sty.CYAN,
1422
1537
  " for ", Sty.DEFAULT,
1423
1538
  f"{chosen_device_name}", Sty.GRAY,
1424
- ". \nConfirm reservation? (y/n): ", (Sty.BOLD, Sty.GREEN),
1539
+ "?\nConfirm reservation? (y/n): ", (Sty.BOLD, Sty.GREEN),
1425
1540
  )).strip().lower()
1426
1541
 
1427
1542
  if confirmation != "y":
@@ -1434,13 +1549,11 @@ def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes,
1434
1549
  do_reserve(chosen_device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str)
1435
1550
 
1436
1551
 
1437
- def _native_reserve_slot(chosen_device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str):
1552
+ def _native_reserve_slot(chosen_device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str, device_label=""):
1438
1553
  token = account.reserve_device(int(chosen_device_id), chosen_slot[0], chosen_slot[1])
1439
1554
  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))
1555
+ _print_native_reserved(chosen_device_id, device_label,
1556
+ f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}", token)
1444
1557
 
1445
1558
 
1446
1559
  def interactive_reserve_next_days_auto():
@@ -1578,19 +1691,23 @@ def interactive_reserve_next_days_auto():
1578
1691
 
1579
1692
  chosen_device_key = sorted_device_ids[sel_i]
1580
1693
  chosen_device_id = str(chosen_device_key)
1581
- chosen_device_name = unmap_arg(dev_resp.results[chosen_device_key])
1694
+ chosen_device_name = str(unmap_arg(dev_resp.results[chosen_device_key]))
1695
+ # 'Pluto SDR [PlutoA] (online)' -> 'Pluto SDR [PlutoA]', 'Pluto SDR @mit (online)' -> 'Pluto SDR @mit'
1696
+ shown = _LOCAL_LABEL.match(chosen_device_name)
1697
+ shown = shown["name"] if shown else chosen_device_name
1582
1698
  block_minutes = int(block_by_dev.get(chosen_device_id, 30))
1583
1699
 
1584
1700
  if block_minutes < 10:
1585
1701
  printf(
1586
1702
  "Your max reservation duration for device ", Sty.DEFAULT,
1587
- f"{chosen_device_id}", Sty.MAGENTA,
1703
+ shown, Sty.MAGENTA,
1588
1704
  f" is < 10 minutes (max_sec={max_by_dev.get(chosen_device_id, 0)}). Cannot create valid reservations.", Sty.DEFAULT,
1589
1705
  )
1590
1706
  return
1591
1707
 
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)
1708
+ _pick_slot_and_reserve(chosen_device_id, shown, block_minutes, fetch_reservations_for_range,
1709
+ functools.partial(_native_reserve_slot, device_label=chosen_device_name),
1710
+ device_label=shown, max_days=days_by_dev.get(chosen_device_id) or None)
1594
1711
 
1595
1712
  except Exception as e:
1596
1713
  print(f"Error: {e}")
@@ -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