有个需求,就是找出xlsx表里面的蓝牙mac地址与sn号对应的信息:
1.详细过程: 电脑打开蓝牙扫描的功能,然后加载xlsx表,如果扫描到xlsx存在的mac地址,那么将对应的sn展示在程序gui上。 sn 号可以复制,也同时展示对应的条形码。
2.要求你电脑有蓝牙功能,否则无法使用。
以下代码在py3.8以上测试通过
python
import asyncio
import threading
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import pandas as pd
import re
import json
import os
from bleak import BleakScanner
import barcode
import pyperclip
try:
from barcode.writer import ImageWriter
except ImportError:
from barcode import writer
ImageWriter = writer.ImageWriter
from PIL import Image, ImageTk
# ========== 全局变量 ==========
scanning = False
loop = None
CONFIG_FILE = "ble_scanner_config.json"
current_sn = None
g_sn = ""
# ========== 辅助函数 ==========
def normalize_mac(mac: str) -> str:
if not isinstance(mac, str):
return ""
clean = re.sub(r'[^A-Fa-f0-9]', '', mac)
return clean.upper()
def load_excel(file_path):
try:
df = pd.read_excel(file_path, dtype=str)
except Exception as e:
raise ValueError(f"读取文件失败: {e}")
mac_col = None
sn_col = None
for col in df.columns:
if 'mac' in col.lower():
mac_col = col
if 'sn' in col.lower() or '序列号' in col:
sn_col = col
if mac_col is None:
raise ValueError("未找到包含 'MAC' 的列,请检查列名")
if sn_col is None:
raise ValueError("未找到包含 'SN' 或 '序列号' 的列,请检查列名")
mac_sn_map = {}
for idx, row in df.iterrows():
mac_val = row[mac_col]
sn_val = row[sn_col]
if pd.notna(mac_val) and pd.notna(sn_val):
norm_mac = normalize_mac(str(mac_val))
if norm_mac:
mac_sn_map[norm_mac] = str(sn_val)
return mac_sn_map
def save_config(file_path):
with open(CONFIG_FILE, "w") as f:
json.dump({"excel_path": file_path}, f)
def load_config():
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r") as f:
try:
config = json.load(f)
return config.get("excel_path", "")
except:
return ""
return ""
# ========== 扫描逻辑 ==========
async def scan_ble_once(timeout=3):
devices = await BleakScanner.discover(timeout=timeout, return_adv=True)
result = {}
for addr, (dev, adv_data) in devices.items():
norm_mac = normalize_mac(addr)
if norm_mac:
rssi = adv_data.rssi if adv_data else -100
result[norm_mac] = {
'name': dev.name or '未知',
'rssi': rssi,
'addr': addr
}
return result
async def scan_and_match(gui_callback, rssi_low, rssi_high, mac_sn_map, stop_event):
round_num = 0
while not stop_event.is_set():
round_num += 1
gui_callback(f"🔄 第 {round_num} 轮扫描...")
devices = await scan_ble_once(timeout=3)
gui_callback(f"📡 发现 {len(devices)} 个设备")
found_in_list = False
for mac, info in devices.items():
if mac in mac_sn_map:
found_in_list = True
rssi = info['rssi']
sn = mac_sn_map[mac]
if rssi_low <= rssi <= rssi_high:
gui_callback(f"✅ 找到匹配设备!MAC: {mac}, SN: {sn}, RSSI: {rssi} dBm")
return mac, sn, rssi
else:
gui_callback(f" MAC {mac} 信号 {rssi} dBm 不在范围内 (SN={sn})")
if not found_in_list:
gui_callback(" 本轮未发现列表中的设备")
await asyncio.sleep(1)
return None, None, None
# ========== GUI 控制 ==========
def start_scan(gui_app):
gui_app.text_log.delete('1.0', tk.END)
gui_app.entry_result.delete(0, tk.END)
global scanning
if scanning:
return
scanning = True
try:
low = int(gui_app.entry_low.get())
high = int(gui_app.entry_high.get())
if low > high:
messagebox.showerror("错误", "下限不能大于上限")
scanning = False
return
except ValueError:
messagebox.showerror("错误", "请输入有效的整数")
scanning = False
return
if not gui_app.mac_sn_map:
messagebox.showerror("错误", "请先加载 Excel 文件")
scanning = False
return
gui_app.btn_start.config(state=tk.DISABLED)
gui_app.btn_stop.config(state=tk.NORMAL)
gui_app.btn_barcode.config(state=tk.DISABLED)
gui_app.clear_log_btn.config(state=tk.NORMAL)
gui_app.paste_sn_btn.config(state=tk.NORMAL)
gui_app.clear_barcode_preview()
gui_app.text_log.insert(tk.END, "🔍 开始扫描...\n")
gui_app.text_log.see(tk.END)
gui_app.stop_event = threading.Event()
def run_async():
global scanning, loop, current_sn
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
result = loop.run_until_complete(
scan_and_match(
gui_callback=gui_app.log_message,
rssi_low=low,
rssi_high=high,
mac_sn_map=gui_app.mac_sn_map,
stop_event=gui_app.stop_event
)
)
mac, sn, rssi = result
if mac:
current_sn = sn
global g_sn
g_sn = sn
gui_app.log_message(f"\n🎯 最终结果:MAC={mac}, SN={sn}, RSSI={rssi} dBm")
gui_app.update_result(f"MAC: {mac} | SN: {sn} | RSSI: {rssi} dBm", 'green')
gui_app.btn_barcode.config(state=tk.NORMAL)
else:
current_sn = None
gui_app.log_message("\n⏹ 扫描已停止,未找到匹配设备。")
gui_app.update_result("未找到匹配设备")
gui_app.btn_barcode.config(state=tk.DISABLED)
except Exception as e:
gui_app.log_message(f"❌ 扫描异常: {e}")
gui_app.btn_barcode.config(state=tk.DISABLED)
finally:
scanning = False
gui_app.root.after(0, gui_app.scan_finished)
thread = threading.Thread(target=run_async, daemon=True)
thread.start()
def stop_scan(gui_app):
global scanning
if hasattr(gui_app, 'stop_event'):
gui_app.stop_event.set()
gui_app.log_message("\n⏹ 用户停止扫描...")
gui_app.btn_start.config(state=tk.NORMAL)
gui_app.btn_stop.config(state=tk.DISABLED)
scanning = False
def clear_log(gui_app):
gui_app.text_log.delete('1.0', tk.END)
def paste_sn():
global g_sn
# 复制文本到剪贴板
pyperclip.copy(g_sn)
if(len(g_sn)>0):
messagebox.showinfo("已复制sn", g_sn)
# ========== GUI 主类 ==========
class BluetoothScannerApp:
def __init__(self, root):
self.root = root
root.title("蓝牙 MAC 扫描匹配器")
root.geometry("900x600")
# 定义条码保存目录(程序所在目录下的 barcodes 文件夹)
self.barcode_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "barcodes")
if not os.path.exists(self.barcode_dir):
os.makedirs(self.barcode_dir)
self.mac_sn_map = {}
self.stop_event = None
# 整体左右分栏
main_frame = tk.Frame(root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
main_frame.grid_columnconfigure(0, weight=1) # 左侧
main_frame.grid_columnconfigure(1, weight=1) # 中间
main_frame.grid_columnconfigure(2, weight=1) # 右侧(预览更宽)
main_frame.grid_rowconfigure(0, weight=1)
# ===== 左侧面板 =====
left_frame = tk.Frame(main_frame)
left_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
left_frame.grid_rowconfigure(0, minsize=30) # 日志区占据剩余高度
left_frame.grid_rowconfigure(1, minsize=50) # 日志区占据剩余高度
left_frame.grid_rowconfigure(2, minsize=300) # 日志区占据剩余高度
left_frame.grid_rowconfigure(3, weight=1) # 日志区占据剩余高度
left_frame.grid_columnconfigure(0, weight=1)
# 1. 文件选择
frame_file = tk.Frame(left_frame)
frame_file.grid(row=0, column=0, sticky="ew", pady=(0, 5))
tk.Label(frame_file, text="Excel 文件:").pack(side=tk.LEFT)
self.file_path_var = tk.StringVar()
tk.Entry(frame_file, textvariable=self.file_path_var, width=40).pack(side=tk.LEFT, padx=5)
tk.Button(frame_file, text="浏览...", command=self.select_file, width=15).pack(side=tk.LEFT)
# 2. RSSI 输入(单独一行)
frame_rssi = tk.Frame(left_frame)
frame_rssi.grid(row=1, column=0, sticky="ew", pady=(0, 5))
tk.Label(frame_rssi, text="RSSI 下限 (dBm):").pack(side=tk.LEFT)
self.entry_low = tk.Entry(frame_rssi, width=8)
self.entry_low.pack(side=tk.LEFT, padx=5)
self.entry_low.insert(0, "-80")
tk.Label(frame_rssi, text="上限:").pack(side=tk.LEFT, padx=(10, 0))
self.entry_high = tk.Entry(frame_rssi, width=8)
self.entry_high.pack(side=tk.LEFT, padx=5)
self.entry_high.insert(0, "-30")
self.preview_label = tk.Label(left_frame, text="条码预览区域", font=("Arial", 12),
bg="white", anchor="center")
self.preview_label.grid(row=2, column=0, sticky="nsew", padx=0, pady=10)
self.preview_image = None
self.original_barcode_image = None
# ===== 右侧面板(条码预览) =====
middle_frame = tk.Frame(main_frame, width=80)
middle_frame.grid(row=0, column=1, sticky="nsew", padx=(20, 0))
middle_frame.grid_rowconfigure(0, minsize=100)
middle_frame.grid_rowconfigure(1, minsize=100)
middle_frame.grid_rowconfigure(2, minsize=100)
middle_frame.grid_rowconfigure(3, minsize=100)
middle_frame.grid_rowconfigure(4, weight=1)
middle_frame.grid_columnconfigure(0, weight=1)
# 3. 蓝色结果显示框
self.entry_result = tk.Entry(middle_frame, font=("Arial", 12, "bold"), fg="blue",
bg="white", relief="flat", state="readonly", width=50, justify="left")
self.entry_result.grid(row=0, column=2, sticky="ew", pady=(0, 5))
self.entry_result.insert(0, "等待扫描...")
self.entry_result.config(state='readonly')
# 按钮单独一行
frame_clear_buttons = tk.Frame(middle_frame)
frame_clear_buttons.grid(row=1, column=2, sticky="ew", pady=(0, 15))
self.clear_log_btn = tk.Button(frame_clear_buttons, text="清除日志", command=lambda: clear_log(self),
width=15)
self.clear_log_btn.pack(side=tk.LEFT, padx=15)
# 三个按钮单独一行
frame_buttons = tk.Frame(middle_frame)
frame_buttons.grid(row=2, column=2, sticky="ew", pady=(0, 15))
self.btn_start = tk.Button(frame_buttons, text="开始扫描", command=lambda: start_scan(self), width=15)
self.btn_start.pack(side=tk.LEFT, padx=15)
self.btn_stop = tk.Button(frame_buttons, text="停止", command=lambda: stop_scan(self), state=tk.DISABLED,
width=15)
self.btn_stop.pack(side=tk.LEFT, padx=15)
# 三个按钮单独一行
frame_sn_buttons = tk.Frame(middle_frame)
frame_sn_buttons.grid(row=3, column=2, sticky="ew", pady=(0, 15))
self.btn_barcode = tk.Button(frame_sn_buttons, text="生成条码 (SN)", command=self.generate_barcode,
state=tk.DISABLED, width=15, bg="lightgreen")
self.btn_barcode.pack(side=tk.LEFT, padx=15)
self.paste_sn_btn = tk.Button(frame_sn_buttons, text="复制SN", command=lambda: paste_sn(), width=15)
self.paste_sn_btn.pack(side=tk.LEFT, padx=15)
# ===== 右侧面板(条码预览) =====
right_frame = tk.Frame(main_frame)
right_frame.grid(row=0, column=2, sticky="nsew", padx=(0, 0))
right_frame.grid_rowconfigure(0, minsize=400)
right_frame.grid_rowconfigure(1, weight=1)
right_frame.grid_columnconfigure(0, weight=1)
right_frame.grid_columnconfigure(1, weight=1)
# 5. 日志框(占据剩余高度)
self.text_log = scrolledtext.ScrolledText(right_frame, width=40, height=50, state='normal')
self.text_log.grid(row=0, column=2, sticky="nsew", padx=10, pady=10)
self.text_log.config(font=("Consolas", 9))
# 自动加载上次配置
self.load_last_config()
def update_result(self, text, color='blue'):
self.entry_result.config(state='normal', foreground=color)
self.entry_result.delete(0, tk.END)
self.entry_result.insert(0, text)
self.entry_result.config(state='readonly')
def clear_barcode_preview(self):
self.preview_label.config(image='', text="条码预览区域")
self.preview_image = None
self.original_barcode_image = None
def generate_barcode(self):
global current_sn
if not current_sn:
messagebox.showwarning("警告", "没有可用的 SN,请先扫描匹配设备")
return
try:
code128 = barcode.get_barcode_class('code128')
# 指定系统字体路径(Windows 常见字体)
font_path = "C:/Windows/Fonts/arial.ttf"
# 如果上述路径不存在,可尝试备用
if not os.path.exists(font_path):
font_path = "C:/Windows/Fonts/msyh.ttc" # 微软雅黑
writer_options = {
'module_width': 0.3,
'module_height': 20,
'font_size': 12,
'text_distance': 5,
'quiet_zone': 6.5,
'font_path': font_path # 关键:指定字体
}
barcode_obj = code128(current_sn, writer=ImageWriter())
pil_img = barcode_obj.render(writer_options=writer_options)
self.original_barcode_image = pil_img
# 预览宽度设为 300
base_width = 300
w_percent = base_width / float(pil_img.size[0])
h_size = int(float(pil_img.size[1]) * w_percent)
pil_img_resized = pil_img.resize((base_width, h_size), Image.Resampling.LANCZOS)
self.preview_image = ImageTk.PhotoImage(pil_img_resized)
self.preview_label.config(image=self.preview_image, text="")
except Exception as e:
# 将详细错误信息输出到日志
import traceback
error_msg = traceback.format_exc()
self.log_message(f"❌ 生成条码预览失败:{error_msg}")
messagebox.showerror("错误", f"生成条码预览失败,请查看日志。\n{e}")
return
# 自动保存
filename = f"SN_{current_sn}.png"
save_path = os.path.join(self.barcode_dir, filename)
try:
self.original_barcode_image.save(save_path)
self.log_message(f"✅ 条码已保存到:{save_path}")
except Exception as e:
messagebox.showerror("错误", f"保存条码文件失败:{e}")
def load_last_config(self):
path = load_config()
if path and os.path.exists(path):
self.file_path_var.set(path)
try:
self.mac_sn_map = load_excel(path)
self.log_message(f"✅ 自动加载上次文件,共 {len(self.mac_sn_map)} 条记录")
self.update_result(f"已加载 {len(self.mac_sn_map)} 个设备")
except Exception as e:
self.log_message(f"⚠️ 上次文件加载失败: {e}")
self.mac_sn_map = {}
def select_file(self):
file_path = filedialog.askopenfilename(
title="选择 Excel 文件",
filetypes=[("Excel 文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
)
if file_path:
self.file_path_var.set(file_path)
try:
self.mac_sn_map = load_excel(file_path)
self.log_message(f"✅ 成功加载文件,共 {len(self.mac_sn_map)} 条记录")
self.update_result(f"已加载 {len(self.mac_sn_map)} 个设备")
save_config(file_path)
self.clear_barcode_preview()
except Exception as e:
messagebox.showerror("加载错误", str(e))
self.mac_sn_map = {}
def log_message(self, msg):
self.text_log.insert(tk.END, msg + "\n")
self.text_log.see(tk.END)
def scan_finished(self):
if self.btn_stop['state'] == tk.NORMAL:
self.btn_start.config(state=tk.NORMAL)
self.btn_stop.config(state=tk.DISABLED)
if __name__ == "__main__":
root = tk.Tk()
app = BluetoothScannerApp(root)
root.mainloop()
以上代码均为原创,请勿转载,请联系我删除!!!