From 2428ffbf7e1448feb7089089b83742c81ffed091 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz <58853838+mryel00@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:02:05 +0100 Subject: [PATCH 001/129] chore: wip --- crowsnest.py | 17 +++++++++++++++++ pylibs/__init__.py | 0 pylibs/cam.py | 0 pylibs/parameter.py | 14 ++++++++++++++ pylibs/section.py | 20 ++++++++++++++++++++ pylibs/streamer.py | 11 +++++++++++ 6 files changed, 62 insertions(+) create mode 100644 crowsnest.py create mode 100644 pylibs/__init__.py create mode 100644 pylibs/cam.py create mode 100644 pylibs/parameter.py create mode 100644 pylibs/section.py create mode 100644 pylibs/streamer.py diff --git a/crowsnest.py b/crowsnest.py new file mode 100644 index 00000000..2a981bbd --- /dev/null +++ b/crowsnest.py @@ -0,0 +1,17 @@ +import configparser + +config_path = "resources/crowsnest.conf" + +config = configparser.ConfigParser() +config.read(config_path) + +# Crowsnest config settings +log_path = '/home/pi/printer_data/logs/crowsnest.log' +log_level = 'debug' +delete_log = False + + + +print(config.sections()) +for key in config['crowsnest']: + print(key) diff --git a/pylibs/__init__.py b/pylibs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pylibs/cam.py b/pylibs/cam.py new file mode 100644 index 00000000..e69de29b diff --git a/pylibs/parameter.py b/pylibs/parameter.py new file mode 100644 index 00000000..61b407d3 --- /dev/null +++ b/pylibs/parameter.py @@ -0,0 +1,14 @@ +class CN_Parameter: + def __init__(self, name, type, default_value=None) -> None: + self.name = name + self.default_value = default_value + self.type = type + + # Parameter is required if no default value is specified + self.required = self.default_value == None + + +if __name__ == "__main__": + print("Do not execute this file directly!") +else: + pass \ No newline at end of file diff --git a/pylibs/section.py b/pylibs/section.py new file mode 100644 index 00000000..289dcb93 --- /dev/null +++ b/pylibs/section.py @@ -0,0 +1,20 @@ +from .parameter import CN_Parameter + +class CN_Section: + + # Section looks like this: + # [ ] + # param1 + # param2 + def __init__(self, keyword: str, parameters: list[CN_Parameter], name: str = '') -> None: + self.keyword = keyword + self.parameters = parameters + self.name = name + + # Parse config according to the needs of the section + def parse_config(): + pass + + # Execute section specific stuff, e.g. starting cam + def execute(): + pass diff --git a/pylibs/streamer.py b/pylibs/streamer.py new file mode 100644 index 00000000..64d8b65f --- /dev/null +++ b/pylibs/streamer.py @@ -0,0 +1,11 @@ +from .parameter import CN_Parameter + +class CN_Streamer: + def __init__(self, name, parameters: list[CN_Parameter]) -> None: + self.name = name + self.parameters = parameters + + def config_check(): + pass + + From a2c027ad46192dc79463783077a84c7a666dafef Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz <58853838+mryel00@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:07:59 +0100 Subject: [PATCH 002/129] chore: wip --- pylibs/parameter.py | 6 ------ pylibs/streamer.py | 2 -- 2 files changed, 8 deletions(-) diff --git a/pylibs/parameter.py b/pylibs/parameter.py index 61b407d3..40369408 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -6,9 +6,3 @@ def __init__(self, name, type, default_value=None) -> None: # Parameter is required if no default value is specified self.required = self.default_value == None - - -if __name__ == "__main__": - print("Do not execute this file directly!") -else: - pass \ No newline at end of file diff --git a/pylibs/streamer.py b/pylibs/streamer.py index 64d8b65f..6bed0978 100644 --- a/pylibs/streamer.py +++ b/pylibs/streamer.py @@ -7,5 +7,3 @@ def __init__(self, name, parameters: list[CN_Parameter]) -> None: def config_check(): pass - - From 52aac83244e78aa33bdf9a9d39cb7e638737a100 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz <58853838+mryel00@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:25:15 +0100 Subject: [PATCH 003/129] chore: wip --- crowsnest.py | 14 ++++++++++---- pylibs/cam.py | 2 ++ resources/crowsnest.conf | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 2a981bbd..ba5fd19d 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,4 +1,5 @@ import configparser +from pylibs.cam import Cam config_path = "resources/crowsnest.conf" @@ -10,8 +11,13 @@ log_level = 'debug' delete_log = False +# Example of printing section and values +for section in config.sections(): + print("Section: " + section) + for key in config[section]: + print('Key: '+key+'\t\tValue: '+config[section][key].replace(' ', '').split('#')[0]) - -print(config.sections()) -for key in config['crowsnest']: - print(key) +# Use if else or dict as match case isn't available before v3.10 +for section in config.sections(): + if section.split(' ')[0] == 'cam': + cam = Cam() diff --git a/pylibs/cam.py b/pylibs/cam.py index e69de29b..dc7b129f 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -0,0 +1,2 @@ +class Cam: + test = 0 \ No newline at end of file diff --git a/resources/crowsnest.conf b/resources/crowsnest.conf index 4a1f2446..5343bc73 100644 --- a/resources/crowsnest.conf +++ b/resources/crowsnest.conf @@ -24,7 +24,7 @@ [crowsnest] -log_path: %LOGPATH% +log_path: LOGPATH log_level: verbose # Valid Options are quiet/verbose/debug delete_log: false # Deletes log on every restart, if set to true no_proxy: false @@ -38,5 +38,5 @@ port: 8080 # HTTP/MJPG Stream/Snapshot Port device: /dev/video0 # See Log for available ... resolution: 640x480 # widthxheight format max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. -#custom_flags: # You can run the Stream Services with custom flags. +custom_flags: # You can run the Stream Services with custom flags. #v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see Log what your cam is capable of. From f4ce9810d360b038709a0a93bb85a4651c86a674 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz <58853838+mryel00@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:11:16 +0100 Subject: [PATCH 004/129] chore: wip --- crowsnest.py | 24 +++++++++++++++++++++--- pylibs/cam.py | 6 ++++-- pylibs/section.py | 8 +++++--- pylibs/streamer.py | 2 +- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index ba5fd19d..87abff6b 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,5 +1,6 @@ import configparser -from pylibs.cam import Cam +from pylibs.cam import CN_Cam +from pylibs.section import CN_Section config_path = "resources/crowsnest.conf" @@ -17,7 +18,24 @@ for key in config[section]: print('Key: '+key+'\t\tValue: '+config[section][key].replace(' ', '').split('#')[0]) +sections = [] # Use if else or dict as match case isn't available before v3.10 for section in config.sections(): - if section.split(' ')[0] == 'cam': - cam = Cam() + section_header = section.split(' ') + section_object = None + if section_header[0] == 'crowsnest': + section_object = 1 + elif section_header[0] == 'cam': + section_object = CN_Cam(' '.join(section_header[1:])) + if section_object == None: + raise Exception(f"Section [{section}] couldn't get parsed") + sections.append(section_object) + +k = CN_Section('k') +k1 = CN_Section('k1') + +k.keyword = 'test' +CN_Section.keyword='test2' + +k2 = CN_Section('k2') +print(CN_Section.keyword, k.keyword) diff --git a/pylibs/cam.py b/pylibs/cam.py index dc7b129f..39e54fa4 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -1,2 +1,4 @@ -class Cam: - test = 0 \ No newline at end of file +from .section import CN_Section + +class CN_Cam(CN_Section): + pass diff --git a/pylibs/section.py b/pylibs/section.py index 289dcb93..42a03749 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -1,16 +1,18 @@ from .parameter import CN_Parameter class CN_Section: - + keyword = 'Section' # Section looks like this: # [ ] # param1 # param2 - def __init__(self, keyword: str, parameters: list[CN_Parameter], name: str = '') -> None: - self.keyword = keyword + def __init__(self, parameters: list[CN_Parameter], name: str = '') -> None: self.parameters = parameters self.name = name + def __init__(self, name: str = '') -> None: + self.name = name + # Parse config according to the needs of the section def parse_config(): pass diff --git a/pylibs/streamer.py b/pylibs/streamer.py index 6bed0978..e6965ccc 100644 --- a/pylibs/streamer.py +++ b/pylibs/streamer.py @@ -1,6 +1,6 @@ from .parameter import CN_Parameter -class CN_Streamer: +class CN_Streamer(): def __init__(self, name, parameters: list[CN_Parameter]) -> None: self.name = name self.parameters = parameters From ed9f857e7a118e5772102a033225119e815dace5 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz <58853838+mryel00@users.noreply.github.com> Date: Sat, 18 Nov 2023 01:33:35 +0100 Subject: [PATCH 005/129] chore: wip --- crowsnest.py | 16 ++++++++++++++++ pylibs/cam.py | 35 ++++++++++++++++++++++++++++++++++- pylibs/section.py | 16 +++++++--------- pylibs/ustreamer.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 pylibs/ustreamer.py diff --git a/crowsnest.py b/crowsnest.py index 87abff6b..c2c6927d 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,6 +1,8 @@ import configparser +import importlib from pylibs.cam import CN_Cam from pylibs.section import CN_Section +from pylibs import * config_path = "resources/crowsnest.conf" @@ -17,16 +19,30 @@ print("Section: " + section) for key in config[section]: print('Key: '+key+'\t\tValue: '+config[section][key].replace(' ', '').split('#')[0]) +print(config) sections = [] # Use if else or dict as match case isn't available before v3.10 for section in config.sections(): section_header = section.split(' ') section_object = None + section_keyword = section_header[0] + + try: + module = importlib.import_module(f'pylibs.{section_keyword}') + module_class = getattr(module, 'load_module')() + CN_Section.available_sections[section_keyword] = module_class + module_class().parse_config(config[section]) + + except (ModuleNotFoundError, AttributeError) as e: + print(str(e)) + continue if section_header[0] == 'crowsnest': section_object = 1 elif section_header[0] == 'cam': section_object = CN_Cam(' '.join(section_header[1:])) + section_object.parse_config(config[section]) + if section_object == None: raise Exception(f"Section [{section}] couldn't get parsed") sections.append(section_object) diff --git a/pylibs/cam.py b/pylibs/cam.py index 39e54fa4..eb562707 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -1,4 +1,37 @@ from .section import CN_Section +from .parameter import CN_Parameter +from configparser import SectionProxy + +import importlib class CN_Cam(CN_Section): - pass + keyword = 'cam' + loaded_modes = {} + + def __init__(self, name: str = '') -> None: + super().__init__(name) + + self.possible_parameters += [ + CN_Parameter('mode', str) + ] + + def parse_config(self, section: SectionProxy): + # Dynamically import module + mode = section["mode"].split()[0] + module = importlib.import_module(f'pylibs.{mode}') + CN_Cam.loaded_modes[mode] = getattr(module, 'load_module')() + + print(CN_Cam.loaded_modes) + t = CN_Cam.loaded_modes[mode]('test') + print(t, t.keyword, t.name) + + def execute(): + pass + +def load_module(): + return CN_Cam + +#if __name__ == "__main__": +# print("This is a module and shouldn't be executed directly") +#else: +# CN_Section.available_sections[CN_Cam.keyword] = CN_Cam diff --git a/pylibs/section.py b/pylibs/section.py index 42a03749..b7ddce10 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -1,22 +1,20 @@ -from .parameter import CN_Parameter +from configparser import SectionProxy class CN_Section: keyword = 'Section' + available_sections = {} # Section looks like this: # [ ] # param1 # param2 - def __init__(self, parameters: list[CN_Parameter], name: str = '') -> None: - self.parameters = parameters - self.name = name - def __init__(self, name: str = '') -> None: self.name = name + self.possible_parameters = [] # Parse config according to the needs of the section - def parse_config(): - pass + def parse_config(self, section: SectionProxy): + raise NotImplementedError("If you see this a module is implemented wrong!!!") # Execute section specific stuff, e.g. starting cam - def execute(): - pass + def execute(self): + raise NotImplementedError("If you see this a module is implemented wrong!!!") diff --git a/pylibs/ustreamer.py b/pylibs/ustreamer.py new file mode 100644 index 00000000..40dc7cb3 --- /dev/null +++ b/pylibs/ustreamer.py @@ -0,0 +1,28 @@ +from .cam import CN_Cam +from .parameter import CN_Parameter +from configparser import SectionProxy + +class CN_Ustreamer(CN_Cam): + keyword = 'ustreamer' + + def __init__(self, name: str = '') -> None: + super().__init__(name) + + self.possible_parameters += [ + CN_Parameter('port', int), + CN_Parameter('device', str), + CN_Parameter('resolution', bool), + CN_Parameter('max_fps', int), + CN_Parameter('custom_flags', str, ''), + CN_Parameter('v4l2ctl', dict, {}), + CN_Parameter('no_proxy', bool, False) + ] + + def parse_config(self, section: SectionProxy): + pass + + def execute(self): + pass + +def load_module(): + return CN_Ustreamer From b4d68f1395bbecdb5a85f0219b39a6e9125f13f6 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz <58853838+mryel00@users.noreply.github.com> Date: Sat, 18 Nov 2023 14:01:28 +0100 Subject: [PATCH 006/129] chore: wip --- crowsnest.py | 20 ++++++++++---------- pylibs/cam.py | 31 ++++++++++++++++++++----------- pylibs/parameter.py | 2 +- pylibs/section.py | 7 ++++--- pylibs/streamer.py | 6 +++--- pylibs/ustreamer.py | 34 +++++++++++++++++++++++----------- 6 files changed, 61 insertions(+), 39 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index c2c6927d..9a9d90a3 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,7 +1,7 @@ import configparser import importlib -from pylibs.cam import CN_Cam -from pylibs.section import CN_Section +from pylibs.cam import Cam +from pylibs.section import Section from pylibs import * config_path = "resources/crowsnest.conf" @@ -31,27 +31,27 @@ try: module = importlib.import_module(f'pylibs.{section_keyword}') module_class = getattr(module, 'load_module')() - CN_Section.available_sections[section_keyword] = module_class + Section.available_sections[section_keyword] = module_class module_class().parse_config(config[section]) - except (ModuleNotFoundError, AttributeError) as e: print(str(e)) continue + if section_header[0] == 'crowsnest': section_object = 1 elif section_header[0] == 'cam': - section_object = CN_Cam(' '.join(section_header[1:])) + section_object = Cam(' '.join(section_header[1:])) section_object.parse_config(config[section]) if section_object == None: raise Exception(f"Section [{section}] couldn't get parsed") sections.append(section_object) -k = CN_Section('k') -k1 = CN_Section('k1') +k = Section('k') +k1 = Section('k1') k.keyword = 'test' -CN_Section.keyword='test2' +Section.keyword='test2' -k2 = CN_Section('k2') -print(CN_Section.keyword, k.keyword) +k2 = Section('k2') +print(Section.keyword, k.keyword) diff --git a/pylibs/cam.py b/pylibs/cam.py index eb562707..c26dc55d 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -1,35 +1,44 @@ -from .section import CN_Section -from .parameter import CN_Parameter +from .section import Section +from .parameter import Parameter from configparser import SectionProxy import importlib -class CN_Cam(CN_Section): +class Cam(Section): keyword = 'cam' loaded_modes = {} def __init__(self, name: str = '') -> None: super().__init__(name) + self.parameters.update({ + 'mode': None + }) + self.possible_parameters += [ - CN_Parameter('mode', str) + Parameter('mode', str) ] def parse_config(self, section: SectionProxy): # Dynamically import module mode = section["mode"].split()[0] - module = importlib.import_module(f'pylibs.{mode}') - CN_Cam.loaded_modes[mode] = getattr(module, 'load_module')() - - print(CN_Cam.loaded_modes) - t = CN_Cam.loaded_modes[mode]('test') - print(t, t.keyword, t.name) + try: + module = importlib.import_module(f'pylibs.{mode}') + module_class = getattr(module, 'load_module')() + Cam.loaded_modes[mode] = module_class + print(Cam.loaded_modes) + t = Cam.loaded_modes[mode]('test') + print(t, t.keyword, t.name) + return module_class(self.name).parse_config(section) + except (ModuleNotFoundError, AttributeError) as e: + print(str(e)) + return def execute(): pass def load_module(): - return CN_Cam + return Cam #if __name__ == "__main__": # print("This is a module and shouldn't be executed directly") diff --git a/pylibs/parameter.py b/pylibs/parameter.py index 40369408..ac1b489e 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -1,4 +1,4 @@ -class CN_Parameter: +class Parameter: def __init__(self, name, type, default_value=None) -> None: self.name = name self.default_value = default_value diff --git a/pylibs/section.py b/pylibs/section.py index b7ddce10..734c162a 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -1,6 +1,6 @@ from configparser import SectionProxy -class CN_Section: +class Section: keyword = 'Section' available_sections = {} # Section looks like this: @@ -10,11 +10,12 @@ class CN_Section: def __init__(self, name: str = '') -> None: self.name = name self.possible_parameters = [] + self.parameters = {} # Parse config according to the needs of the section def parse_config(self, section: SectionProxy): - raise NotImplementedError("If you see this a module is implemented wrong!!!") + raise NotImplementedError("If you see this, a module is implemented wrong!!!") # Execute section specific stuff, e.g. starting cam def execute(self): - raise NotImplementedError("If you see this a module is implemented wrong!!!") + raise NotImplementedError("If you see this, a module is implemented wrong!!!") diff --git a/pylibs/streamer.py b/pylibs/streamer.py index e6965ccc..a65cb7f0 100644 --- a/pylibs/streamer.py +++ b/pylibs/streamer.py @@ -1,7 +1,7 @@ -from .parameter import CN_Parameter +from .parameter import Parameter -class CN_Streamer(): - def __init__(self, name, parameters: list[CN_Parameter]) -> None: +class Streamer(): + def __init__(self, name, parameters: list[Parameter]) -> None: self.name = name self.parameters = parameters diff --git a/pylibs/ustreamer.py b/pylibs/ustreamer.py index 40dc7cb3..fbb42d1e 100644 --- a/pylibs/ustreamer.py +++ b/pylibs/ustreamer.py @@ -1,28 +1,40 @@ -from .cam import CN_Cam -from .parameter import CN_Parameter +from .cam import Cam +from .parameter import Parameter from configparser import SectionProxy -class CN_Ustreamer(CN_Cam): +class Ustreamer(Cam): keyword = 'ustreamer' def __init__(self, name: str = '') -> None: super().__init__(name) + self.parameters.update({ + 'port': None, + 'device': None, + 'resolution': None, + 'max_fps': None, + 'custom_flags': '', + 'v4l2ctl': '', + 'no_proxy': False + }) + self.possible_parameters += [ - CN_Parameter('port', int), - CN_Parameter('device', str), - CN_Parameter('resolution', bool), - CN_Parameter('max_fps', int), - CN_Parameter('custom_flags', str, ''), - CN_Parameter('v4l2ctl', dict, {}), - CN_Parameter('no_proxy', bool, False) + Parameter('port', int), + Parameter('device', str), + Parameter('resolution', str), + Parameter('max_fps', int), + Parameter('custom_flags', str, ''), + Parameter('v4l2ctl', dict, {}), + Parameter('no_proxy', bool, False) ] + def parse_config(self, section: SectionProxy): + pass def execute(self): pass def load_module(): - return CN_Ustreamer + return Ustreamer From 40871f623742d5b6ddd762a0d8a4e7f66e2b5bca Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz <58853838+mryel00@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:49:02 +0100 Subject: [PATCH 007/129] chore: wip --- pylibs/ustreamer.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pylibs/ustreamer.py b/pylibs/ustreamer.py index fbb42d1e..f9c04878 100644 --- a/pylibs/ustreamer.py +++ b/pylibs/ustreamer.py @@ -28,10 +28,14 @@ def __init__(self, name: str = '') -> None: Parameter('no_proxy', bool, False) ] - def parse_config(self, section: SectionProxy): - - pass + for parameter, value in section: + if parameter not in self.parameters: + print(f"Warning: Parameter [{parameter}] is not supported by [{self.keyword}]") + continue + value = value.split('#')[0].strip() + self.parameters[parameter] = value + def execute(self): pass From 4f90585dbf6799b1e0e9a930bf5c306f390c481b Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 23 Nov 2023 15:59:41 +0100 Subject: [PATCH 008/129] chore: wip --- crowsnest.py | 48 +++++++++++++++++++++------------------- pylibs/cam.py | 11 +-------- pylibs/section.py | 9 ++++++-- pylibs/streamer.py | 9 -------- pylibs/ustreamer.py | 44 ------------------------------------ resources/crowsnest.conf | 12 ++++++++++ 6 files changed, 45 insertions(+), 88 deletions(-) delete mode 100644 pylibs/streamer.py delete mode 100644 pylibs/ustreamer.py diff --git a/crowsnest.py b/crowsnest.py index 9a9d90a3..27c4fd56 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,19 +1,25 @@ +import argparse import configparser import importlib -from pylibs.cam import Cam +from pylibs.crowsnest import Crowsnest from pylibs.section import Section -from pylibs import * +from pylibs.core import load_module -config_path = "resources/crowsnest.conf" +parser = argparse.ArgumentParser( + prog='Crowsnest', + description='Crowsnest - A webcam daemon for Raspberry Pi OS distributions like MainsailOS' +) + +parser.add_argument('-c', '--config', help='Path to config file', required=True) + +args = parser.parse_args() + + +config_path = args.config config = configparser.ConfigParser() config.read(config_path) -# Crowsnest config settings -log_path = '/home/pi/printer_data/logs/crowsnest.log' -log_level = 'debug' -delete_log = False - # Example of printing section and values for section in config.sections(): print("Section: " + section) @@ -22,29 +28,25 @@ print(config) sections = [] -# Use if else or dict as match case isn't available before v3.10 + +crowsnest = Crowsnest(config['crowsnest']) + +print(crowsnest) + for section in config.sections(): section_header = section.split(' ') section_object = None section_keyword = section_header[0] - try: - module = importlib.import_module(f'pylibs.{section_keyword}') - module_class = getattr(module, 'load_module')() - Section.available_sections[section_keyword] = module_class - module_class().parse_config(config[section]) - except (ModuleNotFoundError, AttributeError) as e: - print(str(e)) + if section_keyword == 'crowsnest': continue - if section_header[0] == 'crowsnest': - section_object = 1 - elif section_header[0] == 'cam': - section_object = Cam(' '.join(section_header[1:])) - section_object.parse_config(config[section]) - + section_object = load_module('pylibs', section_keyword) + section_object.parse_config(config[section]) + + if section_object == None: - raise Exception(f"Section [{section}] couldn't get parsed") + print(f"Section [{section}] couldn't get parsed") sections.append(section_object) k = Section('k') diff --git a/pylibs/cam.py b/pylibs/cam.py index c26dc55d..b6e4c42c 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -1,5 +1,4 @@ from .section import Section -from .parameter import Parameter from configparser import SectionProxy import importlib @@ -15,20 +14,12 @@ def __init__(self, name: str = '') -> None: 'mode': None }) - self.possible_parameters += [ - Parameter('mode', str) - ] - def parse_config(self, section: SectionProxy): # Dynamically import module mode = section["mode"].split()[0] try: - module = importlib.import_module(f'pylibs.{mode}') + module = importlib.import_module(f'pylibs.streamer.{mode}') module_class = getattr(module, 'load_module')() - Cam.loaded_modes[mode] = module_class - print(Cam.loaded_modes) - t = Cam.loaded_modes[mode]('test') - print(t, t.keyword, t.name) return module_class(self.name).parse_config(section) except (ModuleNotFoundError, AttributeError) as e: print(str(e)) diff --git a/pylibs/section.py b/pylibs/section.py index 734c162a..2fa69a84 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -9,12 +9,17 @@ class Section: # param2 def __init__(self, name: str = '') -> None: self.name = name - self.possible_parameters = [] self.parameters = {} # Parse config according to the needs of the section def parse_config(self, section: SectionProxy): - raise NotImplementedError("If you see this, a module is implemented wrong!!!") + for parameter in section: + value = section[parameter] + if parameter not in self.parameters: + print(f"Warning: Parameter {parameter} is not supported by {self.keyword}") + continue + value = value.split('#')[0].strip() + self.parameters[parameter] = value # Execute section specific stuff, e.g. starting cam def execute(self): diff --git a/pylibs/streamer.py b/pylibs/streamer.py deleted file mode 100644 index a65cb7f0..00000000 --- a/pylibs/streamer.py +++ /dev/null @@ -1,9 +0,0 @@ -from .parameter import Parameter - -class Streamer(): - def __init__(self, name, parameters: list[Parameter]) -> None: - self.name = name - self.parameters = parameters - - def config_check(): - pass diff --git a/pylibs/ustreamer.py b/pylibs/ustreamer.py deleted file mode 100644 index f9c04878..00000000 --- a/pylibs/ustreamer.py +++ /dev/null @@ -1,44 +0,0 @@ -from .cam import Cam -from .parameter import Parameter -from configparser import SectionProxy - -class Ustreamer(Cam): - keyword = 'ustreamer' - - def __init__(self, name: str = '') -> None: - super().__init__(name) - - self.parameters.update({ - 'port': None, - 'device': None, - 'resolution': None, - 'max_fps': None, - 'custom_flags': '', - 'v4l2ctl': '', - 'no_proxy': False - }) - - self.possible_parameters += [ - Parameter('port', int), - Parameter('device', str), - Parameter('resolution', str), - Parameter('max_fps', int), - Parameter('custom_flags', str, ''), - Parameter('v4l2ctl', dict, {}), - Parameter('no_proxy', bool, False) - ] - - def parse_config(self, section: SectionProxy): - for parameter, value in section: - if parameter not in self.parameters: - print(f"Warning: Parameter [{parameter}] is not supported by [{self.keyword}]") - continue - value = value.split('#')[0].strip() - self.parameters[parameter] = value - - - def execute(self): - pass - -def load_module(): - return Ustreamer diff --git a/resources/crowsnest.conf b/resources/crowsnest.conf index 5343bc73..a991710d 100644 --- a/resources/crowsnest.conf +++ b/resources/crowsnest.conf @@ -40,3 +40,15 @@ resolution: 640x480 # widthxheight format max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. custom_flags: # You can run the Stream Services with custom flags. #v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see Log what your cam is capable of. + +[cam 2] +mode: ustreamer # ustreamer - Provides mjpg and snapshots. (All devices) + # camera-streamer - Provides webrtc, mjpg and snapshots. (rpi + Raspi OS based only) +enable_rtsp: false # If camera-streamer is used, this enables also usage of an rtsp server +rtsp_port: 8554 # Set different ports for each device! +port: 8080 # HTTP/MJPG Stream/Snapshot Port +device: /dev/video0 # See Log for available ... +resolution: 640x480 # widthxheight format +max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. +custom_flags: # You can run the Stream Services with custom flags. +#v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see Log what your cam is capable of. From 2326c7d6fc81fe2851d2db85668fb8e8489e3e08 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 24 Nov 2023 11:42:41 +0100 Subject: [PATCH 009/129] chore: wip --- pylibs/cam.py | 4 +++- pylibs/parameter.py | 19 +++++++++++++------ pylibs/section.py | 7 +++++-- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pylibs/cam.py b/pylibs/cam.py index b6e4c42c..f855694c 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -1,5 +1,6 @@ from .section import Section from configparser import SectionProxy +from .parameter import Parameter import importlib @@ -11,12 +12,13 @@ def __init__(self, name: str = '') -> None: super().__init__(name) self.parameters.update({ - 'mode': None + 'mode': Parameter() }) def parse_config(self, section: SectionProxy): # Dynamically import module mode = section["mode"].split()[0] + module_class try: module = importlib.import_module(f'pylibs.streamer.{mode}') module_class = getattr(module, 'load_module')() diff --git a/pylibs/parameter.py b/pylibs/parameter.py index ac1b489e..df83b727 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -1,8 +1,15 @@ class Parameter: - def __init__(self, name, type, default_value=None) -> None: - self.name = name - self.default_value = default_value + def __init__(self, type=str, default=None) -> None: self.type = type - - # Parameter is required if no default value is specified - self.required = self.default_value == None + self.set_value(default) + + def set_value(self, value): + if value is None: + self.value = None + elif self.type == 'bool': + if value.lower() == 'true': + self.value = True + elif value.lower() == 'false': + self.value = False + else: + self.value = self.type(value) diff --git a/pylibs/section.py b/pylibs/section.py index 2fa69a84..40b8abdd 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -1,5 +1,8 @@ +import re from configparser import SectionProxy +from .parameter import Parameter + class Section: keyword = 'Section' available_sections = {} @@ -9,7 +12,7 @@ class Section: # param2 def __init__(self, name: str = '') -> None: self.name = name - self.parameters = {} + self.parameters: dict[str, Parameter] = {} # Parse config according to the needs of the section def parse_config(self, section: SectionProxy): @@ -19,7 +22,7 @@ def parse_config(self, section: SectionProxy): print(f"Warning: Parameter {parameter} is not supported by {self.keyword}") continue value = value.split('#')[0].strip() - self.parameters[parameter] = value + self.parameters[parameter].set_value(value) # Execute section specific stuff, e.g. starting cam def execute(self): From 9978513bacde42dcbc9ae1819b27f53076d28a4a Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 24 Nov 2023 11:43:24 +0100 Subject: [PATCH 010/129] chore: wip --- pylibs/core.py | 13 +++++++++++++ pylibs/crowsnest.py | 13 +++++++++++++ pylibs/streamer/__init__.py | 0 pylibs/streamer/streamer.py | 24 ++++++++++++++++++++++++ pylibs/streamer/ustreamer.py | 18 ++++++++++++++++++ 5 files changed, 68 insertions(+) create mode 100644 pylibs/core.py create mode 100644 pylibs/crowsnest.py create mode 100644 pylibs/streamer/__init__.py create mode 100644 pylibs/streamer/streamer.py create mode 100644 pylibs/streamer/ustreamer.py diff --git a/pylibs/core.py b/pylibs/core.py new file mode 100644 index 00000000..4e4d8383 --- /dev/null +++ b/pylibs/core.py @@ -0,0 +1,13 @@ +import importlib + +# Dynamically import module +# Requires module to have a load_module() function, +# as well as the same name as the section keyword +def load_module(path = '', module_name = ''): + module_class = None + try: + module = importlib.import_module(f'{path}.{module_name}') + module_class = getattr(module, 'load_module')() + except (ModuleNotFoundError, AttributeError) as e: + print('ERROR: '+str(e)) + return module_class diff --git a/pylibs/crowsnest.py b/pylibs/crowsnest.py new file mode 100644 index 00000000..5778fa0c --- /dev/null +++ b/pylibs/crowsnest.py @@ -0,0 +1,13 @@ +from .section import Section +from .parameter import Parameter + +class Crowsnest(Section): + def __init__(self, name: str = '') -> None: + super().__init__(name) + + self.parameters.update({ + 'log_path': Parameter(), + 'log_level': Parameter(str, 'verbose'), + 'delete_log': Parameter(bool, True), + 'no_proxy': Parameter(bool, False) + }) diff --git a/pylibs/streamer/__init__.py b/pylibs/streamer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py new file mode 100644 index 00000000..d013046a --- /dev/null +++ b/pylibs/streamer/streamer.py @@ -0,0 +1,24 @@ +from ..cam import Cam +from ..parameter import Parameter +from configparser import SectionProxy + +class Streamer(Cam): + keyword = '' + + def __init__(self, name: str = '') -> None: + super().__init__(name) + + self.parameters.update({ + 'port': Parameter(int), + 'device': Parameter(), + 'resolution': Parameter(), + 'max_fps': Parameter(int), + 'custom_flags': Parameter(str, ''), + 'v4l2ctl': Parameter(str, '') + }) + + def parse_config(self, section: SectionProxy): + return super().super().parse_config(section) + +def load_module(): + raise NotImplementedError("If you see this, a module is implemented wrong!!!") \ No newline at end of file diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py new file mode 100644 index 00000000..ae528720 --- /dev/null +++ b/pylibs/streamer/ustreamer.py @@ -0,0 +1,18 @@ +from .streamer import Streamer +from ..parameter import Parameter + +class Ustreamer(Streamer): + keyword = 'ustreamer' + + def __init__(self, name: str = '') -> None: + super().__init__(name) + + self.parameters.update({ + 'no_proxy': Parameter(bool, False) + }) + + def execute(self): + pass + +def load_module(): + return Ustreamer From bb91c7d94c18867f623a8ff7c1e9b65eb102746e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 25 Nov 2023 15:19:10 +0100 Subject: [PATCH 011/129] chore: wip --- crowsnest.py | 29 +++++++++++++++++++++++------ pylibs/cam.py | 30 +++++++++++++++--------------- pylibs/core.py | 2 +- pylibs/crowsnest.py | 12 ++++++++++++ pylibs/parameter.py | 21 ++++++++++++--------- pylibs/section.py | 6 +++--- pylibs/streamer/streamer.py | 10 ++++------ pylibs/streamer/ustreamer.py | 26 +++++++++++++++++++++++++- resources/crowsnest.conf | 8 ++++---- 9 files changed, 99 insertions(+), 45 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 27c4fd56..ff1709ff 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,9 +1,10 @@ import argparse import configparser -import importlib from pylibs.crowsnest import Crowsnest from pylibs.section import Section -from pylibs.core import load_module +from pylibs.core import get_module_class + +import logging parser = argparse.ArgumentParser( prog='Crowsnest', @@ -29,9 +30,23 @@ sections = [] -crowsnest = Crowsnest(config['crowsnest']) +crowsnest = Crowsnest('crowsnest') +crowsnest.parse_config(config['crowsnest']) + +logging.basicConfig( + filename=crowsnest.parameters['log_path'].value, + encoding='utf-8', + level=crowsnest.parameters['log_level'].value, + format='[%(asctime)s] %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) -print(crowsnest) +logging.debug('This message should go to the log file') +logging.info('So should this') +logging.warning('And this, too') +logging.error('And non-ASCII stuff, too, like Øresund and Malmö') + +print(crowsnest.name) for section in config.sections(): section_header = section.split(' ') @@ -41,9 +56,11 @@ if section_keyword == 'crowsnest': continue - section_object = load_module('pylibs', section_keyword) + section_class = get_module_class('pylibs', section_keyword) + section_name = ' '.join(section_header[1:]) + section_object = section_class(section_name) section_object.parse_config(config[section]) - + section_object.execute() if section_object == None: print(f"Section [{section}] couldn't get parsed") diff --git a/pylibs/cam.py b/pylibs/cam.py index f855694c..be1720b3 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -1,12 +1,12 @@ -from .section import Section from configparser import SectionProxy +from .section import Section from .parameter import Parameter +from .core import get_module_class -import importlib +import copy class Cam(Section): keyword = 'cam' - loaded_modes = {} def __init__(self, name: str = '') -> None: super().__init__(name) @@ -15,20 +15,20 @@ def __init__(self, name: str = '') -> None: 'mode': Parameter() }) - def parse_config(self, section: SectionProxy): + self.streamer = None + + def parse_config(self, config_section: SectionProxy, *args, **kwargs): # Dynamically import module - mode = section["mode"].split()[0] - module_class - try: - module = importlib.import_module(f'pylibs.streamer.{mode}') - module_class = getattr(module, 'load_module')() - return module_class(self.name).parse_config(section) - except (ModuleNotFoundError, AttributeError) as e: - print(str(e)) + mode = config_section["mode"].split()[0] + mode_class = get_module_class('pylibs.streamer', mode) + self.streamer = mode_class(self.name) + self.streamer.parse_config(config_section) + + def execute(self): + if self.streamer is None: + print("No streamer loaded") return - - def execute(): - pass + self.streamer.execute() def load_module(): return Cam diff --git a/pylibs/core.py b/pylibs/core.py index 4e4d8383..811013b4 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -3,7 +3,7 @@ # Dynamically import module # Requires module to have a load_module() function, # as well as the same name as the section keyword -def load_module(path = '', module_name = ''): +def get_module_class(path = '', module_name = ''): module_class = None try: module = importlib.import_module(f'{path}.{module_name}') diff --git a/pylibs/crowsnest.py b/pylibs/crowsnest.py index 5778fa0c..7d00dc90 100644 --- a/pylibs/crowsnest.py +++ b/pylibs/crowsnest.py @@ -1,6 +1,8 @@ from .section import Section from .parameter import Parameter +from configparser import SectionProxy + class Crowsnest(Section): def __init__(self, name: str = '') -> None: super().__init__(name) @@ -11,3 +13,13 @@ def __init__(self, name: str = '') -> None: 'delete_log': Parameter(bool, True), 'no_proxy': Parameter(bool, False) }) + + def parse_config(self, section: SectionProxy): + super().parse_config(section) + log_level = self.parameters['log_level'].value.lower() + if log_level == 'quiet': + self.parameters['log_level'].value = 'WARNING' + elif log_level == 'debug': + self.parameters['log_level'].value = 'DEBUG' + else: + self.parameters['log_level'].value = 'INFO' diff --git a/pylibs/parameter.py b/pylibs/parameter.py index df83b727..2414fc6c 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -4,12 +4,15 @@ def __init__(self, type=str, default=None) -> None: self.set_value(default) def set_value(self, value): - if value is None: - self.value = None - elif self.type == 'bool': - if value.lower() == 'true': - self.value = True - elif value.lower() == 'false': - self.value = False - else: - self.value = self.type(value) + try: + if value is None: + self.value = None + elif self.type == 'bool': + if value.lower() == 'true': + self.value = True + elif value.lower() == 'false': + self.value = False + else: + self.value = self.type(value) + except ValueError: + print(f"Error: {value} is not of type {self.type}") diff --git a/pylibs/section.py b/pylibs/section.py index 40b8abdd..2d2385cd 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -15,9 +15,9 @@ def __init__(self, name: str = '') -> None: self.parameters: dict[str, Parameter] = {} # Parse config according to the needs of the section - def parse_config(self, section: SectionProxy): - for parameter in section: - value = section[parameter] + def parse_config(self, config_section: SectionProxy, *args, **kwargs): + for parameter in config_section: + value = config_section[parameter] if parameter not in self.parameters: print(f"Warning: Parameter {parameter} is not supported by {self.keyword}") continue diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py index d013046a..b5ef5f9e 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/streamer/streamer.py @@ -1,24 +1,22 @@ -from ..cam import Cam +from ..section import Section from ..parameter import Parameter from configparser import SectionProxy -class Streamer(Cam): +class Streamer(Section): keyword = '' def __init__(self, name: str = '') -> None: super().__init__(name) self.parameters.update({ + 'mode': Parameter(str), 'port': Parameter(int), 'device': Parameter(), - 'resolution': Parameter(), + 'resolution': Parameter(), 'max_fps': Parameter(int), 'custom_flags': Parameter(str, ''), 'v4l2ctl': Parameter(str, '') }) - def parse_config(self, section: SectionProxy): - return super().super().parse_config(section) - def load_module(): raise NotImplementedError("If you see this, a module is implemented wrong!!!") \ No newline at end of file diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index ae528720..a37e9926 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,5 +1,6 @@ from .streamer import Streamer from ..parameter import Parameter +import subprocess class Ustreamer(Streamer): keyword = 'ustreamer' @@ -12,7 +13,30 @@ def __init__(self, name: str = '') -> None: }) def execute(self): - pass + host = '0.0.0.0' if self.parameters['no_proxy'].value else '127.0.0.1' + port = self.parameters['port'].value + res = self.parameters['resolution'].value + fps = self.parameters['max_fps'].value + + + + streamer_args = [ + '--host', host, + '--port', port, + '--resolution', res, + '--desired-fps', fps, + # webroot & allow crossdomain requests + '--allow-origin=\*', + '--static', '"ustreamer-www"', + '--device', '/dev/video0', + '--format', 'MJPEG', + '--encoder', 'HW' + ] + + # custom flags + streamer_args += self.parameters['custom_flags'].value.split() + + ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) def load_module(): return Ustreamer diff --git a/resources/crowsnest.conf b/resources/crowsnest.conf index a991710d..557b795c 100644 --- a/resources/crowsnest.conf +++ b/resources/crowsnest.conf @@ -24,8 +24,8 @@ [crowsnest] -log_path: LOGPATH -log_level: verbose # Valid Options are quiet/verbose/debug +log_path: resources/crowsnest.log # Path to log file +log_level: debug # Valid Options are quiet/verbose/debug delete_log: false # Deletes log on every restart, if set to true no_proxy: false @@ -33,8 +33,8 @@ no_proxy: false mode: ustreamer # ustreamer - Provides mjpg and snapshots. (All devices) # camera-streamer - Provides webrtc, mjpg and snapshots. (rpi + Raspi OS based only) enable_rtsp: false # If camera-streamer is used, this enables also usage of an rtsp server -rtsp_port: 8554 # Set different ports for each device! -port: 8080 # HTTP/MJPG Stream/Snapshot Port +rtsp_port: 8554a # Set different ports for each device! +port: 8080a # HTTP/MJPG Stream/Snapshot Port device: /dev/video0 # See Log for available ... resolution: 640x480 # widthxheight format max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. From bb1e9fc92e41f529bf26dbdcef8d14ffd1867307 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 25 Nov 2023 16:42:55 +0100 Subject: [PATCH 012/129] chore: wip --- pylibs/cam.py | 2 +- pylibs/core.py | 28 ++++++++++++++++++++++++++++ pylibs/section.py | 2 +- pylibs/streamer/ustreamer.py | 12 ++++++------ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/pylibs/cam.py b/pylibs/cam.py index be1720b3..38795185 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -24,7 +24,7 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs): self.streamer = mode_class(self.name) self.streamer.parse_config(config_section) - def execute(self): + async def execute(self): if self.streamer is None: print("No streamer loaded") return diff --git a/pylibs/core.py b/pylibs/core.py index 811013b4..7fb61fbd 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -1,4 +1,6 @@ import importlib +import asyncio +import logging # Dynamically import module # Requires module to have a load_module() function, @@ -11,3 +13,29 @@ def get_module_class(path = '', module_name = ''): except (ModuleNotFoundError, AttributeError) as e: print('ERROR: '+str(e)) return module_class + +async def log_subprocess_output(stream, log_func): + while True: + line = await stream.readline() + if not line: + break + #line = line.decode('utf-8').strip() + log_func(line.decode().strip()) + +async def execute_command(command: str, logger: logging.Logger): + process = await asyncio.create_subprocess_exec( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + stdout_task = asyncio.create_task(log_subprocess_output(process.stdout, logger.info)) + stderr_task = asyncio.create_task(log_subprocess_output(process.stderr, logger.error)) + + return process, stdout_task, stderr_task + # Wait for the subprocess to finish + #await process.wait() + + # Wait for the output handling tasks to finish + #await stdout_task + #await stderr_task \ No newline at end of file diff --git a/pylibs/section.py b/pylibs/section.py index 2d2385cd..85802cb3 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -25,5 +25,5 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs): self.parameters[parameter].set_value(value) # Execute section specific stuff, e.g. starting cam - def execute(self): + async def execute(self): raise NotImplementedError("If you see this, a module is implemented wrong!!!") diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index a37e9926..7b3524eb 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,6 +1,7 @@ from .streamer import Streamer from ..parameter import Parameter import subprocess +import time class Ustreamer(Streamer): keyword = 'ustreamer' @@ -12,19 +13,17 @@ def __init__(self, name: str = '') -> None: 'no_proxy': Parameter(bool, False) }) - def execute(self): + async def execute(self): host = '0.0.0.0' if self.parameters['no_proxy'].value else '127.0.0.1' port = self.parameters['port'].value res = self.parameters['resolution'].value fps = self.parameters['max_fps'].value - - streamer_args = [ '--host', host, - '--port', port, + '--port', str(port), '--resolution', res, - '--desired-fps', fps, + '--desired-fps', str(fps), # webroot & allow crossdomain requests '--allow-origin=\*', '--static', '"ustreamer-www"', @@ -36,7 +35,8 @@ def execute(self): # custom flags streamer_args += self.parameters['custom_flags'].value.split() - ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + def load_module(): return Ustreamer From f83ae467b68cee35c2ad17812dca58fb5ae021a8 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 19:16:59 +0100 Subject: [PATCH 013/129] chore: wip --- pylibs/cam.py | 2 +- pylibs/streamer/ustreamer.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pylibs/cam.py b/pylibs/cam.py index 38795185..0ce0c9d7 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -28,7 +28,7 @@ async def execute(self): if self.streamer is None: print("No streamer loaded") return - self.streamer.execute() + await self.streamer.execute() def load_module(): return Cam diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 7b3524eb..79dd7661 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,7 +1,6 @@ from .streamer import Streamer from ..parameter import Parameter -import subprocess -import time +from ..core import execute_command class Ustreamer(Streamer): keyword = 'ustreamer' @@ -35,7 +34,9 @@ async def execute(self): # custom flags streamer_args += self.parameters['custom_flags'].value.split() - ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd = ['bin/ustreamer/ustreamer'] + streamer_args + await execute_command(cmd, self.logger) + #ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) def load_module(): From 7538d305d21b348e4640f0fbc7d80bf0257125d5 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 19:19:43 +0100 Subject: [PATCH 014/129] chore: wip --- crowsnest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crowsnest.py b/crowsnest.py index ff1709ff..99540545 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -5,6 +5,7 @@ from pylibs.core import get_module_class import logging +import asyncio parser = argparse.ArgumentParser( prog='Crowsnest', @@ -60,7 +61,7 @@ section_name = ' '.join(section_header[1:]) section_object = section_class(section_name) section_object.parse_config(config[section]) - section_object.execute() + asyncio.run(section_object.execute()) if section_object == None: print(f"Section [{section}] couldn't get parsed") From 78ae5a9f97e58250e89bfd3463e2ea5cddb9feb2 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 19:24:08 +0100 Subject: [PATCH 015/129] chore: wip --- pylibs/core.py | 6 +++--- pylibs/streamer/ustreamer.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pylibs/core.py b/pylibs/core.py index 7fb61fbd..2c732d7b 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -22,15 +22,15 @@ async def log_subprocess_output(stream, log_func): #line = line.decode('utf-8').strip() log_func(line.decode().strip()) -async def execute_command(command: str, logger: logging.Logger): +async def execute_command(command: str): process = await asyncio.create_subprocess_exec( command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) - stdout_task = asyncio.create_task(log_subprocess_output(process.stdout, logger.info)) - stderr_task = asyncio.create_task(log_subprocess_output(process.stderr, logger.error)) + stdout_task = asyncio.create_task(log_subprocess_output(process.stdout, logging.info)) + stderr_task = asyncio.create_task(log_subprocess_output(process.stderr, logging.error)) return process, stdout_task, stderr_task # Wait for the subprocess to finish diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 79dd7661..4c26fe46 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -35,7 +35,7 @@ async def execute(self): streamer_args += self.parameters['custom_flags'].value.split() cmd = ['bin/ustreamer/ustreamer'] + streamer_args - await execute_command(cmd, self.logger) + await execute_command(cmd) #ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) From 7553dc915a2fbbb2c559b29e21ad41c30b15fa7e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 19:32:03 +0100 Subject: [PATCH 016/129] chore: wip --- pylibs/core.py | 2 +- pylibs/streamer/ustreamer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pylibs/core.py b/pylibs/core.py index 2c732d7b..25d6533b 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -23,7 +23,7 @@ async def log_subprocess_output(stream, log_func): log_func(line.decode().strip()) async def execute_command(command: str): - process = await asyncio.create_subprocess_exec( + process = await asyncio.create_subprocess_shell( command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 4c26fe46..93104f5b 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -35,7 +35,7 @@ async def execute(self): streamer_args += self.parameters['custom_flags'].value.split() cmd = ['bin/ustreamer/ustreamer'] + streamer_args - await execute_command(cmd) + await execute_command(' '.join(cmd)) #ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) From a61eecf2834783eafc18bb51e450486b0d79a6a6 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 19:33:23 +0100 Subject: [PATCH 017/129] chore: wip --- crowsnest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 99540545..bae913b9 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -42,10 +42,10 @@ datefmt='%Y-%m-%d %H:%M:%S' ) -logging.debug('This message should go to the log file') -logging.info('So should this') -logging.warning('And this, too') -logging.error('And non-ASCII stuff, too, like Øresund and Malmö') +#logging.debug('This message should go to the log file') +#logging.info('So should this') +#logging.warning('And this, too') +#logging.error('And non-ASCII stuff, too, like Øresund and Malmö') print(crowsnest.name) From 3b4196dadc4e11080cb756419e4b6bb386fa4e91 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 19:41:28 +0100 Subject: [PATCH 018/129] chore: wip --- crowsnest.py | 3 ++- pylibs/core.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index bae913b9..14dbdc2a 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -61,7 +61,8 @@ section_name = ' '.join(section_header[1:]) section_object = section_class(section_name) section_object.parse_config(config[section]) - asyncio.run(section_object.execute()) + t = asyncio.run(section_object.execute()) + t.wait() if section_object == None: print(f"Section [{section}] couldn't get parsed") diff --git a/pylibs/core.py b/pylibs/core.py index 25d6533b..fa2860a6 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -1,6 +1,7 @@ import importlib import asyncio import logging +import time # Dynamically import module # Requires module to have a load_module() function, @@ -18,7 +19,8 @@ async def log_subprocess_output(stream, log_func): while True: line = await stream.readline() if not line: - break + time.sleep(0.05) + continue #line = line.decode('utf-8').strip() log_func(line.decode().strip()) From 1990e7aa7ac39ba0ca646bde7da7c946680378fb Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 19:42:12 +0100 Subject: [PATCH 019/129] chore: wip --- resources/crowsnest.conf | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/resources/crowsnest.conf b/resources/crowsnest.conf index 557b795c..3e45e56e 100644 --- a/resources/crowsnest.conf +++ b/resources/crowsnest.conf @@ -34,21 +34,21 @@ mode: ustreamer # ustreamer - Provides mjpg and snapshot # camera-streamer - Provides webrtc, mjpg and snapshots. (rpi + Raspi OS based only) enable_rtsp: false # If camera-streamer is used, this enables also usage of an rtsp server rtsp_port: 8554a # Set different ports for each device! -port: 8080a # HTTP/MJPG Stream/Snapshot Port +port: 8080 # HTTP/MJPG Stream/Snapshot Port device: /dev/video0 # See Log for available ... resolution: 640x480 # widthxheight format max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. custom_flags: # You can run the Stream Services with custom flags. #v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see Log what your cam is capable of. -[cam 2] -mode: ustreamer # ustreamer - Provides mjpg and snapshots. (All devices) - # camera-streamer - Provides webrtc, mjpg and snapshots. (rpi + Raspi OS based only) -enable_rtsp: false # If camera-streamer is used, this enables also usage of an rtsp server -rtsp_port: 8554 # Set different ports for each device! -port: 8080 # HTTP/MJPG Stream/Snapshot Port -device: /dev/video0 # See Log for available ... -resolution: 640x480 # widthxheight format -max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. -custom_flags: # You can run the Stream Services with custom flags. +#[cam 2] +#mode: ustreamer # ustreamer - Provides mjpg and snapshots. (All devices) +# # camera-streamer - Provides webrtc, mjpg and snapshots. (rpi + Raspi OS based only) +#enable_rtsp: false # If camera-streamer is used, this enables also usage of an rtsp server +#rtsp_port: 8554 # Set different ports for each device! +#port: 8080 # HTTP/MJPG Stream/Snapshot Port +#device: /dev/video0 # See Log for available ... +#resolution: 640x480 # widthxheight format +#max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. +#custom_flags: # You can run the Stream Services with custom flags. #v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see Log what your cam is capable of. From cc8b25b987e8f4cf0181832862f2def27154a293 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 19:57:29 +0100 Subject: [PATCH 020/129] chore: wip --- crowsnest.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 14dbdc2a..725a14b0 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -48,25 +48,29 @@ #logging.error('And non-ASCII stuff, too, like Øresund and Malmö') print(crowsnest.name) - -for section in config.sections(): - section_header = section.split(' ') - section_object = None - section_keyword = section_header[0] - - if section_keyword == 'crowsnest': - continue - - section_class = get_module_class('pylibs', section_keyword) - section_name = ' '.join(section_header[1:]) - section_object = section_class(section_name) - section_object.parse_config(config[section]) - t = asyncio.run(section_object.execute()) - t.wait() - - if section_object == None: - print(f"Section [{section}] couldn't get parsed") - sections.append(section_object) +processes = [] +try: + for section in config.sections(): + section_header = section.split(' ') + section_object = None + section_keyword = section_header[0] + + if section_keyword == 'crowsnest': + continue + + section_class = get_module_class('pylibs', section_keyword) + section_name = ' '.join(section_header[1:]) + section_object = section_class(section_name) + section_object.parse_config(config[section]) + t = asyncio.run(section_object.execute()) + t.wait() + + if section_object == None: + print(f"Section [{section}] couldn't get parsed") + sections.append(section_object) +finally: + for process in processes: + process.terminate() k = Section('k') k1 = Section('k1') From b57cc99cc530a26b0971180aec82a7dfdd3a35ac Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 20:06:20 +0100 Subject: [PATCH 021/129] chore: wip --- crowsnest.py | 49 ++++++++++++++++++++++------------------ resources/crowsnest.conf | 1 + 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 725a14b0..c6bc6a36 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -49,28 +49,33 @@ print(crowsnest.name) processes = [] -try: - for section in config.sections(): - section_header = section.split(' ') - section_object = None - section_keyword = section_header[0] - - if section_keyword == 'crowsnest': - continue - - section_class = get_module_class('pylibs', section_keyword) - section_name = ' '.join(section_header[1:]) - section_object = section_class(section_name) - section_object.parse_config(config[section]) - t = asyncio.run(section_object.execute()) - t.wait() - - if section_object == None: - print(f"Section [{section}] couldn't get parsed") - sections.append(section_object) -finally: - for process in processes: - process.terminate() +async def start_processes(): + try: + for section in config.sections(): + section_header = section.split(' ') + section_object = None + section_keyword = section_header[0] + + if section_keyword == 'crowsnest': + continue + + section_class = get_module_class('pylibs', section_keyword) + section_name = ' '.join(section_header[1:]) + section_object = section_class(section_name) + section_object.parse_config(config[section]) + p = await section_object.execute() + processes.append(p) + + if section_object == None: + print(f"Section [{section}] couldn't get parsed") + sections.append(section_object) + for process in processes: + await process.wait() + finally: + for process in processes: + process.terminate() + +asyncio.run(start_processes()) k = Section('k') k1 = Section('k1') diff --git a/resources/crowsnest.conf b/resources/crowsnest.conf index 3e45e56e..398f567b 100644 --- a/resources/crowsnest.conf +++ b/resources/crowsnest.conf @@ -40,6 +40,7 @@ resolution: 640x480 # widthxheight format max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. custom_flags: # You can run the Stream Services with custom flags. #v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see Log what your cam is capable of. +no_proxy: true #[cam 2] #mode: ustreamer # ustreamer - Provides mjpg and snapshots. (All devices) From 6c4373cd3795b77e15c9217584f1fbdfb244b214 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 26 Nov 2023 21:38:51 +0100 Subject: [PATCH 022/129] chore: whip --- pylibs/cam.py | 14 +++++++++++++- pylibs/streamer/ustreamer.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pylibs/cam.py b/pylibs/cam.py index 0ce0c9d7..556bc0ab 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -28,7 +28,19 @@ async def execute(self): if self.streamer is None: print("No streamer loaded") return - await self.streamer.execute() + process = stdout_task = stderr_task = None + try: + process, stdout_task, stderr_task = await self.streamer.execute() + await process.wait() + await stdout_task.wait() + await stderr_task.wait() + except: + if process != None: + await process.terminate() + if stdout_task != None: + await stdout_task.terminate() + if stderr_task != None: + await stderr_task.terminate() def load_module(): return Cam diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 93104f5b..0a33b80d 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -35,7 +35,7 @@ async def execute(self): streamer_args += self.parameters['custom_flags'].value.split() cmd = ['bin/ustreamer/ustreamer'] + streamer_args - await execute_command(' '.join(cmd)) + return await execute_command(' '.join(cmd)) #ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) From 4c02760152f2db14d44206009751f684aa3b131b Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Tue, 5 Dec 2023 13:50:25 +0100 Subject: [PATCH 023/129] chore: wip --- pylibs/core.py | 2 +- pylibs/messages.py | 52 ++++++++++++++++++++++++++++++++++++ pylibs/streamer/streamer.py | 10 +++++++ pylibs/streamer/ustreamer.py | 3 +++ pylibs/v4l2_control.py | 6 +++++ 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 pylibs/messages.py create mode 100644 pylibs/v4l2_control.py diff --git a/pylibs/core.py b/pylibs/core.py index fa2860a6..47a2178d 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -40,4 +40,4 @@ async def execute_command(command: str): # Wait for the output handling tasks to finish #await stdout_task - #await stderr_task \ No newline at end of file + #await stderr_task diff --git a/pylibs/messages.py b/pylibs/messages.py new file mode 100644 index 00000000..036f16de --- /dev/null +++ b/pylibs/messages.py @@ -0,0 +1,52 @@ +import time + +TITLE="\e[31mcrowsnest\e[0m - A webcam daemon for multiple cams and stream services." + +### All message intendations are intended as they are + +missing_args_msg = """ + echo -e "crowsnest: Missing Arguments!" + echo -e "\n\tTry: crowsnest -h\n" +""" + +wrong_args_msg = """ + echo -e "crowsnest: Wrong Arguments!" + echo -e "\n\tTry: crowsnest -h\n" +""" + +help_msg = """ + echo -e "crowsnest - webcam deamon\nUsage:" + echo -e "\t crowsnest [Options]" + echo -e "\n\t\t-h Prints this help." + echo -e "\n\t\t-v Prints Version of crowsnest." + echo -e "\n\t\t-c \n\t\t\tPath to your webcam.conf\n" +}""" + +deprecated_msg_1 = """{ + log_msg "Parameter 'streamer' is deprecated!" + log_msg "Please use mode: [ mjpg | multi ]" + log_msg "ERROR: Please update your crowsnest.conf! Stopped." +} +""" + +unknown_mode_msg = """{ + log_msg "WARN: Unknown Mode configured!" + log_msg "WARN: Using 'mode: mjpg' as fallback!" +}""" + +## v4l2_control lib +detected_broken_dev_msg = """{ + log_msg "WARN: Detected 'brokenfocus' device." + log_msg "INFO: Trying to set to configured Value." +}""" + +# call debug_focus_val_msg +# ex.: debug_focus_val_msg focus_absolute=30 +debug_focus_val_msg = """{ + log_msg "DEBUG: Value is now: ${1}" +}""" + +## blockyfix +blockyfix_msg_1 = """{ + log_msg "INFO: Blockyfix: Setting video_bitrate_mode to constant." +}""" diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py index b5ef5f9e..8bebf4b2 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/streamer/streamer.py @@ -1,6 +1,8 @@ from ..section import Section from ..parameter import Parameter from configparser import SectionProxy +import os +import logging class Streamer(Section): keyword = '' @@ -17,6 +19,14 @@ def __init__(self, name: str = '') -> None: 'custom_flags': Parameter(str, ''), 'v4l2ctl': Parameter(str, '') }) + self.binary_path = None + + def parse_config(self, config_section: SectionProxy, *args, **kwargs): + super().parse_config(config_section, *args, **kwargs) + if self.binary_path is None: + raise Exception("""This shouldn't happen. Please join our discord and open a post inside the support forum!\nhttps://discord.gg/mainsail""") + if not os.path.exists(self.binary_path): + logging.info(f"'{self.binary_path}' not found! Please make sure that everything is installed correctly!") def load_module(): raise NotImplementedError("If you see this, a module is implemented wrong!!!") \ No newline at end of file diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 0a33b80d..93587af4 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,3 +1,4 @@ +from configparser import SectionProxy from .streamer import Streamer from ..parameter import Parameter from ..core import execute_command @@ -11,6 +12,8 @@ def __init__(self, name: str = '') -> None: self.parameters.update({ 'no_proxy': Parameter(bool, False) }) + + self.binary_path = '../../bin/ustreamer/ustreamer' async def execute(self): host = '0.0.0.0' if self.parameters['no_proxy'].value else '127.0.0.1' diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py new file mode 100644 index 00000000..86c21fae --- /dev/null +++ b/pylibs/v4l2_control.py @@ -0,0 +1,6 @@ +import os +from v4l2py import Device + + + +print(os.popen('v4l2-ctl --list-devices').read()) From 0b97b099e77e1d6dc6a3de2865c79ef199f21271 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 28 Dec 2023 13:15:09 +0100 Subject: [PATCH 024/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/core.py | 29 +++++++++++++++--- pylibs/streamer/camera-streamer.py | 49 ++++++++++++++++++++++++++++++ pylibs/streamer/streamer.py | 1 + pylibs/streamer/ustreamer.py | 12 +++----- 4 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 pylibs/streamer/camera-streamer.py diff --git a/pylibs/core.py b/pylibs/core.py index 47a2178d..208d2f00 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -15,24 +15,43 @@ def get_module_class(path = '', module_name = ''): print('ERROR: '+str(e)) return module_class -async def log_subprocess_output(stream, log_func): +async def log_subprocess_output(stream, log_func, line_prefix = ''): while True: line = await stream.readline() if not line: time.sleep(0.05) continue - #line = line.decode('utf-8').strip() + line = line_prefix + line += line.decode('utf-8').strip() log_func(line.decode().strip()) -async def execute_command(command: str): +async def execute_command( + command: str, + info_log_func = logging.info, + error_log_func = logging.error, + info_log_pre = '', + error_log_pre = ''): + process = await asyncio.create_subprocess_shell( command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) - stdout_task = asyncio.create_task(log_subprocess_output(process.stdout, logging.info)) - stderr_task = asyncio.create_task(log_subprocess_output(process.stderr, logging.error)) + stdout_task = asyncio.create_task( + log_subprocess_output( + process.stdout, + info_log_func, + info_log_pre + ) + ) + stderr_task = asyncio.create_task( + log_subprocess_output( + process.stderr, + error_log_func, + error_log_pre + ) + ) return process, stdout_task, stderr_task # Wait for the subprocess to finish diff --git a/pylibs/streamer/camera-streamer.py b/pylibs/streamer/camera-streamer.py new file mode 100644 index 00000000..d06e2b57 --- /dev/null +++ b/pylibs/streamer/camera-streamer.py @@ -0,0 +1,49 @@ +from configparser import SectionProxy +from .streamer import Streamer +from ..parameter import Parameter +from ..core import execute_command + +class Camera_Streamer(Streamer): + keyword = 'ustreamer' + + def __init__(self, name: str = '') -> None: + super().__init__(name) + + self.parameters.update({ + 'enable_rtsp': Parameter(bool, False), + 'rtsp_port': Parameter(int, 8554) + }) + + self.binary_path = 'bin/ustreamer/ustreamer' + + async def execute(self): + host = '0.0.0.0' if self.parameters['no_proxy'].value else '127.0.0.1' + port = self.parameters['port'].value + res = self.parameters['resolution'].value + fps = self.parameters['max_fps'].value + + streamer_args = [ + self.binary_path, + '--host', host, + '--port', str(port), + '--resolution', res, + '--desired-fps', str(fps), + # webroot & allow crossdomain requests + '--allow-origin=\*', + '--static', '"ustreamer-www"', + '--device', '/dev/video0', + '--format', 'MJPEG', + '--encoder', 'HW' + ] + + # custom flags + streamer_args += self.parameters['custom_flags'].value.split() + + cmd = streamer_args + info_log_pre = f'DEBUG: ustreamer [{self.name}]: ' + return await execute_command(' '.join(cmd), info_log_pre=info_log_pre) + #ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + +def load_module(): + return Camera_Streamer diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py index 8bebf4b2..cb5a5bcd 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/streamer/streamer.py @@ -16,6 +16,7 @@ def __init__(self, name: str = '') -> None: 'device': Parameter(), 'resolution': Parameter(), 'max_fps': Parameter(int), + 'no_proxy': Parameter(bool, False), 'custom_flags': Parameter(str, ''), 'v4l2ctl': Parameter(str, '') }) diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 93587af4..895efdbb 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -9,11 +9,7 @@ class Ustreamer(Streamer): def __init__(self, name: str = '') -> None: super().__init__(name) - self.parameters.update({ - 'no_proxy': Parameter(bool, False) - }) - - self.binary_path = '../../bin/ustreamer/ustreamer' + self.binary_path = 'bin/ustreamer/ustreamer' async def execute(self): host = '0.0.0.0' if self.parameters['no_proxy'].value else '127.0.0.1' @@ -22,6 +18,7 @@ async def execute(self): fps = self.parameters['max_fps'].value streamer_args = [ + self.binary_path, '--host', host, '--port', str(port), '--resolution', res, @@ -37,8 +34,9 @@ async def execute(self): # custom flags streamer_args += self.parameters['custom_flags'].value.split() - cmd = ['bin/ustreamer/ustreamer'] + streamer_args - return await execute_command(' '.join(cmd)) + cmd = streamer_args + info_log_pre = f'DEBUG: ustreamer [{self.name}]: ' + return await execute_command(' '.join(cmd), info_log_pre=info_log_pre) #ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) From 31e6d1de7d7239ea569bd5cffaff45a3abe51002 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Tue, 20 Feb 2024 23:02:43 +0100 Subject: [PATCH 025/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 75 +++++++++++++++++++----------------- pylibs/cam.py | 16 +++----- pylibs/core.py | 16 ++++---- pylibs/crowsnest.py | 2 + pylibs/streamer/streamer.py | 4 ++ pylibs/streamer/ustreamer.py | 17 +++++--- 6 files changed, 72 insertions(+), 58 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index c6bc6a36..e50e6044 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -13,43 +13,49 @@ ) parser.add_argument('-c', '--config', help='Path to config file', required=True) +parser.add_argument('-l', '--log_path', help='Path to log file', required=True) args = parser.parse_args() +def setup_logging(): + logging.basicConfig( + filename=args.log_path, + encoding='utf-8', + level=logging.INFO, + format='[%(asctime)s] %(levelname)s: %(message)s', + datefmt='%d/%m/%y %H:%M:%S' + ) + # Change DEBUG to DEB and add custom DEBUG logging level. + logging.addLevelName(10, 'DEV') + logging.addLevelName(15, 'DEBUG') + +# Read config config_path = args.config config = configparser.ConfigParser() config.read(config_path) # Example of printing section and values -for section in config.sections(): - print("Section: " + section) - for key in config[section]: - print('Key: '+key+'\t\tValue: '+config[section][key].replace(' ', '').split('#')[0]) -print(config) +# for section in config.sections(): +# print("Section: " + section) +# for key in config[section]: +# print('Key: '+key+'\t\tValue: '+config[section][key].replace(' ', '').split('#')[0]) +# print(config) -sections = [] crowsnest = Crowsnest('crowsnest') crowsnest.parse_config(config['crowsnest']) +logging.getLogger().setLevel(crowsnest.parameters['log_level'].value) -logging.basicConfig( - filename=crowsnest.parameters['log_path'].value, - encoding='utf-8', - level=crowsnest.parameters['log_level'].value, - format='[%(asctime)s] %(levelname)s: %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) - -#logging.debug('This message should go to the log file') -#logging.info('So should this') -#logging.warning('And this, too') -#logging.error('And non-ASCII stuff, too, like Øresund and Malmö') +print('Log Level: ' + crowsnest.parameters['log_level'].value) print(crowsnest.name) -processes = [] + async def start_processes(): + sec_objs = [] + sec_exec_tasks = set() + try: for section in config.sections(): section_header = section.split(' ') @@ -63,25 +69,24 @@ async def start_processes(): section_name = ' '.join(section_header[1:]) section_object = section_class(section_name) section_object.parse_config(config[section]) - p = await section_object.execute() - processes.append(p) + task = asyncio.create_task(section_object.execute()) + sec_exec_tasks.add(task) + task.add_done_callback(sec_exec_tasks.discard) if section_object == None: print(f"Section [{section}] couldn't get parsed") - sections.append(section_object) - for process in processes: - await process.wait() + sec_objs.append(section_object) + for task in sec_exec_tasks: + if task != None: + await task + except Exception as e: + print(e) finally: - for process in processes: - process.terminate() + for task in sec_exec_tasks: + if task != None: + task.cancel() -asyncio.run(start_processes()) - -k = Section('k') -k1 = Section('k1') +setup_logging() -k.keyword = 'test' -Section.keyword='test2' - -k2 = Section('k2') -print(Section.keyword, k.keyword) +# Run async as it will wait for all tasks to finish +asyncio.run(start_processes()) diff --git a/pylibs/cam.py b/pylibs/cam.py index 556bc0ab..8b367e54 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -4,6 +4,7 @@ from .core import get_module_class import copy +import logging class Cam(Section): keyword = 'cam' @@ -28,19 +29,12 @@ async def execute(self): if self.streamer is None: print("No streamer loaded") return - process = stdout_task = stderr_task = None try: - process, stdout_task, stderr_task = await self.streamer.execute() + process = await self.streamer.execute() await process.wait() - await stdout_task.wait() - await stderr_task.wait() - except: - if process != None: - await process.terminate() - if stdout_task != None: - await stdout_task.terminate() - if stderr_task != None: - await stderr_task.terminate() + logging.error(f'Start of {self.parameters["mode"].value} [cam {self.name}] failed!') + except Exception as e: + pass def load_module(): return Cam diff --git a/pylibs/core.py b/pylibs/core.py index 208d2f00..54726abc 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -2,6 +2,7 @@ import asyncio import logging import time +import os # Dynamically import module # Requires module to have a load_module() function, @@ -16,14 +17,12 @@ def get_module_class(path = '', module_name = ''): return module_class async def log_subprocess_output(stream, log_func, line_prefix = ''): - while True: + line = await stream.readline() + while line: + l = line_prefix + l += line.decode('utf-8').strip() + log_func(l) line = await stream.readline() - if not line: - time.sleep(0.05) - continue - line = line_prefix - line += line.decode('utf-8').strip() - log_func(line.decode().strip()) async def execute_command( command: str, @@ -60,3 +59,6 @@ async def execute_command( # Wait for the output handling tasks to finish #await stdout_task #await stderr_task + +def log_debug(msg): + logging.log(15, msg) diff --git a/pylibs/crowsnest.py b/pylibs/crowsnest.py index 7d00dc90..084f63e0 100644 --- a/pylibs/crowsnest.py +++ b/pylibs/crowsnest.py @@ -21,5 +21,7 @@ def parse_config(self, section: SectionProxy): self.parameters['log_level'].value = 'WARNING' elif log_level == 'debug': self.parameters['log_level'].value = 'DEBUG' + elif log_level == 'dev': + self.parameters['log_level'].value = 'DEV' else: self.parameters['log_level'].value = 'INFO' diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py index cb5a5bcd..a5e3c299 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/streamer/streamer.py @@ -26,8 +26,12 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs): super().parse_config(config_section, *args, **kwargs) if self.binary_path is None: raise Exception("""This shouldn't happen. Please join our discord and open a post inside the support forum!\nhttps://discord.gg/mainsail""") + + def execute(self): if not os.path.exists(self.binary_path): logging.info(f"'{self.binary_path}' not found! Please make sure that everything is installed correctly!") + return False + return True def load_module(): raise NotImplementedError("If you see this, a module is implemented wrong!!!") \ No newline at end of file diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 895efdbb..1b77aaaf 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,17 +1,22 @@ from configparser import SectionProxy from .streamer import Streamer from ..parameter import Parameter -from ..core import execute_command +from ..core import execute_command, log_debug class Ustreamer(Streamer): keyword = 'ustreamer' + binary_path = None def __init__(self, name: str = '') -> None: super().__init__(name) - self.binary_path = 'bin/ustreamer/ustreamer' + if Ustreamer.binary_path is None: + Ustreamer.binary_path = 'bin/ustreamer/ustreamer' + self.binary_path = Ustreamer.binary_path async def execute(self): + if not super().execute(): + return None host = '0.0.0.0' if self.parameters['no_proxy'].value else '127.0.0.1' port = self.parameters['port'].value res = self.parameters['resolution'].value @@ -35,9 +40,11 @@ async def execute(self): streamer_args += self.parameters['custom_flags'].value.split() cmd = streamer_args - info_log_pre = f'DEBUG: ustreamer [{self.name}]: ' - return await execute_command(' '.join(cmd), info_log_pre=info_log_pre) - #ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + log_pre = f'ustreamer [cam {self.name}]: ' + + process,_,_ = await execute_command(' '.join(cmd), error_log_pre=log_pre, error_log_func=log_debug) + + return process def load_module(): From 2d128db3cc79473231da8f73e5ed56c66a82e540 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 24 Feb 2024 13:45:44 +0100 Subject: [PATCH 026/129] chore: wip --- crowsnest.py | 55 +++++---------- pylibs/core.py | 11 ++- pylibs/crowsnest.py | 2 +- pylibs/logger.py | 129 +++++++++++++++++++++++++++++++++++ pylibs/streamer/ustreamer.py | 9 ++- 5 files changed, 158 insertions(+), 48 deletions(-) create mode 100644 pylibs/logger.py diff --git a/crowsnest.py b/crowsnest.py index e50e6044..60a9530f 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -3,54 +3,28 @@ from pylibs.crowsnest import Crowsnest from pylibs.section import Section from pylibs.core import get_module_class +import pylibs.logger as logger -import logging import asyncio parser = argparse.ArgumentParser( prog='Crowsnest', description='Crowsnest - A webcam daemon for Raspberry Pi OS distributions like MainsailOS' ) +config = configparser.ConfigParser() parser.add_argument('-c', '--config', help='Path to config file', required=True) parser.add_argument('-l', '--log_path', help='Path to log file', required=True) args = parser.parse_args() -def setup_logging(): - logging.basicConfig( - filename=args.log_path, - encoding='utf-8', - level=logging.INFO, - format='[%(asctime)s] %(levelname)s: %(message)s', - datefmt='%d/%m/%y %H:%M:%S' - ) - - # Change DEBUG to DEB and add custom DEBUG logging level. - logging.addLevelName(10, 'DEV') - logging.addLevelName(15, 'DEBUG') - -# Read config -config_path = args.config - -config = configparser.ConfigParser() -config.read(config_path) - -# Example of printing section and values -# for section in config.sections(): -# print("Section: " + section) -# for key in config[section]: -# print('Key: '+key+'\t\tValue: '+config[section][key].replace(' ', '').split('#')[0]) -# print(config) - - -crowsnest = Crowsnest('crowsnest') -crowsnest.parse_config(config['crowsnest']) -logging.getLogger().setLevel(crowsnest.parameters['log_level'].value) - -print('Log Level: ' + crowsnest.parameters['log_level'].value) - -print(crowsnest.name) +def parse_config(): + global crowsnest, config, args + config_path = args.config + config.read(config_path) + crowsnest = Crowsnest('crowsnest') + crowsnest.parse_config(config['crowsnest']) + logger.set_log_level(crowsnest.parameters['log_level'].value) async def start_processes(): sec_objs = [] @@ -77,7 +51,7 @@ async def start_processes(): print(f"Section [{section}] couldn't get parsed") sec_objs.append(section_object) for task in sec_exec_tasks: - if task != None: + if task is not None: await task except Exception as e: print(e) @@ -86,7 +60,10 @@ async def start_processes(): if task != None: task.cancel() -setup_logging() - -# Run async as it will wait for all tasks to finish +logger.setup_logging(args.log_path) +logger.log_initial() +parse_config() +logger.log_host_info() +logger.log_config(args.config) +# Run async to wait for all tasks to finish asyncio.run(start_processes()) diff --git a/pylibs/core.py b/pylibs/core.py index 54726abc..ee6b1db1 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -1,9 +1,11 @@ import importlib import asyncio -import logging import time import os +from .logger import log_debug, log_error +# import logging + # Dynamically import module # Requires module to have a load_module() function, # as well as the same name as the section keyword @@ -26,8 +28,8 @@ async def log_subprocess_output(stream, log_func, line_prefix = ''): async def execute_command( command: str, - info_log_func = logging.info, - error_log_func = logging.error, + info_log_func = log_debug, + error_log_func = log_error, info_log_pre = '', error_log_pre = ''): @@ -59,6 +61,3 @@ async def execute_command( # Wait for the output handling tasks to finish #await stdout_task #await stderr_task - -def log_debug(msg): - logging.log(15, msg) diff --git a/pylibs/crowsnest.py b/pylibs/crowsnest.py index 084f63e0..926a4a6d 100644 --- a/pylibs/crowsnest.py +++ b/pylibs/crowsnest.py @@ -18,7 +18,7 @@ def parse_config(self, section: SectionProxy): super().parse_config(section) log_level = self.parameters['log_level'].value.lower() if log_level == 'quiet': - self.parameters['log_level'].value = 'WARNING' + self.parameters['log_level'].value = 'QUIET' elif log_level == 'debug': self.parameters['log_level'].value = 'DEBUG' elif log_level == 'dev': diff --git a/pylibs/logger.py b/pylibs/logger.py new file mode 100644 index 00000000..c044ea35 --- /dev/null +++ b/pylibs/logger.py @@ -0,0 +1,129 @@ +import logging +import re # log_config +import os, subprocess # log_host_info +import sys # log_cams + +DEV = 10 +DEBUG = 15 +QUIET = 25 + +def setup_logging(log_path): + logging.basicConfig( + filename=log_path, + encoding='utf-8', + level=logging.INFO, + format='[%(asctime)s] %(message)s', + datefmt='%d/%m/%y %H:%M:%S' + ) + + # Change DEBUG to DEB and add custom logging level. + logging.addLevelName(DEV, 'DEV') + logging.addLevelName(DEBUG, 'DEBUG') + logging.addLevelName(QUIET, 'QUIET') + +def set_log_level(level): + logging.getLogger().setLevel(level) + +def log_quiet(msg): + logging.log(QUIET, msg) + +def log_info(msg, prefix='INFO: '): + logging.info(prefix + msg) + +def log_debug(msg, prefix='DEBUG: '): + logging.log(DEBUG, prefix + msg) + +def log_error(msg, prefix='ERROR: '): + logging.error(prefix + msg) + + +def log_initial(): + log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') + command = 'git describe --always --tags' + version = subprocess.check_output(command, shell=True).decode('utf-8').strip() + log_quiet(f'Version: {version}') + log_quiet('Prepare Startup ...') + +def log_config(config_path): + log_info("Print Configfile: '" + config_path + "'") + with open(config_path, 'r') as file: + config_txt = file.read() + # Remove comments + config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) + config_txt = config_txt.strip() + # Split the config file into lines + lines = config_txt.split('\n') + for line in lines: + log_info(5*' ' + line.strip(), '') + +def log_host_info(): + log_info("Host Information:") + log_pre = "Host Info: " + + ### OS Infos + # OS Version + with open('/etc/os-release', 'r') as file: + lines = file.readlines() + for line in lines: + if line.startswith('PRETTY_NAME'): + distribution = line.strip().split('=')[1].strip('"') + log_info(f'Distribution: {distribution}', log_pre) + + # Release Version of MainsailOS (if file present) + try: + with open('/etc/mainsailos-release', 'r') as file: + content = file.read() + log_info(f'Release: {content.strip()}', log_pre) + except FileNotFoundError: + pass + + # Kernel Version + uname = os.uname() + log_info(f'Kernel: {uname.sysname} {uname.release} {uname.machine}', log_pre) + + + ### Host Machine Infos + # Host model + command = 'grep "Model" /proc/cpuinfo | cut -d\':\' -f2' + model = subprocess.check_output(command, shell=True).decode('utf-8').strip() + if model == '': + command = 'grep "model name" /proc/cpuinfo | cut -d\':\' -f2 | awk NR==1' + model = subprocess.check_output(command, shell=True).decode('utf-8').strip() + if model == '': + model = 'Unknown' + log_info(f'Model: {model}', log_pre) + + # CPU count + cpu_count = os.cpu_count() + log_info(f"Available CPU Cores: {cpu_count}", log_pre) + + # Avail mem + # psutil.virtual_memory().total + command = 'grep "MemTotal:" /proc/meminfo | awk \'{print $2" "$3}\'' + memtotal = subprocess.check_output(command, shell=True).decode('utf-8').strip() + log_info(f'Available Memory: {memtotal}', log_pre) + + # Avail disk size + # Alternative psutil.disk_usage('/').total + command = 'LC_ALL=C df -h / | awk \'NR==2 {print $4" / "$2}\'' + disksize = subprocess.check_output(command, shell=True).decode('utf-8').strip() + log_info(f'Diskspace (avail. / total): {disksize}', log_pre) + +def log_cams(): + log_info("INFO: Detect available Devices") + libcamera = 0 + v4l = 0 + legacy = 0 + total = libcamera + legacy + v4l + + if total == 0: + log_error("No usable Devices Found. Stopping ") + sys.exit() + + log_info(f"Found {total} Devices (V4L: {v4l}, libcamera: {libcamera}, Legacy: {legacy})") + if libcamera > 0: + log_info(f"Detected 'libcamera' device -> {-1}") + if legacy > 0: + pass + if v4l > 0: + pass \ No newline at end of file diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 1b77aaaf..16a777d0 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,7 +1,8 @@ from configparser import SectionProxy from .streamer import Streamer from ..parameter import Parameter -from ..core import execute_command, log_debug +from ..core import execute_command +from ..logger import log_debug class Ustreamer(Streamer): keyword = 'ustreamer' @@ -42,7 +43,11 @@ async def execute(self): cmd = streamer_args log_pre = f'ustreamer [cam {self.name}]: ' - process,_,_ = await execute_command(' '.join(cmd), error_log_pre=log_pre, error_log_func=log_debug) + process,_,_ = await execute_command( + ' '.join(cmd), + error_log_pre=log_pre, + error_log_func=log_debug + ) return process From b2332c295f4714c6544294e2bb5154c96e5465c1 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 24 Feb 2024 18:34:58 +0100 Subject: [PATCH 027/129] chore: wip --- crowsnest.py | 5 ++++- pylibs/core.py | 15 +++++++++----- pylibs/hwhandler.py | 26 ++++++++++++++++++++++++ pylibs/logger.py | 48 +++++++++++++++++++++++++++++---------------- 4 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 pylibs/hwhandler.py diff --git a/crowsnest.py b/crowsnest.py index 60a9530f..d2cc0a8e 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,7 +1,6 @@ import argparse import configparser from pylibs.crowsnest import Crowsnest -from pylibs.section import Section from pylibs.core import get_module_class import pylibs.logger as logger @@ -62,8 +61,12 @@ async def start_processes(): logger.setup_logging(args.log_path) logger.log_initial() + parse_config() + logger.log_host_info() logger.log_config(args.config) +logger.log_cams() + # Run async to wait for all tasks to finish asyncio.run(start_processes()) diff --git a/pylibs/core.py b/pylibs/core.py index ee6b1db1..98fc2bae 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -1,9 +1,8 @@ import importlib import asyncio -import time -import os +import subprocess -from .logger import log_debug, log_error +from . import logger # import logging # Dynamically import module @@ -28,8 +27,8 @@ async def log_subprocess_output(stream, log_func, line_prefix = ''): async def execute_command( command: str, - info_log_func = log_debug, - error_log_func = log_error, + info_log_func = logger.log_debug, + error_log_func = logger.log_error, info_log_pre = '', error_log_pre = ''): @@ -61,3 +60,9 @@ async def execute_command( # Wait for the output handling tasks to finish #await stdout_task #await stderr_task + +def execute_shell_command(command: str): + try: + return subprocess.check_output(command, shell=True).decode('utf-8').strip() + except subprocess.CalledProcessError as e: + return '' diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py new file mode 100644 index 00000000..1794c71b --- /dev/null +++ b/pylibs/hwhandler.py @@ -0,0 +1,26 @@ +import os + +from . import core + +def get_avail_usb_cams() -> dict: + avail_cmd='find /dev/v4l/by-id/ -iname "*index0" 2> /dev/null' + avail = core.execute_shell_command(avail_cmd) + count = avail.count('\n') + cams = {} + if count > 0: + for cam_path in avail.split('\n'): + cams[cam_path] = {} + cams[cam_path]['realpath'] = os.path.reaelpath(cam_path) + cams[cam_path]['formats'] = get_usb_cam_formats(cam_path) + cams[cam_path]['v4l2ctrls'] = get_usb_cam_v4l2ctrls(cam_path) + return cams + +def get_usb_cam_formats(cam_path) -> str: + command = f'v4l2-ctl -d {cam_path} --list-formats-ext | sed "1,3d"' + formats = core.execute_shell_command(command) + return formats + +def get_usb_cam_v4l2ctrls(cam_path) -> str: + command = f'v4l2-ctl -d {cam_path} --list-ctrls-menus' + v4l2ctrls = core.execute_shell_command(command) + return v4l2ctrls diff --git a/pylibs/logger.py b/pylibs/logger.py index c044ea35..a174aa09 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -1,7 +1,12 @@ import logging -import re # log_config -import os, subprocess # log_host_info -import sys # log_cams +# log_config +import re +# log_host_info +import os +from . import core +# log_cams +import sys +from .hwhandler import get_avail_usb_cams DEV = 10 DEBUG = 15 @@ -36,11 +41,16 @@ def log_debug(msg, prefix='DEBUG: '): def log_error(msg, prefix='ERROR: '): logging.error(prefix + msg) +def log_multiline(msg, log_func, *args): + lines = msg.split('\n') + for line in lines: + log_func(line, *args) + def log_initial(): log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') command = 'git describe --always --tags' - version = subprocess.check_output(command, shell=True).decode('utf-8').strip() + version = core.execute_shell_command(command) log_quiet(f'Version: {version}') log_quiet('Prepare Startup ...') @@ -52,9 +62,7 @@ def log_config(config_path): config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) config_txt = config_txt.strip() # Split the config file into lines - lines = config_txt.split('\n') - for line in lines: - log_info(5*' ' + line.strip(), '') + log_multiline(config_txt, log_info, '\t\t') def log_host_info(): log_info("Host Information:") @@ -85,10 +93,10 @@ def log_host_info(): ### Host Machine Infos # Host model command = 'grep "Model" /proc/cpuinfo | cut -d\':\' -f2' - model = subprocess.check_output(command, shell=True).decode('utf-8').strip() + model = core.execute_shell_command(command) if model == '': command = 'grep "model name" /proc/cpuinfo | cut -d\':\' -f2 | awk NR==1' - model = subprocess.check_output(command, shell=True).decode('utf-8').strip() + model = core.execute_shell_command(command) if model == '': model = 'Unknown' log_info(f'Model: {model}', log_pre) @@ -100,30 +108,36 @@ def log_host_info(): # Avail mem # psutil.virtual_memory().total command = 'grep "MemTotal:" /proc/meminfo | awk \'{print $2" "$3}\'' - memtotal = subprocess.check_output(command, shell=True).decode('utf-8').strip() + memtotal = core.execute_shell_command(command) log_info(f'Available Memory: {memtotal}', log_pre) # Avail disk size # Alternative psutil.disk_usage('/').total command = 'LC_ALL=C df -h / | awk \'NR==2 {print $4" / "$2}\'' - disksize = subprocess.check_output(command, shell=True).decode('utf-8').strip() + disksize = core.execute_shell_command(command) log_info(f'Diskspace (avail. / total): {disksize}', log_pre) def log_cams(): - log_info("INFO: Detect available Devices") + log_info("Detect available Devices") libcamera = 0 - v4l = 0 + v4l = get_avail_usb_cams() legacy = 0 - total = libcamera + legacy + v4l + total = libcamera + legacy + len(v4l.keys()) if total == 0: log_error("No usable Devices Found. Stopping ") sys.exit() - log_info(f"Found {total} Devices (V4L: {v4l}, libcamera: {libcamera}, Legacy: {legacy})") + log_info(f"Found {total} total available Device(s)") if libcamera > 0: log_info(f"Detected 'libcamera' device -> {-1}") if legacy > 0: pass - if v4l > 0: - pass \ No newline at end of file + if not v4l.keys().empty(): + log_info(f"Found {len(v4l.keys())} available v4l2 (UVC) camera(s)") + for cam in v4l.keys(): + log_info(f"{cam} -> {v4l[cam]['realpath']}", '') + log_info(f"Supported Formats:", '') + log_multiline(v4l[cam]['formats'], log_info, '\t\t') + log_info(f"Supported Controls:", '') + log_multiline(v4l[cam]['v4l2ctrls'], log_info, '\t\t') From a7e54e7a572ad4c9498bbbf8519d0555ce4ecd8c Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 24 Feb 2024 18:44:24 +0100 Subject: [PATCH 028/129] chore: wip --- pylibs/hwhandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 1794c71b..67e221a7 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -5,7 +5,7 @@ def get_avail_usb_cams() -> dict: avail_cmd='find /dev/v4l/by-id/ -iname "*index0" 2> /dev/null' avail = core.execute_shell_command(avail_cmd) - count = avail.count('\n') + count = len(avail.readlines()) cams = {} if count > 0: for cam_path in avail.split('\n'): From a8f42bec91cbe0f8f67791c7a945721c19bf6266 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 24 Feb 2024 21:08:44 +0100 Subject: [PATCH 029/129] chore: wip --- pylibs/hwhandler.py | 4 ++-- pylibs/logger.py | 2 +- pylibs/streamer/ustreamer.py | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 67e221a7..954e483c 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -5,12 +5,12 @@ def get_avail_usb_cams() -> dict: avail_cmd='find /dev/v4l/by-id/ -iname "*index0" 2> /dev/null' avail = core.execute_shell_command(avail_cmd) - count = len(avail.readlines()) + count = len(avail.splitlines()) cams = {} if count > 0: for cam_path in avail.split('\n'): cams[cam_path] = {} - cams[cam_path]['realpath'] = os.path.reaelpath(cam_path) + cams[cam_path]['realpath'] = os.path.realpath(cam_path) cams[cam_path]['formats'] = get_usb_cam_formats(cam_path) cams[cam_path]['v4l2ctrls'] = get_usb_cam_v4l2ctrls(cam_path) return cams diff --git a/pylibs/logger.py b/pylibs/logger.py index a174aa09..e75278a0 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -133,7 +133,7 @@ def log_cams(): log_info(f"Detected 'libcamera' device -> {-1}") if legacy > 0: pass - if not v4l.keys().empty(): + if v4l: log_info(f"Found {len(v4l.keys())} available v4l2 (UVC) camera(s)") for cam in v4l.keys(): log_info(f"{cam} -> {v4l[cam]['realpath']}", '') diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 16a777d0..0d8f375a 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,6 +1,6 @@ -from configparser import SectionProxy +import re + from .streamer import Streamer -from ..parameter import Parameter from ..core import execute_command from ..logger import log_debug @@ -50,6 +50,10 @@ async def execute(self): ) return process + + def custom_log(self, msg): + msg = re.sub(r'\[.*?\]', '', msg, count=1) + log_debug(msg) def load_module(): From 947b4e144ff86ebbe54cd17f32948a8319461bb6 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 24 Feb 2024 22:37:45 +0100 Subject: [PATCH 030/129] chore: wip --- pylibs/streamer/ustreamer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 0d8f375a..4862a0bb 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -46,13 +46,13 @@ async def execute(self): process,_,_ = await execute_command( ' '.join(cmd), error_log_pre=log_pre, - error_log_func=log_debug + error_log_func=self.custom_log ) return process def custom_log(self, msg): - msg = re.sub(r'\[.*?\]', '', msg, count=1) + msg = re.sub(r'-- (.*?.*?) \[.*?\] --', r'\1', msg) log_debug(msg) From 20ba005dfa4979957f2cb3ed4903c0ef4fe5ddf8 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 24 Feb 2024 23:37:23 +0100 Subject: [PATCH 031/129] chore: wip --- pylibs/streamer/ustreamer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 4862a0bb..9d917365 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -51,8 +51,11 @@ async def execute(self): return process - def custom_log(self, msg): - msg = re.sub(r'-- (.*?.*?) \[.*?\] --', r'\1', msg) + def custom_log(self, msg: str): + if msg.endswith('==='): + msg = msg[:-28] + else: + msg = re.sub(r'-- (.*?) \[.*?\] --', r'\1', msg) log_debug(msg) From 80a1f5a77611f50b4b825f4f258879659f857f8f Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 25 Feb 2024 00:06:52 +0100 Subject: [PATCH 032/129] chore: wip --- crowsnest.py | 1 - pylibs/cam.py | 8 ++++---- pylibs/streamer/streamer.py | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index d2cc0a8e..4e4f0e9d 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -44,7 +44,6 @@ async def start_processes(): section_object.parse_config(config[section]) task = asyncio.create_task(section_object.execute()) sec_exec_tasks.add(task) - task.add_done_callback(sec_exec_tasks.discard) if section_object == None: print(f"Section [{section}] couldn't get parsed") diff --git a/pylibs/cam.py b/pylibs/cam.py index 8b367e54..31d7767e 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -3,8 +3,7 @@ from .parameter import Parameter from .core import get_module_class -import copy -import logging +from . import logger class Cam(Section): keyword = 'cam' @@ -13,7 +12,7 @@ def __init__(self, name: str = '') -> None: super().__init__(name) self.parameters.update({ - 'mode': Parameter() + 'mode': Parameter(str) }) self.streamer = None @@ -21,6 +20,7 @@ def __init__(self, name: str = '') -> None: def parse_config(self, config_section: SectionProxy, *args, **kwargs): # Dynamically import module mode = config_section["mode"].split()[0] + self.parameters["mode"] = mode mode_class = get_module_class('pylibs.streamer', mode) self.streamer = mode_class(self.name) self.streamer.parse_config(config_section) @@ -32,7 +32,7 @@ async def execute(self): try: process = await self.streamer.execute() await process.wait() - logging.error(f'Start of {self.parameters["mode"].value} [cam {self.name}] failed!') + logger.log_error(f'Start of {self.parameters["mode"].value} [cam {self.name}] failed!') except Exception as e: pass diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py index a5e3c299..23c994b1 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/streamer/streamer.py @@ -13,8 +13,8 @@ def __init__(self, name: str = '') -> None: self.parameters.update({ 'mode': Parameter(str), 'port': Parameter(int), - 'device': Parameter(), - 'resolution': Parameter(), + 'device': Parameter(str), + 'resolution': Parameter(str), 'max_fps': Parameter(int), 'no_proxy': Parameter(bool, False), 'custom_flags': Parameter(str, ''), From 4686b37edca83ece97fc39be43fa3fc63b222bc1 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 25 Feb 2024 00:11:04 +0100 Subject: [PATCH 033/129] chore: wip --- pylibs/cam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/cam.py b/pylibs/cam.py index 31d7767e..82b77932 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -20,7 +20,7 @@ def __init__(self, name: str = '') -> None: def parse_config(self, config_section: SectionProxy, *args, **kwargs): # Dynamically import module mode = config_section["mode"].split()[0] - self.parameters["mode"] = mode + self.parameters["mode"].set_value(mode) mode_class = get_module_class('pylibs.streamer', mode) self.streamer = mode_class(self.name) self.streamer.parse_config(config_section) From 149c35ed4ea11c06062b41d5aa58f2dedb20789c Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 25 Feb 2024 18:29:42 +0100 Subject: [PATCH 034/129] chore: wip --- pylibs/core.py | 10 ++++- pylibs/hwhandler.py | 76 ++++++++++++++++++++++++++++++++---- pylibs/logger.py | 64 ++++++++++++++++++++++-------- pylibs/streamer/ustreamer.py | 6 +-- 4 files changed, 127 insertions(+), 29 deletions(-) diff --git a/pylibs/core.py b/pylibs/core.py index 98fc2bae..76da233c 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -61,8 +61,14 @@ async def execute_command( #await stdout_task #await stderr_task -def execute_shell_command(command: str): +def execute_shell_command(command: str, strip: bool = True) -> str: try: - return subprocess.check_output(command, shell=True).decode('utf-8').strip() + output = subprocess.check_output(command, shell=True).decode('utf-8') + if strip: + output = output.strip() + return output except subprocess.CalledProcessError as e: return '' + +def bytes_to_gigabytes(value: int) -> int: + return int(value / 1024 / 1024 / 1024) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 954e483c..34e69d80 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -1,26 +1,88 @@ import os +import shutil +import re from . import core -def get_avail_usb_cams() -> dict: +def get_avail_uvc_dev() -> dict: avail_cmd='find /dev/v4l/by-id/ -iname "*index0" 2> /dev/null' avail = core.execute_shell_command(avail_cmd) - count = len(avail.splitlines()) cams = {} - if count > 0: + if avail: for cam_path in avail.split('\n'): cams[cam_path] = {} cams[cam_path]['realpath'] = os.path.realpath(cam_path) - cams[cam_path]['formats'] = get_usb_cam_formats(cam_path) - cams[cam_path]['v4l2ctrls'] = get_usb_cam_v4l2ctrls(cam_path) + cams[cam_path]['formats'] = get_uvc_formats(cam_path) + cams[cam_path]['v4l2ctrls'] = get_uvc_v4l2ctrls(cam_path) return cams -def get_usb_cam_formats(cam_path) -> str: +def get_uvc_formats(cam_path: str) -> str: command = f'v4l2-ctl -d {cam_path} --list-formats-ext | sed "1,3d"' formats = core.execute_shell_command(command) return formats -def get_usb_cam_v4l2ctrls(cam_path) -> str: +def get_uvc_v4l2ctrls(cam_path: str) -> str: command = f'v4l2-ctl -d {cam_path} --list-ctrls-menus' v4l2ctrls = core.execute_shell_command(command) return v4l2ctrls + +def get_avail_libcamera() -> dict: + cmd = shutil.which('libcamera-hello') + if not cmd: + return {} + libcam_cmd =f'{cmd} --list-cameras' + libcam = core.execute_shell_command(libcam_cmd, strip=False) + libcams = {} + if 'Available' in libcam: + for path in get_libcamera_paths(libcam): + libcams[path] = { + 'resolutions': get_libcamera_resolutions(libcam, path), + 'controls': get_libcamera_controls(path) + } + pass + return libcams + +def get_libcamera_paths(libcamera_output: str) -> list: + return re.findall(r'\((/base.*?)\)', libcamera_output) + +def get_libcamera_resolutions(libcamera_output: str, camera_path: str) -> list: + # Get the resolution list for only one mode + resolutions = re.findall( + rf"{camera_path}.*?:.*?: (.*?)(?=\n\n|\n *')", + libcamera_output, flags=re.DOTALL + ) + res = [] + if resolutions: + # Maybe cut out fps? re.sub('\[.*? - ', '[', r.strip()) + res = [r.strip() for r in resolutions[0].split('\n')] + return res + +def get_libcamera_controls(camera_path: str) -> list: + ctrls = {} + try: + from libcamera import CameraManager, Rectangle + + libcam_cm = CameraManager.singleton() + for cam in libcam_cm.cameras: + if cam.id != camera_path: + continue + for k, v in cam.controls.items(): + def rectangle_to_tuple(rectangle): + return (rectangle.x, rectangle.y, rectangle.width, rectangle.height) + if isinstance(v.min, Rectangle): + ctrls[k.name] = { + 'min': rectangle_to_tuple(v.min), + 'max': rectangle_to_tuple(v.max), + 'default': rectangle_to_tuple(v.default) + } + else: + ctrls[k.name] = { + 'min': v.min, + 'max': v.max, + 'default': v.default + } + except ImportError: + pass + return ctrls + + diff --git a/pylibs/logger.py b/pylibs/logger.py index e75278a0..978a519a 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -1,4 +1,5 @@ import logging +import shutil # log_config import re # log_host_info @@ -6,12 +7,14 @@ from . import core # log_cams import sys -from .hwhandler import get_avail_usb_cams +from .hwhandler import get_avail_uvc_dev, get_avail_libcamera DEV = 10 DEBUG = 15 QUIET = 25 +indentation = 6*' ' + def setup_logging(log_path): logging.basicConfig( filename=log_path, @@ -62,7 +65,7 @@ def log_config(config_path): config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) config_txt = config_txt.strip() # Split the config file into lines - log_multiline(config_txt, log_info, '\t\t') + log_multiline(config_txt, log_info, indentation) def log_host_info(): log_info("Host Information:") @@ -106,38 +109,65 @@ def log_host_info(): log_info(f"Available CPU Cores: {cpu_count}", log_pre) # Avail mem - # psutil.virtual_memory().total + # psutil.virtual_memory().total command = 'grep "MemTotal:" /proc/meminfo | awk \'{print $2" "$3}\'' memtotal = core.execute_shell_command(command) log_info(f'Available Memory: {memtotal}', log_pre) # Avail disk size - # Alternative psutil.disk_usage('/').total + # Alternative shutil.disk_usage.total command = 'LC_ALL=C df -h / | awk \'NR==2 {print $4" / "$2}\'' disksize = core.execute_shell_command(command) log_info(f'Diskspace (avail. / total): {disksize}', log_pre) def log_cams(): log_info("Detect available Devices") - libcamera = 0 - v4l = get_avail_usb_cams() + libcamera = get_avail_libcamera() + uvc = get_avail_uvc_dev() legacy = 0 - total = libcamera + legacy + len(v4l.keys()) + total = len(libcamera.keys()) + legacy + len(uvc.keys()) if total == 0: log_error("No usable Devices Found. Stopping ") sys.exit() log_info(f"Found {total} total available Device(s)") - if libcamera > 0: - log_info(f"Detected 'libcamera' device -> {-1}") + if libcamera: + log_info(f"Found {len(libcamera.keys())} available 'libcamera' device(s)") + for path, properties in libcamera.items(): + log_libcamera_dev(path, properties) if legacy > 0: pass - if v4l: - log_info(f"Found {len(v4l.keys())} available v4l2 (UVC) camera(s)") - for cam in v4l.keys(): - log_info(f"{cam} -> {v4l[cam]['realpath']}", '') - log_info(f"Supported Formats:", '') - log_multiline(v4l[cam]['formats'], log_info, '\t\t') - log_info(f"Supported Controls:", '') - log_multiline(v4l[cam]['v4l2ctrls'], log_info, '\t\t') + if uvc: + log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") + for path, properties in uvc.items(): + log_uvc_dev(path, properties) + +def log_libcamera_dev(path: str, properties: dict) -> str: + log_info(f"Detected 'libcamera' device -> {path}") + log_info(f"Advertised Formats:", '') + resolutions = properties['resolutions'] + for res in resolutions: + log_info(f"{res}", indentation) + log_info(f"Supported Controls:", '') + controls = properties['controls'] + if controls: + for name, value in controls.items(): + min, max, default = value.values() + str_first = f"{name} ({get_type_str(min)}):" + str_second = f"min={min} max={max} default={default}" + str_indent = (30 - len(str_first)) * ' ' + log_info(str_first + str_indent + str_second, indentation) + else: + log_info("apt package 'python3-libcamera' is not installed! \ +Make sure to install it to log the controls!", indentation) + +def get_type_str(obj): + return str(type(obj)).split('\'')[1] + +def log_uvc_dev(path: str, properties: dict) -> str: + log_info(f"{path} -> {properties['realpath']}", '') + log_info(f"Supported Formats:", '') + log_multiline(properties['formats'], log_info, indentation) + log_info(f"Supported Controls:", '') + log_multiline(properties['v4l2ctrls'], log_info, indentation) \ No newline at end of file diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 9d917365..aae6dbf3 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -14,7 +14,7 @@ def __init__(self, name: str = '') -> None: if Ustreamer.binary_path is None: Ustreamer.binary_path = 'bin/ustreamer/ustreamer' self.binary_path = Ustreamer.binary_path - + async def execute(self): if not super().execute(): return None @@ -36,7 +36,7 @@ async def execute(self): '--format', 'MJPEG', '--encoder', 'HW' ] - + # custom flags streamer_args += self.parameters['custom_flags'].value.split() @@ -50,7 +50,7 @@ async def execute(self): ) return process - + def custom_log(self, msg: str): if msg.endswith('==='): msg = msg[:-28] From 58430380328070cd6c151cff3fa8037d4584d0f2 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 25 Feb 2024 22:08:22 +0100 Subject: [PATCH 035/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/core.py | 3 ++- pylibs/hwhandler.py | 28 ++++++++++++++++++++++++- pylibs/logger.py | 50 ++++++++++++++++++++++++++------------------- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/pylibs/core.py b/pylibs/core.py index 76da233c..051932c6 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -1,6 +1,7 @@ import importlib import asyncio import subprocess +import math from . import logger # import logging @@ -71,4 +72,4 @@ def execute_shell_command(command: str, strip: bool = True) -> str: return '' def bytes_to_gigabytes(value: int) -> int: - return int(value / 1024 / 1024 / 1024) + return math.round(value / 1024 / 1024 / 1024) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 34e69d80..3552af8a 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -5,6 +5,7 @@ from . import core def get_avail_uvc_dev() -> dict: + # TODO: Change to use syslib avail_cmd='find /dev/v4l/by-id/ -iname "*index0" 2> /dev/null' avail = core.execute_shell_command(avail_cmd) cams = {} @@ -69,6 +70,7 @@ def get_libcamera_controls(camera_path: str) -> list: for k, v in cam.controls.items(): def rectangle_to_tuple(rectangle): return (rectangle.x, rectangle.y, rectangle.width, rectangle.height) + if isinstance(v.min, Rectangle): ctrls[k.name] = { 'min': rectangle_to_tuple(v.min), @@ -85,4 +87,28 @@ def rectangle_to_tuple(rectangle): pass return ctrls - +def get_avail_legacy() -> dict: + cmd = shutil.which('vcgencmd') + legacy = {} + if not cmd: + return legacy + count_cmd = f'{cmd} get_camera' + count = core.execute_shell_command(count_cmd) + # Gets the number behind detected: "supported=1 detected=1, libcamera interfaces=0" + if not count: + return legacy + count = count.split('=')[2].split(',')[0] + if count == '0': + return legacy + v4l2_cmd = 'v4l2-ctl --list-devices' + v4l2 = core.execute_shell_command(v4l2_cmd) + legacy_path = '' + lines = v4l2.split('\n') + for i in range(len(lines)): + if 'mmal' in lines[i]: + legacy_path = lines[i+1] + break + legacy[legacy_path] = {} + legacy[legacy_path]['formats'] = get_uvc_formats(legacy_path) + legacy[legacy_path]['v4l2ctrls'] = get_uvc_v4l2ctrls(legacy_path) + return legacy diff --git a/pylibs/logger.py b/pylibs/logger.py index 978a519a..5faa2ce9 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -7,7 +7,7 @@ from . import core # log_cams import sys -from .hwhandler import get_avail_uvc_dev, get_avail_libcamera +from .hwhandler import get_avail_uvc_dev, get_avail_libcamera, get_avail_legacy DEV = 10 DEBUG = 15 @@ -73,12 +73,9 @@ def log_host_info(): ### OS Infos # OS Version - with open('/etc/os-release', 'r') as file: - lines = file.readlines() - for line in lines: - if line.startswith('PRETTY_NAME'): - distribution = line.strip().split('=')[1].strip('"') - log_info(f'Distribution: {distribution}', log_pre) + distribution = grep('/etc/os-release', 'PRETTY_NAME') + distribution = distribution.strip().split('=')[1].strip('"') + log_info(f'Distribution: {distribution}', log_pre) # Release Version of MainsailOS (if file present) try: @@ -95,11 +92,9 @@ def log_host_info(): ### Host Machine Infos # Host model - command = 'grep "Model" /proc/cpuinfo | cut -d\':\' -f2' - model = core.execute_shell_command(command) + model = grep('/proc/cpuinfo', 'Model').split(':')[1].strip() if model == '': - command = 'grep "model name" /proc/cpuinfo | cut -d\':\' -f2 | awk NR==1' - model = core.execute_shell_command(command) + model == grep('/proc/cpuinfo', 'model name').split(':')[1].strip() if model == '': model = 'Unknown' log_info(f'Model: {model}', log_pre) @@ -110,8 +105,7 @@ def log_host_info(): # Avail mem # psutil.virtual_memory().total - command = 'grep "MemTotal:" /proc/meminfo | awk \'{print $2" "$3}\'' - memtotal = core.execute_shell_command(command) + memtotal = grep('/proc/meminfo', 'MemTotal:').split(':')[1].strip() log_info(f'Available Memory: {memtotal}', log_pre) # Avail disk size @@ -120,12 +114,20 @@ def log_host_info(): disksize = core.execute_shell_command(command) log_info(f'Diskspace (avail. / total): {disksize}', log_pre) +def grep(path: str, search: str) -> str: + with open(path, 'r') as file: + lines = file.readlines() + for line in lines: + if search in line: + return line + return '' + def log_cams(): log_info("Detect available Devices") libcamera = get_avail_libcamera() uvc = get_avail_uvc_dev() - legacy = 0 - total = len(libcamera.keys()) + legacy + len(uvc.keys()) + legacy = get_avail_legacy() + total = len(libcamera.keys()) + len(legacy.keys()) + len(uvc.keys()) if total == 0: log_error("No usable Devices Found. Stopping ") @@ -136,12 +138,17 @@ def log_cams(): log_info(f"Found {len(libcamera.keys())} available 'libcamera' device(s)") for path, properties in libcamera.items(): log_libcamera_dev(path, properties) - if legacy > 0: - pass + if legacy: + log_info(f"Detected 'Raspicam' Device -> ${legacy.keys()[0]}") + _, properties = legacy.items()[0] + log_uvc_formats(properties) + log_uvc_v4l2ctrls(properties) if uvc: log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") for path, properties in uvc.items(): - log_uvc_dev(path, properties) + log_info(f"{path} -> {properties['realpath']}", '') + log_uvc_formats(properties) + log_uvc_v4l2ctrls(properties) def log_libcamera_dev(path: str, properties: dict) -> str: log_info(f"Detected 'libcamera' device -> {path}") @@ -162,12 +169,13 @@ def log_libcamera_dev(path: str, properties: dict) -> str: log_info("apt package 'python3-libcamera' is not installed! \ Make sure to install it to log the controls!", indentation) -def get_type_str(obj): +def get_type_str(obj) -> str: return str(type(obj)).split('\'')[1] -def log_uvc_dev(path: str, properties: dict) -> str: - log_info(f"{path} -> {properties['realpath']}", '') +def log_uvc_formats(properties: dict) -> None: log_info(f"Supported Formats:", '') log_multiline(properties['formats'], log_info, indentation) + +def log_uvc_v4l2ctrls(properties: dict) -> None: log_info(f"Supported Controls:", '') log_multiline(properties['v4l2ctrls'], log_info, indentation) \ No newline at end of file From f1bc0b04228ee177d8c3015222e4796c3bacc2d8 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 28 Feb 2024 10:22:55 +0100 Subject: [PATCH 036/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/hwhandler.py | 26 +++++++++++++++----------- pylibs/logger.py | 14 +++++++------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 3552af8a..fb3e2a1d 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -5,21 +5,25 @@ from . import core def get_avail_uvc_dev() -> dict: - # TODO: Change to use syslib - avail_cmd='find /dev/v4l/by-id/ -iname "*index0" 2> /dev/null' - avail = core.execute_shell_command(avail_cmd) + uvc_path = '/dev/v4l/by-id/' + avail_uvc = [] + for file in os.listdir(uvc_path): + path = os.path.join(uvc_path, file) + if os.path.islink(path) and path.endswith("index0"): + avail_uvc.append(path) cams = {} - if avail: - for cam_path in avail.split('\n'): - cams[cam_path] = {} - cams[cam_path]['realpath'] = os.path.realpath(cam_path) - cams[cam_path]['formats'] = get_uvc_formats(cam_path) - cams[cam_path]['v4l2ctrls'] = get_uvc_v4l2ctrls(cam_path) + for cam_path in avail_uvc: + cams[cam_path] = {} + cams[cam_path]['realpath'] = os.path.realpath(cam_path) + cams[cam_path]['formats'] = get_uvc_formats(cam_path) + cams[cam_path]['v4l2ctrls'] = get_uvc_v4l2ctrls(cam_path) return cams def get_uvc_formats(cam_path: str) -> str: - command = f'v4l2-ctl -d {cam_path} --list-formats-ext | sed "1,3d"' + command = f'v4l2-ctl -d {cam_path} --list-formats-ext' formats = core.execute_shell_command(command) + # Remove first 3 lines + formats = '\n'.join(formats.split('\n')[3:]) return formats def get_uvc_v4l2ctrls(cam_path: str) -> str: @@ -106,7 +110,7 @@ def get_avail_legacy() -> dict: lines = v4l2.split('\n') for i in range(len(lines)): if 'mmal' in lines[i]: - legacy_path = lines[i+1] + legacy_path = lines[i+1].strip() break legacy[legacy_path] = {} legacy[legacy_path]['formats'] = get_uvc_formats(legacy_path) diff --git a/pylibs/logger.py b/pylibs/logger.py index 5faa2ce9..f047715d 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -69,7 +69,7 @@ def log_config(config_path): def log_host_info(): log_info("Host Information:") - log_pre = "Host Info: " + log_pre = indentation #"Host Info: " ### OS Infos # OS Version @@ -139,10 +139,10 @@ def log_cams(): for path, properties in libcamera.items(): log_libcamera_dev(path, properties) if legacy: - log_info(f"Detected 'Raspicam' Device -> ${legacy.keys()[0]}") - _, properties = legacy.items()[0] - log_uvc_formats(properties) - log_uvc_v4l2ctrls(properties) + for path, properties in legacy.items(): + log_info(f"Detected 'Raspicam' Device -> {path}") + log_uvc_formats(properties) + log_uvc_v4l2ctrls(properties) if uvc: log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") for path, properties in uvc.items(): @@ -161,9 +161,9 @@ def log_libcamera_dev(path: str, properties: dict) -> str: if controls: for name, value in controls.items(): min, max, default = value.values() - str_first = f"{name} ({get_type_str(min)}):" + str_first = f"{name} ({get_type_str(min)})" str_second = f"min={min} max={max} default={default}" - str_indent = (30 - len(str_first)) * ' ' + str_indent = (30 - len(str_first)) * ' ' + ': ' log_info(str_first + str_indent + str_second, indentation) else: log_info("apt package 'python3-libcamera' is not installed! \ From 63e557d6c9004e59dd7ab3fa47640fcbef6f15a6 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 1 Mar 2024 11:50:14 +0100 Subject: [PATCH 037/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 17 ++-- pylibs/cam.py | 3 +- pylibs/core.py | 15 ++++ pylibs/crowsnest.py | 4 +- pylibs/hwhandler.py | 18 +++- pylibs/logger.py | 140 ----------------------------- pylibs/logging.py | 140 +++++++++++++++++++++++++++++ pylibs/parameter.py | 2 +- pylibs/section.py | 10 ++- pylibs/streamer/camera-streamer.py | 11 ++- pylibs/streamer/streamer.py | 23 +++-- pylibs/streamer/ustreamer.py | 53 ++++++++--- 12 files changed, 259 insertions(+), 177 deletions(-) create mode 100644 pylibs/logging.py diff --git a/crowsnest.py b/crowsnest.py index 4e4f0e9d..5238a368 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -3,6 +3,7 @@ from pylibs.crowsnest import Crowsnest from pylibs.core import get_module_class import pylibs.logger as logger +import pylibs.logging as logging import asyncio @@ -29,6 +30,7 @@ async def start_processes(): sec_objs = [] sec_exec_tasks = set() + logger.log_quiet("Try to start configured Cams / Services...") try: for section in config.sections(): section_header = section.split(' ') @@ -42,12 +44,15 @@ async def start_processes(): section_name = ' '.join(section_header[1:]) section_object = section_class(section_name) section_object.parse_config(config[section]) - task = asyncio.create_task(section_object.execute()) - sec_exec_tasks.add(task) if section_object == None: print(f"Section [{section}] couldn't get parsed") sec_objs.append(section_object) + + for section_object in sec_objs: + task = asyncio.create_task(section_object.execute()) + sec_exec_tasks.add(task) + for task in sec_exec_tasks: if task is not None: await task @@ -59,13 +64,13 @@ async def start_processes(): task.cancel() logger.setup_logging(args.log_path) -logger.log_initial() +logging.log_initial() parse_config() -logger.log_host_info() -logger.log_config(args.config) -logger.log_cams() +logging.log_host_info() +logging.log_config(args.config) +logging.log_cams() # Run async to wait for all tasks to finish asyncio.run(start_processes()) diff --git a/pylibs/cam.py b/pylibs/cam.py index 82b77932..db3477b7 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -6,6 +6,7 @@ from . import logger class Cam(Section): + section_name = 'cam' keyword = 'cam' def __init__(self, name: str = '') -> None: @@ -23,7 +24,7 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs): self.parameters["mode"].set_value(mode) mode_class = get_module_class('pylibs.streamer', mode) self.streamer = mode_class(self.name) - self.streamer.parse_config(config_section) + return self.streamer.parse_config(config_section) async def execute(self): if self.streamer is None: diff --git a/pylibs/core.py b/pylibs/core.py index 051932c6..c3c28681 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -2,6 +2,8 @@ import asyncio import subprocess import math +import shutil +import os from . import logger # import logging @@ -73,3 +75,16 @@ def execute_shell_command(command: str, strip: bool = True) -> str: def bytes_to_gigabytes(value: int) -> int: return math.round(value / 1024 / 1024 / 1024) + +def get_executable(names: list, paths: list) -> str: + for name in names: + exec = shutil.which(name) + if exec: + return exec + for path in paths: + for dpath, _, fnames in os.walk(path): + for fname in fnames: + if fname == name: + exec = os.path.join(dpath, fname) + return exec + return None diff --git a/pylibs/crowsnest.py b/pylibs/crowsnest.py index 926a4a6d..db7a182f 100644 --- a/pylibs/crowsnest.py +++ b/pylibs/crowsnest.py @@ -10,8 +10,8 @@ def __init__(self, name: str = '') -> None: self.parameters.update({ 'log_path': Parameter(), 'log_level': Parameter(str, 'verbose'), - 'delete_log': Parameter(bool, True), - 'no_proxy': Parameter(bool, False) + 'delete_log': Parameter(bool, 'True'), + 'no_proxy': Parameter(bool, 'False') }) def parse_config(self, section: SectionProxy): diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index fb3e2a1d..090b8e09 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -4,6 +4,12 @@ from . import core +avail_cams = { + 'uvc': {}, + 'libcamera': {}, + 'legacy': {} +} + def get_avail_uvc_dev() -> dict: uvc_path = '/dev/v4l/by-id/' avail_uvc = [] @@ -17,6 +23,7 @@ def get_avail_uvc_dev() -> dict: cams[cam_path]['realpath'] = os.path.realpath(cam_path) cams[cam_path]['formats'] = get_uvc_formats(cam_path) cams[cam_path]['v4l2ctrls'] = get_uvc_v4l2ctrls(cam_path) + avail_cams['uvc'].update(cams) return cams def get_uvc_formats(cam_path: str) -> str: @@ -31,6 +38,10 @@ def get_uvc_v4l2ctrls(cam_path: str) -> str: v4l2ctrls = core.execute_shell_command(command) return v4l2ctrls +def has_device_mjpg_hw(cam_path: str) -> bool: + global avail_cams + return 'Motion-JPEG, compressed' in get_uvc_formats(cam_path) + def get_avail_libcamera() -> dict: cmd = shutil.which('libcamera-hello') if not cmd: @@ -44,7 +55,7 @@ def get_avail_libcamera() -> dict: 'resolutions': get_libcamera_resolutions(libcam, path), 'controls': get_libcamera_controls(path) } - pass + avail_cams['libcamera'].update(libcams) return libcams def get_libcamera_paths(libcamera_output: str) -> list: @@ -115,4 +126,9 @@ def get_avail_legacy() -> dict: legacy[legacy_path] = {} legacy[legacy_path]['formats'] = get_uvc_formats(legacy_path) legacy[legacy_path]['v4l2ctrls'] = get_uvc_v4l2ctrls(legacy_path) + avail_cams['legacy'].update(legacy) return legacy + +def is_device_legacy(cam_path: str) -> bool: + global avail_cams + return cam_path in avail_cams['legacy'] diff --git a/pylibs/logger.py b/pylibs/logger.py index f047715d..cc129f8f 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -1,13 +1,4 @@ import logging -import shutil -# log_config -import re -# log_host_info -import os -from . import core -# log_cams -import sys -from .hwhandler import get_avail_uvc_dev, get_avail_libcamera, get_avail_legacy DEV = 10 DEBUG = 15 @@ -48,134 +39,3 @@ def log_multiline(msg, log_func, *args): lines = msg.split('\n') for line in lines: log_func(line, *args) - - -def log_initial(): - log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') - command = 'git describe --always --tags' - version = core.execute_shell_command(command) - log_quiet(f'Version: {version}') - log_quiet('Prepare Startup ...') - -def log_config(config_path): - log_info("Print Configfile: '" + config_path + "'") - with open(config_path, 'r') as file: - config_txt = file.read() - # Remove comments - config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) - config_txt = config_txt.strip() - # Split the config file into lines - log_multiline(config_txt, log_info, indentation) - -def log_host_info(): - log_info("Host Information:") - log_pre = indentation #"Host Info: " - - ### OS Infos - # OS Version - distribution = grep('/etc/os-release', 'PRETTY_NAME') - distribution = distribution.strip().split('=')[1].strip('"') - log_info(f'Distribution: {distribution}', log_pre) - - # Release Version of MainsailOS (if file present) - try: - with open('/etc/mainsailos-release', 'r') as file: - content = file.read() - log_info(f'Release: {content.strip()}', log_pre) - except FileNotFoundError: - pass - - # Kernel Version - uname = os.uname() - log_info(f'Kernel: {uname.sysname} {uname.release} {uname.machine}', log_pre) - - - ### Host Machine Infos - # Host model - model = grep('/proc/cpuinfo', 'Model').split(':')[1].strip() - if model == '': - model == grep('/proc/cpuinfo', 'model name').split(':')[1].strip() - if model == '': - model = 'Unknown' - log_info(f'Model: {model}', log_pre) - - # CPU count - cpu_count = os.cpu_count() - log_info(f"Available CPU Cores: {cpu_count}", log_pre) - - # Avail mem - # psutil.virtual_memory().total - memtotal = grep('/proc/meminfo', 'MemTotal:').split(':')[1].strip() - log_info(f'Available Memory: {memtotal}', log_pre) - - # Avail disk size - # Alternative shutil.disk_usage.total - command = 'LC_ALL=C df -h / | awk \'NR==2 {print $4" / "$2}\'' - disksize = core.execute_shell_command(command) - log_info(f'Diskspace (avail. / total): {disksize}', log_pre) - -def grep(path: str, search: str) -> str: - with open(path, 'r') as file: - lines = file.readlines() - for line in lines: - if search in line: - return line - return '' - -def log_cams(): - log_info("Detect available Devices") - libcamera = get_avail_libcamera() - uvc = get_avail_uvc_dev() - legacy = get_avail_legacy() - total = len(libcamera.keys()) + len(legacy.keys()) + len(uvc.keys()) - - if total == 0: - log_error("No usable Devices Found. Stopping ") - sys.exit() - - log_info(f"Found {total} total available Device(s)") - if libcamera: - log_info(f"Found {len(libcamera.keys())} available 'libcamera' device(s)") - for path, properties in libcamera.items(): - log_libcamera_dev(path, properties) - if legacy: - for path, properties in legacy.items(): - log_info(f"Detected 'Raspicam' Device -> {path}") - log_uvc_formats(properties) - log_uvc_v4l2ctrls(properties) - if uvc: - log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") - for path, properties in uvc.items(): - log_info(f"{path} -> {properties['realpath']}", '') - log_uvc_formats(properties) - log_uvc_v4l2ctrls(properties) - -def log_libcamera_dev(path: str, properties: dict) -> str: - log_info(f"Detected 'libcamera' device -> {path}") - log_info(f"Advertised Formats:", '') - resolutions = properties['resolutions'] - for res in resolutions: - log_info(f"{res}", indentation) - log_info(f"Supported Controls:", '') - controls = properties['controls'] - if controls: - for name, value in controls.items(): - min, max, default = value.values() - str_first = f"{name} ({get_type_str(min)})" - str_second = f"min={min} max={max} default={default}" - str_indent = (30 - len(str_first)) * ' ' + ': ' - log_info(str_first + str_indent + str_second, indentation) - else: - log_info("apt package 'python3-libcamera' is not installed! \ -Make sure to install it to log the controls!", indentation) - -def get_type_str(obj) -> str: - return str(type(obj)).split('\'')[1] - -def log_uvc_formats(properties: dict) -> None: - log_info(f"Supported Formats:", '') - log_multiline(properties['formats'], log_info, indentation) - -def log_uvc_v4l2ctrls(properties: dict) -> None: - log_info(f"Supported Controls:", '') - log_multiline(properties['v4l2ctrls'], log_info, indentation) \ No newline at end of file diff --git a/pylibs/logging.py b/pylibs/logging.py new file mode 100644 index 00000000..ed0ec7eb --- /dev/null +++ b/pylibs/logging.py @@ -0,0 +1,140 @@ +import shutil +# log_config +import re +# log_host_info +import os +from . import core +# log_cams +import sys +from .logger import log_quiet, log_info, log_error, log_multiline, indentation +from .hwhandler import get_avail_uvc_dev, get_avail_libcamera, get_avail_legacy + +def log_initial(): + log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') + command = 'git describe --always --tags' + version = core.execute_shell_command(command) + log_quiet(f'Version: {version}') + log_quiet('Prepare Startup ...') + +def log_config(config_path): + log_info("Print Configfile: '" + config_path + "'") + with open(config_path, 'r') as file: + config_txt = file.read() + # Remove comments + config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) + config_txt = config_txt.strip() + # Split the config file into lines + log_multiline(config_txt, log_info, indentation) + +def log_host_info(): + log_info("Host Information:") + log_pre = indentation #"Host Info: " + + ### OS Infos + # OS Version + distribution = grep('/etc/os-release', 'PRETTY_NAME') + distribution = distribution.strip().split('=')[1].strip('"') + log_info(f'Distribution: {distribution}', log_pre) + + # Release Version of MainsailOS (if file present) + try: + with open('/etc/mainsailos-release', 'r') as file: + content = file.read() + log_info(f'Release: {content.strip()}', log_pre) + except FileNotFoundError: + pass + + # Kernel Version + uname = os.uname() + log_info(f'Kernel: {uname.sysname} {uname.release} {uname.machine}', log_pre) + + + ### Host Machine Infos + # Host model + model = grep('/proc/cpuinfo', 'Model').split(':')[1].strip() + if model == '': + model == grep('/proc/cpuinfo', 'model name').split(':')[1].strip() + if model == '': + model = 'Unknown' + log_info(f'Model: {model}', log_pre) + + # CPU count + cpu_count = os.cpu_count() + log_info(f"Available CPU Cores: {cpu_count}", log_pre) + + # Avail mem + # psutil.virtual_memory().total + memtotal = grep('/proc/meminfo', 'MemTotal:').split(':')[1].strip() + log_info(f'Available Memory: {memtotal}', log_pre) + + # Avail disk size + # Alternative shutil.disk_usage.total + command = 'LC_ALL=C df -h / | awk \'NR==2 {print $4" / "$2}\'' + disksize = core.execute_shell_command(command) + log_info(f'Diskspace (avail. / total): {disksize}', log_pre) + +def grep(path: str, search: str) -> str: + with open(path, 'r') as file: + lines = file.readlines() + for line in lines: + if search in line: + return line + return '' + +def log_cams(): + log_info("Detect available Devices") + libcamera = get_avail_libcamera() + uvc = get_avail_uvc_dev() + legacy = get_avail_legacy() + total = len(libcamera.keys()) + len(legacy.keys()) + len(uvc.keys()) + + if total == 0: + log_error("No usable Devices Found. Stopping ") + sys.exit() + + log_info(f"Found {total} total available Device(s)") + if libcamera: + log_info(f"Found {len(libcamera.keys())} available 'libcamera' device(s)") + for path, properties in libcamera.items(): + log_libcamera_dev(path, properties) + if legacy: + for path, properties in legacy.items(): + log_info(f"Detected 'Raspicam' Device -> {path}") + log_uvc_formats(properties) + log_uvc_v4l2ctrls(properties) + if uvc: + log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") + for path, properties in uvc.items(): + log_info(f"{path} -> {properties['realpath']}", '') + log_uvc_formats(properties) + log_uvc_v4l2ctrls(properties) + +def log_libcamera_dev(path: str, properties: dict) -> str: + log_info(f"Detected 'libcamera' device -> {path}") + log_info(f"Advertised Formats:", '') + resolutions = properties['resolutions'] + for res in resolutions: + log_info(f"{res}", indentation) + log_info(f"Supported Controls:", '') + controls = properties['controls'] + if controls: + for name, value in controls.items(): + min, max, default = value.values() + str_first = f"{name} ({get_type_str(min)})" + str_second = f"min={min} max={max} default={default}" + str_indent = (30 - len(str_first)) * ' ' + ': ' + log_info(str_first + str_indent + str_second, indentation) + else: + log_info("apt package 'python3-libcamera' is not installed! \ +Make sure to install it to log the controls!", indentation) + +def get_type_str(obj) -> str: + return str(type(obj)).split('\'')[1] + +def log_uvc_formats(properties: dict) -> None: + log_info(f"Supported Formats:", '') + log_multiline(properties['formats'], log_info, indentation) + +def log_uvc_v4l2ctrls(properties: dict) -> None: + log_info(f"Supported Controls:", '') + log_multiline(properties['v4l2ctrls'], log_info, indentation) \ No newline at end of file diff --git a/pylibs/parameter.py b/pylibs/parameter.py index 2414fc6c..6ce20279 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -7,7 +7,7 @@ def set_value(self, value): try: if value is None: self.value = None - elif self.type == 'bool': + elif self.type == bool: if value.lower() == 'true': self.value = True elif value.lower() == 'false': diff --git a/pylibs/section.py b/pylibs/section.py index 85802cb3..aa6eeb3f 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -2,8 +2,10 @@ from configparser import SectionProxy from .parameter import Parameter +from . import logger class Section: + section_name = 'Section' keyword = 'Section' available_sections = {} # Section looks like this: @@ -15,7 +17,8 @@ def __init__(self, name: str = '') -> None: self.parameters: dict[str, Parameter] = {} # Parse config according to the needs of the section - def parse_config(self, config_section: SectionProxy, *args, **kwargs): + def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: + success = True for parameter in config_section: value = config_section[parameter] if parameter not in self.parameters: @@ -23,6 +26,11 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs): continue value = value.split('#')[0].strip() self.parameters[parameter].set_value(value) + for parameter in self.parameters: + if self.parameters[parameter].value is None: + logger.log_error(f"Parameter {parameter} not found in Section [{self.section_name} {self.name}]") + success = False + return success # Execute section specific stuff, e.g. starting cam async def execute(self): diff --git a/pylibs/streamer/camera-streamer.py b/pylibs/streamer/camera-streamer.py index d06e2b57..6e99fa78 100644 --- a/pylibs/streamer/camera-streamer.py +++ b/pylibs/streamer/camera-streamer.py @@ -1,7 +1,7 @@ from configparser import SectionProxy from .streamer import Streamer from ..parameter import Parameter -from ..core import execute_command +from ..core import execute_command, get_executable class Camera_Streamer(Streamer): keyword = 'ustreamer' @@ -10,11 +10,16 @@ def __init__(self, name: str = '') -> None: super().__init__(name) self.parameters.update({ - 'enable_rtsp': Parameter(bool, False), + 'enable_rtsp': Parameter(bool, 'False'), 'rtsp_port': Parameter(int, 8554) }) - self.binary_path = 'bin/ustreamer/ustreamer' + if Camera_Streamer.binary_path is None: + Camera_Streamer.binary_path = get_executable( + ['ustreamer.bin', 'ustreamer'], + ['bin/ustreamer'] + ) + self.binary_path = get_executable.binary_path async def execute(self): host = '0.0.0.0' if self.parameters['no_proxy'].value else '127.0.0.1' diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py index 23c994b1..970ae68f 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/streamer/streamer.py @@ -1,11 +1,11 @@ from ..section import Section from ..parameter import Parameter +from .. import logger from configparser import SectionProxy import os -import logging class Streamer(Section): - keyword = '' + binary_path = None def __init__(self, name: str = '') -> None: super().__init__(name) @@ -16,22 +16,29 @@ def __init__(self, name: str = '') -> None: 'device': Parameter(str), 'resolution': Parameter(str), 'max_fps': Parameter(int), - 'no_proxy': Parameter(bool, False), + 'no_proxy': Parameter(bool, 'False'), 'custom_flags': Parameter(str, ''), 'v4l2ctl': Parameter(str, '') }) self.binary_path = None + + self.missing_bin_txt = """\ +'%s' executable not found! +Please make sure everything is installed correctly and up to date! +Run 'make update' inside the crowsnest directory to install and update everything.""" - def parse_config(self, config_section: SectionProxy, *args, **kwargs): - super().parse_config(config_section, *args, **kwargs) + def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: + success = super().parse_config(config_section, *args, **kwargs) if self.binary_path is None: - raise Exception("""This shouldn't happen. Please join our discord and open a post inside the support forum!\nhttps://discord.gg/mainsail""") + logger.log_multiline(self.missing_bin_txt % self.parameters['mode'].value, logger.log_error) + return False + return success def execute(self): if not os.path.exists(self.binary_path): - logging.info(f"'{self.binary_path}' not found! Please make sure that everything is installed correctly!") + logger.log_multiline(self.missing_bin_txt, logger.log_error) return False return True def load_module(): - raise NotImplementedError("If you see this, a module is implemented wrong!!!") \ No newline at end of file + raise NotImplementedError("If you see this, a Streamer module is implemented wrong!!!") diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index aae6dbf3..16ecb8b2 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,50 +1,75 @@ import re +import os from .streamer import Streamer -from ..core import execute_command -from ..logger import log_debug +from ..core import execute_command, get_executable +from .. import logger +from ..hwhandler import is_device_legacy, has_device_mjpg_hw class Ustreamer(Streamer): + section_name = 'cam' keyword = 'ustreamer' - binary_path = None def __init__(self, name: str = '') -> None: super().__init__(name) if Ustreamer.binary_path is None: - Ustreamer.binary_path = 'bin/ustreamer/ustreamer' + Ustreamer.binary_path = get_executable( + ['ustreamer.bin', 'ustreamer'], + ['bin/ustreamer'] + ) self.binary_path = Ustreamer.binary_path async def execute(self): if not super().execute(): return None - host = '0.0.0.0' if self.parameters['no_proxy'].value else '127.0.0.1' + if self.parameters['no_proxy'].value: + host = '0.0.0.0' + logger.log_info("Set to 'no_proxy' mode! Using 0.0.0.0!") + else: + host = '127.0.0.1' port = self.parameters['port'].value res = self.parameters['resolution'].value fps = self.parameters['max_fps'].value + device = self.parameters['device'].value streamer_args = [ - self.binary_path, '--host', host, '--port', str(port), '--resolution', res, '--desired-fps', str(fps), # webroot & allow crossdomain requests - '--allow-origin=\*', - '--static', '"ustreamer-www"', - '--device', '/dev/video0', - '--format', 'MJPEG', - '--encoder', 'HW' + '--allow-origin', '\*', + '--static', '"ustreamer-www"' ] + if is_device_legacy(device): + streamer_args += [ + '--format', 'MJPEG', + '--device-timeout', '5', + '--buffers', '3' + ] + else: + streamer_args += [ + '--device', device, + '--device-timeout', '2' + ] + if has_device_mjpg_hw(device): + streamer_args += [ + '--format', 'MJPEG', + '--encoder', 'HW' + ] + # custom flags streamer_args += self.parameters['custom_flags'].value.split() - cmd = streamer_args + cmd = self.binary_path + ' ' + ' '.join(streamer_args) log_pre = f'ustreamer [cam {self.name}]: ' + logger.log_info(f"Starting ustreamer with Device {device} ...") + logger.log_debug(f"Parameters: {' '.join(streamer_args)}") process,_,_ = await execute_command( - ' '.join(cmd), + cmd, error_log_pre=log_pre, error_log_func=self.custom_log ) @@ -56,7 +81,7 @@ def custom_log(self, msg: str): msg = msg[:-28] else: msg = re.sub(r'-- (.*?) \[.*?\] --', r'\1', msg) - log_debug(msg) + logger.log_debug(msg) def load_module(): From 0dfb3a60991473ca61cb7fb7aaa2074735464fe9 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 2 Mar 2024 00:20:56 +0100 Subject: [PATCH 038/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 10 ++-- pylibs/core.py | 2 +- pylibs/hwhandler.py | 13 +---- pylibs/logger.py | 4 +- pylibs/parameter.py | 6 +- pylibs/section.py | 8 +-- pylibs/streamer/camera-streamer.py | 93 ++++++++++++++++++++++-------- pylibs/streamer/streamer.py | 6 +- pylibs/streamer/ustreamer.py | 12 ++-- pylibs/v4l2_control.py | 33 ++++++++++- 10 files changed, 130 insertions(+), 57 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 5238a368..4b719275 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -43,11 +43,11 @@ async def start_processes(): section_class = get_module_class('pylibs', section_keyword) section_name = ' '.join(section_header[1:]) section_object = section_class(section_name) - section_object.parse_config(config[section]) - - if section_object == None: - print(f"Section [{section}] couldn't get parsed") - sec_objs.append(section_object) + if section_object.parse_config(config[section]): + sec_objs.append(section_object) + logger.log_info(f"Configuration of section [{section}] looks good. Continue ...") + else: + logger.log_error(f"Failed to parse config for section [{section}]!") for section_object in sec_objs: task = asyncio.create_task(section_object.execute()) diff --git a/pylibs/core.py b/pylibs/core.py index c3c28681..4e74481d 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -76,7 +76,7 @@ def execute_shell_command(command: str, strip: bool = True) -> str: def bytes_to_gigabytes(value: int) -> int: return math.round(value / 1024 / 1024 / 1024) -def get_executable(names: list, paths: list) -> str: +def get_executable(names: list[str], paths: list[str]) -> str: for name in names: exec = shutil.which(name) if exec: diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 090b8e09..7282ade2 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -3,6 +3,7 @@ import re from . import core +from .v4l2_control import get_uvc_formats, get_uvc_v4l2ctrls avail_cams = { 'uvc': {}, @@ -26,18 +27,6 @@ def get_avail_uvc_dev() -> dict: avail_cams['uvc'].update(cams) return cams -def get_uvc_formats(cam_path: str) -> str: - command = f'v4l2-ctl -d {cam_path} --list-formats-ext' - formats = core.execute_shell_command(command) - # Remove first 3 lines - formats = '\n'.join(formats.split('\n')[3:]) - return formats - -def get_uvc_v4l2ctrls(cam_path: str) -> str: - command = f'v4l2-ctl -d {cam_path} --list-ctrls-menus' - v4l2ctrls = core.execute_shell_command(command) - return v4l2ctrls - def has_device_mjpg_hw(cam_path: str) -> bool: global avail_cams return 'Motion-JPEG, compressed' in get_uvc_formats(cam_path) diff --git a/pylibs/logger.py b/pylibs/logger.py index cc129f8f..7f4597d7 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -23,8 +23,8 @@ def setup_logging(log_path): def set_log_level(level): logging.getLogger().setLevel(level) -def log_quiet(msg): - logging.log(QUIET, msg) +def log_quiet(msg, prefix=''): + logging.log(QUIET, prefix + msg) def log_info(msg, prefix='INFO: '): logging.info(prefix + msg) diff --git a/pylibs/parameter.py b/pylibs/parameter.py index 6ce20279..b90dffbc 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -1,3 +1,5 @@ +from . import logger + class Parameter: def __init__(self, type=str, default=None) -> None: self.type = type @@ -12,7 +14,9 @@ def set_value(self, value): self.value = True elif value.lower() == 'false': self.value = False + else: + logger.log_error(f"{value} is not 'true' or 'false'! Parameter ignored!") else: self.value = self.type(value) except ValueError: - print(f"Error: {value} is not of type {self.type}") + logger.log_error(f"{value} is not of type {self.type}! Parameter ignored!") diff --git a/pylibs/section.py b/pylibs/section.py index aa6eeb3f..188e64e0 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -5,8 +5,8 @@ from . import logger class Section: - section_name = 'Section' - keyword = 'Section' + section_name = 'section' + keyword = 'section' available_sections = {} # Section looks like this: # [ ] @@ -22,13 +22,13 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: for parameter in config_section: value = config_section[parameter] if parameter not in self.parameters: - print(f"Warning: Parameter {parameter} is not supported by {self.keyword}") + print(f"Warning: Parameter '{parameter}' is not supported by {self.keyword}") continue value = value.split('#')[0].strip() self.parameters[parameter].set_value(value) for parameter in self.parameters: if self.parameters[parameter].value is None: - logger.log_error(f"Parameter {parameter} not found in Section [{self.section_name} {self.name}]") + logger.log_error(f"Parameter '{parameter}' not found in section [{self.section_name} {self.name}]") success = False return success diff --git a/pylibs/streamer/camera-streamer.py b/pylibs/streamer/camera-streamer.py index 6e99fa78..3145c056 100644 --- a/pylibs/streamer/camera-streamer.py +++ b/pylibs/streamer/camera-streamer.py @@ -1,10 +1,11 @@ -from configparser import SectionProxy from .streamer import Streamer from ..parameter import Parameter from ..core import execute_command, get_executable +from ..hwhandler import has_device_mjpg_hw +from .. import logger class Camera_Streamer(Streamer): - keyword = 'ustreamer' + keyword = 'camera-streamer' def __init__(self, name: str = '') -> None: super().__init__(name) @@ -16,38 +17,82 @@ def __init__(self, name: str = '') -> None: if Camera_Streamer.binary_path is None: Camera_Streamer.binary_path = get_executable( - ['ustreamer.bin', 'ustreamer'], - ['bin/ustreamer'] + ['camera-streamer'], + ['bin/camera-streamer'] ) - self.binary_path = get_executable.binary_path - + self.binary_path = Camera_Streamer.binary_path + async def execute(self): - host = '0.0.0.0' if self.parameters['no_proxy'].value else '127.0.0.1' + if not super().execute(): + return None + if self.parameters['no_proxy'].value: + host = '0.0.0.0' + logger.log_info("Set to 'no_proxy' mode! Using 0.0.0.0!") + else: + host = '127.0.0.1' port = self.parameters['port'].value - res = self.parameters['resolution'].value + res = self.parameters['resolution'].value.split('x') + width = res[0] + height = res[1] + fps = self.parameters['max_fps'].value + device = self.parameters['device'].value streamer_args = [ - self.binary_path, - '--host', host, - '--port', str(port), - '--resolution', res, - '--desired-fps', str(fps), - # webroot & allow crossdomain requests - '--allow-origin=\*', - '--static', '"ustreamer-www"', - '--device', '/dev/video0', - '--format', 'MJPEG', - '--encoder', 'HW' + '--camera-path=' + device, + '--http-listen=' + host, + '--http-port=' + str(port), + '--camera-fps=' + str(fps), + '--camera-width=' + width, + '--camera-height=' + height, + '--camera-snapshot.height=' + height, + '--camera-video.height=' + height, + '--camera-stream.height=' + height, + '--camera-auto_reconnect=1' ] - + + v4l2ctl = self.parameters['v4l2ctl'].value + if v4l2ctl: + prefix = "V4L2 Control: " + logger.log_quiet(f"Handling done by camera-streamer", prefix) + logger.log_quiet(f"Trying to set: {v4l2ctl}", prefix) + for ctrl in v4l2ctl.split(','): + streamer_args += [f'--camera-options={ctrl}'] + + if device.startswith('/base') and 'i2c' in device: + streamer_args += [ + '--camera-type=libcamera', + '--camera-format=YUYV' + ] + elif device.startswith('/dev/video') or device.startswith('/dev/v4l'): + streamer_args += [ + '--camera-type=v4l2' + ] + if has_device_mjpg_hw(device): + streamer_args += [ + '--camera-format=MJPEG' + ] + + if self.parameters['enable_rtsp'].value: + streamer_args += [ + '--rtsp-port=' + str(self.parameters['rtsp_port'].value) + ] + # custom flags streamer_args += self.parameters['custom_flags'].value.split() - cmd = streamer_args - info_log_pre = f'DEBUG: ustreamer [{self.name}]: ' - return await execute_command(' '.join(cmd), info_log_pre=info_log_pre) - #ustreamer = subprocess.Popen(['bin/ustreamer/ustreamer'] + streamer_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd = self.binary_path + ' ' + ' '.join(streamer_args) + log_pre = f'ustreamer [cam {self.name}]: ' + + # logger.log_quiet(f"Starting ustreamer with device {device} ...") + logger.log_debug(f"Parameters: {' '.join(streamer_args)}") + process,_,_ = await execute_command( + cmd, + error_log_pre=log_pre, + error_log_func=logger.log_debug + ) + + return process def load_module(): diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py index 970ae68f..876e6250 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/streamer/streamer.py @@ -30,7 +30,8 @@ def __init__(self, name: str = '') -> None: def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: success = super().parse_config(config_section, *args, **kwargs) if self.binary_path is None: - logger.log_multiline(self.missing_bin_txt % self.parameters['mode'].value, logger.log_error) + logger.log_multiline(self.missing_bin_txt % self.parameters['mode'].value, + logger.log_error) return False return success @@ -38,6 +39,9 @@ def execute(self): if not os.path.exists(self.binary_path): logger.log_multiline(self.missing_bin_txt, logger.log_error) return False + logger.log_quiet( + f"Starting {self.keyword} with device {self.parameters['device'].value} ..." + ) return True def load_module(): diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 16ecb8b2..6990b775 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,10 +1,10 @@ import re -import os from .streamer import Streamer from ..core import execute_command, get_executable -from .. import logger from ..hwhandler import is_device_legacy, has_device_mjpg_hw +from ..v4l2_control import set_v4l2ctrl +from .. import logger class Ustreamer(Streamer): section_name = 'cam' @@ -60,14 +60,18 @@ async def execute(self): '--encoder', 'HW' ] + v4l2ctl = self.parameters['v4l2ctl'].value + if v4l2ctl: + set_v4l2ctrl(f'[cam {self.name}]', device, v4l2ctl.split(',')) + # custom flags streamer_args += self.parameters['custom_flags'].value.split() cmd = self.binary_path + ' ' + ' '.join(streamer_args) log_pre = f'ustreamer [cam {self.name}]: ' - logger.log_info(f"Starting ustreamer with Device {device} ...") - logger.log_debug(f"Parameters: {' '.join(streamer_args)}") + # logger.log_quiet(f"Starting ustreamer with device {device} ...") + logger.log_debug(log_pre + f"Parameters: {' '.join(streamer_args)}") process,_,_ = await execute_command( cmd, error_log_pre=log_pre, diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py index 86c21fae..7ea418aa 100644 --- a/pylibs/v4l2_control.py +++ b/pylibs/v4l2_control.py @@ -1,6 +1,33 @@ -import os -from v4l2py import Device +from . import core, logger +def get_uvc_formats(cam_path: str) -> str: + command = f'v4l2-ctl -d {cam_path} --list-formats-ext' + formats = core.execute_shell_command(command) + # Remove first 3 lines + formats = '\n'.join(formats.split('\n')[3:]) + return formats +def get_uvc_v4l2ctrls(cam_path: str) -> str: + command = f'v4l2-ctl -d {cam_path} --list-ctrls-menus' + v4l2ctrls = core.execute_shell_command(command) + return v4l2ctrls -print(os.popen('v4l2-ctl --list-devices').read()) +def set_v4l2ctrl(section: str, cam_path: str, ctrls: list[str] = None) -> str: + prefix = "V4L2 Control: " + if not ctrls: + logger.log_quiet(f"No parameters set for {section}. Skipped.", prefix) + return + logger.log_quiet(f"Device: {section}", prefix) + logger.log_quiet(f"Options: {', '.join(ctrls)}", prefix) + avail_ctrls = get_uvc_v4l2ctrls(cam_path) + for ctrl in ctrls: + if ctrl.split['='][0].strip().lower() not in avail_ctrls: + logger.log_quiet( + f"Parameter '{ctrl}' not available for '{cam_path}'. Skipped.", + prefix + ) + continue + command = f'v4l2-ctl -d {cam_path} -c {ctrl.strip()}' + v4l2ctrls = core.execute_shell_command(command) + if not v4l2ctrls: + logger.log_quiet(f"Failed to set parameter: '{ctrl.strip()}'", prefix) From 4e6d1016a593422695411c7690bd34e59669d344 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 2 Mar 2024 20:01:47 +0100 Subject: [PATCH 039/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 3 +++ pylibs/streamer/ustreamer.py | 13 ++++++++++-- pylibs/v4l2_control.py | 39 +++++++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index 7f4597d7..a7b09a47 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -32,6 +32,9 @@ def log_info(msg, prefix='INFO: '): def log_debug(msg, prefix='DEBUG: '): logging.log(DEBUG, prefix + msg) +def log_warning(msg, prefix='WARN: '): + logging.warning(prefix + msg) + def log_error(msg, prefix='ERROR: '): logging.error(prefix + msg) diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 6990b775..1170fd6f 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -1,9 +1,10 @@ import re +import asyncio from .streamer import Streamer from ..core import execute_command, get_executable from ..hwhandler import is_device_legacy, has_device_mjpg_hw -from ..v4l2_control import set_v4l2ctrl +from ..v4l2_control import set_v4l2ctrls, blockyfix, brokenfocus from .. import logger class Ustreamer(Streamer): @@ -49,6 +50,7 @@ async def execute(self): '--device-timeout', '5', '--buffers', '3' ] + blockyfix(device) else: streamer_args += [ '--device', device, @@ -62,7 +64,7 @@ async def execute(self): v4l2ctl = self.parameters['v4l2ctl'].value if v4l2ctl: - set_v4l2ctrl(f'[cam {self.name}]', device, v4l2ctl.split(',')) + set_v4l2ctrls(f'[cam {self.name}]', device, v4l2ctl.split(',')) # custom flags streamer_args += self.parameters['custom_flags'].value.split() @@ -77,6 +79,13 @@ async def execute(self): error_log_pre=log_pre, error_log_func=self.custom_log ) + asyncio.sleep(0.5) + + for ctl in v4l2ctl.split(','): + if 'focus_absolute' in ctl: + focus_absolute = ctl.split('=')[1].strip() + brokenfocus(device, focus_absolute) + break return process diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py index 7ea418aa..11d7dd09 100644 --- a/pylibs/v4l2_control.py +++ b/pylibs/v4l2_control.py @@ -12,7 +12,12 @@ def get_uvc_v4l2ctrls(cam_path: str) -> str: v4l2ctrls = core.execute_shell_command(command) return v4l2ctrls -def set_v4l2ctrl(section: str, cam_path: str, ctrls: list[str] = None) -> str: +def set_v4l2_ctrl(cam_path: str, ctrl: str) -> str: + command = f'v4l2-ctl -d {cam_path} -c {ctrl}' + v4l2ctrl = core.execute_shell_command(command) + return v4l2ctrl + +def set_v4l2ctrls(section: str, cam_path: str, ctrls: list[str] = None) -> str: prefix = "V4L2 Control: " if not ctrls: logger.log_quiet(f"No parameters set for {section}. Skipped.", prefix) @@ -21,13 +26,37 @@ def set_v4l2ctrl(section: str, cam_path: str, ctrls: list[str] = None) -> str: logger.log_quiet(f"Options: {', '.join(ctrls)}", prefix) avail_ctrls = get_uvc_v4l2ctrls(cam_path) for ctrl in ctrls: - if ctrl.split['='][0].strip().lower() not in avail_ctrls: + if ctrl.split('=')[0].strip().lower() not in avail_ctrls: logger.log_quiet( f"Parameter '{ctrl}' not available for '{cam_path}'. Skipped.", prefix ) continue - command = f'v4l2-ctl -d {cam_path} -c {ctrl.strip()}' - v4l2ctrls = core.execute_shell_command(command) - if not v4l2ctrls: + v4l2ctrl = set_v4l2_ctrl(cam_path, ctrl.strip()) + if not v4l2ctrl: logger.log_quiet(f"Failed to set parameter: '{ctrl.strip()}'", prefix) + logger.log_multiline(get_uvc_v4l2ctrls(cam_path), logger.log_debug) + +def get_cur_v4l2_value(cam_path: str, ctrl: str) -> str: + command = f'v4l2-ctl -d {cam_path} -C {ctrl}' + value = core.execute_shell_command(command) + if value: + return value.split(':')[1].strip() + return value + +def brokenfocus(cam_path: str, focus_absolute_conf: str) -> str: + cur_val = get_cur_v4l2_value(cam_path, 'focus_absolute') + if cur_val and cur_val != focus_absolute_conf: + logger.log_warning(f"Detected 'brokenfocus' device.") + logger.log_info(f"Trying to set to configured Value.") + set_v4l2_ctrl(cam_path, f'focus_absolute={focus_absolute_conf}') + logger.log_debug(f"Value is now: {get_cur_v4l2_value(cam_path, 'focus_absolute')}") + +# This function is to set bitrate on raspicams. +# If raspicams set to variable bitrate, they tend to show +# a "block-like" view after reboots +# To prevent that blockyfix should apply constant bitrate befor start of ustreamer +# See https://github.com/mainsail-crew/crowsnest/issues/33 +def blockyfix(device: str): + set_v4l2_ctrl(device, 'video_bitrate_mode=1') + set_v4l2_ctrl(device, 'video_bitrate=15000000') From be9d08ad8385ebbd23f7c03be0cd23aca7178bf8 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 3 Mar 2024 00:20:28 +0100 Subject: [PATCH 040/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 36 ++++++++++++++++++++++++++++-------- pylibs/core.py | 15 ++++++++++----- pylibs/watchdog.py | 17 +++++++++++++++++ 3 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 pylibs/watchdog.py diff --git a/crowsnest.py b/crowsnest.py index 4b719275..53a629a5 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -2,6 +2,7 @@ import configparser from pylibs.crowsnest import Crowsnest from pylibs.core import get_module_class +from pylibs.watchdog import crowsnest_watchdog import pylibs.logger as logger import pylibs.logging as logging @@ -18,6 +19,8 @@ args = parser.parse_args() +watchdog_running = True + def parse_config(): global crowsnest, config, args config_path = args.config @@ -27,6 +30,7 @@ def parse_config(): logger.set_log_level(crowsnest.parameters['log_level'].value) async def start_processes(): + global config, watchdog_running sec_objs = [] sec_exec_tasks = set() @@ -62,15 +66,31 @@ async def start_processes(): for task in sec_exec_tasks: if task != None: task.cancel() + watchdog_running = False + +async def run_watchdog(): + global watchdog_running + while watchdog_running: + await asyncio.sleep(120) + crowsnest_watchdog() + -logger.setup_logging(args.log_path) -logging.log_initial() +async def main(): + global args + logger.setup_logging(args.log_path) + logging.log_initial() -parse_config() + parse_config() -logging.log_host_info() -logging.log_config(args.config) -logging.log_cams() + logging.log_host_info() + logging.log_config(args.config) + logging.log_cams() -# Run async to wait for all tasks to finish -asyncio.run(start_processes()) + asyncio.gather(start_processes(), run_watchdog()) + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(main()) + finally: + loop.close() diff --git a/pylibs/core.py b/pylibs/core.py index 4e74481d..dd957f26 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -76,15 +76,20 @@ def execute_shell_command(command: str, strip: bool = True) -> str: def bytes_to_gigabytes(value: int) -> int: return math.round(value / 1024 / 1024 / 1024) +def find_file(name: str, path: str) -> str: + for dpath, _, fnames in os.walk(path): + for fname in fnames: + if fname == name: + return os.path.join(dpath, fname) + return None + def get_executable(names: list[str], paths: list[str]) -> str: for name in names: exec = shutil.which(name) if exec: return exec for path in paths: - for dpath, _, fnames in os.walk(path): - for fname in fnames: - if fname == name: - exec = os.path.join(dpath, fname) - return exec + found = find_file(name, path) + if found: + return found return None diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py new file mode 100644 index 00000000..416deb16 --- /dev/null +++ b/pylibs/watchdog.py @@ -0,0 +1,17 @@ +import os +from . import logger + +configured_devices = [] + +def crowsnest_watchdog(): + global configured_devices + prefix = "Crowsnest Watchdog: " + lost_devices = [] + + for device in configured_devices: + if not os.path.exists(device): + lost_devices.append(device) + logger.log_quiet("Lost Devicve: '{device}'", prefix) + elif device in lost_devices and os.path.exists(device): + lost_devices.remove(device) + logger.log_quiet("Device '{device}' returned.", prefix) From 50c6337db1cbebbf81dfc6073119040e53298d05 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 3 Mar 2024 12:23:20 +0100 Subject: [PATCH 041/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest | 72 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/crowsnest b/crowsnest index 39669681..b1851046 100755 --- a/crowsnest +++ b/crowsnest @@ -19,19 +19,19 @@ set -Ee # Base Path BASE_CN_PATH="$(dirname "$(readlink -f "${0}")")" -## Import Librarys -# shellcheck source-path=SCRIPTDIR/../libs/ -. "${BASE_CN_PATH}/libs/camera-streamer.sh" -. "${BASE_CN_PATH}/libs/configparser.sh" -. "${BASE_CN_PATH}/libs/core.sh" -. "${BASE_CN_PATH}/libs/hwhandler.sh" -. "${BASE_CN_PATH}/libs/init_stream.sh" -. "${BASE_CN_PATH}/libs/logging.sh" -. "${BASE_CN_PATH}/libs/messages.sh" -. "${BASE_CN_PATH}/libs/ustreamer.sh" -. "${BASE_CN_PATH}/libs/v4l2_control.sh" -. "${BASE_CN_PATH}/libs/versioncontrol.sh" -. "${BASE_CN_PATH}/libs/watchdog.sh" +# ## Import Librarys +# # shellcheck source-path=SCRIPTDIR/../libs/ +# . "${BASE_CN_PATH}/libs/camera-streamer.sh" +# . "${BASE_CN_PATH}/libs/configparser.sh" +# . "${BASE_CN_PATH}/libs/core.sh" +# . "${BASE_CN_PATH}/libs/hwhandler.sh" +# . "${BASE_CN_PATH}/libs/init_stream.sh" +# . "${BASE_CN_PATH}/libs/logging.sh" +# . "${BASE_CN_PATH}/libs/messages.sh" +# . "${BASE_CN_PATH}/libs/ustreamer.sh" +# . "${BASE_CN_PATH}/libs/v4l2_control.sh" +# . "${BASE_CN_PATH}/libs/versioncontrol.sh" +# . "${BASE_CN_PATH}/libs/watchdog.sh" #### MAIN ## Args given? @@ -74,16 +74,38 @@ while getopts ":vhc:s:d" arg; do esac done -init_logging -initial_check -construct_streamer +# init_logging +# initial_check +# construct_streamer -## Loop and Watchdog -## In this case watchdog acts more like a "cable defect detector" -## The User gets a message if Device is lost. -clean_watchdog -while true ; do - crowsnest_watchdog - sleep 120 & sleep_pid="$!" - wait "${sleep_pid}" -done +# ## Loop and Watchdog +# ## In this case watchdog acts more like a "cable defect detector" +# ## The User gets a message if Device is lost. +# clean_watchdog +# while true ; do +# crowsnest_watchdog +# sleep 120 & sleep_pid="$!" +# wait "${sleep_pid}" +# done + +function set_log_path { + #Workaround sed ~ to BASH VAR $HOME + CROWSNEST_LOG_PATH=$(get_param "crowsnest" log_path | sed "s#^~#${HOME}#gi") + declare -g CROWSNEST_LOG_PATH + #Workaround: Make Dir if not exist + if [ ! -d "$(dirname "${CROWSNEST_LOG_PATH}")" ]; then + mkdir -p "$(dirname "${CROWSNEST_LOG_PATH}")" + fi +} + +function get_param { + local cfg section param + cfg="${CROWSNEST_CFG}" + section="${1}" + param="${2}" + crudini --get "${cfg}" "${section}" "${param}" 2> /dev/null | \ + sed 's/\#.*//;s/[[:space:]]*$//' + return +} + +python3 -m "${BASE_CN_PATH}/crowsnest.py" -c "${CROWSNEST_CFG}" -l "${CROWSNEST_LOG_PATH}" From e748d1ff307cb26c26fbbfce069e221ecc49ec86 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 3 Mar 2024 12:24:33 +0100 Subject: [PATCH 042/129] chore: wip Signed-off-by: Patrick Gehrsitz --- resources/crowsnest.conf | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/resources/crowsnest.conf b/resources/crowsnest.conf index 7a8bf914..9414e44d 100644 --- a/resources/crowsnest.conf +++ b/resources/crowsnest.conf @@ -29,8 +29,8 @@ [crowsnest] -log_path: resources/crowsnest.log # Path to log file -log_level: debug # Valid Options are quiet/verbose/debug +log_path: %LOGPATH% +log_level: verbose # Valid Options are quiet/verbose/debug delete_log: false # Deletes log on every restart, if set to true no_proxy: false @@ -38,23 +38,10 @@ no_proxy: false mode: ustreamer # ustreamer - Provides mjpg and snapshots. (All devices) # camera-streamer - Provides webrtc, mjpg and snapshots. (rpi + Raspi OS based only) enable_rtsp: false # If camera-streamer is used, this enables also usage of an rtsp server -rtsp_port: 8554a # Set different ports for each device! +rtsp_port: 8554 # Set different ports for each device! port: 8080 # HTTP/MJPG Stream/Snapshot Port device: /dev/video0 # See Log for available ... resolution: 640x480 # widthxheight format max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. -custom_flags: # You can run the Stream Services with custom flags. -#v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see Log what your cam is capable of. -no_proxy: true - -#[cam 2] -#mode: ustreamer # ustreamer - Provides mjpg and snapshots. (All devices) -# # camera-streamer - Provides webrtc, mjpg and snapshots. (rpi + Raspi OS based only) -#enable_rtsp: false # If camera-streamer is used, this enables also usage of an rtsp server -#rtsp_port: 8554 # Set different ports for each device! -#port: 8080 # HTTP/MJPG Stream/Snapshot Port -#device: /dev/video0 # See Log for available ... -#resolution: 640x480 # widthxheight format -#max_fps: 15 # If Hardware Supports this it will be forced, otherwise ignored/coerced. #custom_flags: # You can run the Stream Services with custom flags. -#v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see Log what your cam is capable of. +#v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see Log what your cam is capable of. \ No newline at end of file From 6cb61764f17407e0d154feb41a08e19e78119a9e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 3 Mar 2024 14:46:02 +0100 Subject: [PATCH 043/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest | 16 +++++++++++++++- crowsnest.py | 7 ++++++- pylibs/streamer/ustreamer.py | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crowsnest b/crowsnest index b1851046..6126376e 100755 --- a/crowsnest +++ b/crowsnest @@ -33,6 +33,20 @@ BASE_CN_PATH="$(dirname "$(readlink -f "${0}")")" # . "${BASE_CN_PATH}/libs/versioncontrol.sh" # . "${BASE_CN_PATH}/libs/watchdog.sh" +function missing_args_msg { + echo -e "crowsnest: Missing Arguments!" + echo -e "\n\tTry: crowsnest -h\n" +} + +function check_cfg { + if [ ! -r "${1}" ]; then + log_msg "ERROR: No Configuration File found. Exiting!" + exit 1 + else + return 0 + fi +} + #### MAIN ## Args given? if [ "$#" -eq 0 ]; then @@ -108,4 +122,4 @@ function get_param { return } -python3 -m "${BASE_CN_PATH}/crowsnest.py" -c "${CROWSNEST_CFG}" -l "${CROWSNEST_LOG_PATH}" +python3 "${BASE_CN_PATH}/crowsnest.py" -c "${CROWSNEST_CFG}" -l "${CROWSNEST_LOG_PATH}" diff --git a/crowsnest.py b/crowsnest.py index 53a629a5..42de4428 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -86,7 +86,12 @@ async def main(): logging.log_config(args.config) logging.log_cams() - asyncio.gather(start_processes(), run_watchdog()) + task1 = asyncio.create_task(start_processes()) + task2 = asyncio.create_task(run_watchdog()) + await task1 + await task2 + + # asyncio.gather(start_processes(), run_watchdog()) if __name__ == "__main__": loop = asyncio.get_event_loop() diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index 1170fd6f..ba582fc6 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -79,7 +79,7 @@ async def execute(self): error_log_pre=log_pre, error_log_func=self.custom_log ) - asyncio.sleep(0.5) + await asyncio.sleep(0.5) for ctl in v4l2ctl.split(','): if 'focus_absolute' in ctl: From f72d518cd26a008a62ec2c6fe6d35ab36ce6fb89 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 13:11:54 +0100 Subject: [PATCH 044/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/streamer/streamer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py index 876e6250..3dc7e60c 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/streamer/streamer.py @@ -1,6 +1,7 @@ from ..section import Section from ..parameter import Parameter from .. import logger +from ..watchdog import configured_devices from configparser import SectionProxy import os @@ -36,12 +37,14 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: return success def execute(self): + global configured_devices if not os.path.exists(self.binary_path): logger.log_multiline(self.missing_bin_txt, logger.log_error) return False logger.log_quiet( f"Starting {self.keyword} with device {self.parameters['device'].value} ..." ) + configured_devices.append(self.parameters['device'].value) return True def load_module(): From a1d0aefe3d0ea7c35833e39df93195c69d3d15e6 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 13:15:11 +0100 Subject: [PATCH 045/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest | 1 + 1 file changed, 1 insertion(+) diff --git a/crowsnest b/crowsnest index 6126376e..9b11ffb9 100755 --- a/crowsnest +++ b/crowsnest @@ -122,4 +122,5 @@ function get_param { return } +set_log_path python3 "${BASE_CN_PATH}/crowsnest.py" -c "${CROWSNEST_CFG}" -l "${CROWSNEST_LOG_PATH}" From d0efab64e0f12076e01e15525a07b4e219316b61 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 13:17:59 +0100 Subject: [PATCH 046/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/streamer/camera-streamer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pylibs/streamer/camera-streamer.py b/pylibs/streamer/camera-streamer.py index 3145c056..17734333 100644 --- a/pylibs/streamer/camera-streamer.py +++ b/pylibs/streamer/camera-streamer.py @@ -40,7 +40,7 @@ async def execute(self): streamer_args = [ '--camera-path=' + device, - '--http-listen=' + host, + # '--http-listen=' + host, '--http-port=' + str(port), '--camera-fps=' + str(fps), '--camera-width=' + width, @@ -82,9 +82,8 @@ async def execute(self): streamer_args += self.parameters['custom_flags'].value.split() cmd = self.binary_path + ' ' + ' '.join(streamer_args) - log_pre = f'ustreamer [cam {self.name}]: ' + log_pre = f'camera-streamer [cam {self.name}]: ' - # logger.log_quiet(f"Starting ustreamer with device {device} ...") logger.log_debug(f"Parameters: {' '.join(streamer_args)}") process,_,_ = await execute_command( cmd, From 5c8c729590acf4d5de009f3f86502ba870592e7e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 13:20:30 +0100 Subject: [PATCH 047/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/streamer/camera-streamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/streamer/camera-streamer.py b/pylibs/streamer/camera-streamer.py index 17734333..b50f3ec9 100644 --- a/pylibs/streamer/camera-streamer.py +++ b/pylibs/streamer/camera-streamer.py @@ -57,7 +57,7 @@ async def execute(self): logger.log_quiet(f"Handling done by camera-streamer", prefix) logger.log_quiet(f"Trying to set: {v4l2ctl}", prefix) for ctrl in v4l2ctl.split(','): - streamer_args += [f'--camera-options={ctrl}'] + streamer_args += [f'--camera-options={ctrl.strip()}'] if device.startswith('/base') and 'i2c' in device: streamer_args += [ From 9aa21b201020db9e0e7eb656724f333b527431d4 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:04:11 +0100 Subject: [PATCH 048/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 16 ++++++++----- pylibs/core.py | 1 - pylibs/logger.py | 30 ++++++++++++++++-------- pylibs/{logging.py => logging_helper.py} | 0 4 files changed, 30 insertions(+), 17 deletions(-) rename pylibs/{logging.py => logging_helper.py} (100%) diff --git a/crowsnest.py b/crowsnest.py index 42de4428..bdbce3d8 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -4,7 +4,7 @@ from pylibs.core import get_module_class from pylibs.watchdog import crowsnest_watchdog import pylibs.logger as logger -import pylibs.logging as logging +import pylibs.logging_helper as logging_helper import asyncio @@ -76,15 +76,19 @@ async def run_watchdog(): async def main(): - global args + global args, crowsnest logger.setup_logging(args.log_path) - logging.log_initial() + logging_helper.log_initial() parse_config() - logging.log_host_info() - logging.log_config(args.config) - logging.log_cams() + if crowsnest.parameters['delete_log'].value: + logger.setup_logging(args.log_path, 'w') + logging_helper.log_initial() + + logging_helper.log_host_info() + logging_helper.log_config(args.config) + logging_helper.log_cams() task1 = asyncio.create_task(start_processes()) task2 = asyncio.create_task(run_watchdog()) diff --git a/pylibs/core.py b/pylibs/core.py index dd957f26..3cc29976 100644 --- a/pylibs/core.py +++ b/pylibs/core.py @@ -6,7 +6,6 @@ import os from . import logger -# import logging # Dynamically import module # Requires module to have a load_module() function, diff --git a/pylibs/logger.py b/pylibs/logger.py index a7b09a47..8d25c21d 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -1,14 +1,24 @@ import logging +import os + DEV = 10 DEBUG = 15 QUIET = 25 indentation = 6*' ' -def setup_logging(log_path): +logger = logging.getLogger('crowsnest') + +def setup_logging(log_path, filemode='a'): + global logger + logger.propagate = False + # Create log directory if it does not exist. + os.makedirs(os.path.dirname(log_path), exist_ok=True) + logging.basicConfig( filename=log_path, + filemode=filemode, encoding='utf-8', level=logging.INFO, format='[%(asctime)s] %(message)s', @@ -16,27 +26,27 @@ def setup_logging(log_path): ) # Change DEBUG to DEB and add custom logging level. - logging.addLevelName(DEV, 'DEV') - logging.addLevelName(DEBUG, 'DEBUG') - logging.addLevelName(QUIET, 'QUIET') + logger.addLevelName(DEV, 'DEV') + logger.addLevelName(DEBUG, 'DEBUG') + logger.addLevelName(QUIET, 'QUIET') def set_log_level(level): - logging.getLogger().setLevel(level) + logger.getLogger().setLevel(level) def log_quiet(msg, prefix=''): - logging.log(QUIET, prefix + msg) + logger.log(QUIET, prefix + msg) def log_info(msg, prefix='INFO: '): - logging.info(prefix + msg) + logger.info(prefix + msg) def log_debug(msg, prefix='DEBUG: '): - logging.log(DEBUG, prefix + msg) + logger.log(DEBUG, prefix + msg) def log_warning(msg, prefix='WARN: '): - logging.warning(prefix + msg) + logger.warning(prefix + msg) def log_error(msg, prefix='ERROR: '): - logging.error(prefix + msg) + logger.error(prefix + msg) def log_multiline(msg, log_func, *args): lines = msg.split('\n') diff --git a/pylibs/logging.py b/pylibs/logging_helper.py similarity index 100% rename from pylibs/logging.py rename to pylibs/logging_helper.py From 0c7ad377d69b1fbb7b227097409fc640c647b4f8 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:05:14 +0100 Subject: [PATCH 049/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/watchdog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index 416deb16..487f85b6 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -14,4 +14,4 @@ def crowsnest_watchdog(): logger.log_quiet("Lost Devicve: '{device}'", prefix) elif device in lost_devices and os.path.exists(device): lost_devices.remove(device) - logger.log_quiet("Device '{device}' returned.", prefix) + logger.log_quiet(f"Device '{device}' returned.", prefix) From 1b380a49d118d4c31e6ea41afaed64a554556221 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:07:24 +0100 Subject: [PATCH 050/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index 8d25c21d..4c70ed7d 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -26,9 +26,9 @@ def setup_logging(log_path, filemode='a'): ) # Change DEBUG to DEB and add custom logging level. - logger.addLevelName(DEV, 'DEV') - logger.addLevelName(DEBUG, 'DEBUG') - logger.addLevelName(QUIET, 'QUIET') + logging.addLevelName(DEV, 'DEV') + logging.addLevelName(DEBUG, 'DEBUG') + logging.addLevelName(QUIET, 'QUIET') def set_log_level(level): logger.getLogger().setLevel(level) From fea035c7b3e550fb6190cb4f6afc7a02bf6d1539 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:08:12 +0100 Subject: [PATCH 051/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index 4c70ed7d..ff7f008c 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -31,7 +31,7 @@ def setup_logging(log_path, filemode='a'): logging.addLevelName(QUIET, 'QUIET') def set_log_level(level): - logger.getLogger().setLevel(level) + logger.setLevel(level) def log_quiet(msg, prefix=''): logger.log(QUIET, prefix + msg) From 715680cb935bf04e632d56d2608db2558a68149e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:14:47 +0100 Subject: [PATCH 052/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index ff7f008c..866d72f3 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -8,11 +8,8 @@ indentation = 6*' ' -logger = logging.getLogger('crowsnest') - def setup_logging(log_path, filemode='a'): global logger - logger.propagate = False # Create log directory if it does not exist. os.makedirs(os.path.dirname(log_path), exist_ok=True) @@ -29,23 +26,32 @@ def setup_logging(log_path, filemode='a'): logging.addLevelName(DEV, 'DEV') logging.addLevelName(DEBUG, 'DEBUG') logging.addLevelName(QUIET, 'QUIET') + + logger = logging.getLogger('crowsnest') + logger.propagate = False def set_log_level(level): + global logger logger.setLevel(level) def log_quiet(msg, prefix=''): + global logger logger.log(QUIET, prefix + msg) def log_info(msg, prefix='INFO: '): + global logger logger.info(prefix + msg) def log_debug(msg, prefix='DEBUG: '): + global logger logger.log(DEBUG, prefix + msg) def log_warning(msg, prefix='WARN: '): + global logger logger.warning(prefix + msg) def log_error(msg, prefix='ERROR: '): + global logger logger.error(prefix + msg) def log_multiline(msg, log_func, *args): From 761fce4ee52f79d08709578235a80d2d7681fd04 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:16:19 +0100 Subject: [PATCH 053/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index 866d72f3..1bcf8c3f 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -28,7 +28,6 @@ def setup_logging(log_path, filemode='a'): logging.addLevelName(QUIET, 'QUIET') logger = logging.getLogger('crowsnest') - logger.propagate = False def set_log_level(level): global logger From 4887fc2a7fb65a56c63c6202f396f5f13359c1de Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:22:30 +0100 Subject: [PATCH 054/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index 1bcf8c3f..f44ceaa8 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -13,14 +13,14 @@ def setup_logging(log_path, filemode='a'): # Create log directory if it does not exist. os.makedirs(os.path.dirname(log_path), exist_ok=True) - logging.basicConfig( - filename=log_path, - filemode=filemode, - encoding='utf-8', - level=logging.INFO, - format='[%(asctime)s] %(message)s', - datefmt='%d/%m/%y %H:%M:%S' - ) + # logging.basicConfig( + # filename=log_path, + # filemode=filemode, + # encoding='utf-8', + # level=logging.INFO, + # format='[%(asctime)s] %(message)s', + # datefmt='%d/%m/%y %H:%M:%S' + # ) # Change DEBUG to DEB and add custom logging level. logging.addLevelName(DEV, 'DEV') @@ -28,6 +28,12 @@ def setup_logging(log_path, filemode='a'): logging.addLevelName(QUIET, 'QUIET') logger = logging.getLogger('crowsnest') + logger.propagate = True + formatter = logging.Formatter('[%(asctime)s] %(message)s', datefmt='%d/%m/%y %H:%M:%S') + filehandler = logging.FileHandler(log_path, filemode, 'utf-8') + filehandler.setFormatter(formatter) + logger.addHandler() + logger.setLevel(logging.INFO) def set_log_level(level): global logger From 7f9aa2b37334cb9336cb80d9ceda5507ec7ea58e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:22:56 +0100 Subject: [PATCH 055/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index f44ceaa8..d29eaa4d 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -28,7 +28,7 @@ def setup_logging(log_path, filemode='a'): logging.addLevelName(QUIET, 'QUIET') logger = logging.getLogger('crowsnest') - logger.propagate = True + logger.propagate = False formatter = logging.Formatter('[%(asctime)s] %(message)s', datefmt='%d/%m/%y %H:%M:%S') filehandler = logging.FileHandler(log_path, filemode, 'utf-8') filehandler.setFormatter(formatter) From fefb259722d16631a8db820e8a99aa129d3422da Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:23:34 +0100 Subject: [PATCH 056/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/watchdog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index 487f85b6..03ce103b 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -11,7 +11,7 @@ def crowsnest_watchdog(): for device in configured_devices: if not os.path.exists(device): lost_devices.append(device) - logger.log_quiet("Lost Devicve: '{device}'", prefix) + logger.log_quiet(f"Lost Devicve: '{device}'", prefix) elif device in lost_devices and os.path.exists(device): lost_devices.remove(device) logger.log_quiet(f"Device '{device}' returned.", prefix) From 3f500484d43ef21338a6041f7bec1ae1d5ffa219 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:24:22 +0100 Subject: [PATCH 057/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index d29eaa4d..9986fcde 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -32,7 +32,7 @@ def setup_logging(log_path, filemode='a'): formatter = logging.Formatter('[%(asctime)s] %(message)s', datefmt='%d/%m/%y %H:%M:%S') filehandler = logging.FileHandler(log_path, filemode, 'utf-8') filehandler.setFormatter(formatter) - logger.addHandler() + logger.addHandler(filehandler) logger.setLevel(logging.INFO) def set_log_level(level): From 66e5751fa1a6863f6106f501b5b5697393443ecb Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:29:59 +0100 Subject: [PATCH 058/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 2 +- pylibs/logger.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index bdbce3d8..10ca9a0b 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -83,7 +83,7 @@ async def main(): parse_config() if crowsnest.parameters['delete_log'].value: - logger.setup_logging(args.log_path, 'w') + logger.setup_logging(args.log_path, 'w', crowsnest.parameters['log_level'].value) logging_helper.log_initial() logging_helper.log_host_info() diff --git a/pylibs/logger.py b/pylibs/logger.py index 9986fcde..9bce3e3c 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -8,7 +8,7 @@ indentation = 6*' ' -def setup_logging(log_path, filemode='a'): +def setup_logging(log_path, filemode='a', log_level=logging.INFO): global logger # Create log directory if it does not exist. os.makedirs(os.path.dirname(log_path), exist_ok=True) @@ -33,7 +33,7 @@ def setup_logging(log_path, filemode='a'): filehandler = logging.FileHandler(log_path, filemode, 'utf-8') filehandler.setFormatter(formatter) logger.addHandler(filehandler) - logger.setLevel(logging.INFO) + logger.setLevel(log_level) def set_log_level(level): global logger From 3f12fb00ee2a0deb7c01bfa1e28bf1fee7cb358e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 14:41:54 +0100 Subject: [PATCH 059/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/watchdog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index 03ce103b..f37856fe 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -1,7 +1,7 @@ import os from . import logger -configured_devices = [] +configured_devices: list[str] = [] def crowsnest_watchdog(): global configured_devices @@ -9,6 +9,8 @@ def crowsnest_watchdog(): lost_devices = [] for device in configured_devices: + if device.startswith('/base'): + continue if not os.path.exists(device): lost_devices.append(device) logger.log_quiet(f"Lost Devicve: '{device}'", prefix) From 77ca6dde039675e3484296ab0cf01274885273fd Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 17:35:18 +0100 Subject: [PATCH 060/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/streamer/camera-streamer.py | 2 ++ pylibs/streamer/ustreamer.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pylibs/streamer/camera-streamer.py b/pylibs/streamer/camera-streamer.py index b50f3ec9..d40020da 100644 --- a/pylibs/streamer/camera-streamer.py +++ b/pylibs/streamer/camera-streamer.py @@ -87,6 +87,8 @@ async def execute(self): logger.log_debug(f"Parameters: {' '.join(streamer_args)}") process,_,_ = await execute_command( cmd, + info_log_pre=log_pre, + info_log_func=logger.log_debug, error_log_pre=log_pre, error_log_func=logger.log_debug ) diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index ba582fc6..aa343ecf 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -76,6 +76,8 @@ async def execute(self): logger.log_debug(log_pre + f"Parameters: {' '.join(streamer_args)}") process,_,_ = await execute_command( cmd, + info_log_pre=log_pre, + info_log_func=logger.log_debug, error_log_pre=log_pre, error_log_func=self.custom_log ) From 90879353032eec551ca634ce90535f301a101671 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 17:57:10 +0100 Subject: [PATCH 061/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 7 ++++++- pylibs/logging_helper.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crowsnest.py b/crowsnest.py index 10ca9a0b..7d41f697 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -56,7 +56,9 @@ async def start_processes(): for section_object in sec_objs: task = asyncio.create_task(section_object.execute()) sec_exec_tasks.add(task) - + + logger.log_quiet("... Done!") + for task in sec_exec_tasks: if task is not None: await task @@ -67,6 +69,9 @@ async def start_processes(): if task != None: task.cancel() watchdog_running = False + logger.log_quiet("Shutdown or Killed by User!") + logger.log_quiet("Please come again :)") + logger.log_quiet("Goodbye...") async def run_watchdog(): global watchdog_running diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index ed0ec7eb..f19c4183 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -23,6 +23,7 @@ def log_config(config_path): # Remove comments config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) config_txt = config_txt.strip() + config_txt = config_txt.replace('\n\n', '\n') # Split the config file into lines log_multiline(config_txt, log_info, indentation) From b64295f28cdd41345ab1a4a5a244c5f5c4e6ea4f Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 18:16:29 +0100 Subject: [PATCH 062/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logging_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index f19c4183..8867e5fb 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -21,9 +21,9 @@ def log_config(config_path): with open(config_path, 'r') as file: config_txt = file.read() # Remove comments - config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) + config_txt = re.sub(r'\s*$', "", config_txt, flags=re.MULTILINE) + config_txt = re.sub(r'(\[.*\])$', "\n\\1", config_txt, flags=re.MULTILINE) config_txt = config_txt.strip() - config_txt = config_txt.replace('\n\n', '\n') # Split the config file into lines log_multiline(config_txt, log_info, indentation) From 3ed73e63275e240769727f055653fce553b14d7b Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 18:17:24 +0100 Subject: [PATCH 063/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logging_helper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 8867e5fb..f5d9c00c 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -21,6 +21,7 @@ def log_config(config_path): with open(config_path, 'r') as file: config_txt = file.read() # Remove comments + config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) config_txt = re.sub(r'\s*$', "", config_txt, flags=re.MULTILINE) config_txt = re.sub(r'(\[.*\])$', "\n\\1", config_txt, flags=re.MULTILINE) config_txt = config_txt.strip() From 1e6d7c9c93a710500299ff8fbf58eab9b084eda5 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 18:28:41 +0100 Subject: [PATCH 064/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/streamer/camera-streamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/streamer/camera-streamer.py b/pylibs/streamer/camera-streamer.py index d40020da..7bf839ae 100644 --- a/pylibs/streamer/camera-streamer.py +++ b/pylibs/streamer/camera-streamer.py @@ -84,7 +84,7 @@ async def execute(self): cmd = self.binary_path + ' ' + ' '.join(streamer_args) log_pre = f'camera-streamer [cam {self.name}]: ' - logger.log_debug(f"Parameters: {' '.join(streamer_args)}") + logger.log_debug(log_pre + f"Parameters: {' '.join(streamer_args)}") process,_,_ = await execute_command( cmd, info_log_pre=log_pre, From bf5779693bdcaa7661b90f1c6eb7aa975deeaac6 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 4 Mar 2024 20:57:06 +0100 Subject: [PATCH 065/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 8 ++++++-- pylibs/cam.py | 11 ++++++++--- pylibs/section.py | 4 ++-- pylibs/streamer/camera-streamer.py | 7 +++++-- pylibs/streamer/streamer.py | 6 ++++-- pylibs/streamer/ustreamer.py | 7 ++++--- 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 7d41f697..5de96bb2 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -53,11 +53,15 @@ async def start_processes(): else: logger.log_error(f"Failed to parse config for section [{section}]!") + lock = asyncio.Lock() for section_object in sec_objs: - task = asyncio.create_task(section_object.execute()) + task = asyncio.create_task(section_object.execute(lock)) sec_exec_tasks.add(task) - logger.log_quiet("... Done!") + # Let sec_exec_tasks finish first + await asyncio.sleep(0) + async with lock: + logger.log_quiet("... Done!") for task in sec_exec_tasks: if task is not None: diff --git a/pylibs/cam.py b/pylibs/cam.py index db3477b7..04923e28 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -1,8 +1,9 @@ +import asyncio + from configparser import SectionProxy from .section import Section from .parameter import Parameter from .core import get_module_class - from . import logger class Cam(Section): @@ -26,16 +27,20 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs): self.streamer = mode_class(self.name) return self.streamer.parse_config(config_section) - async def execute(self): + async def execute(self, lock: asyncio.Lock): if self.streamer is None: print("No streamer loaded") return try: - process = await self.streamer.execute() + await lock.acquire() + process = await self.streamer.execute(lock) await process.wait() logger.log_error(f'Start of {self.parameters["mode"].value} [cam {self.name}] failed!') except Exception as e: pass + finally: + if lock.locked(): + lock.release() def load_module(): return Cam diff --git a/pylibs/section.py b/pylibs/section.py index 188e64e0..ab10bd62 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -1,4 +1,4 @@ -import re +import asyncio from configparser import SectionProxy from .parameter import Parameter @@ -33,5 +33,5 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: return success # Execute section specific stuff, e.g. starting cam - async def execute(self): + async def execute(self, lock: asyncio.Lock): raise NotImplementedError("If you see this, a module is implemented wrong!!!") diff --git a/pylibs/streamer/camera-streamer.py b/pylibs/streamer/camera-streamer.py index 7bf839ae..a464b518 100644 --- a/pylibs/streamer/camera-streamer.py +++ b/pylibs/streamer/camera-streamer.py @@ -1,3 +1,5 @@ +import asyncio + from .streamer import Streamer from ..parameter import Parameter from ..core import execute_command, get_executable @@ -22,8 +24,8 @@ def __init__(self, name: str = '') -> None: ) self.binary_path = Camera_Streamer.binary_path - async def execute(self): - if not super().execute(): + async def execute(self, lock: asyncio.Lock): + if not await super().execute(lock): return None if self.parameters['no_proxy'].value: host = '0.0.0.0' @@ -92,6 +94,7 @@ async def execute(self): error_log_pre=log_pre, error_log_func=logger.log_debug ) + lock.release() return process diff --git a/pylibs/streamer/streamer.py b/pylibs/streamer/streamer.py index 3dc7e60c..e296a3dd 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/streamer/streamer.py @@ -1,9 +1,11 @@ +import os +import asyncio + from ..section import Section from ..parameter import Parameter from .. import logger from ..watchdog import configured_devices from configparser import SectionProxy -import os class Streamer(Section): binary_path = None @@ -36,7 +38,7 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: return False return success - def execute(self): + async def execute(self, lock: asyncio.Lock): global configured_devices if not os.path.exists(self.binary_path): logger.log_multiline(self.missing_bin_txt, logger.log_error) diff --git a/pylibs/streamer/ustreamer.py b/pylibs/streamer/ustreamer.py index aa343ecf..f53f87e3 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/streamer/ustreamer.py @@ -21,8 +21,8 @@ def __init__(self, name: str = '') -> None: ) self.binary_path = Ustreamer.binary_path - async def execute(self): - if not super().execute(): + async def execute(self, lock: asyncio.Lock): + if not await super().execute(lock): return None if self.parameters['no_proxy'].value: host = '0.0.0.0' @@ -81,8 +81,9 @@ async def execute(self): error_log_pre=log_pre, error_log_func=self.custom_log ) - await asyncio.sleep(0.5) + lock.release() + await asyncio.sleep(0.5) for ctl in v4l2ctl.split(','): if 'focus_absolute' in ctl: focus_absolute = ctl.split('=')[1].strip() From 4287c0f17ac0d57948a703e0792c4209dc5fce06 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 10 Mar 2024 23:16:47 +0100 Subject: [PATCH 066/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pylibs/logger.py b/pylibs/logger.py index 9bce3e3c..fada2ff6 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -1,6 +1,7 @@ import logging import os +import sys DEV = 10 DEBUG = 15 @@ -33,6 +34,11 @@ def setup_logging(log_path, filemode='a', log_level=logging.INFO): filehandler = logging.FileHandler(log_path, filemode, 'utf-8') filehandler.setFormatter(formatter) logger.addHandler(filehandler) + + streamhandler = logging.StreamHandler(sys.stdout) + filehandler.setFormatter(formatter) + logger.addHandler(streamhandler) + logger.setLevel(log_level) def set_log_level(level): From e81388a04db7fa07020661d50622a450ee1fea4f Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 10 Mar 2024 23:20:28 +0100 Subject: [PATCH 067/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index fada2ff6..41d82c1b 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -27,7 +27,7 @@ def setup_logging(log_path, filemode='a', log_level=logging.INFO): logging.addLevelName(DEV, 'DEV') logging.addLevelName(DEBUG, 'DEBUG') logging.addLevelName(QUIET, 'QUIET') - + logger = logging.getLogger('crowsnest') logger.propagate = False formatter = logging.Formatter('[%(asctime)s] %(message)s', datefmt='%d/%m/%y %H:%M:%S') From 42126e8f8667f2217c56fab5d107b8b12c9d7b5a Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 10 Mar 2024 23:30:44 +0100 Subject: [PATCH 068/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index 41d82c1b..3912a071 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -36,7 +36,7 @@ def setup_logging(log_path, filemode='a', log_level=logging.INFO): logger.addHandler(filehandler) streamhandler = logging.StreamHandler(sys.stdout) - filehandler.setFormatter(formatter) + streamhandler.setFormatter(formatter) logger.addHandler(streamhandler) logger.setLevel(log_level) From 3160138eb809e578828fcea11bd6f27577133f0a Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 11 Mar 2024 00:05:18 +0100 Subject: [PATCH 069/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 3 ++- pylibs/logger.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 5de96bb2..b3e0981f 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -7,6 +7,7 @@ import pylibs.logging_helper as logging_helper import asyncio +import pathlib parser = argparse.ArgumentParser( prog='Crowsnest', @@ -92,7 +93,7 @@ async def main(): parse_config() if crowsnest.parameters['delete_log'].value: - logger.setup_logging(args.log_path, 'w', crowsnest.parameters['log_level'].value) + pathlib.Path.unlink(args.log_path) logging_helper.log_initial() logging_helper.log_host_info() diff --git a/pylibs/logger.py b/pylibs/logger.py index 3912a071..abfe6ceb 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -1,4 +1,5 @@ import logging +import logging.handlers import os import sys @@ -31,7 +32,7 @@ def setup_logging(log_path, filemode='a', log_level=logging.INFO): logger = logging.getLogger('crowsnest') logger.propagate = False formatter = logging.Formatter('[%(asctime)s] %(message)s', datefmt='%d/%m/%y %H:%M:%S') - filehandler = logging.FileHandler(log_path, filemode, 'utf-8') + filehandler = logging.handlers.WatchedFileHandler(log_path, filemode, 'utf-8') filehandler.setFormatter(formatter) logger.addHandler(filehandler) From 20b2c539f8a5047f6fd84bb5bbcf0a208157c700 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Tue, 12 Mar 2024 11:45:53 +0100 Subject: [PATCH 070/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logger.py | 13 ++++--------- pylibs/logging_helper.py | 6 +++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pylibs/logger.py b/pylibs/logger.py index abfe6ceb..168dffe3 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -15,15 +15,6 @@ def setup_logging(log_path, filemode='a', log_level=logging.INFO): # Create log directory if it does not exist. os.makedirs(os.path.dirname(log_path), exist_ok=True) - # logging.basicConfig( - # filename=log_path, - # filemode=filemode, - # encoding='utf-8', - # level=logging.INFO, - # format='[%(asctime)s] %(message)s', - # datefmt='%d/%m/%y %H:%M:%S' - # ) - # Change DEBUG to DEB and add custom logging level. logging.addLevelName(DEV, 'DEV') logging.addLevelName(DEBUG, 'DEBUG') @@ -32,14 +23,18 @@ def setup_logging(log_path, filemode='a', log_level=logging.INFO): logger = logging.getLogger('crowsnest') logger.propagate = False formatter = logging.Formatter('[%(asctime)s] %(message)s', datefmt='%d/%m/%y %H:%M:%S') + + # WatchedFileHandler for log file. This handler will reopen the file if it is moved or deleted. filehandler = logging.handlers.WatchedFileHandler(log_path, filemode, 'utf-8') filehandler.setFormatter(formatter) logger.addHandler(filehandler) + # StreamHandler for stdout. streamhandler = logging.StreamHandler(sys.stdout) streamhandler.setFormatter(formatter) logger.addHandler(streamhandler) + # Set log level. logger.setLevel(log_level) def set_log_level(level): diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index f5d9c00c..88e15a51 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -37,7 +37,7 @@ def log_host_info(): distribution = grep('/etc/os-release', 'PRETTY_NAME') distribution = distribution.strip().split('=')[1].strip('"') log_info(f'Distribution: {distribution}', log_pre) - + # Release Version of MainsailOS (if file present) try: with open('/etc/mainsailos-release', 'r') as file: @@ -127,8 +127,8 @@ def log_libcamera_dev(path: str, properties: dict) -> str: str_indent = (30 - len(str_first)) * ' ' + ': ' log_info(str_first + str_indent + str_second, indentation) else: - log_info("apt package 'python3-libcamera' is not installed! \ -Make sure to install it to log the controls!", indentation) + log_info("apt package 'python3-libcamera' is not installed! " + "Make sure to install it to log the controls!", indentation) def get_type_str(obj) -> str: return str(type(obj)).split('\'')[1] From ed6a52d8fa135663f229b7ca764ba918b10df089 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 13 Mar 2024 16:12:19 +0100 Subject: [PATCH 071/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 9 ++++++--- pylibs/cam.py | 7 +------ pylibs/logging_helper.py | 3 +++ pylibs/parameter.py | 2 +- pylibs/section.py | 7 ++++--- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index b3e0981f..7f9cb6b5 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -30,7 +30,7 @@ def parse_config(): crowsnest.parse_config(config['crowsnest']) logger.set_log_level(crowsnest.parameters['log_level'].value) -async def start_processes(): +async def start_sections(): global config, watchdog_running sec_objs = [] sec_exec_tasks = set() @@ -42,6 +42,7 @@ async def start_processes(): section_object = None section_keyword = section_header[0] + # Skip crowsnest section if section_keyword == 'crowsnest': continue @@ -100,10 +101,12 @@ async def main(): logging_helper.log_config(args.config) logging_helper.log_cams() - task1 = asyncio.create_task(start_processes()) + task1 = asyncio.create_task(start_sections()) task2 = asyncio.create_task(run_watchdog()) + await task1 - await task2 + if task2: + task2.cancel() # asyncio.gather(start_processes(), run_watchdog()) diff --git a/pylibs/cam.py b/pylibs/cam.py index 04923e28..c0424f4c 100644 --- a/pylibs/cam.py +++ b/pylibs/cam.py @@ -19,7 +19,7 @@ def __init__(self, name: str = '') -> None: self.streamer = None - def parse_config(self, config_section: SectionProxy, *args, **kwargs): + def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: # Dynamically import module mode = config_section["mode"].split()[0] self.parameters["mode"].set_value(mode) @@ -44,8 +44,3 @@ async def execute(self, lock: asyncio.Lock): def load_module(): return Cam - -#if __name__ == "__main__": -# print("This is a module and shouldn't be executed directly") -#else: -# CN_Section.available_sections[CN_Cam.keyword] = CN_Cam diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 88e15a51..de34545b 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -22,8 +22,11 @@ def log_config(config_path): config_txt = file.read() # Remove comments config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) + # Remove multiple whitespaces next to each other at the end of a line config_txt = re.sub(r'\s*$', "", config_txt, flags=re.MULTILINE) + # Add newlines before sections config_txt = re.sub(r'(\[.*\])$', "\n\\1", config_txt, flags=re.MULTILINE) + # Remove leading and trailing whitespaces config_txt = config_txt.strip() # Split the config file into lines log_multiline(config_txt, log_info, indentation) diff --git a/pylibs/parameter.py b/pylibs/parameter.py index b90dffbc..d1ba315a 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -15,7 +15,7 @@ def set_value(self, value): elif value.lower() == 'false': self.value = False else: - logger.log_error(f"{value} is not 'true' or 'false'! Parameter ignored!") + raise ValueError() else: self.value = self.type(value) except ValueError: diff --git a/pylibs/section.py b/pylibs/section.py index ab10bd62..e057eb2f 100644 --- a/pylibs/section.py +++ b/pylibs/section.py @@ -10,8 +10,8 @@ class Section: available_sections = {} # Section looks like this: # [ ] - # param1 - # param2 + # param1: value1 + # param2: value2 def __init__(self, name: str = '') -> None: self.name = name self.parameters: dict[str, Parameter] = {} @@ -28,7 +28,8 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: self.parameters[parameter].set_value(value) for parameter in self.parameters: if self.parameters[parameter].value is None: - logger.log_error(f"Parameter '{parameter}' not found in section [{self.section_name} {self.name}]") + logger.log_error(f"Parameter '{parameter}' not found in section " + "[{self.section_name} {self.name}]") success = False return success From 3e599a0634bda68306dc514a0ff0847fb6bfe1aa Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 13 Mar 2024 17:19:09 +0100 Subject: [PATCH 072/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 6 +-- pylibs/{streamer => components}/__init__.py | 0 pylibs/{ => components}/cam.py | 10 ++-- pylibs/{ => components}/crowsnest.py | 4 +- pylibs/{ => components}/section.py | 4 +- pylibs/components/streamer/__init__.py | 0 .../streamer/camera-streamer.py | 10 ++-- pylibs/{ => components}/streamer/streamer.py | 8 +-- pylibs/{ => components}/streamer/ustreamer.py | 10 ++-- pylibs/hwhandler.py | 10 ++-- pylibs/logging_helper.py | 10 ++-- pylibs/messages.py | 52 ------------------- pylibs/parameter.py | 2 +- pylibs/{core.py => utils.py} | 2 +- pylibs/v4l2_control.py | 10 ++-- pylibs/watchdog.py | 2 +- 16 files changed, 44 insertions(+), 96 deletions(-) rename pylibs/{streamer => components}/__init__.py (100%) rename pylibs/{ => components}/cam.py (81%) rename pylibs/{ => components}/crowsnest.py (91%) rename pylibs/{ => components}/section.py (93%) create mode 100644 pylibs/components/streamer/__init__.py rename pylibs/{ => components}/streamer/camera-streamer.py (93%) rename pylibs/{ => components}/streamer/streamer.py (91%) rename pylibs/{ => components}/streamer/ustreamer.py (91%) delete mode 100644 pylibs/messages.py rename pylibs/{core.py => utils.py} (99%) diff --git a/crowsnest.py b/crowsnest.py index 7f9cb6b5..0d697257 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,7 +1,7 @@ import argparse import configparser -from pylibs.crowsnest import Crowsnest -from pylibs.core import get_module_class +from pylibs.components.crowsnest import Crowsnest +from pylibs.utils import get_module_class from pylibs.watchdog import crowsnest_watchdog import pylibs.logger as logger import pylibs.logging_helper as logging_helper @@ -46,7 +46,7 @@ async def start_sections(): if section_keyword == 'crowsnest': continue - section_class = get_module_class('pylibs', section_keyword) + section_class = get_module_class('pylibs.components', section_keyword) section_name = ' '.join(section_header[1:]) section_object = section_class(section_name) if section_object.parse_config(config[section]): diff --git a/pylibs/streamer/__init__.py b/pylibs/components/__init__.py similarity index 100% rename from pylibs/streamer/__init__.py rename to pylibs/components/__init__.py diff --git a/pylibs/cam.py b/pylibs/components/cam.py similarity index 81% rename from pylibs/cam.py rename to pylibs/components/cam.py index c0424f4c..5fb7e416 100644 --- a/pylibs/cam.py +++ b/pylibs/components/cam.py @@ -1,10 +1,10 @@ import asyncio from configparser import SectionProxy -from .section import Section -from .parameter import Parameter -from .core import get_module_class -from . import logger +from pylibs.components.section import Section +from pylibs.parameter import Parameter +from pylibs.utils import get_module_class +from pylibs import logger class Cam(Section): section_name = 'cam' @@ -23,7 +23,7 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: # Dynamically import module mode = config_section["mode"].split()[0] self.parameters["mode"].set_value(mode) - mode_class = get_module_class('pylibs.streamer', mode) + mode_class = get_module_class('pylibs.components.streamer', mode) self.streamer = mode_class(self.name) return self.streamer.parse_config(config_section) diff --git a/pylibs/crowsnest.py b/pylibs/components/crowsnest.py similarity index 91% rename from pylibs/crowsnest.py rename to pylibs/components/crowsnest.py index db7a182f..554fb48d 100644 --- a/pylibs/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -1,5 +1,5 @@ -from .section import Section -from .parameter import Parameter +from pylibs.components.section import Section +from pylibs.parameter import Parameter from configparser import SectionProxy diff --git a/pylibs/section.py b/pylibs/components/section.py similarity index 93% rename from pylibs/section.py rename to pylibs/components/section.py index e057eb2f..db3ff900 100644 --- a/pylibs/section.py +++ b/pylibs/components/section.py @@ -1,8 +1,8 @@ import asyncio from configparser import SectionProxy -from .parameter import Parameter -from . import logger +from pylibs.parameter import Parameter +from pylibs import logger class Section: section_name = 'section' diff --git a/pylibs/components/streamer/__init__.py b/pylibs/components/streamer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pylibs/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py similarity index 93% rename from pylibs/streamer/camera-streamer.py rename to pylibs/components/streamer/camera-streamer.py index a464b518..7a072ba5 100644 --- a/pylibs/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -1,10 +1,10 @@ import asyncio -from .streamer import Streamer -from ..parameter import Parameter -from ..core import execute_command, get_executable -from ..hwhandler import has_device_mjpg_hw -from .. import logger +from pylibs.components.streamer.streamer import Streamer +from pylibs.parameter import Parameter +from pylibs.utils import execute_command, get_executable +from pylibs.hwhandler import has_device_mjpg_hw +from pylibs import logger class Camera_Streamer(Streamer): keyword = 'camera-streamer' diff --git a/pylibs/streamer/streamer.py b/pylibs/components/streamer/streamer.py similarity index 91% rename from pylibs/streamer/streamer.py rename to pylibs/components/streamer/streamer.py index e296a3dd..7d6695ba 100644 --- a/pylibs/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -1,10 +1,10 @@ import os import asyncio -from ..section import Section -from ..parameter import Parameter -from .. import logger -from ..watchdog import configured_devices +from pylibs.components.section import Section +from pylibs.parameter import Parameter +from pylibs import logger +from pylibs.watchdog import configured_devices from configparser import SectionProxy class Streamer(Section): diff --git a/pylibs/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py similarity index 91% rename from pylibs/streamer/ustreamer.py rename to pylibs/components/streamer/ustreamer.py index f53f87e3..19132111 100644 --- a/pylibs/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -1,11 +1,11 @@ import re import asyncio -from .streamer import Streamer -from ..core import execute_command, get_executable -from ..hwhandler import is_device_legacy, has_device_mjpg_hw -from ..v4l2_control import set_v4l2ctrls, blockyfix, brokenfocus -from .. import logger +from pylibs.components.streamer.streamer import Streamer +from pylibs.utils import execute_command, get_executable +from pylibs.hwhandler import is_device_legacy, has_device_mjpg_hw +from pylibs.v4l2_control import set_v4l2ctrls, blockyfix, brokenfocus +from pylibs import logger class Ustreamer(Streamer): section_name = 'cam' diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 7282ade2..c73a4a1f 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -2,8 +2,8 @@ import shutil import re -from . import core -from .v4l2_control import get_uvc_formats, get_uvc_v4l2ctrls +from pylibs import utils +from pylibs.v4l2_control import get_uvc_formats, get_uvc_v4l2ctrls avail_cams = { 'uvc': {}, @@ -36,7 +36,7 @@ def get_avail_libcamera() -> dict: if not cmd: return {} libcam_cmd =f'{cmd} --list-cameras' - libcam = core.execute_shell_command(libcam_cmd, strip=False) + libcam = utils.execute_shell_command(libcam_cmd, strip=False) libcams = {} if 'Available' in libcam: for path in get_libcamera_paths(libcam): @@ -97,7 +97,7 @@ def get_avail_legacy() -> dict: if not cmd: return legacy count_cmd = f'{cmd} get_camera' - count = core.execute_shell_command(count_cmd) + count = utils.execute_shell_command(count_cmd) # Gets the number behind detected: "supported=1 detected=1, libcamera interfaces=0" if not count: return legacy @@ -105,7 +105,7 @@ def get_avail_legacy() -> dict: if count == '0': return legacy v4l2_cmd = 'v4l2-ctl --list-devices' - v4l2 = core.execute_shell_command(v4l2_cmd) + v4l2 = utils.execute_shell_command(v4l2_cmd) legacy_path = '' lines = v4l2.split('\n') for i in range(len(lines)): diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index de34545b..833b348c 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -3,16 +3,16 @@ import re # log_host_info import os -from . import core +from pylibs import utils # log_cams import sys -from .logger import log_quiet, log_info, log_error, log_multiline, indentation -from .hwhandler import get_avail_uvc_dev, get_avail_libcamera, get_avail_legacy +from pylibs.logger import log_quiet, log_info, log_error, log_multiline, indentation +from pylibs.hwhandler import get_avail_uvc_dev, get_avail_libcamera, get_avail_legacy def log_initial(): log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') command = 'git describe --always --tags' - version = core.execute_shell_command(command) + version = utils.execute_shell_command(command) log_quiet(f'Version: {version}') log_quiet('Prepare Startup ...') @@ -75,7 +75,7 @@ def log_host_info(): # Avail disk size # Alternative shutil.disk_usage.total command = 'LC_ALL=C df -h / | awk \'NR==2 {print $4" / "$2}\'' - disksize = core.execute_shell_command(command) + disksize = utils.execute_shell_command(command) log_info(f'Diskspace (avail. / total): {disksize}', log_pre) def grep(path: str, search: str) -> str: diff --git a/pylibs/messages.py b/pylibs/messages.py deleted file mode 100644 index 036f16de..00000000 --- a/pylibs/messages.py +++ /dev/null @@ -1,52 +0,0 @@ -import time - -TITLE="\e[31mcrowsnest\e[0m - A webcam daemon for multiple cams and stream services." - -### All message intendations are intended as they are - -missing_args_msg = """ - echo -e "crowsnest: Missing Arguments!" - echo -e "\n\tTry: crowsnest -h\n" -""" - -wrong_args_msg = """ - echo -e "crowsnest: Wrong Arguments!" - echo -e "\n\tTry: crowsnest -h\n" -""" - -help_msg = """ - echo -e "crowsnest - webcam deamon\nUsage:" - echo -e "\t crowsnest [Options]" - echo -e "\n\t\t-h Prints this help." - echo -e "\n\t\t-v Prints Version of crowsnest." - echo -e "\n\t\t-c \n\t\t\tPath to your webcam.conf\n" -}""" - -deprecated_msg_1 = """{ - log_msg "Parameter 'streamer' is deprecated!" - log_msg "Please use mode: [ mjpg | multi ]" - log_msg "ERROR: Please update your crowsnest.conf! Stopped." -} -""" - -unknown_mode_msg = """{ - log_msg "WARN: Unknown Mode configured!" - log_msg "WARN: Using 'mode: mjpg' as fallback!" -}""" - -## v4l2_control lib -detected_broken_dev_msg = """{ - log_msg "WARN: Detected 'brokenfocus' device." - log_msg "INFO: Trying to set to configured Value." -}""" - -# call debug_focus_val_msg -# ex.: debug_focus_val_msg focus_absolute=30 -debug_focus_val_msg = """{ - log_msg "DEBUG: Value is now: ${1}" -}""" - -## blockyfix -blockyfix_msg_1 = """{ - log_msg "INFO: Blockyfix: Setting video_bitrate_mode to constant." -}""" diff --git a/pylibs/parameter.py b/pylibs/parameter.py index d1ba315a..505961c9 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -1,4 +1,4 @@ -from . import logger +from pylibs import logger class Parameter: def __init__(self, type=str, default=None) -> None: diff --git a/pylibs/core.py b/pylibs/utils.py similarity index 99% rename from pylibs/core.py rename to pylibs/utils.py index 3cc29976..8a0fcb08 100644 --- a/pylibs/core.py +++ b/pylibs/utils.py @@ -5,7 +5,7 @@ import shutil import os -from . import logger +from pylibs import logger # Dynamically import module # Requires module to have a load_module() function, diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py index 11d7dd09..11ea09db 100644 --- a/pylibs/v4l2_control.py +++ b/pylibs/v4l2_control.py @@ -1,20 +1,20 @@ -from . import core, logger +from pylibs import logger, utils def get_uvc_formats(cam_path: str) -> str: command = f'v4l2-ctl -d {cam_path} --list-formats-ext' - formats = core.execute_shell_command(command) + formats = utils.execute_shell_command(command) # Remove first 3 lines formats = '\n'.join(formats.split('\n')[3:]) return formats def get_uvc_v4l2ctrls(cam_path: str) -> str: command = f'v4l2-ctl -d {cam_path} --list-ctrls-menus' - v4l2ctrls = core.execute_shell_command(command) + v4l2ctrls = utils.execute_shell_command(command) return v4l2ctrls def set_v4l2_ctrl(cam_path: str, ctrl: str) -> str: command = f'v4l2-ctl -d {cam_path} -c {ctrl}' - v4l2ctrl = core.execute_shell_command(command) + v4l2ctrl = utils.execute_shell_command(command) return v4l2ctrl def set_v4l2ctrls(section: str, cam_path: str, ctrls: list[str] = None) -> str: @@ -39,7 +39,7 @@ def set_v4l2ctrls(section: str, cam_path: str, ctrls: list[str] = None) -> str: def get_cur_v4l2_value(cam_path: str, ctrl: str) -> str: command = f'v4l2-ctl -d {cam_path} -C {ctrl}' - value = core.execute_shell_command(command) + value = utils.execute_shell_command(command) if value: return value.split(':')[1].strip() return value diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index f37856fe..63839754 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -1,5 +1,5 @@ import os -from . import logger +from pylibs import logger configured_devices: list[str] = [] From 633652723dc2d6ed9daf968a9b01f574ed7213d3 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 13 Mar 2024 17:38:36 +0100 Subject: [PATCH 073/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/streamer/camera-streamer.py | 3 ++- pylibs/components/streamer/ustreamer.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index 7a072ba5..d9c7fd6c 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -94,7 +94,8 @@ async def execute(self, lock: asyncio.Lock): error_log_pre=log_pre, error_log_func=logger.log_debug ) - lock.release() + if lock.locked(): + lock.release() return process diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index 19132111..64ced20a 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -81,7 +81,8 @@ async def execute(self, lock: asyncio.Lock): error_log_pre=log_pre, error_log_func=self.custom_log ) - lock.release() + if lock.locked(): + lock.release() await asyncio.sleep(0.5) for ctl in v4l2ctl.split(','): From d38ce5dcdf060d7c94a230a187ab7fba93e21969 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 13 Mar 2024 18:18:28 +0100 Subject: [PATCH 074/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/cam.py | 5 +- pylibs/components/streamer/camera-streamer.py | 10 +-- pylibs/components/streamer/streamer.py | 8 +- pylibs/components/streamer/ustreamer.py | 19 ++--- pylibs/hwhandler.py | 13 ++- pylibs/logging_helper.py | 79 +++++++++---------- 6 files changed, 60 insertions(+), 74 deletions(-) diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index 5fb7e416..8ccd9a1d 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -3,8 +3,7 @@ from configparser import SectionProxy from pylibs.components.section import Section from pylibs.parameter import Parameter -from pylibs.utils import get_module_class -from pylibs import logger +from pylibs import logger, utils class Cam(Section): section_name = 'cam' @@ -23,7 +22,7 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: # Dynamically import module mode = config_section["mode"].split()[0] self.parameters["mode"].set_value(mode) - mode_class = get_module_class('pylibs.components.streamer', mode) + mode_class = utils.get_module_class('pylibs.components.streamer', mode) self.streamer = mode_class(self.name) return self.streamer.parse_config(config_section) diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index d9c7fd6c..e31149a2 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -2,9 +2,7 @@ from pylibs.components.streamer.streamer import Streamer from pylibs.parameter import Parameter -from pylibs.utils import execute_command, get_executable -from pylibs.hwhandler import has_device_mjpg_hw -from pylibs import logger +from pylibs import logger, utils, hwhandler class Camera_Streamer(Streamer): keyword = 'camera-streamer' @@ -18,7 +16,7 @@ def __init__(self, name: str = '') -> None: }) if Camera_Streamer.binary_path is None: - Camera_Streamer.binary_path = get_executable( + Camera_Streamer.binary_path = utils.get_executable( ['camera-streamer'], ['bin/camera-streamer'] ) @@ -70,7 +68,7 @@ async def execute(self, lock: asyncio.Lock): streamer_args += [ '--camera-type=v4l2' ] - if has_device_mjpg_hw(device): + if hwhandler.has_device_mjpg_hw(device): streamer_args += [ '--camera-format=MJPEG' ] @@ -87,7 +85,7 @@ async def execute(self, lock: asyncio.Lock): log_pre = f'camera-streamer [cam {self.name}]: ' logger.log_debug(log_pre + f"Parameters: {' '.join(streamer_args)}") - process,_,_ = await execute_command( + process,_,_ = await utils.execute_command( cmd, info_log_pre=log_pre, info_log_func=logger.log_debug, diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index 7d6695ba..9b9a5702 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -1,11 +1,10 @@ import os import asyncio +from configparser import SectionProxy from pylibs.components.section import Section from pylibs.parameter import Parameter -from pylibs import logger -from pylibs.watchdog import configured_devices -from configparser import SectionProxy +from pylibs import logger, watchdog class Streamer(Section): binary_path = None @@ -39,14 +38,13 @@ def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: return success async def execute(self, lock: asyncio.Lock): - global configured_devices if not os.path.exists(self.binary_path): logger.log_multiline(self.missing_bin_txt, logger.log_error) return False logger.log_quiet( f"Starting {self.keyword} with device {self.parameters['device'].value} ..." ) - configured_devices.append(self.parameters['device'].value) + watchdog.configured_devices.append(self.parameters['device'].value) return True def load_module(): diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index 64ced20a..bec3ade3 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -2,10 +2,7 @@ import asyncio from pylibs.components.streamer.streamer import Streamer -from pylibs.utils import execute_command, get_executable -from pylibs.hwhandler import is_device_legacy, has_device_mjpg_hw -from pylibs.v4l2_control import set_v4l2ctrls, blockyfix, brokenfocus -from pylibs import logger +from pylibs import logger, utils, hwhandler, v4l2_control as v4l2_ctl class Ustreamer(Streamer): section_name = 'cam' @@ -15,7 +12,7 @@ def __init__(self, name: str = '') -> None: super().__init__(name) if Ustreamer.binary_path is None: - Ustreamer.binary_path = get_executable( + Ustreamer.binary_path = utils.get_executable( ['ustreamer.bin', 'ustreamer'], ['bin/ustreamer'] ) @@ -44,19 +41,19 @@ async def execute(self, lock: asyncio.Lock): '--static', '"ustreamer-www"' ] - if is_device_legacy(device): + if hwhandler.is_device_legacy(device): streamer_args += [ '--format', 'MJPEG', '--device-timeout', '5', '--buffers', '3' ] - blockyfix(device) + v4l2_ctl.blockyfix(device) else: streamer_args += [ '--device', device, '--device-timeout', '2' ] - if has_device_mjpg_hw(device): + if hwhandler.has_device_mjpg_hw(device): streamer_args += [ '--format', 'MJPEG', '--encoder', 'HW' @@ -64,7 +61,7 @@ async def execute(self, lock: asyncio.Lock): v4l2ctl = self.parameters['v4l2ctl'].value if v4l2ctl: - set_v4l2ctrls(f'[cam {self.name}]', device, v4l2ctl.split(',')) + v4l2_ctl.set_v4l2ctrls(f'[cam {self.name}]', device, v4l2ctl.split(',')) # custom flags streamer_args += self.parameters['custom_flags'].value.split() @@ -74,7 +71,7 @@ async def execute(self, lock: asyncio.Lock): # logger.log_quiet(f"Starting ustreamer with device {device} ...") logger.log_debug(log_pre + f"Parameters: {' '.join(streamer_args)}") - process,_,_ = await execute_command( + process,_,_ = await utils.execute_command( cmd, info_log_pre=log_pre, info_log_func=logger.log_debug, @@ -88,7 +85,7 @@ async def execute(self, lock: asyncio.Lock): for ctl in v4l2ctl.split(','): if 'focus_absolute' in ctl: focus_absolute = ctl.split('=')[1].strip() - brokenfocus(device, focus_absolute) + v4l2_ctl.brokenfocus(device, focus_absolute) break return process diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index c73a4a1f..890554ed 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -2,8 +2,7 @@ import shutil import re -from pylibs import utils -from pylibs.v4l2_control import get_uvc_formats, get_uvc_v4l2ctrls +from pylibs import utils, v4l2_control as v4l2_ctl avail_cams = { 'uvc': {}, @@ -22,14 +21,14 @@ def get_avail_uvc_dev() -> dict: for cam_path in avail_uvc: cams[cam_path] = {} cams[cam_path]['realpath'] = os.path.realpath(cam_path) - cams[cam_path]['formats'] = get_uvc_formats(cam_path) - cams[cam_path]['v4l2ctrls'] = get_uvc_v4l2ctrls(cam_path) + cams[cam_path]['formats'] = v4l2_ctl.get_uvc_formats(cam_path) + cams[cam_path]['v4l2ctrls'] = v4l2_ctl.get_uvc_v4l2ctrls(cam_path) avail_cams['uvc'].update(cams) return cams def has_device_mjpg_hw(cam_path: str) -> bool: global avail_cams - return 'Motion-JPEG, compressed' in get_uvc_formats(cam_path) + return 'Motion-JPEG, compressed' in v4l2_ctl.get_uvc_formats(cam_path) def get_avail_libcamera() -> dict: cmd = shutil.which('libcamera-hello') @@ -113,8 +112,8 @@ def get_avail_legacy() -> dict: legacy_path = lines[i+1].strip() break legacy[legacy_path] = {} - legacy[legacy_path]['formats'] = get_uvc_formats(legacy_path) - legacy[legacy_path]['v4l2ctrls'] = get_uvc_v4l2ctrls(legacy_path) + legacy[legacy_path]['formats'] = v4l2_ctl.get_uvc_formats(legacy_path) + legacy[legacy_path]['v4l2ctrls'] = v4l2_ctl.get_uvc_v4l2ctrls(legacy_path) avail_cams['legacy'].update(legacy) return legacy diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 833b348c..e8b2cf25 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -1,23 +1,18 @@ -import shutil -# log_config import re -# log_host_info import os -from pylibs import utils -# log_cams import sys -from pylibs.logger import log_quiet, log_info, log_error, log_multiline, indentation -from pylibs.hwhandler import get_avail_uvc_dev, get_avail_libcamera, get_avail_legacy + +from pylibs import utils, logger, hwhandler def log_initial(): - log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') + logger.log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') command = 'git describe --always --tags' version = utils.execute_shell_command(command) - log_quiet(f'Version: {version}') - log_quiet('Prepare Startup ...') + logger.log_quiet(f'Version: {version}') + logger.log_quiet('Prepare Startup ...') def log_config(config_path): - log_info("Print Configfile: '" + config_path + "'") + logger.log_info("Print Configfile: '" + config_path + "'") with open(config_path, 'r') as file: config_txt = file.read() # Remove comments @@ -29,29 +24,29 @@ def log_config(config_path): # Remove leading and trailing whitespaces config_txt = config_txt.strip() # Split the config file into lines - log_multiline(config_txt, log_info, indentation) + logger.log_multiline(config_txt, logger.log_info, logger.indentation) def log_host_info(): - log_info("Host Information:") - log_pre = indentation #"Host Info: " + logger.log_info("Host Information:") + log_pre = logger.indentation ### OS Infos # OS Version distribution = grep('/etc/os-release', 'PRETTY_NAME') distribution = distribution.strip().split('=')[1].strip('"') - log_info(f'Distribution: {distribution}', log_pre) + logger.log_info(f'Distribution: {distribution}', log_pre) # Release Version of MainsailOS (if file present) try: with open('/etc/mainsailos-release', 'r') as file: content = file.read() - log_info(f'Release: {content.strip()}', log_pre) + logger.log_info(f'Release: {content.strip()}', log_pre) except FileNotFoundError: pass # Kernel Version uname = os.uname() - log_info(f'Kernel: {uname.sysname} {uname.release} {uname.machine}', log_pre) + logger.log_info(f'Kernel: {uname.sysname} {uname.release} {uname.machine}', log_pre) ### Host Machine Infos @@ -61,22 +56,22 @@ def log_host_info(): model == grep('/proc/cpuinfo', 'model name').split(':')[1].strip() if model == '': model = 'Unknown' - log_info(f'Model: {model}', log_pre) + logger.log_info(f'Model: {model}', log_pre) # CPU count cpu_count = os.cpu_count() - log_info(f"Available CPU Cores: {cpu_count}", log_pre) + logger.log_info(f"Available CPU Cores: {cpu_count}", log_pre) # Avail mem # psutil.virtual_memory().total memtotal = grep('/proc/meminfo', 'MemTotal:').split(':')[1].strip() - log_info(f'Available Memory: {memtotal}', log_pre) + logger.log_info(f'Available Memory: {memtotal}', log_pre) # Avail disk size # Alternative shutil.disk_usage.total command = 'LC_ALL=C df -h / | awk \'NR==2 {print $4" / "$2}\'' disksize = utils.execute_shell_command(command) - log_info(f'Diskspace (avail. / total): {disksize}', log_pre) + logger.log_info(f'Diskspace (avail. / total): {disksize}', log_pre) def grep(path: str, search: str) -> str: with open(path, 'r') as file: @@ -87,40 +82,40 @@ def grep(path: str, search: str) -> str: return '' def log_cams(): - log_info("Detect available Devices") - libcamera = get_avail_libcamera() - uvc = get_avail_uvc_dev() - legacy = get_avail_legacy() + logger.log_info("Detect available Devices") + libcamera = hwhandler.get_avail_libcamera() + uvc = hwhandler.get_avail_uvc_dev() + legacy = hwhandler.get_avail_legacy() total = len(libcamera.keys()) + len(legacy.keys()) + len(uvc.keys()) if total == 0: - log_error("No usable Devices Found. Stopping ") + logger.log_error("No usable Devices Found. Stopping ") sys.exit() - log_info(f"Found {total} total available Device(s)") + logger.log_info(f"Found {total} total available Device(s)") if libcamera: - log_info(f"Found {len(libcamera.keys())} available 'libcamera' device(s)") + logger.log_info(f"Found {len(libcamera.keys())} available 'libcamera' device(s)") for path, properties in libcamera.items(): log_libcamera_dev(path, properties) if legacy: for path, properties in legacy.items(): - log_info(f"Detected 'Raspicam' Device -> {path}") + logger.log_info(f"Detected 'Raspicam' Device -> {path}") log_uvc_formats(properties) log_uvc_v4l2ctrls(properties) if uvc: - log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") + logger.log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") for path, properties in uvc.items(): - log_info(f"{path} -> {properties['realpath']}", '') + logger.log_info(f"{path} -> {properties['realpath']}", '') log_uvc_formats(properties) log_uvc_v4l2ctrls(properties) def log_libcamera_dev(path: str, properties: dict) -> str: - log_info(f"Detected 'libcamera' device -> {path}") - log_info(f"Advertised Formats:", '') + logger.log_info(f"Detected 'libcamera' device -> {path}") + logger.log_info(f"Advertised Formats:", '') resolutions = properties['resolutions'] for res in resolutions: - log_info(f"{res}", indentation) - log_info(f"Supported Controls:", '') + logger.log_info(f"{res}", logger.indentation) + logger.log_info(f"Supported Controls:", '') controls = properties['controls'] if controls: for name, value in controls.items(): @@ -128,18 +123,18 @@ def log_libcamera_dev(path: str, properties: dict) -> str: str_first = f"{name} ({get_type_str(min)})" str_second = f"min={min} max={max} default={default}" str_indent = (30 - len(str_first)) * ' ' + ': ' - log_info(str_first + str_indent + str_second, indentation) + logger.log_info(str_first + str_indent + str_second, logger.indentation) else: - log_info("apt package 'python3-libcamera' is not installed! " - "Make sure to install it to log the controls!", indentation) + logger.log_info("apt package 'python3-libcamera' is not installed! " + "Make sure to install it to log the controls!", logger.indentation) def get_type_str(obj) -> str: return str(type(obj)).split('\'')[1] def log_uvc_formats(properties: dict) -> None: - log_info(f"Supported Formats:", '') - log_multiline(properties['formats'], log_info, indentation) + logger.log_info(f"Supported Formats:", '') + logger.log_multiline(properties['formats'], logger.log_info, logger.indentation) def log_uvc_v4l2ctrls(properties: dict) -> None: - log_info(f"Supported Controls:", '') - log_multiline(properties['v4l2ctrls'], log_info, indentation) \ No newline at end of file + logger.log_info(f"Supported Controls:", '') + logger.log_multiline(properties['v4l2ctrls'], logger.log_info, logger.indentation) \ No newline at end of file From 1d50ec99d7002868c0662abef22e5f5b2050066c Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 13 Mar 2024 18:20:27 +0100 Subject: [PATCH 075/129] chore: wip Signed-off-by: Patrick Gehrsitz --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e4c2fd4f..022a85b7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ leftover* # tmp file workaround lost* -# ignore bin paths +# Ignore bin paths bin/ustreamer bin/camera-streamer @@ -30,3 +30,6 @@ tools/.config # Ignore pkglist tools/pkglist.sh + +# Ignore pycache +**/__pycache__/ From a255f17aeb8472ae0b43f295f7eb05f81b87fa7c Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 13 Mar 2024 18:22:02 +0100 Subject: [PATCH 076/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 0d697257..216542e3 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,10 +1,8 @@ import argparse import configparser + from pylibs.components.crowsnest import Crowsnest -from pylibs.utils import get_module_class -from pylibs.watchdog import crowsnest_watchdog -import pylibs.logger as logger -import pylibs.logging_helper as logging_helper +from pylibs import utils, watchdog, logger, logging_helper import asyncio import pathlib @@ -46,7 +44,7 @@ async def start_sections(): if section_keyword == 'crowsnest': continue - section_class = get_module_class('pylibs.components', section_keyword) + section_class = utils.get_module_class('pylibs.components', section_keyword) section_name = ' '.join(section_header[1:]) section_object = section_class(section_name) if section_object.parse_config(config[section]): @@ -83,7 +81,7 @@ async def run_watchdog(): global watchdog_running while watchdog_running: await asyncio.sleep(120) - crowsnest_watchdog() + watchdog.crowsnest_watchdog() async def main(): From f8201e2d31d3fd53b85ebb57a48961ab1af3476e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 14 Mar 2024 15:02:06 +0100 Subject: [PATCH 077/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 22 +++++++++++----------- pylibs/components/cam.py | 4 ++-- pylibs/components/crowsnest.py | 4 ++-- pylibs/components/section.py | 2 +- pylibs/components/streamer/streamer.py | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 216542e3..43f11c50 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -20,18 +20,18 @@ watchdog_running = True -def parse_config(): +def initial_parse_config(): global crowsnest, config, args config_path = args.config config.read(config_path) crowsnest = Crowsnest('crowsnest') - crowsnest.parse_config(config['crowsnest']) + crowsnest.parse_config_section(config['crowsnest']) logger.set_log_level(crowsnest.parameters['log_level'].value) async def start_sections(): global config, watchdog_running - sec_objs = [] - sec_exec_tasks = set() + sect_objs = [] + sect_exec_tasks = set() logger.log_quiet("Try to start configured Cams / Services...") try: @@ -47,29 +47,29 @@ async def start_sections(): section_class = utils.get_module_class('pylibs.components', section_keyword) section_name = ' '.join(section_header[1:]) section_object = section_class(section_name) - if section_object.parse_config(config[section]): - sec_objs.append(section_object) + if section_object.parse_config_section(config[section]): + sect_objs.append(section_object) logger.log_info(f"Configuration of section [{section}] looks good. Continue ...") else: logger.log_error(f"Failed to parse config for section [{section}]!") lock = asyncio.Lock() - for section_object in sec_objs: + for section_object in sect_objs: task = asyncio.create_task(section_object.execute(lock)) - sec_exec_tasks.add(task) + sect_exec_tasks.add(task) # Let sec_exec_tasks finish first await asyncio.sleep(0) async with lock: logger.log_quiet("... Done!") - for task in sec_exec_tasks: + for task in sect_exec_tasks: if task is not None: await task except Exception as e: print(e) finally: - for task in sec_exec_tasks: + for task in sect_exec_tasks: if task != None: task.cancel() watchdog_running = False @@ -89,7 +89,7 @@ async def main(): logger.setup_logging(args.log_path) logging_helper.log_initial() - parse_config() + initial_parse_config() if crowsnest.parameters['delete_log'].value: pathlib.Path.unlink(args.log_path) diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index 8ccd9a1d..cf2d9c17 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -18,13 +18,13 @@ def __init__(self, name: str = '') -> None: self.streamer = None - def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: + def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: # Dynamically import module mode = config_section["mode"].split()[0] self.parameters["mode"].set_value(mode) mode_class = utils.get_module_class('pylibs.components.streamer', mode) self.streamer = mode_class(self.name) - return self.streamer.parse_config(config_section) + return self.streamer.parse_config_section(config_section) async def execute(self, lock: asyncio.Lock): if self.streamer is None: diff --git a/pylibs/components/crowsnest.py b/pylibs/components/crowsnest.py index 554fb48d..d8ea16a2 100644 --- a/pylibs/components/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -14,8 +14,8 @@ def __init__(self, name: str = '') -> None: 'no_proxy': Parameter(bool, 'False') }) - def parse_config(self, section: SectionProxy): - super().parse_config(section) + def parse_config_section(self, section: SectionProxy): + super().parse_config_section(section) log_level = self.parameters['log_level'].value.lower() if log_level == 'quiet': self.parameters['log_level'].value = 'QUIET' diff --git a/pylibs/components/section.py b/pylibs/components/section.py index db3ff900..9ff6526d 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -17,7 +17,7 @@ def __init__(self, name: str = '') -> None: self.parameters: dict[str, Parameter] = {} # Parse config according to the needs of the section - def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: + def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: success = True for parameter in config_section: value = config_section[parameter] diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index 9b9a5702..d44189e9 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -29,8 +29,8 @@ def __init__(self, name: str = '') -> None: Please make sure everything is installed correctly and up to date! Run 'make update' inside the crowsnest directory to install and update everything.""" - def parse_config(self, config_section: SectionProxy, *args, **kwargs) -> bool: - success = super().parse_config(config_section, *args, **kwargs) + def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: + success = super().parse_config_section(config_section, *args, **kwargs) if self.binary_path is None: logger.log_multiline(self.missing_bin_txt % self.parameters['mode'].value, logger.log_error) From c9f63e16b55e35e6aafa6c5c69aad7d392ed115b Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 14 Mar 2024 16:11:45 +0100 Subject: [PATCH 078/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 9 ++++---- pylibs/components/cam.py | 21 +++++++++++++------ pylibs/components/crowsnest.py | 7 +++++++ pylibs/components/section.py | 6 +++++- pylibs/components/streamer/camera-streamer.py | 10 ++++++--- pylibs/components/streamer/streamer.py | 7 ++++--- pylibs/components/streamer/ustreamer.py | 10 ++++++--- pylibs/utils.py | 13 ++++++++---- 8 files changed, 59 insertions(+), 24 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 43f11c50..d04a8370 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -44,11 +44,12 @@ async def start_sections(): if section_keyword == 'crowsnest': continue - section_class = utils.get_module_class('pylibs.components', section_keyword) section_name = ' '.join(section_header[1:]) - section_object = section_class(section_name) - if section_object.parse_config_section(config[section]): - sect_objs.append(section_object) + section = utils.load_component(section_keyword, section_name, config[section]) + # section_object = section_class(section_name) + # if section_object.parse_config_section(config[section]): + if section: + sect_objs.append(section) logger.log_info(f"Configuration of section [{section}] looks good. Continue ...") else: logger.log_error(f"Failed to parse config for section [{section}]!") diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index cf2d9c17..8f2aee6d 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -9,7 +9,7 @@ class Cam(Section): section_name = 'cam' keyword = 'cam' - def __init__(self, name: str = '') -> None: + def __init__(self, name: str) -> None: super().__init__(name) self.parameters.update({ @@ -22,9 +22,15 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> # Dynamically import module mode = config_section["mode"].split()[0] self.parameters["mode"].set_value(mode) - mode_class = utils.get_module_class('pylibs.components.streamer', mode) - self.streamer = mode_class(self.name) - return self.streamer.parse_config_section(config_section) + self.streamer = utils.load_component(mode, + self.name, + config_section, + path='pylibs.components.streamer') + if self.streamer: + return True + else: + return False + # return self.streamer.parse_config_section(config_section) async def execute(self, lock: asyncio.Lock): if self.streamer is None: @@ -41,5 +47,8 @@ async def execute(self, lock: asyncio.Lock): if lock.locked(): lock.release() -def load_module(): - return Cam +def load_component(name: str, config_section: SectionProxy, *args, **kwargs): + cam = Cam(name) + if cam.parse_config_section(config_section, *args, **kwargs): + return cam + return None diff --git a/pylibs/components/crowsnest.py b/pylibs/components/crowsnest.py index d8ea16a2..018c131b 100644 --- a/pylibs/components/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -25,3 +25,10 @@ def parse_config_section(self, section: SectionProxy): self.parameters['log_level'].value = 'DEV' else: self.parameters['log_level'].value = 'INFO' + + +def load_component(name: str, config_section: SectionProxy, *args, **kwargs): + cn = Crowsnest(name) + if cn.parse_config_section(config_section, *args, **kwargs): + return cn + return None diff --git a/pylibs/components/section.py b/pylibs/components/section.py index 9ff6526d..0882d1da 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -12,7 +12,7 @@ class Section: # [ ] # param1: value1 # param2: value2 - def __init__(self, name: str = '') -> None: + def __init__(self, name: str) -> None: self.name = name self.parameters: dict[str, Parameter] = {} @@ -36,3 +36,7 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> # Execute section specific stuff, e.g. starting cam async def execute(self, lock: asyncio.Lock): raise NotImplementedError("If you see this, a module is implemented wrong!!!") + + +def load_component(*args, **kwargs): + raise NotImplementedError("If you see this, something went wrong!!!") diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index e31149a2..2d95e6cb 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -1,4 +1,5 @@ import asyncio +from configparser import SectionProxy from pylibs.components.streamer.streamer import Streamer from pylibs.parameter import Parameter @@ -7,7 +8,7 @@ class Camera_Streamer(Streamer): keyword = 'camera-streamer' - def __init__(self, name: str = '') -> None: + def __init__(self, name: str) -> None: super().__init__(name) self.parameters.update({ @@ -98,5 +99,8 @@ async def execute(self, lock: asyncio.Lock): return process -def load_module(): - return Camera_Streamer +def load_component(name: str, config_section: SectionProxy, *args, **kwargs): + cst = Camera_Streamer(name) + if cst.parse_config_section(config_section, *args, **kwargs): + return cst + return None diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index d44189e9..06d09484 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -9,7 +9,7 @@ class Streamer(Section): binary_path = None - def __init__(self, name: str = '') -> None: + def __init__(self, name: str) -> None: super().__init__(name) self.parameters.update({ @@ -47,5 +47,6 @@ async def execute(self, lock: asyncio.Lock): watchdog.configured_devices.append(self.parameters['device'].value) return True -def load_module(): - raise NotImplementedError("If you see this, a Streamer module is implemented wrong!!!") + +def load_component(name: str, config_section: SectionProxy, *args, **kwargs): + raise NotImplementedError("If you see this, something went wrong!!!") diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index bec3ade3..29725c20 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -1,5 +1,6 @@ import re import asyncio +from configparser import SectionProxy from pylibs.components.streamer.streamer import Streamer from pylibs import logger, utils, hwhandler, v4l2_control as v4l2_ctl @@ -8,7 +9,7 @@ class Ustreamer(Streamer): section_name = 'cam' keyword = 'ustreamer' - def __init__(self, name: str = '') -> None: + def __init__(self, name: str) -> None: super().__init__(name) if Ustreamer.binary_path is None: @@ -98,5 +99,8 @@ def custom_log(self, msg: str): logger.log_debug(msg) -def load_module(): - return Ustreamer +def load_component(name: str, config_section: SectionProxy, *args, **kwargs): + ust = Ustreamer(name) + if ust.parse_config_section(config_section, *args, **kwargs): + return ust + return None diff --git a/pylibs/utils.py b/pylibs/utils.py index 8a0fcb08..066853cf 100644 --- a/pylibs/utils.py +++ b/pylibs/utils.py @@ -4,19 +4,24 @@ import math import shutil import os +from configparser import SectionProxy from pylibs import logger # Dynamically import module # Requires module to have a load_module() function, # as well as the same name as the section keyword -def get_module_class(path = '', module_name = ''): +def load_component(component: str, + name: str, + config_section: SectionProxy, + path='pylibs.components', + *args, **kwargs): module_class = None try: - module = importlib.import_module(f'{path}.{module_name}') - module_class = getattr(module, 'load_module')() + component = importlib.import_module(f'{path}.{component}') + module_class = getattr(component, 'load_component')(name, config_section, *args, **kwargs) except (ModuleNotFoundError, AttributeError) as e: - print('ERROR: '+str(e)) + logger.log_error(f"Failed to load module '{component}' from '{path}'") return module_class async def log_subprocess_output(stream, log_func, line_prefix = ''): From 47b3a7400996c715c1a519dff4645d84d2dffa2e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 14 Mar 2024 16:26:34 +0100 Subject: [PATCH 079/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 4 ---- pylibs/components/cam.py | 1 - pylibs/utils.py | 10 ++-------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index d04a8370..07478bf3 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -46,8 +46,6 @@ async def start_sections(): section_name = ' '.join(section_header[1:]) section = utils.load_component(section_keyword, section_name, config[section]) - # section_object = section_class(section_name) - # if section_object.parse_config_section(config[section]): if section: sect_objs.append(section) logger.log_info(f"Configuration of section [{section}] looks good. Continue ...") @@ -107,8 +105,6 @@ async def main(): if task2: task2.cancel() - # asyncio.gather(start_processes(), run_watchdog()) - if __name__ == "__main__": loop = asyncio.get_event_loop() try: diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index 8f2aee6d..87d781d2 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -30,7 +30,6 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> return True else: return False - # return self.streamer.parse_config_section(config_section) async def execute(self, lock: asyncio.Lock): if self.streamer is None: diff --git a/pylibs/utils.py b/pylibs/utils.py index 066853cf..6bf322b6 100644 --- a/pylibs/utils.py +++ b/pylibs/utils.py @@ -8,8 +8,8 @@ from pylibs import logger -# Dynamically import module -# Requires module to have a load_module() function, +# Dynamically import component +# Requires module to have a load_component() function, # as well as the same name as the section keyword def load_component(component: str, name: str, @@ -61,12 +61,6 @@ async def execute_command( ) return process, stdout_task, stderr_task - # Wait for the subprocess to finish - #await process.wait() - - # Wait for the output handling tasks to finish - #await stdout_task - #await stderr_task def execute_shell_command(command: str, strip: bool = True) -> str: try: From 9b68eead959d52bb00a276abb2440e453ca71110 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 14 Mar 2024 16:59:37 +0100 Subject: [PATCH 080/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/streamer/ustreamer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index 29725c20..fbe9f0ab 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -70,7 +70,6 @@ async def execute(self, lock: asyncio.Lock): cmd = self.binary_path + ' ' + ' '.join(streamer_args) log_pre = f'ustreamer [cam {self.name}]: ' - # logger.log_quiet(f"Starting ustreamer with device {device} ...") logger.log_debug(log_pre + f"Parameters: {' '.join(streamer_args)}") process,_,_ = await utils.execute_command( cmd, From 3a9922a8278ce566a31d32d7dbb44c2caa4fa386 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 16 Mar 2024 19:17:01 +0100 Subject: [PATCH 081/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowsnest.py b/crowsnest.py index 07478bf3..4038d513 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -91,7 +91,7 @@ async def main(): initial_parse_config() if crowsnest.parameters['delete_log'].value: - pathlib.Path.unlink(args.log_path) + pathlib.Path(args.log_path).unlink(missing_ok=True) logging_helper.log_initial() logging_helper.log_host_info() From 17f4a31397ac3894151e90805839d6224b01972a Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 17 Mar 2024 13:51:47 +0100 Subject: [PATCH 082/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 12 +++--------- pylibs/watchdog.py | 8 ++++++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 4038d513..e8765f26 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -29,7 +29,7 @@ def initial_parse_config(): logger.set_log_level(crowsnest.parameters['log_level'].value) async def start_sections(): - global config, watchdog_running + global config sect_objs = [] sect_exec_tasks = set() @@ -71,17 +71,11 @@ async def start_sections(): for task in sect_exec_tasks: if task != None: task.cancel() - watchdog_running = False + watchdog.running = False logger.log_quiet("Shutdown or Killed by User!") logger.log_quiet("Please come again :)") logger.log_quiet("Goodbye...") -async def run_watchdog(): - global watchdog_running - while watchdog_running: - await asyncio.sleep(120) - watchdog.crowsnest_watchdog() - async def main(): global args, crowsnest @@ -99,7 +93,7 @@ async def main(): logging_helper.log_cams() task1 = asyncio.create_task(start_sections()) - task2 = asyncio.create_task(run_watchdog()) + task2 = asyncio.create_task(watchdog.run_watchdog()) await task1 if task2: diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index 63839754..e324de45 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -1,7 +1,9 @@ import os +import asyncio from pylibs import logger configured_devices: list[str] = [] +running = True def crowsnest_watchdog(): global configured_devices @@ -17,3 +19,9 @@ def crowsnest_watchdog(): elif device in lost_devices and os.path.exists(device): lost_devices.remove(device) logger.log_quiet(f"Device '{device}' returned.", prefix) + +async def run_watchdog(): + global running + while running: + await asyncio.sleep(120) + crowsnest_watchdog() From 31b355cb5b6f65277108c9049c94426cd719f075 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 17 Mar 2024 15:04:01 +0100 Subject: [PATCH 083/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 6 +-- pylibs/components/cam.py | 23 +++++----- pylibs/components/section.py | 2 +- pylibs/components/streamer/camera-streamer.py | 19 +++------ pylibs/components/streamer/streamer.py | 42 +++++++++---------- pylibs/components/streamer/ustreamer.py | 18 ++------ pylibs/utils.py | 8 ++-- 7 files changed, 49 insertions(+), 69 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index e8765f26..6c5ca1e1 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -45,9 +45,9 @@ async def start_sections(): continue section_name = ' '.join(section_header[1:]) - section = utils.load_component(section_keyword, section_name, config[section]) - if section: - sect_objs.append(section) + component = utils.load_component(section_keyword, section_name) + if component.parse_config_section(config[section]): + sect_objs.append(component) logger.log_info(f"Configuration of section [{section}] looks good. Continue ...") else: logger.log_error(f"Failed to parse config for section [{section}]!") diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index 87d781d2..22b93124 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -2,8 +2,9 @@ from configparser import SectionProxy from pylibs.components.section import Section +from pylibs.components.streamer.streamer import Streamer from pylibs.parameter import Parameter -from pylibs import logger, utils +from pylibs import logger, utils, watchdog class Cam(Section): section_name = 'cam' @@ -16,7 +17,7 @@ def __init__(self, name: str) -> None: 'mode': Parameter(str) }) - self.streamer = None + self.streamer: Streamer = None def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: # Dynamically import module @@ -24,12 +25,8 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> self.parameters["mode"].set_value(mode) self.streamer = utils.load_component(mode, self.name, - config_section, path='pylibs.components.streamer') - if self.streamer: - return True - else: - return False + return self.streamer.parse_config_section(config_section, *args, **kwargs) async def execute(self, lock: asyncio.Lock): if self.streamer is None: @@ -37,6 +34,11 @@ async def execute(self, lock: asyncio.Lock): return try: await lock.acquire() + logger.log_quiet( + f"Starting {self.streamer.keyword} with device " + f"{self.streamer.parameters['device'].value} ..." + ) + watchdog.configured_devices.append(self.streamer.parameters['device'].value) process = await self.streamer.execute(lock) await process.wait() logger.log_error(f'Start of {self.parameters["mode"].value} [cam {self.name}] failed!') @@ -46,8 +48,5 @@ async def execute(self, lock: asyncio.Lock): if lock.locked(): lock.release() -def load_component(name: str, config_section: SectionProxy, *args, **kwargs): - cam = Cam(name) - if cam.parse_config_section(config_section, *args, **kwargs): - return cam - return None +def load_component(name: str): + return Cam(name) diff --git a/pylibs/components/section.py b/pylibs/components/section.py index 0882d1da..f2849357 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -35,7 +35,7 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> # Execute section specific stuff, e.g. starting cam async def execute(self, lock: asyncio.Lock): - raise NotImplementedError("If you see this, a module is implemented wrong!!!") + raise NotImplementedError("If you see this, a componenent is implemented wrong!!!") def load_component(*args, **kwargs): diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index 2d95e6cb..2b60b5a4 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -11,21 +11,15 @@ class Camera_Streamer(Streamer): def __init__(self, name: str) -> None: super().__init__(name) + self.binary_names = ['camera-streamer'] + self.binary_paths = ['bin/camera-streamer'] + self.parameters.update({ 'enable_rtsp': Parameter(bool, 'False'), 'rtsp_port': Parameter(int, 8554) }) - if Camera_Streamer.binary_path is None: - Camera_Streamer.binary_path = utils.get_executable( - ['camera-streamer'], - ['bin/camera-streamer'] - ) - self.binary_path = Camera_Streamer.binary_path - async def execute(self, lock: asyncio.Lock): - if not await super().execute(lock): - return None if self.parameters['no_proxy'].value: host = '0.0.0.0' logger.log_info("Set to 'no_proxy' mode! Using 0.0.0.0!") @@ -99,8 +93,5 @@ async def execute(self, lock: asyncio.Lock): return process -def load_component(name: str, config_section: SectionProxy, *args, **kwargs): - cst = Camera_Streamer(name) - if cst.parse_config_section(config_section, *args, **kwargs): - return cst - return None +def load_component(name: str): + return Camera_Streamer(name) diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index 06d09484..531a5aac 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -1,13 +1,12 @@ -import os -import asyncio +import textwrap from configparser import SectionProxy from pylibs.components.section import Section from pylibs.parameter import Parameter -from pylibs import logger, watchdog +from pylibs import logger, utils class Streamer(Section): - binary_path = None + binaries = {} def __init__(self, name: str) -> None: super().__init__(name) @@ -22,31 +21,32 @@ def __init__(self, name: str) -> None: 'custom_flags': Parameter(str, ''), 'v4l2ctl': Parameter(str, '') }) + self.binary_names = [] + self.binary_paths = [] self.binary_path = None - self.missing_bin_txt = """\ -'%s' executable not found! -Please make sure everything is installed correctly and up to date! -Run 'make update' inside the crowsnest directory to install and update everything.""" - + self.missing_bin_txt = textwrap.dedent("""\ + '%s' executable not found! + Please make sure everything is installed correctly and up to date! + Run 'make update' inside the crowsnest directory to install and update everything.""") + def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: success = super().parse_config_section(config_section, *args, **kwargs) + if not success: + return False + mode = self.parameters['mode'].value + if mode not in Streamer.binaries: + Streamer.binaries[mode] = utils.get_executable( + self.binary_names, + self.binary_paths + ) + self.binary_path = Streamer.binaries[mode] + if self.binary_path is None: logger.log_multiline(self.missing_bin_txt % self.parameters['mode'].value, logger.log_error) return False - return success - - async def execute(self, lock: asyncio.Lock): - if not os.path.exists(self.binary_path): - logger.log_multiline(self.missing_bin_txt, logger.log_error) - return False - logger.log_quiet( - f"Starting {self.keyword} with device {self.parameters['device'].value} ..." - ) - watchdog.configured_devices.append(self.parameters['device'].value) return True - -def load_component(name: str, config_section: SectionProxy, *args, **kwargs): +def load_component(name: str): raise NotImplementedError("If you see this, something went wrong!!!") diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index fbe9f0ab..6a0bc700 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -6,22 +6,15 @@ from pylibs import logger, utils, hwhandler, v4l2_control as v4l2_ctl class Ustreamer(Streamer): - section_name = 'cam' keyword = 'ustreamer' def __init__(self, name: str) -> None: super().__init__(name) - if Ustreamer.binary_path is None: - Ustreamer.binary_path = utils.get_executable( - ['ustreamer.bin', 'ustreamer'], - ['bin/ustreamer'] - ) - self.binary_path = Ustreamer.binary_path + self.binary_names = ['ustreamer.bin', 'ustreamer'] + self.binary_paths = ['bin/ustreamer'] async def execute(self, lock: asyncio.Lock): - if not await super().execute(lock): - return None if self.parameters['no_proxy'].value: host = '0.0.0.0' logger.log_info("Set to 'no_proxy' mode! Using 0.0.0.0!") @@ -98,8 +91,5 @@ def custom_log(self, msg: str): logger.log_debug(msg) -def load_component(name: str, config_section: SectionProxy, *args, **kwargs): - ust = Ustreamer(name) - if ust.parse_config_section(config_section, *args, **kwargs): - return ust - return None +def load_component(name: str): + return Ustreamer(name) diff --git a/pylibs/utils.py b/pylibs/utils.py index 6bf322b6..9dc3eb44 100644 --- a/pylibs/utils.py +++ b/pylibs/utils.py @@ -13,13 +13,11 @@ # as well as the same name as the section keyword def load_component(component: str, name: str, - config_section: SectionProxy, - path='pylibs.components', - *args, **kwargs): + path='pylibs.components'): module_class = None try: component = importlib.import_module(f'{path}.{component}') - module_class = getattr(component, 'load_component')(name, config_section, *args, **kwargs) + module_class = getattr(component, 'load_component')(name) except (ModuleNotFoundError, AttributeError) as e: logger.log_error(f"Failed to load module '{component}' from '{path}'") return module_class @@ -82,6 +80,8 @@ def find_file(name: str, path: str) -> str: return None def get_executable(names: list[str], paths: list[str]) -> str: + if names is None or paths is None: + return None for name in names: exec = shutil.which(name) if exec: From 0269073c8bddfd9a19576e57c0250cccb4562fa4 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 17 Mar 2024 16:17:36 +0100 Subject: [PATCH 084/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/streamer/camera-streamer.py | 1 - pylibs/components/streamer/ustreamer.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index 2b60b5a4..b1ca7303 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -1,5 +1,4 @@ import asyncio -from configparser import SectionProxy from pylibs.components.streamer.streamer import Streamer from pylibs.parameter import Parameter diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index 6a0bc700..9966a24c 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -1,6 +1,5 @@ import re import asyncio -from configparser import SectionProxy from pylibs.components.streamer.streamer import Streamer from pylibs import logger, utils, hwhandler, v4l2_control as v4l2_ctl From 2c4bb6bd98077142bbf761ded01525e4c06bb63a Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 21 Mar 2024 12:03:00 +0100 Subject: [PATCH 085/129] chorte: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logging_helper.py | 26 +++++++++----------------- pylibs/utils.py | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index e8b2cf25..25882fa5 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -1,6 +1,7 @@ import re import os import sys +import shutil from pylibs import utils, logger, hwhandler @@ -32,7 +33,7 @@ def log_host_info(): ### OS Infos # OS Version - distribution = grep('/etc/os-release', 'PRETTY_NAME') + distribution = utils.grep('/etc/os-release', 'PRETTY_NAME') distribution = distribution.strip().split('=')[1].strip('"') logger.log_info(f'Distribution: {distribution}', log_pre) @@ -51,9 +52,9 @@ def log_host_info(): ### Host Machine Infos # Host model - model = grep('/proc/cpuinfo', 'Model').split(':')[1].strip() + model = utils.grep('/proc/cpuinfo', 'Model').split(':')[1].strip() if model == '': - model == grep('/proc/cpuinfo', 'model name').split(':')[1].strip() + model == utils.grep('/proc/cpuinfo', 'model name').split(':')[1].strip() if model == '': model = 'Unknown' logger.log_info(f'Model: {model}', log_pre) @@ -63,23 +64,14 @@ def log_host_info(): logger.log_info(f"Available CPU Cores: {cpu_count}", log_pre) # Avail mem - # psutil.virtual_memory().total - memtotal = grep('/proc/meminfo', 'MemTotal:').split(':')[1].strip() + memtotal = utils.grep('/proc/meminfo', 'MemTotal:').split(':')[1].strip() logger.log_info(f'Available Memory: {memtotal}', log_pre) # Avail disk size - # Alternative shutil.disk_usage.total - command = 'LC_ALL=C df -h / | awk \'NR==2 {print $4" / "$2}\'' - disksize = utils.execute_shell_command(command) - logger.log_info(f'Diskspace (avail. / total): {disksize}', log_pre) - -def grep(path: str, search: str) -> str: - with open(path, 'r') as file: - lines = file.readlines() - for line in lines: - if search in line: - return line - return '' + total, _, free = shutil.disk_usage("/") + total = utils.bytes_to_gigabytes(total) + free = utils.bytes_to_gigabytes(free) + logger.log_info(f'Diskspace (avail. / total): {free}G / {total}G', log_pre) def log_cams(): logger.log_info("Detect available Devices") diff --git a/pylibs/utils.py b/pylibs/utils.py index 9dc3eb44..30d74af0 100644 --- a/pylibs/utils.py +++ b/pylibs/utils.py @@ -1,10 +1,8 @@ import importlib import asyncio import subprocess -import math import shutil import os -from configparser import SectionProxy from pylibs import logger @@ -70,7 +68,7 @@ def execute_shell_command(command: str, strip: bool = True) -> str: return '' def bytes_to_gigabytes(value: int) -> int: - return math.round(value / 1024 / 1024 / 1024) + return round(value / 1024**3) def find_file(name: str, path: str) -> str: for dpath, _, fnames in os.walk(path): @@ -91,3 +89,14 @@ def get_executable(names: list[str], paths: list[str]) -> str: if found: return found return None + +def grep(path: str, search: str) -> str: + try: + with open(path, 'r') as file: + lines = file.readlines() + for line in lines: + if search in line: + return line + except FileNotFoundError: + logger.log_error(f"File '{path}' not found!") + return '' From 451b71cb8b85a6db6295375476ed4c168909405c Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 22 Mar 2024 15:11:26 +0100 Subject: [PATCH 086/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 17 ++++++++++++----- pylibs/components/cam.py | 3 ++- pylibs/logger.py | 3 ++- pylibs/watchdog.py | 4 ++-- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 6c5ca1e1..d5edf2fc 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -5,7 +5,7 @@ from pylibs import utils, watchdog, logger, logging_helper import asyncio -import pathlib +import signal parser = argparse.ArgumentParser( prog='Crowsnest', @@ -29,7 +29,7 @@ def initial_parse_config(): logger.set_log_level(crowsnest.parameters['log_level'].value) async def start_sections(): - global config + global config, sect_exec_tasks sect_objs = [] sect_exec_tasks = set() @@ -62,20 +62,26 @@ async def start_sections(): async with lock: logger.log_quiet("... Done!") + # Catch SIGINT and SIGTERM to exit gracefully and cancel all tasks + signal.signal(signal.SIGINT, exit_gracefully) + signal.signal(signal.SIGTERM, exit_gracefully) + for task in sect_exec_tasks: if task is not None: await task except Exception as e: - print(e) + logger.log_error(e) finally: for task in sect_exec_tasks: - if task != None: + if task is not None: task.cancel() watchdog.running = False logger.log_quiet("Shutdown or Killed by User!") logger.log_quiet("Please come again :)") logger.log_quiet("Goodbye...") +async def exit_gracefully(signum, frame): + asyncio.sleep(1) async def main(): global args, crowsnest @@ -85,7 +91,8 @@ async def main(): initial_parse_config() if crowsnest.parameters['delete_log'].value: - pathlib.Path(args.log_path).unlink(missing_ok=True) + logger.logger.handlers.clear() + logger.setup_logging(args.log_path, 'w', crowsnest.parameters['log_level'].value) logging_helper.log_initial() logging_helper.log_host_info() diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index 22b93124..48d60d8b 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -41,10 +41,11 @@ async def execute(self, lock: asyncio.Lock): watchdog.configured_devices.append(self.streamer.parameters['device'].value) process = await self.streamer.execute(lock) await process.wait() - logger.log_error(f'Start of {self.parameters["mode"].value} [cam {self.name}] failed!') except Exception as e: pass finally: + logger.log_error(f'Start of {self.parameters["mode"].value} [cam {self.name}] failed!') + watchdog.configured_devices.remove(self.streamer.parameters['device'].value) if lock.locked(): lock.release() diff --git a/pylibs/logger.py b/pylibs/logger.py index 168dffe3..9c47a817 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -25,7 +25,8 @@ def setup_logging(log_path, filemode='a', log_level=logging.INFO): formatter = logging.Formatter('[%(asctime)s] %(message)s', datefmt='%d/%m/%y %H:%M:%S') # WatchedFileHandler for log file. This handler will reopen the file if it is moved or deleted. - filehandler = logging.handlers.WatchedFileHandler(log_path, filemode, 'utf-8') + # filehandler = logging.handlers.WatchedFileHandler(log_path, mode=filemode, encoding='utf-8') + filehandler = logging.handlers.RotatingFileHandler(log_path, mode=filemode, encoding='utf-8') filehandler.setFormatter(formatter) logger.addHandler(filehandler) diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index e324de45..35cf0720 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -7,7 +7,7 @@ def crowsnest_watchdog(): global configured_devices - prefix = "Crowsnest Watchdog: " + prefix = "Watchdog: " lost_devices = [] for device in configured_devices: @@ -23,5 +23,5 @@ def crowsnest_watchdog(): async def run_watchdog(): global running while running: - await asyncio.sleep(120) + await asyncio.sleep(10) crowsnest_watchdog() From ab4959499e917166c0d6fcb6d87d1007c9bccc85 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 22 Mar 2024 15:18:34 +0100 Subject: [PATCH 087/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/watchdog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index 35cf0720..d67e3303 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -13,7 +13,7 @@ def crowsnest_watchdog(): for device in configured_devices: if device.startswith('/base'): continue - if not os.path.exists(device): + if device not in lost_devices and not os.path.exists(device): lost_devices.append(device) logger.log_quiet(f"Lost Devicve: '{device}'", prefix) elif device in lost_devices and os.path.exists(device): From 760eb641ae3992f5b16dd37515dd7949613fce95 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 22 Mar 2024 15:40:55 +0100 Subject: [PATCH 088/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/watchdog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index d67e3303..25681ae5 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -3,12 +3,12 @@ from pylibs import logger configured_devices: list[str] = [] +lost_devices: list[str] = [] running = True def crowsnest_watchdog(): - global configured_devices + global configured_devices, lost_devices prefix = "Watchdog: " - lost_devices = [] for device in configured_devices: if device.startswith('/base'): @@ -23,5 +23,5 @@ def crowsnest_watchdog(): async def run_watchdog(): global running while running: - await asyncio.sleep(10) + await asyncio.sleep(120) crowsnest_watchdog() From f4d4b73d7489cee259801204a659d7be2bc0d2ad Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 22 Mar 2024 19:40:01 +0100 Subject: [PATCH 089/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/hwhandler.py | 17 ++- pylibs/logging_helper.py | 27 ++++- pylibs/v4l2/__init__.py | 0 pylibs/v4l2/constants.py | 34 ++++++ pylibs/v4l2/ctl.py | 213 ++++++++++++++++++++++++++++++++++++ pylibs/v4l2/ioctl_macros.py | 77 +++++++++++++ pylibs/v4l2/raw.py | 117 ++++++++++++++++++++ 7 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 pylibs/v4l2/__init__.py create mode 100644 pylibs/v4l2/constants.py create mode 100644 pylibs/v4l2/ctl.py create mode 100644 pylibs/v4l2/ioctl_macros.py create mode 100644 pylibs/v4l2/raw.py diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 890554ed..b76b0635 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -3,6 +3,7 @@ import re from pylibs import utils, v4l2_control as v4l2_ctl +from pylibs.v4l2 import ctl avail_cams = { 'uvc': {}, @@ -10,6 +11,19 @@ 'legacy': {} } +def v4l2_qctl_to_dict(device: str) -> dict: + dev_ctl = ctl.qctrls[device] + values = {} + cur_sec = '' + for control in dev_ctl: + cur_ctl = dev_ctl[control] + if not cur_ctl['values']: + cur_sec = control + values[cur_sec] = {} + continue + values[cur_sec][control] = cur_ctl['values'] + return values + def get_avail_uvc_dev() -> dict: uvc_path = '/dev/v4l/by-id/' avail_uvc = [] @@ -21,8 +35,9 @@ def get_avail_uvc_dev() -> dict: for cam_path in avail_uvc: cams[cam_path] = {} cams[cam_path]['realpath'] = os.path.realpath(cam_path) + ctl.init_device(cam_path) cams[cam_path]['formats'] = v4l2_ctl.get_uvc_formats(cam_path) - cams[cam_path]['v4l2ctrls'] = v4l2_ctl.get_uvc_v4l2ctrls(cam_path) + cams[cam_path]['v4l2ctrls'] = v4l2_qctl_to_dict(cam_path) avail_cams['uvc'].update(cams) return cams diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 25882fa5..85199400 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -4,6 +4,7 @@ import shutil from pylibs import utils, logger, hwhandler +from pylibs.v4l2 import ctl def log_initial(): logger.log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') @@ -93,13 +94,13 @@ def log_cams(): for path, properties in legacy.items(): logger.log_info(f"Detected 'Raspicam' Device -> {path}") log_uvc_formats(properties) - log_uvc_v4l2ctrls(properties) + log_uvc_v4l2ctrls(path, properties) if uvc: logger.log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") for path, properties in uvc.items(): logger.log_info(f"{path} -> {properties['realpath']}", '') log_uvc_formats(properties) - log_uvc_v4l2ctrls(properties) + log_uvc_v4l2ctrls(path, properties) def log_libcamera_dev(path: str, properties: dict) -> str: logger.log_info(f"Detected 'libcamera' device -> {path}") @@ -114,7 +115,7 @@ def log_libcamera_dev(path: str, properties: dict) -> str: min, max, default = value.values() str_first = f"{name} ({get_type_str(min)})" str_second = f"min={min} max={max} default={default}" - str_indent = (30 - len(str_first)) * ' ' + ': ' + str_indent = (35 - len(str_first)) * ' ' + ': ' logger.log_info(str_first + str_indent + str_second, logger.indentation) else: logger.log_info("apt package 'python3-libcamera' is not installed! " @@ -127,6 +128,22 @@ def log_uvc_formats(properties: dict) -> None: logger.log_info(f"Supported Formats:", '') logger.log_multiline(properties['formats'], logger.log_info, logger.indentation) -def log_uvc_v4l2ctrls(properties: dict) -> None: +def log_uvc_v4l2ctrls(device_path: str, properties: dict) -> None: logger.log_info(f"Supported Controls:", '') - logger.log_multiline(properties['v4l2ctrls'], logger.log_info, logger.indentation) \ No newline at end of file + logger.log_info('', '') + for section, controls in properties['v4l2ctrls'].items(): + logger.log_info(f"{section}:", '') + for control, data in controls.items(): + line = f"{control} ({data['type']})" + line += (35 - len(line)) * ' ' + ': ' + if data['type'] in ('int'): + line += f"min={data['min']} max={data['max']} step={data['step']}" + line += f" default={data['default']}" + line += f" value={ctl.get_control(device_path, control)}" + if 'flags' in data: + line += f" flags={data['flags']}" + logger.log_info(line, logger.indentation) + if 'menu' in data: + for value, name in data['menu'].items(): + logger.log_info(f"{value}: {name}", logger.indentation*2) + logger.log_info('', '') diff --git a/pylibs/v4l2/__init__.py b/pylibs/v4l2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pylibs/v4l2/constants.py b/pylibs/v4l2/constants.py new file mode 100644 index 00000000..38b73beb --- /dev/null +++ b/pylibs/v4l2/constants.py @@ -0,0 +1,34 @@ +V4L2_CTRL_MAX_DIMS = 4 +EINVAL = 22 +ENOTTY = 25 + +V4L2_CTRL_TYPE_INTEGER = 1 +V4L2_CTRL_TYPE_BOOLEAN = 2 +V4L2_CTRL_TYPE_MENU = 3 +V4L2_CTRL_TYPE_BUTTON = 4 +V4L2_CTRL_TYPE_INTEGER64 = 5 +V4L2_CTRL_TYPE_CTRL_CLASS = 6 +V4L2_CTRL_TYPE_STRING = 7 +V4L2_CTRL_TYPE_BITMASK = 8 +V4L2_CTRL_TYPE_INTEGER_MENU = 9 + +# Control flags +V4L2_CTRL_FLAG_DISABLED = 0x0001 +V4L2_CTRL_FLAG_GRABBED = 0x0002 +V4L2_CTRL_FLAG_READ_ONLY = 0x0004 +V4L2_CTRL_FLAG_UPDATE = 0x0008 +V4L2_CTRL_FLAG_INACTIVE = 0x0010 +V4L2_CTRL_FLAG_SLIDER = 0x0020 +V4L2_CTRL_FLAG_WRITE_ONLY = 0x0040 +V4L2_CTRL_FLAG_VOLATILE = 0x0080 +V4L2_CTRL_FLAG_HAS_PAYLOAD = 0x0100 +V4L2_CTRL_FLAG_EXECUTE_ON_WRITE = 0x0200 +V4L2_CTRL_FLAG_MODIFY_LAYOUT = 0x0400 +V4L2_CTRL_FLAG_DYNAMIC_ARRAY = 0x0800 +# Query flags, to be ORed with the control ID +V4L2_CTRL_FLAG_NEXT_CTRL = 0x80000000 +V4L2_CTRL_FLAG_NEXT_COMPOUND = 0x40000000 +# User-class control IDs defined by V4L2 +V4L2_CID_MAX_CTRLS = 1024 +# IDs reserved for driver specific controls +V4L2_CID_PRIVATE_BASE = 0x08000000 diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py new file mode 100644 index 00000000..4c572533 --- /dev/null +++ b/pylibs/v4l2/ctl.py @@ -0,0 +1,213 @@ +""" +Python implementation of v4l2-ctl +""" + +import os +import re +import ctypes +import fcntl +import copy +from typing import Generator + +from pylibs.v4l2 import raw, constants + +qctrls: dict[str, raw.v4l2_ext_control] = {} + +def ioctl_safe(fd: int, request: int, arg: ctypes.Structure) -> int: + try: + return fcntl.ioctl(fd, request, arg) + except OSError as e: + return -1 + +def ioctl_iter(fd: int, cmd: int, struct: ctypes.Structure, + start=0, stop=128, step=1, ignore_einval=False + )-> Generator[ctypes.Structure, None, None]: + for i in range(start, stop, step): + struct.index = i + try: + fcntl.ioctl(fd, cmd, struct) + yield struct + except OSError as e: + if e.errno == constants.EINVAL: + if ignore_einval: + continue + break + elif e.errno == constants.ENOTTY: + break + else: + raise + +def v4l2_ctrl_type_to_string(ctrl_type: int) -> str: + if ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER: + return "int" + elif ctrl_type == constants.V4L2_CTRL_TYPE_BOOLEAN: + return "bool" + elif ctrl_type == constants.V4L2_CTRL_TYPE_MENU: + return "menu" + elif ctrl_type == constants.V4L2_CTRL_TYPE_BUTTON: + return "button" + elif ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER64: + return "int64" + elif ctrl_type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + return "ctrl_class" + elif ctrl_type == constants.V4L2_CTRL_TYPE_STRING: + return "str" + elif ctrl_type == constants.V4L2_CTRL_TYPE_BITMASK: + return "bitmask" + elif ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER_MENU: + return "intmenu" + +def name2var(name: str) -> str: + return re.sub('[^0-9a-zA-Z]+', '_', name).lower() + +def ctrlflags2str(flags: int) -> str: + dict_flags = { + constants.V4L2_CTRL_FLAG_GRABBED: "grabbed", + constants.V4L2_CTRL_FLAG_DISABLED: "disabled", + constants.V4L2_CTRL_FLAG_READ_ONLY: "read-only", + constants.V4L2_CTRL_FLAG_UPDATE: "update", + constants.V4L2_CTRL_FLAG_INACTIVE: "inactive", + constants.V4L2_CTRL_FLAG_SLIDER: "slider", + constants.V4L2_CTRL_FLAG_WRITE_ONLY: "write-only", + constants.V4L2_CTRL_FLAG_VOLATILE: "volatile", + constants.V4L2_CTRL_FLAG_HAS_PAYLOAD: "has-payload", + constants.V4L2_CTRL_FLAG_EXECUTE_ON_WRITE: "execute-on-write", + constants.V4L2_CTRL_FLAG_MODIFY_LAYOUT: "modify-layout", + constants.V4L2_CTRL_FLAG_DYNAMIC_ARRAY: "dynamic-array", + 0: None + } + return dict_flags[flags] + +def print_qctrl(fd: int, qc: raw.v4l2_query_ext_ctrl) -> int: + if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + print(f"\n{qc.name.decode()}\n") + return + str_first = f"{name2var(qc.name.decode())} ({v4l2_ctrl_type_to_string(qc.type)})" + str_indent = (35 - len(str_first)) * ' ' + ':' + message = str_first + str_indent + if qc.type in (constants.V4L2_CTRL_TYPE_INTEGER, constants.V4L2_CTRL_TYPE_MENU): + message += f" min={qc.minimum} max={qc.maximum}" + if qc.type == constants.V4L2_CTRL_TYPE_INTEGER: + message += f" step={qc.step}" + if qc.type in (constants.V4L2_CTRL_TYPE_INTEGER, constants.V4L2_CTRL_TYPE_INTEGER_MENU, constants.V4L2_CTRL_TYPE_BOOLEAN): + message += f" default={qc.default_value}" + if qc.nr_of_dims == 0: + ctrl = raw.v4l2_control(id=qc.id) + if not ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl): + message += " value=" + str(ctrl.value) + print(message) + + if qc.type in (constants.V4L2_CTRL_TYPE_MENU, constants.V4L2_CTRL_TYPE_INTEGER_MENU): + for menu in ioctl_iter(fd, raw.VIDIOC_QUERYMENU, raw.v4l2_querymenu(id=qc.id), qc.minimum, qc.maximum + 1, qc.step, True): + if qc.type == constants.V4L2_CTRL_TYPE_MENU: + print(f" {menu.index}: {menu.name.decode()}") + else: + print(f" {menu.index}: {menu.value}") + +def parse_qc(fd: int, qc: raw.v4l2_query_ext_ctrl, device_path: str) -> dict: + """ + Parses the query control to an easy to use dictionary + """ + if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + return {} + controls = {} + controls['type'] = v4l2_ctrl_type_to_string(qc.type) + if qc.type in (constants.V4L2_CTRL_TYPE_INTEGER, constants.V4L2_CTRL_TYPE_MENU): + controls['min'] = qc.minimum + controls['max'] = qc.maximum + if qc.type == constants.V4L2_CTRL_TYPE_INTEGER: + controls['step'] = qc.step + if qc.type in ( + constants.V4L2_CTRL_TYPE_INTEGER, + constants.V4L2_CTRL_TYPE_MENU, + constants.V4L2_CTRL_TYPE_INTEGER_MENU, + constants.V4L2_CTRL_TYPE_BOOLEAN + ): + controls['default'] = qc.default_value + if qc.flags: + controls['flags'] = ctrlflags2str(qc.flags) + if qc.type in (constants.V4L2_CTRL_TYPE_MENU, constants.V4L2_CTRL_TYPE_INTEGER_MENU): + controls['menu'] = {} + for menu in ioctl_iter(fd, raw.VIDIOC_QUERYMENU, raw.v4l2_querymenu(id=qc.id), qc.minimum, qc.maximum + 1, qc.step, True): + if qc.type == constants.V4L2_CTRL_TYPE_MENU: + controls['menu'][menu.index] = menu.name.decode() + else: + controls['menu'][menu.index] = menu.value + return controls + +def init_device(device_path: str) -> None: + """ + Initialize a given device + """ + fd = os.open(device_path, os.O_RDWR) + next_fl = constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND + qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) + qctrls[device_path] = {} + for qc in ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): + if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + name = qc.name.decode() + else: + name = name2var(qc.name.decode()) + qctrls[device_path][name] = {} + qctrls[device_path][name]['qc'] = copy.deepcopy(qc) + qctrls[device_path][name]['values'] = parse_qc(fd, qc, device_path) + # print_qctrl(fd, qc) + qc.id |= next_fl + print(qctrls) + os.close(fd) + + +def list_controls(device_path: str) -> None: + """ + List all controls of a given device + """ + fd = os.open(device_path, os.O_RDWR) + for qc in qctrls[device_path].values(): + print_qctrl(fd, qc['qc']) + # next_fl = constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND + # qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) + # for qc in v4l2_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): + # qctrls[name2var(qc.name)] = copy.deepcopy(qc) + # print_qctrl(fd, qctrl) + # qc.id |= next_fl + os.close(fd) + +def get_camera_capabilities(device_path: str) -> dict: + """ + Get the capabilities of a given device + """ + fd = os.open(device_path, os.O_RDWR) + cap = raw.v4l2_capability() + ioctl_safe(fd, raw.VIDIOC_QUERYCAP, cap) + cap_dict = {} + cap_dict['driver'] = cap.driver.decode() + cap_dict['card'] = cap.card.decode() + cap_dict['bus'] = cap.bus_info.decode() + cap_dict['version'] = cap.version + cap_dict['capabilities'] = cap.capabilities + os.close(fd) + return cap_dict + +def get_control(device_path: str, control: str) -> int: + """ + Get the current value of a control of a given device + """ + fd = os.open(device_path, os.O_RDWR) + ctrl = raw.v4l2_control() + qc: raw.v4l2_query_ext_ctrl = qctrls[device_path][name2var(control)]['qc'] + ctrl.id = qc.id + ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl) + os.close(fd) + return ctrl.value + +def set_control(device_path: str, control: str, value: int) -> None: + """ + Set the value of a control of a given device + """ + fd = os.open(device_path, os.O_RDWR) + ctrl = raw.v4l2_control() + qc: raw.v4l2_query_ext_ctrl = qctrls[device_path][control]['qc'] + ctrl.id = qc.id + ctrl.value = value + ioctl_safe(fd, raw.VIDIOC_S_CTRL, ctrl) + os.close(fd) diff --git a/pylibs/v4l2/ioctl_macros.py b/pylibs/v4l2/ioctl_macros.py new file mode 100644 index 00000000..08ccbb75 --- /dev/null +++ b/pylibs/v4l2/ioctl_macros.py @@ -0,0 +1,77 @@ +# Methods to create IOCTL requests +# +# Copyright (C) 2023 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license + +from __future__ import annotations +import ctypes +from typing import Union, Type, TYPE_CHECKING + +""" +This module contains of Python port of the macros avaialble in +"/include/uapi/asm-generic/ioctl.h" from the linux kernel. +""" + +if TYPE_CHECKING: + IOCParamSize = Union[int, str, Type[ctypes._CData]] + +_IOC_NRBITS = 8 +_IOC_TYPEBITS = 8 + +# NOTE: The following could be platform specific. +_IOC_SIZEBITS = 14 +_IOC_DIRBITS = 2 + +_IOC_NRMASK = (1 << _IOC_NRBITS) - 1 +_IOC_TYPEMASK = (1 << _IOC_TYPEBITS) - 1 +_IOC_SIZEMASK = (1 << _IOC_SIZEBITS) - 1 +_IOC_DIRMASK = (1 << _IOC_DIRBITS) - 1 + +_IOC_NRSHIFT = 0 +_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS +_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS +_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS + +# The constants below may also be platform specific +IOC_NONE = 0 +IOC_WRITE = 1 +IOC_READ = 2 + +def _check_value(val: int, name: str, maximum: int): + if val > maximum: + raise ValueError(f"Value '{val}' for '{name}' exceeds max of {maximum}") + +def _IOC_TYPECHECK(param_size: IOCParamSize) -> int: + if isinstance(param_size, int): + return param_size + elif isinstance(param_size, bytearray): + return len(param_size) + elif isinstance(param_size, str): + ctcls = getattr(ctypes, param_size) + return ctypes.sizeof(ctcls) + return ctypes.sizeof(param_size) + +def IOC(direction: int, cmd_type: int, cmd_number: int, param_size: int) -> int: + _check_value(direction, "direction", _IOC_DIRMASK) + _check_value(cmd_type, "cmd_type", _IOC_TYPEMASK) + _check_value(cmd_number, "cmd_number", _IOC_NRMASK) + _check_value(param_size, "ioc_size", _IOC_SIZEMASK) + return ( + (direction << _IOC_DIRSHIFT) | + (param_size << _IOC_SIZESHIFT) | + (cmd_type << _IOC_TYPESHIFT) | + (cmd_number << _IOC_NRSHIFT) + ) + +def IO(cmd_type: int, cmd_number: int) -> int: + return IOC(IOC_NONE, cmd_type, cmd_number, 0) + +def IOR(cmd_type: int, cmd_number: int, param_size: IOCParamSize) -> int: + return IOC(IOC_READ, cmd_type, cmd_number, _IOC_TYPECHECK(param_size)) + +def IOW(cmd_type: int, cmd_number: int, param_size: IOCParamSize) -> int: + return IOC(IOC_WRITE, cmd_type, cmd_number, _IOC_TYPECHECK(param_size)) + +def IOWR(cmd_type: int, cmd_number: int, param_size: IOCParamSize) -> int: + return IOC(IOC_READ | IOC_WRITE, cmd_type, cmd_number, _IOC_TYPECHECK(param_size)) diff --git a/pylibs/v4l2/raw.py b/pylibs/v4l2/raw.py new file mode 100644 index 00000000..af71e5ec --- /dev/null +++ b/pylibs/v4l2/raw.py @@ -0,0 +1,117 @@ +import ctypes + +from pylibs.v4l2 import ioctl_macros + +from pylibs.v4l2 import constants + +class v4l2_capability(ctypes.Structure): + _fields_ = [ + ("driver",ctypes.c_char * 16), + ("card", ctypes.c_char * 32), + ("bus_info", ctypes.c_char * 32), + ("version", ctypes.c_uint32), + ("capabilities", ctypes.c_uint32), + ("device_caps", ctypes.c_uint32), + ("reserved", ctypes.c_uint32 * 3) + ] + +class v4l2_control(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_uint32), + ("value", ctypes.c_int32) + ] + +class v4l2_ext_control(ctypes.Structure): + _pack_ = True + class ValueUnion(ctypes.Union): + _fields_ = [ + ("value", ctypes.c_int32), + ("value64", ctypes.c_int64), + ("string", ctypes.POINTER(ctypes.c_char)), + ("p_u8", ctypes.POINTER(ctypes.c_uint8)), + ("p_u16", ctypes.POINTER(ctypes.c_uint16)), + ("p_u32", ctypes.POINTER(ctypes.c_uint32)), + ("p_s32", ctypes.POINTER(ctypes.c_int32)), + ("p_s64", ctypes.POINTER(ctypes.c_int64)), + ("ptr", ctypes.POINTER(None)) + ] + + _fields_ = [ + ("id", ctypes.c_uint32), + ("size", ctypes.c_uint32), + ("reserved2", ctypes.c_uint32 * 1), + ("union", ValueUnion) + ] + _anonymous_ = ("union",) + +class v4l2_ext_controls(ctypes.Structure): + class UnionControls(ctypes.Union): + _fields_ = [ + ("ctrl_class", ctypes.c_uint32), + ("which", ctypes.c_uint32) + ] + + _fields_ = [ + ("union", UnionControls), + ("count", ctypes.c_uint32), + ("error_idx", ctypes.c_uint32), + ("request_fd", ctypes.c_int32), + ("reserved", ctypes.c_uint32 * 1), + ("controls", ctypes.POINTER(v4l2_ext_control) ) + ] + _anonymous_ = ("union",) + +class v4l2_queryctrl(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("name", ctypes.c_char * 8), + ("minimum", ctypes.c_int32), + ("maximum", ctypes.c_int32), + ("step", ctypes.c_int32), + ("default_value", ctypes.c_int32), + ("flags", ctypes.c_uint32), + ("reserved", ctypes.c_uint32 * 2) + ] + +class v4l2_query_ext_ctrl(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("name", ctypes.c_char * 32), + ("minimum", ctypes.c_int64), + ("maximum", ctypes.c_int64), + ("step", ctypes.c_uint64), + ("default_value", ctypes.c_int64), + ("flags", ctypes.c_uint32), + ("elem_size", ctypes.c_uint32), + ("elems", ctypes.c_uint32), + ("nr_of_dims", ctypes.c_uint32), + ("dim", ctypes.c_uint32 * constants.V4L2_CTRL_MAX_DIMS), + ("reserved", ctypes.c_uint32 * 32) + ] + +class v4l2_querymenu(ctypes.Structure): + class UnionNameValue(ctypes.Union): + _fields_ = [ + ("name", ctypes.c_char * 32), + ("value", ctypes.c_int64) + ] + _pack_ = True + _fields_ = [ + ("id", ctypes.c_uint32), + ("index", ctypes.c_uint32), + ("union", UnionNameValue), + ("reserved", ctypes.c_uint32) + ] + _anonymous_ = ("union",) + + +VIDIOC_QUERYCAP = ioctl_macros.IOR(ord('V'), 0, v4l2_capability) +VIDIOC_G_CTRL = ioctl_macros.IOWR(ord('V'), 27, v4l2_control) +VIDIOC_S_CTRL = ioctl_macros.IOWR(ord('V'), 28, v4l2_control) +VIDIOC_QUERYCTRL = ioctl_macros.IOWR(ord('V'), 36, v4l2_queryctrl) +VIDIOC_QUERYMENU = ioctl_macros.IOWR(ord('V'), 37, v4l2_querymenu) +VIDIOC_G_EXT_CTRLS = ioctl_macros.IOWR(ord('V'), 71, v4l2_ext_controls) +VIDIOC_S_EXT_CTRLS = ioctl_macros.IOWR(ord('V'), 72, v4l2_ext_controls) +VIDIOC_QUERY_EXT_CTRL = ioctl_macros.IOWR(ord('V'), 103, v4l2_query_ext_ctrl) From ae9292666aa4a10619ea1e8ab1b0bdff6c4d1839 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 23 Mar 2024 00:00:54 +0100 Subject: [PATCH 090/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/v4l2/constants.py | 90 ++++++++++++++------- pylibs/v4l2/ctl.py | 166 ++++++++++----------------------------- pylibs/v4l2/raw.py | 158 +++++++++++++++++++++++++++---------- pylibs/v4l2/utils.py | 157 ++++++++++++++++++++++++++++++++++++ 4 files changed, 377 insertions(+), 194 deletions(-) create mode 100644 pylibs/v4l2/utils.py diff --git a/pylibs/v4l2/constants.py b/pylibs/v4l2/constants.py index 38b73beb..b07e30d9 100644 --- a/pylibs/v4l2/constants.py +++ b/pylibs/v4l2/constants.py @@ -1,34 +1,68 @@ -V4L2_CTRL_MAX_DIMS = 4 -EINVAL = 22 -ENOTTY = 25 +V4L2_CTRL_MAX_DIMS = 4 +EINVAL = 22 +ENOTTY = 25 -V4L2_CTRL_TYPE_INTEGER = 1 -V4L2_CTRL_TYPE_BOOLEAN = 2 -V4L2_CTRL_TYPE_MENU = 3 -V4L2_CTRL_TYPE_BUTTON = 4 -V4L2_CTRL_TYPE_INTEGER64 = 5 -V4L2_CTRL_TYPE_CTRL_CLASS = 6 -V4L2_CTRL_TYPE_STRING = 7 -V4L2_CTRL_TYPE_BITMASK = 8 -V4L2_CTRL_TYPE_INTEGER_MENU = 9 +V4L2_CTRL_TYPE_INTEGER = 1 +V4L2_CTRL_TYPE_BOOLEAN = 2 +V4L2_CTRL_TYPE_MENU = 3 +V4L2_CTRL_TYPE_BUTTON = 4 +V4L2_CTRL_TYPE_INTEGER64 = 5 +V4L2_CTRL_TYPE_CTRL_CLASS = 6 +V4L2_CTRL_TYPE_STRING = 7 +V4L2_CTRL_TYPE_BITMASK = 8 +V4L2_CTRL_TYPE_INTEGER_MENU = 9 + +V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 +V4L2_BUF_TYPE_VIDEO_OUTPUT = 2 +V4L2_BUF_TYPE_VIDEO_OVERLAY = 3 +V4L2_BUF_TYPE_VBI_CAPTURE = 4 +V4L2_BUF_TYPE_VBI_OUTPUT = 5 +V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6 +V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7 +V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8 +V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9 +V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE = 10 +V4L2_BUF_TYPE_SDR_CAPTURE = 11 +V4L2_BUF_TYPE_SDR_OUTPUT = 12 +V4L2_BUF_TYPE_META_CAPTURE = 13 +V4L2_BUF_TYPE_META_OUTPUT = 14 + +V4L2_FMT_FLAG_COMPRESSED = 0x0001 +V4L2_FMT_FLAG_EMULATED = 0x0002 +V4L2_FMT_FLAG_CONTINUOUS_BYTESTREAM = 0x0004 +V4L2_FMT_FLAG_DYN_RESOLUTION = 0x0008 +V4L2_FMT_FLAG_ENC_CAP_FRAME_INTERVAL = 0x0010 +V4L2_FMT_FLAG_CSC_COLORSPACE = 0x0020 +V4L2_FMT_FLAG_CSC_XFER_FUNC = 0x0040 +V4L2_FMT_FLAG_CSC_YCBCR_ENC = 0x0080 +V4L2_FMT_FLAG_CSC_HSV_ENC = V4L2_FMT_FLAG_CSC_YCBCR_ENC +V4L2_FMT_FLAG_CSC_QUANTIZATION = 0x0100 + +V4L2_FRMSIZE_TYPE_DISCRETE = 1 +V4L2_FRMSIZE_TYPE_CONTINUOUS = 2 +V4L2_FRMSIZE_TYPE_STEPWISE = 3 + +V4L2_FRMIVAL_TYPE_DISCRETE = 1 +V4L2_FRMIVAL_TYPE_CONTINUOUS = 2 +V4L2_FRMIVAL_TYPE_STEPWISE = 3 # Control flags -V4L2_CTRL_FLAG_DISABLED = 0x0001 -V4L2_CTRL_FLAG_GRABBED = 0x0002 -V4L2_CTRL_FLAG_READ_ONLY = 0x0004 -V4L2_CTRL_FLAG_UPDATE = 0x0008 -V4L2_CTRL_FLAG_INACTIVE = 0x0010 -V4L2_CTRL_FLAG_SLIDER = 0x0020 -V4L2_CTRL_FLAG_WRITE_ONLY = 0x0040 -V4L2_CTRL_FLAG_VOLATILE = 0x0080 -V4L2_CTRL_FLAG_HAS_PAYLOAD = 0x0100 -V4L2_CTRL_FLAG_EXECUTE_ON_WRITE = 0x0200 -V4L2_CTRL_FLAG_MODIFY_LAYOUT = 0x0400 -V4L2_CTRL_FLAG_DYNAMIC_ARRAY = 0x0800 +V4L2_CTRL_FLAG_DISABLED = 0x0001 +V4L2_CTRL_FLAG_GRABBED = 0x0002 +V4L2_CTRL_FLAG_READ_ONLY = 0x0004 +V4L2_CTRL_FLAG_UPDATE = 0x0008 +V4L2_CTRL_FLAG_INACTIVE = 0x0010 +V4L2_CTRL_FLAG_SLIDER = 0x0020 +V4L2_CTRL_FLAG_WRITE_ONLY = 0x0040 +V4L2_CTRL_FLAG_VOLATILE = 0x0080 +V4L2_CTRL_FLAG_HAS_PAYLOAD = 0x0100 +V4L2_CTRL_FLAG_EXECUTE_ON_WRITE = 0x0200 +V4L2_CTRL_FLAG_MODIFY_LAYOUT = 0x0400 +V4L2_CTRL_FLAG_DYNAMIC_ARRAY = 0x0800 # Query flags, to be ORed with the control ID -V4L2_CTRL_FLAG_NEXT_CTRL = 0x80000000 -V4L2_CTRL_FLAG_NEXT_COMPOUND = 0x40000000 +V4L2_CTRL_FLAG_NEXT_CTRL = 0x80000000 +V4L2_CTRL_FLAG_NEXT_COMPOUND = 0x40000000 # User-class control IDs defined by V4L2 -V4L2_CID_MAX_CTRLS = 1024 +V4L2_CID_MAX_CTRLS = 1024 # IDs reserved for driver specific controls -V4L2_CID_PRIVATE_BASE = 0x08000000 +V4L2_CID_PRIVATE_BASE = 0x08000000 diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index 4c572533..d65eb935 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -3,115 +3,20 @@ """ import os -import re -import ctypes -import fcntl import copy -from typing import Generator -from pylibs.v4l2 import raw, constants +from pylibs.v4l2 import raw, constants, utils qctrls: dict[str, raw.v4l2_ext_control] = {} -def ioctl_safe(fd: int, request: int, arg: ctypes.Structure) -> int: - try: - return fcntl.ioctl(fd, request, arg) - except OSError as e: - return -1 - -def ioctl_iter(fd: int, cmd: int, struct: ctypes.Structure, - start=0, stop=128, step=1, ignore_einval=False - )-> Generator[ctypes.Structure, None, None]: - for i in range(start, stop, step): - struct.index = i - try: - fcntl.ioctl(fd, cmd, struct) - yield struct - except OSError as e: - if e.errno == constants.EINVAL: - if ignore_einval: - continue - break - elif e.errno == constants.ENOTTY: - break - else: - raise - -def v4l2_ctrl_type_to_string(ctrl_type: int) -> str: - if ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER: - return "int" - elif ctrl_type == constants.V4L2_CTRL_TYPE_BOOLEAN: - return "bool" - elif ctrl_type == constants.V4L2_CTRL_TYPE_MENU: - return "menu" - elif ctrl_type == constants.V4L2_CTRL_TYPE_BUTTON: - return "button" - elif ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER64: - return "int64" - elif ctrl_type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: - return "ctrl_class" - elif ctrl_type == constants.V4L2_CTRL_TYPE_STRING: - return "str" - elif ctrl_type == constants.V4L2_CTRL_TYPE_BITMASK: - return "bitmask" - elif ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER_MENU: - return "intmenu" - -def name2var(name: str) -> str: - return re.sub('[^0-9a-zA-Z]+', '_', name).lower() - -def ctrlflags2str(flags: int) -> str: - dict_flags = { - constants.V4L2_CTRL_FLAG_GRABBED: "grabbed", - constants.V4L2_CTRL_FLAG_DISABLED: "disabled", - constants.V4L2_CTRL_FLAG_READ_ONLY: "read-only", - constants.V4L2_CTRL_FLAG_UPDATE: "update", - constants.V4L2_CTRL_FLAG_INACTIVE: "inactive", - constants.V4L2_CTRL_FLAG_SLIDER: "slider", - constants.V4L2_CTRL_FLAG_WRITE_ONLY: "write-only", - constants.V4L2_CTRL_FLAG_VOLATILE: "volatile", - constants.V4L2_CTRL_FLAG_HAS_PAYLOAD: "has-payload", - constants.V4L2_CTRL_FLAG_EXECUTE_ON_WRITE: "execute-on-write", - constants.V4L2_CTRL_FLAG_MODIFY_LAYOUT: "modify-layout", - constants.V4L2_CTRL_FLAG_DYNAMIC_ARRAY: "dynamic-array", - 0: None - } - return dict_flags[flags] - -def print_qctrl(fd: int, qc: raw.v4l2_query_ext_ctrl) -> int: - if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: - print(f"\n{qc.name.decode()}\n") - return - str_first = f"{name2var(qc.name.decode())} ({v4l2_ctrl_type_to_string(qc.type)})" - str_indent = (35 - len(str_first)) * ' ' + ':' - message = str_first + str_indent - if qc.type in (constants.V4L2_CTRL_TYPE_INTEGER, constants.V4L2_CTRL_TYPE_MENU): - message += f" min={qc.minimum} max={qc.maximum}" - if qc.type == constants.V4L2_CTRL_TYPE_INTEGER: - message += f" step={qc.step}" - if qc.type in (constants.V4L2_CTRL_TYPE_INTEGER, constants.V4L2_CTRL_TYPE_INTEGER_MENU, constants.V4L2_CTRL_TYPE_BOOLEAN): - message += f" default={qc.default_value}" - if qc.nr_of_dims == 0: - ctrl = raw.v4l2_control(id=qc.id) - if not ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl): - message += " value=" + str(ctrl.value) - print(message) - - if qc.type in (constants.V4L2_CTRL_TYPE_MENU, constants.V4L2_CTRL_TYPE_INTEGER_MENU): - for menu in ioctl_iter(fd, raw.VIDIOC_QUERYMENU, raw.v4l2_querymenu(id=qc.id), qc.minimum, qc.maximum + 1, qc.step, True): - if qc.type == constants.V4L2_CTRL_TYPE_MENU: - print(f" {menu.index}: {menu.name.decode()}") - else: - print(f" {menu.index}: {menu.value}") - -def parse_qc(fd: int, qc: raw.v4l2_query_ext_ctrl, device_path: str) -> dict: +def parse_qc(fd: int, qc: raw.v4l2_query_ext_ctrl) -> dict: """ Parses the query control to an easy to use dictionary """ if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: return {} controls = {} - controls['type'] = v4l2_ctrl_type_to_string(qc.type) + controls['type'] = utils.v4l2_ctrl_type_to_string(qc.type) if qc.type in (constants.V4L2_CTRL_TYPE_INTEGER, constants.V4L2_CTRL_TYPE_MENU): controls['min'] = qc.minimum controls['max'] = qc.maximum @@ -125,10 +30,10 @@ def parse_qc(fd: int, qc: raw.v4l2_query_ext_ctrl, device_path: str) -> dict: ): controls['default'] = qc.default_value if qc.flags: - controls['flags'] = ctrlflags2str(qc.flags) + controls['flags'] = utils.ctrlflags2str(qc.flags) if qc.type in (constants.V4L2_CTRL_TYPE_MENU, constants.V4L2_CTRL_TYPE_INTEGER_MENU): controls['menu'] = {} - for menu in ioctl_iter(fd, raw.VIDIOC_QUERYMENU, raw.v4l2_querymenu(id=qc.id), qc.minimum, qc.maximum + 1, qc.step, True): + for menu in utils.ioctl_iter(fd, raw.VIDIOC_QUERYMENU, raw.v4l2_querymenu(id=qc.id), qc.minimum, qc.maximum + 1, qc.step, True): if qc.type == constants.V4L2_CTRL_TYPE_MENU: controls['menu'][menu.index] = menu.name.decode() else: @@ -143,33 +48,17 @@ def init_device(device_path: str) -> None: next_fl = constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) qctrls[device_path] = {} - for qc in ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): + for qc in utils.ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: name = qc.name.decode() else: - name = name2var(qc.name.decode()) + name = utils.name2var(qc.name.decode()) qctrls[device_path][name] = {} qctrls[device_path][name]['qc'] = copy.deepcopy(qc) - qctrls[device_path][name]['values'] = parse_qc(fd, qc, device_path) + qctrls[device_path][name]['values'] = parse_qc(fd, qc) # print_qctrl(fd, qc) qc.id |= next_fl - print(qctrls) - os.close(fd) - - -def list_controls(device_path: str) -> None: - """ - List all controls of a given device - """ - fd = os.open(device_path, os.O_RDWR) - for qc in qctrls[device_path].values(): - print_qctrl(fd, qc['qc']) - # next_fl = constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND - # qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) - # for qc in v4l2_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): - # qctrls[name2var(qc.name)] = copy.deepcopy(qc) - # print_qctrl(fd, qctrl) - # qc.id |= next_fl + # print(qctrls) os.close(fd) def get_camera_capabilities(device_path: str) -> dict: @@ -178,7 +67,7 @@ def get_camera_capabilities(device_path: str) -> dict: """ fd = os.open(device_path, os.O_RDWR) cap = raw.v4l2_capability() - ioctl_safe(fd, raw.VIDIOC_QUERYCAP, cap) + utils.ioctl_safe(fd, raw.VIDIOC_QUERYCAP, cap) cap_dict = {} cap_dict['driver'] = cap.driver.decode() cap_dict['card'] = cap.card.decode() @@ -194,9 +83,9 @@ def get_control(device_path: str, control: str) -> int: """ fd = os.open(device_path, os.O_RDWR) ctrl = raw.v4l2_control() - qc: raw.v4l2_query_ext_ctrl = qctrls[device_path][name2var(control)]['qc'] + qc: raw.v4l2_query_ext_ctrl = qctrls[device_path][utils.name2var(control)]['qc'] ctrl.id = qc.id - ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl) + utils.ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl) os.close(fd) return ctrl.value @@ -209,5 +98,34 @@ def set_control(device_path: str, control: str, value: int) -> None: qc: raw.v4l2_query_ext_ctrl = qctrls[device_path][control]['qc'] ctrl.id = qc.id ctrl.value = value - ioctl_safe(fd, raw.VIDIOC_S_CTRL, ctrl) + utils.ioctl_safe(fd, raw.VIDIOC_S_CTRL, ctrl) + os.close(fd) + +def get_formats(device_path: str) -> list: + """ + Get the available formats of a given device + """ + fd = os.open(device_path, os.O_RDWR) + fmt = raw.v4l2_fmtdesc() + frmsize = raw.v4l2_frmsizeenum() + frmival = raw.v4l2_frmivalenum() + fmt.index = 0 + fmt.type = constants.V4L2_BUF_TYPE_VIDEO_CAPTURE + formats = {} + for fmt in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FMT, fmt): + str = f"[{fmt.index}]: '{utils.fcc2s(fmt.pixelformat)}' ({fmt.description.decode()}" + if fmt.flags: + str += f", {utils.fmtflags2str(fmt.flags)}" + str += ')' + formats[str] = {} + frmsize.pixel_format = fmt.pixelformat + for size in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FRAMESIZES, frmsize): + size_str = utils.frmsize_to_str(size) + formats[str][size_str] = [] + frmival.pixel_format = fmt.pixelformat + frmival.width = frmsize.discrete.width + frmival.height = frmsize.discrete.height + for interval in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FRAMEINTERVALS, frmival): + formats[str][size_str].append(utils.frmival_to_str(interval)) os.close(fd) + return formats diff --git a/pylibs/v4l2/raw.py b/pylibs/v4l2/raw.py index af71e5ec..cd3a1469 100644 --- a/pylibs/v4l2/raw.py +++ b/pylibs/v4l2/raw.py @@ -15,64 +15,150 @@ class v4l2_capability(ctypes.Structure): ("reserved", ctypes.c_uint32 * 3) ] +class v4l2_fmtdesc(ctypes.Structure): + _fields_ = [ + ("index", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("description", ctypes.c_char * 32), + ("pixelformat", ctypes.c_uint32), + ("mbus_code", ctypes.c_uint32), + ("reserved", ctypes.c_uint32 * 3) + ] + class v4l2_control(ctypes.Structure): _fields_ = [ ("id", ctypes.c_uint32), ("value", ctypes.c_int32) ] +class v4l2_queryctrl(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("name", ctypes.c_char * 8), + ("minimum", ctypes.c_int32), + ("maximum", ctypes.c_int32), + ("step", ctypes.c_int32), + ("default_value", ctypes.c_int32), + ("flags", ctypes.c_uint32), + ("reserved", ctypes.c_uint32 * 2) + ] + +class v4l2_querymenu(ctypes.Structure): + class UnionNameValue(ctypes.Union): + _fields_ = [ + ("name", ctypes.c_char * 32), + ("value", ctypes.c_int64) + ] + _pack_ = True + _fields_ = [ + ("id", ctypes.c_uint32), + ("index", ctypes.c_uint32), + ("union", UnionNameValue), + ("reserved", ctypes.c_uint32) + ] + _anonymous_ = ("union",) + class v4l2_ext_control(ctypes.Structure): _pack_ = True class ValueUnion(ctypes.Union): _fields_ = [ - ("value", ctypes.c_int32), + ("value", ctypes.c_int32), ("value64", ctypes.c_int64), ("string", ctypes.POINTER(ctypes.c_char)), - ("p_u8", ctypes.POINTER(ctypes.c_uint8)), - ("p_u16", ctypes.POINTER(ctypes.c_uint16)), - ("p_u32", ctypes.POINTER(ctypes.c_uint32)), - ("p_s32", ctypes.POINTER(ctypes.c_int32)), - ("p_s64", ctypes.POINTER(ctypes.c_int64)), - ("ptr", ctypes.POINTER(None)) + ("p_u8", ctypes.POINTER(ctypes.c_uint8)), + ("p_u16", ctypes.POINTER(ctypes.c_uint16)), + ("p_u32", ctypes.POINTER(ctypes.c_uint32)), + ("p_s32", ctypes.POINTER(ctypes.c_int32)), + ("p_s64", ctypes.POINTER(ctypes.c_int64)), + ("ptr", ctypes.POINTER(None)) ] _fields_ = [ - ("id", ctypes.c_uint32), - ("size", ctypes.c_uint32), - ("reserved2", ctypes.c_uint32 * 1), - ("union", ValueUnion) + ("id", ctypes.c_uint32), + ("size", ctypes.c_uint32), + ("reserved2", ctypes.c_uint32 * 1), + ("union", ValueUnion) ] _anonymous_ = ("union",) class v4l2_ext_controls(ctypes.Structure): class UnionControls(ctypes.Union): _fields_ = [ - ("ctrl_class", ctypes.c_uint32), - ("which", ctypes.c_uint32) + ("ctrl_class", ctypes.c_uint32), + ("which", ctypes.c_uint32) ] _fields_ = [ - ("union", UnionControls), - ("count", ctypes.c_uint32), - ("error_idx", ctypes.c_uint32), - ("request_fd", ctypes.c_int32), - ("reserved", ctypes.c_uint32 * 1), - ("controls", ctypes.POINTER(v4l2_ext_control) ) + ("union", UnionControls), + ("count", ctypes.c_uint32), + ("error_idx", ctypes.c_uint32), + ("request_fd", ctypes.c_int32), + ("reserved", ctypes.c_uint32 * 1), + ("controls", ctypes.POINTER(v4l2_ext_control) ) ] _anonymous_ = ("union",) -class v4l2_queryctrl(ctypes.Structure): +class v4l2_frmsize_discrete(ctypes.Structure): _fields_ = [ - ("id", ctypes.c_uint32), + ("width", ctypes.c_uint32), + ("height", ctypes.c_uint32) + ] + +class v4l2_frmsize_stepwise(ctypes.Structure): + _fields_ = [ + ("min_width", ctypes.c_uint32), + ("max_width", ctypes.c_uint32), + ("step_width", ctypes.c_uint32), + ("min_height", ctypes.c_uint32), + ("max_height", ctypes.c_uint32), + ("step_height", ctypes.c_uint32) + ] + +class v4l2_frmsizeenum(ctypes.Structure): + class FrmSize(ctypes.Union): + _fields_ = [ + ("discrete", v4l2_frmsize_discrete), + ("stepwise", v4l2_frmsize_stepwise) + ] + _fields_ = [ + ("index", ctypes.c_uint32), + ("pixel_format", ctypes.c_uint32), ("type", ctypes.c_uint32), - ("name", ctypes.c_char * 8), - ("minimum", ctypes.c_int32), - ("maximum", ctypes.c_int32), - ("step", ctypes.c_int32), - ("default_value", ctypes.c_int32), - ("flags", ctypes.c_uint32), + ("union", FrmSize), ("reserved", ctypes.c_uint32 * 2) ] + _anonymous_ = ("union",) + +class v4l2_fract(ctypes.Structure): + _fields_ = [ + ("numerator", ctypes.c_uint32), + ("denominator", ctypes.c_uint32) + ] +class v4l2_frmival_stepwise(ctypes.Structure): + _fields_ = [ + ("min", v4l2_fract), + ("max", v4l2_fract), + ("step", v4l2_fract) + ] + +class v4l2_frmivalenum(ctypes.Structure): + class FrmIval(ctypes.Union): + _fields_ = [ + ("discrete", v4l2_fract), + ("stepwise", v4l2_frmival_stepwise) + ] + _fields_ = [ + ("index", ctypes.c_uint32), + ("pixel_format",ctypes.c_uint32), + ("width", ctypes.c_uint32), + ("height", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("union", FrmIval), + ("reserved", ctypes.c_uint32 * 2) + ] + _anonymous_ = ("union",) class v4l2_query_ext_ctrl(ctypes.Structure): _fields_ = [ @@ -91,27 +177,15 @@ class v4l2_query_ext_ctrl(ctypes.Structure): ("reserved", ctypes.c_uint32 * 32) ] -class v4l2_querymenu(ctypes.Structure): - class UnionNameValue(ctypes.Union): - _fields_ = [ - ("name", ctypes.c_char * 32), - ("value", ctypes.c_int64) - ] - _pack_ = True - _fields_ = [ - ("id", ctypes.c_uint32), - ("index", ctypes.c_uint32), - ("union", UnionNameValue), - ("reserved", ctypes.c_uint32) - ] - _anonymous_ = ("union",) - VIDIOC_QUERYCAP = ioctl_macros.IOR(ord('V'), 0, v4l2_capability) +VIDIOC_ENUM_FMT = ioctl_macros.IOWR(ord('V'), 2, v4l2_fmtdesc) VIDIOC_G_CTRL = ioctl_macros.IOWR(ord('V'), 27, v4l2_control) VIDIOC_S_CTRL = ioctl_macros.IOWR(ord('V'), 28, v4l2_control) VIDIOC_QUERYCTRL = ioctl_macros.IOWR(ord('V'), 36, v4l2_queryctrl) VIDIOC_QUERYMENU = ioctl_macros.IOWR(ord('V'), 37, v4l2_querymenu) VIDIOC_G_EXT_CTRLS = ioctl_macros.IOWR(ord('V'), 71, v4l2_ext_controls) VIDIOC_S_EXT_CTRLS = ioctl_macros.IOWR(ord('V'), 72, v4l2_ext_controls) +VIDIOC_ENUM_FRAMESIZES = ioctl_macros.IOWR(ord('V'), 74, v4l2_frmsizeenum) +VIDIOC_ENUM_FRAMEINTERVALS = ioctl_macros.IOWR(ord('V'), 75, v4l2_frmivalenum) VIDIOC_QUERY_EXT_CTRL = ioctl_macros.IOWR(ord('V'), 103, v4l2_query_ext_ctrl) diff --git a/pylibs/v4l2/utils.py b/pylibs/v4l2/utils.py new file mode 100644 index 00000000..f9660d63 --- /dev/null +++ b/pylibs/v4l2/utils.py @@ -0,0 +1,157 @@ +import fcntl +import ctypes +import re +from typing import Generator + +from pylibs.v4l2 import raw, constants + + +def ioctl_safe(fd: int, request: int, arg: ctypes.Structure) -> int: + try: + return fcntl.ioctl(fd, request, arg) + except OSError as e: + return -1 + +def ioctl_iter(fd: int, cmd: int, struct: ctypes.Structure, + start=0, stop=128, step=1, ignore_einval=False + )-> Generator[ctypes.Structure, None, None]: + for i in range(start, stop, step): + struct.index = i + try: + fcntl.ioctl(fd, cmd, struct) + yield struct + except OSError as e: + if e.errno == constants.EINVAL: + if ignore_einval: + continue + break + elif e.errno == constants.ENOTTY: + break + else: + raise + +def v4l2_ctrl_type_to_string(ctrl_type: int) -> str: + if ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER: + return "int" + elif ctrl_type == constants.V4L2_CTRL_TYPE_BOOLEAN: + return "bool" + elif ctrl_type == constants.V4L2_CTRL_TYPE_MENU: + return "menu" + elif ctrl_type == constants.V4L2_CTRL_TYPE_BUTTON: + return "button" + elif ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER64: + return "int64" + elif ctrl_type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + return "ctrl_class" + elif ctrl_type == constants.V4L2_CTRL_TYPE_STRING: + return "str" + elif ctrl_type == constants.V4L2_CTRL_TYPE_BITMASK: + return "bitmask" + elif ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER_MENU: + return "intmenu" + +def name2var(name: str) -> str: + return re.sub('[^0-9a-zA-Z]+', '_', name).lower() + +def ctrlflags2str(flags: int) -> str: + dict_flags = { + constants.V4L2_CTRL_FLAG_GRABBED: "grabbed", + constants.V4L2_CTRL_FLAG_DISABLED: "disabled", + constants.V4L2_CTRL_FLAG_READ_ONLY: "read-only", + constants.V4L2_CTRL_FLAG_UPDATE: "update", + constants.V4L2_CTRL_FLAG_INACTIVE: "inactive", + constants.V4L2_CTRL_FLAG_SLIDER: "slider", + constants.V4L2_CTRL_FLAG_WRITE_ONLY: "write-only", + constants.V4L2_CTRL_FLAG_VOLATILE: "volatile", + constants.V4L2_CTRL_FLAG_HAS_PAYLOAD: "has-payload", + constants.V4L2_CTRL_FLAG_EXECUTE_ON_WRITE: "execute-on-write", + constants.V4L2_CTRL_FLAG_MODIFY_LAYOUT: "modify-layout", + constants.V4L2_CTRL_FLAG_DYNAMIC_ARRAY: "dynamic-array", + 0: None + } + return dict_flags[flags] + +def fmtflags2str(flags: int) -> str: + dict_flags = { + constants.V4L2_FMT_FLAG_COMPRESSED: "compressed", + constants.V4L2_FMT_FLAG_EMULATED: "emulated", + constants.V4L2_FMT_FLAG_CONTINUOUS_BYTESTREAM: "continuous-bytestream", + constants.V4L2_FMT_FLAG_DYN_RESOLUTION: "dyn-resolution", + constants.V4L2_FMT_FLAG_ENC_CAP_FRAME_INTERVAL: "enc-cap-frame-interval", + constants.V4L2_FMT_FLAG_CSC_COLORSPACE: "csc-colorspace", + constants.V4L2_FMT_FLAG_CSC_YCBCR_ENC: "csc-ycbcr-enc", + constants.V4L2_FMT_FLAG_CSC_QUANTIZATION: "csc-quantization", + constants.V4L2_FMT_FLAG_CSC_XFER_FUNC: "csc-xfer-func" + } + return dict_flags[flags] + +def fcc2s(val: int) -> str: + s = '' + s += chr(val & 0x7f) + s += chr((val >> 8) & 0x7f) + s += chr((val >> 16) & 0x7f) + s += chr((val >> 24) & 0x7f) + return s + +def frmtype2s(type) -> str: + types = [ + "Unknown", + "Discrete", + "Continuous", + "Stepwise" + ] + if type >= len(types): + return "Unknown" + return types[type] + +def fract2sec(fract: raw.v4l2_fract) -> str: + return "%.3f" % round(fract.numerator / fract.denominator, 3) + +def fract2fps(fract: raw.v4l2_fract) -> str: + return "%.3f" % round(fract.denominator / fract.numerator, 3) + +def frmsize_to_str(frmsize: raw.v4l2_frmsizeenum) -> str: + string = f"Size: {frmtype2s(frmsize.type)} " + if frmsize.type == constants.V4L2_FRMSIZE_TYPE_DISCRETE: + string += "%dx%d" % (frmsize.discrete.width, frmsize.discrete.height) + elif frmsize.type == constants.V4L2_FRMSIZE_TYPE_CONTINUOUS: + string += "%dx%d - %dx%d" % ( + frmsize.stepwise.min_width, + frmsize.stepwise.min_height, + frmsize.stepwise.max_width, + frmsize.stepwise.max_height + ) + elif frmsize.type == constants.V4L2_FRMSIZE_TYPE_STEPWISE: + string += "%ss - %ss with step %ss (%s-%s fps)" % ( + frmsize.stepwise.min_width, + frmsize.stepwise.min_height, + frmsize.stepwise.max_width, + frmsize.stepwise.max_height, + frmsize.stepwise.step_width, + frmsize.stepwise.step_height + ) + return string + +def frmival_to_str(frmival: raw.v4l2_frmivalenum) -> str: + string = f"Interval: {frmtype2s(frmival.type)} " + if frmival.type == constants.V4L2_FRMIVAL_TYPE_DISCRETE: + string += "%ss (%s fps)" % ( + fract2sec(frmival.discrete), + fract2fps(frmival.discrete) + ) + elif frmival.type == constants.V4L2_FRMIVAL_TYPE_CONTINUOUS: + string += "%ss - %ss (%s-%s fps)" % ( + fract2sec(frmival.stepwise.min), + fract2sec(frmival.stepwise.max), + fract2fps(frmival.stepwise.max), + fract2fps(frmival.stepwise.min) + ) + elif frmival.type == constants.V4L2_FRMIVAL_TYPE_STEPWISE: + string += "%ss - %ss with step %ss (%s-%s fps)" % ( + fract2sec(frmival.stepwise.min), + fract2sec(frmival.stepwise.max), + fract2sec(frmival.stepwise.step), + fract2fps(frmival.stepwise.max), + fract2fps(frmival.stepwise.min) + ) + return string From 526a043eb4d18fa40b8f1847c30d9221f0012ae8 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 23 Mar 2024 00:23:20 +0100 Subject: [PATCH 091/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/hwhandler.py | 7 +++++-- pylibs/logging_helper.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index b76b0635..c3e56a84 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -36,14 +36,17 @@ def get_avail_uvc_dev() -> dict: cams[cam_path] = {} cams[cam_path]['realpath'] = os.path.realpath(cam_path) ctl.init_device(cam_path) - cams[cam_path]['formats'] = v4l2_ctl.get_uvc_formats(cam_path) + cams[cam_path]['formats'] = ctl.get_formats(cam_path) cams[cam_path]['v4l2ctrls'] = v4l2_qctl_to_dict(cam_path) avail_cams['uvc'].update(cams) return cams def has_device_mjpg_hw(cam_path: str) -> bool: global avail_cams - return 'Motion-JPEG, compressed' in v4l2_ctl.get_uvc_formats(cam_path) + for key in ctl.get_formats(cam_path).keys(): + if 'Motion-JPEG, compressed' in key: + return True + return False def get_avail_libcamera() -> dict: cmd = shutil.which('libcamera-hello') diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 85199400..5041a839 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -126,13 +126,18 @@ def get_type_str(obj) -> str: def log_uvc_formats(properties: dict) -> None: logger.log_info(f"Supported Formats:", '') - logger.log_multiline(properties['formats'], logger.log_info, logger.indentation) + indent = ' '*8 + for fmt, data in properties['formats'].items(): + logger.log_info(f"{fmt}:", logger.indentation) + for res, fps in data.items(): + logger.log_info(f"{res}", logger.indentation+indent) + for f in fps: + logger.log_info(f"{f}", logger.indentation+indent*2) def log_uvc_v4l2ctrls(device_path: str, properties: dict) -> None: logger.log_info(f"Supported Controls:", '') - logger.log_info('', '') for section, controls in properties['v4l2ctrls'].items(): - logger.log_info(f"{section}:", '') + logger.log_info(f"{section}:", logger.indentation) for control, data in controls.items(): line = f"{control} ({data['type']})" line += (35 - len(line)) * ' ' + ': ' @@ -142,8 +147,8 @@ def log_uvc_v4l2ctrls(device_path: str, properties: dict) -> None: line += f" value={ctl.get_control(device_path, control)}" if 'flags' in data: line += f" flags={data['flags']}" - logger.log_info(line, logger.indentation) + logger.log_info(line, logger.indentation*2) if 'menu' in data: for value, name in data['menu'].items(): - logger.log_info(f"{value}: {name}", logger.indentation*2) + logger.log_info(f"{value}: {name}", logger.indentation*3) logger.log_info('', '') From 80cba413c949a243f04b1ff825ec6ab39769a842 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 23 Mar 2024 17:46:13 +0100 Subject: [PATCH 092/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/hwhandler.py | 49 ++++++++++++++++------------------------ pylibs/logging_helper.py | 2 +- pylibs/v4l2/ctl.py | 44 ++++++++++++++++++++++++++++-------- pylibs/v4l2/utils.py | 12 ++++++++++ pylibs/v4l2_control.py | 12 ++++++---- 5 files changed, 75 insertions(+), 44 deletions(-) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index c3e56a84..07714e90 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -2,8 +2,8 @@ import shutil import re -from pylibs import utils, v4l2_control as v4l2_ctl -from pylibs.v4l2 import ctl +from pylibs import utils +from pylibs.v4l2 import ctl as v4l2_ctl, utils as v4l2_utils avail_cams = { 'uvc': {}, @@ -11,19 +11,6 @@ 'legacy': {} } -def v4l2_qctl_to_dict(device: str) -> dict: - dev_ctl = ctl.qctrls[device] - values = {} - cur_sec = '' - for control in dev_ctl: - cur_ctl = dev_ctl[control] - if not cur_ctl['values']: - cur_sec = control - values[cur_sec] = {} - continue - values[cur_sec][control] = cur_ctl['values'] - return values - def get_avail_uvc_dev() -> dict: uvc_path = '/dev/v4l/by-id/' avail_uvc = [] @@ -35,15 +22,14 @@ def get_avail_uvc_dev() -> dict: for cam_path in avail_uvc: cams[cam_path] = {} cams[cam_path]['realpath'] = os.path.realpath(cam_path) - ctl.init_device(cam_path) - cams[cam_path]['formats'] = ctl.get_formats(cam_path) - cams[cam_path]['v4l2ctrls'] = v4l2_qctl_to_dict(cam_path) + cams[cam_path]['formats'] = v4l2_ctl.get_formats(cam_path) + cams[cam_path]['v4l2ctrls'] = v4l2_utils.get_dev_ctl_parsed_dict(cam_path) avail_cams['uvc'].update(cams) return cams def has_device_mjpg_hw(cam_path: str) -> bool: global avail_cams - for key in ctl.get_formats(cam_path).keys(): + for key in v4l2_ctl.get_formats(cam_path).keys(): if 'Motion-JPEG, compressed' in key: return True return False @@ -121,17 +107,22 @@ def get_avail_legacy() -> dict: count = count.split('=')[2].split(',')[0] if count == '0': return legacy - v4l2_cmd = 'v4l2-ctl --list-devices' - v4l2 = utils.execute_shell_command(v4l2_cmd) - legacy_path = '' - lines = v4l2.split('\n') - for i in range(len(lines)): - if 'mmal' in lines[i]: - legacy_path = lines[i+1].strip() - break + # v4l2_cmd = 'v4l2-ctl --list-devices' + # v4l2 = utils.execute_shell_command(v4l2_cmd) + # legacy_path = '' + # lines = v4l2.split('\n') + # for i in range(len(lines)): + # if 'mmal' in lines[i]: + # legacy_path = lines[i+1].strip() + # break + legacy_path = v4l2_ctl.get_dev_path_by_name('mmal') + if not legacy_path: + return legacy legacy[legacy_path] = {} - legacy[legacy_path]['formats'] = v4l2_ctl.get_uvc_formats(legacy_path) - legacy[legacy_path]['v4l2ctrls'] = v4l2_ctl.get_uvc_v4l2ctrls(legacy_path) + # legacy[legacy_path]['formats'] = v4l2_ctl.get_uvc_formats(legacy_path) + # legacy[legacy_path]['v4l2ctrls'] = v4l2_ctl.get_uvc_v4l2ctrls(legacy_path) + legacy[legacy_path]['formats'] = v4l2_ctl.get_formats(legacy_path) + legacy[legacy_path]['v4l2ctrls'] = v4l2_utils.get_dev_ctl_parsed_dict(legacy_path) avail_cams['legacy'].update(legacy) return legacy diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 5041a839..099bc69e 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -144,7 +144,7 @@ def log_uvc_v4l2ctrls(device_path: str, properties: dict) -> None: if data['type'] in ('int'): line += f"min={data['min']} max={data['max']} step={data['step']}" line += f" default={data['default']}" - line += f" value={ctl.get_control(device_path, control)}" + line += f" value={ctl.get_control_cur_value(device_path, control)}" if 'flags' in data: line += f" flags={data['flags']}" logger.log_info(line, logger.indentation*2) diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index d65eb935..0af204b7 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -7,7 +7,7 @@ from pylibs.v4l2 import raw, constants, utils -qctrls: dict[str, raw.v4l2_ext_control] = {} +dev_ctls: dict[str, dict[str, dict[str, (raw.v4l2_ext_control, str)]]] = {} def parse_qc(fd: int, qc: raw.v4l2_query_ext_ctrl) -> dict: """ @@ -33,7 +33,11 @@ def parse_qc(fd: int, qc: raw.v4l2_query_ext_ctrl) -> dict: controls['flags'] = utils.ctrlflags2str(qc.flags) if qc.type in (constants.V4L2_CTRL_TYPE_MENU, constants.V4L2_CTRL_TYPE_INTEGER_MENU): controls['menu'] = {} - for menu in utils.ioctl_iter(fd, raw.VIDIOC_QUERYMENU, raw.v4l2_querymenu(id=qc.id), qc.minimum, qc.maximum + 1, qc.step, True): + for menu in utils.ioctl_iter( + fd, + raw.VIDIOC_QUERYMENU, + raw.v4l2_querymenu(id=qc.id), qc.minimum, qc.maximum + 1, qc.step, True + ): if qc.type == constants.V4L2_CTRL_TYPE_MENU: controls['menu'][menu.index] = menu.name.decode() else: @@ -47,20 +51,42 @@ def init_device(device_path: str) -> None: fd = os.open(device_path, os.O_RDWR) next_fl = constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) - qctrls[device_path] = {} + dev_ctls[device_path] = {} for qc in utils.ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: name = qc.name.decode() else: name = utils.name2var(qc.name.decode()) - qctrls[device_path][name] = {} - qctrls[device_path][name]['qc'] = copy.deepcopy(qc) - qctrls[device_path][name]['values'] = parse_qc(fd, qc) + dev_ctls[device_path][name] = {} + dev_ctls[device_path][name]['qc'] = copy.deepcopy(qc) + dev_ctls[device_path][name]['values'] = parse_qc(fd, qc) # print_qctrl(fd, qc) qc.id |= next_fl # print(qctrls) os.close(fd) +def get_dev_ctl(device_path: str): + if device_path not in dev_ctls: + init_device(device_path) + return dev_ctls[device_path] + +def get_dev_ctl_parsed_dict(device_path: str) -> dict: + if device_path not in dev_ctls: + init_device(device_path) + return utils.ctl_to_parsed_dict(dev_ctls[device_path]) + +def get_dev_path_by_name(name: str) -> str: + """ + Get the device path by its name + """ + prefix = 'video' + for dev in os.listdir('/dev'): + if dev.startswith(prefix) and dev[len(prefix):].isdigit(): + path = f'/dev/{dev}' + if get_camera_capabilities(path)['card'].contains(name): + return path + return '' + def get_camera_capabilities(device_path: str) -> dict: """ Get the capabilities of a given device @@ -77,13 +103,13 @@ def get_camera_capabilities(device_path: str) -> dict: os.close(fd) return cap_dict -def get_control(device_path: str, control: str) -> int: +def get_control_cur_value(device_path: str, control: str) -> int: """ Get the current value of a control of a given device """ fd = os.open(device_path, os.O_RDWR) ctrl = raw.v4l2_control() - qc: raw.v4l2_query_ext_ctrl = qctrls[device_path][utils.name2var(control)]['qc'] + qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][utils.name2var(control)]['qc'] ctrl.id = qc.id utils.ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl) os.close(fd) @@ -95,7 +121,7 @@ def set_control(device_path: str, control: str, value: int) -> None: """ fd = os.open(device_path, os.O_RDWR) ctrl = raw.v4l2_control() - qc: raw.v4l2_query_ext_ctrl = qctrls[device_path][control]['qc'] + qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][control]['qc'] ctrl.id = qc.id ctrl.value = value utils.ioctl_safe(fd, raw.VIDIOC_S_CTRL, ctrl) diff --git a/pylibs/v4l2/utils.py b/pylibs/v4l2/utils.py index f9660d63..7b0fc92d 100644 --- a/pylibs/v4l2/utils.py +++ b/pylibs/v4l2/utils.py @@ -155,3 +155,15 @@ def frmival_to_str(frmival: raw.v4l2_frmivalenum) -> str: fract2fps(frmival.stepwise.min) ) return string + +def ctl_to_parsed_dict(dev_ctl: raw.v4l2_ext_control) -> dict: + values = {} + cur_sec = '' + for control in dev_ctl: + cur_ctl = dev_ctl[control] + if not cur_ctl['values']: + cur_sec = control + values[cur_sec] = {} + continue + values[cur_sec][control] = cur_ctl['values'] + return values diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py index 11ea09db..5879a464 100644 --- a/pylibs/v4l2_control.py +++ b/pylibs/v4l2_control.py @@ -1,5 +1,7 @@ from pylibs import logger, utils +from pylibs.v4l2 import ctl as v4l2_ctl + def get_uvc_formats(cam_path: str) -> str: command = f'v4l2-ctl -d {cam_path} --list-formats-ext' formats = utils.execute_shell_command(command) @@ -38,11 +40,11 @@ def set_v4l2ctrls(section: str, cam_path: str, ctrls: list[str] = None) -> str: logger.log_multiline(get_uvc_v4l2ctrls(cam_path), logger.log_debug) def get_cur_v4l2_value(cam_path: str, ctrl: str) -> str: - command = f'v4l2-ctl -d {cam_path} -C {ctrl}' - value = utils.execute_shell_command(command) - if value: - return value.split(':')[1].strip() - return value + # command = f'v4l2-ctl -d {cam_path} -C {ctrl}' + # value = utils.execute_shell_command(command) + # if value: + # return value.split(':')[1].strip() + return v4l2_ctl.get_control_cur_value(cam_path, ctrl) def brokenfocus(cam_path: str, focus_absolute_conf: str) -> str: cur_val = get_cur_v4l2_value(cam_path, 'focus_absolute') From 4517a873178d3357218f737118fe876934ddf8ee Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 23 Mar 2024 17:49:07 +0100 Subject: [PATCH 093/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/hwhandler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 07714e90..3bfe8a8f 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -23,7 +23,7 @@ def get_avail_uvc_dev() -> dict: cams[cam_path] = {} cams[cam_path]['realpath'] = os.path.realpath(cam_path) cams[cam_path]['formats'] = v4l2_ctl.get_formats(cam_path) - cams[cam_path]['v4l2ctrls'] = v4l2_utils.get_dev_ctl_parsed_dict(cam_path) + cams[cam_path]['v4l2ctrls'] = v4l2_ctl.get_dev_ctl_parsed_dict(cam_path) avail_cams['uvc'].update(cams) return cams @@ -122,7 +122,7 @@ def get_avail_legacy() -> dict: # legacy[legacy_path]['formats'] = v4l2_ctl.get_uvc_formats(legacy_path) # legacy[legacy_path]['v4l2ctrls'] = v4l2_ctl.get_uvc_v4l2ctrls(legacy_path) legacy[legacy_path]['formats'] = v4l2_ctl.get_formats(legacy_path) - legacy[legacy_path]['v4l2ctrls'] = v4l2_utils.get_dev_ctl_parsed_dict(legacy_path) + legacy[legacy_path]['v4l2ctrls'] = v4l2_ctl.get_dev_ctl_parsed_dict(legacy_path) avail_cams['legacy'].update(legacy) return legacy From 3e66ef29a21cc89f7ed7b7e4b1beb54d65396895 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 24 Mar 2024 22:02:38 +0100 Subject: [PATCH 094/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 13 +++++++----- pylibs/components/cam.py | 8 ++++--- pylibs/components/crowsnest.py | 6 ++++-- pylibs/components/section.py | 11 +++++----- pylibs/hwhandler.py | 36 +++++++++++++++++--------------- pylibs/logging_helper.py | 6 +++--- pylibs/v4l2/ctl.py | 24 ++++++++++----------- pylibs/v4l2/utils.py | 38 ++++++++++++++-------------------- 8 files changed, 72 insertions(+), 70 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index d5edf2fc..6ee2d1eb 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -25,8 +25,9 @@ def initial_parse_config(): config_path = args.config config.read(config_path) crowsnest = Crowsnest('crowsnest') - crowsnest.parse_config_section(config['crowsnest']) - logger.set_log_level(crowsnest.parameters['log_level'].value) + if 'crowsnest' not in config or not crowsnest.parse_config_section(config['crowsnest']): + logger.log_error("Failed to parse config for '[crowsnest]' section! Exiting...") + exit(1) async def start_sections(): global config, sect_exec_tasks @@ -57,12 +58,12 @@ async def start_sections(): task = asyncio.create_task(section_object.execute(lock)) sect_exec_tasks.add(task) - # Let sec_exec_tasks finish first + # Lets sec_exec_tasks finish first await asyncio.sleep(0) async with lock: logger.log_quiet("... Done!") - # Catch SIGINT and SIGTERM to exit gracefully and cancel all tasks + # Catches SIGINT and SIGTERM to exit gracefully and cancel all tasks signal.signal(signal.SIGINT, exit_gracefully) signal.signal(signal.SIGTERM, exit_gracefully) @@ -92,9 +93,11 @@ async def main(): if crowsnest.parameters['delete_log'].value: logger.logger.handlers.clear() - logger.setup_logging(args.log_path, 'w', crowsnest.parameters['log_level'].value) + logger.setup_logging(args.log_path, 'w') logging_helper.log_initial() + logger.set_log_level(crowsnest.parameters['log_level'].value) + logging_helper.log_host_info() logging_helper.log_config(args.config) logging_helper.log_cams() diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index 48d60d8b..5170098d 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -21,11 +21,13 @@ def __init__(self, name: str) -> None: def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: # Dynamically import module - mode = config_section["mode"].split()[0] - self.parameters["mode"].set_value(mode) - self.streamer = utils.load_component(mode, + if not super().parse_config_section(config_section, *args, **kwargs): + return False + self.streamer = utils.load_component(self.parameters['mode'].value, self.name, path='pylibs.components.streamer') + if self.streamer is None: + return False return self.streamer.parse_config_section(config_section, *args, **kwargs) async def execute(self, lock: asyncio.Lock): diff --git a/pylibs/components/crowsnest.py b/pylibs/components/crowsnest.py index 018c131b..b3173977 100644 --- a/pylibs/components/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -14,8 +14,9 @@ def __init__(self, name: str = '') -> None: 'no_proxy': Parameter(bool, 'False') }) - def parse_config_section(self, section: SectionProxy): - super().parse_config_section(section) + def parse_config_section(self, section: SectionProxy) -> bool: + if not super().parse_config_section(section): + return False log_level = self.parameters['log_level'].value.lower() if log_level == 'quiet': self.parameters['log_level'].value = 'QUIET' @@ -25,6 +26,7 @@ def parse_config_section(self, section: SectionProxy): self.parameters['log_level'].value = 'DEV' else: self.parameters['log_level'].value = 'INFO' + return True def load_component(name: str, config_section: SectionProxy, *args, **kwargs): diff --git a/pylibs/components/section.py b/pylibs/components/section.py index f2849357..83f15458 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -19,17 +19,16 @@ def __init__(self, name: str) -> None: # Parse config according to the needs of the section def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: success = True - for parameter in config_section: - value = config_section[parameter] + for parameter, value in config_section.items(): if parameter not in self.parameters: - print(f"Warning: Parameter '{parameter}' is not supported by {self.keyword}") + logger.log_warning(f"Parameter '{parameter}' is not supported by {self.keyword}!") continue value = value.split('#')[0].strip() self.parameters[parameter].set_value(value) - for parameter in self.parameters: - if self.parameters[parameter].value is None: + for parameter, value in self.parameters.items(): + if value is None: logger.log_error(f"Parameter '{parameter}' not found in section " - "[{self.section_name} {self.name}]") + "[{self.section_name} {self.name}] but is required!") success = False return success diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 3bfe8a8f..3bcf9b09 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -20,10 +20,11 @@ def get_avail_uvc_dev() -> dict: avail_uvc.append(path) cams = {} for cam_path in avail_uvc: - cams[cam_path] = {} - cams[cam_path]['realpath'] = os.path.realpath(cam_path) - cams[cam_path]['formats'] = v4l2_ctl.get_formats(cam_path) - cams[cam_path]['v4l2ctrls'] = v4l2_ctl.get_dev_ctl_parsed_dict(cam_path) + cams[cam_path] = { + 'realpath': os.path.realpath(cam_path), + 'formats': v4l2_ctl.get_formats(cam_path), + 'v4l2ctrls': v4l2_ctl.get_dev_ctl_parsed_dict(cam_path) + } avail_cams['uvc'].update(cams) return cams @@ -95,18 +96,18 @@ def rectangle_to_tuple(rectangle): return ctrls def get_avail_legacy() -> dict: - cmd = shutil.which('vcgencmd') legacy = {} - if not cmd: - return legacy - count_cmd = f'{cmd} get_camera' - count = utils.execute_shell_command(count_cmd) + # cmd = shutil.which('vcgencmd') + # if not cmd: + # return legacy + # count_cmd = f'{cmd} get_camera' + # count = utils.execute_shell_command(count_cmd) # Gets the number behind detected: "supported=1 detected=1, libcamera interfaces=0" - if not count: - return legacy - count = count.split('=')[2].split(',')[0] - if count == '0': - return legacy + # if not count: + # return legacy + # count = count.split('=')[2].split(',')[0] + # if count == '0': + # return legacy # v4l2_cmd = 'v4l2-ctl --list-devices' # v4l2 = utils.execute_shell_command(v4l2_cmd) # legacy_path = '' @@ -118,11 +119,12 @@ def get_avail_legacy() -> dict: legacy_path = v4l2_ctl.get_dev_path_by_name('mmal') if not legacy_path: return legacy - legacy[legacy_path] = {} + legacy[legacy_path] = { + 'formats': v4l2_ctl.get_formats(legacy_path), + 'v4l2ctrls': v4l2_ctl.get_dev_ctl_parsed_dict(legacy_path) + } # legacy[legacy_path]['formats'] = v4l2_ctl.get_uvc_formats(legacy_path) # legacy[legacy_path]['v4l2ctrls'] = v4l2_ctl.get_uvc_v4l2ctrls(legacy_path) - legacy[legacy_path]['formats'] = v4l2_ctl.get_formats(legacy_path) - legacy[legacy_path]['v4l2ctrls'] = v4l2_ctl.get_dev_ctl_parsed_dict(legacy_path) avail_cams['legacy'].update(legacy) return legacy diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 099bc69e..621a949d 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -129,10 +129,10 @@ def log_uvc_formats(properties: dict) -> None: indent = ' '*8 for fmt, data in properties['formats'].items(): logger.log_info(f"{fmt}:", logger.indentation) - for res, fps in data.items(): + for res, fps_list in data.items(): logger.log_info(f"{res}", logger.indentation+indent) - for f in fps: - logger.log_info(f"{f}", logger.indentation+indent*2) + for fps in fps_list: + logger.log_info(f"{fps}", logger.indentation+indent*2) def log_uvc_v4l2ctrls(device_path: str, properties: dict) -> None: logger.log_info(f"Supported Controls:", '') diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index 0af204b7..29bee8a2 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -57,12 +57,11 @@ def init_device(device_path: str) -> None: name = qc.name.decode() else: name = utils.name2var(qc.name.decode()) - dev_ctls[device_path][name] = {} - dev_ctls[device_path][name]['qc'] = copy.deepcopy(qc) - dev_ctls[device_path][name]['values'] = parse_qc(fd, qc) - # print_qctrl(fd, qc) + dev_ctls[device_path][name] = { + 'qc': copy.deepcopy(qc), + 'values': parse_qc(fd, qc) + } qc.id |= next_fl - # print(qctrls) os.close(fd) def get_dev_ctl(device_path: str): @@ -83,7 +82,7 @@ def get_dev_path_by_name(name: str) -> str: for dev in os.listdir('/dev'): if dev.startswith(prefix) and dev[len(prefix):].isdigit(): path = f'/dev/{dev}' - if get_camera_capabilities(path)['card'].contains(name): + if name in get_camera_capabilities(path).get('card'): return path return '' @@ -94,12 +93,13 @@ def get_camera_capabilities(device_path: str) -> dict: fd = os.open(device_path, os.O_RDWR) cap = raw.v4l2_capability() utils.ioctl_safe(fd, raw.VIDIOC_QUERYCAP, cap) - cap_dict = {} - cap_dict['driver'] = cap.driver.decode() - cap_dict['card'] = cap.card.decode() - cap_dict['bus'] = cap.bus_info.decode() - cap_dict['version'] = cap.version - cap_dict['capabilities'] = cap.capabilities + cap_dict = { + 'driver': cap.driver.decode(), + 'card': cap.card.decode(), + 'bus': cap.bus_info.decode(), + 'version': cap.version, + 'capabilities': utils.capabilities2str(cap.capabilities) + } os.close(fd) return cap_dict diff --git a/pylibs/v4l2/utils.py b/pylibs/v4l2/utils.py index 7b0fc92d..f8015a45 100644 --- a/pylibs/v4l2/utils.py +++ b/pylibs/v4l2/utils.py @@ -31,24 +31,18 @@ def ioctl_iter(fd: int, cmd: int, struct: ctypes.Structure, raise def v4l2_ctrl_type_to_string(ctrl_type: int) -> str: - if ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER: - return "int" - elif ctrl_type == constants.V4L2_CTRL_TYPE_BOOLEAN: - return "bool" - elif ctrl_type == constants.V4L2_CTRL_TYPE_MENU: - return "menu" - elif ctrl_type == constants.V4L2_CTRL_TYPE_BUTTON: - return "button" - elif ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER64: - return "int64" - elif ctrl_type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: - return "ctrl_class" - elif ctrl_type == constants.V4L2_CTRL_TYPE_STRING: - return "str" - elif ctrl_type == constants.V4L2_CTRL_TYPE_BITMASK: - return "bitmask" - elif ctrl_type == constants.V4L2_CTRL_TYPE_INTEGER_MENU: - return "intmenu" + dict_ctrl_type = { + constants.V4L2_CTRL_TYPE_INTEGER: "int", + constants.V4L2_CTRL_TYPE_BOOLEAN: "bool", + constants.V4L2_CTRL_TYPE_MENU: "menu", + constants.V4L2_CTRL_TYPE_BUTTON: "button", + constants.V4L2_CTRL_TYPE_INTEGER64: "int64", + constants.V4L2_CTRL_TYPE_CTRL_CLASS: "ctrl_class", + constants.V4L2_CTRL_TYPE_STRING: "str", + constants.V4L2_CTRL_TYPE_BITMASK: "bitmask", + constants.V4L2_CTRL_TYPE_INTEGER_MENU: "intmenu" + } + return dict_ctrl_type.get(ctrl_type, "unknown") def name2var(name: str) -> str: return re.sub('[^0-9a-zA-Z]+', '_', name).lower() @@ -69,7 +63,7 @@ def ctrlflags2str(flags: int) -> str: constants.V4L2_CTRL_FLAG_DYNAMIC_ARRAY: "dynamic-array", 0: None } - return dict_flags[flags] + return dict_flags.get(flags) def fmtflags2str(flags: int) -> str: dict_flags = { @@ -83,7 +77,7 @@ def fmtflags2str(flags: int) -> str: constants.V4L2_FMT_FLAG_CSC_QUANTIZATION: "csc-quantization", constants.V4L2_FMT_FLAG_CSC_XFER_FUNC: "csc-xfer-func" } - return dict_flags[flags] + return dict_flags.get(flags) def fcc2s(val: int) -> str: s = '' @@ -159,8 +153,8 @@ def frmival_to_str(frmival: raw.v4l2_frmivalenum) -> str: def ctl_to_parsed_dict(dev_ctl: raw.v4l2_ext_control) -> dict: values = {} cur_sec = '' - for control in dev_ctl: - cur_ctl = dev_ctl[control] + for control, cur_ctl in dev_ctl.items(): + # cur_ctl = dev_ctl[control] if not cur_ctl['values']: cur_sec = control values[cur_sec] = {} From 78ce01390d0cad6553cc74a6915d637bf17cf918 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 24 Mar 2024 23:18:02 +0100 Subject: [PATCH 095/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/cam.py | 6 +++--- pylibs/v4l2/ctl.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index 5170098d..1ab07337 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -21,9 +21,9 @@ def __init__(self, name: str) -> None: def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: # Dynamically import module - if not super().parse_config_section(config_section, *args, **kwargs): - return False - self.streamer = utils.load_component(self.parameters['mode'].value, + mode = config_section["mode"].split()[0] + self.parameters["mode"].set_value(mode) + self.streamer = utils.load_component(mode, self.name, path='pylibs.components.streamer') if self.streamer is None: diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index 29bee8a2..30a215b9 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -98,7 +98,7 @@ def get_camera_capabilities(device_path: str) -> dict: 'card': cap.card.decode(), 'bus': cap.bus_info.decode(), 'version': cap.version, - 'capabilities': utils.capabilities2str(cap.capabilities) + 'capabilities': cap.capabilities } os.close(fd) return cap_dict From 9511077606915eaef2cde1376102b32498c08dc0 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 24 Mar 2024 23:35:09 +0100 Subject: [PATCH 096/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/cam.py | 10 +++++----- pylibs/components/crowsnest.py | 4 ++-- pylibs/components/section.py | 4 ++-- pylibs/components/streamer/camera-streamer.py | 6 +++--- pylibs/components/streamer/streamer.py | 6 +++--- pylibs/components/streamer/ustreamer.py | 4 ++-- pylibs/hwhandler.py | 4 ++-- pylibs/logging_helper.py | 4 ++-- pylibs/parameter.py | 2 +- pylibs/utils.py | 2 +- pylibs/v4l2/ctl.py | 2 +- pylibs/v4l2/raw.py | 4 ++-- pylibs/v4l2/utils.py | 2 +- pylibs/v4l2_control.py | 4 ++-- pylibs/watchdog.py | 2 +- 15 files changed, 30 insertions(+), 30 deletions(-) diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index 1ab07337..a658320f 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -1,10 +1,10 @@ import asyncio from configparser import SectionProxy -from pylibs.components.section import Section -from pylibs.components.streamer.streamer import Streamer -from pylibs.parameter import Parameter -from pylibs import logger, utils, watchdog +from .section import Section +from .streamer.streamer import Streamer +from ..parameter import Parameter +from .. import logger, utils, watchdog class Cam(Section): section_name = 'cam' @@ -46,7 +46,7 @@ async def execute(self, lock: asyncio.Lock): except Exception as e: pass finally: - logger.log_error(f'Start of {self.parameters["mode"].value} [cam {self.name}] failed!') + logger.log_error(f"Start of {self.parameters['mode'].value} [cam {self.name}] failed!") watchdog.configured_devices.remove(self.streamer.parameters['device'].value) if lock.locked(): lock.release() diff --git a/pylibs/components/crowsnest.py b/pylibs/components/crowsnest.py index b3173977..8b0c8932 100644 --- a/pylibs/components/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -1,5 +1,5 @@ -from pylibs.components.section import Section -from pylibs.parameter import Parameter +from .section import Section +from ..parameter import Parameter from configparser import SectionProxy diff --git a/pylibs/components/section.py b/pylibs/components/section.py index 83f15458..b6b0c978 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -1,8 +1,8 @@ import asyncio from configparser import SectionProxy -from pylibs.parameter import Parameter -from pylibs import logger +from ..parameter import Parameter +from .. import logger class Section: section_name = 'section' diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index b1ca7303..42b34993 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -1,8 +1,8 @@ import asyncio -from pylibs.components.streamer.streamer import Streamer -from pylibs.parameter import Parameter -from pylibs import logger, utils, hwhandler +from .streamer import Streamer +from ...parameter import Parameter +from ... import logger, utils, hwhandler class Camera_Streamer(Streamer): keyword = 'camera-streamer' diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index 531a5aac..ad150cc6 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -1,9 +1,9 @@ import textwrap from configparser import SectionProxy -from pylibs.components.section import Section -from pylibs.parameter import Parameter -from pylibs import logger, utils +from ..section import Section +from ...parameter import Parameter +from ... import logger, utils class Streamer(Section): binaries = {} diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index 9966a24c..749d83f3 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -1,8 +1,8 @@ import re import asyncio -from pylibs.components.streamer.streamer import Streamer -from pylibs import logger, utils, hwhandler, v4l2_control as v4l2_ctl +from .streamer import Streamer +from ... import logger, utils, hwhandler, v4l2_control as v4l2_ctl class Ustreamer(Streamer): keyword = 'ustreamer' diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 3bcf9b09..000442ec 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -2,8 +2,8 @@ import shutil import re -from pylibs import utils -from pylibs.v4l2 import ctl as v4l2_ctl, utils as v4l2_utils +from . import utils +from .v4l2 import ctl as v4l2_ctl, utils as v4l2_utils avail_cams = { 'uvc': {}, diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 621a949d..5af88fc3 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -3,8 +3,8 @@ import sys import shutil -from pylibs import utils, logger, hwhandler -from pylibs.v4l2 import ctl +from . import utils, logger, hwhandler +from .v4l2 import ctl def log_initial(): logger.log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') diff --git a/pylibs/parameter.py b/pylibs/parameter.py index 505961c9..d1ba315a 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -1,4 +1,4 @@ -from pylibs import logger +from . import logger class Parameter: def __init__(self, type=str, default=None) -> None: diff --git a/pylibs/utils.py b/pylibs/utils.py index 30d74af0..a13002dc 100644 --- a/pylibs/utils.py +++ b/pylibs/utils.py @@ -4,7 +4,7 @@ import shutil import os -from pylibs import logger +from . import logger # Dynamically import component # Requires module to have a load_component() function, diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index 30a215b9..d475fb3c 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -5,7 +5,7 @@ import os import copy -from pylibs.v4l2 import raw, constants, utils +from . import raw, constants, utils dev_ctls: dict[str, dict[str, dict[str, (raw.v4l2_ext_control, str)]]] = {} diff --git a/pylibs/v4l2/raw.py b/pylibs/v4l2/raw.py index cd3a1469..6d2bf485 100644 --- a/pylibs/v4l2/raw.py +++ b/pylibs/v4l2/raw.py @@ -1,8 +1,8 @@ import ctypes -from pylibs.v4l2 import ioctl_macros +from . import ioctl_macros -from pylibs.v4l2 import constants +from . import constants class v4l2_capability(ctypes.Structure): _fields_ = [ diff --git a/pylibs/v4l2/utils.py b/pylibs/v4l2/utils.py index f8015a45..0ab2e52e 100644 --- a/pylibs/v4l2/utils.py +++ b/pylibs/v4l2/utils.py @@ -3,7 +3,7 @@ import re from typing import Generator -from pylibs.v4l2 import raw, constants +from . import raw, constants def ioctl_safe(fd: int, request: int, arg: ctypes.Structure) -> int: diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py index 5879a464..c386ce1a 100644 --- a/pylibs/v4l2_control.py +++ b/pylibs/v4l2_control.py @@ -1,6 +1,6 @@ -from pylibs import logger, utils +from . import logger, utils -from pylibs.v4l2 import ctl as v4l2_ctl +from .v4l2 import ctl as v4l2_ctl def get_uvc_formats(cam_path: str) -> str: command = f'v4l2-ctl -d {cam_path} --list-formats-ext' diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index 25681ae5..b2fee0b1 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -1,6 +1,6 @@ import os import asyncio -from pylibs import logger +from . import logger configured_devices: list[str] = [] lost_devices: list[str] = [] From da12c7e9fa59ceedb0ce5e9365ccbf76987220ce Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 25 Mar 2024 10:45:04 +0100 Subject: [PATCH 097/129] Chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/v4l2/ctl.py | 161 +++++++++++++++++++++++++-------------------- 1 file changed, 91 insertions(+), 70 deletions(-) diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index d475fb3c..9e6c3ceb 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -44,29 +44,34 @@ def parse_qc(fd: int, qc: raw.v4l2_query_ext_ctrl) -> dict: controls['menu'][menu.index] = menu.value return controls -def init_device(device_path: str) -> None: +def init_device(device_path: str) -> bool: """ Initialize a given device """ - fd = os.open(device_path, os.O_RDWR) - next_fl = constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND - qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) - dev_ctls[device_path] = {} - for qc in utils.ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): - if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: - name = qc.name.decode() - else: - name = utils.name2var(qc.name.decode()) - dev_ctls[device_path][name] = { - 'qc': copy.deepcopy(qc), - 'values': parse_qc(fd, qc) - } - qc.id |= next_fl - os.close(fd) + try: + fd = os.open(device_path, os.O_RDWR) + next_fl = constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND + qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) + dev_ctls[device_path] = {} + for qc in utils.ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): + if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + name = qc.name.decode() + else: + name = utils.name2var(qc.name.decode()) + dev_ctls[device_path][name] = { + 'qc': copy.deepcopy(qc), + 'values': parse_qc(fd, qc) + } + qc.id |= next_fl + os.close(fd) + return True + except FileNotFoundError: + return False -def get_dev_ctl(device_path: str): +def get_dev_ctl(device_path: str) -> dict: if device_path not in dev_ctls: - init_device(device_path) + if not init_device(device_path): + return None return dev_ctls[device_path] def get_dev_ctl_parsed_dict(device_path: str) -> dict: @@ -90,68 +95,84 @@ def get_camera_capabilities(device_path: str) -> dict: """ Get the capabilities of a given device """ - fd = os.open(device_path, os.O_RDWR) - cap = raw.v4l2_capability() - utils.ioctl_safe(fd, raw.VIDIOC_QUERYCAP, cap) - cap_dict = { - 'driver': cap.driver.decode(), - 'card': cap.card.decode(), - 'bus': cap.bus_info.decode(), - 'version': cap.version, - 'capabilities': cap.capabilities - } - os.close(fd) - return cap_dict + try: + fd = os.open(device_path, os.O_RDWR) + cap = raw.v4l2_capability() + utils.ioctl_safe(fd, raw.VIDIOC_QUERYCAP, cap) + cap_dict = { + 'driver': cap.driver.decode(), + 'card': cap.card.decode(), + 'bus': cap.bus_info.decode(), + 'version': cap.version, + 'capabilities': cap.capabilities + } + os.close(fd) + return cap_dict + except FileNotFoundError: + return {} def get_control_cur_value(device_path: str, control: str) -> int: """ Get the current value of a control of a given device """ - fd = os.open(device_path, os.O_RDWR) - ctrl = raw.v4l2_control() - qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][utils.name2var(control)]['qc'] - ctrl.id = qc.id - utils.ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl) - os.close(fd) - return ctrl.value + try: + fd = os.open(device_path, os.O_RDWR) + ctrl = raw.v4l2_control() + qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][utils.name2var(control)]['qc'] + ctrl.id = qc.id + utils.ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl) + os.close(fd) + return ctrl.value + except FileNotFoundError: + return None -def set_control(device_path: str, control: str, value: int) -> None: +def set_control(device_path: str, control: str, value: int) -> bool: """ Set the value of a control of a given device """ - fd = os.open(device_path, os.O_RDWR) - ctrl = raw.v4l2_control() - qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][control]['qc'] - ctrl.id = qc.id - ctrl.value = value - utils.ioctl_safe(fd, raw.VIDIOC_S_CTRL, ctrl) - os.close(fd) + success = False + try: + fd = os.open(device_path, os.O_RDWR) + ctrl = raw.v4l2_control() + qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][control]['qc'] + ctrl.id = qc.id + ctrl.value = value + if utils.ioctl_safe(fd, raw.VIDIOC_S_CTRL, ctrl) != -1: + success = True + os.close(fd) + return True + except FileNotFoundError: + pass + return success -def get_formats(device_path: str) -> list: +def get_formats(device_path: str) -> dict: """ Get the available formats of a given device """ - fd = os.open(device_path, os.O_RDWR) - fmt = raw.v4l2_fmtdesc() - frmsize = raw.v4l2_frmsizeenum() - frmival = raw.v4l2_frmivalenum() - fmt.index = 0 - fmt.type = constants.V4L2_BUF_TYPE_VIDEO_CAPTURE - formats = {} - for fmt in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FMT, fmt): - str = f"[{fmt.index}]: '{utils.fcc2s(fmt.pixelformat)}' ({fmt.description.decode()}" - if fmt.flags: - str += f", {utils.fmtflags2str(fmt.flags)}" - str += ')' - formats[str] = {} - frmsize.pixel_format = fmt.pixelformat - for size in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FRAMESIZES, frmsize): - size_str = utils.frmsize_to_str(size) - formats[str][size_str] = [] - frmival.pixel_format = fmt.pixelformat - frmival.width = frmsize.discrete.width - frmival.height = frmsize.discrete.height - for interval in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FRAMEINTERVALS, frmival): - formats[str][size_str].append(utils.frmival_to_str(interval)) - os.close(fd) - return formats + try: + fd = os.open(device_path, os.O_RDWR) + fmt = raw.v4l2_fmtdesc() + frmsize = raw.v4l2_frmsizeenum() + frmival = raw.v4l2_frmivalenum() + fmt.index = 0 + fmt.type = constants.V4L2_BUF_TYPE_VIDEO_CAPTURE + formats = {} + for fmt in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FMT, fmt): + str = f"[{fmt.index}]: '{utils.fcc2s(fmt.pixelformat)}' ({fmt.description.decode()}" + if fmt.flags: + str += f", {utils.fmtflags2str(fmt.flags)}" + str += ')' + formats[str] = {} + frmsize.pixel_format = fmt.pixelformat + for size in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FRAMESIZES, frmsize): + size_str = utils.frmsize_to_str(size) + formats[str][size_str] = [] + frmival.pixel_format = fmt.pixelformat + frmival.width = frmsize.discrete.width + frmival.height = frmsize.discrete.height + for interval in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FRAMEINTERVALS, frmival): + formats[str][size_str].append(utils.frmival_to_str(interval)) + os.close(fd) + return formats + except FileNotFoundError: + return {} From 71cfe6b6be7fd800f7d53e8bc03911ca87353018 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 25 Mar 2024 10:57:31 +0100 Subject: [PATCH 098/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/watchdog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index b2fee0b1..aa3408cd 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -23,5 +23,5 @@ def crowsnest_watchdog(): async def run_watchdog(): global running while running: - await asyncio.sleep(120) crowsnest_watchdog() + await asyncio.sleep(120) From 09269e98e18b0bc74546e4bbb4ea49197a08d2f8 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 25 Mar 2024 11:04:59 +0100 Subject: [PATCH 099/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/crowsnest.py b/crowsnest.py index 6ee2d1eb..17138ba4 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -103,6 +103,7 @@ async def main(): logging_helper.log_cams() task1 = asyncio.create_task(start_sections()) + await asyncio.sleep(0) task2 = asyncio.create_task(watchdog.run_watchdog()) await task1 From 48f7cd1ecf636247dbf1aaed889e54a920ebda91 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Tue, 26 Mar 2024 12:22:10 +0100 Subject: [PATCH 100/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/v4l2_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py index c386ce1a..899ea65c 100644 --- a/pylibs/v4l2_control.py +++ b/pylibs/v4l2_control.py @@ -30,7 +30,7 @@ def set_v4l2ctrls(section: str, cam_path: str, ctrls: list[str] = None) -> str: for ctrl in ctrls: if ctrl.split('=')[0].strip().lower() not in avail_ctrls: logger.log_quiet( - f"Parameter '{ctrl}' not available for '{cam_path}'. Skipped.", + f"Parameter '{ctrl.strip()}' not available for '{cam_path}'. Skipped.", prefix ) continue From 19b7017e8c20fb3ff03adca2d23fbba5e66678c5 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Tue, 26 Mar 2024 12:56:16 +0100 Subject: [PATCH 101/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 14 ++++++++------ pylibs/components/cam.py | 5 +++-- pylibs/v4l2_control.py | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 17138ba4..46388766 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,12 +1,12 @@ import argparse import configparser +import asyncio +import signal +import traceback from pylibs.components.crowsnest import Crowsnest from pylibs import utils, watchdog, logger, logging_helper -import asyncio -import signal - parser = argparse.ArgumentParser( prog='Crowsnest', description='Crowsnest - A webcam daemon for Raspberry Pi OS distributions like MainsailOS' @@ -34,8 +34,8 @@ async def start_sections(): sect_objs = [] sect_exec_tasks = set() - logger.log_quiet("Try to start configured Cams / Services...") try: + logger.log_quiet("Try to parse configured Cams / Services...") for section in config.sections(): section_header = section.split(' ') section_object = None @@ -47,12 +47,14 @@ async def start_sections(): section_name = ' '.join(section_header[1:]) component = utils.load_component(section_keyword, section_name) + logger.log_quiet(f"Parse configuration of section [{section}] ...") if component.parse_config_section(config[section]): sect_objs.append(component) - logger.log_info(f"Configuration of section [{section}] looks good. Continue ...") + logger.log_quiet(f"Configuration of section [{section}] looks good. Continue ...") else: logger.log_error(f"Failed to parse config for section [{section}]!") + logger.log_quiet("Try to start configured Cams / Services...") lock = asyncio.Lock() for section_object in sect_objs: task = asyncio.create_task(section_object.execute(lock)) @@ -71,7 +73,7 @@ async def start_sections(): if task is not None: await task except Exception as e: - logger.log_error(e) + logger.log_multiline(traceback.format_exc().strip(), logger.log_error) finally: for task in sect_exec_tasks: if task is not None: diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index a658320f..51c65faf 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -1,4 +1,5 @@ import asyncio +import traceback from configparser import SectionProxy from .section import Section @@ -37,14 +38,14 @@ async def execute(self, lock: asyncio.Lock): try: await lock.acquire() logger.log_quiet( - f"Starting {self.streamer.keyword} with device " + f"Start {self.streamer.keyword} with device " f"{self.streamer.parameters['device'].value} ..." ) watchdog.configured_devices.append(self.streamer.parameters['device'].value) process = await self.streamer.execute(lock) await process.wait() except Exception as e: - pass + logger.log_multiline(traceback.format_exc().strip(), logger.log_error) finally: logger.log_error(f"Start of {self.parameters['mode'].value} [cam {self.name}] failed!") watchdog.configured_devices.remove(self.streamer.parameters['device'].value) diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py index 899ea65c..9f590c0e 100644 --- a/pylibs/v4l2_control.py +++ b/pylibs/v4l2_control.py @@ -50,7 +50,7 @@ def brokenfocus(cam_path: str, focus_absolute_conf: str) -> str: cur_val = get_cur_v4l2_value(cam_path, 'focus_absolute') if cur_val and cur_val != focus_absolute_conf: logger.log_warning(f"Detected 'brokenfocus' device.") - logger.log_info(f"Trying to set to configured Value.") + logger.log_info(f"Try to set to configured Value.") set_v4l2_ctrl(cam_path, f'focus_absolute={focus_absolute_conf}') logger.log_debug(f"Value is now: {get_cur_v4l2_value(cam_path, 'focus_absolute')}") From 55477a84347634f239bdea16229d8d49aefdd8dc Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 27 Mar 2024 13:57:23 +0100 Subject: [PATCH 102/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/section.py | 6 +++--- pylibs/components/streamer/camera-streamer.py | 4 +--- pylibs/components/streamer/resolution.py | 11 +++++++++++ pylibs/components/streamer/streamer.py | 3 ++- pylibs/components/streamer/ustreamer.py | 2 +- pylibs/logger.py | 2 +- pylibs/parameter.py | 7 +++++-- 7 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 pylibs/components/streamer/resolution.py diff --git a/pylibs/components/section.py b/pylibs/components/section.py index b6b0c978..62834725 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -26,9 +26,9 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> value = value.split('#')[0].strip() self.parameters[parameter].set_value(value) for parameter, value in self.parameters.items(): - if value is None: - logger.log_error(f"Parameter '{parameter}' not found in section " - "[{self.section_name} {self.name}] but is required!") + if value.value is None: + logger.log_error(f"Parameter '{parameter}' incorrectly set or missing in section " + f"[{self.section_name} {self.name}] but is required!") success = False return success diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index 42b34993..0720a920 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -25,9 +25,7 @@ async def execute(self, lock: asyncio.Lock): else: host = '127.0.0.1' port = self.parameters['port'].value - res = self.parameters['resolution'].value.split('x') - width = res[0] - height = res[1] + width, height = str(self.parameters['resolution'].value).split('x') fps = self.parameters['max_fps'].value device = self.parameters['device'].value diff --git a/pylibs/components/streamer/resolution.py b/pylibs/components/streamer/resolution.py new file mode 100644 index 00000000..0a3e0a01 --- /dev/null +++ b/pylibs/components/streamer/resolution.py @@ -0,0 +1,11 @@ +class Resolution(): + def __init__(self, value:str) -> None: + try: + self.width, self.height = value.split('x') + except ValueError: + # logger.log_error(f"{value} is not of format 'x'! Parameter ignored!") + raise ValueError("Custom Error", f"'{value}' is not of format 'x'! " + "Parameter ignored!") + + def __str__(self) -> str: + return 'x'.join([self.width, self.height]) diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index ad150cc6..3b8acdff 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -1,6 +1,7 @@ import textwrap from configparser import SectionProxy +from .resolution import Resolution from ..section import Section from ...parameter import Parameter from ... import logger, utils @@ -15,7 +16,7 @@ def __init__(self, name: str) -> None: 'mode': Parameter(str), 'port': Parameter(int), 'device': Parameter(str), - 'resolution': Parameter(str), + 'resolution': Parameter(Resolution), 'max_fps': Parameter(int), 'no_proxy': Parameter(bool, 'False'), 'custom_flags': Parameter(str, ''), diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index 749d83f3..33d6ff83 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -27,7 +27,7 @@ async def execute(self, lock: asyncio.Lock): streamer_args = [ '--host', host, '--port', str(port), - '--resolution', res, + '--resolution', str(res), '--desired-fps', str(fps), # webroot & allow crossdomain requests '--allow-origin', '\*', diff --git a/pylibs/logger.py b/pylibs/logger.py index 9c47a817..5a08c5dc 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -6,7 +6,7 @@ DEV = 10 DEBUG = 15 -QUIET = 25 +QUIET = 35 indentation = 6*' ' diff --git a/pylibs/parameter.py b/pylibs/parameter.py index d1ba315a..92fd3f8f 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -18,5 +18,8 @@ def set_value(self, value): raise ValueError() else: self.value = self.type(value) - except ValueError: - logger.log_error(f"{value} is not of type {self.type}! Parameter ignored!") + except ValueError as e: + message = f"'{value}' is not of type '{self.type.__name__}'! Parameter ignored!" + if len(e.args) > 1 and e.args[0] == "Custom Error": + message = e.args[1] + logger.log_error(message) From bffdbb7a8e6c7d55b46d7952c9df2b7feb7548b6 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 27 Mar 2024 19:51:23 +0100 Subject: [PATCH 103/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 42 ++++++++++++++---------- pylibs/components/streamer/resolution.py | 11 ------- pylibs/components/streamer/streamer.py | 13 +++++++- 3 files changed, 37 insertions(+), 29 deletions(-) delete mode 100644 pylibs/components/streamer/resolution.py diff --git a/crowsnest.py b/crowsnest.py index 46388766..f2ecd445 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -23,7 +23,12 @@ def initial_parse_config(): global crowsnest, config, args config_path = args.config - config.read(config_path) + try: + config.read(config_path) + except configparser.ParsingError as e: + logger.log_multiline(e.message, logger.log_error) + logger.log_error("Failed to parse config! Exiting...") + exit(1) crowsnest = Crowsnest('crowsnest') if 'crowsnest' not in config or not crowsnest.parse_config_section(config['crowsnest']): logger.log_error("Failed to parse config for '[crowsnest]' section! Exiting...") @@ -34,6 +39,10 @@ async def start_sections(): sect_objs = [] sect_exec_tasks = set() + # Catches SIGINT and SIGTERM to exit gracefully and cancel all tasks + signal.signal(signal.SIGINT, exit_gracefully) + signal.signal(signal.SIGTERM, exit_gracefully) + try: logger.log_quiet("Try to parse configured Cams / Services...") for section in config.sections(): @@ -52,22 +61,21 @@ async def start_sections(): sect_objs.append(component) logger.log_quiet(f"Configuration of section [{section}] looks good. Continue ...") else: - logger.log_error(f"Failed to parse config for section [{section}]!") - - logger.log_quiet("Try to start configured Cams / Services...") - lock = asyncio.Lock() - for section_object in sect_objs: - task = asyncio.create_task(section_object.execute(lock)) - sect_exec_tasks.add(task) - - # Lets sec_exec_tasks finish first - await asyncio.sleep(0) - async with lock: - logger.log_quiet("... Done!") - - # Catches SIGINT and SIGTERM to exit gracefully and cancel all tasks - signal.signal(signal.SIGINT, exit_gracefully) - signal.signal(signal.SIGTERM, exit_gracefully) + logger.log_error(f"Failed to parse config for section [{section}]! Skipping ...") + + logger.log_quiet("Try to start configured Cams / Services ...") + if sect_objs: + lock = asyncio.Lock() + for section_object in sect_objs: + task = asyncio.create_task(section_object.execute(lock)) + sect_exec_tasks.add(task) + + # Lets sec_exec_tasks finish first + await asyncio.sleep(0) + async with lock: + logger.log_quiet("... Done!") + else: + logger.log_quiet("No Cams / Services to start! Exiting ...") for task in sect_exec_tasks: if task is not None: diff --git a/pylibs/components/streamer/resolution.py b/pylibs/components/streamer/resolution.py deleted file mode 100644 index 0a3e0a01..00000000 --- a/pylibs/components/streamer/resolution.py +++ /dev/null @@ -1,11 +0,0 @@ -class Resolution(): - def __init__(self, value:str) -> None: - try: - self.width, self.height = value.split('x') - except ValueError: - # logger.log_error(f"{value} is not of format 'x'! Parameter ignored!") - raise ValueError("Custom Error", f"'{value}' is not of format 'x'! " - "Parameter ignored!") - - def __str__(self) -> str: - return 'x'.join([self.width, self.height]) diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index 3b8acdff..3c335b3d 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -1,12 +1,23 @@ import textwrap from configparser import SectionProxy -from .resolution import Resolution from ..section import Section from ...parameter import Parameter from ... import logger, utils +class Resolution(): + def __init__(self, value:str) -> None: + try: + self.width, self.height = value.split('x') + except ValueError: + raise ValueError("Custom Error", f"'{value}' is not of format 'x'! " + "Parameter ignored!") + + def __str__(self) -> str: + return 'x'.join([self.width, self.height]) + class Streamer(Section): + section_name = 'cam' binaries = {} def __init__(self, name: str) -> None: From 70b9bf9bb07f5c7a208eabb5e285a268b518f0b9 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 30 Mar 2024 11:20:54 +0100 Subject: [PATCH 104/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/streamer/streamer.py | 3 +-- pylibs/logging_helper.py | 16 +++++++++------- pylibs/parameter.py | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index 3c335b3d..eb180925 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -10,8 +10,7 @@ def __init__(self, value:str) -> None: try: self.width, self.height = value.split('x') except ValueError: - raise ValueError("Custom Error", f"'{value}' is not of format 'x'! " - "Parameter ignored!") + raise ValueError("Custom Error", f"'{value}' is not of format 'x'!") def __str__(self) -> str: return 'x'.join([self.width, self.height]) diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 5af88fc3..e1304002 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -93,14 +93,12 @@ def log_cams(): if legacy: for path, properties in legacy.items(): logger.log_info(f"Detected 'Raspicam' Device -> {path}") - log_uvc_formats(properties) - log_uvc_v4l2ctrls(path, properties) + log_uvc_dev(path, properties) if uvc: logger.log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") for path, properties in uvc.items(): logger.log_info(f"{path} -> {properties['realpath']}", '') - log_uvc_formats(properties) - log_uvc_v4l2ctrls(path, properties) + log_uvc_dev(path, properties) def log_libcamera_dev(path: str, properties: dict) -> str: logger.log_info(f"Detected 'libcamera' device -> {path}") @@ -115,7 +113,7 @@ def log_libcamera_dev(path: str, properties: dict) -> str: min, max, default = value.values() str_first = f"{name} ({get_type_str(min)})" str_second = f"min={min} max={max} default={default}" - str_indent = (35 - len(str_first)) * ' ' + ': ' + str_indent = (30 - len(str_first)) * ' ' + ': ' logger.log_info(str_first + str_indent + str_second, logger.indentation) else: logger.log_info("apt package 'python3-libcamera' is not installed! " @@ -124,6 +122,10 @@ def log_libcamera_dev(path: str, properties: dict) -> str: def get_type_str(obj) -> str: return str(type(obj)).split('\'')[1] +def log_uvc_dev(path: str, properties: dict) -> str: + log_uvc_formats(properties) + log_uvc_v4l2ctrls(path, properties) + def log_uvc_formats(properties: dict) -> None: logger.log_info(f"Supported Formats:", '') indent = ' '*8 @@ -140,9 +142,9 @@ def log_uvc_v4l2ctrls(device_path: str, properties: dict) -> None: logger.log_info(f"{section}:", logger.indentation) for control, data in controls.items(): line = f"{control} ({data['type']})" - line += (35 - len(line)) * ' ' + ': ' + line += (35 - len(line)) * ' ' + ':' if data['type'] in ('int'): - line += f"min={data['min']} max={data['max']} step={data['step']}" + line += f" min={data['min']} max={data['max']} step={data['step']}" line += f" default={data['default']}" line += f" value={ctl.get_control_cur_value(device_path, control)}" if 'flags' in data: diff --git a/pylibs/parameter.py b/pylibs/parameter.py index 92fd3f8f..3bd9095d 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -19,7 +19,8 @@ def set_value(self, value): else: self.value = self.type(value) except ValueError as e: - message = f"'{value}' is not of type '{self.type.__name__}'! Parameter ignored!" + message = f"'{value}' is not of type '{self.type.__name__}'!" if len(e.args) > 1 and e.args[0] == "Custom Error": message = e.args[1] + message += " Parameter ignored!" logger.log_error(message) From 6dd03ca659a46259ddc2004ba20608c3c8928bad Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 30 Mar 2024 12:09:33 +0100 Subject: [PATCH 105/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/logging_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index e1304002..d060c50c 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -117,7 +117,7 @@ def log_libcamera_dev(path: str, properties: dict) -> str: logger.log_info(str_first + str_indent + str_second, logger.indentation) else: logger.log_info("apt package 'python3-libcamera' is not installed! " - "Make sure to install it to log the controls!", logger.indentation) + "Make sure to install it to log the controls!", logger.indentation) def get_type_str(obj) -> str: return str(type(obj)).split('\'')[1] From b01bc7cd605fb5f2e2510c4dddba55f7b30087cb Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 30 Mar 2024 14:11:54 +0100 Subject: [PATCH 106/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/hwhandler.py | 22 ---------------------- pylibs/logging_helper.py | 22 +++------------------- pylibs/utils.py | 22 ++++++++++++++++++++++ pylibs/v4l2/utils.py | 1 - pylibs/v4l2_control.py | 27 +++++++++++++-------------- 5 files changed, 38 insertions(+), 56 deletions(-) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py index 000442ec..b613f52a 100644 --- a/pylibs/hwhandler.py +++ b/pylibs/hwhandler.py @@ -62,7 +62,6 @@ def get_libcamera_resolutions(libcamera_output: str, camera_path: str) -> list: ) res = [] if resolutions: - # Maybe cut out fps? re.sub('\[.*? - ', '[', r.strip()) res = [r.strip() for r in resolutions[0].split('\n')] return res @@ -97,25 +96,6 @@ def rectangle_to_tuple(rectangle): def get_avail_legacy() -> dict: legacy = {} - # cmd = shutil.which('vcgencmd') - # if not cmd: - # return legacy - # count_cmd = f'{cmd} get_camera' - # count = utils.execute_shell_command(count_cmd) - # Gets the number behind detected: "supported=1 detected=1, libcamera interfaces=0" - # if not count: - # return legacy - # count = count.split('=')[2].split(',')[0] - # if count == '0': - # return legacy - # v4l2_cmd = 'v4l2-ctl --list-devices' - # v4l2 = utils.execute_shell_command(v4l2_cmd) - # legacy_path = '' - # lines = v4l2.split('\n') - # for i in range(len(lines)): - # if 'mmal' in lines[i]: - # legacy_path = lines[i+1].strip() - # break legacy_path = v4l2_ctl.get_dev_path_by_name('mmal') if not legacy_path: return legacy @@ -123,8 +103,6 @@ def get_avail_legacy() -> dict: 'formats': v4l2_ctl.get_formats(legacy_path), 'v4l2ctrls': v4l2_ctl.get_dev_ctl_parsed_dict(legacy_path) } - # legacy[legacy_path]['formats'] = v4l2_ctl.get_uvc_formats(legacy_path) - # legacy[legacy_path]['v4l2ctrls'] = v4l2_ctl.get_uvc_v4l2ctrls(legacy_path) avail_cams['legacy'].update(legacy) return legacy diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index d060c50c..5d6dfe66 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -4,7 +4,6 @@ import shutil from . import utils, logger, hwhandler -from .v4l2 import ctl def log_initial(): logger.log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') @@ -124,7 +123,7 @@ def get_type_str(obj) -> str: def log_uvc_dev(path: str, properties: dict) -> str: log_uvc_formats(properties) - log_uvc_v4l2ctrls(path, properties) + log_uvc_v4l2ctrls(path) def log_uvc_formats(properties: dict) -> None: logger.log_info(f"Supported Formats:", '') @@ -136,21 +135,6 @@ def log_uvc_formats(properties: dict) -> None: for fps in fps_list: logger.log_info(f"{fps}", logger.indentation+indent*2) -def log_uvc_v4l2ctrls(device_path: str, properties: dict) -> None: +def log_uvc_v4l2ctrls(device_path: str) -> None: logger.log_info(f"Supported Controls:", '') - for section, controls in properties['v4l2ctrls'].items(): - logger.log_info(f"{section}:", logger.indentation) - for control, data in controls.items(): - line = f"{control} ({data['type']})" - line += (35 - len(line)) * ' ' + ':' - if data['type'] in ('int'): - line += f" min={data['min']} max={data['max']} step={data['step']}" - line += f" default={data['default']}" - line += f" value={ctl.get_control_cur_value(device_path, control)}" - if 'flags' in data: - line += f" flags={data['flags']}" - logger.log_info(line, logger.indentation*2) - if 'menu' in data: - for value, name in data['menu'].items(): - logger.log_info(f"{value}: {name}", logger.indentation*3) - logger.log_info('', '') + logger.log_multiline(utils.get_v4l2_ctl_str(device_path), logger.log_info, logger.indentation) diff --git a/pylibs/utils.py b/pylibs/utils.py index a13002dc..5e27b11d 100644 --- a/pylibs/utils.py +++ b/pylibs/utils.py @@ -5,6 +5,7 @@ import os from . import logger +from .v4l2 import ctl as v4l2_ctl # Dynamically import component # Requires module to have a load_component() function, @@ -100,3 +101,24 @@ def grep(path: str, search: str) -> str: except FileNotFoundError: logger.log_error(f"File '{path}' not found!") return '' + +def get_v4l2_ctl_str(cam_path: str) -> str: + ctrls = v4l2_ctl.get_dev_ctl_parsed_dict(cam_path) + message = '' + for section, controls in ctrls.items(): + message += f"{section}:\n" + for control, data in controls.items(): + line = f"{control} ({data['type']})" + line += (35 - len(line)) * ' ' + ':' + if data['type'] in ('int'): + line += f" min={data['min']} max={data['max']} step={data['step']}" + line += f" default={data['default']}" + line += f" value={v4l2_ctl.get_control_cur_value(cam_path, control)}" + if 'flags' in data: + line += f" flags={data['flags']}" + message += logger.indentation + line + '\n' + if 'menu' in data: + for value, name in data['menu'].items(): + message += logger.indentation*2 + f"{value}: {name}\n" + message += '\n' + return message[:-1] diff --git a/pylibs/v4l2/utils.py b/pylibs/v4l2/utils.py index 0ab2e52e..16957d84 100644 --- a/pylibs/v4l2/utils.py +++ b/pylibs/v4l2/utils.py @@ -154,7 +154,6 @@ def ctl_to_parsed_dict(dev_ctl: raw.v4l2_ext_control) -> dict: values = {} cur_sec = '' for control, cur_ctl in dev_ctl.items(): - # cur_ctl = dev_ctl[control] if not cur_ctl['values']: cur_sec = control values[cur_sec] = {} diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py index 9f590c0e..4c69f8fc 100644 --- a/pylibs/v4l2_control.py +++ b/pylibs/v4l2_control.py @@ -14,10 +14,14 @@ def get_uvc_v4l2ctrls(cam_path: str) -> str: v4l2ctrls = utils.execute_shell_command(command) return v4l2ctrls -def set_v4l2_ctrl(cam_path: str, ctrl: str) -> str: - command = f'v4l2-ctl -d {cam_path} -c {ctrl}' - v4l2ctrl = utils.execute_shell_command(command) - return v4l2ctrl +def set_v4l2_ctrl(cam_path: str, ctrl: str, prefix='') -> str: + try: + c = ctrl.split('=')[0].strip().lower() + v = int(ctrl.split('=')[1].strip()) + if not v4l2_ctl.set_control(cam_path, c, v): + raise ValueError + except (ValueError, IndexError): + logger.log_quiet(f"Failed to set parameter: '{ctrl.strip()}'", prefix) def set_v4l2ctrls(section: str, cam_path: str, ctrls: list[str] = None) -> str: prefix = "V4L2 Control: " @@ -26,24 +30,19 @@ def set_v4l2ctrls(section: str, cam_path: str, ctrls: list[str] = None) -> str: return logger.log_quiet(f"Device: {section}", prefix) logger.log_quiet(f"Options: {', '.join(ctrls)}", prefix) - avail_ctrls = get_uvc_v4l2ctrls(cam_path) + avail_ctrls = utils.get_v4l2_ctl_str(cam_path) for ctrl in ctrls: - if ctrl.split('=')[0].strip().lower() not in avail_ctrls: + c = ctrl.split('=')[0].strip().lower() + if c not in avail_ctrls: logger.log_quiet( f"Parameter '{ctrl.strip()}' not available for '{cam_path}'. Skipped.", prefix ) continue - v4l2ctrl = set_v4l2_ctrl(cam_path, ctrl.strip()) - if not v4l2ctrl: - logger.log_quiet(f"Failed to set parameter: '{ctrl.strip()}'", prefix) - logger.log_multiline(get_uvc_v4l2ctrls(cam_path), logger.log_debug) + set_v4l2_ctrl(cam_path, ctrl, prefix) + logger.log_multiline(utils.get_v4l2_ctl_str(cam_path), logger.log_debug) def get_cur_v4l2_value(cam_path: str, ctrl: str) -> str: - # command = f'v4l2-ctl -d {cam_path} -C {ctrl}' - # value = utils.execute_shell_command(command) - # if value: - # return value.split(':')[1].strip() return v4l2_ctl.get_control_cur_value(cam_path, ctrl) def brokenfocus(cam_path: str, focus_absolute_conf: str) -> str: From d448568067a870021e9fc6944092b593fa9b64b5 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 30 Mar 2024 15:00:04 +0100 Subject: [PATCH 107/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/v4l2_control.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py index 4c69f8fc..13180069 100644 --- a/pylibs/v4l2_control.py +++ b/pylibs/v4l2_control.py @@ -2,18 +2,6 @@ from .v4l2 import ctl as v4l2_ctl -def get_uvc_formats(cam_path: str) -> str: - command = f'v4l2-ctl -d {cam_path} --list-formats-ext' - formats = utils.execute_shell_command(command) - # Remove first 3 lines - formats = '\n'.join(formats.split('\n')[3:]) - return formats - -def get_uvc_v4l2ctrls(cam_path: str) -> str: - command = f'v4l2-ctl -d {cam_path} --list-ctrls-menus' - v4l2ctrls = utils.execute_shell_command(command) - return v4l2ctrls - def set_v4l2_ctrl(cam_path: str, ctrl: str, prefix='') -> str: try: c = ctrl.split('=')[0].strip().lower() From 644495a94e12322ddcab4c385e73c298145ddc7a Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 1 Apr 2024 00:09:34 +0200 Subject: [PATCH 108/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/v4l2/ctl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index 9e6c3ceb..841d5ac3 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -140,7 +140,7 @@ def set_control(device_path: str, control: str, value: int) -> bool: if utils.ioctl_safe(fd, raw.VIDIOC_S_CTRL, ctrl) != -1: success = True os.close(fd) - return True + return success except FileNotFoundError: pass return success From a8ac087325951e611db58fd70418a86edfd7c050 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 3 Apr 2024 14:30:37 +0200 Subject: [PATCH 109/129] Chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/camera/__init__.py | 5 + pylibs/camera/camera.py | 20 ++++ pylibs/camera/camera_manager.py | 22 ++++ pylibs/camera/types/legacy.py | 10 ++ pylibs/camera/types/libcamera.py | 83 +++++++++++++ pylibs/camera/types/uvc.py | 82 +++++++++++++ pylibs/components/streamer/camera-streamer.py | 11 +- pylibs/components/streamer/ustreamer.py | 62 ++++++++-- pylibs/hwhandler.py | 111 ------------------ pylibs/logging_helper.py | 86 ++++++-------- pylibs/utils.py | 24 +--- pylibs/v4l2/__init__.py | 1 + pylibs/v4l2/ctl.py | 46 +++++++- pylibs/v4l2_control.py | 51 -------- 14 files changed, 365 insertions(+), 249 deletions(-) create mode 100644 pylibs/camera/__init__.py create mode 100644 pylibs/camera/camera.py create mode 100644 pylibs/camera/camera_manager.py create mode 100644 pylibs/camera/types/legacy.py create mode 100644 pylibs/camera/types/libcamera.py create mode 100644 pylibs/camera/types/uvc.py delete mode 100644 pylibs/hwhandler.py delete mode 100644 pylibs/v4l2_control.py diff --git a/pylibs/camera/__init__.py b/pylibs/camera/__init__.py new file mode 100644 index 00000000..b7657419 --- /dev/null +++ b/pylibs/camera/__init__.py @@ -0,0 +1,5 @@ +from .types.uvc import UVCCamera as UVC +from .types.legacy import LegacyCamera as Legacy +from .types.libcamera import LibcameraCamera as Libcamera +from .camera import Camera +from . import camera_manager diff --git a/pylibs/camera/camera.py b/pylibs/camera/camera.py new file mode 100644 index 00000000..f0dd1f61 --- /dev/null +++ b/pylibs/camera/camera.py @@ -0,0 +1,20 @@ +import os + +class Camera: + def __init__(self, path: str) -> None: + self.path = path + self.control_values = {} + self.formats = {} + + def path_equals(self, path: str) -> bool: + return self.path == os.path.realpath(path) + + def get_formats_string(self) -> str: + return '' + + def get_controls_string(self) -> str: + return '' + + @staticmethod + def init_camera_type() -> list: + pass diff --git a/pylibs/camera/camera_manager.py b/pylibs/camera/camera_manager.py new file mode 100644 index 00000000..8365e28f --- /dev/null +++ b/pylibs/camera/camera_manager.py @@ -0,0 +1,22 @@ +from .camera import Camera + +def get_all_cameras() -> list: + global cameras + try: + cameras + except NameError: + cameras = [] + return cameras + +def get_cam_by_path(path: str) -> Camera: + global cameras + for camera in get_all_cameras(): + if camera.path_equals(path): + return camera + return None + +def init_camera_type(obj: Camera) -> list: + global cameras + cams = obj.init_camera_type() + get_all_cameras().extend(cams) + return cams diff --git a/pylibs/camera/types/legacy.py b/pylibs/camera/types/legacy.py new file mode 100644 index 00000000..772e31eb --- /dev/null +++ b/pylibs/camera/types/legacy.py @@ -0,0 +1,10 @@ +from . import uvc +from ... import v4l2 + +class LegacyCamera(uvc.UVCCamera): + @staticmethod + def init_camera_type() -> list: + legacy_path = v4l2.ctl.get_dev_path_by_name('mmal') + if not legacy_path: + return [] + return [LegacyCamera(legacy_path)] diff --git a/pylibs/camera/types/libcamera.py b/pylibs/camera/types/libcamera.py new file mode 100644 index 00000000..4ca8eb7b --- /dev/null +++ b/pylibs/camera/types/libcamera.py @@ -0,0 +1,83 @@ +import shutil, re + +from ... import utils +from .. import camera + +class LibcameraCamera(camera.Camera): + def __init__(self, path) -> None: + self.path = path + self.control_values = self._get_controls() + self.formats = [] + + def _get_controls(self) -> str: + ctrls = {} + try: + from libcamera import CameraManager, Rectangle + + libcam_cm = CameraManager.singleton() + for cam in libcam_cm.cameras: + if cam.id != self.path: + continue + for k, v in cam.controls.items(): + def rectangle_to_tuple(rectangle): + return (rectangle.x, rectangle.y, rectangle.width, rectangle.height) + + if isinstance(v.min, Rectangle): + ctrls[k.name] = { + 'min': rectangle_to_tuple(v.min), + 'max': rectangle_to_tuple(v.max), + 'default': rectangle_to_tuple(v.default) + } + else: + ctrls[k.name] = { + 'min': v.min, + 'max': v.max, + 'default': v.default + } + except ImportError: + pass + return ctrls + + def _get_formats(self, libcamera_output: str) -> list: + resolutions = re.findall( + rf"{self.path}.*?:.*?: (.*?)(?=\n\n|\n *')", + libcamera_output, flags=re.DOTALL + ) + res = [] + if resolutions: + res = [r.strip() for r in resolutions[0].split('\n')] + return res + + def get_formats_string(self) -> str: + message = '' + for res in self.formats: + message += f"{res}\n" + return message[:-1] + + def get_controls_string(self) -> str: + if not self.control_values: + return "apt package 'python3-libcamera' is not installed! " \ + "Make sure to install it to log the controls!" + message = '' + for name, value in self.control_values.items(): + min, max, default = value.values() + str_first = f"{name} ({self.get_type_str(min)})" + str_indent = (30 - len(str_first)) * ' ' + ': ' + str_second = f"min={min} max={max} default={default}" + message += str_first + str_indent + str_second + '\n' + return message.strip() + + def get_type_str(self, obj) -> str: + return str(type(obj)).split('\'')[1] + + @staticmethod + def init_camera_type() -> list: + cmd = shutil.which('libcamera-hello') + if not cmd: + return {} + libcam_cmd =f'{cmd} --list-cameras' + libcam = utils.execute_shell_command(libcam_cmd, strip=False) + cams = [LibcameraCamera(path) for path in re.findall(r'\((/base.*?)\)', libcam)] + for cam in cams: + cam.formats = cam._get_formats(libcam) + return cams diff --git a/pylibs/camera/types/uvc.py b/pylibs/camera/types/uvc.py new file mode 100644 index 00000000..a4cd2e1d --- /dev/null +++ b/pylibs/camera/types/uvc.py @@ -0,0 +1,82 @@ +import os + +from .. import camera +from ... import v4l2, logger + +class UVCCamera(camera.Camera): + def __init__(self, path: str) -> None: + if path.startswith('/dev/video'): + self.path = path + self.path_by_id = None + else: + self.path = os.path.realpath(path) + self.path_by_id = path + self.query_controls = v4l2.ctl.get_query_controls(self.path) + + self.control_values = {} + cur_sec = '' + for name, qc in self.query_controls.items(): + parsed_qc = v4l2.ctl.parse_qc_of_path(self.path, qc) + if not parsed_qc: + cur_sec = name + self.control_values[cur_sec] = {} + continue + self.control_values[cur_sec][name] = v4l2.ctl.parse_qc_of_path(self.path, qc) + self.formats = v4l2.ctl.get_formats(self.path) + + + def get_formats_string(self) -> str: + message = '' + indent = ' '*8 + for fmt, data in self.formats.items(): + message += f"{fmt}:\n" + for res, fps_list in data.items(): + message += f"{indent}{res}\n" + for fps in fps_list: + message += f"{indent*2}{fps}\n" + return message[:-1] + + def has_mjpg_hw_encoder(self) -> bool: + for fmt in self.formats.keys(): + if 'Motion-JPEG' in fmt: + return True + return False + + def get_controls_string(self) -> str: + message = '' + for section, controls in self.control_values.items(): + message += f"{section}:\n" + for control, data in controls.items(): + line = f"{control} ({data['type']})" + line += (35 - len(line)) * ' ' + ':' + if data['type'] in ('int'): + line += f" min={data['min']} max={data['max']} step={data['step']}" + line += f" default={data['default']}" + line += f" value={self.get_current_control_value(control)}" + if 'flags' in data: + line += f" flags={data['flags']}" + message += logger.indentation + line + '\n' + if 'menu' in data: + for value, name in data['menu'].items(): + message += logger.indentation*2 + f"{value}: {name}\n" + message += '\n' + return message[:-1] + + def set_control(self, control: str, value: int) -> bool: + return v4l2.ctl.set_control_with_qc(self.path, self.query_controls[control], value) + + def get_current_control_value(self, control: str) -> int: + return v4l2.ctl.get_control_cur_value_with_qc(self.path, self.query_controls[control]) + + @staticmethod + def init_camera_type() -> list: + def get_avail_uvc(path): + avail_uvc = [] + for file in os.listdir(path): + by_id = os.path.join(path, file) + if os.path.islink(by_id) and by_id.endswith("index0"): + avail_uvc.append((by_id, os.path.realpath(by_id))) + return avail_uvc + avail_by_id = get_avail_uvc('/dev/v4l/by-id/') + by_path_path = get_avail_uvc('/dev/v4l/by-path/') + return [UVCCamera(by_id_path) for by_id_path,_ in avail_by_id] diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index 0720a920..dba78544 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -2,7 +2,7 @@ from .streamer import Streamer from ...parameter import Parameter -from ... import logger, utils, hwhandler +from ... import logger, utils, camera class Camera_Streamer(Streamer): keyword = 'camera-streamer' @@ -29,6 +29,7 @@ async def execute(self, lock: asyncio.Lock): fps = self.parameters['max_fps'].value device = self.parameters['device'].value + cam = camera.camera_manager.get_cam_by_path(device) streamer_args = [ '--camera-path=' + device, @@ -51,16 +52,18 @@ async def execute(self, lock: asyncio.Lock): for ctrl in v4l2ctl.split(','): streamer_args += [f'--camera-options={ctrl.strip()}'] - if device.startswith('/base') and 'i2c' in device: + # if device.startswith('/base') and 'i2c' in device: + if isinstance(cam, camera.Libcamera): streamer_args += [ '--camera-type=libcamera', '--camera-format=YUYV' ] - elif device.startswith('/dev/video') or device.startswith('/dev/v4l'): + # elif device.startswith('/dev/video') or device.startswith('/dev/v4l'): + elif isinstance(cam, (camera.UVC, camera.Legacy)): streamer_args += [ '--camera-type=v4l2' ] - if hwhandler.has_device_mjpg_hw(device): + if cam.has_mjpg_hw_encoder(): streamer_args += [ '--camera-format=MJPEG' ] diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index 33d6ff83..fbde7382 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -2,7 +2,7 @@ import asyncio from .streamer import Streamer -from ... import logger, utils, hwhandler, v4l2_control as v4l2_ctl +from ... import logger, utils, camera class Ustreamer(Streamer): keyword = 'ustreamer' @@ -23,6 +23,7 @@ async def execute(self, lock: asyncio.Lock): res = self.parameters['resolution'].value fps = self.parameters['max_fps'].value device = self.parameters['device'].value + cam = camera.camera_manager.get_cam_by_path(device) streamer_args = [ '--host', host, @@ -34,19 +35,19 @@ async def execute(self, lock: asyncio.Lock): '--static', '"ustreamer-www"' ] - if hwhandler.is_device_legacy(device): + if self._is_device_legacy(): streamer_args += [ '--format', 'MJPEG', '--device-timeout', '5', '--buffers', '3' ] - v4l2_ctl.blockyfix(device) + self._blockyfix() else: streamer_args += [ '--device', device, '--device-timeout', '2' ] - if hwhandler.has_device_mjpg_hw(device): + if cam.has_mjpg_hw_encoder(): streamer_args += [ '--format', 'MJPEG', '--encoder', 'HW' @@ -54,7 +55,7 @@ async def execute(self, lock: asyncio.Lock): v4l2ctl = self.parameters['v4l2ctl'].value if v4l2ctl: - v4l2_ctl.set_v4l2ctrls(f'[cam {self.name}]', device, v4l2ctl.split(',')) + self._set_v4l2ctrls(v4l2ctl.split(',')) # custom flags streamer_args += self.parameters['custom_flags'].value.split() @@ -68,7 +69,7 @@ async def execute(self, lock: asyncio.Lock): info_log_pre=log_pre, info_log_func=logger.log_debug, error_log_pre=log_pre, - error_log_func=self.custom_log + error_log_func=self._custom_log ) if lock.locked(): lock.release() @@ -77,18 +78,63 @@ async def execute(self, lock: asyncio.Lock): for ctl in v4l2ctl.split(','): if 'focus_absolute' in ctl: focus_absolute = ctl.split('=')[1].strip() - v4l2_ctl.brokenfocus(device, focus_absolute) + self._brokenfocus(focus_absolute) break return process - def custom_log(self, msg: str): + def _custom_log(self, msg: str): if msg.endswith('==='): msg = msg[:-28] else: msg = re.sub(r'-- (.*?) \[.*?\] --', r'\1', msg) logger.log_debug(msg) + def _set_v4l2_ctrl(self, cam: camera.UVC, ctrl: str, prefix='') -> str: + try: + c = ctrl.split('=')[0].strip().lower() + v = int(ctrl.split('=')[1].strip()) + if not cam.set_control(c, v): + raise ValueError + except (ValueError, IndexError): + logger.log_quiet(f"Failed to set parameter: '{ctrl.strip()}'", prefix) + + def _set_v4l2ctrls(self, ctrls: list[str] = None) -> str: + section = f'[cam {self.name}]' + prefix = "V4L2 Control: " + if not ctrls: + logger.log_quiet(f"No parameters set for {section}. Skipped.", prefix) + return + logger.log_quiet(f"Device: {section}", prefix) + logger.log_quiet(f"Options: {', '.join(ctrls)}", prefix) + cam_path = self.parameters['device'].value + cam = camera.camera_manager.get_cam_by_path(cam_path) + avail_ctrls = cam.get_controls_string() + for ctrl in ctrls: + c = ctrl.split('=')[0].strip().lower() + if c not in avail_ctrls: + logger.log_quiet( + f"Parameter '{ctrl.strip()}' not available for '{cam_path}'. Skipped.", + prefix + ) + continue + self._set_v4l2_ctrl(cam, ctrl, prefix) + # Repulls the string to print current values + logger.log_multiline(cam.get_controls_string(), logger.log_debug, 'DEBUG: v4l2ctl: ') + + def _brokenfocus(self, focus_absolute_conf: str) -> str: + cam = camera.camera_manager.get_cam_by_path(self.parameters['device'].value) + cur_val = cam.get_current_control_value('focus_absolute') + if cur_val and cur_val != focus_absolute_conf: + logger.log_warning(f"Detected 'brokenfocus' device.") + logger.log_info(f"Try to set to configured Value.") + self.set_v4l2_ctrl(cam, f'focus_absolute={focus_absolute_conf}') + logger.log_debug(f"Value is now: {cam.get_current_control_value('focus_absolute')}") + + def _is_device_legacy(self) -> bool: + cam = camera.camera_manager.get_cam_by_path(self.parameters['device'].value) + return isinstance(cam, camera.Legacy) + def load_component(name: str): return Ustreamer(name) diff --git a/pylibs/hwhandler.py b/pylibs/hwhandler.py deleted file mode 100644 index b613f52a..00000000 --- a/pylibs/hwhandler.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -import shutil -import re - -from . import utils -from .v4l2 import ctl as v4l2_ctl, utils as v4l2_utils - -avail_cams = { - 'uvc': {}, - 'libcamera': {}, - 'legacy': {} -} - -def get_avail_uvc_dev() -> dict: - uvc_path = '/dev/v4l/by-id/' - avail_uvc = [] - for file in os.listdir(uvc_path): - path = os.path.join(uvc_path, file) - if os.path.islink(path) and path.endswith("index0"): - avail_uvc.append(path) - cams = {} - for cam_path in avail_uvc: - cams[cam_path] = { - 'realpath': os.path.realpath(cam_path), - 'formats': v4l2_ctl.get_formats(cam_path), - 'v4l2ctrls': v4l2_ctl.get_dev_ctl_parsed_dict(cam_path) - } - avail_cams['uvc'].update(cams) - return cams - -def has_device_mjpg_hw(cam_path: str) -> bool: - global avail_cams - for key in v4l2_ctl.get_formats(cam_path).keys(): - if 'Motion-JPEG, compressed' in key: - return True - return False - -def get_avail_libcamera() -> dict: - cmd = shutil.which('libcamera-hello') - if not cmd: - return {} - libcam_cmd =f'{cmd} --list-cameras' - libcam = utils.execute_shell_command(libcam_cmd, strip=False) - libcams = {} - if 'Available' in libcam: - for path in get_libcamera_paths(libcam): - libcams[path] = { - 'resolutions': get_libcamera_resolutions(libcam, path), - 'controls': get_libcamera_controls(path) - } - avail_cams['libcamera'].update(libcams) - return libcams - -def get_libcamera_paths(libcamera_output: str) -> list: - return re.findall(r'\((/base.*?)\)', libcamera_output) - -def get_libcamera_resolutions(libcamera_output: str, camera_path: str) -> list: - # Get the resolution list for only one mode - resolutions = re.findall( - rf"{camera_path}.*?:.*?: (.*?)(?=\n\n|\n *')", - libcamera_output, flags=re.DOTALL - ) - res = [] - if resolutions: - res = [r.strip() for r in resolutions[0].split('\n')] - return res - -def get_libcamera_controls(camera_path: str) -> list: - ctrls = {} - try: - from libcamera import CameraManager, Rectangle - - libcam_cm = CameraManager.singleton() - for cam in libcam_cm.cameras: - if cam.id != camera_path: - continue - for k, v in cam.controls.items(): - def rectangle_to_tuple(rectangle): - return (rectangle.x, rectangle.y, rectangle.width, rectangle.height) - - if isinstance(v.min, Rectangle): - ctrls[k.name] = { - 'min': rectangle_to_tuple(v.min), - 'max': rectangle_to_tuple(v.max), - 'default': rectangle_to_tuple(v.default) - } - else: - ctrls[k.name] = { - 'min': v.min, - 'max': v.max, - 'default': v.default - } - except ImportError: - pass - return ctrls - -def get_avail_legacy() -> dict: - legacy = {} - legacy_path = v4l2_ctl.get_dev_path_by_name('mmal') - if not legacy_path: - return legacy - legacy[legacy_path] = { - 'formats': v4l2_ctl.get_formats(legacy_path), - 'v4l2ctrls': v4l2_ctl.get_dev_ctl_parsed_dict(legacy_path) - } - avail_cams['legacy'].update(legacy) - return legacy - -def is_device_legacy(cam_path: str) -> bool: - global avail_cams - return cam_path in avail_cams['legacy'] diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index 5d6dfe66..de23d24c 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -3,7 +3,7 @@ import sys import shutil -from . import utils, logger, hwhandler +from . import utils, logger, camera def log_initial(): logger.log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') @@ -75,10 +75,10 @@ def log_host_info(): def log_cams(): logger.log_info("Detect available Devices") - libcamera = hwhandler.get_avail_libcamera() - uvc = hwhandler.get_avail_uvc_dev() - legacy = hwhandler.get_avail_legacy() - total = len(libcamera.keys()) + len(legacy.keys()) + len(uvc.keys()) + libcamera = camera.camera_manager.init_camera_type(camera.Libcamera) + uvc = camera.camera_manager.init_camera_type(camera.UVC) + legacy = camera.camera_manager.init_camera_type(camera.Legacy) + total = len(libcamera) + len(legacy) + len(uvc) if total == 0: logger.log_error("No usable Devices Found. Stopping ") @@ -86,55 +86,41 @@ def log_cams(): logger.log_info(f"Found {total} total available Device(s)") if libcamera: - logger.log_info(f"Found {len(libcamera.keys())} available 'libcamera' device(s)") - for path, properties in libcamera.items(): - log_libcamera_dev(path, properties) + logger.log_info(f"Found {len(libcamera)} available 'libcamera' device(s)") + for cam in libcamera: + log_libcam(cam) if legacy: - for path, properties in legacy.items(): - logger.log_info(f"Detected 'Raspicam' Device -> {path}") - log_uvc_dev(path, properties) + for cam in legacy: + log_legacy_cam(cam) if uvc: - logger.log_info(f"Found {len(uvc.keys())} available v4l2 (UVC) camera(s)") - for path, properties in uvc.items(): - logger.log_info(f"{path} -> {properties['realpath']}", '') - log_uvc_dev(path, properties) + logger.log_info(f"Found {len(uvc)} available v4l2 (UVC) camera(s)") + for cam in uvc: + log_uvc_cam(cam) -def log_libcamera_dev(path: str, properties: dict) -> str: - logger.log_info(f"Detected 'libcamera' device -> {path}") +def log_libcam(cam: camera.Libcamera) -> None: + logger.log_info(f"Detected 'libcamera' device -> {cam.path}") logger.log_info(f"Advertised Formats:", '') - resolutions = properties['resolutions'] - for res in resolutions: - logger.log_info(f"{res}", logger.indentation) + log_camera_formats(cam) logger.log_info(f"Supported Controls:", '') - controls = properties['controls'] - if controls: - for name, value in controls.items(): - min, max, default = value.values() - str_first = f"{name} ({get_type_str(min)})" - str_second = f"min={min} max={max} default={default}" - str_indent = (30 - len(str_first)) * ' ' + ': ' - logger.log_info(str_first + str_indent + str_second, logger.indentation) - else: - logger.log_info("apt package 'python3-libcamera' is not installed! " - "Make sure to install it to log the controls!", logger.indentation) - -def get_type_str(obj) -> str: - return str(type(obj)).split('\'')[1] - -def log_uvc_dev(path: str, properties: dict) -> str: - log_uvc_formats(properties) - log_uvc_v4l2ctrls(path) - -def log_uvc_formats(properties: dict) -> None: + log_camera_ctrls(cam) + +def log_uvc_cam(cam: camera.UVC) -> None: + logger.log_info(f"{cam.path_by_id} -> {cam.path}", '') + logger.log_info(f"Supported Formats:", '') + log_camera_formats(cam) + logger.log_info(f"Supported Controls:", '') + log_camera_ctrls(cam) + +def log_legacy_cam(camera_path: str) -> None: + cam: camera.UVC = camera.camera_manager.get_cam_by_path(camera_path) + logger.log_info(f"Detected 'Raspicam' Device -> {camera_path}") logger.log_info(f"Supported Formats:", '') - indent = ' '*8 - for fmt, data in properties['formats'].items(): - logger.log_info(f"{fmt}:", logger.indentation) - for res, fps_list in data.items(): - logger.log_info(f"{res}", logger.indentation+indent) - for fps in fps_list: - logger.log_info(f"{fps}", logger.indentation+indent*2) - -def log_uvc_v4l2ctrls(device_path: str) -> None: + log_camera_formats(cam) logger.log_info(f"Supported Controls:", '') - logger.log_multiline(utils.get_v4l2_ctl_str(device_path), logger.log_info, logger.indentation) + log_camera_ctrls(cam) + +def log_camera_formats(cam: camera.Camera) -> None: + logger.log_multiline(cam.get_formats_string(), logger.log_info, logger.indentation) + +def log_camera_ctrls(cam: camera.Camera) -> None: + logger.log_multiline(cam.get_controls_string(), logger.log_info, logger.indentation) diff --git a/pylibs/utils.py b/pylibs/utils.py index 5e27b11d..d5c8f45e 100644 --- a/pylibs/utils.py +++ b/pylibs/utils.py @@ -4,8 +4,7 @@ import shutil import os -from . import logger -from .v4l2 import ctl as v4l2_ctl +from . import logger, v4l2 # Dynamically import component # Requires module to have a load_component() function, @@ -101,24 +100,3 @@ def grep(path: str, search: str) -> str: except FileNotFoundError: logger.log_error(f"File '{path}' not found!") return '' - -def get_v4l2_ctl_str(cam_path: str) -> str: - ctrls = v4l2_ctl.get_dev_ctl_parsed_dict(cam_path) - message = '' - for section, controls in ctrls.items(): - message += f"{section}:\n" - for control, data in controls.items(): - line = f"{control} ({data['type']})" - line += (35 - len(line)) * ' ' + ':' - if data['type'] in ('int'): - line += f" min={data['min']} max={data['max']} step={data['step']}" - line += f" default={data['default']}" - line += f" value={v4l2_ctl.get_control_cur_value(cam_path, control)}" - if 'flags' in data: - line += f" flags={data['flags']}" - message += logger.indentation + line + '\n' - if 'menu' in data: - for value, name in data['menu'].items(): - message += logger.indentation*2 + f"{value}: {name}\n" - message += '\n' - return message[:-1] diff --git a/pylibs/v4l2/__init__.py b/pylibs/v4l2/__init__.py index e69de29b..3da1e45b 100644 --- a/pylibs/v4l2/__init__.py +++ b/pylibs/v4l2/__init__.py @@ -0,0 +1 @@ +from . import constants, ctl, ioctl_macros, raw, utils diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index 841d5ac3..80f44a1e 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -44,6 +44,18 @@ def parse_qc(fd: int, qc: raw.v4l2_query_ext_ctrl) -> dict: controls['menu'][menu.index] = menu.value return controls +def parse_qc_of_path(device_path: str, qc: raw.v4l2_query_ext_ctrl) -> dict: + """ + Parses the query control to an easy to use dictionary + """ + try: + fd = os.open(device_path, os.O_RDWR) + controls = parse_qc(fd, qc) + os.close(fd) + return controls + except FileNotFoundError: + return {} + def init_device(device_path: str) -> bool: """ Initialize a given device @@ -68,6 +80,27 @@ def init_device(device_path: str) -> bool: except FileNotFoundError: return False +def get_query_controls(device_path: str) -> dict[str, raw.v4l2_ext_control]: + """ + Initialize a given device + """ + try: + fd = os.open(device_path, os.O_RDWR) + next_fl = constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND + qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) + query_controls: dict[str, raw.v4l2_query_ext_ctrl] = {} + for qc in utils.ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): + if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + name = qc.name.decode() + else: + name = utils.name2var(qc.name.decode()) + query_controls[name] = copy.deepcopy(qc) + qc.id |= next_fl + os.close(fd) + return query_controls + except FileNotFoundError: + return {} + def get_dev_ctl(device_path: str) -> dict: if device_path not in dev_ctls: if not init_device(device_path): @@ -112,13 +145,19 @@ def get_camera_capabilities(device_path: str) -> dict: return {} def get_control_cur_value(device_path: str, control: str) -> int: + """ + Get the current value of a control of a given device + """ + qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][utils.name2var(control)]['qc'] + return get_control_cur_value_with_qc(device_path, qc, control) + +def get_control_cur_value_with_qc(device_path: str, qc: raw.v4l2_query_ext_ctrl) -> int: """ Get the current value of a control of a given device """ try: fd = os.open(device_path, os.O_RDWR) ctrl = raw.v4l2_control() - qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][utils.name2var(control)]['qc'] ctrl.id = qc.id utils.ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl) os.close(fd) @@ -130,11 +169,14 @@ def set_control(device_path: str, control: str, value: int) -> bool: """ Set the value of a control of a given device """ + qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][control]['qc'] + return set_control_with_qc(device_path, qc, value) + +def set_control_with_qc(device_path: str, qc: raw.v4l2_query_ext_ctrl, value: int) -> bool: success = False try: fd = os.open(device_path, os.O_RDWR) ctrl = raw.v4l2_control() - qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][control]['qc'] ctrl.id = qc.id ctrl.value = value if utils.ioctl_safe(fd, raw.VIDIOC_S_CTRL, ctrl) != -1: diff --git a/pylibs/v4l2_control.py b/pylibs/v4l2_control.py deleted file mode 100644 index 13180069..00000000 --- a/pylibs/v4l2_control.py +++ /dev/null @@ -1,51 +0,0 @@ -from . import logger, utils - -from .v4l2 import ctl as v4l2_ctl - -def set_v4l2_ctrl(cam_path: str, ctrl: str, prefix='') -> str: - try: - c = ctrl.split('=')[0].strip().lower() - v = int(ctrl.split('=')[1].strip()) - if not v4l2_ctl.set_control(cam_path, c, v): - raise ValueError - except (ValueError, IndexError): - logger.log_quiet(f"Failed to set parameter: '{ctrl.strip()}'", prefix) - -def set_v4l2ctrls(section: str, cam_path: str, ctrls: list[str] = None) -> str: - prefix = "V4L2 Control: " - if not ctrls: - logger.log_quiet(f"No parameters set for {section}. Skipped.", prefix) - return - logger.log_quiet(f"Device: {section}", prefix) - logger.log_quiet(f"Options: {', '.join(ctrls)}", prefix) - avail_ctrls = utils.get_v4l2_ctl_str(cam_path) - for ctrl in ctrls: - c = ctrl.split('=')[0].strip().lower() - if c not in avail_ctrls: - logger.log_quiet( - f"Parameter '{ctrl.strip()}' not available for '{cam_path}'. Skipped.", - prefix - ) - continue - set_v4l2_ctrl(cam_path, ctrl, prefix) - logger.log_multiline(utils.get_v4l2_ctl_str(cam_path), logger.log_debug) - -def get_cur_v4l2_value(cam_path: str, ctrl: str) -> str: - return v4l2_ctl.get_control_cur_value(cam_path, ctrl) - -def brokenfocus(cam_path: str, focus_absolute_conf: str) -> str: - cur_val = get_cur_v4l2_value(cam_path, 'focus_absolute') - if cur_val and cur_val != focus_absolute_conf: - logger.log_warning(f"Detected 'brokenfocus' device.") - logger.log_info(f"Try to set to configured Value.") - set_v4l2_ctrl(cam_path, f'focus_absolute={focus_absolute_conf}') - logger.log_debug(f"Value is now: {get_cur_v4l2_value(cam_path, 'focus_absolute')}") - -# This function is to set bitrate on raspicams. -# If raspicams set to variable bitrate, they tend to show -# a "block-like" view after reboots -# To prevent that blockyfix should apply constant bitrate befor start of ustreamer -# See https://github.com/mainsail-crew/crowsnest/issues/33 -def blockyfix(device: str): - set_v4l2_ctrl(device, 'video_bitrate_mode=1') - set_v4l2_ctrl(device, 'video_bitrate=15000000') From 390f32cec7bf409e875bc6647468cb3b63ddeb79 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 3 Apr 2024 15:29:28 +0200 Subject: [PATCH 110/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/camera/__init__.py | 6 +++--- pylibs/camera/types/legacy.py | 4 ++-- pylibs/camera/types/libcamera.py | 4 ++-- pylibs/camera/types/uvc.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pylibs/camera/__init__.py b/pylibs/camera/__init__.py index b7657419..1ec2cb04 100644 --- a/pylibs/camera/__init__.py +++ b/pylibs/camera/__init__.py @@ -1,5 +1,5 @@ -from .types.uvc import UVCCamera as UVC -from .types.legacy import LegacyCamera as Legacy -from .types.libcamera import LibcameraCamera as Libcamera +from .types.uvc import UVC +from .types.legacy import Legacy +from .types.libcamera import Libcamera from .camera import Camera from . import camera_manager diff --git a/pylibs/camera/types/legacy.py b/pylibs/camera/types/legacy.py index 772e31eb..854c76a0 100644 --- a/pylibs/camera/types/legacy.py +++ b/pylibs/camera/types/legacy.py @@ -1,10 +1,10 @@ from . import uvc from ... import v4l2 -class LegacyCamera(uvc.UVCCamera): +class Legacy(uvc.UVC): @staticmethod def init_camera_type() -> list: legacy_path = v4l2.ctl.get_dev_path_by_name('mmal') if not legacy_path: return [] - return [LegacyCamera(legacy_path)] + return [Legacy(legacy_path)] diff --git a/pylibs/camera/types/libcamera.py b/pylibs/camera/types/libcamera.py index 4ca8eb7b..55813c5f 100644 --- a/pylibs/camera/types/libcamera.py +++ b/pylibs/camera/types/libcamera.py @@ -3,7 +3,7 @@ from ... import utils from .. import camera -class LibcameraCamera(camera.Camera): +class Libcamera(camera.Camera): def __init__(self, path) -> None: self.path = path self.control_values = self._get_controls() @@ -77,7 +77,7 @@ def init_camera_type() -> list: return {} libcam_cmd =f'{cmd} --list-cameras' libcam = utils.execute_shell_command(libcam_cmd, strip=False) - cams = [LibcameraCamera(path) for path in re.findall(r'\((/base.*?)\)', libcam)] + cams = [Libcamera(path) for path in re.findall(r'\((/base.*?)\)', libcam)] for cam in cams: cam.formats = cam._get_formats(libcam) return cams diff --git a/pylibs/camera/types/uvc.py b/pylibs/camera/types/uvc.py index a4cd2e1d..affe61d9 100644 --- a/pylibs/camera/types/uvc.py +++ b/pylibs/camera/types/uvc.py @@ -3,7 +3,7 @@ from .. import camera from ... import v4l2, logger -class UVCCamera(camera.Camera): +class UVC(camera.Camera): def __init__(self, path: str) -> None: if path.startswith('/dev/video'): self.path = path @@ -79,4 +79,4 @@ def get_avail_uvc(path): return avail_uvc avail_by_id = get_avail_uvc('/dev/v4l/by-id/') by_path_path = get_avail_uvc('/dev/v4l/by-path/') - return [UVCCamera(by_id_path) for by_id_path,_ in avail_by_id] + return [UVC(by_id_path) for by_id_path,_ in avail_by_id] From 03903d0840add3b4a8bf7537ed0f829516edfb54 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 3 Apr 2024 18:25:00 +0200 Subject: [PATCH 111/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/__init__.py | 0 pylibs/components/streamer/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pylibs/components/__init__.py delete mode 100644 pylibs/components/streamer/__init__.py diff --git a/pylibs/components/__init__.py b/pylibs/components/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pylibs/components/streamer/__init__.py b/pylibs/components/streamer/__init__.py deleted file mode 100644 index e69de29b..00000000 From 364c74cff982ff6048bf17d0e53fe335ca0c316e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Wed, 3 Apr 2024 19:08:29 +0200 Subject: [PATCH 112/129] refactor: add abstract class inheritance Signed-off-by: Patrick Gehrsitz --- pylibs/camera/camera.py | 12 ++++++++---- pylibs/camera/types/legacy.py | 8 ++++---- pylibs/components/crowsnest.py | 3 +++ pylibs/components/section.py | 11 +++++++---- pylibs/components/streamer/streamer.py | 3 ++- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/pylibs/camera/camera.py b/pylibs/camera/camera.py index f0dd1f61..f3e5504f 100644 --- a/pylibs/camera/camera.py +++ b/pylibs/camera/camera.py @@ -1,6 +1,7 @@ import os +from abc import ABC, abstractmethod -class Camera: +class Camera(ABC): def __init__(self, path: str) -> None: self.path = path self.control_values = {} @@ -9,12 +10,15 @@ def __init__(self, path: str) -> None: def path_equals(self, path: str) -> bool: return self.path == os.path.realpath(path) + @abstractmethod def get_formats_string(self) -> str: - return '' + ... + @abstractmethod def get_controls_string(self) -> str: - return '' + ... @staticmethod + @abstractmethod def init_camera_type() -> list: - pass + ... diff --git a/pylibs/camera/types/legacy.py b/pylibs/camera/types/legacy.py index 854c76a0..5dcee791 100644 --- a/pylibs/camera/types/legacy.py +++ b/pylibs/camera/types/legacy.py @@ -4,7 +4,7 @@ class Legacy(uvc.UVC): @staticmethod def init_camera_type() -> list: - legacy_path = v4l2.ctl.get_dev_path_by_name('mmal') - if not legacy_path: - return [] - return [Legacy(legacy_path)] + legacy_path = v4l2.ctl.get_dev_path_by_name('mmal') + if not legacy_path: + return [] + return [Legacy(legacy_path)] diff --git a/pylibs/components/crowsnest.py b/pylibs/components/crowsnest.py index 8b0c8932..6d44d0be 100644 --- a/pylibs/components/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -28,6 +28,9 @@ def parse_config_section(self, section: SectionProxy) -> bool: self.parameters['log_level'].value = 'INFO' return True + def execute(self) -> None: + ... + def load_component(name: str, config_section: SectionProxy, *args, **kwargs): cn = Crowsnest(name) diff --git a/pylibs/components/section.py b/pylibs/components/section.py index 62834725..3b0a48cd 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -1,10 +1,11 @@ import asyncio from configparser import SectionProxy +from abc import ABC, abstractmethod from ..parameter import Parameter from .. import logger -class Section: +class Section(ABC): section_name = 'section' keyword = 'section' available_sections = {} @@ -32,10 +33,12 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> success = False return success - # Execute section specific stuff, e.g. starting cam + @abstractmethod async def execute(self, lock: asyncio.Lock): - raise NotImplementedError("If you see this, a componenent is implemented wrong!!!") - + """ + Execute section specific stuff, e.g. starting cam + """ + ... def load_component(*args, **kwargs): raise NotImplementedError("If you see this, something went wrong!!!") diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index eb180925..b784999b 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -1,5 +1,6 @@ import textwrap from configparser import SectionProxy +from abc import ABC from ..section import Section from ...parameter import Parameter @@ -15,7 +16,7 @@ def __init__(self, value:str) -> None: def __str__(self) -> str: return 'x'.join([self.width, self.height]) -class Streamer(Section): +class Streamer(Section, ABC): section_name = 'cam' binaries = {} From f3d89089c4ff0ab5fe3ba34851b40e3aea27e66b Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 13 Apr 2024 18:48:12 +0200 Subject: [PATCH 113/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 2 ++ pylibs/__init__.py | 1 + pylibs/camera/__init__.py | 2 ++ pylibs/camera/camera.py | 2 ++ pylibs/camera/camera_manager.py | 2 ++ pylibs/components/cam.py | 2 ++ pylibs/components/crowsnest.py | 2 ++ pylibs/components/section.py | 2 ++ pylibs/components/streamer/camera-streamer.py | 2 ++ pylibs/components/streamer/streamer.py | 2 ++ pylibs/components/streamer/ustreamer.py | 2 ++ pylibs/logger.py | 2 ++ pylibs/logging_helper.py | 2 ++ pylibs/parameter.py | 2 ++ pylibs/utils.py | 2 ++ pylibs/v4l2/__init__.py | 2 ++ pylibs/v4l2/constants.py | 2 ++ pylibs/v4l2/ctl.py | 4 +--- pylibs/v4l2/raw.py | 2 ++ pylibs/v4l2/utils.py | 2 ++ pylibs/watchdog.py | 2 ++ 21 files changed, 40 insertions(+), 3 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index f2ecd445..49799b8f 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import argparse import configparser import asyncio diff --git a/pylibs/__init__.py b/pylibs/__init__.py index e69de29b..a93a4bf1 100644 --- a/pylibs/__init__.py +++ b/pylibs/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/python3 diff --git a/pylibs/camera/__init__.py b/pylibs/camera/__init__.py index 1ec2cb04..13fd901a 100644 --- a/pylibs/camera/__init__.py +++ b/pylibs/camera/__init__.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + from .types.uvc import UVC from .types.legacy import Legacy from .types.libcamera import Libcamera diff --git a/pylibs/camera/camera.py b/pylibs/camera/camera.py index f3e5504f..2e0283d0 100644 --- a/pylibs/camera/camera.py +++ b/pylibs/camera/camera.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import os from abc import ABC, abstractmethod diff --git a/pylibs/camera/camera_manager.py b/pylibs/camera/camera_manager.py index 8365e28f..3827344e 100644 --- a/pylibs/camera/camera_manager.py +++ b/pylibs/camera/camera_manager.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + from .camera import Camera def get_all_cameras() -> list: diff --git a/pylibs/components/cam.py b/pylibs/components/cam.py index 51c65faf..09e88e4e 100644 --- a/pylibs/components/cam.py +++ b/pylibs/components/cam.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import asyncio import traceback diff --git a/pylibs/components/crowsnest.py b/pylibs/components/crowsnest.py index 6d44d0be..71930e89 100644 --- a/pylibs/components/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + from .section import Section from ..parameter import Parameter diff --git a/pylibs/components/section.py b/pylibs/components/section.py index 3b0a48cd..3411e859 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import asyncio from configparser import SectionProxy from abc import ABC, abstractmethod diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index dba78544..a01c0e32 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import asyncio from .streamer import Streamer diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index b784999b..2b2e037e 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import textwrap from configparser import SectionProxy from abc import ABC diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index fbde7382..670d4fd9 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import re import asyncio diff --git a/pylibs/logger.py b/pylibs/logger.py index 5a08c5dc..c09e8220 100644 --- a/pylibs/logger.py +++ b/pylibs/logger.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import logging import logging.handlers diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index de23d24c..de59b58d 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import re import os import sys diff --git a/pylibs/parameter.py b/pylibs/parameter.py index 3bd9095d..f06b5d10 100644 --- a/pylibs/parameter.py +++ b/pylibs/parameter.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + from . import logger class Parameter: diff --git a/pylibs/utils.py b/pylibs/utils.py index d5c8f45e..b84e18dc 100644 --- a/pylibs/utils.py +++ b/pylibs/utils.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import importlib import asyncio import subprocess diff --git a/pylibs/v4l2/__init__.py b/pylibs/v4l2/__init__.py index 3da1e45b..73c1041d 100644 --- a/pylibs/v4l2/__init__.py +++ b/pylibs/v4l2/__init__.py @@ -1 +1,3 @@ +#!/usr/bin/python3 + from . import constants, ctl, ioctl_macros, raw, utils diff --git a/pylibs/v4l2/constants.py b/pylibs/v4l2/constants.py index b07e30d9..c0369f91 100644 --- a/pylibs/v4l2/constants.py +++ b/pylibs/v4l2/constants.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + V4L2_CTRL_MAX_DIMS = 4 EINVAL = 22 ENOTTY = 25 diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index 80f44a1e..6c7953bd 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -1,6 +1,4 @@ -""" -Python implementation of v4l2-ctl -""" +#!/usr/bin/python3 import os import copy diff --git a/pylibs/v4l2/raw.py b/pylibs/v4l2/raw.py index 6d2bf485..de3af164 100644 --- a/pylibs/v4l2/raw.py +++ b/pylibs/v4l2/raw.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import ctypes from . import ioctl_macros diff --git a/pylibs/v4l2/utils.py b/pylibs/v4l2/utils.py index 16957d84..ed6ae4e9 100644 --- a/pylibs/v4l2/utils.py +++ b/pylibs/v4l2/utils.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import fcntl import ctypes import re diff --git a/pylibs/watchdog.py b/pylibs/watchdog.py index aa3408cd..94dd063a 100644 --- a/pylibs/watchdog.py +++ b/pylibs/watchdog.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import os import asyncio from . import logger From d6c725a8c7a7363b347b87494fcc0a9edfd1ac93 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 25 Apr 2024 16:12:18 +0200 Subject: [PATCH 114/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/camera/types/libcamera.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pylibs/camera/types/libcamera.py b/pylibs/camera/types/libcamera.py index 55813c5f..8db3328f 100644 --- a/pylibs/camera/types/libcamera.py +++ b/pylibs/camera/types/libcamera.py @@ -14,14 +14,14 @@ def _get_controls(self) -> str: try: from libcamera import CameraManager, Rectangle + def rectangle_to_tuple(rectangle): + return (rectangle.x, rectangle.y, rectangle.width, rectangle.height) + libcam_cm = CameraManager.singleton() for cam in libcam_cm.cameras: if cam.id != self.path: continue for k, v in cam.controls.items(): - def rectangle_to_tuple(rectangle): - return (rectangle.x, rectangle.y, rectangle.width, rectangle.height) - if isinstance(v.min, Rectangle): ctrls[k.name] = { 'min': rectangle_to_tuple(v.min), From 90ea43b2ca912ac69eb55be5ca31b75219011a26 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 23 May 2024 14:59:47 +0200 Subject: [PATCH 115/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/camera/camera.py | 6 +++--- pylibs/components/crowsnest.py | 3 --- pylibs/components/section.py | 5 +---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pylibs/camera/camera.py b/pylibs/camera/camera.py index 2e0283d0..ca0d4f4e 100644 --- a/pylibs/camera/camera.py +++ b/pylibs/camera/camera.py @@ -14,13 +14,13 @@ def path_equals(self, path: str) -> bool: @abstractmethod def get_formats_string(self) -> str: - ... + pass @abstractmethod def get_controls_string(self) -> str: - ... + pass @staticmethod @abstractmethod def init_camera_type() -> list: - ... + pass diff --git a/pylibs/components/crowsnest.py b/pylibs/components/crowsnest.py index 71930e89..809d5ec1 100644 --- a/pylibs/components/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -30,9 +30,6 @@ def parse_config_section(self, section: SectionProxy) -> bool: self.parameters['log_level'].value = 'INFO' return True - def execute(self) -> None: - ... - def load_component(name: str, config_section: SectionProxy, *args, **kwargs): cn = Crowsnest(name) diff --git a/pylibs/components/section.py b/pylibs/components/section.py index 3411e859..41629df7 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -37,10 +37,7 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> @abstractmethod async def execute(self, lock: asyncio.Lock): - """ - Execute section specific stuff, e.g. starting cam - """ - ... + pass def load_component(*args, **kwargs): raise NotImplementedError("If you see this, something went wrong!!!") From 172716cf95abeb9cb14117464bb3b7a9894f599c Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 23 May 2024 16:37:28 +0200 Subject: [PATCH 116/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/camera/camera.py | 2 +- pylibs/camera/types/libcamera.py | 2 +- pylibs/camera/types/uvc.py | 28 ++++++++++++++++++---------- pylibs/components/crowsnest.py | 6 +++++- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/pylibs/camera/camera.py b/pylibs/camera/camera.py index ca0d4f4e..62f9d84c 100644 --- a/pylibs/camera/camera.py +++ b/pylibs/camera/camera.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod class Camera(ABC): - def __init__(self, path: str) -> None: + def __init__(self, path: str, *args, **kwargs) -> None: self.path = path self.control_values = {} self.formats = {} diff --git a/pylibs/camera/types/libcamera.py b/pylibs/camera/types/libcamera.py index 8db3328f..31515cf4 100644 --- a/pylibs/camera/types/libcamera.py +++ b/pylibs/camera/types/libcamera.py @@ -4,7 +4,7 @@ from .. import camera class Libcamera(camera.Camera): - def __init__(self, path) -> None: + def __init__(self, path, *args, **kwargs) -> None: self.path = path self.control_values = self._get_controls() self.formats = [] diff --git a/pylibs/camera/types/uvc.py b/pylibs/camera/types/uvc.py index affe61d9..68c23114 100644 --- a/pylibs/camera/types/uvc.py +++ b/pylibs/camera/types/uvc.py @@ -4,10 +4,14 @@ from ... import v4l2, logger class UVC(camera.Camera): - def __init__(self, path: str) -> None: + def __init__(self, path: str, *args, **kwargs) -> None: + self.path_by_path = None + self.path_by_id = None if path.startswith('/dev/video'): self.path = path - self.path_by_id = None + if kwargs.get('other'): + self.path_by_path = kwargs['other'][0] + self.path_by_id = kwargs['other'][1] else: self.path = os.path.realpath(path) self.path_by_id = path @@ -70,13 +74,17 @@ def get_current_control_value(self, control: str) -> int: @staticmethod def init_camera_type() -> list: - def get_avail_uvc(path): - avail_uvc = [] - for file in os.listdir(path): - by_id = os.path.join(path, file) - if os.path.islink(by_id) and by_id.endswith("index0"): - avail_uvc.append((by_id, os.path.realpath(by_id))) + def get_avail_uvc(search_path): + avail_uvc = {} + for file in os.listdir(search_path): + dev_path = os.path.join(search_path, file) + if os.path.islink(dev_path) and dev_path.endswith("index0"): + avail_uvc[os.path.realpath(dev_path)] = dev_path return avail_uvc + avail_by_id = get_avail_uvc('/dev/v4l/by-id/') - by_path_path = get_avail_uvc('/dev/v4l/by-path/') - return [UVC(by_id_path) for by_id_path,_ in avail_by_id] + avail_by_path = dict(filter(lambda pair: 'usb' in pair[1], get_avail_uvc('/dev/v4l/by-path/').items())) + avail_uvc_cameras = {} + for dev_path, by_path in avail_by_path.items(): + avail_uvc_cameras[dev_path] = (by_path, avail_by_id.get(dev_path)) + return [UVC(dev_path, other=other_paths) for dev_path,other_paths in avail_uvc_cameras.items()] diff --git a/pylibs/components/crowsnest.py b/pylibs/components/crowsnest.py index 809d5ec1..c81eeda8 100644 --- a/pylibs/components/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -4,6 +4,7 @@ from ..parameter import Parameter from configparser import SectionProxy +import asyncio class Crowsnest(Section): def __init__(self, name: str = '') -> None: @@ -12,7 +13,7 @@ def __init__(self, name: str = '') -> None: self.parameters.update({ 'log_path': Parameter(), 'log_level': Parameter(str, 'verbose'), - 'delete_log': Parameter(bool, 'True'), + 'delete_log': Parameter(bool, 'True'), 'no_proxy': Parameter(bool, 'False') }) @@ -30,6 +31,9 @@ def parse_config_section(self, section: SectionProxy) -> bool: self.parameters['log_level'].value = 'INFO' return True + async def execute(self, lock: asyncio.Lock): + pass + def load_component(name: str, config_section: SectionProxy, *args, **kwargs): cn = Crowsnest(name) From 1c6a620102962c4234102b26b1297536f00d014d Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 24 May 2024 17:34:40 +0200 Subject: [PATCH 117/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/components/section.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pylibs/components/section.py b/pylibs/components/section.py index 41629df7..1a7e363d 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -26,7 +26,9 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> if parameter not in self.parameters: logger.log_warning(f"Parameter '{parameter}' is not supported by {self.keyword}!") continue - value = value.split('#')[0].strip() + value = value.split('\n') + value = [v.split('#')[0].strip() for v in value] + value = ' '.join(value) self.parameters[parameter].set_value(value) for parameter, value in self.parameters.items(): if value.value is None: From 65a6d0c9e54f25cb0c730bce8b5b1b3cb490f7f9 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 13 Jun 2024 17:56:07 +0200 Subject: [PATCH 118/129] chore: wip Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 71 +++++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 49799b8f..013b4305 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -46,42 +46,45 @@ async def start_sections(): signal.signal(signal.SIGTERM, exit_gracefully) try: - logger.log_quiet("Try to parse configured Cams / Services...") - for section in config.sections(): - section_header = section.split(' ') - section_object = None - section_keyword = section_header[0] - - # Skip crowsnest section - if section_keyword == 'crowsnest': - continue - - section_name = ' '.join(section_header[1:]) - component = utils.load_component(section_keyword, section_name) - logger.log_quiet(f"Parse configuration of section [{section}] ...") - if component.parse_config_section(config[section]): - sect_objs.append(component) - logger.log_quiet(f"Configuration of section [{section}] looks good. Continue ...") - else: - logger.log_error(f"Failed to parse config for section [{section}]! Skipping ...") - - logger.log_quiet("Try to start configured Cams / Services ...") - if sect_objs: - lock = asyncio.Lock() - for section_object in sect_objs: - task = asyncio.create_task(section_object.execute(lock)) - sect_exec_tasks.add(task) - - # Lets sec_exec_tasks finish first - await asyncio.sleep(0) - async with lock: - logger.log_quiet("... Done!") - else: + if not len(config.sections()) > 1: logger.log_quiet("No Cams / Services to start! Exiting ...") + else: + logger.log_quiet("Try to parse configured Cams / Services...") + for section in config.sections(): + section_header = section.split(' ') + section_object = None + section_keyword = section_header[0] + + # Skip crowsnest section + if section_keyword == 'crowsnest': + continue + + section_name = ' '.join(section_header[1:]) + component = utils.load_component(section_keyword, section_name) + logger.log_quiet(f"Parse configuration of section [{section}] ...") + if component.parse_config_section(config[section]): + sect_objs.append(component) + logger.log_quiet(f"Configuration of section [{section}] looks good. Continue ...") + else: + logger.log_error(f"Failed to parse config for section [{section}]! Skipping ...") + + logger.log_quiet("Try to start configured Cams / Services ...") + if sect_objs: + lock = asyncio.Lock() + for section_object in sect_objs: + task = asyncio.create_task(section_object.execute(lock)) + sect_exec_tasks.add(task) + + # Lets sec_exec_tasks finish first + await asyncio.sleep(0) + async with lock: + logger.log_quiet("... Done!") + else: + logger.log_quiet("No Service started! Exiting ...") - for task in sect_exec_tasks: - if task is not None: - await task + for task in sect_exec_tasks: + if task is not None: + await task except Exception as e: logger.log_multiline(traceback.format_exc().strip(), logger.log_error) finally: From 6d5dc81cea5c41f7c32735236ed37f7dfdad2a18 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 28 Jun 2024 15:10:11 +0200 Subject: [PATCH 119/129] chore: wip Signed-off-by: Patrick Gehrsitz --- pylibs/camera/types/uvc.py | 6 +++++- pylibs/components/crowsnest.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pylibs/camera/types/uvc.py b/pylibs/camera/types/uvc.py index 68c23114..e447b51d 100644 --- a/pylibs/camera/types/uvc.py +++ b/pylibs/camera/types/uvc.py @@ -83,7 +83,11 @@ def get_avail_uvc(search_path): return avail_uvc avail_by_id = get_avail_uvc('/dev/v4l/by-id/') - avail_by_path = dict(filter(lambda pair: 'usb' in pair[1], get_avail_uvc('/dev/v4l/by-path/').items())) + avail_by_path = dict(filter( + lambda key_value_pair: 'usb' in key_value_pair[1], + get_avail_uvc('/dev/v4l/by-path/').items() + ) + ) avail_uvc_cameras = {} for dev_path, by_path in avail_by_path.items(): avail_uvc_cameras[dev_path] = (by_path, avail_by_id.get(dev_path)) diff --git a/pylibs/components/crowsnest.py b/pylibs/components/crowsnest.py index c81eeda8..a3780930 100644 --- a/pylibs/components/crowsnest.py +++ b/pylibs/components/crowsnest.py @@ -11,7 +11,7 @@ def __init__(self, name: str = '') -> None: super().__init__(name) self.parameters.update({ - 'log_path': Parameter(), + 'log_path': Parameter(str), 'log_level': Parameter(str, 'verbose'), 'delete_log': Parameter(bool, 'True'), 'no_proxy': Parameter(bool, 'False') From cf28a8c75509360f1e57662cf5338c847fec9fc3 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 28 Jun 2024 15:11:23 +0200 Subject: [PATCH 120/129] refactor: add native comment support of ConfigParser Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 266 +++++++++++++++++------------------ pylibs/components/section.py | 87 ++++++------ 2 files changed, 175 insertions(+), 178 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 013b4305..113a934b 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -1,133 +1,133 @@ -#!/usr/bin/python3 - -import argparse -import configparser -import asyncio -import signal -import traceback - -from pylibs.components.crowsnest import Crowsnest -from pylibs import utils, watchdog, logger, logging_helper - -parser = argparse.ArgumentParser( - prog='Crowsnest', - description='Crowsnest - A webcam daemon for Raspberry Pi OS distributions like MainsailOS' -) -config = configparser.ConfigParser() - -parser.add_argument('-c', '--config', help='Path to config file', required=True) -parser.add_argument('-l', '--log_path', help='Path to log file', required=True) - -args = parser.parse_args() - -watchdog_running = True - -def initial_parse_config(): - global crowsnest, config, args - config_path = args.config - try: - config.read(config_path) - except configparser.ParsingError as e: - logger.log_multiline(e.message, logger.log_error) - logger.log_error("Failed to parse config! Exiting...") - exit(1) - crowsnest = Crowsnest('crowsnest') - if 'crowsnest' not in config or not crowsnest.parse_config_section(config['crowsnest']): - logger.log_error("Failed to parse config for '[crowsnest]' section! Exiting...") - exit(1) - -async def start_sections(): - global config, sect_exec_tasks - sect_objs = [] - sect_exec_tasks = set() - - # Catches SIGINT and SIGTERM to exit gracefully and cancel all tasks - signal.signal(signal.SIGINT, exit_gracefully) - signal.signal(signal.SIGTERM, exit_gracefully) - - try: - if not len(config.sections()) > 1: - logger.log_quiet("No Cams / Services to start! Exiting ...") - else: - logger.log_quiet("Try to parse configured Cams / Services...") - for section in config.sections(): - section_header = section.split(' ') - section_object = None - section_keyword = section_header[0] - - # Skip crowsnest section - if section_keyword == 'crowsnest': - continue - - section_name = ' '.join(section_header[1:]) - component = utils.load_component(section_keyword, section_name) - logger.log_quiet(f"Parse configuration of section [{section}] ...") - if component.parse_config_section(config[section]): - sect_objs.append(component) - logger.log_quiet(f"Configuration of section [{section}] looks good. Continue ...") - else: - logger.log_error(f"Failed to parse config for section [{section}]! Skipping ...") - - logger.log_quiet("Try to start configured Cams / Services ...") - if sect_objs: - lock = asyncio.Lock() - for section_object in sect_objs: - task = asyncio.create_task(section_object.execute(lock)) - sect_exec_tasks.add(task) - - # Lets sec_exec_tasks finish first - await asyncio.sleep(0) - async with lock: - logger.log_quiet("... Done!") - else: - logger.log_quiet("No Service started! Exiting ...") - - for task in sect_exec_tasks: - if task is not None: - await task - except Exception as e: - logger.log_multiline(traceback.format_exc().strip(), logger.log_error) - finally: - for task in sect_exec_tasks: - if task is not None: - task.cancel() - watchdog.running = False - logger.log_quiet("Shutdown or Killed by User!") - logger.log_quiet("Please come again :)") - logger.log_quiet("Goodbye...") - -async def exit_gracefully(signum, frame): - asyncio.sleep(1) - -async def main(): - global args, crowsnest - logger.setup_logging(args.log_path) - logging_helper.log_initial() - - initial_parse_config() - - if crowsnest.parameters['delete_log'].value: - logger.logger.handlers.clear() - logger.setup_logging(args.log_path, 'w') - logging_helper.log_initial() - - logger.set_log_level(crowsnest.parameters['log_level'].value) - - logging_helper.log_host_info() - logging_helper.log_config(args.config) - logging_helper.log_cams() - - task1 = asyncio.create_task(start_sections()) - await asyncio.sleep(0) - task2 = asyncio.create_task(watchdog.run_watchdog()) - - await task1 - if task2: - task2.cancel() - -if __name__ == "__main__": - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(main()) - finally: - loop.close() +#!/usr/bin/python3 + +import argparse +import configparser +import asyncio +import signal +import traceback + +from pylibs.components.crowsnest import Crowsnest +from pylibs import utils, watchdog, logger, logging_helper + +parser = argparse.ArgumentParser( + prog='Crowsnest', + description='Crowsnest - A webcam daemon for Raspberry Pi OS distributions like MainsailOS' +) +config = configparser.ConfigParser(inline_comment_prefixes='#') + +parser.add_argument('-c', '--config', help='Path to config file', required=True) +parser.add_argument('-l', '--log_path', help='Path to log file', required=True) + +args = parser.parse_args() + +watchdog_running = True + +def initial_parse_config(): + global crowsnest, config, args + config_path = args.config + try: + config.read(config_path) + except configparser.ParsingError as e: + logger.log_multiline(e.message, logger.log_error) + logger.log_error("Failed to parse config! Exiting...") + exit(1) + crowsnest = Crowsnest('crowsnest') + if not config.has_section('crowsnest') or not crowsnest.parse_config_section(config['crowsnest']): + logger.log_error("Failed to parse config for '[crowsnest]' section! Exiting...") + exit(1) + +async def start_sections(): + global config, sect_exec_tasks + sect_objs = [] + sect_exec_tasks = set() + + # Catches SIGINT and SIGTERM to exit gracefully and cancel all tasks + signal.signal(signal.SIGINT, exit_gracefully) + signal.signal(signal.SIGTERM, exit_gracefully) + + try: + if not len(config.sections()) > 1: + logger.log_quiet("No Cams / Services to start! Exiting ...") + else: + logger.log_quiet("Try to parse configured Cams / Services...") + for section in config.sections(): + section_header = section.split(' ') + section_object = None + section_keyword = section_header[0] + + # Skip crowsnest section + if section_keyword == 'crowsnest': + continue + + section_name = ' '.join(section_header[1:]) + component = utils.load_component(section_keyword, section_name) + logger.log_quiet(f"Parse configuration of section [{section}] ...") + if component.parse_config_section(config[section]): + sect_objs.append(component) + logger.log_quiet(f"Configuration of section [{section}] looks good. Continue ...") + else: + logger.log_error(f"Failed to parse config for section [{section}]! Skipping ...") + + logger.log_quiet("Try to start configured Cams / Services ...") + if sect_objs: + lock = asyncio.Lock() + for section_object in sect_objs: + task = asyncio.create_task(section_object.execute(lock)) + sect_exec_tasks.add(task) + + # Lets sec_exec_tasks finish first + await asyncio.sleep(0) + async with lock: + logger.log_quiet("... Done!") + else: + logger.log_quiet("No Service started! Exiting ...") + + for task in sect_exec_tasks: + if task is not None: + await task + except Exception as e: + logger.log_multiline(traceback.format_exc().strip(), logger.log_error) + finally: + for task in sect_exec_tasks: + if task is not None: + task.cancel() + watchdog.running = False + logger.log_quiet("Shutdown or Killed by User!") + logger.log_quiet("Please come again :)") + logger.log_quiet("Goodbye...") + +async def exit_gracefully(signum, frame): + asyncio.sleep(1) + +async def main(): + global args, crowsnest + logger.setup_logging(args.log_path) + logging_helper.log_initial() + + initial_parse_config() + + if crowsnest.parameters['delete_log'].value: + logger.logger.handlers.clear() + logger.setup_logging(args.log_path, 'w') + logging_helper.log_initial() + + logger.set_log_level(crowsnest.parameters['log_level'].value) + + logging_helper.log_host_info() + logging_helper.log_config(args.config) + logging_helper.log_cams() + + task1 = asyncio.create_task(start_sections()) + await asyncio.sleep(0) + task2 = asyncio.create_task(watchdog.run_watchdog()) + + await task1 + if task2: + task2.cancel() + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(main()) + finally: + loop.close() diff --git a/pylibs/components/section.py b/pylibs/components/section.py index 1a7e363d..ba8618e7 100644 --- a/pylibs/components/section.py +++ b/pylibs/components/section.py @@ -1,45 +1,42 @@ -#!/usr/bin/python3 - -import asyncio -from configparser import SectionProxy -from abc import ABC, abstractmethod - -from ..parameter import Parameter -from .. import logger - -class Section(ABC): - section_name = 'section' - keyword = 'section' - available_sections = {} - # Section looks like this: - # [ ] - # param1: value1 - # param2: value2 - def __init__(self, name: str) -> None: - self.name = name - self.parameters: dict[str, Parameter] = {} - - # Parse config according to the needs of the section - def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: - success = True - for parameter, value in config_section.items(): - if parameter not in self.parameters: - logger.log_warning(f"Parameter '{parameter}' is not supported by {self.keyword}!") - continue - value = value.split('\n') - value = [v.split('#')[0].strip() for v in value] - value = ' '.join(value) - self.parameters[parameter].set_value(value) - for parameter, value in self.parameters.items(): - if value.value is None: - logger.log_error(f"Parameter '{parameter}' incorrectly set or missing in section " - f"[{self.section_name} {self.name}] but is required!") - success = False - return success - - @abstractmethod - async def execute(self, lock: asyncio.Lock): - pass - -def load_component(*args, **kwargs): - raise NotImplementedError("If you see this, something went wrong!!!") +#!/usr/bin/python3 + +import asyncio +from configparser import SectionProxy +from abc import ABC, abstractmethod + +from ..parameter import Parameter +from .. import logger + +class Section(ABC): + section_name = 'section' + keyword = 'section' + available_sections = {} + # Section looks like this: + # [ ] + # param1: value1 + # param2: value2 + def __init__(self, name: str) -> None: + self.name = name + self.parameters: dict[str, Parameter] = {} + + # Parse config according to the needs of the section + def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: + success = True + for option, value in config_section.items(): + if option not in self.parameters: + logger.log_warning(f"Parameter '{option}' is not supported by {self.keyword}!") + continue + self.parameters[option].set_value(value) + for option, value in self.parameters.items(): + if value.value is None: + logger.log_error(f"Parameter '{option}' incorrectly set or missing in section " + f"[{self.section_name} {self.name}] but is required!") + success = False + return success + + @abstractmethod + async def execute(self, lock: asyncio.Lock): + pass + +def load_component(*args, **kwargs): + raise NotImplementedError("If you see this, something went wrong!!!") From 77cc644fcacede224ca866b0d56f3db4f7909892 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 8 Jul 2024 14:51:52 +0200 Subject: [PATCH 121/129] style: use guard clause Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 70 ++++++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index 113a934b..b5a866c1 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -48,43 +48,43 @@ async def start_sections(): try: if not len(config.sections()) > 1: logger.log_quiet("No Cams / Services to start! Exiting ...") - else: - logger.log_quiet("Try to parse configured Cams / Services...") - for section in config.sections(): - section_header = section.split(' ') - section_object = None - section_keyword = section_header[0] - - # Skip crowsnest section - if section_keyword == 'crowsnest': - continue - - section_name = ' '.join(section_header[1:]) - component = utils.load_component(section_keyword, section_name) - logger.log_quiet(f"Parse configuration of section [{section}] ...") - if component.parse_config_section(config[section]): - sect_objs.append(component) - logger.log_quiet(f"Configuration of section [{section}] looks good. Continue ...") - else: - logger.log_error(f"Failed to parse config for section [{section}]! Skipping ...") - - logger.log_quiet("Try to start configured Cams / Services ...") - if sect_objs: - lock = asyncio.Lock() - for section_object in sect_objs: - task = asyncio.create_task(section_object.execute(lock)) - sect_exec_tasks.add(task) - - # Lets sec_exec_tasks finish first - await asyncio.sleep(0) - async with lock: - logger.log_quiet("... Done!") + return + logger.log_quiet("Try to parse configured Cams / Services...") + for section in config.sections(): + section_header = section.split(' ') + section_object = None + section_keyword = section_header[0] + + # Skip crowsnest section + if section_keyword == 'crowsnest': + continue + + section_name = ' '.join(section_header[1:]) + component = utils.load_component(section_keyword, section_name) + logger.log_quiet(f"Parse configuration of section [{section}] ...") + if component.parse_config_section(config[section]): + sect_objs.append(component) + logger.log_quiet(f"Configuration of section [{section}] looks good. Continue ...") else: - logger.log_quiet("No Service started! Exiting ...") + logger.log_error(f"Failed to parse config for section [{section}]! Skipping ...") + + logger.log_quiet("Try to start configured Cams / Services ...") + if sect_objs: + lock = asyncio.Lock() + for section_object in sect_objs: + task = asyncio.create_task(section_object.execute(lock)) + sect_exec_tasks.add(task) + + # Lets sect_exec_tasks finish first + await asyncio.sleep(0) + async with lock: + logger.log_quiet("... Done!") + else: + logger.log_quiet("No Service started! Exiting ...") - for task in sect_exec_tasks: - if task is not None: - await task + for task in sect_exec_tasks: + if task is not None: + await task except Exception as e: logger.log_multiline(traceback.format_exc().strip(), logger.log_error) finally: From 48b4e32d679f454a51f7022942124186708bea2d Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Mon, 8 Jul 2024 14:55:46 +0200 Subject: [PATCH 122/129] chore: remove unnecessary code Signed-off-by: Patrick Gehrsitz --- crowsnest | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/crowsnest b/crowsnest index 9b11ffb9..7d203f49 100755 --- a/crowsnest +++ b/crowsnest @@ -19,20 +19,6 @@ set -Ee # Base Path BASE_CN_PATH="$(dirname "$(readlink -f "${0}")")" -# ## Import Librarys -# # shellcheck source-path=SCRIPTDIR/../libs/ -# . "${BASE_CN_PATH}/libs/camera-streamer.sh" -# . "${BASE_CN_PATH}/libs/configparser.sh" -# . "${BASE_CN_PATH}/libs/core.sh" -# . "${BASE_CN_PATH}/libs/hwhandler.sh" -# . "${BASE_CN_PATH}/libs/init_stream.sh" -# . "${BASE_CN_PATH}/libs/logging.sh" -# . "${BASE_CN_PATH}/libs/messages.sh" -# . "${BASE_CN_PATH}/libs/ustreamer.sh" -# . "${BASE_CN_PATH}/libs/v4l2_control.sh" -# . "${BASE_CN_PATH}/libs/versioncontrol.sh" -# . "${BASE_CN_PATH}/libs/watchdog.sh" - function missing_args_msg { echo -e "crowsnest: Missing Arguments!" echo -e "\n\tTry: crowsnest -h\n" @@ -88,20 +74,6 @@ while getopts ":vhc:s:d" arg; do esac done -# init_logging -# initial_check -# construct_streamer - -# ## Loop and Watchdog -# ## In this case watchdog acts more like a "cable defect detector" -# ## The User gets a message if Device is lost. -# clean_watchdog -# while true ; do -# crowsnest_watchdog -# sleep 120 & sleep_pid="$!" -# wait "${sleep_pid}" -# done - function set_log_path { #Workaround sed ~ to BASH VAR $HOME CROWSNEST_LOG_PATH=$(get_param "crowsnest" log_path | sed "s#^~#${HOME}#gi") From db64a79767264ed741779623f2161232c0544976 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Tue, 9 Jul 2024 14:51:05 +0200 Subject: [PATCH 123/129] fix: crash with no usb cam connected Signed-off-by: Patrick Gehrsitz --- pylibs/camera/types/uvc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pylibs/camera/types/uvc.py b/pylibs/camera/types/uvc.py index e447b51d..5987de4d 100644 --- a/pylibs/camera/types/uvc.py +++ b/pylibs/camera/types/uvc.py @@ -76,6 +76,8 @@ def get_current_control_value(self, control: str) -> int: def init_camera_type() -> list: def get_avail_uvc(search_path): avail_uvc = {} + if not os.path.exists(search_path): + return avail_uvc for file in os.listdir(search_path): dev_path = os.path.join(search_path, file) if os.path.islink(dev_path) and dev_path.endswith("index0"): From 4dc8c26c0418940ceb9c2e795be0627aca75ee73 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Tue, 9 Jul 2024 15:01:15 +0200 Subject: [PATCH 124/129] fix: various issues with missing uvc cam Signed-off-by: Patrick Gehrsitz --- pylibs/components/streamer/ustreamer.py | 31 +++++++++++-------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index 670d4fd9..5e593130 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -14,6 +14,7 @@ def __init__(self, name: str) -> None: self.binary_names = ['ustreamer.bin', 'ustreamer'] self.binary_paths = ['bin/ustreamer'] + self.cam = None async def execute(self, lock: asyncio.Lock): if self.parameters['no_proxy'].value: @@ -25,7 +26,7 @@ async def execute(self, lock: asyncio.Lock): res = self.parameters['resolution'].value fps = self.parameters['max_fps'].value device = self.parameters['device'].value - cam = camera.camera_manager.get_cam_by_path(device) + self.cam = camera.camera_manager.get_cam_by_path(device) streamer_args = [ '--host', host, @@ -49,7 +50,7 @@ async def execute(self, lock: asyncio.Lock): '--device', device, '--device-timeout', '2' ] - if cam.has_mjpg_hw_encoder(): + if self.cam and self.cam.has_mjpg_hw_encoder(): streamer_args += [ '--format', 'MJPEG', '--encoder', 'HW' @@ -57,7 +58,7 @@ async def execute(self, lock: asyncio.Lock): v4l2ctl = self.parameters['v4l2ctl'].value if v4l2ctl: - self._set_v4l2ctrls(v4l2ctl.split(',')) + self._set_v4l2ctrls(self.cam, v4l2ctl.split(',')) # custom flags streamer_args += self.parameters['custom_flags'].value.split() @@ -92,11 +93,11 @@ def _custom_log(self, msg: str): msg = re.sub(r'-- (.*?) \[.*?\] --', r'\1', msg) logger.log_debug(msg) - def _set_v4l2_ctrl(self, cam: camera.UVC, ctrl: str, prefix='') -> str: + def _set_v4l2_ctrl(self, ctrl: str, prefix='') -> str: try: c = ctrl.split('=')[0].strip().lower() v = int(ctrl.split('=')[1].strip()) - if not cam.set_control(c, v): + if not self.cam or not self.cam.set_control(c, v): raise ValueError except (ValueError, IndexError): logger.log_quiet(f"Failed to set parameter: '{ctrl.strip()}'", prefix) @@ -109,33 +110,29 @@ def _set_v4l2ctrls(self, ctrls: list[str] = None) -> str: return logger.log_quiet(f"Device: {section}", prefix) logger.log_quiet(f"Options: {', '.join(ctrls)}", prefix) - cam_path = self.parameters['device'].value - cam = camera.camera_manager.get_cam_by_path(cam_path) - avail_ctrls = cam.get_controls_string() + avail_ctrls = self.cam.get_controls_string() for ctrl in ctrls: c = ctrl.split('=')[0].strip().lower() if c not in avail_ctrls: logger.log_quiet( - f"Parameter '{ctrl.strip()}' not available for '{cam_path}'. Skipped.", + f"Parameter '{ctrl.strip()}' not available for '{self.parameters['device'].value}'. Skipped.", prefix ) continue - self._set_v4l2_ctrl(cam, ctrl, prefix) + self._set_v4l2_ctrl(self.cam, ctrl, prefix) # Repulls the string to print current values - logger.log_multiline(cam.get_controls_string(), logger.log_debug, 'DEBUG: v4l2ctl: ') + logger.log_multiline(self.cam.get_controls_string(), logger.log_debug, 'DEBUG: v4l2ctl: ') def _brokenfocus(self, focus_absolute_conf: str) -> str: - cam = camera.camera_manager.get_cam_by_path(self.parameters['device'].value) - cur_val = cam.get_current_control_value('focus_absolute') + cur_val = self.cam.get_current_control_value('focus_absolute') if cur_val and cur_val != focus_absolute_conf: logger.log_warning(f"Detected 'brokenfocus' device.") logger.log_info(f"Try to set to configured Value.") - self.set_v4l2_ctrl(cam, f'focus_absolute={focus_absolute_conf}') - logger.log_debug(f"Value is now: {cam.get_current_control_value('focus_absolute')}") + self.set_v4l2_ctrl(self.cam, f'focus_absolute={focus_absolute_conf}') + logger.log_debug(f"Value is now: {self.cam.get_current_control_value('focus_absolute')}") def _is_device_legacy(self) -> bool: - cam = camera.camera_manager.get_cam_by_path(self.parameters['device'].value) - return isinstance(cam, camera.Legacy) + return isinstance(self.cam, camera.Legacy) def load_component(name: str): From 74f14e9f79699b625a5f1c3cbd99a9a0846f1867 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 31 Aug 2024 15:25:05 +0200 Subject: [PATCH 125/129] fix: fix problems with some uvc cams Signed-off-by: Patrick Gehrsitz --- pylibs/v4l2/constants.py | 2 -- pylibs/v4l2/ctl.py | 1 + pylibs/v4l2/utils.py | 7 ++++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pylibs/v4l2/constants.py b/pylibs/v4l2/constants.py index c0369f91..38ae2945 100644 --- a/pylibs/v4l2/constants.py +++ b/pylibs/v4l2/constants.py @@ -1,8 +1,6 @@ #!/usr/bin/python3 V4L2_CTRL_MAX_DIMS = 4 -EINVAL = 22 -ENOTTY = 25 V4L2_CTRL_TYPE_INTEGER = 1 V4L2_CTRL_TYPE_BOOLEAN = 2 diff --git a/pylibs/v4l2/ctl.py b/pylibs/v4l2/ctl.py index 6c7953bd..ffa0052b 100644 --- a/pylibs/v4l2/ctl.py +++ b/pylibs/v4l2/ctl.py @@ -87,6 +87,7 @@ def get_query_controls(device_path: str) -> dict[str, raw.v4l2_ext_control]: next_fl = constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) query_controls: dict[str, raw.v4l2_query_ext_ctrl] = {} + utils.ioctl_safe(fd, raw.VIDIOC_G_EXT_CTRLS, qctrl) for qc in utils.ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: name = qc.name.decode() diff --git a/pylibs/v4l2/utils.py b/pylibs/v4l2/utils.py index ed6ae4e9..9ed25f12 100644 --- a/pylibs/v4l2/utils.py +++ b/pylibs/v4l2/utils.py @@ -3,6 +3,7 @@ import fcntl import ctypes import re +import errno from typing import Generator from . import raw, constants @@ -23,14 +24,14 @@ def ioctl_iter(fd: int, cmd: int, struct: ctypes.Structure, fcntl.ioctl(fd, cmd, struct) yield struct except OSError as e: - if e.errno == constants.EINVAL: + if e.errno == errno.EINVAL: if ignore_einval: continue break - elif e.errno == constants.ENOTTY: + elif e.errno in (errno.ENOTTY, errno.ENODATA, errno.EIO): break else: - raise + break def v4l2_ctrl_type_to_string(ctrl_type: int) -> str: dict_ctrl_type = { From 11d45dc7dddc477e679a8c10f550e2f57d2f9861 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 31 Aug 2024 15:49:26 +0200 Subject: [PATCH 126/129] refactor: use self.keyword instead of hardcoded name Signed-off-by: Patrick Gehrsitz --- pylibs/components/streamer/camera-streamer.py | 4 ++-- pylibs/components/streamer/ustreamer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pylibs/components/streamer/camera-streamer.py b/pylibs/components/streamer/camera-streamer.py index a01c0e32..4006c24b 100644 --- a/pylibs/components/streamer/camera-streamer.py +++ b/pylibs/components/streamer/camera-streamer.py @@ -49,7 +49,7 @@ async def execute(self, lock: asyncio.Lock): v4l2ctl = self.parameters['v4l2ctl'].value if v4l2ctl: prefix = "V4L2 Control: " - logger.log_quiet(f"Handling done by camera-streamer", prefix) + logger.log_quiet(f"Handling done by {self.keyword}", prefix) logger.log_quiet(f"Trying to set: {v4l2ctl}", prefix) for ctrl in v4l2ctl.split(','): streamer_args += [f'--camera-options={ctrl.strip()}'] @@ -79,7 +79,7 @@ async def execute(self, lock: asyncio.Lock): streamer_args += self.parameters['custom_flags'].value.split() cmd = self.binary_path + ' ' + ' '.join(streamer_args) - log_pre = f'camera-streamer [cam {self.name}]: ' + log_pre = f'{self.keyword} [cam {self.name}]: ' logger.log_debug(log_pre + f"Parameters: {' '.join(streamer_args)}") process,_,_ = await utils.execute_command( diff --git a/pylibs/components/streamer/ustreamer.py b/pylibs/components/streamer/ustreamer.py index 5e593130..d02751a2 100644 --- a/pylibs/components/streamer/ustreamer.py +++ b/pylibs/components/streamer/ustreamer.py @@ -64,7 +64,7 @@ async def execute(self, lock: asyncio.Lock): streamer_args += self.parameters['custom_flags'].value.split() cmd = self.binary_path + ' ' + ' '.join(streamer_args) - log_pre = f'ustreamer [cam {self.name}]: ' + log_pre = f'{self.keyword} [cam {self.name}]: ' logger.log_debug(log_pre + f"Parameters: {' '.join(streamer_args)}") process,_,_ = await utils.execute_command( From f4b1c41fef163e641bf2e42297f75a3a56bf1d3e Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sat, 21 Sep 2024 23:35:47 +0200 Subject: [PATCH 127/129] feat: add spyglass support Signed-off-by: Patrick Gehrsitz --- pylibs/components/streamer/spyglass.py | 70 ++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 pylibs/components/streamer/spyglass.py diff --git a/pylibs/components/streamer/spyglass.py b/pylibs/components/streamer/spyglass.py new file mode 100644 index 00000000..029740ec --- /dev/null +++ b/pylibs/components/streamer/spyglass.py @@ -0,0 +1,70 @@ +#!/usr/bin/python3 + +import asyncio + +from .streamer import Streamer +from ...parameter import Parameter +from ... import logger, utils, camera + +class Spyglass(Streamer): + keyword = 'spyglass' + + def __init__(self, name: str) -> None: + super().__init__(name) + + self.binary_names = ['run.py'] + self.binary_paths = ['bin/spyglass'] + + async def execute(self, lock: asyncio.Lock): + if self.parameters['no_proxy'].value: + host = '0.0.0.0' + logger.log_info("Set to 'no_proxy' mode! Using 0.0.0.0!") + else: + host = '127.0.0.1' + port = self.parameters['port'].value + res = self.parameters['resolution'].value + fps = self.parameters['max_fps'].value + device = self.parameters['device'].value + self.cam = camera.camera_manager.get_cam_by_path(device) + + streamer_args = [ + '--camera_num=' + device, + '--bindaddress=' + host, + '--port=' + str(port), + '--fps=' + str(fps), + '--resolution=' + str(res), + '--stream_url=/?action=stream', + '--snapshot_url=/?action=snapshot', + ] + + v4l2ctl = self.parameters['v4l2ctl'].value + if v4l2ctl: + prefix = "V4L2 Control: " + logger.log_quiet(f"Handling done by {self.keyword}", prefix) + logger.log_quiet(f"Trying to set: {v4l2ctl}", prefix) + for ctrl in v4l2ctl.split(','): + streamer_args += [f'--controls={ctrl.strip()}'] + + # custom flags + streamer_args += self.parameters['custom_flags'].value.split() + + venv_path = self.binary_paths[0]+'/.venv/bin/python3' + cmd = venv_path + ' ' + self.binary_path + ' ' + ' '.join(streamer_args) + log_pre = f'{self.keyword} [cam {self.name}]: ' + + logger.log_debug(log_pre + f"Parameters: {' '.join(streamer_args)}") + process,_,_ = await utils.execute_command( + cmd, + info_log_pre=log_pre, + info_log_func=logger.log_debug, + error_log_pre=log_pre, + error_log_func=logger.log_debug + ) + if lock.locked(): + lock.release() + + return process + + +def load_component(name: str): + return Spyglass(name) From 7e6044658d6cfd3a22641adb95808dd093e9d38c Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Thu, 3 Oct 2024 21:50:15 +0200 Subject: [PATCH 128/129] fix: self-compiled search first Signed-off-by: Patrick Gehrsitz --- pylibs/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pylibs/utils.py b/pylibs/utils.py index b84e18dc..73f1a66f 100644 --- a/pylibs/utils.py +++ b/pylibs/utils.py @@ -83,13 +83,15 @@ def get_executable(names: list[str], paths: list[str]) -> str: if names is None or paths is None: return None for name in names: - exec = shutil.which(name) - if exec: - return exec for path in paths: found = find_file(name, path) if found: return found + # Only search for installed packages, if there are no manually compiled binaries + for name in names: + exec = shutil.which(name) + if exec: + return exec return None def grep(path: str, search: str) -> str: From 9d28aa013839ce864858e1b302cbe2f48d69971f Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Sun, 6 Oct 2024 14:58:39 +0200 Subject: [PATCH 129/129] feat: log installed and supported streamer Signed-off-by: Patrick Gehrsitz --- crowsnest.py | 1 + pylibs/components/streamer/streamer.py | 24 +++++++++++++++- pylibs/logging_helper.py | 40 ++++++++++++++++---------- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/crowsnest.py b/crowsnest.py index b5a866c1..8419fa10 100644 --- a/crowsnest.py +++ b/crowsnest.py @@ -114,6 +114,7 @@ async def main(): logger.set_log_level(crowsnest.parameters['log_level'].value) logging_helper.log_host_info() + logging_helper.log_streamer() logging_helper.log_config(args.config) logging_helper.log_cams() diff --git a/pylibs/components/streamer/streamer.py b/pylibs/components/streamer/streamer.py index 2b2e037e..c59ef4de 100644 --- a/pylibs/components/streamer/streamer.py +++ b/pylibs/components/streamer/streamer.py @@ -3,6 +3,8 @@ import textwrap from configparser import SectionProxy from abc import ABC +from os import listdir +from os.path import isfile, join from ..section import Section from ...parameter import Parameter @@ -42,7 +44,8 @@ def __init__(self, name: str) -> None: self.missing_bin_txt = textwrap.dedent("""\ '%s' executable not found! Please make sure everything is installed correctly and up to date! - Run 'make update' inside the crowsnest directory to install and update everything.""") + Run 'make update' inside the crowsnest directory to install and update everything.""" + ) def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> bool: success = super().parse_config_section(config_section, *args, **kwargs) @@ -62,5 +65,24 @@ def parse_config_section(self, config_section: SectionProxy, *args, **kwargs) -> return False return True +def load_all_streamers(): + streamer_path = 'pylibs/components/streamer' + streamer_files = [ + f for f in listdir(streamer_path) + if isfile(join(streamer_path, f)) and f.endswith('.py') + ] + for streamer_file in streamer_files: + streamer_name = streamer_file[:-3] + try: + streamer = utils.load_component(streamer_name, + 'temp', + path=streamer_path.replace('/', '.')) + except NotImplementedError: + continue + Streamer.binaries[streamer_name] = utils.get_executable( + streamer.binary_names, + streamer.binary_paths + ) + def load_component(name: str): raise NotImplementedError("If you see this, something went wrong!!!") diff --git a/pylibs/logging_helper.py b/pylibs/logging_helper.py index de59b58d..f18700a1 100644 --- a/pylibs/logging_helper.py +++ b/pylibs/logging_helper.py @@ -6,6 +6,7 @@ import shutil from . import utils, logger, camera +from .components.streamer.streamer import Streamer, load_all_streamers def log_initial(): logger.log_quiet('crowsnest - A webcam Service for multiple Cams and Stream Services.') @@ -14,21 +15,6 @@ def log_initial(): logger.log_quiet(f'Version: {version}') logger.log_quiet('Prepare Startup ...') -def log_config(config_path): - logger.log_info("Print Configfile: '" + config_path + "'") - with open(config_path, 'r') as file: - config_txt = file.read() - # Remove comments - config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) - # Remove multiple whitespaces next to each other at the end of a line - config_txt = re.sub(r'\s*$', "", config_txt, flags=re.MULTILINE) - # Add newlines before sections - config_txt = re.sub(r'(\[.*\])$', "\n\\1", config_txt, flags=re.MULTILINE) - # Remove leading and trailing whitespaces - config_txt = config_txt.strip() - # Split the config file into lines - logger.log_multiline(config_txt, logger.log_info, logger.indentation) - def log_host_info(): logger.log_info("Host Information:") log_pre = logger.indentation @@ -75,6 +61,30 @@ def log_host_info(): free = utils.bytes_to_gigabytes(free) logger.log_info(f'Diskspace (avail. / total): {free}G / {total}G', log_pre) +def log_streamer(): + logger.log_info("Found Streamer:") + load_all_streamers() + log_pre = logger.indentation + for bin in Streamer.binaries: + if Streamer.binaries[bin] is None: + continue + logger.log_info(f'{bin}: {Streamer.binaries[bin]}', log_pre) + +def log_config(config_path): + logger.log_info("Print Configfile: '" + config_path + "'") + with open(config_path, 'r') as file: + config_txt = file.read() + # Remove comments + config_txt = re.sub(r'#.*$', "", config_txt, flags=re.MULTILINE) + # Remove multiple whitespaces next to each other at the end of a line + config_txt = re.sub(r'\s*$', "", config_txt, flags=re.MULTILINE) + # Add newlines before sections + config_txt = re.sub(r'(\[.*\])$', "\n\\1", config_txt, flags=re.MULTILINE) + # Remove leading and trailing whitespaces + config_txt = config_txt.strip() + # Split the config file into lines + logger.log_multiline(config_txt, logger.log_info, logger.indentation) + def log_cams(): logger.log_info("Detect available Devices") libcamera = camera.camera_manager.init_camera_type(camera.Libcamera)