Skip to content

Instantly share code, notes, and snippets.

@JackDesBwa
Last active December 8, 2022 02:13
Show Gist options
  • Save JackDesBwa/f6084d71bcf58633842f6fdaec8b6b32 to your computer and use it in GitHub Desktop.
Save JackDesBwa/f6084d71bcf58633842f6fdaec8b6b32 to your computer and use it in GitHub Desktop.
Scripts for remote access to the QooCam EGO

QooCam EGO remote scripts

This is a collection of independant scripts to show how to communicate with the QooCam EGO remotely.

They were tested with firmware 2.1.29 in the camera.

They are quite minimal so that you can inpire yourself to create your own awesome utility.

You can also use them directly as is.

#!/usr/bin/env python3
# Script to capture a video with remote interface of the QooCam EGO
import urllib.request
import json
import sys
if len(sys.argv) != 2:
print(f"usage: {sys.argv[0]} <IP>")
exit()
urllib.request.urlopen(f'http://{sys.argv[1]}/osc/info')
with urllib.request.urlopen(f'http://{sys.argv[1]}/osc/commands/execute', data=b'{"name":"camera.startCapture"}') as f:
jresponse = json.loads(f.read().decode())
print(jresponse)
input('Type enter to stop')
with urllib.request.urlopen(f'http://{sys.argv[1]}/osc/commands/execute', data=b'{"name":"camera.stopCapture"}') as f:
jresponse = json.loads(f.read().decode())
print(jresponse)
#!/usr/bin/env python3
# Script to download a thumbnail with remote interface of the QooCam EGO
import urllib.request
import json
import sys
if len(sys.argv) < 3:
print(f"usage: {sys.argv[0]} <IP> <path> [other paths]")
exit()
urllib.request.urlopen(f'http://{sys.argv[1]}/osc/info')
cmd = {
"name": "camera.delete",
"parameters": {
"fileUrls": sys.argv[2:]
}
}
with urllib.request.urlopen(f'http://{sys.argv[1]}/osc/commands/execute', data=json.dumps(cmd).encode()) as f:
jresponse = json.loads(f.read().decode())
print(jresponse)
#!/usr/bin/env python3
# Script to download a file with remote interface of the QooCam EGO
import urllib.request
import json
import sys
if len(sys.argv) != 4:
print(f"usage: {sys.argv[0]} <IP> <path> <out_file>")
exit()
urllib.request.urlopen(f'http://{sys.argv[1]}/osc/info')
cmd = {
"name": "camera._downloadFile",
"parameters": {
"type": "icon",
"fileName": sys.argv[2]
}
}
with urllib.request.urlopen(f'http://{sys.argv[1]}/osc/commands/execute', data=json.dumps(cmd).encode()) as f:
response = f.read()
if response[0] == '{':
print('Not found')
else:
with open(sys.argv[3], 'wb') as f:
f.write(response)
#!/usr/bin/env python3
# Script to download a thumbnail with remote interface of the QooCam EGO
import urllib.request
import json
import sys
if len(sys.argv) != 4:
print(f"usage: {sys.argv[0]} <IP> <path> <out_file.jpg>")
exit()
urllib.request.urlopen(f'http://{sys.argv[1]}/osc/info')
cmd = {
"name": "camera._getThumbnail",
"parameters": {
"type": "icon",
"fileName": sys.argv[2]
}
}
with urllib.request.urlopen(f'http://{sys.argv[1]}/osc/commands/execute', data=json.dumps(cmd).encode()) as f:
response = f.read()
if response[0] == '{':
print('Not found')
else:
with open(sys.argv[3], 'wb') as f:
f.write(response)
#!/usr/bin/env python3
# Script to get option/config with remote interface of the QooCam EGO
import urllib.request
import argparse
import json
import sys
options = [
"captureMode",
"captureModeSupport",
"captureStatus",
"captureStatusSupport",
"dateTimeZone",
"exposureCompensation",
"exposureCompensationSupport",
"exposureDelay",
"exposureDelaySupport",
"exposureProgram",
"exposureProgramSupport",
"fileFormat",
"fileFormatSupport",
"imageStabilization",
"imageStabilizationSupport",
"remainingSpace",
"totalSpace",
"whiteBalance",
"whiteBalanceSupport",
"_focusType",
"_focusTypeSupport",
"_lightFrequency",
"_lightFrequencySupport",
"_shutterSpeedMaxLimit",
"_shutterSpeedMaxLimitSupport",
"_temperature",
"_temperatureSupport",
]
parser = argparse.ArgumentParser(description='QooCam EGO get options')
parser.add_argument('ip', nargs=1, help='IP of the device')
for o in options:
parser.add_argument('--'+o, dest=o, action='store_const', const=True)
args = parser.parse_args()
options_to_get = [k for k, v in vars(args).items() if v is not None and k != 'ip']
urllib.request.urlopen(f'http://{args.ip[0]}/osc/info')
cmd = {
"name": "camera.getOptions",
"parameters": {
"optionNames": options_to_get
}
}
with urllib.request.urlopen(f'http://{args.ip[0]}/osc/commands/execute', data=json.dumps(cmd).encode()) as f:
jresponse = json.loads(f.read().decode())
if 'error' in jresponse:
print('Error!', jresponse['error']['message'])
else:
for k, v in jresponse['results'].items():
print(k+':', v)
#!/usr/bin/env python3
# Script to list files with remote interface of the QooCam EGO
# Equivalent to (+formatting)
# curl -i 'http://${CAMIP}/osc/commands/execute' --data '{"name":"camera.listFiles"}'
import urllib.request
import json
import sys
if len(sys.argv) != 2:
print(f"usage: {sys.argv[0]} <IP>")
exit()
urllib.request.urlopen(f'http://{sys.argv[1]}/osc/info')
with urllib.request.urlopen(f'http://{sys.argv[1]}/osc/commands/execute', data=b'{"name":"camera.listFiles"}') as f:
jresponse = json.loads(f.read().decode())
medias = jresponse['results']['medias']
cmd = {
"name": "camera._getMediasSize",
"parameters": {
"medias": medias
}
}
with urllib.request.urlopen(f'http://{sys.argv[1]}/osc/commands/execute', data=json.dumps(cmd).encode()) as f:
jresponse = json.loads(f.read().decode())
sizes = jresponse['results']['size']
def human_readable(size):
for x in ['B', 'KiB', 'MiB', 'GiB']:
if size < 1024: return f'{size:3.1f} {x}'
size /= 1024
return f'{size:3.1f} GiB'
print('\n'.join((' '*10+human_readable(s))[-10:]+'\t'+p for s,p in zip(sizes, medias)))
#!/usr/bin/env python3
# Script to display live preview of the QooCam EGO in anaglyph
import time
import sys
import cv2
if len(sys.argv) != 2:
print(f"usage: {sys.argv[0]} <IP>")
exit()
try:
cap = cv2.VideoCapture(int(sys.argv[1]))
except:
cap = cv2.VideoCapture(f"rtsp://{sys.argv[1]}/liveRTSP/av0")
ret, frame = cap.read()
if not ret: exit()
h,w,_ = frame.shape
w2 = w//2
print("""Help
====
q: quit
f: print FPS
g: print dp/dv
a: stereo anaglyph
m: monoscopic (left)
s: switch mono/anaglyph
4/6: change window position
2/8: change vertical error
5: reset dp/dv
""")
show_fps = False
stereo = True
dp = 0
dv = 0
while True:
start = time.time()
ret, frame = cap.read()
if not ret: break
img = frame[-dv if dv < 0 else 0 : -dv if dv > 0 else None, w2 - dp if dp < 0 else w2 : -dp if dp > 0 else None]
if stereo:
b,g,r = cv2.split(frame[dv if dv > 0 else 0 : dv if dv < 0 else None, dp if dp > 0 else 0 : w2 + dp if dp < 0 else w2])
img[:,:,2] = 0.114*b+0.587*g+0.299*r
cv2.imshow('QooCam EGO live anaglyph', img)
key = cv2.waitKey(1)
if key == ord('f'): show_fps = not show_fps
elif key == ord('g'): print(f"dp={dp}, dv={dv}")
elif key == ord('a'): stereo = True
elif key == ord('m'): stereo = False
elif key == ord('s'): stereo = not stereo
elif key == ord('q'): break
elif key == ord('4'): dp = max(1 - w2, dp - 1)
elif key == ord('6'): dp = min(dp + 1, w2 - 1)
elif key == ord('8'): dv = max(1 - h//2, dv - 1)
elif key == ord('2'): dv = min(dv + 1, h//2 - 1)
elif key == ord('5'): dv = dp = 0
end = time.time()
if show_fps:
seconds = end - start
print (f"Time taken : {1/seconds} fps ; {seconds} seconds")
#!/usr/bin/env python3
# Script to get option/config with remote interface of the QooCam EGO
import urllib.request
import argparse
import json
import sys
options = [
("captureMode", str),
("dateTimeZone", str),
("exposureCompensation", float),
("exposureDelay", int),
("exposureProgram", int),
("imageStabilization", str), # Accepted but not used (?)
("whiteBalance", str), # Accepted but not used (?)
("_focusType", str),
("_lightFrequency", str),
("_shutterSpeedMaxLimit", str),
("_temperature", str),
]
parser = argparse.ArgumentParser(description='QooCam EGO set options')
parser.add_argument('ip', nargs=1, help='IP of the device')
for o in options:
parser.add_argument('--'+o[0], dest=o[0], type=o[1], nargs=1)
args = parser.parse_args()
options_to_set = {}
for k, v in vars(args).items():
if v is not None and k != 'ip':
options_to_set[k] = v[0]
urllib.request.urlopen(f'http://{args.ip[0]}/osc/info')
cmd = {
"name": "camera.setOptions",
"parameters": {
"options": options_to_set
}
}
with urllib.request.urlopen(f'http://{args.ip[0]}/osc/commands/execute', data=json.dumps(cmd).encode()) as f:
jresponse = json.loads(f.read().decode())
if 'error' in jresponse:
print('Error!', jresponse['error']['message'])
else:
for k, v in jresponse['results'].items():
print(k+':', v)
#!/usr/bin/env python3
# Script to take a picture with remote interface of the QooCam EGO
# Equivalent to
# curl -i 'http://${CAMIP}/osc/commands/execute' --data '{"name":"camera.takePicture"}'
import urllib.request
import json
import sys
if len(sys.argv) != 2:
print(f"usage: {sys.argv[0]} <IP>")
exit()
urllib.request.urlopen(f'http://{sys.argv[1]}/osc/info')
with urllib.request.urlopen(f'http://{sys.argv[1]}/osc/commands/execute', data=b'{"name":"camera.takePicture"}') as f:
jresponse = json.loads(f.read().decode())
print(jresponse)
@DeanZwikel
Copy link

Hi. Thank you for sharing.

I tested the qoocamego_dl_file-py script with my EGO and it aborted with the following message. Could I ask do you know what might cause this? Thank you.

Traceback (most recent call last):
  File "C:\Users\zwike\AppData\Local\Programs\Python\Python37\lib\http\client.py", line 574, in _readall_chunked
    value.append(self._safe_read(chunk_left))
  File "C:\Users\zwike\AppData\Local\Programs\Python\Python37\lib\http\client.py", line 622, in _safe_read
    raise IncompleteRead(b''.join(s), amt)
http.client.IncompleteRead: IncompleteRead(957752 bytes read, 90824 more expected)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\GETEGO\qoocamego_dl_file.py", line 22, in <module>
    response = conn.getresponse().read()
  File "C:\Users\zwike\AppData\Local\Programs\Python\Python37\lib\http\client.py", line 464, in read
    return self._readall_chunked()
  File "C:\Users\zwike\AppData\Local\Programs\Python\Python37\lib\http\client.py", line 578, in _readall_chunked
    raise IncompleteRead(b''.join(value))
http.client.IncompleteRead: IncompleteRead(205520896 bytes read)

@JackDesBwa
Copy link
Author

Could I ask do you know what might cause this?

It looks like it failed on a large file.

By reading the doc again, I saw that the lib that was called is "normally not used directly".
So I rewrote the whole scripts with a better suited library.

However, the first test I did after the change also failed (OK on small image, KO on 1min video)
Trying alternative methods based on utilities coded by good programmers had the same behavior.
Then I noticed that the firmware looked crashed on the device (there was only camera feedback on the screen, no icons or reaction on buttons)

After reboot, all other tries (10) were successful.
So my guess is that the firmware of the QooCam EGO is buggy on this feature.
Unfortunately, it seems that I cannot do more on my side.

@DeanZwikel
Copy link

DeanZwikel commented Jun 24, 2022

Yes. I noticed the same thing when it fails. I have to hold down the camera power button until it turns off and then turn it back on.

I tried again with your new version that uses the different library but got the same result. See below output. Its with the same large file (2.x GB). Could I ask what is the largest mp4 that you have tried?

I tried downloading it from the camera to an Android phone using the QooCam app and it was successful. So, it seems that the way the QooCam app is doing the network transfer is different.

Thank you

Traceback (most recent call last):
  File "C:\Users\zwike\AppData\Local\Programs\Python\Python37\lib\http\client.py", line 574, in _readall_chunked
    value.append(self._safe_read(chunk_left))
  File "C:\Users\zwike\AppData\Local\Programs\Python\Python37\lib\http\client.py", line 622, in _safe_read
    raise IncompleteRead(b''.join(s), amt)
http.client.IncompleteRead: IncompleteRead(513912 bytes read, 534664 more expected)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\GETEGO\qoocamego_dl_file.py", line 22, in <module>
    response = f.read()
  File "C:\Users\zwike\AppData\Local\Programs\Python\Python37\lib\http\client.py", line 464, in read
    return self._readall_chunked()
  File "C:\Users\zwike\AppData\Local\Programs\Python\Python37\lib\http\client.py", line 578, in _readall_chunked
    raise IncompleteRead(b''.join(value))
http.client.IncompleteRead: IncompleteRead(200278016 bytes read)

@JackDesBwa
Copy link
Author

Could I ask what is the largest mp4 that you have tried?

It was a 450MB file.
Just tried with a 2GB one and it fails (firmware crash), even through wget.

However, with large files, it would be preferable to transfer by reading the SD and not via (slow) wifi.

it seems that the way the QooCam app is doing the network transfer is different.

This app does not work well on my phone and was not able to download a single file at all.
Maybe there is a parameter to cut in chunks, or something similar that I was not able to find.
If you find some info, let us know.

@DeanZwikel
Copy link

DeanZwikel commented Jun 24, 2022

It was a 450MB file.

I'm suprised that would work. The largest I can use is ~ 200 MB.

However, with large files, it would be preferable to transfer by reading the SD and not via (slow) wifi.

Yes. It was convenient to use for testing.

If you find some info, let us know.

I may try to use a network sniffer to see the lower level protocol if possible.
I'm also trying to contact Kandao for information regarding developer support.

@DeanZwikel
Copy link

Maybe there is a parameter to cut in chunks, or something similar that I was not able to find.

I tried using requests with chunks and it still fails this same way.

@DeanZwikel
Copy link

I did some investigation with a network sniffer. I watched the QooCam Android app communicate with the EGO. It used port 5555 instead of port 80 that I had been using. I tried using 5555 but was unable to get a response from the EGO. I suspect the QooCam app and EGO may be using some type of secure connection. That may be the reason for the difference in behavior. That said its hard to understand how port 80 seems to work for shorter length messages and media files.

@JackDesBwa
Copy link
Author

I updated the firmware to version 2.1.29 and this HTTP interface seems to work differently. 😕

It still says that it is "Kandao Osc Server" in the response's headers, which previously directed me to Google OSC API and despite the differences this helped me to write those scripts, but it seems that the scripts no longer work [not tested every single one ; live preview is independent of this API and still works]. There is a "commMode type mismatch" message on those I tested.

Concerning download of big files, this version has a USB transfer feature, with a transfer rate of about 22MB/s observed.

As for the 5555 port, understanding what is going on this interface might be a significant job.

@DeanZwikel
Copy link

DeanZwikel commented Jul 3, 2022

I had also updated the firmware to version 2.1.29 and can confirm that the HTTP interface works differently.

Through more testing with a sniffer I can confirm that the QooCam Android and iOS Apps communicate with the EGO via port 5555. This is a secure connection. Before communicating with the EGO, the Apps do a TLS handshake with a server in China to obtain credentials. The Apps then uses those credentials to communicate OSC commands to and download files from the EGO over a secure connection.

Some of the OSC commands still work using port 80. So far, I have confirmed that the /osc/info and the /osc/commands/execute camera.listFiles and camera._getMediasSize commands work. Sometimes the /osc/state works. Sometimes it returns an error. The /osc/commands/execute camera._download_File works with Photo files and with smaller Video files. For larger files, the download aborts before completing. Using the sniffer, it appears that the EGO sends a message indicating the file transmission is complete even though there is more to send. Not sure what the upper limit is on Video file size that works. So far I have observed download speed via WiFi of ~ 10 MB/sec. This is a very convenient way to download Photos and smaller Video files. For larger files, the USB interface is probably preferred.

@DeanZwikel
Copy link

DeanZwikel commented Jul 3, 2022

I found that "/osc/commands/execute" request will work for some commands (for example "camera.takePicture") if it is preceeded by an "/osc/info" request. This is also true for an "/osc/state" request. "/osc/info" seems to put the EGO in the correct "commMode".

@DeanZwikel
Copy link

DeanZwikel commented Jul 3, 2022

I think if you add an "/osc/info" request to the beginning of each of your scripts they may work.

@JackDesBwa
Copy link
Author

Good catch.
Updated.

@DeanZwikel
Copy link

DeanZwikel commented Jul 3, 2022

The CURL "--next" option can be used to send the /osc/info and /osc/commands/execute in one command. For example:

curl http://192.168.1.106/osc/info --next http://192.168.1.106/osc/commands/execute -d "{\"name\":\"camera.takePicture\"}"

This is for Windows which requires the " to be escaped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment