Skip to content

Commit 2459aac

Browse files
ddd999ddd999
authored andcommitted
Video: Photo/video mode
- Enables local still image and video recording by re-organizing the camera subsystem into three distinct modes: streaming video (default), still photo capture, and video capture - Adds Python helper scripts to detect and control cameras for local still/video capture - Now sends a VideoStreamInformation packet whenever video streaming is started - Captures a photo or starts/stops video recording either when the button is pressed on the web UI, or when a MavLink MAV_CMD_DO_DIGICAM_CONTROL message is received - Unit testing and linting fixes
1 parent c16840c commit 2459aac

File tree

12 files changed

+2515
-656
lines changed

12 files changed

+2515
-656
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
# production
1010
/build
1111

12+
# photo/video media
13+
/media
14+
1215
# misc
1316
.DS_Store
1417
.env
@@ -28,4 +31,4 @@ settingsTest.json
2831
/flightlogs
2932
.nyc_output
3033

31-
/local_modules/long/node_modules
34+
/local_modules/long/node_modules

package-lock.json

Lines changed: 89 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"nyc": "^17.1.0",
7575
"pino-colada": "^2.2.2",
7676
"pino-http": "^10.3.0",
77+
"sinon": "^21.0.0",
7778
"vite-plugin-eslint": "^1.8.1",
7879
"vitest": "^3.1.1"
7980
},

python/get_camera_caps.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env python3
2+
# -*- coding:utf-8 vi:ts=4:noexpandtab -*-
3+
4+
import subprocess
5+
import re
6+
import sys
7+
import json
8+
import os
9+
10+
def check_if_v4l2_ctl_avail():
11+
try:
12+
subprocess.run(['v4l2-ctl', '--help'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
13+
except subprocess.CalledProcessError:
14+
print("v4l2-ctl is not installed. Exiting.")
15+
sys.exit(1)
16+
17+
def get_subdev_paths():
18+
base_path = '/dev'
19+
return sorted([os.path.join(base_path, dev) for dev in os.listdir(base_path) if dev.startswith('v4l-subdev')])
20+
21+
def get_video_device_paths():
22+
base_path = '/dev'
23+
return sorted([os.path.join(base_path, dev) for dev in os.listdir(base_path) if dev.startswith('video')])
24+
25+
def get_mbus_codes(dev_path):
26+
command = f"v4l2-ctl -d {dev_path} --list-subdev-mbus-codes 0"
27+
result = subprocess.run(command, shell=True, text=True, capture_output=True)
28+
if result.returncode != 0:
29+
return []
30+
pattern = r"0x([0-9a-fA-F]+):\s+([A-Za-z0-9_]+)"
31+
return re.findall(pattern, result.stdout)
32+
33+
def get_resolutions(dev_path, mbus_code):
34+
command = f"v4l2-ctl -d {dev_path} --list-subdev-framesizes pad=0,code=0x{mbus_code}"
35+
result = subprocess.run(command, shell=True, text=True, capture_output=True)
36+
if result.returncode != 0:
37+
return []
38+
pattern = r"Size Range: (\d+)x(\d+)"
39+
matches = re.findall(pattern, result.stdout)
40+
return [{'width': int(w), 'height': int(h)} for w, h in matches]
41+
42+
def get_formats_and_resolutions(dev_path):
43+
command = f"v4l2-ctl -d {dev_path} --list-formats-ext"
44+
result = subprocess.run(command, shell=True, text=True, capture_output=True)
45+
if result.returncode != 0:
46+
return []
47+
48+
devices = []
49+
fmt_pattern = r"^\s*\[\d+\]: '(\w+)' \(.*?\)"
50+
size_pattern = r"\s+Size: Discrete (\d+)x(\d+)"
51+
current_fmt = None
52+
53+
for line in result.stdout.splitlines():
54+
fmt_match = re.match(fmt_pattern, line)
55+
if fmt_match:
56+
current_fmt = fmt_match.group(1)
57+
continue
58+
59+
size_match = re.match(size_pattern, line)
60+
if size_match and current_fmt:
61+
width, height = map(int, size_match.groups())
62+
devices.append({
63+
'format': current_fmt,
64+
'width': width,
65+
'height': height,
66+
'label': f"{width}x{height}_{current_fmt}",
67+
'value': f"{current_fmt}_{width}x{height}"
68+
})
69+
70+
return devices
71+
72+
def get_card_name(dev_path):
73+
command = f"v4l2-ctl -d {dev_path} --all"
74+
result = subprocess.run(command, shell=True, text=True, capture_output=True)
75+
if result.returncode != 0:
76+
return None
77+
78+
match = re.search(r"Card type\s+:\s+(.+)", result.stdout)
79+
if match:
80+
return match.group(1).strip()
81+
82+
return None
83+
84+
check_if_v4l2_ctl_avail()
85+
86+
devices = []
87+
88+
# Process CSI cameras
89+
for dev_path in get_subdev_paths():
90+
mbus_codes = get_mbus_codes(dev_path)
91+
if not mbus_codes:
92+
continue
93+
94+
card_name = get_card_name(dev_path) or "Unnamed CSI Camera"
95+
96+
device_caps = {
97+
# Don't specify a device path for CSI cameras,
98+
# but generate a unique ID for them.
99+
'id': f"CSI-{re.sub(r'[^a-zA-Z0-9]', '_', card_name)}",
100+
'device': None,
101+
'type': 'CSI',
102+
'card_name': card_name,
103+
'caps': []
104+
}
105+
106+
for mbus_code, pixel_format in mbus_codes:
107+
resolutions = get_resolutions(dev_path, mbus_code)
108+
109+
for res in resolutions:
110+
fmt = pixel_format.split("MEDIA_BUS_FMT_")[1] if "MEDIA_BUS_FMT_" in pixel_format else pixel_format
111+
cap_info = {
112+
'format': fmt,
113+
'width': res['width'],
114+
'height': res['height'],
115+
'label': f"{res['width']}x{res['height']}_{fmt}",
116+
'value': f"{mbus_code}_{fmt}_{res['width']}x{res['height']}"
117+
}
118+
device_caps['caps'].append(cap_info)
119+
120+
if device_caps['caps']:
121+
devices.append(device_caps)
122+
123+
# Process UVC cameras
124+
for dev_path in get_video_device_paths():
125+
caps = get_formats_and_resolutions(dev_path)
126+
if caps:
127+
devices.append({
128+
'id': dev_path, # use the device path as the unique ID for UVC cameras
129+
'device': dev_path,
130+
'type': 'UVC',
131+
'card_name': get_card_name(dev_path) or "Unnamed UVC Camera",
132+
'caps': caps
133+
})
134+
135+
print(json.dumps(devices, indent=4))

0 commit comments

Comments
 (0)