5 Commits
Author SHA1 Message Date
oxpoe a47b5c251c fix(app): arahkan OpenSSL ke CA sistem pada bundel beku (periksa/unduh pembaruan gagal dengan CERTIFICATE_VERIFY_FAILED)
versi 1.2.1
2026-10-13 07:48:00 +07:00
oxpoe 387e428406 feat(app): periksa + perbarui otomatis dari GitHub Releases
- tombol 'Periksa pembaruan' (+ otomatis saat aplikasi mulai pada versi AppImage)
- bila ada rilis lebih baru: tombol 'Perbarui ke vX' — unduh aset rilis, ganti AppImage
  secara atomik, tarik ulang sender dari bundel baru, restart layanan
- item menu tray 'Periksa pembaruan'
- suite --selftest-actions ikut memeriksa pembaruan
- versi 1.2.0
2026-10-13 07:42:12 +07:00
oxpoe 93ce357900 chore: tombol Sponsor di header repo (FUNDING.yml -> halaman donasi) 2026-10-13 07:26:33 +07:00
oxpoe 53ee1c5102 docs: percantik halaman repo — badge lengkap + diagram alur di README, template issue laporan hardware, CONTRIBUTING, SECURITY, dan kartu social preview 2026-10-13 07:25:59 +07:00
oxpoe 93bad1f461 docs: perbarui pratinjau GUI dengan tombol Donasi/GitHub dan label versi 2026-10-13 07:15:08 +07:00
8 changed files with 225 additions and 3 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Shows the ❤ Sponsor button in the repository header.
custom: ["https://donasi.oxpoe.qzz.io/"]
@@ -0,0 +1,57 @@
name: Hardware report
description: Report a GAMEMAX (or other) LCD cooler that works, partly works, or does not work
title: "[hardware] <cooler model>"
labels: ["hardware"]
body:
- type: markdown
attributes:
value: |
Thanks for testing on your hardware! Reports from other panel revisions are how
this project grows. Please fill in what you can.
- type: input
id: cooler
attributes:
label: Cooler / panel model
placeholder: e.g. GAMEMAX IceBurg 360 Digital WH
validations:
required: true
- type: input
id: usb
attributes:
label: USB identity (lsusb)
description: "`lsusb -d 5131:2007` — paste the line, or the VID:PID if it differs"
validations:
required: true
- type: dropdown
id: result
attributes:
label: Result
options:
- Works fully (values correct on the panel)
- Partly works (which fields are wrong?)
- Panel lights up but shows wrong layout
- Not detected / no reaction
validations:
required: true
- type: textarea
id: report
attributes:
label: Output of `python3 src/gamemax_report.py --dry-run`
description: This writes nothing to the device, it only prints the report it would send.
render: text
validations:
required: false
- type: input
id: system
attributes:
label: System
placeholder: distro · kernel · desktop session · motherboard super-I/O (e.g. nct6798)
validations:
required: false
- type: textarea
id: notes
attributes:
label: Notes
description: Which fan channel / temperature source you had to pick, anything unexpected.
validations:
required: false
+33
View File
@@ -0,0 +1,33 @@
# Contributing
Thanks for helping — the most valuable contribution is **hardware reports**: which cooler
works, which firmware revision differs, which sensor channel has to be picked. Use the
*Hardware report* issue template, or send a pull request.
## Before opening a pull request
The sender must stay dependency-free (standard library only) and must never write to the
device unless the user asked for it.
```bash
# nothing is written to the panel:
python3 src/gamemax_report.py --dry-run
python3 src/gamemax_report.py --list-sensors
# the tray app self-checks without a display, and can exercise every action:
uv run --with pyside6 python src/gamemax_gui.py --selftest
uv run --with pyside6 python src/gamemax_gui.py --selftest-actions
# build the AppImage (needs uv + network on first run):
bash packaging/build_appimage.sh
```
`--selftest-actions` really does start/stop the service and toggle autostart, then leaves
the service running — expected.
## Style
- Python 3.10+, no new dependencies, comments only where the code cannot explain itself.
- GUI strings are Indonesian (the original author's language); code and docs are English.
- Keep the protocol knowledge in `docs/reverse-engineering.md` and the README table in sync
when you learn something new about the panel.
+15
View File
@@ -8,7 +8,22 @@ Reverse engineered independently from the Windows vendor software
[`docs/reverse-engineering.md`](docs/reverse-engineering.md) (Indonesian).
[![Release](https://img.shields.io/github/v/release/oxpoe/Gamemax-linux-apps?color=def750&label=release)](https://github.com/oxpoe/Gamemax-linux-apps/releases)
[![Release build](https://github.com/oxpoe/Gamemax-linux-apps/actions/workflows/release.yml/badge.svg)](https://github.com/oxpoe/Gamemax-linux-apps/actions/workflows/release.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-def750)](LICENSE)
[![Platform: Linux](https://img.shields.io/badge/platform-Linux-def750)](#compatibility)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-def750)](src/gamemax_report.py)
[![Dependencies: none](https://img.shields.io/badge/sender_dependencies-none-def750)](src/gamemax_report.py)
[![Donate](https://img.shields.io/badge/%E2%9D%A4-donate-def750)](https://donasi.oxpoe.qzz.io/)
```text
hwmon · /proc/stat · cpufreq · meminfo · statvfs
│
▼
gamemax_report.py ──► 65-byte HID report ──► LCD panel
(stdlib only) hidraw, 5131:2007
▲
gamemax_gui.py (tray) ──► systemd user unit + udev rule
```
Author: **oxpoe** · License: **MIT** · Independent project, **not affiliated with the
vendor** — please read [DISCLAIMER.md](DISCLAIMER.md) before using it.
+14
View File
@@ -0,0 +1,14 @@
# Security
This is a small local tool. The relevant surface:
- It writes 64-byte HID reports to the panel matching `VID 0x5131 / PID 0x2007`
(plus a udev rule that grants the logged-in user access to that `hidraw` node).
- The tray app runs `systemctl --user` and, for the udev rule, `pkexec sh` on a script it
writes to `/tmp`. Nothing else is executed, and no data leaves the machine.
- No secrets are stored. The config file (`~/.config/gamemax-report/config.json`) holds
sensor channel names and display settings only.
If you find a problem — for example a way to make the helper scripts run something else,
or a path that writes outside the documented locations — please open an issue (or a
private security advisory on GitHub). Do not test on hardware you do not own.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

+104 -3
View File
@@ -11,6 +11,9 @@ mengendalikannya dan memakai gamemax_report.py sebagai pengirim.
import json
import math
import os
import re
import tempfile
import urllib.request
import shutil
import subprocess
import sys
@@ -27,7 +30,10 @@ from PySide6.QtWidgets import (
)
from PySide6.QtNetwork import QLocalServer, QLocalSocket
APP_VERSION = "1.1.0"
APP_VERSION = "1.2.1"
REPO = "oxpoe/Gamemax-linux-apps"
RELEASES_API = f"https://api.github.com/repos/{REPO}/releases/latest"
UPDATE_ASSET = "GAMEMAX-LCD-x86_64.AppImage"
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import gamemax_report as gr # noqa: E402
@@ -73,6 +79,39 @@ def crash_log(text):
pass
def version_tuple(tag):
parts = re.findall(r"\d+", tag or "")[:3]
return tuple(int(p) for p in parts) or (0,)
def ensure_ca_bundle():
"""Bundel PyInstaller tidak menemukan CA sistem sendiri; arahkan OpenSSL ke sana."""
if os.environ.get("SSL_CERT_FILE"):
return
for path in ("/etc/ssl/certs/ca-certificates.crt", # Arch/Debian
"/etc/pki/tls/certs/ca-bundle.crt", # Fedora/openSUSE
"/etc/ssl/cert.pem"): # Alpine/BSD
if os.path.exists(path):
os.environ["SSL_CERT_FILE"] = path
return
def latest_release():
"""(tag, url aset) dari rilis terbaru GitHub — dipanggil dari thread pekerja."""
ensure_ca_bundle()
req = urllib.request.Request(RELEASES_API, headers={
"Accept": "application/vnd.github+json", "User-Agent": "gamemax-lcd"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.load(resp)
asset = next((a for a in data.get("assets", []) if a["name"] == UPDATE_ASSET), None)
return data.get("tag_name", ""), (asset or {}).get("browser_download_url", "")
def installed_appimage():
"""Path AppImage yang sedang berjalan (kosong kalau dijalankan dari sumber)."""
return os.environ.get("APPIMAGE", "")
def open_link(url, parent=None):
"""Buka tautan di peramban; kalau gagal, tampilkan pesannya di log."""
if not QDesktopServices.openUrl(QUrl(url)):
@@ -128,10 +167,12 @@ class Task(QThread):
def __init__(self, label, fn):
super().__init__()
self.label, self.fn = label, fn
self.result = None
def run(self):
try:
self.done.emit(self.label, True, self.fn() or "")
self.result = self.fn()
self.done.emit(self.label, True, self.result if isinstance(self.result, str) else "")
except Exception as exc: # noqa: BLE001
self.done.emit(self.label, False, str(exc))
@@ -358,6 +399,16 @@ class Window(QWidget):
links.addWidget(donate)
links.addWidget(github)
links.addStretch(1)
self.check_button = QPushButton("Periksa pembaruan")
self.check_button.setToolTip("Bandingkan dengan rilis terbaru di GitHub")
self.check_button.clicked.connect(lambda: self.start_task("memeriksa pembaruan", self.do_check_update))
links.addWidget(self.check_button)
self.update_button = QPushButton("Perbarui")
self.update_button.setStyleSheet("font-weight:bold;")
self.update_button.hide()
self.update_button.clicked.connect(self.on_update_clicked)
links.addWidget(self.update_button)
links.addStretch(1)
self.version_label = QLabel(f"v{APP_VERSION}")
self.version_label.setStyleSheet("color:#8a8f8a;")
links.addWidget(self.version_label)
@@ -382,6 +433,9 @@ class Window(QWidget):
self.poll = QTimer(self, timeout=self.refresh, interval=1000)
self.poll.start()
self.refresh()
if installed_appimage(): # hanya pada versi terpasang
QTimer.singleShot(2500, lambda: self.start_task("memeriksa pembaruan",
self.do_check_update))
if gr.load_config(CONFIG_PATH).get("blank"):
self.log("PERHATIAN: mode layar kosong sedang aktif dari konfigurasi "
"(layar menampilkan nilai nol). Hilangkan centang 'Layar kosong' "
@@ -424,6 +478,44 @@ class Window(QWidget):
"blank": bool(self.blank.isChecked()),
}
# -------------------------------------------------------------- pembaruan
def do_check_update(self):
tag, url = latest_release()
self._pending_update = (tag, url) if url and version_tuple(tag) > version_tuple(APP_VERSION) else None
if not tag:
return "tidak bisa membaca daftar rilis GitHub"
if self._pending_update:
return f"versi baru tersedia: {tag} (terpasang v{APP_VERSION})"
return f"sudah versi terbaru (v{APP_VERSION})"
def on_update_clicked(self):
pending = getattr(self, "_pending_update", None)
target = installed_appimage()
if not pending:
return
if not target:
self.log("tidak berjalan dari AppImage — perbarui manual dari halaman Releases")
return
tag, url = pending
self.start_task(f"mengunduh {tag}", lambda: self.do_update(tag, url, target))
def do_update(self, tag, url, target):
ensure_ca_bundle()
tmp = target + ".new"
urllib.request.urlretrieve(url, tmp)
os.chmod(tmp, 0o755)
os.replace(tmp, target) # ganti berkas secara atomik
# sender di layanan juga diambil dari bundel baru
with tempfile.TemporaryDirectory() as td:
subprocess.run([target, "--appimage-extract"], cwd=td,
capture_output=True, timeout=180)
sender = os.path.join(td, "squashfs-root/usr/bin/_internal/gamemax_report.py")
if os.path.exists(sender):
os.makedirs(INSTALL_DIR, exist_ok=True)
shutil.copyfile(sender, os.path.join(INSTALL_DIR, "gamemax_report.py"))
systemctl("restart", SERVICE)
return f"diperbarui ke {tag} — tutup jendela ini lalu buka lagi untuk memakai versi baru"
# -------------------------------------------------------------- tugas latar
def start_task(self, label, fn):
if getattr(self, "_task", None) is not None and self._task.isRunning():
@@ -445,6 +537,10 @@ class Window(QWidget):
for button in self.buttons:
button.setEnabled(True)
self.log(("selesai: " if ok else "GAGAL: ") + label + (f" — {message}" if message else ""))
pending = getattr(self, "_pending_update", None)
if label == "memeriksa pembaruan" and pending:
self.update_button.setText(f"Perbarui ke {pending[0]}")
self.update_button.show()
self.refresh()
# -------------------------------------------------------------- aksi
@@ -689,6 +785,9 @@ class Tray:
toggle = QAction("Aktifkan / matikan", menu)
toggle.triggered.connect(lambda: self.window.on_toggle())
self.blank_action.triggered.connect(lambda checked: self.window.blank.setChecked(checked))
check_action = QAction("Periksa pembaruan", menu)
check_action.triggered.connect(lambda: self.window.start_task(
"memeriksa pembaruan", self.window.do_check_update))
donate_action = QAction("❤ Donasi", menu)
donate_action.triggered.connect(lambda: open_link(DONATE_URL, self.window))
github_action = QAction("GitHub", menu)
@@ -699,6 +798,7 @@ class Tray:
menu.addAction(toggle)
menu.addAction(self.blank_action)
menu.addSeparator()
menu.addAction(check_action)
menu.addAction(donate_action)
menu.addAction(github_action)
menu.addSeparator()
@@ -792,7 +892,8 @@ def _main():
("nyalakan layar", w.on_toggle),
("autostart ON", lambda: w.on_autostart(True)),
("kirim uji", w.send_test),
("pasang layanan", lambda: w.install_service(w.config())))
("pasang layanan", lambda: w.install_service(w.config())),
("periksa pembaruan", w.do_check_update))
for name, fn in steps:
try:
fn()