22 Commits
Author SHA1 Message Date
oxpoe 0353b71223 ci: pin URL rilis publik + ganti setup-uv (API GitHub kena rate limit)
release / appimage (push) Successful in 1m40s
2026-10-15 13:08:10 +07:00
oxpoe 7f01abad25 release: v1.3.5 (zsync update URL kini domain publik)
release / appimage (push) Failing after 32s
2026-10-15 13:01:48 +07:00
oxpoe 4886cb570d docs: banner mirror sebutkan rilis/unduhan di Gitea 2026-10-14 22:34:23 +07:00
oxpoe 66894ecdca ci: rilis via API Gitea (ganti action-gh-release) + fix deps build
release / appimage (push) Successful in 1m7s
2026-10-14 21:45:32 +07:00
oxpoe d9b64a5ab5 chore: bump versi 1.3.4 (rebrand ke Gitea)
release / appimage (push) Failing after 1m24s
2026-10-14 21:40:55 +07:00
oxpoe 3f73b97b39 rebrand: ganti sisa teks 'GitHub' -> 'Gitea' (UI, komentar, dokumen) 2026-10-14 21:09:05 +07:00
oxpoe 323e270c51 rebrand: arahkan tautan, badge, label UI & cek-update ke Gitea (bukan GitHub) 2026-10-14 21:08:04 +07:00
oxpoe 3c06d802a9 docs: banner mirror (repo asli di Gitea) 2026-10-14 12:30:39 +07:00
oxpoe 76790e0321 feat(autostart): tray ikut autostart saat login, tersembunyi (--tray-only)
- checkbox 'Mulai otomatis saat login' kini mengurus layanan (sender) DAN entri autostart tray
- entri autostart milik tool integrasi AppImage yang menunjuk AppImage sama ditandai Hidden=true
  supaya saat login hanya satu instance tray yang jalan, tanpa jendela
- install.sh ikut memasang entri autostart tray + menghapusnya saat --uninstall
- sender: retry 1 detik dan pesan galat izin tidak diulang-ulang (ACL udev baru aktif
  beberapa detik setelah login) — sebelumnya ~12 detik penuh galat beruntun saat boot
- versi 1.3.3
2026-10-13 10:27:05 +07:00
oxpoe c3d34a2788 fix(packaging): nama entri menu di dalam AppImage jadi 'IceBurg LCD'
AppImage manager menyalin .desktop dari dalam bundel, sehingga entri di app drawer
masih bernama merek vendor walau UI aplikasinya sudah diganti. Kini bundel membawa
Name=IceBurg LCD (GenericName/Comment juga tanpa merek). Versi 1.3.2
2026-10-13 09:51:06 +07:00
oxpoe 4711ef5532 refactor(ui): nama aplikasi jadi 'IceBurg LCD' (tanpa merek vendor di UI)
- judul jendela, tooltip tray, notifikasi, nama aplikasi Qt, dan deskripsi unit systemd
- entri menu di install.sh + petunjuk di jendela
- README: catatan bahwa UI tidak memakai merek; repo tetap menyebut merek hanya untuk
  menjelaskan hardware yang didukung
- versi 1.3.1
2026-10-13 09:44:17 +07:00
oxpoe c384df7cac feat: install.sh — pemasangan/pelepasan satu perintah
- unduh rilis terbaru (+ vX.Y.Z), verifikasi sha256, pasang AppImage, sender, unit systemd,
  entri menu, ikon, dan aturan udev
- opsi: --uninstall, --remove-data, --no-service, --version
- udev memakai sudo; kalau dijalankan lewat pipa tanpa kata sandi, perintahnya dicetak
- README: cara satu perintah di bagian Install
2026-10-13 09:25:11 +07:00
oxpoe 1f3222bc96 feat: rilis v1.3.0 — popup tutup (tray/keluar), tampilan modern ringan, salin report, ingat ukuran jendela
- closeEvent: dialog 'Sembunyikan ke tray / Keluar / Batal' + 'Ingat pilihan ini'
  (tersimpan sebagai close_action; tombol sembunyikan dinonaktifkan bila tray tidak tersedia)
- QSS tema gelap modern: kartu membulat, tombol utama beraksen, kontrol bergaya; tanpa aset/efek berat
- tombol 'Salin report' (hex ke papan klip) untuk laporan bug
- ukuran & posisi jendela diingat (config geometry)
- perbaikan: dialog tutup tidak muncul saat keluar otomatis (--selftest), tunggu tugas latar saat keluar
- suite uji: 13 langkah, semua lolos di bundel beku
2026-10-13 09:02:50 +07:00
oxpoe ac23195996 build: sematkan info zsync ke AppImage (appimageupdatetool langsung bisa dipakai)
docs: catat bahwa rilis pasca-v1.2.3 membawa update information
2026-10-13 08:15:55 +07:00
oxpoe 7156458c6f feat(updater): verifikasi SHA-256 sebelum mengganti AppImage; CI menerbitkan .sha256 + .zsync
- do_update: unduh → verifikasi checksum rilis → baru ganti berkas (mismatch = dibatalkan)
- workflow: hasilkan GAMEMAX-LCD-x86_64.AppImage.sha256 dan .zsync (zsyncmake), lampirkan ketiganya
- README: bagian Updates menjelaskan checksum + zsync (delta update opsional)
- versi 1.2.3
2026-10-13 08:10:05 +07:00
oxpoe e0090fc1c3 feat: suite uji mencetak pesan hasil + GAMEMAX_DEBUG menyalin log ke stdout
docs: bagian Updates (pemeriksaan & pembaruan satu klik dari GitHub Releases)
versi 1.2.2
2026-10-13 07:53:51 +07:00
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
oxpoe 657a8d10dc feat(app): tombol Donasi + GitHub (jendela & tray), label versi v1.1.0
docs: bagian Support dengan tautan donasi
2026-10-13 07:09:58 +07:00
13 changed files with 791 additions and 56 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
+41 -21
View File
@@ -1,6 +1,6 @@
name: release
# Build the GAMEMAX LCD AppImage from source and attach it to a GitHub release.
# Build the GAMEMAX LCD AppImage from source and attach it to a Gitea release.
# Trigger: push a tag like v1.0.0 (or run manually from the Actions tab).
on:
@@ -11,6 +11,11 @@ on:
permissions:
contents: write
env:
# github.server_url mengikuti alamat saat runner didaftarkan dan bisa jadi IP internal,
# jadi URL yang disematkan ke dalam AppImage ditulis eksplisit di sini.
RELEASE_URL: https://gitea.oxpoe.qzz.io/oxpoe/Gamemax-linux-apps/releases/download
jobs:
appimage:
runs-on: ubuntu-latest
@@ -21,14 +26,16 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libgl1 libegl1 libxkbcommon0 libdbus-1-3 libfuse2 fuse
libgl1 libegl1 libxkbcommon0 libdbus-1-3 libfuse2 fuse zsync libpython3.12
- name: Set up uv
uses: astral-sh/setup-uv@v5
# installers astral, bukan setup-uv: setup-v5 menembak API GitHub dan kena rate limit
run: curl -LsSf https://astral.sh/uv/install.sh | env INSTALLER_PREFIX=/usr/local sh
- name: Build AppImage
env:
APPIMAGE_EXTRACT_AND_RUN: "1"
UPDATE_INFO_URL: ${{ env.RELEASE_URL }}/${{ github.ref_name }}/GAMEMAX-LCD-x86_64.AppImage.zsync
run: bash packaging/build_appimage.sh
- name: Smoke test the bundle (no display)
@@ -38,25 +45,38 @@ jobs:
run: |
./dist/GAMEMAX-LCD-x86_64.AppImage --selftest
- name: Publish release
uses: softprops/action-gh-release@v2
with:
name: GAMEMAX LCD ${{ github.ref_name }}
generate_release_notes: true
body: |
Linux bridge + tray app for the LCD panel of GAMEMAX IceBurg Digital AIO
coolers. Built automatically from `${{ github.sha }}`.
- name: Checksum and zsync metadata
run: |
cd dist
sha256sum GAMEMAX-LCD-x86_64.AppImage > GAMEMAX-LCD-x86_64.AppImage.sha256
zsyncmake -u "${{ env.RELEASE_URL }}/${{ github.ref_name }}/GAMEMAX-LCD-x86_64.AppImage" \
-o GAMEMAX-LCD-x86_64.AppImage.zsync GAMEMAX-LCD-x86_64.AppImage
cat GAMEMAX-LCD-x86_64.AppImage.sha256
ls -l
**Download** `GAMEMAX-LCD-x86_64.AppImage`, then:
- name: Publish release (Gitea API)
env:
TOKEN: ${{ github.token }}
run: |
API="${GITHUB_API_URL:-${GITHUB_SERVER_URL}/api/v1}"
TAG="${GITHUB_REF_NAME}"
cat > /tmp/body.md <<'EOF'
Linux bridge + tray app for the LCD panel of GAMEMAX IceBurg Digital AIO coolers.
```bash
chmod +x GAMEMAX-LCD-x86_64.AppImage
./GAMEMAX-LCD-x86_64.AppImage # tray + window
./GAMEMAX-LCD-x86_64.AppImage --selftest # verify the bundle starts
```
**Download** `GAMEMAX-LCD-x86_64.AppImage`, then:
Device access still needs the udev rule once:
`sudo cp packaging/70-gamemax.rules /etc/udev/rules.d/ && sudo udevadm control --reload`
```bash
chmod +x GAMEMAX-LCD-x86_64.AppImage
./GAMEMAX-LCD-x86_64.AppImage
```
Independent project, not affiliated with the vendor — see DISCLAIMER.md.
files: dist/GAMEMAX-LCD-x86_64.AppImage
Verify with the published SHA-256, or use AppImageUpdate with the `.zsync` asset.
Independent project, not affiliated with the vendor.
EOF
python3 -c 'import json,sys;print(json.dumps({"tag_name":sys.argv[1],"name":"GAMEMAX LCD "+sys.argv[1],"body":open(sys.argv[2]).read()}))' "$TAG" /tmp/body.md > /tmp/rel.json
RID=$(curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" "$API/repos/$GITHUB_REPOSITORY/releases" --data @/tmp/rel.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
echo "release id=$RID"
for f in dist/GAMEMAX-LCD-x86_64.AppImage dist/GAMEMAX-LCD-x86_64.AppImage.sha256 dist/GAMEMAX-LCD-x86_64.AppImage.zsync; do
[ -f "$f" ] || continue
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@$f" "$API/repos/$GITHUB_REPOSITORY/releases/$RID/assets?name=$(basename "$f")" -o /dev/null
done
+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.
+68 -3
View File
@@ -1,3 +1,5 @@
> 🪞 **Mirror.** Repo asli, pengembangan & **rilis/unduhan** ada di **Gitea**: https://gitea.oxpoe.qzz.io/oxpoe/Gamemax-linux-apps/releases · GitHub hanya cermin kode.
# GAMEMAX LCD — Linux bridge & tray app
Drive the LCD panel of a GAMEMAX **IceBurg Digital** AIO cooler from Linux: a small
@@ -7,8 +9,23 @@ Reverse engineered independently from the Windows vendor software
(`PcInfoMonitorPro.exe` / "IceBurg Digital Software"); the protocol notes are in
[`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](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgitea.oxpoe.qzz.io%2Fapi%2Fv1%2Frepos%2Foxpoe%2FGamemax-linux-apps%2Freleases%2Flatest&query=%24.tag_name&label=release&color=def750&logo=gitea)](https://gitea.oxpoe.qzz.io/oxpoe/Gamemax-linux-apps/releases)
[![Release build](https://gitea.oxpoe.qzz.io/oxpoe/Gamemax-linux-apps/actions/workflows/release.yml/badge.svg)](https://gitea.oxpoe.qzz.io/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.
@@ -86,13 +103,19 @@ temperature source (Tctl vs Tccd).
send/sample interval — saved to `~/.config/gamemax-report/config.json`
- **Clear mode indicators**: green "showing sensor data" / orange "blank (zeros)" /
grey "sending stopped", dimmed tray icon and a desktop notification per change
- **Autostart** via a systemd user unit (equivalent of the vendor's Task Scheduler trick)
- **Autostart** at login: a systemd user unit for the sender **and** an XDG autostart entry that
starts the tray app with `--tray-only` (no window pops up; open it from the tray menu). The
*Mulai otomatis saat login* checkbox manages both, and disables any other autostart entry that
would launch the same AppImage with a window.
- **Plug and play**: the service waits for the device, streams while present and keeps
waiting through unplug/replug; a udev rule grants access to the logged-in user
- **Single instance**: launching the app again just raises the running window
- **Self-update from Gitea Releases** (check + one-click update)
- **Donate / Gitea buttons** in the app (window and tray menu)
- **AppImage build** script (`packaging/build_appimage.sh`, PySide6 bundled)
The GUI labels are in Indonesian.
The GUI labels are in Indonesian. The app itself is named **IceBurg LCD** (no vendor brand in
the UI); the repository keeps the hardware and brand name only to describe what it supports.
## Requirements
@@ -106,6 +129,16 @@ The GUI labels are in Indonesian.
## Install
One command (downloads the latest release, verifies its SHA-256, installs the app, the
systemd user unit, the menu entry and the udev rule):
```bash
curl -fsSL https://gitea.oxpoe.qzz.io/oxpoe/Gamemax-linux-apps/raw/branch/main/install.sh | bash
# uninstall: bash install.sh --uninstall (add --remove-data to drop settings too)
```
Manual steps, if you prefer to see every command:
```bash
# 1. device access for the logged-in user (once, needs root)
sudo cp packaging/70-gamemax.rules /etc/udev/rules.d/
@@ -192,6 +225,38 @@ docs/ reverse-engineering.md — full analysis (Indonesian)
assets/ previews and icon
```
## Updates
The app keeps itself current from Gitea Releases:
- On start (when running from an AppImage) and from the **Periksa pembaruan** button or the
tray menu, it asks the Gitea API for the latest release and compares it with its version.
- When a newer release exists, a **Perbarui ke vX.Y.Z** button appears. One click downloads
the release asset, replaces the running AppImage atomically, refreshes the sender from the
new bundle and restarts the service. Restart the app to use the new version.
- The download is verified against the release's published **SHA-256** before the AppImage is
replaced; a mismatch aborts the update. Older releases without a checksum are updated with a
warning in the log.
- Nothing is downloaded without that click, and no other files are touched.
Each release publishes three assets: the AppImage, its `.sha256`, and a `.zsync` file for
`appimageupdatetool` / AppImageUpdate (delta updates). The zsync file is optional — the
built-in updater does not need it — but the AppImage also carries the zsync URL as embedded
update information (releases built after v1.2.3), so `appimageupdatetool ./GAMEMAX-LCD.AppImage`
works directly.
If the check fails (offline, proxy), the message appears in the app log. `GAMEMAX_DEBUG=1`
also mirrors the log to stdout for headless runs.
## Support
If this project is useful to you:
- ❤ **Donate:** <https://donasi.oxpoe.qzz.io/> (buttons for it and for this repository are
in the app: window bottom row and the tray menu)
- ⭐ Star the repo, or open an issue with your cooler model and what worked — reports from
other IceBurg Digital revisions are the best way to widen hardware support.
## Credits
Reverse engineered and written by **oxpoe**, with the help of
+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 Gitea). 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

Executable
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env bash
# GAMEMAX LCD — pemasangan / pelepasan satu perintah.
#
# curl -fsSL https://gitea.oxpoe.qzz.io/oxpoe/Gamemax-linux-apps/raw/branch/main/install.sh | bash
#
# Opsi:
# --uninstall lepaskan pemasangan (layanan, aplikasi, config, ikon, entri menu)
# --no-service pasang aplikasi saja, jangan aktifkan layanan/autostart
# --version vX.Y.Z pasang versi rilis tertentu (default: rilis terbaru)
# --remove-data saat --uninstall: hapus juga ~/.config/gamemax-report
#
# UdeV: aturan izin device dipasang dengan sudo. Kalau skrip dijalankan lewat pipa
# (curl | bash) dan sudo butuh kata sandi, perintahnya dicetak untuk dijalankan manual.
set -euo pipefail
REPO="oxpoe/Gamemax-linux-apps"
ASSET="GAMEMAX-LCD-x86_64.AppImage"
INSTANCE="https://gitea.oxpoe.qzz.io"
RAW="$INSTANCE/$REPO/raw/branch/main"
OPT_DIR="$HOME/.local/opt/gamemax-lcd"
APP="$OPT_DIR/GAMEMAX-LCD.AppImage"
DATA_DIR="$HOME/.local/share/gamemax-report"
UNIT="$HOME/.config/systemd/user/gamemax-report.service"
DESKTOP_DIR="$HOME/.local/share/applications"
ICON_DIR="$HOME/.local/share/icons/hicolor/128x128/apps"
RULE=/etc/udev/rules.d/70-gamemax.rules
SERVICE="gamemax-report.service"
UNINSTALL=0 WITH_SERVICE=1 REMOVE_DATA=0 VERSION="latest"
say() { printf '%s\n' "$*"; }
die() { printf 'galat: %s\n' "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case "$1" in
--uninstall) UNINSTALL=1 ;;
--no-service) WITH_SERVICE=0 ;;
--remove-data) REMOVE_DATA=1 ;;
--version) shift; VERSION="${1:?--version butuh nilai seperti v1.3.0}" ;;
-h|--help) sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "opsi tidak dikenal: $1" ;;
esac
shift
done
need() { command -v "$1" >/dev/null 2>&1 || die "perintah '$1' tidak tersedia"; }
# ---------------------------------------------------------------- uninstall
if [ "$UNINSTALL" = 1 ]; then
say "== melepaskan GAMEMAX LCD =="
command -v systemctl >/dev/null 2>&1 && systemctl --user disable --now "$SERVICE" 2>/dev/null || true
rm -f "$UNIT"
command -v systemctl >/dev/null 2>&1 && { systemctl --user daemon-reload 2>/dev/null || true; \
systemctl --user reset-failed "$SERVICE" 2>/dev/null || true; }
rm -rf "$OPT_DIR" "$DATA_DIR"
rm -f "$DESKTOP_DIR/gamemax-lcd.desktop" "$ICON_DIR/gamemax-lcd.png"
rm -f "$HOME/.config/autostart/iceburg-lcd.desktop"
[ "$REMOVE_DATA" = 1 ] && rm -rf "$HOME/.config/gamemax-report"
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
if [ -f "$RULE" ]; then
if sudo -n rm -f "$RULE" 2>/dev/null; then
sudo -n udevadm control --reload 2>/dev/null || true
say " aturan udev dihapus"
else
say " aturan udev masih ada (butuh sudo): sudo rm -f $RULE && sudo udevadm control --reload"
fi
fi
say "selesai. Aplikasi, layanan, dan berkas pendukung sudah dilepas."
say "catatan: konfigurasi ~/.config/gamemax-report hanya dihapus dengan --remove-data."
exit 0
fi
# ---------------------------------------------------------------- install
need curl
need sha256sum
command -v systemctl >/dev/null 2>&1 || die "systemd (systemctl) dibutuhkan untuk layanan"
if [ "$VERSION" = "latest" ]; then
VERSION="$(curl -fsSL "$INSTANCE/api/v1/repos/$REPO/releases/latest" | sed -n 's/.*"tag_name":"\([^"]*\)".*/\1/p')"
[ -n "$VERSION" ] || die "gagal menentukan rilis terbaru di Gitea"
fi
BASE="$INSTANCE/$REPO/releases/download/$VERSION"
say "== GAMEMAX LCD: pemasangan ($VERSION) =="
mkdir -p "$OPT_DIR" "$DATA_DIR" "$DESKTOP_DIR" "$ICON_DIR" "$HOME/.config/systemd/user"
say "1/6 mengunduh aplikasi …"
curl -fsSL -o "$APP.part" "$BASE/$ASSET"
curl -fsSL -o "$APP.sha256" "$BASE/$ASSET.sha256"
say "2/6 memverifikasi checksum …"
EXPECTED="$(awk '{print $1}' "$APP.sha256")"
ACTUAL="$(sha256sum "$APP.part" | awk '{print $1}')"
[ "$EXPECTED" = "$ACTUAL" ] || { rm -f "$APP.part"; die "checksum tidak cocok — unduhan dibatalkan"; }
say " ok: ${ACTUAL:0:16}…"
mv -f "$APP.part" "$APP"
chmod +x "$APP"
rm -f "$APP.sha256"
say "3/6 mengambil sender + ikon dari bundel …"
TMP="$(mktemp -d)"
( cd "$TMP" && "$APP" --appimage-extract >/dev/null 2>&1 ) || true
INTERNAL="$TMP/squashfs-root/usr/bin/_internal"
if [ -f "$INTERNAL/gamemax_report.py" ]; then
cp "$INTERNAL/gamemax_report.py" "$DATA_DIR/gamemax_report.py"
else
say " ! sender tidak ditemukan di bundel — unduh dari repo"
curl -fsSL -o "$DATA_DIR/gamemax_report.py" "$RAW/src/gamemax_report.py"
fi
[ -f "$INTERNAL/icon.png" ] && cp "$INTERNAL/icon.png" "$ICON_DIR/gamemax-lcd.png" \
|| curl -fsSL -o "$ICON_DIR/gamemax-lcd.png" "$RAW/src/icon.png"
rm -rf "$TMP"
say "4/6 memasang layanan systemd (autostart + plug and play) …"
cat > "$UNIT" <<EOF
[Unit]
Description=IceBurg LCD — HID sensor display bridge (Linux)
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 $DATA_DIR/gamemax_report.py --wait --interval 0.5 --sample 0.5
Restart=always
RestartSec=3
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
if [ "$WITH_SERVICE" = 1 ]; then
systemctl --user enable --now "$SERVICE"
sleep 2
say " layanan: $(systemctl --user is-active "$SERVICE") / $(systemctl --user is-enabled "$SERVICE")"
else
say " dilewati (--no-service)"
fi
say "5/6 memasang entri menu, ikon, dan autostart tray …"
cat > "$DESKTOP_DIR/gamemax-lcd.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=IceBurg LCD
GenericName=Kontrol LCD AIO
Comment=Status, nyala/mati, pemantauan, dan pengaturan LCD AIO IceBurg Digital
Exec="$APP"
Icon=gamemax-lcd
Terminal=false
Categories=System;Utility;
EOF
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
command -v gtk-update-icon-cache >/dev/null 2>&1 && \
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
mkdir -p "$HOME/.config/autostart"
cat > "$HOME/.config/autostart/iceburg-lcd.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=IceBurg LCD (tray)
Comment=Layar LCD AIO — berjalan di tray saat login (tanpa jendela)
Exec="$APP" --tray-only
Icon=iceburg-lcd
Terminal=false
X-GNOME-Autostart-enabled=true
EOF
say " autostart tray: ~/.config/autostart/iceburg-lcd.desktop (--tray-only)"
say "6/6 aturan udev (izin device) …"
RULE_TMP="$(mktemp)"
curl -fsSL -o "$RULE_TMP" "$RAW/packaging/70-gamemax.rules"
if [ -f "$RULE" ] && cmp -s "$RULE" "$RULE_TMP"; then
say " sudah terpasang"
elif sudo -n install -m 644 "$RULE_TMP" "$RULE" 2>/dev/null; then
sudo -n udevadm control --reload 2>/dev/null || true
sudo -n udevadm trigger --subsystem-match=hidraw 2>/dev/null || true
say " terpasang"
elif [ -t 0 ]; then
sudo install -m 644 "$RULE_TMP" "$RULE" && sudo udevadm control --reload && \
sudo udevadm trigger --subsystem-match=hidraw && say " terpasang"
else
say " ! butuh sudo — jalankan sekali:"
say " sudo install -m 644 $RULE_TMP $RULE && sudo udevadm control --reload && sudo udevadm trigger --subsystem-match=hidraw"
fi
rm -f "$RULE_TMP"
say
say "== selesai =="
say " aplikasi : $APP"
say " sender : $DATA_DIR/gamemax_report.py"
say " menu : GAMEMAX IceBurg LCD"
say " layanan : systemctl --user status $SERVICE"
say " log : journalctl --user -u $SERVICE -f"
say
say "buka aplikasinya untuk mengatur slot kipas/sumber suhu, atau:"
say " $APP --window"
+11 -5
View File
@@ -42,22 +42,28 @@ cp "$ROOT_DIR/src/icon.png" "$APPDIR/gamemax-lcd.png"
cat > "$APPDIR/gamemax-lcd.desktop" <<'EOF'
[Desktop Entry]
Type=Application
Name=GAMEMAX IceBurg LCD
GenericName=AI LCD controller
Comment=Status, on/off, monitor, and settings for the GAMEMAX IceBurg AIO LCD
Name=IceBurg LCD
GenericName=Kontrol LCD AIO
Comment=Status, nyala/mati, pemantauan, dan pengaturan LCD AIO IceBurg Digital
Exec=gamemax-lcd
Icon=gamemax-lcd
Terminal=false
Categories=System;Utility;
EOF
# 4. package
# 4. package (UPDATE_INFO_URL menyematkan info zsync agar appimageupdatetool bisa jalan)
UPDATE_INFO_ARG=""
if [ -n "${UPDATE_INFO_URL:-}" ]; then
UPDATE_INFO_ARG="-u zsync|${UPDATE_INFO_URL}"
echo "embedding update information: ${UPDATE_INFO_URL}"
fi
if [ ! -x "$BUILD_DIR/appimagetool" ]; then
curl -fsSL "$APPIMAGETOOL_URL" -o "$BUILD_DIR/appimagetool"
chmod +x "$BUILD_DIR/appimagetool"
fi
OUT="$OUT_DIR/GAMEMAX-LCD-x86_64.AppImage"
ARCH=x86_64 "$BUILD_DIR/appimagetool" "$APPDIR" "$OUT"
# shellcheck disable=SC2086
ARCH=x86_64 "$BUILD_DIR/appimagetool" $UPDATE_INFO_ARG "$APPDIR" "$OUT"
chmod +x "$OUT"
echo "built: $OUT"
echo "verify: $OUT --selftest (or APPIMAGE_EXTRACT_AND_RUN=1 $OUT --selftest)"
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit]
Description=GAMEMAX HID sensor display bridge (PcInfoMonitorPro replacement)
Description=IceBurg LCD — HID sensor display bridge (Linux)
Documentation=file:///mnt/SSD/Work/REVERSE-EGENERING/analysis/01-recon.md
After=graphical-session.target
+362 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""GAMEMAX IceBurg LCD — GUI tray: status, nyala/mati, pemantauan beranimasi, pengaturan.
"""IceBurg LCD — GUI tray: status, nyala/mati, pemantauan beranimasi, pengaturan.
Streaming tetap dijalankan oleh layanan systemd (gamemax-report.service); GUI ini
mengendalikannya dan memakai gamemax_report.py sebagai pengirim.
@@ -8,9 +8,14 @@ mengendalikannya dan memakai gamemax_report.py sebagai pengirim.
gamemax_gui.py --tray-only # tanpa jendela
gamemax_gui.py --selftest # uji muat tanpa tampilan (untuk build AppImage)
"""
import glob
import hashlib
import json
import math
import os
import re
import tempfile
import urllib.request
import shutil
import subprocess
import sys
@@ -18,27 +23,38 @@ import threading
import time
import traceback
from PySide6.QtCore import QPointF, QRectF, Qt, QThread, QTimer, Signal
from PySide6.QtGui import (QAction, QColor, QConicalGradient, QFont, QIcon,
QPainter, QPen, QPixmap)
from PySide6.QtCore import QPointF, QRectF, Qt, QThread, QTimer, QUrl, Signal
from PySide6.QtGui import (QAction, QColor, QConicalGradient, QDesktopServices, QFont,
QIcon, QPainter, QPen, QPixmap)
from PySide6.QtWidgets import (
QApplication, QCheckBox, QComboBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel,
QMenu, QPlainTextEdit, QProgressBar, QPushButton, QSystemTrayIcon, QVBoxLayout, QWidget,
QMenu, QMessageBox, QPlainTextEdit, QProgressBar, QPushButton, QSystemTrayIcon,
QVBoxLayout, QWidget,
)
from PySide6.QtNetwork import QLocalServer, QLocalSocket
APP_VERSION = "1.3.5"
APP_NAME = "IceBurg LCD"
REPO = "oxpoe/Gamemax-linux-apps"
RELEASES_API = f"https://gitea.oxpoe.qzz.io/api/v1/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
DONATE_URL = "https://donasi.oxpoe.qzz.io/"
REPO_URL = "https://gitea.oxpoe.qzz.io/oxpoe/Gamemax-linux-apps"
SERVICE = "gamemax-report.service"
CONFIG_PATH = gr.CONFIG_PATH
INSTALL_DIR = os.path.expanduser("~/.local/share/gamemax-report")
UNIT_PATH = os.path.expanduser(f"~/.config/systemd/user/{SERVICE}")
RULE_PATH = "/etc/udev/rules.d/70-gamemax.rules"
AUTOSTART_DIR = os.path.expanduser("~/.config/autostart")
AUTOSTART_FILE = os.path.join(AUTOSTART_DIR, "iceburg-lcd.desktop")
UNIT_TEMPLATE = """[Unit]
Description=GAMEMAX HID sensor display bridge (PcInfoMonitorPro replacement)
Description=IceBurg LCD — HID sensor display bridge (Linux)
After=graphical-session.target
[Service]
@@ -54,6 +70,34 @@ WantedBy=default.target
RULE_TEMPLATE = ('SUBSYSTEM=="hidraw", ATTRS{idVendor}=="5131", ATTRS{idProduct}=="2007", '
'TAG+="uaccess"\n')
APP_QSS = """
QWidget { background: #14121a; color: #ece6ff; font-size: 13px; }
QLabel#hint { color: #a99fc4; }
QGroupBox { border: 1px solid #2b2740; border-radius: 10px; margin-top: 14px;
padding: 12px 10px 8px 10px; font-weight: 600; }
QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 6px; color: #a99fc4; }
QPushButton { background: #242033; border: 1px solid #34304a; border-radius: 8px;
padding: 7px 13px; }
QPushButton:hover { background: #2c2740; }
QPushButton:pressed { background: #201c2e; }
QPushButton:disabled { color: #6c6486; border-color: #2a2740; }
QPushButton#primary { background: #def750; color: #14121a; border: none; font-weight: 700; }
QPushButton#primary:hover { background: #e9ff6a; }
QPushButton#primary:disabled { background: #4a5230; color: #2a2e20; }
QComboBox, QDoubleSpinBox { background: #1c1926; border: 1px solid #34304a;
border-radius: 8px; padding: 6px 8px; }
QComboBox QAbstractItemView { background: #1c1926; border: 1px solid #2b2740;
selection-background-color: #34304a; }
QPlainTextEdit { background: #1c1926; border: 1px solid #2b2740; border-radius: 10px;
padding: 6px; }
QProgressBar { background: #1c1926; border: 1px solid #2b2740; border-radius: 6px; }
QProgressBar::chunk { background: #def750; border-radius: 5px; }
QCheckBox { spacing: 8px; }
QCheckBox::indicator { width: 15px; height: 15px; border-radius: 4px;
border: 1px solid #6c6486; background: #1c1926; }
QCheckBox::indicator:checked { background: #def750; border-color: #def750; }
QMessageBox { background: #1b1826; }
"""
CRASH_LOG = os.path.join(os.path.dirname(CONFIG_PATH), "gui-crash.log")
ACCENT = QColor(222, 247, 80) # warna aksen GAMEMAX dari appConfig.ini
DIM = QColor(120, 128, 120)
@@ -69,6 +113,116 @@ 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 verify_sha256(path, sha_url):
"""True/False bila berkas checksum tersedia, None bila rilis tidak menyediakannya."""
try:
with urllib.request.urlopen(sha_url, timeout=10) as resp:
expected = resp.read().decode().split()[0].strip().lower()
except Exception: # noqa: BLE001
return None
if len(expected) != 64:
return None
actual = hashlib.sha256(open(path, "rb").read()).hexdigest()
return actual == expected
def latest_release():
"""(tag, url aset) dari rilis terbaru Gitea — dipanggil dari thread pekerja."""
ensure_ca_bundle()
req = urllib.request.Request(RELEASES_API, headers={
"Accept": "application/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)):
if parent is not None:
parent.log(f"tidak bisa membuka tautan: {url}")
def other_autostart_entries(appimage):
"""Entri autostart lain yang menjalankan AppImage yang sama (mis. buatan tool integrasi)."""
found = []
for path in glob.glob(os.path.join(AUTOSTART_DIR, "*.desktop")):
if path == AUTOSTART_FILE:
continue
try:
text = open(path).read()
except OSError:
continue
if appimage and appimage in text:
found.append(path)
return found
def set_tray_autostart(enable, appimage):
"""Autostart ikon tray saat login, tersembunyi (tanpa jendela)."""
if not appimage:
return "tidak berjalan dari AppImage — autostart tray dilewati"
os.makedirs(AUTOSTART_DIR, exist_ok=True)
notes = []
if enable:
with open(AUTOSTART_FILE, "w") as fh:
fh.write(f"""[Desktop Entry]
Type=Application
Name={APP_NAME} (tray)
Comment=Layar LCD AIO — berjalan di tray saat login (tanpa jendela)
Exec="{appimage}" --tray-only
Icon=iceburg-lcd
Terminal=false
X-GNOME-Autostart-enabled=true
""")
notes.append("autostart tray dibuat (--tray-only, tanpa jendela)")
for path in other_autostart_entries(appimage):
try:
text = open(path).read()
if "Hidden=true" not in text:
with open(path, "a") as fh:
fh.write("\nHidden=true\n")
notes.append(f"entri autostart lain dinonaktifkan: {os.path.basename(path)}")
except OSError:
pass
else:
if os.path.exists(AUTOSTART_FILE):
os.unlink(AUTOSTART_FILE)
notes.append("autostart tray dihapus")
for path in other_autostart_entries(appimage):
try:
lines = [l for l in open(path).read().splitlines() if l.strip() != "Hidden=true"]
with open(path, "w") as fh:
fh.write("\n".join(lines) + "\n")
notes.append(f"entri autostart lain diaktifkan kembali: {os.path.basename(path)}")
except OSError:
pass
return "; ".join(notes) or "tidak ada perubahan autostart tray"
def systemctl(*args):
return subprocess.run(["systemctl", "--user", *args], capture_output=True, text=True)
@@ -117,10 +271,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))
@@ -236,15 +392,20 @@ class Window(QWidget):
super().__init__()
self.tray = tray
self.prev_stat = gr.stat_snapshot()
self.setWindowTitle("GAMEMAX IceBurg LCD")
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
self.setWindowIcon(QIcon(icon_path()))
self.setMinimumWidth(640)
root = QVBoxLayout(self)
self._extra = {k: v for k, v in gr.load_config(CONFIG_PATH).items()
if k in ("close_action", "geometry")}
geom = self._extra.get("geometry")
if isinstance(geom, list) and len(geom) == 4:
self.setGeometry(*[int(g) for g in geom])
hint = QLabel("Kontrol layar LCD AIO GAMEMAX. Pilih slot kipas/sumber suhu di "
hint = QLabel("Kontrol layar LCD AIO IceBurg Digital. Pilih slot kipas/sumber suhu di "
"<b>Pengaturan</b> lalu klik <b>Simpan &amp; terapkan</b>.")
hint.setObjectName("hint")
hint.setWordWrap(True)
hint.setStyleSheet("color:#c8ccc8;")
root.addWidget(hint)
# ---- banner mode layar (jelas: normal / kosong / berhenti)
@@ -262,7 +423,7 @@ class Window(QWidget):
controls = QHBoxLayout()
self.toggle = QPushButton("Aktifkan layar")
self.toggle.setMinimumHeight(38)
self.toggle.setStyleSheet("font-weight: bold;")
self.toggle.setObjectName("primary")
self.toggle.clicked.connect(lambda: self.start_task("mengubah status layanan", self.on_toggle))
self.toggle.setToolTip("Mulai/hentikan pengiriman data sensor ke layar LCD")
controls.addWidget(self.toggle, 2)
@@ -336,6 +497,36 @@ class Window(QWidget):
form.addRow("", row)
root.addWidget(settings)
# ---- tautan
links = QHBoxLayout()
donate = QPushButton("❤ Donasi")
donate.setToolTip(DONATE_URL)
donate.clicked.connect(lambda: open_link(DONATE_URL, self))
repo_btn = QPushButton("Gitea")
repo_btn.setToolTip(REPO_URL)
repo_btn.clicked.connect(lambda: open_link(REPO_URL, self))
links.addWidget(donate)
links.addWidget(repo_btn)
links.addStretch(1)
copy_button = QPushButton("Salin report")
copy_button.setToolTip("Salin isi report 65 byte ke papan klip (untuk laporan bug)")
copy_button.clicked.connect(self.copy_report)
links.addWidget(copy_button)
self.check_button = QPushButton("Periksa pembaruan")
self.check_button.setToolTip("Bandingkan dengan rilis terbaru di Gitea")
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)
root.addLayout(links)
# ---- detail + log
self.detail = QPlainTextEdit()
self.detail.setReadOnly(True)
@@ -355,6 +546,10 @@ class Window(QWidget):
self.poll = QTimer(self, timeout=self.refresh, interval=1000)
self.poll.start()
self.refresh()
testing = any(f in sys.argv for f in ("--selftest", "--selftest-actions"))
if installed_appimage() and not testing: # 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' "
@@ -388,6 +583,7 @@ class Window(QWidget):
def config(self):
return {
**self._extra,
"fan_channel": self.fan.currentData() or None,
"temp_channel": self.temp.currentData() or None,
"unit": "c", # panel hanya punya label °C
@@ -397,6 +593,113 @@ class Window(QWidget):
"blank": bool(self.blank.isChecked()),
}
def copy_report(self):
QApplication.clipboard().setText(getattr(self, "_report_hex", ""))
self.log("isi report disalin ke papan klip")
def save_geometry(self):
g = self.geometry()
self._extra["geometry"] = [g.x(), g.y(), g.width(), g.height()]
try:
self.write_config(self.config())
except OSError:
pass
def closeEvent(self, event): # noqa: N802 (Qt)
if getattr(self, "_quitting", False):
self.save_geometry()
event.accept()
return
action = self._extra.get("close_action", "ask")
if action == "tray" and self.tray.available():
self.save_geometry()
self.hide()
event.ignore()
return
if action == "quit":
self.save_geometry()
self._quitting = True
event.accept()
QApplication.quit()
return
box = QMessageBox(self)
box.setWindowTitle(f"Tutup {APP_NAME}")
box.setText("Sembunyikan ke tray atau keluar sepenuhnya?")
box.setInformativeText("Selama tersembunyi di tray, data sensor tetap dikirim ke layar.")
remember = QCheckBox("Ingat pilihan ini")
box.setCheckBox(remember)
hide_btn = box.addButton("Sembunyikan ke tray", QMessageBox.AcceptRole)
quit_btn = box.addButton("Keluar", QMessageBox.DestructiveRole)
box.addButton("Batal", QMessageBox.RejectRole)
if not self.tray.available():
hide_btn.setEnabled(False)
box.setInformativeText("Tray tidak tersedia di desktop ini — menutup jendela akan "
"menghentikan aplikasi.")
box.exec()
clicked = box.clickedButton()
if clicked is hide_btn:
if remember.isChecked():
self._extra["close_action"] = "tray"
self.save_geometry()
self.hide()
event.ignore()
elif clicked is quit_btn:
if remember.isChecked():
self._extra["close_action"] = "quit"
self.save_geometry()
self._quitting = True
event.accept()
QApplication.quit()
else:
event.ignore()
# -------------------------------------------------------------- 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 Gitea"
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)
checked = verify_sha256(tmp, url + ".sha256")
if checked is False:
os.unlink(tmp)
raise RuntimeError("checksum SHA-256 tidak cocok — unduhan dibatalkan")
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)
note = "" if checked is None else " (checksum SHA-256 terverifikasi)"
if checked is None:
note = " (rilis ini tidak menyertakan checksum)"
return (f"diperbarui ke {tag}{note} — 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():
@@ -418,6 +721,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
@@ -441,8 +748,10 @@ class Window(QWidget):
systemctl("daemon-reload")
res = systemctl("enable" if checked else "disable", SERVICE)
state = service_enabled()
note = set_tray_autostart(checked, installed_appimage())
return (f"mulai otomatis saat login: {'AKTIF' if state else 'nonaktif'}"
+ ("" if res.returncode == 0 else f" ({res.stderr.strip()})"))
+ ("" if res.returncode == 0 else f" ({res.stderr.strip()})")
+ f" | {note}")
except Exception as exc: # noqa: BLE001
return f"galat autostart: {exc}"
@@ -562,6 +871,8 @@ class Window(QWidget):
def log(self, text):
self.logview.appendPlainText(text)
if os.environ.get("GAMEMAX_DEBUG"):
print(text, flush=True)
def warn_once(self, text):
if getattr(self, "_warned", None) != text:
@@ -598,7 +909,7 @@ class Window(QWidget):
"kosong": "Layar dikosongkan (nilai nol)",
"berhenti": "Pengiriman data dihentikan"}
if mode != "normal" or getattr(self, "_mode_seen", False):
subprocess.run(["notify-send", "-a", "GAMEMAX LCD", "Status layar",
subprocess.run(["notify-send", "-a", APP_NAME, "Status layar",
labels[mode]], capture_output=True)
self._mode_seen = True
dot = "🟢" if active else "⚪"
@@ -662,12 +973,23 @@ 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))
repo_action = QAction("Gitea", menu)
repo_action.triggered.connect(lambda: open_link(REPO_URL, self.window))
quit_action = QAction("Keluar", menu)
quit_action.triggered.connect(app.quit)
quit_action.triggered.connect(self.quit_app)
menu.addAction(show)
menu.addAction(toggle)
menu.addAction(self.blank_action)
menu.addSeparator()
menu.addAction(check_action)
menu.addAction(donate_action)
menu.addAction(repo_action)
menu.addSeparator()
menu.addAction(quit_action)
self.icon.setContextMenu(menu)
self.icon.activated.connect(
@@ -675,11 +997,18 @@ class Tray:
self.window = Window(self)
self.icon.show()
def available(self):
return QSystemTrayIcon.isSystemTrayAvailable()
def quit_app(self):
self.window._quitting = True
self.app.quit()
def sync(self, active):
blank = bool(gr.load_config(CONFIG_PATH).get("blank"))
state = ("layar kosong (nilai nol)" if blank and active
else ("pengiriman dihentikan" if not active else "normal"))
self.icon.setToolTip(f"GAMEMAX LCD: {state}")
self.icon.setToolTip(f"{APP_NAME}: {state}")
base = icon_path()
self.icon.setIcon(dimmed_icon(base) if state != "normal" else QIcon(base))
window = getattr(self, "window", None)
@@ -693,7 +1022,7 @@ class Tray:
self.window.raise_()
self.window.activateWindow()
if QSystemTrayIcon.isSystemTrayAvailable():
self.icon.showMessage("GAMEMAX LCD", "Jendela dibuka dari permintaan kedua.",
self.icon.showMessage(APP_NAME, "Jendela dibuka dari permintaan kedua.",
QSystemTrayIcon.Information, 3000)
@@ -710,7 +1039,8 @@ def main():
def _main():
app = QApplication(sys.argv)
app.setApplicationName("GAMEMAX LCD")
app.setApplicationName(APP_NAME)
app.setStyleSheet(APP_QSS)
app.setQuitOnLastWindowClosed(False)
# hanya satu instance: yang kedua memunculkan jendela yang sudah ada lalu keluar
@@ -719,10 +1049,10 @@ def _main():
if probe.waitForConnected(400):
probe.write(b"raise")
probe.waitForBytesWritten(400)
print("GAMEMAX LCD sudah berjalan — instance pertama diminta membuka jendelanya.")
print(f"{APP_NAME} sudah berjalan — instance pertama diminta membuka jendelanya.")
# Beri tahu lewat notifikasi desktop juga, karena keluarnya proses kedua bisa
# terlihat seperti crash saat diluncurkan dari menu.
subprocess.run(["notify-send", "-a", "GAMEMAX LCD", "Sudah berjalan",
subprocess.run(["notify-send", "-a", APP_NAME, "Sudah berjalan",
"Aplikasi sudah aktif — jendelanya dibuka, cek taskbar/tray."],
capture_output=True)
return 0
@@ -732,7 +1062,14 @@ def _main():
tray = Tray(app)
server.newConnection.connect(lambda: tray.show_window())
app.aboutToQuit.connect(lambda: (server.close(), QLocalServer.removeServer(SOCKET_NAME)))
def on_quit():
task = getattr(tray.window, "_task", None)
if task is not None and task.isRunning():
task.wait(3000)
server.close()
QLocalServer.removeServer(SOCKET_NAME)
app.aboutToQuit.connect(on_quit)
# Jendela selalu dibuka saat diluncurkan; tray tetap ada kalau didukung desktop.
tray.show_window()
if "--tray-only" in sys.argv:
@@ -758,11 +1095,12 @@ 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()
print(f"[ OK ] {name}")
out = fn()
print(f"[ OK ] {name}" + (f" — {out}" if isinstance(out, str) and out else ""))
except Exception as exc: # noqa: BLE001
print(f"[GAGAL] {name}: {exc}")
# jalur asinkron yang dipakai tombol: centang layar kosong + simpan
@@ -791,6 +1129,7 @@ def _main():
print("log:", " | ".join(w.logview.toPlainText().splitlines()[-6:]))
return 0
if "--selftest" in sys.argv:
tray.window._quitting = True # keluar otomatis: lewati dialog tutup
QTimer.singleShot(1200, app.quit)
rc = app.exec()
if "--selftest" in sys.argv:
+7 -3
View File
@@ -423,9 +423,10 @@ def main():
signal.signal(signal.SIGTERM, lambda *_: stop.set())
signal.signal(signal.SIGINT, lambda *_: stop.set())
print(f"menunggu {VID:#06x}:{PID:#06x} (Ctrl-C atau SIGTERM untuk berhenti)", flush=True)
last_message = None
while not stop.is_set():
if not find_device():
stop.wait(2)
stop.wait(1)
continue
try:
result = stream(a, overrides)
@@ -433,8 +434,11 @@ def main():
sent, dt = result
print(f"perangkat terlepas setelah {sent} report ({sent / dt:.1f}/detik), menunggu lagi", flush=True)
except OSError as exc:
print(f"galat perangkat ({exc}), menunggu lagi", flush=True)
stop.wait(2)
message = f"galat perangkat ({exc}), menunggu lagi"
if message != last_message: # ACL udev baru aktif setelah sesi login
print(message, flush=True)
last_message = message
stop.wait(1)
print("berhenti")
return 0