├── pmca ├── __init__.py ├── commands │ ├── __init__.py │ ├── market.py │ └── usb.py ├── marketserver │ ├── constants.py │ ├── __init__.py │ └── server.py ├── usb │ ├── driver │ │ ├── windows │ │ │ ├── __init__.py │ │ │ ├── wpd.py │ │ │ └── msc.py │ │ ├── __init__.py │ │ ├── osx.py │ │ └── libusb.py │ ├── __init__.py │ └── sony.py ├── xpd │ ├── constants.py │ └── __init__.py ├── spk │ ├── util.py │ ├── constants.py │ └── __init__.py ├── marketclient │ ├── constants.py │ └── __init__.py ├── appstore │ ├── github.py │ └── __init__.py ├── firmware │ └── __init__.py ├── util │ ├── __init__.py │ └── http.py ├── ui │ └── __init__.py └── installer │ └── __init__.py ├── .gitignore ├── cron.yaml ├── robots.txt ├── requirements.txt ├── config.py ├── pmca-gui.spec ├── pmca-console.spec ├── hooks ├── hook-Crypto.py └── rthook-Crypto.py ├── templates ├── plugin │ ├── install.html │ └── start.html ├── apk │ └── upload.html ├── layout.html ├── apps │ └── list.html ├── task │ └── view.html └── home.html ├── app.yaml ├── appveyor.yml ├── LICENSE.txt ├── static ├── style.css └── script.js ├── .travis.yml ├── docs └── AppInstallation.md ├── pmca-console.py ├── README.md ├── certs └── localtest.me.pem ├── pmca-gui.py └── main.py /pmca/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pmca/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | *.pyc 4 | -------------------------------------------------------------------------------- /cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: cleanup 3 | url: /cleanup 4 | schedule: every 60 mins 5 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /ajax/ 3 | Disallow: /camera/ 4 | Disallow: /download/ 5 | Disallow: /plugin/ 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi 2 | comtypes 3 | pycryptodome 4 | # Use an old version of pyinstaller until bug #1957 is fixed 5 | pyinstaller==3.1.0 6 | pyusb 7 | pyyaml 8 | -------------------------------------------------------------------------------- /pmca/marketserver/constants.py: -------------------------------------------------------------------------------- 1 | from .. import spk 2 | 3 | # This is not quite correct, it should be 'application/json', but it only works that way 4 | jsonMimeType = spk.constants.mimeType 5 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | githubClientId = '' 2 | githubClientSecret = '' 3 | 4 | githubAppListUser = 'ma1co' 5 | githubAppListRepo = 'OpenMemories-AppList' 6 | 7 | appengineServer = 'sony-pmca.appspot.com' 8 | -------------------------------------------------------------------------------- /pmca-gui.spec: -------------------------------------------------------------------------------- 1 | # Run `pyinstaller pmca-gui.spec` to generate an executable 2 | 3 | input = 'pmca-gui.py' 4 | output = 'pmca-gui' 5 | console = False 6 | 7 | with open('build.spec') as f: 8 | exec(f.read()) 9 | -------------------------------------------------------------------------------- /pmca-console.spec: -------------------------------------------------------------------------------- 1 | # Run `pyinstaller pmca-console.spec` to generate an executable 2 | 3 | input = 'pmca-console.py' 4 | output = 'pmca-console' 5 | console = True 6 | 7 | with open('build.spec') as f: 8 | exec(f.read()) 9 | -------------------------------------------------------------------------------- /hooks/hook-Crypto.py: -------------------------------------------------------------------------------- 1 | # Hook for pycryptodome extensions 2 | 3 | hiddenimports = [ 4 | 'Crypto.Cipher._raw_aes', 5 | 'Crypto.Cipher._raw_ecb', 6 | 'Crypto.Hash._SHA256', 7 | 'Crypto.Util._cpuid', 8 | 'Crypto.Util._strxor', 9 | ] 10 | -------------------------------------------------------------------------------- /pmca/usb/driver/windows/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def parseDeviceId(id): 4 | match = re.search('(#|\\\\)vid_([a-f0-9]{4})&pid_([a-f0-9]{4})(&|#|\\\\)', id, re.IGNORECASE) 5 | return [int(match.group(i), 16) if match else None for i in [2, 3]] 6 | -------------------------------------------------------------------------------- /templates/plugin/install.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% set title = 'Plugin - Installation' %} 3 | {% block content %} 4 |

Install plugin

5 |

The help text from the Sony website:

6 |
7 | {{ text | safe }} 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /pmca/xpd/constants.py: -------------------------------------------------------------------------------- 1 | mimeType = 'application/x-psn-dstartup2' 2 | sectionName = 'DrmTCD' 3 | 4 | # ScalarAMarket.apk/lib/libscalaramarket-jni.so, ScalarAUsbDlApp.apk/lib/libjniusbdluser.so 5 | cicKey = b'8595e68aa50d25dcc52b4d6e6a62af526efd7523a4cc47e212e82e979728d6f0dd02c7e4e79ddb317d56fea2bd' 6 | -------------------------------------------------------------------------------- /pmca/usb/driver/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from ...util import * 4 | 5 | USB_CLASS_PTP = 6 6 | USB_CLASS_MSC = 8 7 | 8 | UsbDevice = namedtuple('UsbDevice', 'handle, idVendor, idProduct') 9 | 10 | MSC_SENSE_OK = (0, 0, 0) 11 | MSC_SENSE_ERROR_UNKNOWN = (0x2, 0xff, 0xff) 12 | 13 | def parseMscSense(buffer): 14 | return parse8(buffer[2:3]) & 0xf, parse8(buffer[12:13]), parse8(buffer[13:14]) 15 | -------------------------------------------------------------------------------- /hooks/rthook-Crypto.py: -------------------------------------------------------------------------------- 1 | # Runtime hook for pycryptodome extensions 2 | 3 | import Crypto.Util._raw_api 4 | import importlib.machinery 5 | import os.path 6 | import sys 7 | 8 | def load_raw_lib(name, cdecl): 9 | for ext in importlib.machinery.EXTENSION_SUFFIXES: 10 | try: 11 | return Crypto.Util._raw_api.load_lib(os.path.join(sys._MEIPASS, name + ext), cdecl) 12 | except OSError: 13 | pass 14 | 15 | Crypto.Util._raw_api.load_pycryptodome_raw_lib = load_raw_lib 16 | -------------------------------------------------------------------------------- /pmca/spk/util.py: -------------------------------------------------------------------------------- 1 | """Some methods to manage binary data""" 2 | from ..util import * 3 | 4 | def pad(data, size): 5 | """Applies PKCS#7 padding to the supplied string""" 6 | n = size - len(data) % size 7 | return data + n * dump8(n) 8 | 9 | def unpad(data): 10 | """Removes PKCS#7 padding from the supplied string""" 11 | return data[:-parse8(data[-1:])] 12 | 13 | def chunk(data, size): 14 | """Splits a string in chunks of the given size""" 15 | return (data[i:i+size] for i in range(0, len(data), size)) 16 | -------------------------------------------------------------------------------- /templates/apk/upload.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% set title = 'Apk Upload' %} 3 | {% block content %} 4 |

Apk file selection

5 |

To try the USB connection, click here.

6 |
7 |

Please select the apk file you'd like to install on your camera.

8 |

9 |

10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /pmca/marketclient/constants.py: -------------------------------------------------------------------------------- 1 | baseUrl = 'https://www.playmemoriescameraapps.com/portal' 2 | loginUrl = 'https://account.sonyentertainmentnetwork.com/liquid/external/auth/login!authenticate.action' 3 | registerUrl = 'https://account.sonyentertainmentnetwork.com/liquid/external/create-account!input.action' 4 | cameraUserAgent = "Mozilla/5.0 (Build/sccamera)" 5 | localeUs = '99' 6 | 7 | # ScalarAMarket.apk/lib/libscalaramarket-jni.so, ScalarAUsbDlApp.apk/lib/libjniusbdluser.so 8 | downloadAuthUser = 'nexd13c6a29639b35aca8499e8e5f01acf074186849c6d7008974e8daff0f64d822edd4f9218842c' 9 | downloadAuthPassword = '883de3c42682b5f4f6e7e8e5c9fb07860613594eebae9aa77f79a9b86662f6408403e36c48511d47' 10 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: sony-pmca 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | 7 | libraries: 8 | - name: jinja2 9 | version: latest 10 | - name: pycrypto 11 | version: latest 12 | - name: webapp2 13 | version: latest 14 | 15 | handlers: 16 | - url: /(robots\.txt) 17 | static_files: \1 18 | upload: robots\.txt 19 | secure: always 20 | - url: /static 21 | static_dir: static 22 | secure: always 23 | - url: /camera/xpd/.+ 24 | script: main.app 25 | secure: optional 26 | - url: /cleanup 27 | login: admin 28 | auth_fail_action: unauthorized 29 | script: main.app 30 | secure: always 31 | - url: /.* 32 | script: main.app 33 | secure: always 34 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | {% block additionalHead %}{% endblock %} 8 | 9 | 10 | 11 |
12 |
OpenMemories
13 | 18 | {% block content %}{% endblock %} 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /pmca/appstore/github.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ..util import http 3 | 4 | class GithubApi(object): 5 | def __init__(self, user, repo, client=None): 6 | self.apiBase = 'https://api.github.com' 7 | self.rawBase = 'https://raw.githubusercontent.com' 8 | self.user = user 9 | self.repo = repo 10 | self.client = client 11 | 12 | def request(self, endpoint): 13 | url = '/repos/%s/%s' % (self.user, self.repo) + endpoint 14 | if self.client: 15 | url += '?client_id=%s&client_secret=%s' % self.client 16 | return json.loads(http.get(self.apiBase + url).data) 17 | 18 | def getFile(self, branch, path): 19 | return http.get(self.rawBase + '/%s/%s/%s/%s' % (self.user, self.repo, branch, path)).data 20 | 21 | def getReleases(self): 22 | return self.request('/releases') 23 | -------------------------------------------------------------------------------- /templates/apps/list.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% set title = 'Apps' %} 3 | {% block content %} 4 |

Apps

5 |

Here is a list of apps for your camera. Use them at your own risk!

6 | 16 |

Want to add your own? Send us a pull request.

17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/task/view.html: -------------------------------------------------------------------------------- 1 | {% if completed %} 2 |

Device

3 | 13 | 14 |

Installed apps

15 | 20 | {% else %} 21 | (no response yet) 22 | {% endif %} 23 | -------------------------------------------------------------------------------- /pmca/firmware/__init__.py: -------------------------------------------------------------------------------- 1 | """Firmware data file parser""" 2 | 3 | import os 4 | from ..util import * 5 | 6 | DatHeader = Struct('DatHeader', [ 7 | ('magic', Struct.STR % 8), 8 | ]) 9 | datHeaderMagic = b'\x89\x55\x46\x55\x0d\x0a\x1a\x0a' 10 | 11 | DatChunkHeader = Struct('DatChunkHeader', [ 12 | ('size', Struct.INT32), 13 | ('type', Struct.STR % 4), 14 | ], Struct.BIG_ENDIAN) 15 | 16 | def readDat(file): 17 | """"Extract the firmware from a .dat file. Returns the offset and the size of the firmware data.""" 18 | file.seek(0) 19 | header = DatHeader.unpack(file.read(DatHeader.size)) 20 | if header.magic != datHeaderMagic: 21 | raise Exception('Wrong magic') 22 | 23 | while True: 24 | data = file.read(DatChunkHeader.size) 25 | if len(data) != DatChunkHeader.size: 26 | break 27 | chunk = DatChunkHeader.unpack(data) 28 | if chunk.type == b'FDAT': 29 | return file.tell(), chunk.size 30 | file.seek(chunk.size, os.SEEK_CUR) 31 | raise Exception('FDAT chunk not found') 32 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Run windows builds on appveyor.com 2 | 3 | version: "Build #{build}" 4 | 5 | environment: 6 | PYTHON: C:\Python35 7 | 8 | install: 9 | # Path setup 10 | - set PATH=%PYTHON%;%PYTHON%\Scripts;%APPVEYOR_BUILD_FOLDER%;%PATH% 11 | 12 | # Install dependencies 13 | - pip install -r requirements.txt 14 | 15 | # Log versions 16 | - python --version 17 | - pip list 18 | 19 | # Download libusb 20 | - appveyor DownloadFile http://jaist.dl.sourceforge.net/project/libusb-win32/libusb-win32-releases/1.2.6.0/libusb-win32-bin-1.2.6.0.zip 21 | - 7z x libusb-win32-bin-1.2.6.0.zip 22 | - move libusb-win32-bin-1.2.6.0\bin\x86\libusb0_x86.dll libusb0.dll 23 | 24 | build: off 25 | 26 | after_test: 27 | # Run pyinstaller 28 | - pyinstaller pmca-console.spec 29 | - pyinstaller pmca-gui.spec 30 | 31 | artifacts: 32 | - path: dist\* 33 | 34 | deploy: 35 | # Deploy tagged releases 36 | - provider: GitHub 37 | auth_token: {secure: fKuIKISPj15kzBq1zlLtAyscXK43FG6oQQPMw8k8rsbVXRYZfCYtK3XSn6AQDlip} 38 | artifact: /.*/ 39 | on: {appveyor_repo_tag: true} 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ma1co 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pmca/xpd/__init__.py: -------------------------------------------------------------------------------- 1 | """Methods for reading and writing xpd files""" 2 | 3 | from Crypto.Hash import HMAC, SHA256 4 | import sys 5 | 6 | try: 7 | from configparser import ConfigParser 8 | except ImportError: 9 | # Python 2 10 | from ConfigParser import ConfigParser 11 | 12 | if sys.version_info >= (3,): 13 | from io import StringIO 14 | else: 15 | # Python 2 16 | from StringIO import StringIO 17 | 18 | from . import constants 19 | 20 | def parse(data): 21 | """Parses an xpd file 22 | 23 | Returns: 24 | A dict containing the properties 25 | """ 26 | config = ConfigParser() 27 | config.optionxform = str 28 | config.read_file(StringIO(data.decode('latin1'))) 29 | return dict(config.items(constants.sectionName)) 30 | 31 | def dump(items): 32 | """Builds an xpd file""" 33 | config = ConfigParser() 34 | config.optionxform = str 35 | config.add_section(constants.sectionName) 36 | for k, v in items.items(): 37 | config.set(constants.sectionName, k, v) 38 | f = StringIO() 39 | config.write(f) 40 | return f.getvalue().encode('latin1') 41 | 42 | def calculateChecksum(data): 43 | """The function used to calculate CIC checksums for xpd files""" 44 | return HMAC.new(constants.cicKey, data, SHA256).hexdigest() 45 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | font-size: 14px; 4 | margin: 0; 5 | line-height: 1.5em; 6 | } 7 | 8 | h1 { 9 | font-size: 2em; 10 | margin: 1em 0 .5em; 11 | } 12 | 13 | h2 { 14 | font-size: 1.5em; 15 | margin: 1em 0 .5em; 16 | } 17 | 18 | p { 19 | margin: 0 0 1em; 20 | } 21 | 22 | a { 23 | color: #000; 24 | } 25 | 26 | label { 27 | display: block; 28 | } 29 | 30 | .wrapper { 31 | max-width: 600px; 32 | margin: 0 auto; 33 | padding: 15px; 34 | } 35 | 36 | .header { 37 | font-size: 3em; 38 | line-height: 1em; 39 | font-weight: bold; 40 | } 41 | 42 | .nav { 43 | margin: 10px -10px 20px; 44 | line-height: 2em; 45 | border: 1px #aaa; 46 | border-style: solid none solid; 47 | } 48 | 49 | .nav a { 50 | text-decoration: none; 51 | margin: 0 10px; 52 | } 53 | 54 | .github { 55 | position: absolute; 56 | top: 0; 57 | right: 0; 58 | width: 149px; 59 | height: 149px; 60 | background: url(https://aral.github.io/fork-me-on-github-retina-ribbons/right-graphite.png); 61 | } 62 | 63 | .box { 64 | background: #eee; 65 | padding: 10px; 66 | height: 200px; 67 | overflow: auto; 68 | font-family: monospace; 69 | font-size: .8em; 70 | line-height: 1.5em; 71 | } 72 | 73 | .apps li { 74 | margin-bottom: 1em; 75 | } 76 | 77 | .small, .small a { 78 | font-size: .8em; 79 | color: #aaa; 80 | } 81 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% set title = 'Home' %} 3 | {% block content %} 4 |

Home

5 |

The latest Sony cameras include an Android subsystem used to run apps from the proprietary Sony PlayMemories Camera App Store (PMCA). We reverse engineered the installation process. This allows you to install custom Android apps on your camera.

6 | 10 | 11 |

How-to

12 |

Upload your own Android app to install it on your camera. Or try the demo app!

13 | 14 |

Caution!

15 |

This is an experiment in a very early stage. All information has been found through reverse engineering. Even though everything worked fine for our developers, it could cause harm to your hardware. If you break your camera, you get to keep both pieces. We won't take any responsability.

16 |

However, we're just interacting with the isolated Android subsystem. Apps you install this way can be uninstalled in the applications manager. In the worst case, a factory reset of the camera clears all data on the Android partitions.

17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/plugin/start.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% set title = 'USB Communication' %} 3 | {% block additionalHead %} 4 | 5 | 10 | {% endblock %} 11 | {% block content %} 12 |

USB Communication

13 |

This page allows you to communicate with your camera through the USB connection of your computer. Make sure you switch your camera to MTP mode, connect it via USB and turn it on.

14 |

If you haven't installed the PMCA Downloader plugin, install it here.

15 |

If you have problems, please try it again using Internet Explorer.

16 | 17 | 18 | {% if blob %} 19 | 20 | {% endif %} 21 | {% if app %} 22 | 23 | {% endif %} 24 | 25 |

Response

26 |
27 | {% set completed = false %} 28 | {% include 'task/view.html' %} 29 |
30 | 31 |

Log

32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /pmca/util/__init__.py: -------------------------------------------------------------------------------- 1 | """Some utility functions to pack and unpack integers""" 2 | 3 | import struct 4 | from collections import namedtuple 5 | 6 | def parse32le(data): 7 | return struct.unpack('I', data)[0] 14 | 15 | def dump32be(value): 16 | return struct.pack('>I', value) 17 | 18 | def parse16le(data): 19 | return struct.unpack('H', data)[0] 26 | 27 | def dump16be(value): 28 | return struct.pack('>H', value) 29 | 30 | def parse8(data): 31 | return struct.unpack('B', data)[0] 32 | 33 | def dump8(value): 34 | return struct.pack('B', value) 35 | 36 | class Struct(object): 37 | LITTLE_ENDIAN = '<' 38 | BIG_ENDIAN = '>' 39 | PADDING = '%dx' 40 | CHAR = 'c' 41 | STR = '%ds' 42 | INT64 = 'Q' 43 | INT32 = 'I' 44 | INT16 = 'H' 45 | INT8 = 'B' 46 | 47 | def __init__(self, name, fields, byteorder=LITTLE_ENDIAN): 48 | self.tuple = namedtuple(name, (n for n, fmt in fields if not isinstance(fmt, int))) 49 | self.format = byteorder + ''.join(self.PADDING % fmt if isinstance(fmt, int) else fmt for n, fmt in fields) 50 | self.size = struct.calcsize(self.format) 51 | 52 | def unpack(self, data, offset = 0): 53 | return self.tuple(*struct.unpack_from(self.format, data, offset)) 54 | 55 | def pack(self, **kwargs): 56 | return struct.pack(self.format, *self.tuple(**kwargs)) 57 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Run os x builds on travis-ci.org 2 | 3 | os: osx 4 | language: generic 5 | 6 | before_install: 7 | # Update brew 8 | - brew update 9 | - brew --version 10 | 11 | # Install python 12 | - brew install pyenv || brew upgrade pyenv 13 | - eval "$(pyenv init -)" 14 | - env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.5.2 15 | - pyenv local 3.5.2 16 | 17 | # Install libusb 18 | - brew install libusb || brew upgrade libusb 19 | 20 | install: 21 | # Install dependencies 22 | - pip install -r requirements.txt 23 | 24 | # Log versions 25 | - python --version 26 | - pip list 27 | 28 | script: 29 | # Run pyinstaller 30 | - pyinstaller pmca-console.spec 31 | - pyinstaller pmca-gui.spec 32 | 33 | deploy: 34 | # Deploy tagged releases 35 | provider: releases 36 | api_key: {secure: PWMg2dhPJgK4mww9yJcz3myo6oZwSMZU+2MvwDnvMvdZTaUPexHeiszX7RwBpzLNgPB+uJ7dgnjYJLm+LhgVgmeOhioIw9GDzVaC0rwfVVp94Q1tTHoXNlaDAfXQobY5p5avdjA2lSpvTuMDeiuAz2synsU5xmEcVbnhMtoMi8bsm4Nly63519yNkjyaYQU6BKR1lpHy9fh952YFcL2f4vRMYeq2aB3YLBCP1n9AkuocNEXoP+U03Ol+NpMWYcwBJGDunDo7dXkXSlBN5H4Re8JhAcKxfbuvnlkgb+bm+0OyOl0ePHPLOvShxeQlnI6A8NdmBBHLPBE2IIJw4pkhiU5wXvt70/teJLzww/90jNUNw+O7Z4e+HdBCLGENv4c9TnDnqU1RJKmcnWxWIbUNuCAzIsT5NGY7/lrOvlNJTVJ8O/dfmE/mRcGTJsXZyGEviysiLax56mRK4K07l0d5g6giPk0DadIBkANoBKzp4NDeB1E8VC7p0tAJZ1MaYHK8qEGelgtEFxnQm3NVnMv8tLZZEKLPCwgTJZTnyX+oJ/UxfHqO23hw+ntpRL9+dlLQouKnck+FEmGwYUUmIRfzGA21jpBprahEXENtn0efSd0fbYG0PvkrQRA/C4QuLOTkYgWJHWO+9dOww21rJ7YY4sp5yRMeXaBYj1zglUxyecY=} 37 | file_glob: true 38 | file: dist/* 39 | skip_cleanup: true 40 | on: {tags: true} 41 | -------------------------------------------------------------------------------- /pmca/commands/market.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from getpass import getpass 3 | import os 4 | import re 5 | import sys 6 | 7 | if sys.version_info < (3,): 8 | # Python 2 9 | input = raw_input 10 | 11 | from .. import marketclient 12 | from .. import spk 13 | 14 | def marketCommand(token=None): 15 | if not token: 16 | print('Please enter your Sony Entertainment Network credentials\n') 17 | email = input('Email: ') 18 | password = getpass('Password: ') 19 | token = marketclient.login(email, password) 20 | if token: 21 | print('Login successful. Your auth token (use with the -t option):\n%s\n' % token) 22 | else: 23 | print('Login failed') 24 | return 25 | 26 | devices = marketclient.getDevices(token) 27 | print('%d devices found\n' % len(devices)) 28 | 29 | apps = [] 30 | for device in devices: 31 | print('%s (%s)' % (device.name, device.serial)) 32 | for app in marketclient.getApps(device.name): 33 | if not app.price: 34 | apps.append((device.deviceid, app.id)) 35 | print(' [%2d] %s' % (len(apps), app.name)) 36 | print('') 37 | 38 | if apps: 39 | while True: 40 | i = int(input('Enter number of app to download (0 to exit): ')) 41 | if i == 0: 42 | break 43 | app = apps[i - 1] 44 | print('Downloading app %s' % app[1]) 45 | spkName, spkData = marketclient.download(token, app[0], app[1]) 46 | fn = re.sub('(%s)?$' % re.escape(spk.constants.extension), '.apk', spkName) 47 | data = spk.parse(spkData) 48 | 49 | if os.path.exists(fn): 50 | print('File %s exists already' % fn) 51 | else: 52 | with open(fn, 'wb') as f: 53 | f.write(data) 54 | print('App written to %s' % fn) 55 | print('') 56 | -------------------------------------------------------------------------------- /pmca/marketserver/__init__.py: -------------------------------------------------------------------------------- 1 | """Provides the server part of the functionality used to download apps over USB""" 2 | 3 | import json 4 | 5 | from . import constants 6 | from .. import xpd 7 | 8 | def parsePostData(data): 9 | """Parses the data submitted by the camera to the portal url 10 | 11 | Returns: 12 | { 13 | 'accountinfo': { 14 | 'signinid': 'appstore email address', 15 | 'accountid': 'appstore portal id', 16 | 'registered': '1', 17 | }, 18 | 'applications': [{ 19 | 'name': 'application name', 20 | 'version': 'application version', 21 | }, ...], 22 | 'deviceinfo': { 23 | 'deviceid': 'device id', 24 | 'fwversion': 'firmware version', 25 | 'productcode': 'product code', 26 | 'name': 'camera name', 27 | 'battery': 'battery level', 28 | 'freespace': 'free space', 29 | 'storagetotalsize': 'total space', 30 | }, 31 | 'session': { 32 | 'correlationid': 'correlation id', 33 | }, 34 | 'profileversion': { 35 | 'version': '1', 36 | }, 37 | } 38 | """ 39 | return json.loads(data.decode('latin1')) 40 | 41 | def getXpdResponse(correlation, url): 42 | """Creates an xpd file which points the camera to the supplied portal url""" 43 | return xpd.dump({ 44 | 'TCD': url, 45 | 'TKN': correlation, 46 | 'CIC': xpd.calculateChecksum(url.encode('latin1')) 47 | }) 48 | 49 | def getJsonInstallResponse(appName, spkUrl): 50 | """Creates the response that has to be returned by the portal url to install the given app""" 51 | return json.dumps({"actions": [{ 52 | "command": "dlandinstall", 53 | "args": spkUrl, 54 | "attrs": [{ 55 | "attrname": "appname", 56 | "attrvalue": appName, 57 | }], 58 | }]}).encode('latin1') 59 | 60 | def getJsonResponse(): 61 | """Creates the response that has to be returned by the portal url if no more actions have to be taken""" 62 | return json.dumps({"actions": []}).encode('latin1') 63 | -------------------------------------------------------------------------------- /pmca/spk/constants.py: -------------------------------------------------------------------------------- 1 | mimeType = 'application/vnd.sony.spk.package-archive' 2 | extension = '.1.spk' 3 | blockSize = 0x100000 4 | paddingSize = 16 5 | 6 | # ScalarAInstaller.apk/lib/librsaforinstaller.so 7 | rsaModulus = 18546786124031929736450043032989194943881708896956310049376885815353452452737129160084493534424259917885859685404970368509019605185259963806563452189947051455437552314477916953143062044204000233355686480383169448909078186398717520581109145341225470292554937903142958836844346971293647348222873983594916965142879437008833178868483114882377493704135238091195859303789511256231440821207663503100500629361938642335839866157113610679689132138679971646420026197437282015245989139338190682985702090970027710310551550127193083201786677858304822056545507776969720889751356415799936270667430411556285015304886252479659945984429 8 | rsaExponent = 65537 9 | 10 | # We need a string which gives a 128bit AES key when decrypted with RSA. 11 | # Luckily, such a string is provided in every spk file from the appstore. 12 | # Just copy 256 bytes, starting at 0x0C. 13 | # This one is taken from TouchLessShutter100.1.spk 14 | sampleSpkKey = b'\x7e\x29\x35\x14\x25\xec\x82\xc6\x1e\xf1\xd7\x36\xaf\xad\xc2\x80\x96\x6a\x2d\xad\xd5\x3f\xfe\xe3\xd5\x5e\x60\x8a\xfa\xd4\x39\x51\x85\x3a\x1b\xe9\xe3\x62\x65\xb0\x5c\x1e\x43\x45\xac\x49\x19\xd6\xc9\xef\x4e\x02\x1f\x58\xbf\x85\xe4\x85\x1c\x3c\xb2\xbd\x14\x41\x6d\x26\x15\x26\x29\x65\x23\x25\x96\x64\xbe\x8a\xc2\x47\x7f\x7b\xd6\xd1\xc1\x62\xaf\x28\x6c\x65\xa7\xf5\x7a\x18\x00\xe4\x89\xcf\x24\xfc\x58\xfb\x04\x1f\x29\xea\x10\x3f\x5f\xca\x3e\xb9\x96\xba\xaf\x8e\xea\x2c\xfd\x64\x31\xbb\x76\x5d\xce\xd8\x11\x0b\x34\x1d\xb6\xd3\x13\xdc\x40\xa8\x2a\x6e\x21\x34\x27\x2e\x3a\xa3\xc7\x57\x0b\x80\xd5\xd1\xd7\x53\x3f\x2e\xfc\xf3\xff\x0a\x00\xf9\x16\x91\x84\x74\x17\x00\x5d\xa8\x41\xfd\x29\x06\xac\x7e\x07\x67\xfa\xfb\xbb\x1e\x7e\xa0\xe1\xc7\x68\x28\xac\xe6\x6f\xdd\x44\x95\x06\x60\x9c\xc6\x04\xc8\xab\xf1\x1d\x94\x14\xa7\xcb\x29\x6d\x93\x84\x1b\x3a\x06\x2c\x05\xe5\xa3\xf3\x40\x55\x6d\xc2\x5e\x6c\x0e\x46\x74\x66\x24\x2f\xf4\x33\xcc\x20\x0e\xf9\xf2\x9d\x9b\xbd\xf6\x22\x72\x12\xef\x40\xbc\x43\xcb\x74\xb1\xdf\x02\xbe\x31\x53\xa3\x34\xa2' 15 | -------------------------------------------------------------------------------- /pmca/ui/__init__.py: -------------------------------------------------------------------------------- 1 | """Gui related classes""" 2 | import threading 3 | 4 | try: 5 | from queue import Queue, Empty 6 | from tkinter import * 7 | from tkinter.ttk import * 8 | from tkinter.filedialog import askopenfilename 9 | except ImportError: 10 | # Python 2 11 | from Queue import Queue, Empty 12 | from Tkinter import * 13 | from ttk import * 14 | from tkFileDialog import askopenfilename 15 | 16 | class UiRoot(Tk): 17 | def __init__(self): 18 | Tk.__init__(self) 19 | self._queue = Queue() 20 | self._processQueue() 21 | 22 | def run(self, func): 23 | """Post functions to run them on the main thread""" 24 | self._queue.put(func) 25 | 26 | def _processQueue(self): 27 | while True: 28 | try: 29 | self._queue.get(block=False)() 30 | except Empty: 31 | break 32 | self.after(100, self._processQueue) 33 | 34 | 35 | class UiFrame(Frame): 36 | def __init__(self, parent, **kwargs): 37 | Frame.__init__(self, parent, **kwargs) 38 | self._parent = parent 39 | 40 | def run(self, func): 41 | self._parent.run(func) 42 | 43 | 44 | class BackgroundTask(object): 45 | """Similar to Android's AsyncTask""" 46 | def __init__(self, ui): 47 | self.ui = ui 48 | 49 | def doBefore(self): 50 | """Runs on the main thread, returns arg""" 51 | pass 52 | def do(self, arg): 53 | """Runs on a separate thread, returns result""" 54 | pass 55 | def doAfter(self, result): 56 | """Runs on the main thread again""" 57 | pass 58 | 59 | def run(self): 60 | """Invoke this on the main thread only""" 61 | arg = self.doBefore() 62 | threading.Thread(target=self._onThread, args=[arg]).start() 63 | 64 | def _onThread(self, arg): 65 | result = self.do(arg) 66 | self.ui.run(lambda: self.doAfter(result)) 67 | 68 | 69 | class ScrollingText(Frame): 70 | """A wrapper for a Text widget with a scrollbar""" 71 | def __init__(self, parent): 72 | Frame.__init__(self, parent) 73 | Grid.columnconfigure(self, 0, weight=1) 74 | Grid.rowconfigure(self, 0, weight=1) 75 | self.scrollbar = Scrollbar(self) 76 | self.scrollbar.grid(row=0, column=1, sticky=N+S) 77 | self.text = Text(self, yscrollcommand=self.scrollbar.set) 78 | self.text.grid(row=0, column=0, sticky=N+S+W+E) 79 | self.scrollbar.config(command=self.text.yview) 80 | -------------------------------------------------------------------------------- /pmca/util/http.py: -------------------------------------------------------------------------------- 1 | """Some methods to make HTTP requests""" 2 | 3 | from collections import namedtuple 4 | import random 5 | import string 6 | 7 | try: 8 | from urllib.parse import * 9 | from urllib.request import * 10 | from http.cookiejar import * 11 | except ImportError: 12 | # Python 2 13 | from urllib import * 14 | from urllib2 import * 15 | from cookielib import * 16 | from urlparse import * 17 | 18 | HttpResponse = namedtuple('HttpResponse', 'url, data, raw_data, headers, cookies') 19 | 20 | def postForm(url, data, headers={}, cookies={}, auth=None): 21 | return post(url, urlencode(data).encode('latin1'), headers, cookies, auth) 22 | 23 | def postFile(url, fileName, fileData, fieldName='', headers={}, cookies={}, auth=None): 24 | boundary = ''.join(random.choice('-_' + string.digits + string.ascii_letters) for _ in range(40)) 25 | headers['Content-type'] = 'multipart/form-data; boundary=%s' % boundary 26 | data = b'\r\n'.join([ 27 | b'--' + boundary.encode('latin1'), 28 | b'Content-Disposition: form-data; name="' + fieldName.encode('latin1') + b'"; filename="' + fileName.encode('latin1') + b'"', 29 | b'', 30 | fileData, 31 | b'--' + boundary.encode('latin1') + b'--', 32 | b'', 33 | ]) 34 | return request(url, data, headers, cookies, auth) 35 | 36 | def get(url, data={}, headers={}, cookies={}, auth=None): 37 | if data: 38 | url += '?' + urlencode(data) 39 | return request(url, None, headers, cookies, auth) 40 | 41 | def post(url, data, headers={}, cookies={}, auth=None): 42 | return request(url, data, headers, cookies, auth) 43 | 44 | def request(url, data=None, headers={}, cookies={}, auth=None): 45 | if cookies: 46 | headers['Cookie'] = '; '.join(quote(k) + '=' + quote(v) for (k, v) in cookies.items()) 47 | request = Request(str(url), data, headers) 48 | manager = HTTPPasswordMgrWithDefaultRealm() 49 | if auth: 50 | manager.add_password(None, request.get_full_url(), auth[0], auth[1]) 51 | handlers = [HTTPBasicAuthHandler(manager), HTTPDigestAuthHandler(manager)] 52 | try: 53 | import certifi, ssl 54 | handlers.append(HTTPSHandler(context=ssl.create_default_context(cafile=certifi.where()))) 55 | except: 56 | # App engine 57 | pass 58 | response = build_opener(*handlers).open(request) 59 | cj = CookieJar() 60 | cj.extract_cookies(response, request) 61 | headers = dict(response.headers) 62 | raw_contents = response.read() 63 | contents = raw_contents.decode(headers.get('charset', 'latin1')) 64 | return HttpResponse(urlparse(response.geturl()), contents, raw_contents, headers, dict((c.name, c.value) for c in cj)) 65 | -------------------------------------------------------------------------------- /docs/AppInstallation.md: -------------------------------------------------------------------------------- 1 | ## App installation ## 2 | This describes how Android apps are installed on Sony cameras. 3 | 4 | ### Where to begin... ### 5 | If you want to dig into the firmware yourself, here's what to do: 6 | 7 | 1. Use [fwtool](https://github.com/ma1co/fwtool.py) to unpack your favorite camera's firmware 8 | 2. One of the images in the *0700\_part_image* directory contains the Android partition with the interesting apps in the *app* directory 9 | 10 | ### ScalarAMarket ### 11 | This app is used to download apps directly on your camera via the built-in wifi. 12 | It is basically a WebKit wrapper which allows you to access the Sony app store with the special User-Agent header `Mozilla/5.0 (Build/sccamera)`. The requests are redirected to `wifi***.php`. As soon as you decide to download an app, an *xpd* file is loaded which contains the URL to an *spk* file. Using hard coded http authentication, the *spk* file is downloaded. 13 | 14 | ### ScalarAInstaller ### 15 | An *spk* file is a container for an AES encrypted *apk* file. An RSA encrypted version of the key is contained, too. This app decrypts the *apk* data and installs it using the package manager. 16 | 17 | ### ScalarAUsbDlApp ### 18 | This is the most interesting app, because it handles the communication when installing apps through your computer via USB. On the computer side, things are handled directly in your browser through the PMCA Downloader plugin. 19 | 20 | The typical app install process happens as follows: 21 | 22 | 1. You visit the Sony app store using the `usb***.php` urls and decide to download an app to your camera. 23 | 2. A new task sequence is generated, with a unique identifier, the *correlation id* 24 | 3. The plugin downloads an *xpd* file which contains the *portal url* and the 25 | *correlation id*. The file is sent to the camera. (An *xpd* file is an *ini* file which contains some data and an hmac sha256 checksum.) 26 | 4. The camera sends a camera status JSON object (containing serial number, battery level, installed apps, *correlation id*, etc.) to the *portal url* using hard coded authentication. The network data is transmitted via USB. However, everything is SSL encrypted directly in camera. Only https connections signed by a known root CA are allowed. However, the certificate is still accepted if it is expired, revoked and for a different host. 27 | 5. The server replies with a JSON object containing the next actions to take. This includes the URL of the *spk* package to install, which is again downloaded over https. 28 | 29 | ### Summary ### 30 | No origin verification is performed in the whole process. This allows us to replicate the behavior of the Sony app store. 31 | -------------------------------------------------------------------------------- /pmca/spk/__init__.py: -------------------------------------------------------------------------------- 1 | """Methods for reading and writing spk files""" 2 | 3 | from Crypto.Cipher import AES 4 | from Crypto.PublicKey import RSA 5 | from Crypto.Util.number import bytes_to_long, long_to_bytes 6 | import sys 7 | 8 | if sys.version_info >= (3,): 9 | long = int 10 | 11 | from . import constants 12 | from . import util 13 | from ..util import * 14 | 15 | SpkHeader = Struct('SpkHeader', [ 16 | ('magic', Struct.STR % 4), 17 | ('keyOffset', Struct.INT32), 18 | ]) 19 | spkHeaderMagic = b'1spk' 20 | 21 | SpkKeyHeader = Struct('SpkKeyHeader', [ 22 | ('keySize', Struct.INT32), 23 | ]) 24 | 25 | def parse(data): 26 | """Parses an spk file 27 | 28 | Returns: 29 | The contained apk data 30 | """ 31 | encryptedKey, encryptedData = parseContainer(data) 32 | key = decryptKey(encryptedKey) 33 | return decryptData(key, encryptedData) 34 | 35 | def dump(data): 36 | """Builds an spk file containing the apk data specified""" 37 | encryptedKey = constants.sampleSpkKey 38 | key = decryptKey(encryptedKey) 39 | encryptedData = encryptData(key, data) 40 | return dumpContainer(encryptedKey, encryptedData) 41 | 42 | def isSpk(data): 43 | return len(data) >= SpkHeader.size and SpkHeader.unpack(data).magic == spkHeaderMagic 44 | 45 | def parseContainer(data): 46 | """Parses an spk file 47 | 48 | Returns: 49 | ('encrypted key', 'encrypted apk data') 50 | """ 51 | header = SpkHeader.unpack(data) 52 | if header.magic != spkHeaderMagic: 53 | raise Exception('Wrong magic') 54 | keyHeaderOffset = SpkHeader.size + header.keyOffset 55 | keyHeader = SpkKeyHeader.unpack(data, keyHeaderOffset) 56 | keyOffset = keyHeaderOffset + SpkKeyHeader.size 57 | dataOffset = keyOffset + keyHeader.keySize 58 | return data[keyOffset:dataOffset], data[dataOffset:] 59 | 60 | def dumpContainer(encryptedKey, encryptedData): 61 | """Builds an spk file from the encrypted key and data specified""" 62 | return SpkHeader.pack(magic=spkHeaderMagic, keyOffset=0) + SpkKeyHeader.pack(keySize=len(encryptedKey)) + encryptedKey + encryptedData 63 | 64 | def decryptKey(encryptedKey): 65 | """Decrypts an RSA-encrypted key""" 66 | rsa = RSA.construct((long(constants.rsaModulus), long(constants.rsaExponent))) 67 | try: 68 | return rsa.encrypt(encryptedKey, 0)[0] 69 | except NotImplementedError: 70 | # pycryptodome 71 | return long_to_bytes(rsa._encrypt(bytes_to_long(encryptedKey))) 72 | 73 | def decryptData(key, encryptedData): 74 | """Decrypts the apk data using the specified AES key""" 75 | aes = AES.new(key, AES.MODE_ECB) 76 | return b''.join(util.unpad(aes.decrypt(c)) for c in util.chunk(encryptedData, constants.blockSize + constants.paddingSize)) 77 | 78 | def encryptData(key, data): 79 | """Encrypts the apk data using the specified AES key""" 80 | aes = AES.new(key, AES.MODE_ECB) 81 | return b''.join(aes.encrypt(util.pad(c, constants.paddingSize)) for c in util.chunk(data, constants.blockSize)) 82 | -------------------------------------------------------------------------------- /pmca/appstore/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from datetime import datetime 3 | import yaml 4 | 5 | from .github import GithubApi 6 | from ..util import http 7 | 8 | class AppStore(object): 9 | def __init__(self, repo, branch='master', filename='apps.yaml'): 10 | self.repo = repo 11 | self.branch = branch 12 | self.filename = filename 13 | 14 | @property 15 | def apps(self): 16 | if not hasattr(self, '_apps'): 17 | apps = (self._createAppInstance(dict) for dict in self._loadApps()) 18 | self._apps = OrderedDict((app.package, app) for app in apps) 19 | return self._apps 20 | 21 | def _loadApps(self): 22 | for doc in yaml.safe_load_all(self.repo.getFile(self.branch, self.filename)): 23 | if 'package' in doc and 'name' in doc: 24 | yield doc 25 | 26 | def _createAppInstance(self, dict): 27 | return App(self.repo, dict) 28 | 29 | 30 | class App(object): 31 | def __init__(self, repo, dict): 32 | self.repo = repo 33 | self.dict = dict 34 | 35 | def __getattr__(self, name): 36 | if name in ['package', 'author', 'name', 'desc', 'homepage']: 37 | return self.dict.get(name) 38 | raise AttributeError(name) 39 | 40 | @property 41 | def release(self): 42 | if not hasattr(self, '_release'): 43 | dict = self._loadRelease() 44 | self._release = self._createReleaseInstance(dict) if dict else None 45 | return self._release 46 | 47 | def _loadRelease(self): 48 | release = self.dict.get('release', {}) 49 | if release.get('type') == 'github' and 'user' in release and 'repo' in release: 50 | for dict in GithubApi(release['user'], release['repo'], self.repo.client).getReleases(): 51 | asset = self._findGithubAsset(dict.get('assets', [])) 52 | if asset: 53 | return { 54 | 'version': dict.get('name') or dict.get('tag_name'), 55 | 'date': datetime.strptime(dict.get('created_at'), '%Y-%m-%dT%H:%M:%SZ'), 56 | 'desc': dict.get('body'), 57 | 'url': asset, 58 | } 59 | elif release.get('type') == 'yaml' and 'url' in release: 60 | for dict in yaml.safe_load_all(http.get(release['url']).data): 61 | if 'version' in dict and 'url' in dict: 62 | return dict 63 | elif release.get('type') == 'static' and 'version' in release and 'url' in release: 64 | return release 65 | 66 | def _findGithubAsset(self, assets, contentType='application/vnd.android.package-archive'): 67 | for asset in assets: 68 | if asset.get('content_type') == contentType: 69 | return asset.get('browser_download_url') 70 | 71 | def _createReleaseInstance(self, dict): 72 | return Release(self.package, dict) 73 | 74 | 75 | class Release(object): 76 | def __init__(self, package, dict): 77 | self.package = package 78 | self.dict = dict 79 | 80 | def __getattr__(self, name): 81 | if name in ['version', 'date', 'desc', 'url']: 82 | return self.dict.get(name) 83 | raise AttributeError(name) 84 | 85 | @property 86 | def asset(self): 87 | return self._loadAsset() 88 | 89 | def _loadAsset(self): 90 | return http.get(self.url).raw_data 91 | -------------------------------------------------------------------------------- /pmca/marketclient/__init__.py: -------------------------------------------------------------------------------- 1 | """Provides a client to download apps from the PMCA store""" 2 | 3 | from collections import namedtuple 4 | import json 5 | import posixpath 6 | import re 7 | 8 | from . import constants 9 | from ..util import http 10 | from .. import xpd 11 | 12 | MarketDevice = namedtuple('MarketDevice', 'deviceid, name, serial') 13 | MarketApp = namedtuple('MarketApp', 'id, name, img, price, date') 14 | 15 | def download(portalid, deviceid, appid): 16 | """Downloads an app from the PMCA store 17 | 18 | Returns: 19 | ('file name', 'spk data') 20 | """ 21 | xpdData = downloadXpd(portalid, deviceid, appid) 22 | name, url = parseXpd(xpdData) 23 | return downloadSpk(url) 24 | 25 | def login(email, password): 26 | """Tries to authenticate the specified user 27 | 28 | Returns: 29 | The portalid string in case of success or None otherwise 30 | """ 31 | response = http.postForm(constants.loginUrl, data = { 32 | 'j_username': email, 33 | 'j_password': password, 34 | 'returnURL': constants.baseUrl + '/forward.php', 35 | }) 36 | return response.cookies['portalid'] if 'portalid' in response.cookies else None 37 | 38 | def getDevices(portalid): 39 | """Fetches the list of devices for the current user""" 40 | data = json.loads(http.get(constants.baseUrl + '/dialog.php?case=mycamera', cookies = { 41 | 'portalid': portalid, 42 | 'localeid': constants.localeUs, 43 | }).data) 44 | contents = data['mycamera']['contents'] 45 | r = re.compile('
.*?(?P.*?).*?Serial:(?P.*?)', re.DOTALL) 46 | return [MarketDevice(**m.groupdict()) for m in r.finditer(contents)] 47 | 48 | def getPluginInstallText(): 49 | """Fetches the English help text for installing the PMCA Downloader plugin""" 50 | data = json.loads(http.get(constants.baseUrl + '/dialog.php?case=installingPlugin', cookies = { 51 | 'localeid': constants.localeUs, 52 | }).data) 53 | contents = data['installingPlugin']['contents'] 54 | r = re.compile('
(.*?)
', re.DOTALL) 55 | return r.search(contents).group(1) 56 | 57 | def getApps(devicename=None): 58 | """Fetches the list of apps compatible with the given device""" 59 | data = json.loads(http.get(constants.baseUrl + '/api/api_all_contents.php', data = { 60 | 'setname': devicename or '', 61 | }, headers = { 62 | 'X-Requested-With': 'XMLHttpRequest', 63 | }, cookies = { 64 | 'localeid': constants.localeUs, 65 | }).data) 66 | for app in data['contents']: 67 | yield MarketApp(app['app_id'], re.sub('\s+', ' ', app['app_name']), app['appimg_url'], None if app['app_price'] == 'Free' else app['app_price'], int(app['regist_date'])) 68 | 69 | def downloadXpd(portalid, deviceid, appid): 70 | """Fetches the xpd file for the given app 71 | 72 | Returns: 73 | The contents of the xpd file 74 | """ 75 | return http.get(constants.baseUrl + '/wifixpwd.php', data = { 76 | 'EID': appid, 77 | }, headers = { 78 | 'User-Agent': constants.cameraUserAgent, 79 | }, cookies = { 80 | 'portalid': portalid, 81 | 'deviceid': deviceid, 82 | 'localeid': constants.localeUs, 83 | }).raw_data 84 | 85 | def parseXpd(data): 86 | """Parses an xpd file 87 | 88 | Returns: 89 | ('file name', 'spk url') 90 | """ 91 | config = xpd.parse(data) 92 | return config['FNAME'], config['OUS'] 93 | 94 | def downloadSpk(url): 95 | """Downloads an spk file 96 | 97 | Returns: 98 | ('file name', 'spk data') 99 | """ 100 | response = http.get(url, auth = (constants.downloadAuthUser, constants.downloadAuthPassword)) 101 | return posixpath.basename(response.url.path), response.raw_data 102 | -------------------------------------------------------------------------------- /pmca-console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """A command line application to install apps on Android-enabled Sony cameras""" 3 | import argparse 4 | 5 | import config 6 | from pmca.commands.market import * 7 | from pmca.commands.usb import * 8 | from pmca import spk 9 | 10 | if getattr(sys, 'frozen', False): 11 | from frozenversion import version 12 | else: 13 | version = None 14 | 15 | def main(): 16 | """Command line main""" 17 | parser = argparse.ArgumentParser() 18 | if version: 19 | parser.add_argument('--version', action='version', version=version) 20 | subparsers = parser.add_subparsers(dest='command', title='commands') 21 | info = subparsers.add_parser('info', description='Display information about the camera connected via USB') 22 | info.add_argument('-d', dest='driver', choices=['libusb', 'native'], help='specify the driver') 23 | install = subparsers.add_parser('install', description='Installs an apk file on the camera connected via USB. The connection can be tested without specifying a file.') 24 | install.add_argument('-s', dest='server', help='hostname for the remote server (set to empty to start a local server)', default=config.appengineServer) 25 | install.add_argument('-d', dest='driver', choices=['libusb', 'native'], help='specify the driver') 26 | install.add_argument('-o', dest='outFile', type=argparse.FileType('w'), help='write the output to this file') 27 | installMode = install.add_mutually_exclusive_group() 28 | installMode.add_argument('-f', dest='apkFile', type=argparse.FileType('rb'), help='install an apk file') 29 | installMode.add_argument('-a', dest='appPackage', help='the package name of an app from the app list') 30 | installMode.add_argument('-i', dest='appInteractive', action='store_true', help='select an app from the app list (interactive)') 31 | market = subparsers.add_parser('market', description='Download apps from the official Sony app store') 32 | market.add_argument('-t', dest='token', help='Specify an auth token') 33 | apk2spk = subparsers.add_parser('apk2spk', description='Convert apk to spk') 34 | apk2spk.add_argument('inFile', metavar='app.apk', type=argparse.FileType('rb'), help='the apk file to convert') 35 | apk2spk.add_argument('outFile', metavar='app' + spk.constants.extension, type=argparse.FileType('wb'), help='the output spk file') 36 | spk2apk = subparsers.add_parser('spk2apk', description='Convert spk to apk') 37 | spk2apk.add_argument('inFile', metavar='app' + spk.constants.extension, type=argparse.FileType('rb'), help='the spk file to convert') 38 | spk2apk.add_argument('outFile', metavar='app.apk', type=argparse.FileType('wb'), help='the output apk file') 39 | firmware = subparsers.add_parser('firmware', description='Update the firmware') 40 | firmware.add_argument('-f', dest='datFile', type=argparse.FileType('rb'), required=True, help='the firmware file') 41 | firmware.add_argument('-d', dest='driver', choices=['libusb', 'native'], help='specify the driver') 42 | 43 | args = parser.parse_args() 44 | if args.command == 'info': 45 | infoCommand(config.appengineServer, args.driver) 46 | elif args.command == 'install': 47 | if args.appInteractive: 48 | pkg = appSelectionCommand(args.server) 49 | if not pkg: 50 | return 51 | else: 52 | pkg = args.appPackage 53 | installCommand(args.server, args.driver, args.apkFile, pkg, args.outFile) 54 | elif args.command == 'market': 55 | marketCommand(args.token) 56 | elif args.command == 'apk2spk': 57 | args.outFile.write(spk.dump(args.inFile.read())) 58 | elif args.command == 'spk2apk': 59 | args.outFile.write(spk.parse(args.inFile.read())) 60 | elif args.command == 'firmware': 61 | firmwareUpdateCommand(args.datFile, args.driver) 62 | else: 63 | parser.print_usage() 64 | 65 | 66 | if __name__ == '__main__': 67 | main() 68 | -------------------------------------------------------------------------------- /pmca/usb/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from .driver import * 4 | from ..util import * 5 | 6 | MscDeviceInfo = namedtuple('MscDeviceInfo', 'manufacturer, model') 7 | MtpDeviceInfo = namedtuple('MtpDeviceInfo', 'manufacturer, model, serialNumber, operationsSupported, vendorExtension') 8 | 9 | 10 | class MscDevice(object): 11 | """Manages communication with a USB mass storage device""" 12 | MSC_OC_INQUIRY = 0x12 13 | 14 | def __init__(self, driver): 15 | self.driver = driver 16 | self.reset() 17 | 18 | def _checkResponse(self, sense): 19 | if sense != MSC_SENSE_OK: 20 | raise Exception('Mass storage error: Sense 0x%x 0x%x 0x%x' % sense) 21 | 22 | def reset(self): 23 | self.driver.reset() 24 | self.driver.sendCommand(6 * b'\0') 25 | 26 | def _sendInquiryCommand(self, size): 27 | response, data = self.driver.sendReadCommand(dump8(self.MSC_OC_INQUIRY) + 3*b'\0' + dump8(size) + b'\0', size) 28 | self._checkResponse(response) 29 | return data 30 | 31 | def getDeviceInfo(self): 32 | """SCSI Inquiry command""" 33 | l = 5 + parse8(self._sendInquiryCommand(5)[4:5]) 34 | data = self._sendInquiryCommand(l) 35 | 36 | vendor = data[8:16].decode('latin1').rstrip() 37 | product = data[16:32].decode('latin1').rstrip() 38 | return MscDeviceInfo(vendor, product) 39 | 40 | 41 | class MtpDevice(object): 42 | """Manages communication with a PTP/MTP device. Inspired by libptp2""" 43 | PTP_OC_GetDeviceInfo = 0x1001 44 | PTP_OC_OpenSession = 0x1002 45 | PTP_OC_CloseSession = 0x1003 46 | PTP_RC_OK = 0x2001 47 | PTP_RC_SessionNotOpen = 0x2003 48 | PTP_RC_DeviceBusy = 0x2019 49 | PTP_RC_SessionAlreadyOpened = 0x201E 50 | 51 | def __init__(self, driver): 52 | self.driver = driver 53 | self.reset() 54 | self.openSession() 55 | 56 | def _checkResponse(self, code, acceptedCodes=[]): 57 | if code not in [self.PTP_RC_OK] + acceptedCodes: 58 | raise Exception('Response code not OK: 0x%x' % code) 59 | 60 | def _parseString(self, data, offset): 61 | length = parse8(data[offset:offset+1]) 62 | offset += 1 63 | end = offset + 2*length 64 | return end, data[offset:end].decode('utf16')[:-1] 65 | 66 | def _parseIntArray(self, data, offset): 67 | length = parse32le(data[offset:offset+4]) 68 | offset += 4 69 | end = offset + 2*length 70 | return end, [parse16le(data[o:o+2]) for o in range(offset, end, 2)] 71 | 72 | def _parseDeviceInfo(self, data): 73 | offset = 8 74 | offset, vendorExtension = self._parseString(data, offset) 75 | offset += 2 76 | 77 | offset, operationsSupported = self._parseIntArray(data, offset) 78 | offset, eventsSupported = self._parseIntArray(data, offset) 79 | offset, devicePropertiesSupported = self._parseIntArray(data, offset) 80 | offset, captureFormats = self._parseIntArray(data, offset) 81 | offset, imageFormats = self._parseIntArray(data, offset) 82 | 83 | offset, manufacturer = self._parseString(data, offset) 84 | offset, model = self._parseString(data, offset) 85 | offset, version = self._parseString(data, offset) 86 | offset, serial = self._parseString(data, offset) 87 | 88 | return MtpDeviceInfo(manufacturer, model, serial, set(operationsSupported), vendorExtension) 89 | 90 | def reset(self): 91 | self.driver.reset() 92 | 93 | def openSession(self, id=1): 94 | """Opens a new MTP session""" 95 | response = self.driver.sendCommand(self.PTP_OC_OpenSession, [id]) 96 | self._checkResponse(response, [self.PTP_RC_SessionAlreadyOpened]) 97 | 98 | def closeSession(self): 99 | """Closes the current session""" 100 | response = self.driver.sendCommand(self.PTP_OC_CloseSession, []) 101 | self._checkResponse(response, [self.PTP_RC_SessionNotOpen]) 102 | 103 | def getDeviceInfo(self): 104 | """Gets and parses device information""" 105 | response, data = self.driver.sendReadCommand(self.PTP_OC_GetDeviceInfo, []) 106 | self._checkResponse(response) 107 | return self._parseDeviceInfo(data) 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reverse engineering Sony PlayMemories Camera Apps # 2 | The latest Sony cameras include an Android subsystem used to run apps from the proprietary Sony PlayMemories Camera App Store (PMCA). We reverse engineered the installation process. This allows you to install custom Android apps on your camera. 3 | 4 | ## How to use it? ### 5 | There are two ways to install apps on your camera. Be sure it is connected over USB and set to MTP or mass storage mode. 6 | 7 | ### Browser plugin ### 8 | **Go to [sony-pmca.appspot.com](https://sony-pmca.appspot.com/) to try it out!** You can upload your own apps and install them to your camera using the official Sony browser plugin. Since other browser vendors are disabling NPAPI plugins, please try it using **Internet Explorer**. 9 | 10 | ### Local installer ### 11 | Download the [latest release](https://github.com/ma1co/Sony-PMCA-RE/releases/latest) (Windows or OS X) or clone this repository. 12 | 13 | #### Graphical user interface #### 14 | Run `pmca-gui` for a simple gui. 15 | 16 | #### Command line #### 17 | Run `pmca-console` in the command line for more options. Usage: 18 | 19 | * Test the USB connection to your camera (the result is written to the specified file): 20 | 21 | pmca-console install -o outfile.txt 22 | 23 | * Install an app from the app list: 24 | 25 | pmca-console install -i 26 | 27 | * Install an app on your camera (the app is uploaded and served from Google appengine): 28 | 29 | pmca-console install -f app.apk 30 | 31 | * Install an app using a local web server: 32 | 33 | pmca-console install -s "" -f app.apk 34 | 35 | * Download apps from the official Sony app store (interactive): 36 | 37 | pmca-console market 38 | 39 | * Update the camera's firmware: 40 | 41 | pmca-console firmware -f FirmwareData.dat 42 | 43 | #### Windows drivers #### 44 | On Windows, the choice defaults to the default Windows USB drivers. If you want to use libusb on Windows, you'll have to install generic drivers for your camera using [Zadig](http://zadig.akeo.ie/) (select *libusb-win32*). You can then run `pmca-console install -d libusb`. 45 | 46 | #### OS X drivers #### 47 | On OS X, to communicate with cameras in mass storage mode, the Sony [PMCADownloader](https://sony-pmca.appspot.com/plugin/install) browser plugin has to be installed. 48 | 49 | ## Is it safe? ## 50 | This is an experiment in a very early stage. All information has been found through reverse engineering. Even though everything worked fine for our developers, it could cause harm to your hardware. If you break your camera, you get to keep both pieces. **We won't take any responsibility.** 51 | 52 | However, this seems to be safer than tampering with your camera firmware directly. We're just interacting with the isolated Android subsystem. Apps you install this way can be uninstalled in the applications manager. In the worst case, a factory reset of the camera clears all data on the Android partitions. 53 | 54 | ## What about custom apps? ## 55 | You can try your standard Android apps from any app store, they should work more or less, your mileage may vary. Upload your apk file to our website and follow the instructions. Remember that the performance of the cameras is kind of limited. Some of them still run Android 2.3.7. So the Facebook app might not be the best choice. 56 | 57 | If you want to develop your custom app, feel free to do so. Debug and release certificates are accepted by the camera. There are a few special Sony APIs which allow you to take advantage of the features of your camera. Have a look at the [PMCADemo](https://github.com/ma1co/PMCADemo) project where we try to make sense of those. 58 | 59 | ## About this repository ## 60 | * **pmca-console.py**: Source for the USB installer console application. See the releases page for pyinstaller builds for Windows and OS X. 61 | * **pmca-gui.py**: A simple gui for pmca-console. See the releases page. 62 | * **main.py**: The source code for the Google App Engine website served at [sony-pmca.appspot.com](https://sony-pmca.appspot.com/). 63 | * **docs**: Technical documentation 64 | 65 | ## Special thanks ## 66 | Without the work done by the people at [nex-hack](http://www.personal-view.com/faqs/sony-hack/hack-development), this wouldn't have been possible. Thanks a lot! 67 | -------------------------------------------------------------------------------- /pmca/installer/__init__.py: -------------------------------------------------------------------------------- 1 | """Manages the communication between camera, PC and appengine website during app installation""" 2 | 3 | from collections import namedtuple 4 | import json 5 | import select 6 | import socket 7 | 8 | from ..usb.sony import * 9 | from .. import xpd 10 | 11 | Response = namedtuple('Response', 'protocol, code, status, headers, data') 12 | Request = namedtuple('Request', 'protocol, method, url, headers, data') 13 | 14 | Status = namedtuple('Status', 'code, message, percent, totalSize') 15 | Result = namedtuple('Result', 'code, message') 16 | 17 | def _buildRequest(endpoint, contentType, data): 18 | return b'POST ' + endpoint.encode('latin1') + b' REST/1.0\r\nContent-type: ' + contentType.encode('latin1') + b'\r\n\r\n' + data 19 | 20 | def _parseHttp(data): 21 | headers, data = data.split(b'\r\n\r\n')[:2] 22 | headers = headers.decode('latin1').split('\r\n') 23 | firstLine = headers[0] 24 | headers = dict(h.split(': ') for h in headers[1:]) 25 | return firstLine, headers, data 26 | 27 | def _parseRequest(data): 28 | firstLine, headers, data = _parseHttp(data) 29 | method, url, protocol = firstLine.split(' ', 2) 30 | return Request(protocol, method, url, headers, data) 31 | 32 | def _parseResponse(data): 33 | firstLine, headers, data = _parseHttp(data) 34 | protocol, code, status = firstLine.split(' ', 2) 35 | return Response(protocol, int(code), status, headers, data) 36 | 37 | def _parseResult(data): 38 | data = json.loads(data.decode('latin1')) 39 | return Result(data['resultCode'], data['message']) 40 | 41 | def _parseStatus(data): 42 | data = json.loads(data.decode('latin1')) 43 | return Status(data['status'], data['status text'], data['percent'], data['total size']) 44 | 45 | def install(dev, host, port, xpdData, statusFunc=None): 46 | """Sends an xpd file to the camera, lets it access the internet through SSL, waits for the response""" 47 | # Initialize communication 48 | dev.emptyBuffer() 49 | dev.sendInit() 50 | 51 | # Start the installation by sending the xpd data in a REST request 52 | response = dev.sendRequest(_buildRequest('/task/start', xpd.constants.mimeType, xpdData)) 53 | response = _parseResponse(response) 54 | result = _parseResult(response.data) 55 | if result.code != 0: 56 | raise Exception('Response error %s' % str(result)) 57 | 58 | connectionId = 0 59 | sock = None 60 | 61 | # Main loop 62 | while True: 63 | if sock is not None: 64 | ready = select.select([sock], [], [], 0) 65 | if ready[0]: 66 | # There is data waiting on the socket, let's send it to the camera 67 | resp = sock.recv(2 ** 14) 68 | if resp != b'': 69 | dev.sendSslData(connectionId, resp) 70 | else: 71 | dev.sendSslEnd(connectionId) 72 | 73 | # Receive the next message from the camera 74 | message = dev.receive() 75 | if message is None: 76 | # Nothing received, let's wait 77 | continue 78 | 79 | if isinstance(message, SslStartMessage): 80 | # The camera wants us to open an SSL socket 81 | connectionId = message.connectionId 82 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 83 | if message.host != host: 84 | raise Exception('Connecting to wrong host: %s' % message.host) 85 | sock.connect((host, port)) 86 | elif isinstance(message, SslSendDataMessage) and sock and message.connectionId == connectionId: 87 | # The camera wants to send data over the socket 88 | sock.send(message.data) 89 | elif isinstance(message, SslEndMessage) and sock and message.connectionId == connectionId: 90 | # The camera wants to close the socket 91 | sock.close() 92 | sock = None 93 | elif isinstance(message, RequestMessage): 94 | # The camera sends a REST message 95 | request = _parseRequest(message.data) 96 | if request.url == '/task/progress': 97 | # Progress 98 | status = _parseStatus(request.data) 99 | if statusFunc: 100 | statusFunc(status) 101 | elif request.url == '/task/complete': 102 | # The camera completed the task, let's stop this loop 103 | result = _parseResult(request.data) 104 | dev.sendEnd() 105 | return result 106 | else: 107 | raise Exception("Unknown message url %s" % request.url) 108 | else: 109 | raise Exception("Unknown message %s" % str(message)) 110 | -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function ajax(url, callback) { 4 | var xhr = new XMLHttpRequest(); 5 | xhr.open("get", url, true); 6 | xhr.onreadystatechange = function() { 7 | if (xhr.readyState == 4 && xhr.status == 200) 8 | callback(xhr.responseText); 9 | }; 10 | xhr.send(null); 11 | } 12 | 13 | function log(msg) { 14 | var ele = document.getElementById(state.logElementId); 15 | ele.innerHTML += '
' + msg + '
'; 16 | ele.scrollTop = ele.scrollHeight; 17 | } 18 | 19 | function result(msg) { 20 | document.getElementById(state.resultElementId).innerHTML = msg; 21 | } 22 | 23 | var state = { 24 | logElementId: null, 25 | resultElementId: null, 26 | noResultText: '', 27 | plugin: null, 28 | taskKey: null, 29 | }; 30 | 31 | window.pmcaInit = function(logElementId, resultElementId) { 32 | state.logElementId = logElementId; 33 | state.resultElementId = resultElementId; 34 | state.noResultText = document.getElementById(resultElementId).innerHTML; 35 | loadPlugin(); 36 | }; 37 | function loadPlugin() { 38 | log('Loading plugin'); 39 | document.body.innerHTML += ''; 40 | } 41 | window.pmcadl_ev_plugin_loaded = function() { 42 | log('Plugin loaded'); 43 | state.plugin = document.getElementById('pmcaPlugin'); 44 | }; 45 | 46 | function pluginMethod(method, data) { 47 | var result = state.plugin.pmcadpMethod(method, data)['pmcadl_result']; 48 | if (result != 'success') 49 | log('Plugin error: ' + result); 50 | } 51 | 52 | var errorCallbacks = [ 53 | ['pmcadl_ev_detect_none_device', 'No device detected'], 54 | ['pmcadl_ev_detect_multi_devices', 'Multiple devices detected'], 55 | ['pmcadl_ev_detect_pmb', 'Pmb detected'],// ??? 56 | ['pmcadl_ev_detect_pmh', 'Pmh detected'],// ??? 57 | ['pmcadl_ev_detect_device_busy', 'Device busy'], 58 | ['pmcadl_ev_unknown_error', 'Unknown error'], 59 | ['pmcadl_ev_failed_deviceinfo_dl', 'Device info download failed'], 60 | ['pmcadl_ev_failed_deviceinfo_read', 'Device info read failed'], 61 | ['pmcadl_ev_other_instance_exists', 'Other instance detected'], 62 | ['pmcadl_ev_failed_mode_switch', 'Mode switch'], 63 | ['pmcadl_ev_usb_connection_error', 'USB connection'], 64 | ['pmcadl_ev_network_connection_error', 'Network connection'], 65 | ]; 66 | for (var i=0; i>', lambda e: self.modeVar.set(self.MODE_APP)) 163 | self.appCombo.pack(side=LEFT, fill=X, expand=True) 164 | self.setAppList([]) 165 | 166 | self.appLoadButton = Button(appFrame, text='Refresh', command=AppLoadTask(self).run) 167 | self.appLoadButton.pack() 168 | 169 | apkFrame = Labelframe(self, padding=5) 170 | apkFrame['labelwidget'] = Radiobutton(apkFrame, text='Select an apk', variable=self.modeVar, value=self.MODE_APK) 171 | apkFrame.pack(fill=X) 172 | 173 | self.apkFile = Entry(apkFrame) 174 | self.apkFile.pack(side=LEFT, fill=X, expand=True) 175 | 176 | self.apkSelectButton = Button(apkFrame, text='Open apk...', command=self.openApk) 177 | self.apkSelectButton.pack() 178 | 179 | self.installButton = Button(self, text='Install selected app', command=InstallTask(self).run, padding=5) 180 | self.installButton.pack(fill=X, pady=(5, 0)) 181 | 182 | self.run(AppLoadTask(self).run) 183 | 184 | def getMode(self): 185 | return self.modeVar.get() 186 | 187 | def openApk(self): 188 | fn = askopenfilename(filetypes=[('Apk files', '.apk'), ('All files', '.*')]) 189 | if fn: 190 | self.apkFile.delete(0, END) 191 | self.apkFile.insert(0, fn) 192 | self.modeVar.set(self.MODE_APK) 193 | 194 | def getSelectedApk(self): 195 | return self.apkFile.get() 196 | 197 | def setAppList(self, apps): 198 | self.appList = apps 199 | self.appCombo['values'] = [''] + [app.name for app in apps] 200 | self.appCombo.current(0) 201 | 202 | def getSelectedApp(self): 203 | if self.appCombo.current() > 0: 204 | return self.appList[self.appCombo.current() - 1] 205 | 206 | 207 | class FirmwareFrame(UiFrame): 208 | def __init__(self, parent, **kwargs): 209 | UiFrame.__init__(self, parent, **kwargs) 210 | 211 | datFrame = Labelframe(self, padding=5) 212 | datFrame['labelwidget'] = Label(datFrame, text='Firmware file') 213 | datFrame.pack(fill=X) 214 | 215 | self.datFile = Entry(datFrame) 216 | self.datFile.pack(side=LEFT, fill=X, expand=True) 217 | 218 | self.datSelectButton = Button(datFrame, text='Open...', command=self.openDat) 219 | self.datSelectButton.pack() 220 | 221 | self.fwUpdateButton = Button(self, text='Update firmware', command=FirmwareUpdateTask(self).run, padding=5) 222 | self.fwUpdateButton.pack(fill=X, pady=(5, 0)) 223 | 224 | def openDat(self): 225 | fn = askopenfilename(filetypes=[('Firmware files', '.dat'), ('All files', '.*')]) 226 | if fn: 227 | self.datFile.delete(0, END) 228 | self.datFile.insert(0, fn) 229 | 230 | def getSelectedDat(self): 231 | return self.datFile.get() 232 | 233 | 234 | def main(): 235 | """Gui main""" 236 | ui = InstallerUi('pmca-gui' + (' ' + version if version else '')) 237 | ui.mainloop() 238 | 239 | 240 | if __name__ == '__main__': 241 | main() 242 | -------------------------------------------------------------------------------- /pmca/usb/driver/libusb.py: -------------------------------------------------------------------------------- 1 | """A wrapper to use libusb. Default on linux, on Windows you have to install a generic driver for your camera""" 2 | 3 | import usb.core, usb.util 4 | 5 | from . import * 6 | from ...util import * 7 | 8 | PtpHeader = Struct('PtpHeader', [ 9 | ('size', Struct.INT32), 10 | ('type', Struct.INT16), 11 | ('code', Struct.INT16), 12 | ('transaction', Struct.INT32), 13 | ]) 14 | 15 | MscCommandBlockWrapper = Struct('MscCommandBlockWrapper', [ 16 | ('signature', Struct.STR % 4), 17 | ('tag', Struct.INT32), 18 | ('dataTransferLength', Struct.INT32), 19 | ('flags', Struct.INT8), 20 | ('lun', Struct.INT8), 21 | ('commandLength', Struct.INT8), 22 | ('command', Struct.STR % 16), 23 | ]) 24 | 25 | MscCommandStatusWrapper = Struct('MscCommandStatusWrapper', [ 26 | ('signature', Struct.STR % 4), 27 | ('tag', Struct.INT32), 28 | ('dataResidue', Struct.INT32), 29 | ('status', Struct.INT8), 30 | ]) 31 | 32 | 33 | class _UsbContext(object): 34 | def __init__(self, name, classType, driverClass): 35 | self.name = 'libusb-%s' % name 36 | self.classType = classType 37 | self._driverClass = driverClass 38 | 39 | def __enter__(self): 40 | return self 41 | 42 | def __exit__(self, *ex): 43 | pass 44 | 45 | def listDevices(self, vendor): 46 | return _listDevices(vendor, self.classType) 47 | 48 | def openDevice(self, device): 49 | return self._driverClass(device.handle) 50 | 51 | 52 | class MscContext(_UsbContext): 53 | def __init__(self): 54 | super(MscContext, self).__init__('MSC', USB_CLASS_MSC, _MscDriver) 55 | 56 | class MtpContext(_UsbContext): 57 | def __init__(self): 58 | super(MtpContext, self).__init__('MTP', USB_CLASS_PTP, _MtpDriver) 59 | 60 | 61 | def _listDevices(vendor, classType): 62 | """Lists all detected USB devices""" 63 | for dev in usb.core.find(find_all=True, idVendor=vendor): 64 | interface = next((interface for config in dev for interface in config), None) 65 | if interface and interface.bInterfaceClass == classType: 66 | yield UsbDevice(dev, dev.idVendor, dev.idProduct) 67 | 68 | 69 | class _UsbDriver(object): 70 | """Bulk reading and writing to USB devices""" 71 | USB_ENDPOINT_TYPE_BULK = 2 72 | USB_ENDPOINT_MASK = 1 73 | USB_ENDPOINT_OUT = 0 74 | USB_ENDPOINT_IN = 1 75 | 76 | def __init__(self, device): 77 | self.dev = device 78 | self.epIn = self._findEndpoint(self.USB_ENDPOINT_TYPE_BULK, self.USB_ENDPOINT_IN) 79 | self.epOut = self._findEndpoint(self.USB_ENDPOINT_TYPE_BULK, self.USB_ENDPOINT_OUT) 80 | 81 | def __del__(self): 82 | usb.util.dispose_resources(self.dev) 83 | 84 | def _findEndpoint(self, type, direction): 85 | interface = self.dev.get_active_configuration()[(0, 0)] 86 | for ep in interface: 87 | if ep.bmAttributes == type and ep.bEndpointAddress & self.USB_ENDPOINT_MASK == direction: 88 | return ep.bEndpointAddress 89 | raise Exception('No endpoint found') 90 | 91 | def reset(self): 92 | try: 93 | if self.dev.is_kernel_driver_active(0): 94 | self.dev.detach_kernel_driver(0) 95 | except NotImplementedError: 96 | pass 97 | 98 | def read(self, length): 99 | return self.dev.read(self.epIn, length).tostring() 100 | 101 | def write(self, data): 102 | return self.dev.write(self.epOut, data) 103 | 104 | 105 | class _MscDriver(_UsbDriver): 106 | """Communicate with a USB mass storage device""" 107 | MSC_OC_REQUEST_SENSE = 0x03 108 | 109 | DIRECTION_WRITE = 0 110 | DIRECTION_READ = 0x80 111 | 112 | def _writeCommand(self, direction, command, dataSize, tag=0, lun=0): 113 | self.write(MscCommandBlockWrapper.pack( 114 | signature = b'USBC', 115 | tag = tag, 116 | dataTransferLength = dataSize, 117 | flags = direction, 118 | lun = lun, 119 | commandLength = len(command), 120 | command = command.ljust(16, b'\0'), 121 | )) 122 | 123 | def _readResponse(self, failOnError=False): 124 | response = MscCommandStatusWrapper.unpack(self.read(MscCommandStatusWrapper.size)) 125 | if response.signature != b'USBS': 126 | raise Exception('Wrong status signature') 127 | if response.status != 0: 128 | if failOnError: 129 | raise Exception('Mass storage error') 130 | else: 131 | return self.requestSense() 132 | return MSC_SENSE_OK 133 | 134 | def requestSense(self): 135 | size = 18 136 | response, data = self.sendReadCommand(dump8(self.MSC_OC_REQUEST_SENSE) + 3*b'\0' + dump8(size) + b'\0', size, failOnError=True) 137 | return parseMscSense(data) 138 | 139 | def sendCommand(self, command, failOnError=False): 140 | self._writeCommand(self.DIRECTION_WRITE, command, 0) 141 | return self._readResponse(failOnError) 142 | 143 | def sendWriteCommand(self, command, data, failOnError=False): 144 | self._writeCommand(self.DIRECTION_WRITE, command, len(data)) 145 | 146 | stalled = False 147 | try: 148 | self.write(data) 149 | except usb.core.USBError: 150 | # Write stall 151 | stalled = True 152 | self.dev.clear_halt(self.epOut) 153 | 154 | sense = self._readResponse(failOnError) 155 | if stalled and sense == MSC_SENSE_OK: 156 | raise Exception('Mass storage write error') 157 | return sense 158 | 159 | def sendReadCommand(self, command, size, failOnError=False): 160 | self._writeCommand(self.DIRECTION_READ, command, size) 161 | 162 | stalled = False 163 | data = None 164 | try: 165 | data = self.read(size) 166 | except usb.core.USBError: 167 | # Read stall 168 | stalled = True 169 | self.dev.clear_halt(self.epIn) 170 | 171 | sense = self._readResponse(failOnError) 172 | if stalled and sense == MSC_SENSE_OK: 173 | raise Exception('Mass storage read error') 174 | return sense, data 175 | 176 | 177 | class _MtpDriver(_UsbDriver): 178 | """Send and receive PTP/MTP packages to a device. Inspired by libptp2""" 179 | MAX_PKG_LEN = 512 180 | TYPE_COMMAND = 1 181 | TYPE_DATA = 2 182 | TYPE_RESPONSE = 3 183 | 184 | def _writePtp(self, type, code, transaction, data=b''): 185 | self.write(PtpHeader.pack( 186 | size = PtpHeader.size + len(data), 187 | type = type, 188 | code = code, 189 | transaction = transaction, 190 | ) + data) 191 | 192 | def _readPtp(self): 193 | data = b'' 194 | while data == b'': 195 | data = self.read(self.MAX_PKG_LEN) 196 | header = PtpHeader.unpack(data) 197 | if header.size > self.MAX_PKG_LEN: 198 | data += self.read(header.size - self.MAX_PKG_LEN) 199 | return header.type, header.code, header.transaction, data[PtpHeader.size:PtpHeader.size+header.size] 200 | 201 | def _readData(self): 202 | type, code, transaction, data = self._readPtp() 203 | if type != self.TYPE_DATA: 204 | raise Exception('Wrong response type: 0x%x' % type) 205 | return data 206 | 207 | def _readResponse(self): 208 | type, code, transaction, data = self._readPtp() 209 | if type != self.TYPE_RESPONSE: 210 | raise Exception('Wrong response type: 0x%x' % type) 211 | return code 212 | 213 | def _writeInitialCommand(self, code, args): 214 | try: 215 | self.transaction += 1 216 | except AttributeError: 217 | self.transaction = 0 218 | self._writePtp(self.TYPE_COMMAND, code, self.transaction, b''.join([dump32le(arg) for arg in args])) 219 | 220 | def sendCommand(self, code, args): 221 | """Send a PTP/MTP command without data phase""" 222 | self._writeInitialCommand(code, args) 223 | return self._readResponse() 224 | 225 | def sendWriteCommand(self, code, args, data): 226 | """Send a PTP/MTP command with write data phase""" 227 | self._writeInitialCommand(code, args) 228 | self._writePtp(self.TYPE_DATA, code, self.transaction, data) 229 | return self._readResponse() 230 | 231 | def sendReadCommand(self, code, args): 232 | """Send a PTP/MTP command with read data phase""" 233 | self._writeInitialCommand(code, args) 234 | data = self._readData() 235 | return self._readResponse(), data 236 | -------------------------------------------------------------------------------- /pmca/usb/driver/windows/msc.py: -------------------------------------------------------------------------------- 1 | """A wrapper to use IOCTL_SCSI_PASS_THROUGH_DIRECT""" 2 | 3 | from comtypes import GUID 4 | from ctypes import * 5 | from ctypes.wintypes import * 6 | import string 7 | import sys 8 | from win32file import * 9 | 10 | from . import * 11 | from .. import * 12 | 13 | IOCTL_SCSI_PASS_THROUGH_DIRECT = 0x4d014 14 | IOCTL_STORAGE_GET_DEVICE_NUMBER = 0x2d1080 15 | 16 | SCSI_IOCTL_DATA_OUT = 0 17 | SCSI_IOCTL_DATA_IN = 1 18 | SCSI_IOCTL_DATA_UNSPECIFIED = 2 19 | 20 | class SCSI_PASS_THROUGH_DIRECT(Structure): 21 | _fields_ = [ 22 | ('Length', c_ushort), 23 | ('ScsiStatus', c_ubyte), 24 | ('PathId', c_ubyte), 25 | ('TargetId', c_ubyte), 26 | ('Lun', c_ubyte), 27 | ('CdbLength', c_ubyte), 28 | ('SenseInfoLength', c_ubyte), 29 | ('DataIn', c_ubyte), 30 | ('DataTransferLength', c_ulong), 31 | ('TimeOutValue', c_ulong), 32 | ('DataBuffer', c_void_p), 33 | ('SenseInfoOffset', c_ulong), 34 | ('Cdb', c_ubyte * 16), 35 | ] 36 | 37 | class SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER(Structure): 38 | _fields_ = [ 39 | ('sptd', SCSI_PASS_THROUGH_DIRECT), 40 | ('Filler', c_ulong), 41 | ('ucSenseBuf', c_ubyte * 32), 42 | ] 43 | 44 | class SP_DEVICE_INTERFACE_DATA(Structure): 45 | _fields_ = [ 46 | ('cbSize', DWORD), 47 | ('InterfaceClassGuid', GUID), 48 | ('Flags', DWORD), 49 | ('Reserved', POINTER(ULONG)), 50 | ] 51 | 52 | class SP_DEVICE_INTERFACE_DETAIL_DATA(Structure): 53 | _pack_ = 4 if sys.maxsize > 2**32 else 2 54 | _fields_ = [ 55 | ('cbSize', DWORD), 56 | ('DevicePath', WCHAR * 1), 57 | ] 58 | 59 | class SP_DEVINFO_DATA(Structure): 60 | _fields_ = [ 61 | ('cbSize', DWORD), 62 | ('ClassGuid', GUID), 63 | ('DevInst', DWORD), 64 | ('Reserved', POINTER(ULONG)), 65 | ] 66 | 67 | class STORAGE_DEVICE_NUMBER(Structure): 68 | _fields_ = [ 69 | ('DeviceType', DWORD), 70 | ('DeviceNumber', ULONG), 71 | ('PartitionNumber', ULONG), 72 | ] 73 | 74 | SetupDiGetClassDevs = windll.setupapi.SetupDiGetClassDevsW 75 | SetupDiGetClassDevs.restype = HANDLE 76 | SetupDiGetClassDevs.argtypes = [POINTER(GUID), LPCWSTR, HWND, DWORD] 77 | 78 | SetupDiDestroyDeviceInfoList = windll.setupapi.SetupDiDestroyDeviceInfoList 79 | SetupDiDestroyDeviceInfoList.restype = BOOL 80 | SetupDiDestroyDeviceInfoList.argtypes = [HANDLE] 81 | 82 | SetupDiEnumDeviceInterfaces = windll.setupapi.SetupDiEnumDeviceInterfaces 83 | SetupDiEnumDeviceInterfaces.restype = BOOL 84 | SetupDiEnumDeviceInterfaces.argtypes = [HANDLE, c_void_p, POINTER(GUID), DWORD, POINTER(SP_DEVICE_INTERFACE_DATA)] 85 | 86 | SetupDiGetDeviceInterfaceDetail = windll.setupapi.SetupDiGetDeviceInterfaceDetailW 87 | SetupDiGetDeviceInterfaceDetail.restype = BOOL 88 | SetupDiGetDeviceInterfaceDetail.argtypes = [HANDLE, POINTER(SP_DEVICE_INTERFACE_DATA), POINTER(SP_DEVICE_INTERFACE_DETAIL_DATA), DWORD, POINTER(DWORD), POINTER(SP_DEVINFO_DATA)] 89 | 90 | CM_Get_Child = windll.CfgMgr32.CM_Get_Child 91 | CM_Get_Child.restype = DWORD 92 | CM_Get_Child.argtypes = [POINTER(DWORD), DWORD, ULONG] 93 | 94 | CM_Get_Sibling = windll.CfgMgr32.CM_Get_Sibling 95 | CM_Get_Sibling.restype = DWORD 96 | CM_Get_Sibling.argtypes = [POINTER(DWORD), DWORD, ULONG] 97 | 98 | GUID_DEVINTERFACE_USB_DEVICE = GUID('{A5DCBF10-6530-11D2-901F-00C04FB951ED}') 99 | GUID_DEVINTERFACE_DISK = GUID('{53F56307-B6BF-11D0-94F2-00A0C91EFB8B}') 100 | DIGCF_PRESENT = 2 101 | DIGCF_DEVICEINTERFACE = 16 102 | 103 | 104 | class MscContext(object): 105 | def __init__(self): 106 | self.name = 'Windows-MSC' 107 | self.classType = USB_CLASS_MSC 108 | 109 | def __enter__(self): 110 | return self 111 | 112 | def __exit__(self, *ex): 113 | pass 114 | 115 | def listDevices(self, vendor): 116 | return (dev for dev in _listDevices() if dev.idVendor == vendor) 117 | 118 | def openDevice(self, device): 119 | return _MscDriver(device.handle) 120 | 121 | 122 | def _listDeviceClass(guid): 123 | handle = SetupDiGetClassDevs(byref(guid), None, None, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT) 124 | if handle == INVALID_HANDLE_VALUE: 125 | raise Exception('SetupDiGetClassDevs failed') 126 | 127 | i = 0 128 | interfaceData = SP_DEVICE_INTERFACE_DATA() 129 | interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA) 130 | while SetupDiEnumDeviceInterfaces(handle, None, byref(guid), i, byref(interfaceData)): 131 | size = c_ulong(0) 132 | SetupDiGetDeviceInterfaceDetail(handle, byref(interfaceData), None, 0, byref(size), None) 133 | 134 | interfaceDetailData = SP_DEVICE_INTERFACE_DETAIL_DATA() 135 | interfaceDetailData.cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA) 136 | resize(interfaceDetailData, size.value) 137 | devInfoData = SP_DEVINFO_DATA() 138 | devInfoData.cbSize = sizeof(SP_DEVINFO_DATA) 139 | if not SetupDiGetDeviceInterfaceDetail(handle, byref(interfaceData), byref(interfaceDetailData), size, None, byref(devInfoData)): 140 | raise Exception('SetupDiGetDeviceInterfaceDetail failed') 141 | 142 | yield devInfoData.DevInst, wstring_at(byref(interfaceDetailData, SP_DEVICE_INTERFACE_DETAIL_DATA.DevicePath.offset)) 143 | i += 1 144 | 145 | if not SetupDiDestroyDeviceInfoList(handle): 146 | raise Exception('SetupDiDestroyDeviceInfoList failed') 147 | 148 | def _listDeviceChildren(inst): 149 | child = DWORD(inst) 150 | f = CM_Get_Child 151 | while not f(byref(child), child, 0): 152 | yield child.value 153 | f = CM_Get_Sibling 154 | 155 | def _listLogicalDrives(type=DRIVE_REMOVABLE): 156 | mask = GetLogicalDrives() 157 | for i, l in enumerate(string.ascii_uppercase): 158 | if mask & (1 << i) and GetDriveType('%s:\\' % l) == type: 159 | yield '\\\\.\\%s:' % l 160 | 161 | def _getStorageNumber(path): 162 | handle = CreateFile(path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None) 163 | deviceNumber = STORAGE_DEVICE_NUMBER() 164 | DeviceIoControl(handle, IOCTL_STORAGE_GET_DEVICE_NUMBER, None, deviceNumber) 165 | CloseHandle(handle) 166 | return deviceNumber.DeviceType, deviceNumber.DeviceNumber 167 | 168 | def _listDevices(): 169 | """Lists all detected mass storage devices""" 170 | # Similar to what calibre does: https://github.com/kovidgoyal/calibre/blob/master/src/calibre/devices/winusb.py 171 | logicalDrives = dict((_getStorageNumber(l), l) for l in _listLogicalDrives()) 172 | disks = dict(_listDeviceClass(GUID_DEVINTERFACE_DISK)) 173 | usbDevices = dict(_listDeviceClass(GUID_DEVINTERFACE_USB_DEVICE)) 174 | for usbInst, usbPath in usbDevices.items(): 175 | for diskInst in _listDeviceChildren(usbInst): 176 | if diskInst in disks: 177 | storageNumber = _getStorageNumber(disks[diskInst]) 178 | if storageNumber in logicalDrives: 179 | idVendor, idProduct = parseDeviceId(usbPath) 180 | yield UsbDevice(logicalDrives[storageNumber], idVendor, idProduct) 181 | break# only return the first disk for every device 182 | 183 | 184 | class _MscDriver(object): 185 | """Communicate with a USB mass storage device""" 186 | def __init__(self, device): 187 | self.device = device 188 | 189 | def _sendScsiCommand(self, command, direction, data): 190 | sptd = SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER(sptd = SCSI_PASS_THROUGH_DIRECT( 191 | Length = sizeof(SCSI_PASS_THROUGH_DIRECT), 192 | DataIn = direction, 193 | DataTransferLength = sizeof(data) if data else 0, 194 | DataBuffer = cast(data, c_void_p), 195 | CdbLength = len(command), 196 | Cdb = (c_ubyte * 16).from_buffer_copy(command.ljust(16, b'\0')), 197 | TimeOutValue = 5, 198 | SenseInfoLength = SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER.ucSenseBuf.size, 199 | SenseInfoOffset = SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER.ucSenseBuf.offset, 200 | )) 201 | handle = CreateFile('\\\\.\\%s' % self.device, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None) 202 | result = DeviceIoControl(handle, IOCTL_SCSI_PASS_THROUGH_DIRECT, sptd, sizeof(SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER)) 203 | CloseHandle(handle) 204 | if SCSI_PASS_THROUGH_DIRECT.from_buffer_copy(result).ScsiStatus != 0: 205 | sense = parseMscSense(result[SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER.ucSenseBuf.offset:]) 206 | if sense == MSC_SENSE_OK: 207 | raise Exception('Mass storage error') 208 | return sense 209 | return MSC_SENSE_OK 210 | 211 | def reset(self): 212 | pass 213 | 214 | def sendCommand(self, command): 215 | return self._sendScsiCommand(command, SCSI_IOCTL_DATA_UNSPECIFIED, None) 216 | 217 | def sendWriteCommand(self, command, data): 218 | buffer = (c_ubyte * len(data)).from_buffer_copy(data) 219 | return self._sendScsiCommand(command, SCSI_IOCTL_DATA_OUT, buffer) 220 | 221 | def sendReadCommand(self, command, size): 222 | buffer = (c_ubyte * size)() 223 | status = self._sendScsiCommand(command, SCSI_IOCTL_DATA_IN, buffer) 224 | return status, bytes(bytearray(buffer)) 225 | -------------------------------------------------------------------------------- /pmca/commands/usb.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import json 3 | import os 4 | import sys 5 | import time 6 | 7 | if sys.version_info < (3,): 8 | # Python 2 9 | input = raw_input 10 | 11 | import config 12 | from .. import appstore 13 | from .. import firmware 14 | from .. import installer 15 | from ..marketserver.server import * 16 | from ..usb import * 17 | from ..usb.driver import * 18 | from ..usb.sony import * 19 | 20 | scriptRoot = getattr(sys, '_MEIPASS', os.path.dirname(__file__) + '/../..') 21 | 22 | 23 | def printStatus(status): 24 | """Print progress""" 25 | print('%s %d%%' % (status.message, status.percent)) 26 | 27 | 28 | def switchToAppInstaller(dev): 29 | """Switches a camera in MTP mode to app installation mode""" 30 | print('Switching to app install mode') 31 | SonyExtCmdCamera(dev).switchToAppInstaller() 32 | 33 | 34 | defaultAppStoreRepo = appstore.GithubApi(config.githubAppListUser, config.githubAppListRepo) 35 | defaultCertFile = scriptRoot + '/certs/localtest.me.pem' 36 | def createMarketServer(host=None, repo=defaultAppStoreRepo, certFile=defaultCertFile): 37 | if host: 38 | print('Using remote server %s' % host) 39 | return RemoteMarketServer(host) 40 | else: 41 | print('Using local server') 42 | return LocalMarketServer(repo, certFile) 43 | 44 | 45 | def listApps(host=None): 46 | print('Loading app list') 47 | server = createMarketServer(host) 48 | apps = list(server.listApps().values()) 49 | print('Found %d apps' % len(apps)) 50 | return apps 51 | 52 | 53 | def installApp(dev, host=None, apkFile=None, appPackage=None, outFile=None): 54 | """Installs an app on the specified device.""" 55 | with ServerContext(createMarketServer(host)) as server: 56 | if apkFile: 57 | if isinstance(server, RemoteMarketServer): 58 | print('Uploading apk') 59 | server.setApk(os.path.basename(apkFile.name), apkFile.read()) 60 | elif appPackage: 61 | if isinstance(server, LocalMarketServer): 62 | print('Downloading apk') 63 | server.setApp(appPackage) 64 | 65 | print('Starting task') 66 | xpdData = server.getXpd() 67 | 68 | print('Starting communication') 69 | # Point the camera to the web api 70 | result = installer.install(dev, server.host, server.port, xpdData, printStatus) 71 | if result.code != 0: 72 | raise Exception('Communication error %d: %s' % (result.code, result.message)) 73 | 74 | if isinstance(server, RemoteMarketServer): 75 | print('Downloading result') 76 | result = server.getResult() 77 | 78 | print('Task completed successfully') 79 | 80 | if outFile: 81 | print('Writing to output file') 82 | json.dump(result, outFile, indent=2) 83 | 84 | return result 85 | 86 | 87 | class UsbDriverList: 88 | def __init__(self, *contexts): 89 | self._contexts = contexts 90 | self._drivers = [] 91 | 92 | def __enter__(self): 93 | self._drivers = [context.__enter__() for context in self._contexts] 94 | return self 95 | 96 | def __exit__(self, *ex): 97 | for context in self._contexts: 98 | context.__exit__(*ex) 99 | self._drivers = [] 100 | 101 | def listDevices(self, vendor): 102 | for driver in self._drivers: 103 | for dev in driver.listDevices(vendor): 104 | yield (driver.classType, driver.openDevice(dev)) 105 | 106 | 107 | def importDriver(driverName=None): 108 | """Imports the usb driver. Use in a with statement""" 109 | MscContext = None 110 | MtpContext = None 111 | 112 | # Load native drivers 113 | if driverName == 'native' or driverName is None: 114 | if sys.platform == 'win32': 115 | from ..usb.driver.windows.msc import MscContext 116 | from ..usb.driver.windows.wpd import MtpContext 117 | elif sys.platform == 'darwin': 118 | from ..usb.driver.osx import MscContext 119 | else: 120 | print('No native drivers available') 121 | elif driverName != 'libusb': 122 | raise Exception('Unknown driver') 123 | 124 | # Fallback to libusb 125 | if MscContext is None: 126 | from ..usb.driver.libusb import MscContext 127 | if MtpContext is None: 128 | from ..usb.driver.libusb import MtpContext 129 | 130 | drivers = [MscContext(), MtpContext()] 131 | print('Using drivers %s' % ', '.join(d.name for d in drivers)) 132 | return UsbDriverList(*drivers) 133 | 134 | 135 | def listDevices(driverList): 136 | """List all Sony usb devices""" 137 | print('Looking for Sony devices') 138 | for type, drv in driverList.listDevices(SONY_ID_VENDOR): 139 | if type == USB_CLASS_MSC: 140 | print('\nQuerying mass storage device') 141 | # Get device info 142 | info = MscDevice(drv).getDeviceInfo() 143 | 144 | if isSonyMscCamera(info): 145 | print('%s %s is a camera in mass storage mode' % (info.manufacturer, info.model)) 146 | yield SonyMscCamera(drv) 147 | 148 | elif type == USB_CLASS_PTP: 149 | print('\nQuerying MTP device') 150 | # Get device info 151 | info = MtpDevice(drv).getDeviceInfo() 152 | 153 | if isSonyMtpCamera(info): 154 | print('%s %s is a camera in MTP mode' % (info.manufacturer, info.model)) 155 | yield SonyMtpCamera(drv) 156 | elif isSonyMtpAppInstaller(info): 157 | print('%s %s is a camera in app install mode' % (info.manufacturer, info.model)) 158 | yield SonyMtpAppInstaller(drv) 159 | print('') 160 | 161 | 162 | def getDevice(driver): 163 | """Check for exactly one Sony usb device""" 164 | devices = list(listDevices(driver)) 165 | if not devices: 166 | print('No devices found. Ensure your camera is connected.') 167 | elif len(devices) != 1: 168 | print('Too many devices found. Only one camera is supported') 169 | else: 170 | return devices[0] 171 | 172 | 173 | def infoCommand(host=None, driverName=None): 174 | """Display information about the camera connected via usb""" 175 | with importDriver(driverName) as driver: 176 | device = getDevice(driver) 177 | if device: 178 | if isinstance(device, SonyMtpAppInstaller): 179 | info = installApp(device, host) 180 | print('') 181 | props = [ 182 | ('Model', info['deviceinfo']['name']), 183 | ('Product code', info['deviceinfo']['productcode']), 184 | ('Serial number', info['deviceinfo']['deviceid']), 185 | ('Firmware version', info['deviceinfo']['fwversion']), 186 | ] 187 | else: 188 | info = SonyExtCmdCamera(device).getCameraInfo() 189 | updater = SonyUpdaterCamera(device) 190 | updater.init() 191 | firmwareOld, firmwareNew = updater.getFirmwareVersion() 192 | props = [ 193 | ('Model', info.modelName), 194 | ('Product code', info.modelCode), 195 | ('Serial number', info.serial), 196 | ('Firmware version', firmwareOld), 197 | ] 198 | for k, v in props: 199 | print('%-20s%s' % (k + ': ', v)) 200 | 201 | 202 | def installCommand(host=None, driverName=None, apkFile=None, appPackage=None, outFile=None): 203 | """Install the given apk on the camera""" 204 | with importDriver(driverName) as driver: 205 | device = getDevice(driver) 206 | if device and not isinstance(device, SonyMtpAppInstaller): 207 | switchToAppInstaller(device) 208 | device = None 209 | 210 | print('Waiting for camera to switch...') 211 | for i in range(10): 212 | time.sleep(.5) 213 | try: 214 | devices = list(listDevices(driver)) 215 | if len(devices) == 1 and isinstance(devices[0], SonyMtpAppInstaller): 216 | device = devices[0] 217 | break 218 | except: 219 | pass 220 | else: 221 | print('Operation timed out. Please run this command again when your camera has connected.') 222 | 223 | if device: 224 | installApp(device, host, apkFile, appPackage, outFile) 225 | 226 | 227 | def appSelectionCommand(host=None): 228 | apps = listApps(host) 229 | for i, app in enumerate(apps): 230 | print(' [%2d] %s' % (i+1, app.package)) 231 | i = int(input('Enter number of app to install (0 to abort): ')) 232 | if i != 0: 233 | pkg = apps[i - 1].package 234 | print('') 235 | print('Installing %s' % pkg) 236 | return pkg 237 | 238 | 239 | def firmwareUpdateCommand(file, driverName=None): 240 | offset, size = firmware.readDat(file) 241 | 242 | with importDriver(driverName) as driver: 243 | device = getDevice(driver) 244 | if device: 245 | if isinstance(device, SonyMtpAppInstaller): 246 | print('Error: Cannot use camera in app install mode. Please restart the device.') 247 | return 248 | 249 | dev = SonyUpdaterCamera(device) 250 | 251 | print('Initializing firmware update') 252 | dev.init() 253 | file.seek(offset) 254 | dev.checkGuard(file, size) 255 | print('Updating from version %s to version %s' % dev.getFirmwareVersion()) 256 | 257 | try: 258 | dev.switchMode() 259 | print('Switching to updater mode') 260 | print('Please press Ok to reset the camera, then run this command again to install the firmware') 261 | 262 | except SonyUpdaterSequenceError: 263 | def progress(written, total): 264 | p = int(written * 20 / total) * 5 265 | if p != progress.percent: 266 | print('%d%%' % p) 267 | progress.percent = p 268 | progress.percent = -1 269 | 270 | print('Writing firmware') 271 | file.seek(offset) 272 | dev.writeFirmware(file, size, progress) 273 | dev.complete() 274 | print('Done') 275 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """A Google appengine application which allows you to download apps from the PMCA store and to install custom apps on your camera""" 2 | 3 | import datetime 4 | import hashlib 5 | import jinja2 6 | import json 7 | import os 8 | import webapp2 9 | 10 | from google.appengine.ext import blobstore 11 | from google.appengine.api import memcache 12 | from google.appengine.ext import ndb 13 | from google.appengine.ext.webapp import blobstore_handlers 14 | 15 | import config 16 | from pmca import appstore 17 | from pmca import marketclient 18 | from pmca import marketserver 19 | from pmca import spk 20 | from pmca import xpd 21 | 22 | 23 | class Task(ndb.Model): 24 | """The Entity used to save task data in the datastore between requests""" 25 | blob = ndb.BlobKeyProperty(indexed = False) 26 | app = ndb.StringProperty(indexed = False) 27 | date = ndb.DateTimeProperty(auto_now_add = True) 28 | completed = ndb.BooleanProperty(default = False, indexed = False) 29 | response = ndb.TextProperty() 30 | 31 | 32 | class Camera(ndb.Model): 33 | model = ndb.StringProperty(indexed=False) 34 | apps = ndb.JsonProperty() 35 | firstDate = ndb.DateTimeProperty(indexed=False, auto_now_add=True) 36 | lastDate = ndb.DateTimeProperty(indexed=False, auto_now=True) 37 | 38 | 39 | class Counter(ndb.Model): 40 | count = ndb.IntegerProperty(indexed=False, default=0) 41 | 42 | @classmethod 43 | def _getInstance(cls, id): 44 | return cls.get_by_id(id) or cls(id=id) 45 | 46 | @classmethod 47 | def getValue(cls, id): 48 | return cls._getInstance(id).count 49 | 50 | @classmethod 51 | @ndb.transactional 52 | def increment(cls, id): 53 | instance = cls._getInstance(id) 54 | instance.count += 1 55 | instance.put() 56 | 57 | class CameraModelCounter(Counter): 58 | pass 59 | 60 | class AppInstallCounter(Counter): 61 | pass 62 | 63 | class AppUpdateCounter(Counter): 64 | pass 65 | 66 | 67 | def cached(key, func, time=15*60): 68 | # TODO values > 1MB 69 | res = memcache.get(key) 70 | if not res: 71 | res = func() 72 | try: 73 | memcache.add(key, res, time) 74 | except: 75 | pass 76 | return res 77 | 78 | 79 | class RankedApp(appstore.App): 80 | def __getattr__(self, name): 81 | if name == 'rank': 82 | return self.dict['rank'] 83 | return super(RankedApp, self).__getattr__(name) 84 | 85 | class RankedAppStore(appstore.AppStore): 86 | def _loadApps(self): 87 | apps = list(super(RankedAppStore, self)._loadApps()) 88 | for dict in apps: 89 | dict['rank'] = AppInstallCounter.getValue(dict.get('package')) 90 | return sorted(apps, key=lambda dict: dict['rank'], reverse=True) 91 | def _createAppInstance(self, dict): 92 | return RankedApp(self.repo, dict) 93 | 94 | 95 | class CachedRelease(appstore.Release): 96 | def _loadAsset(self): 97 | return cached('app_release_asset_%s_%s_%s' % (self.package, self.version, self.url), super(CachedRelease, self)._loadAsset, 24*3600) 98 | 99 | class CachedApp(RankedApp): 100 | def _loadRelease(self): 101 | return cached('app_release_%s' % self.package, super(CachedApp, self)._loadRelease) 102 | def _createReleaseInstance(self, dict): 103 | return CachedRelease(self.package, dict) 104 | 105 | class CachedAppStore(RankedAppStore): 106 | def _loadApps(self): 107 | return cached('app_list', lambda: list(super(CachedAppStore, self)._loadApps())) 108 | def _createAppInstance(self, dict): 109 | return CachedApp(self.repo, dict) 110 | 111 | 112 | class AppStore(CachedAppStore): 113 | def __init__(self): 114 | repo = appstore.GithubApi(config.githubAppListUser, config.githubAppListRepo, (config.githubClientId, config.githubClientSecret)) 115 | super(AppStore, self).__init__(repo) 116 | 117 | 118 | def diffApps(oldApps, newApps): 119 | oldApps = oldApps.copy() 120 | installedApps = [] 121 | updatedApps = [] 122 | for app, version in newApps.items(): 123 | if oldApps.get(app) != version: 124 | if app not in oldApps: 125 | installedApps.append(app) 126 | updatedApps.append(app) 127 | oldApps[app] = version 128 | return oldApps, installedApps, updatedApps 129 | 130 | def updateAppStats(data): 131 | device = data.get('deviceinfo', {}) 132 | apps = dict((app['name'], app['version']) for app in data.get('applications', []) if 'name' in app and 'version' in app) 133 | if 'name' in device and 'productcode' in device and 'deviceid' in device: 134 | id = hashlib.sha1('camera_%s_%s_%s' % (device['name'], device['productcode'], device['deviceid'])).hexdigest() 135 | camera = Camera.get_by_id(id) 136 | isNewCamera = not camera 137 | if not camera: 138 | camera = Camera(id=id) 139 | camera.model = device['name'] 140 | camera.apps, installed, updated = diffApps(camera.apps or {}, apps) 141 | camera.put() 142 | if isNewCamera: 143 | CameraModelCounter.increment(camera.model) 144 | for app in installed: 145 | AppInstallCounter.increment(app) 146 | for app in updated: 147 | AppUpdateCounter.increment(app) 148 | 149 | 150 | class BaseHandler(webapp2.RequestHandler): 151 | def json(self, data): 152 | """Outputs the given dict as JSON""" 153 | def jsonRepr(o): 154 | if isinstance(o, datetime.datetime): 155 | return o.strftime('%Y-%m-%dT%H:%M:%SZ') 156 | raise TypeError 157 | self.output('application/json', json.dumps(data, default=jsonRepr)) 158 | 159 | def template(self, name, data = {}): 160 | """Renders a jinja2 template""" 161 | self.response.write(jinjaEnv.get_template(name).render(data)) 162 | 163 | def output(self, mimeType, data, filename = None): 164 | """Outputs a file with the given mimetype""" 165 | self.response.content_type = mimeType 166 | if filename: 167 | self.response.headers['Content-Disposition'] = 'attachment;filename="%s"' % filename 168 | self.response.write(data) 169 | 170 | 171 | class HomeHandler(BaseHandler): 172 | """Displays the home page""" 173 | def get(self): 174 | self.template('home.html') 175 | 176 | 177 | class ApkUploadHandler(BaseHandler, blobstore_handlers.BlobstoreUploadHandler): 178 | """Handles the upload of apk files to Google appengine""" 179 | def get(self): 180 | self.template('apk/upload.html', { 181 | 'uploadUrl': blobstore.create_upload_url(self.uri_for('apkUpload')), 182 | }) 183 | 184 | def post(self): 185 | uploads = self.get_uploads() 186 | if len(uploads) == 1: 187 | return self.redirect_to('blobPlugin', blobKey = uploads[0].key()) 188 | self.get() 189 | 190 | 191 | class AjaxUploadHandler(BaseHandler, blobstore_handlers.BlobstoreUploadHandler): 192 | """An api to upload apk files""" 193 | def get(self): 194 | self.json({'url': blobstore.create_upload_url(self.uri_for('ajaxUpload'))}) 195 | 196 | def post(self): 197 | uploads = self.get_uploads() 198 | if len(uploads) != 1: 199 | return self.error(400) 200 | return self.json({'key': str(uploads[0].key())}) 201 | 202 | 203 | class PluginHandler(BaseHandler): 204 | """Displays the page to start a new USB task sequence""" 205 | def get(self, args = {}): 206 | self.template('plugin/start.html', args) 207 | 208 | def getBlob(self, blobKey): 209 | blob = blobstore.get(blobKey) 210 | if not blob: 211 | return self.error(404) 212 | self.get({'blob': blob}) 213 | 214 | def getApp(self, appId): 215 | app = AppStore().apps.get(appId) 216 | if not app: 217 | return self.error(404) 218 | self.get({'app': app}) 219 | 220 | 221 | class PluginInstallHandler(BaseHandler): 222 | """Displays the help text to install the PMCA Downloader plugin""" 223 | def get(self): 224 | self.template('plugin/install.html', { 225 | 'text': cached('plugin_install_text', marketclient.getPluginInstallText, 24*3600), 226 | }) 227 | 228 | 229 | class TaskStartHandler(BaseHandler): 230 | """Creates a new task sequence and returns its id""" 231 | def get(self, task = None): 232 | if task is None: 233 | task = Task() 234 | taskId = task.put().id() 235 | self.json({'id': taskId}) 236 | 237 | def getBlob(self, blobKey): 238 | blob = blobstore.get(blobKey) 239 | if not blob: 240 | return self.error(404) 241 | self.get(Task(blob = blob.key())) 242 | 243 | def getApp(self, appId): 244 | if not AppStore().apps.get(appId): 245 | return self.error(404) 246 | self.get(Task(app = appId)) 247 | 248 | 249 | class TaskViewHandler(BaseHandler): 250 | """Displays the result of the given task sequence (called over AJAX)""" 251 | def queryTask(self, taskKey): 252 | task = ndb.Key(Task, int(taskKey)).get() 253 | if not task: 254 | return self.error(404) 255 | return { 256 | 'completed': task.completed, 257 | 'response': marketserver.parsePostData(task.response) if task.completed else None, 258 | } 259 | 260 | def getTask(self, taskKey): 261 | self.json(self.queryTask(taskKey)) 262 | 263 | def viewTask(self, taskKey): 264 | self.template('task/view.html', self.queryTask(taskKey)) 265 | 266 | 267 | class XpdHandler(BaseHandler): 268 | """Returns the xpd file corresponding to the given task sequence (called by the plugin)""" 269 | def get(self, taskKey): 270 | task = ndb.Key(Task, int(taskKey)).get() 271 | if not task: 272 | return self.error(404) 273 | xpdData = marketserver.getXpdResponse(task.key.id(), self.uri_for('portal', _scheme='https')) 274 | self.output(xpd.constants.mimeType, xpdData) 275 | 276 | 277 | class PortalHandler(BaseHandler): 278 | """Saves the data sent by the camera to the datastore and returns the actions to be taken next (called by the camera)""" 279 | def post(self): 280 | data = self.request.body 281 | dataDict = marketserver.parsePostData(data) 282 | taskKey = int(dataDict.get('session', {}).get('correlationid', 0)) 283 | task = ndb.Key(Task, taskKey).get() 284 | if not task: 285 | return self.error(404) 286 | if not task.completed and task.blob: 287 | response = marketserver.getJsonInstallResponse('App', self.uri_for('blobSpk', blobKey = task.blob, _full = True)) 288 | elif not task.completed and task.app: 289 | response = marketserver.getJsonInstallResponse('App', self.uri_for('appSpk', appId = task.app, _full = True)) 290 | else: 291 | response = marketserver.getJsonResponse() 292 | task.completed = True 293 | task.response = data 294 | task.put() 295 | updateAppStats(dataDict) 296 | self.output(marketserver.constants.jsonMimeType, response) 297 | 298 | 299 | class SpkHandler(BaseHandler): 300 | """Returns an spk file containing an apk file""" 301 | def get(self, apkData): 302 | spkData = spk.dump(apkData) 303 | self.output(spk.constants.mimeType, spkData, "app%s" % spk.constants.extension) 304 | 305 | def getBlob(self, blobKey): 306 | blob = blobstore.get(blobKey) 307 | if not blob: 308 | return self.error(404) 309 | with blob.open() as f: 310 | apkData = f.read() 311 | self.get(apkData) 312 | 313 | def getApp(self, appId): 314 | app = AppStore().apps.get(appId) 315 | if not app or not app.release: 316 | return self.error(404) 317 | apkData = app.release.asset 318 | self.get(apkData) 319 | 320 | 321 | class AppsHandler(BaseHandler): 322 | """Displays apps available on github""" 323 | def get(self): 324 | self.template('apps/list.html', { 325 | 'repo': (config.githubAppListUser, config.githubAppListRepo), 326 | 'apps': AppStore().apps, 327 | }) 328 | 329 | 330 | class CleanupHandler(BaseHandler): 331 | """Deletes all data older than one hour""" 332 | keepForMinutes = 60 333 | def get(self): 334 | deleteBeforeDate = datetime.datetime.now() - datetime.timedelta(minutes = self.keepForMinutes) 335 | for blob in blobstore.BlobInfo.gql('WHERE creation < :1', deleteBeforeDate): 336 | blob.delete() 337 | ndb.delete_multi(Task.gql('WHERE date < :1', deleteBeforeDate).fetch(keys_only = True)) 338 | 339 | 340 | class ApiAppsHandler(BaseHandler): 341 | def get(self): 342 | self.json([dict(list(app.dict.items()) + [('release', app.release.dict if app.release else None)]) for app in AppStore().apps.values()]) 343 | 344 | 345 | class ApiStatsHandler(BaseHandler): 346 | def post(self): 347 | data = self.request.body 348 | updateAppStats(json.loads(data)) 349 | 350 | 351 | app = webapp2.WSGIApplication([ 352 | webapp2.Route('/', HomeHandler, 'home'), 353 | webapp2.Route('/upload', ApkUploadHandler, 'apkUpload'), 354 | webapp2.Route('/plugin', PluginHandler, 'plugin'), 355 | webapp2.Route('/plugin/blob/', PluginHandler, 'blobPlugin', handler_method = 'getBlob'), 356 | webapp2.Route('/plugin/app/', PluginHandler, 'appPlugin', handler_method = 'getApp'), 357 | webapp2.Route('/plugin/install', PluginInstallHandler, 'installPlugin'), 358 | webapp2.Route('/ajax/upload', AjaxUploadHandler, 'ajaxUpload'), 359 | webapp2.Route('/ajax/task/start', TaskStartHandler, 'taskStart'), 360 | webapp2.Route('/ajax/task/start/blob/', TaskStartHandler, 'blobTaskStart', handler_method = 'getBlob'), 361 | webapp2.Route('/ajax/task/start/app/', TaskStartHandler, 'appTaskStart', handler_method = 'getApp'), 362 | webapp2.Route('/ajax/task/get/', TaskViewHandler, 'taskGet', handler_method = 'getTask'), 363 | webapp2.Route('/ajax/task/view/', TaskViewHandler, 'taskView', handler_method = 'viewTask'), 364 | webapp2.Route('/camera/xpd/', XpdHandler, 'xpd'), 365 | webapp2.Route('/camera/portal', PortalHandler, 'portal'), 366 | webapp2.Route('/download/spk/blob/', SpkHandler, 'blobSpk', handler_method = 'getBlob'), 367 | webapp2.Route('/download/spk/app/', SpkHandler, 'appSpk', handler_method = 'getApp'), 368 | webapp2.Route('/apps', AppsHandler, 'apps'), 369 | webapp2.Route('/cleanup', CleanupHandler), 370 | webapp2.Route('/api/apps', ApiAppsHandler, 'apiApps'), 371 | webapp2.Route('/api/stats', ApiStatsHandler, 'apiStats'), 372 | ]) 373 | 374 | jinjaEnv = jinja2.Environment( 375 | loader = jinja2.FileSystemLoader('templates'), 376 | autoescape = True 377 | ) 378 | jinjaEnv.globals['uri_for'] = webapp2.uri_for 379 | jinjaEnv.globals['versionHash'] = hashlib.sha1(os.environ['CURRENT_VERSION_ID']).hexdigest()[:8] 380 | -------------------------------------------------------------------------------- /pmca/usb/sony.py: -------------------------------------------------------------------------------- 1 | """Methods to communicate with Sony MTP devices""" 2 | 3 | import binascii 4 | from collections import namedtuple 5 | from io import BytesIO 6 | import time 7 | 8 | from . import * 9 | from ..util import * 10 | 11 | CameraInfo = namedtuple('CameraInfo', 'plist, modelName, modelCode, serial') 12 | 13 | ResponseMessage = namedtuple('ResponseMessage', 'data') 14 | RequestMessage = namedtuple('RequestMessage', 'data') 15 | InitResponseMessage = namedtuple('InitResponseMessage', 'protocols') 16 | SslStartMessage = namedtuple('SslStartMessage', 'connectionId, host, port') 17 | SslSendDataMessage = namedtuple('SslSendDataMessage', 'connectionId, data') 18 | SslEndMessage = namedtuple('SslEndMessage', 'connectionId') 19 | 20 | SONY_ID_VENDOR = 0x054c 21 | SONY_MANUFACTURER = 'Sony Corporation' 22 | SONY_MANUFACTURER_SHORT = 'Sony' 23 | SONY_MSC_MODELS = ['DSC', 'Camcorder'] 24 | 25 | 26 | def isSonyMscCamera(info): 27 | """Pass a mass storage device info tuple. Guesses if the device is a camera in mass storage mode.""" 28 | return info.manufacturer == SONY_MANUFACTURER_SHORT and info.model in SONY_MSC_MODELS 29 | 30 | def isSonyMtpCamera(info): 31 | """Pass an MTP device info tuple. Guesses if the device is a camera in MTP mode.""" 32 | operations = frozenset([ 33 | SonyMtpCamera.PTP_OC_SonyDiExtCmd_write, 34 | SonyMtpCamera.PTP_OC_SonyDiExtCmd_read, 35 | SonyMtpCamera.PTP_OC_SonyReqReconnect, 36 | ]) 37 | return info.manufacturer == SONY_MANUFACTURER and info.vendorExtension == '' and operations <= info.operationsSupported 38 | 39 | def isSonyMtpAppInstaller(info): 40 | """Pass an MTP device info tuple. Guesses if the device is a camera in app installation mode.""" 41 | operations = frozenset([ 42 | SonyMtpAppInstaller.PTP_OC_GetProxyMessageInfo, 43 | SonyMtpAppInstaller.PTP_OC_GetProxyMessage, 44 | SonyMtpAppInstaller.PTP_OC_SendProxyMessageInfo, 45 | SonyMtpAppInstaller.PTP_OC_SendProxyMessage, 46 | ]) 47 | return info.manufacturer == SONY_MANUFACTURER and 'sony.net/SEN_PRXY_MSG:' in info.vendorExtension and operations <= info.operationsSupported 48 | 49 | 50 | class SonyMscCamera(MscDevice): 51 | """Methods to communicate a camera in mass storage mode""" 52 | MSC_OC_ExtCmd = 0x7a 53 | 54 | MSC_SENSE_DeviceBusy = (0x9, 0x81, 0x81) 55 | 56 | WAIT_BEFORE_WRITE = .03 57 | WAIT_BEFORE_READ = .07 58 | 59 | def sendSonyExtCommand(self, cmd, data, bufferSize): 60 | command = dump8(self.MSC_OC_ExtCmd) + dump32le(cmd) + 7*b'\0' 61 | 62 | time.sleep(self.WAIT_BEFORE_WRITE) 63 | response = self.MSC_SENSE_DeviceBusy 64 | while response == self.MSC_SENSE_DeviceBusy: 65 | response = self.driver.sendWriteCommand(command, data) 66 | self._checkResponse(response) 67 | 68 | if bufferSize == 0: 69 | return b'' 70 | 71 | time.sleep(self.WAIT_BEFORE_READ) 72 | response = self.MSC_SENSE_DeviceBusy 73 | while response == self.MSC_SENSE_DeviceBusy: 74 | response, data = self.driver.sendReadCommand(command, bufferSize) 75 | self._checkResponse(response) 76 | return data 77 | 78 | 79 | class SonyMtpCamera(MtpDevice): 80 | """Methods to communicate a camera in MTP mode""" 81 | 82 | # Operation codes (defined in libInfraMtpServer.so) 83 | PTP_OC_SonyDiExtCmd_write = 0x9280 84 | PTP_OC_SonyDiExtCmd_read = 0x9281 85 | PTP_OC_SonyReqReconnect = 0x9282 86 | PTP_OC_SonyGetBurstshotGroupNum = 0x9283 87 | PTP_OC_SonyGetBurstshotObjectHandles = 0x9284 88 | PTP_OC_SonyGetAVIndexID = 0x9285 89 | 90 | def sendSonyExtCommand(self, cmd, data, bufferSize): 91 | response = self.PTP_RC_DeviceBusy 92 | while response == self.PTP_RC_DeviceBusy: 93 | response = self.driver.sendWriteCommand(self.PTP_OC_SonyDiExtCmd_write, [cmd], data) 94 | self._checkResponse(response) 95 | 96 | if bufferSize == 0: 97 | return b'' 98 | 99 | response = self.PTP_RC_DeviceBusy 100 | while response == self.PTP_RC_DeviceBusy: 101 | response, data = self.driver.sendReadCommand(self.PTP_OC_SonyDiExtCmd_read, [cmd]) 102 | self._checkResponse(response) 103 | return data 104 | 105 | def switchToMsc(self): 106 | """Tells the camera to switch to mass storage mode""" 107 | response = self.driver.sendCommand(self.PTP_OC_SonyReqReconnect, [0]) 108 | self._checkResponse(response) 109 | 110 | 111 | class SonyExtCmdCamera(object): 112 | """Methods to send Sony external commands to a camera""" 113 | 114 | # DevInfoSender (libInfraDevInfoSender.so) 115 | SONY_CMD_DevInfoSender_GetModelInfo = (1, 1) 116 | SONY_CMD_DevInfoSender_GetSupportedCommandIds = (1, 2) 117 | 118 | # KikiLogSender (libInfraKikiLogSender.so) 119 | SONY_CMD_KikiLogSender_InitKikiLog = (2, 1) 120 | SONY_CMD_KikiLogSender_ReadKikiLog = (2, 2) 121 | 122 | # ExtBackupCommunicator (libInfraExtBackupCommunicator.so) 123 | SONY_CMD_ExtBackupCommunicator_GetSupportedCommandIds = (4, 1) 124 | SONY_CMD_ExtBackupCommunicator_NotifyBackupType = (4, 2) 125 | SONY_CMD_ExtBackupCommunicator_NotifyBackupStart = (4, 3) 126 | SONY_CMD_ExtBackupCommunicator_NotifyBackupFinish = (4, 4) 127 | SONY_CMD_ExtBackupCommunicator_ForcePowerOff = (4, 5) 128 | SONY_CMD_ExtBackupCommunicator_GetRegisterableHostNum = (4, 6) 129 | SONY_CMD_ExtBackupCommunicator_GetRegisteredHostNetInfo = (4, 7) 130 | SONY_CMD_ExtBackupCommunicator_ForceRegistHostNetInfo = (4, 8) 131 | SONY_CMD_ExtBackupCommunicator_GetDeviceNetInfo = (4, 9) 132 | 133 | # ScalarExtCmdPlugIn (libInfraScalarExtCmdPlugIn.so) 134 | SONY_CMD_ScalarExtCmdPlugIn_GetSupportedCommandIds = (5, 1) 135 | SONY_CMD_ScalarExtCmdPlugIn_NotifyScalarDlmode = (5, 2) 136 | 137 | # LensCommunicator (libInfraLensCommunicator.so) 138 | SONY_CMD_LensCommunicator_GetSupportedCommandIds = (6, 1) 139 | SONY_CMD_LensCommunicator_GetMountedLensInfo = (6, 2) 140 | 141 | BUFFER_SIZE = 8192 142 | 143 | def __init__(self, dev): 144 | self.dev = dev 145 | 146 | def _sendCommand(self, cmd, bufferSize=BUFFER_SIZE): 147 | data = self.dev.sendSonyExtCommand(cmd[0], 4*b'\0' + dump32le(cmd[1]) + (self.BUFFER_SIZE-8)*b'\0', bufferSize) 148 | if bufferSize == 0: 149 | return b'' 150 | size = parse32le(data[:4]) 151 | return data[16:16+size] 152 | 153 | def getCameraInfo(self): 154 | """Gets information about the camera""" 155 | data = BytesIO(self._sendCommand(self.SONY_CMD_DevInfoSender_GetModelInfo)) 156 | plistSize = parse32le(data.read(4)) 157 | plistData = data.read(plistSize) 158 | data.read(4) 159 | modelSize = parse8(data.read(1)) 160 | modelName = data.read(modelSize).decode('latin1') 161 | modelCode = binascii.hexlify(data.read(5)).decode('latin1') 162 | serial = binascii.hexlify(data.read(4)).decode('latin1') 163 | return CameraInfo(plistData, modelName, modelCode, serial) 164 | 165 | def getKikiLog(self): 166 | """Reads the first part of /tmp/kikilog.dat""" 167 | self._sendCommand(self.SONY_CMD_KikiLogSender_InitKikiLog) 168 | kikilog = b'' 169 | remaining = 1 170 | while remaining: 171 | data = BytesIO(self._sendCommand(self.SONY_CMD_KikiLogSender_ReadKikiLog)) 172 | data.read(4) 173 | remaining = parse32le(data.read(4)) 174 | size = parse32le(data.read(4)) 175 | kikilog += data.read(size) 176 | return kikilog[24:] 177 | 178 | def switchToAppInstaller(self): 179 | """Tells the camera to switch to app installation mode""" 180 | self._sendCommand(self.SONY_CMD_ScalarExtCmdPlugIn_NotifyScalarDlmode, bufferSize=0) 181 | 182 | def powerOff(self): 183 | """Forces the camera to turn off""" 184 | self._sendCommand(self.SONY_CMD_ExtBackupCommunicator_ForcePowerOff, bufferSize=0) 185 | 186 | 187 | class SonyUpdaterSequenceError(Exception): 188 | def __init__(self): 189 | Exception.__init__(self, 'Wrong updater command sequence') 190 | 191 | 192 | class SonyUpdaterCamera(object): 193 | """Methods to send updater commands to a camera""" 194 | 195 | # from libupdaterufp.so 196 | SONY_CMD_Updater = 0 197 | CMD_INIT = 0x1 198 | CMD_CHK_GUARD = 0x10 199 | CMD_QUERY_VERSION = 0x20 200 | CMD_SWITCH_MODE = 0x30 201 | CMD_WRITE_FIRM = 0x40 202 | CMD_COMPLETE = 0x100 203 | CMD_GET_STATE = 0x200 204 | 205 | PacketHeader = Struct('PacketHeader', [ 206 | ('bodySize', Struct.INT32), 207 | ('protocolVersion', Struct.INT16), 208 | ('commandId', Struct.INT16), 209 | ('responseId', Struct.INT16), 210 | ('sequenceNumber', Struct.INT16), 211 | ('reserved', 20), 212 | ]) 213 | protocolVersion = 0x100 214 | 215 | GetStateResponse = Struct('GetStateResponse', [ 216 | ('currentStateId', Struct.INT16), 217 | ]) 218 | 219 | InitResponse = Struct('InitResponse', [ 220 | ('maxCmdPacketSize', Struct.INT32), 221 | ('maxResPacketSize', Struct.INT32), 222 | ('minTimeOut', Struct.INT32), 223 | ('intervalBeforeCommand', Struct.INT32), 224 | ('intervalBeforeResponse', Struct.INT32), 225 | ]) 226 | 227 | QueryVersionResponse = Struct('QueryVersionResponse', [ 228 | ('oldFirmMinorVersion', Struct.INT16), 229 | ('oldFirmMajorVersion', Struct.INT16), 230 | ('newFirmMinorVersion', Struct.INT16), 231 | ('newFirmMajorVersion', Struct.INT16), 232 | ]) 233 | 234 | WriteParam = Struct('WriteParam', [ 235 | ('dataNumber', Struct.INT32), 236 | ('remainingSize', Struct.INT32), 237 | ]) 238 | 239 | WriteResponse = Struct('WriteResponse', [ 240 | ('windowSize', Struct.INT32), 241 | ('numStatus', Struct.INT32), 242 | ]) 243 | 244 | WriteResponseStatus = Struct('WriteResponseStatus', [ 245 | ('code', Struct.INT16), 246 | ]) 247 | 248 | ERR_OK = 0x1 249 | ERR_BUSY = 0x2 250 | ERR_PROV = 0x100 251 | ERR_SEQUENCE = 0x101 252 | ERR_PACKET_SIZE = 0x102 253 | ERR_INVALID_PARAM = 0x103 254 | 255 | STAT_OK = 0x1 256 | STAT_BUSY = 0x2 257 | STAT_INVALID_DATA = 0x40 258 | STAT_LOW_BATTERY = 0x100 259 | STAT_INVALID_MODEL = 0x140 260 | STAT_INVALID_REGION = 0x141 261 | STAT_INVALID_VERSION = 0x142 262 | 263 | BUFFER_SIZE = 512 264 | 265 | def __init__(self, dev): 266 | self.dev = dev 267 | 268 | def _sendCommand(self, command, data=b'', bufferSize=BUFFER_SIZE): 269 | commandHeader = self.PacketHeader.pack( 270 | bodySize = len(data), 271 | protocolVersion = self.protocolVersion, 272 | commandId = command, 273 | responseId = 0, 274 | sequenceNumber = 0, 275 | ) 276 | response = self.dev.sendSonyExtCommand(self.SONY_CMD_Updater, commandHeader + data, bufferSize) 277 | 278 | if bufferSize == 0: 279 | return b'' 280 | responseHeader = self.PacketHeader.unpack(response) 281 | if responseHeader.responseId != self.ERR_OK: 282 | if responseHeader.responseId == self.ERR_SEQUENCE: 283 | raise SonyUpdaterSequenceError() 284 | raise Exception('Response error: 0x%x' % responseHeader.responseId) 285 | return response[self.PacketHeader.size:self.PacketHeader.size+responseHeader.bodySize] 286 | 287 | def _sendWriteCommands(self, command, file, size, progress=None): 288 | i = 0 289 | written = 0 290 | windowSize = 0 291 | while True: 292 | i += 1 293 | data = file.read(min(windowSize, size-written)) 294 | written += len(data) 295 | writeParam = self.WriteParam.pack(dataNumber=i, remainingSize=size-written) 296 | windowSize, status = self._parseWriteResponse(self._sendCommand(command, writeParam + data)) 297 | if progress: 298 | progress(written, size) 299 | if status == [self.STAT_OK]: 300 | break 301 | elif status != [self.STAT_BUSY]: 302 | raise Exception('Firmware update error: ' + ', '.join([self._statusToStr(s) for s in status])) 303 | 304 | def _parseWriteResponse(self, data): 305 | response = self.WriteResponse.unpack(data) 306 | status = [self.WriteResponseStatus.unpack(data, self.WriteResponse.size+i*self.WriteResponseStatus.size).code for i in range(response.numStatus)] 307 | return response.windowSize, status 308 | 309 | def _statusToStr(self, status): 310 | return { 311 | self.STAT_INVALID_DATA: 'Invalid data', 312 | self.STAT_LOW_BATTERY: 'Low battery', 313 | self.STAT_INVALID_MODEL: 'Invalid model', 314 | self.STAT_INVALID_REGION: 'Invalid region', 315 | self.STAT_INVALID_VERSION: 'Invalid version', 316 | }.get(status, 'Unknown (0x%x)' % status) 317 | 318 | def getState(self): 319 | return self.GetStateResponse.unpack(self._sendCommand(self.CMD_GET_STATE)).currentStateId 320 | 321 | def init(self): 322 | self.InitResponse.unpack(self._sendCommand(self.CMD_INIT)) 323 | 324 | def checkGuard(self, file, size): 325 | self._sendWriteCommands(self.CMD_CHK_GUARD, file, size) 326 | 327 | def getFirmwareVersion(self): 328 | response = self.QueryVersionResponse.unpack(self._sendCommand(self.CMD_QUERY_VERSION)) 329 | return ( 330 | '%x.%02x' % (response.oldFirmMajorVersion, response.oldFirmMinorVersion), 331 | '%x.%02x' % (response.newFirmMajorVersion, response.newFirmMinorVersion), 332 | ) 333 | 334 | def switchMode(self): 335 | reserved, status = self._parseWriteResponse(self._sendCommand(self.CMD_SWITCH_MODE)) 336 | if status != [self.STAT_OK] and status != [self.STAT_BUSY]: 337 | raise Exception('Updater mode switch failed') 338 | 339 | def writeFirmware(self, file, size, progress=None): 340 | self._sendWriteCommands(self.CMD_WRITE_FIRM, file, size, progress) 341 | 342 | def complete(self): 343 | self._sendCommand(self.CMD_COMPLETE, bufferSize=0) 344 | 345 | 346 | class SonyMtpAppInstaller(MtpDevice): 347 | """Methods to communicate a camera in app installation mode""" 348 | 349 | # Operation codes (defined in libUsbAppDlSvr.so) 350 | PTP_OC_GetProxyMessageInfo = 0x9488 351 | PTP_OC_GetProxyMessage = 0x9489 352 | PTP_OC_SendProxyMessageInfo = 0x948c 353 | PTP_OC_SendProxyMessage = 0x948d 354 | PTP_OC_GetDeviceCapability = 0x940a 355 | 356 | PTP_RC_NoData = 0xa488 357 | PTP_RC_SonyDeviceBusy = 0xa489 358 | PTP_RC_InternalError = 0xa806 359 | PTP_RC_TooMuchData = 0xa809 360 | 361 | SONY_MSG_Common = 0 362 | SONY_MSG_Common_Start = 0x400 363 | SONY_MSG_Common_Hello = 0x401 364 | SONY_MSG_Common_Bye = 0x402 365 | 366 | SONY_MSG_Tcp = 1 367 | SONY_MSG_Tcp_ProxyConnect = 0x501 368 | SONY_MSG_Tcp_ProxyDisconnect = 0x502 369 | SONY_MSG_Tcp_ProxyData = 0x503 370 | SONY_MSG_Tcp_ProxyEnd = 0x504 371 | 372 | SONY_MSG_Rest = 2 373 | SONY_MSG_Rest_In = 0 374 | SONY_MSG_Rest_Out = 2# anything != 0 375 | 376 | InfoMsgHeader = Struct('InfoMsgHeader', [ 377 | ('', 4),# read: 0x10000 / write: 0 378 | ('magic', Struct.INT16), 379 | ('', 2),# 0 380 | ('dataSize', Struct.INT32), 381 | ('', 2),# read: 0x3000 / write: 0 382 | ('padding', 42), 383 | ], Struct.LITTLE_ENDIAN) 384 | InfoMsgHeaderMagic = 0xb481 385 | 386 | MsgHeader = Struct('MsgHeader', [ 387 | ('type', Struct.INT16), 388 | ], Struct.BIG_ENDIAN) 389 | 390 | CommonMsgHeader = Struct('CommonMsgHeader', [ 391 | ('version', Struct.INT16), 392 | ('type', Struct.INT32), 393 | ('size', Struct.INT32), 394 | ('padding', 6), 395 | ], Struct.BIG_ENDIAN) 396 | CommonMsgVersion = 1 397 | 398 | TcpMsgHeader = Struct('TcpMsgHeader', [ 399 | ('socketFd', Struct.INT32), 400 | ], Struct.BIG_ENDIAN) 401 | 402 | RestMsgHeader = Struct('RestMsgHeader', [ 403 | ('type', Struct.INT16), 404 | ('size', Struct.INT16), 405 | ], Struct.BIG_ENDIAN) 406 | 407 | ProxyConnectMsgHeader = Struct('ProxyConnectMsgHeader', [ 408 | ('port', Struct.INT16), 409 | ('hostSize', Struct.INT32), 410 | ], Struct.BIG_ENDIAN) 411 | 412 | SslDataMsgHeader = Struct('SslDataMsgHeader', [ 413 | ('size', Struct.INT32), 414 | ], Struct.BIG_ENDIAN) 415 | 416 | ProtocolMsgHeader = Struct('ProtocolMsgHeader', [ 417 | ('numProtocols', Struct.INT32), 418 | ], Struct.BIG_ENDIAN) 419 | 420 | ProtocolMsgProto = Struct('ProtocolMsgProto', [ 421 | ('name', Struct.STR % 4), 422 | ('id', Struct.INT16), 423 | ], Struct.BIG_ENDIAN) 424 | ProtocolMsgProtos = [(b'TCPT', 0x01), (b'REST', 0x100)] 425 | 426 | ThreeValueMsg = Struct('ThreeValueMsg', [ 427 | ('a', Struct.INT16), 428 | ('b', Struct.INT32), 429 | ('c', Struct.INT32), 430 | ], Struct.BIG_ENDIAN) 431 | 432 | def _write(self, data): 433 | info = self.InfoMsgHeader.pack(magic=self.InfoMsgHeaderMagic, dataSize=len(data)) 434 | 435 | response = self.PTP_RC_SonyDeviceBusy 436 | while response == self.PTP_RC_SonyDeviceBusy: 437 | response = self.driver.sendWriteCommand(self.PTP_OC_SendProxyMessageInfo, [], info) 438 | self._checkResponse(response) 439 | 440 | response = self.PTP_RC_SonyDeviceBusy 441 | while response == self.PTP_RC_SonyDeviceBusy: 442 | response = self.driver.sendWriteCommand(self.PTP_OC_SendProxyMessage, [], data) 443 | self._checkResponse(response) 444 | 445 | def _read(self): 446 | response, data = self.driver.sendReadCommand(self.PTP_OC_GetProxyMessageInfo, [0]) 447 | self._checkResponse(response) 448 | self.InfoMsgHeader.unpack(data) 449 | 450 | response, data = self.driver.sendReadCommand(self.PTP_OC_GetProxyMessage, [0]) 451 | self._checkResponse(response, [self.PTP_RC_NoData]) 452 | return data 453 | 454 | def receive(self): 455 | """Receives and parses the next message from the camera""" 456 | data = self._read() 457 | if data == b'': 458 | return None 459 | 460 | type = self.MsgHeader.unpack(data).type 461 | data = data[self.MsgHeader.size:] 462 | 463 | if type == self.SONY_MSG_Common: 464 | header = self.CommonMsgHeader.unpack(data) 465 | data = data[self.CommonMsgHeader.size:header.size] 466 | if header.type == self.SONY_MSG_Common_Hello: 467 | n = self.ProtocolMsgHeader.unpack(data).numProtocols 468 | protos = (self.ProtocolMsgProto.unpack(data, self.ProtocolMsgHeader.size+i*self.ProtocolMsgProto.size) for i in range(n)) 469 | return InitResponseMessage([(p.name, p.id) for p in protos]) 470 | elif header.type == self.SONY_MSG_Common_Bye: 471 | raise Exception('Bye from camera') 472 | else: 473 | raise Exception('Unknown common message type: 0x%x' % header.type) 474 | 475 | elif type == self.SONY_MSG_Tcp: 476 | header = self.CommonMsgHeader.unpack(data) 477 | data = data[self.CommonMsgHeader.size:header.size] 478 | tcpHeader = self.TcpMsgHeader.unpack(data) 479 | data = data[self.TcpMsgHeader.size:] 480 | if header.type == self.SONY_MSG_Tcp_ProxyConnect: 481 | proxy = self.ProxyConnectMsgHeader.unpack(data) 482 | host = data[self.ProxyConnectMsgHeader.size:self.ProxyConnectMsgHeader.size+proxy.hostSize] 483 | return SslStartMessage(tcpHeader.socketFd, host.decode('latin1'), proxy.port) 484 | elif header.type == self.SONY_MSG_Tcp_ProxyDisconnect: 485 | return SslEndMessage(tcpHeader.socketFd) 486 | elif header.type == self.SONY_MSG_Tcp_ProxyData: 487 | size = self.SslDataMsgHeader.unpack(data).size 488 | return SslSendDataMessage(tcpHeader.socketFd, data[self.SslDataMsgHeader.size:self.SslDataMsgHeader.size+size]) 489 | else: 490 | raise Exception('Unknown tcp message type: 0x%x' % header.type) 491 | 492 | elif type == self.SONY_MSG_Rest: 493 | header = self.RestMsgHeader.unpack(data) 494 | data = data[self.RestMsgHeader.size:self.RestMsgHeader.size+header.size] 495 | if header.type == self.SONY_MSG_Rest_Out: 496 | return ResponseMessage(data) 497 | elif header.type == self.SONY_MSG_Rest_In: 498 | return RequestMessage(data) 499 | else: 500 | raise Exception('Unknown rest message type: 0x%x' % header.type) 501 | 502 | else: 503 | raise Exception('Unknown message type: 0x%x' % type) 504 | 505 | def _receiveResponse(self, type): 506 | msg = None 507 | while msg is None: 508 | msg = self.receive() 509 | if not isinstance(msg, type): 510 | raise Exception('Wrong response: %s' % str(msg)) 511 | return msg 512 | 513 | def _sendMessage(self, type, data): 514 | self._write(self.MsgHeader.pack(type=type) + data) 515 | 516 | def _sendCommonMessage(self, subType, data, type=SONY_MSG_Common): 517 | self._sendMessage(type, self.CommonMsgHeader.pack( 518 | version = self.CommonMsgVersion, 519 | type = subType, 520 | size = self.CommonMsgHeader.size + len(data) 521 | ) + data) 522 | 523 | def _sendTcpMessage(self, subType, socketFd, data): 524 | self._sendCommonMessage(subType, self.TcpMsgHeader.pack(socketFd=socketFd) + data, self.SONY_MSG_Tcp) 525 | 526 | def _sendRestMessage(self, subType, data): 527 | self._sendMessage(self.SONY_MSG_Rest, self.RestMsgHeader.pack(type=subType, size=len(data)) + data) 528 | 529 | def emptyBuffer(self): 530 | """Receives and discards all pending messages from the camera""" 531 | msg = True 532 | while msg: 533 | msg = self.receive() 534 | 535 | def sendInit(self, protocols=ProtocolMsgProtos): 536 | """Send an initialization message to the camera""" 537 | data = self.ProtocolMsgHeader.pack(numProtocols=len(protocols)) 538 | for name, id in protocols: 539 | data += self.ProtocolMsgProto.pack(name=name, id=id) 540 | self._sendCommonMessage(self.SONY_MSG_Common_Start, data) 541 | return self._receiveResponse(InitResponseMessage).protocols 542 | 543 | def sendRequest(self, data): 544 | """Sends a REST request to the camera. Used to start communication""" 545 | self._sendRestMessage(self.SONY_MSG_Rest_Out, data) 546 | return self._receiveResponse(ResponseMessage).data 547 | 548 | def sendSslData(self, req, data): 549 | """Sends raw SSL response data to the camera""" 550 | self._sendTcpMessage(self.SONY_MSG_Tcp_ProxyData, req, self.SslDataMsgHeader.pack(size=len(data)) + data) 551 | 552 | def sendSslEnd(self, req): 553 | """Lets the camera know that the SSL socket has been closed""" 554 | self._sendTcpMessage(self.SONY_MSG_Tcp_ProxyEnd, req, self.ThreeValueMsg.pack(a=1, b=1, c=0)) 555 | 556 | def sendEnd(self): 557 | """Ends the communication with the camera""" 558 | self._sendCommonMessage(self.SONY_MSG_Common_Bye, self.ThreeValueMsg.pack(a=0, b=0, c=0)) 559 | --------------------------------------------------------------------------------