openaxis 1.0.0rc1__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.
openaxis/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """OpenAxis 1.0 Python client SDK.
2
+
3
+ Import from the submodule that matches the tier you need — each one declares
4
+ its public surface in ``__all__``:
5
+
6
+ - ``openaxis.types`` wire-format messages and value objects (no third-party deps)
7
+ - ``openaxis.process_identity`` native process identity helpers for foreground routing
8
+ - ``openaxis.geometry`` pure-Python Vec3/Quat and pose conversion helpers
9
+ - ``openaxis.diagnostics`` dependency-free diagnostic event formatting
10
+ - ``openaxis.logging`` optional rotating session files and console/UI mirrors
11
+ - ``openaxis.navigation`` synchronous Navigation query evaluation helpers
12
+ - ``openaxis.navigation_session`` synchronous host camera/object coordination
13
+ - ``openaxis.async_navigation_session`` asynchronous host camera coordination
14
+ - ``openaxis.client`` the async WebSocket client (requires websockets + msgpack)
15
+ - ``openaxis.connection_manager`` opt-in reconnect, metadata replay, and shutdown
16
+
17
+ Nothing is re-exported at the package root, so importing the geometry or wire
18
+ types never pulls in the websockets/msgpack transport stack.
19
+ """
@@ -0,0 +1,116 @@
1
+ """Fixed-size navigation counters. Owners serialize access; only flush logs."""
2
+ import logging
3
+
4
+
5
+ class Timing:
6
+ def __init__(self):
7
+ self.count = self.total = self.maximum = 0
8
+
9
+ def add(self, seconds):
10
+ ms = max(0, seconds * 1000)
11
+ self.count += 1
12
+ self.total += ms
13
+ self.maximum = max(self.maximum, ms)
14
+
15
+ def text(self):
16
+ return f'{self.total / self.count if self.count else 0:.1f}/{self.maximum:.1f} [{self.count}]'
17
+
18
+
19
+
20
+ class PerformanceStream:
21
+ def __init__(self):
22
+ self.received = self.coalesced = self.succeeded = self.failed = 0
23
+ self.incoming_gap, self.queue_wait = Timing(), Timing()
24
+ self.observation, self.apply, self.apply_start_gap = Timing(), Timing(), Timing()
25
+ self.turnaround = Timing()
26
+ self.last_received = self.last_apply = self.pending = None
27
+ self.queued_at = 0
28
+
29
+ def is_pending(self, sequence):
30
+ return self.pending == sequence
31
+
32
+ def receive(self, sequence, now):
33
+ self.received += 1
34
+ if self.last_received is not None:
35
+ self.incoming_gap.add(now - self.last_received)
36
+ self.last_received = self.queued_at = now
37
+ self.pending = sequence
38
+
39
+ def process(self, sequence, now):
40
+ if self.pending == sequence:
41
+ self.queue_wait.add(now - self.queued_at)
42
+ self.pending = None
43
+ return self.queued_at
44
+ return None
45
+
46
+ def applied(self, start, end, success, received_at=None):
47
+ if self.last_apply is not None:
48
+ self.apply_start_gap.add(start - self.last_apply)
49
+ self.last_apply = start
50
+ self.apply.add(end - start)
51
+ if success:
52
+ self.succeeded += 1
53
+ if received_at is not None:
54
+ self.turnaround.add(end - received_at)
55
+ else:
56
+ self.failed += 1
57
+
58
+ def text(self, name):
59
+ if not self.received and not self.observation.count and not self.apply.count:
60
+ return f'{name}: no activity'
61
+ overall = (f'turnaround avg {self.turnaround.total / self.turnaround.count:.1f} ms, max {self.turnaround.maximum:.1f} ms [{self.turnaround.count} applied]'
62
+ if self.turnaround.count else 'no updates applied')
63
+ replaced = 100 * self.coalesced / self.received if self.received else 0
64
+ return (f'{name} responsiveness: {overall}; pending poses replaced {replaced:.1f}%\n'
65
+ f'{name}: poses {self.received}, coalesced {self.coalesced}, writes {self.succeeded} ok/{self.failed} failed\n'
66
+ f' timings avg/max ms [samples]: input gap {self.incoming_gap.text()}; queue wait {self.queue_wait.text()}; '
67
+ f'observation {self.observation.text()}; apply {self.apply.text()}; apply gap {self.apply_start_gap.text()}')
68
+
69
+
70
+
71
+ class Gesture:
72
+ def __init__(self, gesture_id, token, now):
73
+ self.gesture_id, self.token, self.started = gesture_id, token, now
74
+ self.camera, self.object = PerformanceStream(), PerformanceStream()
75
+ self.reason, self.ended = '', now
76
+
77
+ def text(self):
78
+ return (f'navigation.performance gesture={self.gesture_id} reason={self.reason} '
79
+ f'duration={max(0, (self.ended - self.started) * 1000):.1f} ms\n'
80
+ f'{self.camera.text("camera")}\n{self.object.text("object")}')
81
+
82
+
83
+
84
+ class NavigationPerformance:
85
+ def __init__(self):
86
+ self.active = None
87
+ self.retired = []
88
+
89
+ def begin(self, gesture_id, token, now):
90
+ self.finish('superseded', now)
91
+ self.active = Gesture(gesture_id, token, now)
92
+
93
+ def stream(self, token, objects=False):
94
+ g = self.active
95
+ return (g.object if objects else g.camera) if g and g.token == token else None
96
+
97
+ def finish(self, reason, now, token=None):
98
+ if self.active is None or (token is not None and self.active.token != token):
99
+ return
100
+ g, self.active = self.active, None
101
+ g.reason, g.ended = reason, now
102
+ self.retired.append(g)
103
+
104
+ def take(self):
105
+ if not self.retired:
106
+ return ()
107
+ retired, self.retired = self.retired, []
108
+ return retired
109
+
110
+ @staticmethod
111
+ def flush(retired):
112
+ for g in retired:
113
+ try:
114
+ logging.getLogger('openaxis').info(g.text())
115
+ except Exception:
116
+ pass # Host logging handlers must not affect navigation.