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.
- blinkview/__init__.py +27 -0
- blinkview/__main__.py +205 -0
- blinkview/assets/icon.png +0 -0
- blinkview/cli.py +134 -0
- blinkview/core/__init__.py +6 -0
- blinkview/core/array_pool.py +175 -0
- blinkview/core/base_daemon.py +318 -0
- blinkview/core/base_reorder.py +31 -0
- blinkview/core/batch_queue.py +166 -0
- blinkview/core/bindable.py +39 -0
- blinkview/core/buffers.py +181 -0
- blinkview/core/central_storage.py +366 -0
- blinkview/core/channel_registry.py +6 -0
- blinkview/core/cold_archive.py +110 -0
- blinkview/core/cold_segment.py +343 -0
- blinkview/core/cold_storage_archiver.py +178 -0
- blinkview/core/config_manager.py +531 -0
- blinkview/core/configurable.py +274 -0
- blinkview/core/constants.py +31 -0
- blinkview/core/device_identity.py +215 -0
- blinkview/core/dtypes.py +41 -0
- blinkview/core/factory.py +145 -0
- blinkview/core/factory_category_registry.py +38 -0
- blinkview/core/factory_registry.py +73 -0
- blinkview/core/frame_warmup_registry.py +45 -0
- blinkview/core/hot_tier_memory_governor.py +152 -0
- blinkview/core/id_history.py +147 -0
- blinkview/core/id_registry/__init__.py +9 -0
- blinkview/core/id_registry/registry.py +421 -0
- blinkview/core/id_registry/tables.py +235 -0
- blinkview/core/id_registry/types.py +33 -0
- blinkview/core/limits.py +37 -0
- blinkview/core/log_fetch.py +591 -0
- blinkview/core/logger.py +268 -0
- blinkview/core/module_snapshot.py +615 -0
- blinkview/core/numba_config.py +75 -0
- blinkview/core/numba_setup.py +128 -0
- blinkview/core/numpy_batch_manager.py +766 -0
- blinkview/core/numpy_log.py +1116 -0
- blinkview/core/pipeline_manager.py +269 -0
- blinkview/core/playback_clock.py +208 -0
- blinkview/core/playback_follow.py +202 -0
- blinkview/core/playback_ranges.py +137 -0
- blinkview/core/plugin_manager.py +72 -0
- blinkview/core/profile_params.py +458 -0
- blinkview/core/registry.py +1212 -0
- blinkview/core/reorderer.py +298 -0
- blinkview/core/session_generation.py +20 -0
- blinkview/core/settings_manager.py +99 -0
- blinkview/core/sources.py +193 -0
- blinkview/core/system_context.py +29 -0
- blinkview/core/task_manager.py +124 -0
- blinkview/core/time_sync_engine.py +145 -0
- blinkview/core/types/empty.py +23 -0
- blinkview/core/types/formatting.py +17 -0
- blinkview/core/types/frames.py +40 -0
- blinkview/core/types/kv_extraction.py +76 -0
- blinkview/core/types/log_batch.py +53 -0
- blinkview/core/types/modules.py +65 -0
- blinkview/core/types/output.py +15 -0
- blinkview/core/types/parsing.py +177 -0
- blinkview/core/types/telemetry.py +27 -0
- blinkview/core/warmup.py +66 -0
- blinkview/core/warmup_registry.py +42 -0
- blinkview/core/zstd_file_compression.py +97 -0
- blinkview/daemon.py +13 -0
- blinkview/gui.py +20 -0
- blinkview/io/BaseReader.py +66 -0
- blinkview/io/__init__.py +38 -0
- blinkview/io/adb_reader.py +667 -0
- blinkview/io/adb_time_syncer.py +483 -0
- blinkview/io/benchmark.py +220 -0
- blinkview/io/binary_file_reader.py +177 -0
- blinkview/io/can_bus.py +265 -0
- blinkview/io/file_tail_reader.py +163 -0
- blinkview/io/rtt.py +466 -0
- blinkview/io/serial_time_syncer.py +410 -0
- blinkview/io/source_handshake.py +148 -0
- blinkview/io/tcp_client.py +221 -0
- blinkview/io/tcp_server.py +252 -0
- blinkview/io/uart.py +470 -0
- blinkview/io/udp_reader.py +232 -0
- blinkview/ops/benchmark.py +50 -0
- blinkview/ops/buffers.py +158 -0
- blinkview/ops/codec_adb_long.py +467 -0
- blinkview/ops/codecs.py +273 -0
- blinkview/ops/constants.py +58 -0
- blinkview/ops/decode_loop.py +243 -0
- blinkview/ops/desktop_timestamp.py +187 -0
- blinkview/ops/discovery.py +91 -0
- blinkview/ops/dispatch.py +124 -0
- blinkview/ops/formatting.py +657 -0
- blinkview/ops/generic.py +24 -0
- blinkview/ops/id_history.py +23 -0
- blinkview/ops/id_registry.py +36 -0
- blinkview/ops/id_resolution.py +127 -0
- blinkview/ops/kv_extraction.py +522 -0
- blinkview/ops/kv_filter.py +187 -0
- blinkview/ops/levels.py +84 -0
- blinkview/ops/module_filter.py +124 -0
- blinkview/ops/module_snapshot.py +267 -0
- blinkview/ops/modules.py +431 -0
- blinkview/ops/normalization.py +5 -0
- blinkview/ops/reorder.py +143 -0
- blinkview/ops/segments.py +658 -0
- blinkview/ops/stage_loop.py +111 -0
- blinkview/ops/string_table.py +78 -0
- blinkview/ops/strings.py +317 -0
- blinkview/ops/telemetry.py +913 -0
- blinkview/ops/telemetry_table.py +155 -0
- blinkview/ops/text_filter.py +83 -0
- blinkview/ops/timestamp_idf.py +62 -0
- blinkview/ops/timestamps.py +391 -0
- blinkview/ops/timesync.py +281 -0
- blinkview/ops/unified_log_scan.py +213 -0
- blinkview/ops/views.py +137 -0
- blinkview/ops/zephyr_timestamp.py +173 -0
- blinkview/parsers/__init__.py +35 -0
- blinkview/parsers/adb_decoder.py +118 -0
- blinkview/parsers/binary_parser.py +572 -0
- blinkview/parsers/can_parser.py +274 -0
- blinkview/parsers/frame_decoders.py +199 -0
- blinkview/parsers/frame_parsers.py +628 -0
- blinkview/parsers/key_value.py +152 -0
- blinkview/parsers/module_gen.py +134 -0
- blinkview/parsers/multi_rule_key_value.py +655 -0
- blinkview/parsers/parser.py +126 -0
- blinkview/parsers/state.py +64 -0
- blinkview/parsers/text_filter.py +25 -0
- blinkview/parsers/time_sync_profiler.py +383 -0
- blinkview/parsers/unified_log_replay.py +412 -0
- blinkview/storage/__init__.py +12 -0
- blinkview/storage/file_logger.py +429 -0
- blinkview/storage/file_manager.py +702 -0
- blinkview/storage/log_file_archive.py +49 -0
- blinkview/subscribers/console.py +122 -0
- blinkview/subscribers/subscriber.py +31 -0
- blinkview/ui/__init__.py +6 -0
- blinkview/ui/cli_args.py +48 -0
- blinkview/ui/constants.py +18 -0
- blinkview/ui/gui_context.py +125 -0
- blinkview/ui/main_window.py +1525 -0
- blinkview/ui/module_gui_meta.py +14 -0
- blinkview/ui/native_dark_mode.py +40 -0
- blinkview/ui/run.py +220 -0
- blinkview/ui/utils/config_node.py +253 -0
- blinkview/ui/utils/config_node_manager.py +140 -0
- blinkview/ui/utils/in_development.py +73 -0
- blinkview/ui/utils/log_velocity_tracker.py +63 -0
- blinkview/ui/utils/ui_state_handler.py +367 -0
- blinkview/ui/utils/update_checker.py +69 -0
- blinkview/ui/utils/view_presets.py +190 -0
- blinkview/ui/utils/window_manager.py +185 -0
- blinkview/ui/utils/window_title.py +26 -0
- blinkview/ui/widget_registry.py +35 -0
- blinkview/ui/widgets/TelemetryWatch.py +1835 -0
- blinkview/ui/widgets/__init__.py +6 -0
- blinkview/ui/widgets/action_button_delegate.py +185 -0
- blinkview/ui/widgets/base_list_item.py +142 -0
- blinkview/ui/widgets/base_sidebar_widget.py +147 -0
- blinkview/ui/widgets/config/__init__.py +6 -0
- blinkview/ui/widgets/config/demo_window.py +56 -0
- blinkview/ui/widgets/config/dynamic_config.py +1280 -0
- blinkview/ui/widgets/config/mock_backend.py +121 -0
- blinkview/ui/widgets/config/style_config.py +26 -0
- blinkview/ui/widgets/config_tool_button_widget.py +209 -0
- blinkview/ui/widgets/config_widget_factory.py +358 -0
- blinkview/ui/widgets/device_sidebar.py +32 -0
- blinkview/ui/widgets/jog_wheel_button.py +131 -0
- blinkview/ui/widgets/kv_filter_line_edit.py +45 -0
- blinkview/ui/widgets/log_highlighter.py +65 -0
- blinkview/ui/widgets/log_table_viewer.py +1839 -0
- blinkview/ui/widgets/log_view_mode.py +12 -0
- blinkview/ui/widgets/log_viewer.py +1364 -0
- blinkview/ui/widgets/message_box.py +86 -0
- blinkview/ui/widgets/mock_config.py +178 -0
- blinkview/ui/widgets/module_filter_model.py +167 -0
- blinkview/ui/widgets/module_filter_sidebar.py +109 -0
- blinkview/ui/widgets/module_filter_table.py +602 -0
- blinkview/ui/widgets/pipelines_sidebar.py +69 -0
- blinkview/ui/widgets/playback_control.py +508 -0
- blinkview/ui/widgets/plotter.py +1673 -0
- blinkview/ui/widgets/profile_params_dialog.py +391 -0
- blinkview/ui/widgets/searchable_log_area.py +464 -0
- blinkview/ui/widgets/telemetry_table.py +1214 -0
- blinkview/ui/widgets/title_bar.py +71 -0
- blinkview/ui/widgets/toast.py +386 -0
- blinkview/ui/widgets/toast_dispatcher.py +64 -0
- blinkview/ui/widgets/update_widget.py +350 -0
- blinkview/ui/windows/detached_tab_window.py +114 -0
- blinkview/utils/adb.py +41 -0
- blinkview/utils/atomic_json_dump.py +50 -0
- blinkview/utils/audit_imports.py +40 -0
- blinkview/utils/cli_updater.py +81 -0
- blinkview/utils/config_handler.py +99 -0
- blinkview/utils/dev_mode.py +37 -0
- blinkview/utils/dict_utils.py +75 -0
- blinkview/utils/format_metric.py +31 -0
- blinkview/utils/generate_id.py +40 -0
- blinkview/utils/global_settings.py +35 -0
- blinkview/utils/install_info.py +144 -0
- blinkview/utils/level_map.py +194 -0
- blinkview/utils/log_filter.py +109 -0
- blinkview/utils/log_level.py +82 -0
- blinkview/utils/paths.py +28 -0
- blinkview/utils/profile_memory.py +63 -0
- blinkview/utils/project_settings.py +333 -0
- blinkview/utils/session_export.py +676 -0
- blinkview/utils/session_lister.py +249 -0
- blinkview/utils/session_metadata.py +39 -0
- blinkview/utils/settings.py +132 -0
- blinkview/utils/settings_updater.py +23 -0
- blinkview/utils/throughput.py +154 -0
- blinkview/utils/time_utils.py +143 -0
- blinkview/utils/updater.py +408 -0
- blinkview/utils/used_modules.py +18 -0
- blinkview/utils/utc_offset.py +27 -0
- blinkview-0.18.1.dist-info/METADATA +396 -0
- blinkview-0.18.1.dist-info/RECORD +222 -0
- blinkview-0.18.1.dist-info/WHEEL +4 -0
- blinkview-0.18.1.dist-info/entry_points.txt +2 -0
- 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,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")
|