remotivelabs-cli 0.5.0a1__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.
Files changed (84) hide show
  1. remotivelabs/cli/__init__.py +0 -0
  2. remotivelabs/cli/api/cloud/tokens.py +62 -0
  3. remotivelabs/cli/broker/__init__.py +33 -0
  4. remotivelabs/cli/broker/defaults.py +1 -0
  5. remotivelabs/cli/broker/discovery.py +43 -0
  6. remotivelabs/cli/broker/export.py +92 -0
  7. remotivelabs/cli/broker/files.py +119 -0
  8. remotivelabs/cli/broker/lib/__about__.py +4 -0
  9. remotivelabs/cli/broker/lib/broker.py +625 -0
  10. remotivelabs/cli/broker/lib/client.py +224 -0
  11. remotivelabs/cli/broker/lib/helper.py +277 -0
  12. remotivelabs/cli/broker/lib/signalcreator.py +196 -0
  13. remotivelabs/cli/broker/license_flows.py +167 -0
  14. remotivelabs/cli/broker/licenses.py +98 -0
  15. remotivelabs/cli/broker/playback.py +117 -0
  16. remotivelabs/cli/broker/record.py +41 -0
  17. remotivelabs/cli/broker/recording_session/__init__.py +3 -0
  18. remotivelabs/cli/broker/recording_session/client.py +67 -0
  19. remotivelabs/cli/broker/recording_session/cmd.py +254 -0
  20. remotivelabs/cli/broker/recording_session/time.py +49 -0
  21. remotivelabs/cli/broker/scripting.py +129 -0
  22. remotivelabs/cli/broker/signals.py +220 -0
  23. remotivelabs/cli/broker/version.py +31 -0
  24. remotivelabs/cli/cloud/__init__.py +17 -0
  25. remotivelabs/cli/cloud/auth/__init__.py +3 -0
  26. remotivelabs/cli/cloud/auth/cmd.py +128 -0
  27. remotivelabs/cli/cloud/auth/login.py +283 -0
  28. remotivelabs/cli/cloud/auth_tokens.py +149 -0
  29. remotivelabs/cli/cloud/brokers.py +109 -0
  30. remotivelabs/cli/cloud/configs.py +109 -0
  31. remotivelabs/cli/cloud/licenses/__init__.py +0 -0
  32. remotivelabs/cli/cloud/licenses/cmd.py +14 -0
  33. remotivelabs/cli/cloud/organisations.py +112 -0
  34. remotivelabs/cli/cloud/projects.py +44 -0
  35. remotivelabs/cli/cloud/recordings.py +580 -0
  36. remotivelabs/cli/cloud/recordings_playback.py +274 -0
  37. remotivelabs/cli/cloud/resumable_upload.py +87 -0
  38. remotivelabs/cli/cloud/sample_recordings.py +25 -0
  39. remotivelabs/cli/cloud/service_account_tokens.py +62 -0
  40. remotivelabs/cli/cloud/service_accounts.py +72 -0
  41. remotivelabs/cli/cloud/storage/__init__.py +5 -0
  42. remotivelabs/cli/cloud/storage/cmd.py +76 -0
  43. remotivelabs/cli/cloud/storage/copy.py +86 -0
  44. remotivelabs/cli/cloud/storage/uri_or_path.py +45 -0
  45. remotivelabs/cli/cloud/uri.py +113 -0
  46. remotivelabs/cli/connect/__init__.py +0 -0
  47. remotivelabs/cli/connect/connect.py +118 -0
  48. remotivelabs/cli/connect/protopie/protopie.py +185 -0
  49. remotivelabs/cli/py.typed +0 -0
  50. remotivelabs/cli/remotive.py +123 -0
  51. remotivelabs/cli/settings/__init__.py +20 -0
  52. remotivelabs/cli/settings/config_file.py +113 -0
  53. remotivelabs/cli/settings/core.py +333 -0
  54. remotivelabs/cli/settings/migration/__init__.py +0 -0
  55. remotivelabs/cli/settings/migration/migrate_all_token_files.py +80 -0
  56. remotivelabs/cli/settings/migration/migrate_config_file.py +64 -0
  57. remotivelabs/cli/settings/migration/migrate_legacy_dirs.py +50 -0
  58. remotivelabs/cli/settings/migration/migrate_token_file.py +52 -0
  59. remotivelabs/cli/settings/migration/migration_tools.py +38 -0
  60. remotivelabs/cli/settings/state_file.py +67 -0
  61. remotivelabs/cli/settings/token_file.py +128 -0
  62. remotivelabs/cli/tools/__init__.py +0 -0
  63. remotivelabs/cli/tools/can/__init__.py +0 -0
  64. remotivelabs/cli/tools/can/can.py +78 -0
  65. remotivelabs/cli/tools/tools.py +9 -0
  66. remotivelabs/cli/topology/__init__.py +28 -0
  67. remotivelabs/cli/topology/all.py +322 -0
  68. remotivelabs/cli/topology/cli/__init__.py +3 -0
  69. remotivelabs/cli/topology/cli/run_in_docker.py +58 -0
  70. remotivelabs/cli/topology/cli/topology_cli.py +16 -0
  71. remotivelabs/cli/topology/cmd.py +130 -0
  72. remotivelabs/cli/topology/start_trial.py +134 -0
  73. remotivelabs/cli/typer/__init__.py +0 -0
  74. remotivelabs/cli/typer/typer_utils.py +27 -0
  75. remotivelabs/cli/utils/__init__.py +0 -0
  76. remotivelabs/cli/utils/console.py +99 -0
  77. remotivelabs/cli/utils/rest_helper.py +369 -0
  78. remotivelabs/cli/utils/time.py +11 -0
  79. remotivelabs/cli/utils/versions.py +120 -0
  80. remotivelabs_cli-0.5.0a1.dist-info/METADATA +51 -0
  81. remotivelabs_cli-0.5.0a1.dist-info/RECORD +84 -0
  82. remotivelabs_cli-0.5.0a1.dist-info/WHEEL +4 -0
  83. remotivelabs_cli-0.5.0a1.dist-info/entry_points.txt +3 -0
  84. remotivelabs_cli-0.5.0a1.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,254 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import datetime
5
+ from dataclasses import asdict, is_dataclass
6
+ from enum import Enum
7
+ from typing import Any, AsyncIterator, Optional
8
+
9
+ import typer
10
+
11
+ from remotivelabs.broker.auth import ApiKeyAuth, NoAuth
12
+ from remotivelabs.broker.recording_session import RecordingSessionClient, RecordingSessionPlaybackStatus
13
+ from remotivelabs.cli.broker.defaults import DEFAULT_GRPC_URL
14
+ from remotivelabs.cli.broker.recording_session.client import RecursiveFilesListingClient
15
+ from remotivelabs.cli.broker.recording_session.time import time_offset_to_us
16
+ from remotivelabs.cli.typer import typer_utils
17
+ from remotivelabs.cli.utils.console import print_generic_error, print_result
18
+
19
+ app = typer_utils.create_typer(
20
+ help="""
21
+ Manage playback of recording sessions
22
+
23
+ All offsets are in microseconds (μs)
24
+ """
25
+ )
26
+
27
+
28
+ def _int_or_none(offset: Optional[str | int]) -> Optional[int]:
29
+ return offset if offset is None else int(offset)
30
+
31
+
32
+ def _print_offset_help(cmd: str) -> str:
33
+ return f"""
34
+ Offsets can be specified in minutes (1:15min), seconds(10s), millis(10000ms) or micros(10000000us), default without suffix is micros.
35
+ Sample offsets
36
+ {cmd} 1.15min, 10s, 10000ms, 10000000us, 10000000,
37
+ """
38
+
39
+
40
+ def _custom_types(o: Any) -> Any:
41
+ if isinstance(o, Enum):
42
+ return o.name
43
+ if is_dataclass(type(o)):
44
+ return asdict(o)
45
+ if isinstance(o, datetime.datetime):
46
+ return o.isoformat(timespec="seconds")
47
+ raise TypeError(f"Object of type {o.__class__.__name__} is not JSON serializable")
48
+
49
+
50
+ async def _list_files_async(path: str, recursive: bool, url: str, api_key: str) -> None:
51
+ if recursive:
52
+ client = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key else NoAuth())
53
+ file_listing_client = RecursiveFilesListingClient(client)
54
+ print_result(
55
+ await file_listing_client.list_all_files(path, file_types=None),
56
+ default=_custom_types,
57
+ )
58
+ else:
59
+ client = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key else NoAuth())
60
+ print_result(await client.list_recording_files(path), default=_custom_types)
61
+
62
+
63
+ @app.command()
64
+ def list_files(
65
+ path: str = typer.Argument("/", help="Optional subdirectory to list files in, defaults to /"),
66
+ recursive: bool = typer.Option(False, help="List subdirectories recursively"),
67
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
68
+ api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
69
+ ) -> None:
70
+ """
71
+ List files on broker.
72
+ """
73
+ try:
74
+ asyncio.run(_list_files_async(path, recursive, url, api_key))
75
+ except Exception as e:
76
+ print_generic_error(str(e))
77
+
78
+
79
+ # --------------------------------------------------------------------------- #
80
+ # play
81
+ # --------------------------------------------------------------------------- #
82
+
83
+
84
+ async def _play_async(path: str, offset: str, url: str, api_key: str) -> None:
85
+ client = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key else NoAuth())
86
+ result = await client.get_session(path=path).play(offset=_int_or_none(offset))
87
+ print_result(result, default=_custom_types)
88
+
89
+
90
+ @app.command(
91
+ help=f"""
92
+ Starts playing the recording at current offset or from specified offset
93
+ {_print_offset_help("--offset")}
94
+ """
95
+ )
96
+ def play( # noqa: PLR0913
97
+ path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
98
+ offset: str = typer.Option(None, callback=time_offset_to_us, help="Offset to play from"),
99
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
100
+ api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
101
+ ) -> None:
102
+ try:
103
+ asyncio.run(_play_async(path, offset, url, api_key))
104
+ except Exception as e:
105
+ print_generic_error(str(e))
106
+
107
+
108
+ async def _repeat_async(path: str, start_offset: str, end_offset: str, clear: bool, url: str, api_key: str) -> None: # noqa: PLR0913
109
+ session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key else NoAuth()).get_session(path)
110
+ if clear:
111
+ result = await session.set_repeat(start_offset=None, end_offset=None)
112
+ else:
113
+ result = await session.set_repeat(start_offset=int(start_offset), end_offset=_int_or_none(end_offset))
114
+ print_result(result, _custom_types)
115
+
116
+
117
+ @app.command(
118
+ help=f"""
119
+ Repeat RecordingSession in specific interval or complete recording
120
+ To remove existing repeat config, use --clear flag.
121
+ {_print_offset_help("--start-offset/--end-offset")}
122
+ """
123
+ )
124
+ def repeat( # noqa: PLR0913
125
+ path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
126
+ start_offset: str = typer.Option(0, callback=time_offset_to_us, help="Repeat start offset, defaults to start"),
127
+ end_offset: str = typer.Option(None, callback=time_offset_to_us, help="Repeat end offset, defaults to end"),
128
+ clear: bool = typer.Option(False, help="Clear repeat"),
129
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
130
+ api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
131
+ ) -> None:
132
+ try:
133
+ asyncio.run(_repeat_async(path, start_offset, end_offset, clear, url, api_key))
134
+ except Exception as e:
135
+ print_generic_error(str(e))
136
+
137
+
138
+ async def _pause_async(path: str, offset: str, url: str, api_key: str) -> None:
139
+ session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key else NoAuth()).get_session(path)
140
+ result = await session.pause(offset=_int_or_none(offset))
141
+ print_result(result, default=_custom_types)
142
+
143
+
144
+ @app.command(
145
+ help=f"""
146
+ Pause the recording at current offset or specified offset
147
+ {_print_offset_help("--offset")}
148
+ """
149
+ )
150
+ def pause(
151
+ path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
152
+ offset: str = typer.Option(None, callback=time_offset_to_us, help="Offset to play from"),
153
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
154
+ api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
155
+ ) -> None:
156
+ try:
157
+ asyncio.run(_pause_async(path, offset, url, api_key))
158
+ except Exception as e:
159
+ print_generic_error(str(e))
160
+
161
+
162
+ async def _seek_async(path: str, offset: str, url: str, api_key: str) -> None:
163
+ session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key else NoAuth()).get_session(path)
164
+ result = await session.seek(offset=int(offset))
165
+ print_result(result, default=_custom_types)
166
+
167
+
168
+ @app.command(
169
+ help=f"""
170
+ Seek to specified offset
171
+ {_print_offset_help("--offset")}
172
+ """
173
+ )
174
+ def seek(
175
+ path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
176
+ offset: str = typer.Option(..., callback=time_offset_to_us, help="Offset to seek to"),
177
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
178
+ api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
179
+ ) -> None:
180
+ try:
181
+ asyncio.run(_seek_async(path, offset, url, api_key))
182
+ except Exception as e:
183
+ print_generic_error(str(e))
184
+
185
+
186
+ async def _open_async(path: str, force: bool, url: str, api_key: str) -> None:
187
+ session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key else NoAuth()).get_session(path)
188
+ result = await session.open(force_reopen=force)
189
+ print_result(result, default=_custom_types)
190
+
191
+
192
+ @app.command()
193
+ def open( # noqa: PLR0913
194
+ path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
195
+ force: bool = typer.Option(False, help="Force close and re-open recording session if exists"),
196
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
197
+ api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
198
+ ) -> None:
199
+ """
200
+ Open a recording session.
201
+ """
202
+ try:
203
+ asyncio.run(_open_async(path, force, url, api_key))
204
+ except Exception as e:
205
+ print_generic_error(str(e))
206
+
207
+
208
+ async def _close_async(path: str, url: str, api_key: str) -> None:
209
+ session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key else NoAuth()).get_session(path)
210
+ result = await session.close()
211
+ print_result(result, default=_custom_types)
212
+
213
+
214
+ @app.command()
215
+ def close(
216
+ path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
217
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
218
+ api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
219
+ ) -> None:
220
+ """
221
+ Close a recording session.
222
+ """
223
+ try:
224
+ asyncio.run(_close_async(path, url, api_key))
225
+ except Exception as e:
226
+ print_generic_error(str(e))
227
+
228
+
229
+ async def _status_async(stream: bool, url: str, api_key: str) -> None:
230
+ client = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key else NoAuth())
231
+
232
+ async def _async_playback_stream() -> None:
233
+ _stream: AsyncIterator[list[RecordingSessionPlaybackStatus]] = client.playback_status()
234
+ async for f in _stream:
235
+ print_result(f, default=_custom_types)
236
+ if not stream:
237
+ break
238
+
239
+ await _async_playback_stream()
240
+
241
+
242
+ @app.command()
243
+ def status(
244
+ stream: bool = typer.Option(False, help="Blocks and continuously streams statuses"),
245
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
246
+ api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
247
+ ) -> None:
248
+ """
249
+ Get the status of all opened Recording sessions
250
+ """
251
+ try:
252
+ asyncio.run(_status_async(stream, url, api_key))
253
+ except Exception as e:
254
+ print_generic_error(str(e))
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Optional
5
+
6
+ import typer
7
+
8
+
9
+ class InvalidOffsetException(typer.BadParameter): # noqa: N818
10
+ """
11
+ Thrown if invalid time, extends BadParameter to hook into typer validation
12
+ """
13
+
14
+
15
+ def time_offset_to_us(value: Optional[str]) -> Optional[int]:
16
+ """
17
+ Parse a time string like '1s', '1ms', '1us', '1.02min' or '30' (default µs) into microseconds (int).
18
+ For minutes: 1.02min == 1 minute 2 seconds
19
+ """
20
+ if value is None:
21
+ return None
22
+
23
+ value = value.strip().lower()
24
+
25
+ # Check if it's a minute format with decimal or colon
26
+ min_match = re.fullmatch(r"([+-]?\d+)[.:](\d+)min", value)
27
+ if min_match:
28
+ minutes = int(min_match.group(1))
29
+ seconds = int(min_match.group(2))
30
+ return (minutes * 60 + seconds) * 1_000_000
31
+
32
+ # Support optional + or - sign before the number
33
+ match = re.fullmatch(r"([+-]?\d+(?:\.\d+)?)(?:\s*(us|µs|ms|s|min))?", value)
34
+ if not match:
35
+ raise InvalidOffsetException(f"Invalid time format: '{value}'")
36
+
37
+ amount, unit = match.groups()
38
+ amount = float(amount)
39
+ unit = unit or "us" # Default to microseconds
40
+
41
+ unit_multipliers = {
42
+ "min": 60_000_000,
43
+ "s": 1_000_000,
44
+ "ms": 1_000,
45
+ "us": 1,
46
+ "µs": 1,
47
+ }
48
+
49
+ return int(amount * unit_multipliers[unit])
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from string import Template
5
+ from typing import List
6
+
7
+ import typer
8
+
9
+ from remotivelabs.cli.typer import typer_utils
10
+ from remotivelabs.cli.utils.console import print_generic_message, print_hint
11
+
12
+ app = typer_utils.create_typer(
13
+ rich_markup_mode="rich",
14
+ help="""
15
+ [Experimental] - Generate template lua script for input and output signals
16
+ """,
17
+ )
18
+
19
+
20
+ def write(signal_name: str, script: str) -> None:
21
+ path = f"{signal_name}.lua"
22
+ with open(path, "w") as f:
23
+ f.write(script)
24
+ print_generic_message(f"Secret token written to {path}")
25
+
26
+
27
+ @app.command("new-script")
28
+ def new_script(
29
+ input_signal: List[str] = typer.Option(..., help="Required input signal names"),
30
+ output_signal: str = typer.Option(..., help="Name of output signal"),
31
+ save: bool = typer.Option(False, help="Save file to disk - Default stored as __output_signal__.lua"),
32
+ ) -> None:
33
+ def to_subscribable_signal(sig: str) -> tuple[str, str]:
34
+ arr = sig.split(":")
35
+ if len(arr) != 2:
36
+ print_hint(f"--input-signal must have format namespace:signal ({sig})")
37
+ sys.exit(1)
38
+ return arr[0], arr[1]
39
+
40
+ signals_to_subscribe_to = list(map(to_subscribable_signal, input_signal))
41
+
42
+ def to_local_signal(sig_name: tuple[str, str]) -> str:
43
+ t = Template(
44
+ """
45
+ {
46
+ name = "$sig_name",
47
+ namespace = "$namespace"
48
+ }"""
49
+ )
50
+ return t.substitute(sig_name=sig_name[1], namespace=sig_name[0])
51
+
52
+ local_signals = ",".join(list(map(to_local_signal, signals_to_subscribe_to)))
53
+
54
+ def to_subscribe_pattern(sig_name: tuple[str, str]) -> str:
55
+ t = Template(
56
+ """
57
+ if (signals["$sig_name"] ~= nil) then
58
+ return return_value_or_bytes(signals["$sig_name"])
59
+ end
60
+ """
61
+ )
62
+ return t.substitute(sig_name=sig_name[1])
63
+
64
+ subscribe_pattern = "".join(list(map(to_subscribe_pattern, signals_to_subscribe_to)))
65
+
66
+ template = Template(
67
+ """
68
+ --
69
+ -- Docs available at https://docs.remotivelabs.com/docs/remotive-broker/scripted_signals
70
+ --
71
+
72
+ local local_signals = {$local_signals
73
+ }
74
+
75
+ -- Required, declare which input is needed to operate this program.
76
+ function input_signals()
77
+ return local_signals
78
+ end
79
+
80
+ -- Provided parameters are used for populating metadata when listing signals.
81
+ function output_signal()
82
+ return "$output_signal"
83
+ end
84
+
85
+ -- Required, declare what frequency you like to get "timer" invoked. 0 means no calls to "timer".
86
+ function timer_frequency_hz()
87
+ return 0
88
+ end
89
+
90
+ -- Invoked with the frequency returned by "timer_frequency_hz".
91
+ -- @param system_timestamp_us: system time stamp
92
+ function timer(system_timestamp_us)
93
+ return return_value_or_bytes("your value")
94
+ end
95
+
96
+ -- Invoked when ANY signal declared in "local_signals" arrive
97
+ -- @param signals_timestamp_us: signal time stamp
98
+ -- @param system_timestamp_us
99
+ -- @param signals: array of signals containing all or a subset of signals declared in "local_signals". Make sure to nil check before use.
100
+ function signals(signals, namespace, signals_timestamp_us, system_timestamp_us)
101
+ -- TODO - replace this code with what you want todo
102
+
103
+ $subscribe_pattern
104
+ return return_nothing()
105
+ end
106
+
107
+ -- helper return function, make sure to use return_value_or_bytes or return_nothing.
108
+ function return_value_or_bytes(value_or_bytes)
109
+ return value_or_bytes
110
+ end
111
+
112
+ -- helper return function, make sure to use return_value_or_bytes or return_nothing.
113
+ function return_nothing()
114
+ return
115
+ end
116
+
117
+ """
118
+ )
119
+
120
+ script = template.substitute(
121
+ local_signals=local_signals,
122
+ subscribe_pattern=subscribe_pattern,
123
+ output_signal=output_signal,
124
+ )
125
+
126
+ if save:
127
+ write(output_signal, script)
128
+ else:
129
+ print_generic_message(script)
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import numbers
5
+ import os
6
+ import signal as os_signal
7
+ import sys
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Iterable, List, TypedDict, Union
11
+
12
+ import grpc
13
+ import plotext as plt # type: ignore
14
+ import typer
15
+
16
+ from remotivelabs.cli.broker.defaults import DEFAULT_GRPC_URL
17
+ from remotivelabs.cli.broker.lib.broker import Broker, SubscribableSignal
18
+ from remotivelabs.cli.typer import typer_utils
19
+ from remotivelabs.cli.utils.console import print_generic_error, print_generic_message, print_grpc_error, print_hint
20
+
21
+ app = typer_utils.create_typer(help=help)
22
+
23
+
24
+ class Signals(TypedDict):
25
+ name: str
26
+ signals: List[Any]
27
+
28
+
29
+ signal_values: Dict[Any, Any] = {}
30
+
31
+
32
+ @app.command(name="list")
33
+ def list_signals(
34
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
35
+ api_key: str = typer.Option(None, help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
36
+ name_starts_with: Union[str, None] = typer.Option(None, help="Signal name prefix to include"),
37
+ name_ends_with: Union[str, None] = typer.Option(None, help="Signal name suffix to include"),
38
+ ) -> None:
39
+ """
40
+ List signal metadata on a broker
41
+
42
+ Filter are inclusive so --name-starts-with and --name-ends-with will include name that matches both
43
+ """
44
+ try:
45
+ broker = Broker(url, api_key)
46
+ available_signals = broker.list_signal_names(prefix=name_starts_with, suffix=name_ends_with)
47
+ print_generic_message(json.dumps(available_signals))
48
+ except grpc.RpcError as rpc_error:
49
+ print_grpc_error(rpc_error)
50
+
51
+
52
+ def read_scripted_code_file(file_path: Path) -> bytes:
53
+ try:
54
+ print_generic_message(str(file_path))
55
+ with open(file_path, "rb") as file:
56
+ return file.read()
57
+ except FileNotFoundError:
58
+ print_generic_error("File not found. Please check your file path.")
59
+ sys.exit(1)
60
+
61
+
62
+ @app.command()
63
+ def subscribe( # noqa: C901, PLR0913, PLR0915
64
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
65
+ api_key: str = typer.Option(None, help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
66
+ signal: List[str] = typer.Option([], help="Signal names to subscribe to, mandatory when not using script"),
67
+ script: Path = typer.Option(
68
+ None,
69
+ exists=True,
70
+ file_okay=True,
71
+ dir_okay=False,
72
+ writable=False,
73
+ readable=True,
74
+ resolve_path=True,
75
+ help="Supply a path to Lua script that to use for signal transformation",
76
+ ),
77
+ on_change_only: bool = typer.Option(default=False, help="Only get signal if value is changed"),
78
+ x_plot: bool = typer.Option(default=False, help="Experimental: Plot the signal in terminal. Note graphs are not aligned by time"),
79
+ x_plot_size: int = typer.Option(default=100, help="Experimental: how many points show for each plot"),
80
+ # samples: int = typer.Option(default=0, he)
81
+ ) -> None:
82
+ """
83
+ Subscribe to a selection of signals
84
+
85
+ Subscribe to two signals and have it printed to terminal
86
+ ```
87
+ remotive broker signals subscribe --url http://localhost:50051 --signal can1:signal1 --signal can0:signal2
88
+ ```
89
+
90
+ Subscribe using a LUA script with signal transformations, read more about scripted signals at https://docs.remotivelabs.com/docs/remotive-broker
91
+ ```
92
+ remotive broker signals subscribe --url http://localhost:50051 --script myvss_script.lua
93
+ ```
94
+ """
95
+
96
+ if script is None:
97
+ if len(signal) == 0:
98
+ print_generic_error("You must use --signal or use --script when subscribing")
99
+ sys.exit(1)
100
+
101
+ if script is not None:
102
+ if len(signal) > 0:
103
+ print_generic_error("You must must not specify --signal when using --script")
104
+ sys.exit(1)
105
+
106
+ plt.title("Signals")
107
+
108
+ def exit_on_ctrlc(_sig: Any, _frame: Any) -> None:
109
+ os._exit(0)
110
+
111
+ def on_frame_plot(x: Iterable[Any]) -> None:
112
+ plt.clt() # to clear the terminal
113
+ plt.cld() # to clear the data only
114
+ frames = list(x)
115
+ plt.clf()
116
+ plt.subplots(len(list(filter(lambda n: n.startswith("ts_"), signal_values.keys()))))
117
+ plt.theme("pro")
118
+
119
+ for frame in frames:
120
+ name = frame["name"]
121
+
122
+ if not isinstance(frame["value"], numbers.Number):
123
+ # Skip non numberic values
124
+ # TODO - would exit and print info message if I knew how to
125
+ continue
126
+
127
+ y = [frame["value"]]
128
+ t = [frame["timestamp_us"]]
129
+
130
+ if name not in signal_values:
131
+ signal_values[name] = [None] * x_plot_size
132
+ signal_values[f"ts_{name}"] = [None] * x_plot_size
133
+ signal_values[name] = signal_values[name] + y
134
+ signal_values[f"ts_{name}"] = signal_values[f"ts_{name}"] + t
135
+
136
+ if len(signal_values[name]) > x_plot_size:
137
+ signal_values[name] = signal_values[name][len(signal_values[name]) - x_plot_size :]
138
+
139
+ if len(signal_values[f"ts_{name}"]) > x_plot_size:
140
+ signal_values[f"ts_{name}"] = signal_values[f"ts_{name}"][len(signal_values[f"ts_{name}"]) - x_plot_size :]
141
+
142
+ cnt = 1
143
+ for key, value in signal_values.items():
144
+ if not key.startswith("ts_"):
145
+ plt.subplot(cnt, 1).plot(signal_values[f"ts_{key}"], value, label=key, color=cnt)
146
+ cnt = cnt + 1
147
+ plt.sleep(0.001) # to add
148
+ plt.show()
149
+
150
+ def on_frame_print(x: Iterable[Any]) -> None:
151
+ # TODO: use log instead of print for debug information?
152
+ print_generic_message(json.dumps(list(x)))
153
+
154
+ os_signal.signal(os_signal.SIGINT, exit_on_ctrlc)
155
+
156
+ if x_plot:
157
+ on_frame_func = on_frame_plot
158
+ else:
159
+ on_frame_func = on_frame_print
160
+
161
+ try:
162
+ if script is not None:
163
+ script_src = read_scripted_code_file(script)
164
+ broker = Broker(url, api_key)
165
+ broker.subscribe_on_script(script_src, on_frame_func, on_change_only)
166
+ else:
167
+
168
+ def to_subscribable_signal(sig: str):
169
+ arr = sig.split(":")
170
+ if len(arr) != 2:
171
+ print_hint(f"--signal must have format namespace:signal ({sig})")
172
+ sys.exit(1)
173
+ return SubscribableSignal(namespace=arr[0], name=arr[1])
174
+
175
+ signals_to_subscribe_to = list(map(to_subscribable_signal, signal))
176
+
177
+ broker = Broker(url, api_key)
178
+ broker.long_name_subscribe(signals_to_subscribe_to, on_frame_func, on_change_only)
179
+ print_generic_message("Subscribing to signals, press Ctrl+C to exit")
180
+ except grpc.RpcError as rpc_error:
181
+ print_grpc_error(rpc_error)
182
+
183
+
184
+ @app.command(help="List namespaces on broker")
185
+ def namespaces(
186
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
187
+ api_key: str = typer.Option(None, help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
188
+ ) -> None:
189
+ try:
190
+ broker = Broker(url, api_key)
191
+ namespaces_json = broker.list_namespaces()
192
+ print_generic_message(json.dumps(namespaces_json))
193
+ except grpc.RpcError as rpc_error:
194
+ print_grpc_error(rpc_error)
195
+
196
+
197
+ @app.command()
198
+ def frame_distribution(
199
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
200
+ api_key: str = typer.Option(None, help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
201
+ namespace: str = typer.Option(..., help="Namespace"),
202
+ ) -> None:
203
+ """
204
+ Use this command to get frames currently available on the specified namespace.
205
+ """
206
+ try:
207
+ broker = Broker(url, api_key)
208
+
209
+ def on_data(data: Dict[str, Any]) -> None:
210
+ timestamp: str = datetime.now().strftime("%H:%M:%S")
211
+ distribution = data["countsByFrameId"]
212
+ if len(distribution) == 0:
213
+ print_hint(f"{timestamp} - No frames available")
214
+ else:
215
+ for d in distribution:
216
+ print_generic_message(f"{timestamp}: {d}")
217
+
218
+ broker.listen_on_frame_distribution(namespace, on_data)
219
+ except grpc.RpcError as rpc_error:
220
+ print_grpc_error(rpc_error)
@@ -0,0 +1,31 @@
1
+ import semver
2
+
3
+
4
+ def ensure_version_is_at_least(version: str, min_version: str) -> None:
5
+ """
6
+ Ensures that broker version is at least a specific version.
7
+ """
8
+
9
+ try:
10
+ # use finalize() to use the release version of this version regardless of pre-release or build
11
+ broker_version = semver.parse_version_info(version).finalize_version()
12
+ required_version = semver.parse_version_info(min_version).finalize_version()
13
+ except ValueError as e:
14
+ raise InvalidBrokerVersionError(str(e))
15
+
16
+ if broker_version < required_version:
17
+ raise UnsupportedBrokerVersionError(current_version=broker_version, min_version=min_version)
18
+
19
+
20
+ class UnsupportedBrokerVersionError(Exception):
21
+ """Raised when broker version is below the minimum supported version."""
22
+
23
+ def __init__(self, current_version: str, min_version: str):
24
+ self.current_version = current_version
25
+ self.min_version = min_version
26
+
27
+
28
+ class InvalidBrokerVersionError(Exception):
29
+ """Raised when version is not major.minor.patch"""
30
+
31
+ pass