From 662b63089965928849fd0eece390a8600ac753cd Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 5 Jun 2024 16:40:04 +0200 Subject: [PATCH] feat: Expand camera control ability (#14) --- README.md | 37 +++++++------ docs/camera-controls.md | 67 ++++++++++++++++++++++ resources/controls_style.css | 69 +++++++++++++++++++++++ resources/spyglass.conf | 5 ++ scripts/spyglass | 5 +- spyglass/camera.py | 6 +- spyglass/camera_options.py | 104 +++++++++++++++++++++++++++++++++++ spyglass/cli.py | 30 +++++++++- spyglass/server.py | 23 +++++++- spyglass/url_parsing.py | 10 +++- tests/test_cli.py | 41 +++++++++++++- 11 files changed, 370 insertions(+), 27 deletions(-) create mode 100644 docs/camera-controls.md create mode 100644 resources/controls_style.css create mode 100644 spyglass/camera_options.py diff --git a/README.md b/README.md index 1e97841..bdb72b0 100644 --- a/README.md +++ b/README.md @@ -38,23 +38,26 @@ This will start the server with the following default configuration: On startup the following arguments are supported: -| Argument | Description | Default | -|-------------------------------|-----------------------------------------------------------------------------------------------------|--------------| -| `-b`, `--bindaddress` | Address where the server will listen for new incoming connections. | `0.0.0.0` | -| `-p`, `--port` | Port where the server will listen for new incoming connections. | `8080` | -| `-r`, `--resolution` | Resolution of the captured frames. This argument expects the format x | `640x480` | -| `-f`, `--fps` | Framerate in frames per second (fps). | `15` | -| `-st`, `--stream_url` | Sets the URL for the mjpeg stream. | `/stream` | -| `-sn`, `--snapshot_url` | Sets the URL for snapshots (single frame of stream). | `/snapshot` | -| `-af`, `--autofocus` | Autofocus mode. Supported modes: `manual`, `continuous` | `continuous` | -| `-l`, `--lensposition` | Set focal distance. 0 for infinite focus, 0.5 for approximate 50cm. Only used with Autofocus manual | `0.0` | -| `-s`, `--autofocusspeed` | Autofocus speed. Supported values: `normal`, `fast`. Only used with Autofocus continuous | `normal` | -| `-ud` `--upsidedown` | Rotate the image by 180° (see below) | | -| `-fh` `--flip_horizontal` | Mirror the image horizontally (see below) | | -| `-fv` `--flip_vertical` | Mirror the image vertically (see below) | | -| `-or` `--orientation_exif` | Set the image orientation using an EXIF header (see below) | | -| `-tf` `--tuning_filter` | Set a tuning filter file name. | | -| `-tfd` `--tuning_filter_dir` | Set the directory to look for tuning filters. | | +| Argument | Description | Default | +|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------|--------------| +| `-b`, `--bindaddress` | Address where the server will listen for new incoming connections. | `0.0.0.0` | +| `-p`, `--port` | Port where the server will listen for new incoming connections. | `8080` | +| `-r`, `--resolution` | Resolution of the captured frames. This argument expects the format \x\ | `640x480` | +| `-f`, `--fps` | Framerate in frames per second (fps). | `15` | +| `-st`, `--stream_url` | Sets the URL for the mjpeg stream. | `/stream` | +| `-sn`, `--snapshot_url` | Sets the URL for snapshots (single frame of stream). | `/snapshot` | +| `-af`, `--autofocus` | Autofocus mode. Supported modes: `manual`, `continuous`. | `continuous` | +| `-l`, `--lensposition` | Set focal distance. 0 for infinite focus, 0.5 for approximate 50cm. Only used with Autofocus manual. | `0.0` | +| `-s`, `--autofocusspeed` | Autofocus speed. Supported values: `normal`, `fast`. Only used with Autofocus continuous | `normal` | +| `-ud`, `--upsidedown` | Rotate the image by 180° (see below) | | +| `-fh`, `--flip_horizontal` | Mirror the image horizontally (see below) | | +| `-fv`, `--flip_vertical` | Mirror the image vertically (see below) | | +| `-or`, `--orientation_exif` | Set the image orientation using an EXIF header (see below) | | +| `-c`, `--controls` | Define camera controls to start spyglass with. Can be used multiple times. This argument expects the format \=\. | | +| `--list-controls` | List all available libcamera controls onto the console. Those can be used with `--controls` | | +| `-tf`, `--tuning_filter` | Set a tuning filter file name. | | +| `-tfd`, `--tuning_filter_dir` | Set the directory to look for tuning filters. | | + Starting the server without any argument is the same as ```shell diff --git a/docs/camera-controls.md b/docs/camera-controls.md new file mode 100644 index 0000000..d53effc --- /dev/null +++ b/docs/camera-controls.md @@ -0,0 +1,67 @@ +Spyglass offers a few CLI parameters for the most commonly used camera controls. +Controls not directly available through the CLI can be used with the `--controls` (`-c`) or `--controls-string` (`-cs`) parameters or the `CONTROLS` section inside the `spyglass.conf`. + + +## How to list available controls? + +Spyglass provides a CLI parameter to list all available controls `--list-controls`. The available controls are then printed onto your shell under `Available controls:`. + +Following shows an example for a Raspberry Pi Module v3: +```sh +Available controls: +NoiseReductionMode (int) : min=0 max=4 default=0 +ScalerCrop (tuple) : min=(0, 0, 0, 0) max=(65535, 65535, 65535, 65535) default=(0, 0, 0, 0) +Sharpness (float) : min=0.0 max=16.0 default=1.0 +AwbEnable (bool) : min=False max=True default=None +FrameDurationLimits (int) : min=33333 max=120000 default=None +ExposureValue (float) : min=-8.0 max=8.0 default=0.0 +AwbMode (int) : min=0 max=7 default=0 +AeExposureMode (int) : min=0 max=3 default=0 +Brightness (float) : min=-1.0 max=1.0 default=0.0 +AfWindows (tuple) : min=(0, 0, 0, 0) max=(65535, 65535, 65535, 65535) default=(0, 0, 0, 0) +AfSpeed (int) : min=0 max=1 default=0 +AfTrigger (int) : min=0 max=1 default=0 +LensPosition (float) : min=0.0 max=32.0 default=1.0 +AfRange (int) : min=0 max=2 default=0 +AfPause (int) : min=0 max=2 default=0 +ExposureTime (int) : min=0 max=66666 default=None +AeEnable (bool) : min=False max=True default=None +AeConstraintMode (int) : min=0 max=3 default=0 +AfMode (int) : min=0 max=2 default=0 +AnalogueGain (float) : min=1.0 max=16.0 default=None +ColourGains (float) : min=0.0 max=32.0 default=None +AfMetering (int) : min=0 max=1 default=0 +AeMeteringMode (int) : min=0 max=3 default=0 +Contrast (float) : min=0.0 max=32.0 default=1.0 +Saturation (float) : min=0.0 max=32.0 default=1.0 +``` + + +## How to apply a camera control? + +There are multiple ways to apply a camera control. All methods are case insensitive. + +### Shell + +There are two different parameters to apply the controls: + +- `--controls`/`-c` can be used multiple times, to set multiple controls. E.g. using `-c brightness=0.5 -c awbenable=false` will apply `0.5` to the `Brightness` and `False` as the new `AwbEnable` value. +- `--controls-string`/`cs` can be used only once. E.g. using `--controls-string "brightness=0.5, awbenable=16"` will apply `0.5` on the `Brightness` and `False` as the new `AwbEnable` control. Note: The `"` are required and the controls need to be separated by a `,`. This is intended only for parsing the config. + +### Config + +The `spyglass.conf` accepts camera controls under the `CONTROLS` option. E.g. `CONTROLS="brightness=0,awbenable=false"` will apply `0.5` to the `Brightness` and `False` as the new `AwbEnable` value. + +### Webinterface + +Spyglass also provides an API endpoint to change the camera controls during runtime. This endpoint is available under `http://:/controls` and cannot be changed. + +Calling it without any parameters will show you a list of all available controls, like `--list-controls`. + +E.g. `http://:/controls?brightness=0.5&awbenable=false` will apply `0.5` to the `Brightness` and `False` as the new `AwbEnable` value. + +If you apply parameters the interface will show you the parameters Spyglass found inside the url and which controls got actually processed: +- `Parsed Controls` shows you the parameters Spyglass found during the request. +- `Processed Controls` shows you the parameters of the `Parsed Controls` Spyglass could actually set for the cam. + +E.g. `http://:/controls?brightness=0.5&foo=bar&foobar` will show you `Parsed Controls: [('brightness', '1'), ('foo', 'bar')]` and `Processed Controls: {'Brightness': 1}`. diff --git a/resources/controls_style.css b/resources/controls_style.css new file mode 100644 index 0000000..e88c32e --- /dev/null +++ b/resources/controls_style.css @@ -0,0 +1,69 @@ +body { + font-family: Arial, sans-serif; + background-color: #f6f6f6; + margin: 40px; + display: flex; + flex-direction: column; + align-items: center; +} + +.card-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + max-width: 1100px; +} + +.card-container:nth-child(odd) .card { + background-color: white; +} + +.card-container:nth-child(even) .card { + background-color: #f0f0f0; +} + +.card { + border-radius: 5px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 0px 20px 20px 20px; /* top right bottom left */ + width: 80%; + box-sizing: border-box; + transition: 0.3s; +} + +.card:hover { + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.card h2 { + color: #2C3E50; + border-bottom: 1px solid #2C3E50; + padding-bottom: 10px; + margin-bottom: 10px; +} + +.card-content { + display: flex; + margin-bottom: 1px; +} + +.setting { + flex: 1; + display: flex; + margin: 0 5px; +} + +.label, +.value { + font-size: 14px; +} + +.label { + font-weight: bold; + color: #7F8C8D; +} + +.value { + margin-left: 4px; +} diff --git a/resources/spyglass.conf b/resources/spyglass.conf index 1977881..1072b56 100644 --- a/resources/spyglass.conf +++ b/resources/spyglass.conf @@ -57,6 +57,11 @@ AF_SPEED="normal" #### r270 - Rotate 270 CW ORIENTATION_EXIF="h" +#### Camera Controls +#### NOTE: Set v4l2 controls your camera supports at startup +#### EXAMPLE: CONTROLS="brightness=0,awbenable=false" +CONTROLS="" + #### Tuning Filter Directory (STRING)[default: none] #### NOTE: Directory where to search for tuning filters(if defined). #### Directory only used if TUNING_FILTER is defined diff --git a/scripts/spyglass b/scripts/spyglass index 4d7ec04..46b6261 100755 --- a/scripts/spyglass +++ b/scripts/spyglass @@ -103,9 +103,10 @@ run_spyglass() { --autofocus "${AUTO_FOCUS:-continuous}" \ --lensposition "${FOCAL_DIST:-0.0}" \ --autofocusspeed "${AF_SPEED:-normal}" \ - --orientation_exif "${ORIENTATION_EXIF:-h}"\ + --orientation_exif "${ORIENTATION_EXIF:-h}" \ --tuning_filter "${TUNING_FILTER:-}"\ - --tuning_filter_dir "${TUNING_FILTER_DIR:-}" + --tuning_filter_dir "${TUNING_FILTER_DIR:-}" \ + --controls-string "${CONTROLS:-0=0}" # 0=0 to prevent error on empty string } #### MAIN diff --git a/spyglass/camera.py b/spyglass/camera.py index 9eec702..2090392 100644 --- a/spyglass/camera.py +++ b/spyglass/camera.py @@ -1,7 +1,7 @@ import libcamera +from spyglass.camera_options import process_controls from picamera2 import Picamera2 - def init_camera( width: int, height: int, @@ -12,6 +12,7 @@ def init_camera( upsidedown=False, flip_horizontal=False, flip_vertical=False, + control_list: list[list[str]]=[], tuning_filter=None, tuning_filter_dir=None): @@ -26,6 +27,9 @@ def init_camera( picam2 = Picamera2(tuning=tuning) controls = {'FrameRate': fps} + c = process_controls(picam2, [tuple(ctrl) for ctrl in control_list]) + controls.update(c) + if 'AfMode' in picam2.camera_controls: controls['AfMode'] = autofocus controls['AfSpeed'] = autofocus_speed diff --git a/spyglass/camera_options.py b/spyglass/camera_options.py new file mode 100644 index 0000000..3c15321 --- /dev/null +++ b/spyglass/camera_options.py @@ -0,0 +1,104 @@ +import libcamera +import ast + +def parse_dictionary_to_html_page(camera, parsed_controls='None', processed_controls='None'): + html = """ + + + """ + html += f""" + + + + Camera Settings + + + """ + html += f""" + +

Available camera options

+

Parsed Controls: {parsed_controls}

+

Processed Controls: {processed_controls}

+ """ + for control, values in camera.camera_controls.items(): + html += f""" +
+
+

{control}

+
+
+ Min: + {values[0]} +
+
+ Max: + {values[1]} +
+
+ Default: + {values[2]} +
+
+
+
+ """ + html += """ + + + """ + return html + +def get_style(): + with (open('resources/controls_style.css', 'r')) as f: + return f.read() + +def process_controls(camera, controls: list[tuple[str, str]]) -> dict[str, any]: + controls_dict_lower = { k.lower(): k for k in camera.camera_controls.keys() } + if controls == None: + return {} + processed_controls = {} + for key, value in controls: + key = key.lower().strip() + if key.lower() in controls_dict_lower.keys(): + value = value.lower().strip() + k = controls_dict_lower[key] + v = parse_from_string(value) + processed_controls[k] = v + return processed_controls + +def parse_from_string(input_string: str) -> any: + try: + return ast.literal_eval(input_string) + except (ValueError, TypeError, SyntaxError): + pass + + if input_string.lower() in ['true', 'false']: + return input_string.lower() == 'true' + + return input_string + +def get_type_str(obj) -> str: + return str(type(obj)).split('\'')[1] + +def get_libcamera_controls_string(camera_path: str) -> str: + ctrls_str = "" + libcam_cm = libcamera.CameraManager.singleton() + cam = libcam_cm.cameras[0] + def rectangle_to_tuple(rectangle): + return (rectangle.x, rectangle.y, rectangle.width, rectangle.height) + for k, v in cam.controls.items(): + if isinstance(v.min, libcamera.Rectangle): + min = rectangle_to_tuple(v.min) + max = rectangle_to_tuple(v.max) + default = rectangle_to_tuple(v.default) + else: + min = v.min + max = v.max + default = v.default + + str_first = f"{k.name} ({get_type_str(min)})" + str_second = f"min={min} max={max} default={default}" + str_indent = (30 - len(str_first)) * ' ' + ': ' + ctrls_str += str_first + str_indent + str_second + '\n' + + return ctrls_str.strip() diff --git a/spyglass/cli.py b/spyglass/cli.py index 8899cc3..6c0e454 100644 --- a/spyglass/cli.py +++ b/spyglass/cli.py @@ -8,6 +8,7 @@ import sys import libcamera + from picamera2.encoders import MJPEGEncoder from picamera2.outputs import FileOutput @@ -16,6 +17,8 @@ from spyglass.camera import init_camera from spyglass.server import StreamingOutput from spyglass.server import run_server +from spyglass import camera_options + MAX_WIDTH = 1920 MAX_HEIGHT = 1920 @@ -41,6 +44,13 @@ def main(args=None): stream_url = parsed_args.stream_url snapshot_url = parsed_args.snapshot_url orientation_exif = parsed_args.orientation_exif + controls = parsed_args.controls + if parsed_args.controls_string: + controls += [c.split('=') for c in parsed_args.controls_string.split(',')] + if parsed_args.list_controls: + print('Available controls:\n'+camera_options.get_libcamera_controls_string(0)) + return + picam2 = init_camera( width, height, @@ -51,6 +61,7 @@ def main(args=None): parsed_args.upsidedown, parsed_args.flip_horizontal, parsed_args.flip_vertical, + controls, parsed_args.tuning_filter, parsed_args.tuning_filter_dir) @@ -58,7 +69,7 @@ def main(args=None): picam2.start_recording(MJPEGEncoder(), FileOutput(output)) try: - run_server(bind_address, port, output, stream_url, snapshot_url, orientation_exif) + run_server(bind_address, port, picam2, output, stream_url, snapshot_url, orientation_exif) finally: picam2.stop_recording() @@ -71,6 +82,12 @@ def resolution_type(arg_value, pat=re.compile(r"^\d+x\d+$")): raise argparse.ArgumentTypeError("invalid value: x expected.") return arg_value +def control_type(arg_value: str): + if '=' in arg_value: + return arg_value.split('=') + else: + raise argparse.ArgumentTypeError(f"invalid control: Missing value: {arg_value}") + def orientation_type(arg_value): if arg_value in option_to_exif_orientation: @@ -105,7 +122,6 @@ def split_resolution(res): raise argparse.ArgumentTypeError("Maximum supported resolution is 1920x1920") return w, h - # endregion args parsers @@ -158,10 +174,20 @@ def get_parser(): ' mhr90 - Mirror horizontal and rotate 90 CW\n' ' r270 - Rotate 270 CW' ) + parser.add_argument('-c', '--controls', default=[], type=control_type, action='extend', nargs='*', + help='Define camera controls to start with spyglass. ' + 'Can be used multiple times.\n' + 'Format: =') + parser.add_argument('-cs', '--controls-string', default='', type=str, + help='Define camera controls to start with spyglass. ' + 'Input as a long string.\n' + 'Format: = =') parser.add_argument('-tf', '--tuning_filter', type=str, default=None, nargs='?', const="", help='Set a tuning filter file name.') parser.add_argument('-tfd', '--tuning_filter_dir', type=str, default=None, nargs='?',const="", help='Set the directory to look for tuning filters.') + parser.add_argument('--list-controls', action='store_true', help='List available camera controls and exits.') + return parser # endregion cli args diff --git a/spyglass/server.py b/spyglass/server.py index 99c28a9..ed2714f 100755 --- a/spyglass/server.py +++ b/spyglass/server.py @@ -3,8 +3,9 @@ import socketserver from http import server from threading import Condition -from spyglass.url_parsing import check_urls_match +from spyglass.url_parsing import check_urls_match, get_url_params from spyglass.exif import create_exif_header +from spyglass.camera_options import parse_dictionary_to_html_page, process_controls from . import logger @@ -24,7 +25,13 @@ class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer): daemon_threads = True -def run_server(bind_address, port, output, stream_url='/stream', snapshot_url='/snapshot', orientation_exif=0): +def run_server(bind_address, + port, + camera, + output: StreamingOutput, + stream_url='/stream', + snapshot_url='/snapshot', + orientation_exif=0): exif_header = create_exif_header(orientation_exif) class StreamingHandler(server.BaseHTTPRequestHandler): @@ -33,6 +40,17 @@ def do_GET(self): self.start_streaming() elif check_urls_match(snapshot_url, self.path): self.send_snapshot() + elif check_urls_match('/controls', self.path): + parsed_controls = get_url_params(self.path) + parsed_controls = parsed_controls if parsed_controls else None + processed_controls = process_controls(camera, parsed_controls) + camera.set_controls(processed_controls) + content = parse_dictionary_to_html_page(camera, parsed_controls, processed_controls).encode('utf-8') + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.send_header('Content-Length', len(content)) + self.end_headers() + self.wfile.write(content) else: self.send_error(404) self.end_headers() @@ -95,6 +113,7 @@ def send_jpeg_content_headers(self, frame, extra_len=0): logger.info('Server listening on %s:%d', bind_address, port) logger.info('Streaming endpoint: %s', stream_url) logger.info('Snapshot endpoint: %s', snapshot_url) + logger.info('Controls endpoint: %s', '/controls') address = (bind_address, port) current_server = StreamingServer(address, StreamingHandler) current_server.serve_forever() diff --git a/spyglass/url_parsing.py b/spyglass/url_parsing.py index 84179b8..3c07126 100644 --- a/spyglass/url_parsing.py +++ b/spyglass/url_parsing.py @@ -20,10 +20,16 @@ def check_paths_match(expected_url, incoming_url): return False +def get_url_params(url): + # Get URL params + params = parse_qsl(urlparse(url).query) + + return params + def check_params_match(expected_url, incoming_url): # Check URL params - exp_params = parse_qsl(urlparse(expected_url).query) - inc_params = parse_qsl(urlparse(incoming_url).query) + exp_params = get_url_params(expected_url) + inc_params = get_url_params(incoming_url) # Create list of matching params matching_params = set(exp_params) & set(inc_params) diff --git a/tests/test_cli.py b/tests/test_cli.py index 0a9396d..a0a3a4f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -16,6 +16,7 @@ DEFAULT_FPS = 15 DEFAULT_AF_SPEED = AF_SPEED_ENUM_NORMAL DEFAULT_AUTOFOCUS_MODE = AF_MODE_ENUM_CONTINUOUS +DEFAULT_CONTROLS = [] DEFAULT_TUNING_FILTER = None DEFAULT_TUNING_FILTER_DIR = None @@ -91,6 +92,7 @@ def test_init_camera_with_defaults(mock_spyglass_camera, mock_spyglass_server): DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -114,6 +116,7 @@ def test_init_camera_resolution(mock_spyglass_server, mock_spyglass_camera): DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -153,6 +156,7 @@ def test_init_camera_fps(mock_spyglass_server, mock_spyglass_camera): DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -176,6 +180,7 @@ def test_init_camera_af_manual(mock_spyglass_server, mock_spyglass_camera): DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -199,6 +204,7 @@ def test_init_camera_af_continuous(mock_spyglass_server, mock_spyglass_camera): DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -222,6 +228,7 @@ def test_init_camera_lens_position(mock_spyglass_server, mock_spyglass_camera): DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -245,6 +252,7 @@ def test_init_camera_af_speed_normal(mock_spyglass_server, mock_spyglass_camera) DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -268,6 +276,7 @@ def test_init_camera_af_speed_fast(mock_spyglass_server, mock_spyglass_camera): DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -291,6 +300,7 @@ def test_init_camera_upside_down(mock_spyglass_server, mock_spyglass_camera): True, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -314,6 +324,7 @@ def test_init_camera_flip_horizontal(mock_spyglass_server, mock_spyglass_camera) DEFAULT_UPSIDE_DOWN, True, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -337,6 +348,31 @@ def test_init_camera_flip_vertical(mock_spyglass_server, mock_spyglass_camera): DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, True, + DEFAULT_CONTROLS, + DEFAULT_TUNING_FILTER, + DEFAULT_TUNING_FILTER_DIR + ) + +@patch("spyglass.server.run_server") +@patch("spyglass.camera.init_camera") +def test_init_camera_controls(mock_spyglass_server, mock_spyglass_camera): + from spyglass import cli + import spyglass.camera + cli.main(args=[ + '-c', 'brightness=-0.4', + '-c', 'awbenable=false' + ]) + spyglass.camera.init_camera.assert_called_once_with( + DEFAULT_WIDTH, + DEFAULT_HEIGHT, + DEFAULT_FPS, + DEFAULT_AUTOFOCUS_MODE, + DEFAULT_LENS_POSITION, + DEFAULT_AF_SPEED, + DEFAULT_UPSIDE_DOWN, + DEFAULT_FLIP_HORIZONTALLY, + DEFAULT_FLIP_VERTICALLY, + [['brightness', '-0.4'],['awbenable', 'false']], DEFAULT_TUNING_FILTER, DEFAULT_TUNING_FILTER_DIR ) @@ -354,7 +390,7 @@ def test_run_server_with_configuration_from_arguments(mock_spyglass_server, mock '-sn', 'snapshot-url', '-or', 'h' ]) - spyglass.server.run_server.assert_called_once_with('1.2.3.4', 1234, ANY, 'streaming-url', 'snapshot-url', 1) + spyglass.server.run_server.assert_called_once_with('1.2.3.4', 1234, ANY, ANY, 'streaming-url', 'snapshot-url', 1) @patch("spyglass.server.run_server") @@ -383,6 +419,7 @@ def test_run_server_with_orientation(mock_spyglass_server, mock_spyglass_camera, '1.2.3.4', 1234, ANY, + ANY, 'streaming-url', 'snapshot-url', expected_output @@ -407,6 +444,7 @@ def test_init_camera_using_only_tuning_filter_file(mock_spyglass_server, mock_sp DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, "test", DEFAULT_TUNING_FILTER_DIR ) @@ -430,6 +468,7 @@ def test_init_camera_using_tuning_filters(mock_spyglass_server, mock_spyglass_ca DEFAULT_UPSIDE_DOWN, DEFAULT_FLIP_HORIZONTALLY, DEFAULT_FLIP_VERTICALLY, + DEFAULT_CONTROLS, "test", "test-dir" )