kopia lustrzana https://github.com/projecthorus/chasemapper
Porównaj commity
72 Commity
Autor | SHA1 | Data |
---|---|---|
Mark Jessop | 583d141f7e | |
Mark Jessop | e6e14c38de | |
Mark Jessop | 6e3caf005a | |
Mark Jessop | 48e8eae239 | |
Mark Jessop | c7df0982de | |
Mark Jessop | 8aec30178c | |
Mark Jessop | 3b02f751f2 | |
Mark Jessop | c3d371ac39 | |
Steven Honson | d0df0e7ad8 | |
Steven Honson | 90b8e180c6 | |
Steven Honson | 8eec272d7f | |
Steven Honson | 8ec7127a76 | |
Steven Honson | 110b297bf4 | |
Steven Honson | 75c75aee90 | |
Mark Jessop | beb9db5b4e | |
Mark Jessop | 8f4a73245f | |
Mark Jessop | aa2176e518 | |
Mark Jessop | 17ffc42749 | |
Mark Jessop | 1d446ef41d | |
Steven Honson | 2709e7733d | |
Mark Jessop | cee93c90bc | |
Mark Jessop | 54e54855af | |
Steven Honson | 36d9948a22 | |
Steven Honson | 8ddf9feab4 | |
Mark Jessop | 7f4ae413cd | |
Mark Jessop | 11ded492db | |
Mark Jessop | 3b6352f7d5 | |
Steven Honson | fc1e3c7e13 | |
Mark Jessop | 8b27726a90 | |
Mark Jessop | 7356c1a0f3 | |
Mark Jessop | 622766a0c6 | |
Mark Jessop | f69d5b71cf | |
Mark Jessop | e44115a4b9 | |
Mark Jessop | baa3e01313 | |
Mark Jessop | c4b613b648 | |
Mark Jessop | a758375e82 | |
Steven Honson | 21bc0b7a94 | |
Steven Honson | f45a8cf532 | |
Mark Jessop | 2643639182 | |
Mark Jessop | 01d09d2a14 | |
Mark Jessop | a67cc1befa | |
Mark Jessop | 64d289a893 | |
Mark Jessop | 45b44da30a | |
MikeTango | 0ef1d153e4 | |
Mark Jessop | f443a8fb9c | |
Mark Jessop | bd88a3ad13 | |
Mark Jessop | fe7412d4e4 | |
Mark Jessop | 9c78b267df | |
Mark Jessop | e03e6d15e1 | |
Mark Jessop | 25f5510b64 | |
Mark Jessop | 17c2f3f844 | |
Mark Jessop | f2c343eb81 | |
Mark Jessop | 757a129d74 | |
Ferry | 1a116381bc | |
Ferry | d49978f222 | |
Ferry | c55a2f3f49 | |
Mark Jessop | 9258120a52 | |
Mark Jessop | 5f85cc1bcc | |
Mark Jessop | d2dcdf845f | |
Mark Jessop | 847d32b18d | |
Mark Jessop | d3cf470cc8 | |
Steven Honson | 1419250d9f | |
Steven Honson | 8f3af89604 | |
Mark Jessop | 6d10913691 | |
Mark Jessop | b9808e7d0d | |
Mark Jessop | 51698a2c47 | |
Mark Jessop | e01999bd86 | |
Steven Honson | ed4c5a3419 | |
Steven Honson | 90f3b7a0f3 | |
Mark Jessop | 32a96b0b91 | |
Mark Jessop | 19f99d30b2 | |
Mark Jessop | e67cbfb1e0 |
|
@ -4,6 +4,8 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
|
@ -26,34 +28,20 @@ jobs:
|
|||
- name: Setup Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Cache Layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/buildx-cache
|
||||
key: buildx-cache-${{ github.sha }}
|
||||
restore-keys: |
|
||||
buildx-cache-
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Images
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/386, linux/arm64, linux/arm/v6, linux/arm/v7
|
||||
cache-from: type=local,src=/tmp/buildx-cache
|
||||
cache-to: type=local,dest=/tmp/buildx-cache-new,mode=max
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ghcr.io/${{ github.repository }}:latest
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Move Cache
|
||||
run: |
|
||||
rm -rf /tmp/buildx-cache
|
||||
mv /tmp/buildx-cache-new /tmp/buildx-cache
|
||||
|
|
19
Dockerfile
19
Dockerfile
|
@ -1,14 +1,15 @@
|
|||
# -------------------
|
||||
# The build container
|
||||
# -------------------
|
||||
FROM python:3.7-buster AS build
|
||||
FROM python:3.9-bullseye AS build
|
||||
|
||||
# Upgrade base packages.
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y \
|
||||
cmake \
|
||||
libgeos-dev && \
|
||||
libgeos-dev \
|
||||
libatlas-base-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy in requirements.txt.
|
||||
|
@ -16,7 +17,7 @@ COPY requirements.txt /root/chasemapper/requirements.txt
|
|||
|
||||
# Install Python packages.
|
||||
RUN pip3 --no-cache-dir install --user --no-warn-script-location \
|
||||
--extra-index-url https://www.piwheels.org/simple \
|
||||
--ignore-installed --no-binary numpy \
|
||||
-r /root/chasemapper/requirements.txt
|
||||
|
||||
# Copy in chasemapper.
|
||||
|
@ -35,27 +36,29 @@ RUN unzip /root/cusf_predictor_wrapper-master.zip -d /root && \
|
|||
# -------------------------
|
||||
# The application container
|
||||
# -------------------------
|
||||
FROM python:3.7-slim-buster
|
||||
FROM python:3.9-slim-bullseye
|
||||
EXPOSE 5001/tcp
|
||||
|
||||
# Upgrade base packages and install application dependencies.
|
||||
RUN case $(uname -m) in \
|
||||
"armv6l") extra_packages="libatlas3-base libgfortran5" ;; \
|
||||
"armv7l") extra_packages="libatlas3-base libgfortran5" ;; \
|
||||
"armv6l") extra_packages="" ;; \
|
||||
"armv7l") extra_packages="" ;; \
|
||||
esac && \
|
||||
apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y \
|
||||
libeccodes0 \
|
||||
libgeos-c1v5 \
|
||||
libglib2.0 \
|
||||
libglib2.0-0 \
|
||||
libatlas3-base \
|
||||
libgfortran5 \
|
||||
tini && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy any additional Python packages from the build container.
|
||||
COPY --from=build /root/.local /root/.local
|
||||
|
||||
# Copy predictor binary and get_wind_data.py from the build container.
|
||||
# Copy predictor binary from the build container.
|
||||
COPY --from=build /root/cusf_predictor_wrapper-master/src/build/pred \
|
||||
/opt/chasemapper/
|
||||
|
||||
|
|
44
README.md
44
README.md
|
@ -7,19 +7,17 @@ The primary purpose of chasemapper is to provide an easy-to-use mapping interfac
|
|||
|
||||
Chasemapper is intended to be run on a 'headless' machine like a Raspberry Pi and is accessed from a tablet or laptop computer via a web browser. Multiple clients can connect to the server to see what's going on, which is a nice way of keeping passengers entertained ;-)
|
||||
|
||||
It will quite happily run alongside other Project Horus applications such as [radiosonde_auto_rx](https://github.com/projecthorus/radiosonde_auto_rx).
|
||||
It will quite happily run alongside other Project Horus applications such as [radiosonde_auto_rx](https://github.com/projecthorus/radiosonde_auto_rx/wiki), or [horusdemodlib](https://github.com/projecthorus/horusdemodlib/wiki)
|
||||
|
||||
### Contacts
|
||||
* [Mark Jessop](https://github.com/darksidelemm) - vk5qi@rfhead.net
|
||||
|
||||
You can often find me in the #highaltitude IRC Channel on [Freenode](https://webchat.freenode.net/).
|
||||
|
||||
## Update Notes
|
||||
* If you have previously had chasemapper or auto_rx installed, you may need to update flask-socketio to the most recent version. You can do this by running `sudo pip3 install -U flask-socketio`
|
||||
* As of mid-April, there have been additions to the chasemapper configuration file format to enable interfacing with the new [v2 Sondehub Tracker](https://v2.sondehub.org/), including a selection for the online tracker in use. You will need to update your configuration files for chasemapper to continue working. Integration with Sondehub v2 is still in progress.
|
||||
* Chasemapper has now dropped support for the HabHub tracker, which is due to be retired later this year (2022). Chasemapper supports uploading chase-car position information to both [SondeHub](https://tracker.sondehub.org) (for radiosonde chasers), and [SondeHub-Amateur]((https://amateur.sondehub.org)) (for amateur high-altitude balloon launches)
|
||||
|
||||
## Docker Install
|
||||
The fastest way to get chasemapper up and running is to use the pre-built docker container. Information on using this is available here: https://github.com/projecthorus/chasemapper/wiki/Docker
|
||||
The fastest (and most recommended) way to get chasemapper up and running is to use the pre-built docker container. Information on using this is available here: https://github.com/projecthorus/chasemapper/wiki/Docker
|
||||
|
||||
|
||||
## 'Local' Install - Dependencies
|
||||
|
@ -29,7 +27,7 @@ If you are using Docker, you can skip this section.
|
|||
|
||||
On a Raspbian/Ubuntu/Debian system, you can get most of the required dependencies using:
|
||||
```
|
||||
$ sudo apt-get install git python3-numpy python3-requests python3-serial python3-dateutil python3-flask python3-pip
|
||||
$ sudo apt-get install git python3-numpy python3-requests python3-serial python3-dateutil python3-flask python3-pip libatlas3-base libgfortran5 libopenblas-dev
|
||||
```
|
||||
On other OSes the required packages should be named something similar.
|
||||
|
||||
|
@ -47,9 +45,8 @@ $ git clone https://github.com/projecthorus/chasemapper.git
|
|||
To use the map, you need some kind of data to plot on it! The mapping backend accepts telemetry data in a few formats:
|
||||
* 'Payload Summary', 'Chase Car Position' and 'Bearing' messages, via UDP broadcast in a JSON format [described here](https://github.com/projecthorus/horus_utils/wiki/5.-UDP-Broadcast-Messages#payload-summary-payload_summary). The standard ports used for these are 55672 (for hobbyist HAB payloads) and 55673 (radiosondes). These can be generated by:
|
||||
* [radiosonde_auto_rx](https://github.com/projecthorus/radiosonde_auto_rx/wiki) - See [here](https://github.com/projecthorus/radiosonde_auto_rx/wiki/Configuration-Settings#payload-summary-output) for configuration info.
|
||||
* Various 'bridge' utilities within the [horus_utils](https://github.com/projecthorus/horus_utils/wiki) repository. For example, [FldigiBridge](https://github.com/projecthorus/horus_utils/wiki#fldigibridge-processing-of-data-from-dl-fldigi) or [HabitatBridge](https://github.com/projecthorus/horus_utils/wiki#habitat-bridge)
|
||||
* The [horusbinary](https://github.com/projecthorus/horusbinary) 4FSK telemetry decoder will emit these messages on port 55672 by default.
|
||||
* My [Kerberos-SDR fork](https://github.com/darksidelemm/kerberossdr) will emit TDOA bearing information in the appropriate UDP message format on port 55672. Note that the bearing functionality is very much experimental at this time.
|
||||
* The [Horus-GUI](https://github.com/projecthorus/horus-gui) and [Horus Binary](https://github.com/projecthorus/horusdemodlib/wiki) 4FSK telemetry decoders will emit these messages on port 55672 by default.
|
||||
* My [Kraken-SDR fork](https://github.com/darksidelemm/krakensdr_doa) will emit TDOA bearing information in the appropriate UDP message format on port 55672. Note that the bearing functionality is very much experimental at this time.
|
||||
* 'OziMux' messages, via UDP broadcast in a simple CSV format [described here](https://github.com/projecthorus/oziplotter/wiki/3---Data-Sources#3---oziplotter-data-inputs).
|
||||
* [radiosonde_auto_rx](https://github.com/projecthorus/radiosonde_auto_rx/wiki) - See [here](https://github.com/projecthorus/radiosonde_auto_rx/wiki/Configuration-Settings#sending-payload-data-to-chasemapper--oziplotter--ozimux) for configuration info, though I suggest using the 'Payload Summary' message as described above as it provides callsign information.
|
||||
* Pi-in-the-Sky's [lora_gateway](https://github.com/PiInTheSky/lora-gateway) - Using the `OziPort=8942` configuration option.
|
||||
|
@ -77,7 +74,7 @@ The server can be stopped with CTRL+C. Sometimes the server doesn't stop cleanly
|
|||
You should then be able to access the webpage by visiting http://your_ip_here:5001/
|
||||
|
||||
## Live Predictions
|
||||
By default, chasemapper will attempt to request flight-path predictions from the [Tawhiri Predictor API](https://tawhiri.readthedocs.io/en/latest/api.html), which requires an internet connection. If you have a semi-reliable internet connection during the flight, this might be all you need to get chasing!
|
||||
By default, chasemapper will attempt to request flight-path predictions from the SondeHub instance of the [Tawhiri Predictor](https://github.com/projecthorus/tawhiri), which requires an internet connection. If you have a semi-reliable internet connection during the flight, this might be all you need to get chasing!
|
||||
|
||||
However, if you think you might be going out of phone coverage range, you may want to set up offline predictions:
|
||||
|
||||
|
@ -94,30 +91,35 @@ You can then click 'Download Model' in the web interface's setting tab to trigge
|
|||
## Chase Car Positions
|
||||
At the moment Chasemapper supports receiving chase-car positions via either GPSD, a Serial-attached GPS, or Horus UDP messages. Refer to the configuration file for setup information for these options.
|
||||
|
||||
This application can also plot your position onto the tracker.habhub.org map, so others can see when you're out balloon chasing. You can also fetch positions of nearby chase cars from Habitat, to see if others are out chasing as well :-) These options can be enabled from the control pane on the left of the web interface, and can also be set within the configuration file.
|
||||
This application can also plot your position onto the tracker.habhub.org map, so others can see when you're out balloon chasing. You can also fetch positions of nearby chase cars from SondeHub/SondeHub-Amateur, to see if others are out chasing as well :-) These options can be enabled from the control pane on the left of the web interface, and can also be set within the configuration file.
|
||||
|
||||
## Offline Mapping via FoxtrotGPS's Tile Cache
|
||||
(This is a work in progress, but is functional.)
|
||||
Chasemapper can serve up map tiles from a specified directory to the web client. Of course, for this to be useful, we need map tiles to server! [FoxtrotGPS](https://www.foxtrotgps.org/) can help us with this, as it caches map tiles to `~/Maps/`, with one subdirectory per map layer (i.e. `~/Maps/OSM/`, `~/Maps/opencyclemap/`).
|
||||
## Offline Mapping
|
||||
Chasemapper can serve up map tiles from a specified directory to the web client. Of course, for this to be useful, we need map tiles to serve!
|
||||
|
||||
This can be enabled by setting `[offline_maps] tile_server_enabled = True`, and changing `[offline_maps] tile_server_path` to point to your tile cache directory (i.e. `/home/pi/Maps/`). Chasemapper will assume each subdirectory in this folder is a valid map layer and will add them to the map layer list at the top-right of the interface.
|
||||
Serving of local map tiles can be enabled by setting `[offline_maps] tile_server_enabled = True`, and changing `[offline_maps] tile_server_path` to point to your tile cache directory (i.e. `/home/pi/Maps/`). Chasemapper will assume each subdirectory in this folder is a valid map layer (e.g. `~/Maps/OSM/`, `~/Maps/opencyclemap/`). and will add them to the map layer list at the top-right of the interface.
|
||||
|
||||
Note that if you want to use these offline maps within a Docker container, you will need to [modify the tile server path](https://github.com/projecthorus/chasemapper/blob/master/horusmapper.cfg.example#L172) in your configuration file to be /opt/chasemapper/Maps/
|
||||
|
||||
### Caching Maps
|
||||
### Option 1 - FoxtrotGPS's Tile Cache
|
||||
Another option to obtain map tiles is [FoxtrotGPS](https://www.foxtrotgps.org/).
|
||||
|
||||
To grab map tiles to use with this, we're going to use FoxtrotGPS's [Cached Maps](https://www.foxtrotgps.org/doc/foxtrotgps.html#Cached-Maps) feature.
|
||||
To grab map tiles using FoxtrotGPS, we're going to use FoxtrotGPS's [Cached Maps](https://www.foxtrotgps.org/doc/foxtrotgps.html#Cached-Maps) feature.
|
||||
|
||||
* Install FoxtrotGPS (Linux only unfortunately, works OK on a Pi!) either [from source](https://www.foxtrotgps.org/releases/), or via your system package manager (`sudo apt-get install foxtrotgps`).
|
||||
* Load up FoxtrotGPS, and pan around the area you are intersted in caching. Pick the map layer you want, right-click on the map, and choose 'Map download'. You can then select how many zoom levels you want to cache, and start it downloading (this may take a while!)
|
||||
* Once you have a set of folders within your `~/Maps` cache directory, you can startup Chasemapper and start using them! Tiles will be served up as they become available.
|
||||
|
||||
### Option 2 - MapTilesDownloader
|
||||
[MapTilesDownloader](https://github.com/Moll1989/MapTilesDownloader) can be setup on your RPi, allowing access via a web browser to select tile regions. Colin Moll's fork (linked above) includes a systemd service for starting this on boot.
|
||||
|
||||
(If anyone has managed to get ECW support working in GDAL recently, please contact me! I would like to convert some topographic maps in ECW format to tiles for use with Chasemapper.)
|
||||
Note that a number of options in this fork are hard-coded, e.g. the [TCP port](https://github.com/Moll1989/MapTilesDownloader/blob/master/src/server.py#L227) it listens on, and the [list of map tiles](https://github.com/Moll1989/MapTilesDownloader/blob/master/src/UI/main.js#L13).
|
||||
|
||||
This option still needs some work to be usable, as it won't write directly into the `~/Maps/` directory.
|
||||
|
||||
## Running as a Systemd Service
|
||||
Chasemapper can be operated in a 'continuous' mode, running as a systemd service. I use this in my chase car so that I can power up my car Raspberry Pi, and have services like auto_rx and chasemapper running immediately. If you're using docker, this is already sorted out for you, and the docker container will run at startup.
|
||||
Chasemapper can be operated in a 'continuous' mode, running as a systemd service. I use this in my chase car so that I can power up my car Raspberry Pi, and have services like auto_rx and chasemapper running immediately.
|
||||
|
||||
If you're using docker, this is already sorted out for you, and the docker container will run at startup.
|
||||
|
||||
To set this up, the chasemapper.service file must be edited to include your username, and the path to this directory.
|
||||
|
||||
|
@ -188,8 +190,8 @@ The above formats are accepted via a horus_udp listener, and so you must have a
|
|||
|
||||
Bearings are plotted on the map as thin lines, which slowly become transparent as they get older, and then disappear. The style of the line and the maximum age bearings shown can be configured in the new bearing settings tab on the left of the screen (click the compass icon). You can also filter bearings by the optionally supplied confidence level ('Confidence Threshold'). Bearings provided while the chase-car is stationary (i.e. when the heading is essentially unknown) are filtered out of the display by default, but can be enabled if desired ('Show stationary bearings'). Most of the filter settings will only take effect by clicking the 'Redraw Bearings' button.
|
||||
|
||||
My [Kerberos-SDR fork](https://github.com/darksidelemm/kerberossdr) will emit relative bearings in the above format on UDP port 55672, including the raw TDOA data, which is plotted on a polar plot on the bottom-right of the display. Bearing data will be emitted as soon as TDOA processing is started. Note that I have only tested with data from a Uniform Circular Array and do not currently handle forward/reverse ambiguities from a linear array configuration. I would *not* suggest running Chasemapper on the same device as the Kerberos-SDR software, due to the high processor load of the Kerberos algorithms.
|
||||
My [Kraken-SDR fork](https://github.com/darksidelemm/krakensdr_doa) will emit relative bearings in the above format on UDP port 55672, including the raw TDOA data, which is plotted on a polar plot on the bottom-right of the display. Bearing data will be emitted as soon as TDOA processing is started. Note that I have only tested with data from a Uniform Circular Array and do not currently handle forward/reverse ambiguities from a linear array configuration. I would *not* suggest running Chasemapper on the same device as the Kerberos-SDR software, due to the high processor load of the Kerberos algorithms.
|
||||
|
||||
Note that the bearing display (in particular the TDOA data polar plot) does put a fairly big strain on some slower devices. Currently the polar plot is generated in a fairly naive way, and definitely has room for improvement.
|
||||
|
||||
I make no promises as to the usefulness and/or performance of this feature in chasemapper - it's essentially a re-implementation of a radio-direction finding mapping system developed by fellow Amateur Radio Experimenters Group members a very long time ago, and has yet to be used 'in anger'. It's also important to note that attempting to direction-find radiosonde/high-altitude balloon payloads which are located at high relative elevations (>40 degrees or so) is likely to lead to very inaccurate results due to coning angle limitations (where a bearing cannot be resolved due to insufficient phase-delta between receive antennae).
|
||||
I make no promises as to the usefulness and/or performance of this feature in chasemapper - it's essentially a re-implementation of a radio-direction finding mapping system developed by fellow Amateur Radio Experimenters Group members a very long time ago. I've used it in a few local amateur radio direction finding competitions have found it to be useful. It's also important to note that attempting to direction-find radiosonde/high-altitude balloon payloads which are located at high relative elevations (>40 degrees or so) is likely to lead to very inaccurate results due to coning angle limitations (where a bearing cannot be resolved due to insufficient phase-delta between receive antennae).
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
|
||||
# Now using Semantic Versioning (https://semver.org/) MAJOR.MINOR.PATCH
|
||||
|
||||
__version__ = "1.2.2"
|
||||
__version__ = "1.5.2"
|
||||
|
|
|
@ -44,6 +44,8 @@ class Bearings(object):
|
|||
# }
|
||||
self.bearings = {}
|
||||
|
||||
self.bearing_sources = []
|
||||
|
||||
self.bearing_lock = Lock()
|
||||
|
||||
# Internal record of the chase car position, which is updated with incoming GPS data.
|
||||
|
@ -163,10 +165,11 @@ class Bearings(object):
|
|||
# Relative bearing - we need to fuse this with the current car position.
|
||||
|
||||
# Temporary hack for KerberosSDR bearings, which are reflected across N/S
|
||||
if _source == "kerberos-sdr":
|
||||
if _source == "krakensdr_doa":
|
||||
bearing["bearing"] = 360.0 - bearing["bearing"]
|
||||
bearing["raw_doa"] = bearing["raw_doa"][::-1]
|
||||
|
||||
|
||||
_new_bearing = {
|
||||
"timestamp": _arrival_time,
|
||||
"src_timestamp": _src_timestamp,
|
||||
|
@ -183,6 +186,10 @@ class Bearings(object):
|
|||
"source": _source,
|
||||
}
|
||||
|
||||
# Allow override of the heading valid calculations if a hearing_override field is supplied
|
||||
if "heading_override" in bearing:
|
||||
_new_bearing["heading_valid"] = bearing["heading_override"]
|
||||
|
||||
elif bearing["bearing_type"] == "absolute":
|
||||
# Absolute bearing - use the provided data as-is
|
||||
|
||||
|
@ -213,6 +220,10 @@ class Bearings(object):
|
|||
|
||||
self.bearings["%.4f" % _arrival_time] = _new_bearing
|
||||
|
||||
if _source not in self.bearing_sources:
|
||||
self.bearing_sources.append(_source)
|
||||
logging.info(f"Bearing Handler - New source of bearings: {_source}")
|
||||
|
||||
# Now we need to do a clean-up of our bearing list.
|
||||
# At this point, we should always have at least 2 bearings in our store
|
||||
if len(self.bearings) == 1:
|
||||
|
|
|
@ -170,6 +170,18 @@ def parse_config_file(filename):
|
|||
logging.info("Missing Stadia API Key setting, using default (none)")
|
||||
chase_config["stadia_api_key"] = "none"
|
||||
|
||||
try:
|
||||
chase_config["turn_rate_threshold"] = config.getfloat("bearings", "turn_rate_threshold")
|
||||
except:
|
||||
logging.info("Missing turn rate gate setting, using default (4m/s)")
|
||||
chase_config["turn_rate_threshold"] = 4.0
|
||||
|
||||
try:
|
||||
chase_config["ascent_rate_averaging"] = config.getint("predictor", "ascent_rate_averaging")
|
||||
except:
|
||||
logging.info("Missing ascent_rate_averaging setting, using default (10)")
|
||||
chase_config["ascent_rate_averaging"] = 10
|
||||
|
||||
# Telemetry Source Profiles
|
||||
|
||||
_profile_count = config.getint("profile_selection", "profile_count")
|
||||
|
|
|
@ -22,7 +22,7 @@ class GenericTrack(object):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, ascent_averaging=6, landing_rate=5.0, heading_gate_threshold=0.0
|
||||
self, ascent_averaging=6, landing_rate=5.0, heading_gate_threshold=0.0, turn_rate_threshold=4.0
|
||||
):
|
||||
""" Create a GenericTrack Object. """
|
||||
|
||||
|
@ -32,13 +32,23 @@ class GenericTrack(object):
|
|||
self.landing_rate = landing_rate
|
||||
# Heading gate threshold (only gate headings if moving faster than this value in m/s)
|
||||
self.heading_gate_threshold = heading_gate_threshold
|
||||
# Turn rate threshold - only gate headings if turning *slower* than this value in degrees/sec
|
||||
self.turn_rate_threshold = turn_rate_threshold
|
||||
|
||||
self.ascent_rate = 0.0
|
||||
self.heading = 0.0
|
||||
self.turn_rate = 100.0
|
||||
self.heading_valid = False
|
||||
self.speed = 0.0
|
||||
self.is_descending = False
|
||||
|
||||
self.supplied_heading = False
|
||||
self.heading_status = None
|
||||
|
||||
|
||||
self.prev_heading = 0.0
|
||||
self.prev_time = 0.0
|
||||
|
||||
# Internal store of track history data.
|
||||
# Data is stored as a list-of-lists, with elements of [datetime, lat, lon, alt, comment]
|
||||
self.track_history = []
|
||||
|
@ -60,12 +70,23 @@ class GenericTrack(object):
|
|||
_comment = ""
|
||||
|
||||
self.track_history.append([_datetime, _lat, _lon, _alt, _comment])
|
||||
self.update_states()
|
||||
|
||||
# If we have been supplied a 'true' heading with the position, override the state to use that.
|
||||
# In this case we are assuming that the heading is being provided by some form of magnetic compass,
|
||||
# and is valid even when the car is stationary.
|
||||
if "heading" in data_dict:
|
||||
# Rotate heading data if we have enough data
|
||||
if len(self.track_history) >=2:
|
||||
self.prev_time = self.track_history[-2][0]
|
||||
self.prev_heading = self.heading
|
||||
|
||||
self.heading = data_dict["heading"]
|
||||
self.heading_valid = True
|
||||
self.supplied_heading = True
|
||||
|
||||
if "heading_status" in data_dict:
|
||||
self.heading_status = data_dict["heading_status"]
|
||||
|
||||
self.update_states()
|
||||
|
||||
return self.get_latest_state()
|
||||
except:
|
||||
|
@ -88,6 +109,8 @@ class GenericTrack(object):
|
|||
"landing_rate": self.landing_rate,
|
||||
"heading": self.heading,
|
||||
"heading_valid": self.heading_valid,
|
||||
"heading_status": self.heading_status,
|
||||
"turn_rate": self.turn_rate,
|
||||
"speed": self.speed,
|
||||
}
|
||||
return _state
|
||||
|
@ -129,7 +152,18 @@ class GenericTrack(object):
|
|||
"Zero time-step encountered in ascent rate calculation - are multiple receivers reporting telemetry simultaneously?"
|
||||
)
|
||||
continue
|
||||
|
||||
# _mean2_time_delta = (
|
||||
# self.track_history[-1][0] - self.track_history[-1*_num_samples][0]
|
||||
# ).total_seconds()
|
||||
|
||||
# _mean2_altitude_delta = (
|
||||
# self.track_history[-1][3] - self.track_history[-1*_num_samples][3]
|
||||
# )
|
||||
|
||||
# _asc_rate2 = _mean2_altitude_delta / _mean2_time_delta
|
||||
|
||||
#print(f"asc_rates: {_asc_rates}, Mean: {np.mean(_asc_rates)}")
|
||||
return np.mean(_asc_rates)
|
||||
|
||||
def calculate_heading(self):
|
||||
|
@ -139,12 +173,24 @@ class GenericTrack(object):
|
|||
else:
|
||||
_pos_1 = self.track_history[-2]
|
||||
_pos_2 = self.track_history[-1]
|
||||
|
||||
# Save previous heading.
|
||||
self.prev_heading = self.heading
|
||||
self.prev_time = _pos_1[0]
|
||||
|
||||
_pos_info = position_info(
|
||||
(_pos_1[1], _pos_1[2], _pos_1[3]), (_pos_2[1], _pos_2[2], _pos_2[3])
|
||||
)
|
||||
# Calculate new heading
|
||||
try:
|
||||
_pos_info = position_info(
|
||||
(_pos_1[1], _pos_1[2], _pos_1[3]), (_pos_2[1], _pos_2[2], _pos_2[3])
|
||||
)
|
||||
except ValueError:
|
||||
logging.debug("Math Domain Error in heading calculation - Identical Sequential Positions")
|
||||
return self.heading
|
||||
|
||||
self.heading = _pos_info["bearing"]
|
||||
|
||||
return self.heading
|
||||
|
||||
return _pos_info["bearing"]
|
||||
|
||||
def calculate_speed(self):
|
||||
""" Calculate Payload Speed in metres per second """
|
||||
|
@ -157,9 +203,14 @@ class GenericTrack(object):
|
|||
_pos_1 = self.track_history[-2]
|
||||
_pos_2 = self.track_history[-1]
|
||||
|
||||
_pos_info = position_info(
|
||||
(_pos_1[1], _pos_1[2], _pos_1[3]), (_pos_2[1], _pos_2[2], _pos_2[3])
|
||||
)
|
||||
|
||||
try:
|
||||
_pos_info = position_info(
|
||||
(_pos_1[1], _pos_1[2], _pos_1[3]), (_pos_2[1], _pos_2[2], _pos_2[3])
|
||||
)
|
||||
except ValueError:
|
||||
logging.debug("Math Domain Error in speed calculation - Identical Sequential Positions")
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
_speed = _pos_info["great_circle_distance"] / _time_delta
|
||||
|
@ -171,16 +222,49 @@ class GenericTrack(object):
|
|||
|
||||
return _speed
|
||||
|
||||
|
||||
def calculate_turn_rate(self):
|
||||
""" Calculate heading rate based on previous heading and current heading """
|
||||
if len(self.track_history) > 2:
|
||||
# Grab current time
|
||||
_current_time = self.track_history[-1][0]
|
||||
|
||||
_time_delta = (_current_time - self.prev_time).total_seconds()
|
||||
|
||||
_heading_delta = (self.heading - self.prev_heading) % 360.0
|
||||
if _heading_delta >= 180.0:
|
||||
_heading_delta -= 360.0
|
||||
|
||||
self.turn_rate = abs(_heading_delta)/_time_delta
|
||||
|
||||
return self.turn_rate
|
||||
|
||||
|
||||
def update_states(self):
|
||||
""" Update internal states based on the current data """
|
||||
self.ascent_rate = self.calculate_ascent_rate()
|
||||
self.speed = self.calculate_speed()
|
||||
self.heading = self.calculate_heading()
|
||||
|
||||
if self.speed > self.heading_gate_threshold:
|
||||
self.heading_valid = True
|
||||
# If we haven't been supplied a heading, calculate one
|
||||
if not self.supplied_heading:
|
||||
self.heading = self.calculate_heading()
|
||||
|
||||
# Calculate the turn rate
|
||||
self.calculate_turn_rate()
|
||||
|
||||
if self.supplied_heading:
|
||||
# Heading supplied - only threshold on turn rate.
|
||||
if self.turn_rate < self.turn_rate_threshold:
|
||||
self.heading_valid = True
|
||||
else:
|
||||
self.heading_valid = False
|
||||
|
||||
else:
|
||||
self.heading_valid = False
|
||||
# Heading calculated - threshold on speed and turn rate.
|
||||
if (self.speed > self.heading_gate_threshold) and (self.turn_rate < self.turn_rate_threshold):
|
||||
self.heading_valid = True
|
||||
else:
|
||||
self.heading_valid = False
|
||||
|
||||
self.is_descending = self.ascent_rate < 0.0
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ class SerialGPS(object):
|
|||
"""
|
||||
Initialise a SerialGPS object.
|
||||
|
||||
This class assumes the serial-connected GPS outputs GPRMC and GPGGA NMEA strings
|
||||
using 8N1 RS232 framing. It also assumes the GPGGA string is send after GPRMC. If this
|
||||
This class assumes the serial-connected GPS outputs GPRMC or GNRMC and GPGGA or GNGGA NMEA strings
|
||||
using 8N1 RS232 framing. It also assumes the GPGGA or GNGGA string is send after GPRMC or GNRMC. If this
|
||||
is not the case, position data may be up to 1 second out.
|
||||
|
||||
Args:
|
||||
|
@ -57,6 +57,9 @@ class SerialGPS(object):
|
|||
self.callback = callback
|
||||
self.uberdebug = uberdebug
|
||||
|
||||
# Indication of what the last expected string is.
|
||||
self.last_string = "GGA"
|
||||
|
||||
# Current GPS state, in a format which matches the Horus UDP
|
||||
# 'Chase Car Position' message.
|
||||
# Note that these packets do not contain a timestamp.
|
||||
|
@ -66,6 +69,8 @@ class SerialGPS(object):
|
|||
"longitude": 0.0,
|
||||
"altitude": 0.0,
|
||||
"speed": 0.0,
|
||||
"fix_status": 0,
|
||||
"heading": None,
|
||||
"valid": False,
|
||||
}
|
||||
|
||||
|
@ -166,21 +171,24 @@ class SerialGPS(object):
|
|||
# '12319.943281'
|
||||
if not dm or dm == "0":
|
||||
return 0.0
|
||||
|
||||
d, m = re.match(r"^(\d+)(\d\d\.\d+)$", dm).groups()
|
||||
try:
|
||||
d, m = re.match(r"^(\d+)(\d\d\.\d+)$", dm).groups()
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
return float(d) + float(m) / 60
|
||||
|
||||
def parse_nmea(self, data):
|
||||
"""
|
||||
Attempt to parse a line of NMEA data.
|
||||
If we have received a GPGGA string containing a position valid flag,
|
||||
If we have received a GPGGA or GNGGA string containing a position valid flag,
|
||||
send the data on to the callback function.
|
||||
"""
|
||||
if self.uberdebug:
|
||||
print(data.strip())
|
||||
|
||||
if "$GPRMC" in data:
|
||||
logging.debug("SerialGPS - Got GPRMC.")
|
||||
if ("$GPRMC" in data) or ("$GNRMC" in data):
|
||||
logging.debug("SerialGPS - Got GPRMC or GNRMC.")
|
||||
gprmc = data.split(",")
|
||||
gprmc_lat = self.dm_to_sd(gprmc[3])
|
||||
gprmc_latns = gprmc[4]
|
||||
|
@ -200,14 +208,17 @@ class SerialGPS(object):
|
|||
|
||||
self.gps_state["speed"] = gprmc_speed * 0.51444 * 3.6
|
||||
|
||||
elif "$GPGGA" in data:
|
||||
logging.debug("SerialGPS - Got GPGGA.")
|
||||
elif ("$GPGGA" in data) or ("$GNGGA" in data):
|
||||
logging.debug("SerialGPS - Got GPGGA or GNGGA.")
|
||||
gpgga = data.split(",")
|
||||
gpgga_lat = self.dm_to_sd(gpgga[2])
|
||||
gpgga_latns = gpgga[3]
|
||||
gpgga_lon = self.dm_to_sd(gpgga[4])
|
||||
gpgga_lonew = gpgga[5]
|
||||
gpgga_fixstatus = gpgga[6]
|
||||
gpgga_fixstatus = int(gpgga[6])
|
||||
gpgga_numSV = int(gpgga[7])
|
||||
self.gps_state["numSV"] = gpgga_numSV
|
||||
self.gps_state["fix_status"] = gpgga_fixstatus
|
||||
self.gps_state["altitude"] = float(gpgga[9])
|
||||
|
||||
if gpgga_latns == "S":
|
||||
|
@ -224,8 +235,50 @@ class SerialGPS(object):
|
|||
self.gps_state["valid"] = False
|
||||
else:
|
||||
self.gps_state["valid"] = True
|
||||
|
||||
if self.last_string == "GGA":
|
||||
self.send_to_callback()
|
||||
|
||||
elif ("$GPTHS" in data) or ("$GNTHS" in data):
|
||||
# Very basic handling of the uBlox NEO-M8U-provided True heading data.
|
||||
# This data *appears* to be the output of the fused solution, once the system
|
||||
# has self-calibrated.
|
||||
# The GNTHS message can be enabled on the USB port by sending: $PUBX,40,THS,0,0,0,1,0,0*55\r\n
|
||||
# to the GPS.
|
||||
logging.debug("SerialGPS - Got Heading Info (GNTHS).")
|
||||
gnths = data.split(",")
|
||||
try:
|
||||
if len(gnths[1]) > 0:
|
||||
# Data is present in the heading field, try and parse it.
|
||||
gnths_heading = float(gnths[1])
|
||||
# Get the heading validity field.
|
||||
gnths_valid = gnths[2]
|
||||
|
||||
if gnths_valid != "V":
|
||||
# Treat anything other than 'V' as a valid heading
|
||||
self.gps_state["heading"] = gnths_heading
|
||||
else:
|
||||
self.gps_state["heading"] = None
|
||||
else:
|
||||
# Blank field, which means data is not valid.
|
||||
self.gps_state["heading"] = None
|
||||
|
||||
|
||||
# Assume that if we are receiving GNTHS strings, that they are the last in the batch.
|
||||
# Stop sending data when we get a GGA string.
|
||||
self.last_string = "THS"
|
||||
|
||||
# Send to callback if we have lock.
|
||||
if self.gps_state["fix_status"] != 0:
|
||||
self.send_to_callback()
|
||||
|
||||
except:
|
||||
# Failed to parse field, which probably means an invalid heading.
|
||||
logging.debug(f"Failed to parse GNTHS: {data}")
|
||||
# Invalidate the heading data, and revert to emitting messages on GGA strings.
|
||||
self.gps_state["heading"] = None
|
||||
self.last_string = "GGA"
|
||||
|
||||
else:
|
||||
# Discard all other lines
|
||||
pass
|
||||
|
@ -238,6 +291,9 @@ class SerialGPS(object):
|
|||
# Generate a copy of the gps state
|
||||
_state = self.gps_state.copy()
|
||||
|
||||
if _state["heading"] is None:
|
||||
_state.pop("heading")
|
||||
|
||||
# Attempt to pass it onto the callback function.
|
||||
if self.callback != None:
|
||||
try:
|
||||
|
|
|
@ -366,6 +366,8 @@ class GPSDAdaptor(object):
|
|||
_gpsd_socket.watch(gpsd_protocol="json")
|
||||
logging.info("GPSD - Connected to GPSD instance at %s" % self.hostname)
|
||||
|
||||
_old_state = {}
|
||||
|
||||
while self.gpsd_thread_running:
|
||||
# We should be getting GPS data every second.
|
||||
# If this isn't the case, we should close the connection and re-connect.
|
||||
|
@ -398,12 +400,15 @@ class GPSDAdaptor(object):
|
|||
"type": "GPS",
|
||||
"latitude": _TPV["lat"],
|
||||
"longitude": _TPV["lon"],
|
||||
"altitude": _alt,
|
||||
"altitude": _TPV["alt"],
|
||||
"speed": _speed,
|
||||
"valid": True,
|
||||
}
|
||||
|
||||
self.send_to_callback(_gps_state)
|
||||
if _gps_state != _old_state:
|
||||
self.send_to_callback(_gps_state)
|
||||
|
||||
_old_state = _gps_state
|
||||
|
||||
# Close the GPSD connection.
|
||||
try:
|
||||
|
|
|
@ -205,12 +205,21 @@ class HabitatChaseUploader(object):
|
|||
""" Set the callsign """
|
||||
self.callsign = call
|
||||
|
||||
def mark_payload_recovered(self, callsign, latitude, longitude, altitude, message):
|
||||
#def mark_payload_recovered(self, callsign, latitude, longitude, altitude, message):
|
||||
def mark_payload_recovered(self, serial=None, callsign=None, lat=0.0, lon=0.0, alt=0.0, message="", recovered=True):
|
||||
""" Upload an indication that a payload (radiosonde or otherwise) has been recovered """
|
||||
|
||||
if serial is None:
|
||||
return
|
||||
|
||||
if recovered:
|
||||
_call = serial + " recovered by " + callsign
|
||||
else:
|
||||
_call = serial + " not recovered by " + callsign
|
||||
|
||||
try:
|
||||
initListenerCallsign(callsign, radio="", antenna=message)
|
||||
uploadListenerPosition(callsign, latitude, longitude, altitude, chase=False)
|
||||
initListenerCallsign(_call, radio="", antenna=message)
|
||||
uploadListenerPosition(_call, lat, lon, alt, chase=False)
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
"Habitat - Unable to mark payload as recovered - %s" % (str(e))
|
||||
|
|
|
@ -29,6 +29,9 @@ class SondehubChaseUploader(object):
|
|||
""" Upload supplied chase car positions to Sondehub on a regular basis """
|
||||
|
||||
SONDEHUB_STATION_POSITION_URL = "https://api.v2.sondehub.org/listeners"
|
||||
SONDEHUB_STATION_POSITION_URL_AMATEUR = "https://api.v2.sondehub.org/amateur/listeners"
|
||||
SONDEHUB_SONDE_RECOVERED_URL = "https://api.v2.sondehub.org/recovered"
|
||||
SONDEHUB_SONDE_RECOVERED_URL_AMATEUR = "https://api.v2.sondehub.org/amateur/recovered"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -37,6 +40,7 @@ class SondehubChaseUploader(object):
|
|||
upload_enabled=True,
|
||||
upload_timeout=10,
|
||||
upload_retries=2,
|
||||
amateur=False # Upload to amateur DB instead of regular sondehub
|
||||
):
|
||||
""" Initialise the Sondehub Chase uploader, and start the update thread """
|
||||
|
||||
|
@ -46,6 +50,7 @@ class SondehubChaseUploader(object):
|
|||
self.upload_enabled = upload_enabled
|
||||
self.upload_timeout = upload_timeout
|
||||
self.upload_retries = upload_retries
|
||||
self.amateur = amateur
|
||||
|
||||
self.car_position = None
|
||||
self.car_position_lock = Lock()
|
||||
|
@ -54,7 +59,14 @@ class SondehubChaseUploader(object):
|
|||
self.uploader_thread = Thread(target=self.upload_thread)
|
||||
self.uploader_thread.start()
|
||||
|
||||
logging.info("Sondehub - Chase-Car Position Uploader Started")
|
||||
if amateur:
|
||||
self.position_url = self.SONDEHUB_STATION_POSITION_URL_AMATEUR
|
||||
self.recovery_url = self.SONDEHUB_SONDE_RECOVERED_URL_AMATEUR
|
||||
logging.info("Sondehub-Amateur - Chase-Car Position Uploader Started")
|
||||
else:
|
||||
self.position_url = self.SONDEHUB_STATION_POSITION_URL
|
||||
self.recovery_url = self.SONDEHUB_SONDE_RECOVERED_URL
|
||||
logging.info("Sondehub - Chase-Car Position Uploader Started")
|
||||
|
||||
def update_position(self, position):
|
||||
""" Update the chase car position state
|
||||
|
@ -136,7 +148,7 @@ class SondehubChaseUploader(object):
|
|||
"Content-Type": "application/json",
|
||||
}
|
||||
_req = requests.put(
|
||||
self.SONDEHUB_STATION_POSITION_URL,
|
||||
self.position_url,
|
||||
json=_position,
|
||||
# TODO: Revisit this second timeout value.
|
||||
timeout=(self.upload_timeout, 6.1),
|
||||
|
@ -172,10 +184,83 @@ class SondehubChaseUploader(object):
|
|||
)
|
||||
logging.debug(f"Attempted to upload {json.dumps(_position)}")
|
||||
|
||||
def mark_payload_recovered(self, callsign, latitude, longitude, altitude, message):
|
||||
|
||||
def mark_payload_recovered(self, serial=None, callsign=None, lat=0.0, lon=0.0, alt=0.0, message="", recovered=True):
|
||||
""" Upload an indication that a payload (radiosonde or otherwise) has been recovered """
|
||||
# TODO
|
||||
pass
|
||||
|
||||
if serial is None:
|
||||
return
|
||||
|
||||
_doc = {
|
||||
"serial": serial,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"alt": alt,
|
||||
"recovered": recovered,
|
||||
"recovered_by": callsign,
|
||||
"description": message
|
||||
}
|
||||
|
||||
_retries = 0
|
||||
_upload_success = False
|
||||
|
||||
_start_time = time.time()
|
||||
|
||||
while _retries < self.upload_retries:
|
||||
# Run the request.
|
||||
try:
|
||||
headers = {
|
||||
"User-Agent": "chasemapper-" + chasemapper.__version__,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
_req = requests.put(
|
||||
self.recovery_url,
|
||||
json=_doc,
|
||||
# TODO: Revisit this second timeout value.
|
||||
timeout=(self.upload_timeout, 6.1),
|
||||
headers=headers,
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error("Sondehub - Recovery Upload Failed: %s" % str(e))
|
||||
return
|
||||
|
||||
if _req.status_code == 200:
|
||||
# 200 is the only status code that we accept.
|
||||
_upload_time = time.time() - _start_time
|
||||
logging.info("Sondehub - Uploaded recovery notification to Sondehub.")
|
||||
_upload_success = True
|
||||
break
|
||||
|
||||
elif _req.status_code == 400:
|
||||
try:
|
||||
_resp = json.loads(_req.text)
|
||||
logging.info(f"Sondehub - {_resp['message']}")
|
||||
except:
|
||||
logging.info(f"Sondehub - Got code 400 from Sondehub.")
|
||||
|
||||
_upload_success = True
|
||||
break
|
||||
|
||||
elif _req.status_code == 500:
|
||||
# Server Error, Retry.
|
||||
_retries += 1
|
||||
continue
|
||||
|
||||
else:
|
||||
logging.error(
|
||||
"Sondehub - Error uploading recovery notification to Sondehub. Status Code: %d %s."
|
||||
% (_req.status_code, _req.text)
|
||||
)
|
||||
break
|
||||
|
||||
if not _upload_success:
|
||||
logging.error(
|
||||
"Sondehub - Recovery notification upload failed after %d retries"
|
||||
% (_retries)
|
||||
)
|
||||
logging.debug(f"Attempted to upload {json.dumps(_doc)}")
|
||||
|
||||
|
||||
|
||||
def close(self):
|
||||
self.uploader_thread_running = False
|
||||
|
|
|
@ -16,7 +16,7 @@ import subprocess
|
|||
from dateutil.parser import parse
|
||||
from threading import Thread
|
||||
|
||||
TAWHIRI_API_URL = "http://predict.cusf.co.uk/api/v1/"
|
||||
TAWHIRI_API_URL = "http://api.v2.sondehub.org/tawhiri"
|
||||
|
||||
|
||||
def get_tawhiri_prediction(
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#
|
||||
# Copy this file to horusmapper.cfg and modify as required.
|
||||
#
|
||||
# This is the default config file for chasemapper 1.5.0, be aware there might be changes and it might not run on older/newer versions.
|
||||
#
|
||||
|
||||
#
|
||||
# Telemetry Source Profiles
|
||||
|
@ -30,17 +32,23 @@ telemetry_source_port = 55673
|
|||
# Car Position Source
|
||||
# none - No Chase-Car GPS
|
||||
# horus_udp - Read Horus UDP Broadcast 'Car GPS' messages
|
||||
# serial - Read GPS positions from a serial-connected GPS receiver.
|
||||
# gpsd - Poll GPSD for positions.
|
||||
# serial - Read GPS positions from a serial-connected GPS receiver. RECOMMENDED
|
||||
# gpsd - Poll a local GPSD instance for positions. (Known to have some reliability issues...)
|
||||
# station - Stationary position (set in the [map] section below)
|
||||
car_source_type = gpsd
|
||||
car_source_type = serial
|
||||
# Car position source port (UDP) - only used if horus_udp is selected, but still needs to be provided.
|
||||
car_source_port = 12345
|
||||
car_source_port = 55672
|
||||
|
||||
# Online Tracker System
|
||||
# Where to upload chase-car positions and balloon recovery notifications to.
|
||||
# Note - you can only select one of these at a time.
|
||||
#
|
||||
# sondehub = Sondehub v2 Database, for viewing on the SondeHub tracker (https://tracker.sondehub.org)
|
||||
# habitat = Habitat Database, for viewing on the HabHub tracker (https://tracker.habhub.org)
|
||||
# Use this for chasing meteorological radiosondes!
|
||||
#
|
||||
# sondehubamateur = SondeHub Amateur Database, for viewing on the SondeHub-Amateur Tracker (https://amateur.sondehub.org)
|
||||
# Use this when chasing your own flights, and you want to show up on the sondehub-amateur tracker.
|
||||
#
|
||||
online_tracker = sondehub
|
||||
|
||||
# Other profiles can be defined in sections like the following:
|
||||
|
@ -61,8 +69,8 @@ car_source_port = 55672
|
|||
# Online Tracker System
|
||||
# Where to upload chase-car positions and balloon recovery notifications to.
|
||||
# sondehub = Sondehub v2 Database, for viewing on the SondeHub tracker (https://tracker.sondehub.org)
|
||||
# habitat = Habitat Database, for viewing on the HabHub tracker (https://tracker.habhub.org)
|
||||
online_tracker = habitat
|
||||
# sondehubamateur = SondeHub Amateur Database, for viewing on the SondeHub-Amateur Tracker (https://amateur.sondehub.org)
|
||||
online_tracker = sondehubamateur
|
||||
|
||||
# If you want add more profiles, you can do so here, e.g.
|
||||
# [profile_3]
|
||||
|
@ -73,6 +81,7 @@ online_tracker = habitat
|
|||
|
||||
[gpsd]
|
||||
# GPSD Host/Port - Only used if selected in a telemetry profile above.
|
||||
# Note that GPSD support is somewhat buggy.
|
||||
gpsd_host = localhost
|
||||
gpsd_port = 2947
|
||||
|
||||
|
@ -121,10 +130,15 @@ stadia_api_key = none
|
|||
# Enable Predictor (True/False) - This can also be enabled from the web client.
|
||||
predictor_enabled = True
|
||||
|
||||
# Predictor defaults - these can be modified at runtime in the web interface.
|
||||
# Predictor defaults - these can also be modified at runtime in the web interface.
|
||||
default_burst = 30000
|
||||
default_descent_rate = 5.0
|
||||
|
||||
# How many data points to average the payload's ascent rate over.
|
||||
# Note that this is data points, not *seconds*, so you may need to tune this for your payload's
|
||||
# position update rate.
|
||||
# Longer averaging means a smoother ascent rate. ~10 seems ok for a typical Horus Binary payload.
|
||||
ascent_rate_averaging = 10
|
||||
|
||||
# Offline Predictions
|
||||
# Use of the offline predictor requires installing the CUSF Predictor Python Wrapper from here:
|
||||
|
@ -170,23 +184,27 @@ tile_server_enabled = False
|
|||
tile_server_path = /home/pi/Maps/
|
||||
|
||||
# If running chasemapper within a docker container, comment out the above line, and uncomment the following:
|
||||
# tile_server_path = /opt/chasemapper/Maps/
|
||||
#tile_server_path = /opt/chasemapper/Maps/
|
||||
|
||||
#
|
||||
# Habitat / SondeHub Chase-Car Position Upload
|
||||
# If you want, this application can upload your chase-car position to the Habhub / Sondehub trackers,
|
||||
# SondeHub Chase-Car Position Upload
|
||||
# If you want, this application can upload your chase-car position to the SondeHub/SondeHub-Amateur trackers,
|
||||
# for those follwing along at home.
|
||||
# The settings below can be modified from the web interface, but they will default to what is set below on startup.
|
||||
#
|
||||
# Note - Variables in this section still refer to habitat to avoid breaking existing configurations.
|
||||
[habitat]
|
||||
# Enable uploading of chase-car position to Habitat (True / False)
|
||||
# Enable uploading of chase-car position to SondeHub / SondeHub-Amateur (True / False)
|
||||
# Which tracker positions are uploaded to depends on the online_tracker setting of the selected
|
||||
# profile (further up in this config file).
|
||||
habitat_upload_enabled = False
|
||||
|
||||
# Callsign to use when uploading. Note that _chase is automatically appended to this callsign
|
||||
# i.e. N0CALL will show up as N0CALL_chase on tracker.habhub.org
|
||||
# when displayed on the tracker maps.
|
||||
# i.e. N0CALL will show up as N0CALL_chase on sondehub.org
|
||||
habitat_call = N0CALL
|
||||
|
||||
# Attempt to upload position to habitat every x seconds.
|
||||
# Attempt to upload position to SondeHub every x seconds.
|
||||
habitat_update_rate = 30
|
||||
|
||||
|
||||
|
@ -234,17 +252,24 @@ max_bearing_age = 10
|
|||
# Only consider car headings to be valid if the car speed is greater than this value in *kph*
|
||||
car_speed_gate = 10
|
||||
|
||||
# Turn rate threshold
|
||||
# Only plot bearings if the turn rate of the vehicle is less than this value, in degrees/second
|
||||
# This helps avoid plotting bearings when the heading and bearind data might be misaligned during
|
||||
# a turn (e.g. around a roundabout)
|
||||
# 4 degrees/second seems to work fairly well.
|
||||
turn_rate_threshold = 4.0
|
||||
|
||||
# Visual Settings - these can be adjust in the Web GUI during runtime
|
||||
|
||||
# Bearing length in km
|
||||
bearing_length = 10
|
||||
|
||||
# Weight of the bearing lines, in pixels.
|
||||
bearing_weight = 0.5
|
||||
bearing_weight = 1.0
|
||||
|
||||
# Color of the bearings.
|
||||
# Valid options are: red, black, blue, green, custom
|
||||
bearing_color = black
|
||||
# Valid options are: red, black, blue, green, white, custom
|
||||
bearing_color = red
|
||||
|
||||
# Custom bearing color, in hexadecimal #RRGGBB
|
||||
bearing_custom_color = #FF0000
|
||||
|
|
128
horusmapper.py
128
horusmapper.py
|
@ -85,6 +85,7 @@ car_track = GenericTrack()
|
|||
|
||||
# Bearing store
|
||||
bearing_store = None
|
||||
bearing_mode = False # Flag to indicate if we are receiving bearings
|
||||
|
||||
# Habitat/Sondehub Chase-Car uploader object
|
||||
online_uploader = None
|
||||
|
@ -104,6 +105,10 @@ def flask_index():
|
|||
""" Render main index page """
|
||||
return flask.render_template("index.html")
|
||||
|
||||
@app.route("/bearing")
|
||||
def flask_bearing_entry():
|
||||
""" Render bearing entry page """
|
||||
return flask.render_template("bearing_entry.html")
|
||||
|
||||
@app.route("/get_telemetry_archive")
|
||||
def flask_get_telemetry_archive():
|
||||
|
@ -132,11 +137,7 @@ def flask_server_tiles(filename):
|
|||
""" Serve up a file from the tile server location """
|
||||
global map_settings
|
||||
if map_settings["tile_server_enabled"]:
|
||||
_filename = flask.safe_join(map_settings["tile_server_path"], filename)
|
||||
if os.path.isfile(_filename):
|
||||
return flask.send_file(_filename)
|
||||
else:
|
||||
flask.abort(404)
|
||||
return flask.send_from_directory(map_settings["tile_server_path"], filename)
|
||||
else:
|
||||
flask.abort(404)
|
||||
|
||||
|
@ -191,15 +192,20 @@ def client_settings_update(data):
|
|||
chasemapper_config["selected_profile"]
|
||||
]["online_tracker"]
|
||||
if _tracker == "habitat":
|
||||
online_uploader = HabitatChaseUploader(
|
||||
update_rate=chasemapper_config["habitat_update_rate"],
|
||||
callsign=chasemapper_config["habitat_call"],
|
||||
logging.error(
|
||||
"Habitat uploader now deprecated due to Habitat retirement, not starting uploader."
|
||||
)
|
||||
elif _tracker == "sondehub":
|
||||
online_uploader = SondehubChaseUploader(
|
||||
update_rate=chasemapper_config["habitat_update_rate"],
|
||||
callsign=chasemapper_config["habitat_call"],
|
||||
)
|
||||
elif _tracker == "sondehubamateur":
|
||||
online_uploader = SondehubChaseUploader(
|
||||
update_rate=chasemapper_config["habitat_update_rate"],
|
||||
callsign=chasemapper_config["habitat_call"],
|
||||
amateur=True
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
"Unknown Online Tracker %s, not starting uploader." % _tracker
|
||||
|
@ -230,7 +236,7 @@ def handle_new_payload_position(data, log_position=True):
|
|||
|
||||
if _callsign not in current_payloads:
|
||||
# New callsign! Create entries in data stores.
|
||||
current_payload_tracks[_callsign] = GenericTrack()
|
||||
current_payload_tracks[_callsign] = GenericTrack(ascent_averaging=chasemapper_config["ascent_rate_averaging"])
|
||||
|
||||
current_payloads[_callsign] = {
|
||||
"telem": {
|
||||
|
@ -700,6 +706,7 @@ def clear_bearing_data(data):
|
|||
global bearing_store
|
||||
logging.warning("Client requested bearing data be cleared.")
|
||||
bearing_store.flush()
|
||||
flask_emit_event("server_bearings_cleared", {"foo":"bar"})
|
||||
|
||||
|
||||
@socketio.on("mark_recovered", namespace="/chasemapper")
|
||||
|
@ -707,18 +714,26 @@ def mark_payload_recovered(data):
|
|||
""" Mark a payload as recovered, by uploading a station position """
|
||||
global online_uploader
|
||||
|
||||
_callsign = data["recovery_title"]
|
||||
print(data)
|
||||
|
||||
_serial = data["payload_call"]
|
||||
_callsign = data["my_call"]
|
||||
_lat = data["last_pos"][0]
|
||||
_lon = data["last_pos"][1]
|
||||
_alt = data["last_pos"][2]
|
||||
_msg = (
|
||||
data["message"]
|
||||
+ " Recovered at "
|
||||
+ datetime.utcnow().strftime("%Y-%m-%d %H:%MZ")
|
||||
)
|
||||
_msg = data["message"]
|
||||
_recovered = data["recovered"]
|
||||
|
||||
if online_uploader != None:
|
||||
online_uploader.mark_payload_recovered(_callsign, _lat, _lon, _alt, _msg)
|
||||
online_uploader.mark_payload_recovered(
|
||||
serial = _serial,
|
||||
callsign = _callsign,
|
||||
lat = _lat,
|
||||
lon = _lon,
|
||||
alt = _alt,
|
||||
message = _msg,
|
||||
recovered=_recovered
|
||||
)
|
||||
else:
|
||||
logging.error("No Online Tracker enabled, could not mark payload as recovered.")
|
||||
|
||||
|
@ -823,27 +838,47 @@ def udp_listener_car_callback(data):
|
|||
"alt": _alt,
|
||||
"comment": _comment,
|
||||
}
|
||||
# Add in true heading data if we have been supplied it
|
||||
# (Which will be the case once I end up building a better car GPS...)
|
||||
# Add in true heading data if we have been supplied it (e.g. from a uBlox NEO-M8U device)
|
||||
if "heading" in data:
|
||||
_car_position_update["heading"] = data["heading"]
|
||||
|
||||
if "heading_status" in data:
|
||||
_car_position_update["heading_status"] = data["heading_status"]
|
||||
|
||||
|
||||
car_track.add_telemetry(_car_position_update)
|
||||
|
||||
_state = car_track.get_latest_state()
|
||||
_heading = _state["heading"]
|
||||
_heading_status = _state["heading_status"]
|
||||
_heading_valid = _state["heading_valid"]
|
||||
_speed = _state["speed"]
|
||||
|
||||
# Push the new car position to the web client
|
||||
flask_emit_event(
|
||||
"telemetry_event",
|
||||
{
|
||||
|
||||
_car_telem = {
|
||||
"callsign": "CAR",
|
||||
"position": [_lat, _lon, _alt],
|
||||
"vel_v": 0.0,
|
||||
"heading": _heading,
|
||||
"heading_valid": _heading_valid,
|
||||
"heading_status": _heading_status,
|
||||
"speed": _speed,
|
||||
},
|
||||
}
|
||||
|
||||
if 'replay_time' in data:
|
||||
# We are getting data from a log file replay, make sure to pass this on
|
||||
_replay_time = parse(data['replay_time'])
|
||||
_replay_time_str = _replay_time.strftime("%Y-%m-%d %H:%M:%SZ")
|
||||
_car_telem['replay_time'] = _replay_time_str
|
||||
|
||||
# Add in some additional status fields if we have them.
|
||||
if 'numSV' in data:
|
||||
_car_telem['numSV'] = data['numSV']
|
||||
|
||||
# Push the new car position to the web client
|
||||
flask_emit_event(
|
||||
"telemetry_event",
|
||||
_car_telem
|
||||
)
|
||||
|
||||
# Update the Online Position Uploader, if one exists.
|
||||
|
@ -855,21 +890,30 @@ def udp_listener_car_callback(data):
|
|||
bearing_store.update_car_position(_state)
|
||||
|
||||
# Add the car position to the logger, but only if we are moving (>10kph = ~3m/s)
|
||||
if (_speed > 3.0) and chase_logger:
|
||||
# .. or if are receving bearing data, in which case we want to store high resolution position data.
|
||||
if ( (_speed > 3.0) or bearing_mode) and chase_logger:
|
||||
_car_position_update["speed"] = _speed
|
||||
_car_position_update["heading"] = _heading
|
||||
chase_logger.add_car_position(_car_position_update)
|
||||
|
||||
|
||||
def udp_listener_bearing_callback(data):
|
||||
global bearing_store, chase_logger
|
||||
global bearing_store, bearing_mode, chase_logger
|
||||
|
||||
if bearing_store != None:
|
||||
bearing_store.add_bearing(data)
|
||||
bearing_mode = True
|
||||
if chase_logger:
|
||||
chase_logger.add_bearing(data)
|
||||
|
||||
|
||||
|
||||
@socketio.on("add_manual_bearing", namespace="/chasemapper")
|
||||
def add_manual_bearing(data):
|
||||
# Add a user-supplied bearing from the web interface
|
||||
udp_listener_bearing_callback(data)
|
||||
|
||||
|
||||
# Data Age Monitoring Thread
|
||||
data_monitor_thread_running = True
|
||||
|
||||
|
@ -917,7 +961,7 @@ def start_listeners(profile):
|
|||
'telemetry_source_port' (int): Data source port
|
||||
'car_source_type' (str): Car Position source type (none, horus_udp, gpsd, or station)
|
||||
'car_source_port' (int): Car Position source port
|
||||
'online_tracker' (str): Which online tracker to upload chase-car info to ('habitat' or 'sondehub')
|
||||
'online_tracker' (str): Which online tracker to upload chase-car info to ('sondehub' or 'sondehubamateur')
|
||||
"""
|
||||
global data_listeners, current_profile, online_uploader, chasemapper_config
|
||||
|
||||
|
@ -941,15 +985,20 @@ def start_listeners(profile):
|
|||
# Start up a new online uploader immediately if uploading is already enabled.
|
||||
if chasemapper_config["habitat_upload_enabled"] == True:
|
||||
if profile["online_tracker"] == "habitat":
|
||||
online_uploader = HabitatChaseUploader(
|
||||
update_rate=chasemapper_config["habitat_update_rate"],
|
||||
callsign=chasemapper_config["habitat_call"],
|
||||
logging.error(
|
||||
"Habitat uploader now deprecated due to Habitat retirement, not starting uploader."
|
||||
)
|
||||
elif profile["online_tracker"] == "sondehub":
|
||||
online_uploader = SondehubChaseUploader(
|
||||
update_rate=chasemapper_config["habitat_update_rate"],
|
||||
callsign=chasemapper_config["habitat_call"],
|
||||
)
|
||||
elif profile["online_tracker"] == "sondehubamateur":
|
||||
online_uploader = SondehubChaseUploader(
|
||||
update_rate=chasemapper_config["habitat_update_rate"],
|
||||
callsign=chasemapper_config["habitat_call"],
|
||||
amateur=True
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
"Unknown Online Tracker %s, not starting uploader"
|
||||
|
@ -1176,6 +1225,7 @@ if __name__ == "__main__":
|
|||
|
||||
# Set speed gate for car position object
|
||||
car_track.heading_gate_threshold = chasemapper_config["car_speed_gate"]
|
||||
car_track.turn_rate_threshold = chasemapper_config["turn_rate_threshold"]
|
||||
|
||||
# Start listeners using the default profile selection.
|
||||
start_listeners(
|
||||
|
@ -1206,11 +1256,21 @@ if __name__ == "__main__":
|
|||
"Starting Chasemapper Server on: http://%s:%d/"
|
||||
% (chasemapper_config["flask_host"], chasemapper_config["flask_port"])
|
||||
)
|
||||
socketio.run(
|
||||
app,
|
||||
host=chasemapper_config["flask_host"],
|
||||
port=chasemapper_config["flask_port"],
|
||||
)
|
||||
try:
|
||||
socketio.run(
|
||||
app,
|
||||
host=chasemapper_config["flask_host"],
|
||||
port=chasemapper_config["flask_port"],
|
||||
allow_unsafe_werkzeug=True
|
||||
)
|
||||
except TypeError as e:
|
||||
print(e)
|
||||
logging.debug("Not using allow_unsafe_werkzeug argument.")
|
||||
socketio.run(
|
||||
app,
|
||||
host=chasemapper_config["flask_host"],
|
||||
port=chasemapper_config["flask_port"]
|
||||
)
|
||||
|
||||
# Close the predictor and data age monitor threads.
|
||||
predictor_thread_running = False
|
||||
|
|
|
@ -20,7 +20,7 @@ import traceback
|
|||
from dateutil.parser import *
|
||||
|
||||
|
||||
def send_bearing(json_data, udp_port=55672):
|
||||
def send_bearing(json_data, udp_port=55672, hostname='<broadcast>'):
|
||||
"""
|
||||
Grab bearing data out of a json log entry and send it via UDP.
|
||||
|
||||
|
@ -36,16 +36,18 @@ def send_bearing(json_data, udp_port=55672):
|
|||
"type": "BEARING",
|
||||
"log_type": "BEARING"}
|
||||
"""
|
||||
packet = {
|
||||
'type' : 'BEARING',
|
||||
'bearing' : json_data['bearing'],
|
||||
'confidence': json_data['confidence'],
|
||||
'power': json_data['power'],
|
||||
'raw_bearing_angles': json_data['raw_bearing_angles'],
|
||||
'raw_doa': json_data['raw_doa'],
|
||||
'bearing_type': 'relative',
|
||||
'source': 'playback'
|
||||
}
|
||||
# Also get bearings of form:
|
||||
# {"type": "BEARING", "bearing_type": "absolute", "source": "EasyBearing", "latitude": -34.9016115,
|
||||
#"longitude": 138.58986819999998, "bearing": 0, "log_type": "BEARING", "log_time": "2021-12-10T07:33:14.156227+00:00"}
|
||||
|
||||
packet = json_data
|
||||
|
||||
packet['replay_time'] = json_data['log_time']
|
||||
|
||||
if 'kerberos' in json_data['source']:
|
||||
# Log data from the kerberos has been flipped in bearing already. Need to make sure this isn't done twice.
|
||||
packet['source'] = 'replay'
|
||||
|
||||
|
||||
# Set up our UDP socket
|
||||
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
|
||||
|
@ -59,8 +61,11 @@ def send_bearing(json_data, udp_port=55672):
|
|||
pass
|
||||
s.bind(('',udp_port))
|
||||
try:
|
||||
s.sendto(json.dumps(packet).encode('ascii'), ('<broadcast>', udp_port))
|
||||
except socket.error:
|
||||
s.sendto(json.dumps(packet).encode('ascii'), (hostname, udp_port))
|
||||
except socket.error as e:
|
||||
s.sendto(json.dumps(packet).encode('ascii'), ('127.0.0.1', udp_port))
|
||||
|
||||
if hostname != '<broadcast>':
|
||||
s.sendto(json.dumps(packet).encode('ascii'), ('127.0.0.1', udp_port))
|
||||
|
||||
|
||||
|
@ -85,9 +90,13 @@ def send_car_position(json_data, udp_port=55672):
|
|||
'longitude' : json_data['lon'],
|
||||
'altitude': json_data['alt'],
|
||||
'speed': json_data['speed'],
|
||||
'valid': True
|
||||
'valid': True,
|
||||
'replay_time': json_data['log_time']
|
||||
}
|
||||
|
||||
if 'heading' in json_data:
|
||||
packet['heading'] = json_data['heading']
|
||||
|
||||
# Set up our UDP socket
|
||||
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
|
||||
s.settimeout(1)
|
||||
|
@ -126,8 +135,9 @@ def send_balloon_telemetry(json_data, udp_port=55672):
|
|||
'longitude' : json_data['lon'],
|
||||
'altitude': json_data['alt'],
|
||||
'callsign': json_data['callsign'],
|
||||
'time': parse(json_data['time']).strftime("%H:%H:%S"),
|
||||
'comment': "Log Playback"
|
||||
'time': parse(json_data['time']).strftime("%H:%M:%S"),
|
||||
'comment': "Log Playback",
|
||||
'replay_time': json_data['log_time']
|
||||
}
|
||||
|
||||
# Set up our UDP socket
|
||||
|
@ -147,7 +157,7 @@ def send_balloon_telemetry(json_data, udp_port=55672):
|
|||
s.sendto(json.dumps(packet).encode('ascii'), ('127.0.0.1', udp_port))
|
||||
|
||||
|
||||
def playback_json(filename, udp_port=55672, speed=1.0, start_time = 0):
|
||||
def playback_json(filename, udp_port=55672, speed=1.0, start_time = 0, hostname='<broadcast>'):
|
||||
""" Read in a JSON log file and play it back in real-time, or with a speed factor """
|
||||
|
||||
with open(filename, 'r') as _log_file:
|
||||
|
@ -173,21 +183,23 @@ def playback_json(filename, udp_port=55672, speed=1.0, start_time = 0):
|
|||
# Running timer
|
||||
_run_time = (_new_time - _first_time).total_seconds()
|
||||
|
||||
|
||||
if _run_time < start_time:
|
||||
continue
|
||||
|
||||
_time_min = int(_run_time)//60
|
||||
_time_sec = _run_time%60.0
|
||||
|
||||
time.sleep(_time_delta/speed)
|
||||
if (_time_delta < 100):
|
||||
time.sleep(_time_delta/speed)
|
||||
|
||||
if _log_data['log_type'] == 'CAR POSITION':
|
||||
send_car_position(_log_data, udp_port)
|
||||
print("%02d:%.2f - Car Position" % (_time_min, _time_sec))
|
||||
print("%s - %02d:%.2f - Car Position" % (_log_data['log_time'], _time_min, _time_sec))
|
||||
|
||||
elif _log_data['log_type'] == 'BEARING':
|
||||
send_bearing(_log_data, udp_port)
|
||||
print("%02d:%.2f - Bearing Data" % (_time_min, _time_sec))
|
||||
send_bearing(_log_data, udp_port, hostname=hostname)
|
||||
print("%s - %02d:%.2f - Bearing Data" % (_log_data['log_time'], _time_min, _time_sec))
|
||||
|
||||
elif _log_data['log_type'] == 'BALLOON TELEMETRY':
|
||||
send_balloon_telemetry(_log_data, udp_port)
|
||||
|
@ -226,5 +238,5 @@ if __name__ == '__main__':
|
|||
else:
|
||||
print("USAGE: python log_playback.py filename.log <speed_multiplier> <start_time>")
|
||||
|
||||
playback_json(filename, udp_port, speed, start_time)
|
||||
playback_json(filename, udp_port, speed, start_time, hostname=hostname)
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
--no-binary eccodes
|
||||
cusfpredict
|
||||
flask
|
||||
flask-socketio
|
||||
|
@ -6,4 +7,4 @@ numpy
|
|||
python-dateutil
|
||||
pytz
|
||||
requests
|
||||
serial
|
||||
pyserial
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -56,6 +56,14 @@ html, body, #map {
|
|||
width: 10em;
|
||||
}
|
||||
|
||||
.paramEntryLeft {
|
||||
display: inline-block;
|
||||
margin-left: auto;
|
||||
text-align: left;
|
||||
padding-right: 0.2em;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.predictorModelValue {
|
||||
display: inline-block;
|
||||
margin-left: auto;
|
||||
|
@ -74,6 +82,18 @@ html, body, #map {
|
|||
font-size:3em;
|
||||
}
|
||||
|
||||
.logTimeData {
|
||||
color:black;
|
||||
font-weight: bold;
|
||||
font-size:3em;
|
||||
}
|
||||
|
||||
.bearingData {
|
||||
color:red;
|
||||
font-weight: bold;
|
||||
font-size:5em;
|
||||
}
|
||||
|
||||
.dataAgeHeader {
|
||||
color:black;
|
||||
font-weight: bold;
|
||||
|
@ -113,6 +133,19 @@ html, body, #map {
|
|||
line-height:45px;
|
||||
}
|
||||
|
||||
#followCarButton {
|
||||
width:45px;
|
||||
height:45px;
|
||||
font-size:20px;
|
||||
line-height:45px;
|
||||
}
|
||||
|
||||
#bearingCW10Deg, #bearingCW5Deg, #bearingCW1Deg, #bearingCCW10Deg, #bearingCCW5Deg, #bearingCCW1Deg {
|
||||
width:45px;
|
||||
height:45px;
|
||||
font-size:20px;
|
||||
line-height:45px;
|
||||
}
|
||||
|
||||
.custom_label {
|
||||
background: rgba(0, 0, 0, 0) !important;
|
||||
|
@ -148,4 +181,11 @@ html, body, #map {
|
|||
|
||||
}
|
||||
|
||||
.ui-dialog { z-index: 1000 !important ;}
|
||||
.ui-dialog { z-index: 1000 !important ;}
|
||||
|
||||
.form-switch {
|
||||
display: flex !important;
|
||||
margin: 5px;
|
||||
flex-direction: row-reverse !important;
|
||||
justify-content: space-between !important;
|
||||
}
|
|
@ -239,11 +239,23 @@ function handleTelemetry(data){
|
|||
// Update car position.
|
||||
chase_car_position.latest_data = data.position;
|
||||
chase_car_position.heading = data.heading; // degrees true
|
||||
chase_car_position.heading_valid = data.heading_valid;
|
||||
chase_car_position.speed = data.speed; // m/s
|
||||
|
||||
// Update range rings, if they are enabled.
|
||||
recenterRangeRings(data.position);
|
||||
|
||||
// Update Detailed GPS / Heading Info
|
||||
if(data.hasOwnProperty('heading_status')){
|
||||
$("#headingStatus").text(data.heading_status);
|
||||
}
|
||||
|
||||
if(data.hasOwnProperty('numSV')){
|
||||
$("#numSVStatus").text(data.numSV.toFixed(0));
|
||||
}
|
||||
|
||||
//console.log(data);
|
||||
|
||||
// Update Chase Car Speed
|
||||
if (document.getElementById("showCarSpeed").checked){
|
||||
if (chase_config['unitselection'] == "imperial") {
|
||||
|
@ -257,6 +269,20 @@ function handleTelemetry(data){
|
|||
$("#chase_car_speed_header").text("");
|
||||
}
|
||||
|
||||
if(data.hasOwnProperty('replay_time')){
|
||||
// Data is coming from a log file, display the time.
|
||||
$("#log_time").text(data.replay_time);
|
||||
}
|
||||
|
||||
// Update heading information
|
||||
if (document.getElementById("showCarHeading").checked){
|
||||
$("#chase_car_heading").text(chase_car_position.heading.toFixed(0) + "˚");
|
||||
$("#chase_car_heading_header").text("Heading");
|
||||
} else {
|
||||
$("#chase_car_heading").text("");
|
||||
$("#chase_car_heading_header").text("");
|
||||
}
|
||||
|
||||
if (chase_car_position.marker == 'NONE'){
|
||||
// Create marker!
|
||||
chase_car_position.marker = L.marker(chase_car_position.latest_data,{title:"Chase Car", icon: carIcon, rotationOrigin: "center center"})
|
||||
|
@ -270,18 +296,16 @@ function handleTelemetry(data){
|
|||
chase_car_position.path.addLatLng(chase_car_position.latest_data);
|
||||
chase_car_position.marker.setLatLng(chase_car_position.latest_data).update();
|
||||
}
|
||||
// Rotate car icon based on heading, but only if we're going faster than 20kph (5.5m/s).
|
||||
if(chase_car_position.speed > 5.5){ // TODO: Remove magic number!
|
||||
var _car_heading = chase_car_position.heading - 90.0;
|
||||
if (_car_heading<=90.0){
|
||||
chase_car_position.marker.setIcon(carIcon);
|
||||
chase_car_position.marker.setRotationAngle(_car_heading);
|
||||
}else{
|
||||
// We are travelling West - we need to use the flipped car icon.
|
||||
_car_heading = _car_heading - 180.0;
|
||||
chase_car_position.marker.setIcon(carIconFlip);
|
||||
chase_car_position.marker.setRotationAngle(_car_heading);
|
||||
}
|
||||
|
||||
var _car_heading = chase_car_position.heading - 90.0;
|
||||
if (_car_heading<=90.0){
|
||||
chase_car_position.marker.setIcon(carIcon);
|
||||
chase_car_position.marker.setRotationAngle(_car_heading);
|
||||
}else{
|
||||
// We are travelling West - we need to use the flipped car icon.
|
||||
_car_heading = _car_heading - 180.0;
|
||||
chase_car_position.marker.setIcon(carIconFlip);
|
||||
chase_car_position.marker.setRotationAngle(_car_heading);
|
||||
}
|
||||
car_data_age = 0.0;
|
||||
}else{
|
||||
|
|
|
@ -15,12 +15,14 @@
|
|||
|
||||
var bearing_store = {};
|
||||
|
||||
var bearing_sources = [];
|
||||
|
||||
var bearings_on = true;
|
||||
var bearings_only_mode = false;
|
||||
|
||||
|
||||
var bearing_confidence_threshold = 50.0;
|
||||
var bearing_max_age = 20*60.0;
|
||||
var bearing_confidence_threshold = 5.0;
|
||||
var bearing_max_age = 10*60.0;
|
||||
|
||||
var bearing_length = 10000;
|
||||
var bearing_weight = 0.5;
|
||||
|
@ -28,6 +30,8 @@ var bearing_color = "#000000";
|
|||
var bearing_max_opacity = 0.8;
|
||||
var bearing_min_opacity = 0.1;
|
||||
|
||||
var bearing_large_plot = false;
|
||||
|
||||
// Store for the latest server timestamp.
|
||||
// Start out with just our own local timestamp.
|
||||
var latest_server_timestamp = Date.now()/1000.0;
|
||||
|
@ -51,9 +55,11 @@ function updateBearingSettings(){
|
|||
} else if (_bearing_color == "blue"){
|
||||
bearing_color = "#0000FF";
|
||||
} else if (_bearing_color == "green"){
|
||||
bearing__color = "#00FF00";
|
||||
bearing_color = "#00AA00";
|
||||
} else if (_bearing_color == "white"){
|
||||
bearing_color = "#FFFFFF";
|
||||
} else if (_bearing_color == "custom"){
|
||||
bearing_color = _ring_custom_color;
|
||||
bearing_color = _bearing_custom_color;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,6 +69,7 @@ function destroyAllBearings(){
|
|||
});
|
||||
|
||||
bearing_store = {};
|
||||
bearing_sources = [];
|
||||
}
|
||||
|
||||
|
||||
|
@ -82,6 +89,11 @@ function bearingValid(bearing){
|
|||
}
|
||||
}
|
||||
|
||||
// Disable showing of this bearing if the source is not selected
|
||||
if (!document.getElementById("bearing_source_" + bearing.source).checked){
|
||||
_show_bearing = false;
|
||||
}
|
||||
|
||||
return _show_bearing;
|
||||
}
|
||||
|
||||
|
@ -102,6 +114,18 @@ function addBearing(timestamp, bearing, live){
|
|||
|
||||
bearing_store[timestamp] = bearing;
|
||||
|
||||
if ( !bearing_sources.includes(bearing.source)){
|
||||
bearing_sources.push(bearing.source);
|
||||
_new_bearing_div_name = "bearing_source_" + bearing.source;
|
||||
bearing_sources_div = "<div class='paramRow'><b>Source: " + bearing.source + "</b> <input type='checkbox' class='paramSelector' id='"+_new_bearing_div_name+"'></div>";
|
||||
$("#bearing_source_selector").append(bearing_sources_div);
|
||||
$("#"+_new_bearing_div_name).prop('checked',true);
|
||||
|
||||
$("#"+_new_bearing_div_name).change(function(){
|
||||
redrawBearings();
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate the end position.
|
||||
var _end = calculateDestination(L.latLng([bearing_store[timestamp].lat, bearing_store[timestamp].lon]), bearing_store[timestamp].true_bearing, bearing_length);
|
||||
|
||||
|
@ -115,19 +139,25 @@ function addBearing(timestamp, bearing, live){
|
|||
opacity: _opacity
|
||||
});
|
||||
|
||||
|
||||
if ( (bearingValid(bearing_store[timestamp]) == true) && (document.getElementById("bearingsEnabled").checked == true) ){
|
||||
_bearing_valid = bearingValid(bearing_store[timestamp]);
|
||||
if ( (_bearing_valid == true) && (document.getElementById("bearingsEnabled").checked == true) ){
|
||||
bearing_store[timestamp].line.addTo(map);
|
||||
}
|
||||
|
||||
if ( (live == true) && (document.getElementById("bearingsEnabled").checked == true) ){
|
||||
|
||||
if(_raw_bearing_angles.length > 0){
|
||||
$("#bearing_table").tabulator("setData", [{id:1, bearing: bearing_store[timestamp].raw_bearing.toFixed(0), confidence: bearing_store[timestamp].confidence.toFixed(0), power: bearing_store[timestamp].power.toFixed(0)}]);
|
||||
if (bearing_store[timestamp].confidence > bearing_confidence_threshold){
|
||||
_valid_text = "YES";
|
||||
}else {
|
||||
_valid_text = "NO";
|
||||
}
|
||||
$("#bearing_table").tabulator("setData", [{id:1, valid_bearing:_valid_text, bearing: bearing_store[timestamp].raw_bearing.toFixed(0), confidence: bearing_store[timestamp].confidence.toFixed(1), power: bearing_store[timestamp].power.toFixed(0)}]);
|
||||
$("#bearing_table").show();
|
||||
|
||||
if(document.getElementById("tdoaEnabled").checked == true){
|
||||
bearingPlotRender(_raw_bearing_angles, _raw_doa);
|
||||
_valid_tdoa = bearing_store[timestamp].confidence > bearing_confidence_threshold;
|
||||
bearingPlotRender(_raw_bearing_angles, _raw_doa, _valid_tdoa);
|
||||
$('#bearing_plot').show();
|
||||
}else{
|
||||
$('#bearing_plot').hide();
|
||||
|
@ -273,6 +303,8 @@ function toggleBearingsOnlyMode(){
|
|||
$("#summary_table").hide();
|
||||
$("#telem_table_btn").hide();
|
||||
$("#telem_table").hide();
|
||||
$("#payload_age").hide();
|
||||
$("#pred_age").hide();
|
||||
|
||||
bearings_only_mode = true;
|
||||
|
||||
|
@ -283,6 +315,8 @@ function toggleBearingsOnlyMode(){
|
|||
$("#summary_table").show();
|
||||
$("#telem_table_btn").show();
|
||||
$("#telem_table").show();
|
||||
$("#payload_age").show();
|
||||
$("#pred_age").show();
|
||||
|
||||
bearings_only_mode = false;
|
||||
|
||||
|
@ -295,52 +329,84 @@ function flushBearings(){
|
|||
var _confirm = confirm("Really clear all Bearing data?");
|
||||
if (_confirm == true){
|
||||
socket.emit('bearing_store_clear', {data: 'plzkthx'});
|
||||
|
||||
destroyAllBearings();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function bearingPlotRender(angles, doa){
|
||||
|
||||
var _config = {
|
||||
"data": [{
|
||||
"t": angles,// [0,45,90,135,180,215,270,315], // theta values (x axis)
|
||||
"r": doa,//[-4,-3,-2,-1,0,-1,-2,-3,-4], // radial values (y axis)
|
||||
"name": "DOA", // name for the legend
|
||||
"visible": true,
|
||||
"color": "blue", // color of data element
|
||||
"opacity": 0.8,
|
||||
"strokeColor": "blue",
|
||||
"strokeDash": "solid", // solid, dot, dash (default)
|
||||
"strokeSize": 2,
|
||||
"visibleInLegend": false,
|
||||
"geometry": "LinePlot" // AreaChart, BarChart, DotPlot, LinePlot (default)
|
||||
}],
|
||||
"layout": {
|
||||
"height": 250, // (default: 450)
|
||||
"width": 250,
|
||||
"orientation":-90,
|
||||
"showlegend": false,
|
||||
"backgroundColor": "ghostwhite",
|
||||
"radialAxis": {
|
||||
"domain": µ.DATAEXTENT,
|
||||
"visible": true
|
||||
},
|
||||
"margin": {
|
||||
"top": 20,
|
||||
"right": 20,
|
||||
"bottom": 20,
|
||||
"left": 20
|
||||
},
|
||||
}};
|
||||
function bearingPlotRender(angles, doa, data_valid){
|
||||
|
||||
// Trying a colorblind-friendly color scheme.
|
||||
if(data_valid == true){
|
||||
_stroke_color = "#1A85FF";
|
||||
} else {
|
||||
_stroke_color = "#D41159";
|
||||
}
|
||||
|
||||
if(document.getElementById("bigTDOAEnabled").checked){
|
||||
_plot_dim = 400;
|
||||
}else{
|
||||
_plot_dim = 250;
|
||||
}
|
||||
|
||||
if(dark_mode == true){
|
||||
_bg_color = "none";
|
||||
} else {
|
||||
_bg_color = "ghostwhite";
|
||||
}
|
||||
|
||||
var _config = {
|
||||
"data": [{
|
||||
"t": angles,// [0,45,90,135,180,215,270,315], // theta values (x axis)
|
||||
"r": doa,//[-4,-3,-2,-1,0,-1,-2,-3,-4], // radial values (y axis)
|
||||
"name": "DOA", // name for the legend
|
||||
"visible": true,
|
||||
"color": _stroke_color, // color of data element
|
||||
"opacity": 1,
|
||||
"strokeColor": _stroke_color,
|
||||
"strokeDash": "solid", // solid, dot, dash (default)
|
||||
"strokeSize": 2,
|
||||
"visibleInLegend": false,
|
||||
"geometry": "AreaChart" // AreaChart, BarChart, DotPlot, LinePlot (default)
|
||||
}],
|
||||
"layout": {
|
||||
"height": _plot_dim, // (default: 450)
|
||||
"width": _plot_dim,
|
||||
"orientation":-90,
|
||||
"showlegend": false,
|
||||
"backgroundColor": _bg_color, // "ghostwhite",
|
||||
"radialAxis": {
|
||||
"domain": µ.DATAEXTENT,
|
||||
"visible": true
|
||||
},
|
||||
"margin": {
|
||||
"top": 20,
|
||||
"right": 20,
|
||||
"bottom": 20,
|
||||
"left": 20
|
||||
},
|
||||
}};
|
||||
|
||||
micropolar.Axis() // instantiate a new axis
|
||||
.config(_config) // configure it
|
||||
.render(d3.select('#bearing_plot'));
|
||||
}
|
||||
|
||||
function toggle_bearing_plot_size(){
|
||||
if(bearing_large_plot == true){
|
||||
bearing_large_plot = false;
|
||||
}else{
|
||||
bearing_large_plot = true;
|
||||
}
|
||||
|
||||
console.log(bearing_large_plot);
|
||||
};
|
||||
|
||||
// TODO: This is not working
|
||||
$("#bearing_plot").click(toggle_bearing_plot_size);
|
||||
|
||||
/**
|
||||
Returns the point that is a distance and heading away from
|
||||
the given origin point.
|
||||
|
@ -392,3 +458,18 @@ function calculateBearingOpacity(bearing_timestamp){
|
|||
|
||||
}
|
||||
|
||||
|
||||
function manualBearing(){
|
||||
current_bearing = parseFloat($('#bearingManualEntry').val());
|
||||
|
||||
_bearing_info = {
|
||||
'type': 'BEARING',
|
||||
'bearing_type': 'absolute',
|
||||
'source': 'EasyBearing',
|
||||
'latitude': chase_car_position.latest_data[0],
|
||||
'longitude': chase_car_position.latest_data[1],
|
||||
'bearing': current_bearing
|
||||
};
|
||||
|
||||
socket.emit('add_manual_bearing', _bearing_info);
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ function updateRangeRings(){
|
|||
|
||||
}
|
||||
|
||||
function reconfigureCarMarker(profile_name){
|
||||
var reconfigureCarMarker = function(profile_name){
|
||||
// Remove chase-car marker if it exists, and is not used.
|
||||
if( (chase_config.profiles[profile_name].car_source_type === "none") || (chase_config.profiles[profile_name].car_source_type === "station")){
|
||||
if (chase_car_position.marker !== "NONE"){
|
||||
|
|
|
@ -159,22 +159,3 @@ function get_habitat_vehicles(){
|
|||
}
|
||||
|
||||
|
||||
// Show/Hide all vehicles.
|
||||
function show_habitat_vehicles(){
|
||||
var state = document.getElementById("showOtherCars").checked;
|
||||
for (_car in chase_vehicles){
|
||||
// Add to map, if its not already on there.
|
||||
if(state){
|
||||
if(!chase_vehicles[_car].onmap){
|
||||
chase_vehicles[_car].marker.addTo(map);
|
||||
chase_vehicles[_car].onmap = true;
|
||||
}
|
||||
} else{
|
||||
if(chase_vehicles[_car].onmap){
|
||||
chase_vehicles[_car].marker.remove();
|
||||
chase_vehicles[_car].onmap = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -79,6 +79,7 @@ function serverSettingsUpdate(data){
|
|||
|
||||
// Update version
|
||||
$('#chasemapper_version').html(chase_config.version);
|
||||
|
||||
}
|
||||
|
||||
function clientSettingsUpdate(){
|
||||
|
|
|
@ -1,32 +1,308 @@
|
|||
//
|
||||
// Project Horus - Browser-Based Chase Mapper - SondeHub Data Scraping
|
||||
// Project Horus - Browser-Based Chase Mapper - SondeHub Websockets Connection.
|
||||
//
|
||||
// Copyright (C) 2021 Mark Jessop <vk5qi@rfhead.net>
|
||||
// Copyright (C) 2022 Mark Jessop <vk5qi@rfhead.net>
|
||||
// Released under GNU GPL v3 or later
|
||||
//
|
||||
|
||||
|
||||
// URL to scrape recent vehicle position data from.
|
||||
// TODO: Allow adjustment of the number of positions to request.
|
||||
var sondehub_vehicle_url = "https://api.v2.sondehub.org/datanew?type=positions&mode=1hour&chase_only=true&position_id=0";
|
||||
function handleSondeHubWebSocketPacket(data){
|
||||
// Handle a packet of vehicle / listener telemetry from a SondeHub / SondeHub-Amateur Websockets Connection.
|
||||
|
||||
// Only process frames where the 'mobile' flag is present and is true.
|
||||
if (data.hasOwnProperty('mobile')){
|
||||
if(data['mobile'] == true){
|
||||
// We have found a mobile station!
|
||||
//console.log(data);
|
||||
|
||||
// Request the latest 100 vehicle positions from spacenear.us
|
||||
function get_sondehub_vehicles(){
|
||||
// Extract position.
|
||||
var v_lat = parseFloat(data.uploader_position[0]);
|
||||
var v_lon = parseFloat(data.uploader_position[1]);
|
||||
var v_alt = parseFloat(data.uploader_position[2]);
|
||||
var vcallsign = data.uploader_callsign;
|
||||
|
||||
// If the vehicle is already known to us, then update it position.
|
||||
// Update any existing entries (even if the range is above the threshold)
|
||||
if (chase_vehicles.hasOwnProperty(vcallsign)){
|
||||
//console.log("Updating: " + vcallsign);
|
||||
// Update the position ID.
|
||||
chase_vehicles[vcallsign].position_id = data.ts;
|
||||
|
||||
if(!snear_request_running){
|
||||
snear_request_running = true;
|
||||
console.log("Requesting vehicles from Sondehub...")
|
||||
$.ajax({
|
||||
url: sondehub_vehicle_url,
|
||||
dataType: 'json',
|
||||
timeout: 15000,
|
||||
async: true, // Yes, this is deprecated...
|
||||
success: function(data) {
|
||||
process_habitat_vehicles(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Since we don't always get a heading with the vehicle position, calculate it.
|
||||
var old_v_pos = {lat:chase_vehicles[vcallsign].latest_data[0],
|
||||
lon: chase_vehicles[vcallsign].latest_data[1],
|
||||
alt:chase_vehicles[vcallsign].latest_data[2]};
|
||||
var new_v_pos = {lat: v_lat, lon:v_lon, alt:v_alt};
|
||||
chase_vehicles[vcallsign].heading = calculate_lookangles(old_v_pos, new_v_pos).azimuth;
|
||||
|
||||
// Update the position data.
|
||||
chase_vehicles[vcallsign].latest_data = [v_lat, v_lon, v_alt];
|
||||
|
||||
// Update the marker position.
|
||||
chase_vehicles[vcallsign].marker.setLatLng(chase_vehicles[vcallsign].latest_data).update();
|
||||
|
||||
// Rotate/replace the icon to match the bearing.
|
||||
var _car_heading = chase_vehicles[vcallsign].heading - 90.0;
|
||||
if (_car_heading<=90.0){
|
||||
chase_vehicles[vcallsign].marker.setIcon(habitat_car_icons[chase_vehicles[vcallsign].colour]);
|
||||
chase_vehicles[vcallsign].marker.setRotationAngle(_car_heading);
|
||||
}else{
|
||||
// We are travelling West - we need to use the flipped car icon.
|
||||
_car_heading = _car_heading - 180.0;
|
||||
chase_vehicles[vcallsign].marker.setIcon(habitat_car_icons_flipped[chase_vehicles[vcallsign].colour]);
|
||||
chase_vehicles[vcallsign].marker.setRotationAngle(_car_heading);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Otherwise, we need to decide if we're going to add it or not.
|
||||
|
||||
// Is it us?
|
||||
if(vcallsign.startsWith(chase_config.habitat_call)){
|
||||
// Don't add!
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the vehicle distance from our current position.
|
||||
var v_pos = {lat: v_lat, lon:v_lon, alt:v_alt};
|
||||
if (chase_car_position.marker === "NONE"){
|
||||
var my_pos = {lat:chase_config.default_lat, lon:chase_config.default_lon, alt:0};
|
||||
}else{
|
||||
var my_pos = {lat:chase_car_position.latest_data[0], lon:chase_car_position.latest_data[1], alt:chase_car_position.latest_data[2]};
|
||||
}
|
||||
var v_range = calculate_lookangles(my_pos, v_pos).range/1000.0;
|
||||
|
||||
// If the range is less than the threshold, add it to our list of chase vehicles.
|
||||
if(v_range < vehicle_max_range){
|
||||
//console.log("Adding: " + vcallsign);
|
||||
chase_vehicles[vcallsign] = {};
|
||||
// Initialise a few default values
|
||||
chase_vehicles[vcallsign].heading = 90;
|
||||
chase_vehicles[vcallsign].latest_data = [v_lat, v_lon, v_alt];
|
||||
chase_vehicles[vcallsign].position_id = data.ts;
|
||||
|
||||
// Get an index for the car icon. This is incremented for each vehicle,
|
||||
// giving each a different colour.
|
||||
chase_vehicles[vcallsign].colour = car_colour_values[car_colour_idx];
|
||||
car_colour_idx = (car_colour_idx+1)%car_colour_values.length;
|
||||
|
||||
// Create marker
|
||||
chase_vehicles[vcallsign].marker = L.marker(chase_vehicles[vcallsign].latest_data,
|
||||
{title:vcallsign,
|
||||
icon: habitat_car_icons[chase_vehicles[vcallsign].colour],
|
||||
rotationOrigin: "center center"});
|
||||
|
||||
// Add tooltip, with custom CSS which removes all tooltip borders, and adds a text shadow.
|
||||
chase_vehicles[vcallsign].marker.bindTooltip(vcallsign,
|
||||
{permanent: true,
|
||||
direction: 'center',
|
||||
offset:[0,25],
|
||||
className:'custom_label'}).openTooltip();
|
||||
if(document.getElementById("showOtherCars").checked){
|
||||
// Add the car to the map if we have the show other cars button checked.
|
||||
chase_vehicles[vcallsign].marker.addTo(map);
|
||||
// Keep our own record of if this marker has been added to a map,
|
||||
// as we shouldn't be using the private _map property of the marker object.
|
||||
chase_vehicles[vcallsign].onmap = true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function flush_sondehub_vehicles(){
|
||||
for (_car in chase_vehicles){
|
||||
// Remove from map if present.
|
||||
if(chase_vehicles[_car].onmap){
|
||||
chase_vehicles[_car].marker.remove();
|
||||
chase_vehicles[_car].onmap = false;
|
||||
}
|
||||
delete chase_vehicles[_car];
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// SondeHub Websockets connection.
|
||||
//
|
||||
var livedata = "wss://ws-reader.v2.sondehub.org/";
|
||||
var clientID = "ChaseMapper-" + Math.floor(Math.random() * 10000000000);
|
||||
var client;
|
||||
var clientConnected = false;
|
||||
var clientActive = false;
|
||||
var clientTopic;
|
||||
|
||||
function onConnect() {
|
||||
if (chase_config.profiles[chase_config.selected_profile].online_tracker === "sondehub") {
|
||||
var topic = "listener/#";
|
||||
client.subscribe(topic);
|
||||
clientTopic = topic;
|
||||
} else if (chase_config.profiles[chase_config.selected_profile].online_tracker === "sondehubamateur") {
|
||||
var topic = "amateur-listener/#";
|
||||
client.subscribe(topic);
|
||||
clientTopic = topic;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
clientConnected = true;
|
||||
clientActive = true;
|
||||
console.log("SondeHub Websockets Connected - Subscribed to " + clientTopic);
|
||||
};
|
||||
|
||||
function connectionError(error) {
|
||||
clientConnected = false;
|
||||
clientActive = false;
|
||||
console.log("SondeHub Websockets Connection Error");
|
||||
};
|
||||
|
||||
function onConnectionLost(responseObject) {
|
||||
if (responseObject.errorCode !== 0) {
|
||||
clientConnected = false;
|
||||
clientActive = false;
|
||||
console.log("SondeHub Websockets Connection Lost");
|
||||
}
|
||||
};
|
||||
|
||||
function onMessageArrived(message) {
|
||||
try {
|
||||
if (clientActive) {
|
||||
var frame = JSON.parse(message.payloadString.toString());
|
||||
handleSondeHubWebSocketPacket(frame);
|
||||
}
|
||||
}
|
||||
catch(err) {}
|
||||
};
|
||||
|
||||
function startSondeHubWebsockets() {
|
||||
if(document.getElementById("showOtherCars").checked){
|
||||
// Clear off any vehicles on the map.
|
||||
flush_sondehub_vehicles();
|
||||
|
||||
if(clientConnected == false){
|
||||
// Not connected yet. Start a new connection.
|
||||
client = new Paho.Client(livedata, clientID);
|
||||
client.onConnectionLost = onConnectionLost;
|
||||
client.onMessageArrived = onMessageArrived;
|
||||
client.connect({onSuccess:onConnect,onFailure:connectionError,reconnect:true});
|
||||
} else {
|
||||
// Already connected, un-sub and re-sub to the correct topic.
|
||||
client.unsubscribe(clientTopic);
|
||||
onConnect();
|
||||
}
|
||||
} else {
|
||||
if(clientConnected || (client != null)){
|
||||
client.disconnect();
|
||||
clientConnected = false;
|
||||
console.log("SondeHub Websockets Disconnected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show/Hide all vehicles.
|
||||
function show_sondehub_vehicles(){
|
||||
var state = document.getElementById("showOtherCars").checked;
|
||||
|
||||
for (_car in chase_vehicles){
|
||||
// Add to map, if its not already on there.
|
||||
if(state){
|
||||
if(!chase_vehicles[_car].onmap){
|
||||
chase_vehicles[_car].marker.addTo(map);
|
||||
chase_vehicles[_car].onmap = true;
|
||||
}
|
||||
} else{
|
||||
if(chase_vehicles[_car].onmap){
|
||||
chase_vehicles[_car].marker.remove();
|
||||
chase_vehicles[_car].onmap = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-connect to websockets if necessary.
|
||||
startSondeHubWebsockets();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Habitat ChaseCar lib (copied from SondeHub Tracker)
|
||||
* Uploads geolocation for chase cars to habitat
|
||||
*
|
||||
* Author: Rossen Gerogiev / Mark Jessop
|
||||
* Requires: jQuery
|
||||
*
|
||||
* Updated to SondeHub v2 by Mark Jessop
|
||||
*/
|
||||
|
||||
ChaseCar = {
|
||||
db_uri: "https://api.v2.sondehub.org/listeners", // Sondehub API
|
||||
recovery_uri: "https://api.v2.sondehub.org/recovered",
|
||||
};
|
||||
|
||||
// Updated SondeHub position upload function.
|
||||
// Refer PUT listeners API here: https://generator.swagger.io/?url=https://raw.githubusercontent.com/projecthorus/sondehub-infra/main/swagger.yaml
|
||||
// @callsign string
|
||||
// @position object (geolocation position object)
|
||||
ChaseCar.updatePosition = function(callsign, position) {
|
||||
if(!position || !position.coords) return;
|
||||
|
||||
// Set altitude to zero if not provided.
|
||||
_position_alt = ((!!position.coords.altitude) ? position.coords.altitude : 0);
|
||||
|
||||
var _doc = {
|
||||
"software_name": "SondeHub Tracker",
|
||||
"software_version": "{VER}",
|
||||
"uploader_callsign": callsign,
|
||||
"uploader_position": [position.coords.latitude, position.coords.longitude, _position_alt],
|
||||
"uploader_antenna": "Mobile Station",
|
||||
"uploader_contact_email": "none@none.com",
|
||||
"mobile": true
|
||||
};
|
||||
|
||||
// push the doc to sondehub
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: ChaseCar.db_uri,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(_doc),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
ChaseCar.markRecovered = function(serial, lat, lon, recovered, callsign, notes){
|
||||
|
||||
var _doc = {
|
||||
"serial": serial,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"alt": 0.0,
|
||||
"recovered": recovered,
|
||||
"recovered_by": callsign,
|
||||
"description": notes
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: ChaseCar.recovery_uri,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(_doc),
|
||||
}).done(function(data) {
|
||||
console.log(data);
|
||||
alert("Recovery Reported OK!");
|
||||
})
|
||||
.fail(function(jqXHR, textStatus, error) {
|
||||
try {
|
||||
_fail_resp = JSON.parse(jqXHR.responseText);
|
||||
alert("Error Submitting Recovery Report: " + _fail_resp.message);
|
||||
} catch(err) {
|
||||
alert("Error Submitting Recovery Report.");
|
||||
}
|
||||
})
|
||||
|
||||
}
|
|
@ -40,7 +40,8 @@ function markPayloadRecovered(callsign){
|
|||
var _recovery_data = {
|
||||
my_call: chase_config.habitat_call,
|
||||
payload_call: callsign,
|
||||
recovery_title: callsign + " recovered by " + chase_config.habitat_call,
|
||||
recovered: $("#recoverySuccessful").is(':checked'),
|
||||
recovery_title: callsign,
|
||||
last_pos: balloon_positions[callsign].latest_data.position,
|
||||
message: ""
|
||||
};
|
||||
|
@ -49,6 +50,13 @@ function markPayloadRecovered(callsign){
|
|||
$('#customRecoveryTitle').val(_recovery_data.recovery_title);
|
||||
$('#recoveryPosition').html(_recovery_data.last_pos[0].toFixed(5) + ", " + _recovery_data.last_pos[1].toFixed(5));
|
||||
|
||||
if (chase_config.profiles[chase_config.selected_profile].online_tracker === "sondehub"){
|
||||
// Only allow the serial number for sondehub uploads
|
||||
$('#customRecoveryTitle').prop('disabled', true);
|
||||
} else {
|
||||
$('#customRecoveryTitle').prop('disabled', false);
|
||||
}
|
||||
|
||||
// Pop up a dialog box so the user can enter a custom message if they want.
|
||||
var divObj = $('#mark-recovered-dialog');
|
||||
divObj.dialog({
|
||||
|
@ -64,13 +72,21 @@ function markPayloadRecovered(callsign){
|
|||
$( this ).dialog( "close" );
|
||||
_recovery_data.message = $('#customRecoveryMessage').val();
|
||||
_recovery_data.recovery_title = $('#customRecoveryTitle').val();
|
||||
_recovery_data.recovered = $("#recoverySuccessful").is(':checked');
|
||||
|
||||
// If the user has requested to use the chase car position, override the last position with it.
|
||||
if(document.getElementById("recoveryCarPosition").checked == true){
|
||||
_recovery_data.last_pos = chase_car_position.latest_data;
|
||||
}
|
||||
|
||||
socket.emit('mark_recovered', _recovery_data);
|
||||
if (chase_config.profiles[chase_config.selected_profile].online_tracker === "sondehub"){
|
||||
// For sondehub recoveries, do the request in-browser.
|
||||
ChaseCar.markRecovered(_recovery_data.payload_call, _recovery_data.last_pos[0], _recovery_data.last_pos[1], _recovery_data.recovered, _recovery_data.my_call, _recovery_data.message);
|
||||
} else {
|
||||
// Habitat 'recoveries' are a bit more involved, so do these in the backend.
|
||||
socket.emit('mark_recovered', _recovery_data);
|
||||
}
|
||||
|
||||
},
|
||||
Cancel: function() {
|
||||
$( this ).dialog( "close" );
|
||||
|
@ -168,6 +184,7 @@ function initTables(){
|
|||
{title:"Longitude", field:"lon", headerSort:false},
|
||||
{title:"Alt (m)", field:"alt", headerSort:false},
|
||||
{title:"V_rate (m/s)", field:"vel_v", headerSort:false},
|
||||
{title:"SVs", field:'sats', headerSort:false, visible:false},
|
||||
{title:"SNR", field:'snr', headerSort:false, visible:false},
|
||||
{title:"Aux", field:'aux', headerSort:false, visible:false}
|
||||
],
|
||||
|
@ -202,11 +219,12 @@ function initTables(){
|
|||
layoutColumnsOnNewData:true,
|
||||
//selectable:1, // TODO...
|
||||
columns:[ //Define Table Columns
|
||||
{title:"Valid", field:'valid_bearing', headerSort:false},
|
||||
{title:"Bearing", field:"bearing", headerSort:false},
|
||||
{title:"Score", field:'confidence', headerSort:false},
|
||||
{title:"Power", field:'power', headerSort:false}
|
||||
],
|
||||
data:[{id: 1, bearing:0.0, confidence:0.0}]
|
||||
data:[{id: 1, valid_bearing:"NO", bearing:0.0, confidence:0.0, power:0.0}]
|
||||
});
|
||||
|
||||
$("#bearing_table").hide();
|
||||
|
@ -226,6 +244,7 @@ function initTablesImperial(){
|
|||
{title:"Longitude", field:"lon", headerSort:false},
|
||||
{title:"Alt (ft)", field:"alt", headerSort:false},
|
||||
{title:"V_rate (ft/min)", field:"vel_v", headerSort:false},
|
||||
{title:"SVs", field:'sats', headerSort:false, visible:false},
|
||||
{title:"SNR", field:'snr', headerSort:false, visible:false},
|
||||
{title:"Aux", field:'aux', headerSort:false, visible:false}
|
||||
],
|
||||
|
@ -260,11 +279,12 @@ function initTablesImperial(){
|
|||
layoutColumnsOnNewData:true,
|
||||
//selectable:1, // TODO...
|
||||
columns:[ //Define Table Columns
|
||||
{title:"Valid", field:'valid_bearing', headerSort:false},
|
||||
{title:"Bearing", field:"bearing", headerSort:false},
|
||||
{title:"Score", field:'confidence', headerSort:false},
|
||||
{title:"Power", field:'power', headerSort:false}
|
||||
],
|
||||
data:[{id: 1, bearing:0.0, confidence:0.0}]
|
||||
data:[{id: 1, valid_bearing:"NO", bearing:0.0, confidence:0.0, power:0.0}]
|
||||
});
|
||||
|
||||
$("#bearing_table").hide();
|
||||
|
@ -305,6 +325,11 @@ function updateTelemetryTable(){
|
|||
}
|
||||
}
|
||||
|
||||
if (balloon_call_data.hasOwnProperty('sats')){
|
||||
balloon_call_data.sats = balloon_call_data.sats.toFixed(0);
|
||||
$("#telem_table").tabulator("showColumn", "sats");
|
||||
}
|
||||
|
||||
// Update table
|
||||
telem_data.push(balloon_call_data);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,331 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Project Horus Chase Mapper</title>
|
||||
|
||||
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/leaflet.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/leaflet-sidebar.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/leaflet-control-topcenter.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/Leaflet.PolylineMeasure.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/leaflet-routing-machine.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/easy-button.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/tabulator_simple.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/jquery-ui.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/chasemapper.css') }}" rel="stylesheet">
|
||||
|
||||
|
||||
<!-- I should probably feel bad for using so many libraries, but apparently this is the way thing are done :-/ -->
|
||||
<script src="{{ url_for('static', filename='js/jquery-3.3.1.min.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='js/jquery-ui.min.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='js/socket.io.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/leaflet.js') }}"></script>
|
||||
<!-- Leaflet plugins... -->
|
||||
<script src="{{ url_for('static', filename='js/leaflet.rotatedMarker.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/leaflet-control-topcenter.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/leaflet-sidebar.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/Leaflet.Control.Custom.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/Leaflet.PolylineMeasure.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/easy-button.js') }}"></script>
|
||||
|
||||
|
||||
|
||||
<!-- Custom scripts -->
|
||||
|
||||
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
|
||||
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
|
||||
|
||||
// Chase car position.
|
||||
// properties will contain:
|
||||
// latest_data: [lat,lon, alt] (latest car position)
|
||||
// heading: Car heading (to point icon appropriately.)
|
||||
// marker: Leaflet marker
|
||||
var chase_car_position = {latest_data: [0.0,0.0,0.0], heading:0, marker: 'NONE', path: 'NONE'};
|
||||
|
||||
var current_bearing = 0;
|
||||
|
||||
var bearing_length = 20000;
|
||||
|
||||
// Leaflet map instance.
|
||||
var map;
|
||||
|
||||
|
||||
|
||||
// Socket.IO Settings
|
||||
var namespace = '/chasemapper';
|
||||
// Socket.IO instance.
|
||||
var socket;
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
// Connect to the Socket.IO server.
|
||||
// The connection URL has the following format:
|
||||
// http[s]://<domain>:<port>[/<namespace>]
|
||||
socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);
|
||||
|
||||
// Grab the System config on startup.
|
||||
// Refer to config.py for the contents of the configuration blob.
|
||||
$.ajax({
|
||||
url: "/get_config",
|
||||
dataType: 'json',
|
||||
async: false, // Yes, this is deprecated...
|
||||
success: function(data) {
|
||||
serverSettingsUpdate(data);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// LEAFLET MAP SETUP
|
||||
//
|
||||
// Setup a basic Leaflet map
|
||||
map = L.map('map').setView([chase_config.default_lat, chase_config.default_lon], 12);
|
||||
|
||||
// Add OSM Map Layer
|
||||
var osm_map = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: ''
|
||||
}).addTo(map);
|
||||
|
||||
// Add OSM Topo Map Layer
|
||||
var osm_topo_map = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
||||
attribution: ''
|
||||
});
|
||||
|
||||
// Add ESRI Satellite Map layers.
|
||||
var esrimapLink =
|
||||
'<a href="http://www.esri.com/">Esri</a>';
|
||||
var esriwholink =
|
||||
'i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community';
|
||||
var esri_sat_map = L.tileLayer(
|
||||
'http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
{
|
||||
attribution: '',
|
||||
maxZoom: 18,
|
||||
});
|
||||
// Dark Matter map layer, one of the maps suitable for 'dark mode'
|
||||
var dark_matter_map = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png', {
|
||||
attribution: ''
|
||||
});
|
||||
|
||||
|
||||
|
||||
var map_layers = {'OSM':osm_map, 'OpenTopo':osm_topo_map, 'ESRI Satellite':esri_sat_map, 'Dark Matter':dark_matter_map};
|
||||
|
||||
// Add ThunderForest layers, if we have a key provided.
|
||||
if (chase_config.thunderforest_api_key !== 'none'){
|
||||
// Thunderforest Outdoors layer.
|
||||
var thunderforest_outdoors = L.tileLayer('https://tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey='+chase_config.thunderforest_api_key,
|
||||
{
|
||||
attribution: ''
|
||||
});
|
||||
map_layers['Outdoors (Terrain)'] = thunderforest_outdoors;
|
||||
|
||||
}
|
||||
|
||||
if (chase_config.stadia_api_key !== 'none'){
|
||||
// Stadia Alidade Smooth Dark Layer.
|
||||
var alidade_smooth_dark = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png?apikey='+chase_config.stadia_api_key,
|
||||
{
|
||||
attribution: ''
|
||||
});
|
||||
map_layers['Alidade Smooth Dark'] = alidade_smooth_dark;
|
||||
|
||||
}
|
||||
|
||||
// Add Offline map layers, if we have any.
|
||||
for (var i = 0, len = chase_config.offline_tile_layers.length; i < len; i++) {
|
||||
var _layer_name = chase_config.offline_tile_layers[i];
|
||||
map_layers['Offline - ' + _layer_name] = L.tileLayer(location.protocol + '//' + document.domain + ':' + location.port + '/tiles/'+_layer_name+'/{z}/{x}/{y}.png');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Add layer selection control (top right).
|
||||
map.addControl(new L.Control.Layers(map_layers));
|
||||
|
||||
|
||||
// Add custom controls, which show various sets of data.
|
||||
|
||||
|
||||
L.easyButton('fa-car', function(btn, map){
|
||||
map.panTo(chase_car_position.latest_data);
|
||||
}, 'Follow Chase Car', 'followCarButton', {
|
||||
position: 'topright'
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
map.zoomControl.setPosition('topright');
|
||||
|
||||
// Chase Car Speed Display
|
||||
L.control.custom({
|
||||
position: 'bottomcenter',
|
||||
content : "<div class='dataAgeHeader' id='bearing_header'></div><div id='bearing_data' class='bearingData'></div>",
|
||||
classes : 'btn-group-vertical btn-group-sm',
|
||||
id: 'bearing_display',
|
||||
style :
|
||||
{
|
||||
margin: '5px',
|
||||
padding: '0px 0 0 0',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
|
||||
chase_car_position.marker = L.marker(chase_car_position.latest_data,{title:"Chase Car", icon: carIcon, rotationOrigin: "center center"})
|
||||
.addTo(map);
|
||||
|
||||
var bearing_path = L.polyline([chase_car_position.latest_data],{title:"Bearing", color:'red', weight:2.0});
|
||||
bearing_path.addTo(map);
|
||||
|
||||
|
||||
function calculateDestination(latlng, heading, distance) {
|
||||
heading = (heading + 360) % 360;
|
||||
var rad = Math.PI / 180,
|
||||
radInv = 180 / Math.PI,
|
||||
R = 6378137, // approximation of Earth's radius
|
||||
lon1 = latlng.lng * rad,
|
||||
lat1 = latlng.lat * rad,
|
||||
rheading = heading * rad,
|
||||
sinLat1 = Math.sin(lat1),
|
||||
cosLat1 = Math.cos(lat1),
|
||||
cosDistR = Math.cos(distance / R),
|
||||
sinDistR = Math.sin(distance / R),
|
||||
lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 *
|
||||
sinDistR * Math.cos(rheading)),
|
||||
lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR *
|
||||
cosLat1, cosDistR - sinLat1 * Math.sin(lat2));
|
||||
lon2 = lon2 * radInv;
|
||||
lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2;
|
||||
return L.latLng([lat2 * radInv, lon2]);
|
||||
}
|
||||
|
||||
function updateBearing(){
|
||||
var _end = calculateDestination(L.latLng(chase_car_position.latest_data), current_bearing, bearing_length);
|
||||
|
||||
bearing_path.setLatLngs([L.latLng(chase_car_position.latest_data), _end]);
|
||||
|
||||
$("#bearing_data").text(current_bearing.toFixed(0));
|
||||
}
|
||||
|
||||
function modulus(x, m){
|
||||
return (x % m + m) % m;
|
||||
}
|
||||
|
||||
// Controls to change bearings
|
||||
L.easyButton('<', function(btn, map){
|
||||
current_bearing = modulus((current_bearing-1),360.0);
|
||||
updateBearing();
|
||||
}, 'Bearing CCW 1 degrees', 'bearingCCW1Deg', {
|
||||
position: 'bottomleft'
|
||||
}).addTo(map);
|
||||
L.easyButton('<<', function(btn, map){
|
||||
current_bearing = modulus((current_bearing-5),360.0);
|
||||
updateBearing();
|
||||
}, 'Bearing CCW 5 degrees', 'bearingCCW5Deg', {
|
||||
position: 'bottomleft'
|
||||
}
|
||||
).addTo(map);
|
||||
L.easyButton('<<<', function(btn, map){
|
||||
current_bearing = modulus((current_bearing-10),360.0);
|
||||
updateBearing();
|
||||
}, 'Bearing CCW 10 degrees', 'bearingCCW10Deg', {
|
||||
position: 'bottomleft'
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
|
||||
L.easyButton('>', function(btn, map){
|
||||
current_bearing = modulus((current_bearing+1),360.0);
|
||||
updateBearing();
|
||||
}, 'Bearing CW 10 degrees', 'bearingCW1Deg', {
|
||||
position: 'bottomright'
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
L.easyButton('>>', function(btn, map){
|
||||
current_bearing = modulus((current_bearing+5),360.0);
|
||||
updateBearing();
|
||||
}, 'Bearing CW 5 degrees', 'bearingCW5Deg', {
|
||||
position: 'bottomright'
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
L.easyButton('>>>', function(btn, map){
|
||||
current_bearing = modulus((current_bearing+10),360.0);
|
||||
updateBearing();
|
||||
}, 'Bearing CW 10 degrees', 'bearingCW10Deg', {
|
||||
position: 'bottomright'
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
// Tell the server we are connected and ready for data.
|
||||
socket.on('connect', function() {
|
||||
socket.emit('client_connected', {data: 'I\'m connected!'});
|
||||
// This will cause the server to emit a few messages telling us to fetch data.
|
||||
});
|
||||
|
||||
|
||||
$("#bearing_data").click(function(){
|
||||
// Push a bearing to the backend for display.
|
||||
_bearing_info = {
|
||||
'type': 'BEARING',
|
||||
'bearing_type': 'absolute',
|
||||
'source': 'EasyBearing',
|
||||
'latitude': chase_car_position.latest_data[0],
|
||||
'longitude': chase_car_position.latest_data[1],
|
||||
'bearing': current_bearing
|
||||
};
|
||||
|
||||
socket.emit('add_manual_bearing', _bearing_info);
|
||||
|
||||
});
|
||||
|
||||
|
||||
function handleTelemetry(data){
|
||||
// Telemetry Event messages contain a dictionary of position data.
|
||||
// It should have the fields:
|
||||
// callsign: string
|
||||
// position: [lat, lon, alt]
|
||||
// vel_v: float
|
||||
// time_to_landing: String
|
||||
// If callsign = 'CAR', the lat/lon/alt will be considered to be a car telemetry position.
|
||||
|
||||
// Handle chase car position updates.
|
||||
if (data.callsign == 'CAR'){
|
||||
// Update car position.
|
||||
chase_car_position.latest_data = data.position;
|
||||
chase_car_position.heading = data.heading; // degrees true
|
||||
chase_car_position.speed = data.speed; // m/s
|
||||
|
||||
chase_car_position.marker.setLatLng(chase_car_position.latest_data).update();
|
||||
updateBearing();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Telemetry event handler.
|
||||
// We will get one of these mesages with every new balloon position
|
||||
socket.on('telemetry_event', function(data) {
|
||||
handleTelemetry(data);
|
||||
});
|
||||
|
||||
|
||||
window.setInterval(function(){
|
||||
// Dunno
|
||||
},1000);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map" class="map"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -44,7 +44,7 @@
|
|||
<script src="{{ url_for('static', filename='js/predictions.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/car.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/bearings.js') }}"></script>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/paho-mqtt.js') }}"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
|
@ -96,6 +96,10 @@
|
|||
// Functions defined later.
|
||||
var setChaseCarTrack;
|
||||
|
||||
// Global dark mode setting
|
||||
var dark_mode = false;
|
||||
var dark_mode_layers = ["Alidade Smooth Dark", "Dark Matter"];
|
||||
|
||||
// Leaflet map instance.
|
||||
var map;
|
||||
// Routing Engine
|
||||
|
@ -130,6 +134,8 @@
|
|||
// (i.e. when another client makes a change)
|
||||
socket.on('server_settings_update', function(data){
|
||||
serverSettingsUpdate(data);
|
||||
// Re-Connect to websockets if necessary.
|
||||
startSondeHubWebsockets();
|
||||
});
|
||||
|
||||
// Add handlers for various text fields.
|
||||
|
@ -378,6 +384,20 @@
|
|||
}
|
||||
})
|
||||
.addTo(map);
|
||||
// Chase Car Heading Display
|
||||
L.control.custom({
|
||||
position: 'bottomleft',
|
||||
content : "<div class='dataAgeHeader' id='chase_car_heading_header'></div><div id='chase_car_heading' class='chaseCarSpeed'></div>",
|
||||
classes : 'btn-group-vertical btn-group-sm',
|
||||
id: 'heading_display',
|
||||
style :
|
||||
{
|
||||
margin: '5px',
|
||||
padding: '0px 0 0 0',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
// Time-to-landing display - shows the time until landing for the currently tracked payload.
|
||||
L.control.custom({
|
||||
|
@ -423,6 +443,21 @@
|
|||
})
|
||||
.addTo(map);
|
||||
|
||||
// Used to show log replay time information, if received.
|
||||
L.control.custom({
|
||||
position: 'bottomcenter',
|
||||
content : "<div id='log_time' class='chaseCarSpeed'></div>",
|
||||
classes : 'btn-group-vertical btn-group-sm',
|
||||
id: 'log_time_control',
|
||||
style :
|
||||
{
|
||||
margin: '5px',
|
||||
padding: '0px 0 0 0',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
|
||||
|
||||
// Follow buttons - these just set the radio buttons on the settings pane.
|
||||
|
@ -463,6 +498,14 @@
|
|||
map.on('dragend',mapMovedEvent);
|
||||
|
||||
|
||||
map.on('baselayerchange', function (e) {
|
||||
if(dark_mode_layers.includes(e.name)){
|
||||
dark_mode = true;
|
||||
}else {
|
||||
dark_mode = false;
|
||||
}
|
||||
});
|
||||
|
||||
initTables();
|
||||
|
||||
|
||||
|
@ -514,6 +557,10 @@
|
|||
bearingUpdate(data);
|
||||
});
|
||||
|
||||
socket.on('server_bearings_cleared', function(data){
|
||||
destroyAllBearings();
|
||||
});
|
||||
|
||||
|
||||
$("#downloadModel").click(function(){
|
||||
socket.emit('download_model', {data: 'plzkthx'});
|
||||
|
@ -630,22 +677,8 @@
|
|||
}
|
||||
}, age_update_rate);
|
||||
|
||||
|
||||
// Habitat/Sondehub Chase Car Position Grabber
|
||||
var habitat_update_rate = 20000;
|
||||
window.setInterval(function(){
|
||||
if(document.getElementById("showOtherCars").checked){
|
||||
if (chase_config.profiles[chase_config.selected_profile].online_tracker === "habitat"){
|
||||
get_habitat_vehicles();
|
||||
}
|
||||
else if (chase_config.profiles[chase_config.selected_profile].online_tracker === "sondehub"){
|
||||
get_sondehub_vehicles();
|
||||
}
|
||||
else{
|
||||
// Do nothing...
|
||||
}
|
||||
}
|
||||
}, habitat_update_rate);
|
||||
// Start connection to Sondehub Websockets.
|
||||
startSondeHubWebsockets();
|
||||
|
||||
// Sadly we can't request location information unless chasemapper is served up via HTTPS
|
||||
// which is a bit difficult to make happen.
|
||||
|
@ -694,6 +727,9 @@
|
|||
<div class="paramRow">
|
||||
<b>Last Position:</b> <div style='float:right;' id='recoveryPosition'></div><br/>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Recovery Successful:</b> <input type="checkbox" class="paramSelector" id="recoverySuccessful" checked><br/>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Use Car Position:</b> <input type="checkbox" class="paramSelector" id="recoveryCarPosition" onclick='setRecoveryCarPosition();'><br/>
|
||||
</div>
|
||||
|
@ -747,23 +783,33 @@
|
|||
<input type="radio" name="autoFollow" value="none"> None
|
||||
</form>
|
||||
</hr>
|
||||
<div class="paramRow">
|
||||
<b>Show Car Track</b> <input type="checkbox" class="paramSelector" id="chaseCarTrack" onclick='setChaseCarTrack();' checked>
|
||||
</div>
|
||||
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="chaseCarTrack" onclick='setChaseCarTrack();' checked>
|
||||
<label class="form-check-label" for="chaseCarTrack">
|
||||
<b>Show Car Track </b>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</hr>
|
||||
<h3>Predictor</h3>
|
||||
<div class="paramRow" id="predictorModel">
|
||||
<b>Current Model: </b> <div class="predictorModelValue" id="predictorModelValue">Disabled</div>
|
||||
<b>Current Model </b> <div class="predictorModelValue" id="predictorModelValue">Disabled</div>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Download Model</b> <button type="button" class="paramSelector" id="downloadModel">Download</button>
|
||||
<b>Download Model </b> <button type="button" class="paramSelector" id="downloadModel">Download</button>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Enable Predictions</b> <input type="checkbox" class="paramSelector" id="predictorEnabled" onclick='clientSettingsUpdate();'>
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="predictorEnabled" onclick='clientSettingsUpdate();'>
|
||||
<label class="form-check-label" for="predictorEnabled">
|
||||
<b>Enable Predictions </b>
|
||||
</label>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Show 'Abort' Predictions</b> <input type="checkbox" class="paramSelector" id="abortPredictionEnabled" onclick='clientSettingsUpdate();'>
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="abortPredictionEnabled" onclick='clientSettingsUpdate();'>
|
||||
<label class="form-check-label" for="abortPredictionEnabled">
|
||||
<b>Show 'Abort' Predictions </b>
|
||||
</label>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Burst Altitude</b><input type="text" class="paramEntry" id="burstAlt"><br/>
|
||||
|
@ -799,32 +845,41 @@
|
|||
</div>
|
||||
-->
|
||||
|
||||
<h3>Habitat Chase Car</h3>
|
||||
<div class="paramRow">
|
||||
<b>Show Nearby Chase-Cars:</b> <input type="checkbox" class="paramSelector" id="showOtherCars" onclick="show_habitat_vehicles();">
|
||||
<h3>Chase Car Upload</h3>
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="showOtherCars" onclick="show_sondehub_vehicles();">
|
||||
<label class="form-check-label" for="showOtherCars">
|
||||
<b>Show Nearby Chase-Cars </b>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="habitatUploadEnabled" onclick='clientSettingsUpdate();'>
|
||||
<label class="form-check-label" for="habitatUploadEnabled">
|
||||
<b>Chase-Car Position Upload </b>
|
||||
</label>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Enable Chase-Car Position Upload</b> <input type="checkbox" class="paramSelector" id="habitatUploadEnabled" onclick='clientSettingsUpdate();'>
|
||||
<b>Online Tracker Callsign </b><input type="text" class="paramEntry" id="habitatCall"><br/>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Habitat Call:</b><input type="text" class="paramEntry" id="habitatCall"><br/>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Update Rate (seconds):</b><input type="text" class="paramEntry" id="habitatUpdateRate"><br/>
|
||||
<b>Update Rate (seconds) </b><input type="text" class="paramEntry" id="habitatUpdateRate"><br/>
|
||||
</div>
|
||||
|
||||
<h3>Range Rings</h3>
|
||||
<div class="paramRow">
|
||||
<b>Enable Range Rings</b> <input type="checkbox" class="paramSelector" id="rangeRingsEnabled" onclick='updateRangeRings();'>
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="rangeRingsEnabled" onclick='updateRangeRings();'>
|
||||
<label class="form-check-label" for="rangeRingsEnabled">
|
||||
<b>Enable Range Rings </b>
|
||||
</label>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Ring Qty</b><input type="text" class="paramEntry" id="ringQuantity" value="5"><br/>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b> <p><p id="ring_spacing">Ring Spacing (m)</p></p></b><input type="text" class="paramEntry" id="ringSpacing" value="1000"><br/>
|
||||
<b> <p><p id="ring_spacing">Ring Spacing (m) </p></p></b><input type="text" class="paramEntry" id="ringSpacing" value="1000"><br/>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Ring Weight (px)</b><input type="text" class="paramEntry" id="ringWeight" value="2"><br/>
|
||||
<b>Ring Weight (px) </b><input type="text" class="paramEntry" id="ringWeight" value="2"><br/>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Ring Color</b>
|
||||
|
@ -841,9 +896,25 @@
|
|||
<b>Custom Color</b><input type="text" class="paramEntry" id="ringCustomColor" value="#FF0000"><br/>
|
||||
</div>
|
||||
|
||||
<h3>Speed Display</h3>
|
||||
<h3>Speed/Heading Display</h3>
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="showCarSpeed">
|
||||
<label class="form-check-label" for="showCarSpeed">
|
||||
<b>Show Chase-Car Speed </b>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="showCarHeading">
|
||||
<label class="form-check-label" for="showCarHeading">
|
||||
<b>Show Chase-Car Heading </b>
|
||||
</label>
|
||||
</div>
|
||||
<h3>GPS/Heading Status</h3>
|
||||
<div class="paramRow">
|
||||
<b>Show Chase Car Speed:</b> <input type="checkbox" class="paramSelector" id="showCarSpeed">
|
||||
<b>SVs Tracked </b> <div class="predictorModelValue" id='numSVStatus'>---</div>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Heading </b> <div class="predictorModelValue" id='headingStatus'>---</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -855,31 +926,47 @@
|
|||
<span class="sidebar-close"><i class="fa fa-caret-left"></i></span>
|
||||
</h1>
|
||||
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="bearingsEnabled" checked onclick='toggleBearingsEnabled();'>
|
||||
<label class="form-check-label" for="bearingsEnabled">
|
||||
<b>Enable Bearings </b>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="bearingsOnlyMode" onclick='toggleBearingsOnlyMode();'>
|
||||
<label class="form-check-label" for="bearingsOnlyMode">
|
||||
<b>Bearing-Only Mode </b>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="tdoaEnabled" checked>
|
||||
<label class="form-check-label" for="tdoaEnabled">
|
||||
<b>Enable TDOA Plot </b>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="bigTDOAEnabled">
|
||||
<label class="form-check-label" for="bigTDOAEnabled">
|
||||
<b>Big TDOA Plot </b>
|
||||
</label>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Enable Bearings</b> <input type="checkbox" class="paramSelector" id="bearingsEnabled" checked onclick='toggleBearingsEnabled();'>
|
||||
<b>Confidence Threshold</b><input type="text" class="paramEntry" id="bearingConfidenceThreshold" value="5.0"><br/>
|
||||
</div>
|
||||
|
||||
<div class="paramRow">
|
||||
<b>Bearing-Only Mode</b> <input type="checkbox" class="paramSelector" id="bearingsOnlyMode" onclick='toggleBearingsOnlyMode();'>
|
||||
<b>Maximum Age (min)</b><input type="text" class="paramEntry" id="bearingMaximumAge" value="10"><br/>
|
||||
</div>
|
||||
|
||||
<div class="paramRow">
|
||||
<b>Enable TDOA Plot</b> <input type="checkbox" class="paramSelector" id="tdoaEnabled" checked>
|
||||
<div class="form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" role="switch" value="" id="showStationaryBearings">
|
||||
<label class="form-check-label" for="showStationaryBearings">
|
||||
<b>Show Stationary Bearings </b>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="paramRow">
|
||||
<b>Confidence Threshold</b><input type="text" class="paramEntry" id="bearingConfidenceThreshold" value="50"><br/>
|
||||
</div>
|
||||
|
||||
<div class="paramRow">
|
||||
<b>Maximum Age (min)</b><input type="text" class="paramEntry" id="bearingMaximumAge" value="20"><br/>
|
||||
</div>
|
||||
|
||||
<div class="paramRow">
|
||||
<b>Show Stationary Bearings</b> <input type="checkbox" class="paramSelector" id="showStationaryBearings">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="paramRow">
|
||||
<button type="button" class="paramSelector" id="clearBearingsBtn" onclick='destroyAllBearings();'>Clear Map</button></br>
|
||||
</div>
|
||||
|
@ -887,12 +974,15 @@
|
|||
<button type="button" class="paramSelector" id="flushBearingsBtn" onclick='flushBearings();'>Flush Bearing Store</button></br>
|
||||
</div>
|
||||
|
||||
<h3>Bearing Sources</h3>
|
||||
<div id="bearing_source_selector"></div>
|
||||
|
||||
<h3>Bearing Style</h3>
|
||||
<div class="paramRow">
|
||||
<b>Bearing Length (km)</b><input type="text" class="paramEntry" id="bearingLength" value="10"><br/>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Bearing Weight (px)</b><input type="text" class="paramEntry" id="bearingWeight" value="0.5"><br/>
|
||||
<b>Bearing Weight (px)</b><input type="text" class="paramEntry" id="bearingWeight" value="1.0"><br/>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Bearing Min Opacity</b><input type="text" class="paramEntry" id="bearingMinOpacity" value="0.1"><br/>
|
||||
|
@ -904,6 +994,7 @@
|
|||
<b>Bearing Color</b>
|
||||
<select class="paramSelector" id="bearingColorSelect" name="bearingColorSelect">
|
||||
<option value='black'>Black</option>
|
||||
<option value='white'>White</option>
|
||||
<option value='red'>Red</option>
|
||||
<option value='green'>Green</option>
|
||||
<option value='blue'>Blue</option>
|
||||
|
@ -919,6 +1010,11 @@
|
|||
<button type="button" class="paramSelector" id="redrawBearingsBtn" onclick='redrawBearings();'>Redraw Bearings</button></br>
|
||||
</div>
|
||||
|
||||
<h3>Manual Bearing Entry</h3>
|
||||
<div class="paramRow">
|
||||
<input type="text" id="bearingManualEntry" value="0"><br/><button type="button" class="paramSelector" id="manualBearingBtn" onclick='manualBearing();'>Plot</button></br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# ChaseMapper - Bearing o'Clock
|
||||
#
|
||||
# Add bearings based on O'Clock position (1 through 12)
|
||||
# Run with: python bearing_o_clock.py bearing_source_name
|
||||
#
|
||||
# Copyright (C) 2019 Mark Jessop <vk5qi@rfhead.net>
|
||||
# Released under GNU GPL v3 or later
|
||||
#
|
||||
#
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
import traceback
|
||||
|
||||
|
||||
def send_relative_bearing(bearing, source, heading_override=False, udp_port=55672):
|
||||
"""
|
||||
Send a basic relative bearing
|
||||
"""
|
||||
packet = {
|
||||
'type' : 'BEARING',
|
||||
'bearing' : bearing,
|
||||
'bearing_type': 'relative',
|
||||
'source': source
|
||||
}
|
||||
|
||||
if heading_override:
|
||||
packet["heading_override"] = True
|
||||
|
||||
# Set up our UDP socket
|
||||
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
|
||||
s.settimeout(1)
|
||||
# Set up socket for broadcast, and allow re-use of the address
|
||||
s.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
except:
|
||||
pass
|
||||
s.bind(('',udp_port))
|
||||
try:
|
||||
s.sendto(json.dumps(packet).encode('ascii'), ('<broadcast>', udp_port))
|
||||
except socket.error:
|
||||
s.sendto(json.dumps(packet).encode('ascii'), ('127.0.0.1', udp_port))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
_source = sys.argv[1]
|
||||
else:
|
||||
_source = "o_clock_entry"
|
||||
|
||||
try:
|
||||
while True:
|
||||
|
||||
print("Enter O-Clock Bearing (1-12):")
|
||||
_val = input()
|
||||
|
||||
try:
|
||||
_val_int = int(_val)
|
||||
|
||||
_bearing = (_val_int%12)*30
|
||||
|
||||
print(f"Sending Relative Bearing: {_bearing}")
|
||||
|
||||
send_relative_bearing(_bearing, _source, heading_override=True)
|
||||
except Exception as e:
|
||||
print(f"Error handling input: {str(e)}")
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
|
||||
|
Ładowanie…
Reference in New Issue