device-protocol-sdk 1.2.2__tar.gz → 1.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/PKG-INFO +31 -31
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/README.md +17 -17
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/abstract_device.py +216 -216
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/model/action_item.py +11 -11
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/model/device_key.py +7 -7
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/model/device_status.py +35 -35
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/pusher.py +724 -584
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk.egg-info/PKG-INFO +31 -31
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/setup.cfg +4 -4
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/setup.py +23 -23
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/__init__.py +0 -0
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/device_pb2.py +0 -0
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/device_pb2_grpc.py +0 -0
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/model/__init__.py +0 -0
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk.egg-info/SOURCES.txt +0 -0
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk.egg-info/dependency_links.txt +0 -0
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk.egg-info/requires.txt +0 -0
- {device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk.egg-info/top_level.txt +0 -0
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: device_protocol_sdk
|
|
3
|
-
Version: 1.2.
|
|
4
|
-
Summary: 无人设备协议开发SDK
|
|
5
|
-
Author: fuhl
|
|
6
|
-
Requires-Python: >=3.8
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: grpcio>=1.48.2
|
|
9
|
-
Requires-Dist: grpcio-tools>=1.48.2
|
|
10
|
-
Requires-Dist: paho-mqtt>=1.6.1
|
|
11
|
-
Requires-Dist: pydantic>=1.9.0
|
|
12
|
-
Requires-Dist: websockets>=10.0
|
|
13
|
-
|
|
14
|
-
# Device Protocol SDK
|
|
15
|
-
|
|
16
|
-
无人机设备协议开发工具包,提供标准化接口实现多类型无人机设备的快速接入。
|
|
17
|
-
|
|
18
|
-
[](https://pypi.org/project/device-protocol-sdk/)
|
|
19
|
-
[](https://pypi.org/project/device-protocol-sdk/)
|
|
20
|
-
|
|
21
|
-
## 功能特性
|
|
22
|
-
|
|
23
|
-
- 🛠️ **协议无关抽象**:统一各类无人机设备的控制接口
|
|
24
|
-
- 🔌 **连接池管理**:自动维护设备连接,支持多设备并行控制
|
|
25
|
-
- 🚀 **实时状态推送**:内置WebSocket状态推送工具
|
|
26
|
-
- 🔍 **动态协议发现**:运行时自动加载用户协议实现
|
|
27
|
-
|
|
28
|
-
## 安装
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
pip install device-protocol-sdk
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: device_protocol_sdk
|
|
3
|
+
Version: 1.2.4
|
|
4
|
+
Summary: 无人设备协议开发SDK
|
|
5
|
+
Author: fuhl
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: grpcio>=1.48.2
|
|
9
|
+
Requires-Dist: grpcio-tools>=1.48.2
|
|
10
|
+
Requires-Dist: paho-mqtt>=1.6.1
|
|
11
|
+
Requires-Dist: pydantic>=1.9.0
|
|
12
|
+
Requires-Dist: websockets>=10.0
|
|
13
|
+
|
|
14
|
+
# Device Protocol SDK
|
|
15
|
+
|
|
16
|
+
无人机设备协议开发工具包,提供标准化接口实现多类型无人机设备的快速接入。
|
|
17
|
+
|
|
18
|
+
[](https://pypi.org/project/device-protocol-sdk/)
|
|
19
|
+
[](https://pypi.org/project/device-protocol-sdk/)
|
|
20
|
+
|
|
21
|
+
## 功能特性
|
|
22
|
+
|
|
23
|
+
- 🛠️ **协议无关抽象**:统一各类无人机设备的控制接口
|
|
24
|
+
- 🔌 **连接池管理**:自动维护设备连接,支持多设备并行控制
|
|
25
|
+
- 🚀 **实时状态推送**:内置WebSocket状态推送工具
|
|
26
|
+
- 🔍 **动态协议发现**:运行时自动加载用户协议实现
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install device-protocol-sdk
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
# Device Protocol SDK
|
|
2
|
-
|
|
3
|
-
无人机设备协议开发工具包,提供标准化接口实现多类型无人机设备的快速接入。
|
|
4
|
-
|
|
5
|
-
[](https://pypi.org/project/device-protocol-sdk/)
|
|
6
|
-
[](https://pypi.org/project/device-protocol-sdk/)
|
|
7
|
-
|
|
8
|
-
## 功能特性
|
|
9
|
-
|
|
10
|
-
- 🛠️ **协议无关抽象**:统一各类无人机设备的控制接口
|
|
11
|
-
- 🔌 **连接池管理**:自动维护设备连接,支持多设备并行控制
|
|
12
|
-
- 🚀 **实时状态推送**:内置WebSocket状态推送工具
|
|
13
|
-
- 🔍 **动态协议发现**:运行时自动加载用户协议实现
|
|
14
|
-
|
|
15
|
-
## 安装
|
|
16
|
-
|
|
17
|
-
```bash
|
|
1
|
+
# Device Protocol SDK
|
|
2
|
+
|
|
3
|
+
无人机设备协议开发工具包,提供标准化接口实现多类型无人机设备的快速接入。
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/device-protocol-sdk/)
|
|
6
|
+
[](https://pypi.org/project/device-protocol-sdk/)
|
|
7
|
+
|
|
8
|
+
## 功能特性
|
|
9
|
+
|
|
10
|
+
- 🛠️ **协议无关抽象**:统一各类无人机设备的控制接口
|
|
11
|
+
- 🔌 **连接池管理**:自动维护设备连接,支持多设备并行控制
|
|
12
|
+
- 🚀 **实时状态推送**:内置WebSocket状态推送工具
|
|
13
|
+
- 🔍 **动态协议发现**:运行时自动加载用户协议实现
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
18
|
pip install device-protocol-sdk
|
{device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/abstract_device.py
RENAMED
|
@@ -1,217 +1,217 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
import threading
|
|
3
|
-
import json,uuid,time
|
|
4
|
-
from typing import List
|
|
5
|
-
from collections import defaultdict
|
|
6
|
-
from .model.device_key import DeviceKey
|
|
7
|
-
from .model.action_item import ActionItem
|
|
8
|
-
from .model.device_status import DeviceStatus,MessageType,MessageLevel
|
|
9
|
-
from typing import Dict, Any, Optional
|
|
10
|
-
import grpc
|
|
11
|
-
from . import device_pb2
|
|
12
|
-
|
|
13
|
-
from contextlib import contextmanager
|
|
14
|
-
from typing import Generator
|
|
15
|
-
import logging
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AbstractDevice(ABC):
|
|
23
|
-
_status_cache: Dict[DeviceKey, dict] = {}
|
|
24
|
-
_connection_pool: Dict[DeviceKey, Any] = {} # 类级连接池
|
|
25
|
-
_lock = threading.RLock()
|
|
26
|
-
|
|
27
|
-
_monitor_flags = defaultdict(bool) # 状态监控标志
|
|
28
|
-
_monitor_futures = {}
|
|
29
|
-
_lock = threading.Lock() # 线程安全锁
|
|
30
|
-
_status_lock = threading.RLock() # 新增:状态读取可重入锁
|
|
31
|
-
|
|
32
|
-
def __init__(self):
|
|
33
|
-
self._grpc_stub = None
|
|
34
|
-
self._current_mission_id = None
|
|
35
|
-
|
|
36
|
-
def set_grpc_stub(self, grpc_stub):
|
|
37
|
-
"""设置gRPC stub用于发送消息"""
|
|
38
|
-
self._grpc_stub = grpc_stub
|
|
39
|
-
|
|
40
|
-
def set_current_mission_id(self, mission_id: str):
|
|
41
|
-
"""设置当前任务的mission_id"""
|
|
42
|
-
self._current_mission_id = mission_id
|
|
43
|
-
|
|
44
|
-
@contextmanager
|
|
45
|
-
def mission_context(self, mission_id: str) -> Generator[None, None, None]:
|
|
46
|
-
"""为任务提供独立的上下文"""
|
|
47
|
-
original_mission_id = getattr(self, '_current_mission_id', None)
|
|
48
|
-
try:
|
|
49
|
-
self._current_mission_id = mission_id
|
|
50
|
-
yield
|
|
51
|
-
finally:
|
|
52
|
-
self._current_mission_id = original_mission_id
|
|
53
|
-
def send_message(self, device_id: int, message_type: MessageType, content: str,level:MessageLevel,
|
|
54
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
55
|
-
mission_id: Optional[str] = None) -> bool:
|
|
56
|
-
"""
|
|
57
|
-
通过gRPC向服务端发送消息
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
message_type: 消息类型
|
|
61
|
-
content: 消息内容
|
|
62
|
-
metadata: 附加元数据
|
|
63
|
-
mission_id: 任务ID,如果为None则使用当前任务ID
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
bool: 发送是否成功
|
|
67
|
-
"""
|
|
68
|
-
target_mission_id = mission_id or self._current_mission_id
|
|
69
|
-
if not target_mission_id:
|
|
70
|
-
logger.warning("未提供mission_id且当前无任务上下文")
|
|
71
|
-
return False
|
|
72
|
-
|
|
73
|
-
if not self._grpc_stub:
|
|
74
|
-
logger.warning("gRPC stub未设置,无法发送消息")
|
|
75
|
-
return False
|
|
76
|
-
|
|
77
|
-
# 确定mission_id
|
|
78
|
-
target_mission_id = mission_id or self._current_mission_id
|
|
79
|
-
if not target_mission_id:
|
|
80
|
-
logger.warning("未提供mission_id且当前无任务ID,无法发送消息")
|
|
81
|
-
return False
|
|
82
|
-
|
|
83
|
-
try:
|
|
84
|
-
# 构建gRPC消息
|
|
85
|
-
device_message = device_pb2.DeviceMessage(
|
|
86
|
-
mission_id=target_mission_id,
|
|
87
|
-
message_type=message_type.value,
|
|
88
|
-
message_level=level.value,
|
|
89
|
-
content=content,
|
|
90
|
-
timestamp=int(time.time()),
|
|
91
|
-
metadata=json.dumps(metadata) if metadata else "",
|
|
92
|
-
device_id=int(device_id)
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
# 发送消息(假设服务端有ReceiveDeviceMessage方法)
|
|
96
|
-
self._grpc_stub.ReceiveDeviceMessage(device_message)
|
|
97
|
-
|
|
98
|
-
except grpc.RpcError as e:
|
|
99
|
-
logger.error(f"gRPC消息发送失败: {e}")
|
|
100
|
-
return False
|
|
101
|
-
except Exception as e:
|
|
102
|
-
logger.error(f"发送消息时发生异常: {e}")
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
def send_text_message(self, device_id: int, text: str,level:MessageLevel, **kwargs) -> bool:
|
|
106
|
-
"""发送纯文本消息的快捷方法"""
|
|
107
|
-
return self.send_message(device_id,MessageType.TEXT, text,level, **kwargs)
|
|
108
|
-
|
|
109
|
-
def send_image_message(self, device_id: int, image_data_or_url: str,level:MessageLevel, **kwargs) -> bool:
|
|
110
|
-
"""发送图像消息的快捷方法"""
|
|
111
|
-
metadata = kwargs.pop('metadata', {})
|
|
112
|
-
if 'description' not in metadata:
|
|
113
|
-
metadata['description'] = '设备上传的图像'
|
|
114
|
-
return self.send_message(device_id,MessageType.IMAGE, image_data_or_url,level, metadata, **kwargs)
|
|
115
|
-
|
|
116
|
-
def send_video_url_message(self, device_id: int, video_url: str,level:MessageLevel, **kwargs) -> bool:
|
|
117
|
-
"""发送视频URL消息的快捷方法"""
|
|
118
|
-
metadata = kwargs.pop('metadata', {})
|
|
119
|
-
if 'description' not in metadata:
|
|
120
|
-
metadata['description'] = '设备视频流地址'
|
|
121
|
-
return self.send_message(device_id,MessageType.VIDEO_URL, video_url,level, metadata, **kwargs)
|
|
122
|
-
|
|
123
|
-
@property
|
|
124
|
-
@abstractmethod
|
|
125
|
-
def protocol_name(self) -> str:
|
|
126
|
-
pass
|
|
127
|
-
|
|
128
|
-
def is_cluster(self):
|
|
129
|
-
return 0
|
|
130
|
-
|
|
131
|
-
@abstractmethod
|
|
132
|
-
def get_action_list(self) -> List[ActionItem]:
|
|
133
|
-
pass
|
|
134
|
-
def connect(self, device_key: DeviceKey) -> Any:
|
|
135
|
-
"""带设备ID的连接池实现"""
|
|
136
|
-
with self._lock:
|
|
137
|
-
if device_key not in self._connection_pool:
|
|
138
|
-
is_connect,client = self._create_client(device_key.device_id,device_key.connection_str)
|
|
139
|
-
if is_connect:
|
|
140
|
-
self._connection_pool[device_key] = client
|
|
141
|
-
|
|
142
|
-
def disconnect(self, device_key: DeviceKey):
|
|
143
|
-
with self._lock:
|
|
144
|
-
if device_key in self._connection_pool:
|
|
145
|
-
self._close_client(self._connection_pool[device_key],device_key.device_id,device_key.connection_str)
|
|
146
|
-
# 清空连接池中对应device_key的数据
|
|
147
|
-
del self._connection_pool[device_key]
|
|
148
|
-
logger.info(f"已从连接池中移除设备 {device_key}")
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def is_connected(self, device_key: DeviceKey) -> bool:
|
|
152
|
-
with self._lock:
|
|
153
|
-
if conn := self._connection_pool.get(device_key):
|
|
154
|
-
return True
|
|
155
|
-
return False
|
|
156
|
-
|
|
157
|
-
@abstractmethod
|
|
158
|
-
def _create_client(self,device_id:int, connection_str: str) -> tuple[bool, Any]:
|
|
159
|
-
pass
|
|
160
|
-
|
|
161
|
-
@abstractmethod
|
|
162
|
-
def _close_client(self,client, device_id: int, connection_str: str) -> Any:
|
|
163
|
-
pass
|
|
164
|
-
|
|
165
|
-
def get_device_status(self,client,device_id:str,connection_str:str) -> DeviceStatus:
|
|
166
|
-
pass
|
|
167
|
-
|
|
168
|
-
def get_device_status_list(self,client,device_id:str,connection_str:str) -> List[DeviceStatus]:
|
|
169
|
-
pass
|
|
170
|
-
|
|
171
|
-
def _ensure_connection(self, device_key: DeviceKey) -> Any:
|
|
172
|
-
"""确保设备连接存在,如果不存在则创建"""
|
|
173
|
-
client = self._connection_pool.get(device_key)
|
|
174
|
-
if client is not None:
|
|
175
|
-
return client
|
|
176
|
-
|
|
177
|
-
# 尝试连接
|
|
178
|
-
success, result = self.connect(device_key)
|
|
179
|
-
if not success:
|
|
180
|
-
raise ConnectionError(f"无法连接设备 {device_key.device_id}: {result}")
|
|
181
|
-
|
|
182
|
-
client = self._connection_pool.get(device_key)
|
|
183
|
-
if client is None:
|
|
184
|
-
raise ConnectionError(f"连接已建立但客户端对象缺失: {device_key.device_id}")
|
|
185
|
-
|
|
186
|
-
return client
|
|
187
|
-
|
|
188
|
-
def get_status(self, device_id: str, connection_str: str) -> DeviceStatus:
|
|
189
|
-
my_device = DeviceKey(device_id=device_id, connection_str=connection_str)
|
|
190
|
-
|
|
191
|
-
# 确保连接存在
|
|
192
|
-
client = self._ensure_connection(my_device)
|
|
193
|
-
is_cluster = self.is_cluster()
|
|
194
|
-
if is_cluster == 0:
|
|
195
|
-
return [self.get_device_status(client, device_id, connection_str)]
|
|
196
|
-
else:
|
|
197
|
-
return self.get_device_status_list(client, device_id, connection_str)
|
|
198
|
-
|
|
199
|
-
def excute_command(self,device_id:str,connection_str:str, command: str, params: Dict[str, Any],
|
|
200
|
-
mission_id: str):
|
|
201
|
-
my_device = DeviceKey(device_id=device_id, connection_str=connection_str)
|
|
202
|
-
this_client = self._ensure_connection(my_device)
|
|
203
|
-
with self.mission_context(mission_id):
|
|
204
|
-
self.execute(this_client,device_id,connection_str,command,params)
|
|
205
|
-
|
|
206
|
-
@abstractmethod
|
|
207
|
-
def execute(self,client,device_id:int,connection_str:str, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
208
|
-
"""执行控制指令"""
|
|
209
|
-
|
|
210
|
-
def to_json(self):
|
|
211
|
-
"""序列化设备信息"""
|
|
212
|
-
actions = [item.dict() for item in self.get_action_list()]
|
|
213
|
-
return json.dumps({
|
|
214
|
-
"protocol": self.protocol_name,
|
|
215
|
-
"action_list": actions,
|
|
216
|
-
"is_cluster": self.is_cluster()
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import threading
|
|
3
|
+
import json,uuid,time
|
|
4
|
+
from typing import List
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from .model.device_key import DeviceKey
|
|
7
|
+
from .model.action_item import ActionItem
|
|
8
|
+
from .model.device_status import DeviceStatus,MessageType,MessageLevel
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
import grpc
|
|
11
|
+
from . import device_pb2
|
|
12
|
+
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from typing import Generator
|
|
15
|
+
import logging
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AbstractDevice(ABC):
|
|
23
|
+
_status_cache: Dict[DeviceKey, dict] = {}
|
|
24
|
+
_connection_pool: Dict[DeviceKey, Any] = {} # 类级连接池
|
|
25
|
+
_lock = threading.RLock()
|
|
26
|
+
|
|
27
|
+
_monitor_flags = defaultdict(bool) # 状态监控标志
|
|
28
|
+
_monitor_futures = {}
|
|
29
|
+
_lock = threading.Lock() # 线程安全锁
|
|
30
|
+
_status_lock = threading.RLock() # 新增:状态读取可重入锁
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self._grpc_stub = None
|
|
34
|
+
self._current_mission_id = None
|
|
35
|
+
|
|
36
|
+
def set_grpc_stub(self, grpc_stub):
|
|
37
|
+
"""设置gRPC stub用于发送消息"""
|
|
38
|
+
self._grpc_stub = grpc_stub
|
|
39
|
+
|
|
40
|
+
def set_current_mission_id(self, mission_id: str):
|
|
41
|
+
"""设置当前任务的mission_id"""
|
|
42
|
+
self._current_mission_id = mission_id
|
|
43
|
+
|
|
44
|
+
@contextmanager
|
|
45
|
+
def mission_context(self, mission_id: str) -> Generator[None, None, None]:
|
|
46
|
+
"""为任务提供独立的上下文"""
|
|
47
|
+
original_mission_id = getattr(self, '_current_mission_id', None)
|
|
48
|
+
try:
|
|
49
|
+
self._current_mission_id = mission_id
|
|
50
|
+
yield
|
|
51
|
+
finally:
|
|
52
|
+
self._current_mission_id = original_mission_id
|
|
53
|
+
def send_message(self, device_id: int, message_type: MessageType, content: str,level:MessageLevel,
|
|
54
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
55
|
+
mission_id: Optional[str] = None) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
通过gRPC向服务端发送消息
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
message_type: 消息类型
|
|
61
|
+
content: 消息内容
|
|
62
|
+
metadata: 附加元数据
|
|
63
|
+
mission_id: 任务ID,如果为None则使用当前任务ID
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
bool: 发送是否成功
|
|
67
|
+
"""
|
|
68
|
+
target_mission_id = mission_id or self._current_mission_id
|
|
69
|
+
if not target_mission_id:
|
|
70
|
+
logger.warning("未提供mission_id且当前无任务上下文")
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
if not self._grpc_stub:
|
|
74
|
+
logger.warning("gRPC stub未设置,无法发送消息")
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
# 确定mission_id
|
|
78
|
+
target_mission_id = mission_id or self._current_mission_id
|
|
79
|
+
if not target_mission_id:
|
|
80
|
+
logger.warning("未提供mission_id且当前无任务ID,无法发送消息")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# 构建gRPC消息
|
|
85
|
+
device_message = device_pb2.DeviceMessage(
|
|
86
|
+
mission_id=target_mission_id,
|
|
87
|
+
message_type=message_type.value,
|
|
88
|
+
message_level=level.value,
|
|
89
|
+
content=content,
|
|
90
|
+
timestamp=int(time.time()),
|
|
91
|
+
metadata=json.dumps(metadata) if metadata else "",
|
|
92
|
+
device_id=int(device_id)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# 发送消息(假设服务端有ReceiveDeviceMessage方法)
|
|
96
|
+
self._grpc_stub.ReceiveDeviceMessage(device_message)
|
|
97
|
+
|
|
98
|
+
except grpc.RpcError as e:
|
|
99
|
+
logger.error(f"gRPC消息发送失败: {e}")
|
|
100
|
+
return False
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"发送消息时发生异常: {e}")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def send_text_message(self, device_id: int, text: str,level:MessageLevel, **kwargs) -> bool:
|
|
106
|
+
"""发送纯文本消息的快捷方法"""
|
|
107
|
+
return self.send_message(device_id,MessageType.TEXT, text,level, **kwargs)
|
|
108
|
+
|
|
109
|
+
def send_image_message(self, device_id: int, image_data_or_url: str,level:MessageLevel, **kwargs) -> bool:
|
|
110
|
+
"""发送图像消息的快捷方法"""
|
|
111
|
+
metadata = kwargs.pop('metadata', {})
|
|
112
|
+
if 'description' not in metadata:
|
|
113
|
+
metadata['description'] = '设备上传的图像'
|
|
114
|
+
return self.send_message(device_id,MessageType.IMAGE, image_data_or_url,level, metadata, **kwargs)
|
|
115
|
+
|
|
116
|
+
def send_video_url_message(self, device_id: int, video_url: str,level:MessageLevel, **kwargs) -> bool:
|
|
117
|
+
"""发送视频URL消息的快捷方法"""
|
|
118
|
+
metadata = kwargs.pop('metadata', {})
|
|
119
|
+
if 'description' not in metadata:
|
|
120
|
+
metadata['description'] = '设备视频流地址'
|
|
121
|
+
return self.send_message(device_id,MessageType.VIDEO_URL, video_url,level, metadata, **kwargs)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def protocol_name(self) -> str:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
def is_cluster(self):
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def get_action_list(self) -> List[ActionItem]:
|
|
133
|
+
pass
|
|
134
|
+
def connect(self, device_key: DeviceKey) -> Any:
|
|
135
|
+
"""带设备ID的连接池实现"""
|
|
136
|
+
with self._lock:
|
|
137
|
+
if device_key not in self._connection_pool:
|
|
138
|
+
is_connect,client = self._create_client(device_key.device_id,device_key.connection_str)
|
|
139
|
+
if is_connect:
|
|
140
|
+
self._connection_pool[device_key] = client
|
|
141
|
+
|
|
142
|
+
def disconnect(self, device_key: DeviceKey):
|
|
143
|
+
with self._lock:
|
|
144
|
+
if device_key in self._connection_pool:
|
|
145
|
+
self._close_client(self._connection_pool[device_key],device_key.device_id,device_key.connection_str)
|
|
146
|
+
# 清空连接池中对应device_key的数据
|
|
147
|
+
del self._connection_pool[device_key]
|
|
148
|
+
logger.info(f"已从连接池中移除设备 {device_key}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def is_connected(self, device_key: DeviceKey) -> bool:
|
|
152
|
+
with self._lock:
|
|
153
|
+
if conn := self._connection_pool.get(device_key):
|
|
154
|
+
return True
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
@abstractmethod
|
|
158
|
+
def _create_client(self,device_id:int, connection_str: str) -> tuple[bool, Any]:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
@abstractmethod
|
|
162
|
+
def _close_client(self,client, device_id: int, connection_str: str) -> Any:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
def get_device_status(self,client,device_id:str,connection_str:str) -> DeviceStatus:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def get_device_status_list(self,client,device_id:str,connection_str:str) -> List[DeviceStatus]:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def _ensure_connection(self, device_key: DeviceKey) -> Any:
|
|
172
|
+
"""确保设备连接存在,如果不存在则创建"""
|
|
173
|
+
client = self._connection_pool.get(device_key)
|
|
174
|
+
if client is not None:
|
|
175
|
+
return client
|
|
176
|
+
|
|
177
|
+
# 尝试连接
|
|
178
|
+
success, result = self.connect(device_key)
|
|
179
|
+
if not success:
|
|
180
|
+
raise ConnectionError(f"无法连接设备 {device_key.device_id}: {result}")
|
|
181
|
+
|
|
182
|
+
client = self._connection_pool.get(device_key)
|
|
183
|
+
if client is None:
|
|
184
|
+
raise ConnectionError(f"连接已建立但客户端对象缺失: {device_key.device_id}")
|
|
185
|
+
|
|
186
|
+
return client
|
|
187
|
+
|
|
188
|
+
def get_status(self, device_id: str, connection_str: str) -> DeviceStatus:
|
|
189
|
+
my_device = DeviceKey(device_id=device_id, connection_str=connection_str)
|
|
190
|
+
|
|
191
|
+
# 确保连接存在
|
|
192
|
+
client = self._ensure_connection(my_device)
|
|
193
|
+
is_cluster = self.is_cluster()
|
|
194
|
+
if is_cluster == 0:
|
|
195
|
+
return [self.get_device_status(client, device_id, connection_str)]
|
|
196
|
+
else:
|
|
197
|
+
return self.get_device_status_list(client, device_id, connection_str)
|
|
198
|
+
|
|
199
|
+
def excute_command(self,device_id:str,connection_str:str, command: str, params: Dict[str, Any],
|
|
200
|
+
mission_id: str):
|
|
201
|
+
my_device = DeviceKey(device_id=device_id, connection_str=connection_str)
|
|
202
|
+
this_client = self._ensure_connection(my_device)
|
|
203
|
+
with self.mission_context(mission_id):
|
|
204
|
+
self.execute(this_client,device_id,connection_str,command,params)
|
|
205
|
+
|
|
206
|
+
@abstractmethod
|
|
207
|
+
def execute(self,client,device_id:int,connection_str:str, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
208
|
+
"""执行控制指令"""
|
|
209
|
+
|
|
210
|
+
def to_json(self):
|
|
211
|
+
"""序列化设备信息"""
|
|
212
|
+
actions = [item.dict() for item in self.get_action_list()]
|
|
213
|
+
return json.dumps({
|
|
214
|
+
"protocol": self.protocol_name,
|
|
215
|
+
"action_list": actions,
|
|
216
|
+
"is_cluster": self.is_cluster()
|
|
217
217
|
}, ensure_ascii=False)
|
{device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/model/action_item.py
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# action_item.py
|
|
2
|
-
from dataclasses import dataclass, asdict
|
|
3
|
-
from typing import Dict, Any
|
|
4
|
-
@dataclass
|
|
5
|
-
class ActionItem:
|
|
6
|
-
name: str # 动作名称
|
|
7
|
-
command_type: str # 动作唯一标识
|
|
8
|
-
description: str # 描述
|
|
9
|
-
params: Dict[str, Any] # JSON Schema 或简单 dict 描述
|
|
10
|
-
|
|
11
|
-
def dict(self) -> Dict[str, Any]:
|
|
1
|
+
# action_item.py
|
|
2
|
+
from dataclasses import dataclass, asdict
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
@dataclass
|
|
5
|
+
class ActionItem:
|
|
6
|
+
name: str # 动作名称
|
|
7
|
+
command_type: str # 动作唯一标识
|
|
8
|
+
description: str # 描述
|
|
9
|
+
params: Dict[str, Any] # JSON Schema 或简单 dict 描述
|
|
10
|
+
|
|
11
|
+
def dict(self) -> Dict[str, Any]:
|
|
12
12
|
return asdict(self)
|
{device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/model/device_key.py
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# device_key.py
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
|
|
4
|
-
@dataclass(frozen=True)
|
|
5
|
-
class DeviceKey:
|
|
6
|
-
"""不可变的设备唯一标识"""
|
|
7
|
-
device_id: str # 业务唯一字符串
|
|
1
|
+
# device_key.py
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class DeviceKey:
|
|
6
|
+
"""不可变的设备唯一标识"""
|
|
7
|
+
device_id: str # 业务唯一字符串
|
|
8
8
|
connection_str: str # 连接参数(如IP/串口号)
|
{device_protocol_sdk-1.2.2 → device_protocol_sdk-1.2.4}/device_protocol_sdk/model/device_status.py
RENAMED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
# device_status.py
|
|
2
|
-
from typing import TypedDict, Optional
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
class DeviceStatus(TypedDict, total=False):
|
|
6
|
-
"""设备状态固定格式"""
|
|
7
|
-
is_lock: Optional[int] # 必填
|
|
8
|
-
heartbeat: Optional[int] # 必填
|
|
9
|
-
battery: Optional[float] # 必填
|
|
10
|
-
airspeed: Optional[float] # 必填
|
|
11
|
-
groundspeed: Optional[float] # 必填
|
|
12
|
-
yaw_degrees: Optional[float] # 必填
|
|
13
|
-
roll: Optional[float] # 必填
|
|
14
|
-
pitch: Optional[float] # 必填
|
|
15
|
-
yaw: Optional[float] # 必填
|
|
16
|
-
lat: Optional[float] # 必填
|
|
17
|
-
lon: Optional[float] # 必填
|
|
18
|
-
alt: Optional[float] # 必填
|
|
19
|
-
vzspeed: Optional[float] # 必填
|
|
20
|
-
height: Optional[float] # 必填
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class MessageType(Enum):
|
|
24
|
-
TEXT = "text" # 纯文本消息
|
|
25
|
-
IMAGE = "image" # 图像消息
|
|
26
|
-
VIDEO_URL = "video_url" # 视频流URL
|
|
27
|
-
PROGRESS = "progress" # 进度更新
|
|
28
|
-
STATUS = "status" # 状态更新
|
|
29
|
-
|
|
30
|
-
#消息级别: failed, warning, info, success
|
|
31
|
-
class MessageLevel(Enum):
|
|
32
|
-
FAILED = "failed"
|
|
33
|
-
WARNING = "warning"
|
|
34
|
-
INFO = "info"
|
|
35
|
-
SUCCESS = "success"
|
|
1
|
+
# device_status.py
|
|
2
|
+
from typing import TypedDict, Optional
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
class DeviceStatus(TypedDict, total=False):
|
|
6
|
+
"""设备状态固定格式"""
|
|
7
|
+
is_lock: Optional[int] # 必填
|
|
8
|
+
heartbeat: Optional[int] # 必填
|
|
9
|
+
battery: Optional[float] # 必填
|
|
10
|
+
airspeed: Optional[float] # 必填
|
|
11
|
+
groundspeed: Optional[float] # 必填
|
|
12
|
+
yaw_degrees: Optional[float] # 必填
|
|
13
|
+
roll: Optional[float] # 必填
|
|
14
|
+
pitch: Optional[float] # 必填
|
|
15
|
+
yaw: Optional[float] # 必填
|
|
16
|
+
lat: Optional[float] # 必填
|
|
17
|
+
lon: Optional[float] # 必填
|
|
18
|
+
alt: Optional[float] # 必填
|
|
19
|
+
vzspeed: Optional[float] # 必填
|
|
20
|
+
height: Optional[float] # 必填
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MessageType(Enum):
|
|
24
|
+
TEXT = "text" # 纯文本消息
|
|
25
|
+
IMAGE = "image" # 图像消息
|
|
26
|
+
VIDEO_URL = "video_url" # 视频流URL
|
|
27
|
+
PROGRESS = "progress" # 进度更新
|
|
28
|
+
STATUS = "status" # 状态更新
|
|
29
|
+
|
|
30
|
+
#消息级别: failed, warning, info, success
|
|
31
|
+
class MessageLevel(Enum):
|
|
32
|
+
FAILED = "failed"
|
|
33
|
+
WARNING = "warning"
|
|
34
|
+
INFO = "info"
|
|
35
|
+
SUCCESS = "success"
|