chip-game/Tools/Configapp/modules/serial_writer.py

545 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# modules/serial_writer.py
import tkinter as tk
from tkinter import ttk, messagebox
from pathlib import Path
import sys
import os
from datetime import datetime
import json
# Добавляем родительскую директорию в путь для импорта
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
# Пытаемся импортировать из config.py
import config
SEGMENT_LENGTHS = config.SEGMENT_LENGTHS
DOCUMENTS_PATH = config.DOCUMENTS_PATH
SERIAL_FILE = config.SERIAL_FILE
APP_STYLE = config.APP_STYLE
except ImportError:
# Локальные значения по умолчанию
SEGMENT_LENGTHS = [4, 9, 4, 4]
DOCUMENTS_PATH = Path.home() / "Documents" / "C4"
SERIAL_FILE = DOCUMENTS_PATH / "serialdata.dat"
APP_STYLE = {
'title_font': ("Segoe UI", 12, "bold"),
'entry_font': ("Consolas", 14),
'label_font': ("Segoe UI", 10),
'padding': {'padx': 20, 'pady': 10}
}
class SerialWriterTab:
"""
Класс для создания вкладки ввода серийного номера.
Теперь это не отдельное приложение, а компонент для конфигуратора.
"""
def __init__(self, parent_frame):
"""
Инициализация компонента вкладки.
Args:
parent_frame: Родительский фрейм, куда будет помещен интерфейс
"""
self.parent = parent_frame
self.segment_lengths = SEGMENT_LENGTHS
self.prefixes = ['A', 'D', 'V', 'N'] # Буквенные префиксы для каждого сегмента
self.vars = []
self.entries = []
self._updating = False # защита от рекурсии в trace
# Определяем путь для файла serial.temp (рядом с исполняемым файлом)
self.executable_dir = Path(sys.executable).parent if hasattr(sys, 'frozen') else Path(__file__).parent.parent
self.serial_temp_file = self.executable_dir / "serial.temp"
# Создаем основной контейнер для вкладки
self.container = ttk.Frame(parent_frame, padding=20)
self.container.pack(fill=tk.BOTH, expand=True)
self._build_ui()
# Загружаем данные о последней записи
self.load_last_serial_info()
# Проверяем существующий серийный номер
self._check_existing_serial()
def _build_ui(self):
"""Построение интерфейса вкладки"""
# Заголовок вкладки
title_frame = ttk.Frame(self.container)
title_frame.pack(fill=tk.X, pady=(0, 20))
title_label = ttk.Label(
title_frame,
text='Серийный номер аппарата "ФИШКА"',
font=APP_STYLE['title_font']
)
title_label.pack()
# Разделитель
ttk.Separator(self.container, orient="horizontal").pack(
fill=tk.X, pady=(0, 20)
)
# Фрейм для полей ввода
entry_frame = ttk.Frame(self.container)
entry_frame.pack(pady=(0, 20))
# Поля ввода серийного номера
self._create_entry_fields(entry_frame)
# Кнопка загрузки последнего серийника
self._create_load_last_button(entry_frame)
# Фрейм для кнопок
button_frame = ttk.Frame(self.container)
button_frame.pack(pady=(20, 0))
self._create_buttons(button_frame)
# Панель статуса
self._create_status_panel()
# Устанавливаем фокус на первое поле
self.entries[0].focus_set()
def _create_entry_fields(self, parent):
"""Создание полей ввода для серийного номера"""
col = 0
for i, (length, prefix) in enumerate(zip(self.segment_lengths, self.prefixes)):
# Создаем Label с префиксом
prefix_label = ttk.Label(
parent,
text=prefix,
font=APP_STYLE['entry_font'],
foreground="black"
)
prefix_label.grid(row=0, column=col, padx=(0, 2), pady=(0, 10))
col += 1
# Поле для цифр (длина на 1 меньше, так как первый символ - буква)
var = tk.StringVar()
var.trace_add("write", lambda *args, i=i: self._on_segment_change(i))
entry = ttk.Entry(
parent,
textvariable=var,
width=length, # Один символ уже занят буквой
justify="center",
font=APP_STYLE['entry_font']
)
entry.grid(row=0, column=col, padx=2, pady=(0, 10))
self.vars.append(var)
self.entries.append(entry)
# Навигация Backspace влево
entry.bind("<KeyPress>", lambda e, i=i: self._on_key(e, i))
col += 1
if i != len(self.segment_lengths) - 1:
slash = ttk.Label(parent, text="/", font=APP_STYLE['entry_font'])
slash.grid(row=0, column=col, padx=2, pady=(0, 10))
col += 1
# Enter на последнем поле = Сохранить
self.entries[-1].bind("<Return>", lambda e: self.save_serial())
def _create_load_last_button(self, parent):
"""Создание кнопки загрузки последнего серийного номера"""
self.load_last_btn = ttk.Button(
parent,
text="Загрузить последний",
command=self.load_last_serial,
width=30
)
self.load_last_btn.grid(row=0, column=20, padx=(20, 0), pady=(0, 10))
# Обновляем состояние кнопки
self.update_load_last_button()
def _create_buttons(self, parent):
"""Создание кнопок управления"""
# Кнопка сохранения
save_btn = ttk.Button(
parent,
text="Сохранить серийный номер",
command=self.save_serial,
width=30
)
save_btn.grid(row=0, column=0, padx=(0, 10))
# Кнопка очистки
clear_btn = ttk.Button(
parent,
text="Очистить поля",
command=self.clear_fields,
width=30
)
clear_btn.grid(row=0, column=1, padx=(0, 10))
# Кнопка проверки
check_btn = ttk.Button(
parent,
text="Проверить формат",
command=self.check_format,
width=30
)
check_btn.grid(row=0, column=2)
def _create_status_panel(self):
"""Создание панели статуса"""
status_frame = ttk.LabelFrame(
self.container,
text="Информация",
padding=10
)
status_frame.pack(fill=tk.X, pady=(20, 0))
# Формат номера
format_label = ttk.Label(
status_frame,
text="Формат: A999/D15112025/V999/N999",
font=APP_STYLE['label_font']
)
format_label.pack(anchor=tk.W, pady=(0, 5))
# Информация о последней записи
self.last_record_label = ttk.Label(
status_frame,
text="Последняя запись: не найдена",
font=APP_STYLE['label_font'],
foreground="gray"
)
self.last_record_label.pack(anchor=tk.W, pady=(0, 5))
# Текущий серийный номер
self.current_serial_label = ttk.Label(
status_frame,
text="Текущий серийный номер: не указан",
font=APP_STYLE['label_font'],
foreground="gray"
)
self.current_serial_label.pack(anchor=tk.W, pady=(0, 5))
# Статус заполнения
self.status_label = ttk.Label(
status_frame,
text="Статус: Не заполнено",
font=APP_STYLE['label_font']
)
self.status_label.pack(anchor=tk.W)
def load_last_serial_info(self):
"""Загрузить информацию о последней записи из файла serial.temp"""
try:
if self.serial_temp_file.exists():
with self.serial_temp_file.open("r", encoding="utf-8") as f:
data = json.load(f)
last_date = data.get("last_write_date", "неизвестно")
last_serial = data.get("last_serial", "неизвестно")
self.last_record_label.config(
text=f"Последняя запись: {last_date} - {last_serial}",
foreground="blue"
)
# Обновляем состояние кнопки загрузки
self.update_load_last_button()
return {
"date": last_date,
"serial": last_serial
}
else:
self.last_record_label.config(
text="Последняя запись: не найдена",
foreground="gray"
)
except Exception as e:
print(f"Ошибка при чтении serial.temp: {e}")
self.last_record_label.config(
text="Последняя запись: ошибка чтения",
foreground="red"
)
return None
def save_last_serial_info(self, serial_number):
"""Сохранить информацию о последней записи в файл serial.temp"""
try:
data = {
"last_write_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"last_serial": serial_number
}
with self.serial_temp_file.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# Обновляем информацию в интерфейсе
self.load_last_serial_info()
except Exception as e:
print(f"Ошибка при сохранении serial.temp: {e}")
def load_last_serial(self):
"""Загрузить последний серийный номер из serial.temp в поля ввода"""
info = self.load_last_serial_info()
if info and info.get("serial"):
serial = info["serial"]
# Разбиваем серийный номер на сегменты
segments = serial.split("/")
if len(segments) == len(self.segment_lengths):
for i, seg in enumerate(segments):
if i < len(self.vars):
# Убираем буквенный префикс и устанавливаем только цифры
if len(seg) >= 2: # Буква + хотя бы одна цифра
digits = seg[1:] # Пропускаем первую букву
self.vars[i].set(digits)
self._update_status()
messagebox.showinfo(
"Загрузка",
f"Загружен последний серийный номер:\n{serial}"
)
return True
else:
messagebox.showwarning(
"Ошибка формата",
f"Формат последнего серийного номера не соответствует ожидаемому"
)
else:
messagebox.showinfo(
"Информация",
"Нет данных о последнем серийном номере"
)
return False
def update_load_last_button(self):
"""Обновить состояние кнопки загрузки последнего серийника"""
if self.serial_temp_file.exists():
self.load_last_btn.config(state=tk.NORMAL)
else:
self.load_last_btn.config(state=tk.DISABLED)
def _check_existing_serial(self):
"""Проверить существующий серийный номер при запуске"""
try:
if SERIAL_FILE.exists():
with SERIAL_FILE.open("r", encoding="utf-8") as f:
serial = f.readline().strip()
if serial:
self.current_serial_label.config(
text=f"Текущий серийный номер: {serial}",
foreground="green"
)
except Exception:
pass
def _on_segment_change(self, index: int):
"""Автоматически ограничивает длину сегмента и переносит фокус далее."""
if self._updating:
return
try:
self._updating = True
value = self.vars[index].get()
max_len = self.segment_lengths[index] - 1 # Один символ уже занят буквой
# Разрешаем только цифры
digits_only = ''.join(filter(str.isdigit, value))
# Обрезаем лишние цифры
if len(digits_only) > max_len:
digits_only = digits_only[:max_len]
# Устанавливаем очищенное значение
if digits_only != value:
self.vars[index].set(digits_only)
# Если сегмент заполнен, переходим к следующему
if len(digits_only) == max_len and index + 1 < len(self.entries):
next_entry = self.entries[index + 1]
next_entry.focus_set()
next_entry.icursor("end")
# Обновляем статус
self._update_status()
finally:
self._updating = False
def _on_key(self, event, index: int):
"""Backspace на пустом поле переносит фокус на предыдущий сегмент."""
if event.keysym == "BackSpace":
entry = self.entries[index]
try:
cursor_pos = entry.index(tk.INSERT)
except tk.TclError:
cursor_pos = 0
if cursor_pos == 0 and index > 0 and entry.get() == "":
prev_entry = self.entries[index - 1]
prev_entry.focus_set()
prev_entry.icursor("end")
def _get_segments(self):
"""Получить значения всех сегментов"""
segments = []
for i, (var, prefix) in enumerate(zip(self.vars, self.prefixes)):
digits = var.get().strip()
segments.append(f"{prefix}{digits}")
return segments
def _get_full_serial(self):
"""Получить полный серийный номер"""
segments = self._get_segments()
return "/".join(segments)
def _check_complete(self):
"""Проверить, полностью ли заполнены все сегменты"""
segments = self._get_segments()
for seg, length in zip(segments, self.segment_lengths):
if len(seg) != length:
return False
return True
def _update_status(self):
"""Обновить статус заполнения"""
if self._check_complete():
serial = self._get_full_serial()
self.status_label.config(
text=f"Статус: ✓ Готово к сохранению: {serial}",
foreground="green"
)
else:
segments = self._get_segments()
partial_serial = "/".join(segments) if any(segments) else "Не заполнено"
self.status_label.config(
text=f"Статус: {partial_serial}",
foreground="orange" if any(segments) else "red"
)
def clear_fields(self):
"""Очистить все поля ввода"""
for var in self.vars:
var.set("")
self.entries[0].focus_set()
self.status_label.config(
text="Статус: Не заполнено",
foreground="red"
)
def check_format(self):
"""Проверить формат введённых данных"""
segments = [v.get().strip() for v in self.vars]
if not segments or all(seg == "" for seg in segments):
messagebox.showinfo(
"Проверка формата",
"Поля не заполнены."
)
return
errors = []
for i, (seg, length, prefix) in enumerate(zip(segments, self.segment_lengths, self.prefixes)):
expected_digits = length - 1 # Один символ - буква
if len(seg) != expected_digits:
errors.append(f"Сегмент {prefix}: ожидается {expected_digits} цифр, получено {len(seg)}")
elif not seg.isdigit():
errors.append(f"Сегмент {prefix}: должны быть только цифры")
if errors:
messagebox.showwarning(
"Проверка формата",
"Найдены ошибки:\n\n" + "\n".join(errors)
)
else:
serial = self._get_full_serial()
messagebox.showinfo(
"Проверка формата",
f"✓ Формат корректен!\n\nСерийный номер:\n{serial}"
)
def save_serial(self):
"""Сохранить серийный номер в файл."""
if not self._check_complete():
messagebox.showerror(
"Ошибка",
"Заполните все части серийного номера полностью."
)
return
serial = self._get_full_serial()
try:
# Создаем директорию, если её нет
DOCUMENTS_PATH.mkdir(parents=True, exist_ok=True)
# Добавляем новую строку в конец файла
with SERIAL_FILE.open("w", encoding="utf-8") as f:
f.write(serial + "\n")
# Сохраняем информацию о последней записи
self.save_last_serial_info(serial)
# Обновляем информацию о текущем серийном номере
self.current_serial_label.config(
text=f"Текущий серийный номер: {serial}",
foreground="green"
)
messagebox.showinfo(
"Готово",
f"Серийный номер успешно сохранён!\n\n"
f"Номер: {serial}\n\n"
f"Информация сохранена в:\n{self.serial_temp_file}"
)
# Обновляем статус
self.status_label.config(
text=f"Статус: ✓ Сохранено: {serial}",
foreground="green"
)
except Exception as e:
messagebox.showerror(
"Ошибка сохранения",
f"Не удалось сохранить серийный номер:\n{e}"
)
def get_serial_number(self):
"""Получить текущий серийный номер (для использования другими модулями)"""
if self._check_complete():
return self._get_full_serial()
return None
def load_serial_from_file(self):
"""Загрузить серийный номер из файла (если существует)"""
try:
if SERIAL_FILE.exists():
with SERIAL_FILE.open("r", encoding="utf-8") as f:
serial = f.readline().strip()
segments = serial.split("/")
if len(segments) == len(self.segment_lengths):
for i, seg in enumerate(segments):
if i < len(self.vars):
# Убираем буквенный префикс и устанавливаем только цифры
if len(seg) >= 2: # Буква + хотя бы одна цифра
digits = seg[1:] # Пропускаем первую букву
self.vars[i].set(digits)
self._update_status()
# Обновляем информацию о текущем серийном номере
self.current_serial_label.config(
text=f"Текущий серийный номер: {serial}",
foreground="green"
)
return True
except Exception:
pass
return False