From 5e2639d18ba44ef3cfec2f7a7440a2aff7dbb2c0 Mon Sep 17 00:00:00 2001 From: lk-iqt <112730501+lk-iqt@users.noreply.github.com> Date: Wed, 1 Nov 2023 16:55:42 -0400 Subject: [PATCH 1/9] Added mavlink-api for interfacing with Pixhawk --- utils/mavlink-api/Dockerfile | 13 + utils/mavlink-api/README.md | 29 ++ utils/mavlink-api/docker-compose.yaml | 14 + utils/mavlink-api/mavlink-api.py | 176 ++++++++++ utils/mavlink-api/poetry.lock | 314 ++++++++++++++++++ utils/mavlink-api/pyproject.toml | 17 + .../mavlink-api/utils/mavlink_serial_test.py | 27 ++ 7 files changed, 590 insertions(+) create mode 100644 utils/mavlink-api/Dockerfile create mode 100644 utils/mavlink-api/README.md create mode 100644 utils/mavlink-api/docker-compose.yaml create mode 100644 utils/mavlink-api/mavlink-api.py create mode 100644 utils/mavlink-api/poetry.lock create mode 100644 utils/mavlink-api/pyproject.toml create mode 100644 utils/mavlink-api/utils/mavlink_serial_test.py diff --git a/utils/mavlink-api/Dockerfile b/utils/mavlink-api/Dockerfile new file mode 100644 index 00000000..ee0f06d5 --- /dev/null +++ b/utils/mavlink-api/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app +ENV PYTHONPATH=${PYTHONPATH}:${PWD} + +RUN pip3 install poetry +COPY pyproject.toml . +RUN poetry config virtualenvs.create false +RUN poetry install --no-dev + +COPY mavlink-api.py . + +ENTRYPOINT ["python3", "-u", "mavlink-api.py"] \ No newline at end of file diff --git a/utils/mavlink-api/README.md b/utils/mavlink-api/README.md new file mode 100644 index 00000000..780b1262 --- /dev/null +++ b/utils/mavlink-api/README.md @@ -0,0 +1,29 @@ +# Mavlink API Interface + +## Setup and calibrate device + +Use Mission Planner or QGroundControl to calibrate and configure device. Set the AHRS to the correct orientation for use. + +## Set up device mount +Find the correct device serial port and attributes with the following: +```bash +lsusb +dmesg | grep tty +udevadm info -a /dev/ttyAMC0 #<--use correct serial port> +``` + +Test if you have the correct port: +```bash +python3 utils/mavlink_serial_test.py /dev/ttyACM9 +``` + +Create a udev rule to map the hardware to a custom port +```bash +echo "KERNEL=="ttyAMC0", ATTRS{idVendor}=="3162", MODE:="0666", SYMLINK+="pixhawk_serial"" >> /etc/udev/rules.d/60-gamutrf.rules +sudo udevadm control --reload-rules && sudo udevadm trigger +``` + +Test again if it is working correctly: +```bash +python3 utils/mavlink_serial_test.py /dev/pixhawk_serial +``` \ No newline at end of file diff --git a/utils/mavlink-api/docker-compose.yaml b/utils/mavlink-api/docker-compose.yaml new file mode 100644 index 00000000..d634a4c1 --- /dev/null +++ b/utils/mavlink-api/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + mavlink-api: + image: mavlink-api:latest + build: + context: . + dockerfile: ./Dockerfile + ports: + - "8888:8888" + devices: + - "/dev/pixhawk_serial:/dev/tty.serial1" +# networks: +# - gamutrf + restart: unless-stopped diff --git a/utils/mavlink-api/mavlink-api.py b/utils/mavlink-api/mavlink-api.py new file mode 100644 index 00000000..ab6dca49 --- /dev/null +++ b/utils/mavlink-api/mavlink-api.py @@ -0,0 +1,176 @@ +from flask import Flask, jsonify +from pymavlink import mavutil +import threading +import time +import serial + +app = Flask(__name__) +class MAVLINKGPSHandler: + def __init__(self): + + self.STALE_TIMEOUT = 30 #seconds + + self.gps_stale = None + self.gps_fix_type = None + self.latitude = None + self.longitude = None + self.altitude = None + self.relative_alt = None + self.time_usec = None + self.vx = None + self.vy = None + self.vz = None + self.time_boot_ms = None + + self.latest_GLOBAL_POSITION_INT_msg = None + self.latest_GLOBAL_POSITION_INT_timestamp = None + self.latest_GPS_RAW_INT_msg = None + self.latest_GPS_RAW_INT_timestamp = None + + self.mavlink_thread = threading.Thread(target=self.handle_mavlink_messages) + self.mavlink_thread.daemon = True + self.mavlink_thread.start() + + def GLOBAL_POSITION_INT_parser(self, data: dict = None) -> dict: + """Selected required data from data received from Pixhawk, and + convert to required units. + Args: + data (dict): Data received from the Pixhawk device + Returns: + clean_data (dict): Required data to be published + """ + #If data supplied, else use last msg + if data == None: + data = self.latest_GLOBAL_POSITION_INT_msg.to_dict() + + #Check for stale GPS + self.gps_stale_check() + + #Parse msg data + self.time_boot_ms = data["time_boot_ms"] #mm + self.latitude = data["lat"]/10000000 # decimal degrees + self.longitude = data["lon"]/10000000 # decimal degrees + self.altitude = data["alt"] #mm + self.relative_alt = data["relative_alt"] #mm + self.heading = data["hdg"]/100.0 # decimal degrees + self.vx = data["vx"]/100.0 # meters/second + self.vy = data["vy"]/100.0 # meters/second + self.vz = data["vz"]/100.0 # meters/second + return + + def GPS_RAW_INT_parser(self, data: dict = None) -> dict: + """Selected required data from data received from Pixhawk, and + convert to required units. + Args: + data (dict): Data received from the Pixhawk device + Returns: + clean_data (dict): Required data to be published + + GPS_FIX_TYPE + [Enum] Type of GPS fix + + Value Field Name Description + 0 GPS_FIX_TYPE_NO_GPS No GPS connected + 1 GPS_FIX_TYPE_NO_FIX No position information, GPS is connected + 2 GPS_FIX_TYPE_2D_FIX 2D position + 3 GPS_FIX_TYPE_3D_FIX 3D position + 4 GPS_FIX_TYPE_DGPS DGPS/SBAS aided 3D position + 5 GPS_FIX_TYPE_RTK_FLOAT RTK float, 3D position + 6 GPS_FIX_TYPE_RTK_FIXED RTK Fixed, 3D position + 7 GPS_FIX_TYPE_STATIC Static fixed, typically used for base stations + 8 GPS_FIX_TYPE_PPP PPP, 3D position. + """ + #If data supplied, else use last msg + if data == None: + data = self.latest_GPS_RAW_INT_msg.to_dict() + + #Check for stale GPS + self.gps_stale_check() + + #Update fix type + self.time_usec = data["time_usec"] #UNIX Epoch time uSec + self.gps_fix_type=data["fix_type"] + + return + + def gps_stale_check(self): + #Check for stale GPS data + if (time.time() - self.latest_GPS_RAW_INT_timestamp > self.STALE_TIMEOUT) or \ + (time.time() - self.latest_GLOBAL_POSITION_INT_timestamp > self.STALE_TIMEOUT): + self.gps_stale = True + else: + self.gps_stale = False + + + def create_gps_json_payload(self): + #Check for stale GPS + self.gps_stale_check() + + #Create payload dict for json + payload={} + + payload["gps_stale"] = self.gps_stale + payload["gps_fix_type"] = self.gps_fix_type + payload["time_boot_ms"] = self.time_boot_ms #mm + payload["time_usec"] = self.time_usec + payload["latitude"] = self.latitude # decimal degrees + payload["longitude"] = self.longitude # decimal degrees + payload["altitude"] = self.altitude #mm + payload["relative_alt"] = self.relative_alt #mm + payload["heading"] = self.heading # decimal degrees + payload["vx"] = self.vx # meters/second + payload["vy"] = self.vy # meters/second + payload["vz"] = self.vz # meters/second + + return payload + + # Function to handle incoming MAVLink messages + def handle_mavlink_messages(self): + # Connect to the MAVLink source (e.g., UDP or serial port) + mavlink_connection = mavutil.mavlink_connection("/dev/tty.serial1", 57600) + + while True: + #msg = mavlink_connection.recv_match(type='GLOBAL_POSITION_INT', blocking=True) + msg = mavlink_connection.recv_match(blocking=True) + if msg.get_type() == 'GLOBAL_POSITION_INT': + self.latest_GLOBAL_POSITION_INT_msg = msg + self.latest_GLOBAL_POSITION_INT_timestamp=time.time() + elif msg.get_type() == 'GPS_RAW_INT': + self.latest_GPS_RAW_INT_msg = msg + self.latest_GPS_RAW_INT_timestamp=time.time() + msg + +# Store the latest GPS data handler +mavlink_gps_handler = MAVLINKGPSHandler() + +# Routes for latest GPS Data +@app.route('/gps-fix-status', methods=['GET']) +def get_latest_gps_fix_status(): + if mavlink_gps_handler.latest_GPS_RAW_INT_msg: + mavlink_gps_handler.GPS_RAW_INT_parser() + return jsonify({"fix_type":mavlink_gps_handler.gps_fix_type,"gps_stale":mavlink_gps_handler.gps_stale}), 200 + else: + return jsonify({'error': 'No GPS data available'}), 404 + +@app.route('/gps-data', methods=['GET']) +def get_latest_gps_data(): + if mavlink_gps_handler.latest_GLOBAL_POSITION_INT_msg: + mavlink_gps_handler.GLOBAL_POSITION_INT_parser() + msg=mavlink_gps_handler.create_gps_json_payload() + return jsonify(msg), 200 + else: + return jsonify({'error': 'No GPS data available'}), 404 + +@app.route('/heading', methods=['GET']) +def get_latest_heading(): + if mavlink_gps_handler.latest_GLOBAL_POSITION_INT_msg: + mavlink_gps_handler.GLOBAL_POSITION_INT_parser() + return jsonify({"heading":mavlink_gps_handler.heading,"gps_stale":mavlink_gps_handler.gps_stale}), 200 + else: + return jsonify({'error': 'No heading data available'}), 404 + +def main(): + app.run(host='0.0.0.0', port=8888) + +if __name__ == '__main__': + main() diff --git a/utils/mavlink-api/poetry.lock b/utils/mavlink-api/poetry.lock new file mode 100644 index 00000000..41f0ffc8 --- /dev/null +++ b/utils/mavlink-api/poetry.lock @@ -0,0 +1,314 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "blinker" +version = "1.6.2" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.7" +files = [ + {file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"}, + {file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "flask" +version = "3.0.0" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flask-3.0.0-py3-none-any.whl", hash = "sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638"}, + {file = "flask-3.0.0.tar.gz", hash = "sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58"}, +] + +[package.dependencies] +blinker = ">=1.6.2" +click = ">=8.1.3" +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.0.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "future" +version = "0.18.3" +description = "Clean single-source support for Python 3 and 2" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, +] + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.7" +files = [ + {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, + {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lxml" +version = "4.9.3" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +files = [ + {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, + {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"}, + {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, + {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, + {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, + {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, + {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, + {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, + {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, + {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, + {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"}, + {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"}, + {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"}, + {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"}, + {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"}, + {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, + {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, + {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, + {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, + {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, + {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, + {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, + {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, + {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, + {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=0.29.35)"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "pymavlink" +version = "2.4.40" +description = "Python MAVLink code" +optional = false +python-versions = "*" +files = [ + {file = "pymavlink-2.4.40-py3-none-any.whl", hash = "sha256:f1be2eb9ceafe1686c99876fa762774dabd66de8134ec10fd5d3c9469bf23949"}, + {file = "pymavlink-2.4.40.tar.gz", hash = "sha256:3d6a552ad344a1fff9e0c811361ac9d8bb8201cf6aacad7224d516fa037ccb30"}, +] + +[package.dependencies] +future = "*" +lxml = "*" + +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +optional = false +python-versions = "*" +files = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] + +[package.extras] +cp2110 = ["hidapi"] + +[[package]] +name = "werkzeug" +version = "3.0.0" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.0-py3-none-any.whl", hash = "sha256:cbb2600f7eabe51dbc0502f58be0b3e1b96b893b05695ea2b35b43d4de2d9962"}, + {file = "werkzeug-3.0.0.tar.gz", hash = "sha256:3ffff4dcc32db52ef3cc94dff3000a3c2846890f3a5a51800a27b909c5e770f0"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "b247a7bcaee2251e28302f8de35188ab20368d79c0ad5f5b272ab06fa29cc4e2" diff --git a/utils/mavlink-api/pyproject.toml b/utils/mavlink-api/pyproject.toml new file mode 100644 index 00000000..c4ae04e1 --- /dev/null +++ b/utils/mavlink-api/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "mavlink-api" +version = "0.1.0" +description = "" +authors = ["lk-iqt <112730501+lk-iqt@users.noreply.github.com>"] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +Flask = "^3.0.0" +pymavlink = "^2.4.40" +pyserial = "^3.5" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/utils/mavlink-api/utils/mavlink_serial_test.py b/utils/mavlink-api/utils/mavlink_serial_test.py new file mode 100644 index 00000000..89c127e9 --- /dev/null +++ b/utils/mavlink-api/utils/mavlink_serial_test.py @@ -0,0 +1,27 @@ +#! /usr/bin/env python3 + +import sys +from pymavlink import mavutil + +if len(sys.argv) < 2: + print("Usage: python mavlink_serial_test.py ") + sys.exit(1) + +serial_port = sys.argv[1] + +try: + mav = mavutil.mavlink_connection(serial_port) + print("Connected to MAVLink at", serial_port) + + # You can perform further operations here + + # For example, you can continuously read messages + while True: + message = mav.recv_match(blocking=True) + if message is not None: + print("Received:", message) + + # You can add more logic here based on the received messages + +except Exception as e: + print("Error:", str(e)) From a5685797226543d4e2ee46199c6976de4328f119 Mon Sep 17 00:00:00 2001 From: lk-iqt <112730501+lk-iqt@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:35:46 -0400 Subject: [PATCH 2/9] Formatting and Dockerfile sha hash --- utils/mavlink-api/Dockerfile | 2 +- utils/mavlink-api/mavlink-api.py | 125 +++++++++++------- .../mavlink-api/utils/mavlink_serial_test.py | 8 +- 3 files changed, 79 insertions(+), 56 deletions(-) diff --git a/utils/mavlink-api/Dockerfile b/utils/mavlink-api/Dockerfile index ee0f06d5..5cfd0c65 100644 --- a/utils/mavlink-api/Dockerfile +++ b/utils/mavlink-api/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim +FROM python:3.11-slim@sha256:1591aa8c01b5b37ab31dbe5662c5bdcf40c2f1bce4ef1c1fd24802dae3d01052 WORKDIR /app ENV PYTHONPATH=${PYTHONPATH}:${PWD} diff --git a/utils/mavlink-api/mavlink-api.py b/utils/mavlink-api/mavlink-api.py index ab6dca49..c75866fb 100644 --- a/utils/mavlink-api/mavlink-api.py +++ b/utils/mavlink-api/mavlink-api.py @@ -5,10 +5,11 @@ import serial app = Flask(__name__) + + class MAVLINKGPSHandler: def __init__(self): - - self.STALE_TIMEOUT = 30 #seconds + self.STALE_TIMEOUT = 30 # seconds self.gps_stale = None self.gps_fix_type = None @@ -26,7 +27,7 @@ def __init__(self): self.latest_GLOBAL_POSITION_INT_timestamp = None self.latest_GPS_RAW_INT_msg = None self.latest_GPS_RAW_INT_timestamp = None - + self.mavlink_thread = threading.Thread(target=self.handle_mavlink_messages) self.mavlink_thread.daemon = True self.mavlink_thread.start() @@ -39,24 +40,24 @@ def GLOBAL_POSITION_INT_parser(self, data: dict = None) -> dict: Returns: clean_data (dict): Required data to be published """ - #If data supplied, else use last msg + # If data supplied, else use last msg if data == None: data = self.latest_GLOBAL_POSITION_INT_msg.to_dict() - #Check for stale GPS + # Check for stale GPS self.gps_stale_check() - #Parse msg data - self.time_boot_ms = data["time_boot_ms"] #mm - self.latitude = data["lat"]/10000000 # decimal degrees - self.longitude = data["lon"]/10000000 # decimal degrees - self.altitude = data["alt"] #mm - self.relative_alt = data["relative_alt"] #mm - self.heading = data["hdg"]/100.0 # decimal degrees - self.vx = data["vx"]/100.0 # meters/second - self.vy = data["vy"]/100.0 # meters/second - self.vz = data["vz"]/100.0 # meters/second - return + # Parse msg data + self.time_boot_ms = data["time_boot_ms"] # mm + self.latitude = data["lat"] / 10000000 # decimal degrees + self.longitude = data["lon"] / 10000000 # decimal degrees + self.altitude = data["alt"] # mm + self.relative_alt = data["relative_alt"] # mm + self.heading = data["hdg"] / 100.0 # decimal degrees + self.vx = data["vx"] / 100.0 # meters/second + self.vy = data["vy"] / 100.0 # meters/second + self.vz = data["vz"] / 100.0 # meters/second + return def GPS_RAW_INT_parser(self, data: dict = None) -> dict: """Selected required data from data received from Pixhawk, and @@ -65,7 +66,7 @@ def GPS_RAW_INT_parser(self, data: dict = None) -> dict: data (dict): Data received from the Pixhawk device Returns: clean_data (dict): Required data to be published - + GPS_FIX_TYPE [Enum] Type of GPS fix @@ -80,43 +81,43 @@ def GPS_RAW_INT_parser(self, data: dict = None) -> dict: 7 GPS_FIX_TYPE_STATIC Static fixed, typically used for base stations 8 GPS_FIX_TYPE_PPP PPP, 3D position. """ - #If data supplied, else use last msg + # If data supplied, else use last msg if data == None: data = self.latest_GPS_RAW_INT_msg.to_dict() - #Check for stale GPS + # Check for stale GPS self.gps_stale_check() - #Update fix type - self.time_usec = data["time_usec"] #UNIX Epoch time uSec - self.gps_fix_type=data["fix_type"] + # Update fix type + self.time_usec = data["time_usec"] # UNIX Epoch time uSec + self.gps_fix_type = data["fix_type"] return - + def gps_stale_check(self): - #Check for stale GPS data - if (time.time() - self.latest_GPS_RAW_INT_timestamp > self.STALE_TIMEOUT) or \ - (time.time() - self.latest_GLOBAL_POSITION_INT_timestamp > self.STALE_TIMEOUT): + # Check for stale GPS data + if (time.time() - self.latest_GPS_RAW_INT_timestamp > self.STALE_TIMEOUT) or ( + time.time() - self.latest_GLOBAL_POSITION_INT_timestamp > self.STALE_TIMEOUT + ): self.gps_stale = True else: self.gps_stale = False - def create_gps_json_payload(self): - #Check for stale GPS + # Check for stale GPS self.gps_stale_check() - #Create payload dict for json - payload={} + # Create payload dict for json + payload = {} payload["gps_stale"] = self.gps_stale payload["gps_fix_type"] = self.gps_fix_type - payload["time_boot_ms"] = self.time_boot_ms #mm + payload["time_boot_ms"] = self.time_boot_ms # mm payload["time_usec"] = self.time_usec payload["latitude"] = self.latitude # decimal degrees payload["longitude"] = self.longitude # decimal degrees - payload["altitude"] = self.altitude #mm - payload["relative_alt"] = self.relative_alt #mm + payload["altitude"] = self.altitude # mm + payload["relative_alt"] = self.relative_alt # mm payload["heading"] = self.heading # decimal degrees payload["vx"] = self.vx # meters/second payload["vy"] = self.vy # meters/second @@ -130,47 +131,69 @@ def handle_mavlink_messages(self): mavlink_connection = mavutil.mavlink_connection("/dev/tty.serial1", 57600) while True: - #msg = mavlink_connection.recv_match(type='GLOBAL_POSITION_INT', blocking=True) + # msg = mavlink_connection.recv_match(type='GLOBAL_POSITION_INT', blocking=True) msg = mavlink_connection.recv_match(blocking=True) - if msg.get_type() == 'GLOBAL_POSITION_INT': + if msg.get_type() == "GLOBAL_POSITION_INT": self.latest_GLOBAL_POSITION_INT_msg = msg - self.latest_GLOBAL_POSITION_INT_timestamp=time.time() - elif msg.get_type() == 'GPS_RAW_INT': + self.latest_GLOBAL_POSITION_INT_timestamp = time.time() + elif msg.get_type() == "GPS_RAW_INT": self.latest_GPS_RAW_INT_msg = msg - self.latest_GPS_RAW_INT_timestamp=time.time() + self.latest_GPS_RAW_INT_timestamp = time.time() msg + # Store the latest GPS data handler mavlink_gps_handler = MAVLINKGPSHandler() + # Routes for latest GPS Data -@app.route('/gps-fix-status', methods=['GET']) +@app.route("/gps-fix-status", methods=["GET"]) def get_latest_gps_fix_status(): if mavlink_gps_handler.latest_GPS_RAW_INT_msg: mavlink_gps_handler.GPS_RAW_INT_parser() - return jsonify({"fix_type":mavlink_gps_handler.gps_fix_type,"gps_stale":mavlink_gps_handler.gps_stale}), 200 + return ( + jsonify( + { + "fix_type": mavlink_gps_handler.gps_fix_type, + "gps_stale": mavlink_gps_handler.gps_stale, + } + ), + 200, + ) else: - return jsonify({'error': 'No GPS data available'}), 404 - -@app.route('/gps-data', methods=['GET']) + return jsonify({"error": "No GPS data available"}), 404 + + +@app.route("/gps-data", methods=["GET"]) def get_latest_gps_data(): if mavlink_gps_handler.latest_GLOBAL_POSITION_INT_msg: mavlink_gps_handler.GLOBAL_POSITION_INT_parser() - msg=mavlink_gps_handler.create_gps_json_payload() + msg = mavlink_gps_handler.create_gps_json_payload() return jsonify(msg), 200 else: - return jsonify({'error': 'No GPS data available'}), 404 - -@app.route('/heading', methods=['GET']) + return jsonify({"error": "No GPS data available"}), 404 + + +@app.route("/heading", methods=["GET"]) def get_latest_heading(): if mavlink_gps_handler.latest_GLOBAL_POSITION_INT_msg: mavlink_gps_handler.GLOBAL_POSITION_INT_parser() - return jsonify({"heading":mavlink_gps_handler.heading,"gps_stale":mavlink_gps_handler.gps_stale}), 200 + return ( + jsonify( + { + "heading": mavlink_gps_handler.heading, + "gps_stale": mavlink_gps_handler.gps_stale, + } + ), + 200, + ) else: - return jsonify({'error': 'No heading data available'}), 404 + return jsonify({"error": "No heading data available"}), 404 + def main(): - app.run(host='0.0.0.0', port=8888) + app.run(host="0.0.0.0", port=8888) + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/utils/mavlink-api/utils/mavlink_serial_test.py b/utils/mavlink-api/utils/mavlink_serial_test.py index 89c127e9..01514873 100644 --- a/utils/mavlink-api/utils/mavlink_serial_test.py +++ b/utils/mavlink-api/utils/mavlink_serial_test.py @@ -12,16 +12,16 @@ try: mav = mavutil.mavlink_connection(serial_port) print("Connected to MAVLink at", serial_port) - + # You can perform further operations here - + # For example, you can continuously read messages while True: message = mav.recv_match(blocking=True) if message is not None: print("Received:", message) - + # You can add more logic here based on the received messages - + except Exception as e: print("Error:", str(e)) From c6dc12b4d6b524ca06567cd80b343c48ef0c8858 Mon Sep 17 00:00:00 2001 From: lk-iqt <112730501+lk-iqt@users.noreply.github.com> Date: Fri, 3 Nov 2023 09:17:29 -0400 Subject: [PATCH 3/9] Update sha --- utils/mavlink-api/Dockerfile | 4 +- utils/mavlink-api/README.md | 2 +- utils/mavlink-api/utils/gps-test.py | 95 +++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 utils/mavlink-api/utils/gps-test.py diff --git a/utils/mavlink-api/Dockerfile b/utils/mavlink-api/Dockerfile index 5cfd0c65..a18223b4 100644 --- a/utils/mavlink-api/Dockerfile +++ b/utils/mavlink-api/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim@sha256:1591aa8c01b5b37ab31dbe5662c5bdcf40c2f1bce4ef1c1fd24802dae3d01052 +FROM python:3.11-slim@sha256:f89d4d260b6a5caa6aa8e0e14b162deb76862890c91779c31f762b22e72a6cef WORKDIR /app ENV PYTHONPATH=${PYTHONPATH}:${PWD} @@ -10,4 +10,4 @@ RUN poetry install --no-dev COPY mavlink-api.py . -ENTRYPOINT ["python3", "-u", "mavlink-api.py"] \ No newline at end of file +ENTRYPOINT ["python3", "mavlink-api.py"] \ No newline at end of file diff --git a/utils/mavlink-api/README.md b/utils/mavlink-api/README.md index 780b1262..53d4775f 100644 --- a/utils/mavlink-api/README.md +++ b/utils/mavlink-api/README.md @@ -14,7 +14,7 @@ udevadm info -a /dev/ttyAMC0 #<--use correct serial port> Test if you have the correct port: ```bash -python3 utils/mavlink_serial_test.py /dev/ttyACM9 +python3 utils/mavlink_serial_test.py /dev/ttyACM0 ``` Create a udev rule to map the hardware to a custom port diff --git a/utils/mavlink-api/utils/gps-test.py b/utils/mavlink-api/utils/gps-test.py new file mode 100644 index 00000000..537496d5 --- /dev/null +++ b/utils/mavlink-api/utils/gps-test.py @@ -0,0 +1,95 @@ +import requests +import json +import logging +import os +import socket +import time +import csv +import time +from datetime import datetime + +import gpsd +import httpx + +def get_adafruit_gps(): + try: + if gpsd.gpsd_stream is None: + gpsd.connect(host="127.0.0.1", port=2947) + packet = gpsd.get_current() + vals= {"timestamp": time.time(), + "position": packet.position(), + "altitude": packet.altitude(), + "gps_time": packet.get_time().timestamp(), + "map_url": packet.map_url(), + "heading": None, + "gps": "fix"} + except (BrokenPipeError, gpsd.NoFixError, AttributeError) as err: + logging.error("could not update with GPS: %s", err) + vals = { + "timestamp": time.time(), + "position": None, + "altitude": None, + "gps_time": None, + "map_url": None, + "heading": None, + "gps": "no", + } + return vals + +def get_pixhawk_gps(): + try: + external_gps_msg = json.loads(httpx.get(f"http://127.0.0.1:8888/gps-data").text) + + vals = { + "timestamp": time.time(), + "position": ( + external_gps_msg["latitude"], + external_gps_msg["longitude"], + ), + "altitude": external_gps_msg["altitude"], + "gps_time": external_gps_msg["time_usec"], + "map_url": None, + "heading": None, + "gps": "fix", + } + + except Exception as err: + logging.error("could not update with external GPS: %s", err) + vals = { + "timestamp": time.time(), + "position": None, + "altitude": None, + "gps_time": None, + "map_url": None, + "heading": None, + "gps": "no", + } + return vals + +def write_to_csv(filename, data): + with open(filename, 'a', newline='') as csvfile: + fieldnames = data.keys() + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + if csvfile.tell() == 0: + writer.writeheader() + writer.writerow(data) + +# Define the file names for the CSV files +adafruit_csv_filename = 'adafruit_gps_data.csv' +pixhawk_csv_filename = 'pixhawk_gps_data.csv' + +while True: + # Get data from Adafruit GPS + adafruit_data = get_adafruit_gps() + if adafruit_data: + write_to_csv(adafruit_csv_filename, adafruit_data) + print("Adafruit GPS data written to CSV") + + # Get data from Pixhawk GPS + pixhawk_data = get_pixhawk_gps() + if pixhawk_data: + write_to_csv(pixhawk_csv_filename, pixhawk_data) + print("Pixhawk GPS data written to CSV") + + # Wait for one minute before the next iteration + time.sleep(60) \ No newline at end of file From e4aa70179561275dae6e853d1a6b3a0246c33fb2 Mon Sep 17 00:00:00 2001 From: lk-iqt <112730501+lk-iqt@users.noreply.github.com> Date: Fri, 3 Nov 2023 09:20:08 -0400 Subject: [PATCH 4/9] new entrypoint --- utils/mavlink-api/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/mavlink-api/Dockerfile b/utils/mavlink-api/Dockerfile index a18223b4..69b98e99 100644 --- a/utils/mavlink-api/Dockerfile +++ b/utils/mavlink-api/Dockerfile @@ -10,4 +10,4 @@ RUN poetry install --no-dev COPY mavlink-api.py . -ENTRYPOINT ["python3", "mavlink-api.py"] \ No newline at end of file +CMD ["python3", "mavlink-api.py"] \ No newline at end of file From 8dad2a8d13783d641f5f8f0b24a59e37c12e8e33 Mon Sep 17 00:00:00 2001 From: lk-iqt <112730501+lk-iqt@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:06:23 -0500 Subject: [PATCH 5/9] Dockerfile hadolint ignore --- utils/mavlink-api/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/mavlink-api/Dockerfile b/utils/mavlink-api/Dockerfile index 69b98e99..b0821564 100644 --- a/utils/mavlink-api/Dockerfile +++ b/utils/mavlink-api/Dockerfile @@ -10,4 +10,5 @@ RUN poetry install --no-dev COPY mavlink-api.py . +# hadolint ignore=DL3003,DL3004 CMD ["python3", "mavlink-api.py"] \ No newline at end of file From eef57e17116972da5bea2ec68da4972645e1885d Mon Sep 17 00:00:00 2001 From: lk-iqt <112730501+lk-iqt@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:13:33 -0500 Subject: [PATCH 6/9] hadolint update --- utils/mavlink-api/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/mavlink-api/Dockerfile b/utils/mavlink-api/Dockerfile index b0821564..724d115e 100644 --- a/utils/mavlink-api/Dockerfile +++ b/utils/mavlink-api/Dockerfile @@ -10,5 +10,5 @@ RUN poetry install --no-dev COPY mavlink-api.py . -# hadolint ignore=DL3003,DL3004 +# hadolint ignore=DL3002,DL3004 CMD ["python3", "mavlink-api.py"] \ No newline at end of file From c621fc107812c38d31f4cc3234728ad664458731 Mon Sep 17 00:00:00 2001 From: lk-iqt <112730501+lk-iqt@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:30:06 -0500 Subject: [PATCH 7/9] Added nosemgrep --- utils/mavlink-api/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/mavlink-api/Dockerfile b/utils/mavlink-api/Dockerfile index 724d115e..d23eec84 100644 --- a/utils/mavlink-api/Dockerfile +++ b/utils/mavlink-api/Dockerfile @@ -1,3 +1,4 @@ +# nosemgrep:github.workflows.config.dockerfile-source-not-pinned FROM python:3.11-slim@sha256:f89d4d260b6a5caa6aa8e0e14b162deb76862890c91779c31f762b22e72a6cef WORKDIR /app @@ -10,5 +11,5 @@ RUN poetry install --no-dev COPY mavlink-api.py . -# hadolint ignore=DL3002,DL3004 +# nosemgrep:github.workflows.config.missing-user CMD ["python3", "mavlink-api.py"] \ No newline at end of file From 9a6aaa951d21b431a0ad4165cfff6e7003d372de Mon Sep 17 00:00:00 2001 From: lk-iqt <112730501+lk-iqt@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:01:01 -0500 Subject: [PATCH 8/9] Pinned poetry --- utils/mavlink-api/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/mavlink-api/Dockerfile b/utils/mavlink-api/Dockerfile index d23eec84..a7596355 100644 --- a/utils/mavlink-api/Dockerfile +++ b/utils/mavlink-api/Dockerfile @@ -4,7 +4,7 @@ FROM python:3.11-slim@sha256:f89d4d260b6a5caa6aa8e0e14b162deb76862890c91779c31f7 WORKDIR /app ENV PYTHONPATH=${PYTHONPATH}:${PWD} -RUN pip3 install poetry +RUN pip3 install poetry==1.1.5 COPY pyproject.toml . RUN poetry config virtualenvs.create false RUN poetry install --no-dev From 6380d0948e0ebbd5c3d68c85dd1704fccf8c2e7f Mon Sep 17 00:00:00 2001 From: lk-iqt <112730501+lk-iqt@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:07:02 -0500 Subject: [PATCH 9/9] port bind issue --- utils/mavlink-api/mavlink-api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/mavlink-api/mavlink-api.py b/utils/mavlink-api/mavlink-api.py index c75866fb..9e2fef96 100644 --- a/utils/mavlink-api/mavlink-api.py +++ b/utils/mavlink-api/mavlink-api.py @@ -192,7 +192,7 @@ def get_latest_heading(): def main(): - app.run(host="0.0.0.0", port=8888) + app.run(host="0.0.0.0", port=8888) # nosec if __name__ == "__main__":