1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-16 00:23:34 -04:00

Compare commits

...

135 Commits

Author SHA1 Message Date
hemna e9febbbc52 Fixed entry_points 2024-04-21 12:33:03 -04:00
hemna fa9ff7d77f Fix for entry_points where python < 3.10
python 3.10 has importlib_metadata.entry_points(group) parameter
it seems that less than 3.10 doesn't.  this patch test for
python version to see how to get the entry point groups.
2024-04-21 12:30:29 -04:00
hemna 8e0de9c5ac Fix for sample-config warning
This patch fixes a small issue with the sample-config command
outputting a warning during generation.
2024-03-27 10:28:42 -04:00
hemna 71cd7e0ab5 Changelog for 3.3.2 2024-03-13 13:49:11 -04:00
hemna d485f484ec Remove warning during sample-config
This patch removes a warning log during sample-config
generation
2024-03-13 13:47:01 -04:00
hemna f810c02d5d Removed print in utils
this patch removes a leftover debug print in utils.load_entry_points
that was causing sample-config output to be bogus.
2024-03-13 13:44:09 -04:00
hemna 50e24abb81 Updates for 3.3.1 2024-03-12 10:41:16 -04:00
hemna 10d023dd7b Fixed failure with fetch-stats
This patch fails nicely with the fetch-stats if it can't connect
with the rpc server on the other end.
2024-03-12 10:37:17 -04:00
hemna cb9456b29d Fixed problem with list-plugins
This patch includes a fix to the list-plugins and
list-extensions commands.
2024-03-12 10:36:26 -04:00
hemna c37e1d58bb Changelog for 3.3.0 2024-03-12 10:08:38 -04:00
hemna 0ca5ceee7e sample-config fix
This patch makes a change on how it's calling importlib.entry_points
to only fetch the group we want, which is 'oslo.config.opts'.
This fixes a problem with python 3.12 compatibility.
2024-03-11 11:53:28 -04:00
hemna 2e9c9d40e1 Fixed registry url post 2024-03-08 11:49:10 -05:00
hemna 66004f639f Changed processpkt message
this includes the pkt.key in the log entry
2024-03-08 11:25:46 -05:00
hemna 0b0afd39ed Fixed RegistryThread not sending requests 2024-03-08 09:18:28 -05:00
hemna aec88d4a7e use log.setup_logging 2024-03-07 12:43:10 -05:00
hemna 24bbea1d49 Disable debug logs for aprslib
This patch adds a disable of propogating the debug logs
from the aprslib parsing.  We don't really need to see
this in our aprsd services.
2024-03-07 09:46:36 -05:00
hemna 5d3f42f411 Make registry thread sleep
This patch adds a required sleep of 1 second in each
registry thread loop to prevent runaway cpu usage
2024-03-07 08:37:09 -05:00
hemna 44a98850c9 Merge pull request #147 from craigerl/loguru
Replace slow rich logging with loguru
2024-03-06 14:18:19 -05:00
hemna 2cb9c2a31c Put threads first after date/time 2024-03-06 13:39:51 -05:00
hemna 2fefa9fcd6 Replace slow rich logging with loguru
This patch removes the rich logging with
the modern loguru logging
2024-03-06 13:00:52 -05:00
hemna d092a43ec9 Updated requirements 2024-03-06 12:59:21 -05:00
hemna d1a09fc6b5 Fixed pep8 2024-02-28 16:24:01 -05:00
hemna ff051bc285 Added list-extensions and updated README.rst
This patch adds the list-extensions command to support
showing the available extensions for APRSD that live on
pypi.
2024-02-28 16:10:55 -05:00
hemna 5fd91a2172 Change defaults for beacon and registry
The beacon frequency is now every 30 minutes by default.
The registry call is now every hour.
2024-02-28 13:23:11 -05:00
hemna a4630c15be Add log info for Beacon and Registry threads 2024-02-27 16:01:15 -05:00
hemna 6a7d7ad79b fixed frequency_seconds to IntOpt 2024-02-27 15:53:03 -05:00
hemna 7a5b55fa77 fixed references to conf 2024-02-27 15:48:58 -05:00
hemna a1e21e795d changed the default packet timeout to 5 minutes 2024-02-27 15:11:39 -05:00
hemna cb291de047 Fixed default service registry url 2024-02-27 15:10:21 -05:00
hemna e9c48c1914 fix pep8 failures 2024-02-27 14:21:04 -05:00
hemna f0ad6d7577 py311 fails in github 2024-02-27 13:46:28 -05:00
hemna 38fe408c82 Don't send uptime to registry 2024-02-27 13:40:39 -05:00
hemna 8264c94bd6 Added sending software string to registry
This patch adds sending the APRSD signature and url
along with the regsitry request.
2024-02-27 11:05:41 -05:00
hemna 1ad2e135dc add py310 gh actions 2024-02-27 08:14:05 -05:00
hemna 1e4f0ca65a Added the new APRS Registry thread
This patch adds the new APRSRegistryThread,
which enabled in config, will send a small
packet of information to the as yet deployed
APRS service registry every 900 seconds.

The data that this thread will send is
the service callsign, a description of the service,
a website url for the service.

The idea being that the registry website that this thread
sends information to, will show all the services that are
running on the ARPS network, so Ham operators can discover
them and try them out.
2024-02-26 18:28:52 -05:00
hemna 41185416cb Added installing extensions to Docker run
This patch adds the installation of APRSD via pip during startup
time for the main server run.sh, admin.sh and listen.sh
2024-02-25 15:05:45 -05:00
hemna 68f23d8ca7 Cleanup some logs
This patch removes some debug logging from the clients.
2024-02-25 15:04:26 -05:00
hemna 11f1e9533e Added BeaconPacket
This patch adds the BeaconPacket and BeaconSendThread.
This will enable APRSD server to send a beacon if enabled in
the config.
2024-02-25 14:21:17 -05:00
hemna 275bf67b9e updated requirements files 2024-02-24 14:30:08 -05:00
hemna 968345944a removed some unneeded code
removed the callsigns locations iterator
2024-02-24 14:28:37 -05:00
hemna df2798eafb Added iterator to objectstore
Since the objectstore mixin uses a iterable to store it's data,
it was easy to add an __iter__ to the objectstore class itself.
2024-02-24 14:27:39 -05:00
hemna e89f8a805b Added some missing classes to threads
Added new APRSDupeThread
2024-02-24 14:26:55 -05:00
hemna b14307270c Added support for loading extensions
This patch adds support for loading extenions
to APRSD!!

You can create another separate aprsd project, and register
your extension in your setup.cfg as a new entry point for aprsd
like

[entry_points]
aprsd.extension =
    cool = my_project.extension

in your my_project/extension.py file
import your commmands and away you go.
2024-02-23 16:53:42 -05:00
hemna ebee8e1439 Merge pull request #146 from craigerl/webchat-location
Added location for callsign tabs in webchat
2024-02-20 10:30:21 -05:00
hemna a7e30b0bed Added location for callsign tabs in webchat
This patch adds the new feature of trying to fetch the location
distance and bearing for each callsign in the webchat tabs.
This is handy when out on the go, you can get a general idea
where the other callsign is when chatting with them.

First aprsd webchat tries to fetch the location with aprs.fi
REST api call.  This assumes internet access.  If this fails,
then webchat will send a special message to REPEAT to ask it for
the location information for the callsign.   This will send over
the air.
2024-02-20 10:18:22 -05:00
hemna 1a5c5f0dce updated gitignore 2024-02-15 14:42:04 -05:00
hemna a00c4ea840 Create codeql.yml
Try out code scanning
2024-02-07 09:43:29 -05:00
hemna a88de2f09c update github action branchs to v8 2024-02-07 08:55:24 -05:00
hemna d6f0f05315 Added Location info on webchat interface
This patch adds a new popover in the webchat tab to show
the location information for a callsign.

webchat will try to hit aprs.fi to fetch the location from the
callsign's last beacon.  If there is no internet, this will fail
and webchat will send a request to REPEAT callsign for the location
information.
2024-02-06 16:52:56 -05:00
hemna 03c58f83cd Updated dev test-plugin command
This patch updates the output of the aprsd dev test-plugin command
to show the packets that would actually get sent by the plugin
results.
2024-01-19 11:30:15 -05:00
hemna a4230d324a Update requirements.txt 2024-01-16 16:45:07 -05:00
hemna 8bceb827ec Update for v3.2.3 2024-01-09 09:12:01 -05:00
hemna 12a3113192 Force fortune path during setup test
For whatever reason shutil.which() can't find
fortune in the path, unless you specify the entire path.
2024-01-09 01:30:43 +00:00
hemna 026a64c003 added /usr/games to path 2024-01-08 22:56:45 +00:00
hemna 682e138ec2 Added fortune to Dockerfile-dev 2024-01-08 19:40:08 +00:00
hemna e4e9c6e98b Merge pull request #144 from v-rzh/vrzh-fix-typo-0
aprsd: main.py: Fix premature return in sample_config
2024-01-08 13:01:34 -05:00
hemna f02824b796 Added missing fortune app 2024-01-08 18:00:26 +00:00
Martiros Shakhzadyan 530ac30a09 aprsd: main.py: Fix premature return in sample_config
Fix a typo in sample_config that causes the function to return before
config is generated.
2024-01-04 08:41:06 -05:00
Craig Lamparter 9350cf6534 Update weather.py because you can't sort icons by penis 2023-12-21 11:07:43 -08:00
Craig Lamparter 651cf014b7 Update weather.py both weather plugins have new Ww regex 2023-12-21 11:01:23 -08:00
Craig Lamparter b6df9de8aa Update weather.py
get back the "starts with w" is the weather command regex
2023-12-21 10:54:07 -08:00
hemna 0fd7daaae0 Merge pull request #140 from craigerl/location_plugin
Rework Location Plugin
2023-11-24 19:48:22 -05:00
hemna 0433768784 Fixed a bug with OWMWeatherPlugin
The weather plugin wasn't able to find the from callsign,
so all of the weather reports were random and wrong.
2023-11-24 19:15:52 -05:00
hemna a8f73610fe Rework Location Plugin
This Patch updates the location plugin to allow configuring which
geopy library's supported geocoders.  This patch also adds a fake
geopy geocoder class that uses the us government's API for location.
2023-11-22 20:55:38 -05:00
hemna c0e2ef1199 Update for v3.2.2 release 2023-11-22 12:35:12 -05:00
hemna 809a41f123 Fix for types 2023-11-17 14:23:29 -05:00
hemna b0bfdaa1fb Fix wsgi for prod 2023-11-17 14:02:29 -05:00
hemna b73373db3f Merge pull request #139 from craigerl/walt-test
Walt test
2023-11-17 13:47:05 -05:00
hemna 6b397cbdf1 pep8 fixes 2023-11-17 13:34:10 -05:00
hemna 638128adf8 remove python 3.12 from github builds 2023-11-17 13:15:44 -05:00
hemna b9dd21bc14 Fixed datetime access in core.py 2023-11-17 13:01:55 -05:00
hemna fae7032346 removed invalid reference to config.py 2023-11-17 11:59:50 -05:00
hemna 4b1214de74 Updated requirements 2023-11-17 11:44:12 -05:00
hemna 763c9ab897 Reworked the admin graphs
This patch fixes some bugs wth the rpc for packets as well
as reworks the admin graphs to use echarts.
2023-11-17 11:39:42 -05:00
hemna fe1ebf2ec1 Test new packet serialization 2023-11-17 11:39:42 -05:00
hemna c01037d398 Merge pull request #138 from craigerl/no-internets
Try to localize js libs and css for no internet
2023-10-31 08:04:42 -04:00
hemna 072a1f4430 Merge pull request #137 from jhmartin/mismatched-arguments
Normalize listen --aprs-login
2023-10-28 19:39:09 -04:00
hemna 8b2613ec47 Try to localize js libs and css for no internet
this patch fixes some issues with webchat not loading css and js
when there is no internet.  The index.html was relying on internet
being available to fetch remote css and js.
2023-10-28 19:26:50 -04:00
Jason Martin d39ce76475 Normalize listen --aprs-login
The click block specifies aprs-login but the error indicated aprs_login
2023-10-27 23:39:56 +00:00
hemna 3e9c3612ba Merge pull request #136 from craigerl/dependabot/pip/werkzeug-3.0.1
Bump werkzeug from 2.3.7 to 3.0.1
2023-10-26 09:22:03 -04:00
hemna 8746a9477c Merge pull request #135 from jhmartin/update-installdoc
Update INSTALL with new conf files
2023-10-26 09:20:20 -04:00
dependabot[bot] 7d0524cee5 Bump werkzeug from 2.3.7 to 3.0.1
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 2.3.7 to 3.0.1.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/2.3.7...3.0.1)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-25 19:55:49 +00:00
Jason Martin 5828643f2e Update INSTALL with new conf files
The name of the config files has changed, update INSTALL with the new names.
2023-10-23 00:14:59 +00:00
hemna 313ea5b6a5 Merge pull request #134 from craigerl/dependabot/pip/urllib3-2.0.7
Bump urllib3 from 2.0.6 to 2.0.7
2023-10-17 17:28:04 -04:00
dependabot[bot] 7853e19c79 Bump urllib3 from 2.0.6 to 2.0.7
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.6...2.0.7)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-17 21:09:50 +00:00
hemna acf2b62bce Changelog for 3.2.1 2023-10-09 11:39:53 -04:00
Craig Lamparter 8e9a0213e9 Update index.html disable form autocomplete 2023-10-07 10:06:42 -07:00
hemna bf905a0e9f Update the packet_dupe_timeout warning
The warning text was hardcoded at the old 60 second value,
instead of using the config option.
2023-10-06 16:06:41 -04:00
hemna 5ae45ce42f Update the webchat paths
This reorders the paths available for selection in webchat and
sets the selected to default
2023-10-06 16:02:00 -04:00
hemna 0155923341 Changed the path option to a ListOpt
Both serial_kiss and tcp_kiss path option is converted to a ListOpt
to help generate a single line during sample-config generation.
2023-10-06 15:44:25 -04:00
hemna 156d9d9592 Fixed default path for tcp_kiss client.
The tcp_kiss client initialization was using the serial_kiss client's
path setting.
2023-10-06 15:41:12 -04:00
hemna 81169600bd Set a default password for admin
This patch sets a default password of "password" for the admin webui.
2023-10-06 15:32:31 -04:00
hemna 746eeb81b0 Fix path for KISS clients
The kiss client send method was always forcing the config
path.  If a packet has a path specified in it, that will
override the config setting for the kiss client setting in the config.
2023-10-05 18:00:45 -04:00
hemna f41488b48a Added packet_dupe_timeout conf
This patch adds the new packet_dump_timeout config option, defaulting to
60 seconds.   If the same packet matching the from, to, msgNo is RX'd
within that timeout the packet is considered a dupe and will be
dropped.  Ack packets are not subject to dupe checking.
2023-10-05 13:56:02 -04:00
hemna 116f201394 Merge pull request #133 from craigerl/dependabot/pip/urllib3-2.0.6
Bump urllib3 from 2.0.4 to 2.0.6
2023-10-05 10:42:41 -04:00
hemna ddd4d25e9d Add ability to change path on every TX packet
This patch adds the ability to webchat to set the path
on every outbound packet for the KISS clients as well as
the fake client.  The path dropdown includes the options for
Default path (which will default to the config setting)
WIDE1-1,WIDE2-1
ARISS
2023-10-05 10:33:07 -04:00
hemna e2f89a6043 Merge pull request #132 from craigerl/RF_dupe_fix
Fix for dupe packets.
2023-10-03 16:18:34 -04:00
hemna 544600a96b Make Packet objects hashable
This patch makes the packet key a property of the Packet object and
makes packet objects comparable and hashable.
2023-10-03 16:01:43 -04:00
dependabot[bot] c16f3a0bb2 Bump urllib3 from 2.0.4 to 2.0.6
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.4 to 2.0.6.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.4...2.0.6)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-03 00:04:40 +00:00
hemna 59cec1317d Don't process AckPackets as dupes
If we RX an AckPacket, then send it on for processing.  There is no need
to check for a dupe.
2023-10-02 08:42:00 -04:00
hemna 751bbc2514 Fixed another msgNo int issue 2023-09-29 15:40:42 -04:00
hemna 9bdfd166fd Fixed issue with packet tracker and msgNO Counter
The packet msgNo field is a string, but is typically is an integer
counter to keep track of a specific packet id.  The counter was
returning an int, but the packet.msgNo is a string.  So, when trying to
delete a packet from the packet tracker, the key for accessing the
packet is the msgNo, which has to be a string.  Passing an int, will
cause the packet tracker to not find the packet, and hence silently
fail.

This patch forces the msgNo counter to be a string.
2023-09-29 10:04:15 -04:00
hemna f79b88ec1b Fixed import of Mutablemapping
python 3.10 moved it to collections.abc
2023-09-28 15:30:54 -04:00
hemna 99a0f877f4 pep8 fixes 2023-09-28 12:34:01 -04:00
hemna 4f87d5da12 rewrote packet_list and drop dupe packets
This patch rewrites the packet_list internally to be a dictionary
instead of a list for very fast lookups.  This was needed to test for
duplicate packets already in the list.

This patch drops packets that have the same data and are < 60 seconds
in age from the last time we got the packet.   On RF based clients
we can get dupes!!
2023-09-28 12:19:18 -04:00
hemna 0d7e50d2ba Log a warning on dupe
This patch logs a warning if we detect a dupe packet inbound.
2023-09-27 15:45:39 -04:00
hemna 1f6c55d2bf Fix for dupe packets.
Sometimes over KISS clients (RF), we can get duplicate packets
due to having many digipeters in range of the TNC that aprsd is
connected to.   We will now filter out any dupe packets that aprsd
is still in the process of doing it's 3 acks.
2023-09-27 14:55:47 -04:00
hemna 740889426a Update Changelog for 3.2.0 2023-09-26 16:15:19 -04:00
hemna c9dc4f67d4 minor cleanup prior to release 2023-09-26 15:27:51 -04:00
hemna 788a72c643 Webchat: fix input maxlength
This changes the maxlength of the input message box to 67 characters.
Also changes the GPS beacon text.
2023-09-26 12:53:08 -04:00
hemna 1e3d0d4faf Merge pull request #131 from craigerl/dependabot/pip/gevent-23.9.1
Bump gevent from 23.9.0.post1 to 23.9.1
2023-09-26 12:08:34 -04:00
hemna 82d25915fc WebChat: cleanup some console.logs 2023-09-26 12:07:28 -04:00
hemna 12dfdefb62 WebChat: flash a dupe message 2023-09-26 12:00:02 -04:00
hemna d63c6854af Webchat: Fix issue accessing msg.id
After the refactor of the messages object in webchat, we are sending
a direct json dict version of the packet now.  This means there is no
msg.id in the dict, but msg.msgNo instead.  This should help fix
the display of dupes.
2023-09-26 11:04:59 -04:00
hemna 6b083d4c4d Webchat: Fix chat css on older browsers
Some older browswers can't handle the new css syntax
for a subclass in the same css definition.
2023-09-26 10:47:53 -04:00
hemna ff358987a9 WebChat: new tab should get focus
When a new tab is created it now gets the focus.
2023-09-26 10:31:00 -04:00
dependabot[bot] 412ab54303 Bump gevent from 23.9.0.post1 to 23.9.1
Bumps [gevent](https://github.com/gevent/gevent) from 23.9.0.post1 to 23.9.1.
- [Release notes](https://github.com/gevent/gevent/releases)
- [Changelog](https://github.com/gevent/gevent/blob/master/docs/changelog_pre.rst)
- [Commits](https://github.com/gevent/gevent/compare/23.9.0.post1...23.9.1)

---
updated-dependencies:
- dependency-name: gevent
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-25 19:28:21 +00:00
hemna 3f5dbe0a12 Webchat: Fix pep8 errors 2023-09-21 18:34:03 -04:00
hemna 9635893934 Webchat: Added tab notifications and raw packet
This patch adds an auto mouseover hover popover for displaying
the raw APRS packet.

This patch also adds the notification counter for an unselected tab.
2023-09-21 16:29:15 -04:00
hemna f151ae4348 WebChat: Prevent sending message without callsign
This patch adds raising an error if the user doesn't set the
to call callsign when sending a message.
2023-09-15 14:32:22 -04:00
hemna 7130ca2fd9 WebChat: fixed content area scrolling
This patch fixes some issues when switching between tabs.
2023-09-15 14:12:55 -04:00
hemna b393060edb Webchat: tweaks to UI for expanding chat
This patch changes the layout containers a bit.  Moved the tabs to the
header section and made the tab contents fill the rest of the height of
the browser and it is the only portion that scrolls.
2023-09-15 11:34:38 -04:00
hemna f770c5ffd5 Webchat: Fixed bug deleteing first tab
This patch fixes a UI issue when the user delets the first tab and the
remaining tabs aren't refreshed/shown.
2023-09-15 09:13:51 -04:00
hemna ef206b1283 Ensure Keepalive doesn't reset client at startup
This patch ensures that the keepalive thread doesn't try and
reset/restart the aprs connection at startup.
2023-09-14 16:46:00 -04:00
hemna 140fa4ace4 Ensure parse_delta_str doesn't puke
This patch fixes an issue where the parse_delta_str regex doesn't
match anything.
2023-09-14 16:23:49 -04:00
hemna 81a19dd101 WebChat: Send GPS Beacon working
This patch adds back in the jquery toast plugin that used to come as
part of the fomantic ui js code.
2023-09-13 12:05:40 -04:00
hemna 9985c8bf25 Merge pull request #130 from craigerl/webchat-saved-bootstrapjs
Webchat saved bootstrapjs
2023-09-12 21:17:49 -04:00
hemna 1400e3e711 webchat: got active tab onclick working
This patch adds the ability to click on the already existing active tab
and have it populate the to_call input box.
2023-09-12 16:37:06 -04:00
hemna 8a90d5480a webchat: set to_call to value of tab when selected
This patch will set the to_call form field to the callsign of the
tab when the tab is activated in the UI.

NOTE: still need to populate it when clicking on the already active
tab.
2023-09-12 15:44:34 -04:00
hemna b4e02c760e Center the webchat input form
This patch centers the input form for the webchat page over the
center of the page.
2023-09-10 12:13:05 -04:00
hemna ba6b410795 Update index.html to use chat.css 2023-09-10 11:12:38 -04:00
hemna 70ddc44b5c Deleted webchat mobile pages
removed user-agents package dependency
2023-09-08 15:45:32 -04:00
hemna 852760220f Added close X on webchat tabs
This patch adds an X on each tab as a way to close the conversation
and nuke the local storage for the conversation.
2023-09-08 12:43:33 -04:00
hemna 14e984c9b4 Reworked webchat with new UI
This patch reworks the webchat UI to work in both desktop
and mobile layouts.  Comprimises were made, but there is 1
codebase now between both desktop and mobile.
This patch also includes the new imessage/sms chat look.
2023-09-08 11:19:24 -04:00
hemna 29f21a9469 Updated the webchat UI to look like iMessage 2023-09-06 11:20:59 -04:00
75 changed files with 3387 additions and 1397 deletions
+84
View File
@@ -0,0 +1,84 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '36 8 * * 0'
jobs:
analyze:
name: Analyze
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners
# Consider using larger runners for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
language: [ 'javascript-typescript', 'python' ]
# CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
# Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v7
uses: tj-actions/branch-names@v8
- name: Extract Branch
id: extract_branch
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v6
uses: tj-actions/branch-names@v8
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Setup Docker Buildx
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v6
uses: tj-actions/branch-names@v8
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Setup Docker Buildx
+2
View File
@@ -58,3 +58,5 @@ AUTHORS
.idea
Makefile.venv
# Copilot
.DS_Store
+3 -3
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -12,11 +12,11 @@ repos:
- id: check-builtin-literals
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.16.0
rev: v2.5.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/dizballanze/gray
rev: v0.10.1
rev: v0.14.0
hooks:
- id: gray
+155
View File
@@ -1,6 +1,161 @@
CHANGES
=======
v3.3.3
------
* Fix for sample-config warning
v3.3.2
------
* Changelog for 3.3.2
* Remove warning during sample-config
* Removed print in utils
v3.3.1
------
* Updates for 3.3.1
* Fixed failure with fetch-stats
* Fixed problem with list-plugins
v3.3.0
------
* Changelog for 3.3.0
* sample-config fix
* Fixed registry url post
* Changed processpkt message
* Fixed RegistryThread not sending requests
* use log.setup\_logging
* Disable debug logs for aprslib
* Make registry thread sleep
* Put threads first after date/time
* Replace slow rich logging with loguru
* Updated requirements
* Fixed pep8
* Added list-extensions and updated README.rst
* Change defaults for beacon and registry
* Add log info for Beacon and Registry threads
* fixed frequency\_seconds to IntOpt
* fixed references to conf
* changed the default packet timeout to 5 minutes
* Fixed default service registry url
* fix pep8 failures
* py311 fails in github
* Don't send uptime to registry
* Added sending software string to registry
* add py310 gh actions
* Added the new APRS Registry thread
* Added installing extensions to Docker run
* Cleanup some logs
* Added BeaconPacket
* updated requirements files
* removed some unneeded code
* Added iterator to objectstore
* Added some missing classes to threads
* Added support for loading extensions
* Added location for callsign tabs in webchat
* updated gitignore
* Create codeql.yml
* update github action branchs to v8
* Added Location info on webchat interface
* Updated dev test-plugin command
* Update requirements.txt
* Update for v3.2.3
v3.2.3
------
* Force fortune path during setup test
* added /usr/games to path
* Added fortune to Dockerfile-dev
* Added missing fortune app
* aprsd: main.py: Fix premature return in sample\_config
* Update weather.py because you can't sort icons by penis
* Update weather.py both weather plugins have new Ww regex
* Update weather.py
* Fixed a bug with OWMWeatherPlugin
* Rework Location Plugin
v3.2.2
------
* Update for v3.2.2 release
* Fix for types
* Fix wsgi for prod
* pep8 fixes
* remove python 3.12 from github builds
* Fixed datetime access in core.py
* removed invalid reference to config.py
* Updated requirements
* Reworked the admin graphs
* Test new packet serialization
* Try to localize js libs and css for no internet
* Normalize listen --aprs-login
* Bump werkzeug from 2.3.7 to 3.0.1
* Update INSTALL with new conf files
* Bump urllib3 from 2.0.6 to 2.0.7
v3.2.1
------
* Changelog for 3.2.1
* Update index.html disable form autocomplete
* Update the packet\_dupe\_timeout warning
* Update the webchat paths
* Changed the path option to a ListOpt
* Fixed default path for tcp\_kiss client
* Set a default password for admin
* Fix path for KISS clients
* Added packet\_dupe\_timeout conf
* Add ability to change path on every TX packet
* Make Packet objects hashable
* Bump urllib3 from 2.0.4 to 2.0.6
* Don't process AckPackets as dupes
* Fixed another msgNo int issue
* Fixed issue with packet tracker and msgNO Counter
* Fixed import of Mutablemapping
* pep8 fixes
* rewrote packet\_list and drop dupe packets
* Log a warning on dupe
* Fix for dupe packets
v3.2.0
------
* Update Changelog for 3.2.0
* minor cleanup prior to release
* Webchat: fix input maxlength
* WebChat: cleanup some console.logs
* WebChat: flash a dupe message
* Webchat: Fix issue accessing msg.id
* Webchat: Fix chat css on older browsers
* WebChat: new tab should get focus
* Bump gevent from 23.9.0.post1 to 23.9.1
* Webchat: Fix pep8 errors
* Webchat: Added tab notifications and raw packet
* WebChat: Prevent sending message without callsign
* WebChat: fixed content area scrolling
* Webchat: tweaks to UI for expanding chat
* Webchat: Fixed bug deleteing first tab
* Ensure Keepalive doesn't reset client at startup
* Ensure parse\_delta\_str doesn't puke
* WebChat: Send GPS Beacon working
* webchat: got active tab onclick working
* webchat: set to\_call to value of tab when selected
* Center the webchat input form
* Update index.html to use chat.css
* Deleted webchat mobile pages
* Added close X on webchat tabs
* Reworked webchat with new UI
* Updated the webchat UI to look like iMessage
* Restore previous conversations in webchat
* Remove VIM from Dockerfile
* recreate client during reset()
* updated github workflows
* Updated documentation build
* Removed admin\_web.py
* Removed some RPC server log noise
* Fixed admin page packet date
+3 -2
View File
@@ -27,9 +27,10 @@ pip install -e .
# CONFIGURE
# Now configure aprsd HERE
./aprsd sample-config # generates a config.yml template
mkdir -p ~/.config/aprsd
./aprsd sample-config > ~/.config/aprsd/aprsd.conf # generates a config template
vi ~/.config/aprsd/config.yml # copy/edit config here
vi ~/.config/aprsd/aprsd.conf # copy/edit config here
aprsd server
+2 -2
View File
@@ -84,5 +84,5 @@ update-requirements: dev ## Update the requirements.txt and dev-requirements.tx
rm dev-requirements.txt
touch requirements.txt
touch dev-requirements.txt
$(VENV)/pip-compile --resolver backtracking --annotation-style line requirements.in
$(VENV)/pip-compile --resolver backtracking --annotation-style line dev-requirements.in
$(VENV)/pip-compile --resolver backtracking --annotation-style=line requirements.in
$(VENV)/pip-compile --resolver backtracking --annotation-style=line dev-requirements.in
+70 -63
View File
@@ -10,32 +10,38 @@ ____________________
`APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python.
APRSD listens on amateur radio aprs-is network for messages and respond to them.
It has a plugin architecture for extensibility. Users of APRSD can write their own
plugins that can respond to APRS-IS messages.
You must have an amateur radio callsign to use this software. APRSD gets
messages for the configured HAM callsign, and sends those messages to a
list of plugins for processing. There are a set of core plugins that
provide responding to messages to check email, get location, ping,
time of day, get weather, and fortune telling as well as version information
of aprsd itself.
What is APRSD
=============
APRSD is a python application for interacting with the APRS network and providing
APRS services for HAM radio operators.
APRSD currently has 4 main commands to use.
* server - Connect to APRS and listen/respond to APRS messages
* webchat - web based chat program over APRS
* send-message - Send a message to a callsign via APRS_IS.
* listen - Listen to packets on the APRS-IS Network based on FILTER.
Each of those commands can connect to the APRS-IS network if internet connectivity
is available. If internet is not available, then APRS can be configured to talk
to a TCP KISS TNC for radio connectivity.
Please `read the docs`_ to learn more!
.. contents:: :local:
APRSD Overview Diagram
======================
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
Typical use case
================
APRSD's typical use case is that of providing an APRS wide service to all HAM
radio operators. For example the callsign 'REPEAT' on the APRS network is actually
an instance of APRSD that can provide a list of HAM repeaters in the area of the
callsign that sent the message.
Ham radio operator using an APRS enabled HAM radio sends a message to check
the weather. An APRS message is sent, and then picked up by APRSD. The
APRS packet is decoded, and the message is sent through the list of plugins
@@ -46,55 +52,6 @@ callsigns to look out for. The watch list can notify you when a HAM callsign
in the list is seen and now available to message on the APRS network.
Current list of built-in plugins
======================================
::
└─> aprsd list-plugins
🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
Pypi.org APRSD Installable Plugin Packages
Install any of the following plugins with 'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
Installation
=============
@@ -187,6 +144,56 @@ look for incomming commands to the callsign configured in the config file
12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60
Current list of built-in plugins
======================================
::
└─> aprsd list-plugins
🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
Pypi.org APRSD Installable Plugin Packages
Install any of the following plugins with 'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
send-message
+35 -1
View File
@@ -50,6 +50,40 @@ common_options = [
]
class AliasedGroup(click.Group):
def command(self, *args, **kwargs):
"""A shortcut decorator for declaring and attaching a command to
the group. This takes the same arguments as :func:`command` but
immediately registers the created command with this instance by
calling into :meth:`add_command`.
Copied from `click` and extended for `aliases`.
"""
def decorator(f):
aliases = kwargs.pop("aliases", [])
cmd = click.decorators.command(*args, **kwargs)(f)
self.add_command(cmd)
for alias in aliases:
self.add_command(cmd, name=alias)
return cmd
return decorator
def group(self, *args, **kwargs):
"""A shortcut decorator for declaring and attaching a group to
the group. This takes the same arguments as :func:`group` but
immediately registers the created command with this instance by
calling into :meth:`add_command`.
Copied from `click` and extended for `aliases`.
"""
def decorator(f):
aliases = kwargs.pop("aliases", [])
cmd = click.decorators.group(*args, **kwargs)(f)
self.add_command(cmd)
for alias in aliases:
self.add_command(cmd, name=alias)
return cmd
return decorator
def add_options(options):
def _add_options(func):
for option in reversed(options):
@@ -104,7 +138,7 @@ def process_standard_options_no_config(f: F) -> F:
ctx.obj["loglevel"] = kwargs["loglevel"]
ctx.obj["config_file"] = kwargs["config_file"]
ctx.obj["quiet"] = kwargs["quiet"]
log.setup_logging_no_config(
log.setup_logging(
ctx.obj["loglevel"],
ctx.obj["quiet"],
)
+38 -4
View File
@@ -7,7 +7,7 @@ from aprslib.exceptions import LoginError
from oslo_config import cfg
from aprsd import exception
from aprsd.clients import aprsis, kiss
from aprsd.clients import aprsis, fake, kiss
from aprsd.packets import core, packet_list
from aprsd.utils import trace
@@ -17,6 +17,7 @@ LOG = logging.getLogger("APRSD")
TRANSPORT_APRSIS = "aprsis"
TRANSPORT_TCPKISS = "tcpkiss"
TRANSPORT_SERIALKISS = "serialkiss"
TRANSPORT_FAKE = "fake"
# Main must create this from the ClientFactory
# object such that it's populated with the
@@ -24,7 +25,7 @@ TRANSPORT_SERIALKISS = "serialkiss"
factory = None
class Client(metaclass=trace.TraceWrapperMetaclass):
class Client:
"""Singleton client class that constructs the aprslib connection."""
_instance = None
@@ -89,7 +90,7 @@ class Client(metaclass=trace.TraceWrapperMetaclass):
pass
class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
class APRSISClient(Client):
_client = None
@@ -174,7 +175,7 @@ class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
return aprs_client
class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
class KISSClient(Client):
_client = None
@@ -248,6 +249,35 @@ class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
return self._client
class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
@staticmethod
def is_enabled():
if CONF.fake_client.enabled:
return True
return False
@staticmethod
def is_configured():
return APRSDFakeClient.is_enabled()
def is_alive(self):
return True
def setup_connection(self):
return fake.APRSDFakeClient()
@staticmethod
def transport():
return TRANSPORT_FAKE
def decode_packet(self, *args, **kwargs):
LOG.debug(f"kwargs {kwargs}")
pkt = kwargs["packet"]
LOG.debug(f"Got an APRS Fake Packet '{pkt}'")
return pkt
class ClientFactory:
_instance = None
@@ -270,8 +300,11 @@ class ClientFactory:
key = TRANSPORT_APRSIS
elif KISSClient.is_enabled():
key = KISSClient.transport()
elif APRSDFakeClient.is_enabled():
key = TRANSPORT_FAKE
builder = self._builders.get(key)
LOG.debug(f"Creating client {key}")
if not builder:
raise ValueError(key)
return builder()
@@ -312,3 +345,4 @@ class ClientFactory:
factory.register(TRANSPORT_APRSIS, APRSISClient)
factory.register(TRANSPORT_TCPKISS, KISSClient)
factory.register(TRANSPORT_SERIALKISS, KISSClient)
factory.register(TRANSPORT_FAKE, APRSDFakeClient)
+1 -2
View File
@@ -106,13 +106,12 @@ class Aprsdis(aprslib.IS):
aprsd.__version__,
)
self.logger.info("Sending login information")
self.logger.debug("Sending login information")
try:
self._sendall(login_str)
self.sock.settimeout(5)
test = self.sock.recv(len(login_str) + 100)
self.logger.debug("Server: '%s'", test)
if is_py3:
test = test.decode("latin-1")
test = test.rstrip()
+73
View File
@@ -0,0 +1,73 @@
import logging
import threading
import time
import aprslib
from oslo_config import cfg
import wrapt
from aprsd import conf # noqa
from aprsd.packets import core
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
'''Fake client for testing.'''
# flag to tell us to stop
thread_stop = False
lock = threading.Lock()
path = []
def __init__(self):
LOG.info("Starting APRSDFakeClient client.")
self.path = ["WIDE1-1", "WIDE2-1"]
def stop(self):
self.thread_stop = True
LOG.info("Shutdown APRSDFakeClient client.")
def is_alive(self):
"""If the connection is alive or not."""
return not self.thread_stop
@wrapt.synchronized(lock)
def send(self, packet: core.Packet):
"""Send an APRS Message object."""
LOG.info(f"Sending packet: {packet}")
payload = None
if isinstance(packet, core.Packet):
packet.prepare()
payload = packet.payload.encode("US-ASCII")
if packet.path:
packet.path
else:
self.path
else:
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
payload = (
":{:<9}:{}".format(
packet.to_call,
msg_payload,
)
).encode("US-ASCII")
LOG.debug(
f"FAKE::Send '{payload}' TO '{packet.to_call}' From "
f"'{packet.from_call}' with PATH \"{self.path}\"",
)
def consumer(self, callback, blocking=False, immortal=False, raw=False):
LOG.debug("Start non blocking FAKE consumer")
# Generate packets here?
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
pkt_raw = aprslib.parse(raw)
pkt = core.Packet.factory(pkt_raw)
callback(packet=pkt)
LOG.debug(f"END blocking FAKE consumer {self}")
time.sleep(8)
+7 -1
View File
@@ -14,6 +14,8 @@ LOG = logging.getLogger("APRSD")
class KISS3Client:
path = []
def __init__(self):
self.setup()
@@ -34,6 +36,7 @@ class KISS3Client:
speed=CONF.kiss_serial.baudrate,
strip_df_start=True,
)
self.path = CONF.kiss_serial.path
elif CONF.kiss_tcp.enabled:
LOG.debug(
"KISS({}) TCP Connection to {}:{}".format(
@@ -47,6 +50,7 @@ class KISS3Client:
port=CONF.kiss_tcp.port,
strip_df_start=True,
)
self.path = CONF.kiss_tcp.path
LOG.debug("Starting KISS interface connection")
self.kiss.start()
@@ -87,10 +91,12 @@ class KISS3Client:
"""Send an APRS Message object."""
payload = None
path = ["WIDE1-1", "WIDE2-1"]
path = self.path
if isinstance(packet, core.Packet):
packet.prepare()
payload = packet.payload.encode("US-ASCII")
if packet.path:
path = packet.path
else:
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
payload = (
+31 -2
View File
@@ -125,8 +125,37 @@ def test_plugin(
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
for x in range(number):
reply = pm.run(packet)
replies = pm.run(packet)
# Plugin might have threads, so lets stop them so we can exit.
# obj.stop_threads()
LOG.info(f"Result{x} = '{reply}'")
for reply in replies:
if isinstance(reply, list):
# one of the plugins wants to send multiple messages
for subreply in reply:
if isinstance(subreply, packets.Packet):
LOG.info(subreply)
else:
LOG.info(
packets.MessagePacket(
from_call=CONF.callsign,
to_call=fromcall,
message_text=subreply,
),
)
elif isinstance(reply, packets.Packet):
# We have a message based object.
LOG.info(reply)
else:
# A plugin can return a null message flag which signals
# us that they processed the message correctly, but have
# nothing to reply with, so we avoid replying with a
# usage string
if reply is not packets.NULL_MESSAGE:
LOG.info(
packets.MessagePacket(
from_call=CONF.callsign,
to_call=fromcall,
message_text=reply,
),
)
pm.stop()
+5 -1
View File
@@ -58,7 +58,11 @@ def fetch_stats(ctx, host, port, magic_word):
with console.status(msg):
client = rpc_client.RPCClient(host, port, magic_word)
stats = client.get_stats_dict()
console.print_json(data=stats)
if stats:
console.print_json(data=stats)
else:
LOG.error(f"Failed to fetch stats via RPC aprsd server at {host}:{port}")
return
aprsd_title = (
"APRSD "
f"[bold cyan]v{stats['aprsd']['version']}[/] "
+87 -10
View File
@@ -26,6 +26,7 @@ from aprsd.plugins import (
LOG = logging.getLogger("APRSD")
PYPI_URL = "https://pypi.org/search/"
def onerror(name):
@@ -89,18 +90,35 @@ def get_module_info(package_name, module_name, module_path):
return obj_list
def get_installed_plugins():
def _get_installed_aprsd_items():
# installed plugins
ip = {}
plugins = {}
extensions = {}
for finder, name, ispkg in pkgutil.iter_modules():
if name.startswith("aprsd_"):
print(f"Found aprsd_ module: {name}")
if ispkg:
module = importlib.import_module(name)
pkgs = walk_package(module)
for pkg in pkgs:
pkg_info = get_module_info(module.__name__, pkg.name, module.__path__[0])
ip[name] = pkg_info
return ip
if "plugin" in name:
plugins[name] = pkg_info
elif "extension" in name:
extensions[name] = pkg_info
return plugins, extensions
def get_installed_plugins():
# installed plugins
plugins, extensions = _get_installed_aprsd_items()
return plugins
def get_installed_extensions():
# installed plugins
plugins, extensions = _get_installed_aprsd_items()
return extensions
def show_built_in_plugins(console):
@@ -144,22 +162,27 @@ def show_built_in_plugins(console):
console.print(table)
def show_pypi_plugins(installed_plugins, console):
def _get_pypi_packages():
query = "aprsd"
api_url = "https://pypi.org/search/"
snippets = []
s = requests.Session()
for page in range(1, 3):
params = {"q": query, "page": page}
r = s.get(api_url, params=params)
r = s.get(PYPI_URL, params=params)
soup = BeautifulSoup(r.text, "html.parser")
snippets += soup.select('a[class*="snippet"]')
if not hasattr(s, "start_url"):
s.start_url = r.url.rsplit("&page", maxsplit=1).pop(0)
return snippets
def show_pypi_plugins(installed_plugins, console):
snippets = _get_pypi_packages()
title = Text.assemble(
("Pypi.org APRSD Installable Plugin Packages\n\n", "bold magenta"),
("Install any of the following plugins with ", "bold yellow"),
("Install any of the following plugins with\n", "bold yellow"),
("'pip install ", "bold white"),
("<Plugin Package Name>'", "cyan"),
)
@@ -171,7 +194,7 @@ def show_pypi_plugins(installed_plugins, console):
table.add_column("Released", style="bold green", justify="center")
table.add_column("Installed?", style="red", justify="center")
for snippet in snippets:
link = urljoin(api_url, snippet.get("href"))
link = urljoin(PYPI_URL, snippet.get("href"))
package = re.sub(r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip())
version = re.sub(r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip())
created = re.sub(r"\s+", " ", snippet.select_one('span[class*="created"]').text.strip())
@@ -194,7 +217,47 @@ def show_pypi_plugins(installed_plugins, console):
console.print("\n")
console.print(table)
return
def show_pypi_extensions(installed_extensions, console):
snippets = _get_pypi_packages()
title = Text.assemble(
("Pypi.org APRSD Installable Extension Packages\n\n", "bold magenta"),
("Install any of the following extensions by running\n", "bold yellow"),
("'pip install ", "bold white"),
("<Plugin Package Name>'", "cyan"),
)
table = Table(title=title)
table.add_column("Extension Package Name", style="cyan", no_wrap=True)
table.add_column("Description", style="yellow")
table.add_column("Version", style="yellow", justify="center")
table.add_column("Released", style="bold green", justify="center")
table.add_column("Installed?", style="red", justify="center")
for snippet in snippets:
link = urljoin(PYPI_URL, snippet.get("href"))
package = re.sub(r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip())
version = re.sub(r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip())
created = re.sub(r"\s+", " ", snippet.select_one('span[class*="created"]').text.strip())
description = re.sub(r"\s+", " ", snippet.select_one('p[class*="description"]').text.strip())
emoji = ":open_file_folder:"
if "aprsd-" not in package or "-extension" not in package:
continue
under = package.replace("-", "_")
if under in installed_extensions:
installed = "Yes"
else:
installed = "No"
table.add_row(
f"[link={link}]{emoji}[/link] {package}",
description, version, created, installed,
)
console.print("\n")
console.print(table)
def show_installed_plugins(installed_plugins, console):
@@ -240,3 +303,17 @@ def list_plugins(ctx):
status.update("Looking for installed APRSD plugins")
show_installed_plugins(installed_plugins, console)
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.pass_context
@cli_helper.process_standard_options_no_config
def list_extensions(ctx):
"""List the built in plugins available to APRSD."""
console = Console()
with console.status("Show APRSD Extensions") as status:
status.update("Fetching pypi.org APRSD Extensions")
installed_extensions = get_installed_extensions()
show_pypi_extensions(installed_extensions, console)
+1 -1
View File
@@ -144,7 +144,7 @@ def listen(
if not aprs_login:
click.echo(ctx.get_help())
click.echo("")
ctx.fail("Must set --aprs_login or APRS_LOGIN")
ctx.fail("Must set --aprs-login or APRS_LOGIN")
ctx.exit()
if not aprs_password:
+10 -1
View File
@@ -11,7 +11,7 @@ from aprsd import main as aprsd_main
from aprsd import packets, plugin, threads, utils
from aprsd.main import cli
from aprsd.rpc import server as rpc_server
from aprsd.threads import rx
from aprsd.threads import registry, rx, tx
CONF = cfg.CONF
@@ -107,6 +107,15 @@ def server(ctx, flush):
process_thread.start()
packets.PacketTrack().restart()
if CONF.enable_beacon:
LOG.info("Beacon Enabled. Starting Beacon thread.")
bcn_thread = tx.BeaconSendThread()
bcn_thread.start()
if CONF.aprs_registry.enabled:
LOG.info("Registry Enabled. Starting Registry thread.")
registry_thread = registry.APRSRegistryThread()
registry_thread.start()
if CONF.rpc_settings.enabled:
rpc = rpc_server.APRSDRPCThread()
+282 -114
View File
@@ -1,7 +1,7 @@
import datetime
import json
import logging
from logging.handlers import RotatingFileHandler
import math
import signal
import sys
import threading
@@ -11,20 +11,22 @@ from aprslib import util as aprslib_util
import click
import flask
from flask import request
from flask.logging import default_handler
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from geopy.distance import geodesic
from oslo_config import cfg
from user_agents import parse as ua_parse
from werkzeug.security import check_password_hash, generate_password_hash
import wrapt
import aprsd
from aprsd import cli_helper, client, conf, packets, stats, threads, utils
from aprsd.log import rich as aprsd_logging
from aprsd import (
cli_helper, client, packets, plugin_utils, stats, threads, utils,
)
from aprsd.log import log
from aprsd.main import cli
from aprsd.threads import aprsd as aprsd_threads
from aprsd.threads import rx, tx
from aprsd.utils import objectstore, trace
from aprsd.utils import trace
CONF = cfg.CONF
@@ -33,6 +35,16 @@ auth = HTTPBasicAuth()
users = {}
socketio = None
# List of callsigns that we don't want to track/fetch their location
callsign_no_track = [
"REPEAT", "WB4BOR-11", "APDW16", "WXNOW", "WXBOT", "BLN0", "BLN1", "BLN2",
"BLN3", "BLN4", "BLN5", "BLN6", "BLN7", "BLN8", "BLN9",
]
# Callsign location information
# callsign: {lat: 0.0, long: 0.0, last_update: datetime}
callsign_locations = {}
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
@@ -58,7 +70,8 @@ def signal_handler(sig, frame):
signal.signal(signal.SIGTERM, sys.exit(0))
class SentMessages(objectstore.ObjectStoreMixin):
class SentMessages:
_instance = None
lock = threading.Lock()
@@ -75,25 +88,7 @@ class SentMessages(objectstore.ObjectStoreMixin):
@wrapt.synchronized(lock)
def add(self, msg):
self.data[msg.msgNo] = self.create(msg.msgNo)
self.data[msg.msgNo]["from"] = msg.from_call
self.data[msg.msgNo]["to"] = msg.to_call
self.data[msg.msgNo]["message"] = msg.message_text.rstrip("\n")
self.data[msg.msgNo]["raw"] = msg.message_text.rstrip("\n")
def create(self, id):
return {
"id": id,
"ts": time.time(),
"ack": False,
"from": None,
"to": None,
"raw": None,
"message": None,
"status": None,
"last_update": None,
"reply": None,
}
self.data[msg.msgNo] = msg.__dict__
@wrapt.synchronized(lock)
def __len__(self):
@@ -139,8 +134,188 @@ def verify_password(username, password):
return username
def calculate_initial_compass_bearing(point_a, point_b):
"""
Calculates the bearing between two points.
The formulae used is the following:
θ = atan2(sin(Δlong).cos(lat2),
cos(lat1).sin(lat2) sin(lat1).cos(lat2).cos(Δlong))
:Parameters:
- `pointA: The tuple representing the latitude/longitude for the
first point. Latitude and longitude must be in decimal degrees
- `pointB: The tuple representing the latitude/longitude for the
second point. Latitude and longitude must be in decimal degrees
:Returns:
The bearing in degrees
:Returns Type:
float
"""
if (type(point_a) is not tuple) or (type(point_b) is not tuple):
raise TypeError("Only tuples are supported as arguments")
lat1 = math.radians(point_a[0])
lat2 = math.radians(point_b[0])
diff_long = math.radians(point_b[1] - point_a[1])
x = math.sin(diff_long) * math.cos(lat2)
y = math.cos(lat1) * math.sin(lat2) - (
math.sin(lat1)
* math.cos(lat2) * math.cos(diff_long)
)
initial_bearing = math.atan2(x, y)
# Now we have the initial bearing but math.atan2 return values
# from -180° to + 180° which is not what we want for a compass bearing
# The solution is to normalize the initial bearing as shown below
initial_bearing = math.degrees(initial_bearing)
compass_bearing = (initial_bearing + 360) % 360
return compass_bearing
def _build_location_from_repeat(message):
# This is a location message Format is
# ^ld^callsign:latitude,longitude,altitude,course,speed,timestamp
a = message.split(":")
LOG.warning(a)
if len(a) == 2:
callsign = a[0].replace("^ld^", "")
b = a[1].split(",")
LOG.warning(b)
if len(b) == 6:
lat = float(b[0])
lon = float(b[1])
alt = float(b[2])
course = float(b[3])
speed = float(b[4])
time = int(b[5])
data = {
"callsign": callsign,
"lat": lat,
"lon": lon,
"altitude": alt,
"course": course,
"speed": speed,
"lasttime": time,
}
LOG.warning(f"Location data from REPEAT {data}")
return data
def _calculate_location_data(location_data):
"""Calculate all of the location data from data from aprs.fi or REPEAT."""
lat = location_data["lat"]
lon = location_data["lon"]
alt = location_data["altitude"]
speed = location_data["speed"]
lasttime = location_data["lasttime"]
# now calculate distance from our own location
distance = 0
if CONF.webchat.latitude and CONF.webchat.longitude:
our_lat = float(CONF.webchat.latitude)
our_lon = float(CONF.webchat.longitude)
distance = geodesic((our_lat, our_lon), (lat, lon)).kilometers
bearing = calculate_initial_compass_bearing(
(our_lat, our_lon),
(lat, lon),
)
return {
"callsign": location_data["callsign"],
"lat": lat,
"lon": lon,
"altitude": alt,
"course": f"{bearing:0.1f}",
"speed": speed,
"lasttime": lasttime,
"distance": f"{distance:0.3f}",
}
def send_location_data_to_browser(location_data):
global socketio
callsign = location_data["callsign"]
LOG.info(f"Got location for {callsign} {callsign_locations[callsign]}")
socketio.emit(
"callsign_location", callsign_locations[callsign],
namespace="/sendmsg",
)
def populate_callsign_location(callsign, data=None):
"""Populate the location for the callsign.
if data is passed in, then we have the location already from
an APRS packet. If data is None, then we need to fetch the
location from aprs.fi or REPEAT.
"""
global socketio
"""Fetch the location for the callsign."""
LOG.debug(f"populate_callsign_location {callsign}")
if data:
location_data = _calculate_location_data(data)
callsign_locations[callsign] = location_data
send_location_data_to_browser(location_data)
return
# First we are going to try to get the location from aprs.fi
# if there is no internets, then this will fail and we will
# fallback to calling REPEAT for the location for the callsign.
fallback = False
if not CONF.aprs_fi.apiKey:
LOG.warning(
"Config aprs_fi.apiKey is not set. Can't get location from aprs.fi "
" falling back to sending REPEAT to get location.",
)
fallback = True
else:
try:
aprs_data = plugin_utils.get_aprs_fi(CONF.aprs_fi.apiKey, callsign)
if not len(aprs_data["entries"]):
LOG.error("Didn't get any entries from aprs.fi")
return
lat = float(aprs_data["entries"][0]["lat"])
lon = float(aprs_data["entries"][0]["lng"])
try: # altitude not always provided
alt = float(aprs_data["entries"][0]["altitude"])
except Exception:
alt = 0
location_data = {
"callsign": callsign,
"lat": lat,
"lon": lon,
"altitude": alt,
"lasttime": int(aprs_data["entries"][0]["lasttime"]),
"course": float(aprs_data["entries"][0].get("course", 0)),
"speed": float(aprs_data["entries"][0].get("speed", 0)),
}
location_data = _calculate_location_data(location_data)
callsign_locations[callsign] = location_data
send_location_data_to_browser(location_data)
return
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi '{ex}'")
LOG.error(ex)
fallback = True
if fallback:
# We don't have the location data
# and we can't get it from aprs.fi
# Send a special message to REPEAT to get the location data
LOG.info(f"Sending REPEAT to get location for callsign {callsign}.")
tx.send(
packets.MessagePacket(
from_call=CONF.callsign,
to_call="REPEAT",
message_text=f"ld {callsign}",
),
)
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
"""Class that handles packets being sent to us."""
def __init__(self, packet_queue, socketio):
self.socketio = socketio
self.connected = False
@@ -149,37 +324,54 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
def process_ack_packet(self, packet: packets.AckPacket):
super().process_ack_packet(packet)
ack_num = packet.get("msgNo")
SentMessages().ack(int(ack_num))
self.socketio.emit(
"ack", SentMessages().get(int(ack_num)),
namespace="/sendmsg",
)
SentMessages().ack(ack_num)
msg = SentMessages().get(ack_num)
if msg:
self.socketio.emit(
"ack", msg,
namespace="/sendmsg",
)
self.got_ack = True
def process_our_message_packet(self, packet: packets.MessagePacket):
global callsign_locations
LOG.info(f"process MessagePacket {repr(packet)}")
packet.get("addresse", None)
fromcall = packet.from_call
message = packet.get("message_text", None)
msg = {
"id": packet.msgNo,
"ts": packet.get("timestamp", time.time()),
"ack": False,
"from": fromcall,
"to": packet.to_call,
"raw": packet.raw,
"message": message,
"status": None,
"last_update": None,
"reply": None,
}
# ok lets see if we have the location for the
# person we just sent a message to.
from_call = packet.get("from_call").upper()
if from_call == "REPEAT":
# We got a message from REPEAT. Is this a location message?
message = packet.get("message_text")
if message.startswith("^ld^"):
location_data = _build_location_from_repeat(message)
callsign = location_data["callsign"]
location_data = _calculate_location_data(location_data)
callsign_locations[callsign] = location_data
send_location_data_to_browser(location_data)
return
elif (
from_call not in callsign_locations
and from_call not in callsign_no_track
):
# We have to ask aprs for the location for the callsign
# We send a message packet to wb4bor-11 asking for location.
populate_callsign_location(from_call)
# Send the packet to the browser.
self.socketio.emit(
"new", msg,
"new", packet.__dict__,
namespace="/sendmsg",
)
class LocationProcessingThread(aprsd_threads.APRSDThread):
"""Class to handle the location processing."""
def __init__(self):
super().__init__("LocationProcessingThread")
def loop(self):
pass
def set_config():
global users
@@ -191,46 +383,43 @@ def _get_transport(stats):
"APRS-IS Server: <a href='http://status.aprs2.net' >"
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
)
else:
# We might be connected to a KISS socket?
if client.KISSClient.is_enabled():
transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
)
)
elif transport == client.TRANSPORT_SERIALKISS:
# for pep8 violation
aprs_connection = (
"SerialKISS://{}@{} baud".format(
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
),
elif client.KISSClient.is_enabled():
transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
)
)
elif transport == client.TRANSPORT_SERIALKISS:
# for pep8 violation
aprs_connection = (
"SerialKISS://{}@{} baud".format(
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
),
)
elif CONF.fake_client.enabled:
transport = client.TRANSPORT_FAKE
aprs_connection = "Fake Client"
return transport, aprs_connection
@flask_app.route("/location/<callsign>", methods=["POST"])
def location(callsign):
LOG.debug(f"Fetch location for callsign {callsign}")
populate_callsign_location(callsign)
@auth.login_required
@flask_app.route("/")
def index():
ua_str = request.headers.get("User-Agent")
# this takes about 2 seconds :(
user_agent = ua_parse(ua_str)
LOG.debug(f"Is mobile? {user_agent.is_mobile}")
stats = _stats()
if user_agent.is_mobile:
html_template = "mobile.html"
else:
html_template = "index.html"
# For development
# html_template = "mobile.html"
html_template = "index.html"
LOG.debug(f"Template {html_template}")
transport, aprs_connection = _get_transport(stats)
@@ -259,7 +448,7 @@ def index():
@auth.login_required
@flask_app.route("//send-message-status")
@flask_app.route("/send-message-status")
def send_message_status():
LOG.debug(request)
msgs = SentMessages()
@@ -323,11 +512,22 @@ class SendMessageNamespace(Namespace):
LOG.debug(f"WS: on_send {data}")
self.request = data
data["from"] = CONF.callsign
path = data.get("path", None)
if not path:
path = []
elif "," in path:
path_opts = path.split(",")
path = [x.strip() for x in path_opts]
else:
path = [path]
pkt = packets.MessagePacket(
from_call=data["from"],
to_call=data["to"].upper(),
message_text=data["message"],
path=path,
)
pkt.prepare()
self.msg = pkt
msgs = SentMessages()
msgs.add(pkt)
@@ -363,53 +563,21 @@ class SendMessageNamespace(Namespace):
def handle_json(self, data):
LOG.debug(f"WS json {data}")
def setup_logging(flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler)
log_level = conf.log.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level)
date_format = CONF.logging.date_format
if CONF.logging.rich_logging and not quiet:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=15,
rich_tracebacks=True, omit_repeated_times=False,
)
rh.setFormatter(log_formatter)
flask_log.addHandler(rh)
log_file = CONF.logging.logfile
if log_file:
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
backupCount=4,
)
fh.setFormatter(log_formatter)
flask_log.addHandler(fh)
def on_get_callsign_location(self, data):
LOG.debug(f"on_callsign_location {data}")
populate_callsign_location(data["callsign"])
@trace.trace
def init_flask(loglevel, quiet):
global socketio, flask_app
setup_logging(flask_app, loglevel, quiet)
log.setup_logging(loglevel, quiet)
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
async_mode="threading",
)
# async_mode="gevent",
# async_mode="eventlet",
# import eventlet
# eventlet.monkey_patch()
socketio.on_namespace(
SendMessageNamespace(
+29
View File
@@ -11,14 +11,21 @@ aprs_group = cfg.OptGroup(
name="aprs_network",
title="APRS-IS Network settings",
)
kiss_serial_group = cfg.OptGroup(
name="kiss_serial",
title="KISS Serial device connection",
)
kiss_tcp_group = cfg.OptGroup(
name="kiss_tcp",
title="KISS TCP/IP Device connection",
)
fake_client_group = cfg.OptGroup(
name="fake_client",
title="Fake Client settings",
)
aprs_opts = [
cfg.BoolOpt(
"enabled",
@@ -65,6 +72,11 @@ kiss_serial_opts = [
default=9600,
help="The Serial device baud rate for communication",
),
cfg.ListOpt(
"path",
default=["WIDE1-1", "WIDE2-1"],
help="The APRS path to use for wide area coverage.",
),
]
kiss_tcp_opts = [
@@ -82,6 +94,19 @@ kiss_tcp_opts = [
default=8001,
help="The KISS TCP/IP network port",
),
cfg.ListOpt(
"path",
default=["WIDE1-1", "WIDE2-1"],
help="The APRS path to use for wide area coverage.",
),
]
fake_client_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable fake client connection.",
),
]
@@ -93,10 +118,14 @@ def register_opts(config):
config.register_opts(kiss_serial_opts, group=kiss_serial_group)
config.register_opts(kiss_tcp_opts, group=kiss_tcp_group)
config.register_group(fake_client_group)
config.register_opts(fake_client_opts, group=fake_client_group)
def list_opts():
return {
aprs_group.name: aprs_opts,
kiss_serial_group.name: kiss_serial_opts,
kiss_tcp_group.name: kiss_tcp_opts,
fake_client_group.name: fake_client_opts,
}
+73
View File
@@ -24,6 +24,11 @@ webchat_group = cfg.OptGroup(
title="Settings specific to the webchat command",
)
registry_group = cfg.OptGroup(
name="aprs_registry",
title="APRS Registry settings",
)
aprsd_opts = [
cfg.StrOpt(
@@ -65,6 +70,37 @@ aprsd_opts = [
"2 means 1 packet every 2 seconds allowed."
"5 means 1 pack packet every 5 seconds allowed",
),
cfg.IntOpt(
"packet_dupe_timeout",
default=300,
help="The number of seconds before a packet is not considered a duplicate.",
),
cfg.BoolOpt(
"enable_beacon",
default=False,
help="Enable sending of a GPS Beacon packet to locate this service. "
"Requires latitude and longitude to be set.",
),
cfg.IntOpt(
"beacon_interval",
default=1800,
help="The number of seconds between beacon packets.",
),
cfg.StrOpt(
"beacon_symbol",
default="/",
help="The symbol to use for the GPS Beacon packet. See: http://www.aprs.net/vm/DOS/SYMBOLS.HTM",
),
cfg.StrOpt(
"latitude",
default=None,
help="Latitude for the GPS Beacon button. If not set, the button will not be enabled.",
),
cfg.StrOpt(
"longitude",
default=None,
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
),
]
watch_list_opts = [
@@ -119,6 +155,7 @@ admin_opts = [
),
cfg.StrOpt(
"password",
default="password",
secret=True,
help="Admin interface password",
),
@@ -190,6 +227,39 @@ webchat_opts = [
),
]
registry_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable sending aprs registry information. This will let the "
"APRS registry know about your service and it's uptime. "
"No personal information is sent, just the callsign, uptime and description. "
"The service callsign is the callsign set in [DEFAULT] section.",
),
cfg.StrOpt(
"description",
default=None,
help="Description of the service to send to the APRS registry. "
"This is what will show up in the APRS registry."
"If not set, the description will be the same as the callsign.",
),
cfg.StrOpt(
"registry_url",
default="https://aprs.hemna.com/api/v1/registry",
help="The APRS registry domain name to send the information to.",
),
cfg.StrOpt(
"service_website",
default=None,
help="The website for your APRS service to send to the APRS registry.",
),
cfg.IntOpt(
"frequency_seconds",
default=3600,
help="The frequency in seconds to send the APRS registry information.",
),
]
def register_opts(config):
config.register_opts(aprsd_opts)
@@ -202,6 +272,8 @@ def register_opts(config):
config.register_opts(rpc_opts, group=rpc_group)
config.register_group(webchat_group)
config.register_opts(webchat_opts, group=webchat_group)
config.register_group(registry_group)
config.register_opts(registry_opts, group=registry_group)
def list_opts():
@@ -211,4 +283,5 @@ def list_opts():
watch_list_group.name: watch_list_opts,
rpc_group.name: rpc_opts,
webchat_group.name: webchat_opts,
registry_group.name: registry_opts,
}
+8 -10
View File
@@ -20,21 +20,19 @@ DEFAULT_LOG_FORMAT = (
" %(message)s - [%(pathname)s:%(lineno)d]"
)
DEFAULT_LOG_FORMAT = (
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
"<yellow>{thread.name: <18}</yellow> | "
"<level>{level: <8}</level> | "
"<level>{message}</level> | "
"<cyan>{name}</cyan>:<cyan>{function:}</cyan>:<magenta>{line:}</magenta>"
)
logging_group = cfg.OptGroup(
name="logging",
title="Logging options",
)
logging_opts = [
cfg.StrOpt(
"date_format",
default=DEFAULT_DATE_FORMAT,
help="Date format for log entries",
),
cfg.BoolOpt(
"rich_logging",
default=True,
help="Enable Rich log",
),
cfg.StrOpt(
"logfile",
default=None,
+108
View File
@@ -18,6 +18,11 @@ owm_wx_group = cfg.OptGroup(
title="Options for the OWMWeatherPlugin",
)
location_group = cfg.OptGroup(
name="location_plugin",
title="Options for the LocationPlugin",
)
aprsfi_opts = [
cfg.StrOpt(
"apiKey",
@@ -62,6 +67,106 @@ avwx_opts = [
),
]
location_opts = [
cfg.StrOpt(
"geopy_geocoder",
choices=[
"ArcGIS", "AzureMaps", "Baidu", "Bing", "GoogleV3", "HERE",
"Nominatim", "OpenCage", "TomTom", "USGov", "What3Words", "Woosmap",
],
default="Nominatim",
help="The geopy geocoder to use. Default is Nominatim."
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
"for more information.",
),
cfg.StrOpt(
"user_agent",
default="APRSD",
help="The user agent to use for the Nominatim geocoder."
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
"for more information.",
),
cfg.StrOpt(
"arcgis_username",
default=None,
help="The username to use for the ArcGIS geocoder."
"See https://geopy.readthedocs.io/en/latest/#arcgis"
"for more information."
"Only used for the ArcGIS geocoder.",
),
cfg.StrOpt(
"arcgis_password",
default=None,
help="The password to use for the ArcGIS geocoder."
"See https://geopy.readthedocs.io/en/latest/#arcgis"
"for more information."
"Only used for the ArcGIS geocoder.",
),
cfg.StrOpt(
"azuremaps_subscription_key",
help="The subscription key to use for the AzureMaps geocoder."
"See https://geopy.readthedocs.io/en/latest/#azuremaps"
"for more information."
"Only used for the AzureMaps geocoder.",
),
cfg.StrOpt(
"baidu_api_key",
help="The API key to use for the Baidu geocoder."
"See https://geopy.readthedocs.io/en/latest/#baidu"
"for more information."
"Only used for the Baidu geocoder.",
),
cfg.StrOpt(
"bing_api_key",
help="The API key to use for the Bing geocoder."
"See https://geopy.readthedocs.io/en/latest/#bing"
"for more information."
"Only used for the Bing geocoder.",
),
cfg.StrOpt(
"google_api_key",
help="The API key to use for the Google geocoder."
"See https://geopy.readthedocs.io/en/latest/#googlev3"
"for more information."
"Only used for the Google geocoder.",
),
cfg.StrOpt(
"here_api_key",
help="The API key to use for the HERE geocoder."
"See https://geopy.readthedocs.io/en/latest/#here"
"for more information."
"Only used for the HERE geocoder.",
),
cfg.StrOpt(
"opencage_api_key",
help="The API key to use for the OpenCage geocoder."
"See https://geopy.readthedocs.io/en/latest/#opencage"
"for more information."
"Only used for the OpenCage geocoder.",
),
cfg.StrOpt(
"tomtom_api_key",
help="The API key to use for the TomTom geocoder."
"See https://geopy.readthedocs.io/en/latest/#tomtom"
"for more information."
"Only used for the TomTom geocoder.",
),
cfg.StrOpt(
"what3words_api_key",
help="The API key to use for the What3Words geocoder."
"See https://geopy.readthedocs.io/en/latest/#what3words"
"for more information."
"Only used for the What3Words geocoder.",
),
cfg.StrOpt(
"woosmap_api_key",
help="The API key to use for the Woosmap geocoder."
"See https://geopy.readthedocs.io/en/latest/#woosmap"
"for more information."
"Only used for the Woosmap geocoder.",
),
]
def register_opts(config):
config.register_group(aprsfi_group)
@@ -72,6 +177,8 @@ def register_opts(config):
config.register_opts(owm_wx_opts, group=owm_wx_group)
config.register_group(avwx_group)
config.register_opts(avwx_opts, group=avwx_group)
config.register_group(location_group)
config.register_opts(location_opts, group=location_group)
def list_opts():
@@ -80,4 +187,5 @@ def list_opts():
query_group.name: query_plugin_opts,
owm_wx_group.name: owm_wx_opts,
avwx_group.name: avwx_opts,
location_group.name: location_opts,
}
+72 -64
View File
@@ -1,13 +1,12 @@
import logging
from logging import NullHandler
from logging.handlers import RotatingFileHandler
from logging.handlers import QueueHandler
import queue
import sys
from loguru import logger
from oslo_config import cfg
from aprsd import conf
from aprsd.log import rich as aprsd_logging
CONF = cfg.CONF
@@ -15,75 +14,84 @@ LOG = logging.getLogger("APRSD")
logging_queue = queue.Queue()
class InterceptHandler(logging.Handler):
def emit(self, record):
# get corresponding Loguru level if it exists
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
# find caller from where originated the logged message
frame, depth = sys._getframe(6), 6
while frame and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
# Setup the log faciility
# to disable log to stdout, but still log to file
# use the --quiet option on the cmdln
def setup_logging(loglevel, quiet):
log_level = conf.log.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
date_format = CONF.logging.date_format
rh = None
fh = None
def setup_logging(loglevel=None, quiet=False):
if not loglevel:
log_level = CONF.logging.log_level
else:
log_level = conf.log.LOG_LEVELS[loglevel]
rich_logging = False
if CONF.logging.get("rich_logging", False) and not quiet:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=20,
rich_tracebacks=True, omit_repeated_times=False,
# intercept everything at the root logger
logging.root.handlers = [InterceptHandler()]
logging.root.setLevel(log_level)
imap_list = [
"imapclient.imaplib", "imaplib", "imapclient",
"imapclient.util",
]
aprslib_list = [
"aprslib",
"aprslib.parsing",
"aprslib.exceptions",
]
# We don't really want to see the aprslib parsing debug output.
disable_list = imap_list + aprslib_list
# remove every other logger's handlers
# and propagate to root logger
for name in logging.root.manager.loggerDict.keys():
logging.getLogger(name).handlers = []
if name in disable_list:
logging.getLogger(name).propagate = False
else:
logging.getLogger(name).propagate = True
handlers = [
{
"sink": sys.stdout, "serialize": False,
"format": CONF.logging.logformat,
},
]
if CONF.logging.logfile:
handlers.append(
{
"sink": CONF.logging.logfile, "serialize": False,
"format": CONF.logging.logformat,
},
)
rh.setFormatter(log_formatter)
LOG.addHandler(rh)
rich_logging = True
log_file = CONF.logging.logfile
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
if log_file:
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
fh.setFormatter(log_formatter)
LOG.addHandler(fh)
imap_logger = None
if CONF.email_plugin.enabled and CONF.email_plugin.debug:
imap_logger = logging.getLogger("imapclient.imaplib")
imap_logger.setLevel(log_level)
if rh:
imap_logger.addHandler(rh)
if fh:
imap_logger.addHandler(fh)
for name in imap_list:
logging.getLogger(name).propagate = True
if CONF.admin.web_enabled:
qh = logging.handlers.QueueHandler(logging_queue)
q_log_formatter = logging.Formatter(
fmt=CONF.logging.logformat,
datefmt=CONF.logging.date_format,
qh = QueueHandler(logging_queue)
handlers.append(
{
"sink": qh, "serialize": False,
"format": CONF.logging.logformat,
},
)
qh.setFormatter(q_log_formatter)
LOG.addHandler(qh)
if not quiet and not rich_logging:
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(log_formatter)
LOG.addHandler(sh)
if imap_logger:
imap_logger.addHandler(sh)
def setup_logging_no_config(loglevel, quiet):
log_level = conf.log.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
log_format = CONF.logging.logformat
date_format = CONF.logging.date_format
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = NullHandler()
fh.setFormatter(log_formatter)
LOG.addHandler(fh)
if not quiet:
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(log_formatter)
LOG.addHandler(sh)
# configure loguru
logger.configure(handlers=handlers)
-160
View File
@@ -1,160 +0,0 @@
from datetime import datetime
from logging import LogRecord
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Union
from rich._log_render import LogRender
from rich.logging import RichHandler
from rich.text import Text, TextType
from rich.traceback import Traceback
if TYPE_CHECKING:
from rich.console import Console, ConsoleRenderable, RenderableType
from rich.table import Table
from aprsd import utils
FormatTimeCallable = Callable[[datetime], Text]
class APRSDRichLogRender(LogRender):
def __init__(
self, *args,
show_thread: bool = False,
thread_width: Optional[int] = 10,
**kwargs,
):
super().__init__(*args, **kwargs)
self.show_thread = show_thread
self.thread_width = thread_width
def __call__(
self,
console: "Console",
renderables: Iterable["ConsoleRenderable"],
log_time: Optional[datetime] = None,
time_format: Optional[Union[str, FormatTimeCallable]] = None,
level: TextType = "",
path: Optional[str] = None,
line_no: Optional[int] = None,
link_path: Optional[str] = None,
thread_name: Optional[str] = None,
) -> "Table":
from rich.containers import Renderables
from rich.table import Table
output = Table.grid(padding=(0, 1))
output.expand = True
if self.show_time:
output.add_column(style="log.time")
if self.show_thread:
rgb = str(utils.rgb_from_name(thread_name)).replace(" ", "")
output.add_column(style=f"rgb{rgb}", width=self.thread_width)
if self.show_level:
output.add_column(style="log.level", width=self.level_width)
output.add_column(ratio=1, style="log.message", overflow="fold")
if self.show_path and path:
output.add_column(style="log.path")
row: List["RenderableType"] = []
if self.show_time:
log_time = log_time or console.get_datetime()
time_format = time_format or self.time_format
if callable(time_format):
log_time_display = time_format(log_time)
else:
log_time_display = Text(log_time.strftime(time_format))
if log_time_display == self._last_time and self.omit_repeated_times:
row.append(Text(" " * len(log_time_display)))
else:
row.append(log_time_display)
self._last_time = log_time_display
if self.show_thread:
row.append(thread_name)
if self.show_level:
row.append(level)
row.append(Renderables(renderables))
if self.show_path and path:
path_text = Text()
path_text.append(
path, style=f"link file://{link_path}" if link_path else "",
)
if line_no:
path_text.append(":")
path_text.append(
f"{line_no}",
style=f"link file://{link_path}#{line_no}" if link_path else "",
)
row.append(path_text)
output.add_row(*row)
return output
class APRSDRichHandler(RichHandler):
"""APRSD's extension of rich's RichHandler to show threads.
show_thread (bool, optional): Show the name of the thread in log entry. Defaults to False.
thread_width (int, optional): The number of characters to show for thread name. Defaults to 10.
"""
def __init__(
self, *args,
show_thread: bool = True,
thread_width: Optional[int] = 10,
**kwargs,
):
super().__init__(*args, **kwargs)
self.show_thread = show_thread
self.thread_width = thread_width
kwargs["show_thread"] = show_thread
kwargs["thread_width"] = thread_width
self._log_render = APRSDRichLogRender(
show_time=True,
show_level=True,
show_path=True,
omit_repeated_times=False,
level_width=None,
show_thread=show_thread,
thread_width=thread_width,
)
def render(
self, *, record: LogRecord,
traceback: Optional[Traceback],
message_renderable: "ConsoleRenderable",
) -> "ConsoleRenderable":
"""Render log for display.
Args:
record (LogRecord): log Record.
traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
Returns:
ConsoleRenderable: Renderable to display log.
"""
path = Path(record.pathname).name
level = self.get_level_text(record)
time_format = None if self.formatter is None else self.formatter.datefmt
log_time = datetime.fromtimestamp(record.created)
thread_name = record.threadName
log_renderable = self._log_render(
self.console,
[message_renderable] if not traceback else [
message_renderable,
traceback,
],
log_time=log_time,
time_format=time_format,
level=level,
path=path,
line_no=record.lineno,
link_path=record.pathname if self.enable_link_path else None,
thread_name=thread_name,
)
return log_renderable
+26 -10
View File
@@ -59,20 +59,25 @@ click_completion.core.startswith = custom_startswith
click_completion.init()
@click.group(context_settings=CONTEXT_SETTINGS)
@click.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
@click.version_option()
@click.pass_context
def cli(ctx):
pass
def main():
# First import all the possible commands for the CLI
# The commands themselves live in the cmds directory
def load_commands():
from .cmds import ( # noqa
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
send_message, server, webchat,
)
def main():
# First import all the possible commands for the CLI
# The commands themselves live in the cmds directory
load_commands()
utils.load_entry_points("aprsd.extension")
cli(auto_envvar_prefix="APRSD")
@@ -117,15 +122,25 @@ def check_version(ctx):
def sample_config(ctx):
"""Generate a sample Config file from aprsd and all installed plugins."""
def _get_selected_entry_points():
import sys
if sys.version_info < (3, 10):
all = imp.entry_points()
selected = []
if "oslo.config.opts" in all:
for x in all["oslo.config.opts"]:
if x.group == "oslo.config.opts":
selected.append(x)
else:
selected = imp.entry_points(group="oslo.config.opts")
return selected
def get_namespaces():
args = []
all = imp.entry_points()
selected = []
if "oslo.config.opts" in all:
for x in all["oslo.config.opts"]:
if x.group == "oslo.config.opts":
selected.append(x)
# selected = imp.entry_points(group="oslo.config.opts")
selected = _get_selected_entry_points()
for entry in selected:
if "aprsd" in entry.name:
args.append("--namespace")
@@ -146,6 +161,7 @@ def sample_config(ctx):
raise SystemExit
raise
generator.generate(conf)
return
@cli.command()
+1 -1
View File
@@ -1,5 +1,5 @@
from aprsd.packets.core import ( # noqa: F401
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, PathPacket,
AckPacket, BeaconPacket, GPSPacket, MessagePacket, MicEPacket, Packet,
RejectPacket, StatusPacket, WeatherPacket,
)
from aprsd.packets.packet_list import PacketList # noqa: F401
+113 -62
View File
@@ -1,7 +1,6 @@
import abc
from dataclasses import asdict, dataclass, field
import datetime
import json
from datetime import datetime
import logging
import re
import time
@@ -9,9 +8,9 @@ import time
from typing import List
import dacite
from dataclasses_json import dataclass_json
from aprsd.utils import counter
from aprsd.utils import json as aprsd_json
LOG = logging.getLogger("APRSD")
@@ -28,12 +27,20 @@ PACKET_TYPE_BEACON = "beacon"
PACKET_TYPE_THIRDPARTY = "thirdparty"
PACKET_TYPE_UNCOMPRESSED = "uncompressed"
NO_DATE = datetime(1900, 10, 24)
def _int_timestamp():
def _init_timestamp():
"""Build a unix style timestamp integer"""
return int(round(time.time()))
def _init_send_time():
# We have to use a datetime here, or the json encoder
# Fails on a NoneType.
return NO_DATE
def _init_msgNo(): # noqa: N802
"""For some reason __post__init doesn't get called.
@@ -45,42 +52,66 @@ def _init_msgNo(): # noqa: N802
return c.value
@dataclass
def factory_from_dict(packet_dict):
pkt_type = get_packet_type(packet_dict)
if pkt_type:
cls = TYPE_LOOKUP[pkt_type]
return cls.from_dict(packet_dict)
def factory_from_json(packet_dict):
pkt_type = get_packet_type(packet_dict)
if pkt_type:
return TYPE_LOOKUP[pkt_type].from_json(packet_dict)
@dataclass_json
@dataclass(unsafe_hash=True)
class Packet(metaclass=abc.ABCMeta):
from_call: str
to_call: str
addresse: str = None
format: str = None
from_call: str = field(default=None)
to_call: str = field(default=None)
addresse: str = field(default=None)
format: str = field(default=None)
msgNo: str = field(default_factory=_init_msgNo) # noqa: N815
packet_type: str = None
timestamp: float = field(default_factory=_int_timestamp)
packet_type: str = field(default=None)
timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False)
# Holds the raw text string to be sent over the wire
# or holds the raw string from input packet
raw: str = None
raw_dict: dict = field(repr=False, default_factory=lambda: {})
raw: str = field(default=None, compare=False, hash=False)
raw_dict: dict = field(repr=False, default_factory=lambda: {}, compare=False, hash=False)
# Built by calling prepare(). raw needs this built first.
payload: str = None
payload: str = field(default=None)
# Fields related to sending packets out
send_count: int = field(repr=False, default=0)
retry_count: int = field(repr=False, default=3)
last_send_time: datetime.timedelta = field(repr=False, default=None)
send_count: int = field(repr=False, default=0, compare=False, hash=False)
retry_count: int = field(repr=False, default=3, compare=False, hash=False)
# last_send_time: datetime = field(
# metadata=dc_json_config(
# encoder=datetime.isoformat,
# decoder=datetime.fromisoformat,
# ),
# repr=True,
# default_factory=_init_send_time,
# compare=False,
# hash=False
# )
last_send_time: float = field(repr=False, default=0, compare=False, hash=False)
last_send_attempt: int = field(repr=False, default=0, compare=False, hash=False)
# Do we allow this packet to be saved to send later?
allow_delay: bool = field(repr=False, default=True)
allow_delay: bool = field(repr=False, default=True, compare=False, hash=False)
path: List[str] = field(default_factory=list, compare=False, hash=False)
via: str = field(default=None, compare=False, hash=False)
def __post__init__(self):
LOG.warning(f"POST INIT {self}")
@property
def __dict__(self):
return asdict(self)
@property
def json(self):
"""
get the json formated string
"""
return json.dumps(self.__dict__, cls=aprsd_json.EnhancedJSONEncoder)
return self.to_json()
def get(self, key, default=None):
"""Emulate a getter on a dict."""
@@ -89,8 +120,13 @@ class Packet(metaclass=abc.ABCMeta):
else:
return default
@property
def key(self):
"""Build a key for finding this packet in a dict."""
return f"{self.from_call}:{self.addresse}:{self.msgNo}"
def update_timestamp(self):
self.timestamp = _int_timestamp()
self.timestamp = _init_timestamp()
def prepare(self):
"""Do stuff here that is needed prior to sending over the air."""
@@ -112,7 +148,6 @@ class Packet(metaclass=abc.ABCMeta):
self.from_call,
self.payload,
)
LOG.debug(f"_build_raw: payload '{self.payload}' raw '{self.raw}'")
@staticmethod
def factory(raw_packet):
@@ -258,18 +293,9 @@ class Packet(metaclass=abc.ABCMeta):
return repr
@dataclass
class PathPacket(Packet):
path: List[str] = field(default_factory=list)
via: str = None
def _build_payload(self):
raise NotImplementedError
@dataclass
class AckPacket(PathPacket):
response: str = None
@dataclass(unsafe_hash=True)
class AckPacket(Packet):
response: str = field(default=None)
def __post__init__(self):
if self.response:
@@ -279,9 +305,9 @@ class AckPacket(PathPacket):
self.payload = f":{self.to_call.ljust(9)}:ack{self.msgNo}"
@dataclass
class RejectPacket(PathPacket):
response: str = None
@dataclass(unsafe_hash=True)
class RejectPacket(Packet):
response: str = field(default=None)
def __post__init__(self):
if self.response:
@@ -291,9 +317,10 @@ class RejectPacket(PathPacket):
self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}"
@dataclass
class MessagePacket(PathPacket):
message_text: str = None
@dataclass_json
@dataclass(unsafe_hash=True)
class MessagePacket(Packet):
message_text: str = field(default=None)
def _filter_for_send(self) -> str:
"""Filter and format message string for FCC."""
@@ -313,24 +340,24 @@ class MessagePacket(PathPacket):
)
@dataclass
class StatusPacket(PathPacket):
status: str = None
messagecapable: bool = False
comment: str = None
@dataclass(unsafe_hash=True)
class StatusPacket(Packet):
status: str = field(default=None)
messagecapable: bool = field(default=False)
comment: str = field(default=None)
def _build_payload(self):
raise NotImplementedError
@dataclass
class GPSPacket(PathPacket):
latitude: float = 0.00
longitude: float = 0.00
altitude: float = 0.00
rng: float = 0.00
posambiguity: int = 0
comment: str = None
@dataclass(unsafe_hash=True)
class GPSPacket(Packet):
latitude: float = field(default=0.00)
longitude: float = field(default=0.00)
altitude: float = field(default=0.00)
rng: float = field(default=0.00)
posambiguity: int = field(default=0)
comment: str = field(default=None)
symbol: str = field(default="l")
symbol_table: str = field(default="/")
@@ -408,12 +435,12 @@ class GPSPacket(PathPacket):
def _build_time_zulu(self):
"""Build the timestamp in UTC/zulu."""
if self.timestamp:
local_dt = datetime.datetime.fromtimestamp(self.timestamp)
local_dt = datetime.fromtimestamp(self.timestamp)
else:
local_dt = datetime.datetime.now()
self.timestamp = datetime.datetime.timestamp(local_dt)
local_dt = datetime.now()
self.timestamp = datetime.timestamp(local_dt)
utc_offset_timedelta = datetime.datetime.utcnow() - local_dt
utc_offset_timedelta = datetime.utcnow() - local_dt
result_utc_datetime = local_dt + utc_offset_timedelta
time_zulu = result_utc_datetime.strftime("%d%H%M")
return time_zulu
@@ -438,6 +465,26 @@ class GPSPacket(PathPacket):
)
@dataclass(unsafe_hash=True)
class BeaconPacket(GPSPacket):
def _build_payload(self):
"""The payload is the non headers portion of the packet."""
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}APRSD Beacon"
)
def _build_raw(self):
self.raw = (
f"{self.from_call}>APZ100:"
f"{self.payload}"
)
@dataclass
class MicEPacket(GPSPacket):
messagecapable: bool = False
@@ -569,7 +616,7 @@ class WeatherPacket(GPSPacket):
class ThirdParty(Packet):
# Holds the encapsulated packet
subpacket: Packet = None
subpacket: Packet = field(default=None, compare=True, hash=False)
def __repr__(self):
"""Build the repr version of the packet."""
@@ -602,7 +649,7 @@ def get_packet_type(packet: dict):
pkt_format = packet.get("format", None)
msg_response = packet.get("response", None)
packet_type = "unknown"
packet_type = PACKET_TYPE_UNKNOWN
if pkt_format == "message" and msg_response == "ack":
packet_type = PACKET_TYPE_ACK
elif pkt_format == "message" and msg_response == "rej":
@@ -622,6 +669,10 @@ def get_packet_type(packet: dict):
packet_type = PACKET_TYPE_WX
elif pkt_format == PACKET_TYPE_THIRDPARTY:
packet_type = PACKET_TYPE_THIRDPARTY
if packet_type == PACKET_TYPE_UNKNOWN:
if "latitude" in packet:
packet_type = PACKET_TYPE_BEACON
return packet_type
+53 -15
View File
@@ -1,10 +1,12 @@
from collections import OrderedDict
from collections.abc import MutableMapping
import logging
import threading
from oslo_config import cfg
import wrapt
from aprsd import stats, utils
from aprsd import stats
from aprsd.packets import seen_list
@@ -12,31 +14,29 @@ CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
class PacketList:
"""Class to track all of the packets rx'd and tx'd by aprsd."""
class PacketList(MutableMapping):
_instance = None
lock = threading.Lock()
packet_list: utils.RingBuffer = utils.RingBuffer(1000)
_total_rx: int = 0
_total_tx: int = 0
types = {}
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._maxlen = 100
cls.d = OrderedDict()
return cls._instance
@wrapt.synchronized(lock)
def __iter__(self):
return iter(self.packet_list)
@wrapt.synchronized(lock)
def rx(self, packet):
"""Add a packet that was received."""
self._total_rx += 1
self.packet_list.append(packet)
self._add(packet)
ptype = packet.__class__.__name__
if not ptype in self.types:
self.types[ptype] = {"tx": 0, "rx": 0}
self.types[ptype]["rx"] += 1
seen_list.SeenList().update_seen(packet)
stats.APRSDStats().rx(packet)
@@ -44,13 +44,51 @@ class PacketList:
def tx(self, packet):
"""Add a packet that was received."""
self._total_tx += 1
self.packet_list.append(packet)
self._add(packet)
ptype = packet.__class__.__name__
if not ptype in self.types:
self.types[ptype] = {"tx": 0, "rx": 0}
self.types[ptype]["tx"] += 1
seen_list.SeenList().update_seen(packet)
stats.APRSDStats().tx(packet)
@wrapt.synchronized(lock)
def get(self):
return self.packet_list.get()
def add(self, packet):
self._add(packet)
def _add(self, packet):
self[packet.key] = packet
def copy(self):
return self.d.copy()
@property
def maxlen(self):
return self._maxlen
@wrapt.synchronized(lock)
def find(self, packet):
return self.get(packet.key)
def __getitem__(self, key):
# self.d.move_to_end(key)
return self.d[key]
def __setitem__(self, key, value):
if key in self.d:
self.d.move_to_end(key)
elif len(self.d) == self.maxlen:
self.d.popitem(last=False)
self.d[key] = value
def __delitem__(self, key):
del self.d[key]
def __iter__(self):
return self.d.__iter__()
def __len__(self):
return len(self.d)
@wrapt.synchronized(lock)
def total_rx(self):
+9 -16
View File
@@ -62,36 +62,29 @@ class PacketTrack(objectstore.ObjectStoreMixin):
def __len__(self):
return len(self.data)
@wrapt.synchronized(lock)
def __str__(self):
result = "{"
for key in self.data.keys():
result += f"{key}: {str(self.data[key])}, "
result += "}"
return result
@wrapt.synchronized(lock)
def add(self, packet):
key = int(packet.msgNo)
key = packet.msgNo
packet._last_send_attempt = 0
self.data[key] = packet
self.total_tracked += 1
@wrapt.synchronized(lock)
def get(self, id):
if id in self.data:
return self.data[id]
def get(self, key):
return self.data.get(key, None)
@wrapt.synchronized(lock)
def remove(self, id):
key = int(id)
if key in self.data.keys():
def remove(self, key):
try:
del self.data[key]
except KeyError:
pass
def restart(self):
"""Walk the list of messages and restart them if any."""
for key in self.data.keys():
pkt = self.data[key]
if pkt.last_send_attempt < pkt.retry_count:
if pkt._last_send_attempt < pkt.retry_count:
tx.send(pkt)
def _resend(self, packet):
+1
View File
@@ -76,6 +76,7 @@ def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
exclude,
)
)
LOG.debug(f"Fetching OWM weather '{url}'")
response = requests.get(url)
except Exception as e:
LOG.error(e)
+4 -1
View File
@@ -8,6 +8,8 @@ from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
DEFAULT_FORTUNE_PATH = '/usr/games/fortune'
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
"""Fortune."""
@@ -19,7 +21,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
fortune_path = None
def setup(self):
self.fortune_path = shutil.which("fortune")
self.fortune_path = shutil.which(DEFAULT_FORTUNE_PATH)
LOG.info(f"Fortune path {self.fortune_path}")
if not self.fortune_path:
self.enabled = False
else:
+88 -6
View File
@@ -2,7 +2,8 @@ import logging
import re
import time
from geopy.geocoders import Nominatim
from geopy.geocoders import ArcGIS, AzureMaps, Baidu, Bing, GoogleV3
from geopy.geocoders import HereV7, Nominatim, OpenCage, TomTom, What3WordsV3, Woosmap
from oslo_config import cfg
from aprsd import packets, plugin, plugin_utils
@@ -13,6 +14,82 @@ CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
class UsLocation:
raw = {}
def __init__(self, info):
self.info = info
def __str__(self):
return self.info
class USGov:
"""US Government geocoder that uses the geopy API.
This is a dummy class the implements the geopy reverse API,
so the factory can return an object that conforms to the API.
"""
def reverse(self, coordinates):
"""Reverse geocode a coordinate."""
LOG.info(f"USGov reverse geocode {coordinates}")
coords = coordinates.split(",")
lat = float(coords[0])
lon = float(coords[1])
result = plugin_utils.get_weather_gov_for_gps(lat, lon)
# LOG.info(f"WEATHER: {result}")
# LOG.info(f"area description {result['location']['areaDescription']}")
if 'location' in result:
loc = UsLocation(result['location']['areaDescription'])
else:
loc = UsLocation("Unknown Location")
LOG.info(f"USGov reverse geocode LOC {loc}")
return loc
def geopy_factory():
"""Factory function for geopy geocoders."""
geocoder = CONF.location_plugin.geopy_geocoder
LOG.info(f"Using geocoder: {geocoder}")
user_agent = CONF.location_plugin.user_agent
LOG.info(f"Using user_agent: {user_agent}")
if geocoder == "Nominatim":
return Nominatim(user_agent=user_agent)
elif geocoder == "USGov":
return USGov()
elif geocoder == "ArcGIS":
return ArcGIS(
username=CONF.location_plugin.arcgis_username,
password=CONF.location_plugin.arcgis_password,
user_agent=user_agent,
)
elif geocoder == "AzureMaps":
return AzureMaps(
user_agent=user_agent,
subscription_key=CONF.location_plugin.azuremaps_subscription_key,
)
elif geocoder == "Baidu":
return Baidu(user_agent=user_agent, api_key=CONF.location_plugin.baidu_api_key)
elif geocoder == "Bing":
return Bing(user_agent=user_agent, api_key=CONF.location_plugin.bing_api_key)
elif geocoder == "GoogleV3":
return GoogleV3(user_agent=user_agent, api_key=CONF.location_plugin.google_api_key)
elif geocoder == "HERE":
return HereV7(user_agent=user_agent, api_key=CONF.location_plugin.here_api_key)
elif geocoder == "OpenCage":
return OpenCage(user_agent=user_agent, api_key=CONF.location_plugin.opencage_api_key)
elif geocoder == "TomTom":
return TomTom(user_agent=user_agent, api_key=CONF.location_plugin.tomtom_api_key)
elif geocoder == "What3Words":
return What3WordsV3(user_agent=user_agent, api_key=CONF.location_plugin.what3words_api_key)
elif geocoder == "Woosmap":
return Woosmap(user_agent=user_agent, api_key=CONF.location_plugin.woosmap_api_key)
else:
raise ValueError(f"Unknown geocoder: {geocoder}")
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
"""Location!"""
@@ -57,19 +134,24 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
# Get some information about their location
try:
tic = time.perf_counter()
geolocator = Nominatim(user_agent="APRSD")
geolocator = geopy_factory()
LOG.info(f"Using GEOLOCATOR: {geolocator}")
coordinates = f"{lat:0.6f}, {lon:0.6f}"
location = geolocator.reverse(coordinates)
address = location.raw.get("address")
LOG.debug(f"GEOLOCATOR address: {address}")
toc = time.perf_counter()
if address:
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
if address.get("country_code") == "us":
area_info = f"{address.get('county')}, {address.get('state')}"
if address.get("country_code") == "us":
area_info = f"{address.get('county')}, {address.get('state')}"
else:
# what to do for address for non US?
area_info = f"{address.get('country'), 'Unknown'}"
else:
# what to do for address for non US?
area_info = f"{address.get('country'), 'Unknown'}"
area_info = str(location)
except Exception as ex:
LOG.error(ex)
LOG.error(f"Failed to fetch Geopy address {ex}")
area_info = "Unknown Location"
+7 -3
View File
@@ -26,7 +26,9 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
"weather" - returns weather near the calling callsign
"""
command_regex = r"^([w][x]|[w][x]\s|weather)"
# command_regex = r"^([w][x]|[w][x]\s|weather)"
command_regex = r"^[wW]"
command_name = "USWeather"
short_description = "Provide USA only weather of GPS Beacon location"
@@ -189,7 +191,9 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
"""
command_regex = r"^([w][x]|[w][x]\s|weather)"
# command_regex = r"^([w][x]|[w][x]\s|weather)"
command_regex = r"^[wW]"
command_name = "OpenWeatherMap"
short_description = "OpenWeatherMap weather of GPS Beacon location"
@@ -211,7 +215,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
@trace.trace
def process(self, packet):
fromcall = packet.get("from")
fromcall = packet.get("from_call")
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
LOG.info(f"OWMWeather Plugin '{message}'")
+1 -1
View File
@@ -4,7 +4,7 @@ import queue
# aprsd.threads
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
from .keep_alive import KeepAliveThread # noqa: F401
from .rx import APRSDRXThread # noqa: F401
from .rx import APRSDRXThread, APRSDDupeRXThread, APRSDProcessPacketThread # noqa: F401
packet_queue = queue.Queue(maxsize=20)
+6 -1
View File
@@ -82,7 +82,12 @@ class KeepAliveThread(APRSDThread):
# check the APRS connection
cl = client.factory.create()
if not cl.is_alive():
# Reset the connection if it's dead and this isn't our
# First time through the loop.
# The first time through the loop can happen at startup where
# The keepalive thread starts before the client has a chance
# to make it's connection the first time.
if not cl.is_alive() and self.cntr > 0:
LOG.error(f"{cl.__class__.__name__} is not alive!!! Resetting")
client.factory.create().reset()
else:
+56
View File
@@ -0,0 +1,56 @@
import logging
import time
from oslo_config import cfg
import requests
import aprsd
from aprsd import threads as aprsd_threads
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
class APRSRegistryThread(aprsd_threads.APRSDThread):
"""This sends service information to the configured APRS Registry."""
_loop_cnt: int = 1
def __init__(self):
super().__init__("APRSRegistryThread")
self._loop_cnt = 1
if not CONF.aprs_registry.enabled:
LOG.error(
"APRS Registry is not enabled. ",
)
LOG.error(
"APRS Registry thread is STOPPING.",
)
self.stop()
LOG.info(
"APRS Registry thread is running and will send "
f"info every {CONF.aprs_registry.frequency_seconds} seconds "
f"to {CONF.aprs_registry.registry_url}.",
)
def loop(self):
# Only call the registry every N seconds
if self._loop_cnt % CONF.aprs_registry.frequency_seconds == 0:
info = {
"callsign": CONF.callsign,
"description": CONF.aprs_registry.description,
"service_website": CONF.aprs_registry.service_website,
"software": f"APRSD version {aprsd.__version__} "
"https://github.com/craigerl/aprsd",
}
try:
requests.post(
f"{CONF.aprs_registry.registry_url}",
json=info,
)
except Exception as e:
LOG.error(f"Failed to send registry info: {e}")
time.sleep(1)
self._loop_cnt += 1
return True
+60 -12
View File
@@ -58,20 +58,72 @@ class APRSDRXThread(APRSDThread):
pass
class APRSDPluginRXThread(APRSDRXThread):
class APRSDDupeRXThread(APRSDRXThread):
"""Process received packets.
This is the main APRSD Server command thread that
receives packets from APRIS and then sends them for
processing in the PluginProcessPacketThread.
receives packets and makes sure the packet
hasn't been seen previously before sending it on
to be processed.
"""
def process_packet(self, *args, **kwargs):
"""This handles the processing of an inbound packet.
When a packet is received by the connected client object,
it sends the raw packet into this function. This function then
decodes the packet via the client, and then processes the packet.
Ack Packets are sent to the PluginProcessPacketThread for processing.
All other packets have to be checked as a dupe, and then only after
we haven't seen this packet before, do we send it to the
PluginProcessPacketThread for processing.
"""
packet = self._client.decode_packet(*args, **kwargs)
# LOG.debug(raw)
packet.log(header="RX")
packets.PacketList().rx(packet)
self.packet_queue.put(packet)
if isinstance(packet, packets.AckPacket):
# We don't need to drop AckPackets, those should be
# processed.
self.packet_queue.put(packet)
else:
# Make sure we aren't re-processing the same packet
# For RF based APRS Clients we can get duplicate packets
# So we need to track them and not process the dupes.
found = False
pkt_list = packets.PacketList()
try:
# Find the packet in the list of already seen packets
# Based on the packet.key
found = pkt_list.find(packet)
except KeyError:
found = False
if not found:
# If we are in the process of already ack'ing
# a packet, we should drop the packet
# because it's a dupe within the time that
# we send the 3 acks for the packet.
pkt_list.rx(packet)
self.packet_queue.put(packet)
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
# If the packet came in within 60 seconds of the
# Last time seeing the packet, then we drop it as a dupe.
LOG.warning(f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.")
else:
LOG.warning(
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
)
pkt_list.rx(packet)
self.packet_queue.put(packet)
class APRSDPluginRXThread(APRSDDupeRXThread):
""""Process received packets.
For backwards compatibility, we keep the APRSDPluginRXThread.
"""
class APRSDProcessPacketThread(APRSDThread):
@@ -113,7 +165,7 @@ class APRSDProcessPacketThread(APRSDThread):
def process_packet(self, packet):
"""Process a packet received from aprs-is server."""
LOG.debug(f"RXPKT-LOOP {self._loop_cnt}")
LOG.debug(f"ProcessPKT-LOOP {self._loop_cnt}")
our_call = CONF.callsign.lower()
from_call = packet.from_call
@@ -125,8 +177,6 @@ class APRSDProcessPacketThread(APRSDThread):
# We don't put ack packets destined for us through the
# plugins.
wl = packets.WatchList()
wl.update_seen(packet)
if (
isinstance(packet, packets.AckPacket)
and packet.addresse.lower() == our_call
@@ -160,7 +210,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.process_other_packet(
packet, for_us=(to_call.lower() == our_call),
)
LOG.debug("Packet processing complete")
LOG.debug(f"Packet processing complete for pkt '{packet.key}'")
return False
@abc.abstractmethod
@@ -214,9 +264,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
to_call = packet.addresse
else:
to_call = None
# msg = packet.get("message_text", None)
# packet.get("msgNo", "0")
# packet.get("response", None)
pm = plugin.PluginManager()
try:
results = pm.run(packet)
+46 -7
View File
@@ -1,4 +1,3 @@
import datetime
import logging
import time
@@ -38,6 +37,7 @@ msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_t)
@msg_throttle_decorator.sleep_and_retry
def send(packet: core.Packet, direct=False, aprs_client=None):
"""Send a packet either in a thread or directly to the client."""
# prepare the packet for sending.
@@ -128,10 +128,10 @@ class SendPacketThread(aprsd_threads.APRSDThread):
# Message is still outstanding and needs to be acked.
if packet.last_send_time:
# Message has a last send time tracking
now = datetime.datetime.now()
now = int(round(time.time()))
sleeptime = (packet.send_count + 1) * 31
delta = now - packet.last_send_time
if delta > datetime.timedelta(seconds=sleeptime):
if delta > sleeptime:
# It's time to try to send it again
send_now = True
else:
@@ -140,7 +140,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
if send_now:
# no attempt time, so lets send it, and start
# tracking the time.
packet.last_send_time = datetime.datetime.now()
packet.last_send_time = int(round(time.time()))
send(packet, direct=True)
packet.send_count += 1
@@ -173,13 +173,13 @@ class SendAckThread(aprsd_threads.APRSDThread):
if self.packet.last_send_time:
# Message has a last send time tracking
now = datetime.datetime.now()
now = int(round(time.time()))
# aprs duplicate detection is 30 secs?
# (21 only sends first, 28 skips middle)
sleep_time = 31
delta = now - self.packet.last_send_time
if delta > datetime.timedelta(seconds=sleep_time):
if delta > sleep_time:
# It's time to try to send it again
send_now = True
elif self.loop_count % 10 == 0:
@@ -190,8 +190,47 @@ class SendAckThread(aprsd_threads.APRSDThread):
if send_now:
send(self.packet, direct=True)
self.packet.send_count += 1
self.packet.last_send_time = datetime.datetime.now()
self.packet.last_send_time = int(round(time.time()))
time.sleep(1)
self.loop_count += 1
return True
class BeaconSendThread(aprsd_threads.APRSDThread):
"""Thread that sends a GPS beacon packet periodically.
Settings are in the [DEFAULT] section of the config file.
"""
_loop_cnt: int = 1
def __init__(self):
super().__init__("BeaconSendThread")
self._loop_cnt = 1
# Make sure Latitude and Longitude are set.
if not CONF.latitude or not CONF.longitude:
LOG.error(
"Latitude and Longitude are not set in the config file."
"Beacon will not be sent and thread is STOPPED.",
)
self.stop()
LOG.info(
"Beacon thread is running and will send "
f"beacons every {CONF.beacon_interval} seconds.",
)
def loop(self):
# Only dump out the stats every N seconds
if self._loop_cnt % CONF.beacon_interval == 0:
pkt = core.BeaconPacket(
from_call=CONF.callsign,
to_call="APRS",
latitude=float(CONF.latitude),
longitude=float(CONF.longitude),
comment="APRSD GPS Beacon",
symbol=CONF.beacon_symbol,
)
send(pkt, direct=True)
self._loop_cnt += 1
time.sleep(1)
return True
+24 -2
View File
@@ -4,6 +4,7 @@ import errno
import os
import re
import sys
import traceback
import update_checker
@@ -18,7 +19,7 @@ from .ring_buffer import RingBuffer # noqa: F401
if sys.version_info.major == 3 and sys.version_info.minor >= 3:
from collections.abc import MutableMapping
else:
from collections import MutableMapping
from collections.abc import MutableMapping
def env(*vars, **kwargs):
@@ -126,4 +127,25 @@ def parse_delta_str(s):
)
else:
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
return {key: float(val) for key, val in m.groupdict().items()}
if m:
return {key: float(val) for key, val in m.groupdict().items()}
else:
return {}
def load_entry_points(group):
"""Load all extensions registered to the given entry point group"""
try:
import importlib_metadata
except ImportError:
# For python 3.10 and later
import importlib.metadata as importlib_metadata
eps = importlib_metadata.entry_points(group=group)
for ep in eps:
try:
ep.load()
except Exception as e:
print(f"Extension {ep.name} of group {group} failed to load with {e}", file=sys.stderr)
print(traceback.format_exc(), file=sys.stderr)
+1 -1
View File
@@ -37,7 +37,7 @@ class PacketCounter:
@property
@wrapt.synchronized(lock)
def value(self):
return self.val.value
return str(self.val.value)
@wrapt.synchronized(lock)
def __repr__(self):
+7 -1
View File
@@ -28,6 +28,9 @@ class ObjectStoreMixin:
def __len__(self):
return len(self.data)
def __iter__(self):
return iter(self.data)
def get_all(self):
with self.lock:
return self.data
@@ -96,11 +99,14 @@ class ObjectStoreMixin:
LOG.debug(
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
)
LOG.debug(f"{self.data}")
else:
LOG.debug(f"{self.__class__.__name__}::No data to load.")
except (pickle.UnpicklingError, Exception) as ex:
LOG.error(f"Failed to UnPickle {self._save_filename()}")
LOG.error(ex)
self.data = {}
else:
LOG.debug(f"{self.__class__.__name__}::No save file found.")
def flush(self):
"""Nuke the old pickle file that stored the old results from last aprsd run."""
+403
View File
@@ -0,0 +1,403 @@
var packet_list = {};
var tx_data = [];
var rx_data = [];
var packet_types_data = {};
var mem_current = []
var mem_peak = []
function start_charts() {
console.log("start_charts() called");
// Initialize the echarts instance based on the prepared dom
create_packets_chart();
create_packets_types_chart();
create_messages_chart();
create_ack_chart();
create_memory_chart();
}
function create_packets_chart() {
// The packets totals TX/RX chart.
pkt_c_canvas = document.getElementById('packetsChart');
packets_chart = echarts.init(pkt_c_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'APRS Packet totals'
},
legend: {},
tooltip : {
trigger: 'axis'
},
toolbox: {
show : true,
feature : {
mark : {show: true},
dataView : {show: true, readOnly: true},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable : true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'tx',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'tx' // refer sensor 1 value
}
},{
name: 'rx',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'rx'
}
}]
};
// Display the chart using the configuration items and data just specified.
packets_chart.setOption(option);
}
function create_packets_types_chart() {
// The packets types chart
pkt_types_canvas = document.getElementById('packetTypesChart');
packet_types_chart = echarts.init(pkt_types_canvas);
// The series and data are built and updated on the fly
// as packets come in.
var option = {
title: {
text: 'Packet Types'
},
legend: {},
tooltip : {
trigger: 'axis'
},
toolbox: {
show : true,
feature : {
mark : {show: true},
dataView : {show: true, readOnly: true},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable : true,
xAxis: { type: 'time' },
yAxis: { },
}
packet_types_chart.setOption(option);
}
function create_messages_chart() {
msg_c_canvas = document.getElementById('messagesChart');
message_chart = echarts.init(msg_c_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Message Packets'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: true},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'tx',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'tx' // refer sensor 1 value
}
},{
name: 'rx',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'rx'
}
}]
};
// Display the chart using the configuration items and data just specified.
message_chart.setOption(option);
}
function create_ack_chart() {
ack_canvas = document.getElementById('acksChart');
ack_chart = echarts.init(ack_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Ack Packets'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: false},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'tx',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'tx' // refer sensor 1 value
}
},{
name: 'rx',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'rx'
}
}]
};
ack_chart.setOption(option);
}
function create_memory_chart() {
ack_canvas = document.getElementById('memChart');
memory_chart = echarts.init(ack_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Memory Usage'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: false},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'current',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'current' // refer sensor 1 value
}
},{
name: 'peak',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'peak'
}
}]
};
memory_chart.setOption(option);
}
function updatePacketData(chart, time, first, second) {
tx_data.push([time, first]);
rx_data.push([time, second]);
option = {
series: [
{
name: 'tx',
data: tx_data,
},
{
name: 'rx',
data: rx_data,
}
]
}
chart.setOption(option);
}
function updatePacketTypesData(time, typesdata) {
//The options series is created on the fly each time based on
//the packet types we have in the data
var series = []
for (const k in typesdata) {
tx = [time, typesdata[k]["tx"]]
rx = [time, typesdata[k]["rx"]]
if (packet_types_data.hasOwnProperty(k)) {
packet_types_data[k]["tx"].push(tx)
packet_types_data[k]["rx"].push(rx)
} else {
packet_types_data[k] = {'tx': [tx], 'rx': [rx]}
}
}
}
function updatePacketTypesChart() {
series = []
for (const k in packet_types_data) {
entry = {
name: k+"tx",
data: packet_types_data[k]["tx"],
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: k+'tx' // refer sensor 1 value
}
}
series.push(entry)
entry = {
name: k+"rx",
data: packet_types_data[k]["rx"],
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: k+'rx' // refer sensor 1 value
}
}
series.push(entry)
}
option = {
series: series
}
console.log(option)
packet_types_chart.setOption(option);
}
function updateTypeChart(chart, key) {
//Generic function to update a packet type chart
if (! packet_types_data.hasOwnProperty(key)) {
return;
}
if (! packet_types_data[key].hasOwnProperty('tx')) {
return;
}
var option = {
series: [{
name: "tx",
data: packet_types_data[key]["tx"],
},
{
name: "rx",
data: packet_types_data[key]["rx"]
}]
}
chart.setOption(option);
}
function updateMemChart(time, current, peak) {
mem_current.push([time, current]);
mem_peak.push([time, peak]);
option = {
series: [
{
name: 'current',
data: mem_current,
},
{
name: 'peak',
data: mem_peak,
}
]
}
memory_chart.setOption(option);
}
function updateMessagesChart() {
updateTypeChart(message_chart, "MessagePacket")
}
function updateAcksChart() {
updateTypeChart(ack_chart, "AckPacket")
}
function update_stats( data ) {
console.log(data);
our_callsign = data["stats"]["aprsd"]["callsign"];
$("#version").text( data["stats"]["aprsd"]["version"] );
$("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
$("#jsonstats").html(html_pretty);
t = Date.parse(data["time"]);
ts = new Date(t);
updatePacketData(packets_chart, ts, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]);
updatePacketTypesData(ts, data["stats"]["packets"]["types"]);
updatePacketTypesChart();
updateMessagesChart();
updateAcksChart();
updateMemChart(ts, data["stats"]["aprsd"]["memory_current"], data["stats"]["aprsd"]["memory_peak"]);
//updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
//updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
//updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
}
+24 -16
View File
@@ -6,6 +6,7 @@
<script src="https://cdn.socket.io/4.7.1/socket.io.min.js" integrity="sha512-+NaO7d6gQ1YPxvc/qHIqZEchjGm207SszoNeMgppoqD/67fEqmc1edS8zrbxPD+4RQI3gDgT/83ihpFW61TG/Q==" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
@@ -15,7 +16,7 @@
<link rel="stylesheet" href="/static/css/prism.css">
<script src="/static/js/prism.js"></script>
<script src="/static/js/main.js"></script>
<script src="/static/js/charts.js"></script>
<script src="/static/js/echarts.js"></script>
<script src="/static/js/tabs.js"></script>
<script src="/static/js/send-message.js"></script>
<script src="/static/js/logs.js"></script>
@@ -83,6 +84,7 @@
<div class="item" data-tab="plugin-tab">Plugins</div>
<div class="item" data-tab="config-tab">Config</div>
<div class="item" data-tab="log-tab">LogFile</div>
<!-- <div class="item" data-tab="oslo-tab">OSLO CONFIG</div> //-->
<div class="item" data-tab="raw-tab">Raw JSON</div>
</div>
@@ -92,25 +94,25 @@
<div class="ui equal width relaxed grid">
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px">
<canvas id="packetsChart"></canvas>
</div>
</div>
<div class="column">
<div class="ui segment" style="height: 300px">
<canvas id="messageChart"></canvas>
</div>
<div class="ui segment" style="height: 300px" id="packetsChart"></div>
</div>
</div>
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px">
<canvas id="emailChart"></canvas>
</div>
<div class="ui segment" style="height: 300px" id="packetTypesChart"></div>
</div>
</div>
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px" id="messagesChart"></div>
</div>
<div class="column">
<div class="ui segment" style="height: 300px">
<canvas id="memChart"></canvas>
<div class="ui segment" style="height: 300px" id="acksChart"></div>
</div>
</div>
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px" id="memChart">
</div>
</div>
</div>
@@ -118,7 +120,7 @@
<div id="stats" class="two column">
<button class="ui button" id="toggleStats">Toggle raw json</button>
<pre id="jsonstats" class="language-json">{{ stats }}</pre>
</div> --!>
</div> //-->
</div>
</div>
@@ -164,9 +166,15 @@
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>
</div>
<!--
<div class="ui bottom attached tab segment" data-tab="oslo-tab">
<h3 class="ui dividing header">OSLO</h3>
<pre id="osloContainer" style="height:600px;overflow-y:auto;" class="language-json">{{ oslo_out|safe }}</pre>
</div> //-->
<div class="ui bottom attached tab segment" data-tab="raw-tab">
<h3 class="ui dividing header">Raw JSON</h3>
<pre id="jsonstats" class="language-json">{{ stats|safe }}</pre>
<pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ stats|safe }}</pre>
</div>
<div class="ui text container">
+115
View File
@@ -0,0 +1,115 @@
input[type=search]::-webkit-search-cancel-button {
-webkit-appearance: searchfield-cancel-button;
}
.speech-wrapper {
padding-top: 0px;
padding: 5px 30px;
background-color: #CCCCCC;
}
.bubble-row {
display: flex;
width: 100%;
justify-content: flex-start;
}
.bubble-row.alt {
justify-content: flex-end;
}
.bubble {
/*width: 350px; */
height: auto;
display: block;
background: #f5f5f5;
border-radius: 4px;
box-shadow: 2px 8px 5px #555;
position: relative;
margin: 0 0 15px;
}
.bubble.alt {
margin: 0 0 15px;
}
.bubble-text {
padding: 5px 5px 0px 8px;
}
.bubble-name {
width: 280px;
font-weight: 600;
font-size: 12px;
margin: 0 0 0px;
color: #3498db;
display: flex;
align-items: center;
.material-symbols-rounded {
margin-left: auto;
font-weight: normal;
color: #808080;
}
}
.bubble-name.alt {
color: #2ecc71;
}
.bubble-timestamp {
margin-right: auto;
font-size: 11px;
text-transform: uppercase;
color: #bbb
}
.bubble-message {
font-size: 16px;
margin: 0px;
padding: 0px 0px 0px 0px;
color: #2b2b2b;
text-align: left;
}
.bubble-arrow {
position: absolute;
width: 0;
bottom:30px;
left: -16px;
height: 0px;
}
.bubble-arrow.alt {
right: -2px;
bottom: 30px;
left: auto;
}
.bubble-arrow:after {
content: "";
position: absolute;
border: 0 solid transparent;
border-top: 9px solid #f5f5f5;
border-radius: 0 20px 0;
width: 15px;
height: 30px;
transform: rotate(145deg);
}
.bubble-arrow.alt:after {
transform: rotate(45deg) scaleY(-1);
}
.popover {
max-width: 400px;
}
.popover-header {
font-size: 8pt;
max-width: 400px;
padding: 5px;
background-color: #ee;
}
.popover-body {
white-space: pre-line;
max-width: 400px;
padding: 5px;
}
+23 -51
View File
@@ -1,36 +1,9 @@
body {
background: #eeeeee;
margin: 2em;
/*margin: 1em;*/
text-align: center;
font-family: system-ui, sans-serif;
}
footer {
padding: 2em;
text-align: center;
height: 10vh;
}
.ui.segment {
background: #eeeeee;
}
ul.list {
list-style-type: disc;
}
ul.list li {
list-style-position: outside;
}
#left {
margin-right: 2px;
height: 300px;
}
#right {
height: 300px;
}
#center {
height: 300px;
height: 100%;
}
#title {
@@ -39,6 +12,7 @@ ul.list li {
#version{
font-size: .5em;
}
#uptime, #aprsis {
font-size: 1em;
}
@@ -67,28 +41,26 @@ ul.list li {
height: 16px;
}
#msgsTabsDiv .ui.tab {
margin:0px;
padding:0px;
display: block;
.wc-container {
display: flex;
flex-flow: column;
height: 100%;
}
.wc-container .wc-row {
/*border: 1px dotted #0313fc;*/
padding: 2px;
}
.wc-container .wc-row.header {
flex: 0 1 auto;
}
.wc-container .wc-row.content {
flex: 1 1 auto;
overflow-y: auto;
}
.wc-container .wc-row.footer {
flex: 0 1 0px;
}
#msgsTabsDiv .header, .tiny.text, .content, .break,
.thumbs.down.outline.icon,
.phone.volume.icon
{
display: inline-block;
float: left;
position: relative;
}
#msgsTabsDiv .tiny.text {
width:100px;
}
#msgsTabsDiv .tiny.header {
width:100px;
text-align: left;
}
#msgsTabsDiv .break {
margin: 2px;
text-align: left;
.material-symbols-rounded.md-10 {
font-size: 18px !important;
}
+1 -1
View File
@@ -37,5 +37,5 @@
border: 1px solid #ccc;
height: 450px;
overflow-y: scroll;
background-color: white;
background-color: #CCCCCC;
}
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,23 @@
/* fallback */
@font-face {
font-family: 'Material Symbols Rounded';
font-style: normal;
font-weight: 200;
src: url(/static/css/upstream/font.woff2) format('woff2');
}
.material-symbols-rounded {
font-family: 'Material Symbols Rounded';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
@@ -0,0 +1,28 @@
/**
* jQuery toast plugin created by Kamran Ahmed copyright MIT license 2014
*/
.jq-toast-wrap { display: block; position: fixed; width: 250px; pointer-events: none !important; margin: 0; padding: 0; letter-spacing: normal; z-index: 9000 !important; }
.jq-toast-wrap * { margin: 0; padding: 0; }
.jq-toast-wrap.bottom-left { bottom: 20px; left: 20px; }
.jq-toast-wrap.bottom-right { bottom: 20px; right: 40px; }
.jq-toast-wrap.top-left { top: 20px; left: 20px; }
.jq-toast-wrap.top-right { top: 20px; right: 40px; }
.jq-toast-single { display: block; width: 100%; padding: 10px; margin: 0px 0px 5px; border-radius: 4px; font-size: 12px; font-family: arial, sans-serif; line-height: 17px; position: relative; pointer-events: all !important; background-color: #444444; color: white; }
.jq-toast-single h2 { font-family: arial, sans-serif; font-size: 14px; margin: 0px 0px 7px; background: none; color: inherit; line-height: inherit; letter-spacing: normal; }
.jq-toast-single a { color: #eee; text-decoration: none; font-weight: bold; border-bottom: 1px solid white; padding-bottom: 3px; font-size: 12px; }
.jq-toast-single ul { margin: 0px 0px 0px 15px; background: none; padding:0px; }
.jq-toast-single ul li { list-style-type: disc !important; line-height: 17px; background: none; margin: 0; padding: 0; letter-spacing: normal; }
.close-jq-toast-single { position: absolute; top: 3px; right: 7px; font-size: 14px; cursor: pointer; }
.jq-toast-loader { display: block; position: absolute; top: -2px; height: 5px; width: 0%; left: 0; border-radius: 5px; background: red; }
.jq-toast-loaded { width: 100%; }
.jq-has-icon { padding: 10px 10px 10px 50px; background-repeat: no-repeat; background-position: 10px; }
.jq-icon-info { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII='); background-color: #31708f; color: #d9edf7; border-color: #bce8f1; }
.jq-icon-warning { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII='); background-color: #8a6d3b; color: #fcf8e3; border-color: #faebcc; }
.jq-icon-error { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII='); background-color: #a94442; color: #f2dede; border-color: #ebccd1; }
.jq-icon-success { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg=='); color: #dff0d8; background-color: #3c763d; border-color: #d6e9c6; }
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe" viewBox="0 0 16 16">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+5 -4
View File
@@ -70,10 +70,11 @@ function showPosition(position) {
}
console.log(msg);
$.toast({
title: 'Sending GPS Beacon',
message: "Latitude: "+position.coords.latitude+"<br>Longitude: "+position.coords.longitude,
showProgress: 'bottom',
classProgress: 'red'
heading: 'Sending GPS Beacon',
text: "Latitude: "+position.coords.latitude+"<br>Longitude: "+position.coords.longitude,
loader: true,
loaderBg: '#9EC600',
position: 'top-center',
});
console.log("Sending GPS msg")
@@ -1,246 +0,0 @@
var cleared = false;
var callsign_list = {};
var message_list = {};
var from_msg_list = {};
const socket = io("/sendmsg");
function size_dict(d){c=0; for (i in d) ++c; return c}
function init_chat() {
socket.on('connect', function () {
console.log("Connected to socketio");
});
socket.on('connected', function(msg) {
console.log("Connected!");
console.log(msg);
});
socket.on("sent", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
sent_msg(msg);
});
socket.on("ack", function(msg) {
update_msg(msg);
});
socket.on("new", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
from_msg(msg);
});
$("#sendform").submit(function(event) {
event.preventDefault();
msg = {'to': $('#to_call').val().toUpperCase(),
'message': $('#message').val(),
}
socket.emit("send", msg);
$('#message').val('');
});
init_gps();
}
function add_callsign(callsign) {
/* Ensure a callsign exists in the left hand nav */
dropdown = $('#callsign_dropdown')
if (callsign in callsign_list) {
console.log(callsign+' already in list.')
return false
}
var callsignTabs = $("#callsignTabs");
tab_name = tab_string(callsign);
tab_content = tab_content_name(callsign);
divname = content_divname(callsign);
item_html = '<div class="active item" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
callsignTabs.append(item_html);
callsign_list[callsign] = {'name': callsign, 'value': callsign, 'text': callsign}
return true
}
function append_message(callsign, msg, msg_html) {
console.log('append_message');
new_callsign = false
if (!message_list.hasOwnProperty(callsign)) {
message_list[callsign] = new Array();
}
message_list[callsign].push(msg);
// Find the right div to place the html
new_callsign = add_callsign(callsign);
append_message_html(callsign, msg_html, new_callsign);
if (new_callsign) {
//click on the new tab
click_div = '#'+tab_string(callsign);
console.log("Click on "+click_div);
$(click_div).click();
}
}
function tab_string(callsign) {
return "msgs"+callsign;
}
function tab_content_name(callsign) {
return tab_string(callsign)+"Content";
}
function content_divname(callsign) {
return "#"+tab_content_name(callsign);
}
function append_message_html(callsign, msg_html, new_callsign) {
var msgsTabs = $('#msgsTabsDiv');
divname_str = tab_content_name(callsign);
divname = content_divname(callsign);
if (new_callsign) {
// we have to add a new DIV
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
msgsTabs.append(msg_div_html);
} else {
var msgDiv = $(divname);
msgDiv.append(msg_html);
}
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
}
function create_message_html(time, from, to, message, ack, msg) {
div_id = from + "_" + msg.id;
msg_html = '<div class="item" id="'+div_id+'">';
msg_html += '<div class="tiny text">'+time+'</div>';
msg_html += '<div class="middle aligned content">';
msg_html += '<div class="tiny red header">'+from+'</div>';
if (ack) {
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>';
} else {
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
}
msg_html += '<div class="middle aligned content">>&nbsp;&nbsp;&nbsp;</div>';
msg_html += '</div>';
msg_html += '<div class="middle aligned content">'+message+'</div>';
msg_html += '</div><br>';
return msg_html
}
function flash_message(msg) {
// Callback function to bring a hidden box back
id = msg.from + "_" + msg.id;
var msgid = $('#'+id);
msgid.effect("pulsate", { times:3 }, 2000);
}
function sent_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg);
append_message(msg['to'], msg, msg_html);
}
function from_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
console.log(msg);
if (!from_msg_list.hasOwnProperty(msg.from)) {
from_msg_list[msg.from] = new Array();
}
if (msg.id in from_msg_list[msg.from]) {
// We already have this message
console.log("We already have this message " + msg);
// Do some flashy thing?
flash_message(msg);
return false
} else {
console.log("Adding message " + msg.id + " to " + msg.from);
from_msg_list[msg.from][msg.id] = msg
}
// We have an existing entry
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
from = msg['from']
msg_html = create_message_html(t, from, false, msg['message'], false, msg);
append_message(from, msg, msg_html);
}
function update_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
id = ts_str.split('.')[0]
pretty_id = "pretty_" + id
loader_id = "loader_" + id
ack_id = "ack_" + id
span_id = "span_" + id
if (msg['ack'] == true) {
var loader_div = $('#' + loader_id);
var ack_div = $('#' + ack_id);
loader_div.removeClass('ui active inline loader');
loader_div.addClass('ui disabled loader');
ack_div.removeClass('thumbs up outline icon');
ack_div.addClass('thumbs up outline icon');
}
$('.ui.accordion').accordion('refresh');
}
function callsign_select(callsign) {
var tocall = $("#to_call");
tocall.val(callsign);
}
function reset_Tabs() {
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
}
function openCallsign(evt, callsign) {
var i, tabcontent, tablinks;
tab_content = tab_content_name(callsign);
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tab_content).style.display = "block";
evt.target.className += " active";
callsign_select(callsign);
}
+358 -144
View File
@@ -1,15 +1,56 @@
var cleared = false;
var callsign_list = {};
var callsign_location = {};
var message_list = {};
var from_msg_list = {};
var selected_tab_callsign = null;
const socket = io("/sendmsg");
MSG_TYPE_TX = "tx";
MSG_TYPE_RX = "rx";
MSG_TYPE_ACK = "ack";
function reload_popovers() {
$('[data-bs-toggle="popover"]').popover(
{html: true, animation: true}
);
}
function build_location_string(msg) {
dt = new Date(parseInt(msg['lasttime']) * 1000);
loc = "Last Location Update: " + dt.toLocaleString();
loc += "<br>Latitude: " + msg['lat'] + "<br>Longitude: " + msg['lon'];
loc += "<br>" + "Altitude: " + msg['altitude'] + " m";
loc += "<br>" + "Speed: " + msg['speed'] + " kph";
loc += "<br>" + "Bearing: " + msg['course'] + "°";
loc += "<br>" + "distance: " + msg['distance'] + " km";
return loc;
}
function build_location_string_small(msg) {
dt = new Date(parseInt(msg['lasttime']) * 1000);
loc = "" + msg['distance'] + "km";
//loc += "Lat " + msg['lat'] + "&nbsp;Lon " + msg['lon'];
loc += "@" + msg['course'] + "°";
//loc += "&nbsp;Distance " + msg['distance'] + " km";
loc += "&nbsp;" + dt.toLocaleString();
return loc;
}
function size_dict(d){c=0; for (i in d) ++c; return c}
function raise_error(msg) {
$.toast({
heading: 'Error',
text: msg,
loader: true,
loaderBg: '#9EC600',
position: 'top-center',
});
}
function init_chat() {
socket.on('connect', function () {
console.log("Connected to socketio");
@@ -21,7 +62,6 @@ function init_chat() {
socket.on("sent", function(msg) {
if (cleared === false) {
console.log("CLEARING #msgsTabsDiv");
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('');
cleared = true;
@@ -45,13 +85,38 @@ function init_chat() {
from_msg(msg);
});
socket.on("callsign_location", function(msg) {
console.log("CALLSIGN Location!");
console.log(msg);
now = new Date();
msg['last_updated'] = now;
callsign_location[msg['callsign']] = msg;
location_id = callsign_location_content(msg['callsign'], true);
location_string = build_location_string_small(msg);
$(location_id).html(location_string);
$(location_id+"Spinner").addClass('d-none');
save_data();
});
$("#sendform").submit(function(event) {
event.preventDefault();
msg = {'to': $('#to_call').val(),
'message': $('#message').val(),
}
socket.emit("send", msg);
$('#message').val('');
to_call = $('#to_call').val();
message = $('#message').val();
path = $('#pkt_path option:selected').val();
if (to_call == "") {
raise_error("You must enter a callsign to send a message")
return false;
} else {
if (message == "") {
raise_error("You must enter a message to send")
return false;
}
msg = {'to': to_call, 'message': message, 'path': path};
//console.log(msg);
socket.emit("send", msg);
$('#message').val('');
}
});
init_gps();
@@ -59,9 +124,67 @@ function init_chat() {
init_messages();
}
function tab_string(callsign, id=false) {
name = "msgs"+callsign;
if (id) {
return "#"+name;
} else {
return name;
}
}
function tab_li_string(callsign, id=false) {
//The id of the LI containing the tab
return tab_string(callsign,id)+"Li";
}
function tab_notification_id(callsign, id=false) {
// The ID of the span that contains the notification count
return tab_string(callsign, id)+"notify";
}
function tab_content_name(callsign, id=false) {
return tab_string(callsign, id)+"Content";
}
function tab_content_speech_wrapper(callsign, id=false) {
return tab_string(callsign, id)+"SpeechWrapper";
}
function tab_content_speech_wrapper_id(callsign) {
return "#"+tab_content_speech_wrapper(callsign);
}
function content_divname(callsign) {
return "#"+tab_content_name(callsign);
}
function callsign_tab(callsign) {
return "#"+tab_string(callsign);
}
function callsign_location_popover(callsign, id=false) {
return tab_string(callsign, id)+"Location";
}
function callsign_location_content(callsign, id=false) {
return tab_string(callsign, id)+"LocationContent";
}
function bubble_msg_id(msg, id=false) {
// The id of the div that contains a specific message
name = msg["from_call"] + "_" + msg["msgNo"];
if (id) {
return "#"+name;
} else {
return name;
}
}
function message_ts_id(msg) {
//Create a 'id' from the message timestamp
ts_str = msg["ts"].toString();
ts_str = msg["timestamp"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0];
return {'timestamp': ts, 'id': id};
@@ -83,31 +206,37 @@ function save_data() {
// Save the relevant data to local storage
localStorage.setItem('callsign_list', JSON.stringify(callsign_list));
localStorage.setItem('message_list', JSON.stringify(message_list));
localStorage.setItem('callsign_location', JSON.stringify(callsign_location));
}
function init_messages() {
// This tries to load any previous conversations from local storage
callsign_list = JSON.parse(localStorage.getItem('callsign_list'));
message_list = JSON.parse(localStorage.getItem('message_list'));
console.log("init_messages");
callsign_location = JSON.parse(localStorage.getItem('callsign_location'));
if (callsign_list == null) {
callsign_list = {};
}
if (message_list == null) {
message_list = {};
}
if (callsign_location == null) {
callsign_location = {};
}
console.log(callsign_list);
console.log(message_list);
console.log(callsign_location);
// Now loop through each callsign and add the tabs
first_callsign = null;
for (callsign in callsign_list) {
console.log("Adding callsign " + callsign);
if (first_callsign === null) {
first_callsign = callsign;
console.log("first_callsign " + first_callsign)
active = true;
} else {
active = false;
}
create_callsign_tab(callsign);
create_callsign_tab(callsign, active);
}
// and then populate the messages in order
for (callsign in message_list) {
@@ -117,205 +246,307 @@ function init_messages() {
msg = message_list[callsign][id];
info = time_ack_from_msg(msg);
t = info['time'];
d = info['date'];
ack_id = false;
acked = false;
if (msg['type'] == MSG_TYPE_TX) {
ack_id = info['ack_id'];
acked = msg['ack'];
}
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg, acked);
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'],
msg['message_text'], ack_id, msg, acked);
append_message_html(callsign, msg_html, new_callsign);
new_callsign = false;
}
}
//Click on the very first tab
if (first_callsign !== null) {
click_div = '#'+tab_string(first_callsign);
var click_timer = setTimeout(function() {
console.log("Click on first tab " + click_div);
$(click_div).click();
clearTimeout(click_timer);
}, 500);
callsign_select(first_callsign);
}
}
function create_callsign_tab(callsign) {
//Create the html for the callsign tab and insert it into the DOM
console.log("create_callsign_tab " + callsign)
var callsignTabs = $("#callsignTabs");
tab_name = tab_string(callsign);
tab_content = tab_content_name(callsign);
divname = content_divname(callsign);
function scroll_main_content(callsign=false) {
var wc = $('#wc-content');
var d = $('#msgsTabContent');
var scrollHeight = wc.prop('scrollHeight');
var clientHeight = wc.prop('clientHeight');
item_html = '<div class="tablinks" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
console.log(item_html);
callsignTabs.append(item_html);
if (callsign) {
div_id = content_divname(callsign);
c_div = $(content_divname(callsign));
//console.log("c_div("+div_id+") " + c_div);
c_height = c_div.height();
c_scroll_height = c_div.prop('scrollHeight');
//console.log("callsign height " + c_height + " scrollHeight " + c_scroll_height);
if (c_height === undefined) {
return false;
}
if (c_height > clientHeight) {
wc.animate({ scrollTop: c_scroll_height }, 500);
} else {
wc.animate({ scrollTop: 0 }, 500);
}
} else {
if (scrollHeight > clientHeight) {
wc.animate({ scrollTop: wc.prop('scrollHeight') }, 500);
} else {
wc.animate({ scrollTop: 0 }, 500);
}
}
}
function add_callsign(callsign) {
function create_callsign_tab(callsign, active=false) {
//Create the html for the callsign tab and insert it into the DOM
var callsignTabs = $("#msgsTabList");
tab_id = tab_string(callsign);
tab_id_li = tab_li_string(callsign);
tab_notify_id = tab_notification_id(callsign);
tab_content = tab_content_name(callsign);
popover_id = callsign_location_popover(callsign);
if (active) {
active_str = "active";
} else {
active_str = "";
}
item_html = '<li class="nav-item" role="presentation" callsign="'+callsign+'" id="'+tab_id_li+'">';
//item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link position-relative '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
item_html += callsign+'&nbsp;&nbsp;';
item_html += '<span onclick="delete_tab(\''+callsign+'\');">×</span>';
item_html += '</button></li>'
callsignTabs.append(item_html);
create_callsign_tab_content(callsign, active);
}
function create_callsign_tab_content(callsign, active=false) {
var callsignTabsContent = $("#msgsTabContent");
tab_id = tab_string(callsign);
tab_content = tab_content_name(callsign);
wrapper_id = tab_content_speech_wrapper(callsign);
if (active) {
active_str = "show active";
} else {
active_str = '';
}
location_str = "Unknown Location"
if (callsign in callsign_location) {
location_str = build_location_string_small(callsign_location[callsign]);
location_class = '';
}
location_id = callsign_location_content(callsign);
item_html = '<div class="tab-pane fade '+active_str+'" id="'+tab_content+'" role="tabpanel" aria-labelledby="'+tab_id+'">';
item_html += '<div class="" style="border: 1px solid #999999;background-color:#aaaaaa;">';
item_html += '<div class="row" style="padding-top:4px;padding-bottom:4px;background-color:#aaaaaa;margin:0px;">';
item_html += '<div class="d-flex col-md-10 justify-content-left" style="padding:0px;margin:0px;">';
item_html += '<button onclick="call_callsign_location(\''+callsign+'\');" style="margin-left:2px;padding: 0px 4px 0px 4px;" type="button" class="btn btn-primary">';
item_html += '<span id="'+location_id+'Spinner" class="d-none spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>Update</button>';
item_html += '&nbsp;<span id="'+location_id+'">'+location_str+'</span></div>';
item_html += '</div>';
item_html += '<div class="speech-wrapper" id="'+wrapper_id+'"></div>';
item_html += '</div>';
callsignTabsContent.append(item_html);
}
function delete_tab(callsign) {
// User asked to delete the tab and the conversation
tab_id = tab_string(callsign, true);
tab_id_li = tab_li_string(callsign, true);
tab_content = tab_content_name(callsign, true);
$(tab_id_li).remove();
$(tab_content).remove();
delete callsign_list[callsign];
delete message_list[callsign];
delete callsign_location[callsign];
// Now select the first tab
first_tab = $("#msgsTabList").children().first().children().first();
console.log(first_tab);
$(first_tab).click();
save_data();
}
function add_callsign(callsign, msg) {
/* Ensure a callsign exists in the left hand nav */
if (callsign in callsign_list) {
return false
}
create_callsign_tab(callsign);
callsign_list[callsign] = true;
return true
len = Object.keys(callsign_list).length;
if (len == 0) {
active = true;
} else {
active = false;
}
create_callsign_tab(callsign, active);
callsign_list[callsign] = '';
return true;
}
function update_callsign_path(callsign, msg) {
//Get the selected path to save for this callsign
path = msg['path']
$('#pkt_path').val(path);
callsign_list[callsign] = path;
}
function append_message(callsign, msg, msg_html) {
console.log("append_message " + callsign + " " + msg + " " + msg_html);
new_callsign = false
if (!message_list.hasOwnProperty(callsign)) {
//message_list[callsign] = new Array();
message_list[callsign] = {};
}
ts_id = message_ts_id(msg);
id = ts_id['id']
message_list[callsign][id] = msg;
if (selected_tab_callsign != callsign) {
// We need to update the notification for the tab
tab_notify_id = tab_notification_id(callsign, true);
// get the current count of notifications
count = parseInt($(tab_notify_id).text());
count += 1;
$(tab_notify_id).text(count);
$(tab_notify_id).removeClass('visually-hidden');
}
// Find the right div to place the html
new_callsign = add_callsign(callsign);
new_callsign = add_callsign(callsign, msg);
update_callsign_path(callsign, msg);
append_message_html(callsign, msg_html, new_callsign);
if (new_callsign) {
//click on the new tab
click_div = '#'+tab_string(callsign);
$(click_div).click();
len = Object.keys(callsign_list).length;
if (new_callsign && len == 1) {
//Now click the tab if and only if there is only one tab
callsign_tab_id = callsign_tab(callsign);
$(callsign_tab_id).click();
callsign_select(callsign);
}
}
function tab_string(callsign) {
return "msgs"+callsign;
}
function tab_content_name(callsign) {
return tab_string(callsign)+"Content";
}
function content_divname(callsign) {
return "#"+tab_content_name(callsign);
}
function append_message_html(callsign, msg_html, new_callsign) {
var msgsTabs = $('#msgsTabsDiv');
console.log("append_message_html " + callsign + " " + msg_html + " " + new_callsign);
divname_str = tab_content_name(callsign);
divname = content_divname(callsign);
if (new_callsign) {
// we have to add a new DIV
console.log("new callsign, create msg_div_html ");
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
console.log(msg_div_html);
msgsTabs.append(msg_div_html);
} else {
var msgDiv = $(divname);
console.log("Appending ("+ msg_html + ") to " + divname);
console.log(msgDiv);
msgDiv.append(msg_html);
console.log(msgDiv);
}
console.log("divname " + divname);
console.log($(divname).length);
tab_content = tab_content_name(callsign);
wrapper_id = tab_content_speech_wrapper_id(callsign);
if ($(divname).length > 0) {
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
$(wrapper_id).append(msg_html);
if ($(wrapper_id).children().length > 0) {
$(wrapper_id).animate({scrollTop: $(wrapper_id)[0].scrollHeight}, "fast");
}
$(divname).trigger('click');
}
function create_message_html(time, from, to, message, ack_id, msg, acked=false) {
div_id = from + "_" + msg.id;
msg_html = '<div class="item" id="'+div_id+'">';
msg_html += '<div class="tiny text">'+time+'</div>';
msg_html += '<div class="middle aligned content">';
msg_html += '<div class="tiny red header">'+from+'</div>';
function create_message_html(date, time, from, to, message, ack_id, msg, acked=false) {
div_id = from + "_" + msg.msgNo;
if (ack_id) {
alt = " alt"
} else {
alt = ""
}
bubble_class = "bubble" + alt + " text-nowrap"
bubble_name_class = "bubble-name" + alt
bubble_msgid = bubble_msg_id(msg);
date_str = date + " " + time;
sane_date_str = date_str.replace(/ /g,"").replaceAll("/","").replaceAll(":","");
bubble_msg_class = "bubble-message";
if (ack_id) {
bubble_arrow_class = "bubble-arrow alt";
popover_placement = "left";
} else {
bubble_arrow_class = "bubble-arrow";
popover_placement = "right";
}
msg_html = '<div class="bubble-row'+alt+'">';
msg_html += '<div id="'+bubble_msgid+'" class="'+ bubble_class + '" ';
msg_html += 'title="APRS Raw Packet" data-bs-placement="'+popover_placement+'" data-bs-toggle="popover" ';
msg_html += 'data-bs-trigger="hover" data-bs-content="'+msg['raw']+'">';
msg_html += '<div class="bubble-text">';
msg_html += '<p class="'+ bubble_name_class +'">'+from+'&nbsp;&nbsp;';
msg_html += '<span class="bubble-timestamp">'+date_str+'</span>';
if (ack_id) {
if (acked) {
msg_html += '<div class="middle aligned content"><i class="thumbs up outline icon" id="' + ack_id + '" data-content="Message ACKed"></i></div>';
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_up</span>';
} else {
msg_html += '<div class="middle aligned content"><i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i></div>';
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_down</span>';
}
} else {
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
}
msg_html += '<div class="middle aligned content">>&nbsp;&nbsp;&nbsp;</div>';
msg_html += '</div>';
msg_html += '<div class="middle aligned content">'+message+'</div>';
msg_html += '</div><br>';
msg_html += "</p>";
msg_html += '<p class="' +bubble_msg_class+ '">'+message+'</p>';
msg_html += '<div class="'+ bubble_arrow_class + '"></div>';
msg_html += "</div></div></div>";
return msg_html
}
function flash_message(msg) {
// Callback function to bring a hidden box back
id = msg.from + "_" + msg.id;
var msgid = $('#'+id);
msgid.effect("pulsate", { times:3 }, 2000);
msg_id = bubble_msg_id(msg, true);
$(msg_id).fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100);
}
function sent_msg(msg) {
info = time_ack_from_msg(msg);
t = info['time'];
d = info['date'];
ack_id = info['ack_id'];
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg, false);
append_message(msg['to'], msg, msg_html);
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'], msg['message_text'], ack_id, msg, false);
append_message(msg['to_call'], msg, msg_html);
save_data();
scroll_main_content(msg['from_call']);
}
function from_msg(msg) {
console.log(msg);
if (!from_msg_list.hasOwnProperty(msg.from)) {
from_msg_list[msg.from] = new Array();
if (!from_msg_list.hasOwnProperty(msg["from_call"])) {
from_msg_list[msg["from_call"]] = new Array();
}
if (msg.id in from_msg_list[msg.from]) {
if (msg["msgNo"] in from_msg_list[msg["from_call"]]) {
// We already have this message
console.log("We already have this message " + msg);
//console.log("We already have this message msgNo=" + msg["msgNo"]);
// Do some flashy thing?
flash_message(msg);
return false
} else {
console.log("Adding message " + msg.id + " to " + msg.from);
from_msg_list[msg.from][msg.id] = msg
from_msg_list[msg["from_call"]][msg["msgNo"]] = msg
}
info = time_ack_from_msg(msg);
t = info['time'];
d = info['date'];
ack_id = info['ack_id'];
from = msg['from']
msg_html = create_message_html(t, from, false, msg['message'], false, msg, false);
from = msg['from_call']
msg_html = create_message_html(d, t, from, false, msg['message_text'], false, msg, false);
append_message(from, msg, msg_html);
save_data();
scroll_main_content(from);
}
function ack_msg(msg) {
// Acknowledge a message
console.log("ack_msg ");
console.log(msg);
console.log(message_list);
// We have an existing entry
ts_id = message_ts_id(msg);
console.log(ts_id)
id = ts_id['id'];
//Mark the message as acked
callsign = msg['to'];
callsign = msg['to_call'];
// Ensure the message_list has this callsign
if (!message_list.hasOwnProperty(callsign)) {
console.log("No message_list for " + callsign);
return false
}
// Ensure the message_list has this id
if (!message_list[callsign].hasOwnProperty(id)) {
console.log("No message_list for " + callsign + " " + id);
return false
}
console.log("Marking message as acked " + callsign + " " + id)
if (message_list[callsign][id]['ack'] == true) {
console.log("Message already acked");
return false;
}
message_list[callsign][id]['ack'] = true;
@@ -323,46 +554,29 @@ function ack_msg(msg) {
if (msg['ack'] == true) {
var ack_div = $('#' + ack_id);
//ack_div.removeClass('thumbs up outline icon');
ack_div.removeClass('thumbs down outline icon');
ack_div.addClass('thumbs up outline icon');
ack_div.html('thumb_up');
}
$('.ui.accordion').accordion('refresh');
//$('.ui.accordion').accordion('refresh');
save_data();
scroll_main_content();
}
function callsign_select(callsign) {
var tocall = $("#to_call");
tocall.val(callsign);
var tocall = $("#to_call");
tocall.val(callsign);
scroll_main_content(callsign);
selected_tab_callsign = callsign;
tab_notify_id = tab_notification_id(callsign, true);
$(tab_notify_id).addClass('visually-hidden');
$(tab_notify_id).text(0);
// Now update the path
$('#pkt_path').val(callsign_list[callsign]);
}
function reset_Tabs() {
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
}
function openCallsign(evt, callsign) {
// This is called when a callsign tab is clicked
var i, tabcontent, tablinks;
tab_content = tab_content_name(callsign);
tabcontent = document.getElementsByClassName("tabcontent");
console.log(tabcontent);
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
console.log(tablinks);
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tab_content).style.display = "block";
evt.target.className += " active";
callsign_select(callsign);
function call_callsign_location(callsign) {
msg = {'callsign': callsign};
socket.emit("get_callsign_location", msg);
location_id = callsign_location_content(callsign, true)+"Spinner";
$(location_id).removeClass('d-none');
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,374 @@
// jQuery toast plugin created by Kamran Ahmed copyright MIT license 2015
if ( typeof Object.create !== 'function' ) {
Object.create = function( obj ) {
function F() {}
F.prototype = obj;
return new F();
};
}
(function( $, window, document, undefined ) {
"use strict";
var Toast = {
_positionClasses : ['bottom-left', 'bottom-right', 'top-right', 'top-left', 'bottom-center', 'top-center', 'mid-center'],
_defaultIcons : ['success', 'error', 'info', 'warning'],
init: function (options, elem) {
this.prepareOptions(options, $.toast.options);
this.process();
},
prepareOptions: function(options, options_to_extend) {
var _options = {};
if ( ( typeof options === 'string' ) || ( options instanceof Array ) ) {
_options.text = options;
} else {
_options = options;
}
this.options = $.extend( {}, options_to_extend, _options );
},
process: function () {
this.setup();
this.addToDom();
this.position();
this.bindToast();
this.animate();
},
setup: function () {
var _toastContent = '';
this._toastEl = this._toastEl || $('<div></div>', {
class : 'jq-toast-single'
});
// For the loader on top
_toastContent += '<span class="jq-toast-loader"></span>';
if ( this.options.allowToastClose ) {
_toastContent += '<span class="close-jq-toast-single">&times;</span>';
};
if ( this.options.text instanceof Array ) {
if ( this.options.heading ) {
_toastContent +='<h2 class="jq-toast-heading">' + this.options.heading + '</h2>';
};
_toastContent += '<ul class="jq-toast-ul">';
for (var i = 0; i < this.options.text.length; i++) {
_toastContent += '<li class="jq-toast-li" id="jq-toast-item-' + i + '">' + this.options.text[i] + '</li>';
}
_toastContent += '</ul>';
} else {
if ( this.options.heading ) {
_toastContent +='<h2 class="jq-toast-heading">' + this.options.heading + '</h2>';
};
_toastContent += this.options.text;
}
this._toastEl.html( _toastContent );
if ( this.options.bgColor !== false ) {
this._toastEl.css("background-color", this.options.bgColor);
};
if ( this.options.textColor !== false ) {
this._toastEl.css("color", this.options.textColor);
};
if ( this.options.textAlign ) {
this._toastEl.css('text-align', this.options.textAlign);
}
if ( this.options.icon !== false ) {
this._toastEl.addClass('jq-has-icon');
if ( $.inArray(this.options.icon, this._defaultIcons) !== -1 ) {
this._toastEl.addClass('jq-icon-' + this.options.icon);
};
};
if ( this.options.class !== false ){
this._toastEl.addClass(this.options.class)
}
},
position: function () {
if ( ( typeof this.options.position === 'string' ) && ( $.inArray( this.options.position, this._positionClasses) !== -1 ) ) {
if ( this.options.position === 'bottom-center' ) {
this._container.css({
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
bottom: 20
});
} else if ( this.options.position === 'top-center' ) {
this._container.css({
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
top: 20
});
} else if ( this.options.position === 'mid-center' ) {
this._container.css({
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
top: ( $(window).outerHeight() / 2 ) - this._container.outerHeight()/2
});
} else {
this._container.addClass( this.options.position );
}
} else if ( typeof this.options.position === 'object' ) {
this._container.css({
top : this.options.position.top ? this.options.position.top : 'auto',
bottom : this.options.position.bottom ? this.options.position.bottom : 'auto',
left : this.options.position.left ? this.options.position.left : 'auto',
right : this.options.position.right ? this.options.position.right : 'auto'
});
} else {
this._container.addClass( 'bottom-left' );
}
},
bindToast: function () {
var that = this;
this._toastEl.on('afterShown', function () {
that.processLoader();
});
this._toastEl.find('.close-jq-toast-single').on('click', function ( e ) {
e.preventDefault();
if( that.options.showHideTransition === 'fade') {
that._toastEl.trigger('beforeHide');
that._toastEl.fadeOut(function () {
that._toastEl.trigger('afterHidden');
});
} else if ( that.options.showHideTransition === 'slide' ) {
that._toastEl.trigger('beforeHide');
that._toastEl.slideUp(function () {
that._toastEl.trigger('afterHidden');
});
} else {
that._toastEl.trigger('beforeHide');
that._toastEl.hide(function () {
that._toastEl.trigger('afterHidden');
});
}
});
if ( typeof this.options.beforeShow == 'function' ) {
this._toastEl.on('beforeShow', function () {
that.options.beforeShow(that._toastEl);
});
};
if ( typeof this.options.afterShown == 'function' ) {
this._toastEl.on('afterShown', function () {
that.options.afterShown(that._toastEl);
});
};
if ( typeof this.options.beforeHide == 'function' ) {
this._toastEl.on('beforeHide', function () {
that.options.beforeHide(that._toastEl);
});
};
if ( typeof this.options.afterHidden == 'function' ) {
this._toastEl.on('afterHidden', function () {
that.options.afterHidden(that._toastEl);
});
};
if ( typeof this.options.onClick == 'function' ) {
this._toastEl.on('click', function () {
that.options.onClick(that._toastEl);
});
};
},
addToDom: function () {
var _container = $('.jq-toast-wrap');
if ( _container.length === 0 ) {
_container = $('<div></div>',{
class: "jq-toast-wrap",
role: "alert",
"aria-live": "polite"
});
$('body').append( _container );
} else if ( !this.options.stack || isNaN( parseInt(this.options.stack, 10) ) ) {
_container.empty();
}
_container.find('.jq-toast-single:hidden').remove();
_container.append( this._toastEl );
if ( this.options.stack && !isNaN( parseInt( this.options.stack ), 10 ) ) {
var _prevToastCount = _container.find('.jq-toast-single').length,
_extToastCount = _prevToastCount - this.options.stack;
if ( _extToastCount > 0 ) {
$('.jq-toast-wrap').find('.jq-toast-single').slice(0, _extToastCount).remove();
};
}
this._container = _container;
},
canAutoHide: function () {
return ( this.options.hideAfter !== false ) && !isNaN( parseInt( this.options.hideAfter, 10 ) );
},
processLoader: function () {
// Show the loader only, if auto-hide is on and loader is demanded
if (!this.canAutoHide() || this.options.loader === false) {
return false;
}
var loader = this._toastEl.find('.jq-toast-loader');
// 400 is the default time that jquery uses for fade/slide
// Divide by 1000 for milliseconds to seconds conversion
var transitionTime = (this.options.hideAfter - 400) / 1000 + 's';
var loaderBg = this.options.loaderBg;
var style = loader.attr('style') || '';
style = style.substring(0, style.indexOf('-webkit-transition')); // Remove the last transition definition
style += '-webkit-transition: width ' + transitionTime + ' ease-in; \
-o-transition: width ' + transitionTime + ' ease-in; \
transition: width ' + transitionTime + ' ease-in; \
background-color: ' + loaderBg + ';';
loader.attr('style', style).addClass('jq-toast-loaded');
},
animate: function () {
var that = this;
this._toastEl.hide();
this._toastEl.trigger('beforeShow');
if ( this.options.showHideTransition.toLowerCase() === 'fade' ) {
this._toastEl.fadeIn(function ( ){
that._toastEl.trigger('afterShown');
});
} else if ( this.options.showHideTransition.toLowerCase() === 'slide' ) {
this._toastEl.slideDown(function ( ){
that._toastEl.trigger('afterShown');
});
} else {
this._toastEl.show(function ( ){
that._toastEl.trigger('afterShown');
});
}
if (this.canAutoHide()) {
var that = this;
window.setTimeout(function(){
if ( that.options.showHideTransition.toLowerCase() === 'fade' ) {
that._toastEl.trigger('beforeHide');
that._toastEl.fadeOut(function () {
that._toastEl.trigger('afterHidden');
});
} else if ( that.options.showHideTransition.toLowerCase() === 'slide' ) {
that._toastEl.trigger('beforeHide');
that._toastEl.slideUp(function () {
that._toastEl.trigger('afterHidden');
});
} else {
that._toastEl.trigger('beforeHide');
that._toastEl.hide(function () {
that._toastEl.trigger('afterHidden');
});
}
}, this.options.hideAfter);
};
},
reset: function ( resetWhat ) {
if ( resetWhat === 'all' ) {
$('.jq-toast-wrap').remove();
} else {
this._toastEl.remove();
}
},
update: function(options) {
this.prepareOptions(options, this.options);
this.setup();
this.bindToast();
},
close: function() {
this._toastEl.find('.close-jq-toast-single').click();
}
};
$.toast = function(options) {
var toast = Object.create(Toast);
toast.init(options, this);
return {
reset: function ( what ) {
toast.reset( what );
},
update: function( options ) {
toast.update( options );
},
close: function( ) {
toast.close( );
}
}
};
$.toast.options = {
text: '',
heading: '',
showHideTransition: 'fade',
allowToastClose: true,
hideAfter: 3000,
loader: true,
loaderBg: '#9EC600',
stack: 5,
position: 'bottom-left',
bgColor: false,
textColor: false,
textAlign: 'left',
icon: false,
beforeShow: function () {},
afterShown: function () {},
beforeHide: function () {},
afterHidden: function () {},
onClick: function () {}
};
})( jQuery, window, document );
+118 -86
View File
@@ -1,106 +1,138 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
<script src="/static/js/upstream/jquery.min.js"></script>
<!-- <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> -->
<link rel="stylesheet" href="/static/css/upstream/jquery-ui.css">
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> -->
<script src="/static/js/upstream/jquery-ui.min.js"></script>
<!-- <script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script> -->
<script src="/static/js/upstream/socket.io.min.js"></script>
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="/static/js/upstream/jquery-3.7.1.min.js"></script>
<script src="/static/js/upstream/jquery.toast.js"></script>
<script src="/static/js/upstream/socket.io.min.js"></script>
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css"> -->
<link rel="stylesheet" href="/static/css/upstream/semantic.min.css">
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script> -->
<script src="/static/js/upstream/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/upstream/bootstrap.min.css">
<script src="/static/js/upstream/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/main.js"></script>
<script src="/static/js/gps.js"></script>
<script src="/static/js/send-message.js"></script>
<!--
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"
crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
crossorigin="anonymous"></script>
-->
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var latitude = parseFloat('{{ latitude|safe }}');
var longitude = parseFloat('{{ longitude|safe }}');
<link rel="stylesheet" href="/static/css/upstream/google-fonts.css">
<link rel="stylesheet" href="/static/css/upstream/jquery.toast.css">
var memory_chart = null
var message_chart = null
<link rel="stylesheet" href="/static/css/chat.css">
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/main.js"></script>
<script src="/static/js/gps.js"></script>
<script src="/static/js/send-message.js"></script>
$(document).ready(function() {
console.log(initial_stats);
start_update();
init_chat();
reset_Tabs();
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var latitude = parseFloat('{{ latitude|safe }}');
var longitude = parseFloat('{{ longitude|safe }}');
console.log("latitude", latitude);
console.log("longitude", longitude);
var memory_chart = null;
var message_chart = null;
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
// Have to disable the beacon button.
$('#send_beacon').prop('disabled', true);
}
$(document).ready(function() {
console.log(initial_stats);
start_update();
init_chat();
//reset_Tabs();
$("#wipe_local").click(function() {
console.log('Wipe local storage');
localStorage.clear();
});
console.log("latitude", latitude);
console.log("longitude", longitude);
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
// Have to disable the beacon button.
$('#send_beacon').prop('disabled', true);
}
$("#wipe_local").click(function() {
console.log('Wipe local storage');
localStorage.clear();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
</div>
// When a tab is clicked, populate the to_call form field.
$(document).on('shown.bs.tab', 'button[data-bs-toggle="tab"]', function (e) {
var tab = $(e.target);
var callsign = tab.attr("callsign");
var to_call = $('#to_call');
to_call.val(callsign);
selected_tab_callsign = callsign;
});
<div class='ui grid text container'>
<div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div>
/*$('[data-bs-toggle="popover"]').popover(
{html: true, animation: true}
);*/
reload_popovers();
});
</script>
</head>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<div class="ui container">
<h3 class="ui dividing header">Send Message</h3>
<div id="sendMsgDiv" class="ui mini text">
<form id="sendform" name="sendmsg" action="">
<div class="ui corner labeled input">
<label for="to_call" class="ui label">Callsign</label>
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
<div class="ui corner label">
<i class="asterisk icon"></i>
<body>
<div class="wc-container">
<div class="wc-row header">
<div class="container-sm">
<div class="row">
<div class="column">
<h1>APRSD WebChat {{ version }}</h1>
</div>
</div>
<div class="row">
<div class="column">
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
<span id='uptime'>NONE</span>
</div>
</div>
<div class="row">
<form class="row gx-1 gy-1 justify-content-center align-items-center" id="sendform" name="sendmsg" action="" autocomplete="off">
<div class="col-sm-2" style="width:150px;">
<label for="to_call" class="visually-hidden">Callsign</label>
<input type="search" class="form-control mb-2 mr-sm-2" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
</div>
</div>
<div class="ui labeled input">
<label for="message" class="ui label">Message</label>
<input type="text" name="message" id="message" size="40" maxlength="40">
</div>
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
<button type="button" class="ui button" id="wipe_local" value="wipe local storage">Wipe LocalStorage</button>
</form>
<div class="col-auto">
<label for="pkt_path" class="visually-hidden">PATH</label>
<select class="form-control mb-2 mr-sm-2" name="pkt_path" id="pkt_path" style="width:auto;">
<option value="" disabled selected>Default Path</option>
<option value="WIDE1-1">WIDE1-1</option>
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
<option value="ARISS">ARISS</option>
</select>
</div>
<div class="col-sm-3">
<label for="message" class="visually-hidden">Message</label>
<input type="search" class="form-control mb-2 mr-sm-2" name="message" id="message" size="40" maxlength="67" placeholder="Message">
</div>
<div class="col-auto">
<input type="submit" name="submit" class="btn btn-primary mb-2" id="send_msg" value="Send"/>
<button type="button" class="btn btn-primary mb-2" id="send_beacon" value="Send Position">Send Position</button>
<!-- <button type="button" class="btn btn-primary mb-2" id="wipe_local" value="wipe local storage">Wipe LocalStorage</button> -->
</div>
</form>
</div>
</div>
<div class="container-sm" style="max-width: 800px">
<ul class="nav nav-tabs" id="msgsTabList" role="tablist"></ul>
</div>
</div>
<div class="ui grid">
<div class="three wide column">
<div class="tab" id="callsignTabs"></div>
</div>
<div class="ten wide column ui raised segment" id="msgsTabsDiv" style="height:450px;padding:0px;">
<div class="wc-row content" id="wc-content">
<div class="container" style="max-width: 800px;">
<div class="tab-content" id="msgsTabContent">
</div>
</div>
</div>
<div class="ui text container" style="padding-top: 40px">
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
<div class="wc-row footer">
<div class="container-sm" style="padding-top: 40px">
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
</div>
</div>
</body>
</div>
</body>
</html>
-98
View File
@@ -1,98 +0,0 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
<script src="/static/js/upstream/jquery.min.js"></script>
<!-- <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> -->
<link rel="stylesheet" href="/static/css/upstream/jquery-ui.css">
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> -->
<script src="/static/js/upstream/jquery-ui.min.js"></script>
<!-- <script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script> -->
<script src="/static/js/upstream/socket.io.min.js"></script>
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css"> -->
<link rel="stylesheet" href="/static/css/upstream/semantic.min.css">
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script> -->
<script src="/static/js/upstream/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<script src="/static/js/main.js"></script>
<script src="/static/js/gps.js"></script>
<script src="/static/js/send-message-mobile.js"></script>
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var latitude = parseFloat('{{ latitude|safe }}');
var longitude = parseFloat('{{ longitude|safe }}');
var memory_chart = null
var message_chart = null
$(document).ready(function() {
console.log(initial_stats);
start_update();
init_chat();
console.log("latitude", latitude);
console.log("longitude", longitude);
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
// Have to disable the beacon button.
$('#send_beacon').prop('disabled', true);
}
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
</div>
<div class='ui grid text container' style="padding-bottom: 5px;">
<div class='left floated twelve wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<div id="sendMsgDiv" class="ui grid" align="left" style="padding-top: 2px;">
<h3 class="sixteen wide column ui dividing header">Send Message</h3>
<form id="sendform" name="sendmsg" action="">
<div class="sixteen wide column ui left labeled icon input">
<div class="ui label">Callsign</div>
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
<i class="users icon"></i>
</div>
<div class="sixteen wide column ui left labeled icon input" style="padding-bottom: 5px;">
<label for="message" class="ui label">Message</label>
<input type="text" name="message" id="message" maxlength="40" placeholder="Message">
<i class="comment icon"></i>
</div>
<div class="right floated column">
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
</div>
</form>
</div>
<div class="ui grid">
<div class="ui top attached tabular raised menu" id="callsignTabs">
</div>
<div class="sixteen wide column ui bottom attached raised tab segment" id="msgsTabsDiv" style="height:250px;padding:5px;">
&nbsp;
</div>
</div>
<div class="ui text container" style="padding-top: 40px">
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
</div>
</body>
</html>
+57 -45
View File
@@ -1,20 +1,20 @@
import datetime
import importlib.metadata as imp
import io
import json
import logging
from logging.handlers import RotatingFileHandler
import time
import flask
from flask import Flask
from flask.logging import default_handler
from flask_httpauth import HTTPBasicAuth
from oslo_config import cfg
from oslo_config import cfg, generator
import socketio
from werkzeug.security import check_password_hash
import aprsd
from aprsd import cli_helper, client, conf, packets, plugin, threads
from aprsd.log import rich as aprsd_logging
from aprsd.log import log
from aprsd.rpc import client as aprsd_rpc_client
@@ -93,12 +93,19 @@ def _stats():
stats_dict["aprsd"]["watch_list"] = new_list
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
rx = tx = 0
types = {}
if packet_list:
rx = packet_list.total_rx()
tx = packet_list.total_tx()
types_copy = packet_list.types.copy()
for key in types_copy:
types[str(key)] = dict(types_copy[key])
stats_dict["packets"] = {
"sent": tx,
"received": rx,
"types": types,
}
if track:
size_tracker = len(track)
@@ -123,7 +130,6 @@ def stats():
@app.route("/")
def index():
stats = _stats()
LOG.debug(stats)
wl = aprsd_rpc_client.RPCClient().get_watch_list()
if wl and wl.is_enabled():
watch_count = len(wl)
@@ -185,6 +191,7 @@ def index():
watch_age=watch_age,
seen_count=seen_count,
plugin_count=plugin_count,
# oslo_out=generate_oslo()
)
@@ -205,10 +212,12 @@ def get_packets():
LOG.debug("/packets called")
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
if packet_list:
packets = packet_list.get()
tmp_list = []
for pkt in packets:
tmp_list.append(pkt.json)
pkts = packet_list.copy()
for key in pkts:
pkt = packet_list.get(key)
if pkt:
tmp_list.append(pkt.json)
return json.dumps(tmp_list)
else:
@@ -225,6 +234,36 @@ def plugins():
return "reloaded"
def _get_namespaces():
args = []
all = imp.entry_points()
selected = []
if "oslo.config.opts" in all:
for x in all["oslo.config.opts"]:
if x.group == "oslo.config.opts":
selected.append(x)
for entry in selected:
if "aprsd" in entry.name:
args.append("--namespace")
args.append(entry.name)
return args
def generate_oslo():
CONF.namespace = _get_namespaces()
string_out = io.StringIO()
generator.generate(CONF, string_out)
return string_out.getvalue()
@auth.login_required
@app.route("/oslo")
def oslo():
return generate_oslo()
@auth.login_required
@app.route("/save")
def save():
@@ -273,39 +312,6 @@ class LoggingNamespace(socketio.Namespace):
self.log_thread.stop()
def setup_logging(flask_app, loglevel):
global app, LOG
log_level = conf.log.LOG_LEVELS[loglevel]
app.logger.setLevel(log_level)
flask_app.logger.removeHandler(default_handler)
date_format = CONF.logging.date_format
if CONF.logging.rich_logging:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=15,
rich_tracebacks=True, omit_repeated_times=False,
)
rh.setFormatter(log_formatter)
app.logger.addHandler(rh)
log_file = CONF.logging.logfile
if log_file:
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
backupCount=4,
)
fh.setFormatter(log_formatter)
app.logger.addHandler(fh)
LOG = app.logger
def init_app(config_file=None, log_level=None):
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
if not config_file:
@@ -327,7 +333,7 @@ if __name__ == "__main__":
sio = socketio.Server(logger=True, async_mode=async_mode)
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
log_level = init_app(log_level="DEBUG")
setup_logging(app, log_level)
log.setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
app.run(
@@ -348,8 +354,10 @@ if __name__ == "uwsgi_file_aprsd_wsgi":
log_level = init_app(
log_level="DEBUG",
config_file="/config/aprsd.conf",
# Commented out for local development.
# config_file=cli_helper.DEFAULT_CONFIG_FILE
)
setup_logging(app, log_level)
log.setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
@@ -362,7 +370,11 @@ if __name__ == "aprsd.wsgi":
sio = socketio.Server(logger=True, async_mode=async_mode)
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
log_level = init_app(config_file="/config/aprsd.conf", log_level="DEBUG")
setup_logging(app, log_level)
log_level = init_app(
log_level="DEBUG",
config_file="/config/aprsd.conf",
# config_file=cli_helper.DEFAULT_CONFIG_FILE,
)
log.setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
+51 -55
View File
@@ -4,84 +4,80 @@
#
# pip-compile --annotation-style=line dev-requirements.in
#
add-trailing-comma==3.0.1 # via gray
alabaster==0.7.13 # via sphinx
attrs==23.1.0 # via jsonschema, referencing
add-trailing-comma==3.1.0 # via gray
alabaster==0.7.16 # via sphinx
autoflake==1.5.3 # via gray
babel==2.12.1 # via sphinx
black==23.7.0 # via gray
build==0.10.0 # via pip-tools
cachetools==5.3.1 # via tox
certifi==2023.7.22 # via requests
babel==2.14.0 # via sphinx
black==24.2.0 # via gray
build==1.1.1 # via pip-tools
cachetools==5.3.3 # via tox
certifi==2024.2.2 # via requests
cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox
charset-normalizer==3.2.0 # via requests
click==8.1.6 # via black, pip-tools
charset-normalizer==3.3.2 # via requests
click==8.1.7 # via black, fixit, moreorless, pip-tools
colorama==0.4.6 # via tox
commonmark==0.9.1 # via rich
configargparse==1.7 # via gray
coverage[toml]==7.3.0 # via pytest-cov
distlib==0.3.7 # via virtualenv
coverage[toml]==7.4.3 # via pytest-cov
distlib==0.3.8 # via virtualenv
docutils==0.20.1 # via sphinx
exceptiongroup==1.1.3 # via pytest
filelock==3.12.2 # via tox, virtualenv
fixit==0.1.4 # via gray
flake8==6.1.0 # via -r dev-requirements.in, fixit, pep8-naming
gray==0.13.0 # via -r dev-requirements.in
identify==2.5.26 # via pre-commit
idna==3.4 # via requests
exceptiongroup==1.2.0 # via pytest
filelock==3.13.1 # via tox, virtualenv
fixit==2.1.0 # via gray
flake8==7.0.0 # via -r dev-requirements.in, pep8-naming
gray==0.14.0 # via -r dev-requirements.in
identify==2.5.35 # via pre-commit
idna==3.6 # via requests
imagesize==1.4.1 # via sphinx
importlib-resources==6.0.1 # via fixit
iniconfig==2.0.0 # via pytest
isort==5.12.0 # via -r dev-requirements.in, gray
jinja2==3.1.2 # via sphinx
jsonschema==4.19.0 # via fixit
jsonschema-specifications==2023.7.1 # via jsonschema
libcst==1.0.1 # via fixit
markupsafe==2.1.3 # via jinja2
isort==5.13.2 # via -r dev-requirements.in, gray
jinja2==3.1.3 # via sphinx
libcst==1.2.0 # via fixit
markupsafe==2.1.5 # via jinja2
mccabe==0.7.0 # via flake8
mypy==1.5.0 # via -r dev-requirements.in
moreorless==0.4.0 # via fixit
mypy==1.8.0 # via -r dev-requirements.in
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
nodeenv==1.8.0 # via pre-commit
packaging==23.1 # via black, build, pyproject-api, pytest, sphinx, tox
pathspec==0.11.2 # via black
packaging==23.2 # via black, build, fixit, pyproject-api, pytest, sphinx, tox
pathspec==0.12.1 # via black, trailrunner
pep8-naming==0.13.3 # via -r dev-requirements.in
pip-tools==7.3.0 # via -r dev-requirements.in
platformdirs==3.10.0 # via black, tox, virtualenv
pluggy==1.2.0 # via pytest, tox
pre-commit==3.3.3 # via -r dev-requirements.in
pycodestyle==2.11.0 # via flake8
pyflakes==3.1.0 # via autoflake, flake8
pygments==2.16.1 # via rich, sphinx
pyproject-api==1.5.3 # via tox
pyproject-hooks==1.0.0 # via build
pytest==7.4.0 # via -r dev-requirements.in, pytest-cov
pip-tools==7.4.1 # via -r dev-requirements.in
platformdirs==4.2.0 # via black, tox, virtualenv
pluggy==1.4.0 # via pytest, tox
pre-commit==3.6.2 # via -r dev-requirements.in
pycodestyle==2.11.1 # via flake8
pyflakes==3.2.0 # via autoflake, flake8
pygments==2.17.2 # via rich, sphinx
pyproject-api==1.6.1 # via tox
pyproject-hooks==1.0.0 # via build, pip-tools
pytest==8.0.2 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.1.0 # via -r dev-requirements.in
pyupgrade==3.10.1 # via gray
pyyaml==6.0.1 # via fixit, libcst, pre-commit
referencing==0.30.2 # via jsonschema, jsonschema-specifications
pyupgrade==3.15.1 # via gray
pyyaml==6.0.1 # via libcst, pre-commit
requests==2.31.0 # via sphinx
rich==12.6.0 # via gray
rpds-py==0.9.2 # via jsonschema, referencing
snowballstemmer==2.2.0 # via sphinx
sphinx==7.1.2 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
sphinxcontrib-applehelp==1.0.7 # via sphinx
sphinxcontrib-devhelp==1.0.5 # via sphinx
sphinxcontrib-htmlhelp==2.0.4 # via sphinx
sphinx==7.2.6 # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.8 # via sphinx
sphinxcontrib-devhelp==1.0.6 # via sphinx
sphinxcontrib-htmlhelp==2.0.5 # via sphinx
sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==1.0.6 # via sphinx
sphinxcontrib-serializinghtml==1.1.8 # via sphinx
sphinxcontrib-qthelp==1.0.7 # via sphinx
sphinxcontrib-serializinghtml==1.1.10 # via sphinx
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
tox==4.8.0 # via -r dev-requirements.in
typing-extensions==4.7.1 # via libcst, mypy, typing-inspect
tomli==2.0.1 # via black, build, coverage, fixit, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
tox==4.14.0 # via -r dev-requirements.in
trailrunner==1.4.0 # via fixit
typing-extensions==4.10.0 # via black, libcst, mypy, typing-inspect
typing-inspect==0.9.0 # via libcst
unify==0.5 # via gray
untokenize==0.1.1 # via unify
urllib3==2.0.4 # via requests
virtualenv==20.24.3 # via pre-commit, tox
wheel==0.41.1 # via pip-tools
urllib3==2.2.1 # via requests
virtualenv==20.25.1 # via pre-commit, tox
wheel==0.42.0 # via pip-tools
# The following packages are considered to be unsafe in a requirements file:
# pip
+4 -3
View File
@@ -23,7 +23,7 @@ RUN set -ex \
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y git build-essential curl libffi-dev \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo fortune \
# Install dependencies
# Clean up
&& apt-get autoremove -y \
@@ -41,6 +41,7 @@ RUN mkdir /config
RUN chown -R appuser:appgroup /app
RUN chown -R appuser:appgroup /config
USER appuser
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
RUN which aprsd
RUN aprsd sample-config > /config/aprsd.conf
@@ -57,5 +58,5 @@ 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
HEALTHCHECK --interval=1m --timeout=12s --start-period=30s \
CMD aprsd healthcheck --config /config/aprsd.conf --loglevel DEBUG
+2 -1
View File
@@ -20,7 +20,7 @@ RUN set -ex \
# Upgrade the package index and install security upgrades
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y git build-essential curl libffi-dev \
&& apt-get install -y git build-essential curl libffi-dev fortune \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
# Install dependencies
# Clean up
@@ -40,6 +40,7 @@ RUN mkdir /config
RUN chown -R appuser:appgroup /app
RUN chown -R appuser:appgroup /config
USER appuser
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
RUN which aprsd
RUN aprsd sample-config > /config/aprsd.conf
+11
View File
@@ -12,6 +12,17 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
pip3 install --user $plugin
done
fi
if [ ! -z "${APRSD_EXTENSIONS}" ]; then
OLDIFS=$IFS
IFS=','
echo "Installing APRSD extensions from pypi '$APRSD_EXTENSIONS'";
for extension in ${APRSD_EXTENSIONS}; do
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$extension'"
pip3 install --user $extension
done
fi
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
LOG_LEVEL="DEBUG"
+11
View File
@@ -12,6 +12,17 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
pip3 install --user $plugin
done
fi
if [ ! -z "${APRSD_EXTENSIONS}" ]; then
OLDIFS=$IFS
IFS=','
echo "Installing APRSD extensions from pypi '$APRSD_EXTENSIONS'";
for extension in ${APRSD_EXTENSIONS}; do
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$extension'"
pip3 install --user $extension
done
fi
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
LOG_LEVEL="DEBUG"
+12
View File
@@ -13,6 +13,18 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
done
fi
if [ ! -z "${APRSD_EXTENSIONS}" ]; then
OLDIFS=$IFS
IFS=','
echo "Installing APRSD extensions from pypi '$APRSD_EXTENSIONS'";
for extension in ${APRSD_EXTENSIONS}; do
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$extension'"
pip3 install --user $extension
done
fi
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
LOG_LEVEL="DEBUG"
fi
+4 -4
View File
@@ -20,21 +20,21 @@ gevent
eventlet
tabulate
# Pinned due to gray needing 12.6.0
rich==12.6.0
rich~=12.6.0
# For the list-plugins pypi.org search scraping
beautifulsoup4
wrapt
# kiss3 uses attrs
kiss3
attrs
# for mobile checking
user-agents
dataclasses
dacite2
oslo.config
rpyc
rpyc>=6.0.0
# Pin this here so it doesn't require a compile on
# raspi
shellingham
geopy
rush
dataclasses-json
loguru
+53 -45
View File
@@ -5,72 +5,80 @@
# pip-compile --annotation-style=line requirements.in
#
aprslib==0.7.2 # via -r requirements.in
attrs==23.1.0 # via -r requirements.in, ax253, kiss3, rush
attrs==23.2.0 # via -r requirements.in, ax253, kiss3, rush
ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.12.2 # via -r requirements.in
bidict==0.22.1 # via python-socketio
bitarray==2.8.1 # via ax253, kiss3
blinker==1.6.2 # via flask
certifi==2023.7.22 # via requests
charset-normalizer==3.2.0 # via requests
click==8.1.6 # via -r requirements.in, click-completion, click-params, flask
beautifulsoup4==4.12.3 # via -r requirements.in
bidict==0.23.1 # via python-socketio
bitarray==2.9.2 # via ax253, kiss3
blinker==1.7.0 # via flask
certifi==2024.2.2 # via requests
charset-normalizer==3.3.2 # via requests
click==8.1.7 # via -r requirements.in, click-completion, click-params, flask
click-completion==0.5.2 # via -r requirements.in
click-params==0.4.1 # via -r requirements.in
click-params==0.5.0 # via -r requirements.in
commonmark==0.9.1 # via rich
dacite2==2.0.0 # via -r requirements.in
dataclasses==0.6 # via -r requirements.in
debtcollector==2.5.0 # via oslo-config
decorator==5.1.1 # via validators
dnspython==2.4.2 # via eventlet
eventlet==0.33.3 # via -r requirements.in
flask==2.3.2 # via -r requirements.in, flask-httpauth, flask-socketio
dataclasses-json==0.6.4 # via -r requirements.in
debtcollector==3.0.0 # via oslo-config
deprecated==1.2.14 # via click-params
dnspython==2.6.1 # via eventlet
eventlet==0.35.2 # via -r requirements.in
flask==3.0.2 # via -r requirements.in, flask-httpauth, flask-socketio
flask-httpauth==4.8.0 # via -r requirements.in
flask-socketio==5.3.5 # via -r requirements.in
flask-socketio==5.3.6 # via -r requirements.in
geographiclib==2.0 # via geopy
geopy==2.3.0 # via -r requirements.in
gevent==23.7.0 # via -r requirements.in
greenlet==2.0.2 # via eventlet, gevent
idna==3.4 # via requests
imapclient==2.3.1 # via -r requirements.in
importlib-metadata==6.8.0 # via ax253, kiss3
geopy==2.4.1 # via -r requirements.in
gevent==24.2.1 # via -r requirements.in
greenlet==3.0.3 # via eventlet, gevent
h11==0.14.0 # via wsproto
idna==3.6 # via requests
imapclient==3.0.1 # via -r requirements.in
importlib-metadata==7.0.1 # via ax253, kiss3
itsdangerous==2.1.2 # via flask
jinja2==3.1.2 # via click-completion, flask
jinja2==3.1.3 # via click-completion, flask
kiss3==8.0.0 # via -r requirements.in
markupsafe==2.1.3 # via jinja2, werkzeug
netaddr==0.8.0 # via oslo-config
oslo-config==9.1.1 # via -r requirements.in
oslo-i18n==6.0.0 # via oslo-config
pbr==5.11.1 # via -r requirements.in, oslo-i18n, stevedore
pluggy==1.2.0 # via -r requirements.in
loguru==0.7.2 # via -r requirements.in
markupsafe==2.1.5 # via jinja2, werkzeug
marshmallow==3.21.1 # via dataclasses-json
mypy-extensions==1.0.0 # via typing-inspect
netaddr==1.2.1 # via oslo-config
oslo-config==9.4.0 # via -r requirements.in
oslo-i18n==6.3.0 # via oslo-config
packaging==23.2 # via marshmallow
pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore
pluggy==1.4.0 # via -r requirements.in
plumbum==1.8.2 # via rpyc
pygments==2.16.1 # via rich
pygments==2.17.2 # via rich
pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3
python-engineio==4.5.1 # via python-socketio
python-socketio==5.8.0 # via -r requirements.in, flask-socketio
pytz==2023.3 # via -r requirements.in
python-engineio==4.9.0 # via python-socketio
python-socketio==5.11.1 # via -r requirements.in, flask-socketio
pytz==2024.1 # via -r requirements.in
pyyaml==6.0.1 # via -r requirements.in, oslo-config
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
rfc3986==2.0.0 # via oslo-config
rich==12.6.0 # via -r requirements.in
rpyc==5.3.1 # via -r requirements.in
rpyc==6.0.0 # via -r requirements.in
rush==2021.4.0 # via -r requirements.in
shellingham==1.5.0.post1 # via -r requirements.in, click-completion
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
soupsieve==2.4.1 # via beautifulsoup4
stevedore==5.1.0 # via oslo-config
shellingham==1.5.4 # via -r requirements.in, click-completion
simple-websocket==1.0.0 # via python-engineio
six==1.16.0 # via -r requirements.in, click-completion
soupsieve==2.5 # via beautifulsoup4
stevedore==5.2.0 # via oslo-config
tabulate==0.9.0 # via -r requirements.in
thesmuggler==1.0.1 # via -r requirements.in
ua-parser==0.18.0 # via user-agents
typing-extensions==4.10.0 # via typing-inspect
typing-inspect==0.9.0 # via dataclasses-json
update-checker==0.18.0 # via -r requirements.in
urllib3==2.0.4 # via requests
user-agents==2.2.0 # via -r requirements.in
validators==0.20.0 # via click-params
werkzeug==2.3.7 # via -r requirements.in, flask
wrapt==1.15.0 # via -r requirements.in, debtcollector
zipp==3.16.2 # via importlib-metadata
urllib3==2.2.1 # via requests
validators==0.22.0 # via click-params
werkzeug==3.0.1 # via -r requirements.in, flask
wrapt==1.16.0 # via -r requirements.in, debtcollector, deprecated
wsproto==1.2.0 # via simple-websocket
zipp==3.17.0 # via importlib-metadata
zope-event==5.0 # via gevent
zope-interface==6.0 # via gevent
zope-interface==6.2 # via gevent
# The following packages are considered to be unsafe in a requirements file:
# setuptools
+6 -2
View File
@@ -56,17 +56,20 @@ class TestSendMessageCommand(unittest.TestCase):
msg_number=1,
message_format=core.PACKET_TYPE_ACK,
)
mock_queue = mock.MagicMock()
socketio = mock.MagicMock()
wcp = webchat.WebChatProcessPacketThread(packet, socketio)
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
wcp.process_ack_packet(packet)
mock_remove.called_once()
mock_socketio.called_once()
@mock.patch("aprsd.threads.tx.send")
@mock.patch("aprsd.packets.PacketList.rx")
@mock.patch("aprsd.cmds.webchat.socketio")
def test_process_our_message_packet(
self,
mock_tx_send,
mock_packet_add,
mock_socketio,
):
@@ -77,8 +80,9 @@ class TestSendMessageCommand(unittest.TestCase):
msg_number=1,
message_format=core.PACKET_TYPE_MESSAGE,
)
mock_queue = mock.MagicMock()
socketio = mock.MagicMock()
wcp = webchat.WebChatProcessPacketThread(packet, socketio)
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
wcp.process_our_message_packet(packet)
mock_packet_add.called_once()
+1
View File
@@ -78,6 +78,7 @@ python =
3.7: py38, pep8
3.8: py38, pep8
3.9: py39, pep8, type-check, docs
3.10: py39, pep8, type-check, docs
[testenv:fmt]
# This will reformat your code to comply with pep8