├── LICENSE
├── .gitignore
├── screenshots
├── gui_launcher.png
├── gui_screen_gtk_1.png
├── gui_screen_gtk_2.png
├── gui_screen_gtk_3.png
├── gui_screen_gtk_4.png
├── gui_screen_gtk_5.png
└── gui_screen_gtk_6.png
├── pkg
├── hu.irl.cameractrls.desktop
├── hu.irl.cameractrls.svg
└── hu.irl.cameractrls.metainfo.xml
├── cameractrlsd.py
├── cameraptzspnav.py
├── README.md
├── cameraptzgame.py
├── CHANGELOG.md
├── cameraptzmidi.py
├── cameractrlsgtk4.py
├── cameractrlsgtk.py
└── cameraview.py
/LICENSE:
--------------------------------------------------------------------------------
1 | LGPL-3.0-or-later
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | rust
3 |
--------------------------------------------------------------------------------
/screenshots/gui_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyersoyer/cameractrls/HEAD/screenshots/gui_launcher.png
--------------------------------------------------------------------------------
/screenshots/gui_screen_gtk_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyersoyer/cameractrls/HEAD/screenshots/gui_screen_gtk_1.png
--------------------------------------------------------------------------------
/screenshots/gui_screen_gtk_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyersoyer/cameractrls/HEAD/screenshots/gui_screen_gtk_2.png
--------------------------------------------------------------------------------
/screenshots/gui_screen_gtk_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyersoyer/cameractrls/HEAD/screenshots/gui_screen_gtk_3.png
--------------------------------------------------------------------------------
/screenshots/gui_screen_gtk_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyersoyer/cameractrls/HEAD/screenshots/gui_screen_gtk_4.png
--------------------------------------------------------------------------------
/screenshots/gui_screen_gtk_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyersoyer/cameractrls/HEAD/screenshots/gui_screen_gtk_5.png
--------------------------------------------------------------------------------
/screenshots/gui_screen_gtk_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soyersoyer/cameractrls/HEAD/screenshots/gui_screen_gtk_6.png
--------------------------------------------------------------------------------
/pkg/hu.irl.cameractrls.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Type=Application
3 | Version=1.0
4 | Name=Cameractrls
5 | Comment=Camera controls for Linux
6 | Exec=cameractrlsgtk.py
7 | Icon=hu.irl.cameractrls
8 | Terminal=false
9 | Categories=Settings;AudioVideo;Video;Office;Utility;GNOME;GTK;
10 | Keywords=picture;photos;camera;webcam;
11 |
--------------------------------------------------------------------------------
/cameractrlsd.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys, os, ctypes, ctypes.util, logging, getopt, time
4 | from collections import namedtuple
5 | from struct import unpack_from, calcsize
6 | from cameractrls import CameraCtrls, find_symlink_in, get_configfilename
7 |
8 | clib = ctypes.util.find_library('c')
9 | if clib is None:
10 | logging.error('libc not found, please install the libc package!')
11 | sys.exit(2)
12 | c = ctypes.CDLL(clib)
13 |
14 | logging.getLogger().setLevel(logging.INFO)
15 |
16 | inotify_init1 = c.inotify_init1
17 | inotify_init1.restype = ctypes.c_int
18 | inotify_init1.argtypes = [ctypes.c_int]
19 | # int inotify_init1(int flags);
20 |
21 | inotify_add_watch = c.inotify_add_watch
22 | inotify_add_watch.restype = ctypes.c_int
23 | inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32]
24 | # int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
25 |
26 | inotify_rm_watch = c.inotify_rm_watch
27 | inotify_rm_watch.restypes = ctypes.c_int
28 | inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int]
29 | # int inotify_rm_watch(int fd, int wd);
30 |
31 | IN_CLOEXEC = os.O_CLOEXEC
32 | IN_NONBLOCK = os.O_NONBLOCK
33 |
34 | IN_ACCESS = 0x00000001 # File was accessed
35 | IN_MODIFY = 0x00000002 # File was modified
36 | IN_ATTRIB = 0x00000004 # Metadata changed
37 | IN_CLOSE_WRITE = 0x00000008 # Writable file was closed
38 | IN_CLOSE_NOWRITE = 0x00000010 # Unwritable file closed
39 | IN_OPEN = 0x00000020 # File was opened
40 | IN_MOVED_FROM = 0x00000040 # File was moved from X
41 | IN_MOVED_TO = 0x00000080 # File was moved to Y
42 | IN_CREATE = 0x00000100 # Subfile was created
43 | IN_DELETE = 0x00000200 # Subfile was deleted
44 | IN_DELETE_SELF = 0x00000400 # Self was deleted
45 | IN_MOVE_SELF = 0x00000800 # Self was moved
46 |
47 | IN_ALL_EVENTS = ( \
48 | IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE | \
49 | IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM | \
50 | IN_MOVED_TO | IN_DELETE | IN_CREATE | IN_DELETE_SELF | \
51 | IN_MOVE_SELF \
52 | )
53 |
54 | IN_UNMOUNT = 0x00002000 # Backing fs was unmounted
55 | IN_Q_OVERFLOW = 0x00004000 # Event queued overflowed
56 | IN_IGNORED = 0x00008000 # File was ignored
57 |
58 | IN_CLOSE = (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)
59 | IN_MOVE = (IN_MOVED_FROM | IN_MOVED_TO)
60 |
61 | IN_ONLYDIR = 0x01000000 # only watch the path if it is a directory
62 | IN_DONT_FOLLOW = 0x02000000 # don't follow a sym link
63 | IN_EXCL_UNLINK = 0x04000000 # exclude events on unlinked objects
64 | IN_MASK_CREATE = 0x10000000 # only create watches
65 | IN_MASK_ADD = 0x20000000 # add to the mask of an already existing watch
66 | IN_ISDIR = 0x40000000 # event occurred against dir
67 | IN_ONESHOT = 0x80000000 # only send event once
68 |
69 | NAME_MAX = 255
70 |
71 | def usage():
72 | print(f'usage: {sys.argv[0]} [--help]\n')
73 | print(f'optional arguments:')
74 | print(f' -h, --help show this help message and exit')
75 |
76 | def preset_device(device):
77 | logging.debug(f'trying to preset_device: {device}')
78 |
79 | configfile = get_configfilename(device)
80 |
81 | # if config file does not exists, we should not open the device
82 | if not os.path.exists(configfile):
83 | logging.debug(f'preset_device: {configfile} does not exists')
84 | return
85 |
86 | logging.info(f'preset_device: {device}')
87 |
88 | try:
89 | fd = os.open(device, os.O_RDWR, 0)
90 | except Exception as e:
91 | logging.warning(f'os.open({device}, os.O_RDWR, 0) failed: {e}')
92 | return
93 |
94 | errs = []
95 |
96 | camera_ctrls = CameraCtrls(device, fd)
97 | camera_ctrls.setup_ctrls({'preset': 'load_1'}, errs)
98 | if errs:
99 | logging.warning(f'preset_device: failed to load_1: {errs}')
100 |
101 | os.close(fd)
102 |
103 | Event = namedtuple('Event', ['wd', 'mask', 'cookie', 'namesize', 'name'])
104 | EVENT_FMT = 'iIII'
105 | EVENT_SIZE = calcsize(EVENT_FMT)
106 |
107 | def parse_events(data):
108 | pos = 0
109 | events = []
110 | while pos < len(data):
111 | wd, mask, cookie, namesize = unpack_from(EVENT_FMT, data, pos)
112 | pos += EVENT_SIZE + namesize
113 | name = data[pos - namesize : pos].split(b'\x00', 1)[0]
114 | events.append(Event(wd, mask, cookie, namesize, name.decode()))
115 | return events
116 |
117 | def main():
118 | try:
119 | arguments, values = getopt.getopt(sys.argv[1:], 'h', ['help'])
120 | except getopt.error as err:
121 | print(err)
122 | usage()
123 | return 2
124 |
125 | for current_argument, current_value in arguments:
126 | if current_argument in ('-h', '--help'):
127 | usage()
128 | return 0
129 |
130 | dev_path = '/dev'
131 | v4l_paths = ['/dev/v4l/by-id/', '/dev/v4l/by-path/']
132 |
133 | for v4l_path in v4l_paths:
134 | for dirpath, dirs, files in os.walk(v4l_path):
135 | for device in files:
136 | preset_device(os.path.join(v4l_path, device))
137 |
138 | fd = inotify_init1(0)
139 | if fd == -1:
140 | logging.error(f'inotify_init1 failed')
141 | return 1
142 |
143 | wd = inotify_add_watch(fd, dev_path.encode(), IN_CREATE)
144 | if wd == -1:
145 | logging.error(f'inotify_add_watch failed {dev_path}')
146 | return 1
147 |
148 | while True:
149 | data = os.read(fd, EVENT_SIZE + NAME_MAX + 1)
150 | for e in parse_events(data):
151 | logging.debug(f'event: {e}')
152 | if e.name.startswith('video'):
153 | time.sleep(2) # waiting for udev to create dirs
154 | path = find_symlink_in(os.path.join(dev_path, e.name), v4l_paths)
155 | if path is None:
156 | logging.warning(f'can\'t find {e.name} in {v4l_paths}')
157 | continue
158 | preset_device(path.path)
159 |
160 | if __name__ == '__main__':
161 | sys.exit(main())
162 |
--------------------------------------------------------------------------------
/cameraptzspnav.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys, os, ctypes, ctypes.util, logging, getopt, select, signal
4 | from cameractrls import CameraCtrls, PTZController
5 |
6 | spnavlib = ctypes.util.find_library('spnav')
7 | if spnavlib is None:
8 | logging.error('spnav not found, please install the libspnav package!')
9 | sys.exit(2)
10 | spnav = ctypes.CDLL(spnavlib)
11 |
12 | #logging.getLogger().setLevel(logging.INFO)
13 |
14 | enum = ctypes.c_uint
15 |
16 | spnav_event_type = enum
17 | (
18 | SPNAV_EVENT_ANY, # used by spnav_remove_events()
19 | SPNAV_EVENT_MOTION,
20 | SPNAV_EVENT_BUTTON, # includes both press and release
21 | ) = range(3)
22 |
23 | class spnav_event_motion(ctypes.Structure):
24 | _fields_ = [
25 | ('type', ctypes.c_int),
26 | ('x', ctypes.c_int),
27 | ('y', ctypes.c_int),
28 | ('z', ctypes.c_int),
29 | ('rx', ctypes.c_int),
30 | ('ry', ctypes.c_int),
31 | ('rz', ctypes.c_int),
32 | ('period', ctypes.c_uint),
33 | ('data', ctypes.POINTER(ctypes.c_int)),
34 | ]
35 |
36 | class spnav_event_button(ctypes.Structure):
37 | _fields_ = [
38 | ('type', ctypes.c_int),
39 | ('press', ctypes.c_int),
40 | ('bnum', ctypes.c_int),
41 | ]
42 |
43 | class spnav_event(ctypes.Union):
44 | _fields_ = [
45 | ('type', ctypes.c_int),
46 | ('motion', spnav_event_motion),
47 | ('button', spnav_event_button),
48 | ]
49 |
50 | spnav_open = spnav.spnav_open
51 | spnav_open.restype = ctypes.c_int
52 | spnav_open.argtypes = []
53 | # int spnav_open(void);
54 |
55 | spnav_dev_name = spnav.spnav_dev_name
56 | spnav_dev_name.restype = ctypes.c_int
57 | spnav_dev_name.argtypes = [ctypes.c_char_p, ctypes.c_int]
58 | # int spnav_dev_name(char *buf, int bufsz);
59 |
60 | spnav_fd = spnav.spnav_fd
61 | spnav_fd.restype = ctypes.c_int
62 | spnav_fd.argtypes = []
63 | # int spnav_fd(void);
64 |
65 | spnav_poll_event = spnav.spnav_poll_event
66 | spnav_poll_event.restype = ctypes.c_int
67 | spnav_poll_event.argtypes = [ctypes.POINTER(spnav_event)]
68 | # int spnav_poll_event(spnav_event *event);
69 |
70 | spnav_close = spnav.spnav_close
71 | spnav_close.restype = ctypes.c_int
72 | spnav_close.argtypes = []
73 | # int spnav_close(void);
74 |
75 | th = 133
76 |
77 | def check_step(cb, value):
78 | if -th < value < th:
79 | return 0
80 | value = round(value/128)
81 | return cb(value, [])
82 |
83 | def check_speed(cb, value):
84 | if -th < value < th:
85 | value = 0
86 | value = round(value/128)
87 | return cb(value, [])
88 |
89 | def usage():
90 | print(f'usage: {sys.argv[0]} [-h] [-l] [-c] [-d DEVICE]\n')
91 | print(f'optional arguments:')
92 | print(f' -h, --help show this help message and exit')
93 | print(f' -l, --list list space navigators')
94 | print(f' -c, --controller use space navigator (0..n), default first')
95 | print(f' -d DEVICE use DEVICE, default /dev/video0')
96 | print()
97 | print(f'example:')
98 | print(f' {sys.argv[0]} -d /dev/video4')
99 |
100 | def main():
101 | try:
102 | arguments, values = getopt.getopt(sys.argv[1:], 'hlc:d:', ['help', 'list', 'controller', 'device'])
103 | except getopt.error as err:
104 | print(err)
105 | usage()
106 | sys.exit(2)
107 |
108 | list_ctrls = False
109 | device = '/dev/video0'
110 |
111 | for current_argument, current_value in arguments:
112 | if current_argument in ('-h', '--help'):
113 | usage()
114 | sys.exit(0)
115 | elif current_argument in ('-l', '--list'):
116 | list_ctrls = True
117 | elif current_argument in ('-d', '--device'):
118 | device = current_value
119 |
120 | if spnav_open() == -1:
121 | logging.error(f'spnav_open failed')
122 | sys.exit(1)
123 |
124 | if list_ctrls:
125 | # only one device is supported by the libspnav
126 | name = ctypes.create_string_buffer(64)
127 | if spnav_dev_name(name, 64) < 0:
128 | logging.warning(f'spnav_dev_name failed')
129 | else:
130 | n = name.raw.decode().strip('\0')
131 | print(f'{n}:0')
132 | sys.exit(0)
133 |
134 | try:
135 | fd = os.open(device, os.O_RDWR, 0)
136 | except Exception as e:
137 | logging.error(f'os.open({device}, os.O_RDWR, 0) failed: {e}')
138 | sys.exit(2)
139 |
140 | camera_ctrls = CameraCtrls(device, fd)
141 | if not camera_ctrls.has_ptz():
142 | logging.error(f'camera {device} cannot do PTZ')
143 | sys.exit(1)
144 |
145 | ptz = PTZController(camera_ctrls)
146 |
147 | epoll = select.epoll()
148 | epoll.register(spnav_fd(), select.POLLIN | select.POLLERR | select.POLLNVAL)
149 |
150 | signal.signal(signal.SIGINT, lambda signum, frame: epoll.close())
151 | event = spnav_event()
152 | while not epoll.closed:
153 | try:
154 | p = epoll.poll()
155 | except OSError:
156 | break
157 |
158 | if len(p) == 0:
159 | continue
160 | (fd , v) = p[0]
161 | if v == select.POLLERR:
162 | logging.warning(f'POLLERR')
163 | break
164 |
165 | if v == select.POLLNVAL:
166 | logging.warning(f'POLLNVAL')
167 | break
168 |
169 | if spnav_poll_event(event) == 0:
170 | logging.warning(f'spnav_poll_event failed')
171 | continue
172 |
173 | if event.type == SPNAV_EVENT_MOTION:
174 | check_step(ptz.do_zoom_step, event.motion.z)
175 | check_step(ptz.do_pan_step, event.motion.x)
176 | check_step(ptz.do_tilt_step, event.motion.y)
177 | check_speed(ptz.do_pan_speed, -event.motion.ry)
178 | check_speed(ptz.do_tilt_speed, event.motion.rx)
179 | elif event.type == SPNAV_EVENT_BUTTON:
180 | if event.button.bnum == 1 and event.button.press == 0:
181 | ptz.do_reset([])
182 |
183 | spnav_close()
184 |
185 | if __name__ == '__main__':
186 | sys.exit(main())
187 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cameractrls
2 |
3 |
4 |
5 | Camera controls for Linux
6 |
7 | It's a standalone Python CLI and GUI (GTK3, GTK4) and camera Viewer (SDL) to set the camera controls in Linux. It can set the V4L2 controls and it is extendable with the non standard controls. Currently it has a Logitech extension (LED mode, LED frequency, BRIO FoV, Relative Pan/Tilt, PTZ presets), Kiyo Pro extension (HDR, HDR mode, FoV, AF mode, Save), Dell UltraSharp WB7022 extension, AnkerWork C310 extension, Preset extension (Save and restore controls), Control Restore Daemon (to restore presets at device connection).
8 |
9 | # Installation
10 |
11 | ## From Flathub
12 |
13 |
14 |
15 | ```
16 | flatpak install flathub hu.irl.cameractrls
17 | ```
18 |
19 | ## From Arch package repository
20 |
21 | ```
22 | pacman -S cameractrls
23 | ```
24 |
25 | ## From Manjaro package repository
26 |
27 | ```
28 | pamac install cameractrls
29 | ```
30 |
31 | ## Git Install method
32 |
33 | Install the dependencies via apt:
34 | ```shell
35 | sudo apt install git libsdl2-2.0-0 libturbojpeg
36 | ```
37 |
38 | or via dnf:
39 | ```shell
40 | sudo dnf install git SDL2 turbojpeg
41 | ```
42 |
43 | Clone the repo
44 | ```shell
45 | git clone https://github.com/soyersoyer/cameractrls.git
46 | cd cameractrls
47 | ```
48 |
49 | # cameractrlsgtk
50 | GTK3 GUI for the Camera controls
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | ### GTK3 GUI install
64 |
65 | Add desktop file to the launcher
66 | ```shell
67 | desktop-file-install --dir="$HOME/.local/share/applications" \
68 | --set-key=Exec --set-value="$PWD/cameractrlsgtk.py" \
69 | --set-key=Path --set-value="$PWD" \
70 | --set-key=Icon --set-value="$PWD/pkg/hu.irl.cameractrls.svg" \
71 | pkg/hu.irl.cameractrls.desktop
72 | ```
73 |
74 | Run from the launcher or from the shell
75 | ```shell
76 | ./cameractrlsgtk.py
77 | ```
78 |
79 | ### GTK4 GUI install
80 |
81 | Add desktop file to the launcher
82 | ```shell
83 | desktop-file-install --dir="$HOME/.local/share/applications" \
84 | --set-key=Exec --set-value="$PWD/cameractrlsgtk4.py" \
85 | --set-key=Path --set-value="$PWD" \
86 | --set-key=Icon --set-value="$PWD/pkg/hu.irl.cameractrls.svg" \
87 | pkg/hu.irl.cameractrls.desktop
88 | ```
89 |
90 | Run from the launcher or from the shell
91 | ```shell
92 | ./cameractrlsgtk4.py
93 | ```
94 |
95 | # cameractrls.py
96 |
97 | The CLI.
98 |
99 | Run the cameractrls
100 | ```shell
101 | ./cameractrls.py
102 | ```
103 | ```
104 | usage: ./cameractrls.py [--help] [-d DEVICE] [--list] [-c CONTROLS]
105 |
106 | optional arguments:
107 | -h, --help show this help message and exit
108 | -d DEVICE use DEVICE, default /dev/video0
109 | -l, --list list the controls and values
110 | -L, --list-devices list capture devices
111 | -c CONTROLS set CONTROLS (eg.: hdr=on,fov=wide)
112 |
113 | example:
114 | ./cameractrls.py -c brightness=128,kiyo_pro_hdr=on,kiyo_pro_fov=wide
115 | ```
116 |
117 | # cameractrlsd.py
118 |
119 | The control restore daemon.
120 |
121 | Add it to SystemD/Desktop portal with the GUI/CLI.
122 |
123 | # cameraview.py
124 |
125 | The camera viewer.
126 |
127 | ```shell
128 | ./cameraview.py -h
129 | ```
130 | ```
131 | usage: ./cameraview.py [--help] [-d DEVICE] [-s SIZE] [-r ANGLE] [-m FLIP] [-c COLORMAP]
132 |
133 | optional arguments:
134 | -h, --help show this help message and exit
135 | -d DEVICE use DEVICE, default /dev/video0
136 | -s SIZE put window inside SIZE rectangle (wxh), default unset
137 | -r ANGLE rotate the image by ANGLE, default 0
138 | -m FLIP mirror the image by FLIP, default no, (no, h, v, hv)
139 | -c COLORMAP set colormap, default none
140 | (none, grayscale, inferno, viridis, ironblack, rainbow)
141 |
142 | example:
143 | ./cameraview.py -d /dev/video2
144 |
145 | shortcuts:
146 | f: toggle fullscreen
147 | r: ANGLE +90 (shift+r -90)
148 | m: FLIP next (shift+m prev)
149 | c: COLORMAP next (shift+c prev)
150 | ```
151 |
152 | # PTZ controls
153 |
154 | ## Keyboard
155 |
156 | Control your PTZ camera with the arrow keys/keypad keys/wasd/Home/End/PageUp/PageDown/+/-/Ctrl++/Ctrl+- of your keyboard while one of the PTZ control is in focus or in the cameraview window.
157 |
158 | Use Alt+PresetNum to select a preset for logitech_pantilt_preset.
159 |
160 | ## 3Dconnexion SpaceMouse
161 |
162 | Control your camera with your 6DoF [SpaceMouse](https://3dconnexion.com/product/spacemouse-compact/).
163 |
164 | ```
165 | Z => zoom_absolute
166 | X => pan_absolute
167 | Y => tilt_absolute
168 | RY => pan_speed
169 | RX => tilt_speed
170 | BTN1 => PTZ reset
171 | ```
172 |
173 | It requires spacenavd and libspnav. (optional, only if you have a SpaceMouse)
174 |
175 | ```shell
176 | sudo apt install spacenavd libspnav0
177 | sudo cp /usr/share/doc/spacenavd/examples/example-spnavrc /etc/spnavrc
178 | ```
179 |
180 | or via dnf:
181 | ```shell
182 | sudo dnf install spacenavd libspnav
183 | sudo cp /usr/share/doc/spacenavd/example-spnavrc /etc/spnavrc
184 | ```
185 |
186 | tip: set `led = auto` in /etc/spnavrc
187 |
188 | ## Game Controllers
189 |
190 | Control you camera with your Game Controller ([PS5 DualSense](https://www.playstation.com/accessories/dualsense-wireless-controller/)/[Xbox controller](https://www.xbox.com/accessories/controllers/xbox-wireless-controller)/etc)
191 |
192 | ```
193 | Left Stick => pan_speed/tilt_speed or pan_absolute/tilt_absolute
194 | Right Stick => pan_absolute/tilt_absolute
195 | DPAD => pan_absolute/tilt_absolute
196 | Left/Right Trigger => zoom_absolute
197 | South/East/West/North/Left Shoulder/Right Shoulder/Back/Start => PTZ Presets 1-8
198 | Guide => PTZ Reset
199 | ```
200 |
201 | ## MIDI Controllers
202 |
203 | Control you camera with your MIDI Controller (e.g. [MPK Mini](https://www.akaipro.com/mpk-mini-mk3.html) or [Arturias](https://www.arturia.com/products?categories=hybrid-synths) or any with configurable knobs/joys)
204 |
205 | Configure with [SysEx Controls](https://github.com/soyersoyer/sysex-controls) or [MPK3 Settings](https://github.com/tsmetana/mpk3-settings) your MIDI Controller as follows:
206 |
207 | ```
208 | With joystick:
209 | CC78 => pan_speed/pan_absolute
210 | CC79 => tilt_speed/tilt_absolute
211 |
212 | With absolute knobs (knob values: 0-127):
213 | CC71 => pan_absolute
214 | CC72 => tilt_absolute
215 | CC73 => zoom_absolute
216 |
217 | With relative knobs (knob values: INC:1 DEC:127):
218 | CC70 => pan_speed
219 | CC74 => tilt_speed
220 | CC75 => pan_absolute
221 | CC76 => tilt_absolute
222 | CC77 => zoom_absolute
223 |
224 | CC121 => PTZ reset
225 | PGM0-7 => PTZ presets 1-8
226 | ```
227 |
228 | # Update cameractrls
229 |
230 | ```shell
231 | git pull
232 | ```
233 |
234 | # Update from 0.5.x -> 0.6.x
235 |
236 | Disable, stop and delete the old systemd paths, services:
237 | ```shell
238 | cd ~/.config/systemd/user
239 | systemctl --user disable --now cameractrls-*
240 | rm cameractrls-*
241 | ```
242 |
243 | # Delete cameractrls
244 |
245 | Disable, stop and delete the systemd service:
246 | ```shell
247 | cd ~/.config/systemd/user
248 | systemctl --user disable --now cameractrlsd.service
249 | rm cameractrlsd.service
250 | ```
251 |
252 | Remove launcher shortcut
253 | ```shell
254 | rm ~/.local/share/applications/hu.irl.cameractrls.desktop
255 | ```
256 |
257 | Delete the cameractrls:
258 | ```shell
259 | rm -rf cameractrls
260 | ```
261 |
--------------------------------------------------------------------------------
/pkg/hu.irl.cameractrls.svg:
--------------------------------------------------------------------------------
1 |
2 |
204 |
--------------------------------------------------------------------------------
/cameraptzgame.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys, os, ctypes, ctypes.util, logging, getopt, signal, atexit, time
4 | from cameractrls import CameraCtrls, PTZController
5 |
6 | sdl2lib = ctypes.util.find_library('SDL2-2.0')
7 | if sdl2lib is None:
8 | logging.error('libSDL2 not found, please install the libsdl2-2.0 package!')
9 | sys.exit(2)
10 | sdl2 = ctypes.CDLL(sdl2lib)
11 |
12 | #logging.getLogger().setLevel(logging.INFO)
13 |
14 | enum = ctypes.c_uint
15 |
16 | SDL_Init = sdl2.SDL_Init
17 | SDL_Init.restype = ctypes.c_int
18 | SDL_Init.argtypes = [ctypes.c_uint32]
19 | # int SDL_Init(Uint32 flags);
20 |
21 | SDL_GetError = sdl2.SDL_GetError
22 | SDL_GetError.restype = ctypes.c_char_p
23 | SDL_GetError.argtypes = []
24 | # const char* SDL_GetError(void);
25 |
26 | SDL_NumJoysticks = sdl2.SDL_NumJoysticks
27 | SDL_NumJoysticks.restype = ctypes.c_int
28 | # int SDL_NumJoysticks(void)
29 |
30 | SDL_IsGameController = sdl2.SDL_IsGameController
31 | SDL_IsGameController.restype = ctypes.c_bool
32 | SDL_IsGameController.argtypes = [ctypes.c_int]
33 | # SDL_bool SDL_IsGameController(int joystick_index);
34 |
35 | SDL_GameControllerNameForIndex = sdl2.SDL_GameControllerNameForIndex
36 | SDL_GameControllerNameForIndex.restype = ctypes.c_char_p
37 | SDL_GameControllerNameForIndex.argtypes = [ctypes.c_int]
38 | # const char* SDL_GameControllerNameForIndex(int joystick_index);
39 |
40 | SDL_JoystickID = ctypes.c_int32
41 |
42 | SDL_GameControllerOpen = sdl2.SDL_GameControllerOpen
43 | SDL_GameControllerOpen.restype = ctypes.c_void_p
44 | SDL_GameControllerOpen.argtypes = [ctypes.c_int]
45 | # SDL_GameController* SDL_GameControllerOpen(int joystick_index);
46 |
47 | SDL_GameControllerRumble = sdl2.SDL_GameControllerRumble
48 | SDL_GameControllerRumble.restype = ctypes.c_int
49 | SDL_GameControllerRumble.argtypes = [ctypes.c_void_p, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint32]
50 | # int SDL_GameControllerRumble(SDL_GameController *gamecontroller, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble, Uint32 duration_ms);
51 |
52 | SDL_GameControllerAxis = enum
53 | SDL_CONTROLLER_AXIS_LEFTX = 0
54 | SDL_CONTROLLER_AXIS_LEFTY = 1
55 | SDL_CONTROLLER_AXIS_RIGHTX = 2
56 | SDL_CONTROLLER_AXIS_RIGHTY = 3
57 | SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4
58 | SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5
59 |
60 | SDL_GameControllerButton = enum
61 | SDL_CONTROLLER_BUTTON_A = 0
62 | SDL_CONTROLLER_BUTTON_B = 1
63 | SDL_CONTROLLER_BUTTON_X = 2
64 | SDL_CONTROLLER_BUTTON_Y = 3
65 | SDL_CONTROLLER_BUTTON_BACK = 4
66 | SDL_CONTROLLER_BUTTON_GUIDE = 5
67 | SDL_CONTROLLER_BUTTON_START = 6
68 | SDL_CONTROLLER_BUTTON_LEFTSTICK = 7
69 | SDL_CONTROLLER_BUTTON_RIGHTSTICK = 8
70 | SDL_CONTROLLER_BUTTON_LEFTSHOULDER = 9
71 | SDL_CONTROLLER_BUTTON_RIGHTSHOULDER = 10
72 | SDL_CONTROLLER_BUTTON_DPAD_UP = 11
73 | SDL_CONTROLLER_BUTTON_DPAD_DOWN = 12
74 | SDL_CONTROLLER_BUTTON_DPAD_LEFT = 13
75 | SDL_CONTROLLER_BUTTON_DPAD_RIGHT = 14
76 |
77 | SDL_GameControllerGetAxis = sdl2.SDL_GameControllerGetAxis
78 | SDL_GameControllerGetAxis.restype = ctypes.c_int16
79 | SDL_GameControllerGetAxis.argtypes = [ctypes.c_void_p, SDL_GameControllerAxis]
80 | # Sint16 SDL_GameControllerGetAxis(SDL_GameController *gamecontroller, SDL_GameControllerAxis axis);
81 |
82 | SDL_GameControllerGetButton = sdl2.SDL_GameControllerGetButton
83 | SDL_GameControllerGetButton.restype = ctypes.c_int8
84 | SDL_GameControllerGetButton.argtypes = [ctypes.c_void_p, SDL_GameControllerButton]
85 | # Uint8 SDL_GameControllerGetButton(SDL_GameController *gamecontroller, SDL_GameControllerButton button);
86 |
87 | SDL_GameControllerClose = sdl2.SDL_GameControllerClose
88 | SDL_GameControllerClose.argtypes = [ctypes.c_void_p]
89 | # void SDL_GameControllerClose(SDL_GameController *gamecontroller);
90 |
91 | SDL_PollEvent = sdl2.SDL_PollEvent
92 | SDL_PollEvent.restype = ctypes.c_int
93 | SDL_PollEvent.argtypes = [ctypes.c_void_p]
94 | # int SDL_PollEvent(SDL_Event * event);
95 |
96 | SDL_Quit = sdl2.SDL_Quit
97 | # void SDL_Quit(void);
98 |
99 | SDL_INIT_GAMECONTROLLER = 0x00002000
100 | SDL_INIT_EVENTS = 0x00004000
101 |
102 | SDL_QUIT = 0x100
103 | SDL_CONTROLLERDEVICEADDED = 0x653
104 |
105 | _event_pad_size = 56 if ctypes.sizeof(ctypes.c_void_p) <= 8 else 64
106 |
107 | class SDL_ControllerDeviceEvent(ctypes.Structure):
108 | _fields_ = [
109 | ('type', ctypes.c_uint32),
110 | ('timestamp', ctypes.c_uint32),
111 | ('which', SDL_JoystickID),
112 | ]
113 |
114 | class SDL_Event(ctypes.Union):
115 | _fields_ = [
116 | ('type', ctypes.c_uint32),
117 | ('cdevice', SDL_ControllerDeviceEvent),
118 | ('padding', (ctypes.c_uint8 * _event_pad_size)),
119 | ]
120 |
121 | SDL_QuitEvent = SDL_Event()
122 | SDL_QuitEvent.type = SDL_QUIT
123 |
124 | SDL_PushEvent = sdl2.SDL_PushEvent
125 | SDL_PushEvent.restype = ctypes.c_int
126 | SDL_PushEvent.argtypes = [ctypes.POINTER(SDL_Event)]
127 | #int SDL_PushEvent(SDL_Event * event);
128 |
129 | def usage():
130 | print(f'usage: {sys.argv[0]} [-h] [-l] [-c] [-d DEVICE]\n')
131 | print(f'optional arguments:')
132 | print(f' -h, --help show this help message and exit')
133 | print(f' -l, --list list gamecontrollers')
134 | print(f' -c, --controller use controller id (0..n), default first')
135 | print(f' -d DEVICE use DEVICE, default /dev/video0')
136 | print()
137 | print(f'example:')
138 | print(f' {sys.argv[0]} -d /dev/video4')
139 |
140 | th = 4096
141 |
142 | def rumble_simple(controller):
143 | SDL_GameControllerRumble(controller, 0x0a00, 0x0aff, 50)
144 |
145 | def check_zoom(controller, axis, cb, scale=1, rumble=rumble_simple):
146 | value = SDL_GameControllerGetAxis(controller, axis)
147 | if value == 0:
148 | return 0
149 | if cb(round(value / 2048) * scale, []):
150 | rumble(controller)
151 |
152 | def check_axis(controller, axis, cb, scale=1, rumble=rumble_simple):
153 | value = SDL_GameControllerGetAxis(controller, axis)
154 | if -th < value < th:
155 | return cb(0, [])
156 | if cb(round(value / 8192) * scale, []):
157 | rumble(controller)
158 |
159 | def check_axis_abs(controller, axis, cb, scale=1, rumble=rumble_simple):
160 | value = SDL_GameControllerGetAxis(controller, axis)
161 | if -th < value < th:
162 | return 0
163 | if cb(round(value / 8192) * scale, []):
164 | rumble(controller)
165 |
166 | def check_button_v(controller, button, cb, scale=1, rumble=rumble_simple):
167 | value = SDL_GameControllerGetButton(controller, button)
168 | if value == 0:
169 | return 0
170 | if cb(value * scale, []):
171 | rumble(controller)
172 |
173 | def check_button(controller, button, cb, rumble=rumble_simple):
174 | value = SDL_GameControllerGetButton(controller, button)
175 | if value == 0:
176 | return 0
177 | if cb([]):
178 | rumble(controller)
179 |
180 | def main():
181 | try:
182 | arguments, values = getopt.getopt(sys.argv[1:], 'hlc:d:', ['help', 'list', 'controller', 'device'])
183 | except getopt.error as err:
184 | print(err)
185 | usage()
186 | sys.exit(2)
187 |
188 | list_ctrls = False
189 | controller_id = None
190 | device = '/dev/video0'
191 |
192 | controller = None
193 | ptz = None
194 |
195 | controllers = []
196 |
197 | for current_argument, current_value in arguments:
198 | if current_argument in ('-h', '--help'):
199 | usage()
200 | sys.exit(0)
201 | elif current_argument in ('-l', '--list'):
202 | list_ctrls = True
203 | elif current_argument in ('-c', '--controller'):
204 | controller_id = int(current_value.split(':')[-1])
205 | elif current_argument in ('-d', '--device'):
206 | device = current_value
207 |
208 | if SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_EVENTS) != 0:
209 | logging.error(f'SDL_Init failed: {SDL_GetError()}')
210 | sys.exit(1)
211 |
212 | signal.signal(signal.SIGINT, lambda signum, frame: SDL_PushEvent(SDL_QuitEvent))
213 | atexit.register(SDL_Quit)
214 |
215 | for i in range(SDL_NumJoysticks()):
216 | if SDL_IsGameController(i):
217 | controllers.append((SDL_GameControllerNameForIndex(i).decode(), i))
218 |
219 | if list_ctrls:
220 | for c in controllers:
221 | print(f'{c[0]}:{c[1]}')
222 | sys.exit(0)
223 |
224 | if controller_id is None and controllers:
225 | controller_id = controllers[0][1]
226 |
227 | if controller_id is None:
228 | logging.warning(f'controller not found, waiting to appear one..')
229 |
230 | if controller_id is not None and not SDL_IsGameController(controller_id):
231 | logging.error(f'controller with id "{controller_id}" is not a game controller')
232 | sys.exit(1)
233 |
234 | try:
235 | fd = os.open(device, os.O_RDWR, 0)
236 | except Exception as e:
237 | logging.error(f'os.open({device}, os.O_RDWR, 0) failed: {e}')
238 | sys.exit(2)
239 |
240 | camera_ctrls = CameraCtrls(device, fd)
241 | if not camera_ctrls.has_ptz():
242 | logging.error(f'camera {device} cannot do PTZ')
243 | sys.exit(1)
244 |
245 | ptz = PTZController(camera_ctrls)
246 |
247 | running = True
248 | event = SDL_Event()
249 | while running:
250 | while SDL_PollEvent(ctypes.byref(event)):
251 | if event.type == SDL_QUIT:
252 | running = False
253 | if event.type == SDL_CONTROLLERDEVICEADDED:
254 | if controller_id is None:
255 | controller_id = event.cdevice.which
256 | if event.cdevice.which == controller_id:
257 | if controller is not None:
258 | SDL_GameControllerClose(controller)
259 | controller = None
260 | controller = SDL_GameControllerOpen(controller_id)
261 | if controller is None:
262 | logging.error(f'SDL_GameControllerOpen failed: {SDL_GetError()}')
263 | sys.exit(1)
264 |
265 | SDL_GameControllerRumble(controller, 0x00ff, 0xffff, 250)
266 |
267 | if ptz.has_pantilt_speed:
268 | check_axis(controller, SDL_CONTROLLER_AXIS_LEFTX, ptz.do_pan_speed)
269 | check_axis(controller, SDL_CONTROLLER_AXIS_LEFTY, ptz.do_tilt_speed, -1)
270 | elif ptz.has_pantilt_absolute:
271 | check_axis(controller, SDL_CONTROLLER_AXIS_LEFTX, ptz.do_pan_step)
272 | check_axis(controller, SDL_CONTROLLER_AXIS_LEFTY, ptz.do_tilt_step, -1)
273 |
274 | check_axis_abs(controller, SDL_CONTROLLER_AXIS_RIGHTX, ptz.do_pan_step)
275 | check_axis_abs(controller, SDL_CONTROLLER_AXIS_RIGHTY, ptz.do_tilt_step, -1)
276 |
277 | check_zoom(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT, ptz.do_zoom_step, -1)
278 | check_zoom(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, ptz.do_zoom_step)
279 |
280 | check_button_v(controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT, ptz.do_pan_step, -1)
281 | check_button_v(controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT, ptz.do_pan_step)
282 | check_button_v(controller, SDL_CONTROLLER_BUTTON_DPAD_UP, ptz.do_tilt_step)
283 | check_button_v(controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN, ptz.do_tilt_step, -1)
284 |
285 | check_button(controller, SDL_CONTROLLER_BUTTON_GUIDE, ptz.do_reset)
286 |
287 | check_button_v(controller, SDL_CONTROLLER_BUTTON_A, ptz.do_preset, 1)
288 | check_button_v(controller, SDL_CONTROLLER_BUTTON_B, ptz.do_preset, 2)
289 | check_button_v(controller, SDL_CONTROLLER_BUTTON_X, ptz.do_preset, 3)
290 | check_button_v(controller, SDL_CONTROLLER_BUTTON_Y, ptz.do_preset, 4)
291 | check_button_v(controller, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, ptz.do_preset, 5)
292 | check_button_v(controller, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, ptz.do_preset, 6)
293 | check_button_v(controller, SDL_CONTROLLER_BUTTON_BACK, ptz.do_preset, 7)
294 | check_button_v(controller, SDL_CONTROLLER_BUTTON_START, ptz.do_preset, 8)
295 |
296 | time.sleep(0.050)
297 |
298 | SDL_GameControllerClose(controller)
299 |
300 | if __name__ == '__main__':
301 | sys.exit(main())
302 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [Unreleased]
4 |
5 | ## [0.6.10] - 2025-12-11
6 |
7 | ### Fixed
8 | - Fixed GTK related issues regarding the AnkerWorks C310 webcam.
9 | - Fixed specific buttons not working for AnkerWorks C310 webcam.
10 | - As of today, this camera is nearly feature complete with the Windows version. The only sections that still need some work are HDR support (that is implemented in code following the Windows app, but it doesn't work on Linux for some reason) and exposure compensation on a specific area (the Windows app allows you to set a square to always keep exposed). The packet stream is documented in the source, in case someone wants to complete the work.
11 |
12 | ## [0.6.9] - 2025-11-14
13 |
14 | ### Fixed
15 | - Fixed a GTK3 GSignal warning
16 | - Fixed a bug where the actual buffer size was not used
17 | - Fixed a bug where auto white balance was reporting errors in the GUI
18 |
19 | ## [0.6.8] - 2025-10-22
20 |
21 | ### Added
22 | - AnkerWork C310 extension (thx RodoMa92)
23 | - Workaround for VIDIOC_QUERYCTRL -EIO return value (thx lbrooney)
24 |
25 | ### Fixed
26 | - GTK4 scale layout
27 | - GTK4 GtkGrid instead of GtkFlowBox
28 | - GTK4 GUI is no longer experimental
29 |
30 | ## [0.6.7] - 2024-09-02
31 |
32 | ### Added
33 | - Dell UltraSharp WB7022 Extension (thx Sensape)
34 | - Add SpinButtons next to Scales
35 | - Allow PageUp/PageDown buttons to work with scales and spin buttons
36 | - Use Alt+n shortcuts to switch between pages
37 |
38 | ### Removed
39 | - Remove Alt+n shortcuts for Logitech PTZ presets
40 |
41 | ## [0.6.6] - 2024-06-11
42 |
43 | ### Fixed
44 |
45 | - Fixes typo, which prevents from saving any config (thx morbno)
46 |
47 | ## [0.6.5] - 2024-06-04
48 |
49 | ### Added
50 | - Gray out the readonly controls (thx Daniel Schaefer @ Framework)
51 |
52 | ### Fixed
53 | - Fix GKT4 scale layout for version 4.14.2+
54 |
55 | ## [0.6.4] - 2024-05-28
56 |
57 | ### Fixed
58 | - Fix config location when XDG_CONFIG_DIR is set (thx Daniel Fox)
59 |
60 | ## [0.6.3] - 2024-04-11
61 |
62 | ### Added
63 | - Added scrollbar to the control panel
64 |
65 | ### Fixed
66 | - Cameractrlsd can start even if there is no /dev/v4l directory
67 | - Preset save writes the config file based on v4l ID
68 |
69 | ## [0.6.2] - 2024-04-07
70 |
71 | ### Added
72 | - Add tooltips to preset controls
73 |
74 | ### Changed
75 | - Sort resolutions in descendng order
76 | - Use only 1 screenshot in metainfo to make the Flatpak image less bloated
77 |
78 | ## [0.6.1] - 2024-04-03
79 |
80 | ### Added
81 | - Set dark mode based on color-scheme
82 | - Enable DesktopPortal launcer for snap as well
83 |
84 | ### Changed
85 | - Check if systemctl enable is successfull
86 | - Set cameractrlsd loglevel to INFO
87 |
88 | ## [0.6.0] - 2024-04-01
89 |
90 | ### Added
91 | - Control Presets
92 | - Cameractrlsd, a deamon which restores the controls at device connection
93 | - Starter for Cameractrlsd with SystemD and DesktopPortal
94 | - MX Brio FoV support (thx wanderboessenkool)
95 | - PTZ control keys in cameraview
96 |
97 | ### Changed
98 | - use GPLv3 License
99 |
100 | ### Fixed
101 | - Various camera opening fixes
102 | - Better relative MIDI control support
103 | - Added Zoom Continuous to zeroers to work
104 |
105 | ### Removed
106 | - removed cameractrlstk
107 |
108 | ## [0.5.15] - 2024-02-17
109 |
110 | ### Fixed
111 | - Do not use PEP-701 as older pythons do not support it
112 |
113 | ## [0.5.14] - 2024-02-17
114 |
115 | ### Added
116 | - 3DConnexion SpaceMouse support to control PTZ cameras
117 | - Game Controllers (PS DualSense/Xbox/etc) support to control PTZ cameras
118 | - MIDI Controllers (MPK Mini or any configurable) support to control PTZ cameras
119 | - Use Page Up/Page Down for Zoom also (PTZ)
120 | - Keyboard controls for Absolute PTZ
121 | - Alt+PresetNum shortcuts for PTZ presets
122 | - Tooltips for headerbar icons
123 |
124 | ### Changed
125 | - Replaced Hamburger menu with About icon
126 |
127 | ### Fixed
128 | - Fix Ctrl detection in PTZ controls
129 | - Eliminating GLib warnings on app closing
130 |
131 | ## [0.5.13] - 2023-12-22
132 |
133 | ### Added
134 | - Logitech PTZ presets
135 | - Better keyboard control for PTZ
136 | - Bigger steps for Zoom with Ctrl+Dir, Page Up/Page Down
137 |
138 | ## [0.5.12] - 2023-12-08
139 |
140 | ### Added
141 | - Brio 505 FoV support (thx squiddity)
142 |
143 | ### Fixed
144 | - fixed 'list more' button size in the GTK4 app
145 | - fixed release events of scale widget in the GTK4 app
146 |
147 | ## [0.5.11] - 2023-10-13
148 |
149 | ### Fixed
150 | - Handle invalid menu control value
151 |
152 | ## [0.5.10] - 2023-08-03
153 |
154 | ### Added
155 | - Listen for pixelformat, resolution, FPS changes from other processes
156 | - Show warnings about invalid FPS values
157 |
158 | ### Changed
159 | - Preview calls S_PARM to make uninitialized cameras work
160 |
161 | ## [0.5.9] - 2023-07-07
162 |
163 | ### Added
164 | - V4L2_CID_HDR_SENSOR_MODE, V4L2_CID_IMAGE_[SOURCE|PROC]_CLASS descriptions
165 |
166 | ### Fixed
167 | - Shortcuts in cameraview
168 | - Float FPS handling
169 |
170 | ### Changed
171 | - Adjust the window size based on rotation
172 | - cameraview calls VIDIOC_S_FMT only for Kiyo Pro (it doesn't work without)
173 |
174 | ## [0.5.8] - 2023-06-26
175 |
176 | ### Added
177 | - Colormaps for all pixel formats (some thermal cameras use YUYV for thermal imaging)
178 |
179 | ## [0.5.7] - 2023-06-24
180 |
181 | ### Fixed
182 | - Fixed rotation in preview
183 | - Clamp percent control values for fewer warnings when using presets
184 |
185 | ### Changed
186 | - Use the GTK bundled view-more icon instead of camera-switch
187 |
188 | ## [0.5.6] - 2023-06-15
189 |
190 | ### Fixed
191 | - Fixed ctrl+q quit by closing the camera before
192 |
193 | ## [0.5.5] - 2023-06-10
194 |
195 | ### Added
196 | - Color presets
197 | - Listen for controls changes from other processes
198 | - 'default' or percent values can also be set
199 | - Improved error reporting
200 | - Exposure time now in µs in the GTK GUIs
201 | - Exposure time, Gain scales has dark-to-light background
202 |
203 | ### Changed
204 | - Removed header buttons border
205 |
206 | ## [0.5.4] - 2023-05-21
207 |
208 | ### Changed
209 | - Limit the initial size of the preview window to control window so it can be placed next to each other
210 |
211 | ## [0.5.3] - 2023-05-21
212 |
213 | ### Added
214 | - Display warnings on the GUIs as well
215 |
216 | ### Fixed
217 | - Fixed device listbox margin in GTK app
218 |
219 | ## [0.5.2] - 2023-05-20
220 |
221 | ### Added
222 | - Two more colormaps
223 | - Capture - Info category with camera informations
224 |
225 | ### Changed
226 | - Show devices by name, not by the long v4l path
227 | - Move device combobox to headerbar
228 | - Add refresh button to headerbar
229 | - Limit the size of the preview to fit next to the window
230 | - Redesigned Zero camera page with snap instructions
231 |
232 | ## [0.5.1] - 2023-05-17
233 |
234 | ### Added
235 | - New Icon (thx Jorge Toledo eldelacajita)
236 | - Rotate, mirror the preview image
237 | - Colormaps (inferno, ironblack) for Thermal/ToF camera GREY previews
238 | - RGB565 format support
239 |
240 | ### Changed
241 | - Use edit-undo-symbolic icon instead of ⟳ in default buttons
242 | - Various GTK/GTK4 fixes
243 | - Breaking: pkg/icon.png -> pkg/hu.irl.cameractrls.svg
244 |
245 | ## [0.5.0] - 2023-04-29
246 |
247 | ### Added
248 | - Brio 501 FoV support (thx Monkatraz)
249 | - Colorized White Balance scale
250 | - GTK4 GUI (experimental)
251 |
252 | ### Changed
253 | - Simpler looking scales
254 | - Icon now comes from the Window Manager
255 | - Breaking: The desktop filename have to be hu.irl.cameractrls.desktop
256 | - Breaking: The desktop file moved to pkg dir
257 | - Breaking: The icon should be installed also
258 |
259 | ## [0.4.14] - 2023-03-05
260 |
261 | ### Added
262 | - Brio 4K Stream Edition FoV support (thx chrishoage)
263 |
264 | ## [0.4.13] - 2023-03-02
265 |
266 | ### Added
267 | - Brio 500 FoV support (thx crabmanX)
268 |
269 | ## [0.4.12] - 2022-12-04
270 |
271 | ### Changed
272 | - Improved error handling and logging
273 | - The icon has been given some bloom to make it visible even on a dark background (thx nekohayo for the suggestion)
274 |
275 | ### Fixed
276 | - Fixed Dynex 1.3MP Webcam preview and fps control (thx dln949 for testing)
277 |
278 | ## [0.4.11] - 2022-10-19
279 |
280 | ### Added
281 | - Pan/Tilt relative and reset controls for some Logitech PTZ cameras (like bcc950)
282 | - LED and focus controls for some old Logitech cameras (like QuickCam Pro 9000)
283 | - V4L2 buttons
284 | - Controls also work with keyboard
285 | - Pan/Tilt speed controls stop when the key or button released
286 | - Highlight focused controls in the TK app
287 | - Gray out the inactive controls
288 | - Quit with Primary+q
289 | - New compression page with the Codec and JPEG categories
290 | - Fullscreen with double-click in the cameraview
291 | - Support YVYU, UYVY, NV21, YV12, RGB24, BGR24, RX24 formats in the cameraview
292 |
293 | ### Changed
294 | - Limit the combobox width in the GTK app
295 | - Controls fill the width in the GTK app
296 |
297 | ## [0.4.10] - 2022-10-07
298 |
299 | ### Added
300 | - Color Balance category
301 | - Tooltips for JPEG controls
302 | - Support cameras with YU12 format
303 | - Support IR cameras with GREY format
304 |
305 | ### Changed
306 | - Advanced/Color Effects moved to Color/Effects
307 | - Basic/Crop/Privacy moved to Advanced/Privacy
308 | - Merge Compression page into Advanced page
309 |
310 | ### Fixed
311 | - Retain aspect ratio in the cameraview's fullscreen mode
312 |
313 | ## [0.4.9] - 2022-08-20
314 |
315 | ### Added
316 | - Control tooltips
317 |
318 | ### Changed
319 | - Reordered pages
320 |
321 | ## [0.4.8] - 2022-08-19
322 |
323 | ### Changed
324 | - Cameractrls, GTK: Crop, Image, Exposure pages for better navigation
325 |
326 |
327 | ## [0.4.7] - 2022-08-19
328 |
329 | ### Added
330 | - Cameractrls: add Logitech BRIO FoV control
331 |
332 | ## [0.4.6] - 2022-07-01
333 |
334 | ### Changed
335 | - Cameraview: use esc to exit
336 | - GTK, TK: close all windows at exit
337 |
338 | ## [0.4.5] - 2022-06-30
339 |
340 | ### Added
341 | - AppData for better flatpak integration
342 |
343 | ## [0.4.4] - 2022-06-29
344 |
345 | ### Fixed
346 | - SystemdSaver: Don't show systemd save, if it is not available
347 | - GTK: show the open camera button properly
348 | - GTK: suppress warnings or silent exits while changing the capture settings
349 |
350 | ## [0.4.3] - 2022-06-23
351 |
352 | ### Fixed
353 | - Fixed systemd saving when systemd user directory doesn't exist
354 | - Fixed cameraview starting, when it's not in the current directory
355 |
356 | ## [0.4.2] - 2022-06-23
357 |
358 | ### Added
359 | - Added JPEG support for the cameraview
360 |
361 | ### Fixed
362 | - Handling cameras that return zero fps
363 |
364 | ## [0.4.1] - 2022-06-23
365 |
366 | ### Added
367 | - Added MJPG support for the cameraview
368 |
369 | ## [0.4.0] - 2022-06-22
370 |
371 | ### Added
372 | - Ability to view the camera (only in YUYV or NV12 format yet)
373 | - Pixelformat, resolution, fps controls
374 |
375 | ### Changed
376 | - LogitechCtrls: removed the (not) default values
377 | - SystemdSaver: don't save the inactive controls and save the controls without default values too.
378 | - Adding gamma to Basic.Image
379 |
380 | ## [0.3.1] - 2022-06-17
381 |
382 | ### Changed
383 | - TK: better ordering for the controls
384 | - GTK, TK: load the icon from an absolute path (script relative)
385 |
386 | ## [0.3.0] - 2022-06-16
387 |
388 | ### Added
389 | - Systemd setting saver, systemd path (inotify watcher) and a systemd service for restoring the controls
390 |
391 | ### Changed
392 | - TK: move the reset button next to the label
393 | - GTK: place the settings savers in the footer
394 | - CLI: show pages and categories in the list of controls too
395 |
396 | ## [0.2.3] - 2022-06-11
397 |
398 | ### Added
399 | - Treat bool like integer V4l2 controls as bool
400 |
401 | ### Fixed
402 | - String to bool converting in the cameractrls CLI
403 |
404 | ### Changed
405 | - Added Hue to Basic.Image
406 | - Reorder Gain and Backligh Compensation in Basic.Exposure
407 |
408 | ## [0.2.2] - 2022-06-10
409 |
410 | ### Added
411 | - Button control type
412 | - Kiyo Pro save control
413 |
414 | ### Fixed
415 | - Kiyo Pro controls shouldn't always save on every change
416 |
417 | ## [0.2.1] - 2022-06-10
418 |
419 | ### Changed
420 | - New icon
421 |
422 | ## [0.2.0] - 2022-06-09
423 |
424 | ### Added
425 | - GTK GUI
426 | - Split controls to pages
427 |
428 | ## [0.1.2] - 2022-06-08
429 |
430 | ### Added
431 | - Hide the default buttons when the values are the defaults
432 |
433 | ## [0.1.1] - 2022-06-07
434 |
435 | ### Added
436 | - When the menu control is too long using Combobox instead of radiobuttons
437 | - Improved device discovery, added /dev/by-path/\*, /dev/video\*
438 |
439 | ## [0.1.0] - 2022-06-03
440 |
441 | ### Added
442 | - CLI script
443 | - V4L2 controls
444 | - Logitech LED controls
445 | - Kiyo Pro controls
446 | - GUI
447 |
--------------------------------------------------------------------------------
/cameraptzmidi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys, os, ctypes, ctypes.util, logging, getopt, select, asyncio, signal
4 | from cameractrls import CameraCtrls, PTZController
5 |
6 | asoundlib = ctypes.util.find_library('asound')
7 | if asoundlib is None:
8 | logging.error('libasound not found, please install the alsa-lib package!')
9 | sys.exit(2)
10 | asound = ctypes.CDLL(asoundlib)
11 |
12 | #logging.getLogger().setLevel(logging.INFO)
13 |
14 | SND_SEQ_OPEN_INPUT = 2
15 |
16 | SND_SEQ_PORT_CAP_READ = (1<<0)
17 | SND_SEQ_PORT_CAP_WRITE = (1<<1)
18 | SND_SEQ_PORT_CAP_SUBS_READ = (1<<5)
19 | SND_SEQ_PORT_CAP_SUBS_WRITE = (1<<6)
20 |
21 | SND_SEQ_PORT_TYPE_HARDWARE = (1<<16)
22 | SND_SEQ_PORT_TYPE_APPLICATION = (1<<20)
23 |
24 | SND_SEQ_EVENT_CONTROLLER = 10
25 | SND_SEQ_EVENT_PGMCHANGE = 11
26 | SND_SEQ_EVENT_PORT_UNSUBSCRIBED = 67
27 |
28 | snd_seq_p = ctypes.c_void_p
29 |
30 | snd_seq_client_type_t = ctypes.c_uint
31 | snd_seq_event_type_t = ctypes.c_ubyte
32 | snd_seq_tick_time_t = ctypes.c_uint
33 |
34 | class snd_seq_client_info(ctypes.Structure):
35 | _fields_ = [
36 | ('client', ctypes.c_int),
37 | ('type', snd_seq_client_type_t),
38 | ('name', ctypes.c_char * 64),
39 | ('filter', ctypes.c_uint),
40 | ('multicast_filter', ctypes.c_ubyte * 8),
41 | ('num_ports', ctypes.c_int),
42 | ('event_lost', ctypes.c_int),
43 | ('card', ctypes.c_int),
44 | ('pid', ctypes.c_int),
45 | ('midi_version', ctypes.c_uint),
46 | ('group_filter', ctypes.c_uint),
47 | ('reserved', ctypes.c_char * 48),
48 | ]
49 |
50 | class snd_seq_addr(ctypes.Structure):
51 | _fields_ = [
52 | ('client', ctypes.c_ubyte),
53 | ('port', ctypes.c_ubyte),
54 | ]
55 |
56 | class snd_seq_port_info(ctypes.Structure):
57 | _fields_ = [
58 | ('addr', snd_seq_addr),
59 | ('name', ctypes.c_char * 64),
60 | ('capability', ctypes.c_uint),
61 | ('type', ctypes.c_uint),
62 | ('midi_channels', ctypes.c_int),
63 | ('midi_voices', ctypes.c_int),
64 | ('synth_voices', ctypes.c_int),
65 | ('read_use', ctypes.c_int),
66 | ('write_use', ctypes.c_int),
67 | ('kernel', ctypes.c_void_p),
68 | ('flags', ctypes.c_uint),
69 | ('time_queue', ctypes.c_ubyte),
70 | ('direction', ctypes.c_ubyte),
71 | ('ump_group', ctypes.c_ubyte),
72 | ('reserved', ctypes.c_char * 57),
73 | ]
74 |
75 | class snd_seq_real_time(ctypes.Structure):
76 | _fields_ = [
77 | ('tv_sec', ctypes.c_uint),
78 | ('tv_nsec', ctypes.c_uint),
79 | ]
80 |
81 | class snd_seq_timestamp(ctypes.Union):
82 | _fields_ = [
83 | ('tick', snd_seq_tick_time_t),
84 | ('time', snd_seq_real_time),
85 | ]
86 |
87 | class snd_seq_ev_ctrl(ctypes.Structure):
88 | _fields_ = [
89 | ('channel', ctypes.c_ubyte),
90 | ('unused1', ctypes.c_ubyte),
91 | ('unused2', ctypes.c_ubyte),
92 | ('unused3', ctypes.c_ubyte),
93 | ('param', ctypes.c_uint),
94 | ('value', ctypes.c_int),
95 | ]
96 |
97 | class snd_seq_event_data(ctypes.Union):
98 | _fields_ = [
99 | ('control', snd_seq_ev_ctrl),
100 | ]
101 |
102 | class snd_seq_event(ctypes.Structure):
103 | _fields_ = [
104 | ('type', snd_seq_event_type_t),
105 | ('flags', ctypes.c_ubyte),
106 | ('tag', ctypes.c_char),
107 | ('queue', ctypes.c_ubyte),
108 | ('time', snd_seq_timestamp),
109 | ('source', snd_seq_addr),
110 | ('dest', snd_seq_addr),
111 | ('data', snd_seq_event_data),
112 | ]
113 |
114 | class pollfd(ctypes.Structure):
115 | _fields_ = [
116 | ('fd', ctypes.c_int),
117 | ('events', ctypes.c_short),
118 | ('revents', ctypes.c_short),
119 | ]
120 |
121 | snd_seq_client_info_p = ctypes.POINTER(snd_seq_client_info)
122 | snd_seq_port_info_p = ctypes.POINTER(snd_seq_port_info)
123 | snd_seq_event_p = ctypes.POINTER(snd_seq_event)
124 |
125 | snd_seq_open = asound.snd_seq_open
126 | snd_seq_open.restype = ctypes.c_int
127 | snd_seq_open.argtypes = [ctypes.POINTER(snd_seq_p), ctypes.c_char_p, ctypes.c_int, ctypes.c_int]
128 | # int snd_seq_open(snd_seq_t **seq, const char *name, int streams, int mode);
129 |
130 | snd_seq_set_client_name = asound.snd_seq_set_client_name
131 | snd_seq_set_client_name.restype = ctypes.c_int
132 | snd_seq_set_client_name.argtypes = [snd_seq_p, ctypes.c_char_p]
133 | # int snd_seq_set_client_name(snd_seq_t *seq, const char *name);
134 |
135 | snd_seq_create_simple_port = asound.snd_seq_create_simple_port
136 | snd_seq_create_simple_port.restype = ctypes.c_int
137 | snd_seq_create_simple_port.argtypes = [snd_seq_p, ctypes.c_char_p, ctypes.c_uint, ctypes.c_uint]
138 | # int snd_seq_create_simple_port(snd_seq_t *seq, const char *name, unsigned int caps, unsigned int type);
139 |
140 | snd_seq_client_info_set_client = asound.snd_seq_client_info_set_client
141 | snd_seq_client_info_set_client.restype = None
142 | snd_seq_client_info_set_client.argtypes = [snd_seq_client_info_p, ctypes.c_int]
143 | # void snd_seq_client_info_set_client(snd_seq_client_info_t *info, int client);
144 |
145 | snd_seq_query_next_client = asound.snd_seq_query_next_client
146 | snd_seq_query_next_client.restype = ctypes.c_int
147 | snd_seq_query_next_client.argtypes = [snd_seq_p, snd_seq_client_info_p]
148 | # int snd_seq_query_next_client(snd_seq_t *handle, snd_seq_client_info_t *info);
149 |
150 | snd_seq_client_info_get_client = asound.snd_seq_client_info_get_client
151 | snd_seq_client_info_get_client.restype = ctypes.c_int
152 | snd_seq_client_info_get_client.argtypes = [snd_seq_client_info_p]
153 | # int snd_seq_client_info_get_client(const snd_seq_client_info_t *info);
154 |
155 | snd_seq_port_info_set_client = asound.snd_seq_port_info_set_client
156 | snd_seq_port_info_set_client.restype = None
157 | snd_seq_port_info_set_client.argtypes = [snd_seq_port_info_p, ctypes.c_int]
158 | # void snd_seq_port_info_set_client(snd_seq_port_info_t *info, int client);
159 |
160 | snd_seq_port_info_set_port = asound.snd_seq_port_info_set_port
161 | snd_seq_port_info_set_port.restype = None
162 | snd_seq_port_info_set_port.argtypes = [snd_seq_port_info_p, ctypes.c_int]
163 | # void snd_seq_port_info_set_port(snd_seq_port_info_t *info, int port);
164 |
165 | snd_seq_query_next_port = asound.snd_seq_query_next_port
166 | snd_seq_query_next_port.restype = ctypes.c_int
167 | snd_seq_query_next_port.argtypes = [snd_seq_p, snd_seq_port_info_p]
168 | # int snd_seq_query_next_port(snd_seq_t *handle, snd_seq_port_info_t *info);
169 |
170 | snd_seq_port_info_get_capability = asound.snd_seq_port_info_get_capability
171 | snd_seq_port_info_get_capability.restype = ctypes.c_uint
172 | snd_seq_port_info_get_capability.argtypes = [snd_seq_port_info_p]
173 | # unsigned int snd_seq_port_info_get_capability(const snd_seq_port_info_t *info);
174 |
175 | snd_seq_port_info_get_type = asound.snd_seq_port_info_get_type
176 | snd_seq_port_info_get_type.restype = ctypes.c_uint
177 | snd_seq_port_info_get_type.argtypes = [snd_seq_port_info_p]
178 | # unsigned int snd_seq_port_info_get_type(const snd_seq_port_info_t *info);
179 |
180 | snd_seq_port_info_get_port = asound.snd_seq_port_info_get_port
181 | snd_seq_port_info_get_port.restype = ctypes.c_int
182 | snd_seq_port_info_get_port.argtypes = [snd_seq_port_info_p]
183 | # int snd_seq_port_info_get_port(const snd_seq_port_info_t *info);
184 |
185 | snd_seq_port_info_get_name = asound.snd_seq_port_info_get_name
186 | snd_seq_port_info_get_name.restype = ctypes.c_char_p
187 | snd_seq_port_info_get_name.argtypes = [snd_seq_port_info_p]
188 | # const char *snd_seq_port_info_get_name(const snd_seq_port_info_t *info)
189 |
190 | snd_seq_set_client_event_filter = asound.snd_seq_set_client_event_filter
191 | snd_seq_set_client_event_filter.restype = ctypes.c_int
192 | snd_seq_set_client_event_filter.argtypes = [snd_seq_p, ctypes.c_int]
193 | # void snd_seq_set_client_event_filter(snd_seq_t *seq, int event_type);
194 |
195 | snd_seq_connect_from = asound.snd_seq_connect_from
196 | snd_seq_connect_from.restype = ctypes.c_int
197 | snd_seq_connect_from.argtypes = [snd_seq_p, ctypes.c_int, ctypes.c_int, ctypes.c_int]
198 | # int snd_seq_connect_from(snd_seq_t *seq, int my_port, int src_client, int src_port);
199 |
200 | snd_seq_poll_descriptors_count = asound.snd_seq_poll_descriptors_count
201 | snd_seq_poll_descriptors_count.restype = ctypes.c_int
202 | snd_seq_poll_descriptors_count.argtypes = [snd_seq_p, ctypes.c_short]
203 | # int snd_seq_poll_descriptors_count(snd_seq_t *handle, short events);
204 |
205 | snd_seq_poll_descriptors = asound.snd_seq_poll_descriptors
206 | snd_seq_poll_descriptors.restype = ctypes.c_int
207 | snd_seq_poll_descriptors.argtypes = [snd_seq_p, ctypes.POINTER(pollfd), ctypes.c_uint, ctypes.c_short]
208 | # int snd_seq_poll_descriptors(snd_seq_t *handle, struct pollfd *pfds, unsigned int space, short events);
209 |
210 | snd_seq_event_input_pending = asound.snd_seq_event_input_pending
211 | snd_seq_event_input_pending.restype = ctypes.c_int
212 | snd_seq_event_input_pending.argtypes = [snd_seq_p, ctypes.c_int]
213 | # int snd_seq_event_input_pending(snd_seq_t *seq, int fetch_sequencer);
214 |
215 | snd_seq_event_input = asound.snd_seq_event_input
216 | snd_seq_event_input.restype = ctypes.c_int
217 | snd_seq_event_input.argtypes = [snd_seq_p, ctypes.POINTER(snd_seq_event_p)]
218 | # int snd_seq_event_input(snd_seq_t *handle, snd_seq_event_t **ev);
219 |
220 | def get_hw_client_ports(seq):
221 | cinfo = snd_seq_client_info()
222 | pinfo = snd_seq_port_info()
223 | ret = []
224 |
225 | snd_seq_client_info_set_client(cinfo, -1)
226 | while snd_seq_query_next_client(seq, cinfo) >= 0:
227 | client = snd_seq_client_info_get_client(cinfo)
228 |
229 | snd_seq_port_info_set_client(pinfo, client)
230 | snd_seq_port_info_set_port(pinfo, -1)
231 | while snd_seq_query_next_port(seq, pinfo) >= 0:
232 | if (snd_seq_port_info_get_capability(pinfo)
233 | & (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ)) \
234 | != (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ):
235 | continue
236 |
237 | if not(snd_seq_port_info_get_type(pinfo) & SND_SEQ_PORT_TYPE_HARDWARE):
238 | continue
239 |
240 | port = snd_seq_port_info_get_port(pinfo)
241 | name = snd_seq_port_info_get_name(pinfo)
242 | ret.append((name.decode(), client, port))
243 | return ret
244 |
245 | def check_cc(ev, cc, cb):
246 | if ev.data.control.param != cc or ev.data.control.value != 0:
247 | return
248 |
249 | cb([])
250 |
251 | def check_abs_knob(ev, cc, cb):
252 | if ev.data.control.param != cc:
253 | return
254 |
255 | cb(ev.data.control.value / 127, [])
256 |
257 | def check_rel_knob(ev, cc, cb, loop=None):
258 | if ev.data.control.param != cc:
259 | return
260 |
261 | if ev.data.control.value in [127, 126, 125]:
262 | step = ev.data.control.value - 128 #-1, -2, -3
263 | elif ev.data.control.value in [1, 2, 3]:
264 | step = ev.data.control.value
265 | else:
266 | return
267 |
268 | cb(step, [])
269 |
270 | if loop is None:
271 | return
272 |
273 | # rel knobs emit 1, 2, 3 or 127, 126, 125 only, set to 0 after a while
274 | if cc in loop.ctx:
275 | loop.ctx[cc].cancel()
276 | loop.ctx[cc] = loop.call_later(0.1, cb, 0, [])
277 |
278 | def check_joy(ev, cc, cb, loop=None):
279 | if ev.data.control.param != cc:
280 | return
281 |
282 | value = round((ev.data.control.value - 64) / 16)
283 | cb(value, [])
284 |
285 | if loop is None:
286 | return
287 |
288 | # joystick only emits the changes, so it stays eg. 99 if we hold, and doesn't
289 | # generate new events, repeat the events when it is not 0
290 | if cc in loop.ctx:
291 | loop.ctx[cc].cancel()
292 | if value != 0:
293 | loop.ctx[cc] = loop.call_later(0.05, check_joy, ev, cc, cb, loop)
294 |
295 | def process_midi(seq, ptz, loop):
296 | evp = snd_seq_event_p()
297 | if snd_seq_event_input(seq, evp) < 0:
298 | return
299 |
300 | # deep copy
301 | ev = type(evp.contents)()
302 | ctypes.pointer(ev)[0] = evp.contents
303 |
304 | if ev.type == SND_SEQ_EVENT_CONTROLLER:
305 | check_abs_knob(ev, 71, ptz.do_pan_percent)
306 | check_abs_knob(ev, 72, ptz.do_tilt_percent)
307 | check_abs_knob(ev, 73, ptz.do_zoom_percent)
308 |
309 | check_rel_knob(ev, 75, ptz.do_pan_step)
310 | check_rel_knob(ev, 76, ptz.do_tilt_step)
311 | check_rel_knob(ev, 77, ptz.do_zoom_step)
312 |
313 | check_rel_knob(ev, 70, ptz.do_pan_speed, loop)
314 | check_rel_knob(ev, 74, ptz.do_tilt_speed, loop)
315 |
316 | if ptz.has_pantilt_speed:
317 | check_joy(ev, 78, ptz.do_pan_speed)
318 | check_joy(ev, 79, ptz.do_tilt_speed)
319 | elif ptz.has_pantilt_absolute:
320 | check_joy(ev, 78, ptz.do_pan_step, loop)
321 | check_joy(ev, 79, ptz.do_tilt_step, loop)
322 |
323 | check_cc(ev, 121, ptz.do_reset)
324 |
325 | elif ev.type == SND_SEQ_EVENT_PGMCHANGE:
326 | ptz.do_preset(ev.data.control.value + 1, [])
327 |
328 | elif ev.type == SND_SEQ_EVENT_PORT_UNSUBSCRIBED:
329 | logging.error(f'midi port unsubscribed')
330 | sys.exit(1)
331 |
332 | def usage():
333 | print(f'usage: {sys.argv[0]} [-h] [-l] [-c] [-d DEVICE]\n')
334 | print(f'optional arguments:')
335 | print(f' -h, --help show this help message and exit')
336 | print(f' -l, --list list midi controllers')
337 | print(f' -c, --controller use controller (client:port), default first')
338 | print(f' -d DEVICE use DEVICE, default /dev/video0')
339 | print()
340 | print(f'example:')
341 | print(f' {sys.argv[0]} -d /dev/video4')
342 |
343 | def main():
344 | try:
345 | arguments, values = getopt.getopt(sys.argv[1:], 'hlc:d:', ['help', 'list', 'controller', 'device'])
346 | except getopt.error as err:
347 | print(err)
348 | usage()
349 | sys.exit(2)
350 |
351 | list_ctrls = False
352 | controller_id = None
353 | device = '/dev/video0'
354 |
355 | for current_argument, current_value in arguments:
356 | if current_argument in ('-h', '--help'):
357 | usage()
358 | sys.exit(0)
359 | elif current_argument in ('-l', '--list'):
360 | list_ctrls = True
361 | elif current_argument in ('-c', '--controller'):
362 | controller_id = current_value
363 | elif current_argument in ('-d', '--device'):
364 | device = current_value
365 |
366 | seq = snd_seq_p()
367 |
368 | if snd_seq_open(seq, b"default", SND_SEQ_OPEN_INPUT, 0) < 0:
369 | logging.error("Could not open sequencer")
370 | sys.exit(2)
371 |
372 | if snd_seq_set_client_name(seq, b"Midi Listener") < 0:
373 | logging.error("Could not set client name")
374 | sys.exit(2)
375 |
376 | if snd_seq_create_simple_port(seq, b"listen:in",
377 | SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE,
378 | SND_SEQ_PORT_TYPE_APPLICATION) < 0:
379 | logging.error("Could not open port")
380 | sys.exit(2)
381 |
382 | ports = get_hw_client_ports(seq)
383 | if not ports:
384 | logging.error("Couldn't find hw devices")
385 | sys.exit(2)
386 |
387 | if list_ctrls:
388 | for p in ports:
389 | print(f'{p[0]}:{p[1]}:{p[2]}')
390 | sys.exit(0)
391 |
392 | client, port = ports[0][1], ports[0][2]
393 |
394 | if controller_id is not None:
395 | spl = controller_id.split(':')
396 | if len(spl) < 2:
397 | logging.error(f'invalid controller id: {controller_id}')
398 | sys.exit(2)
399 | client, port = int(spl[-2]), int(spl[-1])
400 |
401 | logging.info(f'using port: {client}:{port}')
402 |
403 | snd_seq_set_client_event_filter(seq, SND_SEQ_EVENT_CONTROLLER)
404 | snd_seq_set_client_event_filter(seq, SND_SEQ_EVENT_PGMCHANGE)
405 | snd_seq_set_client_event_filter(seq, SND_SEQ_EVENT_PORT_UNSUBSCRIBED)
406 |
407 | if snd_seq_connect_from(seq, 0, client, port) < 0:
408 | logging.error("Cannot connect from port")
409 | sys.exit(2)
410 |
411 | npfd = snd_seq_poll_descriptors_count(seq, select.POLLIN)
412 | pfd = (pollfd * npfd)()
413 | snd_seq_poll_descriptors(seq, pfd, npfd, select.POLLIN)
414 |
415 | loop = asyncio.new_event_loop()
416 | loop.ctx = {}
417 |
418 | loop.add_signal_handler(signal.SIGINT, loop.stop)
419 |
420 | try:
421 | fd = os.open(device, os.O_RDWR, 0)
422 | except Exception as e:
423 | logging.error(f'os.open({device}, os.O_RDWR, 0) failed: {e}')
424 | sys.exit(2)
425 |
426 | camera_ctrls = CameraCtrls(device, fd)
427 | if not camera_ctrls.has_ptz():
428 | logging.error(f'camera {device} cannot do PTZ')
429 | sys.exit(1)
430 |
431 | ptz = PTZController(camera_ctrls)
432 |
433 | for i in range(npfd):
434 | loop.add_reader(pfd[i].fd, process_midi, seq, ptz, loop)
435 |
436 | try:
437 | loop.run_forever()
438 | finally:
439 | loop.close()
440 |
441 | if __name__ == '__main__':
442 | sys.exit(main())
443 |
--------------------------------------------------------------------------------
/pkg/hu.irl.cameractrls.metainfo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | hu.irl.cameractrls
4 | hu.irl.cameractrls.desktop
5 | Cameractrls
6 |
7 | Gergo Koteles
8 |
9 | Camera controls for Linux
10 |
11 |
12 | It's a standalone Python CLI and GUI (GTK3, GTK4) and camera Viewer (SDL) to set the camera controls in Linux.
13 | It can set the V4L2 controls and it is extendable with the non standard controls.
14 | Currently it has a Logitech extension (LED mode, LED frequency, BRIO FoV, Relative Pan/Tilt, PTZ presets), Kiyo Pro extension (HDR, HDR mode, FoV, AF mode, Save), Dell UltraSharp WB7022 extension, AnkerWork C310 extension, Preset extension (Save and restore controls), Control Restore Daemon (to restore presets at device connection).
15 |