bitfocus / companion-module-requests

Repository for tracking module requests
102 stars 11 forks source link

Panasonic Lumix DC-BGH1 Camera Control #402

Open hamuchen opened 3 years ago

hamuchen commented 3 years ago

Panasonic released a box style camera with network control functions pretty recently. I tried a few companion (PTZ-) modules, but none could control the camera.

My use case would be first to trigger an auto focus, more functionality would be nice but not needed. But I will gladly help bring even more features to this module.

With the Lumix tether software it is possible to control all sorts of features. Here is a video (not mine) where you can see all the different controllable features: https://vimeo.com/469453894 Tether Multicam can be found here: https://av.jpn.support.panasonic.com/support/global/cs/soft/download/d_lumixtether_multicam.html

The camera can be connected by WLAN and LAN and I could try (with a helping hand) to reverse engineer the network traffic from the software (via Wireshark?).

There is an SDK available, but only for USB-C: https://av.jpn.support.panasonic.com/support/global/cs/soft/tool/sdk.html (To download it, you need to input a model and SN, gladly there are some available, this combo works: https://www.fk-secondhand.com/produkt/panasonic-lumix-dc-gh5-mit-g-vario-12-60mm-f-35-56-asph-snr-wh8lf001343/ )

Any help would be appreciated.

Cheers, hamu

josephdadams commented 3 years ago

Thanks for offering to help wireshark the traffic. I am still going to have to mark it as missing documentation, but when you've captured the network traffic, you can post that here. It's pretty simple to filter out the data so it just shows what's going on between your computer and that specific device.

chberge commented 3 years ago

Plus one on this request for me. Have two cameras my self, and would love to be able to control them from Companion. Panasonic have released a SDK, but it Windows only for now. But maybe what's needed for a Companion module can be found in the documentation? https://av.jpn.support.panasonic.com/support/global/cs/soft/tool/sdk.html

hamuchen commented 3 years ago

Nice find, sadly I can't play around with this cam at the moment, because it is used in production.

In the download there ist extensive documentation about the SDK, is it still "Missing documentation" @josephdadams ?

josephdadams commented 3 years ago

@hamuchen I would say it is still missing documentation because none of it seems to mention network related protocol info, which is the way we are going to want to control it.

hamuchen commented 3 years ago

@josephdadams Hmm, but when I download the LumixRemoteControlLibraryBeta2.00.zip, there is this file: LumixRemoteControlLibraryBeta2.00/Document/Lumix Remote Control Library API Specification.html There's stuff like:

2.19. LAN Connection This section explains the API used for LAN connection.

Or do you mean, that we can't use that SDK/dll and need to speak directly to the camera?

josephdadams commented 3 years ago

Can you post that file? I didn't see it.

McHauge commented 3 years ago

From what I saw it was windows only, and needed to driver file to work, there is a request for it on the Panasonic Ptz module aswell

omnivoxaps commented 3 years ago

https://av.jpn.support.panasonic.com/support/global/cs/soft/tool/sdk.html

May 12, 2021 Beta2.00 Suppots USB & Ethernet interface Supprots multiple connection

Now they have released SDK for ethernet connection

josephdadams commented 3 years ago

Can someone who has one of these cameras download the API document and post it here?

omnivoxaps commented 3 years ago

https://www.dropbox.com/sh/vdabho3xuhp3g5n/AAAeNpFKfGQfiGaTpFfr3GOqa?dl=0

josephdadams commented 3 years ago

Ok, I looked through it - it still looks like it requires the windows only DLL to communicate. The API document doesn't explicitly mention a port number or anything on how to communicate with the camera directly. So unless I missed something, we are still basically where we were before, where someone is going to have to reverse engineer the LAN traffic in order to figure out the actual protocol and then replicate that in Companion.

hamuchen commented 2 years ago

First: @PhotoJoseph created the same issue in another project, idk if we can cross-reference it: https://github.com/bitfocus/companion-module-panasonic-ptz/issues/15

Second: I have another BGH1 for testing at the moment. I could try to reverse engineer the network traffic, but I will need some help from someone with knowledge in wireshark (and what to look for). So any help is greatly appreciated.

Cheers, hamu

hamuchen commented 2 years ago

Okay, without much research about using wireshark, i started it, selected my network and then put the IP of the camera in the filter like so: ip.dst == IP_OF_YOUR_CAM Then I started using Lumix Teather. by just looking at the log I found out, that there is a way to get the status of the camera simply by calling this url: http://IP_OF_YOUR_CAM/cam.cgi?mode=getstate There you will get an xml like this:

<?xml version="1.0" encoding="UTF-8"?>
<camrply>
  <result>ok</result>
  <state>
    <batt>-1/4</batt>
    <batt_per>-1</batt_per>
    <rec>off</rec>
    <cammode>rec</cammode>
    <video_remaincapacity>17974</video_remaincapacity>
    <remaincapacity>10358</remaincapacity>
    <sdcardstatus>write_enable</sdcardstatus>
    <sd_memory>set</sd_memory>
    <version>VD4.30</version>
    <temperature>low</temperature>
    <burst_interval_status>off</burst_interval_status>
    <sd_access>off</sd_access>
    <rem_disp_typ>time</rem_disp_typ>
    <progress_time>0</progress_time>
    <operate>enable/disable</operate>
    <stop_motion_num>0</stop_motion_num>
    <stop_motion>off</stop_motion>
    <lens>normal</lens>
    <interval_status>off</interval_status>
    <sdi_state>none</sdi_state>
    <sd2_cardstatus>write_enable</sd2_cardstatus>
    <sd2_memory>unset</sd2_memory>
    <sd2_access>off</sd2_access>
    <current_sd>sd1</current_sd>
    <backupmode>off</backupmode>
    <batt_grip>-1/0</batt_grip>
    <batt_per_grip>-1</batt_per_grip>
    <warn_disp>no_disp</warn_disp>
    <cinelike>off</cinelike>
    <slotfunc>allot</slotfunc>
    <sd_usage_rate>0</sd_usage_rate>
    <sd_full>false</sd_full>
    <sd2_full>false</sd2_full>
    <mf_guide>off</mf_guide>
    <interval_remain_num>0</interval_remain_num>
    <interval_end_time>0/0/0/0/00</interval_end_time>
    <video_priority_disp>on</video_priority_disp>
    <lc_state>off</lc_state>
    <lc_expo_time>1</lc_expo_time>
    <lc_shoot_num>0</lc_shoot_num>
    <lc_elapsed_sec>0</lc_elapsed_sec>
    <hrs_num>0/0</hrs_num>
    <hrs_state>off</hrs_state>
  </state>
</camrply>

I don't know, if you can also control the camera at this endpoint or if it is just there, to get information.

When I click something in Lumix Teather, my guess is, this is send by PTP (Picture Transfer Protocol over IP): 2022-03-31_134326 2022-03-31_134247

I have no idea what that protocol does or how to use/abuse it. Is there anyone with that knowledge here?

Cheers, hamu

Manouchehri commented 2 years ago

@hamuchen Could you share the pcap from that capture?

hamuchen commented 2 years ago

Okay, after digging into PTP/IP (sadly searching for this is hard, as there ist another protocol with the same abbr. (Precision Time Protocol). With Linux (I used my trusty Raspi) you can use ghoto2 to try to connect via PTP/IP, which goes like this: gphoto2 --port ptpip:IP_OF_YOUR_CAM --summary Sadly, only an error came back: 2022-03-31_145714 Even trying with debug enabled did not tell me anything more than that: 2022-03-31_145752

Went back to start, cleared wireshark, closed Lumix Teather and opened it again to see the login. And there we new URLs to be called: http://IP_OF_YOUR_CAM:60606/PTPRemote/Server0/ddd Which resulted in this XML (edited):

<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:pana="urn:schemas-panasonic-com:pana">
  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  <device>
    <deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>
    <friendlyName>BGH1-ABC123</friendlyName>
    <manufacturer>Panasonic</manufacturer>
    <modelName>LUMIX</modelName>
    <modelNumber>DC-BGH1</modelNumber>
    <modelDescription/>
    <serialNumber>000000000000000000XHL2109ABC123</serialNumber>
    <modelURL/>
    <manufacturerURL/>
    <UDN>uuid:4D454930-0100-1000-8000-B46C47ABC123</UDN>
    <dlna:X_DLNADOC xmlns:dlna="urn:schemas-dlna-org:device-1-0">M-DMS-1.50</dlna:X_DLNADOC>
    <pana:X_AdditionalFunction>PTPRemoteView</pana:X_AdditionalFunction>
    <pana:X_FirmVersion>2.30</pana:X_FirmVersion>
    <pana:X_CamCategory>MirrorlessILC</pana:X_CamCategory>
    <pana:X_MacAddress>D01769ABC123</pana:X_MacAddress>
    <pana:X_PTPPortNo>15740</pana:X_PTPPortNo>
    <serviceList>
      <service>
        <serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>
        <serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>
        <SCPDURL>http://IP_OF_YOUR_CAM:60606/Server0/CDS_SCPD</SCPDURL>
        <controlURL>http://IP_OF_YOUR_CAM:60606/Server0/CDS_control</controlURL>
        <eventSubURL>http://IP_OF_YOUR_CAM:60606/Server0/CDS_event</eventSubURL>
      </service>
      <service>
        <serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
        <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
        <SCPDURL>http://IP_OF_YOUR_CAM:60606/Server0/CMS_SCPD</SCPDURL>
        <controlURL>http://IP_OF_YOUR_CAM:60606/Server0/CMS_control</controlURL>
        <eventSubURL>http://IP_OF_YOUR_CAM:60606/Server0/CMS_event</eventSubURL>
      </service>
    </serviceList>
  </device>
</root>

Calling controlURL and eventSubURL in the browser, did not result in anything, the result of calling SCPDURL are here (and maybe a hint how to use it?!): http://IP_OF_YOUR_CAM:60606/Server0/CDS_SCPD

<?xml version="1.0"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  <actionList>
    <action>
      <name>GetSearchCapabilities</name>
      <argumentList>
        <argument>
          <name>SearchCaps</name>
          <direction>out</direction>
          <relatedStateVariable>SearchCapabilities</relatedStateVariable>
        </argument>
      </argumentList>
    </action>
    <action>
      <name>GetSortCapabilities</name>
      <argumentList>
        <argument>
          <name>SortCaps</name>
          <direction>out</direction>
          <relatedStateVariable>SortCapabilities</relatedStateVariable>
        </argument>
      </argumentList>
    </action>
    <action>
      <name>GetSystemUpdateID</name>
      <argumentList>
        <argument>
          <name>Id</name>
          <direction>out</direction>
          <relatedStateVariable>SystemUpdateID</relatedStateVariable>
        </argument>
      </argumentList>
    </action>
    <action>
      <name>Browse</name>
      <argumentList>
        <argument>
          <name>ObjectID</name>
          <direction>in</direction>
          <relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
        </argument>
        <argument>
          <name>BrowseFlag</name>
          <direction>in</direction>
          <relatedStateVariable>A_ARG_TYPE_BrowseFlag</relatedStateVariable>
        </argument>
        <argument>
          <name>Filter</name>
          <direction>in</direction>
          <relatedStateVariable>A_ARG_TYPE_Filter</relatedStateVariable>
        </argument>
        <argument>
          <name>StartingIndex</name>
          <direction>in</direction>
          <relatedStateVariable>A_ARG_TYPE_Index</relatedStateVariable>
        </argument>
        <argument>
          <name>RequestedCount</name>
          <direction>in</direction>
          <relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
        </argument>
        <argument>
          <name>SortCriteria</name>
          <direction>in</direction>
          <relatedStateVariable>A_ARG_TYPE_SortCriteria</relatedStateVariable>
        </argument>
        <argument>
          <name>Result</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable>
        </argument>
        <argument>
          <name>NumberReturned</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
        </argument>
        <argument>
          <name>TotalMatches</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
        </argument>
        <argument>
          <name>UpdateID</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_UpdateID</relatedStateVariable>
        </argument>
      </argumentList>
    </action>
  </actionList>
  <serviceStateTable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_ObjectID</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_Result</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_BrowseFlag</name>
      <dataType>string</dataType>
      <allowedValueList>
        <allowedValue>BrowseMetadata</allowedValue>
        <allowedValue>BrowseDirectChildren</allowedValue>
      </allowedValueList>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_Filter</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_SortCriteria</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_Index</name>
      <dataType>ui4</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_Count</name>
      <dataType>ui4</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_UpdateID</name>
      <dataType>ui4</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>SearchCapabilities</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>SortCapabilities</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="yes">
      <name>SystemUpdateID</name>
      <dataType>ui4</dataType>
    </stateVariable>
  </serviceStateTable>
</scpd>

http://IP_OF_YOUR_CAM:60606/Server0/CMS_SCPD

<?xml version="1.0"?>
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  <actionList>
    <action>
      <name>GetProtocolInfo</name>
      <argumentList>
        <argument>
          <name>Source</name>
          <direction>out</direction>
          <relatedStateVariable>SourceProtocolInfo</relatedStateVariable>
        </argument>
        <argument>
          <name>Sink</name>
          <direction>out</direction>
          <relatedStateVariable>SinkProtocolInfo</relatedStateVariable>
        </argument>
      </argumentList>
    </action>
    <action>
      <name>GetCurrentConnectionIDs</name>
      <argumentList>
        <argument>
          <name>ConnectionIDs</name>
          <direction>out</direction>
          <relatedStateVariable>CurrentConnectionIDs</relatedStateVariable>
        </argument>
      </argumentList>
    </action>
    <action>
      <name>GetCurrentConnectionInfo</name>
      <argumentList>
        <argument>
          <name>ConnectionID</name>
          <direction>in</direction>
          <relatedStateVariable>A_ARG_TYPE_ConnectionID</relatedStateVariable>
        </argument>
        <argument>
          <name>RcsID</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_RcsID</relatedStateVariable>
        </argument>
        <argument>
          <name>AVTransportID</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_AVTransportID</relatedStateVariable>
        </argument>
        <argument>
          <name>ProtocolInfo</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_ProtocolInfo</relatedStateVariable>
        </argument>
        <argument>
          <name>PeerConnectionManager</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_ConnectionManager</relatedStateVariable>
        </argument>
        <argument>
          <name>PeerConnectionID</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_ConnectionID</relatedStateVariable>
        </argument>
        <argument>
          <name>Direction</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_Direction</relatedStateVariable>
        </argument>
        <argument>
          <name>Status</name>
          <direction>out</direction>
          <relatedStateVariable>A_ARG_TYPE_ConnectionStatus</relatedStateVariable>
        </argument>
      </argumentList>
    </action>
  </actionList>
  <serviceStateTable>
    <stateVariable sendEvents="yes">
      <name>SourceProtocolInfo</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="yes">
      <name>SinkProtocolInfo</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="yes">
      <name>CurrentConnectionIDs</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_ConnectionStatus</name>
      <dataType>string</dataType>
      <allowedValueList>
        <allowedValue>OK</allowedValue>
        <allowedValue>ContentFormatMismatch</allowedValue>
        <allowedValue>InsufficientBandwidth</allowedValue>
        <allowedValue>UnreliableChannel</allowedValue>
        <allowedValue>Unknown</allowedValue>
      </allowedValueList>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_ConnectionManager</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_Direction</name>
      <dataType>string</dataType>
      <allowedValueList>
        <allowedValue>Input</allowedValue>
        <allowedValue>Output</allowedValue>
      </allowedValueList>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_ProtocolInfo</name>
      <dataType>string</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_ConnectionID</name>
      <dataType>i4</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_AVTransportID</name>
      <dataType>i4</dataType>
    </stateVariable>
    <stateVariable sendEvents="no">
      <name>A_ARG_TYPE_RcsID</name>
      <dataType>i4</dataType>
    </stateVariable>
  </serviceStateTable>
</scpd>

Anyone with an idea where to look further?

Cheers, hamu

hamuchen commented 2 years ago

@hamuchen Could you share the pcap from that capture?

Sorry, just deleted it, but I can record anything you want. Do you want the normal traffic, that is occurring constantly, or the specific, when I press a button?

Cheers, hamu

Manouchehri commented 2 years ago

Useful links:

https://github.com/LVGDDesman/PanasonicControl/blob/main/Network.md

https://github.com/gavinwilliams/open-gh4/blob/master/readme.md

https://github.com/davidkim9/LumixController/blob/master/app/js/Lumix.js

https://github.com/steveseguin/free-wireless-hdmi/blob/master/cam.py

https://davidrihtarsic.github.io/Linux/Panasonic_LX100.html

https://github.com/cleverfox/lumixproto

https://github.com/ywatase/lumix-webcam/blob/master/keep.sh

https://github.com/michaeltandy/lumix-wifi-webcam/blob/master/src/main/java/hack/BasicLumixControl.java#L20

https://github.com/peci1/lumix-link-desktop/blob/master/Control.html

https://github.com/palmdalian/python_lumix_control/blob/master/lumix_control.py

https://github.com/azvampyre/st10-v01b31c/blob/master/ST15FlightMode/src/main/java/com/yuneec/IPCameraManager/cgo4/LumixGH4.java

Manouchehri commented 2 years ago

I see https://github.com/bitfocus/companion-module-panasonic-lumix already exists though.

Manouchehri commented 2 years ago

Some success:

curl -A "Apache-HttpClient" -v http://192.168.1.180:60606/Server0/CMS_event -H "CALLBACK: <http://192.168.1.248:49153/Camera/event>" -H "NT: upnp:event" -X SUBSCRIBE
*   Trying 192.168.1.180:60606...
* Connected to 192.168.1.180 (192.168.1.180) port 60606 (#0)
> SUBSCRIBE /Server0/CMS_event HTTP/1.1
> Host: 192.168.1.180:60606
> User-Agent: Apache-HttpClient
> Accept: */*
> CALLBACK: <http://192.168.1.248:49153/Camera/event>
> NT: upnp:event
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< CONTENT-LENGTH: 0
< DATE: Wed, 20 Apr 2022 03:26:14 GMT
< SERVER: Panasonic/1.0 UPnP/1.0 Panasonic-UPnP-MW/1.0
< SID: uuid:4D454931-0000-1000-8000-B46C476BC240
< TIMEOUT: Second-300
< CONNECTION: close
<
* Closing connection 0
dave@ubuntu:~$ ncat -lvp 49153
Ncat: Version 7.60 ( https://nmap.org/ncat )
Ncat: Generating a temporary 1024-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 4D40 03C9 6FD0 8319 DE2E 8B8C E17E 1159 117F 5490
Ncat: Listening on :::49153
Ncat: Listening on 0.0.0.0:49153
Ncat: Connection from 192.168.1.180.
Ncat: Connection from 192.168.1.180:52648.
NOTIFY /Camera/event HTTP/1.1
HOST: 192.168.1.248:49153
CONTENT-TYPE: text/xml; charset="utf-8"
CONTENT-LENGTH: 1096
NT: upnp:event
NTS: upnp:propchange
SID: uuid:4D454931-0000-1000-8000-B46C476BC240
SEQ: 0
CONNECTION: close

<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
  <e:property>
    <SourceProtocolInfo>http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=00900000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=00900000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=00900000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=01;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=00900000000000000000000000000000,http-get:*:application/octet-stream,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_BL_L31_HD_AAC;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01100000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_HD_1080i_AAC;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01100000000000000000000000000000</SourceProtocolInfo>
  </e:property>
  <e:property>
    <SinkProtocolInfo></SinkProtocolInfo>
  </e:property>
  <e:property>
    <CurrentConnectionIDs>0</CurrentConnectionIDs>
  </e:property>
</e:propertyset>
Manouchehri commented 2 years ago
curl -A "Apache-HttpClient" -v http://192.168.1.180:60606/Server0/CDS_event -H "CALLBACK: <http://192.168.1.248:49153/Camera/event>" -H "NT: upnp:event" -X SUBSCRIBE

This also works. I've got my camera stuck in a weird paired mode though (it thinks Lumix Tether is still connect I believe), trying to figure out how to disconnect that.

volodkuzn commented 2 years ago

I've researched this problem recently. Although I am still unable to control the camera via Ethernet, I found several useful details about camera protocol. Please, be free to contact me regarding this information.

Just a note. I found a library inside LUMIX Tether for Mac with the same name as in SDK for Windows. I bet, it's possible to use it the same way as the windows one.

I also captured some traffic between Lumix BGH-1 using Wireshark (included as an archive). Two protocols are in use: HTTP and PTP/IP. HTTP is used for a handshake, state polling, and picture streaming. PTP/IP is used for capture and capture settings control.

HTTP part (inside spoilers):

Handshake ``` GET /PTPRemote/Server0/ddd HTTP/1.1 HOST: 192.168.54.1:60606 CONNECTION: Close ``` ``` HTTP/1.1 200 OK CONTENT-LENGTH: 1691 CONTENT-TYPE: text/xml; charset="utf-8" DATE: Wed, 20 Apr 2022 16:21:25 GMT CONNECTION: close ``` ``` 1 0 urn:schemas-upnp-org:device:MediaServer:1 BGH1-XLU Panasonic LUMIX DC-BGH1 000000000000000000XGL2012020031 uuid:4D454930-0100-1000-8000-B46C476BB62A M-DMS-1.50 PTPRemoteView 2.30 MirrorlessILC 747A904836F5 15740 urn:schemas-upnp-org:service:ContentDirectory:1 urn:upnp-org:serviceId:ContentDirectory http://192.168.54.1:60606/Server0/CDS_SCPD http://192.168.54.1:60606/Server0/CDS_control http://192.168.54.1:60606/Server0/CDS_event urn:schemas-upnp-org:service:ConnectionManager:1 urn:upnp-org:serviceId:ConnectionManager http://192.168.54.1:60606/Server0/CMS_SCPD http://192.168.54.1:60606/Server0/CMS_control http://192.168.54.1:60606/Server0/CMS_event ```
Handshake 2 ``` GET /cam.cgi?mode=accctrl&type=req_acc_a&value=EC64EBCC-F75F-4617-92E7-7D85A6DBBC7B&value2=LUMIXTether&value3=C40779A1C40779A1 HTTP/1.1 HOST: 192.168.54.1 CONNECTION: Close USER-AGENT: Apache-HttpClient ``` ``` HTTP/1.1 200 OK Content-Type: text/xml Content-Length: 78 Connection: close Date: Wed, 20 Apr 2022 12:56:57 GMT Server: Panasonic ``` ``` ok ```
Getstate ``` GET /cam.cgi?mode=getstate HTTP/1.1 HOST: 192.168.54.1 CONNECTION: Close USER-AGENT: Apache-HttpClient ``` ``` HTTP/1.1 200 OK Content-Type: text/xml Content-Length: 1409 Connection: close Date: Wed, 20 Apr 2022 13:06:38 GMT Server: Panasonic ``` ``` ok -1/4 -1 off rec -1 -1 write_enable set VD4.30 low off off time 0 enable/disable 0 off normal off none write_enable unset off sd1 off -1/0 -1 no_disp off relay2_1 0 false false off 0 0/0/0/0/00 on off 1 0 0 0/0 off ```
Start Stream ``` GET /cam.cgi?mode=startstream&value=55234 HTTP/1.1 HOST: 192.168.54.1 CONNECTION: Keep-Alive USER-AGENT: Apache-HttpClient ``` ``` HTTP/1.1 200 OK Content-Type: text/xml Content-Length: 78 Date: Wed, 20 Apr 2022 16:18:22 GMT Server: Panasonic ``` ``` ok ```

PTP/IP part:

opcodes

Possible protocol realisation (tested on Nikon camera) https://github.com/mmattes/ptpip

Useful Wireshark filters

eth.addr eq b4:6c:47:6b:b6:2a and eth.addr eq 00:e0:4c:68:2b:63 and http contains "GET /cam.cgi" = filter for camera to get "GET" requests
ip.src == 192.168.54.101 and (ptpip.opcode != 0x9108 and ptpip.opcode != 0x9414 and ptpip.opcode != 0x9107 and ptpip.opcode != 0x9402 and ptpip.opcode != 0x940a and ptpip.opcode != 0x9408) = filter non-known opcodes
ip.src == 192.168.54.101 and not tcp.len == 0 and ! (http contains "GET /cam.cgi?mode=getstate HTTP/1.1") and !(ptpip.opcode == 0x9414) and !(ptpip.opcode == 0x9408)
ip.src == 192.168.54.101 and ! ptpip and not tcp.len == 0 and ! http contains "GET /cam.cgi" = filter out known packages

detect_device_start_stop_capture.pcapng.zip

Manouchehri commented 2 years ago

Just a note. I found a library inside LUMIX Tether for Mac with the same name as in SDK for Windows. I bet, it's possible to use it the same way as the windows one.

It is. One issue is that the library itself isn't very good.. for example, the annoying lack of being able to specify the target IP.

@volodkuzn Thanks for documenting the handshake! (I may be wrong, but I think a lot of the PTP/IP control messages can also be done through the HTTP server too.)

volodkuzn commented 2 years ago

It is. One issue is that the library itself isn't very good.. for example, the annoying lack of being able to specify the target IP.

I completely agree with you.

Below are some more results.

  1. Handshake I'm able to reproduce handshake via curl. curl -A "Apache-HttpClient" -v http://192.168.54.1:80/cam.cgi?mode=accctrl\&type=req_acc_a\&value=2F7A6BCD-32E8-4F08-9FD0-5052AC9D7B05\&value2=LUMIXTether\&value3=AC81E05BAC81E05B
    
    GET /cam.cgi?mode=accctrl&type=req_acc_a&value=2F7A6BCD-32E8-4F08-9FD0-5052AC9D7B05&value2=LUMIXTether&value3=AC81E05BAC81E05B HTTP/1.1
    Host: 192.168.54.1
    User-Agent: Apache-HttpClient
    Accept: */*

HTTP/1.1 200 OK Content-Type: text/xml Content-Length: 78 Date: Wed, 27 Apr 2022 09:57:30 GMT Server: Panasonic

<?xml version="1.0" encoding="UTF-8"?>

ok
I found that `value2` could be arbitrary because it doesn't change reply from the camera. `value1` is unique for each session with LUMIXTether, but could be reused or changed (session_id ??). `value3` is an authentication factor because it changes with the password and stays the same throughout different sessions.

I have a concern about pairing because I don't see "tethered" icon on the camera display which normally appears after LumixTether startup.  @Manouchehri, do you have any ideas about that?

2. I've tried to reach camera API mentioned in https://github.com/cleverfox/lumixproto but with no success.
`curl -A "Apache-HttpClient" -v http://192.168.54.1:80/cam.cgi?mode=camcmd\&value=video_recstart`

GET /cam.cgi?mode=camcmd&value=video_recstart HTTP/1.1 Host: 192.168.54.1 User-Agent: Apache-HttpClient Accept: /

HTTP/1.1 200 OK Content-Type: text/xml Content-Length: 88 Date: Wed, 27 Apr 2022 10:08:33 GMT Server: Panasonic

<?xml version="1.0" encoding="UTF-8"?>

err_critical
caseybasichis commented 2 years ago

About to pickup a BS1H, happy to see this is being attempted.
I’m hoping to control it with a Jetson Nano or a phone.

In tinkering, have you noticed any settings for focus distance or AF area? Hard to get a feel for the features from the limited online docs.

Manouchehri commented 2 years ago

@caseybasichis The app isn't very good, but LUMIX Sync can already adjust the focus distance and AF area. LUMIX Tether also works, but there's no Linux library (or Apple Silicon dylib). See https://www.panasonic.com/global/consumer/lumix/lumix-sync-app.html and https://www.youtube.com/watch?v=HA2LfLZCIkg&t=392s

caseybasichis commented 2 years ago

@Manouchehri Thanks for that, didn’t know about the Tether app.

I need automated control from my own software; rules out the iPhone app I think. Does the desktop app have a way to control it programmatically?

Manouchehri commented 2 years ago

I have a concern about pairing because I don't see "tethered" icon on the camera display which normally appears after LumixTether startup. @Manouchehri, do you have any ideas about that?

@volodkuzn Not yet. I think that might actually be better if it's not in tethered mode, since maybe this way multiple clients could connect to the camera.

I feel like this release is incomplete, but here's some of the GPL source code for the BGH1: https://panasonic.net/cns/oss/dsc/DC-BGH1.html

The BGH1 runs a Linux 3.18 kernel. It's maybe a bit out of scope for this ticket... I'd love to get a shell on this thing.

Manouchehri commented 2 years ago

Does the desktop app have a way to control it programmatically?

@caseybasichis Currently only on Windows, and likely macOS (though it's not as documented, should still work).

caseybasichis commented 2 years ago

Does the desktop app have a way to control it programmatically?

@caseybasichis Currently only on Windows, and likely macOS (though it's not as documented, should still work).

That's exciting to hear. I can't find any doc on programmatic/API control of the BS1H through the Windows Tether app. Any leads?

Manouchehri commented 2 years ago

Does the desktop app have a way to control it programmatically?

@caseybasichis Currently only on Windows, and likely macOS (though it's not as documented, should still work).

That's exciting to hear.

I can't find any doc on programmatic/API control of the BS1H through the Windows Tether app.

Any leads?

@caseybasichis You have to download the SDK, the documentation is inside the zip folder. https://av.jpn.support.panasonic.com/support/global/cs/soft/tool/sdk.html

volodkuzn commented 2 years ago

I've made a progress recently. Below is a simple script, which does all the handshakes and captures 5 seconds of video. To run it on your machine you need to figure out IP address of your camera and a value3 for the authentication which could be eavesdropped by Wireshark. Relevant filter for Wireshark: http contains "GET /cam.cgi".

import socket
import struct
from itertools import count
from time import sleep
from typing import Tuple
import attr
import requests

@attr.s(auto_attribs=True)
class PtpIPPacket:
    """Header of PtpIPPacket
    field           example   size
    --------------+---------+------

    length          0x26      4
    pkType          0x06      4
    phaseInfo       0x01      4
    opcode          0x940c    2
    transactionID   0x01      4
    param1          0x11      4
    param2          0x01      4
    zeros           0x00*12   12"""

    length: int
    pkType: int
    phaseInfo: int
    opcode: int
    transactionID: int
    param1: int
    param2: int
    zeros1: int = 0
    zeros2: int = 0

    transactionID_counter = count(1)
    STRUCT_FORMAT: str = "<IIIHIIIQI"

    @classmethod
    def create(cls, opcode: int, param1: int, param2: int) -> "PtpIPPacket":
        return cls(
            length=0x26,
            pkType=0x06,
            phaseInfo=0x01,
            opcode=opcode,
            transactionID=next(cls.transactionID_counter),
            param1=param1,
            param2=param2,
        )

    def as_tuple(self) -> Tuple:
        return attr.astuple(self)

    def serialize(self) -> bytes:
        return struct.pack(self.STRUCT_FORMAT, *self.as_tuple()[:-1])

@attr.s(auto_attribs=True)
class CameraCommand:
    opcode: int
    param1: int
    param2: int

    def construct_packet(self) -> "PtpIPPacket":
        return PtpIPPacket.create(self.opcode, self.param1, self.param2)

    @staticmethod
    def start_capture() -> "PtpIPPacket":
        return CameraCommand(0x940C, 0x07000011, 0x00).construct_packet()

    @staticmethod
    def stop_capture() -> "PtpIPPacket":
        return CameraCommand(0x940C, 0x07000012, 0x01).construct_packet()

    @staticmethod
    def heartbeat() -> "PtpIPPacket":  # ???
        return CameraCommand(0x9414, 0, 0).construct_packet()

    @staticmethod
    def device_info() -> "PtpIPPacket":  # TESTIT
        return CameraCommand(0x1001, 0, 0).construct_packet()

def send_and_recieve(s: "socket.socket", cmd: bytes):
    s.send(cmd)
    resp = s.recv(200)
    print(resp)

def authenicate(host: str, token: str):
    headers = {"User-Agent": "Apache-HttpClient"}
    params = {"mode": "accctrl", "type": "req_acc_a", "value": "2F7A6BCD-32E8-4F08-9FD0-5052AC9D7B05", "value2": "LUMIXTether", "value3": token}
    return requests.get("http://{}/cam.cgi?".format(host), params=params, headers=headers)

camera_ip_address = "192.168.54.1"
token = "AC81E05BAC81E05B"

res = authenicate(camera_ip_address, token)
print(res.text)
port = 15740
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
s.connect((camera_ip_address, port))

initCmdReqGUID = bytearray.fromhex("3400000001000000ffffffffffffffffffffffffffffffff4c0055004d0049005800540065007400680065007200000000000100")
send_and_recieve(s, initCmdReqGUID)

event_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
event_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
event_socket.connect((camera_ip_address, port))
initEventReqCmd = bytearray.fromhex("0c0000000300000001000000")
send_and_recieve(event_socket, initEventReqCmd)

openSessionReq = bytearray.fromhex("2600000006000000010000000210000000000100010000000000000000000000000000000000")
send_and_recieve(s, openSessionReq)

print("Start capture")
ptpip_start_cmd: bytes = CameraCommand.start_capture().serialize()
send_and_recieve(s, ptpip_start_cmd)
sleep(5)

print("Stop capture")
ptpip_stop_cmd: bytes = CameraCommand.stop_capture().serialize()
send_and_recieve(s, ptpip_stop_cmd)

print("Event socket")
print("Events:", event_socket.recv(2000))
s.close()
event_socket.close()
Haavard15 commented 2 years ago

I stumbled upon this while researching controls for the BHG1, might be in interest for you guys: https://github.com/gphoto/libgphoto2/commit/fac2ae7aa20fa567eb3d13ee3e2fb4907e4dcb2a

caseybasichis commented 2 years ago

I've made a progress recently. Below is a simple script, which does all the handshakes and captures 5 seconds of video. To run it on your machine you need to figure out IP address of your camera and a value3 for the authentication which could be eavesdropped by Wireshark. Relevant filter for Wireshark: http contains "GET /cam.cgi".

import socket
import struct
from itertools import count
from time import sleep
from typing import Tuple
import attr
import requests

@attr.s(auto_attribs=True)
class PtpIPPacket:
    """Header of PtpIPPacket
    field           example   size
    --------------+---------+------

    length          0x26      4
    pkType          0x06      4
    phaseInfo       0x01      4
    opcode          0x940c    2
    transactionID   0x01      4
    param1          0x11      4
    param2          0x01      4
    zeros           0x00*12   12"""

    length: int
    pkType: int
    phaseInfo: int
    opcode: int
    transactionID: int
    param1: int
    param2: int
    zeros1: int = 0
    zeros2: int = 0

    transactionID_counter = count(1)
    STRUCT_FORMAT: str = "<IIIHIIIQI"

    @classmethod
    def create(cls, opcode: int, param1: int, param2: int) -> "PtpIPPacket":
        return cls(
            length=0x26,
            pkType=0x06,
            phaseInfo=0x01,
            opcode=opcode,
            transactionID=next(cls.transactionID_counter),
            param1=param1,
            param2=param2,
        )

    def as_tuple(self) -> Tuple:
        return attr.astuple(self)

    def serialize(self) -> bytes:
        return struct.pack(self.STRUCT_FORMAT, *self.as_tuple()[:-1])

@attr.s(auto_attribs=True)
class CameraCommand:
    opcode: int
    param1: int
    param2: int

    def construct_packet(self) -> "PtpIPPacket":
        return PtpIPPacket.create(self.opcode, self.param1, self.param2)

    @staticmethod
    def start_capture() -> "PtpIPPacket":
        return CameraCommand(0x940C, 0x07000011, 0x00).construct_packet()

    @staticmethod
    def stop_capture() -> "PtpIPPacket":
        return CameraCommand(0x940C, 0x07000012, 0x01).construct_packet()

    @staticmethod
    def heartbeat() -> "PtpIPPacket":  # ???
        return CameraCommand(0x9414, 0, 0).construct_packet()

    @staticmethod
    def device_info() -> "PtpIPPacket":  # TESTIT
        return CameraCommand(0x1001, 0, 0).construct_packet()

def send_and_recieve(s: "socket.socket", cmd: bytes):
    s.send(cmd)
    resp = s.recv(200)
    print(resp)

def authenicate(host: str, token: str):
    headers = {"User-Agent": "Apache-HttpClient"}
    params = {"mode": "accctrl", "type": "req_acc_a", "value": "2F7A6BCD-32E8-4F08-9FD0-5052AC9D7B05", "value2": "LUMIXTether", "value3": token}
    return requests.get("http://{}/cam.cgi?".format(host), params=params, headers=headers)

camera_ip_address = "192.168.54.1"
token = "AC81E05BAC81E05B"

res = authenicate(camera_ip_address, token)
print(res.text)
port = 15740
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
s.connect((camera_ip_address, port))

initCmdReqGUID = bytearray.fromhex("3400000001000000ffffffffffffffffffffffffffffffff4c0055004d0049005800540065007400680065007200000000000100")
send_and_recieve(s, initCmdReqGUID)

event_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
event_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
event_socket.connect((camera_ip_address, port))
initEventReqCmd = bytearray.fromhex("0c0000000300000001000000")
send_and_recieve(event_socket, initEventReqCmd)

openSessionReq = bytearray.fromhex("2600000006000000010000000210000000000100010000000000000000000000000000000000")
send_and_recieve(s, openSessionReq)

print("Start capture")
ptpip_start_cmd: bytes = CameraCommand.start_capture().serialize()
send_and_recieve(s, ptpip_start_cmd)
sleep(5)

print("Stop capture")
ptpip_stop_cmd: bytes = CameraCommand.stop_capture().serialize()
send_and_recieve(s, ptpip_stop_cmd)

print("Event socket")
print("Events:", event_socket.recv(2000))
s.close()
event_socket.close()

About to test this with a BS1H. Very excited to get programmatic control of the cam. Have you integrated any of the functions from the libgphoto2 package above?

I’m looking to control: slate/file_name rec start and stop x/y AutoFocus area min/max AutoFocus depth

Aside from stop/start is any of that possible?

volodkuzn commented 2 years ago

@caseybasichis, unfortunately, I haven't implemented anything except start/stop. I reworked this code to add basic event handling and tried to implement ISO change, still it's WIP. As far as I understood from the code of libgphoto2, they implemented USB control only. It's possible to reimplement ethernet version, but it's not a piece of cake. As for a possibility programmatic control, you could go to the Panasonic's official documentation and retrieve information there. It's definitely not possible to change a filename, but everything else must be possible (there are such functions in the native app).

You could find upgraded code below. Please, post there if you find insights.

import copy
import signal
import socket
import struct
import time
import warnings
from collections import defaultdict
from enum import IntEnum
from itertools import count
from multiprocessing import Process
from time import sleep
from typing import ClassVar, Dict, List, Optional, OrderedDict

import attr
import requests
import xmltodict

MAX_RETRIES_NUM: int = 6

@attr.s(auto_attribs=True)
class CameraNetworkConnection:
    ip_address: str
    session_id: str
    token: str
    ptpip_connection: Optional["PtpIPConnection"]

# NOTE: insert your camera parameters here
CAMERA_CONNECTIONS_DICT: Dict[str, CameraNetworkConnection] = {
    "camera_name": CameraNetworkConnection("ip_addr", "session_id", "token", None),
}

class PkType(IntEnum):
    Init_Command_Request = 1
    Init_Command_Ack = 2
    Init_Event_Request = 3
    Init_Event_Ack = 4
    Init_Fail = 5
    Cmd_Request = 6
    Cmd_Response = 7
    Event = 8
    Start_Data_Packet = 9
    Data_Packet = 10
    Cancel_Transaction = 11
    End_Data_Packet = 12
    Ping = 13
    Pong = 14

STRUCT_FORMAT_BY_PKTYPE = {
    PkType.Init_Command_Request: "<IIQQ24sHH",
    PkType.Init_Command_Ack: "<III16s38sHH",
    PkType.Init_Event_Request: "<III",
    PkType.Init_Event_Ack: "<II",
    PkType.Cmd_Request: "<IIIHIIIQI",
    PkType.Cmd_Response: "<IIHI20s",
    PkType.Start_Data_Packet: "<IIIQIIIIII",
    PkType.Event: "<IIHIHHIHH",
}

class EventCode(IntEnum):
    Unknown1 = 0xC102  # length = {92, 170, 192}, 59 packets, ~2 types of payload
    Unknown2 = 0xC103  # length = {92, 118, 144}, 52 packets, ~2 types of payload
    Unknown3 = 0xC104  # length = {92, 118, 144, 170, 196}, 2 packets, 1 type of payload
    Unknown4 = 0xC107  # length = {92, 170, 144, 196}, 750 packets, ~4 types of payload
    NewVideoRecorded = 0xC108  # new object download ??, 15 packets

class PtpIPPacket:
    """Header of PtpIPPacket
    field           example   size
    --------------+---------+------

    length          0x26      4
    pkType          0x06      4
    payload                   length-8"""

    length: int
    pkType: int

    transactionID_counter: ClassVar[count] = count(0)
    struct_format: str

    def __init__(self, packet_type: int):
        self.pkType = packet_type
        self.struct_format = STRUCT_FORMAT_BY_PKTYPE[PkType(packet_type)]
        self.length = struct.calcsize(self.struct_format)

    def as_tuple(self):
        raise NotImplementedError()

    def serialize(self) -> bytes:
        return struct.pack(self.struct_format, *self.as_tuple())

class Init_Command_Request(PtpIPPacket):
    """Header of Init_Command_Request
    field                    size
    ----------------+----------------

    length                    4
    pkType                    4
    c1                        8
    c2                        8
    program_name              24
    version_minor             2
    version_major             2"""

    c1: int
    c2: int
    program_name: str
    version_minor: int
    version_major: int

    def __init__(self):
        super().__init__(PkType.Init_Command_Request)
        self.c1 = (1 << 64) - 1
        self.c2 = (1 << 64) - 1
        self.program_name = "LUMIXTether".encode("UTF-16")[2:]
        # NOTE: [2:] to remove \xff\xfe endianness prefix https://stackoverflow.com/questions/53305258/is-the-0xff-0xfe-prefix-required-on-utf-16-encoded-strings
        self.version_minor = 0x00
        self.version_major = 0x01

    def as_tuple(self):
        return (self.length, self.pkType, self.c1, self.c2, self.program_name, self.version_minor, self.version_major)

@attr.s(auto_attribs=True)
class Init_Command_Ack(PtpIPPacket):
    """Header of Init_Command_Ack
    field                    size
    ----------------+----------------

    length                    4
    pkType                    4
    connection_number         4
    guid (mac_addr?)         16
    host_name                38 (wchar_t)
    version_minor             2
    version_major             2"""

    connection_number: int
    guid: bytes
    host_name: str
    version_minor: int
    version_major: int

    def __attrs_pre_init__(self):
        super().__init__(PkType.Init_Command_Ack)

    @classmethod
    def from_buffer(cls, buffer: bytes) -> "Init_Command_Ack":
        (lenght, pkType, connection_number, guid, _host_name, version_minor, version_major) = struct.unpack(
            STRUCT_FORMAT_BY_PKTYPE[PkType.Init_Command_Ack], buffer
        )
        host_name = _host_name.decode("UTF-16")
        assert host_name == "Panasonic DC-BGH1 \x00"
        return cls(connection_number, guid, host_name, version_minor, version_major)

class Init_Event_Request(PtpIPPacket):
    """Header of Init_Event_Request
    field                    size
    ----------------+----------------

    length                    4
    pkType                    4
    connection_number         4"""

    connection_number: int

    def __init__(self, connection_number: int):
        super().__init__(PkType.Init_Event_Request)
        self.connection_number = connection_number

    def as_tuple(self):
        return (self.length, self.pkType, self.connection_number)

@attr.s(auto_attribs=True)
class Init_Event_Ack(PtpIPPacket):
    """Header of Init_Event_Ack
    field                    size
    ----------------+----------------

    length                    4
    pkType                    4"""

    def __attrs_pre_init__(self):
        super().__init__(PkType.Init_Command_Ack)

    @classmethod
    def from_buffer(cls, buffer: bytes) -> "PtpIPPacket":
        length, pkType = struct.unpack(STRUCT_FORMAT_BY_PKTYPE[PkType.Init_Event_Ack], buffer)
        return cls()

class Cmd_Request(PtpIPPacket):
    """Header of Cmd_Request
    field           example   size
    --------------+---------+------

    length          0x26      4
    pkType          0x06      4
    phaseInfo       0x01      4
    opcode          0x940c    2
    transactionID   0x01      4
    param1          0x11      4
    param2          0x01      4
    zeros           0x00*12   12"""

    phaseInfo: int
    opcode: int
    transactionID: int
    param1: int
    param2: int
    zeros1: int
    zeros2: int

    def __init__(self, opcode: int, param1: int, param2: int):
        super().__init__(PkType.Cmd_Request)
        self.phaseInfo = 0x01
        self.opcode = opcode
        self.transactionID = next(super().transactionID_counter)
        self.param1 = param1
        self.param2 = param2
        self.zeros1 = 0
        self.zeros2 = 0

    def as_tuple(self):
        return (
            self.length,
            self.pkType,
            self.phaseInfo,
            self.opcode,
            self.transactionID,
            self.param1,
            self.param2,
            self.zeros1,
            self.zeros2,
        )

@attr.s(auto_attribs=True)
class Cmd_Response(PtpIPPacket):
    """Header of Cmd_Response
    field                    size
    ----------------+----------------

    length                    4
    pkType                    4
    opcode                    2
    transactionID             4 (equals to Cmd_Request)
    zeros                    20"""

    opcode: int
    transactionID: int
    zeros: bytes = bytes.fromhex("00") * 20

    def __attrs_pre_init__(self):
        super().__init__(PkType.Init_Command_Ack)

    @classmethod
    def from_buffer(cls, buffer: bytes) -> "Cmd_Response":
        length, pkType, opcode, transactionID, zeros = struct.unpack(
            STRUCT_FORMAT_BY_PKTYPE[PkType.Cmd_Response], buffer
        )
        assert zeros == bytes.fromhex("00") * 20
        return cls(opcode, transactionID)

class Start_Data_Packet(PtpIPPacket):
    """Header of Start_Data_Packet
    field           example    size
    --------------+-----------+------

    length          0x26        4
    pkType          0x06        4
    transactionID   0xf5f4      4 (equals to Cmd_Request)
    datalen         0x0c        8
    param1          0x18        4
    param2          0x0c        4
    transactionID   0xf5f4      4 (equals to field transactionID)
    opcode          0x02000031  4 (shutter or ISO)
    param3          0x04        4
    value           0x0186a0    4"""

    transactionID: int
    datalen: int
    param1: int
    param2: int
    opcode: int
    param3: int
    value: int

    def __init__(self, transactionID: int):
        super().__init__(PkType.Start_Data_Packet)
        self.transactionID = transactionID
        self.datalen = 0x0C
        self.param1 = 0x18
        self.param2 = 0x0C
        self.counter = 0xF5F4
        self.opcode = 0x02000031
        self.param3 = 0x04
        self.value = 0x0186A0

    def as_tuple(self):
        return (
            self.length,
            self.pkType,
            self.transactionID,
            self.datalen,
            self.param1,
            self.param2,
            self.transactionID,
            self.opcode,
            self.param3,
            self.value,
        )

@attr.s(auto_attribs=True)
class PtpIPEvent(PtpIPPacket):
    """Header of PtpIPEvent
    field                    size
    ----------------+----------------

    length                    4
    pkType                    4
    eventcode                 2
    transactionID             4 (equals to 0xFFFFFFFF)
    payload                   12"""

    eventcode: int
    transactionID: int
    param1: int
    param2: int
    param3: int
    param4: int
    param5: int

    def __attrs_pre_init__(self):
        super().__init__(PkType.Init_Command_Ack)

    @classmethod
    def from_buffer(cls, buffer: bytes) -> "PtpIPEvent":
        length, pkType, eventcode, transactionID, param1, param2, param3, param4, param5 = struct.unpack(
            STRUCT_FORMAT_BY_PKTYPE[PkType.Event], buffer
        )
        assert transactionID == 0xFFFFFFFF
        return cls(eventcode, transactionID, param1, param2, param3, param4, param5)

@attr.s(auto_attribs=True)
class CameraCommand:
    opcode: int
    param1: int
    param2: int

    def construct_packet(self) -> Cmd_Request:
        return Cmd_Request(self.opcode, self.param1, self.param2)

    @staticmethod
    def open_session() -> Cmd_Request:
        return CameraCommand(0x1002, 0x00010001, 0x00).construct_packet()

    @staticmethod
    def start_capture() -> Cmd_Request:
        return CameraCommand(0x940C, 0x07000011, 0x00).construct_packet()

    @staticmethod
    def stop_capture() -> Cmd_Request:
        return CameraCommand(0x940C, 0x07000012, 0x01).construct_packet()

    @staticmethod
    def heartbeat() -> Cmd_Request:  # ???
        return CameraCommand(0x9414, 0, 0).construct_packet()

    @staticmethod
    def device_info() -> Cmd_Request:  # TESTIT
        return CameraCommand(0x1001, 0, 0).construct_packet()

def init_socket(host: str, port: int) -> socket.socket:
    result = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    result.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    result.connect((host, port))
    return result

def receive_packet_from_socket(s: socket.socket):
    header_buffer = s.recv(8, socket.MSG_PEEK)
    length, _pkType = struct.unpack("<II", header_buffer)
    pktType: PkType = PkType(_pkType)
    msg_buffer = s.recv(length)
    if pktType == PkType.Init_Command_Ack:
        return Init_Command_Ack.from_buffer(msg_buffer)
    elif pktType == PkType.Init_Event_Ack:
        return Init_Event_Ack.from_buffer(msg_buffer)
    elif pktType == PkType.Cmd_Response:
        return Cmd_Response.from_buffer(msg_buffer)
    elif pktType == PkType.Event:
        return PtpIPEvent.from_buffer(msg_buffer)
    else:
        raise Exception("Unknown packet type")

def event_loop(host: str, event_socket: socket.socket, print_stats_interval: float):
    def signal_handler(signum, frame):
        event_socket.close()
        exit(0)

    signal.signal(signal.SIGTERM, signal_handler)

    events = list()
    print_time: float = time.time()
    while True:
        event = receive_packet_from_socket(event_socket)
        event_time: float = time.time()
        assert type(event) is PtpIPEvent
        events.append(event)
        if event_time - print_time > print_stats_interval:
            print_event_stats(host, events)
            print_time = event_time

def print_event_stats(host, events: List[PtpIPEvent]) -> None:
    event_dict: Dict[tuple, int] = defaultdict(int)
    for event in events:
        event_dict[(event.eventcode, event.param1)] += 1
    out_buffer = f"Printing event stats for host {host}\n"
    for key, value in event_dict.items():
        out_buffer += "Event opcode: 0x{:04X} param1 0x{:04X} occured {} times\n".format(*key, value)
    print(out_buffer)

class PtpIPConnection:
    host: str
    port: int
    token: str
    cmd_socket: socket.socket
    event_process: Process
    events: List[PtpIPEvent]

    def __init__(
        self,
        host: str,
        port: int = 15740,
        session_id: str = "2F7A6BCD-32E8-4F08-9FD0-5052AC9D7B05",
        token: str = "AC81E05BAC81E05B",
    ):
        self.host = host
        self.port = port
        PtpIPConnection.authenicate(self.host, session_id, token)
        self.cmd_socket = init_socket(self.host, self.port)
        init_cmd = Init_Command_Request()
        self.send_packet(init_cmd)
        initCmdAck = self.recieve_packet()
        event_socket = init_socket(host, port)
        initEventReqCmd = Init_Event_Request(initCmdAck.connection_number)
        event_socket.send(initEventReqCmd.serialize())
        receive_packet_from_socket(event_socket)
        self.event_process = Process(target=event_loop, args=(self.host, event_socket, 10.0))
        self.event_process.start()
        event_socket.close()
        # FIXME: close session properly and delete exception handling
        try:
            openSessionReq = CameraCommand.open_session()
            self.send_cmd(openSessionReq)
        except Exception as e:
            if "Maximum number of retries reached." not in str(e):
                raise e

    @staticmethod
    def authenicate(host: str, session_id: str, token: str):
        headers = {"User-Agent": "Apache-HttpClient"}
        params = {"mode": "accctrl", "type": "req_acc_a", "value": session_id, "value2": "LUMIXTether", "value3": token}
        answer = requests.get(f"http://{host}/cam.cgi?", params=params, headers=headers).text
        xml_answer = xmltodict.parse(answer)
        if xml_answer["camrply"]["result"] != "ok":
            raise Exception("Authentication failed: {}".format(xml_answer.camrply.result))

    def get_camera_status(self) -> OrderedDict:
        """Beware of unreliable results. For example 'rec' node is 'off' even while recording. SD cards options are ok."""
        headers = {"User-Agent": "Apache-HttpClient"}
        params = {"mode": "getstate"}
        answer = requests.get(f"http://{self.host}/cam.cgi?", params=params, headers=headers).text
        return xmltodict.parse(answer)

    def send_packet(self, packet: PtpIPPacket):
        self.cmd_socket.send(packet.serialize())

    def recieve_packet(self):
        return receive_packet_from_socket(self.cmd_socket)

    def send_cmd(self, cmd: Cmd_Request):
        attempts: int = 0
        success: bool = False
        while (attempts < MAX_RETRIES_NUM) and not success:
            attempts += 1
            self.send_packet(cmd)
            ack = self.recieve_packet()
            assert cmd.transactionID == ack.transactionID
            if ack.opcode == 0x2001:
                success = True
            elif ack.opcode == 0x2002:
                warnings.warn(f"General error occured while sending cmd {cmd.as_tuple()} to ip_addr {self.host}")
                sleep(0.2)  # provide some time to recover
            elif ack.opcode == 0x201E:
                warnings.warn("Error! error_opcode=0x{:X}".format(ack.opcode))  # already opened???
                sleep(0.1)
            else:
                raise Exception("Unknown return opcode 0x{:X}".format(ack.opcode))
        if not success:
            raise Exception("Maximum number of retries reached. Attempts: {}".format(attempts))

    def close(self):
        self.event_process.terminate()
        self.event_process.join()
        self.cmd_socket.close()

class CameraConnections:
    camera_connections: Dict[str, CameraNetworkConnection]

    def __init__(self, camera_connections: Dict[str, CameraNetworkConnection]):
        self.camera_connections = copy.deepcopy(camera_connections)

    def __enter__(self):
        for camera_name, connection in self.camera_connections.items():
            print(f"Connecting to {camera_name}")
            connection.ptpip_connection = PtpIPConnection(
                connection.ip_address, port=15740, session_id=connection.session_id, token=connection.token
            )
        return self

    def send_cmd(self, cmd: Cmd_Request):
        for connection in self.camera_connections.values():
            assert connection.ptpip_connection is not None
            connection.ptpip_connection.send_cmd(cmd)

    def print_event_stats(self):
        for connection in self.camera_connections.values():
            assert connection.ptpip_connection is not None
            connection.ptpip_connection.print_event_stats()

    def drop_events(self):
        for connection in self.camera_connections.values():
            assert connection.ptpip_connection is not None
            connection.ptpip_connection.drop_events()

    def __exit__(self, exc_type, exc_val, exc_tb):
        for camera_name, connection in self.camera_connections.items():
            print(f"Disconnecting from {camera_name}")
            connection.ptpip_connection.close()

def main():  # For testing purposes
    with CameraConnections(CAMERA_CONNECTIONS_DICT) as connections:
        for i in range(1):
            print("_______{}________".format(i))
            print("Start capture")
            connections.send_cmd(CameraCommand.start_capture())
            sleep(10)
            print("Stop capture")
            connections.send_cmd(CameraCommand.stop_capture())
            sleep(2)

if __name__ == "__main__":
    main()

# initCmdReqGUID = bytes.fromhex("3400000001000000ffffffffffffffffffffffffffffffff4c0055004d0049005800540065007400680065007200000000000100")
# initEventReqCmd = bytearray.fromhex("0c0000000300000001000000")
# initEventReqCmd = bytearray.fromhex("0c0000000300000001000000")
# openSessionReq = bytearray.fromhex("2600000006000000010000000210000000000100010000000000000000000000000000000000")