remoteRF-server 0.1.71__tar.gz → 0.1.73__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 (42) hide show
  1. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/PKG-INFO +4 -2
  2. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/README.md +3 -1
  3. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/pyproject.toml +1 -1
  4. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +15 -5
  5. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +1 -1
  6. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/host/host_tunnel_server.py +65 -4
  7. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server.egg-info/PKG-INFO +4 -2
  8. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/setup.cfg +0 -0
  9. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/__init__.py +0 -0
  10. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/__init__.py +0 -0
  11. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  12. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  13. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  14. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/utils/__init__.py +0 -0
  15. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  16. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/utils/api_token.py +0 -0
  17. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  18. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/utils/db_location.py +0 -0
  19. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/utils/list_string.py +0 -0
  20. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  21. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/drivers/__init__.py +0 -0
  22. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  23. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  24. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/host/__init__.py +0 -0
  25. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/host/host_directory_store.py +0 -0
  26. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/server/__init__.py +0 -0
  27. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/server/acc_perms.py +0 -0
  28. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/server/cert_provider.py +0 -0
  29. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/server/device_manager.py +0 -0
  30. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/server/grpc_server.py +0 -0
  31. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/server/reservation.py +0 -0
  32. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/server/rpc_manager.py +0 -0
  33. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/server/user_group_cli.py +0 -0
  34. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/server/user_group_handler.py +0 -0
  35. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/serverrf_cli.py +0 -0
  36. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/tools/__init__.py +0 -0
  37. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server/tools/gen_certs.py +0 -0
  38. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  39. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  40. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  41. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server.egg-info/requires.txt +0 -0
  42. {remoterf_server-0.1.71 → remoterf_server-0.1.73}/src/remoteRF_server.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.71
3
+ Version: 0.1.73
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -346,7 +346,9 @@ todo:
346
346
  <!-- Testing functionality (disregarding Host pipeline) -->
347
347
  <!-- Clean up serverrf CLI + basic documentation for all commands, usergroups, export codes by CSV. -->
348
348
 
349
- HostRF meta data -> push upon connection + retry connection on host side.
349
+ <!-- HostRF meta data -> push upon connection + retry connection on host side. -->
350
+
351
+ Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.)
350
352
 
351
353
  ServerRF display meta data as part of devices list / reservation + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
352
354
 
@@ -335,7 +335,9 @@ todo:
335
335
  <!-- Testing functionality (disregarding Host pipeline) -->
336
336
  <!-- Clean up serverrf CLI + basic documentation for all commands, usergroups, export codes by CSV. -->
337
337
 
338
- HostRF meta data -> push upon connection + retry connection on host side.
338
+ <!-- HostRF meta data -> push upon connection + retry connection on host side. -->
339
+
340
+ Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.)
339
341
 
340
342
  ServerRF display meta data as part of devices list / reservation + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
341
343
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "0.1.71"
7
+ version = "0.1.73"
8
8
  dependencies = [
9
9
  "grpcio",
10
10
  "protobuf",
@@ -25,13 +25,15 @@ _sym_db = _symbol_database.Default()
25
25
  from . import grpc_pb2 as grpc__pb2
26
26
 
27
27
 
28
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fgrpc_host.proto\x12\x08remoterf\x1a\ngrpc.proto\"-\n\tHostHello\x12\x0f\n\x07host_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\"^\n\nDeviceInfo\x12\x10\n\x08local_id\x18\x01 \x01(\r\x12\r\n\x05label\x18\x02 \x01(\t\x12\x11\n\tdevice_id\x18\x03 \x01(\t\x12\x0e\n\x06serial\x18\x04 \x01(\t\x12\x0c\n\x04kind\x18\x05 \x01(\t\"_\n\x0e\x44\x65viceAnnounce\x12%\n\x07\x64\x65vices\x18\x01 \x03(\x0b\x32\x14.remoterf.DeviceInfo\x12\x0f\n\x07unix_ms\x18\x02 \x01(\x04\x12\x15\n\rfull_snapshot\x18\x03 \x01(\x08\"\xab\x01\n\nRpcRequest\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\x18\n\x10global_device_id\x18\x02 \x01(\r\x12\x17\n\x0flocal_device_id\x18\x03 \x01(\r\x12\x11\n\tdevice_id\x18\x04 \x01(\t\x12-\n\x07request\x18\x05 \x01(\x0b\x32\x1c.remote_rf.GenericRPCRequest\x12\x18\n\x10\x64\x65\x61\x64line_unix_ms\x18\x06 \x01(\x04\"i\n\x0bRpcResponse\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12/\n\x08response\x18\x03 \x01(\x0b\x32\x1d.remote_rf.GenericRPCResponse\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\x1c\n\tHeartbeat\x12\x0f\n\x07unix_ms\x18\x01 \x01(\x04\"\x18\n\x06\x43\x61ncel\x12\x0e\n\x06req_id\x18\x01 \x01(\t\"\x97\x02\n\tHostFrame\x12$\n\x05hello\x18\x01 \x01(\x0b\x32\x13.remoterf.HostHelloH\x00\x12\x33\n\x0f\x64\x65vice_announce\x18\x02 \x01(\x0b\x32\x18.remoterf.DeviceAnnounceH\x00\x12+\n\x0brpc_request\x18\x03 \x01(\x0b\x32\x14.remoterf.RpcRequestH\x00\x12-\n\x0crpc_response\x18\x04 \x01(\x0b\x32\x15.remoterf.RpcResponseH\x00\x12(\n\theartbeat\x18\x05 \x01(\x0b\x32\x13.remoterf.HeartbeatH\x00\x12\"\n\x06\x63\x61ncel\x18\x06 \x01(\x0b\x32\x10.remoterf.CancelH\x00\x42\x05\n\x03msg2E\n\nHostTunnel\x12\x37\n\x07\x43onnect\x12\x13.remoterf.HostFrame\x1a\x13.remoterf.HostFrame(\x01\x30\x01\x62\x06proto3')
28
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fgrpc_host.proto\x12\x08remoterf\x1a\ngrpc.proto\"-\n\tHostHello\x12\x0f\n\x07host_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\"^\n\nDeviceInfo\x12\x10\n\x08local_id\x18\x01 \x01(\r\x12\r\n\x05label\x18\x02 \x01(\t\x12\x11\n\tdevice_id\x18\x03 \x01(\t\x12\x0e\n\x06serial\x18\x04 \x01(\t\x12\x0c\n\x04kind\x18\x05 \x01(\t\"_\n\x0e\x44\x65viceAnnounce\x12%\n\x07\x64\x65vices\x18\x01 \x03(\x0b\x32\x14.remoterf.DeviceInfo\x12\x0f\n\x07unix_ms\x18\x02 \x01(\x04\x12\x15\n\rfull_snapshot\x18\x03 \x01(\x08\"\xab\x01\n\nRpcRequest\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\x18\n\x10global_device_id\x18\x02 \x01(\r\x12\x17\n\x0flocal_device_id\x18\x03 \x01(\r\x12\x11\n\tdevice_id\x18\x04 \x01(\t\x12-\n\x07request\x18\x05 \x01(\x0b\x32\x1c.remote_rf.GenericRPCRequest\x12\x18\n\x10\x64\x65\x61\x64line_unix_ms\x18\x06 \x01(\x04\"i\n\x0bRpcResponse\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12/\n\x08response\x18\x03 \x01(\x0b\x32\x1d.remote_rf.GenericRPCResponse\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\x1c\n\tHeartbeat\x12\x0f\n\x07unix_ms\x18\x01 \x01(\x04\"\x18\n\x06\x43\x61ncel\x12\x0e\n\x06req_id\x18\x01 \x01(\t\"e\n\x0bMetaRequest\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\x18\n\x10include_platform\x18\x02 \x01(\x08\x12\x13\n\x0binclude_env\x18\x03 \x01(\x08\x12\x17\n\x0finclude_devices\x18\x04 \x01(\x08\"\xb7\x01\n\x08HostMeta\x12\x0f\n\x07host_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x0f\n\x07unix_ms\x18\x03 \x01(\x04\x12&\n\x02kv\x18\x04 \x03(\x0b\x32\x1a.remoterf.HostMeta.KvEntry\x12%\n\x07\x64\x65vices\x18\x05 \x03(\x0b\x32\x14.remoterf.DeviceInfo\x1a)\n\x07KvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"[\n\x0cMetaResponse\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12 \n\x04meta\x18\x04 \x01(\x0b\x32\x12.remoterf.HostMeta\"\xf7\x02\n\tHostFrame\x12$\n\x05hello\x18\x01 \x01(\x0b\x32\x13.remoterf.HostHelloH\x00\x12\x33\n\x0f\x64\x65vice_announce\x18\x02 \x01(\x0b\x32\x18.remoterf.DeviceAnnounceH\x00\x12+\n\x0brpc_request\x18\x03 \x01(\x0b\x32\x14.remoterf.RpcRequestH\x00\x12-\n\x0crpc_response\x18\x04 \x01(\x0b\x32\x15.remoterf.RpcResponseH\x00\x12(\n\theartbeat\x18\x05 \x01(\x0b\x32\x13.remoterf.HeartbeatH\x00\x12\"\n\x06\x63\x61ncel\x18\x06 \x01(\x0b\x32\x10.remoterf.CancelH\x00\x12-\n\x0cmeta_request\x18\x07 \x01(\x0b\x32\x15.remoterf.MetaRequestH\x00\x12/\n\rmeta_response\x18\x08 \x01(\x0b\x32\x16.remoterf.MetaResponseH\x00\x42\x05\n\x03msg2E\n\nHostTunnel\x12\x37\n\x07\x43onnect\x12\x13.remoterf.HostFrame\x1a\x13.remoterf.HostFrame(\x01\x30\x01\x62\x06proto3')
29
29
 
30
30
  _globals = globals()
31
31
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
32
32
  _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_host_pb2', _globals)
33
33
  if not _descriptor._USE_C_DESCRIPTORS:
34
34
  DESCRIPTOR._loaded_options = None
35
+ _globals['_HOSTMETA_KVENTRY']._loaded_options = None
36
+ _globals['_HOSTMETA_KVENTRY']._serialized_options = b'8\001'
35
37
  _globals['_HOSTHELLO']._serialized_start=41
36
38
  _globals['_HOSTHELLO']._serialized_end=86
37
39
  _globals['_DEVICEINFO']._serialized_start=88
@@ -46,8 +48,16 @@ if not _descriptor._USE_C_DESCRIPTORS:
46
48
  _globals['_HEARTBEAT']._serialized_end=590
47
49
  _globals['_CANCEL']._serialized_start=592
48
50
  _globals['_CANCEL']._serialized_end=616
49
- _globals['_HOSTFRAME']._serialized_start=619
50
- _globals['_HOSTFRAME']._serialized_end=898
51
- _globals['_HOSTTUNNEL']._serialized_start=900
52
- _globals['_HOSTTUNNEL']._serialized_end=969
51
+ _globals['_METAREQUEST']._serialized_start=618
52
+ _globals['_METAREQUEST']._serialized_end=719
53
+ _globals['_HOSTMETA']._serialized_start=722
54
+ _globals['_HOSTMETA']._serialized_end=905
55
+ _globals['_HOSTMETA_KVENTRY']._serialized_start=864
56
+ _globals['_HOSTMETA_KVENTRY']._serialized_end=905
57
+ _globals['_METARESPONSE']._serialized_start=907
58
+ _globals['_METARESPONSE']._serialized_end=998
59
+ _globals['_HOSTFRAME']._serialized_start=1001
60
+ _globals['_HOSTFRAME']._serialized_end=1376
61
+ _globals['_HOSTTUNNEL']._serialized_start=1378
62
+ _globals['_HOSTTUNNEL']._serialized_end=1447
53
63
  # @@protoc_insertion_point(module_scope)
@@ -5,7 +5,7 @@ import warnings
5
5
 
6
6
  from . import grpc_host_pb2 as grpc__host__pb2
7
7
 
8
- GRPC_GENERATED_VERSION = '1.78.0'
8
+ GRPC_GENERATED_VERSION = '1.78.1'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -51,6 +51,31 @@ LOG_MODE = "OFF" # <-- change this
51
51
 
52
52
  _LOG_LOCK = threading.Lock()
53
53
 
54
+ HOST_ID_RE = re.compile(r"^[A-Za-z0-9_.-]{1,64}$") # adjust if you want
55
+
56
+ class HostIdConflictError(RuntimeError):
57
+ def __init__(self, host_id: str, *, age_ms: int, replace_after_ms: int) -> None:
58
+ super().__init__(
59
+ f"host_id '{host_id}' already in use (last heartbeat {age_ms}ms ago). "
60
+ f"Try again after {replace_after_ms}ms or fix the conflicting host config."
61
+ )
62
+ self.host_id = host_id
63
+ self.age_ms = age_ms
64
+ self.replace_after_ms = replace_after_ms
65
+
66
+ def _validate_host_id(raw: str) -> str:
67
+ hid = (raw or "").strip()
68
+ if not hid:
69
+ raise ValueError("Missing host_id (set HOST_ID in host.env; do not auto-generate).")
70
+ if hid == "unknown-host":
71
+ raise ValueError("host_id cannot be 'unknown-host' (set HOST_ID in host.env).")
72
+ if not HOST_ID_RE.fullmatch(hid):
73
+ raise ValueError(
74
+ "Invalid host_id. Allowed: 1..64 chars of [A-Za-z0-9_.-]. "
75
+ f"Got: {hid!r}"
76
+ )
77
+ return hid
78
+
54
79
  def _ts() -> str:
55
80
  t = time.time()
56
81
  sec = int(t)
@@ -387,12 +412,25 @@ class HostTunnelRegistry:
387
412
  # directory/status mutators
388
413
  # ---------------------------
389
414
 
390
- def register_host(self, host_id: str, session: HostSession) -> None:
415
+ def register_host(
416
+ self,
417
+ host_id: str,
418
+ session: HostSession,
419
+ *,
420
+ replace_if_stale_ms: int = 15_000, # tweak: how long without heartbeat before we allow replacement
421
+ ) -> None:
391
422
  info(f"[registry] register_host: host_id={host_id!r}")
423
+
392
424
  with self._lock:
393
425
  old = self._hosts.get(host_id)
394
- if old is not None:
395
- warn(f"[registry] register_host: replacing existing session host_id={host_id!r}")
426
+ if old is not None and old.alive:
427
+ age_ms = int(now_ms() - int(getattr(old, "last_heartbeat_ms", 0) or 0))
428
+ if age_ms < replace_if_stale_ms:
429
+ # conflict: someone else is already using this host_id
430
+ raise HostIdConflictError(host_id, age_ms=age_ms, replace_after_ms=replace_if_stale_ms)
431
+
432
+ # stale: allow replacement
433
+ warn(f"[registry] register_host: replacing STALE session host_id={host_id!r} age_ms={age_ms}")
396
434
  old.close()
397
435
 
398
436
  self._hosts[host_id] = session
@@ -823,7 +861,28 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
823
861
 
824
862
  if which == "hello":
825
863
  hello = frame.hello
826
- host_id = str(getattr(hello, "host_id", "")).strip() or "unknown-host"
864
+ raw = str(getattr(hello, "host_id", "") or "")
865
+ try:
866
+ host_id = _validate_host_id(raw)
867
+ except ValueError as e:
868
+ error(f"[inbound] invalid hello.host_id peer={peer} err={e}")
869
+ fatal["code"] = "INVALID_ARGUMENT"
870
+ fatal["details"] = str(e)
871
+ stop.set()
872
+ return
873
+
874
+ ver = str(getattr(hello, "version", "") or "")
875
+ info(f"[inbound] HELLO host_id={host_id!r} version={ver!r} peer={peer}")
876
+
877
+ session = HostSession(host_id)
878
+ try:
879
+ self.registry.register_host(host_id, session, replace_if_stale_ms=15_000)
880
+ except HostIdConflictError as e:
881
+ error(f"[inbound] HostIdConflictError host_id={host_id!r} err={e}")
882
+ fatal["code"] = "ALREADY_EXISTS"
883
+ fatal["details"] = str(e)
884
+ stop.set()
885
+ return
827
886
  ver = str(getattr(hello, "version", "") or "")
828
887
  info(f"[inbound] HELLO host_id={host_id!r} version={ver!r} peer={peer}")
829
888
 
@@ -976,6 +1035,8 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
976
1035
  error(f"[Connect] aborting stream peer={peer} code={code} details={details}")
977
1036
  if code == "ALREADY_EXISTS":
978
1037
  context.abort(grpc.StatusCode.ALREADY_EXISTS, details)
1038
+ if code == "INVALID_ARGUMENT":
1039
+ context.abort(grpc.StatusCode.INVALID_ARGUMENT, details)
979
1040
  context.abort(grpc.StatusCode.UNKNOWN, details)
980
1041
 
981
1042
  if session is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.71
3
+ Version: 0.1.73
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -346,7 +346,9 @@ todo:
346
346
  <!-- Testing functionality (disregarding Host pipeline) -->
347
347
  <!-- Clean up serverrf CLI + basic documentation for all commands, usergroups, export codes by CSV. -->
348
348
 
349
- HostRF meta data -> push upon connection + retry connection on host side.
349
+ <!-- HostRF meta data -> push upon connection + retry connection on host side. -->
350
+
351
+ Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.)
350
352
 
351
353
  ServerRF display meta data as part of devices list / reservation + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
352
354