antioch-py 2.0.6__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.
Potentially problematic release.
This version of antioch-py might be problematic. Click here for more details.
- antioch/__init__.py +0 -0
- antioch/message.py +87 -0
- antioch/module/__init__.py +53 -0
- antioch/module/clock.py +62 -0
- antioch/module/execution.py +278 -0
- antioch/module/input.py +127 -0
- antioch/module/module.py +218 -0
- antioch/module/node.py +357 -0
- antioch/module/token.py +42 -0
- antioch/session/__init__.py +150 -0
- antioch/session/ark.py +504 -0
- antioch/session/asset.py +65 -0
- antioch/session/error.py +80 -0
- antioch/session/record.py +158 -0
- antioch/session/scene.py +1521 -0
- antioch/session/session.py +220 -0
- antioch/session/task.py +323 -0
- antioch/session/views/__init__.py +40 -0
- antioch/session/views/animation.py +189 -0
- antioch/session/views/articulation.py +245 -0
- antioch/session/views/basis_curve.py +186 -0
- antioch/session/views/camera.py +92 -0
- antioch/session/views/collision.py +75 -0
- antioch/session/views/geometry.py +74 -0
- antioch/session/views/ground_plane.py +63 -0
- antioch/session/views/imu.py +73 -0
- antioch/session/views/joint.py +64 -0
- antioch/session/views/light.py +175 -0
- antioch/session/views/pir_sensor.py +140 -0
- antioch/session/views/radar.py +73 -0
- antioch/session/views/rigid_body.py +282 -0
- antioch/session/views/xform.py +119 -0
- antioch_py-2.0.6.dist-info/METADATA +115 -0
- antioch_py-2.0.6.dist-info/RECORD +99 -0
- antioch_py-2.0.6.dist-info/WHEEL +5 -0
- antioch_py-2.0.6.dist-info/entry_points.txt +2 -0
- antioch_py-2.0.6.dist-info/top_level.txt +2 -0
- common/__init__.py +0 -0
- common/ark/__init__.py +60 -0
- common/ark/ark.py +128 -0
- common/ark/hardware.py +121 -0
- common/ark/kinematics.py +31 -0
- common/ark/module.py +85 -0
- common/ark/node.py +94 -0
- common/ark/scheduler.py +439 -0
- common/ark/sim.py +33 -0
- common/assets/__init__.py +3 -0
- common/constants.py +47 -0
- common/core/__init__.py +52 -0
- common/core/agent.py +296 -0
- common/core/auth.py +305 -0
- common/core/registry.py +331 -0
- common/core/task.py +36 -0
- common/message/__init__.py +59 -0
- common/message/annotation.py +89 -0
- common/message/array.py +500 -0
- common/message/base.py +517 -0
- common/message/camera.py +91 -0
- common/message/color.py +139 -0
- common/message/frame.py +50 -0
- common/message/image.py +171 -0
- common/message/imu.py +14 -0
- common/message/joint.py +47 -0
- common/message/log.py +31 -0
- common/message/pir.py +16 -0
- common/message/point.py +109 -0
- common/message/point_cloud.py +63 -0
- common/message/pose.py +148 -0
- common/message/quaternion.py +273 -0
- common/message/radar.py +58 -0
- common/message/types.py +37 -0
- common/message/vector.py +786 -0
- common/rome/__init__.py +9 -0
- common/rome/client.py +430 -0
- common/rome/error.py +16 -0
- common/session/__init__.py +54 -0
- common/session/environment.py +31 -0
- common/session/sim.py +240 -0
- common/session/views/__init__.py +263 -0
- common/session/views/animation.py +73 -0
- common/session/views/articulation.py +184 -0
- common/session/views/basis_curve.py +102 -0
- common/session/views/camera.py +147 -0
- common/session/views/collision.py +59 -0
- common/session/views/geometry.py +102 -0
- common/session/views/ground_plane.py +41 -0
- common/session/views/imu.py +66 -0
- common/session/views/joint.py +81 -0
- common/session/views/light.py +96 -0
- common/session/views/pir_sensor.py +115 -0
- common/session/views/radar.py +82 -0
- common/session/views/rigid_body.py +236 -0
- common/session/views/viewport.py +21 -0
- common/session/views/xform.py +39 -0
- common/utils/__init__.py +4 -0
- common/utils/comms.py +571 -0
- common/utils/logger.py +123 -0
- common/utils/time.py +42 -0
- common/utils/usd.py +12 -0
antioch/__init__.py
ADDED
|
File without changes
|
antioch/message.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from common.message import (
|
|
2
|
+
Array,
|
|
3
|
+
Bool,
|
|
4
|
+
CameraInfo,
|
|
5
|
+
CircleAnnotation,
|
|
6
|
+
Color,
|
|
7
|
+
DeserializationError,
|
|
8
|
+
Float,
|
|
9
|
+
FrameTransform,
|
|
10
|
+
FrameTransforms,
|
|
11
|
+
Image,
|
|
12
|
+
ImageAnnotations,
|
|
13
|
+
ImageEncoding,
|
|
14
|
+
Int,
|
|
15
|
+
JointState,
|
|
16
|
+
JointStates,
|
|
17
|
+
JointTarget,
|
|
18
|
+
JointTargets,
|
|
19
|
+
Log,
|
|
20
|
+
LogLevel,
|
|
21
|
+
Message,
|
|
22
|
+
MessageError,
|
|
23
|
+
MismatchError,
|
|
24
|
+
Point2,
|
|
25
|
+
Point3,
|
|
26
|
+
PointCloud,
|
|
27
|
+
PointsAnnotation,
|
|
28
|
+
PointsAnnotationType,
|
|
29
|
+
Pose,
|
|
30
|
+
Quaternion,
|
|
31
|
+
RadarDetection,
|
|
32
|
+
RadarScan,
|
|
33
|
+
SerializationError,
|
|
34
|
+
String,
|
|
35
|
+
TextAnnotation,
|
|
36
|
+
Vector2,
|
|
37
|
+
Vector3,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Base types
|
|
42
|
+
"Message",
|
|
43
|
+
"MessageError",
|
|
44
|
+
"DeserializationError",
|
|
45
|
+
"SerializationError",
|
|
46
|
+
"MismatchError",
|
|
47
|
+
# Primitive types
|
|
48
|
+
"Array",
|
|
49
|
+
"Bool",
|
|
50
|
+
"Float",
|
|
51
|
+
"Int",
|
|
52
|
+
"String",
|
|
53
|
+
# Geometry types
|
|
54
|
+
"Point2",
|
|
55
|
+
"Point3",
|
|
56
|
+
"Vector2",
|
|
57
|
+
"Vector3",
|
|
58
|
+
"Pose",
|
|
59
|
+
"Quaternion",
|
|
60
|
+
# Color
|
|
61
|
+
"Color",
|
|
62
|
+
# Camera types
|
|
63
|
+
"CameraInfo",
|
|
64
|
+
"Image",
|
|
65
|
+
"ImageEncoding",
|
|
66
|
+
# Joint types
|
|
67
|
+
"JointState",
|
|
68
|
+
"JointStates",
|
|
69
|
+
"JointTarget",
|
|
70
|
+
"JointTargets",
|
|
71
|
+
# Sensor types
|
|
72
|
+
"RadarDetection",
|
|
73
|
+
"RadarScan",
|
|
74
|
+
"PointCloud",
|
|
75
|
+
# Logging
|
|
76
|
+
"Log",
|
|
77
|
+
"LogLevel",
|
|
78
|
+
# Annotations
|
|
79
|
+
"CircleAnnotation",
|
|
80
|
+
"ImageAnnotations",
|
|
81
|
+
"PointsAnnotation",
|
|
82
|
+
"PointsAnnotationType",
|
|
83
|
+
"TextAnnotation",
|
|
84
|
+
# Frame transforms
|
|
85
|
+
"FrameTransform",
|
|
86
|
+
"FrameTransforms",
|
|
87
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from antioch.module.clock import Clock, now_us
|
|
2
|
+
from antioch.module.execution import Execution, Input, Output
|
|
3
|
+
from antioch.module.module import Module
|
|
4
|
+
from antioch.module.token import Token, TokenType
|
|
5
|
+
from common.ark import Environment, HardwareAccessMode
|
|
6
|
+
from common.message import (
|
|
7
|
+
CameraInfo,
|
|
8
|
+
Image,
|
|
9
|
+
ImageEncoding,
|
|
10
|
+
ImuSample,
|
|
11
|
+
JointState,
|
|
12
|
+
JointStates,
|
|
13
|
+
JointTarget,
|
|
14
|
+
JointTargets,
|
|
15
|
+
Message,
|
|
16
|
+
Pose,
|
|
17
|
+
Quaternion,
|
|
18
|
+
RadarDetection,
|
|
19
|
+
RadarScan,
|
|
20
|
+
Vector3,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
# Core module types
|
|
25
|
+
"Module",
|
|
26
|
+
"Execution",
|
|
27
|
+
"Input",
|
|
28
|
+
"Output",
|
|
29
|
+
# Token types
|
|
30
|
+
"Token",
|
|
31
|
+
"TokenType",
|
|
32
|
+
# Timing
|
|
33
|
+
"Clock",
|
|
34
|
+
"now_us",
|
|
35
|
+
# Enums
|
|
36
|
+
"Environment",
|
|
37
|
+
"HardwareAccessMode",
|
|
38
|
+
# Message types
|
|
39
|
+
"Message",
|
|
40
|
+
"CameraInfo",
|
|
41
|
+
"Image",
|
|
42
|
+
"ImageEncoding",
|
|
43
|
+
"ImuSample",
|
|
44
|
+
"JointState",
|
|
45
|
+
"JointStates",
|
|
46
|
+
"JointTarget",
|
|
47
|
+
"JointTargets",
|
|
48
|
+
"Pose",
|
|
49
|
+
"Quaternion",
|
|
50
|
+
"RadarDetection",
|
|
51
|
+
"RadarScan",
|
|
52
|
+
"Vector3",
|
|
53
|
+
]
|
antioch/module/clock.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from threading import Event
|
|
3
|
+
|
|
4
|
+
from common.utils.time import now_us
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Clock:
|
|
8
|
+
"""
|
|
9
|
+
Real-time clock for LET-to-wall-time mapping (real mode only).
|
|
10
|
+
|
|
11
|
+
Maps logical execution time to wall-clock time using a fixed start time.
|
|
12
|
+
Provides precise timing for real-mode execution to avoid clock drift.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, start_timestamp_us: int, event: Event | None = None):
|
|
16
|
+
"""
|
|
17
|
+
Create a new real-time clock.
|
|
18
|
+
|
|
19
|
+
:param start_timestamp_us: Wall-clock time (microseconds since epoch) corresponding to LET=0.
|
|
20
|
+
:param event: Optional event to check for shutdown requests during waits.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
self._start_timestamp_us = start_timestamp_us
|
|
24
|
+
self._event = event
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def let_us(self) -> int:
|
|
28
|
+
"""
|
|
29
|
+
Get the current logical time in microseconds.
|
|
30
|
+
|
|
31
|
+
:return: Current logical time.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
return now_us() - self._start_timestamp_us
|
|
35
|
+
|
|
36
|
+
def wait_until(self, let_us: int) -> bool:
|
|
37
|
+
"""
|
|
38
|
+
Sleep until reaching target LET.
|
|
39
|
+
|
|
40
|
+
Checks shutdown event every 100ms. Uses hybrid sleep approach:
|
|
41
|
+
- For waits > 10ms: Sleep in chunks (max 100ms) for shutdown responsiveness
|
|
42
|
+
- For waits < 10ms: Sleep in 100μs increments for precision
|
|
43
|
+
|
|
44
|
+
:param let_us: Target logical execution time in microseconds.
|
|
45
|
+
:return: True if wait completed normally, False if interrupted by event.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
target_timestamp_us = self._start_timestamp_us + let_us
|
|
49
|
+
while True:
|
|
50
|
+
# Check if we should exit
|
|
51
|
+
current = now_us()
|
|
52
|
+
if current >= target_timestamp_us:
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
# Check event if provided
|
|
56
|
+
if self._event and self._event.is_set():
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
# Sleep for chunk
|
|
60
|
+
remaining_us = target_timestamp_us - current
|
|
61
|
+
sleep_us = min(remaining_us, 100_000) if remaining_us > 10_000 else min(remaining_us, 100)
|
|
62
|
+
time.sleep(sleep_us / 1_000_000)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import TypeVar, overload
|
|
3
|
+
|
|
4
|
+
from antioch.module.token import Token, TokenType
|
|
5
|
+
from common.ark.module import ModuleParameter
|
|
6
|
+
from common.ark.node import NodeOutput
|
|
7
|
+
from common.message import Image, ImuSample, JointStates, JointTargets, Message, PirStatus, RadarScan
|
|
8
|
+
from common.utils.logger import Logger
|
|
9
|
+
from common.utils.time import now_us
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound=Message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Execution:
|
|
15
|
+
"""
|
|
16
|
+
Node execution context.
|
|
17
|
+
|
|
18
|
+
Provides access to inputs, outputs, hardware, logger, and module parameters
|
|
19
|
+
for user callbacks.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
let_us: int
|
|
24
|
+
budget_us: int
|
|
25
|
+
parameters: dict[str, ModuleParameter]
|
|
26
|
+
logger: Logger
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
name: str,
|
|
31
|
+
budget_us: int,
|
|
32
|
+
parameters: dict[str, ModuleParameter],
|
|
33
|
+
outputs: dict[str, NodeOutput],
|
|
34
|
+
logger: Logger,
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initialize execution context with static configuration.
|
|
38
|
+
|
|
39
|
+
:param name: Node name.
|
|
40
|
+
:param budget_us: Node execution budget in microseconds.
|
|
41
|
+
:param parameters: Module parameters.
|
|
42
|
+
:param outputs: Node output configurations.
|
|
43
|
+
:param logger: Logger for this execution context.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
self.name = name
|
|
47
|
+
self.budget_us = budget_us
|
|
48
|
+
self.parameters = parameters
|
|
49
|
+
self.logger = logger
|
|
50
|
+
|
|
51
|
+
self._output_configs = outputs
|
|
52
|
+
self._outputs = {name: Output(name, output_config, self) for name, output_config in outputs.items()}
|
|
53
|
+
|
|
54
|
+
# Execution runtime state
|
|
55
|
+
self.let_us = 0
|
|
56
|
+
self._start_timestamp_us = 0
|
|
57
|
+
self._start_time_us = 0
|
|
58
|
+
self._inputs: dict[str, Input] = {}
|
|
59
|
+
self._output_data: dict[str, bytes | None] = {}
|
|
60
|
+
self._hardware_reads: dict[str, bytes] = {}
|
|
61
|
+
self._hardware_writes: dict[str, bytes] = {}
|
|
62
|
+
|
|
63
|
+
def start(
|
|
64
|
+
self,
|
|
65
|
+
let_us: int,
|
|
66
|
+
input_data: dict[str, list[Token]] = {},
|
|
67
|
+
hardware_reads: dict[str, bytes] = {},
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Start a new execution with runtime data.
|
|
71
|
+
|
|
72
|
+
:param let_us: Logical execution time.
|
|
73
|
+
:param input_data: Input tokens keyed by input name.
|
|
74
|
+
:param hardware_reads: Hardware read data keyed by hardware name.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
self.let_us = let_us
|
|
78
|
+
self._start_timestamp_us = now_us()
|
|
79
|
+
self._start_time_us = time.monotonic_ns() // 1000
|
|
80
|
+
self._inputs = {name: Input(tokens) for name, tokens in input_data.items()}
|
|
81
|
+
self._output_data.clear()
|
|
82
|
+
self._hardware_reads = hardware_reads
|
|
83
|
+
self._hardware_writes.clear()
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def elapsed_us(self) -> int:
|
|
87
|
+
"""
|
|
88
|
+
Get elapsed time in microseconds since execution start.
|
|
89
|
+
|
|
90
|
+
:return: Elapsed time in microseconds.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
return time.monotonic_ns() // 1000 - self._start_time_us
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def remaining_us(self) -> int:
|
|
97
|
+
"""
|
|
98
|
+
Get remaining time in microseconds until execution budget is exhausted.
|
|
99
|
+
|
|
100
|
+
:return: Remaining time in microseconds.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
return max(0, self.budget_us - self.elapsed_us)
|
|
104
|
+
|
|
105
|
+
def input(self, name: str) -> "Input":
|
|
106
|
+
"""
|
|
107
|
+
Get input by name.
|
|
108
|
+
|
|
109
|
+
:param name: Input name.
|
|
110
|
+
:return: Input wrapper.
|
|
111
|
+
:raises KeyError: If input not found.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
if name not in self._inputs:
|
|
115
|
+
raise KeyError(f"Input '{name}' not found")
|
|
116
|
+
return self._inputs[name]
|
|
117
|
+
|
|
118
|
+
def output(self, name: str) -> "Output":
|
|
119
|
+
"""
|
|
120
|
+
Get output by name.
|
|
121
|
+
|
|
122
|
+
:param name: Output name.
|
|
123
|
+
:return: Output wrapper.
|
|
124
|
+
:raises KeyError: If output not found.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
if name not in self._outputs:
|
|
128
|
+
raise KeyError(f"Output '{name}' not found")
|
|
129
|
+
return self._outputs[name]
|
|
130
|
+
|
|
131
|
+
def read_camera(self, name: str) -> Image | None:
|
|
132
|
+
"""
|
|
133
|
+
Read camera image.
|
|
134
|
+
|
|
135
|
+
:param name: Hardware name.
|
|
136
|
+
:return: Camera image, or None if no data.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
data = self._hardware_reads.get(name)
|
|
140
|
+
return Image.unpack(data) if data else None
|
|
141
|
+
|
|
142
|
+
def read_imu(self, name: str) -> ImuSample | None:
|
|
143
|
+
"""
|
|
144
|
+
Read IMU sample.
|
|
145
|
+
|
|
146
|
+
:param name: Hardware name.
|
|
147
|
+
:return: IMU sensor measurements, or None if no data.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
data = self._hardware_reads.get(name)
|
|
151
|
+
return ImuSample.unpack(data) if data else None
|
|
152
|
+
|
|
153
|
+
def read_radar(self, name: str) -> RadarScan | None:
|
|
154
|
+
"""
|
|
155
|
+
Read radar scan.
|
|
156
|
+
|
|
157
|
+
:param name: Hardware name.
|
|
158
|
+
:return: Radar scan with detections, or None if no data.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
data = self._hardware_reads.get(name)
|
|
162
|
+
return RadarScan.unpack(data) if data else None
|
|
163
|
+
|
|
164
|
+
def read_pir(self, name: str) -> PirStatus | None:
|
|
165
|
+
"""
|
|
166
|
+
Read PIR sensor status.
|
|
167
|
+
|
|
168
|
+
:param name: Hardware name.
|
|
169
|
+
:return: PIR status with detection state and signal info, or None if no data.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
data = self._hardware_reads.get(name)
|
|
173
|
+
return PirStatus.unpack(data) if data else None
|
|
174
|
+
|
|
175
|
+
def read_actuator_group(self, name: str) -> JointStates | None:
|
|
176
|
+
"""
|
|
177
|
+
Read joint states from actuator group.
|
|
178
|
+
|
|
179
|
+
:param name: Hardware name.
|
|
180
|
+
:return: Joint states, or None if no data.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
data = self._hardware_reads.get(name)
|
|
184
|
+
return JointStates.unpack(data) if data else None
|
|
185
|
+
|
|
186
|
+
def write_actuator_group(self, name: str, targets: JointTargets) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Write joint targets to actuator group.
|
|
189
|
+
|
|
190
|
+
Queues joint targets for node completion.
|
|
191
|
+
|
|
192
|
+
:param name: Hardware name.
|
|
193
|
+
:param targets: Joint targets to apply.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
self._hardware_writes[name] = targets.pack()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Input:
|
|
200
|
+
"""
|
|
201
|
+
Input wrapper for accessing tokens.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, tokens: list[Token]):
|
|
205
|
+
"""
|
|
206
|
+
Initialize input.
|
|
207
|
+
|
|
208
|
+
:param tokens: List of tokens for this input.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
self._tokens = tokens
|
|
212
|
+
|
|
213
|
+
@overload
|
|
214
|
+
def data(self, message_cls: type[T], n: int = 1) -> T | None: ...
|
|
215
|
+
|
|
216
|
+
@overload
|
|
217
|
+
def data(self, message_cls: type[T], n: int) -> list[T]: ...
|
|
218
|
+
|
|
219
|
+
def data(self, message_cls: type[T], n: int = 1) -> list[T] | T | None:
|
|
220
|
+
"""
|
|
221
|
+
Get deserialized data from tokens.
|
|
222
|
+
|
|
223
|
+
:param message_cls: Message type to deserialize.
|
|
224
|
+
:param n: Number of tokens to return (1 for single, >1 for list).
|
|
225
|
+
:return: Single message, list of messages, or None if no data.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
data = []
|
|
229
|
+
for token in reversed(self._tokens):
|
|
230
|
+
if token.status == TokenType.DATA and token.payload:
|
|
231
|
+
data.append(message_cls.unpack(token.payload))
|
|
232
|
+
if len(data) >= n:
|
|
233
|
+
break
|
|
234
|
+
return data if n > 1 else (data[0] if data else None)
|
|
235
|
+
|
|
236
|
+
def tokens(self) -> list[Token]:
|
|
237
|
+
"""
|
|
238
|
+
Get all tokens for this input.
|
|
239
|
+
|
|
240
|
+
:return: List of tokens.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
return self._tokens
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class Output:
|
|
247
|
+
"""
|
|
248
|
+
Output wrapper for setting data.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def __init__(self, name: str, output_config: NodeOutput, execution: Execution):
|
|
252
|
+
"""
|
|
253
|
+
Initialize output.
|
|
254
|
+
|
|
255
|
+
:param name: Output name.
|
|
256
|
+
:param output_config: Output configuration from node.
|
|
257
|
+
:param execution: Parent execution context.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
self._name = name
|
|
261
|
+
self._output_config = output_config
|
|
262
|
+
self._execution = execution
|
|
263
|
+
|
|
264
|
+
def set(self, message: Message) -> "Output":
|
|
265
|
+
"""
|
|
266
|
+
Set output data.
|
|
267
|
+
|
|
268
|
+
:param message: Message to serialize and set.
|
|
269
|
+
:return: Self for chaining.
|
|
270
|
+
:raises ValueError: If message type doesn't match expected output type.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
# Validate message type matches expected output type
|
|
274
|
+
if message.get_type() != self._output_config.type:
|
|
275
|
+
raise ValueError(f"Output '{self._name}' expects type '{self._output_config.type}', got '{message.get_type()}'")
|
|
276
|
+
|
|
277
|
+
self._execution._output_data[self._name] = message.pack()
|
|
278
|
+
return self
|
antioch/module/input.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from threading import Lock
|
|
2
|
+
|
|
3
|
+
import zenoh
|
|
4
|
+
from sortedcontainers import SortedDict
|
|
5
|
+
|
|
6
|
+
from antioch.module.token import Token, TokenType
|
|
7
|
+
from common.ark.node import NodeInput
|
|
8
|
+
from common.utils.comms import CommsSession
|
|
9
|
+
|
|
10
|
+
TOKEN_INPUT_PATH = "_token/{path}"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NodeInputBuffer:
|
|
14
|
+
"""
|
|
15
|
+
Thread-safe input buffer with policy-based token retention.
|
|
16
|
+
|
|
17
|
+
Manages tokens for a single node input with automatic arrival validation
|
|
18
|
+
and buffer policy enforcement (last_n, max_age_ms, always_run, required, consume_inputs).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, input_name: str, config: NodeInput, comms: CommsSession):
|
|
22
|
+
"""
|
|
23
|
+
Create a new node input buffer.
|
|
24
|
+
|
|
25
|
+
:param input_name: Name of the input.
|
|
26
|
+
:param config: Input configuration with buffer policies.
|
|
27
|
+
:param comms: Communication session for subscribing to tokens.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
self._input_name = input_name
|
|
31
|
+
self._config = config
|
|
32
|
+
self._lock = Lock()
|
|
33
|
+
self._tokens: SortedDict[tuple[int, str, str], Token] = SortedDict()
|
|
34
|
+
|
|
35
|
+
# Subscribe to token path with callback
|
|
36
|
+
path = TOKEN_INPUT_PATH.format(path=config.path)
|
|
37
|
+
self._subscriber = comms.declare_callback_subscriber(path, self._on_token)
|
|
38
|
+
|
|
39
|
+
def close(self) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Close the input buffer and clean up resources.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
self._subscriber.undeclare()
|
|
45
|
+
|
|
46
|
+
def add_token(self, token: Token) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Add token to buffer.
|
|
49
|
+
|
|
50
|
+
:param token: Token to add.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
with self._lock:
|
|
54
|
+
key = (token.let_us, token.module_name, token.node_name)
|
|
55
|
+
self._tokens[key] = token
|
|
56
|
+
|
|
57
|
+
def get_token(self, let_us: int, source_module: str, source_node: str) -> Token | None:
|
|
58
|
+
"""
|
|
59
|
+
Get specific token or None if not arrived.
|
|
60
|
+
|
|
61
|
+
:param let_us: Logical execution time of the token.
|
|
62
|
+
:param source_module: Source module name.
|
|
63
|
+
:param source_node: Source node name.
|
|
64
|
+
:return: Token if found, None otherwise.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
with self._lock:
|
|
68
|
+
key = (let_us, source_module, source_node)
|
|
69
|
+
return self._tokens.get(key)
|
|
70
|
+
|
|
71
|
+
def prepare_execution(self, let_us: int) -> list[Token] | None:
|
|
72
|
+
"""
|
|
73
|
+
Prepare tokens for execution by collecting and applying buffer policies.
|
|
74
|
+
|
|
75
|
+
:param let_us: Current logical execution time.
|
|
76
|
+
:return: Filtered tokens ready for execution, or None if required input missing.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
with self._lock:
|
|
80
|
+
# Collect tokens with let_us <= current execution time
|
|
81
|
+
tokens = [t for t in self._tokens.values() if t.let_us <= let_us]
|
|
82
|
+
|
|
83
|
+
# Filter to data tokens only (unless always_run allows errors/overruns)
|
|
84
|
+
if not self._config.always_run:
|
|
85
|
+
tokens = [t for t in tokens if t.status == TokenType.DATA]
|
|
86
|
+
|
|
87
|
+
# Check required input constraint
|
|
88
|
+
if self._config.required and len(tokens) == 0:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# Keep only last N tokens
|
|
92
|
+
if len(tokens) > self._config.last_n:
|
|
93
|
+
tokens = tokens[-self._config.last_n :]
|
|
94
|
+
|
|
95
|
+
# Filter out stale tokens based on max age
|
|
96
|
+
if self._config.max_age_ms is not None:
|
|
97
|
+
max_age_us = self._config.max_age_ms * 1000
|
|
98
|
+
tokens = [t for t in tokens if let_us - t.let_us <= max_age_us]
|
|
99
|
+
|
|
100
|
+
# Add filtered tokens back to buffer if not consuming
|
|
101
|
+
if not self._config.consume_inputs:
|
|
102
|
+
for t in tokens:
|
|
103
|
+
key = (t.let_us, t.module_name, t.node_name)
|
|
104
|
+
self._tokens[key] = t
|
|
105
|
+
|
|
106
|
+
return tokens
|
|
107
|
+
|
|
108
|
+
def _on_token(self, sample: zenoh.Sample) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Callback thread: receive and buffer token.
|
|
111
|
+
|
|
112
|
+
:param sample: Zenoh sample containing token.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
token = Token.unpack(sample.payload.to_bytes())
|
|
116
|
+
if token.elapsed() > token.budget_us:
|
|
117
|
+
token = Token(
|
|
118
|
+
module_name=token.module_name,
|
|
119
|
+
node_name=token.node_name,
|
|
120
|
+
output_name=token.output_name,
|
|
121
|
+
let_us=token.let_us,
|
|
122
|
+
start_timestamp_us=token.start_timestamp_us,
|
|
123
|
+
budget_us=token.budget_us,
|
|
124
|
+
status=TokenType.OVERRUN,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self.add_token(token)
|