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
|