aprsd 3.3.3__py2.py3-none-any.whl → 3.4.0__py2.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.py +133 -20
- aprsd/clients/aprsis.py +6 -3
- aprsd/clients/fake.py +1 -1
- aprsd/clients/kiss.py +1 -1
- aprsd/cmds/completion.py +13 -27
- aprsd/cmds/fetch_stats.py +53 -57
- aprsd/cmds/healthcheck.py +32 -30
- aprsd/cmds/list_plugins.py +2 -2
- aprsd/cmds/listen.py +33 -17
- aprsd/cmds/send_message.py +2 -2
- aprsd/cmds/server.py +26 -9
- aprsd/cmds/webchat.py +34 -29
- aprsd/conf/common.py +46 -31
- aprsd/log/log.py +28 -6
- aprsd/main.py +20 -18
- aprsd/packets/__init__.py +3 -2
- aprsd/packets/collector.py +56 -0
- aprsd/packets/core.py +456 -321
- aprsd/packets/log.py +143 -0
- aprsd/packets/packet_list.py +83 -66
- aprsd/packets/seen_list.py +30 -19
- aprsd/packets/tracker.py +60 -62
- aprsd/packets/watch_list.py +64 -38
- aprsd/plugin.py +41 -16
- aprsd/plugins/email.py +35 -7
- aprsd/plugins/time.py +3 -2
- aprsd/plugins/version.py +4 -5
- aprsd/plugins/weather.py +0 -1
- aprsd/stats/__init__.py +20 -0
- aprsd/stats/app.py +46 -0
- aprsd/stats/collector.py +38 -0
- aprsd/threads/__init__.py +3 -2
- aprsd/threads/aprsd.py +67 -36
- aprsd/threads/keep_alive.py +55 -49
- aprsd/threads/log_monitor.py +46 -0
- aprsd/threads/rx.py +43 -24
- aprsd/threads/stats.py +44 -0
- aprsd/threads/tx.py +36 -17
- aprsd/utils/__init__.py +12 -0
- aprsd/utils/counter.py +6 -3
- aprsd/utils/json.py +20 -0
- aprsd/utils/objectstore.py +22 -17
- aprsd/web/admin/static/css/prism.css +4 -189
- aprsd/web/admin/static/js/charts.js +9 -7
- aprsd/web/admin/static/js/echarts.js +71 -9
- aprsd/web/admin/static/js/main.js +47 -6
- aprsd/web/admin/static/js/prism.js +11 -2246
- aprsd/web/admin/templates/index.html +18 -7
- aprsd/web/chat/static/js/gps.js +3 -1
- aprsd/web/chat/static/js/main.js +4 -3
- aprsd/web/chat/static/js/send-message.js +5 -2
- aprsd/web/chat/templates/index.html +1 -0
- aprsd/wsgi.py +62 -127
- {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/METADATA +14 -16
- {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/RECORD +60 -65
- {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/WHEEL +1 -1
- aprsd-3.4.0.dist-info/pbr.json +1 -0
- aprsd/plugins/query.py +0 -81
- aprsd/rpc/__init__.py +0 -14
- aprsd/rpc/client.py +0 -165
- aprsd/rpc/server.py +0 -99
- aprsd/stats.py +0 -266
- aprsd/threads/store.py +0 -30
- aprsd/utils/converters.py +0 -15
- aprsd/web/admin/static/json-viewer/jquery.json-viewer.css +0 -57
- aprsd/web/admin/static/json-viewer/jquery.json-viewer.js +0 -158
- aprsd/web/chat/static/json-viewer/jquery.json-viewer.css +0 -57
- aprsd/web/chat/static/json-viewer/jquery.json-viewer.js +0 -158
- aprsd-3.3.3.dist-info/pbr.json +0 -1
- {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/LICENSE +0 -0
- {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/entry_points.txt +0 -0
- {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/top_level.txt +0 -0
aprsd/packets/core.py
CHANGED
@@ -1,30 +1,40 @@
|
|
1
|
-
import
|
2
|
-
from dataclasses import asdict, dataclass, field
|
1
|
+
from dataclasses import dataclass, field
|
3
2
|
from datetime import datetime
|
4
3
|
import logging
|
5
4
|
import re
|
6
5
|
import time
|
7
6
|
# Due to a failure in python 3.8
|
8
|
-
from typing import List
|
7
|
+
from typing import Any, List, Optional, Type, TypeVar, Union
|
9
8
|
|
10
|
-
import
|
11
|
-
from dataclasses_json import
|
9
|
+
from aprslib import util as aprslib_util
|
10
|
+
from dataclasses_json import (
|
11
|
+
CatchAll, DataClassJsonMixin, Undefined, dataclass_json,
|
12
|
+
)
|
13
|
+
from loguru import logger
|
12
14
|
|
13
15
|
from aprsd.utils import counter
|
14
16
|
|
15
17
|
|
16
|
-
|
18
|
+
# For mypy to be happy
|
19
|
+
A = TypeVar("A", bound="DataClassJsonMixin")
|
20
|
+
Json = Union[dict, list, str, int, float, bool, None]
|
17
21
|
|
22
|
+
LOG = logging.getLogger()
|
23
|
+
LOGU = logger
|
24
|
+
|
25
|
+
PACKET_TYPE_BULLETIN = "bulletin"
|
18
26
|
PACKET_TYPE_MESSAGE = "message"
|
19
27
|
PACKET_TYPE_ACK = "ack"
|
20
28
|
PACKET_TYPE_REJECT = "reject"
|
21
29
|
PACKET_TYPE_MICE = "mic-e"
|
22
|
-
PACKET_TYPE_WX = "
|
30
|
+
PACKET_TYPE_WX = "wx"
|
31
|
+
PACKET_TYPE_WEATHER = "weather"
|
23
32
|
PACKET_TYPE_OBJECT = "object"
|
24
33
|
PACKET_TYPE_UNKNOWN = "unknown"
|
25
34
|
PACKET_TYPE_STATUS = "status"
|
26
35
|
PACKET_TYPE_BEACON = "beacon"
|
27
36
|
PACKET_TYPE_THIRDPARTY = "thirdparty"
|
37
|
+
PACKET_TYPE_TELEMETRY = "telemetry-message"
|
28
38
|
PACKET_TYPE_UNCOMPRESSED = "uncompressed"
|
29
39
|
|
30
40
|
NO_DATE = datetime(1900, 10, 24)
|
@@ -52,68 +62,54 @@ def _init_msgNo(): # noqa: N802
|
|
52
62
|
return c.value
|
53
63
|
|
54
64
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
65
|
+
def _translate_fields(raw: dict) -> dict:
|
66
|
+
translate_fields = {
|
67
|
+
"from": "from_call",
|
68
|
+
"to": "to_call",
|
69
|
+
}
|
70
|
+
# First translate some fields
|
71
|
+
for key in translate_fields:
|
72
|
+
if key in raw:
|
73
|
+
raw[translate_fields[key]] = raw[key]
|
74
|
+
del raw[key]
|
60
75
|
|
76
|
+
# addresse overrides to_call
|
77
|
+
if "addresse" in raw:
|
78
|
+
raw["to_call"] = raw["addresse"]
|
61
79
|
|
62
|
-
|
63
|
-
pkt_type = get_packet_type(packet_dict)
|
64
|
-
if pkt_type:
|
65
|
-
return TYPE_LOOKUP[pkt_type].from_json(packet_dict)
|
80
|
+
return raw
|
66
81
|
|
67
82
|
|
68
83
|
@dataclass_json
|
69
84
|
@dataclass(unsafe_hash=True)
|
70
|
-
class Packet
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
85
|
+
class Packet:
|
86
|
+
_type: str = field(default="Packet", hash=False)
|
87
|
+
from_call: Optional[str] = field(default=None)
|
88
|
+
to_call: Optional[str] = field(default=None)
|
89
|
+
addresse: Optional[str] = field(default=None)
|
90
|
+
format: Optional[str] = field(default=None)
|
91
|
+
msgNo: Optional[str] = field(default=None) # noqa: N815
|
92
|
+
ackMsgNo: Optional[str] = field(default=None) # noqa: N815
|
93
|
+
packet_type: Optional[str] = field(default=None)
|
77
94
|
timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False)
|
78
95
|
# Holds the raw text string to be sent over the wire
|
79
96
|
# or holds the raw string from input packet
|
80
|
-
raw: str = field(default=None, compare=False, hash=False)
|
97
|
+
raw: Optional[str] = field(default=None, compare=False, hash=False)
|
81
98
|
raw_dict: dict = field(repr=False, default_factory=lambda: {}, compare=False, hash=False)
|
82
99
|
# Built by calling prepare(). raw needs this built first.
|
83
|
-
payload: str = field(default=None)
|
100
|
+
payload: Optional[str] = field(default=None)
|
84
101
|
|
85
102
|
# Fields related to sending packets out
|
86
103
|
send_count: int = field(repr=False, default=0, compare=False, hash=False)
|
87
104
|
retry_count: int = field(repr=False, default=3, compare=False, hash=False)
|
88
|
-
# last_send_time: datetime = field(
|
89
|
-
# metadata=dc_json_config(
|
90
|
-
# encoder=datetime.isoformat,
|
91
|
-
# decoder=datetime.fromisoformat,
|
92
|
-
# ),
|
93
|
-
# repr=True,
|
94
|
-
# default_factory=_init_send_time,
|
95
|
-
# compare=False,
|
96
|
-
# hash=False
|
97
|
-
# )
|
98
105
|
last_send_time: float = field(repr=False, default=0, compare=False, hash=False)
|
99
|
-
last_send_attempt: int = field(repr=False, default=0, compare=False, hash=False)
|
100
106
|
|
101
107
|
# Do we allow this packet to be saved to send later?
|
102
108
|
allow_delay: bool = field(repr=False, default=True, compare=False, hash=False)
|
103
109
|
path: List[str] = field(default_factory=list, compare=False, hash=False)
|
104
|
-
via: str = field(default=None, compare=False, hash=False)
|
105
|
-
|
106
|
-
def __post__init__(self):
|
107
|
-
LOG.warning(f"POST INIT {self}")
|
108
|
-
|
109
|
-
@property
|
110
|
-
def json(self):
|
111
|
-
"""
|
112
|
-
get the json formated string
|
113
|
-
"""
|
114
|
-
return self.to_json()
|
110
|
+
via: Optional[str] = field(default=None, compare=False, hash=False)
|
115
111
|
|
116
|
-
def get(self, key, default=None):
|
112
|
+
def get(self, key: str, default: Optional[str] = None):
|
117
113
|
"""Emulate a getter on a dict."""
|
118
114
|
if hasattr(self, key):
|
119
115
|
return getattr(self, key)
|
@@ -121,342 +117,222 @@ class Packet(metaclass=abc.ABCMeta):
|
|
121
117
|
return default
|
122
118
|
|
123
119
|
@property
|
124
|
-
def key(self):
|
120
|
+
def key(self) -> str:
|
125
121
|
"""Build a key for finding this packet in a dict."""
|
126
122
|
return f"{self.from_call}:{self.addresse}:{self.msgNo}"
|
127
123
|
|
128
|
-
def update_timestamp(self):
|
124
|
+
def update_timestamp(self) -> None:
|
129
125
|
self.timestamp = _init_timestamp()
|
130
126
|
|
131
|
-
|
127
|
+
@property
|
128
|
+
def human_info(self) -> str:
|
129
|
+
"""Build a human readable string for this packet.
|
130
|
+
|
131
|
+
This doesn't include the from to and type, but just
|
132
|
+
the human readable payload.
|
133
|
+
"""
|
134
|
+
self.prepare()
|
135
|
+
msg = self._filter_for_send(self.raw).rstrip("\n")
|
136
|
+
return msg
|
137
|
+
|
138
|
+
def prepare(self) -> None:
|
132
139
|
"""Do stuff here that is needed prior to sending over the air."""
|
133
140
|
# now build the raw message for sending
|
141
|
+
if not self.msgNo:
|
142
|
+
self.msgNo = _init_msgNo()
|
134
143
|
self._build_payload()
|
135
144
|
self._build_raw()
|
136
145
|
|
137
|
-
def _build_payload(self):
|
146
|
+
def _build_payload(self) -> None:
|
138
147
|
"""The payload is the non headers portion of the packet."""
|
139
|
-
|
148
|
+
if not self.to_call:
|
149
|
+
raise ValueError("to_call isn't set. Must set to_call before calling prepare()")
|
150
|
+
|
151
|
+
# The base packet class has no real payload
|
140
152
|
self.payload = (
|
141
153
|
f":{self.to_call.ljust(9)}"
|
142
|
-
f":{msg}"
|
143
154
|
)
|
144
155
|
|
145
|
-
def _build_raw(self):
|
156
|
+
def _build_raw(self) -> None:
|
146
157
|
"""Build the self.raw which is what is sent over the air."""
|
147
158
|
self.raw = "{}>APZ100:{}".format(
|
148
159
|
self.from_call,
|
149
160
|
self.payload,
|
150
161
|
)
|
151
162
|
|
152
|
-
|
153
|
-
def factory(raw_packet):
|
154
|
-
"""Factory method to create a packet from a raw packet string."""
|
155
|
-
raw = raw_packet
|
156
|
-
raw["raw_dict"] = raw.copy()
|
157
|
-
translate_fields = {
|
158
|
-
"from": "from_call",
|
159
|
-
"to": "to_call",
|
160
|
-
}
|
161
|
-
# First translate some fields
|
162
|
-
for key in translate_fields:
|
163
|
-
if key in raw:
|
164
|
-
raw[translate_fields[key]] = raw[key]
|
165
|
-
del raw[key]
|
166
|
-
|
167
|
-
if "addresse" in raw:
|
168
|
-
raw["to_call"] = raw["addresse"]
|
169
|
-
|
170
|
-
packet_type = get_packet_type(raw)
|
171
|
-
raw["packet_type"] = packet_type
|
172
|
-
class_name = TYPE_LOOKUP[packet_type]
|
173
|
-
if packet_type == PACKET_TYPE_THIRDPARTY:
|
174
|
-
# We have an encapsulated packet!
|
175
|
-
# So we need to decode it and return the inner packet
|
176
|
-
# as the packet we are going to process.
|
177
|
-
# This is a recursive call to the factory
|
178
|
-
subpacket_raw = raw["subpacket"]
|
179
|
-
subpacket = Packet.factory(subpacket_raw)
|
180
|
-
del raw["subpacket"]
|
181
|
-
# raw["subpacket"] = subpacket
|
182
|
-
packet = dacite.from_dict(data_class=class_name, data=raw)
|
183
|
-
packet.subpacket = subpacket
|
184
|
-
return packet
|
185
|
-
|
186
|
-
if packet_type == PACKET_TYPE_UNKNOWN:
|
187
|
-
# Try and figure it out here
|
188
|
-
if "latitude" in raw:
|
189
|
-
class_name = GPSPacket
|
190
|
-
|
191
|
-
if packet_type == PACKET_TYPE_WX:
|
192
|
-
# the weather information is in a dict
|
193
|
-
# this brings those values out to the outer dict
|
194
|
-
|
195
|
-
for key in raw["weather"]:
|
196
|
-
raw[key] = raw["weather"][key]
|
197
|
-
|
198
|
-
# If we have the broken aprslib, then we need to
|
199
|
-
# Convert the course and speed to wind_speed and wind_direction
|
200
|
-
# aprslib issue #80
|
201
|
-
# https://github.com/rossengeorgiev/aprs-python/issues/80
|
202
|
-
# Wind speed and course is option in the SPEC.
|
203
|
-
# For some reason aprslib multiplies the speed by 1.852.
|
204
|
-
if "wind_speed" not in raw and "wind_direction" not in raw:
|
205
|
-
# Most likely this is the broken aprslib
|
206
|
-
# So we need to convert the wind_gust speed
|
207
|
-
raw["wind_gust"] = round(raw.get("wind_gust", 0) / 0.44704, 3)
|
208
|
-
if "wind_speed" not in raw:
|
209
|
-
wind_speed = raw.get("speed")
|
210
|
-
if wind_speed:
|
211
|
-
raw["wind_speed"] = round(wind_speed / 1.852, 3)
|
212
|
-
raw["weather"]["wind_speed"] = raw["wind_speed"]
|
213
|
-
if "speed" in raw:
|
214
|
-
del raw["speed"]
|
215
|
-
# Let's adjust the rain numbers as well, since it's wrong
|
216
|
-
raw["rain_1h"] = round((raw.get("rain_1h", 0) / .254) * .01, 3)
|
217
|
-
raw["weather"]["rain_1h"] = raw["rain_1h"]
|
218
|
-
raw["rain_24h"] = round((raw.get("rain_24h", 0) / .254) * .01, 3)
|
219
|
-
raw["weather"]["rain_24h"] = raw["rain_24h"]
|
220
|
-
raw["rain_since_midnight"] = round((raw.get("rain_since_midnight", 0) / .254) * .01, 3)
|
221
|
-
raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"]
|
222
|
-
|
223
|
-
if "wind_direction" not in raw:
|
224
|
-
wind_direction = raw.get("course")
|
225
|
-
if wind_direction:
|
226
|
-
raw["wind_direction"] = wind_direction
|
227
|
-
raw["weather"]["wind_direction"] = raw["wind_direction"]
|
228
|
-
if "course" in raw:
|
229
|
-
del raw["course"]
|
230
|
-
|
231
|
-
return dacite.from_dict(data_class=class_name, data=raw)
|
232
|
-
|
233
|
-
def log(self, header=None):
|
234
|
-
"""LOG a packet to the logfile."""
|
235
|
-
asdict(self)
|
236
|
-
log_list = ["\n"]
|
237
|
-
name = self.__class__.__name__
|
238
|
-
if header:
|
239
|
-
if "tx" in header.lower():
|
240
|
-
log_list.append(
|
241
|
-
f"{header}________({name} "
|
242
|
-
f"TX:{self.send_count+1} of {self.retry_count})",
|
243
|
-
)
|
244
|
-
else:
|
245
|
-
log_list.append(f"{header}________({name})")
|
246
|
-
# log_list.append(f" Packet : {self.__class__.__name__}")
|
247
|
-
log_list.append(f" Raw : {self.raw}")
|
248
|
-
if self.to_call:
|
249
|
-
log_list.append(f" To : {self.to_call}")
|
250
|
-
if self.from_call:
|
251
|
-
log_list.append(f" From : {self.from_call}")
|
252
|
-
if hasattr(self, "path") and self.path:
|
253
|
-
log_list.append(f" Path : {'=>'.join(self.path)}")
|
254
|
-
if hasattr(self, "via") and self.via:
|
255
|
-
log_list.append(f" VIA : {self.via}")
|
256
|
-
|
257
|
-
elif isinstance(self, MessagePacket):
|
258
|
-
log_list.append(f" Message : {self.message_text}")
|
259
|
-
|
260
|
-
if hasattr(self, "comment") and self.comment:
|
261
|
-
log_list.append(f" Comment : {self.comment}")
|
262
|
-
|
263
|
-
if self.msgNo:
|
264
|
-
log_list.append(f" Msg # : {self.msgNo}")
|
265
|
-
log_list.append(f"{header}________({name})")
|
266
|
-
|
267
|
-
LOG.info("\n".join(log_list))
|
268
|
-
LOG.debug(repr(self))
|
269
|
-
|
270
|
-
def _filter_for_send(self) -> str:
|
163
|
+
def _filter_for_send(self, msg) -> str:
|
271
164
|
"""Filter and format message string for FCC."""
|
272
165
|
# max? ftm400 displays 64, raw msg shows 74
|
273
166
|
# and ftm400-send is max 64. setting this to
|
274
167
|
# 67 displays 64 on the ftm400. (+3 {01 suffix)
|
275
168
|
# feature req: break long ones into two msgs
|
276
|
-
|
169
|
+
if not msg:
|
170
|
+
return ""
|
171
|
+
|
172
|
+
message = msg[:67]
|
277
173
|
# We all miss George Carlin
|
278
|
-
return re.sub(
|
174
|
+
return re.sub(
|
175
|
+
"fuck|shit|cunt|piss|cock|bitch", "****",
|
176
|
+
message, flags=re.IGNORECASE,
|
177
|
+
)
|
279
178
|
|
280
|
-
def __str__(self):
|
179
|
+
def __str__(self) -> str:
|
281
180
|
"""Show the raw version of the packet"""
|
282
181
|
self.prepare()
|
182
|
+
if not self.raw:
|
183
|
+
raise ValueError("self.raw is unset")
|
283
184
|
return self.raw
|
284
185
|
|
285
|
-
def __repr__(self):
|
186
|
+
def __repr__(self) -> str:
|
286
187
|
"""Build the repr version of the packet."""
|
287
188
|
repr = (
|
288
189
|
f"{self.__class__.__name__}:"
|
289
190
|
f" From: {self.from_call} "
|
290
|
-
"
|
191
|
+
f" To: {self.to_call}"
|
291
192
|
)
|
292
|
-
|
293
193
|
return repr
|
294
194
|
|
295
195
|
|
196
|
+
@dataclass_json
|
296
197
|
@dataclass(unsafe_hash=True)
|
297
198
|
class AckPacket(Packet):
|
298
|
-
|
299
|
-
|
300
|
-
def __post__init__(self):
|
301
|
-
if self.response:
|
302
|
-
LOG.warning("Response set!")
|
199
|
+
_type: str = field(default="AckPacket", hash=False)
|
303
200
|
|
304
201
|
def _build_payload(self):
|
305
|
-
self.payload = f":{self.to_call
|
202
|
+
self.payload = f":{self.to_call: <9}:ack{self.msgNo}"
|
306
203
|
|
307
204
|
|
205
|
+
@dataclass_json
|
206
|
+
@dataclass(unsafe_hash=True)
|
207
|
+
class BulletinPacket(Packet):
|
208
|
+
_type: str = "BulletinPacket"
|
209
|
+
# Holds the encapsulated packet
|
210
|
+
bid: Optional[str] = field(default="1")
|
211
|
+
message_text: Optional[str] = field(default=None)
|
212
|
+
|
213
|
+
@property
|
214
|
+
def key(self) -> str:
|
215
|
+
"""Build a key for finding this packet in a dict."""
|
216
|
+
return f"{self.from_call}:BLN{self.bid}"
|
217
|
+
|
218
|
+
@property
|
219
|
+
def human_info(self) -> str:
|
220
|
+
return f"BLN{self.bid} {self.message_text}"
|
221
|
+
|
222
|
+
def _build_payload(self) -> None:
|
223
|
+
self.payload = (
|
224
|
+
f":BLN{self.bid:<9}"
|
225
|
+
f":{self.message_text}"
|
226
|
+
)
|
227
|
+
|
228
|
+
|
229
|
+
@dataclass_json
|
308
230
|
@dataclass(unsafe_hash=True)
|
309
231
|
class RejectPacket(Packet):
|
310
|
-
|
232
|
+
_type: str = field(default="RejectPacket", hash=False)
|
233
|
+
response: Optional[str] = field(default=None)
|
311
234
|
|
312
235
|
def __post__init__(self):
|
313
236
|
if self.response:
|
314
237
|
LOG.warning("Response set!")
|
315
238
|
|
316
239
|
def _build_payload(self):
|
317
|
-
self.payload = f":{self.to_call
|
240
|
+
self.payload = f":{self.to_call: <9}:rej{self.msgNo}"
|
318
241
|
|
319
242
|
|
320
243
|
@dataclass_json
|
321
244
|
@dataclass(unsafe_hash=True)
|
322
245
|
class MessagePacket(Packet):
|
323
|
-
|
246
|
+
_type: str = field(default="MessagePacket", hash=False)
|
247
|
+
message_text: Optional[str] = field(default=None)
|
324
248
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
# 67 displays 64 on the ftm400. (+3 {01 suffix)
|
330
|
-
# feature req: break long ones into two msgs
|
331
|
-
message = self.message_text[:67]
|
332
|
-
# We all miss George Carlin
|
333
|
-
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
|
249
|
+
@property
|
250
|
+
def human_info(self) -> str:
|
251
|
+
self.prepare()
|
252
|
+
return self._filter_for_send(self.message_text).rstrip("\n")
|
334
253
|
|
335
254
|
def _build_payload(self):
|
336
255
|
self.payload = ":{}:{}{{{}".format(
|
337
256
|
self.to_call.ljust(9),
|
338
|
-
self._filter_for_send().rstrip("\n"),
|
257
|
+
self._filter_for_send(self.message_text).rstrip("\n"),
|
339
258
|
str(self.msgNo),
|
340
259
|
)
|
341
260
|
|
342
261
|
|
262
|
+
@dataclass_json
|
343
263
|
@dataclass(unsafe_hash=True)
|
344
264
|
class StatusPacket(Packet):
|
345
|
-
|
265
|
+
_type: str = field(default="StatusPacket", hash=False)
|
266
|
+
status: Optional[str] = field(default=None)
|
346
267
|
messagecapable: bool = field(default=False)
|
347
|
-
comment: str = field(default=None)
|
268
|
+
comment: Optional[str] = field(default=None)
|
269
|
+
raw_timestamp: Optional[str] = field(default=None)
|
348
270
|
|
349
271
|
def _build_payload(self):
|
350
|
-
|
272
|
+
self.payload = ":{}:{}{{{}".format(
|
273
|
+
self.to_call.ljust(9),
|
274
|
+
self._filter_for_send(self.status).rstrip("\n"),
|
275
|
+
str(self.msgNo),
|
276
|
+
)
|
277
|
+
|
278
|
+
@property
|
279
|
+
def human_info(self) -> str:
|
280
|
+
self.prepare()
|
281
|
+
return self.status
|
351
282
|
|
352
283
|
|
284
|
+
@dataclass_json
|
353
285
|
@dataclass(unsafe_hash=True)
|
354
286
|
class GPSPacket(Packet):
|
287
|
+
_type: str = field(default="GPSPacket", hash=False)
|
355
288
|
latitude: float = field(default=0.00)
|
356
289
|
longitude: float = field(default=0.00)
|
357
290
|
altitude: float = field(default=0.00)
|
358
291
|
rng: float = field(default=0.00)
|
359
292
|
posambiguity: int = field(default=0)
|
360
|
-
|
293
|
+
messagecapable: bool = field(default=False)
|
294
|
+
comment: Optional[str] = field(default=None)
|
361
295
|
symbol: str = field(default="l")
|
362
296
|
symbol_table: str = field(default="/")
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
is_positive = degrees_decimal >= 0
|
379
|
-
degrees_decimal = abs(degrees_decimal)
|
380
|
-
minutes, seconds = divmod(degrees_decimal * 3600, 60)
|
381
|
-
degrees, minutes = divmod(minutes, 60)
|
382
|
-
degrees = degrees if is_positive else -degrees
|
383
|
-
|
384
|
-
degrees = abs(int(degrees))
|
385
|
-
minutes = int(round(minutes + (seconds / 60), 2))
|
386
|
-
hundredths = round(seconds / 60, 2)
|
387
|
-
|
388
|
-
return {
|
389
|
-
"degrees": degrees, "minutes": minutes, "seconds": seconds,
|
390
|
-
"hundredths": hundredths,
|
391
|
-
}
|
392
|
-
|
393
|
-
def convert_latitude(self, degrees_decimal):
|
394
|
-
det = self.decdeg2dmm_m(degrees_decimal)
|
395
|
-
if degrees_decimal > 0:
|
396
|
-
direction = "N"
|
397
|
-
else:
|
398
|
-
direction = "S"
|
399
|
-
|
400
|
-
degrees = str(det.get("degrees")).zfill(2)
|
401
|
-
minutes = str(det.get("minutes")).zfill(2)
|
402
|
-
seconds = det.get("seconds")
|
403
|
-
hun = det.get("hundredths")
|
404
|
-
hundredths = f"{hun:.2f}".split(".")[1]
|
405
|
-
|
406
|
-
LOG.debug(
|
407
|
-
f"LAT degress {degrees} minutes {str(minutes)} "
|
408
|
-
f"seconds {seconds} hundredths {hundredths} direction {direction}",
|
409
|
-
)
|
410
|
-
|
411
|
-
lat = f"{degrees}{str(minutes)}.{hundredths}{direction}"
|
412
|
-
return lat
|
413
|
-
|
414
|
-
def convert_longitude(self, degrees_decimal):
|
415
|
-
det = self.decdeg2dmm_m(degrees_decimal)
|
416
|
-
if degrees_decimal > 0:
|
417
|
-
direction = "E"
|
418
|
-
else:
|
419
|
-
direction = "W"
|
420
|
-
|
421
|
-
degrees = str(det.get("degrees")).zfill(3)
|
422
|
-
minutes = str(det.get("minutes")).zfill(2)
|
423
|
-
seconds = det.get("seconds")
|
424
|
-
hun = det.get("hundredths")
|
425
|
-
hundredths = f"{hun:.2f}".split(".")[1]
|
426
|
-
|
427
|
-
LOG.debug(
|
428
|
-
f"LON degress {degrees} minutes {str(minutes)} "
|
429
|
-
f"seconds {seconds} hundredths {hundredths} direction {direction}",
|
430
|
-
)
|
431
|
-
|
432
|
-
lon = f"{degrees}{str(minutes)}.{hundredths}{direction}"
|
433
|
-
return lon
|
297
|
+
raw_timestamp: Optional[str] = field(default=None)
|
298
|
+
object_name: Optional[str] = field(default=None)
|
299
|
+
object_format: Optional[str] = field(default=None)
|
300
|
+
alive: Optional[bool] = field(default=None)
|
301
|
+
course: Optional[int] = field(default=None)
|
302
|
+
speed: Optional[float] = field(default=None)
|
303
|
+
phg: Optional[str] = field(default=None)
|
304
|
+
phg_power: Optional[int] = field(default=None)
|
305
|
+
phg_height: Optional[float] = field(default=None)
|
306
|
+
phg_gain: Optional[int] = field(default=None)
|
307
|
+
phg_dir: Optional[str] = field(default=None)
|
308
|
+
phg_range: Optional[float] = field(default=None)
|
309
|
+
phg_rate: Optional[int] = field(default=None)
|
310
|
+
# http://www.aprs.org/datum.txt
|
311
|
+
daodatumbyte: Optional[str] = field(default=None)
|
434
312
|
|
435
313
|
def _build_time_zulu(self):
|
436
314
|
"""Build the timestamp in UTC/zulu."""
|
437
315
|
if self.timestamp:
|
438
|
-
|
439
|
-
else:
|
440
|
-
local_dt = datetime.now()
|
441
|
-
self.timestamp = datetime.timestamp(local_dt)
|
442
|
-
|
443
|
-
utc_offset_timedelta = datetime.utcnow() - local_dt
|
444
|
-
result_utc_datetime = local_dt + utc_offset_timedelta
|
445
|
-
time_zulu = result_utc_datetime.strftime("%d%H%M")
|
446
|
-
return time_zulu
|
316
|
+
return datetime.utcfromtimestamp(self.timestamp).strftime("%d%H%M")
|
447
317
|
|
448
318
|
def _build_payload(self):
|
449
319
|
"""The payload is the non headers portion of the packet."""
|
450
320
|
time_zulu = self._build_time_zulu()
|
451
|
-
lat = self.latitude
|
452
|
-
long = self.longitude
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
321
|
+
lat = aprslib_util.latitude_to_ddm(self.latitude)
|
322
|
+
long = aprslib_util.longitude_to_ddm(self.longitude)
|
323
|
+
payload = [
|
324
|
+
"@" if self.timestamp else "!",
|
325
|
+
time_zulu,
|
326
|
+
lat,
|
327
|
+
self.symbol_table,
|
328
|
+
long,
|
329
|
+
self.symbol,
|
330
|
+
]
|
457
331
|
|
458
332
|
if self.comment:
|
459
|
-
|
333
|
+
payload.append(self._filter_for_send(self.comment))
|
334
|
+
|
335
|
+
self.payload = "".join(payload)
|
460
336
|
|
461
337
|
def _build_raw(self):
|
462
338
|
self.raw = (
|
@@ -464,45 +340,129 @@ class GPSPacket(Packet):
|
|
464
340
|
f"{self.payload}"
|
465
341
|
)
|
466
342
|
|
343
|
+
@property
|
344
|
+
def human_info(self) -> str:
|
345
|
+
h_str = []
|
346
|
+
h_str.append(f"Lat:{self.latitude:03.3f}")
|
347
|
+
h_str.append(f"Lon:{self.longitude:03.3f}")
|
348
|
+
if self.altitude:
|
349
|
+
h_str.append(f"Altitude {self.altitude:03.0f}")
|
350
|
+
if self.speed:
|
351
|
+
h_str.append(f"Speed {self.speed:03.0f}MPH")
|
352
|
+
if self.course:
|
353
|
+
h_str.append(f"Course {self.course:03.0f}")
|
354
|
+
if self.rng:
|
355
|
+
h_str.append(f"RNG {self.rng:03.0f}")
|
356
|
+
if self.phg:
|
357
|
+
h_str.append(f"PHG {self.phg}")
|
358
|
+
|
359
|
+
return " ".join(h_str)
|
467
360
|
|
361
|
+
|
362
|
+
@dataclass_json
|
468
363
|
@dataclass(unsafe_hash=True)
|
469
364
|
class BeaconPacket(GPSPacket):
|
365
|
+
_type: str = field(default="BeaconPacket", hash=False)
|
366
|
+
|
470
367
|
def _build_payload(self):
|
471
368
|
"""The payload is the non headers portion of the packet."""
|
472
369
|
time_zulu = self._build_time_zulu()
|
473
|
-
lat =
|
474
|
-
|
370
|
+
lat = aprslib_util.latitude_to_ddm(self.latitude)
|
371
|
+
lon = aprslib_util.longitude_to_ddm(self.longitude)
|
475
372
|
|
476
373
|
self.payload = (
|
477
374
|
f"@{time_zulu}z{lat}{self.symbol_table}"
|
478
|
-
f"{
|
375
|
+
f"{lon}"
|
479
376
|
)
|
480
377
|
|
378
|
+
if self.comment:
|
379
|
+
comment = self._filter_for_send(self.comment)
|
380
|
+
self.payload = f"{self.payload}{self.symbol}{comment}"
|
381
|
+
else:
|
382
|
+
self.payload = f"{self.payload}{self.symbol}APRSD Beacon"
|
383
|
+
|
481
384
|
def _build_raw(self):
|
482
385
|
self.raw = (
|
483
386
|
f"{self.from_call}>APZ100:"
|
484
387
|
f"{self.payload}"
|
485
388
|
)
|
486
389
|
|
390
|
+
@property
|
391
|
+
def key(self) -> str:
|
392
|
+
"""Build a key for finding this packet in a dict."""
|
393
|
+
if self.raw_timestamp:
|
394
|
+
return f"{self.from_call}:{self.raw_timestamp}"
|
395
|
+
else:
|
396
|
+
return f"{self.from_call}:{self.human_info.replace(' ','')}"
|
397
|
+
|
398
|
+
@property
|
399
|
+
def human_info(self) -> str:
|
400
|
+
h_str = []
|
401
|
+
h_str.append(f"Lat:{self.latitude:03.3f}")
|
402
|
+
h_str.append(f"Lon:{self.longitude:03.3f}")
|
403
|
+
h_str.append(f"{self.comment}")
|
404
|
+
return " ".join(h_str)
|
405
|
+
|
487
406
|
|
488
|
-
@
|
407
|
+
@dataclass_json
|
408
|
+
@dataclass(unsafe_hash=True)
|
489
409
|
class MicEPacket(GPSPacket):
|
410
|
+
_type: str = field(default="MicEPacket", hash=False)
|
490
411
|
messagecapable: bool = False
|
491
|
-
mbits: str = None
|
492
|
-
mtype: str = None
|
412
|
+
mbits: Optional[str] = None
|
413
|
+
mtype: Optional[str] = None
|
414
|
+
telemetry: Optional[dict] = field(default=None)
|
493
415
|
# in MPH
|
494
416
|
speed: float = 0.00
|
495
417
|
# 0 to 360
|
496
418
|
course: int = 0
|
497
419
|
|
498
|
-
|
499
|
-
|
420
|
+
@property
|
421
|
+
def key(self) -> str:
|
422
|
+
"""Build a key for finding this packet in a dict."""
|
423
|
+
return f"{self.from_call}:{self.human_info.replace(' ', '')}"
|
424
|
+
|
425
|
+
@property
|
426
|
+
def human_info(self) -> str:
|
427
|
+
h_info = super().human_info
|
428
|
+
return f"{h_info} {self.mbits} mbits"
|
500
429
|
|
501
430
|
|
502
|
-
@
|
431
|
+
@dataclass_json
|
432
|
+
@dataclass(unsafe_hash=True)
|
433
|
+
class TelemetryPacket(GPSPacket):
|
434
|
+
_type: str = field(default="TelemetryPacket", hash=False)
|
435
|
+
messagecapable: bool = False
|
436
|
+
mbits: Optional[str] = None
|
437
|
+
mtype: Optional[str] = None
|
438
|
+
telemetry: Optional[dict] = field(default=None)
|
439
|
+
tPARM: Optional[list[str]] = field(default=None) # noqa: N815
|
440
|
+
tUNIT: Optional[list[str]] = field(default=None) # noqa: N815
|
441
|
+
# in MPH
|
442
|
+
speed: float = 0.00
|
443
|
+
# 0 to 360
|
444
|
+
course: int = 0
|
445
|
+
|
446
|
+
@property
|
447
|
+
def key(self) -> str:
|
448
|
+
"""Build a key for finding this packet in a dict."""
|
449
|
+
if self.raw_timestamp:
|
450
|
+
return f"{self.from_call}:{self.raw_timestamp}"
|
451
|
+
else:
|
452
|
+
return f"{self.from_call}:{self.human_info.replace(' ','')}"
|
453
|
+
|
454
|
+
@property
|
455
|
+
def human_info(self) -> str:
|
456
|
+
h_info = super().human_info
|
457
|
+
return f"{h_info} {self.telemetry}"
|
458
|
+
|
459
|
+
|
460
|
+
@dataclass_json
|
461
|
+
@dataclass(unsafe_hash=True)
|
503
462
|
class ObjectPacket(GPSPacket):
|
463
|
+
_type: str = field(default="ObjectPacket", hash=False)
|
504
464
|
alive: bool = True
|
505
|
-
raw_timestamp: str = None
|
465
|
+
raw_timestamp: Optional[str] = None
|
506
466
|
symbol: str = field(default="r")
|
507
467
|
# in MPH
|
508
468
|
speed: float = 0.00
|
@@ -511,8 +471,8 @@ class ObjectPacket(GPSPacket):
|
|
511
471
|
|
512
472
|
def _build_payload(self):
|
513
473
|
time_zulu = self._build_time_zulu()
|
514
|
-
lat =
|
515
|
-
long =
|
474
|
+
lat = aprslib_util.latitude_to_ddm(self.latitude)
|
475
|
+
long = aprslib_util.longitude_to_ddm(self.longitude)
|
516
476
|
|
517
477
|
self.payload = (
|
518
478
|
f"*{time_zulu}z{lat}{self.symbol_table}"
|
@@ -520,7 +480,8 @@ class ObjectPacket(GPSPacket):
|
|
520
480
|
)
|
521
481
|
|
522
482
|
if self.comment:
|
523
|
-
|
483
|
+
comment = self._filter_for_send(self.comment)
|
484
|
+
self.payload = f"{self.payload}{comment}"
|
524
485
|
|
525
486
|
def _build_raw(self):
|
526
487
|
"""
|
@@ -538,9 +499,15 @@ class ObjectPacket(GPSPacket):
|
|
538
499
|
f"{self.payload}"
|
539
500
|
)
|
540
501
|
|
502
|
+
@property
|
503
|
+
def human_info(self) -> str:
|
504
|
+
h_info = super().human_info
|
505
|
+
return f"{h_info} {self.comment}"
|
506
|
+
|
541
507
|
|
542
|
-
@dataclass()
|
543
|
-
class WeatherPacket(GPSPacket):
|
508
|
+
@dataclass(unsafe_hash=True)
|
509
|
+
class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
510
|
+
_type: str = field(default="WeatherPacket", hash=False)
|
544
511
|
symbol: str = "_"
|
545
512
|
wind_speed: float = 0.00
|
546
513
|
wind_direction: int = 0
|
@@ -552,7 +519,76 @@ class WeatherPacket(GPSPacket):
|
|
552
519
|
rain_since_midnight: float = 0.00
|
553
520
|
humidity: int = 0
|
554
521
|
pressure: float = 0.00
|
555
|
-
comment: str = None
|
522
|
+
comment: Optional[str] = field(default=None)
|
523
|
+
luminosity: Optional[int] = field(default=None)
|
524
|
+
wx_raw_timestamp: Optional[str] = field(default=None)
|
525
|
+
course: Optional[int] = field(default=None)
|
526
|
+
speed: Optional[float] = field(default=None)
|
527
|
+
|
528
|
+
def _translate(self, raw: dict) -> dict:
|
529
|
+
for key in raw["weather"]:
|
530
|
+
raw[key] = raw["weather"][key]
|
531
|
+
|
532
|
+
# If we have the broken aprslib, then we need to
|
533
|
+
# Convert the course and speed to wind_speed and wind_direction
|
534
|
+
# aprslib issue #80
|
535
|
+
# https://github.com/rossengeorgiev/aprs-python/issues/80
|
536
|
+
# Wind speed and course is option in the SPEC.
|
537
|
+
# For some reason aprslib multiplies the speed by 1.852.
|
538
|
+
if "wind_speed" not in raw and "wind_direction" not in raw:
|
539
|
+
# Most likely this is the broken aprslib
|
540
|
+
# So we need to convert the wind_gust speed
|
541
|
+
raw["wind_gust"] = round(raw.get("wind_gust", 0) / 0.44704, 3)
|
542
|
+
if "wind_speed" not in raw:
|
543
|
+
wind_speed = raw.get("speed")
|
544
|
+
if wind_speed:
|
545
|
+
raw["wind_speed"] = round(wind_speed / 1.852, 3)
|
546
|
+
raw["weather"]["wind_speed"] = raw["wind_speed"]
|
547
|
+
if "speed" in raw:
|
548
|
+
del raw["speed"]
|
549
|
+
# Let's adjust the rain numbers as well, since it's wrong
|
550
|
+
raw["rain_1h"] = round((raw.get("rain_1h", 0) / .254) * .01, 3)
|
551
|
+
raw["weather"]["rain_1h"] = raw["rain_1h"]
|
552
|
+
raw["rain_24h"] = round((raw.get("rain_24h", 0) / .254) * .01, 3)
|
553
|
+
raw["weather"]["rain_24h"] = raw["rain_24h"]
|
554
|
+
raw["rain_since_midnight"] = round((raw.get("rain_since_midnight", 0) / .254) * .01, 3)
|
555
|
+
raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"]
|
556
|
+
|
557
|
+
if "wind_direction" not in raw:
|
558
|
+
wind_direction = raw.get("course")
|
559
|
+
if wind_direction:
|
560
|
+
raw["wind_direction"] = wind_direction
|
561
|
+
raw["weather"]["wind_direction"] = raw["wind_direction"]
|
562
|
+
if "course" in raw:
|
563
|
+
del raw["course"]
|
564
|
+
|
565
|
+
del raw["weather"]
|
566
|
+
return raw
|
567
|
+
|
568
|
+
@classmethod
|
569
|
+
def from_dict(cls: Type[A], kvs: Json, *, infer_missing=False) -> A:
|
570
|
+
"""Create from a dictionary that has come directly from aprslib parse"""
|
571
|
+
raw = cls._translate(cls, kvs) # type: ignore
|
572
|
+
return super().from_dict(raw)
|
573
|
+
|
574
|
+
@property
|
575
|
+
def key(self) -> str:
|
576
|
+
"""Build a key for finding this packet in a dict."""
|
577
|
+
if self.raw_timestamp:
|
578
|
+
return f"{self.from_call}:{self.raw_timestamp}"
|
579
|
+
elif self.wx_raw_timestamp:
|
580
|
+
return f"{self.from_call}:{self.wx_raw_timestamp}"
|
581
|
+
|
582
|
+
@property
|
583
|
+
def human_info(self) -> str:
|
584
|
+
h_str = []
|
585
|
+
h_str.append(f"Temp {self.temperature:03.0f}F")
|
586
|
+
h_str.append(f"Humidity {self.humidity}%")
|
587
|
+
h_str.append(f"Wind {self.wind_speed:03.0f}MPH@{self.wind_direction}")
|
588
|
+
h_str.append(f"Pressure {self.pressure}mb")
|
589
|
+
h_str.append(f"Rain {self.rain_24h}in/24hr")
|
590
|
+
|
591
|
+
return " ".join(h_str)
|
556
592
|
|
557
593
|
def _build_payload(self):
|
558
594
|
"""Build an uncompressed weather packet
|
@@ -603,7 +639,8 @@ class WeatherPacket(GPSPacket):
|
|
603
639
|
f"b{self.pressure:05.0f}",
|
604
640
|
]
|
605
641
|
if self.comment:
|
606
|
-
|
642
|
+
comment = self.filter_for_send(self.comment)
|
643
|
+
contents.append(comment)
|
607
644
|
self.payload = "".join(contents)
|
608
645
|
|
609
646
|
def _build_raw(self):
|
@@ -614,9 +651,11 @@ class WeatherPacket(GPSPacket):
|
|
614
651
|
)
|
615
652
|
|
616
653
|
|
617
|
-
|
654
|
+
@dataclass(unsafe_hash=True)
|
655
|
+
class ThirdPartyPacket(Packet, DataClassJsonMixin):
|
656
|
+
_type: str = "ThirdPartyPacket"
|
618
657
|
# Holds the encapsulated packet
|
619
|
-
subpacket: Packet = field(default=None, compare=True, hash=False)
|
658
|
+
subpacket: Optional[type[Packet]] = field(default=None, compare=True, hash=False)
|
620
659
|
|
621
660
|
def __repr__(self):
|
622
661
|
"""Build the repr version of the packet."""
|
@@ -629,26 +668,74 @@ class ThirdParty(Packet):
|
|
629
668
|
|
630
669
|
return repr_str
|
631
670
|
|
671
|
+
@classmethod
|
672
|
+
def from_dict(cls: Type[A], kvs: Json, *, infer_missing=False) -> A:
|
673
|
+
obj = super().from_dict(kvs)
|
674
|
+
obj.subpacket = factory(obj.subpacket) # type: ignore
|
675
|
+
return obj
|
676
|
+
|
677
|
+
@property
|
678
|
+
def key(self) -> str:
|
679
|
+
"""Build a key for finding this packet in a dict."""
|
680
|
+
return f"{self.from_call}:{self.subpacket.key}"
|
681
|
+
|
682
|
+
@property
|
683
|
+
def human_info(self) -> str:
|
684
|
+
sub_info = self.subpacket.human_info
|
685
|
+
return f"{self.from_call}->{self.to_call} {sub_info}"
|
686
|
+
|
687
|
+
|
688
|
+
@dataclass_json(undefined=Undefined.INCLUDE)
|
689
|
+
@dataclass(unsafe_hash=True)
|
690
|
+
class UnknownPacket:
|
691
|
+
"""Catchall Packet for things we don't know about.
|
632
692
|
|
633
|
-
|
693
|
+
All of the unknown attributes are stored in the unknown_fields
|
694
|
+
"""
|
695
|
+
unknown_fields: CatchAll
|
696
|
+
_type: str = "UnknownPacket"
|
697
|
+
from_call: Optional[str] = field(default=None)
|
698
|
+
to_call: Optional[str] = field(default=None)
|
699
|
+
msgNo: str = field(default_factory=_init_msgNo) # noqa: N815
|
700
|
+
format: Optional[str] = field(default=None)
|
701
|
+
raw: Optional[str] = field(default=None)
|
702
|
+
raw_dict: dict = field(repr=False, default_factory=lambda: {}, compare=False, hash=False)
|
703
|
+
path: List[str] = field(default_factory=list, compare=False, hash=False)
|
704
|
+
packet_type: Optional[str] = field(default=None)
|
705
|
+
via: Optional[str] = field(default=None, compare=False, hash=False)
|
706
|
+
|
707
|
+
@property
|
708
|
+
def key(self) -> str:
|
709
|
+
"""Build a key for finding this packet in a dict."""
|
710
|
+
return f"{self.from_call}:{self.packet_type}:{self.to_call}"
|
711
|
+
|
712
|
+
@property
|
713
|
+
def human_info(self) -> str:
|
714
|
+
return str(self.unknown_fields)
|
715
|
+
|
716
|
+
|
717
|
+
TYPE_LOOKUP: dict[str, type[Packet]] = {
|
718
|
+
PACKET_TYPE_BULLETIN: BulletinPacket,
|
634
719
|
PACKET_TYPE_WX: WeatherPacket,
|
720
|
+
PACKET_TYPE_WEATHER: WeatherPacket,
|
635
721
|
PACKET_TYPE_MESSAGE: MessagePacket,
|
636
722
|
PACKET_TYPE_ACK: AckPacket,
|
637
723
|
PACKET_TYPE_REJECT: RejectPacket,
|
638
724
|
PACKET_TYPE_MICE: MicEPacket,
|
639
725
|
PACKET_TYPE_OBJECT: ObjectPacket,
|
640
726
|
PACKET_TYPE_STATUS: StatusPacket,
|
641
|
-
PACKET_TYPE_BEACON:
|
642
|
-
PACKET_TYPE_UNKNOWN:
|
643
|
-
PACKET_TYPE_THIRDPARTY:
|
727
|
+
PACKET_TYPE_BEACON: BeaconPacket,
|
728
|
+
PACKET_TYPE_UNKNOWN: UnknownPacket,
|
729
|
+
PACKET_TYPE_THIRDPARTY: ThirdPartyPacket,
|
730
|
+
PACKET_TYPE_TELEMETRY: TelemetryPacket,
|
644
731
|
}
|
645
732
|
|
646
733
|
|
647
|
-
def get_packet_type(packet: dict):
|
734
|
+
def get_packet_type(packet: dict) -> str:
|
648
735
|
"""Decode the packet type from the packet."""
|
649
736
|
|
650
|
-
pkt_format = packet.get("format"
|
651
|
-
msg_response = packet.get("response"
|
737
|
+
pkt_format = packet.get("format")
|
738
|
+
msg_response = packet.get("response")
|
652
739
|
packet_type = PACKET_TYPE_UNKNOWN
|
653
740
|
if pkt_format == "message" and msg_response == "ack":
|
654
741
|
packet_type = PACKET_TYPE_ACK
|
@@ -662,27 +749,75 @@ def get_packet_type(packet: dict):
|
|
662
749
|
packet_type = PACKET_TYPE_OBJECT
|
663
750
|
elif pkt_format == "status":
|
664
751
|
packet_type = PACKET_TYPE_STATUS
|
752
|
+
elif pkt_format == PACKET_TYPE_BULLETIN:
|
753
|
+
packet_type = PACKET_TYPE_BULLETIN
|
665
754
|
elif pkt_format == PACKET_TYPE_BEACON:
|
666
755
|
packet_type = PACKET_TYPE_BEACON
|
756
|
+
elif pkt_format == PACKET_TYPE_TELEMETRY:
|
757
|
+
packet_type = PACKET_TYPE_TELEMETRY
|
758
|
+
elif pkt_format == PACKET_TYPE_WX:
|
759
|
+
packet_type = PACKET_TYPE_WEATHER
|
667
760
|
elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
|
668
|
-
if packet.get("symbol"
|
669
|
-
packet_type =
|
761
|
+
if packet.get("symbol") == "_":
|
762
|
+
packet_type = PACKET_TYPE_WEATHER
|
670
763
|
elif pkt_format == PACKET_TYPE_THIRDPARTY:
|
671
764
|
packet_type = PACKET_TYPE_THIRDPARTY
|
672
765
|
|
673
766
|
if packet_type == PACKET_TYPE_UNKNOWN:
|
674
767
|
if "latitude" in packet:
|
675
768
|
packet_type = PACKET_TYPE_BEACON
|
769
|
+
else:
|
770
|
+
packet_type = PACKET_TYPE_UNKNOWN
|
676
771
|
return packet_type
|
677
772
|
|
678
773
|
|
679
|
-
def is_message_packet(packet):
|
774
|
+
def is_message_packet(packet: dict) -> bool:
|
680
775
|
return get_packet_type(packet) == PACKET_TYPE_MESSAGE
|
681
776
|
|
682
777
|
|
683
|
-
def is_ack_packet(packet):
|
778
|
+
def is_ack_packet(packet: dict) -> bool:
|
684
779
|
return get_packet_type(packet) == PACKET_TYPE_ACK
|
685
780
|
|
686
781
|
|
687
|
-
def is_mice_packet(packet):
|
782
|
+
def is_mice_packet(packet: dict[Any, Any]) -> bool:
|
688
783
|
return get_packet_type(packet) == PACKET_TYPE_MICE
|
784
|
+
|
785
|
+
|
786
|
+
def factory(raw_packet: dict[Any, Any]) -> type[Packet]:
|
787
|
+
"""Factory method to create a packet from a raw packet string."""
|
788
|
+
raw = raw_packet
|
789
|
+
if "_type" in raw:
|
790
|
+
cls = globals()[raw["_type"]]
|
791
|
+
return cls.from_dict(raw)
|
792
|
+
|
793
|
+
raw["raw_dict"] = raw.copy()
|
794
|
+
raw = _translate_fields(raw)
|
795
|
+
|
796
|
+
packet_type = get_packet_type(raw)
|
797
|
+
|
798
|
+
raw["packet_type"] = packet_type
|
799
|
+
packet_class = TYPE_LOOKUP[packet_type]
|
800
|
+
if packet_type == PACKET_TYPE_WX:
|
801
|
+
# the weather information is in a dict
|
802
|
+
# this brings those values out to the outer dict
|
803
|
+
packet_class = WeatherPacket
|
804
|
+
elif packet_type == PACKET_TYPE_OBJECT and "weather" in raw:
|
805
|
+
packet_class = WeatherPacket
|
806
|
+
elif packet_type == PACKET_TYPE_UNKNOWN:
|
807
|
+
# Try and figure it out here
|
808
|
+
if "latitude" in raw:
|
809
|
+
packet_class = GPSPacket
|
810
|
+
else:
|
811
|
+
# LOG.warning(raw)
|
812
|
+
packet_class = UnknownPacket
|
813
|
+
|
814
|
+
raw.get("addresse", raw.get("to_call"))
|
815
|
+
|
816
|
+
# TODO: Find a global way to enable/disable this
|
817
|
+
# LOGU.opt(colors=True).info(
|
818
|
+
# f"factory(<green>{packet_type: <8}</green>):"
|
819
|
+
# f"(<red>{packet_class.__name__: <13}</red>): "
|
820
|
+
# f"<light-blue>{raw.get('from_call'): <9}</light-blue> -> <cyan>{to: <9}</cyan>")
|
821
|
+
# LOG.info(raw.get('msgNo'))
|
822
|
+
|
823
|
+
return packet_class().from_dict(raw) # type: ignore
|