Open hamuchen opened 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.
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
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 ?
@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.
@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?
Can you post that file? I didn't see it.
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
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
Can someone who has one of these cameras download the API document and post it here?
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.
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
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):
I have no idea what that protocol does or how to use/abuse it. Is there anyone with that knowledge here?
Cheers, hamu
@hamuchen Could you share the pcap from that capture?
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:
Even trying with debug enabled did not tell me anything more than that:
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 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
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/peci1/lumix-link-desktop/blob/master/Control.html
https://github.com/palmdalian/python_lumix_control/blob/master/lumix_control.py
I see https://github.com/bitfocus/companion-module-panasonic-lumix already exists though.
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>
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.
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.
opcodes
Possible protocol realisation (tested on Nikon camera) https://github.com/mmattes/ptpip
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
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.)
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.
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"?>
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"?>
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.
@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
@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?
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.
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).
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?
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
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()
I stumbled upon this while researching controls for the BHG1, might be in interest for you guys: https://github.com/gphoto/libgphoto2/commit/fac2ae7aa20fa567eb3d13ee3e2fb4907e4dcb2a
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?
@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")
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