Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pynest #270

Draft
wants to merge 130 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
130 commits
Select commit Hold shift + click to select a range
2428ffb
chore: wip
mryel00 Oct 31, 2023
a2c027a
chore: wip
mryel00 Nov 14, 2023
52aac83
chore: wip
mryel00 Nov 14, 2023
f4ce981
chore: wip
mryel00 Nov 14, 2023
ed9f857
chore: wip
mryel00 Nov 18, 2023
b4d68f1
chore: wip
mryel00 Nov 18, 2023
40871f6
chore: wip
mryel00 Nov 20, 2023
4f90585
chore: wip
mryel00 Nov 23, 2023
2326c7d
chore: wip
mryel00 Nov 24, 2023
9978513
chore: wip
mryel00 Nov 24, 2023
bb91c7d
chore: wip
mryel00 Nov 25, 2023
bb1e9fc
chore: wip
mryel00 Nov 25, 2023
f83ae46
chore: wip
mryel00 Nov 26, 2023
7538d30
chore: wip
mryel00 Nov 26, 2023
78ae5a9
chore: wip
mryel00 Nov 26, 2023
7553dc9
chore: wip
mryel00 Nov 26, 2023
a61eecf
chore: wip
mryel00 Nov 26, 2023
3b4196d
chore: wip
mryel00 Nov 26, 2023
1990e7a
chore: wip
mryel00 Nov 26, 2023
cc8b25b
chore: wip
mryel00 Nov 26, 2023
b57cc99
chore: wip
mryel00 Nov 26, 2023
6c4373c
chore: whip
mryel00 Nov 26, 2023
4c02760
chore: wip
mryel00 Dec 5, 2023
0b97b09
chore: wip
mryel00 Dec 28, 2023
31e6d1d
chore: wip
mryel00 Feb 20, 2024
2d128db
chore: wip
mryel00 Feb 24, 2024
d7c352a
Merge branch 'upstream/master' into pynest
mryel00 Feb 24, 2024
b2332c2
chore: wip
mryel00 Feb 24, 2024
a7e54e7
chore: wip
mryel00 Feb 24, 2024
a8f42be
chore: wip
mryel00 Feb 24, 2024
947b4e1
chore: wip
mryel00 Feb 24, 2024
20ba005
chore: wip
mryel00 Feb 24, 2024
80a1f5a
chore: wip
mryel00 Feb 24, 2024
4686b37
chore: wip
mryel00 Feb 24, 2024
149c35e
chore: wip
mryel00 Feb 25, 2024
5843038
chore: wip
mryel00 Feb 25, 2024
f1bc0b0
chore: wip
mryel00 Feb 28, 2024
63e557d
chore: wip
mryel00 Mar 1, 2024
0dfb3a6
chore: wip
mryel00 Mar 1, 2024
4e6d101
chore: wip
mryel00 Mar 2, 2024
be9d08a
chore: wip
mryel00 Mar 2, 2024
50c6337
chore: wip
mryel00 Mar 3, 2024
e748d1f
chore: wip
mryel00 Mar 3, 2024
6cb6176
chore: wip
mryel00 Mar 3, 2024
f72d518
chore: wip
mryel00 Mar 4, 2024
a1d0aef
chore: wip
mryel00 Mar 4, 2024
d0efab6
chore: wip
mryel00 Mar 4, 2024
5c8c729
chore: wip
mryel00 Mar 4, 2024
9aa21b2
chore: wip
mryel00 Mar 4, 2024
0c7ad37
chore: wip
mryel00 Mar 4, 2024
1b380a4
chore: wip
mryel00 Mar 4, 2024
fea035c
chore: wip
mryel00 Mar 4, 2024
715680c
chore: wip
mryel00 Mar 4, 2024
761fce4
chore: wip
mryel00 Mar 4, 2024
4887fc2
chore: wip
mryel00 Mar 4, 2024
7f9aa2b
chore: wip
mryel00 Mar 4, 2024
fefb259
chore: wip
mryel00 Mar 4, 2024
3f50048
chore: wip
mryel00 Mar 4, 2024
66e5751
chore: wip
mryel00 Mar 4, 2024
3f12fb0
chore: wip
mryel00 Mar 4, 2024
77ca6dd
chore: wip
mryel00 Mar 4, 2024
9087935
chore: wip
mryel00 Mar 4, 2024
b64295f
chore: wip
mryel00 Mar 4, 2024
3ed73e6
chore: wip
mryel00 Mar 4, 2024
1e6d7c9
chore: wip
mryel00 Mar 4, 2024
bf57796
chore: wip
mryel00 Mar 4, 2024
4287c0f
chore: wip
mryel00 Mar 10, 2024
e81388a
chore: wip
mryel00 Mar 10, 2024
42126e8
chore: wip
mryel00 Mar 10, 2024
3160138
chore: wip
mryel00 Mar 10, 2024
20b2c53
chore: wip
mryel00 Mar 12, 2024
ed6a52d
chore: wip
mryel00 Mar 13, 2024
3e599a0
chore: wip
mryel00 Mar 13, 2024
6336527
chore: wip
mryel00 Mar 13, 2024
d38ce5d
chore: wip
mryel00 Mar 13, 2024
1d50ec9
chore: wip
mryel00 Mar 13, 2024
a255f17
chore: wip
mryel00 Mar 13, 2024
f8201e2
chore: wip
mryel00 Mar 14, 2024
c9f63e1
chore: wip
mryel00 Mar 14, 2024
47b3a74
chore: wip
mryel00 Mar 14, 2024
9b68eea
chore: wip
mryel00 Mar 14, 2024
3a9922a
chore: wip
mryel00 Mar 16, 2024
17f4a31
chore: wip
mryel00 Mar 17, 2024
31b355c
chore: wip
mryel00 Mar 17, 2024
0269073
chore: wip
mryel00 Mar 17, 2024
2c4bb6b
chorte: wip
mryel00 Mar 21, 2024
451b71c
chore: wip
mryel00 Mar 22, 2024
ab49594
chore: wip
mryel00 Mar 22, 2024
760eb64
chore: wip
mryel00 Mar 22, 2024
f4d4b73
chore: wip
mryel00 Mar 22, 2024
ae92926
chore: wip
mryel00 Mar 22, 2024
526a043
chore: wip
mryel00 Mar 22, 2024
80cba41
chore: wip
mryel00 Mar 23, 2024
4517a87
chore: wip
mryel00 Mar 23, 2024
3e66ef2
chore: wip
mryel00 Mar 24, 2024
78ce013
chore: wip
mryel00 Mar 24, 2024
9511077
chore: wip
mryel00 Mar 24, 2024
da12c7e
Chore: wip
mryel00 Mar 25, 2024
71cfe6b
chore: wip
mryel00 Mar 25, 2024
09269e9
chore: wip
mryel00 Mar 25, 2024
48f7cd1
chore: wip
mryel00 Mar 26, 2024
19b7017
chore: wip
mryel00 Mar 26, 2024
55477a8
chore: wip
mryel00 Mar 27, 2024
bffdbb7
chore: wip
mryel00 Mar 27, 2024
70b9bf9
chore: wip
mryel00 Mar 30, 2024
6dd03ca
chore: wip
mryel00 Mar 30, 2024
b01bc7c
chore: wip
mryel00 Mar 30, 2024
d448568
chore: wip
mryel00 Mar 30, 2024
644495a
chore: wip
mryel00 Mar 31, 2024
a8ac087
Chore: wip
mryel00 Apr 3, 2024
390f32c
chore: wip
mryel00 Apr 3, 2024
03903d0
chore: wip
mryel00 Apr 3, 2024
364c74c
refactor: add abstract class inheritance
mryel00 Apr 3, 2024
f3d8908
chore: wip
mryel00 Apr 13, 2024
d6c725a
chore: wip
mryel00 Apr 25, 2024
90ea43b
chore: wip
mryel00 May 23, 2024
172716c
chore: wip
mryel00 May 23, 2024
1c6a620
chore: wip
mryel00 May 24, 2024
65a6d0c
chore: wip
mryel00 Jun 13, 2024
6d5dc81
chore: wip
mryel00 Jun 28, 2024
cf28a8c
refactor: add native comment support of ConfigParser
mryel00 Jun 28, 2024
77cc644
style: use guard clause
mryel00 Jul 8, 2024
48b4e32
chore: remove unnecessary code
mryel00 Jul 8, 2024
db64a79
fix: crash with no usb cam connected
mryel00 Jul 9, 2024
4dc8c26
fix: various issues with missing uvc cam
mryel00 Jul 9, 2024
74f14e9
fix: fix problems with some uvc cams
mryel00 Aug 31, 2024
11d45dc
refactor: use self.keyword instead of hardcoded name
mryel00 Aug 31, 2024
f4b1c41
feat: add spyglass support
mryel00 Sep 21, 2024
7e60446
fix: self-compiled search first
mryel00 Oct 3, 2024
9d28aa0
feat: log installed and supported streamer
mryel00 Oct 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ leftover*
# tmp file workaround
lost*

# ignore bin paths
# Ignore bin paths
bin/ustreamer
bin/camera-streamer

Expand All @@ -30,3 +30,6 @@ tools/.config

# Ignore pkglist
tools/pkglist.sh

# Ignore pycache
**/__pycache__/
59 changes: 34 additions & 25 deletions crowsnest
Original file line number Diff line number Diff line change
Expand Up @@ -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"
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?
Expand Down Expand Up @@ -74,16 +74,25 @@ while getopts ":vhc:s:d" arg; do
esac
done

init_logging
initial_check
construct_streamer
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
}

## 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 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
}

set_log_path
python3 "${BASE_CN_PATH}/crowsnest.py" -c "${CROWSNEST_CFG}" -l "${CROWSNEST_LOG_PATH}"
134 changes: 134 additions & 0 deletions crowsnest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/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 ...")
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_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
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_streamer()
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()
1 change: 1 addition & 0 deletions pylibs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#!/usr/bin/python3
7 changes: 7 additions & 0 deletions pylibs/camera/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/python3

from .types.uvc import UVC
from .types.legacy import Legacy
from .types.libcamera import Libcamera
from .camera import Camera
from . import camera_manager
26 changes: 26 additions & 0 deletions pylibs/camera/camera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/python3

import os
from abc import ABC, abstractmethod

class Camera(ABC):
def __init__(self, path: str, *args, **kwargs) -> None:
self.path = path
self.control_values = {}
self.formats = {}

def path_equals(self, path: str) -> bool:
return self.path == os.path.realpath(path)

@abstractmethod
def get_formats_string(self) -> str:
pass

@abstractmethod
def get_controls_string(self) -> str:
pass

@staticmethod
@abstractmethod
def init_camera_type() -> list:
pass
24 changes: 24 additions & 0 deletions pylibs/camera/camera_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/python3

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
10 changes: 10 additions & 0 deletions pylibs/camera/types/legacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from . import uvc
from ... import v4l2

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)]
83 changes: 83 additions & 0 deletions pylibs/camera/types/libcamera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import shutil, re

from ... import utils
from .. import camera

class Libcamera(camera.Camera):
def __init__(self, path, *args, **kwargs) -> None:
self.path = path
self.control_values = self._get_controls()
self.formats = []

def _get_controls(self) -> str:
ctrls = {}
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():
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 = [Libcamera(path) for path in re.findall(r'\((/base.*?)\)', libcam)]
for cam in cams:
cam.formats = cam._get_formats(libcam)
return cams
Loading