Skip to content

Commit 7ea1301

Browse files
MTSistemiclaude
andcommitted
Safety: cap Turbo/Crazy presets at GPU 2200, clamp undervolted >2200 in the helper, warn on Crazy + Hub ODRS cache
- tuner-presets.json: Turbo and Crazy still pointed the GPU at 2230 @ 1000 mV, the exact undervolted combo that hard-freezes the BC-250. Both now cap at the validated 2200 MHz. - helper _gpu_curve: defensive clamp — any request >2200 MHz at <=1000 mV is reduced to 2200 (2230 needs 1000-1060 mV per community data); >2200 stays allowed when the caller raises the voltage. +2 tests (11 total). - Tuner: applying a 4000 MHz-class preset (Crazy) now warns that it is benchmark-only, persists across reboots, and can hard-freeze some boards (the root cause of the 4 hangs of the last two days). - Hub: ODRS ratings get a 24h on-disk cache (~/.cache/skillfish-hub) — faster launches and stars keep working offline; the manual refresh button bypasses it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bf22551 commit 7ea1301

8 files changed

Lines changed: 127 additions & 16 deletions

File tree

apps/hub/skillfish-hub

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,34 @@ class Catalog:
276276
return out
277277

278278
def load_ratings(self):
279+
# ODRS aggregate ratings, with a 24h on-disk cache: skips a multi-MB
280+
# download on every launch and keeps stars working offline.
281+
cache = os.path.join(os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")),
282+
"skillfish-hub", "odrs-ratings.json")
283+
data = None
279284
try:
280-
with urllib.request.urlopen(ODRS + "/ratings", timeout=15) as r:
281-
data = json.loads(r.read().decode())
282-
for k, v in data.items():
285+
import time as _t
286+
if os.path.exists(cache) and (_t.time() - os.path.getmtime(cache)) < 86400:
287+
with open(cache, encoding="utf-8") as fh:
288+
data = json.load(fh)
289+
except Exception:
290+
data = None # unreadable cache: refetch below
291+
if data is None:
292+
try:
293+
with urllib.request.urlopen(ODRS + "/ratings", timeout=15) as r:
294+
data = json.loads(r.read().decode())
295+
os.makedirs(os.path.dirname(cache), exist_ok=True)
296+
with open(cache, "w", encoding="utf-8") as fh:
297+
json.dump(data, fh)
298+
except Exception:
299+
# offline / ODRS down: fall back to a stale cache if we have one
300+
try:
301+
with open(cache, encoding="utf-8") as fh:
302+
data = json.load(fh)
303+
except Exception:
304+
data = None
305+
try:
306+
for k, v in (data or {}).items():
283307
tot = v.get("total", 0)
284308
if tot:
285309
avg = sum(int(k2[-1]) * v.get(k2, 0) for k2 in ("star1", "star2", "star3", "star4", "star5")) / tot
@@ -1196,6 +1220,12 @@ class Hub(QMainWindow):
11961220
self._ub.done.connect(lambda u: self.nav["updates"].setText(" ⟳ " + L("Aggiornamenti", "Updates") + ((" (%d)" % len(u)) if u else ""))); self._ub.start()
11971221

11981222
def manual_refresh(self):
1223+
# a manual refresh should bypass the 24h ODRS ratings cache too
1224+
try:
1225+
os.remove(os.path.join(os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")),
1226+
"skillfish-hub", "odrs-ratings.json"))
1227+
except OSError:
1228+
pass # no cache yet
11991229
cmds = [["pkexec", HELPER, "apt-update"]]
12001230
if self.backend_on("flatpak"): cmds.append(["flatpak", "update", "--appstream"])
12011231
self._run_chain(cmds, L("Aggiorno gli elenchi dei pacchetti…", "Refreshing the package lists…"))

apps/tuner/skillfish-tuner

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,21 @@ class TunerWindow(QMainWindow):
367367
return p.get("desc_" + LANG) or p.get("desc", "")
368368

369369
def _apply_preset(self, p):
370-
c = p["cpu"]; self.d.cmd(cmd="apply-cpu", mhz=c["frequency"], scale=c["scale"], temp=c["max_temperature"])
370+
c = p["cpu"]
371+
if c["frequency"] >= 4000:
372+
# the Crazy-class presets are benchmark-only: 4000 @ heavy undervolt was
373+
# traced to repeated whole-machine hard-freezes when left applied 24/7
374+
if QMessageBox.warning(self, L("Preset estremo", "Extreme preset"),
375+
L("«%s» è pensato SOLO per sessioni di benchmark con ottimo raffreddamento.\n\n"
376+
"Resta attivo anche dopo il riavvio e su alcune schede causa blocchi totali "
377+
"improvvisi (anche a riposo). Dopo i benchmark torna a Turbo o Performance.\n\nApplicare comunque?" % self._pname(p),
378+
"“%s” is meant ONLY for benchmark sessions with very good cooling.\n\n"
379+
"It stays applied across reboots and on some boards causes sudden total "
380+
"freezes (even at idle). Switch back to Turbo or Performance after benchmarking.\n\nApply anyway?" % self._pname(p)),
381+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
382+
QMessageBox.StandardButton.No) != QMessageBox.StandardButton.Yes:
383+
return
384+
self.d.cmd(cmd="apply-cpu", mhz=c["frequency"], scale=c["scale"], temp=c["max_temperature"])
371385
gg = p["gpu"]; self.d.cmd(cmd="apply-gpu", minmhz=gg["min_mhz"], minmv=gg["min_mv"], maxmhz=gg["max_mhz"], maxmv=gg["max_mv"])
372386
f = p["fan"]; self.d.cmd(cmd="apply-fan", mode=f["mode"], pct=f.get("pct", 50))
373387
if p.get("thermal_guard"):

apps/tuner/skillfish-tuner-helper

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ def _gpu_curve(minmhz,minmv,maxmhz,maxmv):
110110
# Build a SMOOTH multi-point voltage curve (gentle clock/voltage transitions).
111111
# The BC-250 SMU can hard-hang on abrupt jumps, so insert validated mid-points
112112
# (1500/900, 2000/1000) between idle and the requested max instead of a 2-point line.
113+
# Safety clamp: >2200 MHz at <=1000 mV is UNDERVOLTED and hard-freezes the
114+
# machine (reproduced + community data: 2230 needs 1000-1060 mV). Allow >2200
115+
# only when the caller raises the voltage accordingly.
116+
maxmhz, maxmv = int(maxmhz), int(maxmv)
117+
if maxmhz > 2200 and maxmv <= 1000:
118+
maxmhz = 2200
113119
pts=[(int(minmhz),int(minmv))]
114120
for f,v in ((1500,900),(2000,1000)):
115121
if minmhz < f < maxmhz: pts.append((f,v))

system/usr/local/bin/skillfish-hub

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,34 @@ class Catalog:
276276
return out
277277

278278
def load_ratings(self):
279+
# ODRS aggregate ratings, with a 24h on-disk cache: skips a multi-MB
280+
# download on every launch and keeps stars working offline.
281+
cache = os.path.join(os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")),
282+
"skillfish-hub", "odrs-ratings.json")
283+
data = None
279284
try:
280-
with urllib.request.urlopen(ODRS + "/ratings", timeout=15) as r:
281-
data = json.loads(r.read().decode())
282-
for k, v in data.items():
285+
import time as _t
286+
if os.path.exists(cache) and (_t.time() - os.path.getmtime(cache)) < 86400:
287+
with open(cache, encoding="utf-8") as fh:
288+
data = json.load(fh)
289+
except Exception:
290+
data = None # unreadable cache: refetch below
291+
if data is None:
292+
try:
293+
with urllib.request.urlopen(ODRS + "/ratings", timeout=15) as r:
294+
data = json.loads(r.read().decode())
295+
os.makedirs(os.path.dirname(cache), exist_ok=True)
296+
with open(cache, "w", encoding="utf-8") as fh:
297+
json.dump(data, fh)
298+
except Exception:
299+
# offline / ODRS down: fall back to a stale cache if we have one
300+
try:
301+
with open(cache, encoding="utf-8") as fh:
302+
data = json.load(fh)
303+
except Exception:
304+
data = None
305+
try:
306+
for k, v in (data or {}).items():
283307
tot = v.get("total", 0)
284308
if tot:
285309
avg = sum(int(k2[-1]) * v.get(k2, 0) for k2 in ("star1", "star2", "star3", "star4", "star5")) / tot
@@ -1196,6 +1220,12 @@ class Hub(QMainWindow):
11961220
self._ub.done.connect(lambda u: self.nav["updates"].setText(" ⟳ " + L("Aggiornamenti", "Updates") + ((" (%d)" % len(u)) if u else ""))); self._ub.start()
11971221

11981222
def manual_refresh(self):
1223+
# a manual refresh should bypass the 24h ODRS ratings cache too
1224+
try:
1225+
os.remove(os.path.join(os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")),
1226+
"skillfish-hub", "odrs-ratings.json"))
1227+
except OSError:
1228+
pass # no cache yet
11991229
cmds = [["pkexec", HELPER, "apt-update"]]
12001230
if self.backend_on("flatpak"): cmds.append(["flatpak", "update", "--appstream"])
12011231
self._run_chain(cmds, L("Aggiorno gli elenchi dei pacchetti…", "Refreshing the package lists…"))

system/usr/local/bin/skillfish-tuner

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,21 @@ class TunerWindow(QMainWindow):
367367
return p.get("desc_" + LANG) or p.get("desc", "")
368368

369369
def _apply_preset(self, p):
370-
c = p["cpu"]; self.d.cmd(cmd="apply-cpu", mhz=c["frequency"], scale=c["scale"], temp=c["max_temperature"])
370+
c = p["cpu"]
371+
if c["frequency"] >= 4000:
372+
# the Crazy-class presets are benchmark-only: 4000 @ heavy undervolt was
373+
# traced to repeated whole-machine hard-freezes when left applied 24/7
374+
if QMessageBox.warning(self, L("Preset estremo", "Extreme preset"),
375+
L("«%s» è pensato SOLO per sessioni di benchmark con ottimo raffreddamento.\n\n"
376+
"Resta attivo anche dopo il riavvio e su alcune schede causa blocchi totali "
377+
"improvvisi (anche a riposo). Dopo i benchmark torna a Turbo o Performance.\n\nApplicare comunque?" % self._pname(p),
378+
"“%s” is meant ONLY for benchmark sessions with very good cooling.\n\n"
379+
"It stays applied across reboots and on some boards causes sudden total "
380+
"freezes (even at idle). Switch back to Turbo or Performance after benchmarking.\n\nApply anyway?" % self._pname(p)),
381+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
382+
QMessageBox.StandardButton.No) != QMessageBox.StandardButton.Yes:
383+
return
384+
self.d.cmd(cmd="apply-cpu", mhz=c["frequency"], scale=c["scale"], temp=c["max_temperature"])
371385
gg = p["gpu"]; self.d.cmd(cmd="apply-gpu", minmhz=gg["min_mhz"], minmv=gg["min_mv"], maxmhz=gg["max_mhz"], maxmv=gg["max_mv"])
372386
f = p["fan"]; self.d.cmd(cmd="apply-fan", mode=f["mode"], pct=f.get("pct", 50))
373387
if p.get("thermal_guard"):

system/usr/local/bin/skillfish-tuner-helper

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ def _gpu_curve(minmhz,minmv,maxmhz,maxmv):
110110
# Build a SMOOTH multi-point voltage curve (gentle clock/voltage transitions).
111111
# The BC-250 SMU can hard-hang on abrupt jumps, so insert validated mid-points
112112
# (1500/900, 2000/1000) between idle and the requested max instead of a 2-point line.
113+
# Safety clamp: >2200 MHz at <=1000 mV is UNDERVOLTED and hard-freezes the
114+
# machine (reproduced + community data: 2230 needs 1000-1060 mV). Allow >2200
115+
# only when the caller raises the voltage accordingly.
116+
maxmhz, maxmv = int(maxmhz), int(maxmv)
117+
if maxmhz > 2200 and maxmv <= 1000:
118+
maxmhz = 2200
113119
pts=[(int(minmhz),int(minmv))]
114120
for f,v in ((1500,900),(2000,1000)):
115121
if minmhz < f < maxmhz: pts.append((f,v))

system/usr/share/skillfish/tuner-presets.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@
5252
"name": "Turbo",
5353
"name_it": "Turbo",
5454
"name_en": "Turbo",
55-
"desc": "Spinta alta. CPU 3900 MHz ~1199 mV (undervolt), GPU 2230 MHz.",
56-
"desc_it": "Spinta alta. CPU 3900 MHz ~1199 mV (undervolt), GPU 2230 MHz.",
57-
"desc_en": "High boost. CPU 3900 MHz ~1199 mV (undervolt), GPU 2230 MHz.",
55+
"desc": "Spinta alta. CPU 3900 MHz ~1199 mV (undervolt), GPU 2200 MHz.",
56+
"desc_it": "Spinta alta. CPU 3900 MHz ~1199 mV (undervolt), GPU 2200 MHz.",
57+
"desc_en": "High boost. CPU 3900 MHz ~1199 mV (undervolt), GPU 2200 MHz.",
5858
"cpu": {
5959
"frequency": 3900,
6060
"scale": -24,
@@ -63,7 +63,7 @@
6363
"gpu": {
6464
"min_mhz": 350,
6565
"min_mv": 700,
66-
"max_mhz": 2230,
66+
"max_mhz": 2200,
6767
"max_mv": 1000
6868
},
6969
"fan": {
@@ -76,9 +76,9 @@
7676
"name": "Crazy",
7777
"name_it": "Crazy",
7878
"name_en": "Crazy",
79-
"desc": "Massimo validato. CPU 4.0 GHz ~1224 mV (undervolt), GPU 2230 MHz.",
80-
"desc_it": "Massimo validato. CPU 4.0 GHz ~1224 mV (undervolt), GPU 2230 MHz.",
81-
"desc_en": "Validated maximum. CPU 4.0 GHz ~1224 mV (undervolt), GPU 2230 MHz.",
79+
"desc": "Massimo validato. CPU 4.0 GHz ~1224 mV (undervolt), GPU 2200 MHz.",
80+
"desc_it": "Massimo validato. CPU 4.0 GHz ~1224 mV (undervolt), GPU 2200 MHz.",
81+
"desc_en": "Validated maximum. CPU 4.0 GHz ~1224 mV (undervolt), GPU 2200 MHz.",
8282
"cpu": {
8383
"frequency": 4000,
8484
"scale": -36,
@@ -87,7 +87,7 @@
8787
"gpu": {
8888
"min_mhz": 350,
8989
"min_mv": 700,
90-
"max_mhz": 2230,
90+
"max_mhz": 2200,
9191
"max_mv": 1000
9292
},
9393
"fan": {

tests/test_tuner_helper.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ def test_curve_is_ascending(helper):
5353
assert pts == sorted(pts)
5454

5555

56+
def test_curve_clamps_undervolted_2230(helper):
57+
# 2230 @ <=1000 mV is the reproduced hard-freeze combo: must clamp to 2200
58+
pts = helper._gpu_curve(350, 700, 2230, 1000)
59+
assert max(f for f, _ in pts) == 2200
60+
61+
62+
def test_curve_allows_2230_with_proper_voltage(helper):
63+
pts = helper._gpu_curve(350, 700, 2230, 1060)
64+
assert (2230, 1060) in pts
65+
66+
5667
# ---------- apply_gpu: writes the curve, replaces old safe-points ----------
5768

5869
def test_apply_gpu_writes_multipoint_curve(helper):

0 commit comments

Comments
 (0)