adar-api 1.1.1__py3-none-any.whl
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.
- adar_api/__init__.py +27 -0
- adar_api/adar.py +286 -0
- adar_api/coap_exceptions.py +17 -0
- adar_api/coap_observer.py +169 -0
- adar_api/coap_pointcloud.py +204 -0
- adar_api/coap_resources.py +11 -0
- adar_api/device_errors.py +32 -0
- adar_api/device_info.py +73 -0
- adar_api/device_status.py +74 -0
- adar_api/duration.py +50 -0
- adar_api/examples/__init__.py +5 -0
- adar_api/examples/foxglove_layout_ADAR.json +145 -0
- adar_api/examples/pointcloud_to_foxglove.py +114 -0
- adar_api/examples/utils.py +213 -0
- adar_api/network_config.py +162 -0
- adar_api/statistics.py +29 -0
- adar_api-1.1.1.dist-info/METADATA +435 -0
- adar_api-1.1.1.dist-info/RECORD +20 -0
- adar_api-1.1.1.dist-info/WHEEL +4 -0
- adar_api-1.1.1.dist-info/entry_points.txt +3 -0
adar_api/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .adar import Adar
|
|
2
|
+
from .coap_exceptions import CoapException, CoapErrorException
|
|
3
|
+
from .coap_observer import CoapObserver
|
|
4
|
+
from .coap_pointcloud import CoapPointCloud, Point, PointClassification
|
|
5
|
+
from .device_info import DeviceInfo
|
|
6
|
+
from .device_status import DeviceStatus, DeviceState, ZoneStatus
|
|
7
|
+
from .duration import Duration
|
|
8
|
+
from .network_config import NetworkConfig
|
|
9
|
+
from .statistics import Statistics
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DeviceStatus",
|
|
13
|
+
"DeviceState",
|
|
14
|
+
"ZoneStatus",
|
|
15
|
+
"CoapPointCloud",
|
|
16
|
+
"CoapObserver",
|
|
17
|
+
"CoapException",
|
|
18
|
+
"CoapErrorException",
|
|
19
|
+
"Point",
|
|
20
|
+
"PointClassification",
|
|
21
|
+
"NetworkConfig",
|
|
22
|
+
"Adar",
|
|
23
|
+
"DeviceInfo",
|
|
24
|
+
"Duration",
|
|
25
|
+
"Statistics",
|
|
26
|
+
"coap_resources",
|
|
27
|
+
]
|
adar_api/adar.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import AsyncGenerator
|
|
3
|
+
import struct
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
from aiocoap import Context, Message, GET, PUT, DELETE
|
|
7
|
+
|
|
8
|
+
from .coap_exceptions import CoapErrorException, CoapException
|
|
9
|
+
from .coap_pointcloud import CoapPointCloud
|
|
10
|
+
from .device_errors import DeviceErrors
|
|
11
|
+
from .device_info import DeviceInfo
|
|
12
|
+
from .device_status import DeviceStatus, DeviceState
|
|
13
|
+
from .network_config import NetworkConfig
|
|
14
|
+
from .statistics import Statistics
|
|
15
|
+
from .coap_resources import (
|
|
16
|
+
NETWORK_CONFIG_V0,
|
|
17
|
+
FACTORY_RESET_V0,
|
|
18
|
+
ERRORS_V0,
|
|
19
|
+
DEVICE_INFO_V0,
|
|
20
|
+
STATISTICS_V0,
|
|
21
|
+
OBSERVERS_V0,
|
|
22
|
+
POINTCLOUD_V0,
|
|
23
|
+
STATUS_V0,
|
|
24
|
+
TRANSMISSION_CODE_V0,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Adar:
|
|
29
|
+
"""A class representing the Adar sensor."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, ctx: Context, ip_address: str | None, device_tag: str | None = None):
|
|
32
|
+
"""Initialize an ADAR device connection.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
ctx: The CoAP context for communication
|
|
36
|
+
ip_address: The IP address of the ADAR
|
|
37
|
+
device_tag: Optional device tag for identification (defaults to "Adar")
|
|
38
|
+
"""
|
|
39
|
+
self.device_tag = device_tag or "Adar"
|
|
40
|
+
self.ip_address = ip_address
|
|
41
|
+
self.ctx = ctx
|
|
42
|
+
# Reduce COAP Log details:
|
|
43
|
+
ctx.log.setLevel(logging.INFO)
|
|
44
|
+
self.logger = logging.getLogger(device_tag)
|
|
45
|
+
|
|
46
|
+
async def observe_point_cloud(
|
|
47
|
+
self, keep_running: bool = False, msg_count: int | None = None
|
|
48
|
+
) -> AsyncGenerator[CoapPointCloud, None]:
|
|
49
|
+
"""Observe the ADAR Point cloud.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
keep_running: If True, the observer will ignore errors and attempt to automatically reconnect.
|
|
53
|
+
msg_count: If not None, the observer will be stopped once the requested number of messages are received.
|
|
54
|
+
|
|
55
|
+
Yields:
|
|
56
|
+
CoapPointCloud: Decoded point cloud instances.
|
|
57
|
+
"""
|
|
58
|
+
# Need to import here in order to avoid circular dependency
|
|
59
|
+
from .coap_observer import CoapObserver
|
|
60
|
+
|
|
61
|
+
async with CoapObserver(self, POINTCLOUD_V0) as observer:
|
|
62
|
+
async for response in observer.messages(keep_running, msg_count):
|
|
63
|
+
try:
|
|
64
|
+
point_cloud = CoapPointCloud(response)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
if keep_running:
|
|
67
|
+
self.logger.warning(
|
|
68
|
+
"Failed to decode point cloud: %s. Ignoring because keep_running is True",
|
|
69
|
+
e,
|
|
70
|
+
)
|
|
71
|
+
continue
|
|
72
|
+
raise
|
|
73
|
+
yield point_cloud
|
|
74
|
+
|
|
75
|
+
async def get_point_cloud(self) -> CoapPointCloud:
|
|
76
|
+
"""Get one point cloud.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
CoapPointCloud: A single point cloud from the device.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
CoapException: If no response is received from the point cloud observer.
|
|
83
|
+
"""
|
|
84
|
+
async for response in self.observe_point_cloud(msg_count=1):
|
|
85
|
+
return response
|
|
86
|
+
|
|
87
|
+
msg = "No response from point cloud info observer"
|
|
88
|
+
raise CoapException(msg)
|
|
89
|
+
|
|
90
|
+
async def get_network_config(self) -> NetworkConfig:
|
|
91
|
+
"""Read the network config.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
NetworkConfig: The current network configuration of the device.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If the response payload cannot be decoded into NetworkConfig.
|
|
98
|
+
struct.error: If there's an error in the binary data structure.
|
|
99
|
+
"""
|
|
100
|
+
uri = f"coap://{self.ip_address}{NETWORK_CONFIG_V0}"
|
|
101
|
+
request = Message(code=GET, uri=uri)
|
|
102
|
+
response = await self.send_request(request)
|
|
103
|
+
try:
|
|
104
|
+
return NetworkConfig(data=response.payload)
|
|
105
|
+
except (ValueError, struct.error) as e:
|
|
106
|
+
self.logger.exception(f"Failed to decode {response.payload} into NetworkConfig: {e}")
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
async def set_network_config(self, network_config: NetworkConfig) -> None:
|
|
110
|
+
"""Set the network config.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
network_config: The network configuration to apply to the device.
|
|
114
|
+
|
|
115
|
+
Note:
|
|
116
|
+
The device will reboot to apply the new network configuration.
|
|
117
|
+
"""
|
|
118
|
+
uri = f"coap://{self.ip_address}{NETWORK_CONFIG_V0}"
|
|
119
|
+
request = Message(code=PUT, uri=uri, payload=network_config.encode())
|
|
120
|
+
await self.send_request(request)
|
|
121
|
+
self.logger.warning("The device will now reboot to apply the new network config!!!")
|
|
122
|
+
|
|
123
|
+
async def factory_reset(self) -> None:
|
|
124
|
+
"""Send factory reset command.
|
|
125
|
+
|
|
126
|
+
Note:
|
|
127
|
+
The device will reboot to apply factory settings.
|
|
128
|
+
"""
|
|
129
|
+
uri = f"coap://{self.ip_address}{FACTORY_RESET_V0}"
|
|
130
|
+
request = Message(code=PUT, uri=uri, payload="")
|
|
131
|
+
await self.send_request(request)
|
|
132
|
+
self.logger.warning("The device will now reboot to apply factory settings!!!")
|
|
133
|
+
|
|
134
|
+
async def get_device_errors(self) -> DeviceErrors:
|
|
135
|
+
"""Read the device errors.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
DeviceErrors: The current device error information.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If the response payload cannot be decoded into DeviceErrors.
|
|
142
|
+
struct.error: If there's an error in the binary data structure.
|
|
143
|
+
"""
|
|
144
|
+
uri = f"coap://{self.ip_address}{ERRORS_V0}"
|
|
145
|
+
request = Message(code=GET, uri=uri)
|
|
146
|
+
response = await self.send_request(request)
|
|
147
|
+
try:
|
|
148
|
+
return DeviceErrors(data=response.payload)
|
|
149
|
+
except (ValueError, struct.error) as e:
|
|
150
|
+
self.logger.exception(f"Failed to decode {response.payload} into DeviceErrors: {e}")
|
|
151
|
+
raise
|
|
152
|
+
|
|
153
|
+
async def get_device_info(self) -> DeviceInfo:
|
|
154
|
+
"""Read the device info.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
DeviceInfo: Information about the device including identification, name, and firmware version.
|
|
158
|
+
"""
|
|
159
|
+
uri = f"coap://{self.ip_address}{DEVICE_INFO_V0}"
|
|
160
|
+
request = Message(code=GET, uri=uri)
|
|
161
|
+
response = await self.send_request(request)
|
|
162
|
+
return DeviceInfo(data=response.payload)
|
|
163
|
+
|
|
164
|
+
async def get_status(self) -> DeviceStatus:
|
|
165
|
+
"""Read the status of the ADAR.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
DeviceStatus: The current status of the device including zone status, device state, and error information.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: If the response payload cannot be decoded into DeviceStatus.
|
|
172
|
+
AssertionError: If the response payload has an unexpected format.
|
|
173
|
+
"""
|
|
174
|
+
uri = f"coap://{self.ip_address}{STATUS_V0}"
|
|
175
|
+
request = Message(code=GET, uri=uri)
|
|
176
|
+
response = await self.send_request(request)
|
|
177
|
+
try:
|
|
178
|
+
status = DeviceStatus(response.payload)
|
|
179
|
+
# In the public API we have a Python IntEnum DeviceState which
|
|
180
|
+
# maps the integer DeviceState values reported. Here we do
|
|
181
|
+
# a translation from the raw integer value returned by the
|
|
182
|
+
# device to a more ergonomic enum type.
|
|
183
|
+
status.device_state = DeviceState(status.device_state)
|
|
184
|
+
self.logger.debug(f"Got status bytes {status}")
|
|
185
|
+
except (ValueError, AssertionError) as e:
|
|
186
|
+
self.logger.exception(f"Failed to decode {response.payload} into DeviceStatus: {e}")
|
|
187
|
+
raise
|
|
188
|
+
else:
|
|
189
|
+
return status
|
|
190
|
+
|
|
191
|
+
async def get_statistics(self) -> Statistics:
|
|
192
|
+
"""Read the statistics.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Statistics: Statistical information about the device.
|
|
196
|
+
"""
|
|
197
|
+
uri = f"coap://{self.ip_address}{STATISTICS_V0}"
|
|
198
|
+
request = Message(code=GET, uri=uri)
|
|
199
|
+
response = await self.send_request(request)
|
|
200
|
+
return Statistics(data=response.payload)
|
|
201
|
+
|
|
202
|
+
async def get_transmission_code_id(self) -> int:
|
|
203
|
+
"""Read the transmission code ID of the ADAR.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
int: The transmission code ID (1, 2, 4, or 8).
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
CoapException: If the response payload is None or has incorrect length.
|
|
210
|
+
"""
|
|
211
|
+
uri = f"coap://{self.ip_address}{TRANSMISSION_CODE_V0}"
|
|
212
|
+
self.logger.info(f"Executing GET {uri}")
|
|
213
|
+
request = Message(code=GET, uri=uri)
|
|
214
|
+
response = await self.send_request(request)
|
|
215
|
+
if response.payload is None:
|
|
216
|
+
raise CoapException("Response payload should not be None")
|
|
217
|
+
if len(response.payload) != 1:
|
|
218
|
+
raise CoapException("Response payload should have one byte")
|
|
219
|
+
code_id = (
|
|
220
|
+
2 ** response.payload[0]
|
|
221
|
+
) # Decode code ID from payload byte. The encoded payload represents N where 2^N is the code ID.
|
|
222
|
+
self.logger.info(f"Got transmission code bytes {response.payload}, corresponding to code ID {code_id}")
|
|
223
|
+
return code_id
|
|
224
|
+
|
|
225
|
+
async def set_transmission_code_id(self, code_id: int) -> None:
|
|
226
|
+
"""Set the transmission code ID of the ADAR.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
code_id: The transmission code ID to set. Must be one of 1, 2, 4, or 8.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ValueError: If the code_id is not one of the valid values (1, 2, 4, 8).
|
|
233
|
+
CoapException: If the response payload is unexpected.
|
|
234
|
+
"""
|
|
235
|
+
if code_id not in (1, 2, 4, 8):
|
|
236
|
+
raise ValueError(
|
|
237
|
+
f"Invalid transmission code ID {code_id}. Must be one of 1, 2, 4 or 8",
|
|
238
|
+
)
|
|
239
|
+
encoded_code_id = int(math.log2(code_id)) # Encode code ID
|
|
240
|
+
uri = f"coap://{self.ip_address}{TRANSMISSION_CODE_V0}"
|
|
241
|
+
self.logger.info(f"Executing PUT {uri}")
|
|
242
|
+
request = Message(code=PUT, uri=uri, payload=encoded_code_id.to_bytes(1))
|
|
243
|
+
response = await self.send_request(request)
|
|
244
|
+
# Check for empty payload (success)
|
|
245
|
+
if response.payload in (b"", None):
|
|
246
|
+
self.logger.info("Transmission code set successfully")
|
|
247
|
+
else:
|
|
248
|
+
raise CoapException(f"Unexpected response payload: {response.payload}")
|
|
249
|
+
|
|
250
|
+
async def delete_observers(self) -> None:
|
|
251
|
+
"""Delete all registered observers on the device."""
|
|
252
|
+
uri = f"coap://{self.ip_address}{OBSERVERS_V0}"
|
|
253
|
+
request = Message(code=DELETE, uri=uri)
|
|
254
|
+
await self.send_request(request)
|
|
255
|
+
|
|
256
|
+
async def send_request(self, request: Message) -> Message:
|
|
257
|
+
"""Send a coap request to the ADAR.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
request: The CoAP message to send to the device.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Message: The response from the device.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
CoapErrorException: If the request fails or returns an error response.
|
|
267
|
+
AssertionError: If the IP address of the ADAR device has not been set.
|
|
268
|
+
"""
|
|
269
|
+
assert self.ip_address is not None, "The IP address of the ADAR device has not been set"
|
|
270
|
+
self.log_send_message(request)
|
|
271
|
+
response = await self.ctx.request(request).response
|
|
272
|
+
if response.code.is_successful():
|
|
273
|
+
return response
|
|
274
|
+
|
|
275
|
+
raise CoapErrorException(response=response)
|
|
276
|
+
|
|
277
|
+
def log_send_message(self, request: Message) -> None:
|
|
278
|
+
"""Log outgoing CoAP request details.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
request: The CoAP message being sent.
|
|
282
|
+
"""
|
|
283
|
+
msg = f"Sending request {request.code} {request.opt.uri_path}"
|
|
284
|
+
if request.opt.observe is not None:
|
|
285
|
+
msg += f" (observe={request.opt.observe})"
|
|
286
|
+
self.logger.debug(msg)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from aiocoap import Message
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CoapException(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CoapErrorException(CoapException):
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
"""Initialize CoapErrorException with optional CoAP response.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
*args: Variable length argument list passed to parent exception
|
|
14
|
+
**kwargs: Arbitrary keyword arguments, may include 'response' key for CoAP Message
|
|
15
|
+
"""
|
|
16
|
+
super().__init__(*args)
|
|
17
|
+
self.response: Message = kwargs.get("response")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
|
|
5
|
+
####################
|
|
6
|
+
# This is a hack to get the Self type from typing, but it's not available in Python 3.10
|
|
7
|
+
# TODO: Fix this or update to Pyhton 3.11
|
|
8
|
+
from typing import AsyncGenerator, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Self
|
|
12
|
+
else:
|
|
13
|
+
Self = "CoapObserver"
|
|
14
|
+
####################
|
|
15
|
+
|
|
16
|
+
from aiocoap import Message, GET, Context
|
|
17
|
+
from aiocoap.protocol import BlockwiseRequest, Request
|
|
18
|
+
|
|
19
|
+
from .adar import Adar
|
|
20
|
+
from .coap_exceptions import CoapErrorException
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Observe(IntEnum):
|
|
24
|
+
Observe = 0
|
|
25
|
+
NoObserve = 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoapObserver:
|
|
29
|
+
"""
|
|
30
|
+
Class to register as observer to a CoAP server and receive messages
|
|
31
|
+
|
|
32
|
+
Typical usage:
|
|
33
|
+
|
|
34
|
+
async with CoapObserver(adar, "/point_cloud/v0") as observer:
|
|
35
|
+
async for msg in observer.messages():
|
|
36
|
+
points = PointCloud(msg)
|
|
37
|
+
print(points)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, adar: Adar, path: str):
|
|
41
|
+
"""Initialize a CoAP observer for the given ADAR device and path.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
adar: The ADAR device instance to observe
|
|
45
|
+
path: The CoAP path to observe (e.g., "/point_cloud/v0")
|
|
46
|
+
"""
|
|
47
|
+
self._adar = adar
|
|
48
|
+
self.ipaddr = adar.ip_address
|
|
49
|
+
self.path = path
|
|
50
|
+
self._context: Context | None = None
|
|
51
|
+
self._cancelled = False
|
|
52
|
+
self._current_request = None
|
|
53
|
+
self.logger = logging.getLogger(f"{adar.device_tag}-Observe")
|
|
54
|
+
|
|
55
|
+
async def messages(self, keep_running: bool = False, msg_count: int | None = None) -> AsyncGenerator[bytes, None]:
|
|
56
|
+
"""Listen for messages and yield them as they arrive.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
keep_running: If True, the observer will attempt to keep running - i.e. ignore errors and keep trying to reconnect.
|
|
60
|
+
msg_count: The number of messages to receive before stopping. If None, the observer will run until cancelled.
|
|
61
|
+
|
|
62
|
+
Yields:
|
|
63
|
+
bytes: Raw message payload data
|
|
64
|
+
"""
|
|
65
|
+
await self._ensure_coap_context()
|
|
66
|
+
try:
|
|
67
|
+
cnt = 0
|
|
68
|
+
connection_attempts = 0
|
|
69
|
+
while msg_count != cnt and not self._cancelled:
|
|
70
|
+
if connection_attempts > 0:
|
|
71
|
+
self.logger.warning("Try to re-register observer and continue")
|
|
72
|
+
start_observe_message = self._make_get_request(Observe.Observe)
|
|
73
|
+
try:
|
|
74
|
+
(self._current_request, _response) = await self._send_message(start_observe_message)
|
|
75
|
+
except CoapErrorException as e:
|
|
76
|
+
self.logger.warning(f"Failed to connect to server: {e}")
|
|
77
|
+
if keep_running or connection_attempts < 10:
|
|
78
|
+
connection_attempts += 1
|
|
79
|
+
self.logger.warning("Trying again...")
|
|
80
|
+
continue
|
|
81
|
+
raise
|
|
82
|
+
|
|
83
|
+
pr_iter = aiter(self._current_request.observation)
|
|
84
|
+
while msg_count != cnt and not self._cancelled:
|
|
85
|
+
try:
|
|
86
|
+
obs = await asyncio.wait_for(anext(pr_iter), timeout=2)
|
|
87
|
+
if not obs.code.is_successful():
|
|
88
|
+
self.logger.error(f"Error: {obs.code} received")
|
|
89
|
+
if keep_running:
|
|
90
|
+
break
|
|
91
|
+
raise CoapErrorException(response=obs)
|
|
92
|
+
|
|
93
|
+
yield obs.payload
|
|
94
|
+
|
|
95
|
+
cnt += 1
|
|
96
|
+
if msg_count == cnt:
|
|
97
|
+
self.logger.info(f"Received {cnt} messages, stopping.")
|
|
98
|
+
break
|
|
99
|
+
except asyncio.TimeoutError:
|
|
100
|
+
if self._cancelled:
|
|
101
|
+
break
|
|
102
|
+
self.logger.error(f"Timeout waiting for {cnt} messages")
|
|
103
|
+
if keep_running:
|
|
104
|
+
break
|
|
105
|
+
raise
|
|
106
|
+
finally:
|
|
107
|
+
if not self._cancelled:
|
|
108
|
+
await self.stop()
|
|
109
|
+
|
|
110
|
+
async def stop(self):
|
|
111
|
+
"""Stop the observer and clean up resources."""
|
|
112
|
+
self._cancelled = True
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
# Cancel current request if it exists
|
|
116
|
+
if self._current_request is not None:
|
|
117
|
+
self._current_request.cancelled = True
|
|
118
|
+
|
|
119
|
+
# Deregister the observer only if context exists
|
|
120
|
+
if self._context is not None:
|
|
121
|
+
self.logger.info("De-registering observer")
|
|
122
|
+
stop_observe_message = self._make_get_request(Observe.NoObserve)
|
|
123
|
+
pr = self._context.request(stop_observe_message)
|
|
124
|
+
await pr.response
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.logger.warning(f"Error during observer shutdown: {e}")
|
|
127
|
+
raise
|
|
128
|
+
finally:
|
|
129
|
+
if self._context is not None:
|
|
130
|
+
try:
|
|
131
|
+
await self._context.shutdown()
|
|
132
|
+
except Exception as e:
|
|
133
|
+
self.logger.warning(f"Error shutting down context: {e}")
|
|
134
|
+
self._context = None
|
|
135
|
+
|
|
136
|
+
async def _ensure_coap_context(self) -> None:
|
|
137
|
+
"""Ensure we have a valid CoAP context."""
|
|
138
|
+
if self._context is None:
|
|
139
|
+
self._context = await Context.create_client_context()
|
|
140
|
+
|
|
141
|
+
def _make_get_request(self, observe: Observe) -> Message:
|
|
142
|
+
"""Create a GET request with observe option.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
observe: Observe option value
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
CoAP message with observe option set
|
|
149
|
+
"""
|
|
150
|
+
return Message(
|
|
151
|
+
code=GET,
|
|
152
|
+
uri=f"coap://{self._adar.ip_address}{self.path}",
|
|
153
|
+
observe=int(observe), # 0 = observe, 1 = no observe
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def _send_message(self, msg: Message) -> tuple[BlockwiseRequest | Request, Message]:
|
|
157
|
+
self._adar.log_send_message(msg)
|
|
158
|
+
request = self._context.request(msg)
|
|
159
|
+
response_message = await request.response
|
|
160
|
+
if not response_message.code.is_successful():
|
|
161
|
+
self.logger.error(f"Error: {response_message.code} received")
|
|
162
|
+
raise CoapErrorException(response=response_message)
|
|
163
|
+
return request, response_message
|
|
164
|
+
|
|
165
|
+
async def __aenter__(self) -> Self:
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
async def __aexit__(self, *excinfo) -> None:
|
|
169
|
+
await self.stop()
|