-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
26 changed files
with
631 additions
and
280 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# ADSB.lol Proxy API | ||
The enclosed `adsblol_proxy.py` file was developed to be deployed using an [AWS Lambda](https://aws.amazon.com/lambda/) behind an [AWS API Gateway](https://aws.amazon.com/api-gateway/). For the default Skyportal configuration this should fall well within the monthly limits of the AWS Free Tier. | ||
|
||
If, like me, you've never used AWS before this point, the following steps should help you get up and running. If you know what you're doing then you can probably figure all this out without my help. | ||
|
||
## AWS Lambda | ||
### Create Function | ||
From the Lambda console, create a new function with the following configuration: | ||
* Author from scratch | ||
* Whatever function name you want | ||
* Python 3.11 Runtime | ||
* x86_64 architecture | ||
|
||
Once created, edit your Runtime Settings and change the Handler to `adsblol_proxy.lambda_handler`. | ||
|
||
### Create a `.zip` deployment | ||
Our Lambda depends on [`httpx`](https://github.com/encode/httpx/) to make its web request, so it will need to be installed along with its dependencies before we can deploy. One way to achieve this with Lambda is to upload a [`.zip` deployment package](https://docs.aws.amazon.com/lambda/latest/dg/python-package.html#python-package-create-dependencies); the documentation can be a little obtuse but ultimately the goal is to end up with a zip file whose contents look something like this: | ||
|
||
``` | ||
anyio/ | ||
anyio-4.0.0.dist-info/ | ||
certifi/ | ||
certifi-2023.7.22.dist-info/ | ||
h11/ | ||
h11-0.14.0.dist-info/ | ||
httpcore/ | ||
httpcore-1.0.2.dist-info/ | ||
httpx/ | ||
httpx-0.25.1.dist-info/ | ||
idna/ | ||
idna-3.4.dist-info/ | ||
sniffio/ | ||
sniffio-1.3.0.dist-info/ | ||
adsblol_proxy.py | ||
``` | ||
|
||
I accomplished this using a virtual environment, e.g.: | ||
|
||
``` | ||
$ python -m venv ./.venv | ||
$ source ./.venv/Scripts/activate | ||
$ python -m pip install -U pip httpx | ||
``` | ||
|
||
Move or copy the everything from `./.venv/Lib/site-packages` **EXCEPT** `pip` (blows up the file size unnecessarily & we don't need it) into the directory with `adsblol_proxy.py` and zip everything together so you get the layout above. You can then upload this zip file to Lambda & then deploy the code. | ||
|
||
## AWS API Gateway | ||
### Create API | ||
From the API Gateway console, create a new API: | ||
* REST API | ||
* New API | ||
* Whatever name you'd like | ||
* Optional description | ||
* Regional endpoint type | ||
|
||
### Create Method | ||
Under resources, create a new method: | ||
* `GET` method type | ||
* Lambda function integration type | ||
* Enable "Lambda proxy integration" | ||
* If you've already created your Lambda function above, you should be able to select it | ||
* Default timeout should be fine | ||
|
||
### Edit Method | ||
Edit your method request settings: | ||
* Authorization - None | ||
* Request validator - None | ||
* API key required - CHECK | ||
* URL query string parameters | ||
* `lat`, required | ||
* `lon`, required | ||
* `radius`, required | ||
|
||
Once this is done, Deploy your API. You will need to specify a stage name if you haven't previously. Since you're just doing a hobby API you can name it whatever you want; I called mine `live`. | ||
|
||
Make a note of your Invoke URL, which can be found under Stages. It will be something like `https://abcd123.execute-api.us-east-69.amazonaws.com/live/`. | ||
|
||
### Create an API key | ||
Under API Keys create a new API key & store in a secure location that you can access later. | ||
|
||
### Create a Usage Plan | ||
This must be created in order for the API key to work. Fill out the options however you'd like. | ||
|
||
Once this is created you'll need to add a stage, this is what you targeted when you deployed your API. | ||
|
||
Finally, you'll need to add your API key to the Associated API keys. | ||
|
||
## Testing | ||
You can check that your API is functional using `curl`: | ||
|
||
``` | ||
$ curl --location "https://abcd123.execute-api.us-east-69.amazonaws.com/live/?lat=42.41&lon=-71.17&radius=30" --header "x-api-key:<key>" | ||
``` | ||
|
||
Which should give back some aircraft data. | ||
|
||
## Configuring Skyportal | ||
To utilize the proxy server, copy your Invoke URL and API key into `secrets.py`, and set `AIRCRAFT_DATA_SOURCE = "proxy"` in your `skyportal_config.py`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import json | ||
|
||
import httpx | ||
|
||
URL_BASE = "https://api.adsb.lol/v2" | ||
|
||
|
||
def query_data(lat: float, lon: float, radius: float) -> dict: | ||
"""Execute the desired ADSB.lol query & return the JSON response.""" | ||
query_url = f"{URL_BASE}/lat/{lat}/lon/{lon}/dist/{radius}" | ||
r = httpx.get(query_url) | ||
r.raise_for_status() | ||
|
||
return r.json() | ||
|
||
|
||
def simplify_aircraft(flight_data: list[dict]) -> list[dict]: | ||
""" | ||
Simplify the ADSB.lol API response into something more Skyportal memory friendly. | ||
The following cleanup operations are made on the provided aircraft state vectors: | ||
* All non-airborne flights are discarded | ||
* Keys are reorganized into a layout that directly matches `skyportal.AircraftState` | ||
* Value units are converted, where necessary | ||
* Missing keys are set to `None` if expected to be present | ||
""" | ||
airborne_flights = [] | ||
for state_vector in flight_data: | ||
if (baro_alt := state_vector["alt_baro"]) == "ground": | ||
# Skip airborne aircraft | ||
continue | ||
|
||
if (callsign := state_vector.get("flight", None)) is None: | ||
callsign = state_vector.get("r", None) | ||
if callsign is not None: | ||
callsign = callsign.strip() | ||
|
||
# Ground track is likely not transmitted on the ground | ||
# If an aircraft is on the ground it may be transmitting true_heading | ||
if (track := state_vector.get("track", None)) is None: | ||
track = state_vector.get("true_heading", None) | ||
|
||
if (alt_geo := state_vector.get("alt_geom", None)) is not None: | ||
alt_geo *= 0.3048 # Provided in ft | ||
|
||
if (baro_rate := state_vector.get("baro_rate", None)) is not None: | ||
baro_rate *= 0.3048 # Provided in ft | ||
|
||
simplified_vector = { | ||
"icao": state_vector["hex"], | ||
"callsign": callsign, | ||
"lat": state_vector["lat"], | ||
"lon": state_vector["lon"], | ||
"track": track, | ||
"velocity_mps": state_vector["gs"] * 0.5144, # Provided in kts | ||
"on_ground": False, | ||
"baro_altitude_m": baro_alt, | ||
"geo_altitude_m": alt_geo, | ||
"vertical_rate_mps": baro_rate, | ||
"aircraft_category": state_vector.get("category", "NO_INFO"), | ||
} | ||
|
||
airborne_flights.append(simplified_vector) | ||
|
||
return airborne_flights | ||
|
||
|
||
def lambda_handler(event, context) -> dict: # noqa: ANN001 D103 | ||
try: | ||
params = event["queryStringParameters"] | ||
full_data = query_data(lat=params["lat"], lon=params["lon"], radius=params["radius"]) | ||
except httpx.HTTPError as e: | ||
return { | ||
"statusCode": 400, | ||
"body": json.dumps(f"HTTP Exception for {e.request.url} - {e}"), | ||
} | ||
except KeyError as e: | ||
return { | ||
"statusCode": 400, | ||
"body": json.dumps(str(e)), | ||
} | ||
|
||
return { | ||
"statusCode": 200, | ||
"body": json.dumps( | ||
{ | ||
"ac": simplify_aircraft(full_data["ac"]), | ||
"api_time": full_data["now"] / 1000, # Server time given in milliseconds | ||
} | ||
), | ||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Oops, something went wrong.