totalsync-webinterface 0.1.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.
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/PKG-INFO +4 -3
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/README.md +2 -2
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/pyproject.toml +4 -4
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/__init__.py +1 -1
- totalsync_webinterface-0.3.0/totalsync_webinterface/control_window.py +303 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/serial_dump.py +1 -1
- totalsync_webinterface-0.3.0/totalsync_webinterface/teensy_commander.py +355 -0
- totalsync_webinterface-0.1.2/totalsync_webinterface/teensy_commander.py +0 -585
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/.gitignore +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/.hgignore +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/LICENSE +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/camera.py +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/curses_interface.py +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/fake_picamera.py +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/packet.py +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/pin_sheet.py +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/serial_dummy.py +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/CanvasCamera.js +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/CanvasPlot.js +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/favicon.ico +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/index.html +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/interface.js +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/reconnecting-websocket/LICENSE.txt +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/reconnecting-websocket/README.md +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/reconnecting-websocket/package.json +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/reconnecting-websocket/reconnecting-websocket.js +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/style.css +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/webgl-plot/LICENSE +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/webgl-plot/README.md +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/webgl-plot/dist/webglplot.esm.js +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web/webgl-plot/package.json +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/web_interface.py +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/websocket_server/LICENSE +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/websocket_server/__init__.py +0 -0
- {totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/websocket_server/websocket_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: totalsync-webinterface
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: TotalSync acquisition: serial reader, live browser interface and recorder
|
|
5
5
|
Project-URL: Homepage, https://github.com/NeuroNetMem/totalsync
|
|
6
6
|
Project-URL: Repository, https://github.com/NeuroNetMem/totalsync
|
|
@@ -18,6 +18,7 @@ Requires-Python: >=3.11
|
|
|
18
18
|
Requires-Dist: cobs
|
|
19
19
|
Requires-Dist: numpy
|
|
20
20
|
Requires-Dist: pyserial
|
|
21
|
+
Requires-Dist: pyside6-essentials>=6.8
|
|
21
22
|
Requires-Dist: pyzmq
|
|
22
23
|
Requires-Dist: windows-curses; platform_system == 'Windows'
|
|
23
24
|
Provides-Extra: extras
|
|
@@ -41,5 +42,5 @@ totalsync --pinsheet docs/pinSheet_2026.json
|
|
|
41
42
|
totalsync -D # simulated Teensy, no hardware needed
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
The control window is Qt, through `PySide6-Essentials`, which is installed from PyPI as
|
|
46
|
+
a regular dependency. The live data view stays in the browser.
|
|
@@ -14,5 +14,5 @@ totalsync --pinsheet docs/pinSheet_2026.json
|
|
|
14
14
|
totalsync -D # simulated Teensy, no hardware needed
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
The control window is Qt, through `PySide6-Essentials`, which is installed from PyPI as
|
|
18
|
+
a regular dependency. The live data view stays in the browser.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "totalsync-webinterface"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "TotalSync acquisition: serial reader, live browser interface and recorder"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -23,6 +23,9 @@ dependencies = [
|
|
|
23
23
|
"pyzmq",
|
|
24
24
|
# On Linux/macOS `curses` is in the stdlib; only Windows needs the backport.
|
|
25
25
|
'windows-curses ; platform_system == "Windows"',
|
|
26
|
+
# The control window. -Essentials is the QtWidgets/QtGui/QtCore subset of PySide6,
|
|
27
|
+
# without the add-on modules that make the full wheel several times larger.
|
|
28
|
+
"PySide6-Essentials>=6.8",
|
|
26
29
|
]
|
|
27
30
|
|
|
28
31
|
[project.optional-dependencies]
|
|
@@ -30,9 +33,6 @@ extras = [
|
|
|
30
33
|
"pyglet",
|
|
31
34
|
"sounddevice",
|
|
32
35
|
]
|
|
33
|
-
# NOTE: the GUI also needs `tkinter`, which is part of the stdlib but is *not*
|
|
34
|
-
# pip-installable. It ships with the python.org and conda builds; with Homebrew
|
|
35
|
-
# python you need `brew install python-tk`, on Debian/Ubuntu `apt install python3-tk`.
|
|
36
36
|
# `picamera` is Raspberry Pi only and is imported behind a try/except in camera.py.
|
|
37
37
|
|
|
38
38
|
[project.scripts]
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""The control window of the `totalsync` command.
|
|
2
|
+
|
|
3
|
+
One window for the whole session: pick the serial port and the output directory, press
|
|
4
|
+
Play, and from then on Reset, Reload and Quit, with the packet counters in the status bar
|
|
5
|
+
and the address of the browser interface ready to be copied. The data itself is only
|
|
6
|
+
ever shown in the browser.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import serial.tools.list_ports
|
|
15
|
+
import zmq
|
|
16
|
+
from PySide6.QtCore import Qt, QTimer
|
|
17
|
+
from PySide6.QtGui import QGuiApplication
|
|
18
|
+
from PySide6.QtWidgets import (QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QLabel,
|
|
19
|
+
QLineEdit, QComboBox, QMainWindow, QMessageBox, QPushButton,
|
|
20
|
+
QSizePolicy, QVBoxLayout, QWidget)
|
|
21
|
+
|
|
22
|
+
from .teensy_commander import (DEFAULT_SERIAL_PORT, WEB_URL, SerialPortError, TeensyCommander,
|
|
23
|
+
open_web_interface, prepare_output_directory)
|
|
24
|
+
|
|
25
|
+
# How often the status bar is refreshed, and the window through which "nothing received"
|
|
26
|
+
# is judged: a port that stays silent for one whole tick is reported.
|
|
27
|
+
STATUS_INTERVAL_MS = 1000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def default_output_directory():
|
|
31
|
+
"""Where a session is written unless the user says otherwise.
|
|
32
|
+
|
|
33
|
+
The working directory is where the user cd'd to before running the command, so it is
|
|
34
|
+
the best guess available; a Finder or desktop launch, where the working directory is
|
|
35
|
+
'/', is not - nobody wants to record into '/' and it is not writable anyway - hence the
|
|
36
|
+
fall back to $HOME.
|
|
37
|
+
"""
|
|
38
|
+
start = Path.cwd()
|
|
39
|
+
if start == Path(start.anchor) or not os.access(start, os.W_OK):
|
|
40
|
+
start = Path.home()
|
|
41
|
+
return start
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ControlWindow(QMainWindow):
|
|
45
|
+
"""The session window. Idle until Play, then running until it is closed.
|
|
46
|
+
|
|
47
|
+
``commander`` is None until Play succeeds; whoever runs the event loop has to shut
|
|
48
|
+
it down once the window is gone (see teensy_commander.run_gui).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, cli_args, channel_labels=None, curses_screen=None):
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.cli_args = cli_args
|
|
54
|
+
self.channel_labels = channel_labels
|
|
55
|
+
self.curses_screen = curses_screen
|
|
56
|
+
self.commander = None
|
|
57
|
+
# Raw and decoded packet counts, and the time, at the previous status tick. The
|
|
58
|
+
# rate shown is worked out from these rather than taken from the commander, whose
|
|
59
|
+
# packets_per_second is only recalculated when a packet arrives and so stays at
|
|
60
|
+
# its last value, however high, for as long as the port is silent.
|
|
61
|
+
self.last_count = 0
|
|
62
|
+
self.last_decoded = 0
|
|
63
|
+
self.last_tick = 0.0
|
|
64
|
+
# When the port went quiet, or None while packets are arriving. Silence is a
|
|
65
|
+
# state: it is logged once when it starts and once when it ends, not every tick.
|
|
66
|
+
self.silent_since = None
|
|
67
|
+
# Set once the user has confirmed (or a confirmation is not wanted), so that
|
|
68
|
+
# closeEvent does not ask a second time.
|
|
69
|
+
self.quit_confirmed = False
|
|
70
|
+
|
|
71
|
+
self.setWindowTitle('TotalSync')
|
|
72
|
+
self.setMinimumWidth(560)
|
|
73
|
+
|
|
74
|
+
# --- Session: where the data comes from and where it goes.
|
|
75
|
+
self.port_combo = QComboBox()
|
|
76
|
+
# Editable, so that a port the enumeration misses can still be typed in.
|
|
77
|
+
self.port_combo.setEditable(True)
|
|
78
|
+
self.port_combo.setMinimumContentsLength(24)
|
|
79
|
+
refresh_button = QPushButton('Refresh')
|
|
80
|
+
refresh_button.setToolTip('Look for serial ports again')
|
|
81
|
+
refresh_button.clicked.connect(self.refresh_ports)
|
|
82
|
+
port_row = QHBoxLayout()
|
|
83
|
+
port_row.addWidget(self.port_combo, 1)
|
|
84
|
+
port_row.addWidget(refresh_button)
|
|
85
|
+
|
|
86
|
+
self.dir_edit = QLineEdit()
|
|
87
|
+
self.dir_edit.textChanged.connect(self.update_buttons)
|
|
88
|
+
browse_button = QPushButton('Browse…')
|
|
89
|
+
browse_button.clicked.connect(self.browse_output_directory)
|
|
90
|
+
dir_row = QHBoxLayout()
|
|
91
|
+
dir_row.addWidget(self.dir_edit, 1)
|
|
92
|
+
dir_row.addWidget(browse_button)
|
|
93
|
+
|
|
94
|
+
self.session_box = QGroupBox('Session')
|
|
95
|
+
form = QFormLayout(self.session_box)
|
|
96
|
+
form.addRow('Serial port:', port_row)
|
|
97
|
+
form.addRow('Output directory:', dir_row)
|
|
98
|
+
|
|
99
|
+
# --- Data logger: the address of the browser interface.
|
|
100
|
+
url_edit = QLineEdit(WEB_URL)
|
|
101
|
+
url_edit.setReadOnly(True)
|
|
102
|
+
copy_button = QPushButton('Copy')
|
|
103
|
+
copy_button.setToolTip('Copy the address to the clipboard')
|
|
104
|
+
copy_button.clicked.connect(self.copy_url)
|
|
105
|
+
url_box = QGroupBox('Data logger (open in a browser)')
|
|
106
|
+
url_row = QHBoxLayout(url_box)
|
|
107
|
+
url_row.addWidget(url_edit, 1)
|
|
108
|
+
url_row.addWidget(copy_button)
|
|
109
|
+
|
|
110
|
+
# --- Actions. Same behaviour as the buttons of the old dialogs.
|
|
111
|
+
self.play_button = QPushButton('Play')
|
|
112
|
+
self.play_button.setDefault(True)
|
|
113
|
+
self.play_button.clicked.connect(self.play)
|
|
114
|
+
self.reset_button = QPushButton('Reset')
|
|
115
|
+
self.reset_button.setToolTip('Reset the Teensy and reopen the browser interface')
|
|
116
|
+
self.reset_button.clicked.connect(self.reset)
|
|
117
|
+
self.reload_button = QPushButton('Reload')
|
|
118
|
+
self.reload_button.setToolTip('Reopen the browser interface')
|
|
119
|
+
self.reload_button.clicked.connect(open_web_interface)
|
|
120
|
+
quit_button = QPushButton('Quit')
|
|
121
|
+
quit_button.clicked.connect(self.close)
|
|
122
|
+
buttons = QHBoxLayout()
|
|
123
|
+
for button in (self.play_button, self.reset_button, self.reload_button):
|
|
124
|
+
buttons.addWidget(button)
|
|
125
|
+
buttons.addStretch(1)
|
|
126
|
+
buttons.addWidget(quit_button)
|
|
127
|
+
|
|
128
|
+
central = QWidget()
|
|
129
|
+
layout = QVBoxLayout(central)
|
|
130
|
+
layout.addWidget(self.session_box)
|
|
131
|
+
layout.addWidget(url_box)
|
|
132
|
+
layout.addLayout(buttons)
|
|
133
|
+
self.setCentralWidget(central)
|
|
134
|
+
|
|
135
|
+
# A normal (not permanent) widget, so that a temporary showMessage() such as
|
|
136
|
+
# "URL copied" covers it for a moment and then gives it back.
|
|
137
|
+
self.status_label = QLabel('Not running')
|
|
138
|
+
# Two lines of whatever width the window has: Ignored, so that a long directory
|
|
139
|
+
# cannot set the minimum width of the whole window, which it otherwise would.
|
|
140
|
+
self.status_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred)
|
|
141
|
+
self.statusBar().addWidget(self.status_label, 1)
|
|
142
|
+
|
|
143
|
+
self.status_timer = QTimer(self)
|
|
144
|
+
self.status_timer.setInterval(STATUS_INTERVAL_MS)
|
|
145
|
+
self.status_timer.timeout.connect(self.update_status)
|
|
146
|
+
|
|
147
|
+
self.refresh_ports()
|
|
148
|
+
if cli_args.serial_port:
|
|
149
|
+
self.port_combo.setEditText(cli_args.serial_port)
|
|
150
|
+
self.dir_edit.setText(str(cli_args.output_dir or default_output_directory()))
|
|
151
|
+
self.update_buttons()
|
|
152
|
+
|
|
153
|
+
# --- setup state
|
|
154
|
+
|
|
155
|
+
def refresh_ports(self):
|
|
156
|
+
"""Fill the port list from the ports present now, keeping what was typed."""
|
|
157
|
+
current = self.port_combo.currentText()
|
|
158
|
+
ports = sorted(comport.device for comport in serial.tools.list_ports.comports())
|
|
159
|
+
logging.info('Known serial ports: ' + repr(ports))
|
|
160
|
+
self.port_combo.clear()
|
|
161
|
+
self.port_combo.addItems(ports)
|
|
162
|
+
self.port_combo.lineEdit().setPlaceholderText(
|
|
163
|
+
'Serial port' if ports else 'No serial ports found')
|
|
164
|
+
if current or not ports:
|
|
165
|
+
self.port_combo.setEditText(current)
|
|
166
|
+
|
|
167
|
+
def browse_output_directory(self):
|
|
168
|
+
start = self.dir_edit.text() or str(default_output_directory())
|
|
169
|
+
directory = QFileDialog.getExistingDirectory(
|
|
170
|
+
self, 'Where should TotalSync write this session?', start)
|
|
171
|
+
if directory:
|
|
172
|
+
self.dir_edit.setText(directory)
|
|
173
|
+
|
|
174
|
+
def update_buttons(self):
|
|
175
|
+
running = self.commander is not None
|
|
176
|
+
self.session_box.setEnabled(not running)
|
|
177
|
+
self.play_button.setEnabled(not running and bool(self.dir_edit.text().strip()))
|
|
178
|
+
self.reset_button.setEnabled(running)
|
|
179
|
+
self.reload_button.setEnabled(running)
|
|
180
|
+
|
|
181
|
+
def play(self):
|
|
182
|
+
"""Start the session. On any failure say why and stay in the setup state."""
|
|
183
|
+
if self.commander is not None:
|
|
184
|
+
return
|
|
185
|
+
try:
|
|
186
|
+
output_dir = prepare_output_directory(self.dir_edit.text().strip())
|
|
187
|
+
except ValueError as exc:
|
|
188
|
+
QMessageBox.warning(self, 'Output directory', f'Cannot record into that directory: {exc}')
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
port = self.port_combo.currentText().strip() or DEFAULT_SERIAL_PORT
|
|
192
|
+
args = self.cli_args
|
|
193
|
+
logging.info(f'Launching Teensy Commander on serial port {port} and the web interface '
|
|
194
|
+
f'on ports HTTP:{args.http_port} and WS:{args.ws_port}')
|
|
195
|
+
try:
|
|
196
|
+
commander = TeensyCommander(serial_port=port,
|
|
197
|
+
http_port=args.http_port,
|
|
198
|
+
ws_port=args.ws_port,
|
|
199
|
+
curses_screen=self.curses_screen,
|
|
200
|
+
output_dir=output_dir,
|
|
201
|
+
write_bin=args.binfile,
|
|
202
|
+
use_dummy=args.dummy,
|
|
203
|
+
channel_labels=self.channel_labels)
|
|
204
|
+
except SerialPortError as exc:
|
|
205
|
+
QMessageBox.warning(self, 'Serial port', f'{exc}\n\nPick another port and press Play again.')
|
|
206
|
+
return
|
|
207
|
+
except (OSError, zmq.ZMQError) as exc:
|
|
208
|
+
# Typically another totalsync still holding the HTTP, WebSocket or ZMQ ports.
|
|
209
|
+
QMessageBox.critical(self, 'Cannot start', f'Could not start the servers: {exc}')
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
commander.start()
|
|
213
|
+
self.commander = commander
|
|
214
|
+
logging.info(f'Recording into {output_dir}')
|
|
215
|
+
# Only now: reaching this line means the servers are listening, so the page will
|
|
216
|
+
# load rather than greet the user with "unable to connect".
|
|
217
|
+
if not args.no_browser:
|
|
218
|
+
open_web_interface()
|
|
219
|
+
|
|
220
|
+
self.last_count = 0
|
|
221
|
+
self.last_decoded = 0
|
|
222
|
+
self.last_tick = time.monotonic()
|
|
223
|
+
self.silent_since = None
|
|
224
|
+
self.update_buttons()
|
|
225
|
+
self.update_status()
|
|
226
|
+
self.status_timer.start()
|
|
227
|
+
|
|
228
|
+
# --- running state
|
|
229
|
+
|
|
230
|
+
def reset(self):
|
|
231
|
+
self.commander.reset_packet()
|
|
232
|
+
open_web_interface()
|
|
233
|
+
|
|
234
|
+
def copy_url(self):
|
|
235
|
+
QGuiApplication.clipboard().setText(WEB_URL)
|
|
236
|
+
self.statusBar().showMessage(f'Copied {WEB_URL}', 2000)
|
|
237
|
+
|
|
238
|
+
def update_status(self):
|
|
239
|
+
"""Show the packet counters; also the session's periodic health check."""
|
|
240
|
+
commander = self.commander
|
|
241
|
+
if not commander.alive:
|
|
242
|
+
self.force_quit()
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
now = time.monotonic()
|
|
246
|
+
received = commander.serial_dump.n_raw_packets
|
|
247
|
+
decoded = commander.n_packet
|
|
248
|
+
new = received - self.last_count
|
|
249
|
+
elapsed = now - self.last_tick
|
|
250
|
+
rate = (decoded - self.last_decoded) / elapsed if elapsed > 0 else 0.0
|
|
251
|
+
self.last_count, self.last_decoded, self.last_tick = received, decoded, now
|
|
252
|
+
|
|
253
|
+
counters = f'{received} received (+{new}) · {decoded} decoded · {rate:.1f}/s'
|
|
254
|
+
if new:
|
|
255
|
+
if self.silent_since is not None:
|
|
256
|
+
logging.info(f'Serial packets arriving again after '
|
|
257
|
+
f'{now - self.silent_since:.0f} s')
|
|
258
|
+
self.silent_since = None
|
|
259
|
+
self.status_label.setStyleSheet('')
|
|
260
|
+
logging.debug(f'Serial packets: {counters}')
|
|
261
|
+
else:
|
|
262
|
+
if self.silent_since is None:
|
|
263
|
+
# A silent port is the failure this counter exists to make visible, so it
|
|
264
|
+
# goes to the log at a level that survives the default verbosity - once.
|
|
265
|
+
# Silent since the previous tick, the last time anything was seen.
|
|
266
|
+
self.silent_since = now - elapsed
|
|
267
|
+
self.status_label.setStyleSheet('color: #c62828; font-weight: bold;')
|
|
268
|
+
logging.warning(f'Serial packets: {received} received, {decoded} decoded'
|
|
269
|
+
' - nothing received since the last check!')
|
|
270
|
+
counters = (f'nothing received for {now - self.silent_since:.0f} s · '
|
|
271
|
+
f'{received} received · {decoded} decoded · {rate:.1f}/s')
|
|
272
|
+
|
|
273
|
+
# Shortened in the middle to what fits, so both ends of the path stay readable;
|
|
274
|
+
# the tooltip has it in full.
|
|
275
|
+
directory = str(commander.output_dir)
|
|
276
|
+
self.status_label.setToolTip(directory)
|
|
277
|
+
prefix = 'Recording to '
|
|
278
|
+
metrics = self.status_label.fontMetrics()
|
|
279
|
+
room = max(self.status_label.width() - metrics.horizontalAdvance(prefix), 100)
|
|
280
|
+
directory = metrics.elidedText(directory, Qt.TextElideMode.ElideMiddle, room)
|
|
281
|
+
self.status_label.setText(f'{commander.serial_port} · {counters}\n{prefix}{directory}')
|
|
282
|
+
|
|
283
|
+
shell_gui = commander.shell_gui
|
|
284
|
+
if shell_gui and shell_gui.alive and not shell_gui.is_alive():
|
|
285
|
+
logging.critical("Shell GUI died!")
|
|
286
|
+
# TODO: attempt to restart the shell GUI
|
|
287
|
+
|
|
288
|
+
# --- quitting
|
|
289
|
+
|
|
290
|
+
def force_quit(self):
|
|
291
|
+
"""Close without asking: Ctrl-C in the terminal, or the session ended by itself."""
|
|
292
|
+
self.quit_confirmed = True
|
|
293
|
+
self.close()
|
|
294
|
+
|
|
295
|
+
def closeEvent(self, event):
|
|
296
|
+
if not self.quit_confirmed:
|
|
297
|
+
answer = QMessageBox.question(self, 'Quit', 'Really quit TotalSync?')
|
|
298
|
+
if answer != QMessageBox.StandardButton.Yes:
|
|
299
|
+
event.ignore()
|
|
300
|
+
return
|
|
301
|
+
self.quit_confirmed = True
|
|
302
|
+
self.status_timer.stop()
|
|
303
|
+
event.accept()
|
{totalsync_webinterface-0.1.2 → totalsync_webinterface-0.3.0}/totalsync_webinterface/serial_dump.py
RENAMED
|
@@ -12,7 +12,7 @@ class SerialDump:
|
|
|
12
12
|
without a GUI and -- worse -- that the dialog appeared at whatever point in startup
|
|
13
13
|
this object happened to be constructed, which was after the browser had already been
|
|
14
14
|
pointed at a server that was not listening yet. Choosing the directory is a job for
|
|
15
|
-
whoever is talking to the user; see ``
|
|
15
|
+
whoever is talking to the user; see ``ControlWindow`` in control_window.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
def __init__(self, output_dir):
|