# The cam.cgi protocol
Verified on a DC-GH6 with firmware 3.01, over WLAN in the same subnet.
Everything documented here was observed on the device, not derived.
## Overview
The camera runs two services:
| Port | Service | Purpose |
|---|---|---|
| 60606 | UPnP/DLNA | Device description, media access |
| 80 | `cam.cgi` | Control and stream control |
Control commands are plain HTTP GETs with query parameters. The reply is
usually XML of the form `ok`; on errors it
says `err_busy`, `err_param` or similar.
## Prerequisite on the camera
Menu `[Wi-Fi] -> [Wi-Fi Function] -> [Control With Smartphone]`. On the
Bluetooth pairing screen, switch to the Wi-Fi connection with the **DISP.**
button, then choose `[Via Network]` and the WLAN. The camera then stays in a
waiting state, and exactly then it can be reached by HTTP.
`[Direct]` would be wrong: with it the camera becomes its own access point
instead of joining an existing WLAN.
## Device description
```
GET http://:60606/Lumix/Server0/ddd
```
Returns UPnP XML. Relevant fields in the `` element:
| Field | Content |
|---|---|
| `modelNumber` | Model name |
| `UDN` | Identifier prefixed with `uuid:`, needed for the registration |
| `pana:X_FirmVersion` | Firmware version |
| `pana:X_AdditionalFunction` | contains `CPRemoteView` when remote control works |
The request has no side effects and needs no session. So it serves as a
reachability test.
## Registration
Two stages. First the sign-in:
```
GET /cam.cgi?mode=accctrl&type=req_acc_g
```
Then the actual registration, with the UDN (without `uuid:`) and a freely
chosen device name passed **hex-encoded**:
```
GET /cam.cgi?mode=accctrl&type=req_acc_e&value=&value2=
```
For once the reply is not XML but comma-separated plain text. Four forms
were observed, **all with HTTP 200**:
```
ok_under_research_no_msg,,remote,open still working
ok_under_research,,remote,open likewise, shorter form
err_user_refused,,remote,open refused
ok,,remote,open, done
```
In addition, `req_acc_e` at times returns the ordinary
`err_critical` as XML, so not even in the
comma-separated format. Observed while an older session was still occupied.
So the status code is no help in telling them apart, only the body, and that
is not fully known: the interim reply alone came in two spellings, with and
without `_no_msg`. Whoever excludes a list of known error forms runs after
the firmware.
The reverse check is reliable: the first field must be **exactly** `ok`, and
the last one must look like a session ID. Only sixteen-digit hex strings have
been seen there; in all other forms the same position holds `open`, which
would otherwise pass as a session.
**The gap between attempts decides.** Measured: at half-second intervals the
camera stayed at `ok_under_research_no_msg` for twelve attempts and then
stopped answering at all. With three seconds between attempts, the second one
succeeded, reproduced several times. Asking quickly keeps the camera stuck in
the interim reply instead of speeding up the registration.
On first contact, a confirmation on the display is added; until it is
acknowledged, the reply also stays the interim one. Later connections skip
the prompt.
**A new registration tears down the existing session.** The camera then
briefly shows "not connected to the app", and **a running video recording is
aborted with it**.
The exact moment matters, as observed on the device: a network dropout alone
does no harm. The recording continues during the radio silence and **is only
aborted at the moment the connection is set up again**. So the session stays
valid on the camera as long as only the network is gone.
Hence the rule: a missing reply is **not** a reason to give up the session.
Only a reply in which the camera refuses the session is. A time limit for
sitting it out brings nothing anyway: registering again only works with a
reachable camera, and once it answers again, its reply says where things
stand.
Also to be avoided while `rec` is `on`: any mode change via `recmode`. Shortly
after a registration the camera reports `cammode=play`, and a reflexive
switch back at that moment ends the recording.
**`err_busy` on a command doesn't mean the session is lost.** The camera did
answer. Reproduced on the device with a `startstream` refused during a stream
gap: registering again after that loses the recording, although the session
was still valid and the `getstate` keepalive went through unchanged. Whether a
session still exists is sensibly decided by the keepalive alone.
From then on the session ID goes into every request as the header
`X-SESSION_ID`.
Sent afterwards optionally; fails on some firmware versions without
consequences:
```
GET /cam.cgi?mode=setsetting&type=device_name&value=
```
## Commands
| Call | Effect |
|---|---|
| `mode=camcmd&value=recmode` | Force record mode, see below |
| `mode=camcmd&value=oneshot_af` | Trigger autofocus once |
| `mode=camcmd&value=video_recstart` | Start video recording |
| `mode=camcmd&value=video_recstop` | Stop video recording |
| `mode=camcmd&value=capture` | Take a photo |
| `mode=getstate` | State query, doubles as the keepalive |
Among other things, `getstate` returns the battery level (`batt`), the
recording state (`rec`), the remaining capacity (`remaincapacity`), the
remaining video time in seconds (`video_remaincapacity`) and the operating
mode (`cammode`).
## The mode change after the registration
Verified on the DC-GH6 with firmware 3.01, reproduced several times:
**After the registration the camera switches into playback by itself.**
Measured course from the moment the session is up:
| Time | `cammode` |
|---|---|
| 0.7 s | `rec` |
| from about 2 s | `play` |
| after `recmode` | `rec` |
That gives the order that works: register, **wait for the state**, read
`cammode`, and only when it says `play`, send `recmode`. The call is then
acknowledged with `ok` and takes about 0.5 s.
How long the settling takes varies a lot. In one series of measurements the
camera was ready throughout 3.3 s after the registration, in another
`recmode` ran into a 5 s timeout. A fixed wait never fits; the right way is to
read `cammode` every half second and only continue once `rec` has held for
a few seconds without a break.
Sending `recmode` right after the registration gets `err_critical`: at that
moment the camera is still in `rec`, so the command is moot. Afterwards it
drops into playback, and without a second attempt it stays there.
**In playback mode `startstream` is acknowledged with `ok`, but not a single
datagram arrives.** So a silent stream is not a network problem first, but a
hint at `cammode`. Cross-checked: 25 s of listening on the UDP port after a
clean `startstream` in playback gave zero packets.
Not accepted: `camcmd&value=rec_mode` and `setsetting&type=cammode` (both
`err_param`), and `camcmd&value=playmode`.
Further options from others' reverse engineering, not used so far:
`setsetting&type=focusmode` with `afs`/`aff`/`afc`, and
`camctrl&type=touch_trace` for focus by coordinate.
## Settings
```
GET /cam.cgi?mode=getsetting&type=
GET /cam.cgi?mode=setsetting&type=&value=
```
The reply to `getsetting` carries the value as an attribute:
``. An unknown name gives `err_critical`, an unusable
value `err_param`.
Read on the device were, among others, `iso`, `shtrspeed`, `exposure`,
`focusmode`, `photostyle`, `drivemode`, `whitebalance`, `quality`, `afmode`,
`ex_tele_conv` and `liveviewsize`.
**An acknowledged `ok` doesn't mean the value applies.** Measured: `iso=400`
was answered with `ok`, reading back gave `800`. The camera snaps to the
value allowed in the current mode. Whoever sets a value reads it back.
Shutter speed and aperture are APEX values in steps of 1/256:
`shtrspeed="1110/256"` is 4.336, so 1/20 s. For the aperture, N = 2^(Av/2);
Av 1280/256 = 5.0 gives 5.66 and so the marked aperture f/5.6. Cross-checked
on the device.
### List of values
```
GET /cam.cgi?mode=getinfo&type=allmenu
```
Returns the complete menu definition, about 380 KB on the DC-GH6. The allowed
values of a setting are there as `- ` with `cmd_mode="setsetting"`,
`cmd_type` and `cmd_value`; for ISO these are 32 entries, some of them twice,
because the same sensitivity appears in several menu contexts. So the list
comes from the camera and doesn't have to be maintained in the program.
`getinfo&type=curmenu` returns the same structure in short, with `enable`
flags. What they mean is unclear: for ISO it said `enable="no"`, although ISO
could be set without problems.
### Shutter speed
`setsetting&type=shtrspeed&value=/256` is verified on the device: `1024/256`
was set, and the same value came back.
The **camera publishes no list of values for it**: in the menu tree the
shutter speed, like the aperture, appears as `sp_embeded_shutter_speed`
without a command. Whoever wants to build a select field for it must build
the series themselves; whether the camera then accepts every value, is
limited by the frame rate in movie mode, or snaps like with ISO is unchecked.
The interface currently doesn't offer the shutter speed.
### The aperture can't be set
On the DC-GH6 with firmware 3.01 there is **no** way to set the aperture
remotely. Evidence:
* The `camcmdlist` in `getinfo&type=capability` lists exactly six supported
commands: four times zoom, `poweroff`, `4kphoto_marking`.
* The whole menu tree holds 884 `setsetting` entries, among them ISO, shutter
speed and white balance, but none for the aperture. It appears there as
`func_type="sp_embeded_aperture"` without any command.
* Tried without success: `setsetting` and `camctrl` with the types
`aperture`, `fnumber`, `f`, `av`, `iris` and a dozen more spellings, plus
`camcmd&value=aperture_up`, all `err_param`. The attempts were repeated in
the settled state with the same result.
### The aperture can be read
It is in the header of the live view datagram. The anchor is the lens limits:
```
GET /cam.cgi?mode=getinfo&type=lens
-> ok,2304/256,925/256,3755/256,768/256,...,,...
```
The first two numbers are the smallest and largest aperture in APEX
encoding. Exactly these two stand one after the other in the header, each
followed by `0x0100`, and right after them comes the current value in the
same format:
```
09 00 01 00 03 9d 01 00 05 00 01 00
f/22 f/3.5 current f/5.6
```
Because the header has no fixed length, this pattern is searched, not
computed, as with the JPEG start.
## Live stream
```
GET /cam.cgi?mode=startstream&value=
GET /cam.cgi?mode=stopstream
```
The camera sends the frames to **the IP the call came from**. So the
receiver must listen on the same machine and should already be bound before
`startstream` is sent.
After `startstream` the stream needs a moment to start. A second
`startstream` three seconds after the first is answered with `err_busy` and
takes the session with it, measured. Whoever kicks the stream when it stalls
keeps at least eight seconds between attempts.
Every datagram holds a complete JPEG with a proprietary header in front. Its
length varies, see the measurements. The JPEG start is found by the SOI
marker `FF D8 FF`.
## Measurements
Sixty seconds of continuous operation, WLAN in the 5 GHz band:
| Quantity | Value |
|---|---|
| Resolution | 640x360 |
| Frame rate | 29.9 fps |
| Average frame | 23.5 KB |
| Data rate | about 700 KB/s, so about 5.6 Mbit/s |
| Datagrams per frame | exactly one |
| Dropped packets | none |
| Truncated frames | none |
| Header offsets | 340, 324 and 276 bytes in the same run |
| Stream interruptions | none |
| Session over 60 s | stable |
So a frame fits comfortably into one datagram; reassembly across packet
boundaries is not needed on the application level.
**On the IP level it very much is.** 23.5 KB clearly exceed any usual MTU: a
frame is split into about 17 IP fragments, at 30 frames per second more than
500 packets. If a single fragment is lost, the receiver drops the whole
frame.
That sets a requirement for the network path: **no NAT between camera and
receiver.** Address translation has to reassemble fragmented datagrams first,
and that is exactly where they get lost under load. In a container this means
an own address on the LAN (macvlan/ipvlan) or host networking instead of
bridge with port mapping.
## Known failure patterns
**Every command answers `err_busy`.** The camera holds exactly one remote
session, and a crashed client leaves it occupied.
Measured by killing the app with `SIGKILL` in the middle of operation:
| Observation | Result |
|---|---|
| `startstream` right after | `err_busy` |
| Registration afterwards | `err_critical` already on `req_acc_e` |
| Sign-in step `req_acc_g` | still `ok,` |
| Descriptor on port 60606 | still `HTTP 200` |
| Recovery after 3 min of complete radio silence | none |
| Recovery after a good 5 min in total | none |
So the camera stays reachable and even accepts the first sign-in step; only
the actual registration is refused for good.
**There is no known call that releases the session.** Tested without
success: `accctrl` with the types `req_acc_dis`, `req_acc_d`, `req_acc_del`,
`req_acc_end`, `req_acc_stop` and `req_acc_r` (all `err_param`, so unknown to
the camera), and `stopstream` and `camcmd&value=stopstream` without a session
(both `err_critical`).
The remedy remains restarting the camera. Hence, unchanged: every path that
ends the app must send `stopstream`. That is not optional, but the only
safeguard there is.
**Commands go through, but no picture arrives.** Two causes; check them in
this order. First `cammode`: if it says `play`, the camera sends nothing,
although it acknowledges `startstream`. Second the local firewall, blocking
incoming UDP for the Python process; this affects macOS and Windows alike.
**`recmode` runs into the timeout.** The mode change takes longer than a
tightly set HTTP timeout allows. Measured: with a five-second timeout it ran
into it, with fifteen it didn't. A call that timed out still takes effect:
the camera finishes the change, only the reply comes too late.
**`err_critical` on `recmode`.** Not a defect, but the hint that the camera is
already in record mode. See the section on the mode change.