remotivelabs-cli 0.0.15__py3-none-any.whl → 0.0.17__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.
- cli/broker/brokers.py +3 -2
- cli/broker/export.py +25 -13
- cli/broker/lib/broker.py +32 -3
- cli/broker/license_flows.py +167 -0
- cli/broker/licenses.py +97 -0
- cli/broker/signals.py +2 -1
- cli/cloud/recordings.py +1 -2
- cli/cloud/recordings_playback.py +92 -8
- cli/cloud/rest_helper.py +36 -4
- {remotivelabs_cli-0.0.15.dist-info → remotivelabs_cli-0.0.17.dist-info}/METADATA +1 -1
- {remotivelabs_cli-0.0.15.dist-info → remotivelabs_cli-0.0.17.dist-info}/RECORD +14 -12
- {remotivelabs_cli-0.0.15.dist-info → remotivelabs_cli-0.0.17.dist-info}/LICENSE +0 -0
- {remotivelabs_cli-0.0.15.dist-info → remotivelabs_cli-0.0.17.dist-info}/WHEEL +0 -0
- {remotivelabs_cli-0.0.15.dist-info → remotivelabs_cli-0.0.17.dist-info}/entry_points.txt +0 -0
cli/broker/brokers.py
CHANGED
@@ -11,7 +11,7 @@ from zeroconf import (
|
|
11
11
|
Zeroconf,
|
12
12
|
)
|
13
13
|
|
14
|
-
from . import export, files, playback, record, scripting, signals
|
14
|
+
from . import export, files, licenses, playback, record, scripting, signals
|
15
15
|
|
16
16
|
app = typer.Typer(rich_markup_mode="rich")
|
17
17
|
|
@@ -85,7 +85,8 @@ app.add_typer(record.app, name="record", help="Record data on buses")
|
|
85
85
|
app.add_typer(files.app, name="files", help="Upload/Download configurations and recordings")
|
86
86
|
app.add_typer(signals.app, name="signals", help="Find and subscribe to signals")
|
87
87
|
app.add_typer(export.app, name="export", help="Export to external formats")
|
88
|
-
app.add_typer(scripting.app, name="scripting")
|
88
|
+
app.add_typer(scripting.app, name="scripting", help="LUA scripting utilities")
|
89
|
+
app.add_typer(licenses.app, name="license", help="View and request license to broker")
|
89
90
|
|
90
91
|
if __name__ == "__main__":
|
91
92
|
app()
|
cli/broker/export.py
CHANGED
@@ -9,7 +9,7 @@ import typer
|
|
9
9
|
|
10
10
|
from cli.errors import ErrorPrinter
|
11
11
|
|
12
|
-
from .lib.broker import Broker
|
12
|
+
from .lib.broker import Broker, SubscribableSignal
|
13
13
|
|
14
14
|
app = typer.Typer(
|
15
15
|
rich_markup_mode="rich",
|
@@ -24,8 +24,8 @@ but more formats will come soon
|
|
24
24
|
def influxdb(
|
25
25
|
url: str = typer.Option(..., help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
26
26
|
api_key: str = typer.Option(None, help="Cloud Broker API-KEY", envvar="REMOTIVE_BROKER_API_KEY"),
|
27
|
-
signal: List[str] = typer.Option(..., help="List of signal names to subscribe to"),
|
28
|
-
namespace: str = typer.Option(..., help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
27
|
+
signal: List[str] = typer.Option(..., help="List of signal names to subscribe to in format namespace:signal_name"),
|
28
|
+
# namespace: str = typer.Option(..., help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
29
29
|
on_change_only: bool = typer.Option(default=False, help="Only get signal if value is changed"),
|
30
30
|
output: str = typer.Option(None, help="Write results to file, defaults to stdout"),
|
31
31
|
):
|
@@ -36,13 +36,13 @@ def influxdb(
|
|
36
36
|
This is a sample for exporting and importing to InfluxDB using remotive-cli and influx-cli
|
37
37
|
|
38
38
|
Export:
|
39
|
-
remotive broker export influxdb --url [URL] --output signals.influx
|
40
|
-
--signal Control.SteeringWheel_Position --signal Control.Accelerator_PedalPosition \\
|
41
|
-
--signal GpsPosition.GPS_Longitude --signal GpsPosition.GPS_Latitude
|
39
|
+
remotive broker export influxdb --url [URL] --output signals.influx \\
|
40
|
+
--signal vehiclebus:Control.SteeringWheel_Position --signal Control.Accelerator_PedalPosition \\
|
41
|
+
--signal vehiclebus:GpsPosition.GPS_Longitude --signal vehiclebus:GpsPosition.GPS_Latitude
|
42
42
|
|
43
43
|
Output:
|
44
|
-
Control, SteeringWheel_Position=1.0,Accelerator_PedalPosition=0,Speed=0 1664787032944374000
|
45
|
-
GpsPosition, GPS_Longitude=12.982076,GPS_Latitude=55.618748 1664787032948256000
|
44
|
+
Control, namespace:vehiclebus SteeringWheel_Position=1.0,Accelerator_PedalPosition=0,Speed=0 1664787032944374000
|
45
|
+
GpsPosition, namespace:vehiclebus GPS_Longitude=12.982076,GPS_Latitude=55.618748 1664787032948256000
|
46
46
|
|
47
47
|
Import:
|
48
48
|
influx write --org myorg -b my-bucket -p ns --format=lp -f signals.influx
|
@@ -62,10 +62,12 @@ def influxdb(
|
|
62
62
|
signals = list(x)
|
63
63
|
if len(signals) == 0:
|
64
64
|
return
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
65
|
+
sig: str = signals[0]["name"].rpartition(".")[-1]
|
66
|
+
frame = signals[0]["name"].rsplit(".", 1)[0]
|
67
|
+
# frame_name = signals[0]["name"].split(".")[1]
|
68
|
+
namespace = signals[0]["namespace"]
|
69
|
+
signals_str = ",".join(list(map(lambda s: f'{sig}={s["value"]}', signals)))
|
70
|
+
influx_lp = f"{frame},namespace={namespace} {signals_str} {round(signals[0]['timestamp_us'] * 1000)}"
|
69
71
|
if output is not None:
|
70
72
|
f.write(f"{influx_lp}\n")
|
71
73
|
else:
|
@@ -104,7 +106,17 @@ def influxdb(
|
|
104
106
|
# print(namespace)
|
105
107
|
# signals2 = list(map( lambda s: s['signal'], broker.list_signal_names2(namespace)))
|
106
108
|
try:
|
109
|
+
|
110
|
+
def to_subscribable_signal(sig: str):
|
111
|
+
arr = sig.split(":")
|
112
|
+
if len(arr) != 2:
|
113
|
+
ErrorPrinter.print_hint(f"--signal must have format namespace:signal ({sig})")
|
114
|
+
exit(1)
|
115
|
+
|
116
|
+
return SubscribableSignal(namespace=arr[0], name=arr[1])
|
117
|
+
|
118
|
+
signals_to_subscribe_to = list(map(to_subscribable_signal, signal))
|
107
119
|
broker = Broker(url, api_key)
|
108
|
-
broker.
|
120
|
+
broker.long_name_subscribe(signals_to_subscribe_to, per_frame_influx_line_protocol, on_change_only)
|
109
121
|
except grpc.RpcError as rpc_error:
|
110
122
|
ErrorPrinter.print_grpc_error(rpc_error)
|
cli/broker/lib/broker.py
CHANGED
@@ -12,7 +12,7 @@ import typing
|
|
12
12
|
import zipfile
|
13
13
|
from dataclasses import dataclass
|
14
14
|
from threading import Thread
|
15
|
-
from typing import Callable, List, Sequence
|
15
|
+
from typing import Callable, List, Sequence, Union
|
16
16
|
|
17
17
|
import grpc
|
18
18
|
import remotivelabs.broker.generated.sync.traffic_api_pb2 as traffic_api
|
@@ -33,8 +33,16 @@ class SubscribableSignal:
|
|
33
33
|
namespace: str
|
34
34
|
|
35
35
|
|
36
|
+
@dataclass
|
37
|
+
class LicenseInfo:
|
38
|
+
valid: bool
|
39
|
+
expires: str
|
40
|
+
email: str
|
41
|
+
machine_id: str
|
42
|
+
|
43
|
+
|
36
44
|
class Broker:
|
37
|
-
def __init__(self, url: str, api_key):
|
45
|
+
def __init__(self, url: str, api_key: Union[str, None] = None):
|
38
46
|
self.url = url
|
39
47
|
self.api_key = api_key
|
40
48
|
self.q = queue.Queue()
|
@@ -484,7 +492,12 @@ class Broker:
|
|
484
492
|
return subscription
|
485
493
|
|
486
494
|
def __each_signal(self, signals, callback):
|
487
|
-
callback(
|
495
|
+
callback(
|
496
|
+
map(
|
497
|
+
lambda s: {"timestamp_us": s.timestamp, "namespace": s.id.namespace.name, "name": s.id.name, "value": self.__get_value(s)},
|
498
|
+
signals,
|
499
|
+
)
|
500
|
+
)
|
488
501
|
|
489
502
|
@staticmethod
|
490
503
|
def __get_value(signal):
|
@@ -526,3 +539,19 @@ class Broker:
|
|
526
539
|
playbackConfig=playback_config,
|
527
540
|
playbackMode=br.traffic_api_pb2.PlaybackMode(mode=item["mode"], offsetTime=get_offset_time()),
|
528
541
|
)
|
542
|
+
|
543
|
+
def get_license(self) -> LicenseInfo:
|
544
|
+
license_info = self.system_stub.GetLicenseInfo(br.common_pb2.Empty())
|
545
|
+
return LicenseInfo(
|
546
|
+
valid=license_info.status == br.system_api_pb2.LicenseStatus.VALID,
|
547
|
+
expires=license_info.expires,
|
548
|
+
email=license_info.requestId,
|
549
|
+
machine_id=license_info.requestMachineId.decode("utf-8"),
|
550
|
+
)
|
551
|
+
|
552
|
+
def apply_license(self, license_data_b64: bytes):
|
553
|
+
license = br.system_api_pb2.License()
|
554
|
+
license.data = license_data_b64
|
555
|
+
license.termsAgreement = True
|
556
|
+
self.system_stub.SetLicense(license)
|
557
|
+
return self.get_license()
|
@@ -0,0 +1,167 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
import time
|
5
|
+
from typing import Union
|
6
|
+
|
7
|
+
import grpc
|
8
|
+
import requests
|
9
|
+
import typer
|
10
|
+
from rich.console import Console
|
11
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
12
|
+
|
13
|
+
import cli.cloud.rest_helper as rest
|
14
|
+
from cli.broker.lib.broker import Broker, LicenseInfo
|
15
|
+
|
16
|
+
console = Console(stderr=True)
|
17
|
+
|
18
|
+
|
19
|
+
class LicenseFlow:
|
20
|
+
def describe_with_url(self, url: str) -> LicenseInfo:
|
21
|
+
b = LicenseFlow.__try_connect_to_broker(
|
22
|
+
url=url,
|
23
|
+
progress_label=f"Fetching license from broker ({url})...",
|
24
|
+
on_error_progress_label=f"Fetching license from broker ({url})... make sure broker is running.",
|
25
|
+
)
|
26
|
+
return b.get_license()
|
27
|
+
|
28
|
+
def describe_with_hotspot(self, url: Union[str, None] = "http://192.168.4.1:50051"):
|
29
|
+
if url is None:
|
30
|
+
url = "http://192.168.4.1:50051"
|
31
|
+
|
32
|
+
b = LicenseFlow.__try_connect_to_broker(
|
33
|
+
url=url, progress_label=f"Fetching license from broker using hotspot ({url})... Make sure to switch to remotivelabs-xxx Wi-Fi"
|
34
|
+
)
|
35
|
+
return b.get_license()
|
36
|
+
|
37
|
+
def request_with_url_with_internet(self, url: str):
|
38
|
+
console.print("This requires internet connection from your computer during the entire licensing process")
|
39
|
+
|
40
|
+
email = LicenseFlow.__try_authenticate_and_get_email_from_cloud()
|
41
|
+
|
42
|
+
broker_to_license = LicenseFlow.__try_connect_to_broker(
|
43
|
+
url=url,
|
44
|
+
progress_label=f"Fetching existing broker license...({url})...",
|
45
|
+
)
|
46
|
+
|
47
|
+
existing_license = broker_to_license.get_license()
|
48
|
+
if existing_license.valid:
|
49
|
+
apply = typer.confirm(
|
50
|
+
f"This broker already has a valid license, and is licensed to {existing_license.email}. Do you still wish to proceed?"
|
51
|
+
)
|
52
|
+
if not apply:
|
53
|
+
return
|
54
|
+
|
55
|
+
console.print(
|
56
|
+
":point_right: [bold yellow]This will request a new license or use an existing license if "
|
57
|
+
"this hardware has already been licensed[/bold yellow]"
|
58
|
+
)
|
59
|
+
apply_license = typer.confirm("Do you want to continue and agree to terms and conditions at https://remotivelabs.com/license?")
|
60
|
+
if not apply_license:
|
61
|
+
return
|
62
|
+
|
63
|
+
existing_license = broker_to_license.get_license()
|
64
|
+
license_data_b64 = LicenseFlow.__try_request_license(email, existing_license, "Requesting license...")
|
65
|
+
|
66
|
+
with use_progress("Applying license to broker..."):
|
67
|
+
new_license = broker_to_license.apply_license(license_data_b64)
|
68
|
+
if new_license.valid:
|
69
|
+
console.print(f":thumbsup: Successfully applied license, it remains valid until {new_license.expires}")
|
70
|
+
|
71
|
+
def request_with_hotspot(self, url: Union[str, None] = "http://192.168.4.1:50051"):
|
72
|
+
"""
|
73
|
+
This flow expects changes between networks and tries to guide the user accordingly
|
74
|
+
:param url: If None it will use the default hotspot IP
|
75
|
+
"""
|
76
|
+
if url is None:
|
77
|
+
url = "http://192.168.4.1:50051"
|
78
|
+
|
79
|
+
console.print(
|
80
|
+
"Licensing a broker over its wifi hotspot will require you to switch between internet connectivity "
|
81
|
+
"and remotivelabs-xxx wifi hotspot"
|
82
|
+
)
|
83
|
+
|
84
|
+
email = LicenseFlow.__try_authenticate_and_get_email_from_cloud()
|
85
|
+
|
86
|
+
broker_to_license = LicenseFlow.__try_connect_to_broker(
|
87
|
+
url=url,
|
88
|
+
progress_label=f"Fetching license from broker using hotspot ({url})... Make sure to switch to remotivelabs-xxx wifi",
|
89
|
+
)
|
90
|
+
existing_license = broker_to_license.get_license()
|
91
|
+
if existing_license.valid:
|
92
|
+
apply = typer.confirm(
|
93
|
+
f"This broker already has a valid license and is licensed to {existing_license.email}. Do you still wish to proceed?"
|
94
|
+
)
|
95
|
+
if not apply:
|
96
|
+
return
|
97
|
+
|
98
|
+
console.print(
|
99
|
+
":point_right: [bold yellow]This will request a new license or use an existing license if "
|
100
|
+
"this hardware has already been licensed[/bold yellow]"
|
101
|
+
)
|
102
|
+
apply_license = typer.confirm("Do you want to continue and agree to terms and conditions at https://remotivelabs.com/license?")
|
103
|
+
if not apply_license:
|
104
|
+
return
|
105
|
+
|
106
|
+
license_data_b64 = LicenseFlow.__try_request_license(email, existing_license)
|
107
|
+
|
108
|
+
broker_to_license = LicenseFlow.__try_connect_to_broker(
|
109
|
+
url=url,
|
110
|
+
progress_label="Applying license to broker... Make sure to switch back to remotivelabs-xxx wifi hotspot",
|
111
|
+
)
|
112
|
+
new_license = broker_to_license.apply_license(license_data_b64)
|
113
|
+
console.print(f":thumbsup: Successfully applied license, expires {new_license.expires}")
|
114
|
+
|
115
|
+
@staticmethod
|
116
|
+
def __try_connect_to_broker(url: str, progress_label: str, on_error_progress_label: Union[str, None] = None) -> Broker:
|
117
|
+
with use_progress(progress_label) as p:
|
118
|
+
while True:
|
119
|
+
try:
|
120
|
+
broker_to_license = Broker(url=url)
|
121
|
+
break
|
122
|
+
except grpc.RpcError:
|
123
|
+
if on_error_progress_label is not None:
|
124
|
+
p.update(
|
125
|
+
p.task_ids[0],
|
126
|
+
description=on_error_progress_label,
|
127
|
+
)
|
128
|
+
time.sleep(1)
|
129
|
+
if on_error_progress_label is None:
|
130
|
+
console.print(f":white_check_mark: {progress_label}")
|
131
|
+
else:
|
132
|
+
console.print(f":white_check_mark: {on_error_progress_label}")
|
133
|
+
return broker_to_license
|
134
|
+
|
135
|
+
@staticmethod
|
136
|
+
def __try_authenticate_and_get_email_from_cloud() -> str:
|
137
|
+
with use_progress("Fetching user info from cloud... Make sure you are connected to internet"):
|
138
|
+
while True:
|
139
|
+
try:
|
140
|
+
r = rest.handle_get("/api/whoami", return_response=True, use_progress_indicator=False, timeout=5)
|
141
|
+
break
|
142
|
+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
143
|
+
time.sleep(1)
|
144
|
+
console.print(":white_check_mark: Fetching user info from cloud... Make sure you are connected to internet")
|
145
|
+
return r.json()["email"]
|
146
|
+
|
147
|
+
@staticmethod
|
148
|
+
def __try_request_license(
|
149
|
+
email: str,
|
150
|
+
existing_license: LicenseInfo,
|
151
|
+
progress_label: str = "Requesting license... make sure to switch back to network with internet access",
|
152
|
+
) -> bytes:
|
153
|
+
with use_progress(progress_label):
|
154
|
+
while True:
|
155
|
+
try:
|
156
|
+
license_data_b64 = rest.request_license(email, json.loads(existing_license.machine_id)).encode()
|
157
|
+
break
|
158
|
+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
159
|
+
time.sleep(1)
|
160
|
+
console.print(f":white_check_mark: {progress_label}")
|
161
|
+
return license_data_b64
|
162
|
+
|
163
|
+
|
164
|
+
def use_progress(label: str):
|
165
|
+
p = Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True, expand=False)
|
166
|
+
p.add_task(label, total=1)
|
167
|
+
return p
|
cli/broker/licenses.py
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import json
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
import typer
|
6
|
+
from rich import print_json
|
7
|
+
|
8
|
+
from .license_flows import LicenseFlow
|
9
|
+
|
10
|
+
app = typer.Typer()
|
11
|
+
|
12
|
+
help_text = """
|
13
|
+
More info on our docs page
|
14
|
+
https://docs.remotivelabs.com/docs/remotive-broker/getting-started
|
15
|
+
|
16
|
+
--connect describes how the connection is made to the broker and helps us properly connect to broker
|
17
|
+
|
18
|
+
url = Connects to the broker with the specified url, this is
|
19
|
+
hotspot = If you are using RemotiveBox (our reference hardware) you can connect to the
|
20
|
+
broker over its wifi hotspot. Getting the license from a RemotiveBox over its wifi hotspot
|
21
|
+
requires you switch wi-fi network to the RemotiveBox hotspot called 'remotivelabs-xxx' where
|
22
|
+
'xxx' is a random generated id.
|
23
|
+
|
24
|
+
--url is the broker url, this is mandatory when connect type is "url"
|
25
|
+
"""
|
26
|
+
|
27
|
+
|
28
|
+
class Connection(str, Enum):
|
29
|
+
hotspot = "hotspot"
|
30
|
+
url = "url"
|
31
|
+
# TODO - Support discover using mdns
|
32
|
+
|
33
|
+
|
34
|
+
@app.command()
|
35
|
+
def describe(
|
36
|
+
connect: Connection = typer.Option("url", case_sensitive=False, help="How to connect to broker"),
|
37
|
+
url: str = typer.Option("http://localhost:50051", is_eager=False, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
38
|
+
):
|
39
|
+
"""
|
40
|
+
Show licence information
|
41
|
+
|
42
|
+
More info on our docs page
|
43
|
+
https://docs.remotivelabs.com/docs/remotive-broker/getting-started
|
44
|
+
|
45
|
+
--connect describes how the connection is made to the broker and helps us properly connect to broker
|
46
|
+
|
47
|
+
url = Connects to the broker with the specified url, this is
|
48
|
+
hotspot = If you are using RemotiveBox (our reference hardware) you can connect to the
|
49
|
+
broker over its wifi hotspot. Getting the license from a RemotiveBox over its wifi hotspot
|
50
|
+
requires you switch wi-fi network to the RemotiveBox hotspot called 'remotivelabs-xxx' where
|
51
|
+
'xxx' is a random generated id.
|
52
|
+
|
53
|
+
--url is the broker url, this is mandatory when connect type is "url"
|
54
|
+
"""
|
55
|
+
license_flow = LicenseFlow()
|
56
|
+
if connect == Connection.url:
|
57
|
+
existing_license = license_flow.describe_with_url(url)
|
58
|
+
print_json(json.dumps(dataclasses.asdict(existing_license)))
|
59
|
+
if connect == Connection.hotspot:
|
60
|
+
existing_license = license_flow.describe_with_hotspot(url if url != "http://localhost:50051" else None)
|
61
|
+
print_json(json.dumps(dataclasses.asdict(existing_license)))
|
62
|
+
|
63
|
+
|
64
|
+
@app.command()
|
65
|
+
def request(
|
66
|
+
connect: Connection = typer.Option("url", case_sensitive=False, help="How to connect to broker"),
|
67
|
+
url: str = typer.Option(
|
68
|
+
"http://localhost:50051",
|
69
|
+
is_eager=False,
|
70
|
+
help="Broker url, this is mandatory when connect type is 'url'",
|
71
|
+
envvar="REMOTIVE_BROKER_URL",
|
72
|
+
),
|
73
|
+
):
|
74
|
+
"""
|
75
|
+
Requests and applies a new or existing License to a broker, Note that internet access is required on your
|
76
|
+
computer
|
77
|
+
|
78
|
+
More info on our docs page
|
79
|
+
https://docs.remotivelabs.com/docs/remotive-broker/getting-started
|
80
|
+
|
81
|
+
--connect describes how the connection is made to the broker and helps us properly connect to broker
|
82
|
+
|
83
|
+
url = Use url to connect to broker (use --url)
|
84
|
+
hotspot = If you are using RemotiveBox (our reference hardware) you can connect to the
|
85
|
+
broker over its wifi hotspot. Licensing a broker on a RemotiveBox over its wifi hotspot
|
86
|
+
requires you switch wi-fi network to the RemotiveBox hotspot called 'remotivelabs-xxx' where
|
87
|
+
'xxx' is a random generated id.
|
88
|
+
https://docs.remotivelabs.com/docs/remotive-broker/getting-started/remotive-box
|
89
|
+
|
90
|
+
--url is the broker url, this is mandatory when connect type is "url"
|
91
|
+
"""
|
92
|
+
|
93
|
+
license_flow = LicenseFlow()
|
94
|
+
if connect == Connection.url:
|
95
|
+
license_flow.request_with_url_with_internet(url)
|
96
|
+
if connect == Connection.hotspot:
|
97
|
+
license_flow.request_with_hotspot(url if url != "http://localhost:50051" else None)
|
cli/broker/signals.py
CHANGED
@@ -95,7 +95,8 @@ def subscribe( # noqa: C901
|
|
95
95
|
|
96
96
|
if script is not None:
|
97
97
|
if len(signal) > 0:
|
98
|
-
ErrorPrinter.print_generic_error("You must must not specify
|
98
|
+
ErrorPrinter.print_generic_error("You must must not specify --signal when using --script")
|
99
|
+
exit(1)
|
99
100
|
|
100
101
|
plt.title("Signals")
|
101
102
|
|
cli/cloud/recordings.py
CHANGED
@@ -459,8 +459,7 @@ def _do_change_playback_mode(mode: str, recording_session: str, broker: str, pro
|
|
459
459
|
if json_context["recordingSessionId"] != recording_session:
|
460
460
|
ErrorPrinter.print_generic_error(
|
461
461
|
f"The recording id mounted is '{json_context['recordingSessionId']}' "
|
462
|
-
f"which
|
463
|
-
"use cmd below to mount this recording"
|
462
|
+
f"which not the same as you are trying to {mode}, use cmd below to mount this recording"
|
464
463
|
)
|
465
464
|
ErrorPrinter.print_hint(f"remotive cloud recordings mount {recording_session} --project {project}")
|
466
465
|
exit(1)
|
cli/cloud/recordings_playback.py
CHANGED
@@ -3,16 +3,18 @@ from __future__ import annotations
|
|
3
3
|
import datetime
|
4
4
|
import json
|
5
5
|
import tempfile
|
6
|
-
from
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import List, Union
|
7
8
|
|
8
9
|
import grpc
|
9
10
|
import rich
|
10
11
|
import typer
|
12
|
+
from rich import print as rich_rprint
|
11
13
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
12
14
|
|
13
15
|
from cli.errors import ErrorPrinter
|
14
16
|
|
15
|
-
from ..broker.lib.broker import Broker
|
17
|
+
from ..broker.lib.broker import Broker, SubscribableSignal
|
16
18
|
from . import rest_helper as rest
|
17
19
|
|
18
20
|
app = typer.Typer(
|
@@ -89,6 +91,72 @@ def stop(
|
|
89
91
|
_do_change_playback_mode("stop", recording_session, broker, project)
|
90
92
|
|
91
93
|
|
94
|
+
# Copied from signals.py
|
95
|
+
def read_scripted_code_file(file_path: Path) -> bytes:
|
96
|
+
# typer checks that the Path exists
|
97
|
+
with open(file_path, "rb") as file:
|
98
|
+
return file.read()
|
99
|
+
|
100
|
+
|
101
|
+
@app.command()
|
102
|
+
def subscribe(
|
103
|
+
recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
104
|
+
broker: str = typer.Option(None, help="Broker to use"),
|
105
|
+
signal: List[str] = typer.Option(None, help="Signal names to subscribe to, mandatory when not using script"),
|
106
|
+
script: Path = typer.Option(
|
107
|
+
None,
|
108
|
+
exists=True,
|
109
|
+
file_okay=True,
|
110
|
+
dir_okay=False,
|
111
|
+
writable=False,
|
112
|
+
readable=True,
|
113
|
+
resolve_path=True,
|
114
|
+
help="Supply a path to Lua script that to use for signal transformation",
|
115
|
+
),
|
116
|
+
on_change_only: bool = typer.Option(default=False, help="Only get signal if value is changed"),
|
117
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
118
|
+
):
|
119
|
+
"""
|
120
|
+
Allows you to subscribe to signals based on a mounted recording without knowing the broker URL.
|
121
|
+
This simplifies when playing recordings from the cloud.
|
122
|
+
|
123
|
+
Terminal plotting is not yet supported here so we refer to remotive broker signals subscribe --x-plot for this.
|
124
|
+
"""
|
125
|
+
if script is None:
|
126
|
+
if len(signal) == 0:
|
127
|
+
ErrorPrinter.print_generic_error("You must use include at least one signal and one namespace or use script when subscribing")
|
128
|
+
exit(1)
|
129
|
+
if script is not None:
|
130
|
+
if len(signal) > 0:
|
131
|
+
ErrorPrinter.print_generic_error("You must must not specify --signal when using --script")
|
132
|
+
exit(1)
|
133
|
+
|
134
|
+
broker_client = _get_broker_info(project, recording_session, broker, "subscribe")
|
135
|
+
|
136
|
+
try:
|
137
|
+
if script is not None:
|
138
|
+
script_src = read_scripted_code_file(script)
|
139
|
+
broker_client.subscribe_on_script(script_src, lambda sig: rich_rprint(json.dumps(list(sig))), on_change_only)
|
140
|
+
else:
|
141
|
+
|
142
|
+
def to_subscribable_signal(sig: str):
|
143
|
+
arr = sig.split(":")
|
144
|
+
if len(arr) != 2:
|
145
|
+
ErrorPrinter.print_hint(f"--signal must have format namespace:signal ({sig})")
|
146
|
+
exit(1)
|
147
|
+
return SubscribableSignal(namespace=arr[0], name=arr[1])
|
148
|
+
|
149
|
+
signals_to_subscribe_to = list(map(to_subscribable_signal, signal))
|
150
|
+
broker_client.long_name_subscribe(signals_to_subscribe_to, lambda sig: rich_rprint(json.dumps(list(sig))), on_change_only)
|
151
|
+
print("Subscribing to signals, press Ctrl+C to exit")
|
152
|
+
except grpc.RpcError as rpc_error:
|
153
|
+
ErrorPrinter.print_grpc_error(rpc_error)
|
154
|
+
|
155
|
+
except Exception as e:
|
156
|
+
ErrorPrinter.print_generic_error(e)
|
157
|
+
exit(1)
|
158
|
+
|
159
|
+
|
92
160
|
def _do_change_playback_mode(
|
93
161
|
mode: str,
|
94
162
|
recording_session: str,
|
@@ -103,10 +171,8 @@ def _do_change_playback_mode(
|
|
103
171
|
recordings: list = r["recordings"]
|
104
172
|
files = list(map(lambda rec: {"recording": rec["fileName"], "namespace": rec["metadata"]["namespace"]}, recordings))
|
105
173
|
|
106
|
-
if broker is not None
|
107
|
-
|
108
|
-
else:
|
109
|
-
response = rest.handle_get(f"/api/project/{project}/brokers/personal", return_response=True, allow_status_codes=[404])
|
174
|
+
broker_name = broker if broker is not None else "personal"
|
175
|
+
response = rest.handle_get(f"/api/project/{project}/brokers/{broker_name}", return_response=True, allow_status_codes=[404])
|
110
176
|
if response.status_code == 404:
|
111
177
|
broker_arg = ""
|
112
178
|
if broker is not None:
|
@@ -164,8 +230,7 @@ def _verify_recording_on_broker(broker: Broker, recording_session: str, mode: st
|
|
164
230
|
if json_context["recordingSessionId"] != recording_session:
|
165
231
|
ErrorPrinter.print_generic_error(
|
166
232
|
f"The recording id mounted is '{json_context['recordingSessionId']}' "
|
167
|
-
f"which
|
168
|
-
"use cmd below to mount this recording"
|
233
|
+
f"which not the same as you are trying to {mode}, use cmd below to mount this recording"
|
169
234
|
)
|
170
235
|
ErrorPrinter.print_hint(f"remotive cloud recordings mount {recording_session} --project {project}")
|
171
236
|
exit(1)
|
@@ -176,3 +241,22 @@ def _verify_recording_on_broker(broker: Broker, recording_session: str, mode: st
|
|
176
241
|
else:
|
177
242
|
ErrorPrinter.print_grpc_error(rpc_error)
|
178
243
|
exit(1)
|
244
|
+
|
245
|
+
|
246
|
+
def _get_broker_info(project: str, recording_session: str, broker: Union[str, None], mode: str) -> Broker:
|
247
|
+
# Verify it exists
|
248
|
+
rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
|
249
|
+
|
250
|
+
broker_name = broker if broker is not None else "personal"
|
251
|
+
response = rest.handle_get(f"/api/project/{project}/brokers/{broker_name}", return_response=True, allow_status_codes=[404])
|
252
|
+
if response.status_code == 404:
|
253
|
+
broker_arg = ""
|
254
|
+
if broker is not None:
|
255
|
+
broker_arg = f"--broker {broker} --ensure-broker-started"
|
256
|
+
ErrorPrinter.print_generic_error("You need to mount the recording before you play")
|
257
|
+
ErrorPrinter.print_hint(f"remotive cloud recordings mount {recording_session} {broker_arg} --project {project}")
|
258
|
+
exit(1)
|
259
|
+
broker_info = json.loads(response.text)
|
260
|
+
broker_client = Broker(broker_info["url"], None)
|
261
|
+
_verify_recording_on_broker(broker_client, recording_session, mode, project)
|
262
|
+
return broker_client
|
cli/cloud/rest_helper.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import base64
|
3
4
|
import json
|
4
5
|
import logging
|
5
6
|
import os
|
6
7
|
import shutil
|
7
8
|
from importlib.metadata import version
|
8
9
|
from pathlib import Path
|
9
|
-
from typing import List, Union
|
10
|
+
from typing import Dict, List, Union
|
10
11
|
|
11
12
|
import requests
|
12
13
|
from rich.console import Console
|
@@ -23,11 +24,16 @@ if "REMOTIVE_CLOUD_HTTP_LOGGING" in os.environ:
|
|
23
24
|
requests_log.setLevel(logging.DEBUG)
|
24
25
|
requests_log.propagate = True
|
25
26
|
global baseurl
|
27
|
+
global license_server_base_url
|
26
28
|
base_url = "https://cloud.remotivelabs.com"
|
29
|
+
license_server_base_url = "https://license.cloud.remotivelabs.com"
|
27
30
|
|
28
31
|
if "REMOTIVE_CLOUD_BASE_URL" in os.environ:
|
29
32
|
base_url = os.environ["REMOTIVE_CLOUD_BASE_URL"]
|
30
33
|
|
34
|
+
if "cloud-dev" in base_url:
|
35
|
+
license_server_base_url = "https://license.cloud-dev.remotivelabs.com"
|
36
|
+
|
31
37
|
# if 'REMOTIVE_CLOUD_AUTH_TOKEN' not in os.environ:
|
32
38
|
# print('export REMOTIVE_CLOUD_AUTH_TOKEN=auth must be set')
|
33
39
|
# exit(0)
|
@@ -65,12 +71,23 @@ def ensure_auth_token():
|
|
65
71
|
headers["User-Agent"] = f"remotivelabs-cli {cli_version}"
|
66
72
|
|
67
73
|
|
68
|
-
def handle_get(
|
74
|
+
def handle_get(
|
75
|
+
url,
|
76
|
+
params=None,
|
77
|
+
return_response: bool = False,
|
78
|
+
allow_status_codes=None,
|
79
|
+
progress_label="Fetching...",
|
80
|
+
use_progress_indicator: bool = True,
|
81
|
+
timeout: int = 20,
|
82
|
+
):
|
69
83
|
if params is None:
|
70
84
|
params = {}
|
71
85
|
ensure_auth_token()
|
72
|
-
|
73
|
-
|
86
|
+
if use_progress_indicator:
|
87
|
+
with use_progress(progress_label):
|
88
|
+
r = requests.get(f"{base_url}{url}", headers=headers, params=params, timeout=timeout)
|
89
|
+
else:
|
90
|
+
r = requests.get(f"{base_url}{url}", headers=headers, params=params, timeout=timeout)
|
74
91
|
|
75
92
|
if return_response:
|
76
93
|
check_api_result(r, allow_status_codes)
|
@@ -192,3 +209,18 @@ def download_file(save_file_name: str, url: str):
|
|
192
209
|
shutil.copyfileobj(stream_with_progress, out_file)
|
193
210
|
else:
|
194
211
|
check_api_result(download_resp)
|
212
|
+
|
213
|
+
|
214
|
+
def request_license(email: str, machine_id: Dict[str, any]) -> str:
|
215
|
+
# Lets keep the email here so we have the same interface for both authenticated
|
216
|
+
# and not authenticated license requests.
|
217
|
+
# email will be validated in the license server to make sure it matches with the user of the
|
218
|
+
# access token so not any email is sent here
|
219
|
+
ensure_auth_token()
|
220
|
+
payload = {"id": email, "machine_id": machine_id}
|
221
|
+
b64_encoded_bytes = base64.encodebytes(json.dumps(payload).encode())
|
222
|
+
license_jsonb64 = {"licensejsonb64": b64_encoded_bytes.decode("utf-8")}
|
223
|
+
headers["content-type"] = "application/json"
|
224
|
+
r = requests.post(url=f"{license_server_base_url}/api/license/request", headers=headers, data=json.dumps(license_jsonb64))
|
225
|
+
check_api_result(r)
|
226
|
+
return r.json()["license_data"]
|
@@ -1,14 +1,16 @@
|
|
1
1
|
cli/__about__.py,sha256=qXVkxWb3aPCF-4MjQhB0wqL2GEblEH4Qwk70o29UkJk,122
|
2
2
|
cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
cli/broker/brokers.py,sha256=
|
4
|
-
cli/broker/export.py,sha256=
|
3
|
+
cli/broker/brokers.py,sha256=jVbaiCnFoCxQBGhidePg6GxDhurzXtbn8mV59HkzAc4,3181
|
4
|
+
cli/broker/export.py,sha256=Xhpz2P32VMow7DujImCzhiP-WqJvdR1-THErRV_4LPU,4363
|
5
5
|
cli/broker/files.py,sha256=_8elBjbmJ5MEEeFGJ7QYkXzyuLDCDpO6UvEVXHbRy7U,4133
|
6
6
|
cli/broker/lib/__about__.py,sha256=xnZ5V6ZcHW9dhWLWdMzVjYJbEnMKpeXm0_S_mbNzypE,141
|
7
|
-
cli/broker/lib/broker.py,sha256=
|
7
|
+
cli/broker/lib/broker.py,sha256=uolp_AaC3Z_pGeF4rP28FvHvtSRUJFuT5Dxe8aLhdgI,21957
|
8
|
+
cli/broker/license_flows.py,sha256=AaKvZgy_hkP5Mv-1dXtQxQxXGidpvuVDVY3jP2AX0t0,7101
|
9
|
+
cli/broker/licenses.py,sha256=iJeF6aWKUPhXb24t0pyufFRmMGNCFo-G_ZUz1rstqqs,3957
|
8
10
|
cli/broker/playback.py,sha256=oOfC8Jn4Ib-nc9T6ob_uNXZSeCWfft7MrMQPafH4U2I,4846
|
9
11
|
cli/broker/record.py,sha256=gEvo3myHbIl6UyXzhJE741NiwRrFf7doBg6HXzzp5z0,1382
|
10
12
|
cli/broker/scripting.py,sha256=sLDtuktWsVk0fJ3RW4kYyh-_YAVJP3VM0xFIQR499Oo,3392
|
11
|
-
cli/broker/signals.py,sha256=
|
13
|
+
cli/broker/signals.py,sha256=py_qOwTP5ongpOVLKznMfVPw68iB--1eIvnRdOLND7k,6556
|
12
14
|
cli/cloud/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
13
15
|
cli/cloud/auth.py,sha256=xi47cQWjs6q7Kje4XUbTqEk_BgG3NJ1gMOYPU-_4tgw,3470
|
14
16
|
cli/cloud/auth_tokens.py,sha256=5ox0Pxfij8aTgEcDwRBasaSQvDuLIGCJc-INjmIQN2M,3408
|
@@ -16,9 +18,9 @@ cli/cloud/brokers.py,sha256=wa3uMg91IZdrP0tMpTdO9cBIkZHtMHxQ-zEXwFiye_I,4127
|
|
16
18
|
cli/cloud/cloud_cli.py,sha256=nRRXFF_IgUMayerxS-h7oqsNe6tt34Q5VeThq8gatEg,1443
|
17
19
|
cli/cloud/configs.py,sha256=2p1mCHf5BwYNtwbY0Cbed5t6-79WHGKWU4Fv6LuJ21o,4069
|
18
20
|
cli/cloud/projects.py,sha256=-uqltAOficwprOKaPd2R0Itm4sqTz3VJNs9Sc8jtO5k,1369
|
19
|
-
cli/cloud/recordings.py,sha256=
|
20
|
-
cli/cloud/recordings_playback.py,sha256=
|
21
|
-
cli/cloud/rest_helper.py,sha256=
|
21
|
+
cli/cloud/recordings.py,sha256=xbdqUYVmKyChV24cASK3zNPkmCPwIUmX9H5LsG98zfI,20820
|
22
|
+
cli/cloud/recordings_playback.py,sha256=OXRvoE7Zc_jQrofpqjbWYKZ6pfhJVHBe0bQ9GEfv64Q,10879
|
23
|
+
cli/cloud/rest_helper.py,sha256=WK5We-vWg0GwnTq37OyKVuQTc58RFkPuA0kq5wjDtCk,7769
|
22
24
|
cli/cloud/sample_recordings.py,sha256=g1X6JTxvzWInSP9R1BJsDmL4WqvpEKqjdJR_xT4bo1U,639
|
23
25
|
cli/cloud/service_account_tokens.py,sha256=7vjoMd6Xq7orWCUP7TVUVa86JA0OiX8O10NZcHUE6rM,2294
|
24
26
|
cli/cloud/service_accounts.py,sha256=GCYdYPnP5uWVsg1bTIS67CmoPWDng5dupJHmlThrJ80,1606
|
@@ -33,8 +35,8 @@ cli/tools/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
33
35
|
cli/tools/can/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
34
36
|
cli/tools/can/can.py,sha256=kSd1c-nxxXyeKkm19oDILiDBZsKOcpjsUT0T3xox5Qs,2172
|
35
37
|
cli/tools/tools.py,sha256=LwQdWMcJ19pCyKUsVfSB2B3R6ui61NxxFWP0Nrnd5Jk,198
|
36
|
-
remotivelabs_cli-0.0.
|
37
|
-
remotivelabs_cli-0.0.
|
38
|
-
remotivelabs_cli-0.0.
|
39
|
-
remotivelabs_cli-0.0.
|
40
|
-
remotivelabs_cli-0.0.
|
38
|
+
remotivelabs_cli-0.0.17.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
|
39
|
+
remotivelabs_cli-0.0.17.dist-info/METADATA,sha256=UXlSn4wNyCgJU9YgNVYEOUdqskNhdvkd5uLRtY_-eqk,1224
|
40
|
+
remotivelabs_cli-0.0.17.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
41
|
+
remotivelabs_cli-0.0.17.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
|
42
|
+
remotivelabs_cli-0.0.17.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|