Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0353b71223 | ||
|
|
7f01abad25 | ||
|
|
4886cb570d | ||
|
|
66894ecdca | ||
|
|
d9b64a5ab5 | ||
|
|
3f73b97b39 | ||
|
|
323e270c51 | ||
|
|
3c06d802a9 | ||
|
|
76790e0321 | ||
|
|
c3d34a2788 | ||
|
|
4711ef5532 | ||
|
|
c384df7cac | ||
|
|
1f3222bc96 | ||
|
|
ac23195996 | ||
|
|
7156458c6f | ||
|
|
e0090fc1c3 | ||
|
|
a47b5c251c | ||
|
|
387e428406 | ||
|
|
93ce357900 | ||
|
|
53ee1c5102 | ||
|
|
93bad1f461 | ||
|
|
657a8d10dc |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
|
||||
[](https://github.com/oxpoe/Gamemax-linux-apps/releases)
|
||||
[](https://gitea.oxpoe.qzz.io/oxpoe/Gamemax-linux-apps/releases)
|
||||
[](https://gitea.oxpoe.qzz.io/oxpoe/Gamemax-linux-apps/actions/workflows/release.yml)
|
||||
[](LICENSE)
|
||||
[](#compatibility)
|
||||
[](src/gamemax_report.py)
|
||||
[](src/gamemax_report.py)
|
||||
[](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
@@ -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
@@ -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"
|
||||
@@ -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,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
@@ -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 & 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user