aprsd 4.1.2__py3-none-any.whl → 4.2.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.
- aprsd/client/__init__.py +5 -13
- aprsd/client/client.py +156 -0
- aprsd/client/drivers/__init__.py +10 -0
- aprsd/client/drivers/aprsis.py +174 -255
- aprsd/client/drivers/fake.py +59 -11
- aprsd/client/drivers/lib/__init__.py +0 -0
- aprsd/client/drivers/lib/aprslib.py +296 -0
- aprsd/client/drivers/registry.py +86 -0
- aprsd/client/drivers/tcpkiss.py +423 -0
- aprsd/client/stats.py +2 -2
- aprsd/cmds/dev.py +6 -4
- aprsd/cmds/fetch_stats.py +2 -0
- aprsd/cmds/list_plugins.py +6 -133
- aprsd/cmds/listen.py +5 -3
- aprsd/cmds/send_message.py +8 -5
- aprsd/cmds/server.py +7 -11
- aprsd/conf/common.py +7 -1
- aprsd/exception.py +7 -0
- aprsd/log/log.py +1 -1
- aprsd/main.py +0 -7
- aprsd/packets/core.py +168 -169
- aprsd/packets/log.py +69 -59
- aprsd/plugin.py +3 -2
- aprsd/plugin_utils.py +2 -2
- aprsd/plugins/weather.py +2 -2
- aprsd/stats/collector.py +5 -4
- aprsd/threads/rx.py +13 -11
- aprsd/threads/tx.py +32 -31
- aprsd/utils/keepalive_collector.py +7 -5
- aprsd/utils/package.py +176 -0
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/METADATA +48 -48
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/RECORD +37 -37
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/WHEEL +1 -1
- aprsd/client/aprsis.py +0 -183
- aprsd/client/base.py +0 -156
- aprsd/client/drivers/kiss.py +0 -144
- aprsd/client/factory.py +0 -91
- aprsd/client/fake.py +0 -49
- aprsd/client/kiss.py +0 -143
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/entry_points.txt +0 -0
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info/licenses}/AUTHORS +0 -0
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info/licenses}/LICENSE +0 -0
- {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/top_level.txt +0 -0
aprsd/packets/core.py
CHANGED
@@ -19,26 +19,26 @@ from loguru import logger
|
|
19
19
|
from aprsd.utils import counter
|
20
20
|
|
21
21
|
# For mypy to be happy
|
22
|
-
A = TypeVar(
|
22
|
+
A = TypeVar('A', bound='DataClassJsonMixin')
|
23
23
|
Json = Union[dict, list, str, int, float, bool, None]
|
24
24
|
|
25
25
|
LOG = logging.getLogger()
|
26
26
|
LOGU = logger
|
27
27
|
|
28
|
-
PACKET_TYPE_BULLETIN =
|
29
|
-
PACKET_TYPE_MESSAGE =
|
30
|
-
PACKET_TYPE_ACK =
|
31
|
-
PACKET_TYPE_REJECT =
|
32
|
-
PACKET_TYPE_MICE =
|
33
|
-
PACKET_TYPE_WX =
|
34
|
-
PACKET_TYPE_WEATHER =
|
35
|
-
PACKET_TYPE_OBJECT =
|
36
|
-
PACKET_TYPE_UNKNOWN =
|
37
|
-
PACKET_TYPE_STATUS =
|
38
|
-
PACKET_TYPE_BEACON =
|
39
|
-
PACKET_TYPE_THIRDPARTY =
|
40
|
-
PACKET_TYPE_TELEMETRY =
|
41
|
-
PACKET_TYPE_UNCOMPRESSED =
|
28
|
+
PACKET_TYPE_BULLETIN = 'bulletin'
|
29
|
+
PACKET_TYPE_MESSAGE = 'message'
|
30
|
+
PACKET_TYPE_ACK = 'ack'
|
31
|
+
PACKET_TYPE_REJECT = 'reject'
|
32
|
+
PACKET_TYPE_MICE = 'mic-e'
|
33
|
+
PACKET_TYPE_WX = 'wx'
|
34
|
+
PACKET_TYPE_WEATHER = 'weather'
|
35
|
+
PACKET_TYPE_OBJECT = 'object'
|
36
|
+
PACKET_TYPE_UNKNOWN = 'unknown'
|
37
|
+
PACKET_TYPE_STATUS = 'status'
|
38
|
+
PACKET_TYPE_BEACON = 'beacon'
|
39
|
+
PACKET_TYPE_THIRDPARTY = 'thirdparty'
|
40
|
+
PACKET_TYPE_TELEMETRY = 'telemetry-message'
|
41
|
+
PACKET_TYPE_UNCOMPRESSED = 'uncompressed'
|
42
42
|
|
43
43
|
NO_DATE = datetime(1900, 10, 24)
|
44
44
|
|
@@ -67,14 +67,14 @@ def _init_msgNo(): # noqa: N802
|
|
67
67
|
|
68
68
|
def _translate_fields(raw: dict) -> dict:
|
69
69
|
# Direct key checks instead of iteration
|
70
|
-
if
|
71
|
-
raw[
|
72
|
-
if
|
73
|
-
raw[
|
70
|
+
if 'from' in raw:
|
71
|
+
raw['from_call'] = raw.pop('from')
|
72
|
+
if 'to' in raw:
|
73
|
+
raw['to_call'] = raw.pop('to')
|
74
74
|
|
75
75
|
# addresse overrides to_call
|
76
|
-
if
|
77
|
-
raw[
|
76
|
+
if 'addresse' in raw:
|
77
|
+
raw['to_call'] = raw['addresse']
|
78
78
|
|
79
79
|
return raw
|
80
80
|
|
@@ -82,7 +82,7 @@ def _translate_fields(raw: dict) -> dict:
|
|
82
82
|
@dataclass_json
|
83
83
|
@dataclass(unsafe_hash=True)
|
84
84
|
class Packet:
|
85
|
-
_type: str = field(default=
|
85
|
+
_type: str = field(default='Packet', hash=False)
|
86
86
|
from_call: Optional[str] = field(default=None)
|
87
87
|
to_call: Optional[str] = field(default=None)
|
88
88
|
addresse: Optional[str] = field(default=None)
|
@@ -120,7 +120,7 @@ class Packet:
|
|
120
120
|
@property
|
121
121
|
def key(self) -> str:
|
122
122
|
"""Build a key for finding this packet in a dict."""
|
123
|
-
return f
|
123
|
+
return f'{self.from_call}:{self.addresse}:{self.msgNo}'
|
124
124
|
|
125
125
|
def update_timestamp(self) -> None:
|
126
126
|
self.timestamp = _init_timestamp()
|
@@ -133,7 +133,7 @@ class Packet:
|
|
133
133
|
the human readable payload.
|
134
134
|
"""
|
135
135
|
self.prepare()
|
136
|
-
msg = self._filter_for_send(self.raw).rstrip(
|
136
|
+
msg = self._filter_for_send(self.raw).rstrip('\n')
|
137
137
|
return msg
|
138
138
|
|
139
139
|
def prepare(self, create_msg_number=False) -> None:
|
@@ -152,11 +152,11 @@ class Packet:
|
|
152
152
|
)
|
153
153
|
|
154
154
|
# The base packet class has no real payload
|
155
|
-
self.payload = f
|
155
|
+
self.payload = f':{self.to_call.ljust(9)}'
|
156
156
|
|
157
157
|
def _build_raw(self) -> None:
|
158
158
|
"""Build the self.raw which is what is sent over the air."""
|
159
|
-
self.raw =
|
159
|
+
self.raw = '{}>APZ100:{}'.format(
|
160
160
|
self.from_call,
|
161
161
|
self.payload,
|
162
162
|
)
|
@@ -168,13 +168,13 @@ class Packet:
|
|
168
168
|
# 67 displays 64 on the ftm400. (+3 {01 suffix)
|
169
169
|
# feature req: break long ones into two msgs
|
170
170
|
if not msg:
|
171
|
-
return
|
171
|
+
return ''
|
172
172
|
|
173
173
|
message = msg[:67]
|
174
174
|
# We all miss George Carlin
|
175
175
|
return re.sub(
|
176
|
-
|
177
|
-
|
176
|
+
'fuck|shit|cunt|piss|cock|bitch',
|
177
|
+
'****',
|
178
178
|
message,
|
179
179
|
flags=re.IGNORECASE,
|
180
180
|
)
|
@@ -183,100 +183,98 @@ class Packet:
|
|
183
183
|
"""Show the raw version of the packet"""
|
184
184
|
self.prepare()
|
185
185
|
if not self.raw:
|
186
|
-
raise ValueError(
|
186
|
+
raise ValueError('self.raw is unset')
|
187
187
|
return self.raw
|
188
188
|
|
189
189
|
def __repr__(self) -> str:
|
190
190
|
"""Build the repr version of the packet."""
|
191
191
|
return (
|
192
|
-
f
|
193
|
-
f" From: {self.from_call} "
|
194
|
-
f" To: {self.to_call}"
|
192
|
+
f'{self.__class__.__name__}: From: {self.from_call} To: {self.to_call}'
|
195
193
|
)
|
196
194
|
|
197
195
|
|
198
196
|
@dataclass_json
|
199
197
|
@dataclass(unsafe_hash=True)
|
200
198
|
class AckPacket(Packet):
|
201
|
-
_type: str = field(default=
|
199
|
+
_type: str = field(default='AckPacket', hash=False)
|
202
200
|
|
203
201
|
def _build_payload(self):
|
204
|
-
self.payload = f
|
202
|
+
self.payload = f':{self.to_call: <9}:ack{self.msgNo}'
|
205
203
|
|
206
204
|
|
207
205
|
@dataclass_json
|
208
206
|
@dataclass(unsafe_hash=True)
|
209
207
|
class BulletinPacket(Packet):
|
210
|
-
_type: str =
|
208
|
+
_type: str = 'BulletinPacket'
|
211
209
|
# Holds the encapsulated packet
|
212
|
-
bid: Optional[str] = field(default=
|
210
|
+
bid: Optional[str] = field(default='1')
|
213
211
|
message_text: Optional[str] = field(default=None)
|
214
212
|
|
215
213
|
@property
|
216
214
|
def key(self) -> str:
|
217
215
|
"""Build a key for finding this packet in a dict."""
|
218
|
-
return f
|
216
|
+
return f'{self.from_call}:BLN{self.bid}'
|
219
217
|
|
220
218
|
@property
|
221
219
|
def human_info(self) -> str:
|
222
|
-
return f
|
220
|
+
return f'BLN{self.bid} {self.message_text}'
|
223
221
|
|
224
222
|
def _build_payload(self) -> None:
|
225
|
-
self.payload = f
|
223
|
+
self.payload = f':BLN{self.bid:<9}:{self.message_text}'
|
226
224
|
|
227
225
|
|
228
226
|
@dataclass_json
|
229
227
|
@dataclass(unsafe_hash=True)
|
230
228
|
class RejectPacket(Packet):
|
231
|
-
_type: str = field(default=
|
229
|
+
_type: str = field(default='RejectPacket', hash=False)
|
232
230
|
response: Optional[str] = field(default=None)
|
233
231
|
|
234
232
|
def __post__init__(self):
|
235
233
|
if self.response:
|
236
|
-
LOG.warning(
|
234
|
+
LOG.warning('Response set!')
|
237
235
|
|
238
236
|
def _build_payload(self):
|
239
|
-
self.payload = f
|
237
|
+
self.payload = f':{self.to_call: <9}:rej{self.msgNo}'
|
240
238
|
|
241
239
|
|
242
240
|
@dataclass_json
|
243
241
|
@dataclass(unsafe_hash=True)
|
244
242
|
class MessagePacket(Packet):
|
245
|
-
_type: str = field(default=
|
243
|
+
_type: str = field(default='MessagePacket', hash=False)
|
246
244
|
message_text: Optional[str] = field(default=None)
|
247
245
|
|
248
246
|
@property
|
249
247
|
def human_info(self) -> str:
|
250
248
|
self.prepare()
|
251
|
-
return self._filter_for_send(self.message_text).rstrip(
|
249
|
+
return self._filter_for_send(self.message_text).rstrip('\n')
|
252
250
|
|
253
251
|
def _build_payload(self):
|
254
252
|
if self.msgNo:
|
255
|
-
self.payload =
|
253
|
+
self.payload = ':{}:{}{{{}'.format(
|
256
254
|
self.to_call.ljust(9),
|
257
|
-
self._filter_for_send(self.message_text).rstrip(
|
255
|
+
self._filter_for_send(self.message_text).rstrip('\n'),
|
258
256
|
str(self.msgNo),
|
259
257
|
)
|
260
258
|
else:
|
261
|
-
self.payload =
|
259
|
+
self.payload = ':{}:{}'.format(
|
262
260
|
self.to_call.ljust(9),
|
263
|
-
self._filter_for_send(self.message_text).rstrip(
|
261
|
+
self._filter_for_send(self.message_text).rstrip('\n'),
|
264
262
|
)
|
265
263
|
|
266
264
|
|
267
265
|
@dataclass_json
|
268
266
|
@dataclass(unsafe_hash=True)
|
269
267
|
class StatusPacket(Packet):
|
270
|
-
_type: str = field(default=
|
268
|
+
_type: str = field(default='StatusPacket', hash=False)
|
271
269
|
status: Optional[str] = field(default=None)
|
272
270
|
messagecapable: bool = field(default=False)
|
273
271
|
comment: Optional[str] = field(default=None)
|
274
272
|
raw_timestamp: Optional[str] = field(default=None)
|
275
273
|
|
276
274
|
def _build_payload(self):
|
277
|
-
self.payload =
|
275
|
+
self.payload = ':{}:{}{{{}'.format(
|
278
276
|
self.to_call.ljust(9),
|
279
|
-
self._filter_for_send(self.status).rstrip(
|
277
|
+
self._filter_for_send(self.status).rstrip('\n'),
|
280
278
|
str(self.msgNo),
|
281
279
|
)
|
282
280
|
|
@@ -289,7 +287,7 @@ class StatusPacket(Packet):
|
|
289
287
|
@dataclass_json
|
290
288
|
@dataclass(unsafe_hash=True)
|
291
289
|
class GPSPacket(Packet):
|
292
|
-
_type: str = field(default=
|
290
|
+
_type: str = field(default='GPSPacket', hash=False)
|
293
291
|
latitude: float = field(default=0.00)
|
294
292
|
longitude: float = field(default=0.00)
|
295
293
|
altitude: float = field(default=0.00)
|
@@ -297,8 +295,8 @@ class GPSPacket(Packet):
|
|
297
295
|
posambiguity: int = field(default=0)
|
298
296
|
messagecapable: bool = field(default=False)
|
299
297
|
comment: Optional[str] = field(default=None)
|
300
|
-
symbol: str = field(default=
|
301
|
-
symbol_table: str = field(default=
|
298
|
+
symbol: str = field(default='l')
|
299
|
+
symbol_table: str = field(default='/')
|
302
300
|
raw_timestamp: Optional[str] = field(default=None)
|
303
301
|
object_name: Optional[str] = field(default=None)
|
304
302
|
object_format: Optional[str] = field(default=None)
|
@@ -318,7 +316,7 @@ class GPSPacket(Packet):
|
|
318
316
|
def _build_time_zulu(self):
|
319
317
|
"""Build the timestamp in UTC/zulu."""
|
320
318
|
if self.timestamp:
|
321
|
-
return datetime.utcfromtimestamp(self.timestamp).strftime(
|
319
|
+
return datetime.utcfromtimestamp(self.timestamp).strftime('%d%H%M')
|
322
320
|
|
323
321
|
def _build_payload(self):
|
324
322
|
"""The payload is the non headers portion of the packet."""
|
@@ -326,7 +324,7 @@ class GPSPacket(Packet):
|
|
326
324
|
lat = aprslib_util.latitude_to_ddm(self.latitude)
|
327
325
|
long = aprslib_util.longitude_to_ddm(self.longitude)
|
328
326
|
payload = [
|
329
|
-
|
327
|
+
'@' if self.timestamp else '!',
|
330
328
|
time_zulu,
|
331
329
|
lat,
|
332
330
|
self.symbol_table,
|
@@ -337,34 +335,34 @@ class GPSPacket(Packet):
|
|
337
335
|
if self.comment:
|
338
336
|
payload.append(self._filter_for_send(self.comment))
|
339
337
|
|
340
|
-
self.payload =
|
338
|
+
self.payload = ''.join(payload)
|
341
339
|
|
342
340
|
def _build_raw(self):
|
343
|
-
self.raw = f
|
341
|
+
self.raw = f'{self.from_call}>{self.to_call},WIDE2-1:{self.payload}'
|
344
342
|
|
345
343
|
@property
|
346
344
|
def human_info(self) -> str:
|
347
345
|
h_str = []
|
348
|
-
h_str.append(f
|
349
|
-
h_str.append(f
|
346
|
+
h_str.append(f'Lat:{self.latitude:03.3f}')
|
347
|
+
h_str.append(f'Lon:{self.longitude:03.3f}')
|
350
348
|
if self.altitude:
|
351
|
-
h_str.append(f
|
349
|
+
h_str.append(f'Altitude {self.altitude:03.0f}')
|
352
350
|
if self.speed:
|
353
|
-
h_str.append(f
|
351
|
+
h_str.append(f'Speed {self.speed:03.0f}MPH')
|
354
352
|
if self.course:
|
355
|
-
h_str.append(f
|
353
|
+
h_str.append(f'Course {self.course:03.0f}')
|
356
354
|
if self.rng:
|
357
|
-
h_str.append(f
|
355
|
+
h_str.append(f'RNG {self.rng:03.0f}')
|
358
356
|
if self.phg:
|
359
|
-
h_str.append(f
|
357
|
+
h_str.append(f'PHG {self.phg}')
|
360
358
|
|
361
|
-
return
|
359
|
+
return ' '.join(h_str)
|
362
360
|
|
363
361
|
|
364
362
|
@dataclass_json
|
365
363
|
@dataclass(unsafe_hash=True)
|
366
364
|
class BeaconPacket(GPSPacket):
|
367
|
-
_type: str = field(default=
|
365
|
+
_type: str = field(default='BeaconPacket', hash=False)
|
368
366
|
|
369
367
|
def _build_payload(self):
|
370
368
|
"""The payload is the non headers portion of the packet."""
|
@@ -372,42 +370,42 @@ class BeaconPacket(GPSPacket):
|
|
372
370
|
lat = aprslib_util.latitude_to_ddm(self.latitude)
|
373
371
|
lon = aprslib_util.longitude_to_ddm(self.longitude)
|
374
372
|
|
375
|
-
self.payload = f
|
373
|
+
self.payload = f'@{time_zulu}z{lat}{self.symbol_table}{lon}'
|
376
374
|
|
377
375
|
if self.comment:
|
378
376
|
comment = self._filter_for_send(self.comment)
|
379
|
-
self.payload = f
|
377
|
+
self.payload = f'{self.payload}{self.symbol}{comment}'
|
380
378
|
else:
|
381
|
-
self.payload = f
|
379
|
+
self.payload = f'{self.payload}{self.symbol}APRSD Beacon'
|
382
380
|
|
383
381
|
def _build_raw(self):
|
384
|
-
self.raw = f
|
382
|
+
self.raw = f'{self.from_call}>APZ100:{self.payload}'
|
385
383
|
|
386
384
|
@property
|
387
385
|
def key(self) -> str:
|
388
386
|
"""Build a key for finding this packet in a dict."""
|
389
387
|
if self.raw_timestamp:
|
390
|
-
return f
|
388
|
+
return f'{self.from_call}:{self.raw_timestamp}'
|
391
389
|
else:
|
392
|
-
return f
|
390
|
+
return f'{self.from_call}:{self.human_info.replace(" ", "")}'
|
393
391
|
|
394
392
|
@property
|
395
393
|
def human_info(self) -> str:
|
396
394
|
h_str = []
|
397
|
-
h_str.append(f
|
398
|
-
h_str.append(f
|
399
|
-
h_str.append(f
|
400
|
-
return
|
395
|
+
h_str.append(f'Lat:{self.latitude:03.3f}')
|
396
|
+
h_str.append(f'Lon:{self.longitude:03.3f}')
|
397
|
+
h_str.append(f'{self.comment}')
|
398
|
+
return ' '.join(h_str)
|
401
399
|
|
402
400
|
|
403
401
|
@dataclass_json
|
404
402
|
@dataclass(unsafe_hash=True)
|
405
403
|
class MicEPacket(GPSPacket):
|
406
|
-
_type: str = field(default=
|
404
|
+
_type: str = field(default='MicEPacket', hash=False)
|
407
405
|
messagecapable: bool = False
|
408
406
|
mbits: Optional[str] = None
|
409
407
|
mtype: Optional[str] = None
|
410
|
-
telemetry: Optional[dict] = field(default=None)
|
408
|
+
telemetry: Optional[dict] = field(default=None, hash=False)
|
411
409
|
# in MPH
|
412
410
|
speed: float = 0.00
|
413
411
|
# 0 to 360
|
@@ -416,24 +414,24 @@ class MicEPacket(GPSPacket):
|
|
416
414
|
@property
|
417
415
|
def key(self) -> str:
|
418
416
|
"""Build a key for finding this packet in a dict."""
|
419
|
-
return f
|
417
|
+
return f'{self.from_call}:{self.human_info.replace(" ", "")}'
|
420
418
|
|
421
419
|
@property
|
422
420
|
def human_info(self) -> str:
|
423
421
|
h_info = super().human_info
|
424
|
-
return f
|
422
|
+
return f'{h_info} {self.mbits} mbits'
|
425
423
|
|
426
424
|
|
427
425
|
@dataclass_json
|
428
426
|
@dataclass(unsafe_hash=True)
|
429
427
|
class TelemetryPacket(GPSPacket):
|
430
|
-
_type: str = field(default=
|
428
|
+
_type: str = field(default='TelemetryPacket', hash=False)
|
431
429
|
messagecapable: bool = False
|
432
430
|
mbits: Optional[str] = None
|
433
431
|
mtype: Optional[str] = None
|
434
432
|
telemetry: Optional[dict] = field(default=None)
|
435
|
-
tPARM: Optional[list[str]] = field(default=None) # noqa: N815
|
436
|
-
tUNIT: Optional[list[str]] = field(default=None) # noqa: N815
|
433
|
+
tPARM: Optional[list[str]] = field(default=None, hash=False) # noqa: N815
|
434
|
+
tUNIT: Optional[list[str]] = field(default=None, hash=False) # noqa: N815
|
437
435
|
# in MPH
|
438
436
|
speed: float = 0.00
|
439
437
|
# 0 to 360
|
@@ -443,23 +441,23 @@ class TelemetryPacket(GPSPacket):
|
|
443
441
|
def key(self) -> str:
|
444
442
|
"""Build a key for finding this packet in a dict."""
|
445
443
|
if self.raw_timestamp:
|
446
|
-
return f
|
444
|
+
return f'{self.from_call}:{self.raw_timestamp}'
|
447
445
|
else:
|
448
|
-
return f
|
446
|
+
return f'{self.from_call}:{self.human_info.replace(" ", "")}'
|
449
447
|
|
450
448
|
@property
|
451
449
|
def human_info(self) -> str:
|
452
450
|
h_info = super().human_info
|
453
|
-
return f
|
451
|
+
return f'{h_info} {self.telemetry}'
|
454
452
|
|
455
453
|
|
456
454
|
@dataclass_json
|
457
455
|
@dataclass(unsafe_hash=True)
|
458
456
|
class ObjectPacket(GPSPacket):
|
459
|
-
_type: str = field(default=
|
457
|
+
_type: str = field(default='ObjectPacket', hash=False)
|
460
458
|
alive: bool = True
|
461
459
|
raw_timestamp: Optional[str] = None
|
462
|
-
symbol: str = field(default=
|
460
|
+
symbol: str = field(default='r')
|
463
461
|
# in MPH
|
464
462
|
speed: float = 0.00
|
465
463
|
# 0 to 360
|
@@ -470,11 +468,11 @@ class ObjectPacket(GPSPacket):
|
|
470
468
|
lat = aprslib_util.latitude_to_ddm(self.latitude)
|
471
469
|
long = aprslib_util.longitude_to_ddm(self.longitude)
|
472
470
|
|
473
|
-
self.payload = f
|
471
|
+
self.payload = f'*{time_zulu}z{lat}{self.symbol_table}{long}{self.symbol}'
|
474
472
|
|
475
473
|
if self.comment:
|
476
474
|
comment = self._filter_for_send(self.comment)
|
477
|
-
self.payload = f
|
475
|
+
self.payload = f'{self.payload}{comment}'
|
478
476
|
|
479
477
|
def _build_raw(self):
|
480
478
|
"""
|
@@ -487,18 +485,18 @@ class ObjectPacket(GPSPacket):
|
|
487
485
|
The frequency, uplink_tone, offset is part of the comment
|
488
486
|
"""
|
489
487
|
|
490
|
-
self.raw = f
|
488
|
+
self.raw = f'{self.from_call}>APZ100:;{self.to_call:9s}{self.payload}'
|
491
489
|
|
492
490
|
@property
|
493
491
|
def human_info(self) -> str:
|
494
492
|
h_info = super().human_info
|
495
|
-
return f
|
493
|
+
return f'{h_info} {self.comment}'
|
496
494
|
|
497
495
|
|
498
496
|
@dataclass(unsafe_hash=True)
|
499
497
|
class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
500
|
-
_type: str = field(default=
|
501
|
-
symbol: str =
|
498
|
+
_type: str = field(default='WeatherPacket', hash=False)
|
499
|
+
symbol: str = '_'
|
502
500
|
wind_speed: float = 0.00
|
503
501
|
wind_direction: int = 0
|
504
502
|
wind_gust: float = 0.00
|
@@ -516,8 +514,8 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
|
516
514
|
speed: Optional[float] = field(default=None)
|
517
515
|
|
518
516
|
def _translate(self, raw: dict) -> dict:
|
519
|
-
for key in raw[
|
520
|
-
raw[key] = raw[
|
517
|
+
for key in raw['weather']:
|
518
|
+
raw[key] = raw['weather'][key]
|
521
519
|
|
522
520
|
# If we have the broken aprslib, then we need to
|
523
521
|
# Convert the course and speed to wind_speed and wind_direction
|
@@ -525,36 +523,36 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
|
525
523
|
# https://github.com/rossengeorgiev/aprs-python/issues/80
|
526
524
|
# Wind speed and course is option in the SPEC.
|
527
525
|
# For some reason aprslib multiplies the speed by 1.852.
|
528
|
-
if
|
526
|
+
if 'wind_speed' not in raw and 'wind_direction' not in raw:
|
529
527
|
# Most likely this is the broken aprslib
|
530
528
|
# So we need to convert the wind_gust speed
|
531
|
-
raw[
|
532
|
-
if
|
533
|
-
wind_speed = raw.get(
|
529
|
+
raw['wind_gust'] = round(raw.get('wind_gust', 0) / 0.44704, 3)
|
530
|
+
if 'wind_speed' not in raw:
|
531
|
+
wind_speed = raw.get('speed')
|
534
532
|
if wind_speed:
|
535
|
-
raw[
|
536
|
-
raw[
|
537
|
-
if
|
538
|
-
del raw[
|
533
|
+
raw['wind_speed'] = round(wind_speed / 1.852, 3)
|
534
|
+
raw['weather']['wind_speed'] = raw['wind_speed']
|
535
|
+
if 'speed' in raw:
|
536
|
+
del raw['speed']
|
539
537
|
# Let's adjust the rain numbers as well, since it's wrong
|
540
|
-
raw[
|
541
|
-
raw[
|
542
|
-
raw[
|
543
|
-
raw[
|
544
|
-
raw[
|
545
|
-
(raw.get(
|
538
|
+
raw['rain_1h'] = round((raw.get('rain_1h', 0) / 0.254) * 0.01, 3)
|
539
|
+
raw['weather']['rain_1h'] = raw['rain_1h']
|
540
|
+
raw['rain_24h'] = round((raw.get('rain_24h', 0) / 0.254) * 0.01, 3)
|
541
|
+
raw['weather']['rain_24h'] = raw['rain_24h']
|
542
|
+
raw['rain_since_midnight'] = round(
|
543
|
+
(raw.get('rain_since_midnight', 0) / 0.254) * 0.01, 3
|
546
544
|
)
|
547
|
-
raw[
|
545
|
+
raw['weather']['rain_since_midnight'] = raw['rain_since_midnight']
|
548
546
|
|
549
|
-
if
|
550
|
-
wind_direction = raw.get(
|
547
|
+
if 'wind_direction' not in raw:
|
548
|
+
wind_direction = raw.get('course')
|
551
549
|
if wind_direction:
|
552
|
-
raw[
|
553
|
-
raw[
|
554
|
-
if
|
555
|
-
del raw[
|
550
|
+
raw['wind_direction'] = wind_direction
|
551
|
+
raw['weather']['wind_direction'] = raw['wind_direction']
|
552
|
+
if 'course' in raw:
|
553
|
+
del raw['course']
|
556
554
|
|
557
|
-
del raw[
|
555
|
+
del raw['weather']
|
558
556
|
return raw
|
559
557
|
|
560
558
|
@classmethod
|
@@ -567,20 +565,20 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
|
567
565
|
def key(self) -> str:
|
568
566
|
"""Build a key for finding this packet in a dict."""
|
569
567
|
if self.raw_timestamp:
|
570
|
-
return f
|
568
|
+
return f'{self.from_call}:{self.raw_timestamp}'
|
571
569
|
elif self.wx_raw_timestamp:
|
572
|
-
return f
|
570
|
+
return f'{self.from_call}:{self.wx_raw_timestamp}'
|
573
571
|
|
574
572
|
@property
|
575
573
|
def human_info(self) -> str:
|
576
574
|
h_str = []
|
577
|
-
h_str.append(f
|
578
|
-
h_str.append(f
|
579
|
-
h_str.append(f
|
580
|
-
h_str.append(f
|
581
|
-
h_str.append(f
|
575
|
+
h_str.append(f'Temp {self.temperature:03.0f}F')
|
576
|
+
h_str.append(f'Humidity {self.humidity}%')
|
577
|
+
h_str.append(f'Wind {self.wind_speed:03.0f}MPH@{self.wind_direction}')
|
578
|
+
h_str.append(f'Pressure {self.pressure}mb')
|
579
|
+
h_str.append(f'Rain {self.rain_24h}in/24hr')
|
582
580
|
|
583
|
-
return
|
581
|
+
return ' '.join(h_str)
|
584
582
|
|
585
583
|
def _build_payload(self):
|
586
584
|
"""Build an uncompressed weather packet
|
@@ -610,49 +608,49 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
|
610
608
|
time_zulu = self._build_time_zulu()
|
611
609
|
|
612
610
|
contents = [
|
613
|
-
f
|
614
|
-
f
|
615
|
-
f
|
611
|
+
f'@{time_zulu}z{self.latitude}{self.symbol_table}',
|
612
|
+
f'{self.longitude}{self.symbol}',
|
613
|
+
f'{self.wind_direction:03d}',
|
616
614
|
# Speed = sustained 1 minute wind speed in mph
|
617
|
-
f
|
618
|
-
f
|
615
|
+
f'{self.symbol_table}',
|
616
|
+
f'{self.wind_speed:03.0f}',
|
619
617
|
# wind gust (peak wind speed in mph in the last 5 minutes)
|
620
|
-
f
|
618
|
+
f'g{self.wind_gust:03.0f}',
|
621
619
|
# Temperature in degrees F
|
622
|
-
f
|
620
|
+
f't{self.temperature:03.0f}',
|
623
621
|
# Rainfall (in hundredths of an inch) in the last hour
|
624
|
-
f
|
622
|
+
f'r{self.rain_1h * 100:03.0f}',
|
625
623
|
# Rainfall (in hundredths of an inch) in last 24 hours
|
626
|
-
f
|
624
|
+
f'p{self.rain_24h * 100:03.0f}',
|
627
625
|
# Rainfall (in hundredths of an inch) since midnigt
|
628
|
-
f
|
626
|
+
f'P{self.rain_since_midnight * 100:03.0f}',
|
629
627
|
# Humidity
|
630
|
-
f
|
628
|
+
f'h{self.humidity:02d}',
|
631
629
|
# Barometric pressure (in tenths of millibars/tenths of hPascal)
|
632
|
-
f
|
630
|
+
f'b{self.pressure:05.0f}',
|
633
631
|
]
|
634
632
|
if self.comment:
|
635
|
-
comment = self.
|
633
|
+
comment = self._filter_for_send(self.comment)
|
636
634
|
contents.append(comment)
|
637
|
-
self.payload =
|
635
|
+
self.payload = ''.join(contents)
|
638
636
|
|
639
637
|
def _build_raw(self):
|
640
|
-
self.raw = f
|
638
|
+
self.raw = f'{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:{self.payload}'
|
641
639
|
|
642
640
|
|
643
641
|
@dataclass(unsafe_hash=True)
|
644
642
|
class ThirdPartyPacket(Packet, DataClassJsonMixin):
|
645
|
-
_type: str =
|
643
|
+
_type: str = 'ThirdPartyPacket'
|
646
644
|
# Holds the encapsulated packet
|
647
645
|
subpacket: Optional[type[Packet]] = field(default=None, compare=True, hash=False)
|
648
646
|
|
649
647
|
def __repr__(self):
|
650
648
|
"""Build the repr version of the packet."""
|
651
649
|
repr_str = (
|
652
|
-
f
|
653
|
-
f
|
654
|
-
f
|
655
|
-
f
|
650
|
+
f'{self.__class__.__name__}:'
|
651
|
+
f' From: {self.from_call} '
|
652
|
+
f' To: {self.to_call} '
|
653
|
+
f' Subpacket: {repr(self.subpacket)}'
|
656
654
|
)
|
657
655
|
|
658
656
|
return repr_str
|
@@ -666,12 +664,12 @@ class ThirdPartyPacket(Packet, DataClassJsonMixin):
|
|
666
664
|
@property
|
667
665
|
def key(self) -> str:
|
668
666
|
"""Build a key for finding this packet in a dict."""
|
669
|
-
return f
|
667
|
+
return f'{self.from_call}:{self.subpacket.key}'
|
670
668
|
|
671
669
|
@property
|
672
670
|
def human_info(self) -> str:
|
673
671
|
sub_info = self.subpacket.human_info
|
674
|
-
return f
|
672
|
+
return f'{self.from_call}->{self.to_call} {sub_info}'
|
675
673
|
|
676
674
|
|
677
675
|
@dataclass_json(undefined=Undefined.INCLUDE)
|
@@ -683,11 +681,12 @@ class UnknownPacket:
|
|
683
681
|
"""
|
684
682
|
|
685
683
|
unknown_fields: CatchAll
|
686
|
-
_type: str =
|
684
|
+
_type: str = 'UnknownPacket'
|
687
685
|
from_call: Optional[str] = field(default=None)
|
688
686
|
to_call: Optional[str] = field(default=None)
|
689
687
|
msgNo: str = field(default_factory=_init_msgNo) # noqa: N815
|
690
688
|
format: Optional[str] = field(default=None)
|
689
|
+
timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False)
|
691
690
|
raw: Optional[str] = field(default=None)
|
692
691
|
raw_dict: dict = field(
|
693
692
|
repr=False, default_factory=lambda: {}, compare=False, hash=False
|
@@ -701,7 +700,7 @@ class UnknownPacket:
|
|
701
700
|
@property
|
702
701
|
def key(self) -> str:
|
703
702
|
"""Build a key for finding this packet in a dict."""
|
704
|
-
return f
|
703
|
+
return f'{self.from_call}:{self.packet_type}:{self.to_call}'
|
705
704
|
|
706
705
|
@property
|
707
706
|
def human_info(self) -> str:
|
@@ -728,20 +727,20 @@ TYPE_LOOKUP: dict[str, type[Packet]] = {
|
|
728
727
|
def get_packet_type(packet: dict) -> str:
|
729
728
|
"""Decode the packet type from the packet."""
|
730
729
|
|
731
|
-
pkt_format = packet.get(
|
732
|
-
msg_response = packet.get(
|
730
|
+
pkt_format = packet.get('format')
|
731
|
+
msg_response = packet.get('response')
|
733
732
|
packet_type = PACKET_TYPE_UNKNOWN
|
734
|
-
if pkt_format ==
|
733
|
+
if pkt_format == 'message' and msg_response == 'ack':
|
735
734
|
packet_type = PACKET_TYPE_ACK
|
736
|
-
elif pkt_format ==
|
735
|
+
elif pkt_format == 'message' and msg_response == 'rej':
|
737
736
|
packet_type = PACKET_TYPE_REJECT
|
738
|
-
elif pkt_format ==
|
737
|
+
elif pkt_format == 'message':
|
739
738
|
packet_type = PACKET_TYPE_MESSAGE
|
740
|
-
elif pkt_format ==
|
739
|
+
elif pkt_format == 'mic-e':
|
741
740
|
packet_type = PACKET_TYPE_MICE
|
742
|
-
elif pkt_format ==
|
741
|
+
elif pkt_format == 'object':
|
743
742
|
packet_type = PACKET_TYPE_OBJECT
|
744
|
-
elif pkt_format ==
|
743
|
+
elif pkt_format == 'status':
|
745
744
|
packet_type = PACKET_TYPE_STATUS
|
746
745
|
elif pkt_format == PACKET_TYPE_BULLETIN:
|
747
746
|
packet_type = PACKET_TYPE_BULLETIN
|
@@ -752,13 +751,13 @@ def get_packet_type(packet: dict) -> str:
|
|
752
751
|
elif pkt_format == PACKET_TYPE_WX:
|
753
752
|
packet_type = PACKET_TYPE_WEATHER
|
754
753
|
elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
|
755
|
-
if packet.get(
|
754
|
+
if packet.get('symbol') == '_':
|
756
755
|
packet_type = PACKET_TYPE_WEATHER
|
757
756
|
elif pkt_format == PACKET_TYPE_THIRDPARTY:
|
758
757
|
packet_type = PACKET_TYPE_THIRDPARTY
|
759
758
|
|
760
759
|
if packet_type == PACKET_TYPE_UNKNOWN:
|
761
|
-
if
|
760
|
+
if 'latitude' in packet:
|
762
761
|
packet_type = PACKET_TYPE_BEACON
|
763
762
|
else:
|
764
763
|
packet_type = PACKET_TYPE_UNKNOWN
|
@@ -780,32 +779,32 @@ def is_mice_packet(packet: dict[Any, Any]) -> bool:
|
|
780
779
|
def factory(raw_packet: dict[Any, Any]) -> type[Packet]:
|
781
780
|
"""Factory method to create a packet from a raw packet string."""
|
782
781
|
raw = raw_packet
|
783
|
-
if
|
784
|
-
cls = globals()[raw[
|
782
|
+
if '_type' in raw:
|
783
|
+
cls = globals()[raw['_type']]
|
785
784
|
return cls.from_dict(raw)
|
786
785
|
|
787
|
-
raw[
|
786
|
+
raw['raw_dict'] = raw.copy()
|
788
787
|
raw = _translate_fields(raw)
|
789
788
|
|
790
789
|
packet_type = get_packet_type(raw)
|
791
790
|
|
792
|
-
raw[
|
791
|
+
raw['packet_type'] = packet_type
|
793
792
|
packet_class = TYPE_LOOKUP[packet_type]
|
794
793
|
if packet_type == PACKET_TYPE_WX:
|
795
794
|
# the weather information is in a dict
|
796
795
|
# this brings those values out to the outer dict
|
797
796
|
packet_class = WeatherPacket
|
798
|
-
elif packet_type == PACKET_TYPE_OBJECT and
|
797
|
+
elif packet_type == PACKET_TYPE_OBJECT and 'weather' in raw:
|
799
798
|
packet_class = WeatherPacket
|
800
799
|
elif packet_type == PACKET_TYPE_UNKNOWN:
|
801
800
|
# Try and figure it out here
|
802
|
-
if
|
801
|
+
if 'latitude' in raw:
|
803
802
|
packet_class = GPSPacket
|
804
803
|
else:
|
805
804
|
# LOG.warning(raw)
|
806
805
|
packet_class = UnknownPacket
|
807
806
|
|
808
|
-
raw.get(
|
807
|
+
raw.get('addresse', raw.get('to_call'))
|
809
808
|
|
810
809
|
# TODO: Find a global way to enable/disable this
|
811
810
|
# LOGU.opt(colors=True).info(
|