1、update
403
.gitignore
vendored
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
### PyCharm+iml template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### PyCharm template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### Python template
|
||||||
|
# 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/
|
||||||
|
|
||||||
|
### PyCharm+all template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
# db
|
||||||
|
*.db
|
||||||
|
|
||||||
|
vendors/image-framework/
|
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
20
.idea/dataSources.xml
generated
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="detect [2]" uuid="d5455d14-881f-400d-86d6-b6e6d6aae180">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:D:\xwd\cnnc\detect-gui\detect.db</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
<libraries>
|
||||||
|
<library>
|
||||||
|
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
|
||||||
|
</library>
|
||||||
|
<library>
|
||||||
|
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
|
||||||
|
</library>
|
||||||
|
</libraries>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
10
.idea/detect-gui.iml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="detect-gui" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/logs/app.log" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
58
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<Languages>
|
||||||
|
<language minSize="92" name="Python" />
|
||||||
|
</Languages>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredUrls">
|
||||||
|
<list>
|
||||||
|
<option value="http://0.0.0.0" />
|
||||||
|
<option value="http://127.0.0.1" />
|
||||||
|
<option value="http://activemq.apache.org/schema/" />
|
||||||
|
<option value="http://cxf.apache.org/schemas/" />
|
||||||
|
<option value="http://java.sun.com/" />
|
||||||
|
<option value="http://javafx.com/fxml" />
|
||||||
|
<option value="http://javafx.com/javafx/" />
|
||||||
|
<option value="http://json-schema.org/draft" />
|
||||||
|
<option value="http://localhost" />
|
||||||
|
<option value="http://maven.apache.org/POM/" />
|
||||||
|
<option value="http://maven.apache.org/xsd/" />
|
||||||
|
<option value="http://primefaces.org/ui" />
|
||||||
|
<option value="http://schema.cloudfoundry.org/spring/" />
|
||||||
|
<option value="http://schemas.xmlsoap.org/" />
|
||||||
|
<option value="http://tiles.apache.org/" />
|
||||||
|
<option value="http://www.ibm.com/webservices/xsd" />
|
||||||
|
<option value="http://www.jboss.com/xml/ns/" />
|
||||||
|
<option value="http://www.jboss.org/j2ee/schema/" />
|
||||||
|
<option value="http://www.springframework.org/schema/" />
|
||||||
|
<option value="http://www.springframework.org/security/tags" />
|
||||||
|
<option value="http://www.springframework.org/tags" />
|
||||||
|
<option value="http://www.thymeleaf.org" />
|
||||||
|
<option value="http://www.w3.org/" />
|
||||||
|
<option value="http://xmlns.jcp.org/" />
|
||||||
|
<option value="http://{setting.ptz.rgb.camera.server}{setting.ptz.rest.prefix}" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<value>
|
||||||
|
<list size="2">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="python_jose" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="opencv_python" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="N806" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
7
.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="detect-gui" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="detect-gui" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/detect-gui.iml" filepath="$PROJECT_DIR$/.idea/detect-gui.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"type_traits": "cpp"
|
||||||
|
},
|
||||||
|
"cmake.ignoreCMakeListsMissing": true
|
||||||
|
}
|
15
README.MD
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
## 相机SDK
|
||||||
|
```bash
|
||||||
|
/opt/HuarayTech/MVviewer/lib/libMVSDK.so
|
||||||
|
```
|
||||||
|
|
||||||
|
## GPIO
|
||||||
|
### 设置gpio功能
|
||||||
|
pin11控制雷达
|
||||||
|
```bash
|
||||||
|
bspmm 0x0102F00FC 0x0201
|
||||||
|
```
|
||||||
|
pin12控制相机
|
||||||
|
```bash
|
||||||
|
bspmm 0x0102F010C 0x0201
|
||||||
|
```
|
BIN
assets/bg.png
Normal file
After Width: | Height: | Size: 775 KiB |
BIN
assets/fonts/NotoSansSC-Regular.otf
Normal file
BIN
assets/icon_analyse.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/icon_bim.png
Normal file
After Width: | Height: | Size: 842 B |
BIN
assets/icon_cali.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/icon_charge_finish.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
assets/icon_charging.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
assets/icon_com.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
assets/icon_device.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
assets/icon_setting.png
Normal file
After Width: | Height: | Size: 921 B |
BIN
assets/icon_task.png
Normal file
After Width: | Height: | Size: 668 B |
BIN
assets/img1.jpg
Normal file
After Width: | Height: | Size: 9.0 MiB |
BIN
assets/img2.png
Normal file
After Width: | Height: | Size: 356 KiB |
BIN
assets/logo.png
Normal file
After Width: | Height: | Size: 10 KiB |
0
components/__init__.py
Normal file
864
components/camera.py
Normal file
@ -0,0 +1,864 @@
|
|||||||
|
from dynaconf.base import Settings
|
||||||
|
|
||||||
|
from core.edge_component import EdgeComponent, device, service
|
||||||
|
from device.mv.ImageConvert import *
|
||||||
|
from device.mv.MVSDK import *
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
from core.logging import logger
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
g_cameraStatusUserInfo = b"statusInfo"
|
||||||
|
|
||||||
|
class BITMAPFILEHEADER(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('bfType', c_ushort),
|
||||||
|
('bfSize', c_uint),
|
||||||
|
('bfReserved1', c_ushort),
|
||||||
|
('bfReserved2', c_ushort),
|
||||||
|
('bfOffBits', c_uint),
|
||||||
|
]
|
||||||
|
|
||||||
|
class BITMAPINFOHEADER(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('biSize', c_uint),
|
||||||
|
('biWidth', c_int),
|
||||||
|
('biHeight', c_int),
|
||||||
|
('biPlanes', c_ushort),
|
||||||
|
('biBitCount', c_ushort),
|
||||||
|
('biCompression', c_uint),
|
||||||
|
('biSizeImage', c_uint),
|
||||||
|
('biXPelsPerMeter', c_int),
|
||||||
|
('biYPelsPerMeter', c_int),
|
||||||
|
('biClrUsed', c_uint),
|
||||||
|
('biClrImportant', c_uint),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 调色板,只有8bit及以下才需要
|
||||||
|
class RGBQUAD(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('rgbBlue', c_ubyte),
|
||||||
|
('rgbGreen', c_ubyte),
|
||||||
|
('rgbRed', c_ubyte),
|
||||||
|
('rgbReserved', c_ubyte),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# 相机设备类
|
||||||
|
class CameraDevice:
|
||||||
|
def __init__(self, camera_id, key, vendor_name, model_name, serial_number):
|
||||||
|
self.camera_id = camera_id
|
||||||
|
self.key = key
|
||||||
|
self.vendor_name = vendor_name
|
||||||
|
self.model_name = model_name
|
||||||
|
self.serial_number = serial_number
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (f"CameraDevice(camera_id={self.camera_id}, key={self.key}, "
|
||||||
|
f"vendor_name={self.vendor_name}, model_name={self.model_name}, "
|
||||||
|
f"serial_number={self.serial_number})")
|
||||||
|
|
||||||
|
|
||||||
|
class CameraSignals(QObject):
|
||||||
|
# QT 信号
|
||||||
|
on_device_list = pyqtSignal(list)
|
||||||
|
on_device_link = pyqtSignal(int, str)
|
||||||
|
on_device_change = pyqtSignal()
|
||||||
|
on_device_error = pyqtSignal()
|
||||||
|
on_device_data = pyqtSignal(object)
|
||||||
|
|
||||||
|
|
||||||
|
# pyqt的相机管理类
|
||||||
|
@device("camera", auto_start=True)
|
||||||
|
class Camera(EdgeComponent):
|
||||||
|
signals = CameraSignals()
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
super().__init__(context)
|
||||||
|
global mv_camera
|
||||||
|
mv_camera = self
|
||||||
|
self.devices = None
|
||||||
|
self.current_index = 0
|
||||||
|
self.streamSource = None
|
||||||
|
|
||||||
|
def configure(self, setting: Settings) -> None:
|
||||||
|
self.logger.info(f"Camera configure done.")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def start(self):
|
||||||
|
if MVSDKdll is None:
|
||||||
|
raise RuntimeError("相机设备SDK未正确加载")
|
||||||
|
|
||||||
|
camera_count = self.start_discovery()
|
||||||
|
if camera_count <= 0:
|
||||||
|
self.logger.info('no cameras found!')
|
||||||
|
else:
|
||||||
|
self.open_camera()
|
||||||
|
super().start()
|
||||||
|
self.logger.info("Camera start!")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def stop(self):
|
||||||
|
self.stop_camera()
|
||||||
|
super().stop()
|
||||||
|
self.logger.info("Camera stop!")
|
||||||
|
|
||||||
|
def start_discovery(self):
|
||||||
|
cameraCnt, cameraList = enumCameras()
|
||||||
|
if cameraCnt is None:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
self.devices = cameraList
|
||||||
|
camera_devices = []
|
||||||
|
# 显示相机信息
|
||||||
|
for index in range(0, cameraCnt):
|
||||||
|
camera = cameraList[index]
|
||||||
|
self.logger.info("Camera Id = " + str(index))
|
||||||
|
self.logger.info("Key = " + str(camera.getKey(camera)))
|
||||||
|
self.logger.info("vendor name = " + str(camera.getVendorName(camera)))
|
||||||
|
self.logger.info("Model name = " + str(camera.getModelName(camera)))
|
||||||
|
self.logger.info("Serial number = " + str(camera.getSerialNumber(camera)))
|
||||||
|
camera_devices.append(CameraDevice(index, str(camera.getKey(camera)), str(camera.getVendorName(camera)),
|
||||||
|
str(camera.getModelName(camera)), str(camera.getSerialNumber(camera))))
|
||||||
|
self.signals.on_device_list.emit(camera_devices)
|
||||||
|
|
||||||
|
return cameraCnt
|
||||||
|
|
||||||
|
def open_camera(self, index = 0):
|
||||||
|
# 打开相机
|
||||||
|
camera = self.devices[index]
|
||||||
|
nRet = openCamera(camera)
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("openCamera fail.")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 创建流对象
|
||||||
|
streamSourceInfo = GENICAM_StreamSourceInfo()
|
||||||
|
streamSourceInfo.channelId = 0
|
||||||
|
streamSourceInfo.pCamera = pointer(camera)
|
||||||
|
|
||||||
|
self.streamSource = pointer(GENICAM_StreamSource())
|
||||||
|
nRet = GENICAM_createStreamSource(pointer(streamSourceInfo), byref(self.streamSource))
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("create StreamSource fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 注册相机连接状态回调
|
||||||
|
nRet = subscribeCameraStatus(camera)
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("subscribeCameraStatus fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
self.current_index = index
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def stop_camera(self):
|
||||||
|
camera = self.devices[self.current_index]
|
||||||
|
# 关闭相机
|
||||||
|
nRet = closeCamera(camera)
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("closeCamera fail")
|
||||||
|
# 释放相关资源
|
||||||
|
if self.streamSource is not None:
|
||||||
|
self.streamSource.contents.release(self.streamSource)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 反向注册相机连接状态回调
|
||||||
|
nRet = unsubscribeCameraStatus(camera)
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("unsubscribeCameraStatus fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 释放相关资源
|
||||||
|
if self.streamSource is not None:
|
||||||
|
self.streamSource.contents.release(self.streamSource)
|
||||||
|
|
||||||
|
def set_exposure_time(self, exposure_time: int):
|
||||||
|
# 设置曝光时间
|
||||||
|
camera = self.devices[self.current_index]
|
||||||
|
setExposureTime(camera, exposure_time)
|
||||||
|
|
||||||
|
def set_soft_trigger(self):
|
||||||
|
# 设置软触发
|
||||||
|
camera = self.devices[self.current_index]
|
||||||
|
setSoftTriggerConf(camera)
|
||||||
|
|
||||||
|
def set_line_trigger(self):
|
||||||
|
# 设置外触发
|
||||||
|
camera = self.devices[self.current_index]
|
||||||
|
setLineTriggerConf(camera)
|
||||||
|
|
||||||
|
def set_roi(self, offset_x, offset_y, width, height):
|
||||||
|
# 设置相机ROI
|
||||||
|
camera = self.devices[self.current_index]
|
||||||
|
setROI(camera, offset_x, offset_y, width, height)
|
||||||
|
|
||||||
|
def grab_one(self):
|
||||||
|
# 设置软触发
|
||||||
|
camera = self.devices[self.current_index]
|
||||||
|
nRet = setSoftTriggerConf(camera)
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("set SoftTriggerConf fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
self.streamSource.contents.release(self.streamSource)
|
||||||
|
return -1
|
||||||
|
else:
|
||||||
|
self.logger.info("set SoftTriggerConf success!")
|
||||||
|
|
||||||
|
# 开始拉流
|
||||||
|
nRet = self.streamSource.contents.startGrabbing(self.streamSource, c_ulonglong(0),
|
||||||
|
c_int(GENICAM_EGrabStrategy.grabStrartegySequential))
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("startGrabbing fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
self.streamSource.contents.release(self.streamSource)
|
||||||
|
return -1
|
||||||
|
time.sleep(1)
|
||||||
|
# 软触发取一张图
|
||||||
|
nRet = grabOne(camera)
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("grabOne fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
self.streamSource.contents.release(self.streamSource)
|
||||||
|
return -1
|
||||||
|
else:
|
||||||
|
self.logger.info("trigger time: " + str(datetime.datetime.now()))
|
||||||
|
|
||||||
|
# 主动取图
|
||||||
|
frame = pointer(GENICAM_Frame())
|
||||||
|
nRet = self.streamSource.contents.getFrame(self.streamSource, byref(frame), c_uint(100000))
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("SoftTrigger getFrame fail! timeOut [100000]ms")
|
||||||
|
# 释放相关资源
|
||||||
|
self.streamSource.contents.release(self.streamSource)
|
||||||
|
return -1
|
||||||
|
else:
|
||||||
|
self.logger.info("SoftTrigger getFrame success BlockId = " + str(frame.contents.getBlockId(frame)))
|
||||||
|
self.logger.info("get frame time: " + str(datetime.datetime.now()))
|
||||||
|
|
||||||
|
nRet = frame.contents.valid(frame)
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("frame is invalid!")
|
||||||
|
# 释放驱动图像缓存资源
|
||||||
|
frame.contents.release(frame)
|
||||||
|
# 释放相关资源
|
||||||
|
self.streamSource.contents.release(self.streamSource)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 将裸数据图像拷出
|
||||||
|
imageSize = frame.contents.getImageSize(frame)
|
||||||
|
self.logger.info(f"imageSize is {imageSize}!")
|
||||||
|
buffAddr = frame.contents.getImage(frame)
|
||||||
|
frameBuff = c_buffer(b'\0', imageSize)
|
||||||
|
memmove(frameBuff, c_char_p(buffAddr), imageSize)
|
||||||
|
|
||||||
|
# 给转码所需的参数赋值
|
||||||
|
convertParams = IMGCNV_SOpenParam()
|
||||||
|
convertParams.dataSize = imageSize
|
||||||
|
convertParams.height = frame.contents.getImageHeight(frame)
|
||||||
|
convertParams.width = frame.contents.getImageWidth(frame)
|
||||||
|
convertParams.paddingX = frame.contents.getImagePaddingX(frame)
|
||||||
|
convertParams.paddingY = frame.contents.getImagePaddingY(frame)
|
||||||
|
convertParams.pixelForamt = frame.contents.getImagePixelFormat(frame)
|
||||||
|
|
||||||
|
# 释放驱动图像缓存
|
||||||
|
frame.contents.release(frame)
|
||||||
|
|
||||||
|
# 保存bmp图片
|
||||||
|
bmpInfoHeader = BITMAPINFOHEADER()
|
||||||
|
bmpFileHeader = BITMAPFILEHEADER()
|
||||||
|
|
||||||
|
uRgbQuadLen = 0
|
||||||
|
rgbQuad = (RGBQUAD * 256)() # 调色板信息
|
||||||
|
rgbBuff = c_buffer(b'\0', convertParams.height * convertParams.width * 3)
|
||||||
|
|
||||||
|
# 如果图像格式是 Mono8 不需要转码
|
||||||
|
if convertParams.pixelForamt == EPixelType.gvspPixelMono8:
|
||||||
|
# 初始化调色板rgbQuad 实际应用中 rgbQuad 只需初始化一次
|
||||||
|
for i in range(0, 256):
|
||||||
|
rgbQuad[i].rgbBlue = rgbQuad[i].rgbGreen = rgbQuad[i].rgbRed = i
|
||||||
|
|
||||||
|
uRgbQuadLen = sizeof(RGBQUAD) * 256
|
||||||
|
bmpFileHeader.bfSize = sizeof(bmpFileHeader) + sizeof(bmpInfoHeader) + uRgbQuadLen + convertParams.dataSize
|
||||||
|
bmpInfoHeader.biBitCount = 8
|
||||||
|
else:
|
||||||
|
# 转码 => BGR24
|
||||||
|
rgbSize = c_int()
|
||||||
|
nRet = IMGCNV_ConvertToBGR24(cast(frameBuff, c_void_p), byref(convertParams),
|
||||||
|
cast(rgbBuff, c_void_p), byref(rgbSize))
|
||||||
|
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("image convert fail! errorCode = " + str(nRet))
|
||||||
|
# 释放相关资源
|
||||||
|
self.streamSource.contents.release(self.streamSource)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
bmpFileHeader.bfSize = sizeof(bmpFileHeader) + sizeof(bmpInfoHeader) + rgbSize.value
|
||||||
|
bmpInfoHeader.biBitCount = 24
|
||||||
|
|
||||||
|
bmpFileHeader.bfType = 0x4D42 # 文件头类型 'BM'(42 4D)
|
||||||
|
bmpFileHeader.bfReserved1 = 0 # 保留字
|
||||||
|
bmpFileHeader.bfReserved2 = 0 # 保留字
|
||||||
|
bmpFileHeader.bfOffBits = 54 + uRgbQuadLen # 位图像素数据的起始位置
|
||||||
|
|
||||||
|
bmpInfoHeader.biSize = 40 # 信息头所占字节数
|
||||||
|
bmpInfoHeader.biWidth = convertParams.width
|
||||||
|
bmpInfoHeader.biHeight = -convertParams.height
|
||||||
|
bmpInfoHeader.biPlanes = 1 # 位图平面数
|
||||||
|
|
||||||
|
bmpInfoHeader.biCompression = 0 # 压缩类型,0 即不压缩
|
||||||
|
bmpInfoHeader.biSizeImage = 0
|
||||||
|
bmpInfoHeader.biXPelsPerMeter = 0
|
||||||
|
bmpInfoHeader.biYPelsPerMeter = 0
|
||||||
|
bmpInfoHeader.biClrUsed = 0
|
||||||
|
bmpInfoHeader.biClrImportant = 0
|
||||||
|
|
||||||
|
fileName = './image.bmp'
|
||||||
|
imageFile = open(fileName, 'wb+')
|
||||||
|
|
||||||
|
imageFile.write(struct.pack('H', bmpFileHeader.bfType))
|
||||||
|
imageFile.write(struct.pack('I', bmpFileHeader.bfSize))
|
||||||
|
imageFile.write(struct.pack('H', bmpFileHeader.bfReserved1))
|
||||||
|
imageFile.write(struct.pack('H', bmpFileHeader.bfReserved2))
|
||||||
|
imageFile.write(struct.pack('I', bmpFileHeader.bfOffBits))
|
||||||
|
|
||||||
|
imageFile.write(struct.pack('I', bmpInfoHeader.biSize))
|
||||||
|
imageFile.write(struct.pack('i', bmpInfoHeader.biWidth))
|
||||||
|
imageFile.write(struct.pack('i', bmpInfoHeader.biHeight))
|
||||||
|
imageFile.write(struct.pack('H', bmpInfoHeader.biPlanes))
|
||||||
|
imageFile.write(struct.pack('H', bmpInfoHeader.biBitCount))
|
||||||
|
imageFile.write(struct.pack('I', bmpInfoHeader.biCompression))
|
||||||
|
imageFile.write(struct.pack('I', bmpInfoHeader.biSizeImage))
|
||||||
|
imageFile.write(struct.pack('i', bmpInfoHeader.biXPelsPerMeter))
|
||||||
|
imageFile.write(struct.pack('i', bmpInfoHeader.biYPelsPerMeter))
|
||||||
|
imageFile.write(struct.pack('I', bmpInfoHeader.biClrUsed))
|
||||||
|
imageFile.write(struct.pack('I', bmpInfoHeader.biClrImportant))
|
||||||
|
|
||||||
|
if convertParams.pixelForamt == EPixelType.gvspPixelMono8:
|
||||||
|
# 写入调色板信息
|
||||||
|
for i in range(0, 256):
|
||||||
|
imageFile.write(struct.pack('B', rgbQuad[i].rgbBlue))
|
||||||
|
imageFile.write(struct.pack('B', rgbQuad[i].rgbGreen))
|
||||||
|
imageFile.write(struct.pack('B', rgbQuad[i].rgbRed))
|
||||||
|
imageFile.write(struct.pack('B', rgbQuad[i].rgbReserved))
|
||||||
|
|
||||||
|
imageFile.writelines(frameBuff)
|
||||||
|
else:
|
||||||
|
imageFile.writelines(rgbBuff)
|
||||||
|
|
||||||
|
imageFile.close()
|
||||||
|
self.logger.info("save " + fileName + " success.")
|
||||||
|
self.logger.info("save bmp time: " + str(datetime.datetime.now()))
|
||||||
|
|
||||||
|
# 停止拉流
|
||||||
|
nRet = self.streamSource.contents.stopGrabbing(self.streamSource)
|
||||||
|
if nRet != 0:
|
||||||
|
self.logger.error("stopGrabbing fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
self.streamSource.contents.release(self.streamSource)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 相机管理类实例
|
||||||
|
mv_camera = None
|
||||||
|
|
||||||
|
# 取流回调函数
|
||||||
|
def onGetFrame(frame):
|
||||||
|
nRet = frame.contents.valid(frame)
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("frame is invalid!")
|
||||||
|
# 释放驱动图像缓存资源
|
||||||
|
frame.contents.release(frame)
|
||||||
|
return
|
||||||
|
|
||||||
|
mv_camera.logger.info("BlockId = %d" %(frame.contents.getBlockId(frame)))
|
||||||
|
#此处客户应用程序应将图像拷贝出使用
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
# 释放驱动图像缓存资源
|
||||||
|
frame.contents.release(frame)
|
||||||
|
|
||||||
|
# 取流回调函数Ex
|
||||||
|
def onGetFrameEx(frame, userInfo):
|
||||||
|
nRet = frame.contents.valid(frame)
|
||||||
|
if ( nRet != 0):
|
||||||
|
mv_camera.logger.error("frame is invalid!")
|
||||||
|
# 释放驱动图像缓存资源
|
||||||
|
frame.contents.release(frame)
|
||||||
|
return
|
||||||
|
|
||||||
|
mv_camera.logger.info("BlockId = %d userInfo = %s" %(frame.contents.getBlockId(frame), c_char_p(userInfo).value))
|
||||||
|
#此处客户应用程序应将图像拷贝出使用
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
# 释放驱动图像缓存资源
|
||||||
|
frame.contents.release(frame)
|
||||||
|
|
||||||
|
# 相机连接状态回调函数
|
||||||
|
def deviceLinkNotify(connectArg, linkInfo):
|
||||||
|
mv_camera.on_device_link.emit(connectArg.contents.m_event, c_char_p(linkInfo).value.decode('utf-8'))
|
||||||
|
if EVType.offLine == connectArg.contents.m_event:
|
||||||
|
mv_camera.logger.info("camera has off line, userInfo [%s]" %(c_char_p(linkInfo).value))
|
||||||
|
elif EVType.onLine == connectArg.contents.m_event:
|
||||||
|
mv_camera.logger.info("camera has on line, userInfo [%s]" %(c_char_p(linkInfo).value))
|
||||||
|
|
||||||
|
|
||||||
|
connectCallBackFuncEx = connectCallBackEx(deviceLinkNotify)
|
||||||
|
frameCallbackFunc = callbackFunc(onGetFrame)
|
||||||
|
frameCallbackFuncEx = callbackFuncEx(onGetFrameEx)
|
||||||
|
|
||||||
|
# 注册相机连接状态回调
|
||||||
|
def subscribeCameraStatus(camera):
|
||||||
|
# 注册上下线通知
|
||||||
|
eventSubscribe = pointer(GENICAM_EventSubscribe())
|
||||||
|
eventSubscribeInfo = GENICAM_EventSubscribeInfo()
|
||||||
|
eventSubscribeInfo.pCamera = pointer(camera)
|
||||||
|
nRet = GENICAM_createEventSubscribe(byref(eventSubscribeInfo), byref(eventSubscribe))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create eventSubscribe fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
nRet = eventSubscribe.contents.subscribeConnectArgsEx(eventSubscribe, connectCallBackFuncEx, g_cameraStatusUserInfo)
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("subscribeConnectArgsEx fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
eventSubscribe.contents.release(eventSubscribe)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 不再使用时,需释放相关资源
|
||||||
|
eventSubscribe.contents.release(eventSubscribe)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 反注册相机连接状态回调
|
||||||
|
def unsubscribeCameraStatus(camera):
|
||||||
|
# 反注册上下线通知
|
||||||
|
eventSubscribe = pointer(GENICAM_EventSubscribe())
|
||||||
|
eventSubscribeInfo = GENICAM_EventSubscribeInfo()
|
||||||
|
eventSubscribeInfo.pCamera = pointer(camera)
|
||||||
|
nRet = GENICAM_createEventSubscribe(byref(eventSubscribeInfo), byref(eventSubscribe))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create eventSubscribe fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
nRet = eventSubscribe.contents.unsubscribeConnectArgsEx(eventSubscribe, connectCallBackFuncEx, g_cameraStatusUserInfo)
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("unsubscribeConnectArgsEx fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
eventSubscribe.contents.release(eventSubscribe)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 不再使用时,需释放相关资源
|
||||||
|
eventSubscribe.contents.release(eventSubscribe)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 设置软触发
|
||||||
|
def setSoftTriggerConf(camera):
|
||||||
|
# 创建control节点
|
||||||
|
acqCtrlInfo = GENICAM_AcquisitionControlInfo()
|
||||||
|
acqCtrlInfo.pCamera = pointer(camera)
|
||||||
|
acqCtrl = pointer(GENICAM_AcquisitionControl())
|
||||||
|
nRet = GENICAM_createAcquisitionControl(pointer(acqCtrlInfo), byref(acqCtrl))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create AcquisitionControl fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 设置触发源为软触发
|
||||||
|
trigSourceEnumNode = acqCtrl.contents.triggerSource(acqCtrl)
|
||||||
|
nRet = trigSourceEnumNode.setValueBySymbol(byref(trigSourceEnumNode), b"Software")
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("set TriggerSource value [Software] fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
trigSourceEnumNode.release(byref(trigSourceEnumNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 需要释放Node资源
|
||||||
|
trigSourceEnumNode.release(byref(trigSourceEnumNode))
|
||||||
|
|
||||||
|
# 设置触发方式
|
||||||
|
trigSelectorEnumNode = acqCtrl.contents.triggerSelector(acqCtrl)
|
||||||
|
nRet = trigSelectorEnumNode.setValueBySymbol(byref(trigSelectorEnumNode), b"FrameStart")
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("set TriggerSelector value [FrameStart] fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
trigSelectorEnumNode.release(byref(trigSelectorEnumNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 需要释放Node资源
|
||||||
|
trigSelectorEnumNode.release(byref(trigSelectorEnumNode))
|
||||||
|
|
||||||
|
# 打开触发模式
|
||||||
|
trigModeEnumNode = acqCtrl.contents.triggerMode(acqCtrl)
|
||||||
|
nRet = trigModeEnumNode.setValueBySymbol(byref(trigModeEnumNode), b"On")
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("set TriggerMode value [On] fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
trigModeEnumNode.release(byref(trigModeEnumNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 需要释放相关资源
|
||||||
|
trigModeEnumNode.release(byref(trigModeEnumNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 设置外触发
|
||||||
|
def setLineTriggerConf(camera):
|
||||||
|
# 创建control节点
|
||||||
|
acqCtrlInfo = GENICAM_AcquisitionControlInfo()
|
||||||
|
acqCtrlInfo.pCamera = pointer(camera)
|
||||||
|
acqCtrl = pointer(GENICAM_AcquisitionControl())
|
||||||
|
nRet = GENICAM_createAcquisitionControl(pointer(acqCtrlInfo), byref(acqCtrl))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create AcquisitionControl fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 设置触发源为软触发
|
||||||
|
trigSourceEnumNode = acqCtrl.contents.triggerSource(acqCtrl)
|
||||||
|
nRet = trigSourceEnumNode.setValueBySymbol(byref(trigSourceEnumNode), b"Line1")
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("set TriggerSource value [Line1] fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
trigSourceEnumNode.release(byref(trigSourceEnumNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 需要释放Node资源
|
||||||
|
trigSourceEnumNode.release(byref(trigSourceEnumNode))
|
||||||
|
|
||||||
|
# 设置触发方式
|
||||||
|
trigSelectorEnumNode = acqCtrl.contents.triggerSelector(acqCtrl)
|
||||||
|
nRet = trigSelectorEnumNode.setValueBySymbol(byref(trigSelectorEnumNode), b"FrameStart")
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("set TriggerSelector value [FrameStart] fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
trigSelectorEnumNode.release(byref(trigSelectorEnumNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 需要释放Node资源
|
||||||
|
trigSelectorEnumNode.release(byref(trigSelectorEnumNode))
|
||||||
|
|
||||||
|
# 打开触发模式
|
||||||
|
trigModeEnumNode = acqCtrl.contents.triggerMode(acqCtrl)
|
||||||
|
nRet = trigModeEnumNode.setValueBySymbol(byref(trigModeEnumNode), b"On")
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("set TriggerMode value [On] fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
trigModeEnumNode.release(byref(trigModeEnumNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 需要释放Node资源
|
||||||
|
trigModeEnumNode.release(byref(trigModeEnumNode))
|
||||||
|
|
||||||
|
# 设置触发沿
|
||||||
|
trigActivationEnumNode = acqCtrl.contents.triggerActivation(acqCtrl)
|
||||||
|
nRet = trigActivationEnumNode.setValueBySymbol(byref(trigActivationEnumNode), b"RisingEdge")
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("set TriggerActivation value [RisingEdge] fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
trigActivationEnumNode.release(byref(trigActivationEnumNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 需要释放Node资源
|
||||||
|
trigActivationEnumNode.release(byref(trigActivationEnumNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 打开相机
|
||||||
|
def openCamera(camera):
|
||||||
|
# 连接相机
|
||||||
|
nRet = camera.connect(camera, c_int(GENICAM_ECameraAccessPermission.accessPermissionControl))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("camera connect fail!")
|
||||||
|
return -1
|
||||||
|
else:
|
||||||
|
mv_camera.logger.info("camera connect success.")
|
||||||
|
|
||||||
|
# # 注册相机连接状态回调
|
||||||
|
# nRet = subscribeCameraStatus(camera)
|
||||||
|
# if nRet != 0:
|
||||||
|
# mv_camera.logger.error("subscribeCameraStatus fail!")
|
||||||
|
# return -1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 关闭相机
|
||||||
|
def closeCamera(camera):
|
||||||
|
# 反注册相机连接状态回调
|
||||||
|
nRet = unsubscribeCameraStatus(camera)
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("unsubscribeCameraStatus fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 断开相机
|
||||||
|
# nRet = camera.disConnect(byref(camera))
|
||||||
|
# if nRet != 0:
|
||||||
|
# mv_camera.logger.error("disConnect camera fail!")
|
||||||
|
# return -1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 设置曝光
|
||||||
|
def setExposureTime(camera, dVal):
|
||||||
|
# 通用属性设置:设置曝光 --根据属性类型,直接构造属性节点。如曝光是 double类型,构造doubleNode节点
|
||||||
|
exposureTimeNode = pointer(GENICAM_DoubleNode())
|
||||||
|
exposureTimeNodeInfo = GENICAM_DoubleNodeInfo()
|
||||||
|
exposureTimeNodeInfo.pCamera = pointer(camera)
|
||||||
|
exposureTimeNodeInfo.attrName = b"ExposureTime"
|
||||||
|
nRet = GENICAM_createDoubleNode(byref(exposureTimeNodeInfo), byref(exposureTimeNode))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create ExposureTime Node fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 设置曝光时间
|
||||||
|
nRet = exposureTimeNode.contents.setValue(exposureTimeNode, c_double(dVal))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("set ExposureTime value [%f]us fail!" % (dVal))
|
||||||
|
# 释放相关资源
|
||||||
|
exposureTimeNode.contents.release(exposureTimeNode)
|
||||||
|
return -1
|
||||||
|
else:
|
||||||
|
mv_camera.logger.info("set ExposureTime value [%f]us success." % (dVal))
|
||||||
|
|
||||||
|
# 释放节点资源
|
||||||
|
exposureTimeNode.contents.release(exposureTimeNode)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 枚举相机
|
||||||
|
def enumCameras():
|
||||||
|
# 获取系统单例
|
||||||
|
system = pointer(GENICAM_System())
|
||||||
|
nRet = GENICAM_getSystemInstance(byref(system))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("getSystemInstance fail!")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 发现相机
|
||||||
|
cameraList = pointer(GENICAM_Camera())
|
||||||
|
cameraCnt = c_uint()
|
||||||
|
nRet = system.contents.discovery(system, byref(cameraList), byref(cameraCnt), c_int(GENICAM_EProtocolType.typeAll));
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("discovery fail!")
|
||||||
|
return None, None
|
||||||
|
elif cameraCnt.value < 1:
|
||||||
|
mv_camera.logger.info("discovery no camera!")
|
||||||
|
return None, None
|
||||||
|
else:
|
||||||
|
mv_camera.logger.info("cameraCnt: " + str(cameraCnt.value))
|
||||||
|
return cameraCnt.value, cameraList
|
||||||
|
|
||||||
|
def grabOne(camera):
|
||||||
|
# 创建流对象
|
||||||
|
streamSourceInfo = GENICAM_StreamSourceInfo()
|
||||||
|
streamSourceInfo.channelId = 0
|
||||||
|
streamSourceInfo.pCamera = pointer(camera)
|
||||||
|
|
||||||
|
streamSource = pointer(GENICAM_StreamSource())
|
||||||
|
nRet = GENICAM_createStreamSource(pointer(streamSourceInfo), byref(streamSource))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create StreamSource fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 创建control节点
|
||||||
|
acqCtrlInfo = GENICAM_AcquisitionControlInfo()
|
||||||
|
acqCtrlInfo.pCamera = pointer(camera)
|
||||||
|
acqCtrl = pointer(GENICAM_AcquisitionControl())
|
||||||
|
nRet = GENICAM_createAcquisitionControl(pointer(acqCtrlInfo), byref(acqCtrl))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create AcquisitionControl fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
streamSource.contents.release(streamSource)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 执行一次软触发
|
||||||
|
trigSoftwareCmdNode = acqCtrl.contents.triggerSoftware(acqCtrl)
|
||||||
|
nRet = trigSoftwareCmdNode.execute(byref(trigSoftwareCmdNode))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("Execute triggerSoftware fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
trigSoftwareCmdNode.release(byref(trigSoftwareCmdNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
streamSource.contents.release(streamSource)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 释放相关资源
|
||||||
|
trigSoftwareCmdNode.release(byref(trigSoftwareCmdNode))
|
||||||
|
acqCtrl.contents.release(acqCtrl)
|
||||||
|
streamSource.contents.release(streamSource)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 设置感兴趣区域 --- 感兴趣区域的宽高 和 xy方向的偏移量 入参值应符合对应相机的递增规则
|
||||||
|
def setROI(camera, OffsetX, OffsetY, nWidth, nHeight):
|
||||||
|
#获取原始的宽度
|
||||||
|
widthMaxNode = pointer(GENICAM_IntNode())
|
||||||
|
widthMaxNodeInfo = GENICAM_IntNodeInfo()
|
||||||
|
widthMaxNodeInfo.pCamera = pointer(camera)
|
||||||
|
widthMaxNodeInfo.attrName = b"WidthMax"
|
||||||
|
nRet = GENICAM_createIntNode(byref(widthMaxNodeInfo), byref(widthMaxNode))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create WidthMax Node fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
oriWidth = c_longlong()
|
||||||
|
nRet = widthMaxNode.contents.getValue(widthMaxNode, byref(oriWidth))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("widthMaxNode getValue fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
widthMaxNode.contents.release(widthMaxNode)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 释放相关资源
|
||||||
|
widthMaxNode.contents.release(widthMaxNode)
|
||||||
|
|
||||||
|
# 获取原始的高度
|
||||||
|
heightMaxNode = pointer(GENICAM_IntNode())
|
||||||
|
heightMaxNodeInfo = GENICAM_IntNodeInfo()
|
||||||
|
heightMaxNodeInfo.pCamera = pointer(camera)
|
||||||
|
heightMaxNodeInfo.attrName = b"HeightMax"
|
||||||
|
nRet = GENICAM_createIntNode(byref(heightMaxNodeInfo), byref(heightMaxNode))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create HeightMax Node fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
oriHeight = c_longlong()
|
||||||
|
nRet = heightMaxNode.contents.getValue(heightMaxNode, byref(oriHeight))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("heightMaxNode getValue fail!")
|
||||||
|
# 释放相关资源
|
||||||
|
heightMaxNode.contents.release(heightMaxNode)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 释放相关资源
|
||||||
|
heightMaxNode.contents.release(heightMaxNode)
|
||||||
|
|
||||||
|
# 检验参数
|
||||||
|
if (oriWidth.value < (OffsetX + nWidth)) or (oriHeight.value < (OffsetY + nHeight)):
|
||||||
|
mv_camera.logger.error("please check input param!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 设置宽度
|
||||||
|
widthNode = pointer(GENICAM_IntNode())
|
||||||
|
widthNodeInfo = GENICAM_IntNodeInfo()
|
||||||
|
widthNodeInfo.pCamera = pointer(camera)
|
||||||
|
widthNodeInfo.attrName = b"Width"
|
||||||
|
nRet = GENICAM_createIntNode(byref(widthNodeInfo), byref(widthNode))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create Width Node fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
nRet = widthNode.contents.setValue(widthNode, c_longlong(nWidth))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("widthNode setValue [%d] fail!" % (nWidth))
|
||||||
|
# 释放相关资源
|
||||||
|
widthNode.contents.release(widthNode)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 释放相关资源
|
||||||
|
widthNode.contents.release(widthNode)
|
||||||
|
|
||||||
|
# 设置高度
|
||||||
|
heightNode = pointer(GENICAM_IntNode())
|
||||||
|
heightNodeInfo = GENICAM_IntNodeInfo()
|
||||||
|
heightNodeInfo.pCamera = pointer(camera)
|
||||||
|
heightNodeInfo.attrName = b"Height"
|
||||||
|
nRet = GENICAM_createIntNode(byref(heightNodeInfo), byref(heightNode))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create Height Node fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
nRet = heightNode.contents.setValue(heightNode, c_longlong(nHeight))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("heightNode setValue [%d] fail!" % (nHeight))
|
||||||
|
# 释放相关资源
|
||||||
|
heightNode.contents.release(heightNode)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 释放相关资源
|
||||||
|
heightNode.contents.release(heightNode)
|
||||||
|
|
||||||
|
# 设置OffsetX
|
||||||
|
OffsetXNode = pointer(GENICAM_IntNode())
|
||||||
|
OffsetXNodeInfo = GENICAM_IntNodeInfo()
|
||||||
|
OffsetXNodeInfo.pCamera = pointer(camera)
|
||||||
|
OffsetXNodeInfo.attrName = b"OffsetX"
|
||||||
|
nRet = GENICAM_createIntNode(byref(OffsetXNodeInfo), byref(OffsetXNode))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create OffsetX Node fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
nRet = OffsetXNode.contents.setValue(OffsetXNode, c_longlong(OffsetX))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("OffsetX setValue [%d] fail!" % (OffsetX))
|
||||||
|
# 释放相关资源
|
||||||
|
OffsetXNode.contents.release(OffsetXNode)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 释放相关资源
|
||||||
|
OffsetXNode.contents.release(OffsetXNode)
|
||||||
|
|
||||||
|
# 设置OffsetY
|
||||||
|
OffsetYNode = pointer(GENICAM_IntNode())
|
||||||
|
OffsetYNodeInfo = GENICAM_IntNodeInfo()
|
||||||
|
OffsetYNodeInfo.pCamera = pointer(camera)
|
||||||
|
OffsetYNodeInfo.attrName = b"OffsetY"
|
||||||
|
nRet = GENICAM_createIntNode(byref(OffsetYNodeInfo), byref(OffsetYNode))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("create OffsetY Node fail!")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
nRet = OffsetYNode.contents.setValue(OffsetYNode, c_longlong(OffsetY))
|
||||||
|
if nRet != 0:
|
||||||
|
mv_camera.logger.error("OffsetY setValue [%d] fail!" % (OffsetY))
|
||||||
|
# 释放相关资源
|
||||||
|
OffsetYNode.contents.release(OffsetYNode)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 释放相关资源
|
||||||
|
OffsetYNode.contents.release(OffsetYNode)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def demo():
|
||||||
|
# 发现相机
|
||||||
|
cameraCnt, cameraList = enumCameras()
|
||||||
|
if cameraCnt is None:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 显示相机信息
|
||||||
|
for index in range(0, cameraCnt):
|
||||||
|
camera = cameraList[index]
|
||||||
|
print("\nCamera Id = " + str(index))
|
||||||
|
print("Key = " + str(camera.getKey(camera)))
|
||||||
|
print("vendor name = " + str(camera.getVendorName(camera)))
|
||||||
|
print("Model name = " + str(camera.getModelName(camera)))
|
||||||
|
print("Serial number = " + str(camera.getSerialNumber(camera)))
|
||||||
|
|
||||||
|
camera = cameraList[0]
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
pass
|
||||||
|
# camera_count = mv_camera.start_discovery()
|
||||||
|
# if camera_count <= 0:
|
||||||
|
# mv_camera.logger.info('no cameras found!')
|
||||||
|
# else:
|
||||||
|
# mv_camera.open()
|
||||||
|
# mv_camera.set_roi(0, 0, 3000, 3000)
|
||||||
|
# mv_camera.grab_one()
|
||||||
|
#
|
||||||
|
# time.sleep(2)
|
||||||
|
#
|
||||||
|
# mv_camera.set_exposure_time(20000)
|
||||||
|
# mv_camera.stop()
|
||||||
|
# # 3s exit
|
||||||
|
# time.sleep(3)
|
171
components/dat_task.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from dynaconf.base import Settings
|
||||||
|
from exceptiongroup import catch
|
||||||
|
|
||||||
|
from core.edge_component import EdgeComponent, action, service
|
||||||
|
|
||||||
|
|
||||||
|
@action("dat_task", auto_start=True)
|
||||||
|
class TaskTable(EdgeComponent):
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
super(TaskTable, self).__init__(context)
|
||||||
|
self.db_path = None
|
||||||
|
|
||||||
|
def configure(self, setting: Settings) -> None:
|
||||||
|
self.db_path = self.context.get_component("_database").db_path
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def start(self) -> None:
|
||||||
|
super().start()
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def insert_task(self, entity):
|
||||||
|
cmd_str = "INSERT INTO dat_task ("
|
||||||
|
value_str = " VALUES ("
|
||||||
|
data = []
|
||||||
|
for k, v in entity.items():
|
||||||
|
if v is not None and k != "id":
|
||||||
|
cmd_str += " {},".format(k)
|
||||||
|
value_str += " ?,"
|
||||||
|
data.append(v)
|
||||||
|
cmd_str = cmd_str[:len(cmd_str) - 1]
|
||||||
|
value_str = value_str[:len(value_str) - 1]
|
||||||
|
cmd_str += ")" + value_str + " )"
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
cursor = None
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = self.conn.cursor()
|
||||||
|
cursor.execute(cmd_str, data)
|
||||||
|
self.conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if cursor is not None:
|
||||||
|
cursor.close()
|
||||||
|
if conn is not None:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def update_task(self, entity):
|
||||||
|
if entity["id"] is None:
|
||||||
|
return
|
||||||
|
cmd_str = "UPDATE dat_task SET"
|
||||||
|
data = []
|
||||||
|
for k, v in entity.items():
|
||||||
|
if v is not None and k != "id":
|
||||||
|
cmd_str += " {}=?,".format(k)
|
||||||
|
data.append(v)
|
||||||
|
cmd_str = cmd_str[:len(cmd_str) - 1]
|
||||||
|
if cmd_str.find('?') < 0:
|
||||||
|
return
|
||||||
|
cmd_str += " WHERE id=?"
|
||||||
|
data.append(entity["id"])
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
cursor = None
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(cmd_str, data)
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if cursor is not None:
|
||||||
|
cursor.close()
|
||||||
|
if conn is not None:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def list_task(self, state):
|
||||||
|
conn = None
|
||||||
|
cursor = None
|
||||||
|
l = None
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT * FROM dat_task WHERE state <= ? order by create_time", (state,))
|
||||||
|
l = cursor.fetchall()
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if cursor is not None:
|
||||||
|
cursor.close()
|
||||||
|
if conn is not None:
|
||||||
|
conn.close()
|
||||||
|
return l
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def add_server_tasks(self, tasks):
|
||||||
|
"""
|
||||||
|
接收来自PC的任务
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if tasks is None or len(tasks) == 0:
|
||||||
|
self.logger.warn("没有接收到任务数据!")
|
||||||
|
return
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
cursor = None
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
for task in tasks:
|
||||||
|
cursor.execute("SELECT * FROM dat_task WHERE id = ? and device_sn = ?", (task["id"], task["deviceSn"]))
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
if len(rows) == 0:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO dat_task (id, name, device_sn, param_json, state, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(task['id'], task['name'], task['deviceSn'], task['paramJson'], task['state'], task['createTime'], task['updateTime'])
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(e)
|
||||||
|
finally:
|
||||||
|
if cursor is not None:
|
||||||
|
cursor.close()
|
||||||
|
if conn is not None:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def sync_client_tasks(self, device_id, device_sn):
|
||||||
|
"""
|
||||||
|
上传本地任务数据
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
result = None
|
||||||
|
if device_id is None or device_sn is None == 0:
|
||||||
|
self.logger.warn("没有接收到任务数据!")
|
||||||
|
return result
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
cursor = None
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT id, device_sn, result_json, start_time, create_time, state FROM dat_task WHERE id = ? and device_sn = ?", (device_id, device_sn))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row is not None:
|
||||||
|
result = dict()
|
||||||
|
result["id"] = row[0]
|
||||||
|
result["result_json"] = row[2]
|
||||||
|
result["start_time"] = row[3]
|
||||||
|
result["end_time"] = row[4]
|
||||||
|
result["state"] = row[5]
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(e)
|
||||||
|
finally:
|
||||||
|
if cursor is not None:
|
||||||
|
cursor.close()
|
||||||
|
if conn is not None:
|
||||||
|
conn.close()
|
||||||
|
return result
|
133
components/gpio.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import logging
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
from dynaconf.base import Settings
|
||||||
|
from core.edge_component import device, EdgeComponent, service
|
||||||
|
|
||||||
|
|
||||||
|
class GpioSignals(QObject):
|
||||||
|
gpio_lidar_opened = pyqtSignal(int)
|
||||||
|
gpio_lidar_closed = pyqtSignal(int)
|
||||||
|
gpio_camera_opened = pyqtSignal(int)
|
||||||
|
gpio_camera_closed = pyqtSignal(int)
|
||||||
|
|
||||||
|
@device("gpio", auto_start=True)
|
||||||
|
class GPIOManager(EdgeComponent):
|
||||||
|
signals = GpioSignals()
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
super(GPIOManager, self).__init__(context)
|
||||||
|
self.lidar_pin = 80
|
||||||
|
self.camera_pin = 91
|
||||||
|
self.export_path = '/sys/class/gpio/export'
|
||||||
|
self.unexport_path = '/sys/class/gpio/unexport'
|
||||||
|
self.lidar_value_path = None
|
||||||
|
self.lidar_direction_path = None
|
||||||
|
self.camera_value_path = None
|
||||||
|
self.camera_direction_path = None
|
||||||
|
|
||||||
|
def configure(self, setting: Settings) -> None:
|
||||||
|
self.lidar_pin = setting.get('gpio.lidar.pin', 80)
|
||||||
|
self.camera_pin = setting.get('gpio.camera.pin', 91)
|
||||||
|
|
||||||
|
self.lidar_value_path = f'/sys/class/gpio/gpio{self.lidar_pin}/value'
|
||||||
|
self.lidar_direction_path = f'/sys/class/gpio/gpio{self.lidar_pin}/direction'
|
||||||
|
self.camera_value_path = f'/sys/class/gpio/gpio{self.camera_pin}/value'
|
||||||
|
self.camera_direction_path = f'/sys/class/gpio/gpio{self.camera_pin}/direction'
|
||||||
|
|
||||||
|
self.logger.info(f"Gpio configure done.")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def start(self) -> None:
|
||||||
|
"""导出 GPIO 引脚并设置为输出"""
|
||||||
|
self.stop()
|
||||||
|
try:
|
||||||
|
with open(self.export_path, 'w') as f:
|
||||||
|
f.write(str(self.lidar_pin))
|
||||||
|
with open(self.lidar_direction_path, 'w') as f:
|
||||||
|
f.write('out')
|
||||||
|
self.logger.info(f"Export GPIO {self.lidar_pin}")
|
||||||
|
|
||||||
|
with open(self.export_path, 'w') as f:
|
||||||
|
f.write(str(self.camera_pin))
|
||||||
|
with open(self.camera_direction_path, 'w') as f:
|
||||||
|
f.write('out')
|
||||||
|
self.logger.info(f"Export GPIO {self.camera_pin}")
|
||||||
|
except IOError as e:
|
||||||
|
self.logger.error(f"Error exporting GPIO {self.lidar_pin}/{self.camera_pin}: {e}")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""释放 GPIO 引脚"""
|
||||||
|
try:
|
||||||
|
with open(self.unexport_path, 'w') as f:
|
||||||
|
f.write(str(self.lidar_pin))
|
||||||
|
self.logger.info(f"Release GPIO {self.lidar_pin}")
|
||||||
|
|
||||||
|
with open(self.unexport_path, 'w') as f:
|
||||||
|
f.write(str(self.camera_pin))
|
||||||
|
self.logger.info(f"Release GPIO {self.camera_pin}")
|
||||||
|
except IOError as e:
|
||||||
|
self.logger.error(f"Error unexporting GPIO {self.lidar_pin}/{self.camera_pin}: {e}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lidar_value(self):
|
||||||
|
try:
|
||||||
|
with open(self.lidar_value_path, 'r') as f:
|
||||||
|
return int(f.read().strip())
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error reading GPIO value: {e}")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
@lidar_value.setter
|
||||||
|
def lidar_value(self, val):
|
||||||
|
try:
|
||||||
|
with open(self.lidar_value_path, 'w') as f:
|
||||||
|
f.write(str(val))
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error setting GPIO value: {e}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def camera_value(self):
|
||||||
|
try:
|
||||||
|
with open(self.camera_value_path, 'r') as f:
|
||||||
|
return int(f.read().strip())
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error reading GPIO value: {e}")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
@camera_value.setter
|
||||||
|
def camera_value(self, val):
|
||||||
|
try:
|
||||||
|
with open(self.camera_value_path, 'w') as f:
|
||||||
|
f.write(str(val))
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error setting GPIO value: {e}")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def open_lidar(self):
|
||||||
|
"""设置 GPIO 为低电平并发出信号"""
|
||||||
|
self.lidar_value = 0 # 设置为低电平
|
||||||
|
self.signals.gpio_lidar_opened.emit(self.lidar_pin)
|
||||||
|
self.logger.info(f"Setting GPIO {self.lidar_pin}: LOW")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def close_lidar(self):
|
||||||
|
"""设置 GPIO 为高电平并发出信号"""
|
||||||
|
self.lidar_value = 1 # 设置为高电平
|
||||||
|
self.signals.gpio_lidar_closed.emit(self.lidar_pin)
|
||||||
|
self.logger.info(f"Setting GPIO {self.lidar_pin}: HIGH")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def open_camera(self):
|
||||||
|
"""设置 GPIO 为低电平并发出信号"""
|
||||||
|
self.camera_value = 0 # 设置为低电平
|
||||||
|
self.signals.gpio_camera_opened.emit(self.camera_pin)
|
||||||
|
self.logger.info(f"Setting GPIO {self.camera_pin}: LOW")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def close_camera(self):
|
||||||
|
"""设置 GPIO 为高电平并发出信号"""
|
||||||
|
self.camera_value = 1 # 设置为高电平
|
||||||
|
self.signals.gpio_camera_closed.emit(self.camera_pin)
|
||||||
|
self.logger.info(f"Setting GPIO {self.camera_pin}: HIGH")
|
135
components/image_framework.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import os
|
||||||
|
from ctypes import *
|
||||||
|
import numpy as np
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
from dynaconf.base import Settings
|
||||||
|
from numpy.ctypeslib import as_array
|
||||||
|
import platform
|
||||||
|
import time
|
||||||
|
|
||||||
|
from core.edge_component import action, EdgeComponent, service
|
||||||
|
from util import get_system_and_library_suffix
|
||||||
|
|
||||||
|
|
||||||
|
# 给回调函数使用
|
||||||
|
class _SubRoiData_(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('mPparentId', c_long),
|
||||||
|
('mId', c_long),
|
||||||
|
('mInfo', c_char * 256),
|
||||||
|
('isGood', c_bool),
|
||||||
|
('vpOffset', c_float * 2),
|
||||||
|
('mRoiPoints', c_float * 8), # 4 * 2
|
||||||
|
('mInnerConners', c_float * 8), # 4 * 2
|
||||||
|
('mInnerConners3D', c_float * 12), # 4 * 3
|
||||||
|
('edges', c_float * 4),
|
||||||
|
('center', c_float * 2),
|
||||||
|
('cloud_size', c_int),
|
||||||
|
('mVc2D', POINTER(c_float)),
|
||||||
|
('mVc3D', POINTER(c_float)),
|
||||||
|
('H', c_float * 9),
|
||||||
|
]
|
||||||
|
|
||||||
|
SubRoiData = _SubRoiData_
|
||||||
|
|
||||||
|
class _CallbackInfo_(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('code', c_int),
|
||||||
|
('errorInfo', c_char * 256),
|
||||||
|
('bGetData', c_bool),
|
||||||
|
('mId', c_long),
|
||||||
|
('mInfo', c_char * 256),
|
||||||
|
('mImPath', c_char * 256),
|
||||||
|
('mIm2D3DPtr', c_char * 256),
|
||||||
|
('roi_size', c_int),
|
||||||
|
('subRois', SubRoiData * 16),
|
||||||
|
]
|
||||||
|
|
||||||
|
CallbackInfo = _CallbackInfo_
|
||||||
|
# 回调函数类型定义
|
||||||
|
CallbackType = CFUNCTYPE(None, POINTER(CallbackInfo))
|
||||||
|
|
||||||
|
|
||||||
|
class ImageFrameworkSignals(QObject):
|
||||||
|
# QT 信号
|
||||||
|
on_image_result = pyqtSignal(object)
|
||||||
|
|
||||||
|
|
||||||
|
@action("image-framework", auto_start=True)
|
||||||
|
class ImageFramework(EdgeComponent):
|
||||||
|
signals = ImageFrameworkSignals()
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
super().__init__(context)
|
||||||
|
self.image_framework_sdk = None
|
||||||
|
self.data_callback = None
|
||||||
|
|
||||||
|
def configure(self, setting: Settings) -> None:
|
||||||
|
self.init_image_framework_sdk()
|
||||||
|
self.logger.info(f"Image framework configure done.")
|
||||||
|
|
||||||
|
def init_image_framework_sdk(self):
|
||||||
|
try:
|
||||||
|
# 加载C++库
|
||||||
|
current_file_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
(arch, suffix) = get_system_and_library_suffix()
|
||||||
|
self.image_framework_sdk = CDLL(os.path.join(current_file_dir, f'../vendors/image-framework/{arch}/image_framework.{suffix}'))
|
||||||
|
# 设置回调函数
|
||||||
|
self.init_callback()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Load Image framework sdk failed: {str(e)}")
|
||||||
|
|
||||||
|
def init_callback(self):
|
||||||
|
def _callback(data):
|
||||||
|
contents = data.contents
|
||||||
|
self.logger.info(f"image framework callback data, bGetData:{contents.bGetData}")
|
||||||
|
if contents.bGetData:
|
||||||
|
record = CallbackInfo()
|
||||||
|
self.image_framework_sdk.LibapGetRecord(byref(record))
|
||||||
|
self.logger.info(f"image framework callback data, roi_size:{record.roi_size}")
|
||||||
|
self.logger.info(f"image framework callback data, code:{record.code}")
|
||||||
|
self.logger.info(f"image framework callback data, errorInfo:{record.errorInfo}")
|
||||||
|
for i in range(record.roi_size):
|
||||||
|
roi_data = record.subRois[i]
|
||||||
|
self.logger.info(f"roi data, roi_data[{i}].mId:{roi_data.mId}")
|
||||||
|
self.logger.info(f"roi data, roi_data[{i}].isGood:{roi_data.isGood}")
|
||||||
|
self.logger.info(f"roi data, roi_data[{i}].mRoiPoints:{list(roi_data.mRoiPoints)}")
|
||||||
|
|
||||||
|
self.logger.info("image framework callback done.")
|
||||||
|
|
||||||
|
self.data_callback = CallbackType(_callback)
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def start(self):
|
||||||
|
super().start()
|
||||||
|
if self.image_framework_sdk is None:
|
||||||
|
raise RuntimeError("Image framework SDK未正确加载!")
|
||||||
|
|
||||||
|
ret = self.image_framework_sdk.LibapiInit(1, "", self.data_callback)
|
||||||
|
if ret != 0:
|
||||||
|
raise RuntimeError("Image framework 初始化失败!")
|
||||||
|
|
||||||
|
self.logger.info("Image framework started.")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def stop(self):
|
||||||
|
self.image_framework_sdk.stop_sdk()
|
||||||
|
self.logger.info("Image framework stopped.")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def start_detect(self):
|
||||||
|
ret = self.image_framework_sdk.LibapiStartDetection()
|
||||||
|
if ret != 0:
|
||||||
|
raise RuntimeError("启动检测失败!")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def continue_detect(self):
|
||||||
|
ret = self.image_framework_sdk.LibapiContinuetDetection()
|
||||||
|
if ret != 0:
|
||||||
|
raise RuntimeError("继续检测失败!")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def stop_detect(self):
|
||||||
|
ret = self.image_framework_sdk.LibapStopDetection()
|
||||||
|
if ret != 0:
|
||||||
|
raise RuntimeError("停止检测失败!")
|
282
components/lidar.py
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import os
|
||||||
|
import ctypes
|
||||||
|
from ctypes import *
|
||||||
|
import numpy as np
|
||||||
|
from dynaconf.base import Settings
|
||||||
|
|
||||||
|
from core.edge_component import device, EdgeComponent, service
|
||||||
|
from core.logging import logger
|
||||||
|
import time
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
from core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
# 定义 livox_status 的返回值类型
|
||||||
|
livox_status = ctypes.c_int
|
||||||
|
# 预定义的常量
|
||||||
|
kBroadcastCodeSize = 16 # 广播码长度
|
||||||
|
|
||||||
|
# 定义枚举类型
|
||||||
|
class DeviceType(c_int):
|
||||||
|
kDeviceTypeHub = 0
|
||||||
|
kDeviceTypeLidarMid40 = 1
|
||||||
|
kDeviceTypeLidarTele = 2
|
||||||
|
kDeviceTypeLidarHorizon = 3
|
||||||
|
kDeviceTypeLidarMid70 = 6
|
||||||
|
kDeviceTypeLidarAvia = 7
|
||||||
|
|
||||||
|
class LidarState(c_int):
|
||||||
|
kLidarStateInit = 0
|
||||||
|
kLidarStateNormal = 1
|
||||||
|
kLidarStatePowerSaving = 2
|
||||||
|
kLidarStateStandBy = 3
|
||||||
|
kLidarStateError = 4
|
||||||
|
kLidarStateUnknown = 5
|
||||||
|
|
||||||
|
class LidarMode(c_int):
|
||||||
|
kLidarModeNormal = 1
|
||||||
|
kLidarModePowerSaving = 2
|
||||||
|
kLidarModeStandby = 3
|
||||||
|
|
||||||
|
class LidarFeature(c_int):
|
||||||
|
kLidarFeatureNone = 0
|
||||||
|
kLidarFeatureRainFog = 1
|
||||||
|
|
||||||
|
class LidarIpMode(c_int):
|
||||||
|
kLidarDynamicIpMode = 0
|
||||||
|
kLidarStaticIpMode = 1
|
||||||
|
|
||||||
|
class LidarScanPattern(c_int):
|
||||||
|
kNoneRepetitiveScanPattern = 0
|
||||||
|
kRepetitiveScanPattern = 1
|
||||||
|
|
||||||
|
class LivoxStatus(c_int):
|
||||||
|
kStatusSendFailed = -9
|
||||||
|
kStatusHandlerImplNotExist = -8
|
||||||
|
kStatusInvalidHandle = -7
|
||||||
|
kStatusChannelNotExist = -6
|
||||||
|
kStatusNotEnoughMemory = -5
|
||||||
|
kStatusTimeout = -4
|
||||||
|
kStatusNotSupported = -3
|
||||||
|
kStatusNotConnected = -2
|
||||||
|
kStatusFailure = -1
|
||||||
|
kStatusSuccess = 0
|
||||||
|
|
||||||
|
# 定义结构体和联合体
|
||||||
|
class LidarErrorCode(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("temp_status", c_uint32, 2),
|
||||||
|
("volt_status", c_uint32, 2),
|
||||||
|
("motor_status", c_uint32, 2),
|
||||||
|
("dirty_warn", c_uint32, 2),
|
||||||
|
("firmware_err", c_uint32, 1),
|
||||||
|
("pps_status", c_uint32, 1),
|
||||||
|
("device_status", c_uint32, 1),
|
||||||
|
("fan_status", c_uint32, 1),
|
||||||
|
("self_heating", c_uint32, 1),
|
||||||
|
("ptp_status", c_uint32, 1),
|
||||||
|
("time_sync_status", c_uint32, 3),
|
||||||
|
("rsvd", c_uint32, 13),
|
||||||
|
("system_status", c_uint32, 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
class HubErrorCode(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("sync_status", c_uint32, 2),
|
||||||
|
("temp_status", c_uint32, 2),
|
||||||
|
("lidar_status", c_uint32, 1),
|
||||||
|
("lidar_link_status", c_uint32, 1),
|
||||||
|
("firmware_err", c_uint32, 1),
|
||||||
|
("rsvd", c_uint32, 23),
|
||||||
|
("system_status", c_uint32, 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
class ErrorMessage(Union):
|
||||||
|
_fields_ = [
|
||||||
|
("error_code", c_uint32),
|
||||||
|
("lidar_error_code", LidarErrorCode),
|
||||||
|
("hub_error_code", HubErrorCode)
|
||||||
|
]
|
||||||
|
|
||||||
|
class StatusUnion(Union):
|
||||||
|
_fields_ = [
|
||||||
|
("error_code", c_uint32), # 假设这个字段为示例
|
||||||
|
("lidar_error_code", LidarErrorCode),
|
||||||
|
("hub_error_code", HubErrorCode)
|
||||||
|
]
|
||||||
|
|
||||||
|
# 定义 DeviceInfo 结构体
|
||||||
|
class DeviceInfo(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("broadcast_code", c_char * kBroadcastCodeSize), # 广播码,最大15个字符,带终止符
|
||||||
|
("handle", c_uint8), # 设备句柄
|
||||||
|
("slot", c_uint8), # 插槽编号
|
||||||
|
("id", c_uint8), # LiDAR id
|
||||||
|
("type", DeviceType), # 设备类型
|
||||||
|
("data_port", c_uint16), # 点云数据UDP端口
|
||||||
|
("cmd_port", c_uint16), # 控制命令UDP端口
|
||||||
|
("sensor_port", c_uint16), # IMU数据UDP端口
|
||||||
|
("ip", c_char * 16), # IP地址
|
||||||
|
("state", LidarState), # LiDAR状态
|
||||||
|
("feature", LidarFeature), # LiDAR特性
|
||||||
|
("status", StatusUnion), # LiDAR工作状态
|
||||||
|
("firmware_version", c_uint8 * 4) # 固件版本
|
||||||
|
]
|
||||||
|
|
||||||
|
# 定义 BroadcastDeviceInfo 结构体
|
||||||
|
class BroadcastDeviceInfo(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("broadcast_code", c_char * kBroadcastCodeSize), # 广播码,最多15个字符,带终止符
|
||||||
|
("dev_type", c_uint8), # 设备类型,参考 DeviceType
|
||||||
|
("reserved", c_uint16), # 保留字段
|
||||||
|
("ip", c_char * 16) # 设备 IP 地址
|
||||||
|
]
|
||||||
|
|
||||||
|
# 定义 LivoxEthPacket 结构体
|
||||||
|
class LivoxEthPacket(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("version", c_uint8), # Packet protocol version.
|
||||||
|
("slot", c_uint8), # Slot number used for connecting LiDAR.
|
||||||
|
("id", c_uint8), # LiDAR id.
|
||||||
|
("rsvd", c_uint8), # Reserved.
|
||||||
|
("err_code", c_uint32), # Device error status indicator information.
|
||||||
|
("timestamp_type", c_uint8), # Timestamp type.
|
||||||
|
("data_type", c_uint8), # Point cloud coordinate format.
|
||||||
|
("timestamp", c_uint8 * 8), # Nanosecond or UTC format timestamp.
|
||||||
|
("data", c_uint8 * 1) # Point cloud data (can be extended as needed).
|
||||||
|
]
|
||||||
|
|
||||||
|
# Extend cartesian coordinate format.
|
||||||
|
class LivoxExtendRawPoint(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('x', c_int32), # X axis, Unit: mm
|
||||||
|
('y', c_int32), # Y axis, Unit: mm
|
||||||
|
('z', c_int32), # Z axis, Unit: mm
|
||||||
|
('reflectivity', c_uint8), # Reflectivity
|
||||||
|
('tag', c_uint8) # Tag
|
||||||
|
]
|
||||||
|
|
||||||
|
# 回调函数类型定义
|
||||||
|
DataCallbackType = CFUNCTYPE(None, c_uint8, POINTER(c_int32), c_uint32)
|
||||||
|
ErrorCallbackType = CFUNCTYPE(None, c_uint8, c_uint8, POINTER(ErrorMessage))
|
||||||
|
DeviceChangeCallbackType = CFUNCTYPE(None, POINTER(DeviceInfo), c_uint8)
|
||||||
|
DeviceBroadcastCallbackType = CFUNCTYPE(None, POINTER(BroadcastDeviceInfo))
|
||||||
|
CommonCommandCallback = CFUNCTYPE(None, c_int, c_uint8, c_uint8)
|
||||||
|
|
||||||
|
|
||||||
|
class LidarSignals(QObject):
|
||||||
|
# QT 信号
|
||||||
|
on_device_broadcast = pyqtSignal(str)
|
||||||
|
# 0 - 已连接,1 - 已断开
|
||||||
|
on_device_change = pyqtSignal(int)
|
||||||
|
on_device_error = pyqtSignal(ErrorMessage)
|
||||||
|
on_device_data = pyqtSignal(object)
|
||||||
|
|
||||||
|
|
||||||
|
@device("lidar", auto_start=True)
|
||||||
|
class LidarDevice(EdgeComponent):
|
||||||
|
signals = LidarSignals()
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
super().__init__(context)
|
||||||
|
self.livox_sdk = None
|
||||||
|
self.data_callback = None
|
||||||
|
self.error_callback = None
|
||||||
|
self.device_change_callback = None
|
||||||
|
self.device_broadcast_callback = None
|
||||||
|
self.common_command_callback = None
|
||||||
|
|
||||||
|
def configure(self, setting: Settings) -> None:
|
||||||
|
self.init_livox_sdk()
|
||||||
|
self.logger.info(f"Lidar configure done.")
|
||||||
|
|
||||||
|
def init_livox_sdk(self):
|
||||||
|
try:
|
||||||
|
# 加载C++库
|
||||||
|
current_file_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
self.livox_sdk = ctypes.CDLL(os.path.join(current_file_dir, '../vendors/livox/liblivox_sdk_wrapper.so'))
|
||||||
|
self.livox_sdk.connect.argtypes = [c_char_p] # 设置参数类型
|
||||||
|
self.livox_sdk.connect.restype = c_int # 设置返回值类型
|
||||||
|
# 设置回调函数
|
||||||
|
self.init_livox_callback()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"load livox sdk failed: {str(e)}")
|
||||||
|
|
||||||
|
def init_livox_callback(self):
|
||||||
|
# 数据回调函数
|
||||||
|
def data_callback(handle, points, data_num):
|
||||||
|
# 将 ctypes 指针转换为 numpy 数组,点数 * 4 个 uint32 值 (x, y, z, reflectivity_and_tag)
|
||||||
|
np_data = np.ctypeslib.as_array(points, shape=(data_num * 4,))
|
||||||
|
# 将 x, y, z 组成新的二维 numpy 数组
|
||||||
|
xyz = np_data.reshape(data_num, 4)[:, :3] # 提取前 3 个数值 (x, y, z)
|
||||||
|
# 结果
|
||||||
|
self.signals.on_device_data.emit(xyz)
|
||||||
|
|
||||||
|
# 通用指令回调函数
|
||||||
|
def common_command_callback(status, handle, response):
|
||||||
|
logger.info(f"Common from handle {handle}, status: {status}, response: {response}")
|
||||||
|
|
||||||
|
# 错误回调函数
|
||||||
|
def error_callback(status, handle, error_message):
|
||||||
|
logger.error(f"Error from handle {handle}, status: {status}")
|
||||||
|
|
||||||
|
# 设备状态变化回调函数
|
||||||
|
def device_change_callback(info, event_type):
|
||||||
|
logger.info(
|
||||||
|
f"Device {bytes(info.contents.broadcast_code).decode('utf-8')} changed state, event type: {event_type}")
|
||||||
|
self.signals.on_device_change.emit(event_type)
|
||||||
|
|
||||||
|
# 设备广播回调函数
|
||||||
|
def device_broadcast_callback(info):
|
||||||
|
broadcast_code_bytes = bytes(info.contents.broadcast_code)
|
||||||
|
broadcast_code = broadcast_code_bytes.decode('utf-8')
|
||||||
|
logger.info(f"New device broadcast received: {broadcast_code}")
|
||||||
|
if broadcast_code == settings.get("lidar.target.barcode", "3GGDLCM00201561"):
|
||||||
|
logger.info(f"Connect device: {broadcast_code}")
|
||||||
|
self.livox_sdk.connect(broadcast_code_bytes)
|
||||||
|
self.signals.on_device_broadcast.emit(broadcast_code)
|
||||||
|
|
||||||
|
self.data_callback = DataCallbackType(data_callback)
|
||||||
|
self.error_callback = ErrorCallbackType(data_callback)
|
||||||
|
self.device_change_callback = DeviceChangeCallbackType(device_change_callback)
|
||||||
|
self.device_broadcast_callback = DeviceBroadcastCallbackType(device_broadcast_callback)
|
||||||
|
self.common_command_callback = CommonCommandCallback(common_command_callback)
|
||||||
|
# 注册回调函数
|
||||||
|
self.livox_sdk.register_data_callback(self.data_callback)
|
||||||
|
self.livox_sdk.register_error_callback(self.error_callback)
|
||||||
|
self.livox_sdk.register_device_change_callback(self.device_change_callback)
|
||||||
|
self.livox_sdk.register_device_broadcast_callback(self.device_broadcast_callback)
|
||||||
|
self.livox_sdk.register_common_command_callback(self.common_command_callback)
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def start(self):
|
||||||
|
if self.livox_sdk is None:
|
||||||
|
raise RuntimeError("雷达设备SDK未正确加载")
|
||||||
|
|
||||||
|
if self.livox_sdk.init_sdk():
|
||||||
|
self.logger.info("Livox SDK initialized.")
|
||||||
|
if self.livox_sdk.start_discovery():
|
||||||
|
self.logger.info("Started discovering Livox devices.")
|
||||||
|
else:
|
||||||
|
self.logger.error("Failed to discover devices.")
|
||||||
|
else:
|
||||||
|
self.logger.error("Failed to initialize Livox SDK.")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def set_mode(self, mode):
|
||||||
|
"""
|
||||||
|
设置雷达模式:1-normal,2-power saving
|
||||||
|
"""
|
||||||
|
self.logger.info(f'set mode: {mode}')
|
||||||
|
return self.livox_sdk.set_mode(mode)
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def stop(self):
|
||||||
|
self.livox_sdk.stop_sdk()
|
||||||
|
self.logger.info("Livox SDK stopped.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
pass
|
135
components/ups.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
from threading import Timer
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from dynaconf.base import Settings
|
||||||
|
from serial import Serial, SerialException
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
from core.edge_component import device, EdgeComponent, service
|
||||||
|
|
||||||
|
|
||||||
|
def get_crc(pc_rec, i_len):
|
||||||
|
i_crc = 0xFFFF # 初始化CRC值
|
||||||
|
if i_len <= 0:
|
||||||
|
return None # 长度为0时返回None
|
||||||
|
|
||||||
|
for ix in range(i_len):
|
||||||
|
i_crc ^= pc_rec[ix] # 进行异或运算
|
||||||
|
for iy in range(8):
|
||||||
|
if (i_crc & 1) != 0:
|
||||||
|
i_crc = (i_crc >> 1) ^ 0xA001 # CRC多项式
|
||||||
|
else:
|
||||||
|
i_crc >>= 1 # 右移一位
|
||||||
|
|
||||||
|
# 返回高字节和低字节
|
||||||
|
return [(i_crc >> 8) & 0xFF, i_crc & 0xFF]
|
||||||
|
|
||||||
|
|
||||||
|
def read_packet(addr):
|
||||||
|
pack = bytearray([0xAB, 0xCD, 0x00, 0x03, addr, 0x00, 0x00])
|
||||||
|
crc = get_crc(pack, 7)
|
||||||
|
pack.append(crc[0])
|
||||||
|
pack.append(crc[1])
|
||||||
|
return pack
|
||||||
|
|
||||||
|
|
||||||
|
class UpsSignals(QObject):
|
||||||
|
battery_change = pyqtSignal(int)
|
||||||
|
state_change = pyqtSignal(int)
|
||||||
|
|
||||||
|
|
||||||
|
@device("ups", auto_start=True)
|
||||||
|
class Ups(EdgeComponent):
|
||||||
|
|
||||||
|
signals = UpsSignals()
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
super(Ups, self).__init__(context)
|
||||||
|
self.port = '/dev/ttyUSB0'
|
||||||
|
self.bytesize = 8
|
||||||
|
self.parity = 'N'
|
||||||
|
self.stopbits = 1
|
||||||
|
self.baudrate = 115200
|
||||||
|
self.serial = None
|
||||||
|
self.read_timer = None
|
||||||
|
self.cmd_read_delay = 0.3
|
||||||
|
|
||||||
|
def configure(self, setting: Settings) -> None:
|
||||||
|
self.port = setting.get('ups.port', '/dev/ttyUSB0')
|
||||||
|
self.baudrate = setting.get('ups.baudrate', 115200)
|
||||||
|
self.bytesize = setting.get('ups.bytesize', 8)
|
||||||
|
self.stopbits = setting.get('ups.stopbits', 1)
|
||||||
|
self.parity = setting.get('ups.parity', 'N')
|
||||||
|
self.cmd_read_delay = setting.get('ups.cmd.read.delay', 0.3)
|
||||||
|
|
||||||
|
self.logger.info(f"Ups configure done.")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def start(self) -> None:
|
||||||
|
# 打开串口
|
||||||
|
try:
|
||||||
|
self.serial = Serial(port=self.port, baudrate=self.baudrate, stopbits=self.stopbits, parity=self.parity,
|
||||||
|
bytesize=self.bytesize, timeout=2.0)
|
||||||
|
self.logger.info(f"open serial port: {self.port} baudrate:{self.baudrate} stopbits:{self.stopbits} bytesize:{self.bytesize}")
|
||||||
|
except SerialException as e:
|
||||||
|
self.logger.error(f"Failed to open serial port: {e}")
|
||||||
|
|
||||||
|
# 启动读取数据
|
||||||
|
self.read_battery()
|
||||||
|
self.logger.info('Ups start!')
|
||||||
|
super().start()
|
||||||
|
|
||||||
|
def read_battery(self):
|
||||||
|
if self.serial is not None and self.serial.isOpen():
|
||||||
|
# 读取状态
|
||||||
|
read_state_pack = read_packet(0x05)
|
||||||
|
self.serial.write(read_state_pack)
|
||||||
|
# self.logger.info(f"发送查询状态指令: {read_state_pack.hex()}")
|
||||||
|
sleep(self.cmd_read_delay)
|
||||||
|
if self.serial.in_waiting >= 9:
|
||||||
|
# 返回指令释义:
|
||||||
|
# AB CD 00 03 05 64 00 00 00
|
||||||
|
data = self.serial.read(9)
|
||||||
|
# self.logger.info(f'收到查询状态数据:{data.hex()}')
|
||||||
|
if data[0] == 0xAB and data[1] == 0xCD:
|
||||||
|
state = data[6]
|
||||||
|
# self.logger.info(f'当前状态:{state}')
|
||||||
|
self.signals.state_change.emit(state)
|
||||||
|
|
||||||
|
# 读取电量
|
||||||
|
read_battery_pack = read_packet(0x01)
|
||||||
|
self.serial.write(read_battery_pack)
|
||||||
|
# self.logger.info(f"发送查询电量指令: {read_battery_pack.hex()}")
|
||||||
|
sleep(self.cmd_read_delay)
|
||||||
|
if self.serial.in_waiting >= 9:
|
||||||
|
# 返回指令释义:
|
||||||
|
# abcd000301003c4042
|
||||||
|
data = self.serial.read(9)
|
||||||
|
# self.logger.info(f'收到查询电量数据:{data.hex()}')
|
||||||
|
if data[0] == 0xAB and data[1] == 0xCD:
|
||||||
|
battery = data[6]
|
||||||
|
# self.logger.info(f'当前电量:{battery}')
|
||||||
|
self.signals.battery_change.emit(battery)
|
||||||
|
|
||||||
|
self.read_timer = Timer(3, self.read_battery)
|
||||||
|
self.read_timer.start()
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def stop(self) -> None:
|
||||||
|
if self.read_timer is not None:
|
||||||
|
self.read_timer.cancel()
|
||||||
|
|
||||||
|
if self.serial is not None:
|
||||||
|
self.serial.close()
|
||||||
|
self.serial = None
|
||||||
|
|
||||||
|
self.logger.info('Ups stop!')
|
||||||
|
super().stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
ups = Ups()
|
||||||
|
ups.start()
|
||||||
|
sleep(30)
|
||||||
|
ups.stop()
|
42
config.ini
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# 2D config
|
||||||
|
[2D]
|
||||||
|
w=9344
|
||||||
|
h=7000
|
||||||
|
fx=11241.983
|
||||||
|
fy=11244.0599
|
||||||
|
cx=4553.03821
|
||||||
|
cy=3516.9118
|
||||||
|
k1=-0.04052072
|
||||||
|
k2=0.22211572
|
||||||
|
p1=0.00042405
|
||||||
|
p2=-0.00367346
|
||||||
|
k3=-0.15639485
|
||||||
|
yolo_label=0
|
||||||
|
yolo_prob=0.25
|
||||||
|
yolo_modol_path=./best_rgb_ymj_csc_80_2.om
|
||||||
|
|
||||||
|
# 3D config
|
||||||
|
[3D]
|
||||||
|
r=-0.321605
|
||||||
|
p=-1.5938
|
||||||
|
y=1.91032
|
||||||
|
tx=0.109253
|
||||||
|
ty=0.0710213
|
||||||
|
tz=0.0175978
|
||||||
|
dminx=-2.5
|
||||||
|
dmaxx=2.5
|
||||||
|
dminy=-2.5
|
||||||
|
dmaxy=2.5
|
||||||
|
dminz=0.0
|
||||||
|
dmaxz=3.8
|
||||||
|
kk=0.02
|
||||||
|
|
||||||
|
# system config
|
||||||
|
[sys]
|
||||||
|
fake=true
|
||||||
|
camera_cap_fake=true
|
||||||
|
lidar_cap_fake=true
|
||||||
|
npu_fake=true
|
||||||
|
conners_detect_fake=true
|
||||||
|
fake_image_fpath=./vendors/image-framework/output.png
|
||||||
|
fake_lidar_fpath=./vendors/image-framework/20241027112343190.ply
|
5
config/app.toml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[default]
|
||||||
|
logging = { level = "DEBUG", directory = "./logs", console = true }
|
||||||
|
rest = { port = 8000, static = 'local' }
|
||||||
|
tcp = { enable = true, port = 13000 }
|
||||||
|
device = { name = "预埋件检测设备", model = "DT-01", sn = "DT01000001" }
|
26
config/dev.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[dev]
|
||||||
|
mqtt.server="146.56.195.129"
|
||||||
|
mqtt.port=1883
|
||||||
|
mqtt.username="cps-device-detect-1"
|
||||||
|
mqtt.password=""
|
||||||
|
|
||||||
|
sqlite3.path='detect.db'
|
||||||
|
sqlite3.sql='sql'
|
||||||
|
|
||||||
|
sched.db='sqlite:///detect-sched.db'
|
||||||
|
sched.coalesce=false
|
||||||
|
sched.max_instances=3
|
||||||
|
sched.thread_pool_workers=10
|
||||||
|
sched.process_pool_workers=3
|
||||||
|
|
||||||
|
lidar.target.baracode='3GGDLCM00201561'
|
||||||
|
|
||||||
|
ups.port='/dev/ttyUSB0'
|
||||||
|
ups.baudrate=115200
|
||||||
|
ups.bytesize=8
|
||||||
|
ups.stopbits=1
|
||||||
|
ups.parity='N'
|
||||||
|
ups.cmd.read.delay=0.3
|
||||||
|
|
||||||
|
gpio.lidar.pin=80
|
||||||
|
gpio.camera.pin=84
|
26
config/prod.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[prod]
|
||||||
|
mqtt.server="146.56.195.129"
|
||||||
|
mqtt.port=1883
|
||||||
|
mqtt.username="cps-device-detect-1"
|
||||||
|
mqtt.password=""
|
||||||
|
|
||||||
|
sqlite3.path='detect.db'
|
||||||
|
sqlite3.sql='sql'
|
||||||
|
|
||||||
|
sched.db='sqlite:///detect-sched.db'
|
||||||
|
sched.coalesce=false
|
||||||
|
sched.max_instances=3
|
||||||
|
sched.thread_pool_workers=10
|
||||||
|
sched.process_pool_workers=3
|
||||||
|
|
||||||
|
lidar.target.baracode='3GGDLCM00201561'
|
||||||
|
|
||||||
|
ups.port='/dev/ttyUSB0'
|
||||||
|
ups.baudrate=115200
|
||||||
|
ups.bytesize=8
|
||||||
|
ups.stopbits=1
|
||||||
|
ups.parity='N'
|
||||||
|
ups.cmd.read.delay=0.3
|
||||||
|
|
||||||
|
gpio.lidar.pin=80
|
||||||
|
gpio.camera.pin=91
|
0
core/__init__.py
Normal file
16
core/config.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from dynaconf import Dynaconf
|
||||||
|
|
||||||
|
current_file_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# 设置环境变量
|
||||||
|
os.environ['ENV_FOR_DYNACONF'] = 'dev'
|
||||||
|
settings = Dynaconf(
|
||||||
|
envvar_prefix="DYNACONF",
|
||||||
|
settings_files=[
|
||||||
|
os.path.join(current_file_dir, '../config/app.toml'),
|
||||||
|
os.path.join(current_file_dir, '../config/dev.toml'),
|
||||||
|
os.path.join(current_file_dir, '../config/prod.toml')
|
||||||
|
],
|
||||||
|
environments=True,
|
||||||
|
)
|
26
core/context.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
|
||||||
|
class AppContext:
|
||||||
|
"""
|
||||||
|
存放一些全局服务对象
|
||||||
|
"""
|
||||||
|
# 显示比例
|
||||||
|
_display_ratio = 1
|
||||||
|
# EdgeContext
|
||||||
|
_edge_context = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_ratio(ratio):
|
||||||
|
AppContext._display_ratio = ratio
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_ratio():
|
||||||
|
return AppContext._display_ratio
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_edge_context(ctx):
|
||||||
|
AppContext._edge_context = ctx
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_edge_context():
|
||||||
|
return AppContext._edge_context
|
130
core/edge_component.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from dynaconf.base import Settings
|
||||||
|
|
||||||
|
from core.config import settings
|
||||||
|
from core.edge_logger import LoggingMixin
|
||||||
|
|
||||||
|
|
||||||
|
def device(name, auto_start=False):
|
||||||
|
def decorator(cls):
|
||||||
|
cls.component_name = name
|
||||||
|
cls.component_type = "device"
|
||||||
|
cls.component_auto_start = auto_start
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def action(name, auto_start=True):
|
||||||
|
def decorator(cls):
|
||||||
|
cls.component_name = name
|
||||||
|
cls.component_type = "action"
|
||||||
|
cls.component_auto_start = auto_start
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def service():
|
||||||
|
"""
|
||||||
|
定义标记服务的装饰器,只有有此装饰器的函数才可以被远程调用
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
func._component_service = True
|
||||||
|
if not inspect.iscoroutinefunction(func):
|
||||||
|
# 如果原函数不是协程函数,则直接返回原函数
|
||||||
|
return func
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class EdgeComponent(LoggingMixin, ABC):
|
||||||
|
|
||||||
|
component_auto_start = False
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
super().__init__()
|
||||||
|
self.context = context
|
||||||
|
self.is_started = False
|
||||||
|
|
||||||
|
async def execute(self, func_name, *args, **kwargs):
|
||||||
|
func = getattr(self, func_name, None)
|
||||||
|
if func is None:
|
||||||
|
raise RuntimeError(f"Function {func_name} not found in {self.__class__.__name__}")
|
||||||
|
|
||||||
|
if hasattr(func, '_component_service') and func._component_service:
|
||||||
|
if callable(func):
|
||||||
|
method_signature = inspect.signature(func)
|
||||||
|
method_params = method_signature.parameters
|
||||||
|
call_args = []
|
||||||
|
for param_name, param_info in method_params.items():
|
||||||
|
if param_name == 'self' or param_info.kind == 4:
|
||||||
|
continue
|
||||||
|
param_type = param_info.annotation
|
||||||
|
if param_name in kwargs:
|
||||||
|
# 尝试将参数值转换为指定类型
|
||||||
|
try:
|
||||||
|
if param_type is inspect.Parameter.empty or param_type is Any:
|
||||||
|
# 如果参数类型未注明或为 Any 类型,则不进行强制类型转换
|
||||||
|
call_args.append(kwargs[param_name])
|
||||||
|
else:
|
||||||
|
# 否则,尝试将参数值转换为指定类型
|
||||||
|
call_args.append(param_type(kwargs[param_name]))
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
raise ValueError(f"Invalid value '{kwargs[param_name]}' for parameter '{param_name}': {e}")
|
||||||
|
elif param_info.default is inspect.Parameter.empty:
|
||||||
|
# 如果参数没有默认值且未在 kwargs 中指定,则抛出异常
|
||||||
|
raise ValueError(f"Missing required parameter '{param_name}' for method '{func_name}'.")
|
||||||
|
else:
|
||||||
|
# 否则,使用参数的默认值
|
||||||
|
call_args.append(param_info.default)
|
||||||
|
|
||||||
|
func_kwargs = {key: value for key, value in kwargs.items() if
|
||||||
|
key not in [param_name for param_name, _ in method_params.items()]}
|
||||||
|
if inspect.iscoroutinefunction(func):
|
||||||
|
return await func(*tuple(call_args), **func_kwargs)
|
||||||
|
else:
|
||||||
|
return await asyncio.to_thread(func, *tuple(call_args), **func_kwargs)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Function {func.__name__} not found in {self.__class__.__name__}")
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Function {func.__name__} does not have the required annotation.")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def start(self):
|
||||||
|
"""
|
||||||
|
启动
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.is_started = True
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def configure(self, setting: Settings) -> None:
|
||||||
|
"""
|
||||||
|
配置
|
||||||
|
:param setting:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
停止
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.is_started = False
|
473
core/edge_context.py
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
import asyncio
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
import re
|
||||||
|
from typing import Type, Dict, Optional, Any, List
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from httpx import delete
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from uvicorn import Server, Config
|
||||||
|
|
||||||
|
from core.edge_component import EdgeComponent, service
|
||||||
|
from core.config import settings
|
||||||
|
from core.edge_internal import Database, Scheduler
|
||||||
|
from core.edge_logger import LoggingMixin
|
||||||
|
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
|
from core.edge_routes import setup_routes
|
||||||
|
from core.edge_task import EdgeTask, EdgeTaskStep
|
||||||
|
from core.edge_util import camel_to_snake
|
||||||
|
|
||||||
|
MQTT_TELEMETRY_TOPIC = "v1/devices/me/telemetry"
|
||||||
|
MQTT_RPC_TOPIC_PREFIX = "v1/devices/me/rpc/request"
|
||||||
|
MQTT_RPC_TOPIC_RESP_PREFIX = "v1/devices/me/rpc/response"
|
||||||
|
MQTT_RPC_TOPIC = f"{MQTT_RPC_TOPIC_PREFIX}/+"
|
||||||
|
|
||||||
|
|
||||||
|
class EdgeContext(LoggingMixin):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
# 初始化mqtt客户端
|
||||||
|
self.client = mqtt.Client()
|
||||||
|
self.client.on_connect = self.on_connect
|
||||||
|
self.client.on_disconnect = self.on_disconnect
|
||||||
|
self.client.on_message = self.on_message
|
||||||
|
# 搜索边缘组件并实例化
|
||||||
|
self.components: Dict[str, EdgeComponent] = self._load_components()
|
||||||
|
self._init_internal_components()
|
||||||
|
self._init_components()
|
||||||
|
self.logger.info(f"当前边缘组件:{','.join(self.components.keys())}")
|
||||||
|
# loop
|
||||||
|
self.loop = None
|
||||||
|
# tcp server
|
||||||
|
self.tcp_server = None
|
||||||
|
self.tcp_clients = set() # 保存客户端连接
|
||||||
|
# rest
|
||||||
|
rest_port = settings.get("rest.port", 8000)
|
||||||
|
self.app = FastAPI()
|
||||||
|
# 挂载静态文件目录
|
||||||
|
local_dir = settings.get('rest.static', 'local')
|
||||||
|
os.makedirs(local_dir, exist_ok=True)
|
||||||
|
self.app.mount("/local", StaticFiles(directory=local_dir), name="local")
|
||||||
|
self.config = Config(app=self.app, host="0.0.0.0", port=rest_port)
|
||||||
|
self.server = Server(self.config)
|
||||||
|
setup_routes(self, self.app)
|
||||||
|
|
||||||
|
def _load_components(self) -> Dict[str, EdgeComponent]:
|
||||||
|
package_name = 'components'
|
||||||
|
components = {}
|
||||||
|
# 动态导入 components 目录下的所有模块
|
||||||
|
package = importlib.import_module(package_name)
|
||||||
|
for _, module_name, _ in pkgutil.iter_modules(package.__path__, package_name + "."):
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
# 查找模块中的所有 EdgeComponent 子类
|
||||||
|
for name, cls in inspect.getmembers(module, inspect.isclass):
|
||||||
|
if issubclass(cls, EdgeComponent) and cls is not EdgeComponent:
|
||||||
|
# 确保是非抽象类且没有子类
|
||||||
|
if not inspect.isabstract(cls) and not cls.__subclasses__() and hasattr(cls, 'component_name'):
|
||||||
|
instance = cls(self)
|
||||||
|
components[cls.component_name] = instance
|
||||||
|
|
||||||
|
return components
|
||||||
|
|
||||||
|
def _init_internal_components(self):
|
||||||
|
database = Database(self)
|
||||||
|
scheduler = Scheduler(self)
|
||||||
|
database.configure(settings)
|
||||||
|
scheduler.configure(settings)
|
||||||
|
database.start()
|
||||||
|
scheduler.start()
|
||||||
|
self.components[database.__class__.component_name] = database
|
||||||
|
self.components[scheduler.__class__.component_name] = scheduler
|
||||||
|
|
||||||
|
def _init_components(self):
|
||||||
|
for componentKey in self.components.keys():
|
||||||
|
if componentKey in [Database.component_name, Scheduler.component_name]:
|
||||||
|
continue
|
||||||
|
component = self.components[componentKey]
|
||||||
|
component.configure(settings)
|
||||||
|
if component.__class__.component_auto_start:
|
||||||
|
try:
|
||||||
|
if not component.is_started:
|
||||||
|
component.start()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"{componentKey}启动失败", e)
|
||||||
|
|
||||||
|
def _get_non_abstract_subclasses(self, cls: Type[EdgeComponent]) -> Dict[str, EdgeComponent]:
|
||||||
|
subclasses = set(cls.__subclasses__())
|
||||||
|
non_abstract_subclasses = {}
|
||||||
|
|
||||||
|
for subclass in subclasses:
|
||||||
|
if not inspect.isabstract(subclass) and not subclass.__subclasses__() and hasattr(subclass, 'component_name'):
|
||||||
|
instance = subclass()
|
||||||
|
instance.configure(settings)
|
||||||
|
if subclass.component_auto_start:
|
||||||
|
instance.start()
|
||||||
|
non_abstract_subclasses[subclass.component_name] = instance
|
||||||
|
else:
|
||||||
|
non_abstract_subclasses.update(self._get_non_abstract_subclasses(subclass))
|
||||||
|
|
||||||
|
return non_abstract_subclasses
|
||||||
|
|
||||||
|
async def execute(self, func_name, *args, **kwargs):
|
||||||
|
func = getattr(self, func_name, None)
|
||||||
|
if func is None:
|
||||||
|
raise RuntimeError(f"Function {func_name} not found in {self.__class__.__name__}")
|
||||||
|
|
||||||
|
if hasattr(func, '_component_service') and func._component_service:
|
||||||
|
if callable(func):
|
||||||
|
method_signature = inspect.signature(func)
|
||||||
|
method_params = method_signature.parameters
|
||||||
|
call_args = []
|
||||||
|
for param_name, param_info in method_params.items():
|
||||||
|
if param_name == 'self' or param_info.kind == 4:
|
||||||
|
continue
|
||||||
|
param_type = param_info.annotation
|
||||||
|
if param_name in kwargs:
|
||||||
|
# 尝试将参数值转换为指定类型
|
||||||
|
try:
|
||||||
|
if param_type is inspect.Parameter.empty or param_type is Any:
|
||||||
|
# 如果参数类型未注明或为 Any 类型,则不进行强制类型转换
|
||||||
|
call_args.append(kwargs[param_name])
|
||||||
|
else:
|
||||||
|
# 否则,尝试将参数值转换为指定类型
|
||||||
|
call_args.append(param_type(kwargs[param_name]))
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
raise ValueError(f"Invalid value '{kwargs[param_name]}' for parameter '{param_name}': {e}")
|
||||||
|
elif param_info.default is inspect.Parameter.empty:
|
||||||
|
# 如果参数没有默认值且未在 kwargs 中指定,则抛出异常
|
||||||
|
raise ValueError(f"Missing required parameter '{param_name}' for method '{func_name}'.")
|
||||||
|
else:
|
||||||
|
# 否则,使用参数的默认值
|
||||||
|
call_args.append(param_info.default)
|
||||||
|
|
||||||
|
func_kwargs = {key: value for key, value in kwargs.items() if
|
||||||
|
key not in [param_name for param_name, _ in method_params.items()]}
|
||||||
|
if inspect.iscoroutinefunction(func):
|
||||||
|
return await func(*tuple(call_args), **func_kwargs)
|
||||||
|
else:
|
||||||
|
return await asyncio.to_thread(func, *tuple(call_args), **func_kwargs)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Function {func.__name__} not found in {self.__class__.__name__}")
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Function {func.__name__} does not have the required annotation.")
|
||||||
|
|
||||||
|
def get_component(self, name: str) -> EdgeComponent:
|
||||||
|
component_instance = self.components.get(name)
|
||||||
|
if component_instance:
|
||||||
|
return component_instance
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Component {name} not found")
|
||||||
|
|
||||||
|
async def handle_message(self, client, userdata, message):
|
||||||
|
self.logger.info(f"Receive: topic={message.topic} content={message.payload}")
|
||||||
|
# 'v1/devices/me/rpc/request/0'
|
||||||
|
topic = message.topic
|
||||||
|
if topic.startswith(MQTT_RPC_TOPIC_PREFIX):
|
||||||
|
rpc_id = topic.rsplit('/', 1)[-1]
|
||||||
|
payload = json.loads(message.payload)
|
||||||
|
response_topic = f"{MQTT_RPC_TOPIC_RESP_PREFIX}/{rpc_id}"
|
||||||
|
|
||||||
|
requestId = payload["requestId"]
|
||||||
|
response = {
|
||||||
|
"requestId": requestId,
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
}
|
||||||
|
reqeust_type = payload["type"]
|
||||||
|
if not reqeust_type in ['service', 'task']:
|
||||||
|
response['type'] = reqeust_type
|
||||||
|
response['code'] = -1
|
||||||
|
response['message'] = 'invalid reqeust type!'
|
||||||
|
else:
|
||||||
|
if reqeust_type == 'service':
|
||||||
|
method = payload.get('method')
|
||||||
|
component = payload.get('component')
|
||||||
|
method = camel_to_snake(method)
|
||||||
|
if payload.get("method") is None:
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": -1,
|
||||||
|
"message": "没有method参数!"}
|
||||||
|
else:
|
||||||
|
if component == "" or component is None:
|
||||||
|
executor = self
|
||||||
|
else:
|
||||||
|
executor = self.get_component(component)
|
||||||
|
if executor:
|
||||||
|
try:
|
||||||
|
kwargs = payload.get("params")
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = {}
|
||||||
|
result = await executor.execute(method, **kwargs)
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": 0,
|
||||||
|
"message": "success", "result": result}
|
||||||
|
except Exception as e:
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": -1, "message": str(e)}
|
||||||
|
else:
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": -2,
|
||||||
|
"message": f"Executor {component} not found"}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
kwargs = payload.get("params")
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = {}
|
||||||
|
result = await self.execute_task(**kwargs)
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": 0, "message": "success",
|
||||||
|
"result": result}
|
||||||
|
except Exception as e:
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": -1, "message": str(e)}
|
||||||
|
|
||||||
|
self.client.publish(response_topic, json.dumps(response))
|
||||||
|
|
||||||
|
def on_connect(self, client, userdata, flags, rc):
|
||||||
|
if rc == 0:
|
||||||
|
self.logger.info("Connected to MQTT Broker")
|
||||||
|
client.subscribe(MQTT_RPC_TOPIC)
|
||||||
|
else:
|
||||||
|
self.logger.info("Failed to connect, return code %d\n", rc)
|
||||||
|
|
||||||
|
def on_disconnect(self, client, userdata, rc):
|
||||||
|
self.logger.info("MQTT disconnect, return code %d\n", rc)
|
||||||
|
|
||||||
|
def on_message(self, client, userdata, message):
|
||||||
|
asyncio.run(self.handle_message(client, userdata, message))
|
||||||
|
|
||||||
|
def _stop_components(self):
|
||||||
|
for componentKey in self.components.keys():
|
||||||
|
component = self.components[componentKey]
|
||||||
|
component.configure(settings)
|
||||||
|
if component.__class__.component_auto_start:
|
||||||
|
try:
|
||||||
|
if component.is_started:
|
||||||
|
component.stop()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"{componentKey}停止失败", e)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
# mqtt
|
||||||
|
mqtt_server = settings.get("mqtt.server", "127.0.0.1")
|
||||||
|
mqtt_port = settings.get("mqtt.port", 1883)
|
||||||
|
mqtt_username = settings.get("mqtt.username", "")
|
||||||
|
mqtt_password = settings.get("mqtt.password", "")
|
||||||
|
self.logger.info(f"mqtt: {mqtt_server}:{mqtt_port}:{mqtt_username}")
|
||||||
|
self.client.username_pw_set(mqtt_username, mqtt_password)
|
||||||
|
self.client.connect(mqtt_server, mqtt_port, 60)
|
||||||
|
# self.client.loop_start()
|
||||||
|
# 在单独的事件循环中启动 TCP 服务器和 FastAPI 服务器
|
||||||
|
self.loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(self.loop)
|
||||||
|
tasks = []
|
||||||
|
# tcp server
|
||||||
|
tcp_enable = settings.get("tcp.enable", False)
|
||||||
|
if tcp_enable:
|
||||||
|
tasks.append(self.loop.create_task(self.start_tcp_server()))
|
||||||
|
# 启动 FastAPI 服务器
|
||||||
|
tasks.append(self.loop.create_task(self.server.serve()))
|
||||||
|
self.logger.info('edge context start!')
|
||||||
|
self.loop.run_until_complete(asyncio.gather(*tasks))
|
||||||
|
# 保持事件循环的持续运行
|
||||||
|
self.loop.run_forever()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._stop_components()
|
||||||
|
|
||||||
|
self.client.loop_stop()
|
||||||
|
self.client.disconnect()
|
||||||
|
|
||||||
|
# rest
|
||||||
|
# 停止 FastAPI 服务器
|
||||||
|
asyncio.run(self.stop_rest_server())
|
||||||
|
# tcp server
|
||||||
|
tcp_enable = settings.get("tcp.enable", False)
|
||||||
|
if tcp_enable:
|
||||||
|
asyncio.run(self.stop_tcp_server())
|
||||||
|
|
||||||
|
if self.loop:
|
||||||
|
self.loop.stop()
|
||||||
|
# 确保事件循环停止后再关闭
|
||||||
|
if not self.loop.is_running():
|
||||||
|
self.loop.close()
|
||||||
|
|
||||||
|
self.logger.info("Edge context stopped!")
|
||||||
|
|
||||||
|
async def handle_tcp(self, reader, writer):
|
||||||
|
addr = writer.get_extra_info('peername')
|
||||||
|
self.logger.info(f"Connected by {addr}")
|
||||||
|
self.tcp_clients.add(writer) # 添加客户端连接
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await reader.read(4096)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
|
||||||
|
requestId = None
|
||||||
|
try:
|
||||||
|
req = json.loads(data.decode('utf-8'))
|
||||||
|
self.logger.info(f"Received JSON request: {req}")
|
||||||
|
requestId = req["requestId"]
|
||||||
|
response = {
|
||||||
|
"requestId": requestId,
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
}
|
||||||
|
reqeust_type = req["type"]
|
||||||
|
if not reqeust_type in ['service', 'task']:
|
||||||
|
response['type'] = reqeust_type
|
||||||
|
response['code'] = -1
|
||||||
|
response['message'] = 'invalid reqeust type!'
|
||||||
|
else:
|
||||||
|
if reqeust_type == 'service':
|
||||||
|
method = req.get('method')
|
||||||
|
component = req.get('component')
|
||||||
|
method = camel_to_snake(method)
|
||||||
|
if req.get("method") is None:
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": -1,
|
||||||
|
"message": "没有method参数!"}
|
||||||
|
else:
|
||||||
|
if component == "" or component is None:
|
||||||
|
executor = self
|
||||||
|
else:
|
||||||
|
executor = self.get_component(component)
|
||||||
|
if executor:
|
||||||
|
try:
|
||||||
|
kwargs = req.get("params")
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = {}
|
||||||
|
result = await executor.execute(method, **kwargs)
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": 0,
|
||||||
|
"message": "success", "result": result}
|
||||||
|
except Exception as e:
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": -1,
|
||||||
|
"message": str(e)}
|
||||||
|
else:
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": -2,
|
||||||
|
"message": f"Executor {component} not found"}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
kwargs = req.get("params")
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = {}
|
||||||
|
result = await self.execute_task(**kwargs)
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": 0,
|
||||||
|
"message": "success", "result": result}
|
||||||
|
except Exception as e:
|
||||||
|
response = {"requestId": requestId, "type": reqeust_type, "code": -1, "message": str(e)}
|
||||||
|
writer.write(json.dumps(response).encode('utf-8'))
|
||||||
|
await writer.drain()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.logger.error("Received invalid JSON")
|
||||||
|
error_response = json.dumps({"requestId": requestId, "code": -9, "message": "Invalid JSON"})
|
||||||
|
writer.write(error_response.encode('utf-8'))
|
||||||
|
await writer.drain()
|
||||||
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
self.logger.error(f"Connection with {addr} cancelled.")
|
||||||
|
finally:
|
||||||
|
# 移除客户端连接
|
||||||
|
self.tcp_clients.remove(writer)
|
||||||
|
self.logger.info(f"Closing connection with {addr}")
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
|
||||||
|
async def start_tcp_server(self):
|
||||||
|
# Start the server
|
||||||
|
tcp_port = settings.get("tcp.port", 13000)
|
||||||
|
self.tcp_server = await asyncio.start_server(self.handle_tcp, '0.0.0.0', tcp_port)
|
||||||
|
addr = self.tcp_server.sockets[0].getsockname()
|
||||||
|
self.logger.info(f"Tcp Serving on {addr}")
|
||||||
|
|
||||||
|
# Create a future to keep the server running
|
||||||
|
async with self.tcp_server:
|
||||||
|
await self.tcp_server.serve_forever()
|
||||||
|
|
||||||
|
async def broadcast(self, message):
|
||||||
|
# 向所有连接的客户端发送消息
|
||||||
|
for client in self.tcp_clients:
|
||||||
|
client.write(message.encode())
|
||||||
|
await client.drain()
|
||||||
|
|
||||||
|
async def stop_tcp_server(self):
|
||||||
|
self.logger.info("Stopping tcp server...")
|
||||||
|
self.tcp_server.close() # Stop accepting new connections
|
||||||
|
await self.tcp_server.wait_closed() # Wait for the server to close existing connections
|
||||||
|
for client in self.tcp_clients:
|
||||||
|
client.close()
|
||||||
|
await client.wait_closed()
|
||||||
|
self.tcp_clients.clear()
|
||||||
|
self.logger.info("Tcp Server stopped.")
|
||||||
|
|
||||||
|
async def stop_rest_server(self):
|
||||||
|
self.logger.info("Stopping fastapi server...")
|
||||||
|
self.server.should_exit = True
|
||||||
|
# await self.server.shutdown()
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
self.logger.info("Fastapi Server stopped.")
|
||||||
|
|
||||||
|
@service()
|
||||||
|
async def execute_task(self, **kwargs):
|
||||||
|
"""
|
||||||
|
执行任务
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
task = EdgeTask(**kwargs)
|
||||||
|
|
||||||
|
async def execute_step(s: EdgeTaskStep):
|
||||||
|
try:
|
||||||
|
if s.component is None or s.component == "":
|
||||||
|
component = self
|
||||||
|
else:
|
||||||
|
component = self.get_component(s.component)
|
||||||
|
res = await component.execute(s.method, **s.params)
|
||||||
|
return {"status": "success", "result": res}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
async def execute_steps(steps: List[EdgeTaskStep]):
|
||||||
|
tasks = [execute_step(sub_step) for sub_step in steps]
|
||||||
|
return await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
self.logger.info(f"执行任务{task.name}({task.id})开始")
|
||||||
|
results = []
|
||||||
|
if len(task.steps) > 0:
|
||||||
|
for step in task.steps:
|
||||||
|
if isinstance(step, list):
|
||||||
|
# 如果是 List[EdgeTaskStep],并行执行这些步骤
|
||||||
|
result = await execute_steps(step)
|
||||||
|
else:
|
||||||
|
# 否则,顺序执行单个步骤
|
||||||
|
result = await execute_step(step)
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
self.logger.info(f"执行任务{task.name}({task.id})结束")
|
||||||
|
return results
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def list_components(self):
|
||||||
|
"""
|
||||||
|
列出当前的边缘组件
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return list(self.components.keys())
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def list_component_services(self, comp=None):
|
||||||
|
"""
|
||||||
|
列出指定边缘组件的服务函数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
comp = self if comp is None else self.get_component(comp)
|
||||||
|
if comp is None:
|
||||||
|
raise RuntimeError(f"Component {comp} not found")
|
||||||
|
|
||||||
|
methods = inspect.getmembers(comp.__class__, predicate=inspect.isfunction)
|
||||||
|
services = []
|
||||||
|
for name, method in methods:
|
||||||
|
if hasattr(method, '_component_service') and method._component_service:
|
||||||
|
services.append(name)
|
||||||
|
return services
|
345
core/edge_internal.py
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import traceback
|
||||||
|
from sqlite3 import Error
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
|
||||||
|
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from apscheduler.schedulers.base import STATE_RUNNING
|
||||||
|
from dynaconf.base import Settings
|
||||||
|
|
||||||
|
from core.edge_component import EdgeComponent, action, service
|
||||||
|
from core.logging import logger
|
||||||
|
|
||||||
|
|
||||||
|
ctx = None
|
||||||
|
|
||||||
|
def execute_action(component, method, **args):
|
||||||
|
try:
|
||||||
|
comp = ctx.get_component(component)
|
||||||
|
result = asyncio.run(comp.execute(method, **args))
|
||||||
|
logger.info(result)
|
||||||
|
logger.info(f'执行定时任务【{component}.{method}】')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'执行定时任务【{component}.{method}】出错:{str(e)}')
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
def job2dict(job):
|
||||||
|
return {'id': job.id, 'name': job.name, 'trigger': str(job.trigger), 'next_run_time': job.next_run_time,
|
||||||
|
'args': job.args, 'kwargs': job.kwargs}
|
||||||
|
|
||||||
|
# 检查表是否存在
|
||||||
|
def check_table_exists(cursor, table_name):
|
||||||
|
cursor.execute(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}';")
|
||||||
|
return cursor.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
|
@action("_database", auto_start=True)
|
||||||
|
class Database(EdgeComponent):
|
||||||
|
def __init__(self, context):
|
||||||
|
super().__init__(context)
|
||||||
|
self.device_info = None
|
||||||
|
self.sql_path = None
|
||||||
|
self.db_path = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.logger.info("Database started")
|
||||||
|
self.init_db()
|
||||||
|
super().start()
|
||||||
|
|
||||||
|
def configure(self, setting: Settings) -> None:
|
||||||
|
self.db_path = setting.get("sqlite3.path")
|
||||||
|
self.sql_path = setting.get("sqlite3.sql")
|
||||||
|
self.device_info = setting.get("device")
|
||||||
|
self.logger.info(f"Database configure done.")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
super().stop()
|
||||||
|
self.logger.info("Database stopped")
|
||||||
|
|
||||||
|
# 读取SQL文件并执行建表语句
|
||||||
|
def init_db(self):
|
||||||
|
global conn, cursor
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
sql_files = sorted(os.listdir(self.sql_path))
|
||||||
|
executed_sqls = []
|
||||||
|
cursor = conn.cursor()
|
||||||
|
if check_table_exists(cursor, 'sys_system'):
|
||||||
|
self.logger.info(f"Table sys_system already exists, no need to initialize.")
|
||||||
|
cursor.execute("select file from sys_sql_history order by id")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
executed_sqls = [row[0] for row in rows]
|
||||||
|
for sql in sql_files:
|
||||||
|
if not sql in executed_sqls:
|
||||||
|
sql_file = os.path.join(self.sql_path, sql)
|
||||||
|
with open(sql_file, 'r') as file:
|
||||||
|
sql_script = file.read()
|
||||||
|
cursor.executescript(sql_script)
|
||||||
|
self.logger.info(f"Execute SQL:{sql_file}")
|
||||||
|
cursor.execute(f"INSERT INTO sys_sql_history (file) VALUES (?)", (sql,))
|
||||||
|
conn.commit()
|
||||||
|
self.logger.info(f"SQL[{sql_file}] initialized successfully.")
|
||||||
|
|
||||||
|
self.logger.info("Database initialized successfully.")
|
||||||
|
except Error as e:
|
||||||
|
self.logger.error(f"Error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 系统信息
|
||||||
|
@service()
|
||||||
|
def system_info(self):
|
||||||
|
global conn, cursor
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("select code, val from sys_system")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
self.logger.info("List system info successfully.")
|
||||||
|
sys_info = dict(rows)
|
||||||
|
if self.device_info is not None:
|
||||||
|
sys_info.update(self.device_info)
|
||||||
|
return sys_info
|
||||||
|
except Error as e:
|
||||||
|
self.logger.error(f"Error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 插入用户数据
|
||||||
|
@service()
|
||||||
|
def insert_user(self, username, password):
|
||||||
|
global conn, cursor
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
f"INSERT INTO sys_user (username, password) VALUES (?, ?)",
|
||||||
|
(username, password)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
self.logger.info("User inserted successfully.")
|
||||||
|
except Error as e:
|
||||||
|
self.logger.error(f"Error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 根据用户名删除用户
|
||||||
|
@service()
|
||||||
|
def delete_user(self, username):
|
||||||
|
global conn, cursor
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("DELETE FROM sys_user WHERE username = ?", (username,))
|
||||||
|
conn.commit()
|
||||||
|
self.logger.info(f"User {username} deleted successfully.")
|
||||||
|
except Error as e:
|
||||||
|
self.logger.error(f"Error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def list_users(self, username=None):
|
||||||
|
global conn, cursor
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
if username is None:
|
||||||
|
cursor.execute("SELECT id, username, created_at FROM sys_user order by id")
|
||||||
|
else:
|
||||||
|
cursor.execute("SELECT id, username, created_at FROM sys_user WHERE username = ?", (username,))
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
keys = ['id', 'username', 'created_at']
|
||||||
|
return [dict(zip(keys, values)) for values in rows]
|
||||||
|
except Error as e:
|
||||||
|
self.logger.error(f"Error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 根据用户名更新用户密码
|
||||||
|
@service()
|
||||||
|
def update_user(self, username, password):
|
||||||
|
global conn, cursor
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE sys_user set password = ? WHERE username = ?", (password, username,))
|
||||||
|
conn.commit()
|
||||||
|
self.logger.info(f"User {username} update successfully.")
|
||||||
|
except Error as e:
|
||||||
|
self.logger.error(f"Error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 新增任务记录
|
||||||
|
@service()
|
||||||
|
def add_task(self, name, steps, result=None, state=0, creator='admin'):
|
||||||
|
global conn, cursor
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
id = cursor.execute(
|
||||||
|
"insert into sys_task(name, creator, steps, result, state)values(?,?,?,?,?)",
|
||||||
|
(name, creator, json.dumps(steps), json.dumps(result), state)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
self.logger.info(f"Task {id} insert successfully.")
|
||||||
|
except Error as e:
|
||||||
|
self.logger.error(f"Error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 列出任务记录
|
||||||
|
@service()
|
||||||
|
def list_tasks(self, id=None):
|
||||||
|
global conn, cursor
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
if id is None:
|
||||||
|
cursor.execute("SELECT id, name, creator, steps, result, state, created_at FROM sys_task order by id "
|
||||||
|
"desc")
|
||||||
|
else:
|
||||||
|
cursor.execute("SELECT id, name, creator, steps, result, state, created_at FROM sys_task WHERE id = ?", (id,))
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
keys = ['id', 'name', 'creator', 'steps', 'result', 'state', 'created_at']
|
||||||
|
return [dict(zip(keys, values)) for values in rows]
|
||||||
|
except Error as e:
|
||||||
|
self.logger.error(f"Error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@action("_scheduler", auto_start=True)
|
||||||
|
class Scheduler(EdgeComponent):
|
||||||
|
def __init__(self, context):
|
||||||
|
super().__init__(context)
|
||||||
|
global ctx
|
||||||
|
self.scheduler = None
|
||||||
|
ctx = self.context
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
try:
|
||||||
|
self.scheduler.start()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'定时器启动失败:{str(e)}')
|
||||||
|
raise e
|
||||||
|
super().start()
|
||||||
|
self.logger.info("Scheduler started")
|
||||||
|
|
||||||
|
def configure(self, setting: Settings) -> None:
|
||||||
|
db_url = setting.get('sched.db')
|
||||||
|
job_stores = {
|
||||||
|
'default': SQLAlchemyJobStore(url=db_url)
|
||||||
|
}
|
||||||
|
executors = {
|
||||||
|
'default': ThreadPoolExecutor(setting.get('sched.thread_pool_workers', 10)),
|
||||||
|
'processpool': ProcessPoolExecutor(setting.get('sched.process_pool_workers', 3))
|
||||||
|
}
|
||||||
|
job_defaults = {
|
||||||
|
'coalesce': setting.get('sched.coalesce', False),
|
||||||
|
'max_instances': setting.get('sched.max_instances', 3)
|
||||||
|
}
|
||||||
|
self.scheduler = BackgroundScheduler(jobstores=job_stores, executors=executors, job_defaults=job_defaults, daemon=True)
|
||||||
|
self.logger.info(f"Scheduler configure done.")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.scheduler is not None and self.scheduler.state == STATE_RUNNING:
|
||||||
|
self.scheduler.shutdown()
|
||||||
|
self.logger.info("Scheduler stopped")
|
||||||
|
super().stop()
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def add_job(self, schedule_comp: str, schedule_method: str, trigger: str = 'cron',
|
||||||
|
schedule_kwargs: dict[str, Any] = None, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
设置定时任务
|
||||||
|
:param schedule_comp:
|
||||||
|
:param schedule_method:
|
||||||
|
:param trigger:
|
||||||
|
:param schedule_kwargs:
|
||||||
|
:param kwargs:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not self.scheduler.running:
|
||||||
|
raise RuntimeError('定时器未启动')
|
||||||
|
|
||||||
|
self.logger.debug(f'设置定时任务:{schedule_comp}.{schedule_method}:{json.dumps(schedule_kwargs)}')
|
||||||
|
job = self.scheduler.add_job(func=execute_action, args=[schedule_comp, schedule_method], kwargs=schedule_kwargs,
|
||||||
|
trigger=trigger, **kwargs)
|
||||||
|
return job.id
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def get_jobs(self):
|
||||||
|
if not self.scheduler.running:
|
||||||
|
raise RuntimeError('定时器未启动')
|
||||||
|
|
||||||
|
return [job2dict(job) for job in self.scheduler.get_jobs()]
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def get_job(self, job_id):
|
||||||
|
if not self.scheduler.running:
|
||||||
|
raise RuntimeError('定时器未启动')
|
||||||
|
|
||||||
|
job = self.scheduler.get_job(job_id=job_id)
|
||||||
|
return job2dict(job)
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def pause_job(self, job_id):
|
||||||
|
if not self.scheduler.running:
|
||||||
|
raise RuntimeError('定时器未启动')
|
||||||
|
|
||||||
|
job = self.scheduler.pause_job(job_id=job_id)
|
||||||
|
return job2dict(job)
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def resume_job(self, job_id):
|
||||||
|
if not self.scheduler.running:
|
||||||
|
raise RuntimeError('定时器未启动')
|
||||||
|
|
||||||
|
job = self.scheduler.resume_job(job_id=job_id)
|
||||||
|
return job2dict(job)
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def remove_job(self, job_id):
|
||||||
|
if not self.scheduler.running:
|
||||||
|
raise RuntimeError('定时器未启动')
|
||||||
|
|
||||||
|
self.scheduler.remove_job(job_id=job_id)
|
||||||
|
|
||||||
|
@service()
|
||||||
|
def remove_jobs(self):
|
||||||
|
if not self.scheduler.running:
|
||||||
|
raise RuntimeError('定时器未启动')
|
||||||
|
|
||||||
|
self.scheduler.remove_all_jobs()
|
||||||
|
|
6
core/edge_logger.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from core.logging import logger
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingMixin:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logger
|
62
core/edge_routes.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
|
from core.edge_util import camel_to_snake
|
||||||
|
from core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def setup_routes(context, app):
|
||||||
|
@app.get("/")
|
||||||
|
async def system_info():
|
||||||
|
info = context.get_component("_database").system_info()
|
||||||
|
return {"code": 0, "message": "success", "result": info}
|
||||||
|
|
||||||
|
@app.get("/components")
|
||||||
|
async def components():
|
||||||
|
return {"code": 0, "message": "success", "result": list(context.components.keys())}
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def add_process_time_header(request: Request, call_next):
|
||||||
|
if request.url.path.startswith("/service"):
|
||||||
|
req_body = await request.body()
|
||||||
|
req_body = req_body.decode("utf-8")
|
||||||
|
if req_body == '':
|
||||||
|
req_body = "{}"
|
||||||
|
req = json.loads(req_body)
|
||||||
|
req.update(request.query_params)
|
||||||
|
method = req.get('method')
|
||||||
|
component = req.get('component')
|
||||||
|
method = camel_to_snake(method)
|
||||||
|
if req.get("method") is None:
|
||||||
|
response = {"code": -1, "type": "service", "message": "没有method参数!"}
|
||||||
|
else:
|
||||||
|
if component == "" or component is None:
|
||||||
|
executor = context
|
||||||
|
else:
|
||||||
|
executor = context.get_component(component)
|
||||||
|
if executor:
|
||||||
|
try:
|
||||||
|
kwargs = {key: value for key, value in req.items() if key not in ["method", "component"]}
|
||||||
|
result = await executor.execute(method, **kwargs)
|
||||||
|
response = {"code": 0, "type": "service", "message": "success", "result": result}
|
||||||
|
except Exception as e:
|
||||||
|
response = {"code": -1, "type": "service", "message": str(e)}
|
||||||
|
else:
|
||||||
|
response = {"code": -2, "type": "service", "message": f"Executor {component} not found"}
|
||||||
|
return JSONResponse(response)
|
||||||
|
elif request.url.path.startswith("/task"):
|
||||||
|
req_body = await request.body()
|
||||||
|
req_body = req_body.decode("utf-8")
|
||||||
|
if req_body == '':
|
||||||
|
req_body = "{}"
|
||||||
|
req = json.loads(req_body)
|
||||||
|
try:
|
||||||
|
result = await context.execute_task(**req)
|
||||||
|
response = {"code": 0, "type": "task", "message": "success", "result": result}
|
||||||
|
except Exception as e:
|
||||||
|
response = {"code": -1, "type": "task", "message": str(e)}
|
||||||
|
return JSONResponse(response)
|
||||||
|
else:
|
||||||
|
return await call_next(request)
|
17
core/edge_task.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Union, Dict, Any, List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class EdgeTaskStep(BaseModel):
|
||||||
|
component: Optional[str]
|
||||||
|
method: str
|
||||||
|
params: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class EdgeTask(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
creator: str
|
||||||
|
createTime: datetime
|
||||||
|
steps: List[Union[EdgeTaskStep, List[EdgeTaskStep]]]
|
9
core/edge_util.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def camel_to_snake(name):
|
||||||
|
# 匹配大写字母并在前面添加下划线,然后将结果转换为小写
|
||||||
|
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||||
|
# 处理驼峰形式中的连续大写字母
|
||||||
|
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
|
||||||
|
return s2.lower()
|
28
core/logging.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from core.config import settings
|
||||||
|
|
||||||
|
def setup_logger():
|
||||||
|
log_dir = settings.logging.directory
|
||||||
|
log_level = settings.logging.level
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
# 创建并配置全局 logger
|
||||||
|
logger_name = 'app'
|
||||||
|
_logger = logging.getLogger('app')
|
||||||
|
if len(_logger.handlers) == 0:
|
||||||
|
handler = logging.FileHandler(os.path.join(log_dir, f"{logger_name}.log"), encoding='utf-8')
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
_logger.addHandler(handler)
|
||||||
|
if settings.get("logging.console", False):
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
_logger.addHandler(console_handler)
|
||||||
|
_logger.setLevel(getattr(logging, log_level.upper(), logging.DEBUG))
|
||||||
|
|
||||||
|
# 关闭日志传播,防止重复处理
|
||||||
|
_logger.propagate = False
|
||||||
|
return _logger
|
||||||
|
|
||||||
|
# 程序启动时调用一次 logger 配置
|
||||||
|
logger = setup_logger()
|
0
device/__init__.py
Normal file
67
device/mv/ImageConvert.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding: utf-8
|
||||||
|
'''
|
||||||
|
Created on 2017-10-26
|
||||||
|
|
||||||
|
@author:
|
||||||
|
'''
|
||||||
|
from ctypes import *
|
||||||
|
|
||||||
|
# 加载ImageConvert库
|
||||||
|
ImageConvertdll = None
|
||||||
|
try:
|
||||||
|
ImageConvertdll = cdll.LoadLibrary("/opt/HuarayTech/MVviewer/lib/libImageConvert.so")
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
#定义枚举类型
|
||||||
|
def enum(**enums):
|
||||||
|
return type('Enum', (), enums)
|
||||||
|
|
||||||
|
# ImageConvert.h => enum tagIMGCNV_EErr
|
||||||
|
IMGCNV_EErr = enum(
|
||||||
|
IMGCNV_SUCCESS = 0,
|
||||||
|
IMGCNV_ILLEGAL_PARAM = 1,
|
||||||
|
IMGCNV_ERR_ORDER = 2,
|
||||||
|
IMGCNV_NO_MEMORY = 3,
|
||||||
|
IMGCNV_NOT_SUPPORT = 4,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ImageConvert.h => struct tagIMGCNV_SOpenParam
|
||||||
|
class IMGCNV_SOpenParam(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('width', c_int),
|
||||||
|
('height', c_int),
|
||||||
|
('paddingX', c_int),
|
||||||
|
('paddingY', c_int),
|
||||||
|
('dataSize', c_int),
|
||||||
|
('pixelForamt', c_uint),
|
||||||
|
]
|
||||||
|
# ImageConvert.h => enum tagIMGCNV_EBayerDemosaic
|
||||||
|
IMGCNV_EErr = enum(
|
||||||
|
IMGCNV_DEMOSAIC_NEAREST_NEIGHBOR = 0,
|
||||||
|
IMGCNV_DEMOSAIC_BILINEAR = 1,
|
||||||
|
IMGCNV_DEMOSAIC_EDGE_SENSING = 2,
|
||||||
|
IMGCNV_DEMOSAIC_NOT_SUPPORT = 255,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ImageConvert.h => IMGCNV_ConvertToBGR24(unsigned char* pSrcData, IMGCNV_SOpenParam* pOpenParam, unsigned char* pDstData, int* pDstDataSize)
|
||||||
|
IMGCNV_ConvertToBGR24 = None if ImageConvertdll is None else ImageConvertdll.IMGCNV_ConvertToBGR24
|
||||||
|
|
||||||
|
# ImageConvert.h => IMGCNV_ConvertToRGB24(unsigned char* pSrcData, IMGCNV_SOpenParam* pOpenParam, unsigned char* pDstData, int* pDstDataSize)
|
||||||
|
IMGCNV_ConvertToRGB24 = None if ImageConvertdll is None else ImageConvertdll.IMGCNV_ConvertToRGB24
|
||||||
|
|
||||||
|
# ImageConvert.h => IMGCNV_ConvertToMono8(unsigned char* pSrcData, IMGCNV_SOpenParam* pOpenParam, unsigned char* pDstData, int* pDstDataSize)
|
||||||
|
IMGCNV_ConvertToMono8 = None if ImageConvertdll is None else ImageConvertdll.IMGCNV_ConvertToMono8
|
||||||
|
|
||||||
|
#ImageConvert.h => IMGCNV_ConvertToBGR24_Ex(unsigned char* pSrcData, IMGCNV_SOpenParam* pOpenParam, unsigned char* pDstData, int* pDstDataSize, IMGCNV_EBayerDemosaic eBayerDemosaic)
|
||||||
|
IMGCNV_ConvertToBGR24_Ex = None if ImageConvertdll is None else ImageConvertdll.IMGCNV_ConvertToBGR24_Ex
|
||||||
|
|
||||||
|
#ImageConvert.h => IMGCNV_ConvertToRGB24_Ex(unsigned char* pSrcData, IMGCNV_SOpenParam* pOpenParam, unsigned char* pDstData, int* pDstDataSize, IMGCNV_EBayerDemosaic eBayerDemosaic)
|
||||||
|
IMGCNV_ConvertToRGB24_Ex = None if ImageConvertdll is None else ImageConvertdll.IMGCNV_ConvertToRGB24_Ex
|
||||||
|
|
||||||
|
#ImageConvert.h => CALLMETHOD IMGCNV_ConvertToMono8_Ex(unsigned char* pSrcData, IMGCNV_SOpenParam* pOpenParam, unsigned char* pDstData, int* pDstDataSize, IMGCNV_EBayerDemosaic eBayerDemosaic)
|
||||||
|
IMGCNV_ConvertToMono8_Ex = None if ImageConvertdll is None else ImageConvertdll.IMGCNV_ConvertToMono8_Ex
|
||||||
|
|
||||||
|
#ImageConvert.h => IMGCNV_ConvertToBGRA32_Ex(unsigned char* pSrcData, IMGCNV_SOpenParam* pOpenParam, unsigned char* pDstData, int* pDstDataSize, IMGCNV_EBayerDemosaic eBayerDemosaic)
|
||||||
|
IMGCNV_ConvertToBGRA32_Ex = None if ImageConvertdll is None else ImageConvertdll.IMGCNV_ConvertToBGRA32_Ex
|
1309
device/mv/MVSDK.py
Normal file
0
device/mv/__init__.py
Normal file
177
main.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QDate, QThread
|
||||||
|
from PyQt5.QtGui import QPixmap, QFontDatabase, QFont, QPainter, \
|
||||||
|
QLinearGradient, QColor
|
||||||
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QStackedWidget, \
|
||||||
|
QFrame, QSpacerItem, QSizePolicy, QProgressBar
|
||||||
|
|
||||||
|
from core.edge_context import EdgeContext
|
||||||
|
from core.logging import logger
|
||||||
|
from core.context import AppContext
|
||||||
|
from util import load_stylesheet
|
||||||
|
from widget.device import DeviceWidget
|
||||||
|
from widget.task_list import TaskListWidget
|
||||||
|
|
||||||
|
class DetectWindow(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.ratio = AppContext.get_ratio()
|
||||||
|
# 电池状态
|
||||||
|
self.battery = 40
|
||||||
|
self.battery_state = 0
|
||||||
|
|
||||||
|
# 设置主窗口标题
|
||||||
|
self.setWindowTitle('预埋件检测系统')
|
||||||
|
|
||||||
|
# 创建主窗口的中央Widget
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
# self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint) # 去掉窗口边框
|
||||||
|
|
||||||
|
# 创建主布局
|
||||||
|
main_layout = QVBoxLayout(central_widget)
|
||||||
|
main_layout.setContentsMargins(0, 0, 0, 0) # 设置边距为零
|
||||||
|
main_layout.setSpacing(0) # 设置间隔为零
|
||||||
|
# ---------- 顶部标题栏 ----------
|
||||||
|
header_widget = QWidget()
|
||||||
|
header_widget.setObjectName("headerWidget")
|
||||||
|
header_widget.setFixedHeight(int(self.ratio * 64))
|
||||||
|
header_layout = QHBoxLayout(header_widget)
|
||||||
|
header_layout.setContentsMargins(int(self.ratio * 20), 0, int(self.ratio * 20), 0)
|
||||||
|
header_layout.setSpacing(0)
|
||||||
|
# 左侧标题
|
||||||
|
header_left_widget = QWidget()
|
||||||
|
header_left_layout = QHBoxLayout(header_left_widget)
|
||||||
|
header_left_layout.setContentsMargins(0, 0, 0, 0) # 设置边距为零
|
||||||
|
header_left_layout.setSpacing(0) # 设置间隔为零
|
||||||
|
# logo
|
||||||
|
logo_label = QLabel()
|
||||||
|
pixmap = QPixmap("assets/logo.png")
|
||||||
|
scaled_pixmap = pixmap.scaled(int(self.ratio * 64) - 4, int(self.ratio * 64) - 4, True)
|
||||||
|
logo_label.setPixmap(scaled_pixmap)
|
||||||
|
header_left_layout.addWidget(logo_label)
|
||||||
|
header_left_layout.addSpacing(int(self.ratio * 5))
|
||||||
|
# title
|
||||||
|
self.title_label = QLabel("预埋件检测系统")
|
||||||
|
self.title_label.setStyleSheet(f"color: white;font-size: {int(self.ratio * 24)}px;")
|
||||||
|
header_left_layout.addWidget(self.title_label)
|
||||||
|
header_layout.addWidget(header_left_widget, alignment=Qt.AlignLeft)
|
||||||
|
# 右侧日期和电量
|
||||||
|
header_right_widget = QWidget()
|
||||||
|
header_right_layout = QHBoxLayout(header_right_widget)
|
||||||
|
self.date_label = QLabel(QDate.currentDate().toString('yyyy年MM月dd日'))
|
||||||
|
self.date_label.setStyleSheet(f"color: #898AC5;font-size: {int(self.ratio * 14)}px;")
|
||||||
|
header_right_layout.addWidget(self.date_label)
|
||||||
|
header_right_layout.addSpacing(int(self.ratio * 10))
|
||||||
|
# 充电图标
|
||||||
|
self.chargeIcon = QLabel(self)
|
||||||
|
self.chargeIcon.setPixmap(QPixmap("assets/icon_charge_finish.png").scaled(36, 36))
|
||||||
|
self.chargeIcon.setVisible(False)
|
||||||
|
# 电量
|
||||||
|
self.batteryBar = QProgressBar(self)
|
||||||
|
self.batteryBar.setMinimum(0)
|
||||||
|
self.batteryBar.setMaximum(100)
|
||||||
|
self.batteryBar.setValue(self.battery)
|
||||||
|
self.batteryBar.setFixedHeight(int(self.ratio * 24))
|
||||||
|
self.batteryBar.setFixedWidth(int(self.ratio * 64))
|
||||||
|
# 设置电池显示样式
|
||||||
|
self.batteryBar.setAlignment(Qt.AlignCenter)
|
||||||
|
self.batteryBar.setFormat("%p%")
|
||||||
|
self.batteryBar.setStyleSheet("QProgressBar::chunk { background-color: #1afa29; }")
|
||||||
|
header_right_layout.addWidget(self.chargeIcon)
|
||||||
|
header_right_layout.addWidget(self.batteryBar)
|
||||||
|
header_layout.addWidget(header_right_widget, alignment=Qt.AlignRight)
|
||||||
|
# 添加标题栏
|
||||||
|
main_layout.addWidget(header_widget)
|
||||||
|
|
||||||
|
# 任务列表
|
||||||
|
task_list_content = TaskListWidget()
|
||||||
|
task_list_content.setObjectName("taskListWidget")
|
||||||
|
task_list_content.setStyleSheet("background-color: #161B3B")
|
||||||
|
main_layout.addWidget(task_list_content)
|
||||||
|
|
||||||
|
# 信号
|
||||||
|
self.init_signals()
|
||||||
|
self.battery_change(self.battery)
|
||||||
|
|
||||||
|
# 设置窗口默认全屏
|
||||||
|
self.resize(1024, 768)
|
||||||
|
# self.showFullScreen()
|
||||||
|
#
|
||||||
|
def init_signals(self):
|
||||||
|
ups = AppContext.get_edge_context().get_component('ups')
|
||||||
|
ups.signals.battery_change.connect(self.battery_change)
|
||||||
|
ups.signals.state_change.connect(self.battery_state_change)
|
||||||
|
#
|
||||||
|
def battery_change(self, battery):
|
||||||
|
"""更新电池电量"""
|
||||||
|
self.battery = battery
|
||||||
|
self.batteryBar.setValue(battery)
|
||||||
|
if self.battery < 20:
|
||||||
|
self.batteryBar.setStyleSheet("QProgressBar::chunk { background-color: red; }")
|
||||||
|
elif self.battery < 50:
|
||||||
|
self.batteryBar.setStyleSheet("QProgressBar::chunk { background-color: yellow; }")
|
||||||
|
else:
|
||||||
|
self.batteryBar.setStyleSheet("QProgressBar::chunk { background-color: #1afa29; }")
|
||||||
|
|
||||||
|
def battery_state_change(self, battery_state):
|
||||||
|
"""更新充电状态"""
|
||||||
|
self.battery_state = battery_state
|
||||||
|
if self.battery_state & 0x08 == 0x08:
|
||||||
|
self.chargeIcon.setPixmap(QPixmap("assets/icon_charging.png").scaled(36, 36))
|
||||||
|
self.chargeIcon.setVisible(True)
|
||||||
|
elif self.battery_state & 0x04 == 0x04:
|
||||||
|
self.chargeIcon.setPixmap(QPixmap("assets/icon_charge_finish.png").scaled(36, 36))
|
||||||
|
self.chargeIcon.setVisible(True)
|
||||||
|
else:
|
||||||
|
self.chargeIcon.setVisible(False)
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
super().showEvent(event)
|
||||||
|
|
||||||
|
def on_quit():
|
||||||
|
AppContext.get_edge_context().stop()
|
||||||
|
edge_worker.quit()
|
||||||
|
logger.info("Application quit.")
|
||||||
|
|
||||||
|
|
||||||
|
class EdgeContextWorker(QThread):
|
||||||
|
def __init__(self, context):
|
||||||
|
super().__init__()
|
||||||
|
self.edge_context = context
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.edge_context.start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.aboutToQuit.connect(on_quit)
|
||||||
|
|
||||||
|
screen = app.primaryScreen()
|
||||||
|
dpi = screen.logicalDotsPerInch()
|
||||||
|
AppContext.set_ratio(dpi / 96.0)
|
||||||
|
|
||||||
|
stylesheet = load_stylesheet("styles/global.qss")
|
||||||
|
app.setStyleSheet(stylesheet)
|
||||||
|
|
||||||
|
# 加载自定义字体
|
||||||
|
font_id = QFontDatabase.addApplicationFont("assets/fonts/NotoSansSC-Regular.otf")
|
||||||
|
if font_id != -1:
|
||||||
|
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
|
||||||
|
custom_font = QFont(font_family, 14) # 使用字体族名创建字体,并设置大小
|
||||||
|
app.setFont(custom_font) # 设置全局字体
|
||||||
|
|
||||||
|
# 启动 EdgeContext 服务
|
||||||
|
edge_context = EdgeContext()
|
||||||
|
AppContext.set_edge_context(edge_context)
|
||||||
|
edge_worker = EdgeContextWorker(edge_context)
|
||||||
|
edge_worker.start()
|
||||||
|
|
||||||
|
window = DetectWindow()
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
0
model/__init__.py
Normal file
6
model/base_model.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class DataModel(QObject):
|
||||||
|
# 定义一个信号,当数据变化时发射该信号
|
||||||
|
property_changed = pyqtSignal(str, object) # 接受属性名称和新值
|
11
req.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
PyOpenGL==3.1.1a1
|
||||||
|
numpy==1.26.4
|
||||||
|
openpyxl==3.1.5
|
||||||
|
pandas==2.0.3
|
||||||
|
pyserial==3.5
|
||||||
|
APScheduler==3.10.4
|
||||||
|
paho-mqtt==2.1.0
|
||||||
|
dynaconf==3.2.5
|
||||||
|
fastapi==0.111.0
|
||||||
|
uvicorn==0.30.1
|
||||||
|
SQLAlchemy==2.0.34
|
38
sql/1.0.0__init.sql
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
-- sql history
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_sql_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
file TEXT NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT (DATETIME(CURRENT_TIMESTAMP, 'localtime'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- system
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_system (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
val TEXT NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT (DATETIME(CURRENT_TIMESTAMP, 'localtime'))
|
||||||
|
);
|
||||||
|
insert into sys_system(code, val) values ('software_version', '1.0.0');
|
||||||
|
insert into sys_system(code, val) values ('firmware_version', '1.0.0');
|
||||||
|
insert into sys_system(code, val) values ('algorithm_version', '1.0.0');
|
||||||
|
insert into sys_system(code, val) values ('model_version', '1.0.0');
|
||||||
|
|
||||||
|
-- user
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_user (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT (DATETIME(CURRENT_TIMESTAMP, 'localtime'))
|
||||||
|
);
|
||||||
|
insert into sys_user(username, password) values ('admin', 'winner!');
|
||||||
|
|
||||||
|
-- task
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_task (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
creator TEXT NULL,
|
||||||
|
steps TEXT NOT NULL,
|
||||||
|
result TEXT NULL,
|
||||||
|
state INTEGER default 0,
|
||||||
|
created_at TIMESTAMP DEFAULT (DATETIME(CURRENT_TIMESTAMP, 'localtime'))
|
||||||
|
);
|
34
sql/1.0.1__add_dat_task_tables.sql
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
-- task
|
||||||
|
CREATE TABLE IF NOT EXISTS dat_task
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
device_sn TEXT NOT NULL,
|
||||||
|
param_json TEXT NOT NULL,
|
||||||
|
result_json TEXT NULL,
|
||||||
|
state INTEGER DEFAULT 0,
|
||||||
|
start_time DATETIME NULL,
|
||||||
|
end_time DATETIME NULL,
|
||||||
|
create_time DATETIME NULL,
|
||||||
|
update_time DATETIME NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- task data
|
||||||
|
CREATE TABLE IF NOT EXISTS dat_task_progress
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
device_sn TEXT NOT NULL,
|
||||||
|
task_data_json TEXT NOT NULL,
|
||||||
|
create_time TIMESTAMP DEFAULT (DATETIME(CURRENT_TIMESTAMP, 'localtime'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- task log
|
||||||
|
CREATE TABLE IF NOT EXISTS dat_task_log
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
device_sn TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
create_time TIMESTAMP DEFAULT (DATETIME(CURRENT_TIMESTAMP, 'localtime'))
|
||||||
|
);
|
246
styles/global.qss
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
QMainWindow {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel {
|
||||||
|
color: #D9E1E7;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0 solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuButton {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#menuWidget {
|
||||||
|
background-color: #1E244D;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#headerWidget {
|
||||||
|
background-color: #2A2F55;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskListWidget QLabel,
|
||||||
|
TaskListWidget QPlainTextEdit {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskListWidget QLabel#THeader {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
TaskListWidget QPushButton {
|
||||||
|
color: #D9E1E7;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton {
|
||||||
|
color: #D9E1E7;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #3A36DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton::disabled {
|
||||||
|
color: #999999;
|
||||||
|
background-color: rgba(58,54,219,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
QLineEdit {
|
||||||
|
color: #D9E1E7;
|
||||||
|
border: 1px solid #D9E1E7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 3 10;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#jsonWidget {
|
||||||
|
border: 1px solid #D9E1E7;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPlainTextEdit {
|
||||||
|
color: #D9E1E7;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
QAbstractScrollArea#mapArea {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #D9E1E7;
|
||||||
|
}
|
||||||
|
|
||||||
|
QAbstractScrollArea QWidget {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === QScrollBar:horizontal === */
|
||||||
|
QScrollBar:horizontal {
|
||||||
|
background: rgba(58,54,219,0.45);
|
||||||
|
height: 5px;
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:horizontal {
|
||||||
|
background: rgba(58,54,219,0.9);
|
||||||
|
min-width: 20px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::add-line:horizontal {
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::sub-line:horizontal {
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === QScrollBar:vertical === */
|
||||||
|
QScrollBar:vertical {
|
||||||
|
background: rgba(58,54,219,0.45);
|
||||||
|
width: 5px;
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical {
|
||||||
|
background: rgba(58,54,219,0.9);
|
||||||
|
min-height: 20px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::add-line:vertical {
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::sub-line:vertical {
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskRunDialog {
|
||||||
|
background-color: #161B3B;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#viewWidget {
|
||||||
|
background-color: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton#stopTaskButton {
|
||||||
|
background-color: #FF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmbedItem QLabel {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmbedItem QPushButton {
|
||||||
|
border: 1px solid #999999;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmbedItem QPushButton#STD {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background-color: #FF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#checkWidget,
|
||||||
|
QWidget#viewCheckWidget {
|
||||||
|
border: 1px dashed #6cff6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendCheckWidget {
|
||||||
|
width: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
max-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
max-height: 20px;
|
||||||
|
border: 1px dashed #6cff6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendCheckLabel {
|
||||||
|
color: #6cff6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendStdWidget {
|
||||||
|
width: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
max-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
max-height: 20px;
|
||||||
|
background-color: #FF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendStdLabel {
|
||||||
|
color: #FF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendOkWidget {
|
||||||
|
width: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
max-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
max-height: 20px;
|
||||||
|
border: 1px solid #6cff6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendOkLabel {
|
||||||
|
color: #6cff6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendNgWidget {
|
||||||
|
width: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
max-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
max-height: 20px;
|
||||||
|
border: 1px solid #FF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendNgLabel {
|
||||||
|
color: #FF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendNormalWidget {
|
||||||
|
width: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
max-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
max-height: 20px;
|
||||||
|
border: 1px solid #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#legendNormalLabel {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmbedDetail {
|
||||||
|
background-color: #161B3B;
|
||||||
|
border: 1px solid #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmbedDetail QPushButton {
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
30
test/gpio_test.bash
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 导出 GPIO 80
|
||||||
|
echo 80 > /sys/class/gpio/export
|
||||||
|
|
||||||
|
# 设置为输出
|
||||||
|
echo "out" > /sys/class/gpio/gpio80/direction
|
||||||
|
|
||||||
|
# 设置为高电平
|
||||||
|
echo "1" > /sys/class/gpio/gpio80/value
|
||||||
|
echo "GPIO 80 set to HIGH"
|
||||||
|
sleep 5 # 等待 5 秒
|
||||||
|
|
||||||
|
# 设置为低电平
|
||||||
|
echo "0" > /sys/class/gpio/gpio80/value
|
||||||
|
echo "GPIO 80 set to LOW"
|
||||||
|
sleep 5 # 等待 5 秒
|
||||||
|
|
||||||
|
# 再次设置为高电平
|
||||||
|
echo "1" > /sys/class/gpio/gpio80/value
|
||||||
|
echo "GPIO 80 set to HIGH again"
|
||||||
|
sleep 5 # 等待 5 秒
|
||||||
|
|
||||||
|
# 最后设置为低电平
|
||||||
|
echo "0" > /sys/class/gpio/gpio80/value
|
||||||
|
echo "GPIO 80 set to LOW again"
|
||||||
|
|
||||||
|
# 释放 GPIO 80
|
||||||
|
echo 80 > /sys/class/gpio/unexport
|
||||||
|
|
100
test/point_cloud_test.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import sys
|
||||||
|
import random
|
||||||
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QOpenGLWidget
|
||||||
|
from PyQt5.QtCore import Qt, pyqtSignal, QObject
|
||||||
|
from OpenGL.GL import *
|
||||||
|
from OpenGL.GLUT import *
|
||||||
|
from OpenGL.GLU import *
|
||||||
|
|
||||||
|
class PointCloudWidget(QOpenGLWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(PointCloudWidget, self).__init__(parent)
|
||||||
|
self.point_cloud = [] # 初始化点云数据为空
|
||||||
|
self.point_size = 5 # 点的大小
|
||||||
|
|
||||||
|
def update_point_cloud(self, new_point_cloud):
|
||||||
|
"""更新点云数据并触发刷新"""
|
||||||
|
self.point_cloud = new_point_cloud
|
||||||
|
self.update() # 触发重绘
|
||||||
|
|
||||||
|
def initializeGL(self):
|
||||||
|
"""初始化OpenGL参数"""
|
||||||
|
glClearColor(0.0, 0.0, 0.0, 1.0) # 背景颜色:黑色
|
||||||
|
glEnable(GL_DEPTH_TEST) # 启用深度测试
|
||||||
|
glPointSize(self.point_size) # 设置点的大小
|
||||||
|
|
||||||
|
def paintGL(self):
|
||||||
|
"""绘制点云"""
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # 清除颜色和深度缓冲
|
||||||
|
glLoadIdentity() # 重置当前矩阵
|
||||||
|
gluLookAt(0, 0, 5, 0, 0, 0, 0, 1, 0) # 设置观察视角
|
||||||
|
|
||||||
|
glBegin(GL_POINTS) # 开始绘制点
|
||||||
|
for point in self.point_cloud:
|
||||||
|
x, y, z, r, g, b = point
|
||||||
|
glColor3f(r, g, b) # 设置点的颜色
|
||||||
|
glVertex3f(x, y, z) # 设置点的位置
|
||||||
|
glEnd() # 结束绘制
|
||||||
|
|
||||||
|
glFlush() # 刷新绘图
|
||||||
|
|
||||||
|
def resizeGL(self, w, h):
|
||||||
|
"""调整窗口大小时重置视口和投影矩阵"""
|
||||||
|
glViewport(0, 0, w, h)
|
||||||
|
glMatrixMode(GL_PROJECTION)
|
||||||
|
glLoadIdentity()
|
||||||
|
gluPerspective(45.0, w / h, 1.0, 100.0)
|
||||||
|
glMatrixMode(GL_MODELVIEW)
|
||||||
|
|
||||||
|
|
||||||
|
class PointCloudManager(QObject):
|
||||||
|
"""负责发送点云数据的管理类"""
|
||||||
|
point_cloud_signal = pyqtSignal(list) # 信号,用于发送点云数据
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def generate_and_send_point_cloud(self):
|
||||||
|
"""模拟生成点云数据并通过信号发送"""
|
||||||
|
new_point_cloud = self.generate_point_cloud(1000) # 生成1000个随机点
|
||||||
|
self.point_cloud_signal.emit(new_point_cloud) # 发射信号
|
||||||
|
|
||||||
|
def generate_point_cloud(self, num_points):
|
||||||
|
"""生成随机点云数据"""
|
||||||
|
points = []
|
||||||
|
for _ in range(num_points):
|
||||||
|
x = random.uniform(-1.0, 1.0) # X坐标
|
||||||
|
y = random.uniform(-1.0, 1.0) # Y坐标
|
||||||
|
z = random.uniform(-1.0, 1.0) # Z坐标
|
||||||
|
r = random.random() # 红色通道
|
||||||
|
g = random.random() # 绿色通道
|
||||||
|
b = random.random() # 蓝色通道
|
||||||
|
points.append((x, y, z, r, g, b))
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super(MainWindow, self).__init__()
|
||||||
|
self.setWindowTitle("PyQt5 + OpenGL 实时点云显示")
|
||||||
|
self.setGeometry(100, 100, 800, 600)
|
||||||
|
|
||||||
|
# 创建OpenGL显示组件
|
||||||
|
self.gl_widget = PointCloudWidget()
|
||||||
|
self.setCentralWidget(self.gl_widget)
|
||||||
|
|
||||||
|
# 创建点云管理器
|
||||||
|
self.point_cloud_manager = PointCloudManager()
|
||||||
|
|
||||||
|
# 将信号连接到PointCloudWidget的更新槽
|
||||||
|
self.point_cloud_manager.point_cloud_signal.connect(self.gl_widget.update_point_cloud)
|
||||||
|
|
||||||
|
# 模拟点云数据生成与发送
|
||||||
|
self.point_cloud_manager.generate_and_send_point_cloud()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = MainWindow()
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec_())
|
24
test/tcp_server_test.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
HOST = '127.0.0.1'
|
||||||
|
PORT = 13000
|
||||||
|
|
||||||
|
async def send_json_request(data):
|
||||||
|
reader, writer = await asyncio.open_connection(HOST, PORT)
|
||||||
|
|
||||||
|
# 发送JSON数据
|
||||||
|
writer.write(json.dumps(data).encode('utf-8'))
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
# 异步接收响应数据
|
||||||
|
response_data = await reader.read(1024)
|
||||||
|
print(f"Received response: {response_data.decode('utf-8')}")
|
||||||
|
|
||||||
|
# 关闭连接
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
request_data = {"requestId": "asdasdasd", "type": "service", "component": "_database", "method": "system_info"}
|
||||||
|
asyncio.run(send_json_request(request_data))
|
29
util/__init__.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import platform
|
||||||
|
from PyQt5.QtCore import QFile, QTextStream
|
||||||
|
|
||||||
|
|
||||||
|
def load_stylesheet(file_path):
|
||||||
|
"""加载 QSS 文件"""
|
||||||
|
file = QFile(file_path)
|
||||||
|
if not file.open(QFile.ReadOnly | QFile.Text):
|
||||||
|
print(f"无法打开样式表文件 {file_path}")
|
||||||
|
return ""
|
||||||
|
stream = QTextStream(file)
|
||||||
|
stylesheet = stream.readAll()
|
||||||
|
file.close()
|
||||||
|
return stylesheet
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_and_library_suffix():
|
||||||
|
system = platform.system().lower()
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
|
||||||
|
if system == 'windows':
|
||||||
|
return 'win', 'dll'
|
||||||
|
elif system == 'linux':
|
||||||
|
if 'linux_arm64' in machine or 'aarch64' in machine:
|
||||||
|
return 'linux_arm64', 'so'
|
||||||
|
else:
|
||||||
|
return 'linux', 'so'
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported system: {system}")
|
11
vendors/livox/CMakeLists.txt
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(LivoxSDKWrapper)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 11)
|
||||||
|
|
||||||
|
include_directories(/usr/local/include)
|
||||||
|
link_directories(/usr/local/lib)
|
||||||
|
|
||||||
|
add_library(livox_sdk_wrapper SHARED livox_sdk_wrapper.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(livox_sdk_wrapper livox_sdk_static pthread)
|
4
vendors/livox/README.MD
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
找到 CMakeLists.txt 中编译静态库的地方,确保添加 -fPIC 标志。可以通过以下方式添加:
|
||||||
|
```cmake
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
|
||||||
|
```
|
7
vendors/livox/build.sh
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
rm -rf build
|
||||||
|
mkdir build
|
||||||
|
cd build
|
||||||
|
cmake ..
|
||||||
|
make
|
||||||
|
cp liblivox_sdk_wrapper.so ../
|
BIN
vendors/livox/liblivox_sdk_wrapper.so
vendored
Normal file
275
vendors/livox/livox_sdk_wrapper.cpp
vendored
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "livox_sdk.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
kDeviceStateDisconnect = 0,
|
||||||
|
kDeviceStateConnect = 1,
|
||||||
|
kDeviceStateSampling = 2,
|
||||||
|
} DeviceState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t handle;
|
||||||
|
DeviceState device_state;
|
||||||
|
DeviceInfo info;
|
||||||
|
} DeviceItem;
|
||||||
|
|
||||||
|
DeviceItem devices[kMaxLidarCount];
|
||||||
|
int lidar_count = 0;
|
||||||
|
|
||||||
|
/** 回调函数声明 */
|
||||||
|
void OnLidarErrorStatusCallback(livox_status status, uint8_t handle, ErrorMessage *message);
|
||||||
|
void GetLidarData(uint8_t handle, LivoxEthPacket *data, uint32_t data_num, void *client_data);
|
||||||
|
void OnSampleCallback(livox_status status, uint8_t handle, uint8_t response, void *data);
|
||||||
|
void OnCommonCommandCallback(livox_status status, uint8_t handle, uint8_t response, void *data);
|
||||||
|
void OnStopSampleCallback(livox_status status, uint8_t handle, uint8_t response, void *data);
|
||||||
|
void OnDeviceInfoChange(const DeviceInfo *info, DeviceEvent type);
|
||||||
|
void OnDeviceBroadcast(const BroadcastDeviceInfo *info);
|
||||||
|
|
||||||
|
/** 用于Python的回调函数指针 */
|
||||||
|
void (*python_data_callback)(uint8_t, uint32_t *, uint32_t) = NULL;
|
||||||
|
void (*python_error_callback)(livox_status, uint8_t, ErrorMessage*) = NULL;
|
||||||
|
void (*python_device_change_callback)(const DeviceInfo*, DeviceEvent) = NULL;
|
||||||
|
void (*python_device_broadcast_callback)(const BroadcastDeviceInfo*) = NULL;
|
||||||
|
void (*python_common_command_callback)(livox_status, uint8_t, uint8_t) = NULL;
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
/** 初始化Livox SDK */
|
||||||
|
bool init_sdk() {
|
||||||
|
printf("Livox SDK initializing.\n");
|
||||||
|
if (!Init()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
printf("Livox SDK has been initialized.\n");
|
||||||
|
LivoxSdkVersion _sdkversion;
|
||||||
|
GetLivoxSdkVersion(&_sdkversion);
|
||||||
|
printf("Livox SDK version %d.%d.%d .\n", _sdkversion.major, _sdkversion.minor, _sdkversion.patch);
|
||||||
|
|
||||||
|
memset(devices, 0, sizeof(devices));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动设备扫描 */
|
||||||
|
bool start_discovery() {
|
||||||
|
SetBroadcastCallback(OnDeviceBroadcast);
|
||||||
|
SetDeviceStateUpdateCallback(OnDeviceInfoChange);
|
||||||
|
|
||||||
|
if (!Start()) {
|
||||||
|
printf("Failed to start device discovery.\n");
|
||||||
|
Uninit();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
printf("Started device discovery.\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止SDK并清理 */
|
||||||
|
void stop_sdk() {
|
||||||
|
printf("Stopping Livox SDK.\n");
|
||||||
|
for (int i = 0; i < kMaxLidarCount; ++i) {
|
||||||
|
if (devices[i].device_state == kDeviceStateSampling) {
|
||||||
|
LidarStopSampling(devices[i].handle, OnStopSampleCallback, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Uninit();
|
||||||
|
}
|
||||||
|
|
||||||
|
int connect(const char *broadcast_code) {
|
||||||
|
uint8_t handle = 0;
|
||||||
|
livox_status result = AddLidarToConnect(broadcast_code, &handle);
|
||||||
|
if (result == kStatusSuccess) {
|
||||||
|
/** Set the point cloud data for a specific Livox LiDAR. */
|
||||||
|
SetDataCallback(handle, GetLidarData, NULL);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int start_sampling(uint8_t handle) {
|
||||||
|
livox_status result = LidarStartSampling(handle, OnSampleCallback, NULL);
|
||||||
|
devices[handle].device_state = kDeviceStateSampling;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int stop_sampling(uint8_t handle) {
|
||||||
|
livox_status result = LidarStopSampling(handle, OnSampleCallback, NULL);
|
||||||
|
devices[handle].device_state = kDeviceStateSampling;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int set_mode(LidarMode mode) {
|
||||||
|
return LidarSetMode(devices[0].handle, mode, OnCommonCommandCallback, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册数据回调函数给Python调用 */
|
||||||
|
void register_data_callback(void (*data_callback)(uint8_t handle, uint32_t *data, uint32_t data_num)) {
|
||||||
|
python_data_callback = data_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册错误回调函数给Python调用 */
|
||||||
|
void register_error_callback(void (*error_callback)(livox_status status, uint8_t handle, ErrorMessage *message)) {
|
||||||
|
python_error_callback = error_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册设备状态变化回调给Python调用 */
|
||||||
|
void register_device_change_callback(void (*device_change_callback)(const DeviceInfo* info, DeviceEvent type)) {
|
||||||
|
python_device_change_callback = device_change_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册设备广播回调给Python调用 */
|
||||||
|
void register_device_broadcast_callback(void (*device_broadcast_callback)(const BroadcastDeviceInfo *info)) {
|
||||||
|
python_device_broadcast_callback = device_broadcast_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册设备广播回调给Python调用 */
|
||||||
|
void register_common_command_callback(void (*common_command_callback)(livox_status status, uint8_t handle, uint8_t response)) {
|
||||||
|
python_common_command_callback = common_command_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 回调函数的定义 */
|
||||||
|
void OnLidarErrorStatusCallback(livox_status status, uint8_t handle, ErrorMessage *message) {
|
||||||
|
if (python_error_callback) {
|
||||||
|
python_error_callback(status, handle, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnCommonCommandCallback(livox_status status, uint8_t handle, uint8_t response, void *data) {
|
||||||
|
if (python_common_command_callback) {
|
||||||
|
python_common_command_callback(status, handle, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetLidarData(uint8_t handle, LivoxEthPacket *data, uint32_t data_num, void *client_data) {
|
||||||
|
if (data) {
|
||||||
|
/** Parsing the timestamp and the point cloud data. */
|
||||||
|
uint64_t cur_timestamp = *((uint64_t *)(data->timestamp));
|
||||||
|
if(data ->data_type == kCartesian) {
|
||||||
|
LivoxRawPoint *p_point_data = (LivoxRawPoint *)data->data;
|
||||||
|
}else if ( data ->data_type == kSpherical) {
|
||||||
|
LivoxSpherPoint *p_point_data = (LivoxSpherPoint *)data->data;
|
||||||
|
}else if ( data ->data_type == kExtendCartesian) {
|
||||||
|
LivoxExtendRawPoint *p_point_data = (LivoxExtendRawPoint *)data->data;
|
||||||
|
LivoxExtendRawPoint point_array[data_num];
|
||||||
|
uint32_t *converted_data = (uint32_t *)malloc(data_num * 4 * sizeof(uint32_t));
|
||||||
|
for (uint32_t i = 0; i < data_num; ++i) {
|
||||||
|
LivoxExtendRawPoint *point = &p_point_data[i];
|
||||||
|
// 将结构体的 x, y, z 放入 uint32_t 数组中
|
||||||
|
converted_data[i * 4] = point->x;
|
||||||
|
converted_data[i * 4 + 1] = point->y;
|
||||||
|
converted_data[i * 4 + 2] = point->z;
|
||||||
|
// 将 reflectivity 和 tag 合并成一个 uint32_t(高16位和低16位)
|
||||||
|
converted_data[i * 4 + 3] = (uint32_t)(point->reflectivity << 8 | point->tag);
|
||||||
|
}
|
||||||
|
// 调用 Python 回调函数
|
||||||
|
if (python_data_callback) {
|
||||||
|
python_data_callback(handle, converted_data, data_num);
|
||||||
|
}
|
||||||
|
}else if ( data ->data_type == kExtendSpherical) {
|
||||||
|
LivoxExtendSpherPoint *p_point_data = (LivoxExtendSpherPoint *)data->data;
|
||||||
|
}else if ( data ->data_type == kDualExtendCartesian) {
|
||||||
|
LivoxDualExtendRawPoint *p_point_data = (LivoxDualExtendRawPoint *)data->data;
|
||||||
|
}else if ( data ->data_type == kDualExtendSpherical) {
|
||||||
|
LivoxDualExtendSpherPoint *p_point_data = (LivoxDualExtendSpherPoint *)data->data;
|
||||||
|
}else if ( data ->data_type == kImu) {
|
||||||
|
LivoxImuPoint *p_point_data = (LivoxImuPoint *)data->data;
|
||||||
|
}else if ( data ->data_type == kTripleExtendCartesian) {
|
||||||
|
LivoxTripleExtendRawPoint *p_point_data = (LivoxTripleExtendRawPoint *)data->data;
|
||||||
|
}else if ( data ->data_type == kTripleExtendSpherical) {
|
||||||
|
LivoxTripleExtendSpherPoint *p_point_data = (LivoxTripleExtendSpherPoint *)data->data;
|
||||||
|
}
|
||||||
|
// printf("data_type %d\n", data->data_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnSampleCallback(livox_status status, uint8_t handle, uint8_t response, void *data) {
|
||||||
|
if (status == kStatusSuccess && response != 0) {
|
||||||
|
devices[handle].device_state = kDeviceStateConnect;
|
||||||
|
} else if (status == kStatusTimeout) {
|
||||||
|
devices[handle].device_state = kDeviceStateConnect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnStopSampleCallback(livox_status status, uint8_t handle, uint8_t response, void *data) {
|
||||||
|
// 停止采样时的回调处理
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Query the firmware version of Livox LiDAR. */
|
||||||
|
void OnDeviceInformation(livox_status status, uint8_t handle, DeviceInformationResponse *ack, void *data) {
|
||||||
|
if (status != kStatusSuccess) {
|
||||||
|
printf("Device Query Informations Failed %d\n", status);
|
||||||
|
}
|
||||||
|
if (ack) {
|
||||||
|
printf("firm ver: %d.%d.%d.%d\n",
|
||||||
|
ack->firmware_version[0],
|
||||||
|
ack->firmware_version[1],
|
||||||
|
ack->firmware_version[2],
|
||||||
|
ack->firmware_version[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LidarConnect(const DeviceInfo *info) {
|
||||||
|
uint8_t handle = info->handle;
|
||||||
|
QueryDeviceInformation(handle, OnDeviceInformation, NULL);
|
||||||
|
if (devices[handle].device_state == kDeviceStateDisconnect) {
|
||||||
|
devices[handle].device_state = kDeviceStateConnect;
|
||||||
|
devices[handle].info = *info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LidarDisConnect(const DeviceInfo *info) {
|
||||||
|
uint8_t handle = info->handle;
|
||||||
|
devices[handle].device_state = kDeviceStateDisconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LidarStateChange(const DeviceInfo *info) {
|
||||||
|
uint8_t handle = info->handle;
|
||||||
|
devices[handle].info = *info;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDeviceInfoChange(const DeviceInfo *info, DeviceEvent type) {
|
||||||
|
if (info == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t handle = info->handle;
|
||||||
|
if (handle >= kMaxLidarCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type == kEventConnect) {
|
||||||
|
LidarConnect(info);
|
||||||
|
printf("[WARNING] Lidar sn: [%s] Connect!!!\n", info->broadcast_code);
|
||||||
|
} else if (type == kEventDisconnect) {
|
||||||
|
LidarDisConnect(info);
|
||||||
|
printf("[WARNING] Lidar sn: [%s] Disconnect!!!\n", info->broadcast_code);
|
||||||
|
} else if (type == kEventStateChange) {
|
||||||
|
LidarStateChange(info);
|
||||||
|
printf("[WARNING] Lidar sn: [%s] StateChange!!!\n", info->broadcast_code);
|
||||||
|
}
|
||||||
|
printf("Device Working State %d\n", devices[handle].info.state);
|
||||||
|
if (devices[handle].device_state == kDeviceStateConnect) {
|
||||||
|
if (devices[handle].info.state == kLidarStateInit) {
|
||||||
|
printf("Device State Change Progress %u\n", devices[handle].info.status.progress);
|
||||||
|
} else {
|
||||||
|
printf("Device State Error Code 0X%08x\n", devices[handle].info.status.status_code.error_code);
|
||||||
|
}
|
||||||
|
printf("Device feature %d\n", devices[handle].info.feature);
|
||||||
|
SetErrorMessageCallback(handle, OnLidarErrorStatusCallback);
|
||||||
|
if (devices[handle].info.state == kLidarStateNormal) {
|
||||||
|
LidarStartSampling(handle, OnSampleCallback, NULL);
|
||||||
|
devices[handle].device_state = kDeviceStateSampling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (python_device_change_callback) {
|
||||||
|
python_device_change_callback(info, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDeviceBroadcast(const BroadcastDeviceInfo *info) {
|
||||||
|
if (python_device_broadcast_callback) {
|
||||||
|
python_device_broadcast_callback(info);
|
||||||
|
}
|
||||||
|
}
|
212
vendors/livox/sdk_test.py
vendored
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import ctypes
|
||||||
|
from ctypes import *
|
||||||
|
|
||||||
|
# 加载C++库
|
||||||
|
livox_sdk = ctypes.CDLL('./liblivox_sdk_wrapper.so')
|
||||||
|
|
||||||
|
# 预定义的常量
|
||||||
|
kBroadcastCodeSize = 16 # 广播码长度
|
||||||
|
# 定义枚举类型
|
||||||
|
class DeviceType(c_int):
|
||||||
|
kDeviceTypeHub = 0
|
||||||
|
kDeviceTypeLidarMid40 = 1
|
||||||
|
kDeviceTypeLidarTele = 2
|
||||||
|
kDeviceTypeLidarHorizon = 3
|
||||||
|
kDeviceTypeLidarMid70 = 6
|
||||||
|
kDeviceTypeLidarAvia = 7
|
||||||
|
|
||||||
|
class LidarState(c_int):
|
||||||
|
kLidarStateInit = 0
|
||||||
|
kLidarStateNormal = 1
|
||||||
|
kLidarStatePowerSaving = 2
|
||||||
|
kLidarStateStandBy = 3
|
||||||
|
kLidarStateError = 4
|
||||||
|
kLidarStateUnknown = 5
|
||||||
|
|
||||||
|
class LidarMode(c_int):
|
||||||
|
kLidarModeNormal = 1
|
||||||
|
kLidarModePowerSaving = 2
|
||||||
|
kLidarModeStandby = 3
|
||||||
|
|
||||||
|
class LidarFeature(c_int):
|
||||||
|
kLidarFeatureNone = 0
|
||||||
|
kLidarFeatureRainFog = 1
|
||||||
|
|
||||||
|
class LidarIpMode(c_int):
|
||||||
|
kLidarDynamicIpMode = 0
|
||||||
|
kLidarStaticIpMode = 1
|
||||||
|
|
||||||
|
class LidarScanPattern(c_int):
|
||||||
|
kNoneRepetitiveScanPattern = 0
|
||||||
|
kRepetitiveScanPattern = 1
|
||||||
|
|
||||||
|
class LivoxStatus(c_int):
|
||||||
|
kStatusSendFailed = -9
|
||||||
|
kStatusHandlerImplNotExist = -8
|
||||||
|
kStatusInvalidHandle = -7
|
||||||
|
kStatusChannelNotExist = -6
|
||||||
|
kStatusNotEnoughMemory = -5
|
||||||
|
kStatusTimeout = -4
|
||||||
|
kStatusNotSupported = -3
|
||||||
|
kStatusNotConnected = -2
|
||||||
|
kStatusFailure = -1
|
||||||
|
kStatusSuccess = 0
|
||||||
|
|
||||||
|
# 定义结构体和联合体
|
||||||
|
class LidarErrorCode(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("temp_status", c_uint32, 2),
|
||||||
|
("volt_status", c_uint32, 2),
|
||||||
|
("motor_status", c_uint32, 2),
|
||||||
|
("dirty_warn", c_uint32, 2),
|
||||||
|
("firmware_err", c_uint32, 1),
|
||||||
|
("pps_status", c_uint32, 1),
|
||||||
|
("device_status", c_uint32, 1),
|
||||||
|
("fan_status", c_uint32, 1),
|
||||||
|
("self_heating", c_uint32, 1),
|
||||||
|
("ptp_status", c_uint32, 1),
|
||||||
|
("time_sync_status", c_uint32, 3),
|
||||||
|
("rsvd", c_uint32, 13),
|
||||||
|
("system_status", c_uint32, 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
class HubErrorCode(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("sync_status", c_uint32, 2),
|
||||||
|
("temp_status", c_uint32, 2),
|
||||||
|
("lidar_status", c_uint32, 1),
|
||||||
|
("lidar_link_status", c_uint32, 1),
|
||||||
|
("firmware_err", c_uint32, 1),
|
||||||
|
("rsvd", c_uint32, 23),
|
||||||
|
("system_status", c_uint32, 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
class ErrorMessage(Union):
|
||||||
|
_fields_ = [
|
||||||
|
("error_code", c_uint32),
|
||||||
|
("lidar_error_code", LidarErrorCode),
|
||||||
|
("hub_error_code", HubErrorCode)
|
||||||
|
]
|
||||||
|
|
||||||
|
class StatusUnion(Union):
|
||||||
|
_fields_ = [
|
||||||
|
("error_code", c_uint32), # 假设这个字段为示例
|
||||||
|
("lidar_error_code", LidarErrorCode),
|
||||||
|
("hub_error_code", HubErrorCode)
|
||||||
|
]
|
||||||
|
|
||||||
|
# 定义 DeviceInfo 结构体
|
||||||
|
class DeviceInfo(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("broadcast_code", c_char * kBroadcastCodeSize), # 广播码,最大15个字符,带终止符
|
||||||
|
("handle", c_uint8), # 设备句柄
|
||||||
|
("slot", c_uint8), # 插槽编号
|
||||||
|
("id", c_uint8), # LiDAR id
|
||||||
|
("type", DeviceType), # 设备类型
|
||||||
|
("data_port", c_uint16), # 点云数据UDP端口
|
||||||
|
("cmd_port", c_uint16), # 控制命令UDP端口
|
||||||
|
("sensor_port", c_uint16), # IMU数据UDP端口
|
||||||
|
("ip", c_char * 16), # IP地址
|
||||||
|
("state", LidarState), # LiDAR状态
|
||||||
|
("feature", LidarFeature), # LiDAR特性
|
||||||
|
("status", StatusUnion), # LiDAR工作状态
|
||||||
|
("firmware_version", c_uint8 * 4) # 固件版本
|
||||||
|
]
|
||||||
|
|
||||||
|
# 定义 BroadcastDeviceInfo 结构体
|
||||||
|
class BroadcastDeviceInfo(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("broadcast_code", c_char * kBroadcastCodeSize), # 广播码,最多15个字符,带终止符
|
||||||
|
("dev_type", c_uint8), # 设备类型,参考 DeviceType
|
||||||
|
("reserved", c_uint16), # 保留字段
|
||||||
|
("ip", c_char * 16) # 设备 IP 地址
|
||||||
|
]
|
||||||
|
|
||||||
|
# 定义 LivoxEthPacket 结构体
|
||||||
|
class LivoxEthPacket(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("version", c_uint8), # Packet protocol version.
|
||||||
|
("slot", c_uint8), # Slot number used for connecting LiDAR.
|
||||||
|
("id", c_uint8), # LiDAR id.
|
||||||
|
("rsvd", c_uint8), # Reserved.
|
||||||
|
("err_code", c_uint32), # Device error status indicator information.
|
||||||
|
("timestamp_type", c_uint8), # Timestamp type.
|
||||||
|
("data_type", c_uint8), # Point cloud coordinate format.
|
||||||
|
("timestamp", c_uint8 * 8), # Nanosecond or UTC format timestamp.
|
||||||
|
("data", c_uint8 * 1) # Point cloud data (can be extended as needed).
|
||||||
|
]
|
||||||
|
|
||||||
|
# 定义 LivoxExtendRawPoint 结构体
|
||||||
|
class LivoxExtendRawPoint(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('x', c_int32), # X axis, Unit: mm
|
||||||
|
('y', c_int32), # Y axis, Unit: mm
|
||||||
|
('z', c_int32), # Z axis, Unit: mm
|
||||||
|
('reflectivity', c_uint8), # Reflectivity
|
||||||
|
('tag', c_uint8) # Tag
|
||||||
|
]
|
||||||
|
|
||||||
|
# 回调函数类型定义
|
||||||
|
DataCallbackType = CFUNCTYPE(None, c_uint8, POINTER(LivoxEthPacket), c_uint32)
|
||||||
|
ErrorCallbackType = CFUNCTYPE(None, c_uint8, c_uint8, POINTER(ErrorMessage))
|
||||||
|
DeviceChangeCallbackType = CFUNCTYPE(None, POINTER(DeviceInfo), c_uint8)
|
||||||
|
DeviceBroadcastCallbackType = CFUNCTYPE(None, POINTER(BroadcastDeviceInfo))
|
||||||
|
|
||||||
|
|
||||||
|
point_size = sizeof(LivoxExtendRawPoint)
|
||||||
|
|
||||||
|
# 数据回调函数
|
||||||
|
@DataCallbackType
|
||||||
|
def data_callback(handle, packet, data_num):
|
||||||
|
# print(f"Received data from handle {handle}, data num: {data_num}")
|
||||||
|
# 1. 计算数据的总字节数
|
||||||
|
total_data_size = data_num * point_size
|
||||||
|
# 2. 将 packet.data 转换为指向 LivoxExtendRawPoint 数组的指针
|
||||||
|
point_data_array_type = LivoxExtendRawPoint * data_num # 定义类型
|
||||||
|
point_data_pointer = cast(packet.data, POINTER(point_data_array_type)) # 将data转换为LivoxExtendRawPoint指针
|
||||||
|
# 3. 获取具体的点云数据
|
||||||
|
point_data_array = point_data_pointer.contents # 访问数组内容
|
||||||
|
# 4. 遍历每个 LivoxExtendRawPoint 并打印数据
|
||||||
|
for i in range(data_num):
|
||||||
|
point = point_data_array[i]
|
||||||
|
print(f"Point {i}: X={point.x}, Y={point.y}, Z={point.z}, Reflectivity={point.reflectivity}, Tag={point.tag}")
|
||||||
|
|
||||||
|
# 错误回调函数
|
||||||
|
@ErrorCallbackType
|
||||||
|
def error_callback(status, handle, error_message):
|
||||||
|
print(f"Error from handle {handle}, status: {status}")
|
||||||
|
|
||||||
|
# 设备状态变化回调函数
|
||||||
|
@DeviceChangeCallbackType
|
||||||
|
def device_change_callback(info, event_type):
|
||||||
|
print(f"Device {bytes(info.contents.broadcast_code).decode('utf-8')} changed state, event type: {event_type}")
|
||||||
|
|
||||||
|
# 设备广播回调函数
|
||||||
|
@DeviceBroadcastCallbackType
|
||||||
|
def device_broadcast_callback(info):
|
||||||
|
print(f"New device broadcast received: {bytes(info.contents.broadcast_code).decode('utf-8')}")
|
||||||
|
|
||||||
|
# 注册回调函数
|
||||||
|
livox_sdk.register_data_callback(data_callback)
|
||||||
|
livox_sdk.register_error_callback(error_callback)
|
||||||
|
livox_sdk.register_device_change_callback(device_change_callback)
|
||||||
|
livox_sdk.register_device_broadcast_callback(device_broadcast_callback)
|
||||||
|
|
||||||
|
# 调用初始化和开始发现设备的函数
|
||||||
|
if livox_sdk.init_sdk():
|
||||||
|
print("Livox SDK initialized.")
|
||||||
|
else:
|
||||||
|
print("Failed to initialize Livox SDK.")
|
||||||
|
|
||||||
|
if livox_sdk.start_discovery():
|
||||||
|
print("Started discovering Livox devices.")
|
||||||
|
else:
|
||||||
|
print("Failed to discover devices.")
|
||||||
|
|
||||||
|
# 等待数据回调
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
pass
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
livox_sdk.stop_sdk()
|
||||||
|
print("SDK stopped.")
|
0
widget/__init__.py
Normal file
133
widget/device.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QFrame
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QColor
|
||||||
|
|
||||||
|
from core.context import AppContext
|
||||||
|
|
||||||
|
|
||||||
|
# 相机组件
|
||||||
|
class CameraWidget(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.ratio = AppContext.get_ratio()
|
||||||
|
|
||||||
|
# 设备信息部分
|
||||||
|
device_info = QWidget()
|
||||||
|
device_info.setFixedHeight(self.ratio * 80)
|
||||||
|
device_layout = QHBoxLayout()
|
||||||
|
self.serial_label = QLabel("高清相机: DH51215AAK00001")
|
||||||
|
self.status_label = QLabel("状态: 未连接")
|
||||||
|
self.power_button = QPushButton("通电")
|
||||||
|
self.connect_button = QPushButton("连接")
|
||||||
|
self.capture_button = QPushButton("拍照")
|
||||||
|
self.power_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.power_button.setFixedWidth(self.ratio * 64)
|
||||||
|
self.connect_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.connect_button.setFixedWidth(self.ratio * 64)
|
||||||
|
self.capture_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.capture_button.setFixedWidth(self.ratio * 64)
|
||||||
|
device_layout.addWidget(self.serial_label)
|
||||||
|
device_layout.addWidget(self.status_label)
|
||||||
|
device_layout.addWidget(self.power_button)
|
||||||
|
device_layout.addWidget(self.connect_button)
|
||||||
|
device_layout.addWidget(self.capture_button)
|
||||||
|
device_info.setLayout(device_layout)
|
||||||
|
|
||||||
|
# 事件
|
||||||
|
self.power_button.clicked.connect(self.on_power)
|
||||||
|
self.on_power() # init
|
||||||
|
|
||||||
|
# 信号
|
||||||
|
AppContext.get_edge_context().get_component('gpio').signals.gpio_camera_opened.connect(
|
||||||
|
lambda pin: self.power_button.setText("断电"))
|
||||||
|
AppContext.get_edge_context().get_component('gpio').signals.gpio_camera_closed.connect(
|
||||||
|
lambda pin: self.power_button.setText("通电"))
|
||||||
|
|
||||||
|
# 数据展示部分
|
||||||
|
data_display = QFrame()
|
||||||
|
data_display.setStyleSheet("background-color: white;")
|
||||||
|
data_display.setFrameShape(QFrame.StyledPanel)
|
||||||
|
|
||||||
|
# 垂直布局
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addWidget(device_info)
|
||||||
|
layout.addWidget(data_display)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def on_power(self):
|
||||||
|
gpio_manager = AppContext.get_edge_context().get_component('gpio')
|
||||||
|
if gpio_manager.camera_value == 0:
|
||||||
|
gpio_manager.close_camera()
|
||||||
|
else:
|
||||||
|
gpio_manager.open_camera()
|
||||||
|
|
||||||
|
|
||||||
|
# 雷达组件
|
||||||
|
class LidarWidget(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.ratio = AppContext.get_ratio()
|
||||||
|
|
||||||
|
# 设备信息部分
|
||||||
|
device_info = QWidget()
|
||||||
|
device_info.setFixedHeight(self.ratio * 80)
|
||||||
|
device_layout = QHBoxLayout()
|
||||||
|
self.serial_label = QLabel("激光雷达: 3GGDLCM00201561")
|
||||||
|
self.status_label = QLabel("状态: 未连接")
|
||||||
|
self.power_button = QPushButton("通电")
|
||||||
|
self.connect_button = QPushButton("启动")
|
||||||
|
self.power_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.power_button.setFixedWidth(self.ratio * 64)
|
||||||
|
self.connect_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.connect_button.setFixedWidth(self.ratio * 64)
|
||||||
|
device_layout.addWidget(self.serial_label)
|
||||||
|
device_layout.addWidget(self.status_label)
|
||||||
|
device_layout.addWidget(self.power_button)
|
||||||
|
device_layout.addWidget(self.connect_button)
|
||||||
|
device_info.setLayout(device_layout)
|
||||||
|
|
||||||
|
# 事件
|
||||||
|
self.power_button.clicked.connect(self.on_power)
|
||||||
|
self.on_power() # init
|
||||||
|
|
||||||
|
# 信号
|
||||||
|
AppContext.get_edge_context().get_component('gpio').signals.gpio_lidar_opened.connect(
|
||||||
|
lambda pin: self.power_button.setText("断电"))
|
||||||
|
AppContext.get_edge_context().get_component('gpio').signals.gpio_lidar_closed.connect(
|
||||||
|
lambda pin: self.power_button.setText("通电"))
|
||||||
|
|
||||||
|
# 数据展示部分
|
||||||
|
data_display = QFrame()
|
||||||
|
data_display.setStyleSheet("background-color: white;")
|
||||||
|
data_display.setFrameShape(QFrame.StyledPanel)
|
||||||
|
|
||||||
|
# 垂直布局
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addWidget(device_info)
|
||||||
|
layout.addWidget(data_display)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def on_power(self):
|
||||||
|
gpio_manager = AppContext.get_edge_context().get_component('gpio')
|
||||||
|
if gpio_manager.lidar_value == 0:
|
||||||
|
gpio_manager.close_lidar()
|
||||||
|
else:
|
||||||
|
gpio_manager.open_lidar()
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceWidget(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
layout = QHBoxLayout()
|
||||||
|
self.camera_widget = CameraWidget()
|
||||||
|
self.lidar_widget = LidarWidget()
|
||||||
|
|
||||||
|
layout.addWidget(self.camera_widget)
|
||||||
|
layout.addWidget(self.lidar_widget)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
54
widget/embed_detail.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtWidgets import QLabel, QPushButton, QGridLayout, QDialog
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedDetail(QDialog):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(EmbedDetail, self).__init__(parent)
|
||||||
|
|
||||||
|
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||||
|
self.setWindowFlags(Qt.FramelessWindowHint)
|
||||||
|
self.setWindowModality(Qt.ApplicationModal)
|
||||||
|
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||||
|
self.setWindowTitle("预埋件详情")
|
||||||
|
|
||||||
|
self.item_label_x = QLabel(self)
|
||||||
|
self.item_label_x.setText("X:")
|
||||||
|
self.item_label_x_value = QLabel(self)
|
||||||
|
|
||||||
|
self.item_label_y = QLabel(self)
|
||||||
|
self.item_label_y.setText("Y:")
|
||||||
|
self.item_label_y_value = QLabel(self)
|
||||||
|
|
||||||
|
self.item_label_w = QLabel(self)
|
||||||
|
self.item_label_w.setText("W:")
|
||||||
|
self.item_label_w_value = QLabel(self)
|
||||||
|
|
||||||
|
self.item_label_h = QLabel(self)
|
||||||
|
self.item_label_h.setText("H:")
|
||||||
|
self.item_label_h_value = QLabel(self)
|
||||||
|
|
||||||
|
item_button = QPushButton(self)
|
||||||
|
item_button.setText("关闭")
|
||||||
|
item_button.clicked.connect(self.reject)
|
||||||
|
|
||||||
|
layout = QGridLayout()
|
||||||
|
layout.setContentsMargins(20,10,20,15)
|
||||||
|
layout.setHorizontalSpacing(5)
|
||||||
|
layout.addWidget(self.item_label_x, 0, 0)
|
||||||
|
layout.addWidget(self.item_label_x_value, 0, 1)
|
||||||
|
layout.addWidget(self.item_label_y, 1, 0)
|
||||||
|
layout.addWidget(self.item_label_y_value, 1, 1)
|
||||||
|
layout.addWidget(self.item_label_w, 2, 0)
|
||||||
|
layout.addWidget(self.item_label_w_value, 2, 1)
|
||||||
|
layout.addWidget(self.item_label_h, 3, 0)
|
||||||
|
layout.addWidget(self.item_label_h_value, 3, 1)
|
||||||
|
layout.addWidget(item_button, 4, 0, 1, 2)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def setInfo(self, param):
|
||||||
|
self.item_label_x_value.setText(str(param["x"]))
|
||||||
|
self.item_label_y_value.setText(str(param["y"]))
|
||||||
|
self.item_label_w_value.setText(str(param["w"]))
|
||||||
|
self.item_label_h_value.setText(str(param["h"]))
|
56
widget/embed_item.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from PyQt5.QtCore import QEvent, QRect, QSize
|
||||||
|
from PyQt5.QtWidgets import QLabel, QPushButton, QFrame
|
||||||
|
from widget.embed_detail import EmbedDetail
|
||||||
|
|
||||||
|
class EmbedItem(QFrame):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(EmbedItem, self).__init__(parent)
|
||||||
|
|
||||||
|
self.rect = None
|
||||||
|
self.param = None
|
||||||
|
|
||||||
|
self.item_button = QPushButton(self)
|
||||||
|
self.item_button.clicked.connect(self.click)
|
||||||
|
self.item_label = QLabel(self)
|
||||||
|
|
||||||
|
def resizeEvent(self, event: QEvent):
|
||||||
|
super().resizeEvent(event)
|
||||||
|
button_size = self.item_button.size()
|
||||||
|
label_size = self.item_label.sizeHint()
|
||||||
|
self.setFixedSize(max(button_size.width(), label_size.width()), button_size.height() + label_size.height() +2)
|
||||||
|
widget_size = self.size()
|
||||||
|
if label_size.width() > button_size.width():
|
||||||
|
self.item_button.move((widget_size.width() - button_size.width()) / 2, 0)
|
||||||
|
self.item_label.move(0, button_size.height() + 2)
|
||||||
|
self.move(self.rect.x() - (widget_size.width() - button_size.width()) / 2, self.rect.y())
|
||||||
|
else:
|
||||||
|
self.item_label.move((widget_size.width() - label_size.width()) / 2, button_size.height() + 2)
|
||||||
|
self.item_button.move(0, 0)
|
||||||
|
self.move(self.rect.x(), self.rect.y())
|
||||||
|
|
||||||
|
def setItemParam(self, param):
|
||||||
|
self.param = param
|
||||||
|
self.rect = QRect(param["x"], param["y"], param["w"], param["h"])
|
||||||
|
self.item_button.setFixedSize(QSize(param["w"], param["h"]))
|
||||||
|
self.item_button.setObjectName(param["code"])
|
||||||
|
self.item_label.setText(param["code"])
|
||||||
|
self.item_label.move(0, self.rect.height() + 5)
|
||||||
|
|
||||||
|
def setIndex(self, index):
|
||||||
|
self.item_button.setText(index)
|
||||||
|
|
||||||
|
def setChecked(self):
|
||||||
|
self.item_button.setStyleSheet("border: 1px solid #6cff6c;")
|
||||||
|
|
||||||
|
def setError(self):
|
||||||
|
self.item_button.setStyleSheet("border: 1px solid #FF4444;")
|
||||||
|
|
||||||
|
def click(self):
|
||||||
|
if self.item_button.text() is None or self.item_button.text() == "": return
|
||||||
|
detail = EmbedDetail()
|
||||||
|
detail.setInfo(self.param)
|
||||||
|
relative_pos = self.item_button.pos()
|
||||||
|
global_pos = self.mapToGlobal(relative_pos)
|
||||||
|
detail.move(global_pos.x() + self.item_button.width() + 2, global_pos.y() + 2)
|
||||||
|
detail.exec_()
|
98
widget/task_edit.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QLabel, QHBoxLayout, QPushButton, QFileDialog, \
|
||||||
|
QPlainTextEdit, QGridLayout
|
||||||
|
|
||||||
|
from core.context import AppContext
|
||||||
|
from widget.task_run import TaskRunDialog
|
||||||
|
|
||||||
|
|
||||||
|
class TaskEditWidget(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(TaskEditWidget, self).__init__(parent)
|
||||||
|
self.ratio = AppContext.get_ratio()
|
||||||
|
self.run_dialog = None
|
||||||
|
|
||||||
|
# 导入数据按钮
|
||||||
|
file_button = QPushButton('导入任务数据')
|
||||||
|
file_button.setObjectName('fileButton')
|
||||||
|
file_button.setFixedHeight(self.ratio * 42)
|
||||||
|
file_button.setStyleSheet("background-color: #3A36DB;")
|
||||||
|
file_button.clicked.connect(self.import_excel)
|
||||||
|
|
||||||
|
# 任务名称
|
||||||
|
item_layout = QHBoxLayout()
|
||||||
|
item_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
name_label = QLabel('任务名称')
|
||||||
|
self.name_text = QLineEdit()
|
||||||
|
self.name_text.setObjectName('name_text')
|
||||||
|
self.name_text.setReadOnly(True)
|
||||||
|
self.name_text.textChanged.connect(self.text_changed)
|
||||||
|
item_layout.addWidget(name_label)
|
||||||
|
item_layout.addSpacing(10)
|
||||||
|
item_layout.addWidget(self.name_text)
|
||||||
|
name_item = QWidget()
|
||||||
|
name_item.setLayout(item_layout)
|
||||||
|
|
||||||
|
# 数据内容
|
||||||
|
json_widget = QWidget()
|
||||||
|
json_widget.setObjectName('jsonWidget')
|
||||||
|
json_layout = QGridLayout()
|
||||||
|
json_layout.setContentsMargins(10, 10, 1, 10)
|
||||||
|
self.file_content = QPlainTextEdit()
|
||||||
|
self.file_content.setReadOnly(True)
|
||||||
|
self.file_content.textChanged.connect(self.text_changed)
|
||||||
|
json_layout.addWidget(self.file_content)
|
||||||
|
json_widget.setLayout(json_layout)
|
||||||
|
|
||||||
|
# 任务开始按钮
|
||||||
|
self.start_task_button = QPushButton('开始任务')
|
||||||
|
self.start_task_button.setObjectName('startTaskButton')
|
||||||
|
self.start_task_button.setEnabled(False)
|
||||||
|
self.start_task_button.setFixedWidth(self.ratio * 200)
|
||||||
|
self.start_task_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.start_task_button.clicked.connect(self.start_task)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.setSpacing(20)
|
||||||
|
layout.setContentsMargins(100,30,100,30)
|
||||||
|
layout.addWidget(file_button)
|
||||||
|
layout.addWidget(name_item)
|
||||||
|
layout.addWidget(json_widget)
|
||||||
|
layout.addWidget(self.start_task_button, alignment=Qt.AlignRight)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def import_excel(self):
|
||||||
|
options = QFileDialog.Options()
|
||||||
|
file_path, _ = QFileDialog.getOpenFileName(self, "选择 Excel 文件", "", "Excel Files (*.xlsx *.xls)", options=options)
|
||||||
|
|
||||||
|
if file_path:
|
||||||
|
try:
|
||||||
|
# Read Excel file into a pandas DataFrame
|
||||||
|
df = pd.read_excel(file_path)
|
||||||
|
# Convert DataFrame to JSON
|
||||||
|
json_data = json.loads(df.to_json(orient='records'))
|
||||||
|
for data in json_data:
|
||||||
|
if data["name"] is not None and data["name"] != '' :
|
||||||
|
self.name_text.setText(data["name"])
|
||||||
|
del data["name"]
|
||||||
|
json_data_str = json.dumps(json_data, indent=4, ensure_ascii=False)
|
||||||
|
self.file_content.setPlainText(json_data_str)
|
||||||
|
except Exception as e:
|
||||||
|
self.file_content.setPlainText(f'Error: {e}')
|
||||||
|
|
||||||
|
def start_task(self):
|
||||||
|
task_table = AppContext.get_edge_context().get_component("dat_task")
|
||||||
|
task_table.insert_task({ "name": self.name_text.text(), "param_json": self.file_content.toPlainText(), "start_time": datetime.now() })
|
||||||
|
self.run_dialog = TaskRunDialog(json.loads(self.file_content.toPlainText()))
|
||||||
|
dialog_result = self.run_dialog.exec_()
|
||||||
|
if dialog_result == 0:
|
||||||
|
self.name_text.setText(None)
|
||||||
|
self.file_content.setPlainText(None)
|
||||||
|
|
||||||
|
def text_changed(self):
|
||||||
|
self.start_task_button.setEnabled(self.file_content.toPlainText() !='' and self.name_text.text() != '')
|
138
widget/task_list.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from PyQt5.QtCore import Qt, QRect
|
||||||
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QGridLayout, QLineEdit, QLabel, QHBoxLayout, QPushButton, QFileDialog, \
|
||||||
|
QScrollArea, QPlainTextEdit
|
||||||
|
|
||||||
|
from core.context import AppContext
|
||||||
|
from widget.task_run import TaskRunDialog
|
||||||
|
|
||||||
|
|
||||||
|
class TaskListWidget(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(TaskListWidget, self).__init__(parent)
|
||||||
|
self.ratio = AppContext.get_ratio()
|
||||||
|
self.run_dialog = None
|
||||||
|
|
||||||
|
widget = QWidget()
|
||||||
|
|
||||||
|
scroll_area = QScrollArea()
|
||||||
|
scroll_area.setStyleSheet("border: none;")
|
||||||
|
scroll_area.setWidgetResizable(True)
|
||||||
|
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||||
|
|
||||||
|
self.content_widget = QWidget()
|
||||||
|
|
||||||
|
scroll_area.setWidget(self.content_widget)
|
||||||
|
|
||||||
|
widget_layout = QVBoxLayout(widget)
|
||||||
|
widget_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
widget_layout.setSpacing(0)
|
||||||
|
widget_layout.addWidget(scroll_area)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
layout.addWidget(widget)
|
||||||
|
|
||||||
|
self.initUi()
|
||||||
|
|
||||||
|
def initUi(self):
|
||||||
|
task_table = AppContext.get_edge_context().get_component("dat_task")
|
||||||
|
tasks = task_table.list_task(1)
|
||||||
|
|
||||||
|
# 添加 TITLE
|
||||||
|
x = 0
|
||||||
|
y = 0
|
||||||
|
offset = 10
|
||||||
|
x = x + offset
|
||||||
|
y = y + offset
|
||||||
|
label_name = QLabel(self.content_widget)
|
||||||
|
label_name.setObjectName("THeader")
|
||||||
|
label_name.setText("任务名称")
|
||||||
|
label_name.setGeometry(QRect(x, y, 200, 30))
|
||||||
|
x = x + 200 + offset
|
||||||
|
label_sn = QLabel(self.content_widget)
|
||||||
|
label_sn.setObjectName("THeader")
|
||||||
|
label_sn.setText("设备序列号")
|
||||||
|
label_sn.setGeometry(QRect(x, y, 150, 30))
|
||||||
|
x = x + 150 + offset
|
||||||
|
label_param = QLabel(self.content_widget)
|
||||||
|
label_param.setObjectName("THeader")
|
||||||
|
label_param.setText("参数")
|
||||||
|
label_param.setGeometry(QRect(x, y, 400, 30))
|
||||||
|
x = x + 400 + offset
|
||||||
|
label_start = QLabel(self.content_widget)
|
||||||
|
label_start.setObjectName("THeader")
|
||||||
|
label_start.setText("开始时间")
|
||||||
|
label_start.setGeometry(QRect(x, y, 150, 30))
|
||||||
|
x = x + 150 + offset
|
||||||
|
label_end = QLabel(self.content_widget)
|
||||||
|
label_end.setObjectName("THeader")
|
||||||
|
label_end.setText("结束时间")
|
||||||
|
label_end.setGeometry(QRect(x, y, 150, 30))
|
||||||
|
x = x + 150 + offset
|
||||||
|
label_create = QLabel(self.content_widget)
|
||||||
|
label_create.setObjectName("THeader")
|
||||||
|
label_create.setText("创建时间")
|
||||||
|
label_create.setGeometry(QRect(x, y, 150, 30))
|
||||||
|
x = x + 150 + offset
|
||||||
|
label_action = QLabel(self.content_widget)
|
||||||
|
label_action.setObjectName("THeader")
|
||||||
|
label_action.setText("操作")
|
||||||
|
label_action.setGeometry(QRect(x, y, 200, 30))
|
||||||
|
|
||||||
|
y = y + 30 + offset
|
||||||
|
for i in range(10):
|
||||||
|
x = offset
|
||||||
|
task = tasks[0]
|
||||||
|
label_name = QLabel(self.content_widget)
|
||||||
|
label_name.setText(task[1])
|
||||||
|
label_name.setGeometry(QRect(x, y, 200, 30))
|
||||||
|
x = x + 200 + offset
|
||||||
|
label_sn = QLabel(self.content_widget)
|
||||||
|
label_sn.setText(task[2])
|
||||||
|
label_sn.setGeometry(QRect(x, y, 150, 30))
|
||||||
|
x = x + 150 + offset
|
||||||
|
param = QPlainTextEdit(self.content_widget)
|
||||||
|
param.setPlainText(task[3])
|
||||||
|
param.setReadOnly(True)
|
||||||
|
param.setGeometry(QRect(x, y, 400, 120))
|
||||||
|
x = x + 400 + offset
|
||||||
|
label_start = QLabel(self.content_widget)
|
||||||
|
label_start.setText(task[6])
|
||||||
|
label_start.setGeometry(QRect(x, y, 150, 30))
|
||||||
|
x = x + 150 + offset
|
||||||
|
label_end = QLabel(self.content_widget)
|
||||||
|
label_end.setText(task[7])
|
||||||
|
label_end.setGeometry(QRect(x, y, 150, 30))
|
||||||
|
x = x + 150 + offset
|
||||||
|
label_create = QLabel(self.content_widget)
|
||||||
|
label_create.setText(task[8])
|
||||||
|
label_create.setGeometry(QRect(x, y, 150, 30))
|
||||||
|
x = x + 150 + offset
|
||||||
|
run_task = QPushButton(self.content_widget)
|
||||||
|
run_task.setStyleSheet("background-color: #3A36DB;")
|
||||||
|
run_task.setObjectName("run_task")
|
||||||
|
run_task.setText("开始任务")
|
||||||
|
run_task.setGeometry(QRect(x, y, 120, 30))
|
||||||
|
run_task.clicked.connect(lambda: self.start_task(task))
|
||||||
|
|
||||||
|
# run_task.clicked.connect(self.start_task)
|
||||||
|
|
||||||
|
y = y + 120 + offset
|
||||||
|
|
||||||
|
self.content_widget.setFixedHeight(y + offset)
|
||||||
|
|
||||||
|
def start_task(self, task):
|
||||||
|
task_table = AppContext.get_edge_context().get_component("dat_task")
|
||||||
|
task_table.update_task({ "id": task[0], "state": 1, "start_time": datetime.now() })
|
||||||
|
self.run_dialog = TaskRunDialog(json.loads(task[3]))
|
||||||
|
dialog_result = self.run_dialog.exec_()
|
||||||
|
if dialog_result == 0:
|
||||||
|
c = 0
|
||||||
|
# self.name_text.setText(None)
|
||||||
|
# self.file_content.setPlainText(None)
|
252
widget/task_run.py
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QPixmap
|
||||||
|
from PyQt5.QtWidgets import QDialog, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QScrollArea, QPushButton, \
|
||||||
|
QSpacerItem, QSizePolicy, QLabel
|
||||||
|
|
||||||
|
from core.context import AppContext
|
||||||
|
from widget.embed_item import EmbedItem
|
||||||
|
|
||||||
|
class TaskRunDialog(QDialog):
|
||||||
|
def __init__(self, param, parent=None):
|
||||||
|
super(TaskRunDialog, self).__init__(parent)
|
||||||
|
self.ratio = AppContext.get_ratio()
|
||||||
|
self.checkIndex = 1
|
||||||
|
self.param = param
|
||||||
|
self.setWindowState(Qt.WindowMaximized)
|
||||||
|
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||||
|
self.setWindowFlags(Qt.FramelessWindowHint)
|
||||||
|
self.setWindowModality(Qt.ApplicationModal)
|
||||||
|
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||||
|
self.setWindowTitle("检测窗口")
|
||||||
|
|
||||||
|
self.bim_widget = None
|
||||||
|
self.check_widget = None
|
||||||
|
self.embed_widgets = []
|
||||||
|
|
||||||
|
self.start_adjust_button = None
|
||||||
|
self.start_check_button = None
|
||||||
|
self.stop_check_button = None
|
||||||
|
self.stop_task_button = None
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
|
||||||
|
# bim
|
||||||
|
self.bim_widget = QWidget()
|
||||||
|
self.bim_widget.setObjectName("bimWidget")
|
||||||
|
self.bim_widget.setFixedWidth(3000)
|
||||||
|
|
||||||
|
# 选择区
|
||||||
|
self.check_widget = QWidget(self.bim_widget)
|
||||||
|
self.check_widget.setObjectName("checkWidget")
|
||||||
|
self.check_widget.setFixedSize(160, 160)
|
||||||
|
self.check_widget.move(90, 230)
|
||||||
|
|
||||||
|
# 预埋件
|
||||||
|
self.embed_widgets = []
|
||||||
|
for item in self.param:
|
||||||
|
embed_item = EmbedItem(self.bim_widget)
|
||||||
|
embed_item.setObjectName(item["code"])
|
||||||
|
embed_item.setItemParam(item)
|
||||||
|
self.embed_widgets.append(embed_item)
|
||||||
|
|
||||||
|
map_widget = QWidget()
|
||||||
|
legend_widget = QWidget()
|
||||||
|
|
||||||
|
legend_layout = QHBoxLayout()
|
||||||
|
legend_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
legend_layout.setSpacing(0)
|
||||||
|
legend_widget.setLayout(legend_layout)
|
||||||
|
|
||||||
|
legend_check_widget = QWidget()
|
||||||
|
legend_check_widget.setObjectName("legendCheckWidget")
|
||||||
|
legend_std_widget = QWidget()
|
||||||
|
legend_std_widget.setObjectName("legendStdWidget")
|
||||||
|
legend_ok_widget = QWidget()
|
||||||
|
legend_ok_widget.setObjectName("legendOkWidget")
|
||||||
|
legend_ng_widget = QWidget()
|
||||||
|
legend_ng_widget.setObjectName("legendNgWidget")
|
||||||
|
legend_normal_widget = QWidget()
|
||||||
|
legend_normal_widget.setObjectName("legendNormalWidget")
|
||||||
|
|
||||||
|
legend_check_label = QLabel()
|
||||||
|
legend_check_label.setObjectName("legendCheckLabel")
|
||||||
|
legend_check_label.setText("检测区域")
|
||||||
|
legend_std_label = QLabel()
|
||||||
|
legend_std_label.setObjectName("legendStdLabel")
|
||||||
|
legend_std_label.setText("基准件")
|
||||||
|
legend_ok_label = QLabel()
|
||||||
|
legend_ok_label.setObjectName("legendOkLabel")
|
||||||
|
legend_ok_label.setText("合格件")
|
||||||
|
legend_ng_label = QLabel()
|
||||||
|
legend_ng_label.setObjectName("legendNgLabel")
|
||||||
|
legend_ng_label.setText("不合格件")
|
||||||
|
legend_normal_label = QLabel()
|
||||||
|
legend_normal_label.setObjectName("legendNormalLabel")
|
||||||
|
legend_normal_label.setText("待检测件")
|
||||||
|
|
||||||
|
legend_layout.addStretch(1)
|
||||||
|
legend_layout.addWidget(legend_check_widget)
|
||||||
|
legend_layout.addSpacing(5)
|
||||||
|
legend_layout.addWidget(legend_check_label)
|
||||||
|
legend_layout.addSpacing(40)
|
||||||
|
legend_layout.addWidget(legend_std_widget)
|
||||||
|
legend_layout.addSpacing(5)
|
||||||
|
legend_layout.addWidget(legend_std_label)
|
||||||
|
legend_layout.addSpacing(40)
|
||||||
|
legend_layout.addWidget(legend_ok_widget)
|
||||||
|
legend_layout.addSpacing(5)
|
||||||
|
legend_layout.addWidget(legend_ok_label)
|
||||||
|
legend_layout.addSpacing(40)
|
||||||
|
legend_layout.addWidget(legend_ng_widget)
|
||||||
|
legend_layout.addSpacing(5)
|
||||||
|
legend_layout.addWidget(legend_ng_label)
|
||||||
|
legend_layout.addSpacing(40)
|
||||||
|
legend_layout.addWidget(legend_normal_widget)
|
||||||
|
legend_layout.addSpacing(5)
|
||||||
|
legend_layout.addWidget(legend_normal_label)
|
||||||
|
legend_layout.addStretch(1)
|
||||||
|
|
||||||
|
content_widget = QWidget()
|
||||||
|
content_widget.setObjectName("contentWidget")
|
||||||
|
|
||||||
|
picture_label = QLabel()
|
||||||
|
picture_label.setObjectName("pictureWidget")
|
||||||
|
picture_label.setFixedWidth(450)
|
||||||
|
picture_label.setFixedHeight(450)
|
||||||
|
picture_label.setScaledContents(True)
|
||||||
|
picture_pixmap = QPixmap("assets/img2.png")
|
||||||
|
picture_label.setPixmap(picture_pixmap)
|
||||||
|
|
||||||
|
view_widget = QWidget()
|
||||||
|
view_widget.setObjectName("viewWidget")
|
||||||
|
view_widget.setFixedHeight(450)
|
||||||
|
|
||||||
|
view_label = QLabel(view_widget)
|
||||||
|
view_label.setObjectName("viewLabel")
|
||||||
|
view_label.setFixedHeight(450)
|
||||||
|
view_label.setScaledContents(True)
|
||||||
|
view_pixmap = QPixmap("assets/img1.jpg")
|
||||||
|
sc = view_pixmap.height() / 450
|
||||||
|
view_label.setFixedWidth(view_pixmap.width() / sc)
|
||||||
|
view_label.setPixmap(view_pixmap)
|
||||||
|
view_label.setGeometry((view_widget.width() - view_label.width() / 2), 0, view_label.width(), view_label.height())
|
||||||
|
|
||||||
|
view_check_widget = QWidget(view_widget)
|
||||||
|
view_check_widget.setObjectName("viewCheckWidget")
|
||||||
|
view_check_widget.setGeometry(view_label.geometry().x() + 145, 170, 240, 240)
|
||||||
|
|
||||||
|
# view_widget = QWidget()
|
||||||
|
# view_widget.setObjectName("viewWidget")
|
||||||
|
# view_widget.setFixedWidth(1000)
|
||||||
|
# view_widget.setFixedHeight(450)
|
||||||
|
# picture_widget = QWidget()
|
||||||
|
# picture_widget.setObjectName("pictureWidget")
|
||||||
|
# picture_widget.setFixedHeight(450)
|
||||||
|
|
||||||
|
tool_widget = QWidget()
|
||||||
|
tool_widget.setObjectName("toolWidget")
|
||||||
|
tool_widget.setFixedWidth(150)
|
||||||
|
tool_widget.setFixedHeight(450)
|
||||||
|
|
||||||
|
tool_layout = QVBoxLayout()
|
||||||
|
tool_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
tool_layout.setSpacing(0)
|
||||||
|
|
||||||
|
self.start_adjust_button = QPushButton()
|
||||||
|
self.start_adjust_button.setObjectName("startAdjustButton")
|
||||||
|
self.start_adjust_button.setText("开始校准")
|
||||||
|
self.start_adjust_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.start_adjust_button.clicked.connect(self.start_adjust)
|
||||||
|
|
||||||
|
self.start_check_button = QPushButton()
|
||||||
|
self.start_check_button.setObjectName("startCheckButton")
|
||||||
|
self.start_check_button.setEnabled(False)
|
||||||
|
self.start_check_button.setText("开始检测")
|
||||||
|
self.start_check_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.start_check_button.clicked.connect(self.start_check)
|
||||||
|
|
||||||
|
self.stop_check_button = QPushButton()
|
||||||
|
self.stop_check_button.setObjectName("stopCheckButton")
|
||||||
|
self.stop_check_button.setEnabled(False)
|
||||||
|
self.stop_check_button.setText("停止检测")
|
||||||
|
self.stop_check_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.stop_check_button.clicked.connect(self.stop_check)
|
||||||
|
|
||||||
|
self.stop_task_button = QPushButton()
|
||||||
|
self.stop_task_button.setObjectName("stopTaskButton")
|
||||||
|
self.stop_task_button.setText("结束任务")
|
||||||
|
self.stop_task_button.setFixedHeight(self.ratio * 42)
|
||||||
|
self.stop_task_button.clicked.connect(self.reject)
|
||||||
|
|
||||||
|
tool_widget.setLayout(tool_layout)
|
||||||
|
|
||||||
|
spacer = QSpacerItem(10, 10, QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||||
|
tool_layout.addItem(spacer)
|
||||||
|
tool_layout.addWidget(self.start_adjust_button)
|
||||||
|
tool_layout.addSpacing(20)
|
||||||
|
tool_layout.addWidget(self.start_check_button)
|
||||||
|
tool_layout.addSpacing(20)
|
||||||
|
tool_layout.addWidget(self.stop_check_button)
|
||||||
|
tool_layout.addSpacing(20)
|
||||||
|
tool_layout.addWidget(self.stop_task_button)
|
||||||
|
tool_layout.addSpacing(20)
|
||||||
|
|
||||||
|
content_layout = QHBoxLayout()
|
||||||
|
content_layout.setContentsMargins(0, 0, 0, 20)
|
||||||
|
content_layout.setSpacing(0)
|
||||||
|
content_layout.addWidget(view_widget)
|
||||||
|
content_layout.addSpacing(20)
|
||||||
|
content_layout.addWidget(picture_label)
|
||||||
|
content_layout.addSpacing(20)
|
||||||
|
content_layout.addWidget(tool_widget)
|
||||||
|
content_widget.setLayout(content_layout)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
layout.addWidget(map_widget)
|
||||||
|
layout.addSpacing(5)
|
||||||
|
layout.addWidget(legend_widget)
|
||||||
|
layout.addSpacing(20)
|
||||||
|
layout.addWidget(content_widget)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
map_area = QScrollArea()
|
||||||
|
map_area.setObjectName("mapArea")
|
||||||
|
map_area.setWidgetResizable(True)
|
||||||
|
map_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||||
|
map_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
map_widget.setLayout(QGridLayout())
|
||||||
|
map_widget.layout().setContentsMargins(0, 0, 0, 0)
|
||||||
|
map_widget.layout().setSpacing(0)
|
||||||
|
map_widget.layout().addWidget(map_area)
|
||||||
|
map_area.setWidget(self.bim_widget)
|
||||||
|
|
||||||
|
def start_adjust(self):
|
||||||
|
self.start_adjust_button.setEnabled(False)
|
||||||
|
self.start_check_button.setEnabled(True)
|
||||||
|
self.stop_check_button.setEnabled(False)
|
||||||
|
|
||||||
|
def start_check(self):
|
||||||
|
self.start_adjust_button.setEnabled(False)
|
||||||
|
self.start_check_button.setEnabled(False)
|
||||||
|
self.stop_check_button.setEnabled(True)
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
for item in self.embed_widgets:
|
||||||
|
if item.objectName() == 'STD': continue
|
||||||
|
if self.check_widget.geometry().contains(item.rect):
|
||||||
|
item.setIndex(str(self.checkIndex))
|
||||||
|
if index % 2 == 0:
|
||||||
|
item.setChecked()
|
||||||
|
else:
|
||||||
|
item.setError()
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
def stop_check(self):
|
||||||
|
self.start_adjust_button.setEnabled(True)
|
||||||
|
self.start_check_button.setEnabled(False)
|
||||||
|
self.stop_check_button.setEnabled(False)
|
||||||
|
self.checkIndex += 1
|