1、update

This commit is contained in:
熊玮 2024-11-21 11:39:52 +08:00
commit fb543a3b0d
78 changed files with 6795 additions and 0 deletions

403
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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>

View 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>

View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
{
"files.associations": {
"type_traits": "cpp"
},
"cmake.ignoreCMakeListsMissing": true
}

15
README.MD Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

BIN
assets/icon_analyse.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/icon_bim.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

BIN
assets/icon_cali.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
assets/icon_charging.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/icon_com.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
assets/icon_device.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
assets/icon_setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

BIN
assets/icon_task.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

BIN
assets/img1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 MiB

BIN
assets/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

0
components/__init__.py Normal file
View File

864
components/camera.py Normal file
View 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
View 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
View 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")

View 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
View 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-normal2-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
View 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
View 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
View 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
View 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
View 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
View File

16
core/config.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

67
device/mv/ImageConvert.py Normal file
View 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

File diff suppressed because it is too large Load Diff

0
device/mv/__init__.py Normal file
View File

BIN
image.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 MiB

177
main.py Normal file
View 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
View File

6
model/base_model.py Normal file
View File

@ -0,0 +1,6 @@
from PyQt5.QtCore import QObject, pyqtSignal
class DataModel(QObject):
# 定义一个信号,当数据变化时发射该信号
property_changed = pyqtSignal(str, object) # 接受属性名称和新值

11
req.txt Normal file
View 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
View 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'))
);

View 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
View 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
View 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
View 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
View 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))

BIN
ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

29
util/__init__.py Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

275
vendors/livox/livox_sdk_wrapper.cpp vendored Normal file
View 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
View 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
View File

133
widget/device.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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