PanaControl

PanaControl

Docker app from Tomjoad's Repository

Overview

Remote control for LUMIX cameras over the cam.cgi protocol: live view in the browser, autofocus, start and stop video recording, ISO, and the current aperture as a reading. Developed against a DC-GH6 with firmware 3.01. Give the container its own address on the LAN (network br0, fixed IP). The camera sends the live view by UDP to exactly the address the request came from, so camera and container must be in the same subnet: with a router or a separate VLAN in between, the control works but no picture arrives. Don't use bridge with port mapping: a frame of about 23 KB travels as one UDP datagram in 17 IP fragments, and Docker's NAT drops whole frames when one fragment is lost. Host networking works too. The web interface is on port 8080 of the container's address. It has no login: whoever reaches it controls the camera. For access from outside, put a reverse proxy with authentication in front. Stopping: --stop-timeout 30 gives the app time to release the camera's remote session. A hard kill locks the camera out until it is switched off and on again. Based on linuxserver.io's Alpine image. The app writes no files and runs as PUID:PGID, never as root. Upgrading from 1.x: remove --read-only, --cap-drop ALL and the --cap-add options from Extra Parameters, or the container won't start.

PanaControl

Remote control and live view for LUMIX cameras over the cam.cgi protocol: live view in the browser, autofocus, start and stop video recording, ISO, and the current aperture as a reading. Developed against a DC-GH6 (firmware 3.01) and kept platform-neutral. The container is built on linuxserver.io's Alpine image with its conventions (PUID, PGID, UMASK, TZ).

Status: usable and verified on the device. Live view at 29.9 fps, autofocus, start and stop recording.

Always stop it in order (Ctrl+C, docker stop, Unraid's Stop). Only then the app sends stopstream. A process killed hard leaves the camera's remote session occupied, and the camera then answers every command with err_busy until it is switched off and on again.

Requirements

  • Camera and computer (or container) in the same subnet
  • Camera in its waiting state: [Wi-Fi] -> [Wi-Fi Function] -> [Control With Smartphone], switch to the Wi-Fi connection with the DISP. button, then [Via Network]
  • For the native start: Python 3.11 or newer (tomllib from the standard library)

Supported architectures and tags

Architecture Available
x86-64 yes
arm64 no

Only x86-64 is built: PanaControl runs on an x86-64 server, and no ARM host is in use.

Tag Meaning
latest The latest release
2.1.0, 2.1 A specific release / the latest patch of a minor version
sha-<commit> A specific build
dev Test build from a branch, made by a manual run; not for regular use

Images are built by GitHub Actions for every version tag, published to ghcr.io and signed keylessly with Sigstore. They are public: no login is needed to pull them.

Usage

docker compose

cp .env.example .env      # set the camera's address
docker compose up -d

See docker-compose.yml. It publishes the ports through Docker's NAT, which can drop live view frames; on a Linux server an own address on the LAN (macvlan/ipvlan) or network_mode: host is better, see Network.

docker run

docker run -d \
  --name panacontrol \
  --network <MACVLAN-NETWORK> --ip <STATIC-IP> \
  --stop-timeout 30 \
  --security-opt no-new-privileges \
  -e PUID=1000 -e PGID=1000 -e TZ=Europe/Berlin \
  -e LUMIX_CAMERA_IP=<CAMERA-IP> \
  --restart unless-stopped \
  ghcr.io/tom-joad/panacontrol:latest

Unraid

Install PanaControl from Community Applications; the guide is unraid/README-UNRAID.md. The template runs the container on br0 with a fixed IP.

Parameters

Parameter Default Meaning
--network -- An own address on the LAN (macvlan/ipvlan, br0 on Unraid) or host; see Network
-p 8080:8080 -- Web interface (only with port mapping)
-p 49199:49199/udp -- Live view; the same number on both sides (only with port mapping)
-e PUID 911 User the app runs as (Unraid: 99), not 0
-e PGID 911 Group the app runs as (Unraid: 100), not 0
-e UMASK 022 File mode mask; the app writes no files
-e TZ Etc/UTC Time zone of the log time stamps, e.g. Europe/Berlin
-e LUMIX_CAMERA_IP -- (required) The camera's address
-e LUMIX_DEVICE_NAME lumix-remote Name shown on the camera's display during registration
-e LUMIX_STREAM_PORT 49199 UDP port for the live view
-e LUMIX_WEB_HOST 0.0.0.0 in the image Where the interface listens
-e LUMIX_WEB_PORT 8080 Port of the interface
-e LUMIX_LOG_LEVEL INFO DEBUG for troubleshooting
--stop-timeout 30 -- Time for the ordered shutdown, see above

In the container the configuration comes from environment variables, so no config.toml has to be mounted. They take precedence over config.toml.

There are no volumes: the app keeps no state and writes no files. This deviates from the usual /config volume on purpose.

Network

The camera sends the frames by UDP to the address startstream came from, and to exactly the port it was told. A frame of about 23.5 KB is one UDP datagram in about 17 IP fragments, at 30 frames per second. Docker's NAT (bridge with port mapping) has to reassemble each of them and drops whole frames under load: the live view then has gaps of 10 to 40 seconds while the control keeps working.

So give the container its own address on the LAN (macvlan or ipvlan; on Unraid br0 with a fixed IP) or use network_mode: host. If you must use port mapping, the UDP port needs the same number on both sides.

Docker Desktop on macOS runs the host namespace in a VM: the published UDP port works there, network_mode: host doesn't. Day to day on a Mac, start it natively.

Upgrading from 1.x

2.0.0 moves from python:3.14-alpine with its own entrypoint to linuxserver.io's base image. Its init needs a writable root file system and the usual capabilities, so remove the 1.x hardening options: --read-only, --cap-drop ALL, --cap-add SETUID, --cap-add SETGID (compose: read_only, cap_drop, cap_add). Keep --stop-timeout 30 and --security-opt no-new-privileges. Settings, ports and the network stay the same. On Unraid, see Upgrading from 1.x to 2.0.0.

Native start

git clone https://github.com/Tom-Joad/PanaControl.git && cd PanaControl
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp config.example.toml config.toml   # set the IP
python3 -m lumixremote.app

On Windows, run.bat does these steps.

Then open the interface at the address set in config.toml, by default http://127.0.0.1:8080. The app keeps the registration alive, polls the state every five seconds and kicks the stream when it stalls. Several browsers may watch at the same time; there is still only one connection to the camera.

Route Purpose
GET / Interface
GET /stream.mjpg Live view as multipart/x-mixed-replace
GET /api/status Connection state, battery, recording, frame rate
POST /api/af Autofocus
POST /api/rec/start Start recording
POST /api/rec/stop Stop recording
POST /api/iso Set the sensitivity, JSON {"value": "800"}

The interface takes the ISO values from the camera's menu definition; they are not in the program. Set values are read back, because the camera snaps to the value allowed in the current mode.

The aperture can only be read. The DC-GH6 with firmware 3.01 offers no call to set it remotely; the reasoning and the candidates tested are in docs/protocol.md. The current value is read from the header of the live view datagram and shown.

Security

The interface has no login: whoever reaches it controls the camera and sees the live view. Keep it on a trusted network, or put a reverse proxy with authentication in front. Commands that a browser sends from another web site are refused, so a page open in the same browser can't start or stop a recording; scripts such as curl still work. Reporting vulnerabilities: SECURITY.md.

Troubleshooting

docker logs -f panacontrol

The log starts with the TomJoad Images banner and the user IDs. The app then writes one JSON object per line (ts, level, event and the details); the first one is "event":"starting" with the version and the settings that matter. The state is at http://<address>:8080/api/status. More symptoms and causes are in unraid/README-UNRAID.md.

Verify the signature:

cosign verify \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github.com/Tom-Joad/PanaControl/\.github/workflows/build-and-push\.yml@refs/tags/v' \
  ghcr.io/tom-joad/panacontrol:latest

Building and testing

docker build -t panacontrol:test .
tests/smoke.sh panacontrol:test

python -m pytest runs the unit tests (pip install -r requirements-dev.txt first). The smoke test needs no camera. It checks the settings, the switch to PUID:PGID, TZ, the start banner, the interface and that docker stop ends the app in order. GitHub Actions runs it on every push.

Diagnostic run

python3 -m lumixremote.probe
Switch Effect
--ip 192.0.2.10 work without a configuration file
--duration 120 measuring time in seconds
--test-commands also test autofocus and record start/stop
--no-restart don't kick the stream when it stalls (endurance test)
-v verbose log

On the first run, the firewall asks about incoming connections for Python. If that is refused, the control works but no picture arrives. The run saves a sample frame to out/probe_frame.jpg. It answers: frame rate and resolution in practice, whether the header before the JPEG has a constant length, whether the stream holds without kicking, and whether the session survives a longer pause without commands.

Layout

File Content
lumixremote/camera.py DLNA probe, registration, commands, reconnect
lumixremote/stream.py UDP receiver, JPEG extraction, statistics
lumixremote/app.py Connection keeper, routes, MJPEG output
lumixremote/static/ Interface without a build step, stylesheets and fonts
lumixremote/probe.py Measuring run and results
docs/protocol.md What was verified on the device
unraid/README-UNRAID.md Running on an Unraid server
unraid/PanaControl.xml Container template for Unraid
root/etc/s6-overlay/ s6 services: settings check, app as abc, start banner
tests/smoke.sh Smoke test of the image
.github/workflows/build-and-push.yml Checks; for tags build, publish, sign

config.toml is excluded on purpose. IP, device name, UDN and serial number don't belong in the repository; the client reads the UDN from the DLNA descriptor at runtime.

Changes are listed in the CHANGELOG.

Related apps

Details

Repository
ghcr.io/tom-joad/panacontrol:latest
Last Updated2026-10-19
First Seen2026-10-19

Runtime arguments

Web UI
http://[IP]:[PORT:8080]/
Network
br0
Shell
bash
Privileged
false
Extra Params
--stop-timeout 30 --security-opt no-new-privileges

Template configuration

LUMIX_CAMERA_IPVariable

The camera's address on the local network, for example 192.0.2.10

PUIDVariable

User the app runs as (99 = nobody). Not 0.

Default
99
PGIDVariable

Group the app runs as (100 = users). Not 0.

Default
100
TZVariable

Time zone for the time stamps in the log, for example Europe/Berlin

Default
Etc/UTC
UMASKVariable

File mode mask. The app writes no files; kept for consistency with other linuxserver.io images.

Default
002
LUMIX_DEVICE_NAMEVariable

Name shown on the camera's display during registration

Default
lumix-remote
LUMIX_STREAM_PORTVariable

UDP port for the live view, on the container's own address

Default
49199
LUMIX_WEB_HOSTVariable

Where the interface listens inside the container. 0.0.0.0 opens it to the network; it has no login.

Default
0.0.0.0
LUMIX_WEB_PORTVariable

Port of the interface on the container's own address

Default
8080
LUMIX_LOG_LEVELVariable

DEBUG for troubleshooting

Default
INFO