Using a SONY QX10 (and QX100) lenscamera with L5 possible!

(short version: possible, usable, can be made better and more usable by community)

So… I like to have a camera on my L5 (or any phone) to take a quick pic. But I also like to get a bit better pics and only the high end phones do those, compared to actual cameras. Two different needs. When I originally awaited L5, I came across the Sony DSC-QX10 and DSC-QX100 lens cameras (possibly also QX30 and QX1, but those are bigger, less convenient): 18Mpix, optical zoom lenses that have a battery, memorycard and a wifi (and micro usb for data and charging, can use OTG adapter to DL pics to phone when not using camera). They are some ten eight-ish years old but actually pretty good quality and the range with bigger (than phones) optics are from a different world. Bigger sensor too (not referring to megapixels but noise ratio and quality, although more mpix than L5) - although, the current highends and their algorithms are comparable IMHO (lots has happened in a decade).



Anyway, I wanted to see if this was an option - especially since some holiday travel is in the near future - and got one rather cheap second hand. And wow, who ever at Sony designed the thing, knew back then what the measurements for L5 would be. Or L5 was designed to perfectly fit the clamp that the QX has. Someone earned an upvote for that :+1: But if you’re using a case/shell, it won’t fit (L5 is Max width and thickness) [edit: it is possible to force the clamp even on to a case width but not recommended - small adjustments to case would solve this]. On the other hand, it should be simple to add the required holes on the back of a case design to replace the removable clamp (and probably even save a few mm on thickness plus some airvent holes).


The hard part - and what I’d say still needs a bit of work - was to find software to connect the QX to L5. There have been scripts to do that for linux in the past but for someone not fluent in python, it was a hurdle. There are a few scripts (some which have actual action buttons for stuff) that use the open api but all seemed to be for python2 and/or pyqt4, which are not supported and available on L5. Luckily one was made with python3 and I managed to figure out to make enough of an upgrade from pyqt4 to pyqt5 (very very little actually, just added “from PyQt5.QtWidgets import *” and edited the view size) to make it work. Well, it works enough for minimal use: it gives a viewfinder picture. [edit to add: the QX does not have to be attached to the phone to work - they can be quite a distance apart - but for now, the remote shutter is not there, nor other still/video controls]


So, yay, anyone can use a QX10 (or QX100, a bit different specs but same control api) while having a view with their L5 locally (wifi is from device to device only - not net via wifi when taking pics). Yes, I’m using the new wlan card. I’ll add the script below. Original script can be found here (only the .py is needed).

Now, I don’t have the skills, but if someone form the community would be up to it, there are example scripts (python2 mostly) that give some pointers on what to do (1, 2).

While connecting, it takes a minute and for some reason the camera takes three pictures that it downloads to L5 (directory of the script). But other pics that you take are recorded on the QX memory card as you use the physical buttons on its side (zoom +/- and shutter - which can be seen as a bonus, since it’s probably faster and more reliable that way. That’s not to say that those same buttons shouldn’t be on the software too. The window also closes poorly, not like L5 apps do. There are a number of api options that can be found from the manual, that haven’t been implemented (see for instance the original code’s github).

Currently I’m cleaning up my install of the scripts to a designated folder and starting to look at how to set up a desktop shortcut. For now, this “PoC” works for me. I hope this interests others too.

sony-qx-controller-L5.py (original):

#!/usr/bin/env python3

# Script for managing Sony QX10 or QX100 from Librem5 linux phone.
# Supports AUTHORIZATION (!!!), which allows to use a lot of undocumented commands (such as setStillSize and others).

# Before using this script: manually connect to Wi-Fi; set up IP 10.0.1.1, mask 255.0.0.0 (and if that's not enough - default gateway: 10.0.0.1).
# Note. Password can be taken from a text file in the internal memory of the device, connect it with USB and switch on for accessing this memory.

# This script depended on PyQt4 for displaying liveview. It has been updated to use pyqt5. Original script: github.com/Tsar/sony_qx_controller

import sys, json, time
import http.client, urllib.parse
import threading
import base64, hashlib

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

lock = threading.Lock()

app = QApplication(sys.argv)
image = QImage()

class ImageDisplay(QLabel):
    def __init__(self):
        QLabel.__init__(self)

    def paintEvent(self, event):
        global lock
        lock.acquire()
        try:
            self.setPixmap(QPixmap.fromImage(image))
        finally:
            lock.release()
        QLabel.paintEvent(self, event)

imgDisplay = ImageDisplay()
imgDisplay.setMinimumSize(320, 240)
imgDisplay.show()

pId = 0
headers = {"Content-type": "text/plain", "Accept": "*/*", "X-Requested-With": "com.sony.playmemories.mobile"}

AUTH_CONST_STRING = "90adc8515a40558968fe8318b5b023fdd48d3828a2dda8905f3b93a3cd8e58dc"
METHODS_TO_ENABLE = "camera/setFlashMode:camera/getFlashMode:camera/getSupportedFlashMode:camera/getAvailableFlashMode:camera/setExposureCompensation:camera/getExposureCompensation:camera/getSupportedExposureCompensation:camera/getAvailableExposureCompensation:camera/setSteadyMode:camera/getSteadyMode:camera/getSupportedSteadyMode:camera/getAvailableSteadyMode:camera/setViewAngle:camera/getViewAngle:camera/getSupportedViewAngle:camera/getAvailableViewAngle:camera/setMovieQuality:camera/getMovieQuality:camera/getSupportedMovieQuality:camera/getAvailableMovieQuality:camera/setFocusMode:camera/getFocusMode:camera/getSupportedFocusMode:camera/getAvailableFocusMode:camera/setStillSize:camera/getStillSize:camera/getSupportedStillSize:camera/getAvailableStillSize:camera/setBeepMode:camera/getBeepMode:camera/getSupportedBeepMode:camera/getAvailableBeepMode:camera/setCameraFunction:camera/getCameraFunction:camera/getSupportedCameraFunction:camera/getAvailableCameraFunction:camera/setLiveviewSize:camera/getLiveviewSize:camera/getSupportedLiveviewSize:camera/getAvailableLiveviewSize:camera/setTouchAFPosition:camera/getTouchAFPosition:camera/cancelTouchAFPosition:camera/setFNumber:camera/getFNumber:camera/getSupportedFNumber:camera/getAvailableFNumber:camera/setShutterSpeed:camera/getShutterSpeed:camera/getSupportedShutterSpeed:camera/getAvailableShutterSpeed:camera/setIsoSpeedRate:camera/getIsoSpeedRate:camera/getSupportedIsoSpeedRate:camera/getAvailableIsoSpeedRate:camera/setExposureMode:camera/getExposureMode:camera/getSupportedExposureMode:camera/getAvailableExposureMode:camera/setWhiteBalance:camera/getWhiteBalance:camera/getSupportedWhiteBalance:camera/getAvailableWhiteBalance:camera/setProgramShift:camera/getSupportedProgramShift:camera/getStorageInformation:camera/startLiveviewWithSize:camera/startIntervalStillRec:camera/stopIntervalStillRec:camera/actFormatStorage:system/setCurrentTime"

def postRequest(conn, target, req):
    global pId
    pId += 1
    req["id"] = pId
    print("REQUEST  [%s]: " % target, end = "")
    print(req)
    conn.request("POST", "/sony/" + target, json.dumps(req), headers)
    response = conn.getresponse()
    print("RESPONSE [%s]: " % target, end = "")
    #print(response.status, response.reason)
    data = json.loads(response.read().decode("UTF-8"))
    print(data)
    if data["id"] != pId:
        print("FATAL ERROR: Response id does not match")
        return {}
    if "error" in data:
        print("WARNING: Response contains error code: %d; error message: [%s]" % tuple(data["error"]))
    print("")
    return data

def exitWithError(conn, message):
    print("ERROR: %s" % message)
    conn.close()
    sys.exit(1)

def parseUrl(url):
    parsedUrl = urllib.parse.urlparse(url)
    return parsedUrl.hostname, parsedUrl.port, parsedUrl.path + "?" + parsedUrl.query, parsedUrl.path[1:]

def downloadImage(url):
    host, port, address, img_name = parseUrl(url)
    conn2 = http.client.HTTPConnection(host, port)
    conn2.request("GET", address)
    response = conn2.getresponse()
    if response.status == 200:
        with open(img_name, "wb") as img:
            img.write(response.read())
    else:
        print("ERROR: Could not download picture, error = [%d %s]" % (response.status, response.reason))

#def symb5(c):
#    s = str(c)
#    while len(s) < 5:
#        s = "0" + s
#    return s

def liveviewFromUrl(url):
    global image
    global lock
    host, port, address, img_name = parseUrl(url)
    conn3 = http.client.HTTPConnection(host, port)
    conn3.request("GET", address)
    response = conn3.getresponse()
    #flow = open("liveview", "wb")
    if response.status == 200:
        buf = b''
        c = 0
        while not response.closed:
            nextPart = response.read(1024)
            #flow.write(nextPart)
            #flow.flush()

            # TODO: It would be better to use description from the documentation (page 51) for parsing liveview stream
            jpegStart = nextPart.find(b'\xFF\xD8\xFF')
            jpegEnd = nextPart.find(b'\xFF\xD9')
            if jpegEnd != -1:
                c += 1
                buf += nextPart[:jpegEnd + 2]
                #with open("live_" + symb5(c) + ".jpg", "wb") as liveImg:
                #    liveImg.write(buf)
                lock.acquire()
                try:
                    image.loadFromData(buf)
                finally:
                    lock.release()
            if jpegStart != -1:
                buf = nextPart[jpegStart:]
            else:
                buf += nextPart

def communicationThread():
    #target = "/sony/camera"
    #target = "/sony/system"
    #target = "/sony/accessControl"

    #req = {"method": "getVersions", "params": [], "id": 1}
    #req = {"method": "getApplicationInfo", "params": [], "id": 2, "version": "1.0"}
    #req = {"method": "getEvent", "params": [False], "id": 3, "version": "1.0"}        # (!!!) get method list
    #req = {"method": "getEvent", "params": [True], "id": 4, "version": "1.0"}
    #req = {"method": "getMethodTypes", "params": ["1.0"], "id": 8, "version": "1.0"}

    conn = http.client.HTTPConnection("10.0.0.1", 10000)

    resp = postRequest(conn, "camera", {"method": "getVersions", "params": []})
    if resp["result"][0][0] != "1.0":
        exitWithError(conn, "Unsupported version")

    resp = postRequest(conn, "accessControl", {"method": "actEnableMethods", "params": [{"methods": "", "developerName": "", "developerID": "", "sg": ""}], "version": "1.0"})
    dg = resp["result"][0]["dg"]

    h = hashlib.sha256()
    h.update(bytes(AUTH_CONST_STRING + dg, "UTF-8"))
    sg = base64.b64encode(h.digest()).decode("UTF-8")

    resp = postRequest(conn, "accessControl", {"method": "actEnableMethods", "params": [{"methods": METHODS_TO_ENABLE, "developerName": "Sony Corporation", "developerID": "7DED695E-75AC-4ea9-8A85-E5F8CA0AF2F3", "sg": sg}], "version": "1.0"})

    resp = postRequest(conn, "system", {"method": "getMethodTypes", "params": ["1.0"], "version": "1.0"})
    resp = postRequest(conn, "accessControl", {"method": "getMethodTypes", "params": ["1.0"], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "getStillSize", "params": [], "version": "1.0"})
    #resp = postRequest(conn, "camera", {"method": "getSupportedStillSize", "params": [], "version": "1.0"})
    #resp = postRequest(conn, "camera", {"method": "getAvailableStillSize", "params": [], "version": "1.0"})

    #resp = postRequest(conn, "camera", {"method": "setStillSize", "params": ["20M", "3:2"], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "setFocusMode", "params": ["AF-S"], "version": "1.0"})
    resp = postRequest(conn, "camera", {"method": "getFocusMode", "params": [], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "stopLiveview", "params": [], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "setPostviewImageSize", "params": ["Original"], "version": "1.0"})
    while "error" in resp:
        resp = postRequest(conn, "camera", {"method": "setPostviewImageSize", "params": ["Original"], "version": "1.0"})
    resp = postRequest(conn, "camera", {"method": "getPostviewImageSize", "params": [], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "actTakePicture", "params": [], "version": "1.0"})
    downloadImage(resp["result"][0][0])

    resp = postRequest(conn, "camera", {"method": "setPostviewImageSize", "params": ["2M"], "version": "1.0"})
    while "error" in resp:
        resp = postRequest(conn, "camera", {"method": "setPostviewImageSize", "params": ["2M"], "version": "1.0"})
    resp = postRequest(conn, "camera", {"method": "getPostviewImageSize", "params": [], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "actTakePicture", "params": [], "version": "1.0"})
    downloadImage(resp["result"][0][0])

    resp = postRequest(conn, "camera", {"method": "setPostviewImageSize", "params": ["Original"], "version": "1.0"})
    while "error" in resp:
        resp = postRequest(conn, "camera", {"method": "setPostviewImageSize", "params": ["Original"], "version": "1.0"})
    resp = postRequest(conn, "camera", {"method": "getPostviewImageSize", "params": [], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "actTakePicture", "params": [], "version": "1.0"})
    downloadImage(resp["result"][0][0])

    resp = postRequest(conn, "camera", {"method": "getAvailableFocusMode", "params": [], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "getSupportedFocusMode", "params": [], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "getTouchAFPosition", "params": [], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "getSupportedFNumber", "params": [], "version": "1.0"})

    #resp = postRequest(conn, "camera", {"method": "setFocusMode", "params": ["MF"], "version": "1.0"})
    #resp = postRequest(conn, "camera", {"method": "getFocusMode", "params": [], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "getEvent", "params": [False], "version": "1.0"})

    resp = postRequest(conn, "camera", {"method": "startLiveview", "params": [], "version": "1.0"})
    liveview = threading.Thread(target = liveviewFromUrl, args = (resp["result"][0],))
    liveview.start()

    resp = postRequest(conn, "camera", {"method": "actZoom", "params": ["in", "start"], "version": "1.0"})
    time.sleep(2)
    resp = postRequest(conn, "camera", {"method": "actZoom", "params": ["in", "stop"], "version": "1.0"})

    time.sleep(3)

    resp = postRequest(conn, "camera", {"method": "actZoom", "params": ["out", "start"], "version": "1.0"})
    time.sleep(2.5)
    resp = postRequest(conn, "camera", {"method": "actZoom", "params": ["out", "stop"], "version": "1.0"})

    conn.close()

if __name__ == "__main__":
    communication = threading.Thread(target = communicationThread)
    communication.start()

    sys.exit(app.exec_())
22 Likes

Some notes for possible future improvements:

  • starting the script takes three unwanted pics automatically and blindly (before view)
  • saves both jpg and dng from all pics - maybe just jpg on most would be convenient
  • image from camera is larger than viewport/window, so it gets cropped - still usable but not ideal
  • needs also remote shutter button on window for more elaborate use cases (camera at a distance, like selfie stick or stand)
  • video recording mode enabling would be nice (as a webcam too?)
  • re-naming of files with date and time would be ideal (they come from camera, not script but maybe…)
  • close and exit cleanly - maybe close wifi availability of L5 too (security by default preference)?
  • selection of frame size would be bonus (why use anything else than full?)
  • battery level indication would be bonus (now can just estimate from number of photos)
  • have an option for L5 camera to take simultaneous picture (a general wider image vs. sony zoomed image)
  • select save folder
  • option for location data to metadata (select what metadata is used)
  • gif recording?
2 Likes