racs-python 0.0.2__tar.gz → 0.3.0__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.
- {racs_python-0.0.2 → racs_python-0.3.0}/PKG-INFO +96 -24
- racs_python-0.3.0/README.md +111 -0
- {racs_python-0.0.2 → racs_python-0.3.0}/pyproject.toml +12 -4
- racs_python-0.3.0/src/racs_python/__init__.py +41 -0
- racs_python-0.3.0/src/racs_python/burst.py +119 -0
- racs_python-0.3.0/src/racs_python/camera.py +229 -0
- racs_python-0.3.0/src/racs_python/client.py +362 -0
- racs_python-0.3.0/src/racs_python/errors.py +78 -0
- racs_python-0.3.0/src/racs_python/frame.py +45 -0
- racs_python-0.3.0/src/racs_python/live.py +121 -0
- racs_python-0.3.0/src/racs_python/models.py +200 -0
- {racs_python-0.0.2 → racs_python-0.3.0}/src/racs_python.egg-info/PKG-INFO +96 -24
- {racs_python-0.0.2 → racs_python-0.3.0}/src/racs_python.egg-info/SOURCES.txt +8 -1
- racs_python-0.3.0/src/racs_python.egg-info/requires.txt +10 -0
- racs_python-0.3.0/tests/test_fake_server.py +217 -0
- racs_python-0.3.0/tests/test_unit.py +132 -0
- racs_python-0.0.2/README.md +0 -45
- racs_python-0.0.2/src/racs_python/__init__.py +0 -3
- racs_python-0.0.2/src/racs_python/client.py +0 -123
- racs_python-0.0.2/src/racs_python/models.py +0 -62
- racs_python-0.0.2/src/racs_python.egg-info/requires.txt +0 -2
- {racs_python-0.0.2 → racs_python-0.3.0}/LICENSE +0 -0
- {racs_python-0.0.2 → racs_python-0.3.0}/setup.cfg +0 -0
- {racs_python-0.0.2 → racs_python-0.3.0}/src/racs_python.egg-info/dependency_links.txt +0 -0
- {racs_python-0.0.2 → racs_python-0.3.0}/src/racs_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: racs-python
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Client library to access racs api server
|
|
5
5
|
Author-email: Jonas Ahlf <javerik@javerik.space>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -349,55 +349,127 @@ Keywords: racs,aravis,camera,client,api,genicam,gev,gigevision
|
|
|
349
349
|
Classifier: Programming Language :: Python :: 3
|
|
350
350
|
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
|
|
351
351
|
Classifier: Operating System :: OS Independent
|
|
352
|
-
Requires-Python: >=3.
|
|
352
|
+
Requires-Python: >=3.9
|
|
353
353
|
Description-Content-Type: text/markdown
|
|
354
354
|
License-File: LICENSE
|
|
355
355
|
Requires-Dist: requests>=2.31
|
|
356
356
|
Requires-Dist: pydantic>=2.0
|
|
357
|
+
Requires-Dist: websocket-client>=1.6
|
|
358
|
+
Provides-Extra: numpy
|
|
359
|
+
Requires-Dist: numpy; extra == "numpy"
|
|
360
|
+
Provides-Extra: test
|
|
361
|
+
Requires-Dist: pytest; extra == "test"
|
|
362
|
+
Requires-Dist: numpy; extra == "test"
|
|
357
363
|
Dynamic: license-file
|
|
358
364
|
|
|
359
365
|
# RACS python client library
|
|
360
366
|
|
|
361
|
-
|
|
367
|
+
Client for the [racs-api server](https://codeberg.org/javerik/RACS-RustAravisCamServe): device
|
|
368
|
+
discovery and connection, typed GenICam feature access, single frames, the live stream
|
|
369
|
+
(WebSocket) and burst jobs.
|
|
362
370
|
|
|
363
371
|
## Install
|
|
364
372
|
|
|
365
373
|
```shell
|
|
366
|
-
pip install racs-python
|
|
374
|
+
pip install racs-python # numpy is optional:
|
|
375
|
+
pip install "racs-python[numpy]" # adds Frame.to_numpy()
|
|
367
376
|
```
|
|
368
377
|
|
|
369
378
|
## Usage
|
|
370
379
|
|
|
371
380
|
> **Make sure an instance of racs-api server is running**
|
|
372
381
|
|
|
382
|
+
A `Camera` holds the device id, so nothing else needs to pass it around:
|
|
383
|
+
|
|
373
384
|
```python
|
|
374
385
|
from racs_python import RacsClient
|
|
375
386
|
|
|
376
|
-
client = RacsClient("http://localhost:8742/api/v1"
|
|
387
|
+
client = RacsClient("http://localhost:8742") # "/api/v1" is added to a bare server URL
|
|
377
388
|
|
|
378
|
-
#
|
|
379
|
-
devices = client.get_devices()
|
|
389
|
+
devices = client.get_devices() # network discovery
|
|
380
390
|
print(f"Found {len(devices.available)} available devices.")
|
|
381
391
|
|
|
382
|
-
|
|
383
|
-
target_id = devices.available[0].id
|
|
384
|
-
|
|
385
|
-
# 2. Connection
|
|
386
|
-
client.connect_device(target_id)
|
|
387
|
-
print(f"Connected to device {target_id}")
|
|
392
|
+
cam = client.connect(devices.available[0].id) # or client.connect(5, ip="192.168.0.10")
|
|
388
393
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
+
# Features are read and written as Python values
|
|
395
|
+
print(cam["Width"]) # 1280 (int)
|
|
396
|
+
cam["ExposureTime"] = 5000.0
|
|
397
|
+
cam["ReverseX"] = True
|
|
398
|
+
width, height = cam.get_many("Width", "Height").values()
|
|
399
|
+
cam.execute("TriggerSoftware")
|
|
400
|
+
|
|
401
|
+
# Single frames
|
|
402
|
+
with open("frame.jpg", "wb") as f:
|
|
403
|
+
f.write(cam.grab_jpeg(quality=90))
|
|
404
|
+
frame = cam.grab_raw() # Mono8, with width/height
|
|
405
|
+
pixels = frame.to_numpy() # (height, width) uint8
|
|
406
|
+
|
|
407
|
+
cam.disconnect()
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Already connected devices: `client.cameras()`, or `client.camera(id)` for a handle without a request.
|
|
411
|
+
|
|
412
|
+
### Live stream
|
|
413
|
+
|
|
414
|
+
The stream holds the camera until it is closed. Features can still be read and written while it
|
|
415
|
+
runs; a change of size arrives as a new `stream.info` and applies to the frames after it.
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
from itertools import islice
|
|
419
|
+
|
|
420
|
+
with cam.live(format="raw", pixel_depth="mono16", fps=10) as stream:
|
|
421
|
+
for frame in islice(stream, 100):
|
|
422
|
+
pixels = frame.to_numpy() # uint16 at bit_depth > 8
|
|
423
|
+
print(frame.frame_number, frame.dropped, frame.bit_depth, pixels.mean())
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`format="jpeg"` (the default) delivers JPEG files in `frame.data`. A stream that the server ends
|
|
427
|
+
because of an error raises `LiveStreamError`; `cam.stream_error()` reports the last one.
|
|
394
428
|
|
|
395
|
-
|
|
396
|
-
jpg_data = client.grab_jpg_frame(target_id)
|
|
397
|
-
with open("frame.jpg", "wb") as f:
|
|
398
|
-
f.write(jpg_data)
|
|
429
|
+
### Packet size
|
|
399
430
|
|
|
400
|
-
|
|
401
|
-
|
|
431
|
+
Every grab, stream and burst uses the camera's stream packet size (`GevSCPSPacketSize`, bytes per
|
|
432
|
+
packet including IP and UDP headers) as it is. Jumbo frames such as 7960 need a link MTU of 9000.
|
|
402
433
|
|
|
434
|
+
```python
|
|
435
|
+
info = cam.packet_size() # packet_size, min/max/inc, link_mtu, interface
|
|
436
|
+
cam["GevSCPSPacketSize"] = 7960 # a multiple of info.inc, not above the link MTU
|
|
437
|
+
auto = cam.auto_packet_size() # the largest size whose test packets arrive
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Both work during a live stream, which pauses briefly and carries on with the new size
|
|
441
|
+
(`stream.info.packet_size`).
|
|
442
|
+
|
|
443
|
+
### Burst jobs
|
|
444
|
+
|
|
445
|
+
A burst grabs up to 500 frames on the server in the background. Leaving the `with` block deletes
|
|
446
|
+
the job and its frames.
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
with cam.burst(50, format="raw", pixel_depth="mono16") as job:
|
|
450
|
+
for frame in job.frames(): # yields each frame as soon as it is captured
|
|
451
|
+
...
|
|
452
|
+
info = job.wait() # final status
|
|
403
453
|
```
|
|
454
|
+
|
|
455
|
+
`job.image(i)` returns the encoded bytes of one frame, `job.images()` lists what has been
|
|
456
|
+
captured, and `cam.job(job_id)` re-attaches to a job by id.
|
|
457
|
+
|
|
458
|
+
### Errors
|
|
459
|
+
|
|
460
|
+
Every error is a `RacsError`. HTTP errors are `RacsHttpError` (a `requests.HTTPError`) with
|
|
461
|
+
`status_code` and `body`; the specific ones are `CameraBusyError` (the camera is held by a stream,
|
|
462
|
+
burst or grab), `NotFoundError` and `ImageFailedError`. Feature reads raise `FeatureError`, a
|
|
463
|
+
failed burst `BurstJobFailedError`.
|
|
464
|
+
|
|
465
|
+
### Low-level client
|
|
466
|
+
|
|
467
|
+
`RacsClient` maps one method onto each REST route and takes the device id every time, e.g.
|
|
468
|
+
`client.get_device_gc_node_value(0, "Width")` or `client.start_grab_job(0, 20, "raw")`. Values
|
|
469
|
+
are strings there, exactly as the server sends them.
|
|
470
|
+
|
|
471
|
+
## Tests
|
|
472
|
+
|
|
473
|
+
`client_libs/run-tests.sh python` builds the repository's fake-camera server
|
|
474
|
+
(`cargo run --example fake_server`), starts it and runs the tests against it. `pytest` on its own
|
|
475
|
+
runs only the tests that need no server.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# RACS python client library
|
|
2
|
+
|
|
3
|
+
Client for the [racs-api server](https://codeberg.org/javerik/RACS-RustAravisCamServe): device
|
|
4
|
+
discovery and connection, typed GenICam feature access, single frames, the live stream
|
|
5
|
+
(WebSocket) and burst jobs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
pip install racs-python # numpy is optional:
|
|
11
|
+
pip install "racs-python[numpy]" # adds Frame.to_numpy()
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
> **Make sure an instance of racs-api server is running**
|
|
17
|
+
|
|
18
|
+
A `Camera` holds the device id, so nothing else needs to pass it around:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from racs_python import RacsClient
|
|
22
|
+
|
|
23
|
+
client = RacsClient("http://localhost:8742") # "/api/v1" is added to a bare server URL
|
|
24
|
+
|
|
25
|
+
devices = client.get_devices() # network discovery
|
|
26
|
+
print(f"Found {len(devices.available)} available devices.")
|
|
27
|
+
|
|
28
|
+
cam = client.connect(devices.available[0].id) # or client.connect(5, ip="192.168.0.10")
|
|
29
|
+
|
|
30
|
+
# Features are read and written as Python values
|
|
31
|
+
print(cam["Width"]) # 1280 (int)
|
|
32
|
+
cam["ExposureTime"] = 5000.0
|
|
33
|
+
cam["ReverseX"] = True
|
|
34
|
+
width, height = cam.get_many("Width", "Height").values()
|
|
35
|
+
cam.execute("TriggerSoftware")
|
|
36
|
+
|
|
37
|
+
# Single frames
|
|
38
|
+
with open("frame.jpg", "wb") as f:
|
|
39
|
+
f.write(cam.grab_jpeg(quality=90))
|
|
40
|
+
frame = cam.grab_raw() # Mono8, with width/height
|
|
41
|
+
pixels = frame.to_numpy() # (height, width) uint8
|
|
42
|
+
|
|
43
|
+
cam.disconnect()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Already connected devices: `client.cameras()`, or `client.camera(id)` for a handle without a request.
|
|
47
|
+
|
|
48
|
+
### Live stream
|
|
49
|
+
|
|
50
|
+
The stream holds the camera until it is closed. Features can still be read and written while it
|
|
51
|
+
runs; a change of size arrives as a new `stream.info` and applies to the frames after it.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from itertools import islice
|
|
55
|
+
|
|
56
|
+
with cam.live(format="raw", pixel_depth="mono16", fps=10) as stream:
|
|
57
|
+
for frame in islice(stream, 100):
|
|
58
|
+
pixels = frame.to_numpy() # uint16 at bit_depth > 8
|
|
59
|
+
print(frame.frame_number, frame.dropped, frame.bit_depth, pixels.mean())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`format="jpeg"` (the default) delivers JPEG files in `frame.data`. A stream that the server ends
|
|
63
|
+
because of an error raises `LiveStreamError`; `cam.stream_error()` reports the last one.
|
|
64
|
+
|
|
65
|
+
### Packet size
|
|
66
|
+
|
|
67
|
+
Every grab, stream and burst uses the camera's stream packet size (`GevSCPSPacketSize`, bytes per
|
|
68
|
+
packet including IP and UDP headers) as it is. Jumbo frames such as 7960 need a link MTU of 9000.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
info = cam.packet_size() # packet_size, min/max/inc, link_mtu, interface
|
|
72
|
+
cam["GevSCPSPacketSize"] = 7960 # a multiple of info.inc, not above the link MTU
|
|
73
|
+
auto = cam.auto_packet_size() # the largest size whose test packets arrive
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Both work during a live stream, which pauses briefly and carries on with the new size
|
|
77
|
+
(`stream.info.packet_size`).
|
|
78
|
+
|
|
79
|
+
### Burst jobs
|
|
80
|
+
|
|
81
|
+
A burst grabs up to 500 frames on the server in the background. Leaving the `with` block deletes
|
|
82
|
+
the job and its frames.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
with cam.burst(50, format="raw", pixel_depth="mono16") as job:
|
|
86
|
+
for frame in job.frames(): # yields each frame as soon as it is captured
|
|
87
|
+
...
|
|
88
|
+
info = job.wait() # final status
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`job.image(i)` returns the encoded bytes of one frame, `job.images()` lists what has been
|
|
92
|
+
captured, and `cam.job(job_id)` re-attaches to a job by id.
|
|
93
|
+
|
|
94
|
+
### Errors
|
|
95
|
+
|
|
96
|
+
Every error is a `RacsError`. HTTP errors are `RacsHttpError` (a `requests.HTTPError`) with
|
|
97
|
+
`status_code` and `body`; the specific ones are `CameraBusyError` (the camera is held by a stream,
|
|
98
|
+
burst or grab), `NotFoundError` and `ImageFailedError`. Feature reads raise `FeatureError`, a
|
|
99
|
+
failed burst `BurstJobFailedError`.
|
|
100
|
+
|
|
101
|
+
### Low-level client
|
|
102
|
+
|
|
103
|
+
`RacsClient` maps one method onto each REST route and takes the device id every time, e.g.
|
|
104
|
+
`client.get_device_gc_node_value(0, "Width")` or `client.start_grab_job(0, 20, "raw")`. Values
|
|
105
|
+
are strings there, exactly as the server sends them.
|
|
106
|
+
|
|
107
|
+
## Tests
|
|
108
|
+
|
|
109
|
+
`client_libs/run-tests.sh python` builds the repository's fake-camera server
|
|
110
|
+
(`cargo run --example fake_server`), starts it and runs the tests against it. `pytest` on its own
|
|
111
|
+
runs only the tests that need no server.
|
|
@@ -4,19 +4,20 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "racs-python"
|
|
7
|
-
version = "0.0
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Jonas Ahlf", email="javerik@javerik.space" },
|
|
10
10
|
]
|
|
11
11
|
description = "Client library to access racs api server"
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
14
|
license = { file = "LICENSE" }
|
|
15
15
|
|
|
16
16
|
# Added specific version constraints for stability
|
|
17
17
|
dependencies = [
|
|
18
18
|
"requests>=2.31",
|
|
19
|
-
"pydantic>=2.0" # Required for the code provided (V2)
|
|
19
|
+
"pydantic>=2.0", # Required for the code provided (V2)
|
|
20
|
+
"websocket-client>=1.6" # Live stream
|
|
20
21
|
]
|
|
21
22
|
|
|
22
23
|
classifiers = [
|
|
@@ -29,4 +30,11 @@ keywords = ["racs", "aravis", "camera", "client", "api", "genicam", "gev", "gige
|
|
|
29
30
|
|
|
30
31
|
[project.urls]
|
|
31
32
|
"Homepage" = "https://codeberg.org/javerik/RACS-RustAravisCamServe"
|
|
32
|
-
"Bug Tracker" = "https://codeberg.org/javerik/RACS-RustAravisCamServe/issues"
|
|
33
|
+
"Bug Tracker" = "https://codeberg.org/javerik/RACS-RustAravisCamServe/issues"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
numpy = ["numpy"] # Frame.to_numpy()
|
|
37
|
+
test = ["pytest", "numpy"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from .burst import BurstJob
|
|
2
|
+
from .camera import Camera
|
|
3
|
+
from .client import RacsClient
|
|
4
|
+
from .errors import (
|
|
5
|
+
BurstJobFailedError,
|
|
6
|
+
CameraBusyError,
|
|
7
|
+
FeatureError,
|
|
8
|
+
ImageFailedError,
|
|
9
|
+
LiveStreamError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RacsError,
|
|
12
|
+
RacsHttpError,
|
|
13
|
+
)
|
|
14
|
+
from .frame import Frame
|
|
15
|
+
from .live import LiveStream
|
|
16
|
+
from .models import (
|
|
17
|
+
AutoPacketSize,
|
|
18
|
+
BurstFormat,
|
|
19
|
+
BurstJobCreated,
|
|
20
|
+
BurstJobInfo,
|
|
21
|
+
DeviceConnections,
|
|
22
|
+
DeviceDiscoverInfo,
|
|
23
|
+
FeatureState,
|
|
24
|
+
GenICamAccessMode,
|
|
25
|
+
GenICamEnumInfo,
|
|
26
|
+
GenICamNodeCategory,
|
|
27
|
+
GenICamNodeInfo,
|
|
28
|
+
GenICamNodeType,
|
|
29
|
+
GenICamVisibility,
|
|
30
|
+
ImageEntryFailed,
|
|
31
|
+
ImageEntryOk,
|
|
32
|
+
JobStatus,
|
|
33
|
+
LiveFormat,
|
|
34
|
+
OnFailure,
|
|
35
|
+
PacketSizeInfo,
|
|
36
|
+
PixelDepth,
|
|
37
|
+
StreamErrorInfo,
|
|
38
|
+
StreamInfo,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import TYPE_CHECKING, Iterator, List, Optional, Union
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from .errors import BurstJobFailedError, NotFoundError, RacsError
|
|
6
|
+
from .frame import Frame
|
|
7
|
+
from .models import BurstJobInfo, ImageEntry, ImageEntryOk, JobStatus
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .camera import Camera
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BurstJob:
|
|
14
|
+
"""
|
|
15
|
+
A burst job on the server. The frames stay there until the job is deleted, or expire 10 min
|
|
16
|
+
after it finishes.
|
|
17
|
+
|
|
18
|
+
Used as a context manager, the job is deleted on exit::
|
|
19
|
+
|
|
20
|
+
with cam.burst(20, format="raw") as job:
|
|
21
|
+
for frame in job.frames():
|
|
22
|
+
...
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, camera: "Camera", job_id: Union[UUID, str]):
|
|
26
|
+
self.camera = camera
|
|
27
|
+
self.id = job_id if isinstance(job_id, UUID) else UUID(str(job_id))
|
|
28
|
+
# The first status that carried the frame geometry; it is the same for every frame.
|
|
29
|
+
self._geometry: Optional[BurstJobInfo] = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def _client(self):
|
|
33
|
+
return self.camera.client
|
|
34
|
+
|
|
35
|
+
def info(self) -> BurstJobInfo:
|
|
36
|
+
"""The job's status and progress."""
|
|
37
|
+
info = self._client.get_grab_job(self.camera.id, self.id)
|
|
38
|
+
if info.width is not None:
|
|
39
|
+
self._geometry = info
|
|
40
|
+
return info
|
|
41
|
+
|
|
42
|
+
def images(self) -> List[ImageEntry]:
|
|
43
|
+
"""Every frame attempted so far, in capture order: ImageEntryOk or ImageEntryFailed."""
|
|
44
|
+
return self._client.list_grab_job_images(self.camera.id, self.id)
|
|
45
|
+
|
|
46
|
+
def image(self, index: int) -> bytes:
|
|
47
|
+
"""The encoded bytes of frame ``index``."""
|
|
48
|
+
return self._client.get_grab_job_image(self.camera.id, self.id, index)
|
|
49
|
+
|
|
50
|
+
def frame(self, index: int) -> Frame:
|
|
51
|
+
"""Frame ``index`` with its geometry, ready for :meth:`Frame.to_numpy` (raw jobs)."""
|
|
52
|
+
data = self.image(index)
|
|
53
|
+
geometry = self._geometry or self.info()
|
|
54
|
+
if geometry.width is None or geometry.height is None:
|
|
55
|
+
raise RacsError(f"burst job {self.id} reports no frame geometry")
|
|
56
|
+
return Frame(
|
|
57
|
+
data=data,
|
|
58
|
+
width=geometry.width,
|
|
59
|
+
height=geometry.height,
|
|
60
|
+
format=geometry.format.value,
|
|
61
|
+
pixel_format=geometry.pixel_format or "Mono8",
|
|
62
|
+
bit_depth=geometry.bit_depth or 8,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def wait(self, timeout: Optional[float] = None, poll_interval: float = 0.1) -> BurstJobInfo:
|
|
66
|
+
"""
|
|
67
|
+
Block until the job is completed or cancelled and return its final status. Raises
|
|
68
|
+
BurstJobFailedError if it failed and TimeoutError after ``timeout`` seconds.
|
|
69
|
+
"""
|
|
70
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
71
|
+
while True:
|
|
72
|
+
info = self.info()
|
|
73
|
+
if info.status.is_terminal:
|
|
74
|
+
if info.status == JobStatus.FAILED:
|
|
75
|
+
raise BurstJobFailedError(info)
|
|
76
|
+
return info
|
|
77
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
78
|
+
raise TimeoutError(f"burst job {self.id} still {info.status.value} after {timeout} s")
|
|
79
|
+
time.sleep(poll_interval)
|
|
80
|
+
|
|
81
|
+
def frames(self, poll_interval: float = 0.1) -> Iterator[Frame]:
|
|
82
|
+
"""
|
|
83
|
+
Yield every captured frame in order, as soon as it is on the server; failed frames (only
|
|
84
|
+
with ``on_failure="skip"``) are left out. Ends when the job is done, and raises
|
|
85
|
+
BurstJobFailedError after the last frame if the job failed.
|
|
86
|
+
"""
|
|
87
|
+
next_index = 0
|
|
88
|
+
while True:
|
|
89
|
+
# Status first: once it is terminal, the listing that follows is complete.
|
|
90
|
+
info = self.info()
|
|
91
|
+
for entry in self.images():
|
|
92
|
+
if entry.index < next_index:
|
|
93
|
+
continue
|
|
94
|
+
next_index = entry.index + 1
|
|
95
|
+
if isinstance(entry, ImageEntryOk):
|
|
96
|
+
yield self.frame(entry.index)
|
|
97
|
+
if info.status.is_terminal:
|
|
98
|
+
if info.status == JobStatus.FAILED:
|
|
99
|
+
raise BurstJobFailedError(info)
|
|
100
|
+
return
|
|
101
|
+
time.sleep(poll_interval)
|
|
102
|
+
|
|
103
|
+
def delete(self) -> None:
|
|
104
|
+
"""Cancel the job if it still runs, and drop it and its frames from the server."""
|
|
105
|
+
self._client.delete_grab_job(self.camera.id, self.id)
|
|
106
|
+
|
|
107
|
+
cancel = delete
|
|
108
|
+
|
|
109
|
+
def __enter__(self) -> "BurstJob":
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def __exit__(self, *exc) -> None:
|
|
113
|
+
try:
|
|
114
|
+
self.delete()
|
|
115
|
+
except NotFoundError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def __repr__(self) -> str:
|
|
119
|
+
return f"BurstJob(device_id={self.camera.id}, id='{self.id}')"
|