stream speed test files from temp file instead of os.random, progress bar for speed test utility. Ctrl + l to clear and output box for utils.

This commit is contained in:
zenith committed 2026-06-15 06:08:33 -04:00
1 parent 94f5e7188a
commit 8dffd508c3
1 file changed
+321 -113
+321 -113
View File
@@ -1,12 +1,45 @@
import RNS
import os
import time
import tempfile
import threading
import nomadnet
import urwid
from .Transport import TransportDisplay, _get_cols_rows
from .ReadlineEdit import ReadlineEdit
from .FileBrowser import FileBrowser
from nomadnet.vendor.additional_urwid_widgets.FormWidgets import Dropdown
class OutputLog:
def __init__(self, title="Output"):
self.walker = urwid.SimpleListWalker([])
self.listbox = urwid.ListBox(self.walker)
self.widget = urwid.LineBox(
urwid.ScrollBar(self.listbox),
title=title,
tlcorner="╭", tline="─",
trcorner="╮", lline="│",
rline="│", blcorner="╰",
bline="─", brcorner="╯"
)
def log(self, message, attr="body_text"):
self.walker.append(urwid.Text((attr, message)))
try:
self.listbox.set_focus(len(self.walker) - 1)
self.listbox.set_focus_valign("bottom")
except Exception:
pass
def clear(self):
self.walker[:] = []
def _utility_body(form_rows, output):
return urwid.Pile([
('pack', urwid.Pile(form_rows)),
('weight', 1, output.widget),
])
DEFAULT_PROBE_SIZE = 16
DEFAULT_PROBE_TIMEOUT = 12
@@ -209,7 +242,7 @@ class ProbeShortcuts:
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(
urwid.Text("[Up/Down] Navigate [Enter] Probe / Activate [Esc] Back"),
urwid.Text("[Up/Down] Navigate [Enter] Probe / Activate [C-l] Clear [Esc] Back"),
"shortcutbar"
)
@@ -222,7 +255,13 @@ class UtilityViewFiller(urwid.WidgetWrap):
if key == "esc":
self.view.go_back()
return None
if key == "ctrl l":
if hasattr(self.view, "clear_output"):
self.view.clear_output()
return None
result = super().keypress(size, key)
if hasattr(self.view, "after_keypress"):
self.view.after_keypress()
if key == "up" and result == "up":
self.view.go_back_to_header()
return None
@@ -270,12 +309,9 @@ class ProbeView:
self._field_row("Probe count:", self.count_edit),
urwid.Divider(),
button_row,
urwid.Divider("─"),
urwid.Text(("key", "Output")),
]
self.walker = urwid.SimpleFocusListWalker(rows)
self.listbox = urwid.ListBox(self.walker)
self.widget = UtilityViewFiller(self.listbox, self)
urwid.Divider("─"), ]
self.output = OutputLog()
self.widget = UtilityViewFiller(_utility_body(rows, self.output), self)
self.update_interface_status()
def _field_row(self, label, edit):
@@ -301,11 +337,10 @@ class ProbeView:
pass
def _log(self, message, attr="body_text"):
self.walker.append(urwid.Text((attr, message)))
try:
self.listbox.set_focus(len(self.walker) - 1)
except Exception:
pass
self.output.log(message, attr)
def clear_output(self):
self.output.clear()
def _draw(self):
try:
@@ -516,7 +551,7 @@ class PathShortcuts:
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(
urwid.Text("[Up/Down] Navigate [Enter] Resolve / Activate [Esc] Back"),
urwid.Text("[Up/Down] Navigate [Enter] Resolve / Activate [C-l] Clear [Esc] Back"),
"shortcutbar"
)
@@ -557,12 +592,9 @@ class PathView:
urwid.Padding(urwid.Text(("inactive_text", "Requests the path from the network if it is not already known.")), left=22),
urwid.Divider(),
button_row,
urwid.Divider("─"),
urwid.Text(("key", "Output")),
]
self.walker = urwid.SimpleFocusListWalker(rows)
self.listbox = urwid.ListBox(self.walker)
self.widget = UtilityViewFiller(self.listbox, self)
urwid.Divider("─"), ]
self.output = OutputLog()
self.widget = UtilityViewFiller(_utility_body(rows, self.output), self)
self.update_interface_status()
def update_interface_status(self):
@@ -582,11 +614,10 @@ class PathView:
pass
def _log(self, message, attr="body_text"):
self.walker.append(urwid.Text((attr, message)))
try:
self.listbox.set_focus(len(self.walker) - 1)
except Exception:
pass
self.output.log(message, attr)
def clear_output(self):
self.output.clear()
def _draw(self):
try:
@@ -685,7 +716,7 @@ class IdentityShortcuts:
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(
urwid.Text("[Up/Down] Navigate [Enter] Look up / Activate [Esc] Back"),
urwid.Text("[Up/Down] Navigate [Enter] Look up / Activate [C-l] Clear [Esc] Back"),
"shortcutbar"
)
@@ -723,12 +754,9 @@ class IdentityView:
urwid.Padding(urwid.Text(("inactive_text", "Recalls the identity, requesting it from the network if needed.")), left=22),
urwid.Divider(),
button_row,
urwid.Divider("─"),
urwid.Text(("key", "Output")),
]
self.walker = urwid.SimpleFocusListWalker(rows)
self.listbox = urwid.ListBox(self.walker)
self.widget = UtilityViewFiller(self.listbox, self)
urwid.Divider("─"), ]
self.output = OutputLog()
self.widget = UtilityViewFiller(_utility_body(rows, self.output), self)
def _addr(self, obj):
try:
@@ -763,11 +791,10 @@ class IdentityView:
pass
def _log(self, message, attr="body_text"):
self.walker.append(urwid.Text((attr, message)))
try:
self.listbox.set_focus(len(self.walker) - 1)
except Exception:
pass
self.output.log(message, attr)
def clear_output(self):
self.output.clear()
def _draw(self):
try:
@@ -833,7 +860,7 @@ class SendFileShortcuts:
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(
urwid.Text("[Up/Down] Navigate [Enter] Choose / Send [Esc] Back"),
urwid.Text("[Up/Down] Navigate [Enter] Choose / Send [C-l] Clear [Esc] Back"),
"shortcutbar"
)
@@ -882,12 +909,9 @@ class SendFileView:
urwid.Divider(),
button_row,
urwid.Divider("─"),
self.progress_text,
urwid.Text(("key", "Output")),
]
self.walker = urwid.SimpleFocusListWalker(rows)
self.listbox = urwid.ListBox(self.walker)
self.widget = UtilityViewFiller(self.listbox, self)
self.progress_text, ]
self.output = OutputLog()
self.widget = UtilityViewFiller(_utility_body(rows, self.output), self)
self.update_interface_status()
def update_interface_status(self):
@@ -907,11 +931,10 @@ class SendFileView:
pass
def _log(self, message, attr="body_text"):
self.walker.append(urwid.Text((attr, message)))
try:
self.listbox.set_focus(len(self.walker) - 1)
except Exception:
pass
self.output.log(message, attr)
def clear_output(self):
self.output.clear()
def _draw(self):
try:
@@ -1101,10 +1124,28 @@ class SendFileView:
self.send_button.original_widget.set_label("Send")
self._draw()
SPEEDTEST_APP_NAME = "nomadnetwork"
SPEEDTEST_ASPECTS = ("utilities", "speedtest")
DEFAULT_SPEEDTEST_MB = 1
MAX_SPEEDTEST_MB = 50
SPEEDTEST_INMEM_MAX = 8 * 1024 * 1024
SPEEDTEST_SIZES = [
("2 KB", 2 * 1024),
("8 KB", 8 * 1024),
("32 KB", 32 * 1024),
("128 KB", 128 * 1024),
("512 KB", 512 * 1024),
("1 MB", 1024 * 1024),
("4 MB", 4 * 1024 * 1024),
("16 MB", 16 * 1024 * 1024),
("64 MB", 64 * 1024 * 1024),
("256 MB", 256 * 1024 * 1024),
("1000 MB", 1000 * 1024 * 1024),
("2000 MB", 2000 * 1024 * 1024),
]
SPEEDTEST_SIZE_LABELS = [label for label, _ in SPEEDTEST_SIZES]
SPEEDTEST_BYTES_BY_LABEL = {label: nbytes for label, nbytes in SPEEDTEST_SIZES}
DEFAULT_SPEEDTEST_LABEL = "1 MB"
def _fmt_bitrate(bytes_per_sec):
bits = float(bytes_per_sec) * 8
@@ -1114,11 +1155,19 @@ def _fmt_bitrate(bytes_per_sec):
bits /= 1000
return "%.1f Tbps" % bits
def _size_str(num, suffix="B"):
num = float(num)
for unit in ("", "K", "M", "G", "T"):
if abs(num) < 1024.0:
return "%.1f %s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f P%s" % (num, suffix)
class SpeedTestShortcuts:
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(
urwid.Text("[Up/Down] Navigate [Enter] Start / Activate [Esc] Back"),
urwid.Text("[Up/Down] Navigate [Enter] Start / Cancel [C-l] Clear [Esc] Back"),
"shortcutbar"
)
@@ -1134,22 +1183,26 @@ class SpeedTestView:
self._listening = False
self._incoming_link = None
self._recv_link = None
self._recv_resource = None
self._recv_started = None
self._recv_bytes = 0
self._recv_polling = False
self._sending = False
self._client_link = None
self._client_resource = None
self._send_started = None
self._send_bytes = 0
self._tmppath = None
self.hash_edit = ReadlineEdit("", "")
self.size_edit = ReadlineEdit("", str(DEFAULT_SPEEDTEST_MB))
self.address_text = urwid.Text(("value", "—"))
self.size_dropdown = Dropdown("", SPEEDTEST_SIZE_LABELS, DEFAULT_SPEEDTEST_LABEL)
self.address_text = urwid.Text(("value", "-"))
self.status_text = urwid.Text("")
self.progress_text = urwid.Text("")
self.listen_status = urwid.Text("")
self.rate_text = urwid.Text("")
self.progress_bar = urwid.ProgressBar("progress_empty", "progress_full", current=0, done=100)
self.start_button = urwid.AttrMap(urwid.Button("Start test", on_press=self.on_start), "button_normal", focus_map="button_focus")
self.start_button = urwid.AttrMap(urwid.Button("Send test file", on_press=self.on_start), "button_normal", focus_map="button_focus")
self.back_button = urwid.AttrMap(urwid.Button("Back", on_press=lambda b: self.go_back()), "button_normal", focus_map="button_focus")
button_row = urwid.Columns([
(urwid.WEIGHT, 0.45, self.start_button),
@@ -1161,6 +1214,7 @@ class SpeedTestView:
urwid.Text(("form_title", "Speed test"), align=urwid.CENTER),
urwid.Divider("─"),
self.status_text,
self.listen_status,
urwid.Columns([
(22, urwid.Text(("key", "Your speedtest hash:"), align=urwid.RIGHT)),
self.address_text,
@@ -1171,18 +1225,20 @@ class SpeedTestView:
urwid.AttrMap(self.hash_edit, "list_normal", focus_map="list_focus"),
], dividechars=1),
urwid.Columns([
(22, urwid.Text(("key", "Test size (MB):"), align=urwid.RIGHT)),
urwid.AttrMap(self.size_edit, "list_normal", focus_map="list_focus"),
(22, urwid.Text(("key", "Test size:"), align=urwid.RIGHT)),
self.size_dropdown,
], dividechars=1),
urwid.Divider(),
button_row,
urwid.Divider("─"),
self.progress_text,
urwid.Text(("key", "Output")),
urwid.Columns([
(22, urwid.Text(("key", "Progress:"), align=urwid.RIGHT)),
self.progress_bar,
], dividechars=1),
urwid.Padding(self.rate_text, left=24),
]
self.walker = urwid.SimpleFocusListWalker(rows)
self.listbox = urwid.ListBox(self.walker)
self.widget = UtilityViewFiller(self.listbox, self)
self.output = OutputLog()
self.widget = UtilityViewFiller(_utility_body(rows, self.output), self)
def update_interface_status(self):
if _has_active_interfaces(self.app):
@@ -1190,11 +1246,58 @@ class SpeedTestView:
else:
self.status_text.set_text(("warning_text", "(!) No interfaces are active"))
def _peer_hash_entered(self):
return len(self.hash_edit.edit_text.strip()) == (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
def _update_listen_status(self):
dot = self.g.get("connected", "*")
if self._sending:
self.listen_status.set_text(("connected_status", "%s %s Sending test…" % (dot, self.g.get("arrow_u", "^"))))
elif self._recv_resource is not None or self._recv_link is not None:
self.listen_status.set_text(("connected_status", "%s %s Receiving test…" % (dot, self.g.get("arrow_d", "v"))))
elif self._listening and self.server_destination is not None:
if self._peer_hash_entered():
self.listen_status.set_text(("connected_status", "%s Listening and ready to receive or send to from entered peer" % dot))
else:
self.listen_status.set_text(("warning_text", "%s Listening enter the peer's hash to accept their test" % dot))
else:
self.listen_status.set_text(("inactive_text", "Not listening"))
def after_keypress(self):
self._update_listen_status()
def _log(self, message, attr="body_text"):
self.output.log(message, attr)
def clear_output(self):
self.output.clear()
def _draw(self):
try:
self.app.ui.loop.draw_screen()
except Exception:
pass
def _schedule(self, fn):
try:
self.app.ui.loop.set_alarm_in(0, lambda *a: (fn(), self._draw()))
except Exception:
try:
fn()
except Exception:
pass
def _set_progress(self, pct):
try:
self.progress_bar.set_completion(max(0, min(100, pct)))
except Exception:
pass
def _update_address(self):
if self.server_destination is not None:
self.address_text.set_text(("value", RNS.prettyhexrep(self.server_destination.hash)))
else:
self.address_text.set_text(("value", "—"))
self.address_text.set_text(("value", "-"))
def start_listening(self):
if self.server_destination is None:
@@ -1210,9 +1313,11 @@ class SpeedTestView:
self.server_destination = None
self._listening = True
self._update_address()
self._update_listen_status()
def stop_listening(self):
self._listening = False
self._recv_polling = False
try:
if self._incoming_link is not None:
self._incoming_link.teardown()
@@ -1220,6 +1325,7 @@ class SpeedTestView:
pass
self._incoming_link = None
self._recv_link = None
self._recv_resource = None
try:
if self.server_destination is not None:
RNS.Transport.deregister_destination(self.server_destination)
@@ -1227,6 +1333,26 @@ class SpeedTestView:
pass
self.server_destination = None
self._update_address()
self._update_listen_status()
def _log_link_mtu(self, link):
try:
parts = []
mtu = getattr(link, "mtu", None)
mdu = getattr(link, "mdu", None)
if mtu:
parts.append("MTU: %s (MDU %s)" % (RNS.prettysize(mtu), RNS.prettysize(mdu) if mdu else "?"))
rtt = getattr(link, "rtt", None)
if rtt:
if hasattr(RNS, "prettyshorttime"):
rtt_str = RNS.prettyshorttime(rtt)
else:
rtt_str = "%.0f ms" % (rtt * 1000) if rtt < 1 else "%.3f s" % rtt
parts.append("RTT: %s" % rtt_str)
if parts:
self._log("Link " + ", ".join(parts))
except Exception:
pass
def _is_active(self):
try:
@@ -1246,6 +1372,7 @@ class SpeedTestView:
return None
def go_back(self):
self.cancel(silent=True)
self.stop_listening()
if self.parent is not None:
self.parent.show_landing()
@@ -1256,27 +1383,13 @@ class SpeedTestView:
except Exception:
pass
def _log(self, message, attr="body_text"):
self.walker.append(urwid.Text((attr, message)))
try:
self.listbox.set_focus(len(self.walker) - 1)
except Exception:
pass
def _draw(self):
try:
self.app.ui.loop.draw_screen()
except Exception:
pass
def _schedule(self, fn):
try:
self.app.ui.loop.set_alarm_in(0, lambda *a: (fn(), self._draw()))
except Exception:
def _cleanup_temp(self):
if self._tmppath:
try:
fn()
os.remove(self._tmppath)
except Exception:
pass
self._tmppath = None
def _on_incoming_link(self, link):
if not self._is_active():
@@ -1311,37 +1424,63 @@ class SpeedTestView:
return
self._recv_link = link
self._recv_started = None
self._recv_bytes = 0
self._recv_resource = None
link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
link.set_resource_started_callback(self._on_recv_started)
link.set_resource_concluded_callback(self._on_recv_concluded)
self._set_progress(0)
self._log("Incoming speed test from %s" % RNS.prettyhexrep(identity.hash), "connected_status")
self._log_link_mtu(link)
def _on_recv_started(self, resource):
self._schedule(self._recv_started_now)
self._schedule(lambda: self._recv_started_now(resource))
def _recv_started_now(self):
def _recv_started_now(self, resource):
self._recv_resource = resource
self._recv_started = time.time()
self.progress_text.set_text(("body_text", "Receiving…"))
self._recv_polling = True
self.rate_text.set_text(("body_text", "Receiving…"))
self._update_listen_status()
self.app.ui.loop.set_alarm_in(0.3, self._recv_poll)
def _recv_poll(self, loop, user_data):
if not self._recv_polling or self._recv_resource is None:
return
try:
pct = int(self._recv_resource.get_progress() * 100)
except Exception:
pct = 0
self._set_progress(pct)
self.rate_text.set_text(("body_text", "Downloading… %d%%" % max(0, min(100, pct))))
self._draw()
if self._recv_polling:
loop.set_alarm_in(0.3, self._recv_poll)
def _on_recv_concluded(self, resource):
self._schedule(lambda: self._recv_concluded(resource))
def _recv_concluded(self, resource):
self._recv_polling = False
try:
complete = resource.status == RNS.Resource.COMPLETE
except Exception:
complete = False
if not complete:
self._log("Incoming transfer failed", "warning_text")
self.rate_text.set_text("")
return
size = self._resource_size(resource)
elapsed = max(0.001, time.time() - (self._recv_started or time.time()))
self._log("Download: %s (%s in %.2fs)" % (_fmt_bitrate(size / elapsed), _size_str(size), elapsed), "connected_status")
self.progress_text.set_text("")
self._set_progress(100)
rate = _fmt_bitrate(size / elapsed)
self._log("Download: %s (%s in %.2fs)" % (rate, _size_str(size), elapsed), "connected_status")
self.rate_text.set_text(("connected_status", "Download: %s" % rate))
self._recv_resource = None
self._recv_link = None
self._update_listen_status()
def _resource_size(self, resource):
for getter in ("get_transfer_size", "getTransferSize"):
for getter in ("get_transfer_size", "get_data_size"):
try:
fn = getattr(resource, getter, None)
if fn:
@@ -1357,6 +1496,7 @@ class SpeedTestView:
def on_start(self, button=None):
if self._sending:
self.cancel()
return
self.update_interface_status()
@@ -1372,16 +1512,14 @@ class SpeedTestView:
self._log("Invalid peer hash", "error_text")
return
try:
mb = float(self.size_edit.edit_text.strip() or str(DEFAULT_SPEEDTEST_MB))
except ValueError:
mb = DEFAULT_SPEEDTEST_MB
mb = max(0.01, min(MAX_SPEEDTEST_MB, mb))
self._send_bytes = int(mb * 1024 * 1024)
label = self.size_dropdown.get_value()
self._send_bytes = SPEEDTEST_BYTES_BY_LABEL.get(label, 1024 * 1024)
self._sending = True
self.start_button.original_widget.set_label("Testing…")
self.progress_text.set_text("")
self.start_button.original_widget.set_label("Cancel")
self._set_progress(0)
self.rate_text.set_text("")
self._update_listen_status()
if RNS.Transport.has_path(self._dest_hash):
self._establish_link()
@@ -1401,6 +1539,8 @@ class SpeedTestView:
return DEFAULT_PROBE_TIMEOUT
def _await_path(self, loop, user_data):
if not self._sending:
return
if RNS.Transport.has_path(self._dest_hash):
self._establish_link()
elif time.time() > self._deadline:
@@ -1455,18 +1595,64 @@ class SpeedTestView:
link.identify(self.app.identity)
except Exception:
pass
self.app.ui.loop.set_alarm_in(0.6, lambda l, u: self._send_resource(link))
self._log_link_mtu(link)
self.app.ui.loop.set_alarm_in(0.6, lambda l, u: self._prepare_and_send(link))
def _send_resource(self, link):
def _prepare_and_send(self, link):
if not self._sending or self._client_resource is not None:
return
nbytes = self._send_bytes
if nbytes <= SPEEDTEST_INMEM_MAX:
self._begin_resource(link, os.urandom(nbytes), None)
return
self._log("Preparing %s payload …" % _size_str(nbytes))
def worker():
path = None
try:
fd, path = tempfile.mkstemp(prefix="nn_speedtest_")
written = 0
with os.fdopen(fd, "wb") as f:
while written < nbytes and self._sending:
chunk = min(1024 * 1024, nbytes - written)
f.write(os.urandom(chunk))
written += chunk
except Exception as e:
if path:
try:
os.remove(path)
except Exception:
pass
self._schedule(lambda: (self._log("Could not prepare payload: %s" % str(e), "error_text"), self._finish()))
return
if not self._sending:
if path:
try:
os.remove(path)
except Exception:
pass
return
self._schedule(lambda: self._begin_resource(link, None, path))
threading.Thread(target=worker, daemon=True).start()
def _begin_resource(self, link, data_bytes, tmppath):
if not self._sending:
if tmppath:
try:
os.remove(tmppath)
except Exception:
pass
return
self._tmppath = tmppath
try:
data = os.urandom(self._send_bytes)
src = data_bytes if data_bytes is not None else open(tmppath, "rb")
self._send_started = time.time()
self._client_resource = RNS.Resource(data, link, callback=self._on_send_done, progress_callback=self._on_send_progress, auto_compress=False)
self._client_resource = RNS.Resource(src, link, callback=self._on_send_done, progress_callback=self._on_send_progress, auto_compress=False)
self._log("Sending %s test payload …" % _size_str(self._send_bytes))
except Exception as e:
self._log("Could not start transfer: %s" % str(e), "error_text")
self._cleanup_temp()
self._finish()
def _on_send_progress(self, resource):
@@ -1477,7 +1663,8 @@ class SpeedTestView:
pct = int(resource.get_progress() * 100)
except Exception:
pct = 0
self.progress_text.set_text(("body_text", "Uploading… %d%%" % pct))
self._set_progress(pct)
self.rate_text.set_text(("body_text", "Uploading… %d%%" % max(0, min(100, pct))))
def _on_send_done(self, resource):
self._schedule(lambda: self._send_done(resource))
@@ -1488,16 +1675,20 @@ class SpeedTestView:
except Exception:
complete = False
if complete:
self._set_progress(100)
elapsed = max(0.001, time.time() - (self._send_started or time.time()))
self._log("Upload: %s (%s in %.2fs)" % (_fmt_bitrate(self._send_bytes / elapsed), _size_str(self._send_bytes), elapsed), "connected_status")
rate = _fmt_bitrate(self._send_bytes / elapsed)
self._log("Upload: %s (%s in %.2fs)" % (rate, _size_str(self._send_bytes), elapsed), "connected_status")
self.rate_text.set_text(("connected_status", "Upload: %s" % rate))
else:
self._log("Transfer failed or was not accepted", "warning_text")
self.rate_text.set_text("")
try:
if self._client_link is not None:
self._client_link.teardown()
except Exception:
pass
self.progress_text.set_text("")
self._cleanup_temp()
self._finish()
def _on_client_link_closed(self, link):
@@ -1508,16 +1699,33 @@ class SpeedTestView:
self._log("Link closed before the test started", "warning_text")
self._finish()
def _finish(self):
def cancel(self, silent=False):
if not self._sending:
return
self._sending = False
try:
if self._client_resource is not None:
self._client_resource.cancel()
except Exception:
pass
try:
if self._client_link is not None:
self._client_link.teardown()
except Exception:
pass
self._client_resource = None
self._cleanup_temp()
if not silent:
self._log("Speed test cancelled", "warning_text")
self.rate_text.set_text("")
self._set_progress(0)
self.start_button.original_widget.set_label("Start test")
self._update_listen_status()
self._draw()
def _size_str(num, suffix="B"):
units = ["", "K", "M", "G", "T"]
num = float(num)
for unit in units:
if abs(num) < 1024.0:
return "%.1f %s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f P%s" % (num, suffix)
def _finish(self):
self._sending = False
self._client_resource = None
self.start_button.original_widget.set_label("Start test")
self._update_listen_status()
self._draw()