5 Commits

Author SHA1 Message Date
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
6 changed files with 270 additions and 47 deletions

View File

@@ -160,16 +160,6 @@ jobs:
with: with:
fetch-depth: 0 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 - name: Extract version metadata
id: meta id: meta
run: | run: |
@@ -182,21 +172,37 @@ jobs:
echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT" echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT"
# Build tag list: version + latest (only for stable releases) - name: Login to Gitea Container Registry
TAGS="$REGISTRY:$TAG,$REGISTRY:$VERSION" run: |
if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then echo "${{ secrets.GITEA_TOKEN }}" | docker login \
TAGS="$TAGS,$REGISTRY:latest" "$(echo '${{ gitea.server_url }}' | sed 's|https\?://||')" \
fi -u "${{ gitea.actor }}" --password-stdin
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
- name: Build and push Docker image - name: Build Docker image
uses: docker/build-push-action@v5 run: |
with: TAG="${{ gitea.ref_name }}"
context: ./server REGISTRY="${{ steps.meta.outputs.registry }}"
push: true
tags: ${{ steps.meta.outputs.tags }} docker build \
labels: | --label "org.opencontainers.image.version=${{ steps.meta.outputs.version }}" \
org.opencontainers.image.version=${{ steps.meta.outputs.version }} --label "org.opencontainers.image.revision=${{ gitea.sha }}" \
org.opencontainers.image.revision=${{ gitea.sha }} -t "$REGISTRY:$TAG" \
cache-from: type=gha -t "$REGISTRY:${{ steps.meta.outputs.version }}" \
cache-to: type=gha,mode=max ./server
# Tag as latest only for stable releases
if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
docker tag "$REGISTRY:$TAG" "$REGISTRY:latest"
fi
- name: Push Docker image
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

@@ -73,13 +73,86 @@ if [ -z "$PTH_FILE" ]; then
exit 1 exit 1
fi 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" sed -i 's/^#\s*import site/import site/' "$PTH_FILE"
if ! grep -q 'Lib\\site-packages' "$PTH_FILE"; then if ! grep -q 'Lib\\site-packages' "$PTH_FILE"; then
echo 'Lib\site-packages' >> "$PTH_FILE" echo 'Lib\site-packages' >> "$PTH_FILE"
fi 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")" echo " Patched $(basename "$PTH_FILE")"
# ── Bundle tkinter into embedded Python ───────────────────────
# Embedded Python doesn't include tkinter. We download it from the
# official Windows Python nuget package (same version) which contains
# the _tkinter.pyd, tkinter/ package, and Tcl/Tk DLLs.
echo "[3b/8] Bundling tkinter for screen overlay support..."
# Python minor version for nuget package (e.g., 3.11.9 -> 3.11)
PYTHON_MINOR="${PYTHON_VERSION%.*}"
# Download the full Python nuget package (contains all stdlib + DLLs)
NUGET_URL="https://www.nuget.org/api/v2/package/python/${PYTHON_VERSION}"
NUGET_PKG="$BUILD_DIR/python-nuget.zip"
if [ ! -f "$NUGET_PKG" ]; then
curl -sL "$NUGET_URL" -o "$NUGET_PKG"
fi
NUGET_DIR="$BUILD_DIR/python-nuget"
rm -rf "$NUGET_DIR"
mkdir -p "$NUGET_DIR"
unzip -qo "$NUGET_PKG" -d "$NUGET_DIR"
# Copy _tkinter.pyd (the C extension)
TKINTER_PYD=$(find "$NUGET_DIR" -name "_tkinter.pyd" | head -1)
if [ -n "$TKINTER_PYD" ]; then
cp "$TKINTER_PYD" "$PYTHON_DIR/"
echo " Copied _tkinter.pyd"
else
echo " WARNING: _tkinter.pyd not found in nuget package"
fi
# Copy tkinter Python package from the stdlib zip or Lib/
# The nuget package has Lib/tkinter/
TKINTER_PKG=$(find "$NUGET_DIR" -type d -name "tkinter" | head -1)
if [ -n "$TKINTER_PKG" ]; then
mkdir -p "$PYTHON_DIR/Lib"
cp -r "$TKINTER_PKG" "$PYTHON_DIR/Lib/tkinter"
echo " Copied tkinter/ package"
else
echo " WARNING: tkinter package not found in nuget package"
fi
# Copy Tcl/Tk DLLs (tcl86t.dll, tk86t.dll, etc.)
for dll in tcl86t.dll tk86t.dll; do
DLL_PATH=$(find "$NUGET_DIR" -name "$dll" | head -1)
if [ -n "$DLL_PATH" ]; then
cp "$DLL_PATH" "$PYTHON_DIR/"
echo " Copied $dll"
fi
done
# Copy Tcl/Tk data directories (tcl8.6, tk8.6)
for tcldir in tcl8.6 tk8.6; do
TCL_PATH=$(find "$NUGET_DIR" -type d -name "$tcldir" | head -1)
if [ -n "$TCL_PATH" ]; then
cp -r "$TCL_PATH" "$PYTHON_DIR/$tcldir"
echo " Copied $tcldir/"
fi
done
# Add Lib to ._pth so tkinter package is importable
if ! grep -q '^Lib$' "$PTH_FILE"; then
echo 'Lib' >> "$PTH_FILE"
fi
rm -rf "$NUGET_DIR"
echo " tkinter bundled successfully"
# ── Download pip and install into embedded Python ──────────── # ── Download pip and install into embedded Python ────────────
echo "[4/8] Installing pip into embedded Python..." echo "[4/8] Installing pip into embedded Python..."
@@ -242,16 +315,8 @@ set WLED_CONFIG_PATH=%~dp0app\config\default_config.yaml
if not exist "%~dp0data" mkdir "%~dp0data" if not exist "%~dp0data" mkdir "%~dp0data"
if not exist "%~dp0logs" mkdir "%~dp0logs" if not exist "%~dp0logs" mkdir "%~dp0logs"
echo. :: Start the server — reads port from config, prints its own banner
echo ============================================= "%~dp0python\python.exe" -m wled_controller.main
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
pause pause
LAUNCHER LAUNCHER
@@ -259,6 +324,64 @@ LAUNCHER
# Convert launcher to Windows line endings # Convert launcher to Windows line endings
sed -i 's/$/\r/' "$DIST_DIR/LedGrab.bat" 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 ─────────────────────────────────────────────── # ── Create ZIP ───────────────────────────────────────────────
ZIP_PATH="$BUILD_DIR/$ZIP_NAME" ZIP_PATH="$BUILD_DIR/$ZIP_NAME"

View File

@@ -106,6 +106,11 @@ $pthContent = $pthContent -replace '#\s*import site', 'import site'
if ($pthContent -notmatch 'Lib\\site-packages') { if ($pthContent -notmatch 'Lib\\site-packages') {
$pthContent = $pthContent.TrimEnd() + "`nLib\site-packages`n" $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 Set-Content -Path $pthFile.FullName -Value $pthContent -NoNewline
Write-Host " Patched $($pthFile.Name)" 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" 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" 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 LAUNCHER
sed -i "s/VERSION_PLACEHOLDER/${VERSION_CLEAN}/" "$DIST_DIR/run.sh" sed -i "s/VERSION_PLACEHOLDER/${VERSION_CLEAN}/" "$DIST_DIR/run.sh"
chmod +x "$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 ─────────────────────────────────────────── # ── Create tarball ───────────────────────────────────────────
echo "[7/7] Creating $TAR_NAME..." echo "[7/7] Creating $TAR_NAME..."

View File

@@ -278,7 +278,12 @@ class OverlayManager:
def _start_tk_thread(self) -> None: def _start_tk_thread(self) -> None:
def _run(): 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: try:
self._tk_root = tk.Tk() 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"Starting LED Grab v{__version__}")
logger.info(f"Python version: {sys.version}") logger.info(f"Python version: {sys.version}")
logger.info(f"Server listening on {config.server.host}:{config.server.port}") 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 # Validate authentication configuration
if not config.auth.api_keys: if not config.auth.api_keys: