edgelens-robot 0.2.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.
- edgelens_robot-0.2.1/.gitignore +12 -0
- edgelens_robot-0.2.1/EdgeLensLibrary/__init__.py +14 -0
- edgelens_robot-0.2.1/EdgeLensLibrary/_version.py +1 -0
- edgelens_robot-0.2.1/EdgeLensLibrary/library.py +328 -0
- edgelens_robot-0.2.1/LICENSE +21 -0
- edgelens_robot-0.2.1/PKG-INFO +162 -0
- edgelens_robot-0.2.1/README.md +136 -0
- edgelens_robot-0.2.1/pyproject.toml +51 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Robot Framework library for the EdgeLens Edge AI Device Farm.
|
|
2
|
+
|
|
3
|
+
Import in a Robot suite via ``Library EdgeLensLibrary``. Keywords
|
|
4
|
+
mirror the Python SDK's surface — session lifecycle (Request Device /
|
|
5
|
+
Release Device), file ops (Deploy Model), the display
|
|
6
|
+
namespace (Play On Display / Show On Display / ...), the inference
|
|
7
|
+
namespace (Start Inference / Run Inference / ...), and helpers (Get
|
|
8
|
+
SSH Details / Execute On Device / Get Device Info).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from EdgeLensLibrary._version import __version__ as __version__
|
|
12
|
+
from EdgeLensLibrary.library import EdgeLensLibrary
|
|
13
|
+
|
|
14
|
+
__all__ = ["EdgeLensLibrary"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.1"
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""``EdgeLensLibrary`` — the keyword-driven Robot Framework surface.
|
|
2
|
+
|
|
3
|
+
Designed for the SUITE scope (one device per suite by default — cheaper
|
|
4
|
+
than per-test allocation, matches the pytest plugin's session scope).
|
|
5
|
+
Pass ``scope=TEST`` at import time for per-test sessions when tests
|
|
6
|
+
mutate device state.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
*** Settings ***
|
|
11
|
+
Library EdgeLensLibrary api_key=%{EDGELENS_API_KEY}
|
|
12
|
+
|
|
13
|
+
*** Test Cases ***
|
|
14
|
+
Camera Renders The Test Pattern
|
|
15
|
+
Request Device os=stock-os
|
|
16
|
+
Deploy Model ${MODEL_PATH}
|
|
17
|
+
Start Inference
|
|
18
|
+
${stats}= Run Inference duration=30
|
|
19
|
+
Should Be True ${stats.fps.avg} > 15
|
|
20
|
+
[Teardown] Release Device
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
from typing import Any, Optional
|
|
27
|
+
|
|
28
|
+
from edgelens import EdgeLensClient, Session
|
|
29
|
+
from edgelens.errors import EdgeLensError
|
|
30
|
+
from robot.api import logger as robot_logger
|
|
31
|
+
from robot.api.deco import keyword, library
|
|
32
|
+
|
|
33
|
+
from EdgeLensLibrary._version import __version__
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@library(scope="SUITE", auto_keywords=False, version=__version__)
|
|
37
|
+
class EdgeLensLibrary:
|
|
38
|
+
"""Keyword-driven Robot Framework interface to the EdgeLens SDK.
|
|
39
|
+
|
|
40
|
+
Constructor arguments (set on the ``Library EdgeLensLibrary`` line):
|
|
41
|
+
|
|
42
|
+
- ``api_key``: API key. Defaults to ``$EDGELENS_API_KEY``.
|
|
43
|
+
- ``base_url``: Override the platform base URL.
|
|
44
|
+
- ``scope``: Library scope — ``SUITE`` (default), ``TEST``, or ``GLOBAL``.
|
|
45
|
+
|
|
46
|
+
Set scope via the import line:
|
|
47
|
+
|
|
48
|
+
::
|
|
49
|
+
|
|
50
|
+
Library EdgeLensLibrary scope=TEST
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
ROBOT_LIBRARY_VERSION = __version__
|
|
54
|
+
ROBOT_AUTO_KEYWORDS = False
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
api_key: Optional[str] = None,
|
|
59
|
+
base_url: Optional[str] = None,
|
|
60
|
+
scope: str = "SUITE",
|
|
61
|
+
):
|
|
62
|
+
# Robot translates the keyword scope into ROBOT_LIBRARY_SCOPE
|
|
63
|
+
# at class level; @library(scope=...) handles the static case
|
|
64
|
+
# but we expose it via constructor for late binding too.
|
|
65
|
+
self.__class__.ROBOT_LIBRARY_SCOPE = scope.upper() # type: ignore[attr-defined]
|
|
66
|
+
|
|
67
|
+
self._api_key = api_key or os.environ.get("EDGELENS_API_KEY")
|
|
68
|
+
self._base_url = base_url
|
|
69
|
+
self._client: Optional[EdgeLensClient] = None
|
|
70
|
+
self._session: Optional[Session] = None
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# Internal plumbing
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def _ensure_client(self) -> EdgeLensClient:
|
|
77
|
+
if self._client is None:
|
|
78
|
+
kwargs: dict[str, Any] = {}
|
|
79
|
+
if self._api_key:
|
|
80
|
+
kwargs["api_key"] = self._api_key
|
|
81
|
+
if self._base_url:
|
|
82
|
+
kwargs["base_url"] = self._base_url
|
|
83
|
+
self._client = EdgeLensClient(**kwargs)
|
|
84
|
+
return self._client
|
|
85
|
+
|
|
86
|
+
def _require_session(self) -> Session:
|
|
87
|
+
if self._session is None:
|
|
88
|
+
raise EdgeLensError(
|
|
89
|
+
"No active device session. Call `Request Device` first."
|
|
90
|
+
)
|
|
91
|
+
return self._session
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# Session lifecycle
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
@keyword("Request Device")
|
|
98
|
+
def request_device(
|
|
99
|
+
self,
|
|
100
|
+
device_type: str = "recamera-2002",
|
|
101
|
+
os: str = "stock-os",
|
|
102
|
+
timeout: int = 3600,
|
|
103
|
+
wait_timeout: int = 300,
|
|
104
|
+
) -> str:
|
|
105
|
+
"""Allocate a device and wait for ``status=ready``.
|
|
106
|
+
|
|
107
|
+
Returns the session id. The allocated session is stored on the
|
|
108
|
+
library instance — subsequent keywords (`Deploy Model`,
|
|
109
|
+
`Execute On Device`, ...) target it implicitly.
|
|
110
|
+
|
|
111
|
+
Example::
|
|
112
|
+
|
|
113
|
+
Request Device device_type=recamera-2002 os=stock-os
|
|
114
|
+
"""
|
|
115
|
+
client = self._ensure_client()
|
|
116
|
+
try:
|
|
117
|
+
session = client.create_session(
|
|
118
|
+
device_type=device_type, os=os, timeout=int(timeout),
|
|
119
|
+
)
|
|
120
|
+
except ValueError as e:
|
|
121
|
+
raise EdgeLensError(f"Request Device failed: {e}") from e
|
|
122
|
+
# Until self._session is set, nothing else can release this billed
|
|
123
|
+
# session. BaseException so a Ctrl-C during the silent wait still
|
|
124
|
+
# releases it.
|
|
125
|
+
try:
|
|
126
|
+
session.wait_ready(timeout=int(wait_timeout))
|
|
127
|
+
except BaseException:
|
|
128
|
+
try:
|
|
129
|
+
session.release()
|
|
130
|
+
except Exception as release_exc: # noqa: BLE001
|
|
131
|
+
robot_logger.warn(
|
|
132
|
+
f"Release of session {session.id} failed after Request "
|
|
133
|
+
f"Device did not complete: {release_exc} (original error "
|
|
134
|
+
"follows)"
|
|
135
|
+
)
|
|
136
|
+
raise
|
|
137
|
+
self._session = session
|
|
138
|
+
return session.id
|
|
139
|
+
|
|
140
|
+
@keyword("Release Device")
|
|
141
|
+
def release_device(self) -> None:
|
|
142
|
+
"""End the active session and return the device to the pool.
|
|
143
|
+
|
|
144
|
+
No-op when there's no active session — safe to call from
|
|
145
|
+
``[Teardown]`` even on failed setup.
|
|
146
|
+
"""
|
|
147
|
+
if self._session is None:
|
|
148
|
+
return
|
|
149
|
+
try:
|
|
150
|
+
self._session.release()
|
|
151
|
+
finally:
|
|
152
|
+
self._session = None
|
|
153
|
+
|
|
154
|
+
@keyword("Get Session Id")
|
|
155
|
+
def get_session_id(self) -> str:
|
|
156
|
+
return self._require_session().id
|
|
157
|
+
|
|
158
|
+
@keyword("Get Session Status")
|
|
159
|
+
def get_session_status(self) -> str:
|
|
160
|
+
session = self._require_session()
|
|
161
|
+
session.refresh()
|
|
162
|
+
return session.status.value
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
# Device operations
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
@keyword("Execute On Device")
|
|
169
|
+
def execute_on_device(self, command: str, timeout: float = 60.0) -> dict:
|
|
170
|
+
"""Run ``command`` via SSH (proxied). Returns a dict with
|
|
171
|
+
``stdout``, ``stderr``, ``exit_code``. Raises on transport
|
|
172
|
+
failure (use ``Run Keyword And Ignore Error`` if you want soft
|
|
173
|
+
handling).
|
|
174
|
+
"""
|
|
175
|
+
result = self._require_session().device.exec(command, timeout=float(timeout))
|
|
176
|
+
return {
|
|
177
|
+
"stdout": result.stdout,
|
|
178
|
+
"stderr": result.stderr,
|
|
179
|
+
"exit_code": result.exit_code,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@keyword("Deploy Model")
|
|
183
|
+
def deploy_model(
|
|
184
|
+
self,
|
|
185
|
+
model: str,
|
|
186
|
+
config: Optional[str] = None,
|
|
187
|
+
files: Optional[list] = None,
|
|
188
|
+
) -> dict:
|
|
189
|
+
"""Deploy a model file (and optional config + extra files) to the device.
|
|
190
|
+
|
|
191
|
+
On reCamaginedOS, files land in /userdata/Models/ readable but not
|
|
192
|
+
executable; ``chmod +x`` a deployed binary before running it.
|
|
193
|
+
"""
|
|
194
|
+
return dict(
|
|
195
|
+
self._require_session().device.deploy(model=model, config=config, files=files)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@keyword("Get SSH Details")
|
|
199
|
+
def get_ssh_details(self) -> dict:
|
|
200
|
+
"""Return SSH connection details (user/host/port).
|
|
201
|
+
|
|
202
|
+
The password is NOT included by
|
|
203
|
+
default. Robot Framework's log.html / output.xml record
|
|
204
|
+
keyword return values verbatim, so returning the password
|
|
205
|
+
here would leak it to CI artifacts.
|
|
206
|
+
|
|
207
|
+
Use ``Set SSH Password Variable`` to make the password available
|
|
208
|
+
to the suite without writing it to the log.
|
|
209
|
+
"""
|
|
210
|
+
creds = self._require_session().device.ssh()
|
|
211
|
+
return {
|
|
212
|
+
"user": creds.user,
|
|
213
|
+
"host": creds.host,
|
|
214
|
+
"port": creds.port,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@keyword("Set SSH Password Variable", tags=["secret"])
|
|
218
|
+
def set_ssh_password_variable(
|
|
219
|
+
self, name: str = "${SSH_PASSWORD}", scope: str = "TEST",
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Store the SSH password in a variable without logging its value.
|
|
222
|
+
|
|
223
|
+
``scope`` is ``TEST`` (or ``TASK``), ``SUITE`` or ``GLOBAL``.
|
|
224
|
+
Example::
|
|
225
|
+
|
|
226
|
+
Set SSH Password Variable
|
|
227
|
+
SSHLibrary.Login ${user} ${SSH_PASSWORD}
|
|
228
|
+
|
|
229
|
+
Robot logs keyword arguments only at TRACE level, so passing the
|
|
230
|
+
variable on keeps the value out of log.html and output.xml at the
|
|
231
|
+
default INFO level. Do not run with ``--loglevel TRACE`` or
|
|
232
|
+
``DEBUG`` when the log is published.
|
|
233
|
+
"""
|
|
234
|
+
from robot.libraries.BuiltIn import BuiltIn
|
|
235
|
+
from robot.utils import escape
|
|
236
|
+
|
|
237
|
+
setters = {
|
|
238
|
+
"TEST": "set_test_variable",
|
|
239
|
+
"TASK": "set_test_variable",
|
|
240
|
+
"SUITE": "set_suite_variable",
|
|
241
|
+
"GLOBAL": "set_global_variable",
|
|
242
|
+
}
|
|
243
|
+
setter = setters.get(scope.upper())
|
|
244
|
+
if setter is None:
|
|
245
|
+
raise ValueError(f"scope must be one of {', '.join(setters)}")
|
|
246
|
+
password = self._require_session().device.ssh().password
|
|
247
|
+
builtin = BuiltIn()
|
|
248
|
+
# BuiltIn's Set * Variable keywords log "${name} = <value>" at INFO;
|
|
249
|
+
# suppress logging while the value is set.
|
|
250
|
+
previous = builtin.set_log_level("NONE")
|
|
251
|
+
try:
|
|
252
|
+
getattr(builtin, setter)(name, escape(password))
|
|
253
|
+
finally:
|
|
254
|
+
builtin.set_log_level(previous)
|
|
255
|
+
|
|
256
|
+
@keyword("Get SSH Password", tags=["secret"])
|
|
257
|
+
def get_ssh_password(self) -> str:
|
|
258
|
+
"""Return the SSH password as a string.
|
|
259
|
+
|
|
260
|
+
Robot logs the assignment ``${pw}= Get SSH Password`` at INFO
|
|
261
|
+
level, value included, so the password lands in log.html and
|
|
262
|
+
output.xml. Use ``Set SSH Password Variable`` instead.
|
|
263
|
+
"""
|
|
264
|
+
return self._require_session().device.ssh().password
|
|
265
|
+
|
|
266
|
+
@keyword("Get Device Info")
|
|
267
|
+
def get_device_info(self) -> dict:
|
|
268
|
+
info = self._require_session().device.info()
|
|
269
|
+
if hasattr(info, "model_dump"):
|
|
270
|
+
return info.model_dump()
|
|
271
|
+
return dict(getattr(info, "__dict__", {}))
|
|
272
|
+
|
|
273
|
+
@keyword("Get Stream URL")
|
|
274
|
+
def get_stream_url(self) -> str:
|
|
275
|
+
return self._require_session().device.stream_url()
|
|
276
|
+
|
|
277
|
+
# ------------------------------------------------------------------
|
|
278
|
+
# Display
|
|
279
|
+
# ------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
@keyword("Play On Display")
|
|
282
|
+
def play_on_display(self, source: str, loop: bool = False) -> None:
|
|
283
|
+
self._require_session().device.display.play(source, loop=bool(loop))
|
|
284
|
+
|
|
285
|
+
@keyword("Show On Display")
|
|
286
|
+
def show_on_display(self, source: str) -> None:
|
|
287
|
+
self._require_session().device.display.show(source)
|
|
288
|
+
|
|
289
|
+
@keyword("Stop Display")
|
|
290
|
+
def stop_display(self) -> None:
|
|
291
|
+
self._require_session().device.display.stop()
|
|
292
|
+
|
|
293
|
+
@keyword("Get Display Status")
|
|
294
|
+
def get_display_status(self) -> dict:
|
|
295
|
+
return dict(self._require_session().device.display.status())
|
|
296
|
+
|
|
297
|
+
# ------------------------------------------------------------------
|
|
298
|
+
# Inference
|
|
299
|
+
# ------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
@keyword("Start Inference")
|
|
302
|
+
def start_inference(self) -> None:
|
|
303
|
+
self._require_session().device.inference.start()
|
|
304
|
+
|
|
305
|
+
@keyword("Stop Inference")
|
|
306
|
+
def stop_inference(self) -> None:
|
|
307
|
+
self._require_session().device.inference.stop()
|
|
308
|
+
|
|
309
|
+
@keyword("Run Inference Once")
|
|
310
|
+
def run_inference_once(self) -> dict:
|
|
311
|
+
return dict(self._require_session().device.inference.once())
|
|
312
|
+
|
|
313
|
+
@keyword("Get Inference Stats")
|
|
314
|
+
def get_inference_stats(self) -> dict:
|
|
315
|
+
snap = self._require_session().device.inference.snapshot()
|
|
316
|
+
if hasattr(snap, "model_dump"):
|
|
317
|
+
return snap.model_dump()
|
|
318
|
+
return dict(getattr(snap, "__dict__", {}))
|
|
319
|
+
|
|
320
|
+
@keyword("Run Inference")
|
|
321
|
+
def run_inference(self, duration: float = 30.0, collect_frames: bool = False) -> dict:
|
|
322
|
+
"""Run inference for ``duration`` seconds, return aggregated stats."""
|
|
323
|
+
stats = self._require_session().device.inference.run(
|
|
324
|
+
duration=float(duration), collect_frames=bool(collect_frames)
|
|
325
|
+
)
|
|
326
|
+
if hasattr(stats, "model_dump"):
|
|
327
|
+
return stats.model_dump()
|
|
328
|
+
return dict(getattr(stats, "__dict__", {}))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EdgeLens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: edgelens-robot
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Robot Framework library for the EdgeLens Edge AI Device Farm.
|
|
5
|
+
Project-URL: Homepage, https://edgelens.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/edgelens-ai/edgelens-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/edgelens-ai/edgelens-sdk/issues
|
|
8
|
+
Author-email: EdgeLens <support@edgelens.ai>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: edgelens,robotframework,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Robot Framework
|
|
14
|
+
Classifier: Framework :: Robot Framework :: Library
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: edgelens-sdk<0.3,>=0.2.1
|
|
20
|
+
Requires-Dist: robotframework>=6.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# edgelens-robot
|
|
28
|
+
|
|
29
|
+
Robot Framework library for the EdgeLens Edge AI Device Farm. Keyword-
|
|
30
|
+
driven wrapper around the Python SDK — write `Request Device`,
|
|
31
|
+
`Deploy Model`, `Run Inference`, and let Robot drive the camera.
|
|
32
|
+
|
|
33
|
+
Counterpart to [`edgelens-pytest`](../pytest) for pytest users and
|
|
34
|
+
[`@edgelens/playwright-plugin`](../playwright) for JS/TS.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pip install edgelens-robot
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Python 3.9+ and Robot Framework 6.0+.
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
```robotframework
|
|
47
|
+
*** Settings ***
|
|
48
|
+
Library EdgeLensLibrary api_key=%{EDGELENS_API_KEY}
|
|
49
|
+
Suite Teardown Release Device
|
|
50
|
+
|
|
51
|
+
*** Test Cases ***
|
|
52
|
+
Camera Renders The Test Pattern
|
|
53
|
+
Request Device os=stock-os
|
|
54
|
+
Deploy Model ./face.cvimodel
|
|
55
|
+
Start Inference
|
|
56
|
+
${stats}= Run Inference duration=30
|
|
57
|
+
Should Be True ${stats}[fps][avg] > 15
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run it:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
EDGELENS_API_KEY=el_test_xxx robot tests/
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Keywords
|
|
67
|
+
|
|
68
|
+
### Session lifecycle
|
|
69
|
+
|
|
70
|
+
| Keyword | Arguments | Returns |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `Request Device` | `device_type=recamera-2002`, `os=stock-os`, `timeout=3600`, `wait_timeout=300` | Session id |
|
|
73
|
+
| `Release Device` | — | (no-op if no session) |
|
|
74
|
+
| `Get Session Id` | — | Session id |
|
|
75
|
+
| `Get Session Status` | — | Refreshes + returns status string |
|
|
76
|
+
|
|
77
|
+
### Device operations
|
|
78
|
+
|
|
79
|
+
| Keyword | Arguments | Returns |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `Execute On Device` | `command`, `timeout=60.0` | `{stdout, stderr, exit_code}` dict |
|
|
82
|
+
| `Deploy Model` | `model`, `config=None`, `files=None` | dict |
|
|
83
|
+
| `Get SSH Details` | — | `{user, host, port}` dict |
|
|
84
|
+
| `Set SSH Password Variable` | `name=${SSH_PASSWORD}`, `scope=TEST` | Sets the variable without logging the value |
|
|
85
|
+
| `Get SSH Password` | — | Password string; Robot logs the assigned value at INFO, so prefer `Set SSH Password Variable` |
|
|
86
|
+
| `Get Device Info` | — | full info report dict |
|
|
87
|
+
| `Get Stream URL` | — | URL string (stock OS only) |
|
|
88
|
+
|
|
89
|
+
### Display
|
|
90
|
+
|
|
91
|
+
| Keyword | Arguments |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `Play On Display` | `source`, `loop=False` |
|
|
94
|
+
| `Show On Display` | `source` |
|
|
95
|
+
| `Stop Display` | — |
|
|
96
|
+
| `Get Display Status` | — → `{ready, state, source}` |
|
|
97
|
+
|
|
98
|
+
### Inference (stock OS only)
|
|
99
|
+
|
|
100
|
+
| Keyword | Arguments |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `Start Inference` | — |
|
|
103
|
+
| `Stop Inference` | — |
|
|
104
|
+
| `Run Inference Once` | — → detection dict |
|
|
105
|
+
| `Get Inference Stats` | — → snapshot dict |
|
|
106
|
+
| `Run Inference` | `duration=30.0`, `collect_frames=False` → full stats dict |
|
|
107
|
+
|
|
108
|
+
## Library scope
|
|
109
|
+
|
|
110
|
+
Default is **SUITE** — one camera per suite, shared across every test in
|
|
111
|
+
the file. The right tradeoff for read-only suites where allocation
|
|
112
|
+
takes 30s+. Override on the `Library` line:
|
|
113
|
+
|
|
114
|
+
```robotframework
|
|
115
|
+
Library EdgeLensLibrary scope=TEST # fresh camera per test
|
|
116
|
+
Library EdgeLensLibrary scope=GLOBAL # one camera across the whole run
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
| Argument | Default | Purpose |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `api_key` | `%{EDGELENS_API_KEY}` | API key. Pass via env or library import. |
|
|
124
|
+
| `base_url` | — | Self-hosted EdgeLens deployments. |
|
|
125
|
+
| `scope` | `SUITE` | `SUITE` / `TEST` / `GLOBAL`. |
|
|
126
|
+
|
|
127
|
+
## CI
|
|
128
|
+
|
|
129
|
+
The `edgelensai/edgelens-sdk` image ships Robot Framework — drop the library
|
|
130
|
+
in and you're set:
|
|
131
|
+
|
|
132
|
+
### GitHub Actions
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
- uses: edgelens-ai/edgelens-sdk/ci/github-action@v0.2.1
|
|
136
|
+
with:
|
|
137
|
+
api-key: ${{ secrets.EDGELENS_API_KEY }}
|
|
138
|
+
command: robot --output output.xml tests/
|
|
139
|
+
junit-file: output.xml
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### GitLab CI
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
include:
|
|
146
|
+
- remote: "https://raw.githubusercontent.com/edgelens-ai/edgelens-sdk/main/ci/gitlab/edgelens.gitlab-ci.yml"
|
|
147
|
+
|
|
148
|
+
edgelens-robot:
|
|
149
|
+
extends: .edgelens:robot
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Develop
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
python -m venv .venv && source .venv/bin/activate
|
|
156
|
+
pip install -e ".[dev]"
|
|
157
|
+
pytest
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## See also
|
|
161
|
+
|
|
162
|
+
Pairs with edgelens-pytest and @edgelens/playwright-plugin.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# edgelens-robot
|
|
2
|
+
|
|
3
|
+
Robot Framework library for the EdgeLens Edge AI Device Farm. Keyword-
|
|
4
|
+
driven wrapper around the Python SDK — write `Request Device`,
|
|
5
|
+
`Deploy Model`, `Run Inference`, and let Robot drive the camera.
|
|
6
|
+
|
|
7
|
+
Counterpart to [`edgelens-pytest`](../pytest) for pytest users and
|
|
8
|
+
[`@edgelens/playwright-plugin`](../playwright) for JS/TS.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pip install edgelens-robot
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Python 3.9+ and Robot Framework 6.0+.
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```robotframework
|
|
21
|
+
*** Settings ***
|
|
22
|
+
Library EdgeLensLibrary api_key=%{EDGELENS_API_KEY}
|
|
23
|
+
Suite Teardown Release Device
|
|
24
|
+
|
|
25
|
+
*** Test Cases ***
|
|
26
|
+
Camera Renders The Test Pattern
|
|
27
|
+
Request Device os=stock-os
|
|
28
|
+
Deploy Model ./face.cvimodel
|
|
29
|
+
Start Inference
|
|
30
|
+
${stats}= Run Inference duration=30
|
|
31
|
+
Should Be True ${stats}[fps][avg] > 15
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run it:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
EDGELENS_API_KEY=el_test_xxx robot tests/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Keywords
|
|
41
|
+
|
|
42
|
+
### Session lifecycle
|
|
43
|
+
|
|
44
|
+
| Keyword | Arguments | Returns |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `Request Device` | `device_type=recamera-2002`, `os=stock-os`, `timeout=3600`, `wait_timeout=300` | Session id |
|
|
47
|
+
| `Release Device` | — | (no-op if no session) |
|
|
48
|
+
| `Get Session Id` | — | Session id |
|
|
49
|
+
| `Get Session Status` | — | Refreshes + returns status string |
|
|
50
|
+
|
|
51
|
+
### Device operations
|
|
52
|
+
|
|
53
|
+
| Keyword | Arguments | Returns |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `Execute On Device` | `command`, `timeout=60.0` | `{stdout, stderr, exit_code}` dict |
|
|
56
|
+
| `Deploy Model` | `model`, `config=None`, `files=None` | dict |
|
|
57
|
+
| `Get SSH Details` | — | `{user, host, port}` dict |
|
|
58
|
+
| `Set SSH Password Variable` | `name=${SSH_PASSWORD}`, `scope=TEST` | Sets the variable without logging the value |
|
|
59
|
+
| `Get SSH Password` | — | Password string; Robot logs the assigned value at INFO, so prefer `Set SSH Password Variable` |
|
|
60
|
+
| `Get Device Info` | — | full info report dict |
|
|
61
|
+
| `Get Stream URL` | — | URL string (stock OS only) |
|
|
62
|
+
|
|
63
|
+
### Display
|
|
64
|
+
|
|
65
|
+
| Keyword | Arguments |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `Play On Display` | `source`, `loop=False` |
|
|
68
|
+
| `Show On Display` | `source` |
|
|
69
|
+
| `Stop Display` | — |
|
|
70
|
+
| `Get Display Status` | — → `{ready, state, source}` |
|
|
71
|
+
|
|
72
|
+
### Inference (stock OS only)
|
|
73
|
+
|
|
74
|
+
| Keyword | Arguments |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `Start Inference` | — |
|
|
77
|
+
| `Stop Inference` | — |
|
|
78
|
+
| `Run Inference Once` | — → detection dict |
|
|
79
|
+
| `Get Inference Stats` | — → snapshot dict |
|
|
80
|
+
| `Run Inference` | `duration=30.0`, `collect_frames=False` → full stats dict |
|
|
81
|
+
|
|
82
|
+
## Library scope
|
|
83
|
+
|
|
84
|
+
Default is **SUITE** — one camera per suite, shared across every test in
|
|
85
|
+
the file. The right tradeoff for read-only suites where allocation
|
|
86
|
+
takes 30s+. Override on the `Library` line:
|
|
87
|
+
|
|
88
|
+
```robotframework
|
|
89
|
+
Library EdgeLensLibrary scope=TEST # fresh camera per test
|
|
90
|
+
Library EdgeLensLibrary scope=GLOBAL # one camera across the whole run
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
| Argument | Default | Purpose |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `api_key` | `%{EDGELENS_API_KEY}` | API key. Pass via env or library import. |
|
|
98
|
+
| `base_url` | — | Self-hosted EdgeLens deployments. |
|
|
99
|
+
| `scope` | `SUITE` | `SUITE` / `TEST` / `GLOBAL`. |
|
|
100
|
+
|
|
101
|
+
## CI
|
|
102
|
+
|
|
103
|
+
The `edgelensai/edgelens-sdk` image ships Robot Framework — drop the library
|
|
104
|
+
in and you're set:
|
|
105
|
+
|
|
106
|
+
### GitHub Actions
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
- uses: edgelens-ai/edgelens-sdk/ci/github-action@v0.2.1
|
|
110
|
+
with:
|
|
111
|
+
api-key: ${{ secrets.EDGELENS_API_KEY }}
|
|
112
|
+
command: robot --output output.xml tests/
|
|
113
|
+
junit-file: output.xml
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### GitLab CI
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
include:
|
|
120
|
+
- remote: "https://raw.githubusercontent.com/edgelens-ai/edgelens-sdk/main/ci/gitlab/edgelens.gitlab-ci.yml"
|
|
121
|
+
|
|
122
|
+
edgelens-robot:
|
|
123
|
+
extends: .edgelens:robot
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Develop
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
python -m venv .venv && source .venv/bin/activate
|
|
130
|
+
pip install -e ".[dev]"
|
|
131
|
+
pytest
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## See also
|
|
135
|
+
|
|
136
|
+
Pairs with edgelens-pytest and @edgelens/playwright-plugin.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "edgelens-robot"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Robot Framework library for the EdgeLens Edge AI Device Farm."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "EdgeLens", email = "support@edgelens.ai" }]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
keywords = ["robotframework", "edgelens", "testing"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Framework :: Robot Framework",
|
|
17
|
+
"Framework :: Robot Framework :: Library",
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"edgelens-sdk>=0.2.1,<0.3",
|
|
25
|
+
"robotframework>=6.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=7.0",
|
|
31
|
+
"ruff>=0.1",
|
|
32
|
+
"mypy>=1.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://edgelens.ai"
|
|
37
|
+
Repository = "https://github.com/edgelens-ai/edgelens-sdk"
|
|
38
|
+
Issues = "https://github.com/edgelens-ai/edgelens-sdk/issues"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
include = ["/EdgeLensLibrary", "/README.md", "/LICENSE"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["EdgeLensLibrary"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 100
|
|
48
|
+
target-version = "py39"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.version]
|
|
51
|
+
path = "EdgeLensLibrary/_version.py"
|