import socket
import requests
import re
import json
import os
import sys
import threading
import tkinter as tk
from tkinter import ttk, messagebox
from PIL import Image
import pystray
from pystray import MenuItem as item
# Функция для поиска ресурсов внутри скомпилированного EXE
def resource_path(relative_path):
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
class HambookBridge:
def __init__(self):
# Файл настроек в папке пользователя
self.config_file = os.path.join(os.path.expanduser("~"), 'hambook_config.json')
self.load_config()
self.root = tk.Tk()
self.root.title("Hambook Bridge V3")
self.root.geometry("550x550")
try:
self.root.iconbitmap(resource_path("icon.ico"))
except:
pass
self.root.protocol('WM_DELETE_WINDOW', self.hide_window)
self.create_widgets()
self.create_context_menu()
self.running = True
# Запуск UDP слушателя
self.thread = threading.Thread(target=self.udp_listener, daemon=True, name="UDP-Thread")
self.thread.start()
def load_config(self):
self.config = {
'api_key': '',
'my_call': '',
'api_url': 'https://hambook.site/api/v1/qso/index.php',
'udp_port': '2233'
}
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r') as f:
self.config.update(json.load(f))
except:
pass
def save_config(self):
self.config['api_key'] = self.entry_key.get().strip()
self.config['my_call'] = self.entry_my_call.get().strip().upper()
self.config['udp_port'] = self.entry_port.get().strip()
try:
with open(self.config_file, 'w') as f:
json.dump(self.config, f)
messagebox.showinfo("Hambook", "Настройки сохранены!")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сохранить: {e}")
def create_context_menu(self):
self.menu = tk.Menu(self.root, tearoff=0)
self.menu.add_command(label="Вырезать", command=lambda: self.root.focus_get().event_generate("<<Cut>>"))
self.menu.add_command(label="Копировать", command=lambda: self.root.focus_get().event_generate("<<Copy>>"))
self.menu.add_command(label="Вставить", command=lambda: self.root.focus_get().event_generate("<<Paste>>"))
def show_menu(event):
if isinstance(event.widget, tk.Entry) or isinstance(event.widget, tk.Text):
self.menu.post(event.x_root, event.y_root)
self.root.bind_class("Entry", "<Button-3>", show_menu)
self.root.bind_class("Text", "<Button-3>", show_menu)
def create_widgets(self):
tab_control = ttk.Notebook(self.root)
# Вкладка ЛОГ
self.tab_log = ttk.Frame(tab_control)
tab_control.add(self.tab_log, text='Лог передач')
self.log_text = tk.Text(self.tab_log, state='disabled', height=18, font=('Consolas', 9))
self.log_text.pack(fill='both', expand=True, padx=10, pady=10)
# Вкладка НАСТРОЙКИ
self.tab_set = ttk.Frame(tab_control)
tab_control.add(self.tab_set, text='Настройки')
container = tk.Frame(self.tab_set, padx=20, pady=20)
container.pack(fill='both')
tk.Label(container, text="API Ключ (из профиля Hambook):", font=('Arial', 9, 'bold')).pack(anchor='w')
self.entry_key = tk.Entry(container, width=50)
self.entry_key.insert(0, self.config.get('api_key', ''))
self.entry_key.pack(fill='x', pady=(5, 15))
tk.Label(container, text="Мой позывной (Station Callsign):", font=('Arial', 9, 'bold')).pack(anchor='w')
self.entry_my_call = tk.Entry(container, width=20)
self.entry_my_call.insert(0, self.config.get('my_call', ''))
self.entry_my_call.pack(anchor='w', pady=(5, 15))
tk.Label(container, text="UDP Порт (MSHV/JTDX):", font=('Arial', 9, 'bold')).pack(anchor='w')
self.entry_port = tk.Entry(container, width=15)
self.entry_port.insert(0, self.config.get('udp_port', '2233'))
self.entry_port.pack(anchor='w', pady=5)
tk.Button(container, text="СОХРАНИТЬ НАСТРОЙКИ", command=self.save_config,
bg='#0d6efd', fg='white', font=('Arial', 10, 'bold'), pady=8).pack(fill='x', pady=20)
tab_control.pack(expand=1, fill="both")
def log(self, message):
self.log_text.config(state='normal')
self.log_text.insert('end', f"{message}\n")
self.log_text.see('end')
self.log_text.config(state='disabled')
def hide_window(self):
self.root.withdraw()
def show_window(self):
self.root.deiconify()
self.root.lift()
def quit_app(self, icon, item):
self.running = False
icon.stop()
self.root.after(0, self.root.destroy)
def udp_listener(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
port = int(self.config.get('udp_port', 2233))
try:
sock.bind(("0.0.0.0", port))
self.log(f"--- Слушаю UDP порт {port} ---")
except Exception as e:
self.log(f"ОШИБКА: Порт {port} занят!")
return
while self.running:
try:
sock.settimeout(1.0)
data, addr = sock.recvfrom(4096)
raw = data.decode('utf-8', errors='ignore')
# Парсинг ADIF тегов
qso = {m[0].lower(): m[2].strip() for m in re.findall(r'<(\w+):(\d+)>([^<]+)', raw)}
if 'call' in qso:
call = qso['call']
self.log(f"QSO: {call} ({qso.get('band', '')} {qso.get('mode', '')})")
# Формируем полный пакет данных для API
payload = {
'key': self.config['api_key'],
'call': call,
'band': qso.get('band', ''),
'mode': qso.get('mode', ''),
'submode': qso.get('submode', ''),
'freq': qso.get('freq', ''),
'rst_sent': qso.get('rst_sent', ''),
'rst_rcvd': qso.get('rst_rcvd', ''),
'qso_date': qso.get('qso_date', ''),
'time_on': qso.get('time_on', ''),
'gridsquare': qso.get('gridsquare', ''),
'qth': qso.get('qth', ''),
'name': qso.get('name', ''),
'country': qso.get('country', ''),
'dxcc': qso.get('dxcc', ''),
'state': qso.get('state', ''),
'cnty': qso.get('cnty', ''),
# Приоритет: ADIF -> Настройки программы
'my_call': qso.get('station_callsign') or qso.get('operator') or self.config.get('my_call', ''),
'operator': qso.get('operator', ''),
'my_gridsquare': qso.get('my_gridsquare', ''),
'my_rig': qso.get('my_rig', ''),
'tx_pwr': qso.get('tx_pwr', ''),
'comment': qso.get('comment', 'Sent via Hambook Bridge')
}
try:
r = requests.post(self.config['api_url'], data=payload, timeout=5)
res = r.json()
if res.get('status') == 'success':
self.log(f" Успешно! ID: {res.get('id')}")
else:
self.log(f" Ошибка сайта: {res.get('message') or res.get('errors')}")
except Exception as e:
self.log(f" Ошибка сети: {str(e)}")
except socket.timeout:
continue
except Exception as e:
self.log(f" Ошибка: {str(e)}")
def run_tray(self):
icon_path = resource_path("icon.ico")
try:
img = Image.open(icon_path) if os.path.exists(icon_path) else Image.new('RGB', (64, 64), (13, 110, 253))
menu = pystray.Menu(item('Открыть Hambook Bridge', self.show_window, default=True), item('Выход', self.quit_app))
self.icon = pystray.Icon("Hambook", img, "Hambook Bridge", menu)
self.icon.run()
except:
pass
if __name__ == "__main__":
app = HambookBridge()
threading.Thread(target=app.run_tray, daemon=True, name="Tray-Thread").start()
app.root.mainloop()
|