dronecot 1.0.0b5__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.
dronecot/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright Sensors & Signals LLC https://www.snstac.com
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ """Drone Open Remote ID to TAK Gateway.
20
+
21
+ :source: <https://github.com/snstac/dronecot>
22
+ """
23
+
24
+ __version__ = "1.0.0-beta5"
25
+ __author__ = "Greg Albrecht <gba@snstac.com>"
26
+ __copyright__ = "Copyright Sensors & Signals LLC https://www.snstac.com"
27
+ __license__ = "Apache License, Version 2.0"
28
+
29
+ from .constants import ( # NOQA
30
+ DEFAULT_MQTT_BROKER,
31
+ DEFAULT_MQTT_PORT,
32
+ DEFAULT_MQTT_TOPIC,
33
+ DEFAULT_GPS_INFO_CMD,
34
+ )
35
+ from .functions import ( # NOQA
36
+ xml_to_cot,
37
+ create_tasks,
38
+ )
39
+ from .classes import MQTTWorker, RIDWorker # NOQA
40
+ from .open_drone_id import ODIDValidBlocks, decode_valid_blocks, parse_payload # NOQA
dronecot/classes.py ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright Sensors & Signals LLC https://www.snstac.com
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ """DroneCOT Class Definitions."""
20
+
21
+ import base64
22
+ import json
23
+
24
+ from typing import Optional
25
+
26
+ import asyncio_mqtt as aiomqtt
27
+ from bitstruct import *
28
+ import lzma
29
+
30
+ import pytak
31
+ import dronecot
32
+
33
+ __author__ = "Greg Albrecht <gba@snstac.com>"
34
+ __copyright__ = "Copyright Sensors & Signals LLC https://www.snstac.com"
35
+ __license__ = "Apache License, Version 2.0"
36
+
37
+
38
+ class MQTTWorker(pytak.QueueWorker):
39
+ """Queue Worker for MQTT."""
40
+
41
+ async def parse_message(self, message):
42
+ """Parse Open Drone ID message from MQTT."""
43
+ try:
44
+ # not compressed
45
+ payload = message.payload.decode()
46
+ # remove newline (\n) char or \0 char as it will prevent decoding of json
47
+ if ord(payload[-1:]) == 0 or ord(payload[-1:]) == 10:
48
+ payload = payload[:-1]
49
+ except (UnicodeDecodeError, AttributeError):
50
+ # lzma compressed
51
+ payload = lzma.decompress(message.payload).decode()
52
+ # remove \0 char as it will prevent decoding of json
53
+ if ord(payload[-1:]) == 0:
54
+ payload = payload[:-1]
55
+
56
+ position = 0
57
+ while position != -1:
58
+ position = payload.find("}{")
59
+ if position == -1:
60
+ json_obj = json.loads(payload)
61
+ else:
62
+ message_payload = payload[0 : position + 1]
63
+ payload = payload[position + 1 :]
64
+ json_obj = json.loads(message_payload)
65
+
66
+ proto = json_obj.get("protocol")
67
+ if proto != 1.0:
68
+ return
69
+
70
+ if json_obj.get("data"):
71
+ await self.handle_sensor_data(json_obj)
72
+ elif json_obj.get("status"):
73
+ await self.handle_sensor_status(json_obj)
74
+
75
+ async def handle_sensor_data(self, message):
76
+ """Process decoded data from the sensor."""
77
+ data = message.get("data")
78
+ uasdata = data.get("UASdata")
79
+ if not uasdata:
80
+ return
81
+
82
+ uasdata = base64.b64decode(uasdata)
83
+
84
+ valid_blocks = dronecot.decode_valid_blocks(uasdata, dronecot.ODIDValidBlocks())
85
+ pl = dronecot.parse_payload(uasdata, valid_blocks)
86
+ await self.put_queue(pl)
87
+
88
+ async def handle_sensor_status(self, message):
89
+ """Process sensor status."""
90
+ status = message.get("status")
91
+ if not status:
92
+ return
93
+ await self.put_queue(message)
94
+
95
+ async def run(self, _=-1) -> None:
96
+ """Run this Thread, Reads from Pollers."""
97
+ self._logger.info("Run: MQTTWorker")
98
+
99
+ client_id = self.config.get("MQTT_CLIENT_ID", "dronecot")
100
+ topic = self.config.get("MQTT_TOPIC", dronecot.DEFAULT_MQTT_TOPIC)
101
+ broker = self.config.get("MQTT_BROKER", dronecot.DEFAULT_MQTT_BROKER)
102
+ port = self.config.get("MQTT_PORT", dronecot.DEFAULT_MQTT_PORT)
103
+ mqtt_username = self.config.get("MQTT_USERNAME")
104
+ mqtt_password = self.config.get("MQTT_PASSWORD")
105
+
106
+ async with aiomqtt.Client(
107
+ hostname=broker,
108
+ port=port,
109
+ username=mqtt_username,
110
+ password=mqtt_password,
111
+ client_id=client_id,
112
+ ) as client:
113
+ self._logger.info("Connected to MQTT Broker %s:%d/%s", broker, port, topic)
114
+ async with client.messages() as messages:
115
+ await client.subscribe(topic)
116
+ async for message in messages:
117
+ await self.parse_message(message)
118
+
119
+
120
+ class RIDWorker(pytak.QueueWorker):
121
+ """Queue Worker for RID."""
122
+
123
+ def __init__(self, queue, net_queue, config):
124
+ """Initialize this class."""
125
+ super().__init__(queue, config)
126
+ self.net_queue = net_queue
127
+ self.config = config
128
+
129
+ async def handle_data(self, data: dict) -> None:
130
+ """Handle Data from ADS-B receiver: Render to CoT, put on TX queue.
131
+
132
+ Parameters
133
+ ----------
134
+ data : `list[dict, ]`
135
+ List of craft data as key/value arrays.
136
+ """
137
+ # self._logger.info(data)
138
+ if "status" in data:
139
+ event = dronecot.xml_to_cot(data, self.config, "sensor_status_to_cot")
140
+ await self.put_queue(event)
141
+ else:
142
+ uas_event: Optional[bytes] = dronecot.xml_to_cot(
143
+ data, self.config, "rid_uas_to_cot_xml"
144
+ )
145
+ op_event: Optional[bytes] = dronecot.xml_to_cot(
146
+ data, self.config, "rid_op_to_cot_xml"
147
+ )
148
+ await self.put_queue(uas_event)
149
+ await self.put_queue(op_event)
150
+
151
+ async def run(self, _=-1) -> None:
152
+ """Run the main process loop."""
153
+ self._logger.info("Run: RIDWorker")
154
+
155
+ while 1:
156
+ data = await self.net_queue.get()
157
+ if not data:
158
+ # await asyncio.sleep(0.01)
159
+ continue
160
+
161
+ await self.handle_data(data)
dronecot/commands.py ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright Sensors & Signals LLC https://www.snstac.com
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ """PyTAK Command Line."""
20
+
21
+ import pytak
22
+
23
+ __author__ = "Greg Albrecht <gba@snstac.com>"
24
+ __copyright__ = "Copyright Sensors & Signals LLC https://www.snstac.com"
25
+ __license__ = "Apache License, Version 2.0"
26
+
27
+
28
+ def main() -> None:
29
+ """Boilerplate main func."""
30
+ # PyTAK CLI tool boilerplate:
31
+ pytak.cli(__name__.split(".", maxsplit=1)[0])
32
+
33
+
34
+ if __name__ == "__main__":
35
+ main()
dronecot/constants.py ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright Sensors & Signals LLC https://www.snstac.com
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ """DroneCOT Constants."""
20
+
21
+ __author__ = "Greg Albrecht <gba@snstac.com>"
22
+ __copyright__ = "Copyright Sensors & Signals LLC https://www.snstac.com"
23
+ __license__ = "Apache License, Version 2.0"
24
+
25
+
26
+ DEFAULT_MQTT_BROKER: str = "localhost"
27
+ DEFAULT_MQTT_PORT: int = 1883
28
+ DEFAULT_MQTT_TOPIC: str = "#"
29
+ DEFAULT_GPS_INFO_CMD: str = "gpspipe --json -n 5"
dronecot/functions.py ADDED
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright Sensors & Signals LLC https://www.snstac.com
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ """DroneCOT Functions."""
20
+
21
+ import asyncio
22
+ import json
23
+ import os
24
+ import xml.etree.ElementTree as ET
25
+
26
+ from configparser import SectionProxy
27
+ from typing import Optional, Set, Union
28
+
29
+ import pytak
30
+ import dronecot
31
+
32
+ __author__ = "Greg Albrecht <gba@snstac.com>"
33
+ __copyright__ = "Copyright Sensors & Signals LLC https://www.snstac.com"
34
+ __license__ = "Apache License, Version 2.0"
35
+
36
+
37
+ APP_NAME = "dronecot"
38
+
39
+
40
+ def create_tasks(config: SectionProxy, clitool: pytak.CLITool) -> Set[pytak.Worker,]:
41
+ """Create specific coroutine task set for this application.
42
+
43
+ Parameters
44
+ ----------
45
+ config : `SectionProxy`
46
+ Configuration options & values.
47
+ clitool : `pytak.CLITool`
48
+ A PyTAK Worker class instance.
49
+
50
+ Returns
51
+ -------
52
+ `set`
53
+ Set of PyTAK Worker classes for this application.
54
+ """
55
+ tasks = set()
56
+
57
+ net_queue: asyncio.Queue = asyncio.Queue()
58
+
59
+ tasks.add(dronecot.MQTTWorker(net_queue, config))
60
+ tasks.add(dronecot.RIDWorker(clitool.tx_queue, net_queue, config))
61
+
62
+ return tasks
63
+
64
+
65
+ # {'AltitudeBaro': nan,
66
+ # 'AltitudeGeo': 212.0,
67
+ # 'AreaCeiling': nan,
68
+ # 'AreaCount': 1,
69
+ # 'AreaFloor': nan,
70
+ # 'AreaRadius': 500,
71
+ # 'BaroAccuracy': 4,
72
+ # 'CategoryEU': 1,
73
+ # 'ClassEU': 5,
74
+ # 'ClassificationType': 1,
75
+ # 'Desc': 'Recreational',
76
+ # 'DescType': 0,
77
+ # 'Direction': 126.0,
78
+ # 'Height': 115.5,
79
+ # 'HeightType': 0,
80
+ # 'HorizAccuracy': 10,
81
+ # 'Latitude': 37.7599566,
82
+ # 'Longitude': -122.4983164,
83
+ # 'OperatorAltitudeGeo': 96.0,
84
+ # 'OperatorID': 'WCzFzGDgIzxEnUcT',
85
+ # 'OperatorIdType': 0,
86
+ # 'OperatorLatitude': 37.7599983,
87
+ # 'OperatorLocationType': 0,
88
+ # 'OperatorLongitude': -122.4973975,
89
+ # 'SpeedAccuracy': 1,
90
+ # 'SpeedHorizontal': 12.75,
91
+ # 'SpeedVertical': nan,
92
+ # 'Status': 0,
93
+ # 'TSAccuracy': 15,
94
+ # 'Timestamp': ('2041-06-11 01:04 UTC',),
95
+ # 'TimestampRaw': 708224640,
96
+ # 'VertAccuracy': 4}
97
+
98
+
99
+ def rid_op_to_cot_xml( # NOQA pylint: disable=too-many-locals,too-many-branches,too-many-statements
100
+ data: dict,
101
+ config: Union[SectionProxy, dict, None] = None,
102
+ ) -> Optional[ET.Element]:
103
+ """
104
+ Serialize Open Drone ID data as Cursor on Target.
105
+
106
+ Parameters
107
+ ----------
108
+ craft : `dict`
109
+ Key/Value data struct of decoded ADS-B aircraft data.
110
+ config : `configparser.SectionProxy`
111
+ Configuration options and values.
112
+ Uses config options: UID_KEY, COT_STALE, COT_HOST_ID
113
+ kown_craft : `dict`
114
+ Optional list of know craft to transform CoT data.
115
+
116
+ Returns
117
+ -------
118
+ `xml.etree.ElementTree.Element`
119
+ Cursor-On-Target XML ElementTree object.
120
+ """
121
+ lat = data.get("OperatorLatitude")
122
+ lon = data.get("OperatorLongitude")
123
+
124
+ if lat is None or lon is None:
125
+ return None
126
+
127
+ config = config or {}
128
+ remarks_fields: list = []
129
+
130
+ op_id = data.get("OperatorID", "Unknown-OperatorID")
131
+ uasid = data.get("BasicID_0", "Unknown-BasicID_0")
132
+
133
+ cot_uid: str = f"RID.{op_id}"
134
+ cot_type: str = "a-n-G"
135
+
136
+ cot_stale: int = int(config.get("COT_STALE", pytak.DEFAULT_COT_STALE))
137
+ cot_host_id: str = config.get("COT_HOST_ID", pytak.DEFAULT_HOST_ID)
138
+
139
+ cotx = ET.Element("_dronecot_")
140
+ cotx.set("cot_host_id", cot_host_id)
141
+
142
+ remarks_fields.append(f"UAS ID={uasid} OperatorID={op_id}")
143
+ cotx.set("OperatorID", op_id)
144
+ cotx.set("UASID", op_id)
145
+ callsign = op_id
146
+
147
+ contact: ET.Element = ET.Element("contact")
148
+ contact.set("callsign", callsign)
149
+
150
+ detail = ET.Element("detail")
151
+ detail.append(contact)
152
+ detail.append(cotx)
153
+
154
+ remarks = ET.Element("remarks")
155
+ remarks_fields.append(f"{cot_host_id}")
156
+ _remarks = " ".join(list(filter(None, remarks_fields)))
157
+ remarks.text = _remarks
158
+ detail.append(remarks)
159
+
160
+ cot_d = {
161
+ "lat": str(lat),
162
+ "lon": str(lon),
163
+ "ce": str(data.get("HorizAccuracy", "9999999.0")),
164
+ "le": str(data.get("VertAccuracy", "9999999.0")),
165
+ "hae": str(data.get("OperatorAltitudeGeo", "9999999.0")),
166
+ "uid": cot_uid,
167
+ "cot_type": cot_type,
168
+ "stale": cot_stale,
169
+ }
170
+ cot = pytak.gen_cot_xml(**cot_d)
171
+ cot.set("access", config.get("COT_ACCESS", pytak.DEFAULT_COT_ACCESS))
172
+
173
+ _detail = cot.findall("detail")[0]
174
+ flowtags = _detail.findall("_flow-tags_")
175
+ detail.extend(flowtags)
176
+ cot.remove(_detail)
177
+ cot.append(detail)
178
+
179
+ return cot
180
+
181
+
182
+ def rid_uas_to_cot_xml( # NOQA pylint: disable=too-many-locals,too-many-branches,too-many-statements
183
+ data: dict,
184
+ config: Union[SectionProxy, dict, None] = None,
185
+ ) -> Optional[ET.Element]:
186
+ """
187
+ Serialize Open Drone ID data as Cursor on Target.
188
+
189
+ Parameters
190
+ ----------
191
+ craft : `dict`
192
+ Key/Value data struct of decoded ADS-B aircraft data.
193
+ config : `configparser.SectionProxy`
194
+ Configuration options and values.
195
+ Uses config options: UID_KEY, COT_STALE, COT_HOST_ID
196
+ kown_craft : `dict`
197
+ Optional list of know craft to transform CoT data.
198
+
199
+ Returns
200
+ -------
201
+ `xml.etree.ElementTree.Element`
202
+ Cursor-On-Target XML ElementTree object.
203
+ """
204
+ lat = data.get("Latitude")
205
+ lon = data.get("Longitude")
206
+
207
+ if lat is None or lon is None:
208
+ return None
209
+
210
+ config = config or {}
211
+ remarks_fields: list = []
212
+
213
+ op_id = data.get("OperatorID", "Unknown-OperatorID")
214
+ uasid = data.get("BasicID_0", "Unknown-BasicID_0")
215
+
216
+ cot_uid: str = f"RID.{uasid}"
217
+ cot_type: str = "a-n-A-M-F-Q"
218
+
219
+ cot_stale: int = int(config.get("COT_STALE", pytak.DEFAULT_COT_STALE))
220
+ cot_host_id: str = config.get("COT_HOST_ID", pytak.DEFAULT_HOST_ID)
221
+
222
+ cotx = ET.Element("_dronecot_")
223
+ cotx.set("cot_host_id", cot_host_id)
224
+
225
+ remarks_fields.append(f"OperatorID={op_id}")
226
+ cotx.set("OperatorID", op_id)
227
+ callsign = uasid
228
+
229
+ contact: ET.Element = ET.Element("contact")
230
+ contact.set("callsign", callsign)
231
+
232
+ track: ET.Element = ET.Element("track")
233
+ track.set("speed", str(data.get("SpeedHorizontal", 0)))
234
+
235
+ link: ET.Element = ET.Element("link")
236
+ link.set("uid", op_id)
237
+ link.set("production_time", pytak.cot_time())
238
+ link.set("type", "a-n-G")
239
+ link.set("parent_callsign", op_id)
240
+ link.set("relation", "p-p")
241
+
242
+ detail = ET.Element("detail")
243
+ detail.append(contact)
244
+ detail.append(track)
245
+ detail.append(cotx)
246
+ detail.append(link)
247
+
248
+ remarks = ET.Element("remarks")
249
+ remarks_fields.append(f"{cot_host_id}")
250
+ _remarks = " ".join(list(filter(None, remarks_fields)))
251
+ remarks.text = _remarks
252
+ detail.append(remarks)
253
+
254
+ cot_d = {
255
+ "lat": str(lat),
256
+ "lon": str(lon),
257
+ "ce": str(data.get("HorizAccuracy", "9999999.0")),
258
+ "le": str(data.get("VertAccuracy", "9999999.0")),
259
+ "hae": str(data.get("AltitudeGeo", "9999999.0")),
260
+ "uid": cot_uid,
261
+ "cot_type": cot_type,
262
+ "stale": cot_stale,
263
+ }
264
+ cot = pytak.gen_cot_xml(**cot_d)
265
+ cot.set("access", config.get("COT_ACCESS", pytak.DEFAULT_COT_ACCESS))
266
+
267
+ _detail = cot.findall("detail")[0]
268
+ flowtags = _detail.findall("_flow-tags_")
269
+ detail.extend(flowtags)
270
+ cot.remove(_detail)
271
+ cot.append(detail)
272
+
273
+ return cot
274
+
275
+
276
+ def sensor_status_to_cot( # NOQA pylint: disable=too-many-locals,too-many-branches,too-many-statements
277
+ data: dict,
278
+ config: Union[SectionProxy, dict, None] = None,
279
+ ) -> Optional[ET.Element]:
280
+ """Serialize sensor status data s Cursor on Target."""
281
+ config = config or {}
282
+ lat = config.get("SENSOR_LAT")
283
+ lon = config.get("SENSOR_LON")
284
+ alt = config.get("SENSOR_ALT", "9999999.0")
285
+
286
+ if lat is None or lon is None:
287
+ gps_info = get_gps_info(config)
288
+ if gps_info:
289
+ print(gps_info)
290
+ else:
291
+ return None
292
+
293
+ if lat is None or lon is None:
294
+ return None
295
+
296
+ config = config or {}
297
+ remarks_fields: list = []
298
+
299
+ sensor_id = config.get("SENSOR_ID", "Unknown-SENSOR_ID")
300
+
301
+ cot_uid: str = f"CUAS.{sensor_id}"
302
+ cot_type: str = "a-f-G-E-S"
303
+
304
+ cot_stale: int = int(config.get("COT_STALE", pytak.DEFAULT_COT_STALE))
305
+ cot_host_id: str = config.get("COT_HOST_ID", pytak.DEFAULT_HOST_ID)
306
+
307
+ cotx = ET.Element("_dronecot_")
308
+ cotx.set("cot_host_id", cot_host_id)
309
+
310
+ remarks_fields.append(f"C-UAS Sensor {sensor_id}")
311
+ cotx.set("sensor_id", sensor_id)
312
+ callsign = sensor_id
313
+
314
+ contact: ET.Element = ET.Element("contact")
315
+ contact.set("callsign", callsign)
316
+
317
+ track: ET.Element = ET.Element("track")
318
+ track.set("speed", str(data.get("SpeedHorizontal", 0)))
319
+
320
+ detail = ET.Element("detail")
321
+ detail.append(contact)
322
+ detail.append(track)
323
+ detail.append(cotx)
324
+
325
+ remarks = ET.Element("remarks")
326
+ remarks_fields.append(f"{cot_host_id}")
327
+ _remarks = " ".join(list(filter(None, remarks_fields)))
328
+ remarks.text = _remarks
329
+ detail.append(remarks)
330
+
331
+ cot_d = {
332
+ "lat": str(lat),
333
+ "lon": str(lon),
334
+ "ce": str(data.get("HorizAccuracy", "9999999.0")),
335
+ "le": str(data.get("VertAccuracy", "9999999.0")),
336
+ "hae": str(data.get("AltitudeGeo", "9999999.0")),
337
+ "uid": cot_uid,
338
+ "cot_type": cot_type,
339
+ "stale": cot_stale,
340
+ }
341
+ cot = pytak.gen_cot_xml(**cot_d)
342
+ cot.set("access", config.get("COT_ACCESS", pytak.DEFAULT_COT_ACCESS))
343
+
344
+ _detail = cot.findall("detail")[0]
345
+ flowtags = _detail.findall("_flow-tags_")
346
+ detail.extend(flowtags)
347
+ cot.remove(_detail)
348
+ cot.append(detail)
349
+
350
+ return cot
351
+
352
+
353
+ def xml_to_cot(
354
+ data: dict, config: Union[SectionProxy, dict, None] = None, func=None
355
+ ) -> Optional[bytes]:
356
+ """Return CoT XML object as an XML string."""
357
+ cot: Optional[ET.Element] = getattr(dronecot.functions, func)(data, config)
358
+ return (
359
+ b"\n".join([pytak.DEFAULT_XML_DECLARATION, ET.tostring(cot)]) if cot else None
360
+ )
361
+
362
+
363
+ def get_gps_info(config) -> Optional[dict]:
364
+ """Get GPS Info data."""
365
+ gpspipe_data: Optional[str] = None
366
+ gps_data: Optional[str] = None
367
+ gps_info_cmd = config.get("GPS_INFO_CMD", dronecot.DEFAULT_GPS_INFO_CMD)
368
+
369
+ with os.popen(gps_info_cmd) as gps_info_cmd:
370
+ gpspipe_data = gps_info_cmd.read()
371
+
372
+ if not gpspipe_data:
373
+ return None
374
+
375
+ if "\n" in gpspipe_data:
376
+ for data in gpspipe_data.split("\n"):
377
+ if "TPV" in data:
378
+ gps_data = data
379
+ continue
380
+
381
+ if not gps_data:
382
+ return None
383
+
384
+ gps_info = json.loads(gps_data)
385
+ return gps_info