blinkview 0.18.1__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.
Files changed (222) hide show
  1. blinkview/__init__.py +27 -0
  2. blinkview/__main__.py +205 -0
  3. blinkview/assets/icon.png +0 -0
  4. blinkview/cli.py +134 -0
  5. blinkview/core/__init__.py +6 -0
  6. blinkview/core/array_pool.py +175 -0
  7. blinkview/core/base_daemon.py +318 -0
  8. blinkview/core/base_reorder.py +31 -0
  9. blinkview/core/batch_queue.py +166 -0
  10. blinkview/core/bindable.py +39 -0
  11. blinkview/core/buffers.py +181 -0
  12. blinkview/core/central_storage.py +366 -0
  13. blinkview/core/channel_registry.py +6 -0
  14. blinkview/core/cold_archive.py +110 -0
  15. blinkview/core/cold_segment.py +343 -0
  16. blinkview/core/cold_storage_archiver.py +178 -0
  17. blinkview/core/config_manager.py +531 -0
  18. blinkview/core/configurable.py +274 -0
  19. blinkview/core/constants.py +31 -0
  20. blinkview/core/device_identity.py +215 -0
  21. blinkview/core/dtypes.py +41 -0
  22. blinkview/core/factory.py +145 -0
  23. blinkview/core/factory_category_registry.py +38 -0
  24. blinkview/core/factory_registry.py +73 -0
  25. blinkview/core/frame_warmup_registry.py +45 -0
  26. blinkview/core/hot_tier_memory_governor.py +152 -0
  27. blinkview/core/id_history.py +147 -0
  28. blinkview/core/id_registry/__init__.py +9 -0
  29. blinkview/core/id_registry/registry.py +421 -0
  30. blinkview/core/id_registry/tables.py +235 -0
  31. blinkview/core/id_registry/types.py +33 -0
  32. blinkview/core/limits.py +37 -0
  33. blinkview/core/log_fetch.py +591 -0
  34. blinkview/core/logger.py +268 -0
  35. blinkview/core/module_snapshot.py +615 -0
  36. blinkview/core/numba_config.py +75 -0
  37. blinkview/core/numba_setup.py +128 -0
  38. blinkview/core/numpy_batch_manager.py +766 -0
  39. blinkview/core/numpy_log.py +1116 -0
  40. blinkview/core/pipeline_manager.py +269 -0
  41. blinkview/core/playback_clock.py +208 -0
  42. blinkview/core/playback_follow.py +202 -0
  43. blinkview/core/playback_ranges.py +137 -0
  44. blinkview/core/plugin_manager.py +72 -0
  45. blinkview/core/profile_params.py +458 -0
  46. blinkview/core/registry.py +1212 -0
  47. blinkview/core/reorderer.py +298 -0
  48. blinkview/core/session_generation.py +20 -0
  49. blinkview/core/settings_manager.py +99 -0
  50. blinkview/core/sources.py +193 -0
  51. blinkview/core/system_context.py +29 -0
  52. blinkview/core/task_manager.py +124 -0
  53. blinkview/core/time_sync_engine.py +145 -0
  54. blinkview/core/types/empty.py +23 -0
  55. blinkview/core/types/formatting.py +17 -0
  56. blinkview/core/types/frames.py +40 -0
  57. blinkview/core/types/kv_extraction.py +76 -0
  58. blinkview/core/types/log_batch.py +53 -0
  59. blinkview/core/types/modules.py +65 -0
  60. blinkview/core/types/output.py +15 -0
  61. blinkview/core/types/parsing.py +177 -0
  62. blinkview/core/types/telemetry.py +27 -0
  63. blinkview/core/warmup.py +66 -0
  64. blinkview/core/warmup_registry.py +42 -0
  65. blinkview/core/zstd_file_compression.py +97 -0
  66. blinkview/daemon.py +13 -0
  67. blinkview/gui.py +20 -0
  68. blinkview/io/BaseReader.py +66 -0
  69. blinkview/io/__init__.py +38 -0
  70. blinkview/io/adb_reader.py +667 -0
  71. blinkview/io/adb_time_syncer.py +483 -0
  72. blinkview/io/benchmark.py +220 -0
  73. blinkview/io/binary_file_reader.py +177 -0
  74. blinkview/io/can_bus.py +265 -0
  75. blinkview/io/file_tail_reader.py +163 -0
  76. blinkview/io/rtt.py +466 -0
  77. blinkview/io/serial_time_syncer.py +410 -0
  78. blinkview/io/source_handshake.py +148 -0
  79. blinkview/io/tcp_client.py +221 -0
  80. blinkview/io/tcp_server.py +252 -0
  81. blinkview/io/uart.py +470 -0
  82. blinkview/io/udp_reader.py +232 -0
  83. blinkview/ops/benchmark.py +50 -0
  84. blinkview/ops/buffers.py +158 -0
  85. blinkview/ops/codec_adb_long.py +467 -0
  86. blinkview/ops/codecs.py +273 -0
  87. blinkview/ops/constants.py +58 -0
  88. blinkview/ops/decode_loop.py +243 -0
  89. blinkview/ops/desktop_timestamp.py +187 -0
  90. blinkview/ops/discovery.py +91 -0
  91. blinkview/ops/dispatch.py +124 -0
  92. blinkview/ops/formatting.py +657 -0
  93. blinkview/ops/generic.py +24 -0
  94. blinkview/ops/id_history.py +23 -0
  95. blinkview/ops/id_registry.py +36 -0
  96. blinkview/ops/id_resolution.py +127 -0
  97. blinkview/ops/kv_extraction.py +522 -0
  98. blinkview/ops/kv_filter.py +187 -0
  99. blinkview/ops/levels.py +84 -0
  100. blinkview/ops/module_filter.py +124 -0
  101. blinkview/ops/module_snapshot.py +267 -0
  102. blinkview/ops/modules.py +431 -0
  103. blinkview/ops/normalization.py +5 -0
  104. blinkview/ops/reorder.py +143 -0
  105. blinkview/ops/segments.py +658 -0
  106. blinkview/ops/stage_loop.py +111 -0
  107. blinkview/ops/string_table.py +78 -0
  108. blinkview/ops/strings.py +317 -0
  109. blinkview/ops/telemetry.py +913 -0
  110. blinkview/ops/telemetry_table.py +155 -0
  111. blinkview/ops/text_filter.py +83 -0
  112. blinkview/ops/timestamp_idf.py +62 -0
  113. blinkview/ops/timestamps.py +391 -0
  114. blinkview/ops/timesync.py +281 -0
  115. blinkview/ops/unified_log_scan.py +213 -0
  116. blinkview/ops/views.py +137 -0
  117. blinkview/ops/zephyr_timestamp.py +173 -0
  118. blinkview/parsers/__init__.py +35 -0
  119. blinkview/parsers/adb_decoder.py +118 -0
  120. blinkview/parsers/binary_parser.py +572 -0
  121. blinkview/parsers/can_parser.py +274 -0
  122. blinkview/parsers/frame_decoders.py +199 -0
  123. blinkview/parsers/frame_parsers.py +628 -0
  124. blinkview/parsers/key_value.py +152 -0
  125. blinkview/parsers/module_gen.py +134 -0
  126. blinkview/parsers/multi_rule_key_value.py +655 -0
  127. blinkview/parsers/parser.py +126 -0
  128. blinkview/parsers/state.py +64 -0
  129. blinkview/parsers/text_filter.py +25 -0
  130. blinkview/parsers/time_sync_profiler.py +383 -0
  131. blinkview/parsers/unified_log_replay.py +412 -0
  132. blinkview/storage/__init__.py +12 -0
  133. blinkview/storage/file_logger.py +429 -0
  134. blinkview/storage/file_manager.py +702 -0
  135. blinkview/storage/log_file_archive.py +49 -0
  136. blinkview/subscribers/console.py +122 -0
  137. blinkview/subscribers/subscriber.py +31 -0
  138. blinkview/ui/__init__.py +6 -0
  139. blinkview/ui/cli_args.py +48 -0
  140. blinkview/ui/constants.py +18 -0
  141. blinkview/ui/gui_context.py +125 -0
  142. blinkview/ui/main_window.py +1525 -0
  143. blinkview/ui/module_gui_meta.py +14 -0
  144. blinkview/ui/native_dark_mode.py +40 -0
  145. blinkview/ui/run.py +220 -0
  146. blinkview/ui/utils/config_node.py +253 -0
  147. blinkview/ui/utils/config_node_manager.py +140 -0
  148. blinkview/ui/utils/in_development.py +73 -0
  149. blinkview/ui/utils/log_velocity_tracker.py +63 -0
  150. blinkview/ui/utils/ui_state_handler.py +367 -0
  151. blinkview/ui/utils/update_checker.py +69 -0
  152. blinkview/ui/utils/view_presets.py +190 -0
  153. blinkview/ui/utils/window_manager.py +185 -0
  154. blinkview/ui/utils/window_title.py +26 -0
  155. blinkview/ui/widget_registry.py +35 -0
  156. blinkview/ui/widgets/TelemetryWatch.py +1835 -0
  157. blinkview/ui/widgets/__init__.py +6 -0
  158. blinkview/ui/widgets/action_button_delegate.py +185 -0
  159. blinkview/ui/widgets/base_list_item.py +142 -0
  160. blinkview/ui/widgets/base_sidebar_widget.py +147 -0
  161. blinkview/ui/widgets/config/__init__.py +6 -0
  162. blinkview/ui/widgets/config/demo_window.py +56 -0
  163. blinkview/ui/widgets/config/dynamic_config.py +1280 -0
  164. blinkview/ui/widgets/config/mock_backend.py +121 -0
  165. blinkview/ui/widgets/config/style_config.py +26 -0
  166. blinkview/ui/widgets/config_tool_button_widget.py +209 -0
  167. blinkview/ui/widgets/config_widget_factory.py +358 -0
  168. blinkview/ui/widgets/device_sidebar.py +32 -0
  169. blinkview/ui/widgets/jog_wheel_button.py +131 -0
  170. blinkview/ui/widgets/kv_filter_line_edit.py +45 -0
  171. blinkview/ui/widgets/log_highlighter.py +65 -0
  172. blinkview/ui/widgets/log_table_viewer.py +1839 -0
  173. blinkview/ui/widgets/log_view_mode.py +12 -0
  174. blinkview/ui/widgets/log_viewer.py +1364 -0
  175. blinkview/ui/widgets/message_box.py +86 -0
  176. blinkview/ui/widgets/mock_config.py +178 -0
  177. blinkview/ui/widgets/module_filter_model.py +167 -0
  178. blinkview/ui/widgets/module_filter_sidebar.py +109 -0
  179. blinkview/ui/widgets/module_filter_table.py +602 -0
  180. blinkview/ui/widgets/pipelines_sidebar.py +69 -0
  181. blinkview/ui/widgets/playback_control.py +508 -0
  182. blinkview/ui/widgets/plotter.py +1673 -0
  183. blinkview/ui/widgets/profile_params_dialog.py +391 -0
  184. blinkview/ui/widgets/searchable_log_area.py +464 -0
  185. blinkview/ui/widgets/telemetry_table.py +1214 -0
  186. blinkview/ui/widgets/title_bar.py +71 -0
  187. blinkview/ui/widgets/toast.py +386 -0
  188. blinkview/ui/widgets/toast_dispatcher.py +64 -0
  189. blinkview/ui/widgets/update_widget.py +350 -0
  190. blinkview/ui/windows/detached_tab_window.py +114 -0
  191. blinkview/utils/adb.py +41 -0
  192. blinkview/utils/atomic_json_dump.py +50 -0
  193. blinkview/utils/audit_imports.py +40 -0
  194. blinkview/utils/cli_updater.py +81 -0
  195. blinkview/utils/config_handler.py +99 -0
  196. blinkview/utils/dev_mode.py +37 -0
  197. blinkview/utils/dict_utils.py +75 -0
  198. blinkview/utils/format_metric.py +31 -0
  199. blinkview/utils/generate_id.py +40 -0
  200. blinkview/utils/global_settings.py +35 -0
  201. blinkview/utils/install_info.py +144 -0
  202. blinkview/utils/level_map.py +194 -0
  203. blinkview/utils/log_filter.py +109 -0
  204. blinkview/utils/log_level.py +82 -0
  205. blinkview/utils/paths.py +28 -0
  206. blinkview/utils/profile_memory.py +63 -0
  207. blinkview/utils/project_settings.py +333 -0
  208. blinkview/utils/session_export.py +676 -0
  209. blinkview/utils/session_lister.py +249 -0
  210. blinkview/utils/session_metadata.py +39 -0
  211. blinkview/utils/settings.py +132 -0
  212. blinkview/utils/settings_updater.py +23 -0
  213. blinkview/utils/throughput.py +154 -0
  214. blinkview/utils/time_utils.py +143 -0
  215. blinkview/utils/updater.py +408 -0
  216. blinkview/utils/used_modules.py +18 -0
  217. blinkview/utils/utc_offset.py +27 -0
  218. blinkview-0.18.1.dist-info/METADATA +396 -0
  219. blinkview-0.18.1.dist-info/RECORD +222 -0
  220. blinkview-0.18.1.dist-info/WHEEL +4 -0
  221. blinkview-0.18.1.dist-info/entry_points.txt +2 -0
  222. blinkview-0.18.1.dist-info/licenses/LICENSE +373 -0
blinkview/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ #
5
+ # Copyright (c) 2026 Roland Uuesoo
6
+
7
+ import os
8
+ from pathlib import Path
9
+
10
+ __version__ = "0.18.1"
11
+ __author__ = "Roland Uuesoo"
12
+
13
+ __app_name__ = "BlinkView"
14
+ __org_domain__ = "ee.incubator"
15
+
16
+ # Your specific ID requirement
17
+ __app_id__ = f"{__org_domain__}.{__app_name__.lower()}"
18
+
19
+
20
+ # Robust icon path (relative to this file)
21
+ ICON_PATH = (Path(__file__).parent / "assets" / "icon.png").absolute()
22
+
23
+
24
+ # These must come BEFORE numpy, scipy, or any telemetry logic
25
+ os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
26
+ os.environ.setdefault("MKL_NUM_THREADS", "1")
27
+ os.environ.setdefault("OMP_NUM_THREADS", "1")
blinkview/__main__.py ADDED
@@ -0,0 +1,205 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ #
5
+ # Copyright (c) 2026 Roland Uuesoo
6
+
7
+ import faulthandler
8
+ import sys
9
+ from argparse import ArgumentParser
10
+
11
+ from blinkview.ui.cli_args import setup_gui_parser, setup_replay_parser
12
+
13
+
14
+ def run_init(args):
15
+ from blinkview.utils.project_settings import ProjectSettings
16
+
17
+ ProjectSettings.init(args.path)
18
+
19
+
20
+ def run_gui(args):
21
+ from .gui import main
22
+
23
+ main(args)
24
+
25
+
26
+ def run_cli(args):
27
+ from .cli import main
28
+
29
+ main()
30
+
31
+
32
+ def run_daemon(args):
33
+ print(f"🔌 Starting Daemon on port: {args.port}")
34
+
35
+
36
+ def apply_session_profile(args, session_info):
37
+ """Replays with the profile the session was recorded with (metadata.json config.profile)
38
+ unless --profile/--config was given explicitly. Only if that profile still exists in this
39
+ workspace - FileManager creates a missing profile dir on the fly, so a renamed/deleted
40
+ profile would otherwise silently open as a brand-new empty one."""
41
+ if args.profile is not None or args.config is not None or not session_info.profile:
42
+ return
43
+
44
+ from blinkview.utils.project_settings import get_workspace_dir
45
+
46
+ if (get_workspace_dir() / "profiles" / session_info.profile).is_dir():
47
+ args.profile = session_info.profile
48
+ print(f"Using session's profile '{session_info.profile}'.")
49
+ else:
50
+ print(f"Session's profile '{session_info.profile}' not found in this workspace - using the active profile.")
51
+
52
+
53
+ def run_replay(args):
54
+ # utils/session_lister.py deliberately avoids importing anything from blinkview.storage/
55
+ # blinkview.parsers (the numba/id_registry cluster, ~600 modules) so --list stays fast.
56
+ from blinkview.utils.session_lister import list_sessions, resolve_log_root, resolve_session, unified_log_parts
57
+
58
+ log_dir, project_name = resolve_log_root(log_dir=args.logdir)
59
+
60
+ if args.list:
61
+ sessions = [s for s in list_sessions(log_dir, project_name) if unified_log_parts(s)]
62
+ if not sessions:
63
+ print(f"No replay sessions found for project '{project_name}' in {log_dir}.")
64
+ return
65
+ for s in sessions:
66
+ print(f"{s.session_id} [{s.status}] {s.display_name} (profile={s.profile}, created={s.created_at})")
67
+ return
68
+
69
+ session_info = resolve_session(log_dir, project_name, name=args.name, last=args.last)
70
+ if session_info is None:
71
+ print(f"No matching replay session found (name={args.name!r}, last={args.last}).")
72
+ sys.exit(1)
73
+
74
+ parts = unified_log_parts(session_info)
75
+ if not parts:
76
+ print(f"Session '{session_info.session_id}' has no unified log to replay.")
77
+ sys.exit(1)
78
+
79
+ apply_session_profile(args, session_info)
80
+
81
+ from blinkview.ui.run import run as run_gui
82
+
83
+ run_gui(args, replay_mode=True, replay_session_info=session_info)
84
+
85
+
86
+ # --- Parser Setup ---
87
+ def enable_fault_handler():
88
+ """Native crashes (Qt, numba, mmap) kill the process without a Python exception; with this enabled a fatal
89
+ signal prints every thread's Python stack (and the C stack of the faulting one) to stderr.
90
+
91
+ Best effort: faulthandler needs a real file descriptor on sys.stderr, which does not exist when stderr is
92
+ captured (tests) or missing (a GUI process started without a console)."""
93
+ try:
94
+ faulthandler.enable(all_threads=True)
95
+ except (RuntimeError, OSError, ValueError, AttributeError): # io.UnsupportedOperation is an OSError
96
+ pass
97
+
98
+
99
+ def main():
100
+ enable_fault_handler()
101
+
102
+ parser = ArgumentParser(description="BlinkView Telemetry Suite - 2026")
103
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
104
+
105
+ from blinkview import __version__
106
+
107
+ parser.add_argument("-v", "--version", action="version", version=f"BlinkView {__version__}")
108
+
109
+ # INIT Command
110
+ init_parser = subparsers.add_parser("init", help="Setup project")
111
+ init_parser.add_argument(
112
+ "path", nargs="?", default=".", help="Directory to initialize (default: current directory)"
113
+ )
114
+ init_parser.set_defaults(func=run_init)
115
+
116
+ # PROFILE Command
117
+ profile_parser = subparsers.add_parser("switch", help="Switch profiles")
118
+ from blinkview.utils.project_settings import handle_profile_args, setup_project_parser
119
+
120
+ setup_project_parser(profile_parser)
121
+ profile_parser.set_defaults(func=handle_profile_args)
122
+
123
+ # GUI Command
124
+ gui_parser = subparsers.add_parser("gui", help="Open Interface")
125
+ setup_gui_parser(gui_parser)
126
+
127
+ gui_parser.set_defaults(func=run_gui)
128
+
129
+ cli_parser = subparsers.add_parser("cli", help="Command Line Interface")
130
+ cli_parser.set_defaults(func=run_cli)
131
+
132
+ # DAEMON Command
133
+ daemon_parser = subparsers.add_parser("daemon", help="Background service")
134
+ daemon_parser.add_argument("--port", type=int, default=8000)
135
+ daemon_parser.set_defaults(func=run_daemon)
136
+
137
+ # REPLAY Command
138
+ replay_parser = subparsers.add_parser("replay", help="Load a previously recorded session")
139
+ setup_replay_parser(replay_parser)
140
+ replay_parser.set_defaults(func=run_replay)
141
+
142
+ config_parser = subparsers.add_parser("config", help="Get and set project or global options")
143
+
144
+ # Link to the handler
145
+ from blinkview.utils.config_handler import handle_config, setup_config_parser
146
+
147
+ setup_config_parser(config_parser)
148
+ config_parser.set_defaults(func=handle_config)
149
+
150
+ export_parser = subparsers.add_parser("export", prog="blink export", help="Write a recorded session's log out as filtered text")
151
+ from blinkview.utils.session_export import run_export, setup_export_parser
152
+
153
+ setup_export_parser(export_parser)
154
+ export_parser.set_defaults(func=run_export)
155
+
156
+ update_parser = subparsers.add_parser("update", help="Manage BlinkView versions")
157
+ from blinkview.utils.cli_updater import handle_update, setup_update_parser
158
+
159
+ setup_update_parser(update_parser)
160
+ update_parser.set_defaults(func=handle_update)
161
+
162
+ # --- ARGUMENT INJECTION LOGIC ---
163
+ # This checks if the first arg is a valid command.
164
+ # If not, it inserts 'gui' as the first argument so argparse handles it.
165
+ valid_commands = subparsers.choices.keys()
166
+
167
+ # sys.argv[0] is the script name. We check sys.argv[1].
168
+ if len(sys.argv) > 1:
169
+ # If it's not a command and not -v/--version/--help...
170
+ if sys.argv[1] not in valid_commands and sys.argv[1] not in ["-h", "--help", "-v", "--version"]:
171
+ sys.argv.insert(1, "gui")
172
+ elif len(sys.argv) == 1:
173
+ # No arguments at all? Default to gui.
174
+ sys.argv.append("gui")
175
+
176
+ args = parser.parse_args()
177
+
178
+ if args.command != "config":
179
+ from blinkview.utils.updater import get_update_notice
180
+
181
+ # stderr: a notice, not command output - `blink replay --list > file` (or an export to
182
+ # stdout) must not start with it.
183
+ msg = get_update_notice()
184
+ if msg:
185
+ print(f"[{msg}]\n", file=sys.stderr)
186
+
187
+ try:
188
+ # --- Execution ---
189
+ if args.command is None:
190
+ # Fallback if the user just types 'blinkview'
191
+ gui_args = gui_parser.parse_args(sys.argv[1:])
192
+ run_gui(gui_args)
193
+ else:
194
+ # This one line replaces the entire 'match' statement!
195
+ args.func(args)
196
+ except Exception:
197
+ # dump formatexc
198
+ from traceback import print_exc
199
+
200
+ print_exc()
201
+ sys.exit(1)
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()
Binary file
blinkview/cli.py ADDED
@@ -0,0 +1,134 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ #
5
+ # Copyright (c) 2026 Roland Uuesoo
6
+
7
+ import signal
8
+ from queue import Empty, Queue
9
+ from threading import Thread
10
+
11
+ import readchar
12
+ from rich.console import Console
13
+
14
+ # Internal BlinkView imports
15
+ from blinkview.core.registry import Registry
16
+ from blinkview.subscribers.console import ConsoleSubscriber # noqa: F401 - registers "console" with SubscriberFactory
17
+ from blinkview.utils.log_level import LogLevel
18
+
19
+
20
+ class BlinkViewApp:
21
+ def __init__(self):
22
+ self.console = Console()
23
+ self.registry = Registry("test")
24
+ self.console_sub = None
25
+
26
+ self.input_queue = Queue()
27
+ self.running = False
28
+
29
+ def init_console(self, level=LogLevel.ALL):
30
+ """Lazy-loads and configures the console subscriber."""
31
+ if self.console_sub is None:
32
+ self.console_sub = self.registry.build_subscriber("CLI", "Console", console=self.console)
33
+ self.console_sub.start()
34
+
35
+ self.console_sub.set_level(level)
36
+
37
+ def _signal_handler(self, signum, frame):
38
+ """Catches OS signals to shut down cleanly."""
39
+ self.input_queue.put("q")
40
+
41
+ def _keyboard_listener(self):
42
+ """Background thread to catch raw keystrokes."""
43
+ while self.running:
44
+ try:
45
+ key = readchar.readkey().lower()
46
+ self.input_queue.put(key)
47
+ if key == "q":
48
+ break
49
+ except KeyboardInterrupt:
50
+ self.input_queue.put("q")
51
+ break
52
+
53
+ def handle_input(self, key: str):
54
+ """Routes the parsed keystrokes to the correct actions."""
55
+ if key == "q":
56
+ self.console.print("\n[bold yellow]Quit signal received...[/bold yellow]")
57
+ self.running = False
58
+
59
+ elif key in ("i", "a", "d", "e", "w"):
60
+ # Map keys directly to LogLevels
61
+ level_map = {
62
+ "a": LogLevel.ALL,
63
+ "d": LogLevel.DEBUG,
64
+ "i": LogLevel.INFO,
65
+ "w": LogLevel.WARN,
66
+ "e": LogLevel.ERROR,
67
+ }
68
+ if self.console_sub:
69
+ self.console_sub.set_level(level_map[key])
70
+
71
+ elif key == "r":
72
+ # destroy and recreate registry
73
+ is_console_enabled = self.console_sub is not None
74
+ if is_console_enabled:
75
+ log_level = self.console_sub.log_level
76
+ self.console_sub.stop()
77
+ self.registry.stop()
78
+ self.console_sub = None
79
+ self.registry = Registry("test")
80
+ self.registry.start()
81
+ self.console.print("[bold cyan]Registry restarted.[/bold cyan]")
82
+ if is_console_enabled:
83
+ self.init_console(log_level)
84
+
85
+ elif key == "s":
86
+ if self.console_sub is None:
87
+ self.init_console()
88
+ else:
89
+ self.console_sub.streaming = not self.console_sub.streaming
90
+ state = "resumed" if self.console_sub.streaming else "paused"
91
+ self.console.print(f"Streaming {state}. Press 's' to toggle.")
92
+
93
+ def run(self):
94
+ """Main application loop."""
95
+ self.console.print("Starting BlinkView CLI...")
96
+
97
+ # Wire up OS signals
98
+ signal.signal(signal.SIGINT, self._signal_handler)
99
+ signal.signal(signal.SIGTERM, self._signal_handler)
100
+
101
+ self.registry.start()
102
+ self.init_console(LogLevel.WARN)
103
+
104
+ self.console.print('[bold green]BLINK ACTIVE[/bold green] | [dim]Press "s" to pause, "q" to quit[/dim]')
105
+
106
+ self.running = True
107
+ listener_thread = Thread(target=self._keyboard_listener, daemon=True)
108
+ listener_thread.start()
109
+
110
+ try:
111
+ while self.running:
112
+ try:
113
+ # Timeout is required here to make the UI loop responsive
114
+ key = self.input_queue.get(timeout=0.05)
115
+ self.handle_input(key)
116
+ except Empty:
117
+ # Do your 20Hz background UI updates here if needed
118
+ pass
119
+
120
+ finally:
121
+ self.console.print("Exiting...")
122
+ if self.console_sub:
123
+ self.console_sub.stop()
124
+ self.registry.stop()
125
+ self.console.print("[grey50]BlinkView session ended.[/grey50]")
126
+
127
+
128
+ def main():
129
+ app = BlinkViewApp()
130
+ app.run()
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
@@ -0,0 +1,6 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ #
5
+ # Copyright (c) 2026 Roland Uuesoo
6
+
@@ -0,0 +1,175 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ #
5
+ # Copyright (c) 2026 Roland Uuesoo
6
+
7
+ import threading
8
+ import time
9
+
10
+ import numpy as np
11
+
12
+ from blinkview.core import dtypes
13
+
14
+
15
+ class PooledArrayHandle:
16
+ """
17
+ High-performance slotted handle.
18
+ Directly holds the raw power-of-two slab.
19
+ """
20
+
21
+ __slots__ = ("_pool", "array", "bucket_key", "_refcount", "_lock")
22
+
23
+ def __init__(self, pool, array, bucket_key):
24
+ self._pool = pool
25
+ self.array: np.ndarray = array
26
+ self.bucket_key = bucket_key
27
+ self._refcount = 1
28
+ self._lock = threading.Lock()
29
+
30
+ def retain(self):
31
+ with self._lock:
32
+ if self._refcount == 0:
33
+ raise RuntimeError("Cannot retain a released array.")
34
+ self._refcount += 1
35
+ return self
36
+
37
+ def release(self):
38
+ with self._lock:
39
+ if self._refcount <= 0:
40
+ return
41
+ self._refcount -= 1
42
+ if self._refcount == 0:
43
+ # Return the full, un-sliced slab
44
+ self._pool._put(self.bucket_key, self.array)
45
+ self.array = None
46
+
47
+ def __enter__(self):
48
+ return self
49
+
50
+ def __exit__(self, exc_type, exc_val, exc_tb):
51
+ self.release()
52
+
53
+ @property
54
+ def capacity(self) -> int:
55
+ """Returns the total allocated size of the raw slab."""
56
+ return a.shape[0] if (a := self.array) is not None else 0
57
+
58
+
59
+ class NumpyArrayPool:
60
+ __slots__ = ("min_bytes", "max_bytes", "lock", "buckets")
61
+
62
+ def __init__(self, min_bytes=1024, max_bytes=1024 * 1024):
63
+ self.min_bytes = min_bytes
64
+ self.max_bytes = max_bytes
65
+ self.lock = threading.Lock()
66
+ self.buckets = {}
67
+
68
+ def _calc_slab_size(self, size_in_bytes):
69
+ """Always returns the next power of two, regardless of pool limits."""
70
+ # Force conversion to Python int to support .bit_length()
71
+ size_in_bytes = int(size_in_bytes)
72
+
73
+ if size_in_bytes <= self.min_bytes:
74
+ return self.min_bytes
75
+ # Round up to next power of 2
76
+ return 1 << (size_in_bytes - 1).bit_length()
77
+
78
+ def acquire(self, element_count, dtype=dtypes.BYTE):
79
+ """
80
+ Acquires an array capable of holding at least `element_count` items.
81
+ """
82
+ dt = np.dtype(dtype)
83
+ # Calculate the required bytes internally
84
+ size_in_bytes = int(element_count) * dt.itemsize
85
+
86
+ slab_size = self._calc_slab_size(size_in_bytes)
87
+ bucket_key = (slab_size, dt)
88
+
89
+ base_arr = None
90
+
91
+ # Try to fetch from pool if within limits
92
+ if self.min_bytes <= slab_size <= self.max_bytes:
93
+ with self.lock:
94
+ bucket = self.buckets.get(bucket_key)
95
+ if bucket:
96
+ _, base_arr = bucket.pop() # LIFO
97
+
98
+ # --- DEBUG: POOL HIT ---
99
+ # mb_size = slab_size / (1024 * 1024)
100
+ # print(f"[POOL] ♻️ REUSE mb={mb_size:.4f} bytes={slab_size} dtype={dt.name}")
101
+
102
+ # Always allocate a full power-of-two slab if no pooled array was found
103
+ if base_arr is None:
104
+ num_elements = slab_size // dt.itemsize
105
+
106
+ # --- DEBUG: OS ALLOCATION ---
107
+ # mb_size = slab_size / (1024 * 1024)
108
+ # print(f"[POOL] 🚨 ALLOC mb={mb_size:.4f} bytes={slab_size} dtype={dt.name} elements={num_elements}")
109
+
110
+ base_arr = np.empty(num_elements, dtype=dt)
111
+
112
+ return PooledArrayHandle(self, base_arr, bucket_key)
113
+
114
+ def _put(self, bucket_key, array):
115
+ """Thread-safe return to pool with aging timestamp."""
116
+ slab_size, dtype = bucket_key
117
+ if self.min_bytes <= slab_size <= self.max_bytes:
118
+ with self.lock:
119
+ # Initialize bucket lazily to keep __init__ clean
120
+ try:
121
+ bucket = self.buckets[bucket_key]
122
+ except KeyError:
123
+ bucket = []
124
+ self.buckets[bucket_key] = bucket
125
+
126
+ bucket.append((time.monotonic(), array))
127
+
128
+ # mb_size = slab_size / (1024 * 1024)
129
+ # print(f"[POOL] ♻️ RETUR mb={mb_size:.4f} bytes={slab_size} dtype={dtype}")
130
+ # else:
131
+ # mb_size = slab_size / (1024 * 1024)
132
+ # print(f"[POOL] 🚨 DELET mb={mb_size:.4f} bytes={slab_size} dtype={dtype}")
133
+
134
+ def cleanup(self, max_age_seconds):
135
+ """Evicts arrays that have been idle for too long."""
136
+ now = time.monotonic()
137
+ with self.lock:
138
+ for key in list(self.buckets.keys()):
139
+ bucket = self.buckets[key]
140
+ # Filter bucket: keep only arrays returned within the time window
141
+ self.buckets[key] = [(ts, arr) for ts, arr in bucket if (now - ts) < max_age_seconds]
142
+
143
+ def get(self, element_count, dtype=dtypes.BYTE):
144
+ """Sugar for context manager usage."""
145
+ return self.acquire(element_count, dtype)
146
+
147
+ def create(self, wrapper_class, *args, **kwargs):
148
+ """
149
+ Instantiates a high-level wrapper object (like PooledLogBatch),
150
+ automatically injecting this pool as the first argument.
151
+ """
152
+ return wrapper_class(self, *args, **kwargs)
153
+
154
+ def audit(self):
155
+ print("\n" + "=" * 40)
156
+ print(" NUMPY POOL STORAGE AUDIT")
157
+ print("=" * 40)
158
+ total_mb = 0
159
+ total_count = 0
160
+
161
+ with self.lock:
162
+ for (size_bytes, dtype), bucket in self.buckets.items():
163
+ count = len(bucket)
164
+ mb = (size_bytes * count) / (1024 * 1024)
165
+ total_mb += mb
166
+ total_count += count
167
+ if count > 0:
168
+ print(
169
+ f"Bucket: {size_bytes:>7} bytes | Dtype: {dtype.name:<7} | Count: {count:>6} | Total: {mb:>7.2f} MB"
170
+ )
171
+
172
+ print("-" * 40)
173
+ print(f"GRAND TOTAL POOLED: {total_mb:.2f} MB")
174
+ print(f"TOTAL ARRAY OBJECTS: {total_count:,}")
175
+ print("=" * 40 + "\n")