From ddb2202429679cf1733dffee660ec8d420aa192f Mon Sep 17 00:00:00 2001 From: Sergey Revyakin Date: Tue, 17 Feb 2026 15:46:26 +0700 Subject: [PATCH] init --- .dockerignore | 17 + .gitignore | 184 +++ .venv-sdr/bin/Activate.ps1 | 247 ++++ .venv-sdr/bin/activate | 70 ++ .venv-sdr/bin/activate.csh | 27 + .venv-sdr/bin/activate.fish | 69 ++ .venv-sdr/bin/f2py | 6 + .venv-sdr/bin/normalizer | 6 + .venv-sdr/bin/numpy-config | 6 + .venv-sdr/bin/pip | 8 + .venv-sdr/bin/pip3 | 8 + .venv-sdr/bin/pip3.12 | 8 + .venv-sdr/bin/python | 1 + .venv-sdr/bin/python3 | 1 + .venv-sdr/bin/python3.12 | 1 + .venv-sdr/lib64 | 1 + .venv-sdr/pyvenv.cfg | 5 + NN_server/Model.py | 272 +++++ NN_server/Models/Ensemble.py | 41 + NN_server/Models/Resnet18_1_2400.py | 192 ++++ NN_server/Models/Resnet18_2_2400.py | 132 +++ NN_server/Models/Resnet18_3.py | 132 +++ NN_server/Models/Resnet18_4.py | 132 +++ NN_server/Models/Resnet18_5.py | 134 +++ NN_server/Models/Resnet18_6.py | 134 +++ NN_server/Models/Resnet50_1.py | 136 +++ NN_server/Models/Resnet50_2.py | 135 +++ NN_server/Models/Transformer.py | 146 +++ NN_server/Models/detection_250_1.py | 154 +++ NN_server/Models/detection_250_2.py | 154 +++ NN_server/Models/detection_250_3_2444.py | 154 +++ NN_server/Models/detection_640_1.py | 151 +++ NN_server/Models/ensemble_1200.py | 252 ++++ NN_server/Models/ensemble_2400.py | 252 ++++ NN_server/Models/ensemble_915.py | 252 ++++ NN_server/Models/identification_1.py | 163 +++ NN_server/Models/identification_2.py | 163 +++ NN_server/Models/identification_3.py | 208 ++++ NN_server/Models/identification_4.py | 163 +++ NN_server/Models/identification_final.py | 216 ++++ NN_server/Models/identification_final_2.py | 206 ++++ NN_server/Models/identification_new_2400.py | 208 ++++ .../antlr4-python3-runtime-4.11.1.tar.gz | Bin 0 -> 116945 bytes NN_server/server.py | 312 +++++ README.md | 130 +++ common/__init__.py | 1 + common/runtime.py | 99 ++ deploy/docker/Dockerfile.nn_server | 34 + deploy/docker/Dockerfile.server_to_master | 20 + deploy/docker/docker-compose.yml | 43 + deploy/requirements/nn_common.txt | 11 + deploy/requirements/nn_gpu_pinned.txt | 6 + deploy/requirements/sdr_host.txt | 6 + deploy/requirements/server_to_master.txt | 6 + deploy/systemd/dronedetector-compose.service | 17 + deploy/systemd/dronedetector-sdr-1200.service | 19 + deploy/systemd/dronedetector-sdr-2400.service | 19 + deploy/systemd/dronedetector-sdr-3300.service | 19 + deploy/systemd/dronedetector-sdr-433.service | 19 + deploy/systemd/dronedetector-sdr-4500.service | 19 + deploy/systemd/dronedetector-sdr-5200.service | 19 + deploy/systemd/dronedetector-sdr-5800.service | 19 + deploy/systemd/dronedetector-sdr-750.service | 19 + deploy/systemd/dronedetector-sdr-868.service | 19 + deploy/systemd/dronedetector-sdr-915.service | 19 + deploy/systemd/precheck-sdr.sh | 22 + install_all.sh | 246 ++++ orange_scripts/compose_send_data_1200.py | 114 ++ orange_scripts/compose_send_data_2400.py | 115 ++ orange_scripts/compose_send_data_915.py | 115 ++ orange_scripts/main_1200.py | 173 +++ orange_scripts/main_2400.py | 173 +++ orange_scripts/main_915.py | 173 +++ src/core/__init__.py | 0 src/core/data_buffer.py | 163 +++ src/core/multichannelswitcher.py | 168 +++ src/core/sig_n_medi_collect.py | 107 ++ src/core/spectrum.py | 44 + src/core/waterfall.py | 167 +++ src/embedded_3300.py | 120 ++ src/embedded_433.py | 120 ++ src/embedded_4500.py | 120 ++ src/embedded_5200.py | 120 ++ src/embedded_5800.py | 120 ++ src/embedded_750.py | 123 ++ src/embedded_868.py | 120 ++ src/main_3300.py | 208 ++++ src/main_433.py | 208 ++++ src/main_4500.py | 208 ++++ src/main_5200.py | 208 ++++ src/main_5800.py | 208 ++++ src/main_750.py | 208 ++++ src/main_868.py | 208 ++++ src/server.py | 1010 +++++++++++++++++ src/server_to_master.py | 500 ++++++++ src/server_to_tablet.py | 314 +++++ src/unused/__init__.py | 0 src/unused/embedded_1200.py | 108 ++ src/unused/embedded_2400.py | 276 +++++ src/unused/embedded_915.py | 109 ++ src/unused/main_1200.py | 208 ++++ src/unused/main_2400.py | 198 ++++ src/unused/main_915.py | 208 ++++ src/unused/npu/simplenet.weak.v2.4.rknn | Bin 0 -> 194896 bytes src/utils/GPS_get_coords.py | 86 ++ src/utils/__init__.py | 0 src/utils/datas_processing.py | 133 +++ src/utils/gen_mac.py | 38 + src/utils/utils.py | 11 + 109 files changed, 13100 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .venv-sdr/bin/Activate.ps1 create mode 100644 .venv-sdr/bin/activate create mode 100644 .venv-sdr/bin/activate.csh create mode 100644 .venv-sdr/bin/activate.fish create mode 100755 .venv-sdr/bin/f2py create mode 100755 .venv-sdr/bin/normalizer create mode 100755 .venv-sdr/bin/numpy-config create mode 100755 .venv-sdr/bin/pip create mode 100755 .venv-sdr/bin/pip3 create mode 100755 .venv-sdr/bin/pip3.12 create mode 120000 .venv-sdr/bin/python create mode 120000 .venv-sdr/bin/python3 create mode 120000 .venv-sdr/bin/python3.12 create mode 120000 .venv-sdr/lib64 create mode 100644 .venv-sdr/pyvenv.cfg create mode 100644 NN_server/Model.py create mode 100644 NN_server/Models/Ensemble.py create mode 100644 NN_server/Models/Resnet18_1_2400.py create mode 100644 NN_server/Models/Resnet18_2_2400.py create mode 100644 NN_server/Models/Resnet18_3.py create mode 100644 NN_server/Models/Resnet18_4.py create mode 100644 NN_server/Models/Resnet18_5.py create mode 100644 NN_server/Models/Resnet18_6.py create mode 100644 NN_server/Models/Resnet50_1.py create mode 100644 NN_server/Models/Resnet50_2.py create mode 100644 NN_server/Models/Transformer.py create mode 100644 NN_server/Models/detection_250_1.py create mode 100644 NN_server/Models/detection_250_2.py create mode 100644 NN_server/Models/detection_250_3_2444.py create mode 100644 NN_server/Models/detection_640_1.py create mode 100644 NN_server/Models/ensemble_1200.py create mode 100644 NN_server/Models/ensemble_2400.py create mode 100644 NN_server/Models/ensemble_915.py create mode 100644 NN_server/Models/identification_1.py create mode 100644 NN_server/Models/identification_2.py create mode 100644 NN_server/Models/identification_3.py create mode 100644 NN_server/Models/identification_4.py create mode 100644 NN_server/Models/identification_final.py create mode 100644 NN_server/Models/identification_final_2.py create mode 100644 NN_server/Models/identification_new_2400.py create mode 100644 NN_server/antlr4-python3-runtime-4.11.1.tar.gz create mode 100644 NN_server/server.py create mode 100644 README.md create mode 100644 common/__init__.py create mode 100644 common/runtime.py create mode 100644 deploy/docker/Dockerfile.nn_server create mode 100644 deploy/docker/Dockerfile.server_to_master create mode 100644 deploy/docker/docker-compose.yml create mode 100644 deploy/requirements/nn_common.txt create mode 100644 deploy/requirements/nn_gpu_pinned.txt create mode 100644 deploy/requirements/sdr_host.txt create mode 100644 deploy/requirements/server_to_master.txt create mode 100644 deploy/systemd/dronedetector-compose.service create mode 100644 deploy/systemd/dronedetector-sdr-1200.service create mode 100644 deploy/systemd/dronedetector-sdr-2400.service create mode 100644 deploy/systemd/dronedetector-sdr-3300.service create mode 100644 deploy/systemd/dronedetector-sdr-433.service create mode 100644 deploy/systemd/dronedetector-sdr-4500.service create mode 100644 deploy/systemd/dronedetector-sdr-5200.service create mode 100644 deploy/systemd/dronedetector-sdr-5800.service create mode 100644 deploy/systemd/dronedetector-sdr-750.service create mode 100644 deploy/systemd/dronedetector-sdr-868.service create mode 100644 deploy/systemd/dronedetector-sdr-915.service create mode 100755 deploy/systemd/precheck-sdr.sh create mode 100755 install_all.sh create mode 100644 orange_scripts/compose_send_data_1200.py create mode 100644 orange_scripts/compose_send_data_2400.py create mode 100644 orange_scripts/compose_send_data_915.py create mode 100644 orange_scripts/main_1200.py create mode 100644 orange_scripts/main_2400.py create mode 100644 orange_scripts/main_915.py create mode 100644 src/core/__init__.py create mode 100644 src/core/data_buffer.py create mode 100644 src/core/multichannelswitcher.py create mode 100644 src/core/sig_n_medi_collect.py create mode 100644 src/core/spectrum.py create mode 100644 src/core/waterfall.py create mode 100644 src/embedded_3300.py create mode 100644 src/embedded_433.py create mode 100644 src/embedded_4500.py create mode 100644 src/embedded_5200.py create mode 100644 src/embedded_5800.py create mode 100644 src/embedded_750.py create mode 100644 src/embedded_868.py create mode 100644 src/main_3300.py create mode 100644 src/main_433.py create mode 100644 src/main_4500.py create mode 100644 src/main_5200.py create mode 100644 src/main_5800.py create mode 100644 src/main_750.py create mode 100644 src/main_868.py create mode 100644 src/server.py create mode 100644 src/server_to_master.py create mode 100644 src/server_to_tablet.py create mode 100644 src/unused/__init__.py create mode 100644 src/unused/embedded_1200.py create mode 100644 src/unused/embedded_2400.py create mode 100644 src/unused/embedded_915.py create mode 100644 src/unused/main_1200.py create mode 100644 src/unused/main_2400.py create mode 100644 src/unused/main_915.py create mode 100644 src/unused/npu/simplenet.weak.v2.4.rknn create mode 100644 src/utils/GPS_get_coords.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/datas_processing.py create mode 100644 src/utils/gen_mac.py create mode 100644 src/utils/utils.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..184623a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.vscode +.venv-sdr +__pycache__/ +*.pyc +*.pyo + +# Heavy host-only SDR sources +gnuradio/ +gr-osmosdr/ +gr-osmosdr-0.2.6/ + +# Local runtime artifacts +NN_server/result/ + +# Legacy install artifacts not needed in docker image +install_scripts/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c0ed61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,184 @@ +# ---> JupyterNotebooks +# gitignore template for Jupyter Notebooks +# website: http://jupyter.org/ + +NN_server/NN/ +.env +.ipynb_checkpoints +*/.ipynb_checkpoints/* +volk/ +torchsig/ +gnuradio/ +gr_osmosdr/ +gr_osmosdr-0.2.6/ +/.vscode +/docs + +# IPython +profile_default/ +ipython_config.py + +# Remove previous ipynb_checkpoints +# git rm -r .ipynb_checkpoints/ + +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + diff --git a/.venv-sdr/bin/Activate.ps1 b/.venv-sdr/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/.venv-sdr/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/.venv-sdr/bin/activate b/.venv-sdr/bin/activate new file mode 100644 index 0000000..fffb05b --- /dev/null +++ b/.venv-sdr/bin/activate @@ -0,0 +1,70 @@ +# This file must be used with "source bin/activate" *from bash* +# You cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # Call hash to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + hash -r 2> /dev/null + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +# on Windows, a path can contain colons and backslashes and has to be converted: +if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then + # transform D:\path\to\venv to /d/path/to/venv on MSYS + # and to /cygdrive/d/path/to/venv on Cygwin + export VIRTUAL_ENV=$(cygpath /home/sibscience-4/from_ssh/DroneDetector/.venv-sdr) +else + # use the path as-is + export VIRTUAL_ENV=/home/sibscience-4/from_ssh/DroneDetector/.venv-sdr +fi + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/"bin":$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1='(.venv-sdr) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(.venv-sdr) ' + export VIRTUAL_ENV_PROMPT +fi + +# Call hash to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +hash -r 2> /dev/null diff --git a/.venv-sdr/bin/activate.csh b/.venv-sdr/bin/activate.csh new file mode 100644 index 0000000..0d944dc --- /dev/null +++ b/.venv-sdr/bin/activate.csh @@ -0,0 +1,27 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. + +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV /home/sibscience-4/from_ssh/DroneDetector/.venv-sdr + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(.venv-sdr) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(.venv-sdr) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/.venv-sdr/bin/activate.fish b/.venv-sdr/bin/activate.fish new file mode 100644 index 0000000..3221701 --- /dev/null +++ b/.venv-sdr/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/). You cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV /home/sibscience-4/from_ssh/DroneDetector/.venv-sdr + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/"bin $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) '(.venv-sdr) ' (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT '(.venv-sdr) ' +end diff --git a/.venv-sdr/bin/f2py b/.venv-sdr/bin/f2py new file mode 100755 index 0000000..65445c2 --- /dev/null +++ b/.venv-sdr/bin/f2py @@ -0,0 +1,6 @@ +#!/home/sibscience-4/from_ssh/DroneDetector/.venv-sdr/bin/python3 +import sys +from numpy.f2py.f2py2e import main +if __name__ == '__main__': + sys.argv[0] = sys.argv[0].removesuffix('.exe') + sys.exit(main()) diff --git a/.venv-sdr/bin/normalizer b/.venv-sdr/bin/normalizer new file mode 100755 index 0000000..8af39ee --- /dev/null +++ b/.venv-sdr/bin/normalizer @@ -0,0 +1,6 @@ +#!/home/sibscience-4/from_ssh/DroneDetector/.venv-sdr/bin/python3 +import sys +from charset_normalizer.cli import cli_detect +if __name__ == '__main__': + sys.argv[0] = sys.argv[0].removesuffix('.exe') + sys.exit(cli_detect()) diff --git a/.venv-sdr/bin/numpy-config b/.venv-sdr/bin/numpy-config new file mode 100755 index 0000000..fd93d80 --- /dev/null +++ b/.venv-sdr/bin/numpy-config @@ -0,0 +1,6 @@ +#!/home/sibscience-4/from_ssh/DroneDetector/.venv-sdr/bin/python3 +import sys +from numpy._configtool import main +if __name__ == '__main__': + sys.argv[0] = sys.argv[0].removesuffix('.exe') + sys.exit(main()) diff --git a/.venv-sdr/bin/pip b/.venv-sdr/bin/pip new file mode 100755 index 0000000..4f1d903 --- /dev/null +++ b/.venv-sdr/bin/pip @@ -0,0 +1,8 @@ +#!/home/sibscience-4/from_ssh/DroneDetector/.venv-sdr/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-sdr/bin/pip3 b/.venv-sdr/bin/pip3 new file mode 100755 index 0000000..4f1d903 --- /dev/null +++ b/.venv-sdr/bin/pip3 @@ -0,0 +1,8 @@ +#!/home/sibscience-4/from_ssh/DroneDetector/.venv-sdr/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-sdr/bin/pip3.12 b/.venv-sdr/bin/pip3.12 new file mode 100755 index 0000000..4f1d903 --- /dev/null +++ b/.venv-sdr/bin/pip3.12 @@ -0,0 +1,8 @@ +#!/home/sibscience-4/from_ssh/DroneDetector/.venv-sdr/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-sdr/bin/python b/.venv-sdr/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/.venv-sdr/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/.venv-sdr/bin/python3 b/.venv-sdr/bin/python3 new file mode 120000 index 0000000..ae65fda --- /dev/null +++ b/.venv-sdr/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/.venv-sdr/bin/python3.12 b/.venv-sdr/bin/python3.12 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/.venv-sdr/bin/python3.12 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/.venv-sdr/lib64 b/.venv-sdr/lib64 new file mode 120000 index 0000000..7951405 --- /dev/null +++ b/.venv-sdr/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/.venv-sdr/pyvenv.cfg b/.venv-sdr/pyvenv.cfg new file mode 100644 index 0000000..ca5fb64 --- /dev/null +++ b/.venv-sdr/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = true +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3 -m venv --system-site-packages /home/sibscience-4/from_ssh/DroneDetector/.venv-sdr diff --git a/NN_server/Model.py b/NN_server/Model.py new file mode 100644 index 0000000..776a658 --- /dev/null +++ b/NN_server/Model.py @@ -0,0 +1,272 @@ +from tqdm import tqdm +import numpy as np +import random +import os +import re +import gc + + +class Model(object): + _model_id = 0 + _ind_inference = 1 + _result_list = dict() + + @staticmethod + def get_model_id(): + try: + return Model._model_id + except Exception as exc: + print(str(exc)) + + @staticmethod + def _get_inc_model_id(): + try: + Model._model_id += 1 + return Model._model_id + except Exception as exc: + print(str(exc)) + + @staticmethod + def get_ind_inference(): + try: + return Model._ind_inference + except Exception as exc: + print(str(exc)) + + @staticmethod + def get_inc_ind_inference(): + try: + Model._ind_inference += 1 + except Exception as exc: + print(str(exc)) + + @staticmethod + def _init_result_list(type_model=''): + try: + Model._result_list[type_model] = {} + except Exception as exc: + print(str(exc)) + + @staticmethod + def get_result_list(): + try: + def get_max(dict_inf=None): + try: + return max([(len(i[0]) if len(i) else 0) for i in dict_inf.values()]) + except Exception as error: + print(str(error)) + return 0 + + max_length_label = max([get_max(i) for i in Model._result_list.values()]) + num_inf = max(list(map(int, list(Model._result_list.values())[0].keys()))) + max_length_type_model = max([len(i) for i in Model._result_list.keys()]) + num_gaps = (max_length_type_model + 4) * (len(Model._result_list) + 1) + 2 * (len(Model._result_list) + 2) + print('_' * num_gaps) + + print('||' + ' ' * (max_length_type_model + 4) + '|', end='') + for type_model in Model._result_list.keys(): + print('|' + ' ' * ((max_length_type_model - len(type_model)) // 2 + 2), end='') + print(type_model, end='') + print(' ' * ((max_length_type_model - len(type_model)) // 2 + 2) + '|', end='') + print('|') + + for ind_inf in range(1, num_inf+1): + print('||' + ' ' * ((max_length_type_model - len(str(ind_inf))) // 2 + 2), end='') + print(str(ind_inf) if len(str(ind_inf)) % 2 == 0 else str(ind_inf) + ' ', end='') + print(' ' * ((max_length_type_model - len(str(ind_inf))) // 2 + 2) + '|', end='') + + for info_inference in Model._result_list.values(): + if len(info_inference[ind_inf]) != 0: + length_gap_left = (max_length_label - len(info_inference[ind_inf][0])) // 2 + (max_length_label - len(info_inference[ind_inf][0])) % 2 + length_gap_right = (max_length_label - len(info_inference[ind_inf][0])) // 2 + 1 + to_print = (' ' * length_gap_left + info_inference[ind_inf][0] + ' ' * length_gap_right) + (str(info_inference[ind_inf][1]) + if len(str(info_inference[ind_inf][1])) != 3 else str(info_inference[ind_inf][1]) + ' ') + else: + to_print = ' ' * max_length_type_model + print('|' + ' ' * ((max_length_type_model - len(to_print)) // 2 + 2), end='') + print(to_print, end='') + print(' ' * ((max_length_type_model - len(to_print)) // 2 + 2) + '|', end='') + print('|') + + print('_' * num_gaps) + except Exception as exc: + print(str(exc)) + + @staticmethod + def _add_in_result_list(type_model='', ind_inference=0, list_to_add=None): + try: + Model._result_list[type_model][ind_inference] = list_to_add + except Exception as exc: + print(str(exc)) + + def __init__(self, file_model='', file_config='', src_example='', src_result='', type_model='', + build_model_func=None, pre_func=None, inference_func=None, post_func=None, classes=None, + number_synthetic_examples=0, number_src_data_for_one_synthetic_example=0, path_to_src_dataset=''): + try: + self._file_model = file_model + self._file_config = file_config + self._src_example = src_example + self._src_result = src_result + self._type_model = type_model + self._build_model_func = build_model_func + self._pre_func = pre_func + self._inference_func = inference_func + self._post_func = post_func + self._classes = classes + self._num_outputs = len(self._classes.keys()) + self._number_synthetic_examples = number_synthetic_examples + self._number_src_data_for_one_synthetic_example = number_src_data_for_one_synthetic_example + self._path_to_src_dataset = path_to_src_dataset + self._data = None + self._shablon = ' Модель ' + str(self._model_id+1) + ' с типом ' + str(self._type_model) + self._model = self._build_model() + self._model_id = Model._get_inc_model_id() + self._init_result_list(type_model=self._type_model) + except Exception as exc: + print(str(exc)) + + def __str__(self): + try: + if self._model is None: + return self._shablon + ' не работает!' + '\n' + else: + return self._shablon + ' работает!' + '\n' + except Exception as exc: + print(str(exc)) + + def get_mapping(self): + return list(self._classes.values()) + + def get_model_name(self): + return self._type_model + + def get_shablon(self): + return self._shablon + + def get_model(self): + return self._model + + def _build_model(self): + try: + print('Инициализация' + self._shablon) + return self._build_model_func(file_model=self._file_model, file_config=self._file_config, + num_classes=len(self._classes)) + except Exception as exc: + print(str(exc)) + + def _prepare_data(self, data=None): + try: + print('Подготовка данных' + self._shablon) + self._data = self._pre_func(data, src=self._src_result, ind_inference=Model.get_ind_inference()) + except Exception as exc: + print(str(exc)) + + def _post_data(self, prediction=None): + print('Постобработка данных' + self._shablon) + self._ind_inference += 1 + self._post_func(src=self._src_result, data=self._data, model_id=self._model_id, model_type=self._type_model, + ind_inference=Model.get_ind_inference(), prediction=prediction) + + def get_test_inference(self): + try: + self._test_inference() + except Exception as exc: + print(str(exc)) + + def _create_synthetic_examples(self): + try: + print('#' * 100) + print('Создание синтетических примеров: ' + self._shablon) + + path_to_example_directory = os.path.join(self._src_example, self._type_model) + os.mkdir(path_to_example_directory) + for ind in tqdm(range(self._number_synthetic_examples)): + try: + label = self._classes[random.randint(0, self._num_outputs-1)] + path_to_src_directory = os.path.join(self._path_to_src_dataset, label) + with open(path_to_src_directory + '/' + os.listdir(path_to_src_directory)[0], 'rb') as data_file: + data = np.frombuffer(data_file.read(), dtype=np.float32) + array_example = np.zeros(np.shape(data)) + for _ in range(self._number_src_data_for_one_synthetic_example): + with open(path_to_src_directory + '/' + random.choice(os.listdir(path_to_src_directory)), 'rb') as data_file: + data = np.frombuffer(data_file.read(), dtype=np.float32) + array_example = np.add(array_example, data) + np.save(path_to_example_directory + '/' + label + '_' + str(ind+1), array_example / self._number_src_data_for_one_synthetic_example) + except Exception as exc: + print(str(exc)) + + print('Создание синтетических примеров завершено!') + print() + except Exception as exc: + print(str(exc)) + print() + + def _test_inference(self): + try: + self._create_synthetic_examples() + + count_access = 1 + count_attempt = 1 + path_to_example = os.path.join(self._src_example, self._type_model) + _, _, files = next(os.walk(path_to_example)) + + if files: + ind_inference = 0 + for file in files: + with open(path_to_example + '/' + file, 'rb') as data_file: + self._data = np.frombuffer(data_file.read(), dtype=np.float32) + + print() + self._prepare_data(data=self._data) + + print('Тестовый инференс' + self._shablon + ' попытка ' + str(count_attempt)) + prediction, probability = self._inference_func(data=self._data, model=self._model, mapping=self._classes, + shablon=self._shablon) + + for value in self._classes.values(): + if value in re.split('[._/]', file): + if value == prediction: + print('Тест ' + str(count_attempt) + ' пройден!\n') + count_access += 1 + else: + print('Тест ' + str(count_attempt) + ' провален!\n') + count_attempt += 1 + break + + print() + print('Постобработка данных' + self._shablon) + ind_inference += 1 + self._post_func(src=path_to_example+'/', data=self._data, ind_inference=ind_inference, model_id=self._model_id, model_type=self._type_model, prediction=prediction) + + print('\nТестовый инференс' + self._shablon + ' пройден с результатом ' + str(100 * (count_access - 1) / (count_attempt - 1)) + ' %') + print('#' * 100) + print() + else: + print('\nНет данных для тестового инференса') + print() + + except Exception as exc: + print(str(exc)) + + def get_inference(self, data=None): + try: + return self._inference(data=data) + except Exception as exc: + print(str(exc)) + return None + + def _inference(self, data=None): + try: + Model._add_in_result_list(type_model=self._type_model, ind_inference=self.get_ind_inference(), list_to_add=[]) + self._prepare_data(data=data) + print('Инференс' + self._shablon) + prediction, probability = self._inference_func(data=self._data, model=self._model, mapping=self._classes, + shablon=self._shablon) + Model._add_in_result_list(type_model=self._type_model, ind_inference=self.get_ind_inference(), list_to_add=[prediction, probability]) + self._post_data(prediction=prediction) + + gc.collect() + return prediction, probability + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/Ensemble.py b/NN_server/Models/Ensemble.py new file mode 100644 index 0000000..5dbaedd --- /dev/null +++ b/NN_server/Models/Ensemble.py @@ -0,0 +1,41 @@ +import torch +import torch.nn as nn +from torch.nn import functional as F +from torchvision import datasets, transforms +from torchensemble import VotingClassifier + + +class Ensemble(nn.Module): + def __init__(self): + super(Ensemble, self).__init__() + self.linear1 = nn.Linear(2048, 512) + self.linear2 = nn.Linear(512, 128) + self.linear3 = nn.Linear(128, 32) + self.linear4 = nn.Linear(32, 3) + + def forward(self, data): + data = data.view(data.size(0), -1) + output = F.relu(self.linear1(data)) + output = F.relu(self.linear2(output)) + output = self.linear3(output) + return output + + +transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) +]) + +train = datasets.VisionDataset('../Dataset', train=True, download=True, transform=transform) +test = datasets.VisionDataset('../Dataset', train=False, transform=transform) +train_loader = torch.utils.data.DataLoader(train, batch_size=128, shuffle=True) +test_loader = torch.utils.data.DataLoader(test, batch_size=128, shuffle=True) + +model = VotingClassifier(estimator=Ensemble, n_estimators=10, cuda=True) + +criterion = nn.CrossEntropyLoss() +model.set_criterion(criterion) + +model.set_optimizer('Adam', lr=1e-3, weight_decay=5e-4) + +model.fit(train_loader, epochs=50, test_loader=test_loader) diff --git a/NN_server/Models/Resnet18_1_2400.py b/NN_server/Models/Resnet18_1_2400.py new file mode 100644 index 0000000..599842f --- /dev/null +++ b/NN_server/Models/Resnet18_1_2400.py @@ -0,0 +1,192 @@ +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import matplotlib +import mlconfig +import torch +import cv2 +import gc +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + matplotlib.use('Agg') + plt.ioff() + + figsize = (16, 8) + dpi = 32 + + # if int(ind_inference) <= 1500: + # np.save(src + '_inference_2400_' + str(ind_inference) + '.npy', data) + + fig1 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_real = data[0] + plt.plot(sig_real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_imag = data[1] + plt.plot(sig_imag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + img = np.asarray([img1, img2], dtype=np.float32) + + del sig_real + del sig_imag + del fig1 + del fig2 + del img1 + del img2 + del buf + del buf1 + del img_arr + del img_arr1 + + cv2.destroyAllWindows() + gc.collect() + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + matplotlib.use('Agg') + plt.ioff() + torch.cuda.empty_cache() + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Linear(in_features=512, out_features=num_classes, bias=True) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + cv2.destroyAllWindows() + gc.collect() + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + matplotlib.use('Agg') + plt.ioff() + torch.cuda.empty_cache() + device = 'cuda' if torch.cuda.is_available() else 'cpu' + print(device) + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + output = output.cpu() + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + del img + del label + del expon + del output + + cv2.destroyAllWindows() + gc.collect() + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + matplotlib.use('Agg') + plt.ioff() + ''' + if int(ind_inference) <= 100: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + ''' + plt.clf() + plt.cla() + plt.close() + cv2.destroyAllWindows() + gc.collect() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/Resnet18_2_2400.py b/NN_server/Models/Resnet18_2_2400.py new file mode 100644 index 0000000..75ad9a7 --- /dev/null +++ b/NN_server/Models/Resnet18_2_2400.py @@ -0,0 +1,132 @@ +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 8) + dpi = 80 + + fig1 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_real = data[0] + plt.plot(sig_real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_imag = data[1] + plt.plot(sig_imag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + img = np.asarray([img1, img2], dtype=np.float32) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Linear(in_features=512, out_features=num_classes, bias=True) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/Resnet18_3.py b/NN_server/Models/Resnet18_3.py new file mode 100644 index 0000000..75ad9a7 --- /dev/null +++ b/NN_server/Models/Resnet18_3.py @@ -0,0 +1,132 @@ +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 8) + dpi = 80 + + fig1 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_real = data[0] + plt.plot(sig_real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_imag = data[1] + plt.plot(sig_imag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + img = np.asarray([img1, img2], dtype=np.float32) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Linear(in_features=512, out_features=num_classes, bias=True) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/Resnet18_4.py b/NN_server/Models/Resnet18_4.py new file mode 100644 index 0000000..75ad9a7 --- /dev/null +++ b/NN_server/Models/Resnet18_4.py @@ -0,0 +1,132 @@ +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 8) + dpi = 80 + + fig1 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_real = data[0] + plt.plot(sig_real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_imag = data[1] + plt.plot(sig_imag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + img = np.asarray([img1, img2], dtype=np.float32) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Linear(in_features=512, out_features=num_classes, bias=True) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/Resnet18_5.py b/NN_server/Models/Resnet18_5.py new file mode 100644 index 0000000..75a4163 --- /dev/null +++ b/NN_server/Models/Resnet18_5.py @@ -0,0 +1,134 @@ +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 8) + dpi = 80 + + fig1 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_real = data[0] + plt.plot(sig_real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_imag = data[1] + plt.plot(sig_imag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + img = np.asarray([img1, img2], dtype=np.float32) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential(nn.Linear(in_features=512, out_features=128, bias=True), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.Linear(in_features=32, out_features=num_classes, bias=True)) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/Resnet18_6.py b/NN_server/Models/Resnet18_6.py new file mode 100644 index 0000000..d5d03c3 --- /dev/null +++ b/NN_server/Models/Resnet18_6.py @@ -0,0 +1,134 @@ +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 8) + dpi = 80 + + fig1 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_real = data[0] + plt.plot(sig_real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_imag = data[1] + plt.plot(sig_imag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + img = np.asarray([img1, img2], dtype=np.float32) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + # model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + # padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential(nn.Linear(in_features=512, out_features=128, bias=True), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.Linear(in_features=32, out_features=num_classes, bias=True)) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/Resnet50_1.py b/NN_server/Models/Resnet50_1.py new file mode 100644 index 0000000..00d65b6 --- /dev/null +++ b/NN_server/Models/Resnet50_1.py @@ -0,0 +1,136 @@ +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet50(data=None, src ='', ind_inference=0): + try: + figsize = (16, 8) + dpi = 80 + + fig1 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_real = data[0] + plt.plot(sig_real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_imag = data[1] + plt.plot(sig_imag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + img = np.asarray([img1, img2], dtype=np.float32) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet50(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential( + nn.Linear(in_features=2048, out_features=512, bias=True), + nn.Linear(in_features=512, out_features=128, bias=True), + nn.Linear(in_features=128, out_features=num_classes, bias=True) + ) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet50(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet50(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/Resnet50_2.py b/NN_server/Models/Resnet50_2.py new file mode 100644 index 0000000..b1ee721 --- /dev/null +++ b/NN_server/Models/Resnet50_2.py @@ -0,0 +1,135 @@ +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet50(data=None, src ='', ind_inference=0): + try: + figsize = (16, 8) + dpi = 80 + + fig1 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_real = data[0] + plt.plot(sig_real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + sig_imag = data[1] + plt.plot(sig_imag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + + img = np.asarray([img1, img2], dtype=np.float32) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet50(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential(nn.Linear(in_features=2048, out_features=512, bias=True), + nn.Linear(in_features=512, out_features=128, bias=True), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.Linear(in_features=32, out_features=num_classes, bias=True)) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet50(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet50(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str(model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/Transformer.py b/NN_server/Models/Transformer.py new file mode 100644 index 0000000..cc59329 --- /dev/null +++ b/NN_server/Models/Transformer.py @@ -0,0 +1,146 @@ +import sklearn +from sklearn.ensemble import BaggingClassifier +import numpy as np +import torch +from torch.utils._contextlib import F +from torch.utils.data import TensorDataset, DataLoader +import torch.nn as nn +from torch.optim import Adam + + +class SimpleCNN(nn.Module): + def __init__(self): + super(SimpleCNN, self).__init__() + + self.conv1 = nn.Conv2d(in_channels=1, out_channels=3, kernel_size=5, stride=1, padding=2) + self.conv1_s = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=5, stride=2, padding=2) + self.conv2 = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=1) + self.conv2_s = nn.Conv2d(in_channels=6, out_channels=6, kernel_size=3, stride=2, padding=1) + self.conv3 = nn.Conv2d(in_channels=6, out_channels=10, kernel_size=3, stride=1, padding=1) + self.conv3_s = nn.Conv2d(in_channels=10, out_channels=10, kernel_size=3, stride=2, padding=1) + + self.flatten = nn.Flatten() + self.fc1 = nn.Linear(10, 10) + + def forward(self, x): + x = F.relu(self.conv1(x)) + x = F.relu(self.conv1_s(x)) + x = F.relu(self.conv2(x)) + x = F.relu(self.conv2_s(x)) + x = F.relu(self.conv3(x)) + x = F.relu(self.conv3_s(x)) + + x = self.flatten(x) + x = self.fc1(x) + x = F.softmax(x) + + return x + + +class PytorchModel(sklearn.base.BaseEstimator): + def __init__(self, net_type, net_params, optim_type, optim_params, loss_fn, + input_shape, batch_size=32, accuracy_tol=0.02, tol_epochs=10, + cuda=True): + self.classes_ = None + self.optim = None + self.net = None + self.net_type = net_type + self.net_params = net_params + self.optim_type = optim_type + self.optim_params = optim_params + self.loss_fn = loss_fn + + self.input_shape = input_shape + self.batch_size = batch_size + self.accuracy_tol = accuracy_tol + self.tol_epochs = tol_epochs + self.cuda = cuda + + def fit(self, X, y): + self.net = self.net_type(**self.net_params) + if self.cuda: + self.net = self.net.cuda() + self.optim = self.optim_type(self.net.parameters(), **self.optim_params) + + uniq_classes = np.sort(np.unique(y)) + self.classes_ = uniq_classes + + X = X.reshape(-1, *self.input_shape) + x_tensor = torch.tensor(X.astype(np.float32)) + y_tensor = torch.tensor(y.astype(np.long)) + train_dataset = TensorDataset(x_tensor, y_tensor) + train_loader = DataLoader(train_dataset, batch_size=self.batch_size, + shuffle=True, drop_last=False) + last_accuracies = [] + epoch = 0 + keep_training = True + while keep_training: + self.net.train() + train_samples_count = 0 + true_train_samples_count = 0 + + for batch in train_loader: + x_data, y_data = batch[0], batch[1] + if self.cuda: + x_data = x_data.cuda() + y_data = y_data.cuda() + + y_pred = self.net(x_data) + loss = self.loss_fn(y_pred, y_data) + + self.optim.zero_grad() + loss.backward() + self.optim.step() + + y_pred = y_pred.argmax(dim=1, keepdim=False) + true_classified = (y_pred == y_data).sum().item() + true_train_samples_count += true_classified + train_samples_count += len(x_data) + + train_accuracy = true_train_samples_count / train_samples_count + last_accuracies.append(train_accuracy) + + if len(last_accuracies) > self.tol_epochs: + last_accuracies.pop(0) + + if len(last_accuracies) == self.tol_epochs: + accuracy_difference = max(last_accuracies) - min(last_accuracies) + if accuracy_difference <= self.accuracy_tol: + keep_training = False + + def predict_proba(self, X, y=None): + X = X.reshape(-1, *self.input_shape) + x_tensor = torch.tensor(X.astype(np.float32)) + if y: + y_tensor = torch.tensor(y.astype(np.float32)) + else: + y_tensor = torch.zeros(len(X), dtype=torch.long) + test_dataset = TensorDataset(x_tensor, y_tensor) + test_loader = DataLoader(test_dataset, batch_size=self.batch_size, + shuffle=False, drop_last=False) + + self.net.eval() + predictions = [] + for batch in test_loader: + x_data, y_data = batch[0], batch[1] + if self.cuda: + x_data = x_data.cuda() + y_data = y_data.cuda() + + y_pred = self.net(x_data) + + predictions.append(y_pred.detach().cpu().numpy()) + + predictions = np.concatenate(predictions) + return predictions + + def predict(self, x, y=None): + predictions = self.predict_proba(x, y) + predictions = predictions.argmax(axis=1) + return predictions + + +base_model = PytorchModel(net_type=SimpleCNN, net_params=dict(), optim_type=Adam, + optim_params={"lr": 1e-3}, loss_fn=nn.CrossEntropyLoss(), + input_shape=(1, 8, 8), batch_size=32, accuracy_tol=0.02, + tol_epochs=10, cuda=True) diff --git a/NN_server/Models/detection_250_1.py b/NN_server/Models/detection_250_1.py new file mode 100644 index 0000000..4fa1393 --- /dev/null +++ b/NN_server/Models/detection_250_1.py @@ -0,0 +1,154 @@ +import torchsig.transforms.transforms as transform +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 16) + dpi = 64 + + signal = np.vectorize(complex)(data[0], data[1]) + spec = transform.Spectrogram(nperseg=1024) + spectr = np.array(spec(signal)[:, :figsize[0] * dpi]) + mag = np.abs(signal) + real = signal.real + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf2 = io.BytesIO() + fig2.savefig(buf2, format="png", dpi=dpi) + buf2.seek(0) + img_arr2 = np.frombuffer(buf2.getvalue(), dtype=np.uint8) + buf2.close() + img2 = cv2.imdecode(img_arr2, 1) + img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + fig3 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(mag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf3 = io.BytesIO() + fig3.savefig(buf3, format="png", dpi=dpi) + buf3.seek(0) + img_arr3 = np.frombuffer(buf3.getvalue(), dtype=np.uint8) + buf3.close() + img3 = cv2.imdecode(img_arr3, 1) + img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig3) + + resize = (256,256) + resized_real = cv2.resize(img2, resize) + resized_mag = cv2.resize(img3, resize) + resized_spectr = cv2.resize(spectr, resize) + img = np.array([resized_real, resized_mag, resized_spectr]) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Linear(in_features=512, out_features=num_classes, bias=True) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/detection_250_2.py b/NN_server/Models/detection_250_2.py new file mode 100644 index 0000000..aa05414 --- /dev/null +++ b/NN_server/Models/detection_250_2.py @@ -0,0 +1,154 @@ +import torchsig.torchsig.transforms as transform +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 16) + dpi = 64 + + signal = np.vectorize(complex)(data[0], data[1]) + spec = transform.Spectrogram(nperseg=1024) + spectr = np.array(spec(signal)[:, :figsize[0] * dpi]) + mag = np.abs(signal) + real = signal.real + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf2 = io.BytesIO() + fig2.savefig(buf2, format="png", dpi=dpi) + buf2.seek(0) + img_arr2 = np.frombuffer(buf2.getvalue(), dtype=np.uint8) + buf2.close() + img2 = cv2.imdecode(img_arr2, 1) + img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + fig3 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(mag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf3 = io.BytesIO() + fig3.savefig(buf3, format="png", dpi=dpi) + buf3.seek(0) + img_arr3 = np.frombuffer(buf3.getvalue(), dtype=np.uint8) + buf3.close() + img3 = cv2.imdecode(img_arr3, 1) + img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig3) + + resize = (256, 256) + resized_real = cv2.resize(img2, resize) + resized_mag = cv2.resize(img3, resize) + resized_spectr = cv2.resize(spectr, resize) + img = np.array([resized_real, resized_mag, resized_spectr]) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Linear(in_features=512, out_features=num_classes, bias=True) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/detection_250_3_2444.py b/NN_server/Models/detection_250_3_2444.py new file mode 100644 index 0000000..e9466ca --- /dev/null +++ b/NN_server/Models/detection_250_3_2444.py @@ -0,0 +1,154 @@ +import torchsig.transforms.transforms as transform +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 16) + dpi = 64 + + signal = np.vectorize(complex)(data[0], data[1]) + spec = transform.Spectrogram(nperseg=1024) + spectr = np.array(spec(signal)[:, :figsize[0] * dpi]) + mag = np.abs(signal) + real = signal.real + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf2 = io.BytesIO() + fig2.savefig(buf2, format="png", dpi=dpi) + buf2.seek(0) + img_arr2 = np.frombuffer(buf2.getvalue(), dtype=np.uint8) + buf2.close() + img2 = cv2.imdecode(img_arr2, 1) + img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + fig3 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(mag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf3 = io.BytesIO() + fig3.savefig(buf3, format="png", dpi=dpi) + buf3.seek(0) + img_arr3 = np.frombuffer(buf3.getvalue(), dtype=np.uint8) + buf3.close() + img3 = cv2.imdecode(img_arr3, 1) + img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig3) + + resize = (256, 256) + resized_real = cv2.resize(img2, resize) + resized_mag = cv2.resize(img3, resize) + resized_spectr = cv2.resize(spectr, resize) + img = np.array([resized_real, resized_mag, resized_spectr]) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Linear(in_features=512, out_features=num_classes, bias=True) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/detection_640_1.py b/NN_server/Models/detection_640_1.py new file mode 100644 index 0000000..6b65159 --- /dev/null +++ b/NN_server/Models/detection_640_1.py @@ -0,0 +1,151 @@ +import torchsig.transforms.transforms as transform +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 16) + dpi = 64 + + signal = np.vectorize(complex)(data[0], data[1]) + spec = transform.Spectrogram(nperseg=1024) + spectr = np.array(spec(signal)[:, :figsize[0] * dpi]) + mag = np.abs(signal) + real = signal.real + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf2 = io.BytesIO() + fig2.savefig(buf2, format="png", dpi=dpi) + buf2.seek(0) + img_arr2 = np.frombuffer(buf2.getvalue(), dtype=np.uint8) + buf2.close() + img2 = cv2.imdecode(img_arr2, 1) + img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + fig3 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(mag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf3 = io.BytesIO() + fig3.savefig(buf3, format="png", dpi=dpi) + buf3.seek(0) + img_arr3 = np.frombuffer(buf3.getvalue(), dtype=np.uint8) + buf3.close() + img3 = cv2.imdecode(img_arr3, 1) + img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig3) + + img = np.array([img2, img3, spectr]) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = torch.nn.Sequential(torch.nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + print(model) + model.fc = nn.Linear(in_features=512, out_features=num_classes, bias=True) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/ensemble_1200.py b/NN_server/Models/ensemble_1200.py new file mode 100644 index 0000000..69ee5f1 --- /dev/null +++ b/NN_server/Models/ensemble_1200.py @@ -0,0 +1,252 @@ +import torchsig.transforms.dataset_transforms as transform +import torchsig.transforms.functional as F +from importlib import import_module +from torchvision import models +import torch.nn as nn +import matplotlib +import numpy as np +import torch +import cv2 +import gc +import io + + +def pre_func_ensemble(data=None, src ='', ind_inference=0): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + + figsize = (16, 16) + dpi = 16 + + signal = np.vectorize(complex)(data[0], data[1]) + + # if int(ind_inference) <= 1500: + # np.save(src + '_inference_1200_' + str(ind_inference) + '.npy', signal) + print(0) + spectr = np.asarray(F.spectrogram(signal,fft_size=256,fft_stride=256), dtype=np.float32) + print('a') + print(spectr.shape) + print('b') + fig1 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + print(1) + + plt.plot(sigr, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig1) + print(2) + + fig2 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + + plt.plot(sigi, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + print(3) + + img = np.array([img1, img2, spectr[:,:figsize[0]*dpi]]) + + cv2.destroyAllWindows() + del signal + del spectr + del img1 + del img2 + del sigr + del sigi + del buf + del buf1 + del img_arr + del img_arr1 + cv2.destroyAllWindows() + gc.collect() + + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_ensemble(file_model='', file_config='', num_classes=None): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + torch.cuda.empty_cache() + model1 = models.resnet18(pretrained=False) + model2 = models.resnet50(pretrained=False) + model3 = models.resnet101(pretrained=False) + + num_classes = 2 + + model1.fc = nn.Linear(model1.fc.in_features, num_classes) + model2.fc = nn.Linear(model2.fc.in_features, num_classes) + model3.fc = nn.Linear(model3.fc.in_features, num_classes) + + class Ensemble(nn.Module): + def __init__(self, model1, model2, model3): + super(Ensemble, self).__init__() + self.model1 = model1 + self.model2 = model2 + self.model3 = model3 + self.fc = nn.Linear(3 * num_classes, num_classes) + + def forward(self, x): + x1 = self.model1(x) + x2 = self.model2(x) + x3 = self.model3(x) + x = torch.cat((x1, x2, x3), dim=1) + x = self.fc(x) + return x + + model = Ensemble(model1, model2, model3) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + del model1 + del model2 + del model3 + gc.collect() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_ensemble(data=None, model=None, mapping=None, shablon=''): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data).cpu(), 0).to(device) + + with torch.no_grad(): + output = model(img.float()) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + output = output.cpu() + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + del img + del label + del expon + del output + + cv2.destroyAllWindows() + gc.collect() + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_ensemble(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + + if int(ind_inference) <= 100: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_mod_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + plt.clf() + plt.cla() + plt.close() + cv2.destroyAllWindows() + gc.collect() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/ensemble_2400.py b/NN_server/Models/ensemble_2400.py new file mode 100644 index 0000000..380d436 --- /dev/null +++ b/NN_server/Models/ensemble_2400.py @@ -0,0 +1,252 @@ +import torchsig.transforms.dataset_transforms as transform +import torchsig.transforms.functional as F +from importlib import import_module +from torchvision import models +import torch.nn as nn +import matplotlib +import numpy as np +import torch +import cv2 +import gc +import io + + +def pre_func_ensemble(data=None, src ='', ind_inference=0): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + + figsize = (16, 16) + dpi = 16 + + signal = np.vectorize(complex)(data[0], data[1]) + + # if int(ind_inference) <= 1500: + # np.save(src + '_inference_2400_' + str(ind_inference) + '.npy', signal) + print(0) + spectr = np.asarray(F.spectrogram(signal,fft_size=256,fft_stride=256), dtype=np.float32) + print('a') + print(spectr.shape) + print('b') + fig1 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + print(1) + + plt.plot(sigr, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig1) + print(2) + + fig2 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + + plt.plot(sigi, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + print(3) + + img = np.array([img1, img2, spectr[:,:figsize[0]*dpi]]) + + cv2.destroyAllWindows() + del signal + del spectr + del img1 + del img2 + del sigr + del sigi + del buf + del buf1 + del img_arr + del img_arr1 + cv2.destroyAllWindows() + gc.collect() + + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_ensemble(file_model='', file_config='', num_classes=None): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + torch.cuda.empty_cache() + model1 = models.resnet18(pretrained=False) + model2 = models.resnet50(pretrained=False) + model3 = models.resnet101(pretrained=False) + + num_classes = 2 + + model1.fc = nn.Linear(model1.fc.in_features, num_classes) + model2.fc = nn.Linear(model2.fc.in_features, num_classes) + model3.fc = nn.Linear(model3.fc.in_features, num_classes) + + class Ensemble(nn.Module): + def __init__(self, model1, model2, model3): + super(Ensemble, self).__init__() + self.model1 = model1 + self.model2 = model2 + self.model3 = model3 + self.fc = nn.Linear(3 * num_classes, num_classes) + + def forward(self, x): + x1 = self.model1(x) + x2 = self.model2(x) + x3 = self.model3(x) + x = torch.cat((x1, x2, x3), dim=1) + x = self.fc(x) + return x + + model = Ensemble(model1, model2, model3) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + del model1 + del model2 + del model3 + gc.collect() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_ensemble(data=None, model=None, mapping=None, shablon=''): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data).cpu(), 0).to(device) + + with torch.no_grad(): + output = model(img.float()) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + output = output.cpu() + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + del img + del label + del expon + del output + + cv2.destroyAllWindows() + gc.collect() + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_ensemble(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + + if int(ind_inference) <= 100: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_mod_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + plt.clf() + plt.cla() + plt.close() + cv2.destroyAllWindows() + gc.collect() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/ensemble_915.py b/NN_server/Models/ensemble_915.py new file mode 100644 index 0000000..f87ca8e --- /dev/null +++ b/NN_server/Models/ensemble_915.py @@ -0,0 +1,252 @@ +import torchsig.transforms.dataset_transforms as transform +import torchsig.transforms.functional as F +from importlib import import_module +from torchvision import models +import torch.nn as nn +import matplotlib +import numpy as np +import torch +import cv2 +import gc +import io + + +def pre_func_ensemble(data=None, src ='', ind_inference=0): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + + figsize = (16, 16) + dpi = 16 + + signal = np.vectorize(complex)(data[0], data[1]) + + # if int(ind_inference) <= 1500: + # np.save(src + '_inference_915_' + str(ind_inference) + '.npy', signal) + print(0) + spectr = np.asarray(F.spectrogram(signal,fft_size=256,fft_stride=256), dtype=np.float32) + print('a') + print(spectr.shape) + print('b') + fig1 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + print(1) + + plt.plot(sigr, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig1) + print(2) + + fig2 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + + plt.plot(sigi, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + print(3) + + img = np.array([img1, img2, spectr[:,:figsize[0]*dpi]]) + + cv2.destroyAllWindows() + del signal + del spectr + del img1 + del img2 + del sigr + del sigi + del buf + del buf1 + del img_arr + del img_arr1 + cv2.destroyAllWindows() + gc.collect() + + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_ensemble(file_model='', file_config='', num_classes=None): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + torch.cuda.empty_cache() + model1 = models.resnet18(pretrained=False) + model2 = models.resnet50(pretrained=False) + model3 = models.resnet101(pretrained=False) + + num_classes = 2 + + model1.fc = nn.Linear(model1.fc.in_features, num_classes) + model2.fc = nn.Linear(model2.fc.in_features, num_classes) + model3.fc = nn.Linear(model3.fc.in_features, num_classes) + + class Ensemble(nn.Module): + def __init__(self, model1, model2, model3): + super(Ensemble, self).__init__() + self.model1 = model1 + self.model2 = model2 + self.model3 = model3 + self.fc = nn.Linear(3 * num_classes, num_classes) + + def forward(self, x): + x1 = self.model1(x) + x2 = self.model2(x) + x3 = self.model3(x) + x = torch.cat((x1, x2, x3), dim=1) + x = self.fc(x) + return x + + model = Ensemble(model1, model2, model3) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + del model1 + del model2 + del model3 + gc.collect() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_ensemble(data=None, model=None, mapping=None, shablon=''): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data).cpu(), 0).to(device) + + with torch.no_grad(): + output = model(img.float()) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + output = output.cpu() + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + del img + del label + del expon + del output + + cv2.destroyAllWindows() + gc.collect() + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_ensemble(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + + if int(ind_inference) <= 100: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_mod_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close(fig) + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + plt.clf() + plt.cla() + plt.close() + cv2.destroyAllWindows() + gc.collect() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/identification_1.py b/NN_server/Models/identification_1.py new file mode 100644 index 0000000..0665fc2 --- /dev/null +++ b/NN_server/Models/identification_1.py @@ -0,0 +1,163 @@ +import torchsig.transforms.transforms as transform +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 16) + dpi = 64 + + signal = np.vectorize(complex)(data[0], data[1]) + spec = transform.Spectrogram(nperseg=1024) + spectr = np.array(spec(signal)[:, :figsize[0] * dpi]) + mag = np.abs(signal) + real = signal.real + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf2 = io.BytesIO() + fig2.savefig(buf2, format="png", dpi=dpi) + buf2.seek(0) + img_arr2 = np.frombuffer(buf2.getvalue(), dtype=np.uint8) + buf2.close() + img2 = cv2.imdecode(img_arr2, 1) + img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + fig3 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(mag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf3 = io.BytesIO() + fig3.savefig(buf3, format="png", dpi=dpi) + buf3.seek(0) + img_arr3 = np.frombuffer(buf3.getvalue(), dtype=np.uint8) + buf3.close() + img3 = cv2.imdecode(img_arr3, 1) + img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig3) + + resize = (256, 256) + resized_real = cv2.resize(img2, resize) + resized_mag = cv2.resize(img3, resize) + resized_spectr = cv2.resize(spectr, resize) + img = np.array([resized_real, resized_mag, resized_spectr]) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + torch.cuda.empty_cache() + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = nn.Sequential(nn.Conv2d(2, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential( + nn.Linear(in_features=512, out_features=128, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=32, out_features=5, bias=True) + ) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/identification_2.py b/NN_server/Models/identification_2.py new file mode 100644 index 0000000..21b6a71 --- /dev/null +++ b/NN_server/Models/identification_2.py @@ -0,0 +1,163 @@ +import torchsig.transforms.transforms as transform +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 16) + dpi = 64 + + signal = np.vectorize(complex)(data[0], data[1]) + spec = transform.Spectrogram(nperseg=1024) + spectr = np.array(spec(signal)[:, :figsize[0] * dpi]) + mag = np.abs(signal) + real = signal.real + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf2 = io.BytesIO() + fig2.savefig(buf2, format="png", dpi=dpi) + buf2.seek(0) + img_arr2 = np.frombuffer(buf2.getvalue(), dtype=np.uint8) + buf2.close() + img2 = cv2.imdecode(img_arr2, 1) + img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + fig3 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(mag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf3 = io.BytesIO() + fig3.savefig(buf3, format="png", dpi=dpi) + buf3.seek(0) + img_arr3 = np.frombuffer(buf3.getvalue(), dtype=np.uint8) + buf3.close() + img3 = cv2.imdecode(img_arr3, 1) + img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig3) + + resize = (256, 256) + resized_real = cv2.resize(img2, resize) + resized_mag = cv2.resize(img3, resize) + resized_spectr = cv2.resize(spectr, resize) + img = np.array([resized_real, resized_mag, resized_spectr]) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + torch.cuda.empty_cache() + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = nn.Sequential(nn.Conv2d(3, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential( + nn.Linear(in_features=512, out_features=128, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=32, out_features=5, bias=True) + ) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/identification_3.py b/NN_server/Models/identification_3.py new file mode 100644 index 0000000..1bc045b --- /dev/null +++ b/NN_server/Models/identification_3.py @@ -0,0 +1,208 @@ +import torchsig.transforms.transforms as transform +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import matplotlib +import numpy as np +import mlconfig +import torch +import cv2 +import gc +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + matplotlib.use('Agg') + plt.ioff() + + figsize = (8, 8) + dpi = 32 + + signal = np.vectorize(complex)(data[0], data[1]) + spec = transform.Spectrogram(nperseg=256) + spectr = np.array(spec(signal)[:, :figsize[0] * dpi]) + mag = np.abs(signal) + real = signal.real + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf2 = io.BytesIO() + fig2.savefig(buf2, format="png", dpi=dpi) + buf2.seek(0) + img_arr2 = np.frombuffer(buf2.getvalue(), dtype=np.uint8) + buf2.close() + img2 = cv2.imdecode(img_arr2, 1) + img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + fig3 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(mag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf3 = io.BytesIO() + fig3.savefig(buf3, format="png", dpi=dpi) + buf3.seek(0) + img_arr3 = np.frombuffer(buf3.getvalue(), dtype=np.uint8) + buf3.close() + img3 = cv2.imdecode(img_arr3, 1) + img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig3) + + resize = (256, 256) + resized_real = cv2.resize(img2, resize) + resized_mag = cv2.resize(img3, resize) + # resized_spectr = cv2.resize(spectr, resize) + img = np.array([resized_real, resized_mag, spectr]) + + cv2.destroyAllWindows() + del signal + del spec + del spectr + del real + del mag + del buf2 + del img_arr2 + del img2 + del buf3 + del img_arr3 + del img3 + del resized_real + del resized_mag + cv2.destroyAllWindows() + gc.collect() + + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + matplotlib.use('Agg') + plt.ioff() + torch.cuda.empty_cache() + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = nn.Sequential(nn.Conv2d(3, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential( + nn.Linear(in_features=512, out_features=128, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=32, out_features=5, bias=True) + ) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + gc.collect() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img.float()) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + del label + del expon + del output + cv2.destroyAllWindows() + gc.collect() + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + matplotlib.use('Agg') + plt.ioff() + + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/identification_4.py b/NN_server/Models/identification_4.py new file mode 100644 index 0000000..32710f2 --- /dev/null +++ b/NN_server/Models/identification_4.py @@ -0,0 +1,163 @@ +import torchsig.transforms.transforms as transform +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import numpy as np +import mlconfig +import torch +import cv2 +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + figsize = (16, 16) + dpi = 64 + + signal = np.vectorize(complex)(data[0], data[1]) + spec = transform.Spectrogram(nperseg=1024) + spectr = np.array(spec(signal)[:, :figsize[0] * dpi]) + mag = np.abs(signal) + real = signal.real + + fig2 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(real, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf2 = io.BytesIO() + fig2.savefig(buf2, format="png", dpi=dpi) + buf2.seek(0) + img_arr2 = np.frombuffer(buf2.getvalue(), dtype=np.uint8) + buf2.close() + img2 = cv2.imdecode(img_arr2, 1) + img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + fig3 = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + + plt.plot(mag, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + buf3 = io.BytesIO() + fig3.savefig(buf3, format="png", dpi=dpi) + buf3.seek(0) + img_arr3 = np.frombuffer(buf3.getvalue(), dtype=np.uint8) + buf3.close() + img3 = cv2.imdecode(img_arr3, 1) + img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig3) + + resize = (256, 256) + resized_real = cv2.resize(img2, resize) + resized_mag = cv2.resize(img3, resize) + resized_spectr = cv2.resize(spectr, resize) + img = np.array([resized_real, resized_mag, resized_spectr]) + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + torch.cuda.empty_cache() + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = nn.Sequential(nn.Conv2d(3, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential( + nn.Linear(in_features=512, out_features=128, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=32, out_features=5, bias=True) + ) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img.float()) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/identification_final.py b/NN_server/Models/identification_final.py new file mode 100644 index 0000000..859c70a --- /dev/null +++ b/NN_server/Models/identification_final.py @@ -0,0 +1,216 @@ +import torchsig.transforms.transforms as transform +import torchsig.transforms.functional as F +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import matplotlib +import numpy as np +import mlconfig +import torch +import cv2 +import gc +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + import matplotlib.pyplot as plt + matplotlib.use('Agg') + plt.ioff() + + figsize = (16, 16) + dpi = 16 + + signal = np.vectorize(complex)(data[0], data[1]) + + # if int(ind_inference) <= 1500: + # np.save(src + '_inference_2400_' + str(ind_inference) + '.npy', signal) + print(0) + spectr = np.asarray(F.spectrogram(signal,fft_size=256,fft_stride=256), dtype=np.float32) + print('a') + print(spectr.shape) + print('b') + fig1 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + print(1) + + plt.plot(sigr, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig1) + print(2) + + fig2 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + + plt.plot(sigi, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + print(3) + + img = np.array([img1, img2, spectr[:,:figsize[0]*dpi]]) + + cv2.destroyAllWindows() + del signal + del spectr + del img1 + del img2 + del sigr + del sigi + del buf + del buf1 + del img_arr + del img_arr1 + cv2.destroyAllWindows() + gc.collect() + + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + matplotlib.use('Agg') + plt.ioff() + torch.cuda.empty_cache() + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = nn.Sequential(nn.Conv2d(3, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential( + nn.Linear(in_features=512, out_features=128, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=32, out_features=16, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.5, inplace=False), + nn.Linear(in_features=16, out_features=3, bias=True) + ) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + gc.collect() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img.float()) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + del label + del expon + del output + cv2.destroyAllWindows() + gc.collect() + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + matplotlib.use('Agg') + plt.ioff() + + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/identification_final_2.py b/NN_server/Models/identification_final_2.py new file mode 100644 index 0000000..a816c38 --- /dev/null +++ b/NN_server/Models/identification_final_2.py @@ -0,0 +1,206 @@ +import torchsig.transforms.transforms as transform +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import matplotlib +import numpy as np +import mlconfig +import torch +import cv2 +import gc +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + matplotlib.use('Agg') + plt.ioff() + + figsize = (8, 8) + dpi = 32 + + signal = np.vectorize(complex)(data[0], data[1]) + spec = transform.Spectrogram(nperseg=256) + spectr = np.array(spec(signal)[:,:figsize[0] * dpi]) + fig1 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + + plt.plot(sigr, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig1) + + fig2 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + + plt.plot(sigi, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + img = np.array([img1, img2, spectr]) + + cv2.destroyAllWindows() + del signal + del spec + del spectr + del img1 + del img2 + del sigr + del sigi + del buf + del buf1 + del img_arr + del img_arr1 + cv2.destroyAllWindows() + gc.collect() + + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + matplotlib.use('Agg') + plt.ioff() + torch.cuda.empty_cache() + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = nn.Sequential(nn.Conv2d(3, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential( + nn.Linear(in_features=512, out_features=128, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.7, inplace=False), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.7, inplace=False), + nn.Linear(in_features=32, out_features=16, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.7, inplace=False), + nn.Linear(in_features=16, out_features=3, bias=True) + ) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + gc.collect() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img.float()) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + del label + del expon + del output + cv2.destroyAllWindows() + gc.collect() + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + matplotlib.use('Agg') + plt.ioff() + + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/identification_new_2400.py b/NN_server/Models/identification_new_2400.py new file mode 100644 index 0000000..8e3076f --- /dev/null +++ b/NN_server/Models/identification_new_2400.py @@ -0,0 +1,208 @@ +import torchsig.transforms.dataset_transforms as transform +import torchsig.transforms.functional as F +from importlib import import_module +import matplotlib.pyplot as plt +import torch.nn as nn +import matplotlib +import numpy as np +import mlconfig +import torch +import cv2 +import gc +import io + + +def pre_func_resnet18(data=None, src ='', ind_inference=0): + try: + matplotlib.use('Agg') + plt.ioff() + + figsize = (8, 8) + dpi = 32 + + signal = np.vectorize(complex)(data[0], data[1]) + np.save(src + '_inference_' + str(ind_inference) + '.npy', signal) + spec = transform.Spectrogram(nperseg=256, fft_size=32) + spectr = np.array(spec(signal)[:,:figsize[0] * dpi]) + fig1 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + + plt.plot(sigr, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf1 = io.BytesIO() + fig1.savefig(buf1, format="png", dpi=dpi) + buf1.seek(0) + img_arr1 = np.frombuffer(buf1.getvalue(), dtype=np.uint8) + buf1.close() + img1 = cv2.imdecode(img_arr1, 1) + img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig1) + + fig2 = plt.figure(figsize = figsize) + plt.axes(ylim=(-1, 1)) + sigr = signal.real + sigi = signal.imag + + plt.plot(sigi, color='black') + plt.gca().set_axis_off() + plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0) + plt.margins(0,0) + buf = io.BytesIO() + fig2.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + img = cv2.imdecode(img_arr, 1) + img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + plt.clf() + plt.cla() + plt.close() + plt.close(fig2) + + img = np.array([img1, img2, spectr]) + + cv2.destroyAllWindows() + del signal + del spec + del spectr + del img1 + del img2 + del sigr + del sigi + del buf + del buf1 + del img_arr + del img_arr1 + cv2.destroyAllWindows() + gc.collect() + + print('Подготовка данных завершена') + print() + return img + + except Exception as e: + print(str(e)) + return None + + +def build_func_resnet18(file_model='', file_config='', num_classes=None): + try: + matplotlib.use('Agg') + plt.ioff() + torch.cuda.empty_cache() + config = mlconfig.load(file_config) + model = getattr(import_module(config.model.architecture.rsplit('.', maxsplit=1)[0]), + config.model.architecture.rsplit('.', maxsplit=1)[1])() + model.conv1 = nn.Sequential(nn.Conv2d(3, 3, kernel_size=(7, 7), stride=(2, 2), + padding=(3, 3), bias=False), model.conv1) + model.fc = nn.Sequential( + nn.Linear(in_features=512, out_features=128, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.7, inplace=False), + nn.Linear(in_features=128, out_features=32, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.7, inplace=False), + nn.Linear(in_features=32, out_features=16, bias=True), + nn.ReLU(inplace=True), + nn.Dropout(p=0.7, inplace=False), + nn.Linear(in_features=16, out_features=3, bias=True) + ) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if device != 'cpu': + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + gc.collect() + + print('Инициализация модели завершена') + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_resnet18(data=None, model=None, mapping=None, shablon=''): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + device = 'cuda' if torch.cuda.is_available() else 'cpu' + img = torch.unsqueeze(torch.tensor(data), 0).to(device) + + with torch.no_grad(): + output = model(img.float()) + _, predict = torch.max(output.data, 1) + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print('PREDICTION' + shablon + ': ' + prediction) + + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + del label + del expon + del output + cv2.destroyAllWindows() + gc.collect() + print('Уверенность' + shablon + ' в предсказании: ' + str(probability)) + + print('Инференс завершен') + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_resnet18(src='', model_type='', prediction='', model_id=0, ind_inference=0, data=None): + try: + matplotlib.use('Agg') + plt.ioff() + + fig, ax = plt.subplots() + ax.imshow(data[0], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_real_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[1], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_imag_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + fig, ax = plt.subplots() + ax.imshow(data[2], cmap='gray') + plt.savefig(src + '_inference_' + str(ind_inference) + '_' + prediction + '_spec_' + str( + model_id) + '_' + model_type + '.png') + plt.clf() + plt.cla() + plt.close() + + del fig + del ax + cv2.destroyAllWindows() + gc.collect() + + print('Постобработка завершена') + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/antlr4-python3-runtime-4.11.1.tar.gz b/NN_server/antlr4-python3-runtime-4.11.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..64bcd9214a41ffcb001dc061ea7dbe08d85bbcbc GIT binary patch literal 116945 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 gcNU9|5hoe= 0.95))) or (int(freq) == 1200 and (prediction_list[0] in ['drone'] and float(probability) >= 0.95)): + result += 8 + if int(freq) in [915]: + result = 0 + if int(freq) in []: + result = 8 + data_to_send={ + 'freq': str(freq), + 'amplitude': result + #'triggered': False if result < 7 else True, + #'light_len': result + } + response = requests.post("http://{0}:{1}/process_data".format(gen_server_ip, gen_server_port), json=data_to_send) + if response.status_code == 200: + print("Данные успешно отправлены!") + print("Частота: " + str(freq)) + print("Отправлено светодиодов: " + str(result)) + else: + print("Ошибка при отправке данных: ", response.status_code) + except Exception as exc: + print(str(exc)) + break + + Model.get_inc_ind_inference() + print() + print('#' * 100) + + for alg in alg_list: + print('-' * 100) + print(str(alg)) + alg.get_inference([np.asarray(data['data_real'], dtype=np.float32), np.asarray(data['data_imag'], dtype=np.float32)]) + print('-' * 100) + print() + + #Algorithm.get_inc_ind_inference() + print() + print('#' * 100) + + del data + gc.collect() + + return jsonify(result_msg) + + except Exception as exc: + print(str(exc)) + + +''' +def run_flask(): + app.run(host=config['SERVER_IP'], port=int(config['SERVER_PORT'])) + + +async def process_tasks(): + workers = [asyncio.create_task(worker(queue=queue, semaphore=semaphore)) for _ in range(2)] + await asyncio.gather(*workers) + + +async def main(): + asyncio.create_task(process_tasks()) + + flask_thread = threading.Thread(target=run_flask) + flask_thread.start() + + while True: + if queue.qsize() <= 1: + asyncio.create_task(process_tasks()) + await asyncio.sleep(1) + + +@app.route('/receive_data', methods=['POST']) +def add_task(): + queue_size = queue.qsize() + if queue_size > 1: + return {} + + print() + data = json.loads(request.json) + print('#' * 100) + print('Получен пакет ' + str(Model.get_ind_inference())) + freq = int(data['freq']) + print('Частота ' + str(freq)) + + + result_msg = {} + for model in model_list: + if str(freq) in model.get_model_name(): + print('-' * 100) + print(str(model)) + result_msg[str(model.get_model_name())] = {'freq': freq} + asyncio.run_coroutine_threadsafe(queue.put({'freq': freq, 'model': model, 'data': data}), loop) + do_inference(model=model, data=data, freq=freq) + break + + del data + gc.collect() + return jsonify(result_msg) + + +async def worker(queue, semaphore): + while True: + task = await queue.get() + if task is None: + break + async with semaphore: + try: + await do_inference(model=task['model'], data=task['data'], freq=task['freq']) + except Exception as e: + print(str(e)) + print(results) + queue.task_done() + + +async def do_inference(model=None, data=None, freq=0): + prediction_list = [] + print("Длина очереди" + str(queue.qsize())) + inference(model=model, data=data, freq=freq) + + try: + results = [] + for pred in prediction_list: + if pred[1] == 'drone': + results.append([pred[0],8]) + else: + results.append([pred[0],0]) + for result in results: + try: + data_to_send={ + 'freq': result[0], + 'amplitude': result[1], + 'triggered': False if result[1] < 7 else True, + 'light_len': result[1] + } + response = requests.post("http://{0}:{1}/process_data".format(gen_server_ip, gen_server_port), json=data_to_send) + await response.text + if response.status_code == 200: + print("Данные успешно отправлены!") + print("Отправлено светодиодов: " + str(data_to_send['light_len'])) + else: + print("Ошибка при отправке данных: ", response.status_code) + except Exception as exc: + print(str(exc)) + except Exception as exc: + print(str(exc)) + + Model.get_inc_ind_inference() + print() + print('#' * 100) + + del data + gc.collect() + + +def inference(model=None, data=None, freq=0): + prediction, probability = model.get_inference([np.asarray(data['data_real'], dtype=np.float32), np.asarray(data['data_imag'], dtype=np.float32)]) + result_msg[str(model.get_model_name())]['prediction'] = prediction + result_msg[str(model.get_model_name())]['probability'] = str(probability) + queue_size = queue.qsize() + print(queue_size) + prediction_list.append([freq, prediction]) + print('-' * 100) + print() + + +if __name__ == '__main__': + init_data_for_inference() + #asyncio.run(main) + loop.run_until_complete(main()) +''' + +def run_flask(): + print(config['SERVER_IP']) + app.run(host=config['SERVER_IP'], port=int(config['SERVER_PORT'])) + + +if __name__ == '__main__': + init_data_for_inference() + + flask_thread = threading.Thread(target=run_flask) + flask_thread.start() + + #app.run(host=config['SERVER_IP'], port=int(config['SERVER_PORT'])) diff --git a/README.md b/README.md new file mode 100644 index 0000000..8dc8ea2 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# DroneDetector v2 + +Отдельный проектный контур (без миграции legacy), в котором: +- SDR-сканеры работают **на хосте** под `systemd`; +- `server_to_master` и `NN_server` работают в **Docker Compose**; +- весь runtime-конфиг хранится в **одном корневом `.env`**. + +## 1. Быстрый старт + +### Prerequisites +- Ubuntu/Debian (apt) +- NVIDIA GPU + установленный драйвер (`nvidia-smi` должен работать) +- Интернет для установки пакетов и сборки Docker-образов +- HackRF + GNU Radio стек (будет установлен через `install_all.sh`) + +### Установка и запуск +```bash +cd /home/sibscience-4/from_ssh/DroneDetector +chmod +x install_all.sh +./install_all.sh +``` + +`install_all.sh`: +1. выполняет preflight; +2. ставит host non-python зависимости SDR; +3. настраивает Docker + NVIDIA runtime; +4. поднимает compose сервисы; +5. устанавливает/перезапускает `systemd` unit'ы; +6. проверяет статус, при ошибке печатает логи. + +## 2. Матрица сервисов + +### Host / systemd (SDR) +- `dronedetector-sdr-433.service` -> `src/main_433.py` +- `dronedetector-sdr-750.service` -> `src/main_750.py` +- `dronedetector-sdr-868.service` -> `src/main_868.py` +- `dronedetector-sdr-3300.service` -> `src/main_3300.py` +- `dronedetector-sdr-4500.service` -> `src/main_4500.py` +- `dronedetector-sdr-5200.service` -> `src/main_5200.py` +- `dronedetector-sdr-5800.service` -> `src/main_5800.py` +- `dronedetector-sdr-915.service` -> `orange_scripts/main_915.py` +- `dronedetector-sdr-1200.service` -> `orange_scripts/main_1200.py` +- `dronedetector-sdr-2400.service` -> `orange_scripts/main_2400.py` + +### Docker Compose +- `dronedetector-server-to-master` -> `src/server_to_master.py` +- `dronedetector-nn-server` -> `NN_server/server.py` + +Compose unit: +- `dronedetector-compose.service` + +## 3. Конфигурация + +Единственный источник runtime-конфига: `./.env`. + +Все entrypoint'ы загружают root `.env` через `common/runtime.py` и валидируют обязательные переменные. При ошибке сервис падает сразу с понятным сообщением. + +## 4. API (без изменения контрактов) + +- NN_server: `POST /receive_data` +- server_to_master: `POST /process_data` + +Форматы payload/ответов сохранены в текущей логике сервисов. + +## 5. Диагностика + +### systemd +```bash +systemctl status dronedetector-sdr-*.service +journalctl -u dronedetector-sdr-868.service -n 200 --no-pager +systemctl status dronedetector-compose.service +journalctl -u dronedetector-compose.service -n 200 --no-pager +``` + +### docker compose +```bash +docker compose -f deploy/docker/docker-compose.yml ps +docker compose -f deploy/docker/docker-compose.yml logs dronedetector-server-to-master +docker compose -f deploy/docker/docker-compose.yml logs dronedetector-nn-server +``` + +## 6. Host non-python dependencies + +Устанавливаются `install_all.sh`: +- GNU Radio +- gr-osmosdr +- libhackrf/hackrf-tools (`hackrf` package) +- libusb +- udev-related runtime via distro packages + +SDR precheck перед каждым unit запуском: +- наличие `hackrf_info` +- наличие `gnuradio-config-info` +- импорт `osmosdr` +- детект устройства HackRF + +## 7. install_all.sh: параметры и поведение + +Скрипт idempotent: повторный запуск допустим. + +Что делает: +- preflight (OS, диск, `.env`, GPU) +- host deps +- `.venv-sdr` c `--system-site-packages` +- Docker Engine (если отсутствует) +- NVIDIA Container Toolkit +- `docker compose up -d --build` +- установка unit'ов в `/etc/systemd/system` +- verify + авто-логи при ошибке + +## 8. Типовые ошибки `.env` + +Примеры fail-fast сообщений: +- `[src/server_to_master.py] invalid .env configuration: ...` +- `[NN_server/server.py] no NN_* model entries configured` +- `[orange_scripts/compose_send_data_915.py] invalid .env configuration: ...` + +Частые причины: +- пустое обязательное поле (`SERVER_PORT`, `lochost`, `hack_868` и т.д.) +- неверный тип (`SERVER_PORT=abc`) +- неправильный serial HackRF (не найден среди `lsusb -v -d 1d50:6089 | grep iSerial`) + +## 9. Ручная приемка + +1. `./install_all.sh` выполняется до конца. +2. `docker compose -f deploy/docker/docker-compose.yml up -d` поднимает оба контейнера. +3. Все `dronedetector-sdr-*` имеют `active (running)`. +4. Тестовый POST в `NN_server /receive_data` доходит до `server_to_master /process_data`. +5. Контур работает минимум 1 минуту без падений. + diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..9201029 --- /dev/null +++ b/common/__init__.py @@ -0,0 +1 @@ +"""Shared runtime helpers for DroneDetector.""" diff --git a/common/runtime.py b/common/runtime.py new file mode 100644 index 0000000..bcc608b --- /dev/null +++ b/common/runtime.py @@ -0,0 +1,99 @@ +import os +import subprocess +from pathlib import Path +from typing import Callable, Dict, Any + +from dotenv import load_dotenv + + +class EnvValidationError(RuntimeError): + """Raised when required environment variables are missing or malformed.""" + + +def load_root_env(file_path: str) -> Path: + """Load repository root .env by walking up from file_path.""" + start = Path(file_path).resolve() + for parent in [start.parent, *start.parents]: + env_file = parent / ".env" + if env_file.exists(): + load_dotenv(env_file, override=True) + return env_file + raise EnvValidationError(f"Root .env was not found for {file_path}") + + +def as_int(raw: str) -> int: + return int(str(raw).strip()) + + +def as_float(raw: str) -> float: + return float(str(raw).strip()) + + +def as_str(raw: str) -> str: + value = str(raw).strip() + if value.startswith("\"") and value.endswith("\""): + value = value[1:-1] + if value.startswith("'") and value.endswith("'"): + value = value[1:-1] + return value + + +def as_bool(raw: str) -> bool: + value = as_str(raw).lower() + if value in {"1", "true", "yes", "y", "on"}: + return True + if value in {"0", "false", "no", "n", "off"}: + return False + raise ValueError("expected one of 1/0 true/false yes/no on/off") + + +def validate_env(source: str, schema: Dict[str, Callable[[str], Any]]) -> Dict[str, Any]: + """Validate required env vars against simple caster schema.""" + values: Dict[str, Any] = {} + errors = [] + + for key, caster in schema.items(): + raw = os.getenv(key) + if raw is None or str(raw).strip() == "": + errors.append(f"{key}: missing") + continue + try: + values[key] = caster(raw) + except Exception as exc: # pragma: no cover - used in runtime only + errors.append(f"{key}: invalid value {raw!r} ({exc})") + + if errors: + msg = "\n - " + "\n - ".join(errors) + raise EnvValidationError(f"[{source}] invalid .env configuration:{msg}") + + return values + + +def resolve_hackrf_index(serial_env_key: str, source: str) -> str: + """Resolve HackRF index from expected serial in env.""" + serial = validate_env(source, {serial_env_key: as_str})[serial_env_key] + + try: + output = subprocess.check_output( + "lsusb -v -d 1d50:6089 | grep iSerial", + shell=True, + text=True, + ) + except subprocess.CalledProcessError as exc: + raise EnvValidationError( + f"[{source}] could not read HackRF serials via lsusb: {exc}" + ) from exc + + lines = [line.strip() for line in output.splitlines() if line.strip()] + if not lines: + raise EnvValidationError( + f"[{source}] no HackRF devices found (lsusb returned empty serial list)" + ) + + serials = [line.split()[-1] for line in lines] + if serial not in serials: + raise EnvValidationError( + f"[{source}] serial {serial!r} not found among connected HackRF devices: {serials}" + ) + + return str(serials.index(serial)) diff --git a/deploy/docker/Dockerfile.nn_server b/deploy/docker/Dockerfile.nn_server new file mode 100644 index 0000000..5e25ff3 --- /dev/null +++ b/deploy/docker/Dockerfile.nn_server @@ -0,0 +1,34 @@ +FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04 + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + python3-venv \ + git \ + libglib2.0-0 \ + libsm6 \ + libxext6 \ + libxrender-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY deploy/requirements/nn_gpu_pinned.txt /tmp/nn_gpu_pinned.txt +COPY deploy/requirements/nn_common.txt /tmp/nn_common.txt + +RUN python3 -m pip install --no-cache-dir --upgrade pip && \ + python3 -m pip install --no-cache-dir -r /tmp/nn_gpu_pinned.txt && \ + python3 -m pip install --no-cache-dir -r /tmp/nn_common.txt + +COPY . /app + +RUN python3 -m pip install --no-cache-dir -e /app/torchsig + +WORKDIR /app +EXPOSE 8080 +CMD ["python3", "-m", "NN_server.server"] diff --git a/deploy/docker/Dockerfile.server_to_master b/deploy/docker/Dockerfile.server_to_master new file mode 100644 index 0000000..61156f2 --- /dev/null +++ b/deploy/docker/Dockerfile.server_to_master @@ -0,0 +1,20 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +COPY deploy/requirements/server_to_master.txt /tmp/requirements.txt +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r /tmp/requirements.txt + +COPY . /app + +EXPOSE 5000 +CMD ["python3", "-m", "src.server_to_master"] diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..11e6022 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,43 @@ +services: + dronedetector-server-to-master: + container_name: dronedetector-server-to-master + build: + context: ../.. + dockerfile: deploy/docker/Dockerfile.server_to_master + env_file: + - ../../.env + environment: + - PYTHONPATH=/app + working_dir: /app + command: ["python3", "-m", "src.server_to_master"] + restart: unless-stopped + ports: + - "5000:5000" + networks: + - dronedetector-net + + dronedetector-nn-server: + container_name: dronedetector-nn-server + build: + context: ../.. + dockerfile: deploy/docker/Dockerfile.nn_server + env_file: + - ../../.env + environment: + - PYTHONPATH=/app + working_dir: /app + command: ["python3", "-m", "NN_server.server"] + restart: unless-stopped + depends_on: + - dronedetector-server-to-master + ports: + - "8080:8080" + volumes: + - ../../NN_server/result:/app/NN_server/result + gpus: all + networks: + - dronedetector-net + +networks: + dronedetector-net: + name: dronedetector-net diff --git a/deploy/requirements/nn_common.txt b/deploy/requirements/nn_common.txt new file mode 100644 index 0000000..1dc4cb4 --- /dev/null +++ b/deploy/requirements/nn_common.txt @@ -0,0 +1,11 @@ +flask==3.1.0 +python-dotenv==1.0.1 +numpy==2.1.3 +matplotlib==3.10.0 +tqdm==4.67.1 +requests==2.32.3 +pyyaml==6.0.2 +mlconfig==0.3.2 +scikit-learn==1.6.0 +torchensemble==0.2.0 +opencv-python-headless==4.10.0.84 diff --git a/deploy/requirements/nn_gpu_pinned.txt b/deploy/requirements/nn_gpu_pinned.txt new file mode 100644 index 0000000..54a7131 --- /dev/null +++ b/deploy/requirements/nn_gpu_pinned.txt @@ -0,0 +1,6 @@ +--index-url https://download.pytorch.org/whl/cu128 +--extra-index-url https://pypi.org/simple + +torch==2.10.0+cu128 +torchvision==0.25.0+cu128 +torchaudio==2.10.0+cu128 diff --git a/deploy/requirements/sdr_host.txt b/deploy/requirements/sdr_host.txt new file mode 100644 index 0000000..eee105e --- /dev/null +++ b/deploy/requirements/sdr_host.txt @@ -0,0 +1,6 @@ +python-dotenv==1.0.1 +numpy==1.26.4 +requests==2.32.3 +pysmb==1.2.10 +pynmea2==1.19.0 +pyserial==3.5 diff --git a/deploy/requirements/server_to_master.txt b/deploy/requirements/server_to_master.txt new file mode 100644 index 0000000..d226832 --- /dev/null +++ b/deploy/requirements/server_to_master.txt @@ -0,0 +1,6 @@ +fastapi==0.115.6 +uvicorn[standard]==0.32.1 +httpx==0.28.1 +requests==2.32.3 +websockets==12.0 +python-dotenv==1.0.1 diff --git a/deploy/systemd/dronedetector-compose.service b/deploy/systemd/dronedetector-compose.service new file mode 100644 index 0000000..53f9473 --- /dev/null +++ b/deploy/systemd/dronedetector-compose.service @@ -0,0 +1,17 @@ +[Unit] +Description=DroneDetector Docker Compose Services +After=network-online.target docker.service +Wants=network-online.target +Requires=docker.service + +[Service] +Type=oneshot +WorkingDirectory=__PROJECT_ROOT__ +RemainAfterExit=yes +ExecStart=/usr/bin/docker compose -f __PROJECT_ROOT__/deploy/docker/docker-compose.yml up -d --build +ExecStop=/usr/bin/docker compose -f __PROJECT_ROOT__/deploy/docker/docker-compose.yml down +ExecReload=/usr/bin/docker compose -f __PROJECT_ROOT__/deploy/docker/docker-compose.yml up -d --build +TimeoutStartSec=0 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-1200.service b/deploy/systemd/dronedetector-sdr-1200.service new file mode 100644 index 0000000..832d7a4 --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-1200.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 1200 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python orange_scripts/main_1200.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-2400.service b/deploy/systemd/dronedetector-sdr-2400.service new file mode 100644 index 0000000..20e4c4d --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-2400.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 2400 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python orange_scripts/main_2400.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-3300.service b/deploy/systemd/dronedetector-sdr-3300.service new file mode 100644 index 0000000..2ce98a7 --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-3300.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 3300 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python src/main_3300.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-433.service b/deploy/systemd/dronedetector-sdr-433.service new file mode 100644 index 0000000..056ba55 --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-433.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 433 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python src/main_433.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-4500.service b/deploy/systemd/dronedetector-sdr-4500.service new file mode 100644 index 0000000..7634da2 --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-4500.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 4500 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python src/main_4500.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-5200.service b/deploy/systemd/dronedetector-sdr-5200.service new file mode 100644 index 0000000..57dcbdf --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-5200.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 5200 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python src/main_5200.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-5800.service b/deploy/systemd/dronedetector-sdr-5800.service new file mode 100644 index 0000000..80ce01b --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-5800.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 5800 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python src/main_5800.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-750.service b/deploy/systemd/dronedetector-sdr-750.service new file mode 100644 index 0000000..8eb551b --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-750.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 750 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python src/main_750.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-868.service b/deploy/systemd/dronedetector-sdr-868.service new file mode 100644 index 0000000..6c6607a --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-868.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 868 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python src/main_868.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/dronedetector-sdr-915.service b/deploy/systemd/dronedetector-sdr-915.service new file mode 100644 index 0000000..51e9bb7 --- /dev/null +++ b/deploy/systemd/dronedetector-sdr-915.service @@ -0,0 +1,19 @@ +[Unit] +Description=DroneDetector SDR Scanner 915 MHz +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=__RUN_USER__ +Group=__RUN_GROUP__ +WorkingDirectory=__PROJECT_ROOT__ +EnvironmentFile=__PROJECT_ROOT__/.env +Environment=PYTHONPATH=__PROJECT_ROOT__ +ExecStartPre=/usr/local/bin/dronedetector-precheck-sdr.sh +ExecStart=__PROJECT_ROOT__/.venv-sdr/bin/python orange_scripts/main_915.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/precheck-sdr.sh b/deploy/systemd/precheck-sdr.sh new file mode 100755 index 0000000..2c0800a --- /dev/null +++ b/deploy/systemd/precheck-sdr.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v hackrf_info >/dev/null 2>&1; then + echo "[dronedetector-precheck] hackrf_info not found. Install hackrf-tools/hackrf package." >&2 + exit 1 +fi + +if ! command -v gnuradio-config-info >/dev/null 2>&1; then + echo "[dronedetector-precheck] gnuradio-config-info not found. Install gnuradio." >&2 + exit 1 +fi + +if ! python3 -c "import osmosdr" >/dev/null 2>&1; then + echo "[dronedetector-precheck] Python module osmosdr not importable." >&2 + exit 1 +fi + +if ! hackrf_info 2>/dev/null | grep -q "Found HackRF"; then + echo "[dronedetector-precheck] HackRF device was not detected by hackrf_info." >&2 + exit 1 +fi diff --git a/install_all.sh b/install_all.sh new file mode 100755 index 0000000..ddbe3b5 --- /dev/null +++ b/install_all.sh @@ -0,0 +1,246 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="${PROJECT_ROOT}/deploy/docker/docker-compose.yml" +SYSTEMD_TARGET_DIR="/etc/systemd/system" +RUN_USER="${SUDO_USER:-${USER}}" +RUN_GROUP="$(id -gn "${RUN_USER}")" + +SDR_UNITS=( + dronedetector-sdr-433.service + dronedetector-sdr-750.service + dronedetector-sdr-868.service + dronedetector-sdr-3300.service + dronedetector-sdr-4500.service + dronedetector-sdr-5200.service + dronedetector-sdr-5800.service + dronedetector-sdr-915.service + dronedetector-sdr-1200.service + dronedetector-sdr-2400.service +) + +log() { + printf '[install_all] %s\n' "$*" +} + +die() { + printf '[install_all] ERROR: %s\n' "$*" >&2 + exit 1 +} + +print_failure_logs() { + log "Collecting diagnostics..." + systemctl --no-pager --full status dronedetector-compose.service || true + for unit in "${SDR_UNITS[@]}"; do + systemctl --no-pager --full status "$unit" || true + done + + if command -v docker >/dev/null 2>&1; then + docker compose -f "$COMPOSE_FILE" ps || true + docker compose -f "$COMPOSE_FILE" logs --tail=150 dronedetector-server-to-master || true + docker compose -f "$COMPOSE_FILE" logs --tail=150 dronedetector-nn-server || true + fi + + journalctl -u dronedetector-compose.service -n 150 --no-pager || true + for unit in "${SDR_UNITS[@]}"; do + journalctl -u "$unit" -n 120 --no-pager || true + done +} + +trap 'rc=$?; if [[ $rc -ne 0 ]]; then print_failure_logs; fi' EXIT + +require_root() { + if [[ "${EUID}" -ne 0 ]]; then + log "Switching to root via sudo..." + exec sudo -E bash "$0" "$@" + fi +} + +preflight() { + log "Preflight checks" + [[ -f "${PROJECT_ROOT}/.env" ]] || die "Missing ${PROJECT_ROOT}/.env" + [[ -f "${COMPOSE_FILE}" ]] || die "Missing ${COMPOSE_FILE}" + + if ! command -v apt-get >/dev/null 2>&1; then + die "This installer currently supports Debian/Ubuntu only (apt-get required)." + fi + + local free_mb + free_mb="$(df -Pm "${PROJECT_ROOT}" | awk 'NR==2 {print $4}')" + if [[ -z "$free_mb" || "$free_mb" -lt 10240 ]]; then + die "At least 10 GB free disk space is required." + fi + + if ! command -v nvidia-smi >/dev/null 2>&1; then + die "nvidia-smi is required. GPU/NVIDIA driver is not available on host." + fi + + log "Preflight OK" +} + +install_host_non_python_deps() { + log "Installing host non-python dependencies" + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gnupg \ + lsb-release \ + jq \ + git \ + python3 \ + python3-pip \ + python3-venv \ + build-essential \ + pkg-config \ + libusb-1.0-0 \ + libusb-1.0-0-dev \ + hackrf \ + gnuradio \ + gr-osmosdr +} + +setup_sdr_python_env() { + log "Setting up SDR python environment" + local venv_path="${PROJECT_ROOT}/.venv-sdr" + + if [[ ! -d "$venv_path" ]]; then + python3 -m venv --system-site-packages "$venv_path" + fi + + "$venv_path/bin/pip" install --upgrade pip + "$venv_path/bin/pip" install -r "${PROJECT_ROOT}/deploy/requirements/sdr_host.txt" + + chown -R "${RUN_USER}:${RUN_GROUP}" "$venv_path" +} + +install_docker_if_needed() { + if command -v docker >/dev/null 2>&1; then + log "Docker already installed" + return + fi + + log "Installing Docker Engine" + . /etc/os-release + local distro_id="${ID}" + if [[ "$distro_id" != "ubuntu" && "$distro_id" != "debian" ]]; then + die "Unsupported distro for Docker auto-install: ${distro_id}" + fi + + install -m 0755 -d /etc/apt/keyrings + curl -fsSL "https://download.docker.com/linux/${distro_id}/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${distro_id} \ + ${VERSION_CODENAME} stable" | tee /etc/apt/sources.list.d/docker.list >/dev/null + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + systemctl enable --now docker +} + +install_nvidia_container_toolkit() { + log "Installing/Configuring NVIDIA Container Toolkit" + + if ! dpkg -s nvidia-container-toolkit >/dev/null 2>&1; then + curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \ + gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg + + curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ + sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ + tee /etc/apt/sources.list.d/nvidia-container-toolkit.list + + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends nvidia-container-toolkit + fi + + nvidia-ctk runtime configure --runtime=docker + systemctl restart docker +} + +build_and_run_compose() { + log "Building and starting Docker services" + docker compose -f "$COMPOSE_FILE" up -d --build +} + +install_systemd_units() { + log "Installing systemd units" + + install -m 0755 "${PROJECT_ROOT}/deploy/systemd/precheck-sdr.sh" /usr/local/bin/dronedetector-precheck-sdr.sh + + local src dst + for src in "${PROJECT_ROOT}"/deploy/systemd/*.service; do + dst="${SYSTEMD_TARGET_DIR}/$(basename "$src")" + sed \ + -e "s|__PROJECT_ROOT__|${PROJECT_ROOT}|g" \ + -e "s|__RUN_USER__|${RUN_USER}|g" \ + -e "s|__RUN_GROUP__|${RUN_GROUP}|g" \ + "$src" > "$dst" + done + + systemctl daemon-reload + systemctl enable dronedetector-compose.service + systemctl restart dronedetector-compose.service + + for unit in "${SDR_UNITS[@]}"; do + systemctl enable "$unit" + systemctl restart "$unit" + done +} + +wait_for_systemd_active() { + local unit="$1" + local timeout_seconds="${2:-60}" + local i + + for ((i=0; i median else 1 + print(channel, median, flag) + + +def work(lvl): + global flag + global channel + global f_base + global f_step + global f_roof + global f + global EOCF + global signal_arr + + y = np.array(lvl).ravel() + signal_arr = np.concatenate((signal_arr, y), axis=None) + + if f >= f_roof: + f = f_base + signal_arr = [] + channel = 1 + return f, EOCF + else: + if flag == 0 and len(signal_arr) >= PARAMS['point_amount']: + median(signal_arr[:PARAMS['point_amount']]) + signal_arr = [] + if flag == 0: + f += f_step + channel += 1 + if len(signal_arr) >= PARAMS['split_size']: + send_data(signal_arr[:PARAMS['split_size']]) + flag = 0 + signal_arr = [] + channel += 1 + f += f_step + return f, EOCF diff --git a/orange_scripts/compose_send_data_2400.py b/orange_scripts/compose_send_data_2400.py new file mode 100644 index 0000000..b2eddcc --- /dev/null +++ b/orange_scripts/compose_send_data_2400.py @@ -0,0 +1,115 @@ + +from common.runtime import load_root_env, validate_env, as_float, as_int, as_str +import numpy as np +import requests +import os +import sys +import json +import time + + +load_root_env(__file__) +validate_env("orange_scripts/compose_send_data_2400.py", { + "POROG_2400": as_float, + "SERVER_IP_2": as_str, + "SERVER_PORT_2": as_int, +}) +porog = float(os.getenv('POROG_2400')) +server_ip_1 = os.getenv('SERVER_IP_1') +server_port_1 = os.getenv('SERVER_PORT_1') +server_ip_2 = os.getenv('SERVER_IP_2') +server_port_2 = os.getenv('SERVER_PORT_2') +PARAMS = {'split_size': 400_000, 'point_amount': 100_000} +PARAMS['show_amount'] = 0.8 * PARAMS['point_amount'] +token = 0 +channel = 1 +flag = 0 + +############################## +# HYPERPARAMETERS +############################## +f_base = 2.4e9 +f_step = 20e6 +f_roof = 2.5e9 +############################## +# Variables +############################## +f = f_base +EOCF = 0 +signal_arr = [] + + +class NumpyArrayEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + else: + return super(NumpyArrayEncoder, self).default(obj) + + +def send_data(sig): + try: + global token + print('#' * 10) + print('\nОтправка пакета ' + str(token+1)) + data_to_send = { + "freq": 2400, + "channel": int(channel), + "token": int(token+1), + "data_real": np.asarray(np.array(sig, dtype=np.complex64).real, dtype=np.float32), + "data_imag": np.asarray(np.array(sig, dtype=np.complex64).imag, dtype=np.float32) + } + mod_data_to_send = json.dumps(data_to_send, cls=NumpyArrayEncoder) + response = requests.post("http://{0}:{1}/receive_data".format(server_ip_2, server_port_2), json=mod_data_to_send) + if response.status_code == 200: + token += 1 + print(response.text) + print('#' * 10) + else: + print("Ошибка при отправке данных: ", response.status_code) + print('#' * 10) + except Exception as exc: + print(str(exc)) + + +def median(sig): + global flag + median = abs(float(np.median(sorted(np.asarray(np.abs(np.array(sig, dtype=np.complex64)), dtype=np.float32))[int(PARAMS['show_amount']):]))) + flag = 0 if porog > median else 1 + print(channel, median, flag) + + +def work(lvl): + global flag + global channel + global f_base + global f_step + global f_roof + global f + global EOCF + global signal_arr + + y = np.array(lvl).ravel() + signal_arr = np.concatenate((signal_arr, y), axis=None) + + if f >= f_roof: + f = f_base + signal_arr = [] + channel = 1 + return f, EOCF + else: + if flag == 0 and len(signal_arr) >= PARAMS['point_amount']: + median(signal_arr[:PARAMS['point_amount']]) + signal_arr = [] + if flag == 0: + f += f_step + channel += 1 + if len(signal_arr) >= PARAMS['split_size']: + send_data(signal_arr[:PARAMS['split_size']]) + flag = 0 + signal_arr = [] + channel += 1 + f += f_step + return f, EOCF diff --git a/orange_scripts/compose_send_data_915.py b/orange_scripts/compose_send_data_915.py new file mode 100644 index 0000000..21f2443 --- /dev/null +++ b/orange_scripts/compose_send_data_915.py @@ -0,0 +1,115 @@ + +from common.runtime import load_root_env, validate_env, as_float, as_int, as_str +import numpy as np +import requests +import os +import sys +import json +import time + + +load_root_env(__file__) +validate_env("orange_scripts/compose_send_data_915.py", { + "POROG_915": as_float, + "SERVER_IP_1": as_str, + "SERVER_PORT_1": as_int, +}) +porog = float(os.getenv('POROG_915')) +server_ip_1 = os.getenv('SERVER_IP_1') +server_port_1 = os.getenv('SERVER_PORT_1') +server_ip_2 = os.getenv('SERVER_IP_2') +server_port_2 = os.getenv('SERVER_PORT_2') +PARAMS = {'split_size': 400_000, 'point_amount': 100_000} +PARAMS['show_amount'] = 0.8 * PARAMS['point_amount'] +token = 0 +channel = 1 +flag = 0 + +############################## +# HYPERPARAMETERS +############################## +f_base = 0.91e9 +f_step = 20e6 +f_roof = 0.98e9 +############################## +# Variables +############################## +f = f_base +EOCF = 0 +signal_arr = [] + + +class NumpyArrayEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + else: + return super(NumpyArrayEncoder, self).default(obj) + + +def send_data(sig): + try: + global token + print('#' * 10) + print('\nОтправка пакета ' + str(token+1)) + data_to_send = { + "freq": 915, + "channel": int(channel), + "token": int(token+1), + "data_real": np.asarray(np.array(sig, dtype=np.complex64).real, dtype=np.float32), + "data_imag": np.asarray(np.array(sig, dtype=np.complex64).imag, dtype=np.float32) + } + mod_data_to_send = json.dumps(data_to_send, cls=NumpyArrayEncoder) + response = requests.post("http://{0}:{1}/receive_data".format(server_ip_1, server_port_1), json=mod_data_to_send) + if response.status_code == 200: + token += 1 + print(response.text) + print('#' * 10) + else: + print("Ошибка при отправке данных: ", response.status_code) + print('#' * 10) + except Exception as exc: + print(str(exc)) + + +def median(sig): + global flag + median = abs(float(np.median(sorted(np.asarray(np.abs(np.array(sig, dtype=np.complex64)), dtype=np.float32))[int(PARAMS['show_amount']):]))) + flag = 0 if porog > median else 1 + print(channel, median, flag) + + +def work(lvl): + global flag + global channel + global f_base + global f_step + global f_roof + global f + global EOCF + global signal_arr + + y = np.array(lvl).ravel() + signal_arr = np.concatenate((signal_arr, y), axis=None) + + if f >= f_roof: + f = f_base + signal_arr = [] + channel = 1 + return f, EOCF + else: + if flag == 0 and len(signal_arr) >= PARAMS['point_amount']: + median(signal_arr[:PARAMS['point_amount']]) + signal_arr = [] + if flag == 0: + f += f_step + channel += 1 + if len(signal_arr) >= PARAMS['split_size']: + send_data(signal_arr[:PARAMS['split_size']]) + flag = 0 + signal_arr = [] + channel += 1 + f += f_step + return f, EOCF diff --git a/orange_scripts/main_1200.py b/orange_scripts/main_1200.py new file mode 100644 index 0000000..1b7d930 --- /dev/null +++ b/orange_scripts/main_1200.py @@ -0,0 +1,173 @@ +from gnuradio import blocks, gr +import sys +import signal +import compose_send_data_1200 as my_freq +import osmosdr +import time +import threading +import subprocess +import os +from common.runtime import load_root_env, resolve_hackrf_index + + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('HACKID_1200', 'orange_scripts/main_1200.py') + serial_number = os.getenv('HACKID_1200') + pos = None + output = [] + try: + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq)[0] + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(16, 0) + self.rtlsdr_source_0.set_if_gain(16, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)[0]) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + +def main(top_block_cls=get_center_freq, options=None): + time.sleep(3) + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + input('Press Enter to quit: ') + except EOFError: + pass + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/orange_scripts/main_2400.py b/orange_scripts/main_2400.py new file mode 100644 index 0000000..7bbe61b --- /dev/null +++ b/orange_scripts/main_2400.py @@ -0,0 +1,173 @@ +from gnuradio import blocks, gr +import sys +import signal +import compose_send_data_2400 as my_freq +import osmosdr +import time +import threading +import subprocess +import os +from common.runtime import load_root_env, resolve_hackrf_index + + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('HACKID_2400', 'orange_scripts/main_2400.py') + serial_number = os.getenv('HACKID_2400') + pos = None + output = [] + try: + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq)[0] + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(16, 0) + self.rtlsdr_source_0.set_if_gain(16, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)[0]) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + +def main(top_block_cls=get_center_freq, options=None): + time.sleep(3) + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + input('Press Enter to quit: ') + except EOFError: + pass + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/orange_scripts/main_915.py b/orange_scripts/main_915.py new file mode 100644 index 0000000..9fc90d5 --- /dev/null +++ b/orange_scripts/main_915.py @@ -0,0 +1,173 @@ +from gnuradio import blocks, gr +import sys +import signal +import compose_send_data_915 as my_freq +import osmosdr +import time +import threading +import subprocess +import os +from common.runtime import load_root_env, resolve_hackrf_index + + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('HACKID_915', 'orange_scripts/main_915.py') + serial_number = os.getenv('HACKID_915') + pos = None + output = [] + try: + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq)[0] + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(16, 0) + self.rtlsdr_source_0.set_if_gain(16, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)[0]) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + +def main(top_block_cls=get_center_freq, options=None): + time.sleep(3) + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + input('Press Enter to quit: ') + except EOFError: + pass + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/data_buffer.py b/src/core/data_buffer.py new file mode 100644 index 0000000..0dde66a --- /dev/null +++ b/src/core/data_buffer.py @@ -0,0 +1,163 @@ +import statistics + +# Более лучшая версия кода есть в FRScanner + +class DataBuffer: + """ + Класс с реализацией циклического буффера. + + Атрибуты: + current_column: Указатель на текущий столбец буфера, который обновляем. + thinning_counter: Прореживающий множитель на текующей итерации. + current_counter: Указатель на количество чтений между последним обновлением столбца и предыдущим атрибутом. + num_of_thinning_iter: Прореживающий множитель. Раз в это количечество раз будет обнволяться столбец буфера. + line_size: Количество строк буфера = количеству каналов. + columns_size: Количество столбцов = фиксированное число. + multiply_factor: Прцоентный показатель превышения сигналом уровня шума. ex m_p = 1.1 => триггер, если + сигнал превышает шум на 10%. + num_for_alarm: Количество раз, превышающих шум, при которых триггеримся = фиксированное число. + is_init: Флаг инициализации буфера. = True, если инициализирован. + buffer: Массив для буфера. + buffer_medians: Массив для медиан столбцов букера. + buffer_alarms: Массив для количества тревог по столбца буфера. + """ + + def __init__(self, columns_size, num_of_thinning_iter, num_of_channels, multiply_factor, num_for_alarm): + """ + Инициализируем класс. + + :param columns_size: + :param num_of_thinning_iter: + :param num_of_channels: + :param multiply_factor: + :param num_for_alarm: + """ + self.current_column = 0 + self.thinning_counter = 1 + self.current_counter = 1 + self.num_of_thinning_iter = num_of_thinning_iter + self.line_size = num_of_channels + self.columns_size = columns_size + self.multiply_factor = multiply_factor + self.num_for_alarm = num_for_alarm + self.is_init = False + self.buffer = [[0 for _ in range(self.columns_size)] for _ in range(self.line_size)] + self.buffer_medians = [0] * self.line_size + self.buffer_alarms = [0] * self.line_size + + def get_buffer(self): + return self.buffer + + def get_medians(self): + return self.buffer_medians + + def get_alarms(self): + return self.buffer_alarms + + def check_init(self): + return self.is_init + + def print(self): + print('buffer is: ') + for i in range(self.line_size): + print(self.buffer[i], end=' ') + print() + + def medians(self): + """ + Вычислить медиану по строке буфера. + :return: None + """ + if self.check_init(): + for i in range(self.line_size): + self.buffer_medians[i] = statistics.median(self.buffer[i]) + # print('meidans is: ', self.buffer_medians) + # return self.buffer_medians + + def alarms_fill_zeros(self): + self.buffer_alarms = [0] * self.line_size + def update(self, data): + """ + Обновление буфера. + Если номер текущего чтения совпадает с количеством прореживающего множителя на текущем обновлении буфера, то + 1. Обновляем буфер. + 2. Двигаем курсор на след столбец. Если был последний столбец, то двигаем курсор в начало. + 3. Берем медианы по буферу, если он уже проиницализирован. + 4. Сбрасываем счетчик текущих чтений. + 5. Если был последний столбец (и мы уже переключились на первый), то + Если прореживающий множитель на текующей итерации был единица, то мы иницилизировались + До тех пор, пока множитель на итерации меньше фиксированного, увеличиваем в два раза. + В противном случае - увеличиваем номер чтения. + :param data: Массив с метриками сигнала по каналам. + :return: None + """ + + # TODO: Добавить время релаксации - если система затриггерилась, то перестать обновлять буфер на N чтений, + # где N задается в .env-template. Сейчас есть бага, что буфер перестает обновляться только когда система + # триггерится. Между тем, когда приходит аларм и num_for_alarm, когда сигнал алармовский, но система еще не + # триггерится, буфер продолжает обновляться. В таких условиях буфер может набрать в себя алармовских сигналов + # и повысить пороги. Пример такой ситуации: дрон висит на 1км, система его видит, но сигнал превышает порог раз + # через раз и аларм срабатывает не всегда. В таких условиях наберется высокий сигнал, повысятся пороги и когда + # дрон начнет движение вперед, он будет заметен на более низкой дистанции, чем обычно, так как пороги повышены. + + if self.current_counter == self.thinning_counter: + for i in range(self.line_size): + self.buffer[i][self.current_column] = data[i] + self.current_column = (self.current_column + 1) % self.columns_size + #print('Столбец {0} обновлен. Перешли к столбцу {1}: '.format(self.current_column - 1, self.current_column)) + self.medians() + self.current_counter = 1 + if self.current_column == 0: + if self.thinning_counter == 1: + self.is_init = True + self.medians() + print('Начальная калибровка завершена.') + if self.thinning_counter < self.num_of_thinning_iter: + self.thinning_counter *= 2 + # print('thinning counter обновлен: ', self.thinning_counter) + + else: + self.current_counter += 1 + # print('curr counter обновлен: ', self.current_counter) + + def check_alarm(self, data): + """ + Проверка триггера системы. + Если значение по каналу превышает медиану (порог) шума на какой-то процент, то инкремент буфер аларма по каналу. + Превышение num_for_alarm подряд - триггер. Если после n превышений, где n self.multiply_factor * self.buffer_medians[i] + print(data[i]/self.buffer_medians[i]) + if exceeding: + self.buffer_alarms[i] += 1 + # print('Инкремент буффер алармов по каналу {0}, текущее число по этому каналу: {1}'.format(i,self.buffer_alarms[i])) + else: + self.buffer_alarms[i] = 0 + # print('Обнулили буффер алармов по каналу {0}, текущее число по этому каналу: {1}'.format(i,self.buffer_alarms[i])) + + if self.buffer_alarms[i] >= self.num_for_alarm: + # print('Сработала тревога по каналу {0}, текущее число по этому каналу: {1}'.format(i,self.buffer_alarms[i])) + self.buffer_alarms = [0] * self.line_size + return True + + return False + + def check_single_alarm(self, median, cur_channel): + """ + Проверка, является ли текущая медиана по каналу превышающей порог. + :param median: меди (хар-ка) по каналу. + :param cur_channel: индекс канала внутри частоты. + :return: Да/нет. + """ + if self.check_init(): + exceeding = median > self.multiply_factor * self.buffer_medians[cur_channel] + print(median/self.buffer_medians[cur_channel]) + if exceeding: + return True + else: + return False \ No newline at end of file diff --git a/src/core/multichannelswitcher.py b/src/core/multichannelswitcher.py new file mode 100644 index 0000000..ed6107c --- /dev/null +++ b/src/core/multichannelswitcher.py @@ -0,0 +1,168 @@ +import os +from core.data_buffer import DataBuffer + + +def get_centre_freq(freq): + """ + Получить название частоты по ее диапазону. + :param freq: Частота, которую обрабатываем. + :return: Название частоты. + """ + c_freq = 0 + if 5.46e9 <= freq <= 6.0e9: + c_freq = 5800 + if 5.0e9 <= freq <= 5.4e9: + c_freq = 5200 + if 4.5e9 <= freq <= 4.7e9: + c_freq = 4500 + if 3.3e9 <= freq <= 3.5e9: + c_freq = 3300 + if 2.4e9 <= freq <= 2.5e9: + c_freq = 2400 + if 1e9 <= freq <= 1.36e9: + c_freq = 1200 + if 0.9e9 <= freq <= 0.960e9: + c_freq = 915 + if 0.830e9 <= freq <= 0.890e9: + c_freq = 868 + if 0.700e9 <= freq <= 0.780e9: + c_freq = 750 + if 0.380e9 <= freq <= 0.500e9: + c_freq = 433 + return str(c_freq) + + +class MultiChannel: + """ + Класс с реализацией переключателя каналов. Присутствует поддержка нескольких частот, а поэтому + Атрибуты: + steps: Массив шагов для разных частот. Ex. steps = [-20e6, -5e6, -3e6], i-ый элемент соответствует i-ой + частоте для обработке, типа 1.2, 915 и 868. + bases: Массив верхних границ диапазонов рассматриваемых частот. Ex bases = [1.36e9, 0.93e9, 0.87e9] для + 1.2, 915 и 868. + roofs: То же самое, только нижних границ. Ex roofs = [1e9, 0.9e9, 0.85e9] + cur_channel: Указатель на текущий канал, который обрабатываем. + cur_roof: Указатель на нижнюю границу текущей обрабатываемой частоты. + cur_step: Указатель на шаг текущей обрабатываемой частоты. + num_chs: Массив из каналов по обрабатываемым частотам. Вычисляется автоматически исходя из границ и шага. + init_freq: Чекер на инициализацию частоты перед началом работы скрипта. Нужен из-за особенности + работы графов GNURadio и функции work в embedded Python блоке. + DB: Список из циклических буферов для соответствующих чатсот. + """ + + def __init__(self, steps, bases, roofs): + """ + Инициализация класса. + :param steps: Список с шагами для соответствующих частот. + :param bases: Список верхних границ диапазонов частот, с которыми работаем. + :param roofs: Список нижних границ --//--. + """ + + self.steps = steps + self.bases = bases + self.roofs = roofs + self.cur_channel = self.bases[0] + self.cur_roof = self.roofs[0] + self.cur_step = self.steps[0] + self.num_chs = [] + self.init_freq = False + self.DB = [] + + def init_f(self): + """ + Инициализация начальной частоты, с которой начинаем обработку. + :return: Верхняя граница первой частоты из набора частот. + """ + self.init_freq = True + return self.bases[0] + + def get_cur_channel(self): + """ + Получить текущий обрабатываемый канал. + :return: Канал обработки. + """ + return self.cur_channel + + def change_channel(self): + """ + Функция смены канала. Идет от верхней границы диапазона частоты к нижней с шагом step. Если дошли до нижней + границы, то переключаемся на следующую частоту посредством переноса курсора текущего канала на верхнюю границу + новой частоты и указатель нижней границы также двигаем на следующую позицию. Если частота для обработки одна, то + указатель текущего канала возвращается в начало - верхней границы этой же частоты. Указатель нижней границы не + изменяется. + + :return: Канал после смены. + """ + if not self.init_freq: + return self.init_f() + + if self.cur_channel <= self.cur_roof: + if self.cur_roof == self.roofs[-1]: + self.cur_channel = self.bases[0] + self.cur_roof = self.roofs[0] + self.cur_step = self.steps[0] + else: + next_roofs = self.roofs.index(self.cur_roof) + 1 + self.cur_channel = self.bases[next_roofs] + self.cur_roof = self.roofs[next_roofs] + self.cur_step = self.steps[next_roofs] + else: + self.cur_channel += self.cur_step + # print('Канал частоты изменен на ', self.cur_channel / 1000000) + return self.get_cur_channel() + + def get_num_chs(self, idx_freq): + """ + Вычисляет количество каналов на частоте исходя из верхнего, нижнего диапазонов и шага. + :param idx_freq: id частоты внутри класса. Т.е. в данный момент обрабатывается несколько частот, то id = + индексу верхней границы в bases для данной частоты, или нижней границы в roofs или шагу в steps. + В примерах из описания атрибутов индекс частоты 915 будет равен единице (т.к. идет вторым элементом в списках). + :return: Количество каналов. + """ + if (idx_freq + 1) > len(self.num_chs): + tmp = self.bases[idx_freq] + counter = 0 + while tmp >= self.roofs[idx_freq]: + counter += 1 + tmp += self.steps[idx_freq] + self.num_chs.append(counter) + return counter + else: + return self.num_chs[idx_freq] + + def check_f(self, freq): + """ + Проверить наличие частоты в классе. Если да, то вернуть количество каналов и циклический буфер этой частоты. + :param freq: Частота. + :return: Количество каналов, циклический буфер выбранной частоты ИЛИ none. + """ + for i in range(len(self.bases)): + if self.roofs[i] <= freq <= self.bases[i]: + return self.get_num_chs(i), self.DB[i] + else: + return None, None + + def fill_DB(self): + """ + Инициализировать циклические буферы для всех частот в отдельный список. + :return: N0nE. + """ + for i in range(len(self.bases)): + freq = get_centre_freq(self.bases[i]) + buffer_columns_size = int(os.getenv('buffer_columns_size_' + str(freq))) + num_of_thinning_iter = int(os.getenv('num_of_thinning_iter_' + str(freq))) + multiply_factor = float(os.getenv('multiply_factor_' + str(freq))) + num_for_alarm = int(os.getenv('num_for_alarm_' + str(freq))) + num_chs = self.get_num_chs(i) + self.DB.append( + DataBuffer(buffer_columns_size, num_of_thinning_iter, num_chs, multiply_factor, num_for_alarm)) + + def db_alarms_zeros(self, circle_buffer): + """ + При отработке системы зануляет алармы во всех буферах, кроме текущего, т.к. в текущем уже занулилось. + :param circle_buffer: Циклический буфер текущей обрабатываемой частоты. + :return: None. + """ + for i in range(len(self.DB)): + if self.DB[i] != circle_buffer: + self.DB[i].alarms_fill_zeros() diff --git a/src/core/sig_n_medi_collect.py b/src/core/sig_n_medi_collect.py new file mode 100644 index 0000000..dddcb3d --- /dev/null +++ b/src/core/sig_n_medi_collect.py @@ -0,0 +1,107 @@ +import os +import numpy as np +from typing import Union +from common.runtime import load_root_env + +load_root_env(__file__) + + +def get_signal_length(freq): + length = int(os.getenv('signal_length_' + str(freq))) + return length + + +class Signal: + """ + Класс сбора и предобработки сигнала. + + Атрибуты: + length: Длина сигнала. + signal: Массив, в который собираем сигнал. + """ + + def __init__(self, conv_method='average'): + self.conv_method = conv_method + self.signal = [] + self.signal_abs = [] + + def get_signal(self): + """ + Возвращает собранный сигнал. + :return: Массив с сигналом. + """ + return self.signal, self.signal_abs + + def clear(self) -> None: + """ + Очистить массив с сигналом после предобработки? + :return: None + """ + self.signal = [] + self.signal_abs = [] + + def signal_preprocessing(self, length) -> float: + """ + Предобработка сигнала. + + :return: Число типа float - "характеристика сигнала". + """ + signal = np.array([self.signal.real[0:length], self.signal.imag[0:length]], dtype=np.float32) + signal_abs = np.linalg.norm(signal, axis=0) # Поэлементный модуль комплексного числа. shape.result + # (1, self.length) + if self.conv_method == 'max': + result = np.max(signal_abs) + else: + result = np.median(signal_abs) + self.signal = signal + self.signal_abs = signal_abs + return result + + def fill_signal(self, lvl, length) -> Union[int, float]: + """ + Сбор сигнала в соответствующий массив. Если уже собран, то предобработка. + :param lvl: Массив, без ограничения общности, с неизвестной длиной, содержащий сигнал. + :param length: + :return: 0 - если еще нет нужного количества сигнала, "характеристика" иначе. + """ + if len(self.signal) <= length: + y = np.array(lvl).ravel() + self.signal = np.concatenate((self.signal, y), axis=None) + return 0 + else: + preproc_signal = self.signal_preprocessing(length) + #self.clear() + return preproc_signal + + +class SignalsArray: + """ + Класс для сохранения медиан сигналов на частотах. + Атрибуты: + sig_array: Список для сохранения медиан. + counter: Индикатор наполненности массива. + """ + def __init__(self): + self.sig_array = [] + self.counter = 0 + + def fill_sig_arr(self, metrica, num_chs=3): + """ + Аппендим характеристику сигнала (метрику) в массив длиной num_chs. + :param metrica: Характеристика сигнала (метрика). + :param num_chs: Количество каналов на частоте. + :return: Индекс канала внутри частоты и массив с характеристиками, если заполнен, иначе - пустой. + """ + if num_chs: + if self.counter < num_chs: + self.sig_array.append(metrica) + self.counter += 1 + if self.counter == num_chs: + arr = self.sig_array + self.sig_array = [] + self.counter = 0 + return num_chs - 1, arr + else: + return self.counter - 1, [] + else: + return 0, [] diff --git a/src/core/spectrum.py b/src/core/spectrum.py new file mode 100644 index 0000000..c51596c --- /dev/null +++ b/src/core/spectrum.py @@ -0,0 +1,44 @@ +class Spectrum: + """ + Класс для работы с "характеристиками" сигнала (предобработанным сигналом). + + Атрибуты: + freqs: Список (массив) частот, который проходим. + spec_elems: Список соответсвующих "характеристик" для каждой частоты. + """ + + def __init__(self, freqs: list): + """ + Инициализирует новый класс спектра. + :param freqs: Список частот. + """ + self.freqs = freqs + self.spec_elems = [] + + def get_freqs(self) -> list: + """ + Возвращает список частот. + :return: Массив с частотами. + """ + return self.freqs + + def get_spectrum(self) -> list: + """ + Возвращает собранный спектр. + :return: Массив с характеристиками сигнала. + """ + return self.spec_elems + + def add(self, elem: float) -> None: + """ + Добавляет характеристику в спектр. + :return: None. + """ + self.spec_elems.append(elem) + + def clear(self) -> None: + """ + Очистить собранный спектр. + :return: None. + """ + self.spec_elems = [] diff --git a/src/core/waterfall.py b/src/core/waterfall.py new file mode 100644 index 0000000..a5952ec --- /dev/null +++ b/src/core/waterfall.py @@ -0,0 +1,167 @@ +import gc +import os +import copy +from typing import Tuple +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import matplotlib.colors as mcolors +import DroneScanner.utils.utils as utils +from matplotlib.ticker import AutoMinorLocator + + +class Waterfall: + """ + Класс для вывода графика типа "Водопад" + + Атрибуты: + _freqs: Список частот. + _delay: Количество чтений, после которых обновляем водопад. + _size: Размер водопада по оси Y. + _counter_til_init: Счетчик от _size до 0. Отвечает за инициализацию сolors и valuest_to_plot. + _skip_update_counter: + _points_scale: Масштаб точек графика в зависимости от количества частот. + debug_flag: Флаг для отладки класса. + _save_plots: + _cur_plot_file_idx: + _max_plot_files: + _dir_to_save: + colors: Массив цветов точек графика. + values_to_plot: Значения по оси Y. + + """ + + def __init__(self, freqs: list, delay: int, size: int, debug_flag: bool, save_plots=False, max_plot_files=1000, + dir_to_save='/home/orangepi/plots'): + """ + Инициализирует новый водопад. + + :param freqs: Список частот. + :param delay: Количество чтений, после которых обновляем водопад. + :param size: Размер водопада по оси Y. + :param debug_flag: Флаг для отладки класса. + :param save_plots: + :param max_plot_files: + :param dir_to_save: + """ + self._freqs = freqs + self._delay = delay + self._size = size + self._counter_til_init = size - 1 + self._skip_update_counter = 1 + self._points_scale = 500 / len(self._freqs) + self.debug_flag = debug_flag + self._save_plots = save_plots + self._cur_plot_file_idx = 0 + self._max_plot_files = max_plot_files + self._dir_to_save = dir_to_save + self.colors = [] + self.values_to_plot = [] + + if not os.path.exists(self._dir_to_save): + os.makedirs(self._dir_to_save) + + def interpolate_color(self, start_color: tuple, end_color: tuple, factor: float) -> tuple: + """ + Интерполяция цвета между start_color и end_color в зависимости от factor. + Чем больше factor, тем ближе end_color, меньше - ближе к start_color. + :param start_color: Тройка (R, G, B), где R, G, B из 0..255. + :param end_color: Тройка (R, G, B), где R, G, B из 0..255. + :param factor: Число от 0 до 1. + :return: Тройка (a, b, c), где 0 <= a,b,c => 1 + """ + return tuple((start + (end - start) * factor) / 255 for start, end in zip(start_color, end_color)) + + def decorate(self, data: list) -> list: + """ + По строчке с датой строим строчку соответствующих цветов в зависимости от элементов из data. + :param data: Строка длиной len(freqs) с характеристиками сигнала (предобработанным сигналом). + :return: Строка длиной len(freqs) с тройками, соответствующие некоторым цветам из (R, G, B). + """ + green = (0, 255, 0) + red = (255, 0, 0) + colored_data = [] + + for elem in data: + colored_data.append(self.interpolate_color(green, red, elem)) + + return colored_data + + def transform_value_structure(self) -> Tuple[list, list, list]: + """ + Приведение одномерного freqs и двумерных values_to_plot и colors в одномерные длиной len(freqs) x size + для построения по ним водопада. + :return: Координаты точек по х и y и соответствующие цвета, в которые нужно покрасить точки. + """ + x = [] + y = [] + z = [] + for i in range(utils.get_num_columns_of_array(self.values_to_plot)): + for j in range(utils.get_num_rows_of_array(self.values_to_plot) - 1, -1, -1): + x.append(self._freqs[i]) + y.append(self.values_to_plot[j][i]) + z.append(self.colors[j][i]) + return x, y, z + + def plot(self) -> None: + """ + Построение водопада при помощи plt.scatter по x,y, с=z, s=_points_scale и автомасштабируемой сеткой. + :return: None. + """ + x, y, colors = self.transform_value_structure() + # colors_hex = [mcolors.to_hex(color) for color in colors] + plt.scatter(x, y, c=colors, s=self._points_scale, marker='s') + + # Добавление цветовой шкалы. + plt.colorbar(cm.ScalarMappable(norm=mcolors.Normalize(vmin=0, vmax=1), cmap='RdYlGn_r'), label='Значение') + + # Настройка ограничения по ОY + plt.ylim(-self._size / 10, (self._size - 1) + (self._size / 10)) + + # Настройка заголовков + plt.xlabel('Частоты') + plt.ylabel('Итерации') + plt.title('Waterfall') + + # Добавление автомасштабируемой сетки. + plt.grid(which='major', color='gray', linestyle='-', linewidth=0.5) + plt.minorticks_on() + plt.gca().xaxis.set_minor_locator(AutoMinorLocator()) + plt.gca().yaxis.set_minor_locator(AutoMinorLocator()) + plt.grid(which='minor', color='gray', linestyle=':', linewidth=0.5) + + if self._save_plots: + plt.savefig(f'{self._dir_to_save}/waterfall_{self._cur_plot_file_idx}.png') + if self._cur_plot_file_idx == self._max_plot_files: + self._cur_plot_file_idx = 0 + else: + self._cur_plot_file_idx += 1 + else: + plt.show() + + plt.pause(0.001) + plt.close() + gc.collect() + + def update(self, data: list) -> None: + """ + Конструирование массивов values_to_plot (значения по Y) и colors (цвета точек) с последующим + обновлением colors (см. документацию). + :param data: + :return: None. + """ + if self._skip_update_counter == self._delay: + data_data = copy.deepcopy(data) + colored_data = self.decorate(data_data) + + if self._counter_til_init != -1: + self.values_to_plot.append([self._counter_til_init for _ in range(len(self._freqs))]) + self._counter_til_init -= 1 + else: + self.colors.pop() + + self.colors.insert(0, colored_data) + self.plot() + self._skip_update_counter = 1 + + else: + self._skip_update_counter += 1 diff --git a/src/embedded_3300.py b/src/embedded_3300.py new file mode 100644 index 0000000..bc79a34 --- /dev/null +++ b/src/embedded_3300.py @@ -0,0 +1,120 @@ +import os +import datetime +from common.runtime import load_root_env, validate_env, as_bool, as_str +from smb.SMBConnection import SMBConnection +from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data +from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length +from core.multichannelswitcher import MultiChannel, get_centre_freq + +load_root_env(__file__) +freq_suffix = os.path.splitext(os.path.basename(__file__))[0].split("_")[-1] +validate_env(__file__, { + "send_to_module_flag": as_bool, + "save_data_flag": as_bool, + "elems_to_save": as_str, + "file_types_to_save": as_str, + "lochost": as_str, + "locport": as_str, + "freq_endpoint": as_str, + "path_to_save_medians": as_str, + "path_to_save_alarms": as_str, + "module_name": as_str, + f"f_step_{freq_suffix}": as_str, + f"f_bases_{freq_suffix}": as_str, + f"f_roofs_{freq_suffix}": as_str, +}) + +debug_flag = as_bool(os.getenv('debug_flag', '0')) +send_to_module_flag = as_bool(os.getenv('send_to_module_flag', '0')) +save_data_flag = as_bool(os.getenv('save_data_flag', '0')) +module_name = os.getenv('module_name') +elems_to_save = os.getenv('elems_to_save') +file_types_to_save = os.getenv('file_types_to_save') +localhost = os.getenv('lochost') +localport = os.getenv('locport') +f_step = [*map(float, os.getenv('f_step_3300').split())] +f_bases = [*map(float, os.getenv('f_bases_3300').split())] +f_roofs = [*map(float, os.getenv('f_roofs_3300').split())] +path_to_save_medians = os.getenv('path_to_save_medians') +path_to_save_alarms = os.getenv('path_to_save_alarms') +smb_host = os.getenv('smb_host') +smb_port = os.getenv('smb_port') +smb_user = os.getenv('smb_user') +smb_pass = os.getenv('smb_pass') +shared_folder = os.getenv('shared_folder') +the_pc_name = os.getenv('the_pc_name') +remote_pc_name = os.getenv('remote_pc_name') +smb_domain = os.getenv('smb_domain') +freq_endpoint = os.getenv('freq_endpoint') + +elems_to_save = elems_to_save.split(',') +file_types_to_save = file_types_to_save.split(',') + +tmp_signal = Signal() +tmp_sigs_array = SignalsArray() +multi_channel = MultiChannel(f_step, f_bases, f_roofs) +f = multi_channel.init_f() +multi_channel.fill_DB() + +if debug_flag: + conn = SMBConnection(smb_user, smb_pass, the_pc_name, remote_pc_name, use_ntlm_v2=True) + conn.connect(smb_host, 139) + filelist = conn.listPath(shared_folder, '/') + print(filelist) + + +def work(lvl): + + f = multi_channel.get_cur_channel() + freq = get_centre_freq(f) + signal_length = get_signal_length(freq) + median = tmp_signal.fill_signal(lvl, signal_length) + + if median: + try: + num_chs, circle_buffer = multi_channel.check_f(f) + cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs) + + if sigs_array: + print('Значения на {0}: {1}'.format(freq, sigs_array)) + print('Пороги: ', circle_buffer.get_medians()) + alarm = circle_buffer.check_alarm(sigs_array) + + if alarm: + print('----ALARM---- ', freq) + multi_channel.db_alarms_zeros(circle_buffer) + else: + circle_buffer.update(sigs_array) + + if send_to_module_flag: + send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) + + if save_data_flag: + if not circle_buffer.check_init() and circle_buffer.current_column - 1 == 0: + save_data(path_to_save_medians, freq, 'DateTime', 'ALARM', 'max signal', list(range(num_chs)), + list(range(num_chs))) + if circle_buffer.check_init(): + save_data(path_to_save_medians, freq, datetime.datetime.now(), alarm, max(sigs_array), sigs_array, + circle_buffer.get_medians()) + + if debug_flag: + single_alarm = circle_buffer.check_single_alarm(median, cur_channel) + print(cur_channel, single_alarm) + if single_alarm: + data = pack_elems(elems_to_save, file_types_to_save, tmp_signal.get_signal()) + print('SAVE CURRENT SIGNAL SROCHNO TI MENYA SLISHISH?!?!?!?') + try: + remote_save_data(conn, data, module_name, freq, shared_folder, path_to_save_alarms) + except Exception as e: + print(f"Ошибка: {e}") + else: + print('VSE OKI DOKI SIGNAL SOKHRANYAT NE NUZHNO!!!') + + f = multi_channel.change_channel() + except Exception as e: + print(str(e)) + print(".", end='') + + tmp_signal.clear() + + return f diff --git a/src/embedded_433.py b/src/embedded_433.py new file mode 100644 index 0000000..475b2d3 --- /dev/null +++ b/src/embedded_433.py @@ -0,0 +1,120 @@ +import os +import datetime +from common.runtime import load_root_env, validate_env, as_bool, as_str +from smb.SMBConnection import SMBConnection +from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data +from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length +from core.multichannelswitcher import MultiChannel, get_centre_freq + +load_root_env(__file__) +freq_suffix = os.path.splitext(os.path.basename(__file__))[0].split("_")[-1] +validate_env(__file__, { + "send_to_module_flag": as_bool, + "save_data_flag": as_bool, + "elems_to_save": as_str, + "file_types_to_save": as_str, + "lochost": as_str, + "locport": as_str, + "freq_endpoint": as_str, + "path_to_save_medians": as_str, + "path_to_save_alarms": as_str, + "module_name": as_str, + f"f_step_{freq_suffix}": as_str, + f"f_bases_{freq_suffix}": as_str, + f"f_roofs_{freq_suffix}": as_str, +}) + +debug_flag = as_bool(os.getenv('debug_flag', '0')) +send_to_module_flag = as_bool(os.getenv('send_to_module_flag', '0')) +save_data_flag = as_bool(os.getenv('save_data_flag', '0')) +module_name = os.getenv('module_name') +elems_to_save = os.getenv('elems_to_save') +file_types_to_save = os.getenv('file_types_to_save') +localhost = os.getenv('lochost') +localport = os.getenv('locport') +f_step = [*map(float, os.getenv('f_step_433').split())] +f_bases = [*map(float, os.getenv('f_bases_433').split())] +f_roofs = [*map(float, os.getenv('f_roofs_433').split())] +path_to_save_medians = os.getenv('path_to_save_medians') +path_to_save_alarms = os.getenv('path_to_save_alarms') +smb_host = os.getenv('smb_host') +smb_port = os.getenv('smb_port') +smb_user = os.getenv('smb_user') +smb_pass = os.getenv('smb_pass') +shared_folder = os.getenv('shared_folder') +the_pc_name = os.getenv('the_pc_name') +remote_pc_name = os.getenv('remote_pc_name') +smb_domain = os.getenv('smb_domain') +freq_endpoint = os.getenv('freq_endpoint') + +elems_to_save = elems_to_save.split(',') +file_types_to_save = file_types_to_save.split(',') + +tmp_signal = Signal() +tmp_sigs_array = SignalsArray() +multi_channel = MultiChannel(f_step, f_bases, f_roofs) +f = multi_channel.init_f() +multi_channel.fill_DB() + +if debug_flag: + conn = SMBConnection(smb_user, smb_pass, the_pc_name, remote_pc_name, use_ntlm_v2=True) + conn.connect(smb_host, 139) + filelist = conn.listPath(shared_folder, '/') + print(filelist) + + +def work(lvl): + + f = multi_channel.get_cur_channel() + freq = get_centre_freq(f) + signal_length = get_signal_length(freq) + median = tmp_signal.fill_signal(lvl, signal_length) + + if median: + try: + num_chs, circle_buffer = multi_channel.check_f(f) + cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs) + + if sigs_array: + print('Значения на {0}: {1}'.format(freq, sigs_array)) + print('Пороги: ', circle_buffer.get_medians()) + alarm = circle_buffer.check_alarm(sigs_array) + + if alarm: + print('----ALARM---- ', freq) + multi_channel.db_alarms_zeros(circle_buffer) + else: + circle_buffer.update(sigs_array) + + if send_to_module_flag: + send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) + + if save_data_flag: + if not circle_buffer.check_init() and circle_buffer.current_column - 1 == 0: + save_data(path_to_save_medians, freq, 'DateTime', 'ALARM', 'max signal', list(range(num_chs)), + list(range(num_chs))) + if circle_buffer.check_init(): + save_data(path_to_save_medians, freq, datetime.datetime.now(), alarm, max(sigs_array), sigs_array, + circle_buffer.get_medians()) + + if debug_flag: + single_alarm = circle_buffer.check_single_alarm(median, cur_channel) + print(cur_channel, single_alarm) + if single_alarm: + data = pack_elems(elems_to_save, file_types_to_save, tmp_signal.get_signal()) + print('SAVE CURRENT SIGNAL SROCHNO TI MENYA SLISHISH?!?!?!?') + try: + remote_save_data(conn, data, module_name, freq, shared_folder, path_to_save_alarms) + except Exception as e: + print(f"Ошибка: {e}") + else: + print('VSE OKI DOKI SIGNAL SOKHRANYAT NE NUZHNO!!!') + + f = multi_channel.change_channel() + except Exception as e: + print(str(e)) + print(".", end='') + + tmp_signal.clear() + + return f diff --git a/src/embedded_4500.py b/src/embedded_4500.py new file mode 100644 index 0000000..d188d69 --- /dev/null +++ b/src/embedded_4500.py @@ -0,0 +1,120 @@ +import os +import datetime +from common.runtime import load_root_env, validate_env, as_bool, as_str +from smb.SMBConnection import SMBConnection +from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data +from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length +from core.multichannelswitcher import MultiChannel, get_centre_freq + +load_root_env(__file__) +freq_suffix = os.path.splitext(os.path.basename(__file__))[0].split("_")[-1] +validate_env(__file__, { + "send_to_module_flag": as_bool, + "save_data_flag": as_bool, + "elems_to_save": as_str, + "file_types_to_save": as_str, + "lochost": as_str, + "locport": as_str, + "freq_endpoint": as_str, + "path_to_save_medians": as_str, + "path_to_save_alarms": as_str, + "module_name": as_str, + f"f_step_{freq_suffix}": as_str, + f"f_bases_{freq_suffix}": as_str, + f"f_roofs_{freq_suffix}": as_str, +}) + +debug_flag = as_bool(os.getenv('debug_flag', '0')) +send_to_module_flag = as_bool(os.getenv('send_to_module_flag', '0')) +save_data_flag = as_bool(os.getenv('save_data_flag', '0')) +module_name = os.getenv('module_name') +elems_to_save = os.getenv('elems_to_save') +file_types_to_save = os.getenv('file_types_to_save') +localhost = os.getenv('lochost') +localport = os.getenv('locport') +f_step = [*map(float, os.getenv('f_step_4500').split())] +f_bases = [*map(float, os.getenv('f_bases_4500').split())] +f_roofs = [*map(float, os.getenv('f_roofs_4500').split())] +path_to_save_medians = os.getenv('path_to_save_medians') +path_to_save_alarms = os.getenv('path_to_save_alarms') +smb_host = os.getenv('smb_host') +smb_port = os.getenv('smb_port') +smb_user = os.getenv('smb_user') +smb_pass = os.getenv('smb_pass') +shared_folder = os.getenv('shared_folder') +the_pc_name = os.getenv('the_pc_name') +remote_pc_name = os.getenv('remote_pc_name') +smb_domain = os.getenv('smb_domain') +freq_endpoint = os.getenv('freq_endpoint') + +elems_to_save = elems_to_save.split(',') +file_types_to_save = file_types_to_save.split(',') + +tmp_signal = Signal() +tmp_sigs_array = SignalsArray() +multi_channel = MultiChannel(f_step, f_bases, f_roofs) +f = multi_channel.init_f() +multi_channel.fill_DB() + +if debug_flag: + conn = SMBConnection(smb_user, smb_pass, the_pc_name, remote_pc_name, use_ntlm_v2=True) + conn.connect(smb_host, 139) + filelist = conn.listPath(shared_folder, '/') + print(filelist) + + +def work(lvl): + + f = multi_channel.get_cur_channel() + freq = get_centre_freq(f) + signal_length = get_signal_length(freq) + median = tmp_signal.fill_signal(lvl, signal_length) + + if median: + try: + num_chs, circle_buffer = multi_channel.check_f(f) + cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs) + + if sigs_array: + print('Значения на {0}: {1}'.format(freq, sigs_array)) + print('Пороги: ', circle_buffer.get_medians()) + alarm = circle_buffer.check_alarm(sigs_array) + + if alarm: + print('----ALARM---- ', freq) + multi_channel.db_alarms_zeros(circle_buffer) + else: + circle_buffer.update(sigs_array) + + if send_to_module_flag: + send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) + + if save_data_flag: + if not circle_buffer.check_init() and circle_buffer.current_column - 1 == 0: + save_data(path_to_save_medians, freq, 'DateTime', 'ALARM', 'max signal', list(range(num_chs)), + list(range(num_chs))) + if circle_buffer.check_init(): + save_data(path_to_save_medians, freq, datetime.datetime.now(), alarm, max(sigs_array), sigs_array, + circle_buffer.get_medians()) + + if debug_flag: + single_alarm = circle_buffer.check_single_alarm(median, cur_channel) + print(cur_channel, single_alarm) + if single_alarm: + data = pack_elems(elems_to_save, file_types_to_save, tmp_signal.get_signal()) + print('SAVE CURRENT SIGNAL SROCHNO TI MENYA SLISHISH?!?!?!?') + try: + remote_save_data(conn, data, module_name, freq, shared_folder, path_to_save_alarms) + except Exception as e: + print(f"Ошибка: {e}") + else: + print('VSE OKI DOKI SIGNAL SOKHRANYAT NE NUZHNO!!!') + + f = multi_channel.change_channel() + except Exception as e: + print(str(e)) + print(".", end='') + + tmp_signal.clear() + + return f diff --git a/src/embedded_5200.py b/src/embedded_5200.py new file mode 100644 index 0000000..26bb54f --- /dev/null +++ b/src/embedded_5200.py @@ -0,0 +1,120 @@ +import os +import datetime +from common.runtime import load_root_env, validate_env, as_bool, as_str +from smb.SMBConnection import SMBConnection +from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data +from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length +from core.multichannelswitcher import MultiChannel, get_centre_freq + +load_root_env(__file__) +freq_suffix = os.path.splitext(os.path.basename(__file__))[0].split("_")[-1] +validate_env(__file__, { + "send_to_module_flag": as_bool, + "save_data_flag": as_bool, + "elems_to_save": as_str, + "file_types_to_save": as_str, + "lochost": as_str, + "locport": as_str, + "freq_endpoint": as_str, + "path_to_save_medians": as_str, + "path_to_save_alarms": as_str, + "module_name": as_str, + f"f_step_{freq_suffix}": as_str, + f"f_bases_{freq_suffix}": as_str, + f"f_roofs_{freq_suffix}": as_str, +}) + +debug_flag = as_bool(os.getenv('debug_flag', '0')) +send_to_module_flag = as_bool(os.getenv('send_to_module_flag', '0')) +save_data_flag = as_bool(os.getenv('save_data_flag', '0')) +module_name = os.getenv('module_name') +elems_to_save = os.getenv('elems_to_save') +file_types_to_save = os.getenv('file_types_to_save') +localhost = os.getenv('lochost') +localport = os.getenv('locport') +f_step = [*map(float, os.getenv('f_step_5200').split())] +f_bases = [*map(float, os.getenv('f_bases_5200').split())] +f_roofs = [*map(float, os.getenv('f_roofs_5200').split())] +path_to_save_medians = os.getenv('path_to_save_medians') +path_to_save_alarms = os.getenv('path_to_save_alarms') +smb_host = os.getenv('smb_host') +smb_port = os.getenv('smb_port') +smb_user = os.getenv('smb_user') +smb_pass = os.getenv('smb_pass') +shared_folder = os.getenv('shared_folder') +the_pc_name = os.getenv('the_pc_name') +remote_pc_name = os.getenv('remote_pc_name') +smb_domain = os.getenv('smb_domain') +freq_endpoint = os.getenv('freq_endpoint') + +elems_to_save = elems_to_save.split(',') +file_types_to_save = file_types_to_save.split(',') + +tmp_signal = Signal() +tmp_sigs_array = SignalsArray() +multi_channel = MultiChannel(f_step, f_bases, f_roofs) +f = multi_channel.init_f() +multi_channel.fill_DB() + +if debug_flag: + conn = SMBConnection(smb_user, smb_pass, the_pc_name, remote_pc_name, use_ntlm_v2=True) + conn.connect(smb_host, 139) + filelist = conn.listPath(shared_folder, '/') + print(filelist) + + +def work(lvl): + + f = multi_channel.get_cur_channel() + freq = get_centre_freq(f) + signal_length = get_signal_length(freq) + median = tmp_signal.fill_signal(lvl, signal_length) + + if median: + try: + num_chs, circle_buffer = multi_channel.check_f(f) + cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs) + + if sigs_array: + print('Значения на {0}: {1}'.format(freq, sigs_array)) + print('Пороги: ', circle_buffer.get_medians()) + alarm = circle_buffer.check_alarm(sigs_array) + + if alarm: + print('----ALARM---- ', freq) + multi_channel.db_alarms_zeros(circle_buffer) + else: + circle_buffer.update(sigs_array) + + if send_to_module_flag: + send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) + + if save_data_flag: + if not circle_buffer.check_init() and circle_buffer.current_column - 1 == 0: + save_data(path_to_save_medians, freq, 'DateTime', 'ALARM', 'max signal', list(range(num_chs)), + list(range(num_chs))) + if circle_buffer.check_init(): + save_data(path_to_save_medians, freq, datetime.datetime.now(), alarm, max(sigs_array), sigs_array, + circle_buffer.get_medians()) + + if debug_flag: + single_alarm = circle_buffer.check_single_alarm(median, cur_channel) + print(cur_channel, single_alarm) + if single_alarm: + data = pack_elems(elems_to_save, file_types_to_save, tmp_signal.get_signal()) + print('SAVE CURRENT SIGNAL SROCHNO TI MENYA SLISHISH?!?!?!?') + try: + remote_save_data(conn, data, module_name, freq, shared_folder, path_to_save_alarms) + except Exception as e: + print(f"Ошибка: {e}") + else: + print('VSE OKI DOKI SIGNAL SOKHRANYAT NE NUZHNO!!!') + + f = multi_channel.change_channel() + except Exception as e: + print(str(e)) + print(".", end='') + + tmp_signal.clear() + + return f diff --git a/src/embedded_5800.py b/src/embedded_5800.py new file mode 100644 index 0000000..ed8f50f --- /dev/null +++ b/src/embedded_5800.py @@ -0,0 +1,120 @@ +import os +import datetime +from common.runtime import load_root_env, validate_env, as_bool, as_str +from smb.SMBConnection import SMBConnection +from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data +from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length +from core.multichannelswitcher import MultiChannel, get_centre_freq + +load_root_env(__file__) +freq_suffix = os.path.splitext(os.path.basename(__file__))[0].split("_")[-1] +validate_env(__file__, { + "send_to_module_flag": as_bool, + "save_data_flag": as_bool, + "elems_to_save": as_str, + "file_types_to_save": as_str, + "lochost": as_str, + "locport": as_str, + "freq_endpoint": as_str, + "path_to_save_medians": as_str, + "path_to_save_alarms": as_str, + "module_name": as_str, + f"f_step_{freq_suffix}": as_str, + f"f_bases_{freq_suffix}": as_str, + f"f_roofs_{freq_suffix}": as_str, +}) + +debug_flag = as_bool(os.getenv('debug_flag', '0')) +send_to_module_flag = as_bool(os.getenv('send_to_module_flag', '0')) +save_data_flag = as_bool(os.getenv('save_data_flag', '0')) +module_name = os.getenv('module_name') +elems_to_save = os.getenv('elems_to_save') +file_types_to_save = os.getenv('file_types_to_save') +localhost = os.getenv('lochost') +localport = os.getenv('locport') +f_step = [*map(float, os.getenv('f_step_5800').split())] +f_bases = [*map(float, os.getenv('f_bases_5800').split())] +f_roofs = [*map(float, os.getenv('f_roofs_5800').split())] +path_to_save_medians = os.getenv('path_to_save_medians') +path_to_save_alarms = os.getenv('path_to_save_alarms') +smb_host = os.getenv('smb_host') +smb_port = os.getenv('smb_port') +smb_user = os.getenv('smb_user') +smb_pass = os.getenv('smb_pass') +shared_folder = os.getenv('shared_folder') +the_pc_name = os.getenv('the_pc_name') +remote_pc_name = os.getenv('remote_pc_name') +smb_domain = os.getenv('smb_domain') +freq_endpoint = os.getenv('freq_endpoint') + +elems_to_save = elems_to_save.split(',') +file_types_to_save = file_types_to_save.split(',') + +tmp_signal = Signal() +tmp_sigs_array = SignalsArray() +multi_channel = MultiChannel(f_step, f_bases, f_roofs) +f = multi_channel.init_f() +multi_channel.fill_DB() + +if debug_flag: + conn = SMBConnection(smb_user, smb_pass, the_pc_name, remote_pc_name, use_ntlm_v2=True) + conn.connect(smb_host, 139) + filelist = conn.listPath(shared_folder, '/') + print(filelist) + + +def work(lvl): + + f = multi_channel.get_cur_channel() + freq = get_centre_freq(f) + signal_length = get_signal_length(freq) + median = tmp_signal.fill_signal(lvl, signal_length) + + if median: + try: + num_chs, circle_buffer = multi_channel.check_f(f) + cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs) + + if sigs_array: + print('Значения на {0}: {1}'.format(freq, sigs_array)) + print('Пороги: ', circle_buffer.get_medians()) + alarm = circle_buffer.check_alarm(sigs_array) + + if alarm: + print('----ALARM---- ', freq) + multi_channel.db_alarms_zeros(circle_buffer) + else: + circle_buffer.update(sigs_array) + + if send_to_module_flag: + send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) + + if save_data_flag: + if not circle_buffer.check_init() and circle_buffer.current_column - 1 == 0: + save_data(path_to_save_medians, freq, 'DateTime', 'ALARM', 'max signal', list(range(num_chs)), + list(range(num_chs))) + if circle_buffer.check_init(): + save_data(path_to_save_medians, freq, datetime.datetime.now(), alarm, max(sigs_array), sigs_array, + circle_buffer.get_medians()) + + if debug_flag: + single_alarm = circle_buffer.check_single_alarm(median, cur_channel) + print(cur_channel, single_alarm) + if single_alarm: + data = pack_elems(elems_to_save, file_types_to_save, tmp_signal.get_signal()) + print('SAVE CURRENT SIGNAL SROCHNO TI MENYA SLISHISH?!?!?!?') + try: + remote_save_data(conn, data, module_name, freq, shared_folder, path_to_save_alarms) + except Exception as e: + print(f"Ошибка: {e}") + else: + print('VSE OKI DOKI SIGNAL SOKHRANYAT NE NUZHNO!!!') + + f = multi_channel.change_channel() + except Exception as e: + print(str(e)) + print(".", end='') + + tmp_signal.clear() + + return f diff --git a/src/embedded_750.py b/src/embedded_750.py new file mode 100644 index 0000000..efed49b --- /dev/null +++ b/src/embedded_750.py @@ -0,0 +1,123 @@ +import os +import datetime +from common.runtime import load_root_env, validate_env, as_bool, as_str +from smb.SMBConnection import SMBConnection +from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data +from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length +from core.multichannelswitcher import MultiChannel, get_centre_freq + +load_root_env(__file__) +freq_suffix = os.path.splitext(os.path.basename(__file__))[0].split("_")[-1] +validate_env(__file__, { + "send_to_module_flag": as_bool, + "save_data_flag": as_bool, + "elems_to_save": as_str, + "file_types_to_save": as_str, + "lochost": as_str, + "locport": as_str, + "freq_endpoint": as_str, + "path_to_save_medians": as_str, + "path_to_save_alarms": as_str, + "module_name": as_str, + f"f_step_{freq_suffix}": as_str, + f"f_bases_{freq_suffix}": as_str, + f"f_roofs_{freq_suffix}": as_str, +}) + +debug_flag = as_bool(os.getenv('debug_flag', '0')) +send_to_module_flag = as_bool(os.getenv('send_to_module_flag', '0')) +save_data_flag = as_bool(os.getenv('save_data_flag', '0')) +module_name = os.getenv('module_name') +elems_to_save = os.getenv('elems_to_save') +file_types_to_save = os.getenv('file_types_to_save') +localhost = os.getenv('lochost') +localport = os.getenv('locport') +f_step = [*map(float, os.getenv('f_step_750').split())] +f_bases = [*map(float, os.getenv('f_bases_750').split())] +f_roofs = [*map(float, os.getenv('f_roofs_750').split())] +path_to_save_medians = os.getenv('path_to_save_medians') +path_to_save_alarms = os.getenv('path_to_save_alarms') +smb_host = os.getenv('smb_host') +smb_port = os.getenv('smb_port') +smb_user = os.getenv('smb_user') +smb_pass = os.getenv('smb_pass') +shared_folder = os.getenv('shared_folder') +the_pc_name = os.getenv('the_pc_name') +remote_pc_name = os.getenv('remote_pc_name') +smb_domain = os.getenv('smb_domain') +freq_endpoint = os.getenv('freq_endpoint') + +elems_to_save = elems_to_save.split(',') +file_types_to_save = file_types_to_save.split(',') + +tmp_signal = Signal() +tmp_sigs_array = SignalsArray() +multi_channel = MultiChannel(f_step, f_bases, f_roofs) +f = multi_channel.init_f() +multi_channel.fill_DB() + +if debug_flag: + conn = SMBConnection(smb_user, smb_pass, the_pc_name, remote_pc_name, use_ntlm_v2=True) + conn.connect(smb_host, 139) + filelist = conn.listPath(shared_folder, '/') + print(filelist) + + +def work(lvl): + + f = multi_channel.get_cur_channel() + freq = get_centre_freq(f) + signal_length = get_signal_length(freq) + median = tmp_signal.fill_signal(lvl, signal_length) + + if median: + print(1) + try: + num_chs, circle_buffer = multi_channel.check_f(f) + print(num_chs, circle_buffer) + cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs) + print(3) + + if sigs_array: + print('Значения на {0}: {1}'.format(freq, sigs_array)) + print('Пороги: ', circle_buffer.get_medians()) + alarm = circle_buffer.check_alarm(sigs_array) + + if alarm: + print('----ALARM---- ', freq) + multi_channel.db_alarms_zeros(circle_buffer) + else: + circle_buffer.update(sigs_array) + + if send_to_module_flag: + send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) + + if save_data_flag: + if not circle_buffer.check_init() and circle_buffer.current_column - 1 == 0: + save_data(path_to_save_medians, freq, 'DateTime', 'ALARM', 'max signal', list(range(num_chs)), + list(range(num_chs))) + if circle_buffer.check_init(): + save_data(path_to_save_medians, freq, datetime.datetime.now(), alarm, max(sigs_array), sigs_array, + circle_buffer.get_medians()) + + if debug_flag: + single_alarm = circle_buffer.check_single_alarm(median, cur_channel) + print(cur_channel, single_alarm) + if single_alarm: + data = pack_elems(elems_to_save, file_types_to_save, tmp_signal.get_signal()) + print('SAVE CURRENT SIGNAL SROCHNO TI MENYA SLISHISH?!?!?!?') + try: + remote_save_data(conn, data, module_name, freq, shared_folder, path_to_save_alarms) + except Exception as e: + print(f"Ошибка: {e}") + else: + print('VSE OKI DOKI SIGNAL SOKHRANYAT NE NUZHNO!!!') + + f = multi_channel.change_channel() + except Exception as e: + print(str(e)) + print(".", end='') + + tmp_signal.clear() + + return f diff --git a/src/embedded_868.py b/src/embedded_868.py new file mode 100644 index 0000000..55d620b --- /dev/null +++ b/src/embedded_868.py @@ -0,0 +1,120 @@ +import os +import datetime +from common.runtime import load_root_env, validate_env, as_bool, as_str +from smb.SMBConnection import SMBConnection +from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data +from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length +from core.multichannelswitcher import MultiChannel, get_centre_freq + +load_root_env(__file__) +freq_suffix = os.path.splitext(os.path.basename(__file__))[0].split("_")[-1] +validate_env(__file__, { + "send_to_module_flag": as_bool, + "save_data_flag": as_bool, + "elems_to_save": as_str, + "file_types_to_save": as_str, + "lochost": as_str, + "locport": as_str, + "freq_endpoint": as_str, + "path_to_save_medians": as_str, + "path_to_save_alarms": as_str, + "module_name": as_str, + f"f_step_{freq_suffix}": as_str, + f"f_bases_{freq_suffix}": as_str, + f"f_roofs_{freq_suffix}": as_str, +}) + +debug_flag = as_bool(os.getenv('debug_flag', '0')) +send_to_module_flag = as_bool(os.getenv('send_to_module_flag', '0')) +save_data_flag = as_bool(os.getenv('save_data_flag', '0')) +module_name = os.getenv('module_name') +elems_to_save = os.getenv('elems_to_save') +file_types_to_save = os.getenv('file_types_to_save') +localhost = os.getenv('lochost') +localport = os.getenv('locport') +f_step = [*map(float, os.getenv('f_step_868').split())] +f_bases = [*map(float, os.getenv('f_bases_868').split())] +f_roofs = [*map(float, os.getenv('f_roofs_868').split())] +path_to_save_medians = os.getenv('path_to_save_medians') +path_to_save_alarms = os.getenv('path_to_save_alarms') +smb_host = os.getenv('smb_host') +smb_port = os.getenv('smb_port') +smb_user = os.getenv('smb_user') +smb_pass = os.getenv('smb_pass') +shared_folder = os.getenv('shared_folder') +the_pc_name = os.getenv('the_pc_name') +remote_pc_name = os.getenv('remote_pc_name') +smb_domain = os.getenv('smb_domain') +freq_endpoint = os.getenv('freq_endpoint') + +elems_to_save = elems_to_save.split(',') +file_types_to_save = file_types_to_save.split(',') + +tmp_signal = Signal() +tmp_sigs_array = SignalsArray() +multi_channel = MultiChannel(f_step, f_bases, f_roofs) +f = multi_channel.init_f() +multi_channel.fill_DB() + +if debug_flag: + conn = SMBConnection(smb_user, smb_pass, the_pc_name, remote_pc_name, use_ntlm_v2=True) + conn.connect(smb_host, 139) + filelist = conn.listPath(shared_folder, '/') + print(filelist) + + +def work(lvl): + + f = multi_channel.get_cur_channel() + freq = get_centre_freq(f) + signal_length = get_signal_length(freq) + median = tmp_signal.fill_signal(lvl, signal_length) + + if median: + try: + num_chs, circle_buffer = multi_channel.check_f(f) + cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs) + + if sigs_array: + print('Значения на {0}: {1}'.format(freq, sigs_array)) + print('Пороги: ', circle_buffer.get_medians()) + alarm = circle_buffer.check_alarm(sigs_array) + + if alarm: + print('----ALARM---- ', freq) + multi_channel.db_alarms_zeros(circle_buffer) + else: + circle_buffer.update(sigs_array) + + if send_to_module_flag: + send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) + + if save_data_flag: + if not circle_buffer.check_init() and circle_buffer.current_column - 1 == 0: + save_data(path_to_save_medians, freq, 'DateTime', 'ALARM', 'max signal', list(range(num_chs)), + list(range(num_chs))) + if circle_buffer.check_init(): + save_data(path_to_save_medians, freq, datetime.datetime.now(), alarm, max(sigs_array), sigs_array, + circle_buffer.get_medians()) + + if debug_flag: + single_alarm = circle_buffer.check_single_alarm(median, cur_channel) + print(cur_channel, single_alarm) + if single_alarm: + data = pack_elems(elems_to_save, file_types_to_save, tmp_signal.get_signal()) + print('SAVE CURRENT SIGNAL SROCHNO TI MENYA SLISHISH?!?!?!?') + try: + remote_save_data(conn, data, module_name, freq, shared_folder, path_to_save_alarms) + except Exception as e: + print(f"Ошибка: {e}") + else: + print('VSE OKI DOKI SIGNAL SOKHRANYAT NE NUZHNO!!!') + + f = multi_channel.change_channel() + except Exception as e: + print(str(e)) + print(".", end='') + + tmp_signal.clear() + + return f diff --git a/src/main_3300.py b/src/main_3300.py new file mode 100644 index 0000000..fbf3cde --- /dev/null +++ b/src/main_3300.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_3300 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from common.runtime import load_root_env, resolve_hackrf_index + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('hack_3300', 'src/main_3300.py') + serial_number = os.getenv('hack_3300') + pos = None + output = [] + try: + # command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq) + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(100, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #for k in range(0, 3): + # light_diods_on_boot() + + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + print('СЕРВИСНАЯ ИНФОРМАЦИЯ: ') + print('debug_flag: ', my_freq.debug_flag) + print('save_data_flag: ', my_freq.save_data_flag) + print('send_to_module_flag: ', my_freq.send_to_module_flag) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '1200'))) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '715'))) + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/main_433.py b/src/main_433.py new file mode 100644 index 0000000..58d7783 --- /dev/null +++ b/src/main_433.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_433 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from common.runtime import load_root_env, resolve_hackrf_index + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('hack_433', 'src/main_433.py') + serial_number = os.getenv('hack_433') + pos = None + output = [] + try: + # command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq) + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(10, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #for k in range(0, 3): + # light_diods_on_boot() + + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + print('СЕРВИСНАЯ ИНФОРМАЦИЯ: ') + print('debug_flag: ', my_freq.debug_flag) + print('save_data_flag: ', my_freq.save_data_flag) + print('send_to_module_flag: ', my_freq.send_to_module_flag) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '1200'))) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '715'))) + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/main_4500.py b/src/main_4500.py new file mode 100644 index 0000000..4d8a844 --- /dev/null +++ b/src/main_4500.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_4500 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from common.runtime import load_root_env, resolve_hackrf_index + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('hack_4500', 'src/main_4500.py') + serial_number = os.getenv('hack_4500') + pos = None + output = [] + try: + # command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq) + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(100, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #for k in range(0, 3): + # light_diods_on_boot() + + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + print('СЕРВИСНАЯ ИНФОРМАЦИЯ: ') + print('debug_flag: ', my_freq.debug_flag) + print('save_data_flag: ', my_freq.save_data_flag) + print('send_to_module_flag: ', my_freq.send_to_module_flag) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '1200'))) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '715'))) + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/main_5200.py b/src/main_5200.py new file mode 100644 index 0000000..73495f9 --- /dev/null +++ b/src/main_5200.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_5200 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from common.runtime import load_root_env, resolve_hackrf_index + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('hack_5200', 'src/main_5200.py') + serial_number = os.getenv('hack_5200') + pos = None + output = [] + try: + # command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq) + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(100, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #for k in range(0, 3): + # light_diods_on_boot() + + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + print('СЕРВИСНАЯ ИНФОРМАЦИЯ: ') + print('debug_flag: ', my_freq.debug_flag) + print('save_data_flag: ', my_freq.save_data_flag) + print('send_to_module_flag: ', my_freq.send_to_module_flag) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '1200'))) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '715'))) + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/main_5800.py b/src/main_5800.py new file mode 100644 index 0000000..1395e4a --- /dev/null +++ b/src/main_5800.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_5800 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from common.runtime import load_root_env, resolve_hackrf_index + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('hack_5800', 'src/main_5800.py') + serial_number = os.getenv('hack_5800') + pos = None + output = [] + try: + # command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq) + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(5, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(100, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #for k in range(0, 3): + # light_diods_on_boot() + + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + print('СЕРВИСНАЯ ИНФОРМАЦИЯ: ') + print('debug_flag: ', my_freq.debug_flag) + print('save_data_flag: ', my_freq.save_data_flag) + print('send_to_module_flag: ', my_freq.send_to_module_flag) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '1200'))) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '715'))) + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/main_750.py b/src/main_750.py new file mode 100644 index 0000000..092ed43 --- /dev/null +++ b/src/main_750.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_750 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from common.runtime import load_root_env, resolve_hackrf_index + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('hack_750', 'src/main_750.py') + serial_number = os.getenv('hack_750') + pos = None + output = [] + try: + # command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq) + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(100, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #for k in range(0, 3): + # light_diods_on_boot() + + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + print('СЕРВИСНАЯ ИНФОРМАЦИЯ: ') + print('debug_flag: ', my_freq.debug_flag) + print('save_data_flag: ', my_freq.save_data_flag) + print('send_to_module_flag: ', my_freq.send_to_module_flag) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '1200'))) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '715'))) + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/main_868.py b/src/main_868.py new file mode 100644 index 0000000..baed2d3 --- /dev/null +++ b/src/main_868.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_868 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from common.runtime import load_root_env, resolve_hackrf_index + +load_root_env(__file__) + + +def get_hack_id(): + return resolve_hackrf_index('hack_868', 'src/main_868.py') + serial_number = os.getenv('hack_868') + pos = None + output = [] + try: + # command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq) + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(100, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #for k in range(0, 3): + # light_diods_on_boot() + + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + print('СЕРВИСНАЯ ИНФОРМАЦИЯ: ') + print('debug_flag: ', my_freq.debug_flag) + print('save_data_flag: ', my_freq.save_data_flag) + print('send_to_module_flag: ', my_freq.send_to_module_flag) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '1200'))) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '715'))) + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/server.py b/src/server.py new file mode 100644 index 0000000..1e3530f --- /dev/null +++ b/src/server.py @@ -0,0 +1,1010 @@ +# #! /usr/bin/env python +# # -*- coding: utf-8 -*- +# import os +# import json +# import httpx +# import asyncio +# import requests +# import websockets +# from copy import deepcopy +# from fastapi import FastAPI +# from dotenv import load_dotenv +# from datetime import datetime, timedelta +# import csv + +# app = FastAPI() + +# ############################################################################ +# # VARIABLES +# ############################################################################ +# dotenv_path = os.path.join(os.path.dirname(__file__), '.env') +# load_dotenv(dotenv_path) +# lochost = os.getenv('lochost') +# locport = os.getenv('locport') +# jamhost = os.getenv('jamhost') +# jamport = os.getenv('jamport') +# master_server_ip = os.getenv('master_server_ip') +# master_server_port = os.getenv('master_server_port') + +# freqs = [str(x) for x in os.getenv('freqs').split(',')] +# num_of_clear_packs = int(os.getenv('num_of_clear_packs')) +# threshold_to_alarm = int(os.getenv('threshold_to_alarm')) + +# time_to_jam = int(os.getenv('time_to_jam')) +# time_to_fresh = int(os.getenv('time_to_fresh')) +# active_interval_to_send = int(os.getenv('active_interval_to_send')) +# passive_interval_to_send = int(os.getenv('passive_interval_to_send')) +# jammer_timeout = int(os.getenv('jammer_timeout')) +# master_timeout = int(os.getenv('master_timeout')) +# amount_connection_attempts = int(os.getenv('amount_connection_attempts')) + +# debug_module_flag = bool(os.getenv('debug_module_flag')) +# send_to_module_flag = bool(os.getenv('send_to_module_flag')) +# send_to_master_flag = bool(os.getenv('send_to_master_flag')) +# send_to_jammer_flag = bool(os.getenv('send_to_jammer_flag')) + +# latitude = float(os.getenv('latitude')) +# longitude = float(os.getenv('longitude')) + + +# flag = 0 +# max_len_bulk = 1 +# bulk_data = [] + +# sending_data_task = None +# jam_server_connect = None + +# alarm = False +# alarm_list = [] +# jammer_event = False + +# data_queue = [None] * len(freqs) +# freqs_alarm = {freq: 0 for freq in freqs} + + +# #TODO: +# # 1. Вырезать flag, если нужно и возможно. +# # 2. Пофиксить костыль с asyncio.create_task(sending_data) - после jammer_event'a перестает подавать признаки жизни, +# # поэтому гасим, когда включается глушилка и запускаем заново, когда глушилка отключается. +# # 3. Пофиксить момент с data_queue:из-за асинхронных функций и старой реализации сервака происходит так ,что +# # прилетает пакет аларма, система это видит, хочет его отправить, начинает отправку и из-за того, что это немного +# # долгий процесс, то успевает прилететь чистый пакет и на мастер улетает чистый пакет. +# # 4. Добавить print, только если deub_module_flag. + + +# ############################################################################ +# # GPS MODULE - INACTIVE +# ############################################################################ +# # Создание планировщика +# # scheduler = BackgroundScheduler(daemon=True) +# # scheduler.start() +# # @app.route('/get_gps', methods = ['POST']) +# # @scheduler.scheduled_job(IntervalTrigger(minutes=1)) +# # def update_gps_coordinates(): +# # # data_gps = request.json +# # result = { +# # 'latitude': latitude, +# # 'longitude': longitude +# # } +# # try: +# # url = "http://{0}:{1}/data/gps/{2}".format(master_server_ip, master_server_port, mac_address) +# # response = requests.post(url, json=result) +# # if response.status_code == 200: +# # print('gps успешно отправлен') +# # else: +# # print('gps не был отправлен') +# # +# # except Exception: +# # print('gps не были отправлены из-за отстутствия сервера в поле видимости') +# # return result +# # +# # +# # @scheduler.scheduled_job(IntervalTrigger(seconds=10)) +# # def send_gps_to_master(): +# # try: +# # subprocess.run(["python3", "GPS_get_coords.py"]) +# # #mac_address = get_mac_address() +# # data_gps = update_gps_coordinates() +# # url = "http://{0}:{1}/data/gps/{2}".format(master_server_ip, master_server_port, mac_address) +# # response = requests.post(url, json=data_gps) +# # if response.status_code == 200: +# # print('gps успешно отправлен') +# # else: +# # print('gps не был отправлены по какой-то причине') +# # except Exception: +# # print('gps не были отправлены по причине отсутствия сервера в поле видимости') +# # + + +# ############################################################################ +# # MODULE RIGISTR +# ############################################################################ +# def get_mac_address(interface='eth0'): +# """ +# Получить мак текущего устройства, на котором развернут модуль сервер. +# :param interface: +# """ +# try: +# result = os.popen('sudo ifconfig ' + interface).read() +# mac_index = result.find('ether') # Индекс начала строки с MAC-адресом +# if mac_index != -1: +# mac_address = result[mac_index + 6:mac_index + 23] +# return mac_address +# else: +# return None +# except Exception as e: +# print("Ошибка при получении MAC-адреса:" + str(e)) +# return None + + +# def get_ip_address(interface='eth0'): +# """ +# Получить айпишник текущего устройства, на котором развернут модуль сервер. +# :param interface: +# """ +# try: +# result = os.popen('sudo ifconfig ' + interface).read() +# ip_index = result.find('inet') # Индекс начала строки с IP-адресом +# if ip_index != -1: +# ip_address = result[ip_index + 5:ip_index + 17] +# return ip_address.strip() +# else: +# return None +# except Exception as e: +# print("Ошибка при получении IP-адреса:" + str(e)) +# return None + + +# def register_module(): +# """ +# Регистрация модуля на мастер сервере. +# """ + +# data = {'mac': get_mac_address(), +# 'ip': get_ip_address(), +# 'moduleType': 'freq'} +# try: +# url = f"http://{master_server_ip}:{master_server_port}/module/register" +# response = requests.post(url, json=data) +# response.raise_for_status() # Проверка успешности запроса +# print("Модуль зарегистрирован успешно = ", data) +# except requests.exceptions.RequestException as e: +# flag = 1 +# print("Ошибка при регистрации модуля:" + str(e), data) + + +# ############################################################################ +# # SEND DATA TO MASTER +# ############################################################################ +# async def send_to_master(ModuleDataSingleV2, flag): +# """ +# Отправка данных на мастер по посту или через булк. По посту установлен лимит времени на отправку. В случае его +# превышения - данные не отправлены. В случа неудачи отправки по любому из методов - данные не отправлены. +# :param ModuleDataSingleV2: Пакет данных. +# :param flag: +# :return: +# """ +# mac_address = get_mac_address() +# async with httpx.AsyncClient() as client: +# for _ in range(amount_connection_attempts): +# try: +# if flag == 0: +# url = f"http://{master_server_ip}:{master_server_port}/data/single/{mac_address}" +# else: +# url = f"http://{master_server_ip}:{master_server_port}/data/bulk/{mac_address}" +# response = await client.post(url, json=ModuleDataSingleV2, timeout=master_timeout) +# if response.status_code == 200: +# print('Данные успешно отправлены') +# flag = 0 +# bulk_data.clear() +# return 0 +# else: +# flag = 1 +# if len(bulk_data) > max_len_bulk: # Если лимит bulk_data превышен, то удаляем первый элемент списка +# bulk_data.pop(0) +# bulk_data.append(ModuleDataSingleV2) +# print('Данные не были отправлены по какой-то причине') +# except Exception as e: +# if len(bulk_data) > max_len_bulk: # Если лимит bulk_data превышен, то удаляем первый элемент списка +# bulk_data.pop(0) +# bulk_data.append(ModuleDataSingleV2) +# flag = 1 +# print('*'*100) +# print('Данные не были отправлены по причине отсутствия сервера в поле видимости') + + +# ############################################################################ +# # PROCESS DATA +# ############################################################################ +# async def check_alarm(amplitude: int): +# """ +# Проверка амплитуды на превышение границы отработки системы. +# :param amplitude: Амплитуда. +# :return: Превышает/не превышает. +# """ +# if amplitude > threshold_to_alarm: +# return True +# else: +# return False + + +# async def agregate_data(data_to_agregate: list): +# """ +# Сбор пакета для отправки на мастер сервер. +# :param data_to_agregate: Список из частотных пакетов. Длина списка = количесту обнаруживаемых частот. Может +# содержать None, если частота ничего не присылает на модуль сервер. +# :return: Пакет данных для отправки на мастер. +# """ + +# data = [] + +# if any(item is not None for item in data_to_agregate): +# for item in data_to_agregate: +# if item is not None: +# item['freq']=int(item['freq']) +# data.append(item) + +# now = datetime.utcnow() - timedelta(seconds=2) +# now = now.strftime("%Y-%m-%d %H:%M:%S") +# data = { +# "registeredAt": now, +# "data": data +# } + +# for i in range(len(freqs)): +# data_queue[i] = None + +# return data + + +# async def sending_data(): + +# #TODO: Надо по-хорошему нормально эту функцию переписать +# """ +# Отправка пакета данных на мастер сервер раз в некоторое время в определенном формате. Время отправки зависит +# от текущего статуса тревоги (аларм/не аларм). +# """ + +# global alarm +# global jammer_event +# global alarm_list + +# while True: +# print('while true!') +# print(data_queue) + + +# # Если перед отправкой на мастер все было чисто, то ждем 60 сек. +# # Если во время этих 60 сек. пришел пакет с алармом, то рассматриваем ситуации: +# if not alarm: +# alarm_list = [] +# ModuleDataSingleV2 = await agregate_data(deepcopy(data_queue)) +# print(f'На Мастер будет отправлена следующая информация: {ModuleDataSingleV2}') +# await send_to_master(ModuleDataSingleV2, flag) + +# for i in range(passive_interval_to_send, 0, -1): +# print('ТАЙМЕР ', i) +# await asyncio.sleep(1) +# if alarm: +# break + +# # Если стоит флаг отправить данные на джеммер и при этом еще не был получен ивент на глушилку, то +# # отправляем на джеммер данные. +# else: +# print(alarm_list) +# data = deepcopy(data_queue) +# for i in alarm_list: +# try: +# p = list(map(lambda x: str(x['freq']), data)).index(i) +# print(p) +# data[p]['amplitude'] = 9 +# data[p]['triggered'] = True +# except ValueError: +# data.append({'freq': i, 'amplitude': 9, 'triggered': True}) + +# print(data) + +# alarm_list = [] +# ModuleDataSingleV2 = await agregate_data(deepcopy(data)) +# print(f'На Мастер будет отправлена следующая информация: {ModuleDataSingleV2}') +# await send_to_master(ModuleDataSingleV2, flag) + +# if await send_jam_server_alarm(): +# print('Отправили на сервис подавления и все дошло успешно') +# else: +# print('Не смогли отправить на сервис подавления') +# continue + +# # В случае аларма ждем секунду перед новой отправкой данных. +# await asyncio.sleep(active_interval_to_send) + + +# @app.post('/waterfall') +# async def waterfall(data: dict): +# print('Received data: ', data) + + +# @app.post('/process_data') +# async def process_data(data: dict): +# """ +# Прием данных со скриптов детекции в формате data = {"freq": freq, +# "amplitude": amplitude +# } +# где freq - строка, amplitude - int и их первичная обработка. +# :param data: словарь с двумя ключами и значениями. +# """ + +# global alarm +# global alarm_list + +# print('Received data: ', data) +# data_dict = deepcopy(data) + +# # Агрегируем N пакетов данных от частот в один общий список, он используется в функции agregate data. +# # Каждая позиция списка фиксируется за отдельной частотой. +# freq = data_dict['freq'] +# for i in range(len(freqs)): +# if freq == freqs[i]: +# #Так делаем потому, что сервак является центром принятия решений по триггеру. +# trigger = await check_alarm(data_dict['amplitude']) +# data_dict.update({'triggered': trigger}) + +# data_queue[i] = deepcopy(data_dict) +# data_dict.clear() + +# # Если прилетел триггер и глушилка не включена, то запускаем/обновляем счетчик чистых пакетов на этой +# # частоте. +# if trigger and not jammer_event: +# freqs_alarm[freq] = num_of_clear_packs +# print(f'freqs_alarm выглядит следующим обазом: {freqs_alarm}') + +# #Если прилетел триггер и модуль еще не заалармлен, то алармим. +# if trigger and not alarm: +# print('Приелет триггерa со сканнера. Работаем, ребята!') +# alarm = True +# alarm_list.append(str(freq)) + +# # Если прилетел триггер и модуль заалармлен, но при этом глушилка не работает и счетчик чистых пакетов +# # данной частоты не равен нулю, то уменьшаем его. А когда в словаре счетчиков все нули, то убираем alarm. +# elif not trigger and alarm and not jammer_event and freqs_alarm[freq] != 0: +# freqs_alarm[freq] -= 1 +# print(f'Чистый пакет. Уменьшаем в выбранной частоте: {freqs_alarm}') + +# if all(value == 0 for value in freqs_alarm.values()): +# alarm = False +# print(f'Прилетело {num_of_clear_packs}. Отключаем аларм и freqs_alarm выглядит так: {freqs_alarm}') + +# else: +# continue + +# print('После получения данных data_queue выглядит следующим образом: ', data_queue) + + +# ############################################################################ +# # JAMMER +# ############################################################################ +# async def jammer_active(): +# """ +# Включение подавителя. +# Отменяем таску на отправку данных на мастер. Зануляем словарь чистых пакетов и объявляем о прилете ивента с сервиса +# подавления. +# """ + +# global jammer_event +# global freqs_alarm +# global sending_data_task + +# if sending_data_task is not None: +# sending_data_task.cancel() + +# freqs_alarm = {freq: 0 for freq in freqs} +# # with open('university_records.csv', 'w', newline='') as csvfile: +# # fieldnames = ['freq', 'branch', 'year', 'cgpa'] +# # writer = csv.DictWriter(csvfile, fieldnames=fieldnames) +# # writer.writeheader() +# # writer.writerows(data) +# jammer_event = True + +# print('АКТИВИРУЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААА!!!!') +# print('-' * 20) +# print('Статус по переменным:') +# print(f'freqs_alarm: {freqs_alarm}') +# print(f'jammer_event: {jammer_event}') +# print(f'alarm: {alarm}') +# print('-' * 20) + + +# async def jammer_deactive(): +# """ +# Отключение подавителя. +# Отрубаем аларм на модуле, отрубаем ивент сервера подавителей и запускаем таску отправки данных на мастер. +# :return: +# """ + +# global jammer_event +# global alarm +# global sending_data_task +# alarm = False +# jammer_event = False +# sending_data_task = asyncio.create_task(sending_data()) + +# print('ОТКЛЮЧАЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААААА!!!!') +# print('-' * 20) +# print('Статус по переменным:') +# print(f'freqs_alarm: {freqs_alarm}') +# print(f'jammer_event: {jammer_event}') +# print(f'alarm: {alarm}') +# print('-' * 20) + + +# async def send_jam_server_alarm(): +# """ +# Отправка алармовского пакета на сервер подавления по вебсокету. На отправку дается jammer_timeout секунд. +# При неудаче - данные не отправлены. +# """ + +# global jam_server_connect + +# msg = {'type': 'freq_alarm', +# 'data': True} + +# # if jam_server_connect: +# # try: +# # await jam_server_connect.send(json.dumps(msg)) +# # await asyncio.wait_for(jam_server_connect.recv(), jammer_timeout) +# # return True +# # except (asyncio.TimeoutError, websockets.exceptions.WebSocketException) as e: +# # print(f"WebSocket error or timeout: {e}") +# # return False +# # else: +# # return False + +# async with httpx.AsyncClient() as client: +# try: +# url = f"http://{jamhost}:{jamport}/jammer/unsafe_run" +# response = await client.post(url, timeout=jammer_timeout) +# if response.status_code in [200, 208]: +# print('Данные успешно отправлены') +# return True +# else: +# print('Данные не были отправлены по какой-то причине') +# except (httpx.RequestError, asyncio.TimeoutError) as e: +# print('Данные не были отправлены по причине отсутствия сервера в поле видимости') +# except Exception as e: +# print('Ошибка ', str(e)) + + +# async def jam_server(): +# """ +# Прием данных по вебсокету с сервера подавления и их обработка. Включение/отключение подавителя. +# При разрыве соединения принудительно отключаем подавитель. +# """ + +# uri = f'ws://{jamhost}:{jamport}/ws' +# global jam_server_connect +# while True: +# try: +# jam_server_connect = await websockets.connect(uri) +# while True: +# data_from_jam_server = await jam_server_connect.recv() +# data_from_jam_server = json.loads(data_from_jam_server) +# print('Принял с сервера глушилок: ', data_from_jam_server) +# if data_from_jam_server['type'] == 'run': +# alarm_status = (data_from_jam_server['data'])['state'] +# print(alarm_status) +# if alarm_status: +# await jammer_active() +# else: +# await asyncio.sleep(5) +# await jammer_deactive() +# except Exception as e: +# jam_server_connect = None +# if jammer_event: +# await jammer_deactive() + + +# @app.on_event("startup") +# async def startup_event(): +# """ +# Запускаем параллельно задачи jam_server и sending_data. +# """ + +# global sending_data_task +# asyncio.create_task(jam_server()) +# sending_data_task = asyncio.create_task(sending_data()) + + +# if __name__ == '__main__': +# import uvicorn +# # update_gps_coordinates() +# register_module() # Регистрация модуля на сервере +# uvicorn.run(app, host=lochost, port=int(locport)) + + +#! /usr/bin/env python +# -*- coding: utf-8 -*- +import os +import json +import httpx +import uvicorn +import asyncio +import requests +import websockets +from copy import deepcopy +from fastapi import FastAPI +from dotenv import load_dotenv +from datetime import datetime, timedelta +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +app = FastAPI() + +############################################################################ +# VARIABLES +############################################################################ +dotenv_path = os.path.join(os.path.dirname(__file__), '.env') +load_dotenv(dotenv_path) +lochost = os.getenv('lochost') +locport = os.getenv('locport') +jamhost = os.getenv('jamhost') +jamport = os.getenv('jamport') +master_server_ip = os.getenv('master_server_ip') +master_server_port = os.getenv('master_server_port') + +freqs = [str(x) for x in os.getenv('freqs').split(',')] +num_of_clear_packs = int(os.getenv('num_of_clear_packs')) +threshold_to_alarm = int(os.getenv('threshold_to_alarm')) + +time_to_jam = int(os.getenv('time_to_jam')) +time_to_fresh = int(os.getenv('time_to_fresh')) +active_interval_to_send = int(os.getenv('active_interval_to_send')) +passive_interval_to_send = int(os.getenv('passive_interval_to_send')) +jammer_timeout = int(os.getenv('jammer_timeout')) +master_timeout = int(os.getenv('master_timeout')) + +debug_module_flag = bool(os.getenv('debug_module_flag')) +send_to_module_flag = bool(os.getenv('send_to_module_flag')) +send_to_master_flag = bool(os.getenv('send_to_master_flag')) +send_to_jammer_flag = bool(os.getenv('send_to_jammer_flag')) + +latitude = float(os.getenv('latitude')) +longitude = float(os.getenv('longitude')) + + +p = 1 +flag = 0 +max_len_bulk = 1 +bulk_data = [] + +sending_data_task = None +jam_server_connect = None + +alarm = False +jammer_event = False + +data_queue = [None] * len(freqs) +freqs_alarm = {freq: 0 for freq in freqs} + + +#TODO: +# 1. Вырезать flag, если нужно и возможно. +# 2. Пофиксить костыль с asyncio.create_task(sending_data) - после jammer_event'a перестает подавать признаки жизни, +# поэтому гасим, когда включается глушилка и запускаем заново, когда глушилка отключается. +# 3. Пофиксить момент с data_queue:из-за асинхронных функций и старой реализации сервака происходит так ,что +# прилетает пакет аларма, система это видит, хочет его отправить, начинает отправку и из-за того, что это немного +# долгий процесс, то успевает прилететь чистый пакет и на мастер улетает чистый пакет. +# 4. Добавить print, только если deub_module_flag. + + +############################################################################ +# GPS MODULE - INACTIVE +############################################################################ +# Создание планировщика +# scheduler = BackgroundScheduler(daemon=True) +# scheduler.start() +# @app.route('/get_gps', methods = ['POST']) +# @scheduler.scheduled_job(IntervalTrigger(minutes=1)) +# def update_gps_coordinates(): +# # data_gps = request.json +# result = { +# 'latitude': latitude, +# 'longitude': longitude +# } +# try: +# url = "http://{0}:{1}/data/gps/{2}".format(master_server_ip, master_server_port, mac_address) +# response = requests.post(url, json=result) +# if response.status_code == 200: +# print('gps успешно отправлен') +# else: +# print('gps не был отправлен') +# +# except Exception: +# print('gps не были отправлены из-за отстутствия сервера в поле видимости') +# return result +# +# +# @scheduler.scheduled_job(IntervalTrigger(seconds=10)) +# def send_gps_to_master(): +# try: +# subprocess.run(["python3", "GPS_get_coords.py"]) +# #mac_address = get_mac_address() +# data_gps = update_gps_coordinates() +# url = "http://{0}:{1}/data/gps/{2}".format(master_server_ip, master_server_port, mac_address) +# response = requests.post(url, json=data_gps) +# if response.status_code == 200: +# print('gps успешно отправлен') +# else: +# print('gps не был отправлены по какой-то причине') +# except Exception: +# print('gps не были отправлены по причине отсутствия сервера в поле видимости') +# + + +############################################################################ +# MODULE RIGISTR +############################################################################ +def get_mac_address(interface='enp5s0'): + """ + Получить мак текущего устройства, на котором развернут модуль сервер. + :param interface: + """ + try: + result = os.popen('sudo ifconfig ' + interface).read() + mac_index = result.find('ether') # Индекс начала строки с MAC-адресом + if mac_index != -1: + mac_address = result[mac_index + 6:mac_index + 23] + return mac_address + else: + return None + except Exception as e: + print("Ошибка при получении MAC-адреса:" + str(e)) + return None + + +def get_ip_address(interface='enp5s0'): + """ + Получить айпишник текущего устройства, на котором развернут модуль сервер. + :param interface: + """ + try: + result = os.popen('sudo ifconfig ' + interface).read() + ip_index = result.find('inet') # Индекс начала строки с IP-адресом + if ip_index != -1: + ip_address = result[ip_index + 5:ip_index + 19] + return ip_address.strip() + else: + return None + except Exception as e: + print("Ошибка при получении IP-адреса:" + str(e)) + return None + + +def register_module(): + """ + Регистрация модуля на мастер сервере. + """ + + data = {'mac': get_mac_address(), + 'ip': get_ip_address(), + 'moduleType': 'freq'} + try: + url = f"http://{master_server_ip}:{master_server_port}/module/register" + response = requests.post(url, json=data) + response.raise_for_status() # Проверка успешности запроса + print("Модуль зарегистрирован успешно = ", data) + except requests.exceptions.RequestException as e: + flag = 1 + print("Ошибка при регистрации модуля:" + str(e), data) + + +############################################################################ +# SEND DATA TO MASTER +############################################################################ +async def send_to_master(ModuleDataSingleV2, flag): + """ + Отправка данных на мастер по посту или через булк. По посту установлен лимит времени на отправку. В случае его + превышения - данные не отправлены. В случа неудачи отправки по любому из методов - данные не отправлены. + :param ModuleDataSingleV2: Пакет данных. + :param flag: + :return: + """ + mac_address = get_mac_address() + async with httpx.AsyncClient() as client: + try: + if flag == 0: + url = f"http://{master_server_ip}:{master_server_port}/data/single/{mac_address}" + else: + url = f"http://{master_server_ip}:{master_server_port}/data/bulk/{mac_address}" + response = await client.post(url, json=ModuleDataSingleV2, timeout=master_timeout) + if response.status_code == 200: + print('Данные успешно отправлены') + flag = 0 + bulk_data.clear() + else: + print(response.text) + flag = 1 + if len(bulk_data) > max_len_bulk: # Если лимит bulk_data превышен, то удаляем первый элемент списка + bulk_data.pop(0) + bulk_data.append(ModuleDataSingleV2) + print('Данные не были отправлены по какой-то причине') + except (httpx.RequestError, asyncio.TimeoutError) as e: + if len(bulk_data) > max_len_bulk: # Если лимит bulk_data превышен, то удаляем первый элемент списка + bulk_data.pop(0) + bulk_data.append(ModuleDataSingleV2) + flag = 1 + print('Данные не были отправлены по причине отсутствия сервера в поле видимости') + + +############################################################################ +# PROCESS DATA +############################################################################ +async def check_alarm(amplitude: int): + """ + Проверка амплитуды на превышение границы отработки системы. + :param amplitude: Амплитуда. + :return: Превышает/не превышает. + """ + if amplitude > threshold_to_alarm: + return True + else: + return False + + +async def agregate_data(data_to_agregate: list): + """ + Сбор пакета для отправки на мастер сервер. + :param data_to_agregate: Список из частотных пакетов. Длина списка = количесту обнаруживаемых частот. Может + содержать None, если частота ничего не присылает на модуль сервер. + :return: Пакет данных для отправки на мастер. + """ + + data = [] + + if any(item is not None for item in data_to_agregate): + for item in data_to_agregate: + if item is not None: + item['freq']=int(item['freq']) + data.append(item) + + now = datetime.utcnow() - timedelta(seconds=2) + now = now.strftime("%Y-%m-%d %H:%M:%S") + data = { + "registeredAt": now, + "data": data + } + + for i in range(len(freqs)): + data_queue[i] = None + + return data + + +async def sending_data(): + #TODO: Надо по-хорошему нормально эту функцию переписать + """ + Отправка пакета данных на мастер сервер раз в некоторое время в определенном формате. Время отправки зависит + от текущего статуса тревоги (аларм/не аларм). + """ + + global alarm + global jammer_event + + while True: + print('while true!') + ModuleDataSingleV2 = await agregate_data(deepcopy(data_queue)) + if send_to_master_flag: + print(f'На Мастер будет отправлена следующая информация: {ModuleDataSingleV2}') + await send_to_master(ModuleDataSingleV2, flag) + + # Если перед отправкой на мастер все было чисто, то ждем 60 сек. + # Если во время этих 60 сек. пришел пакет с алармом, то рассматриваем ситуации: + if not alarm: + for i in range(passive_interval_to_send, 0, -1): + print('ТАЙМЕР ', i) + await asyncio.sleep(1) + if alarm: + break + + # Если стоит флаг отправить данные на джеммер и при этом еще не был получен ивент на глушилку, то + # отправляем на джеммер данные. + elif alarm and send_to_jammer_flag and not jammer_event: + if await send_jam_server_alarm(): + print('Отправили на сервис подавления и все дошло успешно') + else: + print('Не смогли отправить на сервис подавления') + + # Сюда почему-то не заходит и вообще функция не подает признаков жизни после запуска подваителя(( + if alarm and jammer_event: + print('ПОДАВИТЕЛЬ РАБОТАЕТ РАЗБЕГАЙСЯ ААААААААААААААААААА') + + # В случае аларма ждем секунду перед новой отправкой данных. + if alarm: + await asyncio.sleep(active_interval_to_send) + + +@app.post('/waterfall') +async def waterfall(data: dict): + print('Received data: ', data) + + +@app.post('/process_data') +async def process_data(data: dict): + """ + Прием данных со скриптов детекции в формате data = {"freq": freq, + "amplitude": amplitude + } + где freq - строка, amplitude - int и их первичная обработка. + :param data: словарь с двумя ключами и значениями. + """ + + global alarm + + print('Received data: ', data) + data_dict = deepcopy(data) + + # Агрегируем N пакетов данных от частот в один общий список, он используется в функции agregate data. + # Каждая позиция списка фиксируется за отдельной частотой. + freq = data_dict['freq'] + for i in range(len(freqs)): + if freq == freqs[i]: + #Так делаем потому, что сервак является центром принятия решений по триггеру. + trigger = await check_alarm(data_dict['amplitude']) + data_dict.update({'triggered': trigger}) + + data_queue[i] = deepcopy(data_dict) + data_dict.clear() + + # Если прилетел триггер и глушилка не включена, то запускаем/обновляем счетчик чистых пакетов на этой + # частоте. + if trigger and not jammer_event: + freqs_alarm[freq] = num_of_clear_packs + print(f'freqs_alarm выглядит следующим обазом: {freqs_alarm}') + + #Если прилетел триггер и модуль еще не заалармлен, то алармим. + if trigger and not alarm: + print('Приелет триггерa со сканнера. Работаем, ребята!') + alarm = True + + # Если прилетел триггер и модуль заалармлен, но при этом глушилка не работает и счетчик чистых пакетов + # данной частоты не равен нулю, то уменьшаем его. А когда в словаре счетчиков все нули, то убираем alarm. + elif not trigger and alarm and not jammer_event and freqs_alarm[freq] != 0: + freqs_alarm[freq] -= 1 + print(f'Чистый пакет. Уменьшаем в выбранной частоте: {freqs_alarm}') + + if all(value == 0 for value in freqs_alarm.values()): + alarm = False + print(f'Прилетело {num_of_clear_packs}. Отключаем аларм и freqs_alarm выглядит так: {freqs_alarm}') + + else: + continue + + print('После получения данных data_queue выглядит следующим образом: ', data_queue) + + +############################################################################ +# JAMMER +############################################################################ +async def jammer_active(): + global p + """ + Включение подавителя. + Отменяем таску на отправку данных на мастер. Зануляем словарь чистых пакетов и объявляем о прилете ивента с сервиса + подавления. + """ + + global jammer_event + global freqs_alarm + global sending_data_task + + if sending_data_task is not None: + sending_data_task.cancel() + p = 0 + print(p) + + freqs_alarm = {freq: 0 for freq in freqs} + jammer_event = True + + print('АКТИВИРУЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААА!!!!') + print('-' * 20) + print('Статус по переменным:') + print(f'freqs_alarm: {freqs_alarm}') + print(f'jammer_event: {jammer_event}') + print(f'alarm: {alarm}') + print('-' * 20) + + +async def jammer_deactive(): + global p + """ + Отключение подавителя. + Отрубаем аларм на модуле, отрубаем ивент сервера подавителей и запускаем таску отправки данных на мастер. + :return: + """ + + global jammer_event + global alarm + global sending_data_task + alarm = False + jammer_event = False + if p == 0: + sending_data_task = asyncio.create_task(sending_data()) + p = 1 + #print(p) + + + print('ОТКЛЮАЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААААА!!!!') + print('-' * 20) + print('Статус по переменным:') + print(f'freqs_alarm: {freqs_alarm}') + print(f'jammer_event: {jammer_event}') + print(f'alarm: {alarm}') + print('-' * 20) + + +async def send_jam_server_alarm(): + """ + Отправка алармовского пакета на сервер подавления по вебсокету. На отправку дается jammer_timeout секунд. + При неудаче - данные не отправлены. + """ + + global jam_server_connect + + msg = {'type': 'freq_alarm', + 'data': True} + + if jam_server_connect: + try: + await jam_server_connect.send(json.dumps(msg)) + await asyncio.wait_for(jam_server_connect.recv()) + return True + except (asyncio.TimeoutError, websockets.exceptions.WebSocketException) as e: + print(f"WebSocket error or timeout: {e}") + return False + else: + return False + + +async def jam_server(): + """ + Прием данных по вебсокету с сервера подавления и их обработка. Включение/отключение подавителя. + При разрыве соединения принудительно отключаем подавитель. + """ + + uri = f'ws://{jamhost}:{jamport}/ws' + global jam_server_connect + while True: + try: + jam_server_connect = await websockets.connect(uri) + while True: + data_from_jam_server = await jam_server_connect.recv() + data_from_jam_server = json.loads(data_from_jam_server) + print('Принял с сервера глушилок: ', data_from_jam_server) + if data_from_jam_server['type'] == 'run': + alarm_status = (data_from_jam_server['data'])['state'] + print(alarm_status) + if alarm_status: + await jammer_active() + else: + await jammer_deactive() + except Exception as e: + jam_server_connect = None + if jammer_event: + await jammer_deactive() + + + +@app.on_event("startup") +async def startup_event(): + """ + Запускаем параллельно задачи jam_server и sending_data. + """ + + global sending_data_task + asyncio.create_task(jam_server()) + sending_data_task = asyncio.create_task(sending_data()) + + +if __name__ == '__main__': + # update_gps_coordinates() + register_module() # Регистрация модуля на сервере + uvicorn.run(app, host=lochost, port=int(locport)) \ No newline at end of file diff --git a/src/server_to_master.py b/src/server_to_master.py new file mode 100644 index 0000000..d054e73 --- /dev/null +++ b/src/server_to_master.py @@ -0,0 +1,500 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +import os +import json +import httpx +import asyncio +import requests +import websockets +from copy import deepcopy +from fastapi import FastAPI +from common.runtime import load_root_env, validate_env, as_bool, as_float, as_int, as_str +from datetime import datetime, timedelta + +app = FastAPI() + +############################################################################ +# VARIABLES +############################################################################ +load_root_env(__file__) +validate_env("src/server_to_master.py", { + "lochost": as_str, + "locport": as_int, + "jamhost": as_str, + "jamport": as_int, + "master_server_ip": as_str, + "master_server_port": as_int, + "freqs": as_str, + "num_of_clear_packs": as_int, + "threshold_to_alarm": as_int, + "time_to_jam": as_int, + "time_to_fresh": as_int, + "active_interval_to_send": as_int, + "passive_interval_to_send": as_int, + "jammer_timeout": as_int, + "master_timeout": as_int, + "debug_module_flag": as_bool, + "send_to_module_flag": as_bool, + "send_to_master_flag": as_bool, + "send_to_jammer_flag": as_bool, + "latitude": as_float, + "longitude": as_float, +}) +lochost = os.getenv('lochost') +locport = os.getenv('locport') +jamhost = os.getenv('jamhost') +jamport = os.getenv('jamport') +master_server_ip = os.getenv('master_server_ip') +master_server_port = os.getenv('master_server_port') + +freqs = [str(x) for x in os.getenv('freqs').split(',')] +num_of_clear_packs = int(os.getenv('num_of_clear_packs')) +threshold_to_alarm = int(os.getenv('threshold_to_alarm')) + +time_to_jam = int(os.getenv('time_to_jam')) +time_to_fresh = int(os.getenv('time_to_fresh')) +active_interval_to_send = int(os.getenv('active_interval_to_send')) +passive_interval_to_send = int(os.getenv('passive_interval_to_send')) +jammer_timeout = int(os.getenv('jammer_timeout')) +master_timeout = int(os.getenv('master_timeout')) + +debug_module_flag = as_bool(os.getenv('debug_module_flag', '0')) +send_to_module_flag = as_bool(os.getenv('send_to_module_flag', '0')) +send_to_master_flag = as_bool(os.getenv('send_to_master_flag', '0')) +send_to_jammer_flag = as_bool(os.getenv('send_to_jammer_flag', '0')) + +latitude = float(os.getenv('latitude')) +longitude = float(os.getenv('longitude')) + +i = 0 +flag = 0 +max_len_bulk = 1 +bulk_data = [] + +sending_data_task = None +jam_server_connect = None + +alarm = False +jammer_event = False + +data_queue = [None] * len(freqs) +freqs_alarm = {freq: 0 for freq in freqs} + + +#TODO: +# 1. Вырезать flag, если нужно и возможно. +# 2. Пофиксить костыль с asyncio.create_task(sending_data) - после jammer_event'a перестает подавать признаки жизни, +# поэтому гасим, когда включается глушилка и запускаем заново, когда глушилка отключается. +# 3. Пофиксить момент с data_queue:из-за асинхронных функций и старой реализации сервака происходит так ,что +# прилетает пакет аларма, система это видит, хочет его отправить, начинает отправку и из-за того, что это немного +# долгий процесс, то успевает прилететь чистый пакет и на мастер улетает чистый пакет. +# 4. Добавить print, только если deub_module_flag. + + +############################################################################ +# GPS MODULE - INACTIVE +############################################################################ +# Создание планировщика +# scheduler = BackgroundScheduler(daemon=True) +# scheduler.start() +# @app.route('/get_gps', methods = ['POST']) +# @scheduler.scheduled_job(IntervalTrigger(minutes=1)) +# def update_gps_coordinates(): +# # data_gps = request.json +# result = { +# 'latitude': latitude, +# 'longitude': longitude +# } +# try: +# url = "http://{0}:{1}/data/gps/{2}".format(master_server_ip, master_server_port, mac_address) +# response = requests.post(url, json=result) +# if response.status_code == 200: +# print('gps успешно отправлен') +# else: +# print('gps не был отправлен') +# +# except Exception: +# print('gps не были отправлены из-за отстутствия сервера в поле видимости') +# return result +# +# +# @scheduler.scheduled_job(IntervalTrigger(seconds=10)) +# def send_gps_to_master(): +# try: +# subprocess.run(["python3", "GPS_get_coords.py"]) +# #mac_address = get_mac_address() +# data_gps = update_gps_coordinates() +# url = "http://{0}:{1}/data/gps/{2}".format(master_server_ip, master_server_port, mac_address) +# response = requests.post(url, json=data_gps) +# if response.status_code == 200: +# print('gps успешно отправлен') +# else: +# print('gps не был отправлены по какой-то причине') +# except Exception: +# print('gps не были отправлены по причине отсутствия сервера в поле видимости') +# + + +############################################################################ +# MODULE RIGISTR +############################################################################ +def get_mac_address(interface='enp5s0'): + """ + Получить мак текущего устройства, на котором развернут модуль сервер. + :param interface: + """ + try: + result = os.popen('sudo ifconfig ' + interface).read() + mac_index = result.find('ether') # Индекс начала строки с MAC-адресом + if mac_index != -1: + mac_address = result[mac_index + 6:mac_index + 23] + return mac_address + else: + return None + except Exception as e: + print("Ошибка при получении MAC-адреса:" + str(e)) + return None + + +def get_ip_address(interface='enp5s0'): + """ + Получить айпишник текущего устройства, на котором развернут модуль сервер. + :param interface: + """ + try: + result = os.popen('sudo ifconfig ' + interface).read() + ip_index = result.find('inet') # Индекс начала строки с IP-адресом + if ip_index != -1: + ip_address = result[ip_index + 5:ip_index + 19] + return ip_address.strip() + else: + return None + except Exception as e: + print("Ошибка при получении IP-адреса:" + str(e)) + return None + + +def register_module(): + """ + Регистрация модуля на мастер сервере. + """ + + data = {'mac': get_mac_address(), + 'ip': get_ip_address()} + try: + url = f"http://{master_server_ip}:{master_server_port}/module/register" + response = requests.post(url, json=data) + response.raise_for_status() # Проверка успешности запроса + print("Модуль зарегистрирован успешно = ", data) + except requests.exceptions.RequestException as e: + flag = 1 + print("Ошибка при регистрации модуля:" + str(e), data) + + +############################################################################ +# SEND DATA TO MASTER +############################################################################ +async def send_to_master(ModuleDataSingleV2, flag): + """ + Отправка данных на мастер по посту или через булк. По посту установлен лимит времени на отправку. В случае его + превышения - данные не отправлены. В случа неудачи отправки по любому из методов - данные не отправлены. + :param ModuleDataSingleV2: Пакет данных. + :param flag: + :return: + """ + mac_address = get_mac_address() + async with httpx.AsyncClient() as client: + try: + if flag == 0: + url = f"http://{master_server_ip}:{master_server_port}/data/single/{mac_address}" + else: + url = f"http://{master_server_ip}:{master_server_port}/data/bulk/{mac_address}" + response = await client.post(url, json=ModuleDataSingleV2, timeout=master_timeout) + if response.status_code == 200: + print('Данные успешно отправлены') + flag = 0 + bulk_data.clear() + else: + flag = 1 + if len(bulk_data) > max_len_bulk: # Если лимит bulk_data превышен, то удаляем первый элемент списка + bulk_data.pop(0) + bulk_data.append(ModuleDataSingleV2) + print('Данные не были отправлены по какой-то причине') + except (httpx.RequestError, asyncio.TimeoutError) as e: + if len(bulk_data) > max_len_bulk: # Если лимит bulk_data превышен, то удаляем первый элемент списка + bulk_data.pop(0) + bulk_data.append(ModuleDataSingleV2) + flag = 1 + print('Данные не были отправлены по причине отсутствия сервера в поле видимости') + + +############################################################################ +# PROCESS DATA +############################################################################ +async def check_alarm(amplitude: int): + """ + Проверка амплитуды на превышение границы отработки системы. + :param amplitude: Амплитуда. + :return: Превышает/не превышает. + """ + if amplitude > threshold_to_alarm: + return True + else: + return False + + +async def agregate_data(data_to_agregate: list): + """ + Сбор пакета для отправки на мастер сервер. + :param data_to_agregate: Список из частотных пакетов. Длина списка = количесту обнаруживаемых частот. Может + содержать None, если частота ничего не присылает на модуль сервер. + :return: Пакет данных для отправки на мастер. + """ + + data = [] + + if any(item is not None for item in data_to_agregate): + for item in data_to_agregate: + if item is not None: + item['freq'] = int(item['freq']) + data.append(item) + + now = datetime.utcnow() - timedelta(seconds=2) + now = now.strftime("%Y-%m-%d %H:%M:%S") + data = { + "registeredAt": now, + "data": data + } + + for i in range(len(freqs)): + data_queue[i] = None + + return data + + +async def sending_data(): + #TODO: Надо по-хорошему нормально эту функцию переписать + """ + Отправка пакета данных на мастер сервер раз в некоторое время в определенном формате. Время отправки зависит + от текущего статуса тревоги (аларм/не аларм). + """ + + global i + global alarm + global jammer_event + + if i == 0: + while True: + i=1 + print('while true!') + ModuleDataSingleV2 = await agregate_data(deepcopy(data_queue)) + if send_to_master_flag: + print(f'На Мастер будет отправлена следующая информация: {ModuleDataSingleV2}') + await send_to_master(ModuleDataSingleV2, flag) + + # Если перед отправкой на мастер все было чисто, то ждем 60 сек. + # Если во время этих 60 сек. пришел пакет с алармом, то рассматриваем ситуации: + if not alarm: + for i in range(passive_interval_to_send, 0, -1): + print('ТАЙМЕР ', i) + await asyncio.sleep(1) + if alarm: + break + + # Если стоит флаг отправить данные на джеммер и при этом еще не был получен ивент на глушилку, то + # отправляем на джеммер данные. + elif alarm and send_to_jammer_flag and not jammer_event: + if await send_jam_server_alarm(): + print('Отправили на сервис подавления и все дошло успешно') + else: + print('Не смогли отправить на сервис подавления') + + # Сюда почему-то не заходит и вообще функция не подает признаков жизни после запуска подваителя(( + if alarm and jammer_event: + print('ПОДАВИТЕЛЬ РАБОТАЕТ РАЗБЕГАЙСЯ ААААААААААААААААААА') + + # В случае аларма ждем секунду перед новой отправкой данных. + if alarm: + await asyncio.sleep(active_interval_to_send) + i = 0 + + +@app.post('/waterfall') +async def waterfall(data: dict): + print('Received data: ', data) + + +@app.post('/process_data') +async def process_data(data: dict): + """ + Прием данных со скриптов детекции в формате data = {"freq": freq, + "amplitude": amplitude + } + где freq - строка, amplitude - int и их первичная обработка. + :param data: словарь с двумя ключами и значениями. + """ + + global alarm + + print('Received data: ', data) + data_dict = deepcopy(data) + + # Агрегируем N пакетов данных от частот в один общий список, он используется в функции agregate data. + # Каждая позиция списка фиксируется за отдельной частотой. + freq = data_dict['freq'] + for i in range(len(freqs)): + if freq == freqs[i]: + #Так делаем потому, что сервак является центром принятия решений по триггеру. + trigger = await check_alarm(data_dict['amplitude']) + data_dict.update({'triggered': trigger}) + + data_queue[i] = deepcopy(data_dict) + data_dict.clear() + + # Если прилетел триггер и глушилка не включена, то запускаем/обновляем счетчик чистых пакетов на этой + # частоте. + if trigger and not jammer_event: + freqs_alarm[freq] = num_of_clear_packs + print(f'freqs_alarm выглядит следующим обазом: {freqs_alarm}') + + #Если прилетел триггер и модуль еще не заалармлен, то алармим. + if trigger and not alarm: + print('Приелет триггерa со сканнера. Работаем, ребята!') + alarm = True + + # Если прилетел триггер и модуль заалармлен, но при этом глушилка не работает и счетчик чистых пакетов + # данной частоты не равен нулю, то уменьшаем его. А когда в словаре счетчиков все нули, то убираем alarm. + elif not trigger and alarm and not jammer_event and freqs_alarm[freq] != 0: + freqs_alarm[freq] -= 1 + print(f'Чистый пакет. Уменьшаем в выбранной частоте: {freqs_alarm}') + + if all(value == 0 for value in freqs_alarm.values()): + alarm = False + print(f'Прилетело {num_of_clear_packs}. Отключаем аларм и freqs_alarm выглядит так: {freqs_alarm}') + + else: + continue + + print('После получения данных data_queue выглядит следующим образом: ', data_queue) + + +############################################################################ +# JAMMER +############################################################################ +async def jammer_active(): + """ + Включение подавителя. + Отменяем таску на отправку данных на мастер. Зануляем словарь чистых пакетов и объявляем о прилете ивента с сервиса + подавления. + """ + + global jammer_event + global freqs_alarm + global sending_data_task + + if sending_data_task is not None: + sending_data_task.cancel() + + freqs_alarm = {freq: 0 for freq in freqs} + jammer_event = True + + print('АКТИВИРУЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААА!!!!') + print('-' * 20) + print('Статус по переменным:') + print(f'freqs_alarm: {freqs_alarm}') + print(f'jammer_event: {jammer_event}') + print(f'alarm: {alarm}') + print('-' * 20) + + +async def jammer_deactive(): + """ + Отключение подавителя. + Отрубаем аларм на модуле, отрубаем ивент сервера подавителей и запускаем таску отправки данных на мастер. + :return: + """ + + global jammer_event + global alarm + global sending_data_task + alarm = False + jammer_event = False + sending_data_task = asyncio.create_task(sending_data()) + + print('ОТКЛЮАЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААААА!!!!') + print('-' * 20) + print('Статус по переменным:') + print(f'freqs_alarm: {freqs_alarm}') + print(f'jammer_event: {jammer_event}') + print(f'alarm: {alarm}') + print('-' * 20) + + +async def send_jam_server_alarm(): + """ + Отправка алармовского пакета на сервер подавления по вебсокету. На отправку дается jammer_timeout секунд. + При неудаче - данные не отправлены. + """ + + global jam_server_connect + + msg = {'type': 'freq_alarm', + 'data': True} + + if jam_server_connect: + try: + await jam_server_connect.send(json.dumps(msg)) + await asyncio.wait_for(jam_server_connect.recv(), jammer_timeout) + return True + except (asyncio.TimeoutError, websockets.exceptions.WebSocketException) as e: + print(f"WebSocket error or timeout: {e}") + return False + else: + return False + + +async def jam_server(): + """ + Прием данных по вебсокету с сервера подавления и их обработка. Включение/отключение подавителя. + При разрыве соединения принудительно отключаем подавитель. + """ + + uri = f'ws://{jamhost}:{jamport}/ws' + global jam_server_connect + while True: + try: + jam_server_connect = await websockets.connect(uri) + while True: + data_from_jam_server = await jam_server_connect.recv() + data_from_jam_server = json.loads(data_from_jam_server) + print('Принял с сервера глушилок: ', data_from_jam_server) + if data_from_jam_server['type'] == 'run': + alarm_status = (data_from_jam_server['data'])['state'] + print(alarm_status) + if alarm_status: + await jammer_active() + else: + await jammer_deactive() + except Exception as e: + jam_server_connect = None + if jammer_event: + await jammer_deactive() + + + +@app.on_event("startup") +async def startup_event(): + """ + Запускаем параллельно задачи jam_server и sending_data. + """ + + global sending_data_task + asyncio.create_task(jam_server()) + sending_data_task = asyncio.create_task(sending_data()) + + +if __name__ == '__main__': + import uvicorn + # update_gps_coordinates() + register_module() # Регистрация модуля на сервере + uvicorn.run(app, host=lochost, port=int(locport)) \ No newline at end of file diff --git a/src/server_to_tablet.py b/src/server_to_tablet.py new file mode 100644 index 0000000..87ad217 --- /dev/null +++ b/src/server_to_tablet.py @@ -0,0 +1,314 @@ +import json +import os +import asyncio +import websockets +from common.runtime import load_root_env +from typing import List +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException + +load_root_env(__file__) + +threshold_to_alarm = int(os.getenv('threshold_to_alarm')) +time_to_jam = int(os.getenv('time_to_jam')) +time_to_fresh = int(os.getenv('time_to_fresh')) +lochost = os.getenv('lochost') +locport = int(os.getenv('locport')) +jamhost = os.getenv('jamhost') +jamport = os.getenv('jamport') + + +# TODO Добавить обработку кнопки и водопад. + + +class FreqConfig: + """ + Конфиг частот, отображаемых на планшете. + Атрибуты: + freq_config: Словарь с частотами и преднастройкой (вкл/выкл). + """ + def __init__(self): + self.freq_config = { + '433': False, + '500': False, + '700': True, + '868': True, + '915': True, + '1200': True, + '1500': False, + '2400': True, + '5200': True, + '5800': True + } + + def get(self): + return self.freq_config + + def get_status(self, freq): + """ + Проверка активности выбранной частоты. + :param freq: Частота для проверки. + :return: Вкл/выкл. + """ + return self.freq_config[freq] + + def set_active(self, freq: str, status: bool): + """ + Переключение частоты в состояние вкл/выкл. + :param freq: Частота для отключения/включения. + :param status: Положение, в которое переключается частота (True - вкл, False - выкл). + :return: None. + """ + self.freq_config[freq] = status + + +app = FastAPI() +websocket_connections: List[WebSocket] = [] +jam_server_connect = None +alarm = False +freqconfig = FreqConfig() + + +def check_active_tablets() -> None: + """ + Проверка активных соединений с каким-либо планшетом. + """ + if not websocket_connections: + raise HTTPException(status_code=400, detail="No active WebSocket connections = No tablets in sight.") + + +async def send_to_tablets(package) -> None: + """ + Рассылка данных по планшетам. + :param package: Собранный пакет данных для отправки. + """ + + global alarm + + if not websocket_connections: + print('Нет подключенных планшетов/клиентов.') + else: + for websocket in websocket_connections: + try: + print('Пытаемся отправить данные клиенту ', websocket) + await websocket.send_json(package) + print(f'Пакет {package} успешно отправлен.') + except Exception as e: + print(f"Не смогли отправить данные клиенту по вебсокету: {e}") + + +async def check_alarm(amplitude: int): + if amplitude > threshold_to_alarm: + return await send_jam_server_alarm() + else: + return False + + +# async def freq_active(freq: str): +# """ +# Запуск скрина с частотой после ее активации в частотном конфиге. +# :param freq: Частота для запуска скрина. +# """ +# +# # TODO добавить чек частоты в скринах, а то вдруг мы запускаем скрин, а он уже запущен. +# print(f'АКТИВИРУЕМ ЧАСТОТУ {freq}') +# command = f"screen -dmS {freq} sh -c \"export PYTHONPATH=/home/orangepi && bash -c \'python3 " \ +# f"/home/orangepi/DroneScanner/src/main_5800.py\' && exec bash\"" +# screen = subprocess.run(command, shell=True, capture_output=True, text=True) +# +# # Проверяем результат выполнения +# if screen.returncode == 0: +# print("Команда успешно выполнена") +# else: +# print("Ошибка при выполнении команды") +# print("STDERR:", screen.stderr) +# +# +# async def freq_deactive(freq: str): +# """ +# Килл скрин с частотой после ее деактивации в частотном конфиге. +# :param freq: Частота для скрин килл. +# """ +# +# print(f'ОТКЛЮЧАЕМ ЧАСТОТУ {freq}') + + +@app.post('/send-waterfall/') +async def send_waterfall(data: dict) -> None: + """ + Прием водопада и отсылка на планшеты, если необходимо. + :param data: Водопад в виде пакета data = {...} + """ + print('bubble tea!') + + +async def set_freq_config(data: dict): + """ + Переключение состояний частот пришедших с одного планшета и рассылка этой информации на остальные. + Запуск/отключение необходимых скринов. + :param data: Словарь вида {частота: состояние} + :return: None. + """ + for freq, activ in data.items(): + freqconfig.set_active(freq, activ) + print(f'Частота {freq} перешла в состояние {activ}: {freqconfig.get_status(freq)}') + msg = {'type': 'freq_config', + 'data': {freq: activ}} + await send_to_tablets(msg) + # if activ: + # await freq_active(freq) + # + # else: + # await freq_deactive(freq) + + +@app.post("/process_data") +async def send_data(data: dict): + """ + Прием данных со скриптов детекции в формате data = {"freq": freq, + "amplitude": amplitude + } + где freq - строка, amplitude - int, их обработка и рассылка по планшетам. + :param data: Словарь. + """ + + global alarm + global jam_server_connect + # check_active_tablets() + print('На сервер пришли данные: ', data) + + if alarm and jam_server_connect: + print('Подавитель активен.') + return {'message': 'Подавитель активен.'} + elif alarm and jam_server_connect is None: + alarm = False + + if not freqconfig.get_status(data['freq']): + print('Частота выключена.') + return {'message': 'Частота выключена.'} + + if await check_alarm(data['amplitude']): + print('Затриггерились') + + msg = {'type': 'freq', + 'data': data + } + + await send_to_tablets(msg) + + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket) -> None: + """ + Прием данных (freq config) по вебсокету от клиента (планшетов) и их обработка. + При подключении к серверу - отсылка на клиента текущее состояние частотного конфига. + :param websocket: + :return: + """ + await websocket.accept() + websocket_connections.append(websocket) + msg = {'type': 'freq_config', + 'data': freqconfig.get() + } + await websocket.send_json(msg) + try: + while True: + try: + data_from_client = await websocket.receive_json() # Ожидание получения данных от клиента + if 'freq' in data_from_client: + data = data_from_client['freq'] + print('Приняли с планшета: ', data) + await set_freq_config(data) + except WebSocketDisconnect: + print("Client disconnected") + websocket_connections.remove(websocket) + break + except Exception as e: + print(f"Error receiving data: {e}") + except WebSocketDisconnect: + websocket_connections.remove(websocket) + # print(e) + # print(f"Client disconnected: {e.code} - {e.reason}") + + +################################################################################## +# Подключение к серверу глушилок, как клиент по вебсокету и обработка информации. +################################################################################## +async def jammer_active(): + """ + Активируем подавитель. + """ + + global alarm + print('АКТИВИРУЕМ ПОДАВИТЕЛЬ!!!!') + alarm = True + + +async def jammer_deactive(): + """ + Отключаем подавитель. + """ + + global alarm + print('ОТКЛЮАЕМ ПОДАВИТЕЛЬ!!!!') + alarm = False + + +async def send_jam_server_alarm(): + """ + Отправить пакет с триггером на сервер подавителей. + :return: True, если соединение с сервером активно и данные отправлены успешно. False - иначе. + """ + + global jam_server_connect + msg = {'type': 'freq_alarm', + 'data': True} + if jam_server_connect: + await jam_server_connect.send(json.dumps(msg)) + return True + else: + return False + + +async def jam_server(): + """ + Подключиться к серверу подавителей по вебсокету как клиент. Получение и обработка пакетов - активация/деактивация + подавителя. + """ + uri = f'ws://{jamhost}:{jamport}/ws' + global jam_server_connect + while True: + try: + jam_server_connect = await websockets.connect(uri) + while True: + data_from_jam_server = await jam_server_connect.recv() + data_from_jam_server = json.loads(data_from_jam_server) + print('Принял с сервера глушилок: ', data_from_jam_server) + if data_from_jam_server['type'] == 'run': + alarm_status = (data_from_jam_server['data'])['state'] + print(alarm_status) + if alarm_status: + await jammer_active() + else: + await jammer_deactive() + except Exception as e: + jam_server_connect = None + if alarm: + await jammer_deactive() + + +@app.on_event("startup") +async def startup_event(): + """ + Запуск подключения к серверу подавления и получения от него сообщений. + """ + + asyncio.create_task(jam_server()) + + +################################################################################## +# Запуск приложения. +################################################################################## +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host=lochost, port=locport) diff --git a/src/unused/__init__.py b/src/unused/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/unused/embedded_1200.py b/src/unused/embedded_1200.py new file mode 100644 index 0000000..cfc84d6 --- /dev/null +++ b/src/unused/embedded_1200.py @@ -0,0 +1,108 @@ +import os +import datetime +from smb.SMBConnection import SMBConnection +from dotenv import load_dotenv +from DroneScanner.utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data +from DroneScanner.core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length +from DroneScanner.core.multichannelswitcher import MultiChannel, get_centre_freq + +dotenv_path = os.path.join(os.path.dirname(__file__), '../../.env') +if os.path.exists(dotenv_path): + load_dotenv(dotenv_path) + +debug_flag = bool(os.getenv('debug_flag')) +send_to_module_flag = bool(os.getenv('send_to_module_flag')) +save_data_flag = bool(os.getenv('save_data_flag')) +module_name = os.getenv('module_name') +elems_to_save = os.getenv('elems_to_save') +file_types_to_save = os.getenv('file_types_to_save') +localhost = os.getenv('lochost') +localport = os.getenv('locport') +f_step = [*map(float, os.getenv('f_step_1200').split())] +f_bases = [*map(float, os.getenv('f_bases_1200').split())] +f_roofs = [*map(float, os.getenv('f_roofs_1200').split())] +path_to_save_medians = os.getenv('path_to_save_medians') +path_to_save_alarms = os.getenv('path_to_save_alarms') +smb_host = os.getenv('smb_host') +smb_port = os.getenv('smb_port') +smb_user = os.getenv('smb_user') +smb_pass = os.getenv('smb_pass') +shared_folder = os.getenv('shared_folder') +the_pc_name = os.getenv('the_pc_name') +remote_pc_name = os.getenv('remote_pc_name') +smb_domain = os.getenv('smb_domain') + +elems_to_save = elems_to_save.split(',') +file_types_to_save = file_types_to_save.split(',') + +tmp_signal = Signal() +tmp_sigs_array = SignalsArray() +multi_channel = MultiChannel(f_step, f_bases, f_roofs) +f = multi_channel.init_f() +multi_channel.fill_DB() + +if debug_flag: + conn = SMBConnection(smb_user, smb_pass, the_pc_name, remote_pc_name, use_ntlm_v2=True) + conn.connect(smb_host, 139) + filelist = conn.listPath(shared_folder, '/') + print(filelist) + + +def work(lvl): + + f = multi_channel.get_cur_channel() + freq = get_centre_freq(f) + signal_length = get_signal_length(freq) + median, signal, abs_signal = tmp_signal.fill_sig(lvl, signal_length) + + if median != -1: + try: + num_chs, circle_buffer = multi_channel.check_f(f) + + #print(f, freq, num_chs, signal_length) + #print(median) + + cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs) + + if sigs_array: + print('Значения на {0}: {1}'.format(freq, sigs_array)) + print('Пороги: ', circle_buffer.get_medians()) + alarm = circle_buffer.check_alarm(sigs_array) + + if alarm: + print('----ALARM---- ', freq) + multi_channel.db_alarms_zeros(circle_buffer) + else: + circle_buffer.update(sigs_array) + + if send_to_module_flag: + send_data(agregator(freq, alarm), localhost, localport) + + if save_data_flag: + if not circle_buffer.check_init() and circle_buffer.current_column - 1 == 0: + save_data(path_to_save_medians, freq, 'DateTime', 'ALARM', 'max signal', list(range(num_chs)), + list(range(num_chs))) + if circle_buffer.check_init(): + save_data(path_to_save_medians, freq, datetime.datetime.now(), alarm, max(sigs_array), sigs_array, + circle_buffer.get_medians()) + # print(circle_buffer.get_buffer()) + # print(circle_buffer.get_medians()) + # print(circle_buffer.get_alarms()) + if debug_flag: + single_alarm = circle_buffer.check_single_alarm(median, cur_channel) + print(cur_channel, single_alarm) + if single_alarm: + data = pack_elems(elems_to_save, file_types_to_save, signal, abs_signal) + #print('SAVE CURRENT SIGNAL SROCHNO TI MENYA SLISHISH?!?!?!?') + try: + remote_save_data(conn, data, module_name, freq, shared_folder, path_to_save_alarms) + except Exception as e: + print(f"Ошибка: {e}") + #else: + #print('VSE OKI DOKI SIGNAL SOKHRANYAT NE NUZHNO!!!') + + f = multi_channel.change_channel() + except Exception as e: + print(str(e)) + print(".", end='') + return f diff --git a/src/unused/embedded_2400.py b/src/unused/embedded_2400.py new file mode 100644 index 0000000..3cca892 --- /dev/null +++ b/src/unused/embedded_2400.py @@ -0,0 +1,276 @@ + +# this module will be imported in the into your flowgraph +import numpy as np +import os + +import platform +from rknnlite.api import RKNNLite + +import wiringpi as wpi +from wiringpi import GPIO + +import requests +import json +from dotenv import load_dotenv + + +dotenv_path = os.path.join(os.path.dirname(__file__), '../../.env') +load_dotenv(dotenv_path) +############################## +# wiringPi +############################## + +wpi.wiringPiSetup() + +############################## +# RKNN +############################## + +DEVICE_COMPATIBLE_NODE = '/proc/device-tree/compatible' + +RK3588_MODEL = { +'path': os.getenv('path_to_NN'), +'model': None, +'split_size': 50_000, +'N_predictions': 40, +'N_samples_confidence_threshold': 0.65, +} + +RK3588_MODEL['model'] = RKNNLite() + +try: + ret1 = RK3588_MODEL['model'].load_rknn(RK3588_MODEL['path']) + assert(ret1 == 0) +except Exception as e: + print(e) + +ret2 = RK3588_MODEL['model'].init_runtime(core_mask=RKNNLite.NPU_CORE_0) +assert(ret2 == 0) + +############################## +# HYPERPARAMETERS +############################## + +f_base = 2.48e9 +f_step = -10e6 +f_roof = 2.4e9 + +signal_num = 10 +#signal_dir = '/home/orangepi/CDD/Complex_DroneDetection/gnuradio/signal/' +#if not os.path.exists(signal_dir): +# os.mkdir(signal_dir) + + +reading_signal_delay = 0 +iterations = 3 # read signal iterations +height_threshold = 100 + +weak_avg_amount = 70 +weak_samples_confidence = 0.50 +#classes = {0: 'noise', 1: 'DJI', 2: 'other', 3: 'for weak'} +classes = {0: 'noise', 1: 'DJI_video', 2: 'DJI_control', 3: 'WIFI'} + +#pins = [11, 4, 3, 14, 12, 0, 1, 2, 5, 7] +pins = [11, 4, 3, 14, 12, 0, 1, 2, 5, 7] + +on_state = 0 +off_state = 1 +p2p_border = np.array([0.05, 0.065, 0.08, 0.95, 0.115, 0.125, 0.135, 0.185, 0.25, 0.3]) +amp_border = np.array([0, 0.06, 0.07, 0.075, 0.08, 0.085, 0.09, 0.095, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.20]) +#amp_decays = np.array([0, -0.15, -0.14, -0.13, -0.12, -0.11, -0.10,-0.09,-0.08,-0.07,-0.06,-0.05,-0.04,-0.03,-0.02,-0.01, -0.009, -0.008, -0.007]) +amp_decays = np.array([0, -0.07, -0.0675, -0.065, -0.0625, -0.06, -0.05,-0.045,-0.04,-0.035,-0.03,-0.025,-0.02,-0.015,-0.01,-0.008, -0.004, -0.002, -0.001]) + +assert(len(amp_border) == len(amp_decays)) +amp_slice_size = 50000 +pred_amps = [] +############################## +# Variables +############################## + +running_amp = 0 + +weak_detected = 0 +counter = 0 +ctrs = {0: 0, 1: 0, 2: 0, 3: 0} +avg_probs = {0: 0., 1: 0., 2: 0., 3: 0.} +avg_amps = {0: 0., 1: 0., 2: 0., 3: 0.} +max_amp = 0 + +max_freq = 0 +it = 0 # current reading flag +f = f_base # local frequency +EOCF = 0 # End of changing frequency flag +signal_arr = [] + +weak_avg_ctr = 0 +weak_ctr = 0 +avg_confidence = 0 +strong_confidence_threshold = 0.6 +weak_confidence_threshold = 0.85 +current_pin = 0 + +vals = [] +############################### support functions +############################## + +def calc_running_amp(sig): + global amp_slice_size + running_amp = np.average(np.sort(np.abs(sig).flatten())[::-1][:amp_slice_size]) + #print(running_amp) + return running_amp + +def softmax(x): + """Compute softmax values for each sets of scores in x.""" + e_x = np.exp(x - np.max(x)) + return e_x / e_x.sum() + + +def compute_signal_distance(signal: np.array) -> float: + """ + Computes a peak to peak distanse of the input signal. + + Arguments: + signal, np.array - an array of complex points of the input signal + + Returns: + sig_dist, float - peak to peak amplitude signal distance + """ + # sig_dist = np.max(np.abs(signal)) - np.min(np.abs(signal)) + sig_dist = np.abs(np.max(signal) - np.min(signal)) + return sig_dist + + +def send_data(sig_dist): + len_threshold = int(os.getenv('len_threshold')) + localhost = os.getenv('lochost') + localport = os.getenv('locport') + length = int(np.sum(np.where(p2p_border <= sig_dist, 1, 0))) + + if length >= len_threshold: + trigger = True + else: + trigger = False + + data_to_send = { + "freq": '2400', + "amplitude": length + # "triggered": trigger, + # "light_len": length + } + + response = requests.post("http://{0}:{1}/send-freq/".format(localhost, localport), json=data_to_send) + + if response.status_code == 200: + print("Данные успешно отправлены и приняты!") + else: + print("Ошибка при отправке данных: ", response.status_code) + + +############################## +# main function +############################## + +def work(lvl): + + global f_base + global f_step + global f_roof + + global signal_tag + + global reading_signal_delay + global iterations + + global max_amp + global max_freq + global it + + global f + + global EOCF + global strong_model + global weak_model + + global weak_ctr + global weak_avg_ctr + global avg_confidence + global strong_confidence_threshold + global weak_confidence_threshold + global weak_samples_confidence + global height_threshold + global signal_arr + global ctrs + global avg_probs + global avg_amps + global amp_border + global amp_decays + global classes + global pred_amps + global weak_detected + global vals + outputs = [] + + + y = np.array(lvl).ravel() + signal_arr = np.concatenate((signal_arr, y), axis=None) + + if f <= f_roof: + f = f_base + signal_arr = [] + send_data(np.max(np.array(vals))) + vals = [] + return f, EOCF + else: + label = None + if len(signal_arr) >= RK3588_MODEL['split_size']: # the signal length `soft` constraint + + sig = np.array([signal_arr.real[0:RK3588_MODEL['split_size']], signal_arr.imag[0:RK3588_MODEL['split_size']]], dtype=np.float32) + running_amp = calc_running_amp(sig) + # feeds the input signal into weak classifier + outputs = RK3588_MODEL['model'].inference(inputs=[sig]) + signal_arr = [] + + label = np.argmax(outputs, axis=2)[0][0] + probability = softmax(outputs[0][0])[label] + + #print(classes[label], round(probability, 2), int(f)) + + weak_ctr += 1 + if (label != 0) and (label != 3) and probability > weak_confidence_threshold: + weak_avg_ctr += 1 + ctrs[label] += 1 + avg_probs[label] += probability + avg_amps[label] += running_amp + if weak_ctr == RK3588_MODEL['N_predictions']: + prob = round(softmax(outputs[0][0])[label], 2) + #print('Detected: ', classes[label], ' Probability: ' , prob, 'Frequency: ', str(f)) + print('---------> Frequency: ', f) + for key in avg_probs.keys(): + avg_probs[key] = float(avg_probs[key] / max(1, ctrs[key])) + avg_amps[key] = float(avg_amps[key] / max(1, ctrs[key])) + + for key in ctrs.keys(): + print("avg prob: ", "%.4f" % avg_probs[key],"avg_amp: ", "%.4f" % avg_amps[key],"ctr: ", ctrs[key],"class: ", classes[key]) + #sfm = softmax(list(avg_probs.values())) + #print(sfm) + label = np.argmax(list(ctrs.values())) + weak_avg_ctr = ctrs[label] + weak_avg_prob = avg_probs[label] # / max(1, ctrs[label]) + weak_avg_amp = avg_amps[label] + amp_decay = 0 #amp_decays[min(np.sum(np.where(amp_border <= weak_avg_amp, 1, 0)), len(amp_border) - 1)] + final_confidence_threshold = weak_confidence_threshold + amp_decay + print('-' * 50, classes[label], weak_avg_ctr,"%.4f" % float(weak_avg_prob), '/', "%.4f" % final_confidence_threshold) + #print(classes[label] , ': ', sfm[label]) + avg_probs = {0: 0., 1: 0., 2: 0., 3: 0.} + avg_amps = {0: 0., 1: 0., 2: 0., 3: 0.} + ctrs = {0: 0, 1: 0, 2: 0, 3: 0} + if weak_avg_ctr >= RK3588_MODEL['N_predictions'] * RK3588_MODEL['N_samples_confidence_threshold'] and label != 0 and weak_avg_prob > final_confidence_threshold: + print('!!!' * 30, f) + vals.append(weak_avg_amp) + else: + vals.append(0) + f += f_step + + weak_ctr = 0 + weak_avg_ctr = 0 + return f, EOCF diff --git a/src/unused/embedded_915.py b/src/unused/embedded_915.py new file mode 100644 index 0000000..c8bfb74 --- /dev/null +++ b/src/unused/embedded_915.py @@ -0,0 +1,109 @@ +import os +import datetime +from smb.SMBConnection import SMBConnection +from dotenv import load_dotenv +from DroneScanner.utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data +from DroneScanner.core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length +from DroneScanner.core.multichannelswitcher import MultiChannel, get_centre_freq + +dotenv_path = os.path.join(os.path.dirname(__file__), '../../.env') +if os.path.exists(dotenv_path): + load_dotenv(dotenv_path) + +debug_flag = bool(os.getenv('debug_flag')) +send_to_module_flag = bool(os.getenv('send_to_module_flag')) +save_data_flag = bool(os.getenv('save_data_flag')) +module_name = os.getenv('module_name') +elems_to_save = os.getenv('elems_to_save') +file_types_to_save = os.getenv('file_types_to_save') +localhost = os.getenv('lochost') +localport = os.getenv('locport') +f_step = [*map(float, os.getenv('f_step_915').split())] +f_bases = [*map(float, os.getenv('f_bases_915').split())] +f_roofs = [*map(float, os.getenv('f_roofs_915').split())] +path_to_save_medians = os.getenv('path_to_save_medians') +path_to_save_alarms = os.getenv('path_to_save_alarms') +smb_host = os.getenv('smb_host') +smb_port = os.getenv('smb_port') +smb_user = os.getenv('smb_user') +smb_pass = os.getenv('smb_pass') +shared_folder = os.getenv('shared_folder') +the_pc_name = os.getenv('the_pc_name') +remote_pc_name = os.getenv('remote_pc_name') +smb_domain = os.getenv('smb_domain') + +elems_to_save = elems_to_save.split(',') +file_types_to_save = file_types_to_save.split(',') + +tmp_signal = Signal() +tmp_sigs_array = SignalsArray() +multi_channel = MultiChannel(f_step, f_bases, f_roofs) +f = multi_channel.init_f() +multi_channel.fill_DB() + +if debug_flag: + conn = SMBConnection(smb_user, smb_pass, the_pc_name, remote_pc_name, use_ntlm_v2=True) + conn.connect(smb_host, 139) + filelist = conn.listPath(shared_folder, '/') + print(filelist) + + +def work(lvl): + + f = multi_channel.get_cur_channel() + freq = get_centre_freq(f) + signal_length = get_signal_length(freq) + median, signal, abs_signal = tmp_signal.fill_sig(lvl, signal_length) + + if median != -1: + try: + num_chs, circle_buffer = multi_channel.check_f(f) + + #print(f, freq, num_chs) + #print(median) + + cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs) + + if sigs_array: + print('Значения на {0}: {1}'.format(freq, sigs_array)) + print('Пороги: ', circle_buffer.get_medians()) + alarm = circle_buffer.check_alarm(sigs_array) + + if alarm: + print('----ALARM---- ', freq) + multi_channel.db_alarms_zeros(circle_buffer) + else: + circle_buffer.update(sigs_array) + + if send_to_module_flag: + send_data(agregator(freq, alarm), localhost, localport) + + if save_data_flag: + if not circle_buffer.check_init() and circle_buffer.current_column - 1 == 0: + save_data(path_to_save_medians, freq, 'DateTime', 'ALARM', 'max signal', list(range(num_chs)), + list(range(num_chs))) + if circle_buffer.check_init(): + save_data(path_to_save_medians, freq, datetime.datetime.now(), alarm, max(sigs_array), sigs_array, + circle_buffer.get_medians()) + # print(circle_buffer.get_buffer()) + # print(circle_buffer.get_medians()) + # print(circle_buffer.get_alarms()) + if debug_flag: + single_alarm = circle_buffer.check_single_alarm(median, cur_channel) + print(cur_channel, single_alarm) + if single_alarm: + data = pack_elems(elems_to_save, file_types_to_save, signal, abs_signal) + print('SAVE CURRENT SIGNAL SROCHNO TI MENYA SLISHISH?!?!?!?') + try: + remote_save_data(conn, data, module_name, freq, shared_folder, path_to_save_alarms) + except Exception as e: + print(f"Ошибка: {e}") + else: + print('VSE OKI DOKI SIGNAL SOKHRANYAT NE NUZHNO!!!') + + f = multi_channel.change_channel() + except Exception as e: + print(str(e)) + print(".", end='') + + return f diff --git a/src/unused/main_1200.py b/src/unused/main_1200.py new file mode 100644 index 0000000..b0bc9b5 --- /dev/null +++ b/src/unused/main_1200.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_1200 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from dotenv import load_dotenv + +dotenv_path = os.path.join(os.path.dirname(__file__), '../../.env') +load_dotenv(dotenv_path) + + +def get_hack_id(): + serial_number = os.getenv('hack2') + pos = None + output = [] + try: + # command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq) + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(100, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #for k in range(0, 3): + # light_diods_on_boot() + + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + print('СЕРВИСНАЯ ИНФОРМАЦИЯ: ') + print('debug_flag: ', my_freq.debug_flag) + print('save_data_flag: ', my_freq.save_data_flag) + print('send_to_module_flag: ', my_freq.send_to_module_flag) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '1200'))) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '715'))) + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/unused/main_2400.py b/src/unused/main_2400.py new file mode 100644 index 0000000..5cce195 --- /dev/null +++ b/src/unused/main_2400.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_2400 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from dotenv import load_dotenv + + +dotenv_path = os.path.join(os.path.dirname(__file__), '../../.env') +load_dotenv(dotenv_path) + +def get_hack_id(): + serial_number = os.getenv('hack1') + pos = None + output = [] + try: + #command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq)[0] + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(100, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)[0]) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #light_diods_on_boot() + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + input('Press Enter to quit: ') + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/unused/main_915.py b/src/unused/main_915.py new file mode 100644 index 0000000..cebb917 --- /dev/null +++ b/src/unused/main_915.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: get_center_freq +# GNU Radio version: 3.8.1.0 + +from gnuradio import blocks +from gnuradio import gr +import sys +import signal +import embedded_915 as my_freq # embedded python module +import osmosdr +import time +import threading +import subprocess +import os + +from dotenv import load_dotenv + +dotenv_path = os.path.join(os.path.dirname(__file__), '../../.env') +load_dotenv(dotenv_path) + + +def get_hack_id(): + serial_number = os.getenv('hack3') + pos = None + output = [] + try: + # command = '/home/orangepi/hackrf/host/build/hackrf-tools/src/hackrf_info' + command = 'lsusb -v -d 1d50:6089 | grep iSerial' + output.append(subprocess.check_output(command, shell=True, text=True)) + + # indexes = [line.split(":")[1].strip() for line in output_lines if "Index" in line] + # serial_numbers = [line.split(":")[1].strip() for line in output_lines if "Serial number" in line] + # print(indexes) + # print(serial_numbers) + # for i, number in enumerate(serial_numbers): + # if number == serial_number: + # pos = i + # break + # if pos is not None: + # id = indexes[pos] + # else: + # print('Такого хака нет!') + except subprocess.CalledProcessError as e: + print(f"Команда завершилась с кодом возврата {e.returncode}") + print(e) + print(output) + output_lines = output[0].strip().split('\n') + print(output_lines) + serial_numbers = [line.split()[-1] for line in output_lines] + print(serial_numbers) + for i, number in enumerate(serial_numbers): + if number == serial_number: + id = i + break + if id is not None: + print('HackId is: {0}'.format(id)) + return str(id) + else: + print('Такого хака нет!') + + + + +class get_center_freq(gr.top_block): + + def __init__(self): + gr.top_block.__init__(self, "get_center_freq") + + ################################################## + # Variables + ################################################## + self.prob_freq = prob_freq = 0 + self.top_peaks_amount = top_peaks_amount = 20 + self.samp_rate = samp_rate = 20e6 + self.poll_rate = poll_rate = 10000 + self.num_points = num_points = 8192 + self.flag = flag = 1 + self.decimation = decimation = 1 + self.center_freq = center_freq = my_freq.work(prob_freq) + + ################################################## + # Blocks + ################################################## + self.probSigVec = blocks.probe_signal_vc(4096) + self.rtlsdr_source_0 = osmosdr.source( + args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id() + ) + self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t()) + self.rtlsdr_source_0.set_sample_rate(samp_rate) + self.rtlsdr_source_0.set_center_freq(center_freq, 0) + self.rtlsdr_source_0.set_freq_corr(0, 0) + self.rtlsdr_source_0.set_gain(100, 0) + self.rtlsdr_source_0.set_if_gain(100, 0) + self.rtlsdr_source_0.set_bb_gain(0, 0) + self.rtlsdr_source_0.set_antenna('', 0) + self.rtlsdr_source_0.set_bandwidth(0, 0) + self.rtlsdr_source_0.set_min_output_buffer(4096) + def _prob_freq_probe(): + while True: + + val = self.probSigVec.level() + try: + self.set_prob_freq(val) + except AttributeError: + pass + time.sleep(1.0 / (poll_rate)) + _prob_freq_thread = threading.Thread(target=_prob_freq_probe) + _prob_freq_thread.daemon = True + _prob_freq_thread.start() + + self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096) + + + + ################################################## + # Connections + ################################################## + self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0)) + self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0)) + + def get_prob_freq(self): + return self.prob_freq + + def set_prob_freq(self, prob_freq): + self.prob_freq = prob_freq + self.set_center_freq(my_freq.work(self.prob_freq)) + + def get_top_peaks_amount(self): + return self.top_peaks_amount + + def set_top_peaks_amount(self, top_peaks_amount): + self.top_peaks_amount = top_peaks_amount + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.rtlsdr_source_0.set_sample_rate(self.samp_rate) + + def get_poll_rate(self): + return self.poll_rate + + def set_poll_rate(self, poll_rate): + self.poll_rate = poll_rate + + def get_num_points(self): + return self.num_points + + def set_num_points(self, num_points): + self.num_points = num_points + + def get_flag(self): + return self.flag + + def set_flag(self, flag): + self.flag = flag + + def get_decimation(self): + return self.decimation + + def set_decimation(self, decimation): + self.decimation = decimation + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.rtlsdr_source_0.set_center_freq(self.center_freq, 0) + + + +def main(top_block_cls=get_center_freq, options=None): + #for k in range(0, 3): + # light_diods_on_boot() + + tb = top_block_cls() + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + sys.exit(0) + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + tb.start() + try: + print('СЕРВИСНАЯ ИНФОРМАЦИЯ: ') + print('debug_flag: ', my_freq.debug_flag) + print('save_data_flag: ', my_freq.save_data_flag) + print('send_to_module_flag: ', my_freq.send_to_module_flag) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '1200'))) + #print('multiply_factor: ', float(os.getenv('multiply_factor_' + '715'))) + except EOFError: + pass + #tb.stop() + tb.wait() + + +if __name__ == '__main__': + main() diff --git a/src/unused/npu/simplenet.weak.v2.4.rknn b/src/unused/npu/simplenet.weak.v2.4.rknn new file mode 100644 index 0000000000000000000000000000000000000000..c1969b3667fbf6921ebec37e4cacd5bcaa979321 GIT binary patch literal 194896 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

@aRa^)-th)hh}ZH@aSl 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 WaVWDhm#$nrdXCv9Ee{;}^8W#N+Al2t literal 0 HcmV?d00001 diff --git a/src/utils/GPS_get_coords.py b/src/utils/GPS_get_coords.py new file mode 100644 index 0000000..23f5527 --- /dev/null +++ b/src/utils/GPS_get_coords.py @@ -0,0 +1,86 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +import serial +import time +import string +import pynmea2 +import io +import re +import os +from dotenv import load_dotenv +# + +class GPSReader(): + def __init__(self, port=os.getenv('gpsport'), baudrate=9600, timeout=0.3): + self.port = port + self._ser = serial.Serial(port, baudrate=baudrate, timeout=timeout) + self._sio = io.TextIOWrapper(io.BufferedRWPair(self._ser,self._ser)) + self.gps_data = dict({'lat':0, 'lng':0}) + self.dataout = pynmea2.NMEAStreamReader() + def get_coords(self): + pass + + def get_gps_data(self): + try: + newdata=self._sio.readline() + if newdata[0:6] == "$GPRMC": + newmsg = pynmea2.parse(newdata) + lat = newmsg.latitude + lng = newmsg.longitude + self.gps_data['lat'] = lat + self.gps_data['lng'] = lng + ''' + newdata = pynmea2.parse(newdata) + k = repr(newdata) + print(k) + print(len(k)) + #lat = newdata.latitude + #lng = newdata.longitude + #sats = newdata.num_sats + #self.gps_data["lat"] = lat + #self.gps_data["lng"] = lng + #self.gps_data["sats"] = sats + if k[0] == '<': + d = dict({}) + data = re.sub(r'[<()>,]', "", k) + station = k[0:2] + data = data[3:].split(' ') + for i in data: + u = i.split('=') + d[u[0]] = u[1] + self.gps_data[station] = d + #print("!!!!!!!!!!!!!!!!!!!!!") + #print(self.gps_data[station]) + ''' + ''' + if lat != 0 and lng != 0 and sats != 0: + print('-' * 60) + print("Latitude: " + str(lat) + " Longitude: " + str(lng) + " num_sats: " + str(sats)) print('-' * 60) + sats = 0 + lat = 0 + lng = 0 + ''' + except Exception as e: + print(str(e)) + +load_dotenv('envserver.env') +gps = GPSReader() +localhost = os.getenv('lochost') +localport = os.getenv('locport') + + +while True: + try: + gps.get_gps_data() + except Exception as e: + print("1", str(e)) + +try: + response = requests.post("http://{0}:{1}/get_gps".format(localhost, localport), json=gps) + + if response.status_code == 200: + print("Данные успешно отправлены и приняты!") + else: + print("Ошибка при отправке данных:", response.status_code) +except Exception: + print('gps не отправлен') \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/datas_processing.py b/src/utils/datas_processing.py new file mode 100644 index 0000000..c0820f2 --- /dev/null +++ b/src/utils/datas_processing.py @@ -0,0 +1,133 @@ +import os +import io +import csv +import itertools +import requests +import numpy as np +from datetime import datetime + + +def pack_elems(names, file_types, *elems): + if len(names) != len(file_types) or len(names) != len(elems): + raise ValueError('Длин массивов имен и типов файлов и не совпадает с количество элементов для сохранения') + return {name: {'file_type': file_type, 'elem': elem} for name, file_type, elem in zip(names, file_types, elems)} + + +def agregator(freq, alarm): + if alarm: + amplitude = 9 + else: + amplitude = 0 + + data = {"freq": freq, + "amplitude": amplitude + } + return data + + +def send_data(data, localhost, localport, endpoint): + """ + Отправка данных по посту на модуль сервер. + :param data: Данные для отправки. + :param localhost: Хост модуль сервера. + :param localport: Порт модуль сервера. + """ + + try: + response = requests.post("http://{0}:{1}/{2}".format(localhost, localport, endpoint), json=data) + if response.status_code == 200: + print("Данные успешно отправлены и приняты!") + else: + print("Ошибка при отправке данных: ", response.status_code) + except Exception as e: + print(str(e)) + + +def save_data(path_to_save, freq, *args): + """ + Сохранение данных в csv файл. Используется для сохранения метрик и медиан сигнала на каналах с датой и временем + - для анализа. + :param path_to_save: Путь для сохранения. + :param freq: Обрабатываемая частота. + :param args: Что сохраняем в файл. + """ + + try: + if not os.path.exists(path_to_save): + print('Folder was created.') + os.makedirs(path_to_save) + + with open(path_to_save + 'data_' + str(freq) + '.csv', 'a', newline='') as f: + writer = csv.writer(f) + args2 = itertools.chain(*(arg if isinstance(arg, list) else [arg] for arg in args)) + writer.writerow(args2) + print('Write csv.') + + except Exception as e: + print(str(e)) + + +def prepare_folders_paths(path): + folders = path.split('/') + folders.pop() + folders = [elem + '/' for elem in folders] + print(folders) + cur_path = '' + print(cur_path) + return folders, cur_path + + +def remote_save_data(conn, data, module_name, freq, share_folder, path_to_save): + """ + Сохранение данных (сигнала) в файл на удаленный диск. + :param conn: + :param data: + :param module_name: + :param freq: + :param share_folder: + :param path_to_save: + :return: + """ + # cur_datetime = datetime.now().strftime('%d_%m_%Y_%H_%M_%S') + # file_name = f'alarm_{module_name}_{freq}_{cur_datetime}.npy' + # path = f"{path_to_save_medians}{module_name}/{str(freq)}/" + # path_to_file = f"{path}{file_name}" + # print(path_to_file) + # + # folders, cur_path = prepare_folders_paths(path) + # + # buffer = io.BytesIO() + # np.save(buffer, data) + # buffer.seek(0) + # + # for i in range(len(folders)): + # cur_path = cur_path + folders[i] + # try: + # conn.listPath(share_folder, cur_path) + # except Exception: + # conn.createDirectory(share_folder, cur_path) + # + # conn.storeFile(share_folder, path_to_file, buffer) + for name, values in data.items(): + elem_name = name + file_type = values['file_type'] + elem_data = values['elem'] + print(elem_data.shape) + buffer = io.BytesIO() + np.save(buffer, elem_data) + buffer.seek(0) + + cur_datetime = datetime.now().strftime('%d_%m_%Y_%H_%M_%S') + file_name = f'alarm_{elem_name}_{module_name}_{freq}_{cur_datetime}.{file_type}' + path = f"{path_to_save}{module_name}/{str(freq)}/{elem_name}/" + path_to_file = f"{path}{file_name}" + folders, cur_path = prepare_folders_paths(path) + + for i in range(len(folders)): + cur_path = cur_path + folders[i] + try: + conn.listPath(share_folder, cur_path) + except Exception: + conn.createDirectory(share_folder, cur_path) + + conn.storeFile(share_folder, path_to_file, buffer) diff --git a/src/utils/gen_mac.py b/src/utils/gen_mac.py new file mode 100644 index 0000000..0f58c45 --- /dev/null +++ b/src/utils/gen_mac.py @@ -0,0 +1,38 @@ +import re +import subprocess + +def get_cpu_serial(): + # Получаем серийный номер процессора Orange Pi + try: + output = subprocess.check_output("cat /proc/cpuinfo", shell=True).decode() + serial_match = re.search(r"Serial\s+:\s+(\w+)", output) + if serial_match: + return serial_match.group(1) + except Exception as e: + print(f"Ошибка при получении серийного номера процессора: {e}") + return None + +def generate_mac_address(serial): + # Генерируем MAC-адрес на основе серийного номера + if serial: + # Преобразуем серийный номер в часть MAC-адреса + serial = serial[-6:] + # Добавляем префикс, чтобы получить уникальный MAC-адрес + mac_address = f"00:1E:06:{serial[0:2]}:{serial[2:4]}:{serial[4:6]}" + return mac_address + return None + +if __name__ == "__main__": + # Получите серийный номер процессора + cpu_serial = get_cpu_serial() + + if cpu_serial: + # Генерируйте MAC-адрес на основе серийного номера + new_mac_address = generate_mac_address(cpu_serial) + + if new_mac_address: + print(new_mac_address) + else: + print("Не удалось сгенерировать MAC-адрес.") + else: + print("Не удалось получить серийный номер процессора.") diff --git a/src/utils/utils.py b/src/utils/utils.py new file mode 100644 index 0000000..83a6140 --- /dev/null +++ b/src/utils/utils.py @@ -0,0 +1,11 @@ +def get_num_columns_of_array(array: list) -> int: + return len(array[0]) + + +def get_num_rows_of_array(array: list) -> int: + return len(array) + + +def data_normalize(data: list, norm_type: str, max_val=None): + if norm_type == '0,1': + return [data[i] / max_val for i in range(len(data))] \ No newline at end of file