Add vendor name in bluetooth rx app (#2696)

* add macaddress db, add vendor name in bluetooth rx app

* show "missing macaddress.db" instead of unknown if db not found

* bluetooth rx list with colors based on mac vendor

* bug fix
This commit is contained in:
Tommaso Ventafridda 2025-06-16 19:57:58 +02:00 committed by GitHub
parent 18bc2cf11c
commit fa4b74fd6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 231 additions and 2 deletions

View File

@ -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<BleRecentEntries>::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];

View File

@ -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},

View File

@ -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;

View File

@ -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__*/

View File

@ -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";

View File

@ -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__ */

View File

@ -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())

Binary file not shown.