mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-20 22:49:06 +00:00
c0aa4a1738
* enhance make_airlines_db tool * enhance make_icao24_db tool * update airlinescode (.txt, .db), aircraftdatabase/icao24 (.csv, .db)
296 lines
10 KiB
Python
Executable File
296 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright (C) 2021 ArjanOnwezen
|
|
# Copyright (C) 2025 Tommaso Ventafridda
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2, or (at your option)
|
|
# any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; see the file COPYING. If not, write to
|
|
# the Free Software Foundation, Inc., 51 Franklin Street,
|
|
# Boston, MA 02110-1301, USA.
|
|
#
|
|
|
|
# -------------------------------------------------------------------------------------
|
|
# Create icao24.db, used for ADS-B receiver application, using
|
|
# https://opensky-network.org/datasets/metadata/aircraftDatabase.csv
|
|
# as a source.
|
|
# -------------------------------------------------------------------------------------
|
|
import csv
|
|
import os
|
|
import shutil
|
|
import urllib.request
|
|
from dataclasses import dataclass
|
|
from typing import Dict, Set, Tuple
|
|
|
|
|
|
@dataclass
|
|
class AircraftRecord:
|
|
"""Represents an aircraft record with all relevant fields"""
|
|
|
|
icao24: str
|
|
registration: str
|
|
manufacturer: str
|
|
model: str
|
|
actype: str
|
|
owner: str
|
|
operator: str
|
|
|
|
def __hash__(self):
|
|
return hash(self.icao24)
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, AircraftRecord):
|
|
return False
|
|
return self.icao24 == other.icao24
|
|
|
|
def data_equals(self, other):
|
|
"""Check if all data fields are equal (excluding ICAO24 which is the key)"""
|
|
if not isinstance(other, AircraftRecord):
|
|
return False
|
|
return (
|
|
self.registration == other.registration
|
|
and self.manufacturer == other.manufacturer
|
|
and self.model == other.model
|
|
and self.actype == other.actype
|
|
and self.owner == other.owner
|
|
and self.operator == other.operator
|
|
)
|
|
|
|
|
|
def backup_previous_csv() -> bool:
|
|
"""Backup the existing CSV file if it exists. Returns True if backup was made."""
|
|
if os.path.exists("aircraftDatabase.csv"):
|
|
shutil.copy2("aircraftDatabase.csv", "aircraftDatabase_previous.csv")
|
|
print("Backed up previous CSV file")
|
|
return True
|
|
return False
|
|
|
|
|
|
def download_aircraft_database() -> None:
|
|
"""Download the aircraft database file from OpenSky Network"""
|
|
url = "https://opensky-network.org/datasets/metadata/aircraftDatabase.csv"
|
|
print(f"Downloading aircraft database from {url}...")
|
|
try:
|
|
urllib.request.urlretrieve(url, "aircraftDatabase.csv")
|
|
print("Download completed successfully.")
|
|
except Exception as e:
|
|
print(f"Error downloading aircraft database file: {e}")
|
|
raise
|
|
|
|
|
|
def parse_csv_file(filename: str) -> Dict[str, AircraftRecord]:
|
|
"""Parse CSV file and return a dictionary of records"""
|
|
records = {}
|
|
|
|
if not os.path.exists(filename):
|
|
return records
|
|
|
|
try:
|
|
with open(filename, "rt", encoding="utf-8") as csv_file:
|
|
sorted_lines = sorted(csv_file.readlines()[1:])
|
|
for row in csv.reader(
|
|
sorted_lines,
|
|
quotechar='"',
|
|
delimiter=",",
|
|
quoting=csv.QUOTE_ALL,
|
|
skipinitialspace=True,
|
|
):
|
|
# only store in case enough info is available
|
|
if len(row) == 27 and len(row[0]) == 6 and len(row[1]) > 0:
|
|
icao24_code = row[0][:6].upper()
|
|
registration = row[1][:8]
|
|
manufacturer = row[3][:32]
|
|
model = row[4][:32]
|
|
# in case icao aircraft type isn't, use ac type like BALL for balloon
|
|
if len(row[8]) == 3:
|
|
actype = row[8][:3]
|
|
else:
|
|
actype = row[5][:4]
|
|
owner = row[13][:32]
|
|
operator = row[9][:32]
|
|
|
|
records[icao24_code] = AircraftRecord(
|
|
icao24=icao24_code,
|
|
registration=registration,
|
|
manufacturer=manufacturer,
|
|
model=model,
|
|
actype=actype,
|
|
owner=owner,
|
|
operator=operator,
|
|
)
|
|
except Exception as e:
|
|
print(f"Warning: Could not parse CSV file {filename}: {e}")
|
|
return {}
|
|
|
|
return records
|
|
|
|
|
|
def compare_records(
|
|
old_records: Dict[str, AircraftRecord], new_records: Dict[str, AircraftRecord]
|
|
) -> Tuple[Set[str], Set[str], Set[str]]:
|
|
"""Compare old and new records, return sets of new, deleted, and changed ICAO codes"""
|
|
old_keys = set(old_records.keys())
|
|
new_keys = set(new_records.keys())
|
|
|
|
new_icao_codes = new_keys - old_keys
|
|
deleted_icao_codes = old_keys - new_keys
|
|
|
|
# Check for changes in existing records
|
|
changed_icao_codes = set()
|
|
for icao in old_keys & new_keys:
|
|
if not old_records[icao].data_equals(new_records[icao]):
|
|
changed_icao_codes.add(icao)
|
|
|
|
return new_icao_codes, deleted_icao_codes, changed_icao_codes
|
|
|
|
|
|
def write_database(records: Dict[str, AircraftRecord]) -> int:
|
|
"""Write records to database file using original format"""
|
|
# bytearrays to store icao24 codes and data
|
|
icao24_codes = bytearray()
|
|
data = bytearray()
|
|
row_count = 0
|
|
|
|
# Sort by ICAO code for consistency
|
|
sorted_records = sorted(records.values(), key=lambda r: r.icao24)
|
|
|
|
for record in sorted_records:
|
|
# Encode data fields
|
|
registration = record.registration.encode("ascii", "ignore")
|
|
manufacturer = record.manufacturer.encode("ascii", "ignore")
|
|
model = record.model.encode("ascii", "ignore")
|
|
actype = record.actype.encode("ascii", "ignore")
|
|
owner = record.owner.encode("ascii", "ignore")
|
|
operator = record.operator.encode("ascii", "ignore")
|
|
|
|
# Add ICAO code with null terminator (original format)
|
|
icao24_codes.extend(bytearray(record.icao24 + "\0", encoding="ascii"))
|
|
|
|
# Add padded data fields (original format)
|
|
registration_padding = bytearray(
|
|
"\0" * (9 - len(registration)), encoding="ascii"
|
|
)
|
|
manufacturer_padding = bytearray(
|
|
"\0" * (33 - len(manufacturer)), encoding="ascii"
|
|
)
|
|
model_padding = bytearray("\0" * (33 - len(model)), encoding="ascii")
|
|
actype_padding = bytearray("\0" * (5 - len(actype)), encoding="ascii")
|
|
owner_padding = bytearray("\0" * (33 - len(owner)), encoding="ascii")
|
|
operator_padding = bytearray("\0" * (33 - len(operator)), encoding="ascii")
|
|
|
|
data.extend(
|
|
bytearray(
|
|
registration
|
|
+ registration_padding
|
|
+ manufacturer
|
|
+ manufacturer_padding
|
|
+ model
|
|
+ model_padding
|
|
+ actype
|
|
+ actype_padding
|
|
+ owner
|
|
+ owner_padding
|
|
+ operator
|
|
+ operator_padding
|
|
)
|
|
)
|
|
row_count += 1
|
|
|
|
with open("icao24.db", "wb") as database:
|
|
database.write(icao24_codes + data)
|
|
|
|
return row_count
|
|
|
|
|
|
def create_database() -> None:
|
|
"""Create the ICAO24 database from the downloaded file"""
|
|
# Backup existing CSV before downloading new one
|
|
has_previous = backup_previous_csv()
|
|
|
|
# Download new CSV
|
|
download_aircraft_database()
|
|
|
|
# Parse both CSV files for comparison
|
|
old_records = {}
|
|
if has_previous:
|
|
print("Parsing previous CSV file...")
|
|
old_records = parse_csv_file("aircraftDatabase_previous.csv")
|
|
print(f"Found {len(old_records)} records in previous CSV")
|
|
|
|
print("Parsing new CSV file...")
|
|
new_records = parse_csv_file("aircraftDatabase.csv")
|
|
print(f"Found {len(new_records)} records in new CSV")
|
|
|
|
# Compare records if we have a previous version
|
|
if old_records:
|
|
new_icao_codes, deleted_icao_codes, changed_icao_codes = compare_records(
|
|
old_records, new_records
|
|
)
|
|
|
|
# Print change statistics
|
|
print("\n" + "=" * 50)
|
|
print("CSV CHANGE SUMMARY")
|
|
print("=" * 50)
|
|
print(f"New records: {len(new_icao_codes):>8}")
|
|
print(f"Deleted records: {len(deleted_icao_codes):>8}")
|
|
print(f"Changed records: {len(changed_icao_codes):>8}")
|
|
print(f"Total records: {len(new_records):>8}")
|
|
print("=" * 50)
|
|
|
|
# Show examples of changes (limited to avoid spam)
|
|
if new_icao_codes and len(new_icao_codes) <= 10:
|
|
print(f"\nNew ICAO24 codes: {', '.join(sorted(new_icao_codes))}")
|
|
elif new_icao_codes:
|
|
sample_new = sorted(list(new_icao_codes))[:5]
|
|
print(
|
|
f"\nSample new ICAO24 codes: {', '.join(sample_new)} (and {len(new_icao_codes)-5} more)"
|
|
)
|
|
|
|
if deleted_icao_codes and len(deleted_icao_codes) <= 10:
|
|
print(f"Deleted ICAO24 codes: {', '.join(sorted(deleted_icao_codes))}")
|
|
elif deleted_icao_codes:
|
|
sample_deleted = sorted(list(deleted_icao_codes))[:5]
|
|
print(
|
|
f"Sample deleted ICAO24 codes: {', '.join(sample_deleted)} (and {len(deleted_icao_codes)-5} more)"
|
|
)
|
|
|
|
if changed_icao_codes and len(changed_icao_codes) <= 10:
|
|
print(f"Changed ICAO24 codes: {', '.join(sorted(changed_icao_codes))}")
|
|
elif changed_icao_codes:
|
|
sample_changed = sorted(list(changed_icao_codes))[:5]
|
|
print(
|
|
f"Sample changed ICAO24 codes: {', '.join(sample_changed)} (and {len(changed_icao_codes)-5} more)"
|
|
)
|
|
else:
|
|
print("\nNo previous CSV found - this appears to be the first run")
|
|
|
|
# Create database from new records
|
|
print("\nCreating ICAO24 database...")
|
|
row_count = write_database(new_records)
|
|
print("Total of", row_count, "ICAO codes stored in database")
|
|
|
|
|
|
def cleanup_previous_csv() -> None:
|
|
"""Remove the previous CSV file if it exists"""
|
|
if os.path.exists("aircraftDatabase_previous.csv"):
|
|
os.remove("aircraftDatabase_previous.csv")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
create_database()
|
|
cleanup_previous_csv()
|
|
except Exception as e:
|
|
print(f"Error creating ICAO24 database: {e}")
|
|
else:
|
|
print("ICAO24 database created successfully.")
|