8 Commits

Author SHA1 Message Date
0e54616000 fix: use msiextract for tkinter, fix step numbering, graceful Docker fallback
Some checks failed
Build Release / create-release (push) Successful in 0s
Lint & Test / test (push) Failing after 14s
Build Release / build-linux (push) Successful in 1m27s
Build Release / build-docker (push) Successful in 6s
Build Release / build-windows (push) Successful in 3m26s
- Replace 7z with msiextract (msitools) to extract tkinter from
  python.org's individual MSI packages (tcltk.msi + lib.msi)
- Fix build step numbering to /9
- Docker job continues on login failure (registry may not be enabled)
- Show makensis output for debugging
2026-03-22 03:44:37 +03:00
3633793972 fix: extract tkinter from Python installer via 7z, fix NSIS icon path
Some checks failed
Build Release / create-release (push) Successful in 1s
Lint & Test / test (push) Failing after 15s
Build Release / build-linux (push) Successful in 1m20s
Build Release / build-docker (push) Failing after 9s
Build Release / build-windows (push) Successful in 3m19s
- Replace nuget approach (doesn't contain tkinter) with extracting
  from the official Python amd64.exe installer using 7z
- Remove MUI_ICON/MUI_UNICON (no .ico file available, use NSIS default)
- Add p7zip-full to CI dependencies
2026-03-22 03:40:06 +03:00
7f799a914d feat: add NSIS Windows installer to release workflow
Some checks failed
Build Release / create-release (push) Successful in 1s
Lint & Test / test (push) Failing after 15s
Build Release / build-linux (push) Successful in 1m21s
Build Release / build-docker (push) Failing after 9s
Build Release / build-windows (push) Failing after 1m37s
- installer.nsi: per-user install to AppData, Start Menu shortcuts,
  optional desktop shortcut and autostart, clean uninstall (preserves
  data/), Add/Remove Programs registration
- build-dist-windows.sh: runs makensis after ZIP if available
- release.yml: install nsis in CI, upload both ZIP and setup.exe
- Fix Docker registry login (sed -E for https:// stripping)
2026-03-22 03:35:34 +03:00
d5b5c255e8 feat: bundle tkinter into Windows portable build
Some checks failed
Build Release / create-release (push) Successful in 5s
Lint & Test / test (push) Failing after 21s
Build Release / build-linux (push) Successful in 1m7s
Build Release / build-docker (push) Failing after 4s
Build Release / build-windows (push) Successful in 1m50s
Download _tkinter.pyd, tkinter package, and Tcl/Tk DLLs from the
official Python nuget package and copy them into the embedded Python
directory. This enables the screen overlay visualization during
calibration in the portable build.
2026-03-22 03:32:02 +03:00
564e4c9c9c fix: accurate port banner and tkinter graceful fallback
Some checks failed
Build Release / build-linux (push) Successful in 1m24s
Build Release / create-release (push) Successful in 1s
Lint & Test / test (push) Failing after 13s
Build Release / build-windows (push) Successful in 1m35s
Build Release / build-docker (push) Failing after 9s
- Move startup banner into main.py so it shows the actual configured
  port instead of a hardcoded 8080 in the launcher scripts
- Wrap tkinter import in try/except so embedded Python (which lacks
  tkinter) logs a warning instead of crashing the overlay thread
2026-03-22 03:30:19 +03:00
7c80500d48 feat: add autostart scripts and fix port configuration in launchers
Some checks failed
Build Release / create-release (push) Successful in 1s
Build Release / build-windows (push) Successful in 1m16s
Build Release / build-docker (push) Failing after 8s
Build Release / build-linux (push) Successful in 59s
Lint & Test / test (push) Successful in 1m52s
Windows: install-autostart.bat (Startup folder shortcut),
uninstall-autostart.bat. Linux: install-service.sh (systemd unit),
uninstall-service.sh.

Both launchers now use python -m wled_controller.main so port is
read from config/env instead of being hardcoded to 8080.
2026-03-22 03:25:05 +03:00
39e3d64654 fix: replace Docker Buildx with plain docker build/push
All checks were successful
Lint & Test / test (push) Successful in 1m51s
Buildx requires container networking that fails on TrueNAS runners.
Plain docker build + docker push works without Buildx setup.
2026-03-22 03:20:37 +03:00
47a62b1aed fix: add app/src to embedded Python ._pth for module discovery
Some checks failed
Build Release / create-release (push) Successful in 1s
Build Release / build-windows (push) Successful in 1m14s
Lint & Test / test (push) Successful in 1m50s
Build Release / build-docker (push) Failing after 46s
Build Release / build-linux (push) Successful in 1m37s
Windows embedded Python ignores PYTHONPATH when a ._pth file exists.
Add ../app/src to the ._pth so wled_controller is importable.
Fixes ModuleNotFoundError on portable builds.
2026-03-22 03:14:12 +03:00
7 changed files with 489 additions and 71 deletions

View File

@@ -63,37 +63,50 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends zip libportaudio2
sudo apt-get install -y --no-install-recommends zip libportaudio2 nsis msitools
- name: Cross-build Windows distribution
run: |
chmod +x build-dist-windows.sh
./build-dist-windows.sh "${{ gitea.ref_name }}"
- name: Upload build artifact
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: LedGrab-${{ gitea.ref_name }}-win-x64
path: build/LedGrab-*.zip
path: |
build/LedGrab-*.zip
build/LedGrab-*-setup.exe
retention-days: 90
- name: Attach ZIP to release
- name: Attach assets to release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
TAG="${{ gitea.ref_name }}"
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
ZIP_FILE=$(ls build/LedGrab-*.zip | head -1)
ZIP_NAME=$(basename "$ZIP_FILE")
# Upload ZIP
ZIP_FILE=$(ls build/LedGrab-*.zip | head -1)
if [ -f "$ZIP_FILE" ]; then
curl -s -X POST \
"$BASE_URL/releases/$RELEASE_ID/assets?name=$ZIP_NAME" \
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$ZIP_FILE")" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$ZIP_FILE"
echo "Uploaded: $(basename "$ZIP_FILE")"
fi
echo "Uploaded: $ZIP_NAME"
# Upload installer
SETUP_FILE=$(ls build/LedGrab-*-setup.exe 2>/dev/null | head -1)
if [ -f "$SETUP_FILE" ]; then
curl -s -X POST \
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$SETUP_FILE")" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$SETUP_FILE"
echo "Uploaded: $(basename "$SETUP_FILE")"
fi
# ── Linux tarball ──────────────────────────────────────────
build-linux:
@@ -160,43 +173,55 @@ jobs:
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ gitea.server_url }}
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Extract version metadata
id: meta
run: |
TAG="${{ gitea.ref_name }}"
VERSION="${TAG#v}"
REGISTRY="${{ gitea.server_url }}/${{ gitea.repository }}"
# Lowercase the registry path (Docker requires it)
REGISTRY=$(echo "$REGISTRY" | tr '[:upper:]' '[:lower:]' | sed 's|https\?://||')
# Strip protocol and lowercase for Docker registry path
SERVER_HOST=$(echo "${{ gitea.server_url }}" | sed -E 's|https?://||')
REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
REGISTRY="${SERVER_HOST}/${REPO}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT"
echo "server_host=$SERVER_HOST" >> "$GITHUB_OUTPUT"
# Build tag list: version + latest (only for stable releases)
TAGS="$REGISTRY:$TAG,$REGISTRY:$VERSION"
- name: Login to Gitea Container Registry
id: docker-login
continue-on-error: true
run: |
echo "${{ secrets.GITEA_TOKEN }}" | docker login \
"${{ steps.meta.outputs.server_host }}" \
-u "${{ gitea.actor }}" --password-stdin
- name: Build Docker image
if: steps.docker-login.outcome == 'success'
run: |
TAG="${{ gitea.ref_name }}"
REGISTRY="${{ steps.meta.outputs.registry }}"
docker build \
--label "org.opencontainers.image.version=${{ steps.meta.outputs.version }}" \
--label "org.opencontainers.image.revision=${{ gitea.sha }}" \
-t "$REGISTRY:$TAG" \
-t "$REGISTRY:${{ steps.meta.outputs.version }}" \
./server
# Tag as latest only for stable releases
if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
TAGS="$TAGS,$REGISTRY:latest"
docker tag "$REGISTRY:$TAG" "$REGISTRY:latest"
fi
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./server
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: |
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
org.opencontainers.image.revision=${{ gitea.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Push Docker image
if: steps.docker-login.outcome == 'success'
run: |
TAG="${{ gitea.ref_name }}"
REGISTRY="${{ steps.meta.outputs.registry }}"
docker push "$REGISTRY:$TAG"
docker push "$REGISTRY:${{ steps.meta.outputs.version }}"
if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
docker push "$REGISTRY:latest"
fi

View File

@@ -47,7 +47,7 @@ echo ""
# ── Clean ────────────────────────────────────────────────────
if [ -d "$DIST_DIR" ]; then
echo "[1/8] Cleaning previous build..."
echo "[1/9] Cleaning previous build..."
rm -rf "$DIST_DIR"
fi
mkdir -p "$DIST_DIR"
@@ -57,7 +57,7 @@ mkdir -p "$DIST_DIR"
PYTHON_ZIP_URL="https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip"
PYTHON_ZIP_PATH="$BUILD_DIR/python-embed-win.zip"
echo "[2/8] Downloading Windows embedded Python ${PYTHON_VERSION}..."
echo "[2/9] Downloading Windows embedded Python ${PYTHON_VERSION}..."
if [ ! -f "$PYTHON_ZIP_PATH" ]; then
curl -sL "$PYTHON_ZIP_URL" -o "$PYTHON_ZIP_PATH"
fi
@@ -66,23 +66,105 @@ unzip -qo "$PYTHON_ZIP_PATH" -d "$PYTHON_DIR"
# ── Patch ._pth to enable site-packages ──────────────────────
echo "[3/8] Patching Python path configuration..."
echo "[3/9] Patching Python path configuration..."
PTH_FILE=$(ls "$PYTHON_DIR"/python*._pth 2>/dev/null | head -1)
if [ -z "$PTH_FILE" ]; then
echo "ERROR: Could not find python*._pth in $PYTHON_DIR" >&2
exit 1
fi
# Uncomment 'import site' and add Lib\site-packages
# Uncomment 'import site', add Lib\site-packages and app source path
sed -i 's/^#\s*import site/import site/' "$PTH_FILE"
if ! grep -q 'Lib\\site-packages' "$PTH_FILE"; then
echo 'Lib\site-packages' >> "$PTH_FILE"
fi
# Embedded Python ._pth overrides PYTHONPATH, so we must add the app
# source directory here for wled_controller to be importable
if ! grep -q '\.\./app/src' "$PTH_FILE"; then
echo '../app/src' >> "$PTH_FILE"
fi
echo " Patched $(basename "$PTH_FILE")"
# ── Bundle tkinter into embedded Python ───────────────────────
# Embedded Python doesn't include tkinter. We download the individual
# MSI packages from python.org (tcltk.msi + lib.msi) and extract them
# using msiextract (from msitools).
echo "[4/9] Bundling tkinter for screen overlay support..."
TK_EXTRACT="$BUILD_DIR/tk-extract"
rm -rf "$TK_EXTRACT"
mkdir -p "$TK_EXTRACT"
MSI_BASE="https://www.python.org/ftp/python/${PYTHON_VERSION}/amd64"
# Download tcltk.msi (contains _tkinter.pyd, tcl/tk DLLs, tcl8.6/, tk8.6/)
TCLTK_MSI="$BUILD_DIR/tcltk.msi"
if [ ! -f "$TCLTK_MSI" ]; then
curl -sL "$MSI_BASE/tcltk.msi" -o "$TCLTK_MSI"
fi
# Download lib.msi (contains tkinter/ Python package in the stdlib)
LIB_MSI="$BUILD_DIR/lib.msi"
if [ ! -f "$LIB_MSI" ]; then
curl -sL "$MSI_BASE/lib.msi" -o "$LIB_MSI"
fi
if command -v msiextract &>/dev/null; then
# Extract both MSIs
(cd "$TK_EXTRACT" && msiextract "$TCLTK_MSI" 2>/dev/null)
(cd "$TK_EXTRACT" && msiextract "$LIB_MSI" 2>/dev/null)
# Copy _tkinter.pyd
TKINTER_PYD=$(find "$TK_EXTRACT" -name "_tkinter.pyd" 2>/dev/null | head -1)
if [ -n "$TKINTER_PYD" ]; then
cp "$TKINTER_PYD" "$PYTHON_DIR/"
echo " Copied _tkinter.pyd"
else
echo " WARNING: _tkinter.pyd not found in tcltk.msi"
fi
# Copy Tcl/Tk DLLs
for dll in tcl86t.dll tk86t.dll; do
DLL_PATH=$(find "$TK_EXTRACT" -name "$dll" 2>/dev/null | head -1)
if [ -n "$DLL_PATH" ]; then
cp "$DLL_PATH" "$PYTHON_DIR/"
echo " Copied $dll"
fi
done
# Copy tkinter Python package
TKINTER_PKG=$(find "$TK_EXTRACT" -type d -name "tkinter" 2>/dev/null | head -1)
if [ -n "$TKINTER_PKG" ]; then
mkdir -p "$PYTHON_DIR/Lib"
cp -r "$TKINTER_PKG" "$PYTHON_DIR/Lib/tkinter"
echo " Copied tkinter/ package"
fi
# Copy tcl/tk data directories
for tcldir in tcl8.6 tk8.6; do
TCL_PATH=$(find "$TK_EXTRACT" -type d -name "$tcldir" 2>/dev/null | head -1)
if [ -n "$TCL_PATH" ]; then
cp -r "$TCL_PATH" "$PYTHON_DIR/$tcldir"
echo " Copied $tcldir/"
fi
done
echo " tkinter bundled successfully"
else
echo " WARNING: msiextract not found — skipping tkinter (install msitools)"
fi
# Add Lib to ._pth so tkinter package is importable
if ! grep -q '^Lib$' "$PTH_FILE"; then
echo 'Lib' >> "$PTH_FILE"
fi
rm -rf "$TK_EXTRACT"
# ── Download pip and install into embedded Python ────────────
echo "[4/8] Installing pip into embedded Python..."
echo "[5/9] Installing pip into embedded Python..."
SITE_PACKAGES="$PYTHON_DIR/Lib/site-packages"
mkdir -p "$SITE_PACKAGES"
@@ -104,7 +186,7 @@ done
# ── Download Windows wheels for all dependencies ─────────────
echo "[5/8] Downloading Windows dependencies..."
echo "[6/9] Downloading Windows dependencies..."
WHEEL_DIR="$BUILD_DIR/win-wheels"
mkdir -p "$WHEEL_DIR"
@@ -207,14 +289,14 @@ echo " Installed $WHEEL_COUNT packages"
# ── Build frontend ───────────────────────────────────────────
echo "[6/8] Building frontend bundle..."
echo "[7/9] Building frontend bundle..."
(cd "$SERVER_DIR" && npm ci --loglevel error && npm run build) 2>&1 | {
grep -v 'RemoteException' || true
}
# ── Copy application files ───────────────────────────────────
echo "[7/8] Copying application files..."
echo "[8/9] Copying application files..."
mkdir -p "$APP_DIR"
cp -r "$SERVER_DIR/src" "$APP_DIR/src"
@@ -227,7 +309,7 @@ find "$APP_DIR" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
# ── Create launcher ──────────────────────────────────────────
echo "[8/8] Creating launcher and packaging..."
echo "[8b/9] Creating launcher and packaging..."
cat > "$DIST_DIR/LedGrab.bat" << LAUNCHER
@echo off
@@ -242,16 +324,8 @@ set WLED_CONFIG_PATH=%~dp0app\config\default_config.yaml
if not exist "%~dp0data" mkdir "%~dp0data"
if not exist "%~dp0logs" mkdir "%~dp0logs"
echo.
echo =============================================
echo LedGrab v${VERSION_CLEAN}
echo Open http://localhost:8080 in your browser
echo =============================================
echo.
:: Start the server (open browser after short delay)
start "" /b cmd /c "timeout /t 2 /nobreak >nul && start http://localhost:8080"
"%~dp0python\python.exe" -m uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080
:: Start the server — reads port from config, prints its own banner
"%~dp0python\python.exe" -m wled_controller.main
pause
LAUNCHER
@@ -259,6 +333,64 @@ LAUNCHER
# Convert launcher to Windows line endings
sed -i 's/$/\r/' "$DIST_DIR/LedGrab.bat"
# ── Create autostart scripts ─────────────────────────────────
cat > "$DIST_DIR/install-autostart.bat" << 'AUTOSTART'
@echo off
:: Install LedGrab to start automatically on Windows login
:: Creates a shortcut in the Startup folder
set SHORTCUT_NAME=LedGrab
set STARTUP_DIR=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup
set TARGET=%~dp0LedGrab.bat
set SHORTCUT=%STARTUP_DIR%\%SHORTCUT_NAME%.lnk
echo Installing LedGrab autostart...
:: Use PowerShell to create a proper shortcut
powershell -NoProfile -Command ^
"$ws = New-Object -ComObject WScript.Shell; ^
$sc = $ws.CreateShortcut('%SHORTCUT%'); ^
$sc.TargetPath = '%TARGET%'; ^
$sc.WorkingDirectory = '%~dp0'; ^
$sc.WindowStyle = 7; ^
$sc.Description = 'LedGrab ambient lighting server'; ^
$sc.Save()"
if exist "%SHORTCUT%" (
echo.
echo [OK] LedGrab will start automatically on login.
echo Shortcut: %SHORTCUT%
echo.
echo To remove: run uninstall-autostart.bat
) else (
echo.
echo [ERROR] Failed to create shortcut.
)
pause
AUTOSTART
sed -i 's/$/\r/' "$DIST_DIR/install-autostart.bat"
cat > "$DIST_DIR/uninstall-autostart.bat" << 'UNAUTOSTART'
@echo off
:: Remove LedGrab from Windows startup
set SHORTCUT=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\LedGrab.lnk
if exist "%SHORTCUT%" (
del "%SHORTCUT%"
echo.
echo [OK] LedGrab autostart removed.
) else (
echo.
echo LedGrab autostart was not installed.
)
pause
UNAUTOSTART
sed -i 's/$/\r/' "$DIST_DIR/uninstall-autostart.bat"
# ── Create ZIP ───────────────────────────────────────────────
ZIP_PATH="$BUILD_DIR/$ZIP_NAME"
@@ -267,8 +399,29 @@ rm -f "$ZIP_PATH"
(cd "$BUILD_DIR" && zip -rq "$ZIP_NAME" "$DIST_NAME")
ZIP_SIZE=$(du -h "$ZIP_PATH" | cut -f1)
# ── Build NSIS installer (if makensis is available) ──────────
SETUP_NAME="LedGrab-v${VERSION_CLEAN}-win-x64-setup.exe"
SETUP_PATH="$BUILD_DIR/$SETUP_NAME"
if command -v makensis &>/dev/null; then
echo "[9/9] Building NSIS installer..."
makensis -DVERSION="${VERSION_CLEAN}" "$SCRIPT_DIR/installer.nsi"
if [ -f "$SETUP_PATH" ]; then
SETUP_SIZE=$(du -h "$SETUP_PATH" | cut -f1)
echo " Installer: $SETUP_PATH ($SETUP_SIZE)"
else
echo " WARNING: makensis ran but installer not found at $SETUP_PATH"
fi
else
echo "[9/9] Skipping installer (makensis not found — install nsis to enable)"
fi
echo ""
echo "=== Build complete ==="
echo " Archive: $ZIP_PATH"
echo " Size: $ZIP_SIZE"
echo " ZIP: $ZIP_PATH ($ZIP_SIZE)"
if [ -f "$SETUP_PATH" ]; then
echo " Installer: $SETUP_PATH ($SETUP_SIZE)"
fi
echo ""

View File

@@ -106,6 +106,11 @@ $pthContent = $pthContent -replace '#\s*import site', 'import site'
if ($pthContent -notmatch 'Lib\\site-packages') {
$pthContent = $pthContent.TrimEnd() + "`nLib\site-packages`n"
}
# Embedded Python ._pth overrides PYTHONPATH, so add the app source path
# directly for wled_controller to be importable
if ($pthContent -notmatch '\.\.[/\\]app[/\\]src') {
$pthContent = $pthContent.TrimEnd() + "`n..\app\src`n"
}
Set-Content -Path $pthFile.FullName -Value $pthContent -NoNewline
Write-Host " Patched $($pthFile.Name)"

View File

@@ -103,20 +103,100 @@ export WLED_CONFIG_PATH="$SCRIPT_DIR/app/config/default_config.yaml"
mkdir -p "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs"
echo ""
echo " ============================================="
echo " LedGrab vVERSION_PLACEHOLDER"
echo " Open http://localhost:8080 in your browser"
echo " ============================================="
echo ""
source "$SCRIPT_DIR/venv/bin/activate"
exec python -m uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080
exec python -m wled_controller.main
LAUNCHER
sed -i "s/VERSION_PLACEHOLDER/${VERSION_CLEAN}/" "$DIST_DIR/run.sh"
chmod +x "$DIST_DIR/run.sh"
# ── Create autostart scripts ─────────────────────────────────
cat > "$DIST_DIR/install-service.sh" << 'SERVICE_INSTALL'
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SERVICE_NAME="ledgrab"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
RUN_SCRIPT="$SCRIPT_DIR/run.sh"
CURRENT_USER="$(whoami)"
if [ "$EUID" -ne 0 ] && [ "$CURRENT_USER" != "root" ]; then
echo "This script requires root privileges. Re-running with sudo..."
exec sudo "$0" "$@"
fi
# Resolve the actual user (not root) when run via sudo
ACTUAL_USER="${SUDO_USER:-$CURRENT_USER}"
ACTUAL_HOME=$(eval echo "~$ACTUAL_USER")
echo "Installing LedGrab systemd service..."
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=LedGrab ambient lighting server
After=network.target
[Service]
Type=simple
User=$ACTUAL_USER
WorkingDirectory=$SCRIPT_DIR
ExecStart=$RUN_SCRIPT
Restart=on-failure
RestartSec=5
Environment=HOME=$ACTUAL_HOME
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
systemctl start "$SERVICE_NAME"
echo ""
echo " [OK] LedGrab service installed and started."
echo ""
echo " Commands:"
echo " sudo systemctl status $SERVICE_NAME # Check status"
echo " sudo systemctl stop $SERVICE_NAME # Stop"
echo " sudo systemctl restart $SERVICE_NAME # Restart"
echo " sudo journalctl -u $SERVICE_NAME -f # View logs"
echo ""
echo " To remove: run ./uninstall-service.sh"
SERVICE_INSTALL
chmod +x "$DIST_DIR/install-service.sh"
cat > "$DIST_DIR/uninstall-service.sh" << 'SERVICE_UNINSTALL'
#!/usr/bin/env bash
set -euo pipefail
SERVICE_NAME="ledgrab"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
if [ "$EUID" -ne 0 ] && [ "$(whoami)" != "root" ]; then
echo "This script requires root privileges. Re-running with sudo..."
exec sudo "$0" "$@"
fi
if [ ! -f "$SERVICE_FILE" ]; then
echo "LedGrab service is not installed."
exit 0
fi
echo "Removing LedGrab systemd service..."
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
systemctl disable "$SERVICE_NAME" 2>/dev/null || true
rm -f "$SERVICE_FILE"
systemctl daemon-reload
echo ""
echo " [OK] LedGrab service removed."
SERVICE_UNINSTALL
chmod +x "$DIST_DIR/uninstall-service.sh"
# ── Create tarball ───────────────────────────────────────────
echo "[7/7] Creating $TAR_NAME..."

146
installer.nsi Normal file
View File

@@ -0,0 +1,146 @@
; LedGrab NSIS Installer Script
; Cross-compilable on Linux: apt install nsis && makensis installer.nsi
;
; Expects the portable build to already exist at build/LedGrab/
; (run build-dist-windows.sh first)
!include "MUI2.nsh"
!include "FileFunc.nsh"
; ── Metadata ────────────────────────────────────────────────
!define APPNAME "LedGrab"
!define DESCRIPTION "Ambient lighting system — captures screen content and drives LED strips in real time"
!define VERSIONMAJOR 0
!define VERSIONMINOR 1
!define VERSIONBUILD 0
; Set from command line: makensis -DVERSION=0.1.0 installer.nsi
!ifndef VERSION
!define VERSION "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
!endif
Name "${APPNAME} v${VERSION}"
OutFile "build\${APPNAME}-v${VERSION}-win-x64-setup.exe"
InstallDir "$LOCALAPPDATA\${APPNAME}"
InstallDirRegKey HKCU "Software\${APPNAME}" "InstallDir"
RequestExecutionLevel user
SetCompressor /SOLID lzma
; ── Modern UI Configuration ─────────────────────────────────
!define MUI_ABORTWARNING
; ── Pages ───────────────────────────────────────────────────
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
; ── Installer Sections ──────────────────────────────────────
Section "!${APPNAME} (required)" SecCore
SectionIn RO
SetOutPath "$INSTDIR"
; Copy the entire portable build
File /r "build\LedGrab\python"
File /r "build\LedGrab\app"
File "build\LedGrab\LedGrab.bat"
; Create data and logs directories
CreateDirectory "$INSTDIR\data"
CreateDirectory "$INSTDIR\logs"
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
; Start Menu shortcuts
CreateDirectory "$SMPROGRAMS\${APPNAME}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\LedGrab.bat" \
"" "" "" SW_SHOWMINIMIZED "" "${DESCRIPTION}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
; Registry: install location + Add/Remove Programs entry
WriteRegStr HKCU "Software\${APPNAME}" "InstallDir" "$INSTDIR"
WriteRegStr HKCU "Software\${APPNAME}" "Version" "${VERSION}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"DisplayName" "${APPNAME}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"DisplayVersion" "${VERSION}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"InstallLocation" "$INSTDIR"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"Publisher" "Alexei Dolgolyov"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"URLInfoAbout" "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"NoModify" 1
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"NoRepair" 1
; Calculate installed size for Add/Remove Programs
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"EstimatedSize" "$0"
SectionEnd
Section "Desktop shortcut" SecDesktop
CreateShortcut "$DESKTOP\${APPNAME}.lnk" "$INSTDIR\LedGrab.bat" \
"" "" "" SW_SHOWMINIMIZED "" "${DESCRIPTION}"
SectionEnd
Section "Start with Windows" SecAutostart
CreateShortcut "$SMSTARTUP\${APPNAME}.lnk" "$INSTDIR\LedGrab.bat" \
"" "" "" SW_SHOWMINIMIZED "" "${DESCRIPTION}"
SectionEnd
; ── Section Descriptions ────────────────────────────────────
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SecCore} \
"Install ${APPNAME} server and all required files."
!insertmacro MUI_DESCRIPTION_TEXT ${SecDesktop} \
"Create a shortcut on your desktop."
!insertmacro MUI_DESCRIPTION_TEXT ${SecAutostart} \
"Start ${APPNAME} automatically when you log in."
!insertmacro MUI_FUNCTION_DESCRIPTION_END
; ── Uninstaller ─────────────────────────────────────────────
Section "Uninstall"
; Remove shortcuts
Delete "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk"
Delete "$SMPROGRAMS\${APPNAME}\Uninstall.lnk"
RMDir "$SMPROGRAMS\${APPNAME}"
Delete "$DESKTOP\${APPNAME}.lnk"
Delete "$SMSTARTUP\${APPNAME}.lnk"
; Remove application files (but NOT data/ — preserve user config)
RMDir /r "$INSTDIR\python"
RMDir /r "$INSTDIR\app"
Delete "$INSTDIR\LedGrab.bat"
Delete "$INSTDIR\uninstall.exe"
; Remove logs (but keep data/)
RMDir /r "$INSTDIR\logs"
; Try to remove install dir (only succeeds if empty — data/ may remain)
RMDir "$INSTDIR"
; Remove registry keys
DeleteRegKey HKCU "Software\${APPNAME}"
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
SectionEnd

View File

@@ -278,7 +278,12 @@ class OverlayManager:
def _start_tk_thread(self) -> None:
def _run():
import tkinter as tk # lazy import — tkinter unavailable in headless CI
try:
import tkinter as tk # lazy import — tkinter unavailable in embedded Python / headless CI
except ImportError:
logger.warning("tkinter not available — screen overlay disabled")
self._tk_ready.set()
return
try:
self._tk_root = tk.Tk()

View File

@@ -98,6 +98,10 @@ async def lifespan(app: FastAPI):
logger.info(f"Starting LED Grab v{__version__}")
logger.info(f"Python version: {sys.version}")
logger.info(f"Server listening on {config.server.host}:{config.server.port}")
print(f"\n =============================================")
print(f" LED Grab v{__version__}")
print(f" Open http://localhost:{config.server.port} in your browser")
print(f" =============================================\n")
# Validate authentication configuration
if not config.auth.api_keys: