1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-17 09:07:17 -04:00

Compare commits

...

10 Commits

Author SHA1 Message Date
hemna f228144f4b Updated Changelog for v3.1.1 2023-08-07 13:01:52 -04:00
hemna db9e1d23d1 Fixed pep8 failures 2023-08-07 11:07:01 -04:00
hemna 986df391b2 re-enable USWeatherPlugin to use mapClick
The old MApClick.php api seems to work...re-enabling
2023-07-31 21:53:02 -04:00
hemna 3994235380 Merge pull request #128 from craigerl/fix_kiss
Fix sending packets over KISS interface
2023-07-28 18:08:54 -04:00
hemna 9ebf2f9a30 Fix sending packets over KISS interface
The KISS client sends the path as part of the headers, so we had
to strip out the path from the payload of each message so the path
wouldn't get listed twice.
2023-07-28 17:25:06 -04:00
hemna 011cfc55e1 Use config web_ip for running admin ui from module
When running the web admin interface with
'python -m aprsd.wsgi' the Flask app global now uses
the web_ip config entry for listening.  Also disabled
debug output.
2023-07-26 08:47:22 -04:00
hemna e0c3c5cbbf remove loop log 2023-07-25 20:45:55 -04:00
hemna 26f354b3a9 Max out the client reconnect backoff to 5
This patch adjusts the backoff mechanism for aprs client
reconnect to a max backoff sleep of 5 seconds.   This prevents
an exponential backoff when connection retrying.
2023-07-24 17:03:29 -04:00
hemna 922a6dbb35 Merge pull request #125 from craigerl/update-Dockerfile
Update the Dockerfile
2023-07-24 14:34:06 -04:00
hemna d03c4fc096 Update the Dockerfile
This updates the main Dockerfile to be the same as the
Dockerfile-dev other than using the official pypi package for
aprsd.
2023-07-24 11:36:28 -04:00
11 changed files with 160 additions and 109 deletions
+12
View File
@@ -1,9 +1,21 @@
CHANGES
=======
v3.1.1
------
* Fixed pep8 failures
* re-enable USWeatherPlugin to use mapClick
* Fix sending packets over KISS interface
* Use config web\_ip for running admin ui from module
* remove loop log
* Max out the client reconnect backoff to 5
* Update the Dockerfile
v3.1.0
------
* Changelog updates for v3.1.0
* Use CONF.admin.web\_port for single launch web admin
* Fixed sio namespace registration
* Update Dockerfile-dev to include uwsgi
+7 -1
View File
@@ -49,8 +49,10 @@ class Client:
@property
def client(self):
if not self._client:
LOG.info("Creating APRS client")
self._client = self.setup_connection()
if self.filter:
LOG.info("Creating APRS client filter")
self._client.set_filter(self.filter)
return self._client
@@ -159,7 +161,11 @@ class APRSISClient(Client):
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
connected = False
time.sleep(backoff)
backoff = backoff * 2
# Don't allow the backoff to go to inifinity.
if backoff > 5:
backoff = 5
else:
backoff += 1
continue
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
self._client = aprs_client
+9 -15
View File
@@ -81,27 +81,18 @@ class KISS3Client:
LOG.debug("Start blocking KISS consumer")
self._parse_callback = callback
self.kiss.read(callback=self.parse_frame, min_frames=None)
LOG.debug("END blocking KISS consumer")
LOG.debug(f"END blocking KISS consumer {self.kiss}")
def send(self, packet):
"""Send an APRS Message object."""
# payload = (':%-9s:%s' % (
# msg.tocall,
# payload
# )).encode('US-ASCII'),
# payload = str(msg).encode('US-ASCII')
payload = None
path = ["WIDE1-1", "WIDE2-1"]
if isinstance(packet, core.AckPacket):
msg_payload = f"ack{packet.msgNo}"
elif isinstance(packet, core.Packet):
payload = packet.raw.encode("US-ASCII")
path = ["WIDE2-1"]
if isinstance(packet, core.Packet):
packet.prepare()
payload = packet.payload.encode("US-ASCII")
else:
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
if not payload:
payload = (
":{:<9}:{}".format(
packet.to_call,
@@ -109,9 +100,12 @@ class KISS3Client:
)
).encode("US-ASCII")
LOG.debug(f"Send '{payload}' TO KISS")
LOG.debug(
f"KISS Send '{payload}' TO '{packet.to_call}' From "
f"'{packet.from_call}' with PATH '{path}'",
)
frame = Frame.ui(
destination=packet.to_call,
destination="APZ100",
source=packet.from_call,
path=path,
info=payload,
+6 -2
View File
@@ -265,8 +265,12 @@ def _stats():
time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = stats_obj.stats()
# Webchat doesnt need these
del stats_dict["aprsd"]["watch_list"]
del stats_dict["aprsd"]["seen_list"]
if "watch_list" in stats_dict["aprsd"]:
del stats_dict["aprsd"]["watch_list"]
if "seen_list" in stats_dict["aprsd"]:
del stats_dict["aprsd"]["seen_list"]
if "threads" in stats_dict["aprsd"]:
del stats_dict["aprsd"]["threads"]
# del stats_dict["email"]
# del stats_dict["plugins"]
# del stats_dict["messages"]
+61 -39
View File
@@ -57,6 +57,8 @@ class Packet(metaclass=abc.ABCMeta):
# or holds the raw string from input packet
raw: str = None
raw_dict: dict = field(repr=False, default_factory=lambda: {})
# Built by calling prepare(). raw needs this built first.
payload: str = None
# Fields related to sending packets out
send_count: int = field(repr=False, default=0)
@@ -92,11 +94,24 @@ class Packet(metaclass=abc.ABCMeta):
def prepare(self):
"""Do stuff here that is needed prior to sending over the air."""
# now build the raw message for sending
self._build_payload()
self._build_raw()
def _build_payload(self):
"""The payload is the non headers portion of the packet."""
msg = self._filter_for_send().rstrip("\n")
self.payload = (
f":{self.to_call.ljust(9)}"
f":{msg}"
)
def _build_raw(self):
"""Build the self.raw string which is what is sent over the air."""
self.raw = self._filter_for_send().rstrip("\n")
"""Build the self.raw which is what is sent over the air."""
self.raw = "{}>APZ100:{}".format(
self.from_call,
self.payload,
)
LOG.debug(f"_build_raw: payload '{self.payload}' raw '{self.raw}'")
@staticmethod
def factory(raw_packet):
@@ -233,7 +248,7 @@ class PathPacket(Packet):
path: List[str] = field(default_factory=list)
via: str = None
def _build_raw(self):
def _build_payload(self):
raise NotImplementedError
@@ -245,13 +260,8 @@ class AckPacket(PathPacket):
if self.response:
LOG.warning("Response set!")
def _build_raw(self):
"""Build the self.raw which is what is sent over the air."""
self.raw = "{}>APZ100::{}:ack{}".format(
self.from_call,
self.to_call.ljust(9),
self.msgNo,
)
def _build_payload(self):
self.payload = f":{self.to_call.ljust(9)}:ack{self.msgNo}"
@dataclass
@@ -262,13 +272,8 @@ class RejectPacket(PathPacket):
if self.response:
LOG.warning("Response set!")
def _build_raw(self):
"""Build the self.raw which is what is sent over the air."""
self.raw = "{}>APZ100::{} :rej{}".format(
self.from_call,
self.to_call.ljust(9),
self.msgNo,
)
def _build_payload(self):
self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}"
@dataclass
@@ -285,10 +290,8 @@ class MessagePacket(PathPacket):
# We all miss George Carlin
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
def _build_raw(self):
"""Build the self.raw which is what is sent over the air."""
self.raw = "{}>APZ100::{}:{}{{{}".format(
self.from_call,
def _build_payload(self):
self.payload = ":{}:{}{{{}".format(
self.to_call.ljust(9),
self._filter_for_send().rstrip("\n"),
str(self.msgNo),
@@ -301,7 +304,7 @@ class StatusPacket(PathPacket):
messagecapable: bool = False
comment: str = None
def _build_raw(self):
def _build_payload(self):
raise NotImplementedError
@@ -400,16 +403,24 @@ class GPSPacket(PathPacket):
time_zulu = result_utc_datetime.strftime("%d%H%M")
return time_zulu
def _build_raw(self):
def _build_payload(self):
"""The payload is the non headers portion of the packet."""
time_zulu = self._build_time_zulu()
lat = self.latitude
long = self.longitude
self.payload = (
f"@{time_zulu}z{lat}{self.symbol_table}"
f"{long}{self.symbol}"
)
if self.comment:
self.payload = f"{self.payload}{self.comment}"
def _build_raw(self):
self.raw = (
f"{self.from_call}>{self.to_call},WIDE2-1:"
f"@{time_zulu}z{self.latitude}{self.symbol_table}"
f"{self.longitude}{self.symbol}"
f"{self.payload}"
)
if self.comment:
self.raw = f"{self.raw}{self.comment}"
@dataclass
@@ -422,7 +433,7 @@ class MicEPacket(GPSPacket):
# 0 to 360
course: int = 0
def _build_raw(self):
def _build_payload(self):
raise NotImplementedError
@@ -436,6 +447,19 @@ class ObjectPacket(GPSPacket):
# 0 to 360
course: int = 0
def _build_payload(self):
time_zulu = self._build_time_zulu()
lat = self.convert_latitude(self.latitude)
long = self.convert_longitude(self.longitude)
self.payload = (
f"*{time_zulu}z{lat}{self.symbol_table}"
f"{long}{self.symbol}"
)
if self.comment:
self.payload = f"{self.payload}{self.comment}"
def _build_raw(self):
"""
REPEAT builds packets like
@@ -446,17 +470,11 @@ class ObjectPacket(GPSPacket):
callsign is the station callsign for the object
The frequency, uplink_tone, offset is part of the comment
"""
time_zulu = self._build_time_zulu()
lat = self.convert_latitude(self.latitude)
long = self.convert_longitude(self.longitude)
self.raw = (
f"{self.from_call}>APZ100:;{self.to_call:9s}"
f"*{time_zulu}z{lat}{self.symbol_table}"
f"{long}{self.symbol}"
f"{self.payload}"
)
if self.comment:
self.raw = f"{self.raw}{self.comment}"
@dataclass()
@@ -474,7 +492,7 @@ class WeatherPacket(GPSPacket):
pressure: float = 0.00
comment: str = None
def _build_raw(self):
def _build_payload(self):
"""Build an uncompressed weather packet
Format =
@@ -502,7 +520,6 @@ class WeatherPacket(GPSPacket):
time_zulu = self._build_time_zulu()
contents = [
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:",
f"@{time_zulu}z{self.latitude}{self.symbol_table}",
f"{self.longitude}{self.symbol}",
f"{self.wind_direction:03d}",
@@ -523,11 +540,16 @@ class WeatherPacket(GPSPacket):
# Barometric pressure (in tenths of millibars/tenths of hPascal)
f"b{self.pressure:05.0f}",
]
if self.comment:
contents.append(self.comment)
self.payload = "".join(contents)
self.raw = "".join(contents)
def _build_raw(self):
self.raw = (
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:"
f"{self.payload}"
)
TYPE_LOOKUP = {
+3 -3
View File
@@ -33,9 +33,9 @@ def get_weather_gov_for_gps(lat, lon):
)
try:
url2 = (
# "https://forecast.weather.gov/MapClick.php?lat=%s"
# "&lon=%s&FcstType=json" % (lat, lon)
f"https://api.weather.gov/points/{lat},{lon}"
"https://forecast.weather.gov/MapClick.php?lat=%s"
"&lon=%s&FcstType=json" % (lat, lon)
# f"https://api.weather.gov/points/{lat},{lon}"
)
LOG.debug(f"Fetching weather '{url2}'")
response = requests.get(url2, headers=headers)
+8 -1
View File
@@ -37,11 +37,18 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
def process(self, packet):
LOG.info("Weather Plugin")
fromcall = packet.from_call
message = packet.get("message_text", None)
# message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
else:
searchcall = fromcall
api_key = CONF.aprs_fi.apiKey
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi data {ex}")
return "Failed to fetch aprs.fi location"
+4 -1
View File
@@ -18,7 +18,10 @@ LOG = logging.getLogger("APRSD")
def magic_word_authenticator(sock):
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
if magic != CONF.rpc_settings.magic_word:
raise AuthenticationError(f"wrong magic word {magic}")
raise AuthenticationError(
f"wrong magic word passed in '{magic}'"
f" != '{CONF.rpc_settings.magic_word}'",
)
return sock, None
-1
View File
@@ -246,7 +246,6 @@ class APRSDStats:
},
"plugins": plugin_stats,
}
LOG.debug(f"STATS = {stats}")
LOG.info("APRSD Stats: DONE")
return stats
+6 -1
View File
@@ -330,7 +330,12 @@ if __name__ == "__main__":
setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
app.run(threaded=True, debug=True, port=CONF.admin.web_port)
app.run(
threaded=True,
debug=False,
port=CONF.admin.web_port,
host=CONF.admin.web_ip,
)
if __name__ == "uwsgi_file_aprsd_wsgi":
+44 -45
View File
@@ -1,62 +1,61 @@
#FROM python:3-bullseye as aprsd
FROM ubuntu:22.04 as aprsd
FROM python:3.11-slim as build
# Dockerfile for building a container during aprsd development.
ARG UID
ARG GID
ARG TZ
ARG VERSION=3.0.3
ARG BUILDX_QEMU_ENV
ENV APRS_USER=aprs
ENV HOME=/home/aprs
ARG VERSION=3.1.0
ENV TZ=${TZ:-US/Eastern}
ENV UID=${UID:-1000}
ENV GID=${GID:-1000}
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
ENV APRSD_PIP_VERSION=${VERSION}
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update
RUN apt install -y git build-essential
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev
RUN apt install -y python3 python3-pip python3-dev python3-lxml python3-setuptools-rust
RUN apt install -y libffi-dev cargo pkg-config
RUN pip3 install -U pip
RUN pip3 install -U setuptools_rust
ENV PIP_DEFAULT_TIMEOUT=100 \
# Allow statements and log messages to immediately appear
PYTHONUNBUFFERED=1 \
# disable a pip version check to reduce run-time & log-spam
PIP_DISABLE_PIP_VERSION_CHECK=1 \
# cache is useless in docker image, so disable to reduce image size
PIP_NO_CACHE_DIR=1
RUN addgroup --gid $GID $APRS_USER
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
RUN set -ex \
# Create a non-root user
&& addgroup --system --gid 1001 appgroup \
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
# Upgrade the package index and install security upgrades
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y git build-essential curl vim libffi-dev \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
# Install dependencies
# Clean up
&& apt-get autoremove -y \
&& apt-get clean -y
# Handle an extremely specific issue when building the cryptography package for
# 32-bit architectures within QEMU running on a 64-bit host (issue #30).
RUN if [ "${BUILDX_QEMU_ENV}" = "true" -a "$(getconf LONG_BIT)" = "32" ]; then \
pip3 install -U cryptography==3.3.2; \
else \
pip3 install cryptography ;\
fi
# Ensure /config is there with a default config file
USER root
# Install aprsd
### Final stage
FROM build as final
WORKDIR /app
RUN pip3 install aprsd==$APRSD_PIP_VERSION
RUN mkdir -p /config
RUN pip install gevent uwsgi
RUN which aprsd
RUN mkdir /config
RUN chown -R appuser:appgroup /app
RUN chown -R appuser:appgroup /config
USER appuser
RUN which aprsd
RUN aprsd sample-config > /config/aprsd.conf
RUN chown -R $APRS_USER:$APRS_USER /config
RUN chown -R $APRS_USER:$APRS_USER $HOME
# override this to run another configuration
ENV CONF default
VOLUME ["/config", "/plugins"]
ADD bin/run.sh /app
ADD bin/listen.sh /app
ADD bin/admin.sh /app
USER $APRS_USER
ADD bin/run.sh /usr/local/bin
ADD bin/listen.sh /usr/local/bin
ENTRYPOINT ["/usr/local/bin/run.sh"]
# For the web admin interface
EXPOSE 8001
ENTRYPOINT ["/app/run.sh"]
VOLUME ["/config"]
# Set the user to run the application
USER appuser
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
CMD aprsd healthcheck --config /config/aprsd.conf