diff --git a/firmware/application/apps/ble_rx_app.cpp b/firmware/application/apps/ble_rx_app.cpp index 5be7e51fe..4345bd954 100644 --- a/firmware/application/apps/ble_rx_app.cpp +++ b/firmware/application/apps/ble_rx_app.cpp @@ -2,6 +2,7 @@ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2017 Furrtek * Copyright (C) 2023 TJ Baginski + * Copyright (C) 2025 Tommaso Ventafridda * * This file is part of PortaPack. * @@ -83,6 +84,50 @@ void reverse_byte_array(uint8_t* arr, int length) { } } +MAC_VENDOR_STATUS lookup_mac_vendor_status(const uint8_t* mac_address, std::string& vendor_name) { + static bool db_checked = false; + static bool db_exists = false; + + if (!db_checked) { + database db; + database::MacAddressDBRecord dummy_record; + int test_result = db.retrieve_macaddress_record(&dummy_record, "000000"); + db_exists = (test_result != DATABASE_NOT_FOUND); + db_checked = true; + } + + if (!db_exists) { + vendor_name = "macaddress.db not found"; + return MAC_DB_NOT_FOUND; + } + + database db; + database::MacAddressDBRecord record; + + // Convert MAC address to hex string + std::string mac_hex = ""; + for (int i = 0; i < 3; i++) { + // Only need first 3 bytes for OUI + mac_hex += to_string_hex(mac_address[i], 2); + } + + int result = db.retrieve_macaddress_record(&record, mac_hex); + + if (result == DATABASE_RECORD_FOUND) { + vendor_name = std::string(record.vendor_name); + return MAC_VENDOR_FOUND; + } else { + vendor_name = "Unknown"; + return MAC_VENDOR_NOT_FOUND; + } +} + +std::string lookup_mac_vendor(const uint8_t* mac_address) { + std::string vendor_name; + lookup_mac_vendor_status(mac_address, vendor_name); + return vendor_name; +} + namespace ui { std::string pdu_type_to_string(ADV_PDU_TYPE type) { @@ -165,7 +210,10 @@ void RecentEntriesTable::draw( line += pad_string_with_spaces(db_spacing) + dbStr; line.resize(target_rect.width() / 8, ' '); - painter.draw_string(target_rect.location(), style, line); + + Style row_style = (entry.vendor_status == MAC_VENDOR_FOUND) ? style : Style{style.font, style.background, Color::grey()}; + + painter.draw_string(target_rect.location(), row_style, line); } BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const BleRecentEntry& entry) @@ -178,10 +226,14 @@ BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const Bl &text_mac_address, &label_pdu_type, &text_pdu_type, + &label_vendor, + &text_vendor, &labels}); text_mac_address.set(to_string_mac_address(entry.packetData.macAddress, 6, false)); text_pdu_type.set(pdu_type_to_string(entry.pduType)); + std::string vendor_name = lookup_mac_vendor(entry.packetData.macAddress); + text_vendor.set(vendor_name); button_done.on_select = [&nav](const ui::Button&) { nav.pop(); @@ -370,6 +422,10 @@ void BleRecentEntryDetailView::paint(Painter& painter) { void BleRecentEntryDetailView::set_entry(const BleRecentEntry& entry) { entry_ = entry; + text_mac_address.set(to_string_mac_address(entry.packetData.macAddress, 6, false)); + text_pdu_type.set(pdu_type_to_string(entry.pduType)); + std::string vendor_name = lookup_mac_vendor(entry.packetData.macAddress); + text_vendor.set(vendor_name); set_dirty(); } @@ -952,6 +1008,11 @@ void BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry, entry.pduType = pdu_type; entry.channelNumber = channel_number; + if (entry.vendor_status == MAC_VENDOR_UNKNOWN) { + std::string vendor_name; + entry.vendor_status = lookup_mac_vendor_status(entry.packetData.macAddress, vendor_name); + } + // Parse Data Section into buffer to be interpretted later. for (int i = 0; i < packet->dataLen; i++) { entry.packetData.data[i] = packet->data[i]; diff --git a/firmware/application/apps/ble_rx_app.hpp b/firmware/application/apps/ble_rx_app.hpp index 95eec5032..5d6ae25ae 100644 --- a/firmware/application/apps/ble_rx_app.hpp +++ b/firmware/application/apps/ble_rx_app.hpp @@ -2,6 +2,7 @@ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2017 Furrtek * Copyright (C) 2023 TJ Baginski + * Copyright (C) 2025 Tommaso Ventafridda * * This file is part of PortaPack. * @@ -33,6 +34,7 @@ #include "ui_record_view.hpp" #include "app_settings.hpp" #include "radio_state.hpp" +#include "database.hpp" #include "log_file.hpp" #include "utility.hpp" #include "usb_serial_thread.hpp" @@ -72,6 +74,13 @@ typedef enum { RESERVED8 = 15 } ADV_PDU_TYPE; +typedef enum { + MAC_VENDOR_UNKNOWN = 0, + MAC_VENDOR_FOUND = 1, + MAC_VENDOR_NOT_FOUND = 2, + MAC_DB_NOT_FOUND = 3 +} MAC_VENDOR_STATUS; + struct BleRecentEntry { using Key = uint64_t; @@ -87,6 +96,7 @@ struct BleRecentEntry { uint16_t numHits; ADV_PDU_TYPE pduType; uint8_t channelNumber; + MAC_VENDOR_STATUS vendor_status; bool entryFound; BleRecentEntry() @@ -105,6 +115,7 @@ struct BleRecentEntry { numHits{}, pduType{}, channelNumber{}, + vendor_status{MAC_VENDOR_UNKNOWN}, entryFound{} { } @@ -152,6 +163,13 @@ class BleRecentEntryDetailView : public View { {9 * 8, 1 * 16, 17 * 8, 16}, "-"}; + Labels label_vendor{ + {{0 * 8, 2 * 16}, "Vendor:", Theme::getInstance()->fg_light->foreground}}; + + Text text_vendor{ + {7 * 8, 2 * 16, 23 * 8, 16}, + "-"}; + Labels labels{ {{0 * 8, 3 * 16}, "Len", Theme::getInstance()->fg_light->foreground}, {{5 * 8, 3 * 16}, "Type", Theme::getInstance()->fg_light->foreground}, diff --git a/firmware/application/database.cpp b/firmware/application/database.cpp index 432453b9f..5905277e0 100644 --- a/firmware/application/database.cpp +++ b/firmware/application/database.cpp @@ -2,6 +2,7 @@ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2016 Furrtek * Copyright (C) 2022 Arjan Onwezen + * Copyright (C) 2025 Tommaso Ventafridda * * This file is part of PortaPack. * @@ -56,6 +57,16 @@ int database::retrieve_aircraft_record(AircraftDBRecord* record, std::string sea return (result); } +int database::retrieve_macaddress_record(MacAddressDBRecord* record, std::string search_term) { + file_path = macaddress_dir / u"macaddress.db"; + index_item_length = 7; + record_length = 64; + + result = retrieve_record(file_path, index_item_length, record_length, record, search_term); + + return (result); +} + int database::retrieve_record(std::filesystem::path file_path, int index_item_length, int record_length, void* record, std::string search_term) { if (search_term.empty()) return DATABASE_RECORD_NOT_FOUND; diff --git a/firmware/application/database.hpp b/firmware/application/database.hpp index 4720da87a..862fa692d 100644 --- a/firmware/application/database.hpp +++ b/firmware/application/database.hpp @@ -2,6 +2,7 @@ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2016 Furrtek * Copyright (C) 2022 Arjan Onwezen + * Copyright (C) 2025 Tommaso Ventafridda * * This file is part of PortaPack. * @@ -60,6 +61,12 @@ class database { int retrieve_aircraft_record(AircraftDBRecord* record, std::string search_term); + struct MacAddressDBRecord { + char vendor_name[64]; // vendor/manufacturer name + }; + + int retrieve_macaddress_record(MacAddressDBRecord* record, std::string search_term); + private: std::filesystem::path file_path = ""; // path including filename int index_item_length = 0; // length of index item @@ -77,4 +84,4 @@ class database { int retrieve_record(std::filesystem::path file_path, int index_item_length, int record_length, void* record, std::string search_term); }; -#endif /*__DATABASE_H__*/ +#endif /*__DATABASE_H__*/ \ No newline at end of file diff --git a/firmware/application/file_path.cpp b/firmware/application/file_path.cpp index f95e7b5b1..a08811fb6 100644 --- a/firmware/application/file_path.cpp +++ b/firmware/application/file_path.cpp @@ -52,3 +52,4 @@ const std::filesystem::path ook_editor_dir = u"OOKFILES"; const std::filesystem::path hopper_dir = u"HOPPER"; const std::filesystem::path subghz_dir = u"SUBGHZ"; const std::filesystem::path waterfalls_dir = u"WATERFALLS"; +const std::filesystem::path macaddress_dir = u"MACADDRESS"; diff --git a/firmware/application/file_path.hpp b/firmware/application/file_path.hpp index 4c64a86a1..0f8548ee0 100644 --- a/firmware/application/file_path.hpp +++ b/firmware/application/file_path.hpp @@ -54,5 +54,6 @@ extern const std::filesystem::path ook_editor_dir; extern const std::filesystem::path hopper_dir; extern const std::filesystem::path subghz_dir; extern const std::filesystem::path waterfalls_dir; +extern const std::filesystem::path macaddress_dir; #endif /* __FILE_PATH_H__ */ diff --git a/firmware/tools/make_macaddress_db/make_macaddress_db.py b/firmware/tools/make_macaddress_db/make_macaddress_db.py new file mode 100644 index 000000000..81338c207 --- /dev/null +++ b/firmware/tools/make_macaddress_db/make_macaddress_db.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# 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 macaddress.db, used for BLE receiver application, using +# https://standards-oui.ieee.org/oui/oui.txt as a source. +# ------------------------------------------------------------------------------------- + +import urllib.request +import unicodedata +import re +from typing import List, Tuple + + +def download_oui_file() -> str: + """Download the OUI file from IEEE""" + url = "https://standards-oui.ieee.org/oui/oui.txt" + print(f"Downloading OUI database from {url}...") + try: + with urllib.request.urlopen(url) as response: + content = response.read().decode("utf-8") + print("Download completed successfully.") + return content + except Exception as e: + print(f"Error downloading OUI file: {e}") + raise + + +def parse_oui_data(content: str) -> List[Tuple[str, str]]: + """Parse the OUI file content and extract MAC prefixes and vendor names""" + entries = [] + lines = content.split("\n") + + for _, line in enumerate(lines): + # Look for lines with (hex) pattern + if "(hex)" in line: + # Extract MAC prefix and vendor name + match = re.match( + r"^([0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2})\s+\(hex\)\s+(.+)$", + line.strip(), + ) + if match: + # Remove dashes: "28-6F-B9" -> "286FB9" + mac_prefix = match.group(1).replace( + "-", "" + ) + vendor_name = match.group(2).strip() + + # Normalize vendor name and limit to 63 characters + vendor_name = ( + unicodedata.normalize("NFKD", vendor_name[:63]) + .encode("ascii", "ignore") + .decode("ascii") + ) + + if mac_prefix and vendor_name: + entries.append((mac_prefix, vendor_name)) + + print(f"Parsed {len(entries)} MAC address entries.") + return entries + + +def create_database( + entries: List[Tuple[str, str]], output_filename: str = "macaddress.db" +): + """Create the binary database file.""" + # Sort entries by MAC prefix for binary search + entries.sort(key=lambda x: x[0]) + + mac_prefixes = bytearray() + vendor_data = bytearray() + row_count = 0 + + for mac_prefix, vendor_name in entries: + # MAC prefix: 6 hex chars + null terminator = 7 bytes + mac_prefixes += bytearray(mac_prefix + "\0", encoding="ascii") + + # Vendor name: pad to 64 bytes + vendor_bytes = vendor_name.encode("ascii", "ignore") + vendor_padding = bytearray("\0" * (64 - len(vendor_bytes)), encoding="ascii") + vendor_data += vendor_bytes + vendor_padding + + row_count += 1 + + # Write database: index section followed by data section + with open(output_filename, "wb") as database: + database.write(mac_prefixes + vendor_data) + + print(f"Created database '{output_filename}' with {row_count} MAC address entries.") + print(f"Index section: {len(mac_prefixes)} bytes") + print(f"Data section: {len(vendor_data)} bytes") + print(f"Total database size: {len(mac_prefixes) + len(vendor_data)} bytes") + + +def main(): + """Main function to create the MAC address database.""" + try: + oui_content = download_oui_file() + entries = parse_oui_data(oui_content) + + if not entries: + print("No valid entries found in OUI file!") + return + + create_database(entries) + + print("MAC address database creation completed successfully!") + + except Exception as e: + print(f"Error creating MAC address database: {e}") + return 1 + + +if __name__ == "__main__": + exit(main()) diff --git a/sdcard/MACADDRESS/macaddress.db b/sdcard/MACADDRESS/macaddress.db new file mode 100644 index 000000000..e3cb6ecf4 Binary files /dev/null and b/sdcard/MACADDRESS/macaddress.db differ