Files
adaptronic_label-printer/label_printer_gui.py

1061 lines
39 KiB
Python

"""
Label Printer GUI Application using Kivy
Simplified mobile-friendly interface for printing labels.
"""
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.spinner import Spinner
from kivy.uix.scrollview import ScrollView
from kivy.uix.popup import Popup
from kivy.core.window import Window
from kivy.uix.image import Image as KivyImage
from kivy.graphics import Color, Rectangle
import os
import threading
import platform
import time
import datetime
import glob
import configparser
import subprocess
from print_label import print_label_standalone, get_available_printers
from kivy.clock import Clock
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from PIL import Image, ImageDraw
# Optional system tray support
try:
import pystray
from pystray import MenuItem as item
PYSTRAY_AVAILABLE = True
except ImportError:
PYSTRAY_AVAILABLE = False
print("Warning: pystray not available. System tray functionality disabled.")
# Set window size - portrait/phone dimensions (375x667 like iPhone)
# Adjusted to be slightly wider for touch-friendly UI
Window.size = (420, 700)
class FileMonitorHandler(FileSystemEventHandler):
"""Handler for file system events"""
def __init__(self, app, file_path):
self.app = app
self.file_path = file_path
self.last_modified = 0
def on_modified(self, event):
"""Called when a file is modified"""
if event.src_path == self.file_path:
# Debounce - avoid multiple triggers
current_time = time.time()
if current_time - self.last_modified > 1: # 1 second debounce
self.last_modified = current_time
Clock.schedule_once(lambda dt: self.app.on_file_changed(), 0)
class LabelPrinterApp(App):
"""Simplified Kivy application for label printing with file monitoring"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Initialize conf folder with default templates
self.initialize_conf_folder()
# Build printer display names and mapping to full names
full_printers = get_available_printers()
self.printer_display_map = {} # display_name -> full_name
self.available_printers = []
for full_name in full_printers:
display_name = self._shorten_printer_name(full_name)
# Ensure unique display names
if display_name in self.printer_display_map:
display_name = full_name[:20]
self.printer_display_map[display_name] = full_name
self.available_printers.append(display_name)
# File monitoring variables
self.observer = None
self.monitored_file = None
self.monitoring_active = False
# Configuration file path
self.config_file = os.path.join('conf', 'app.conf')
# System tray variables
self.tray_icon = None
self.is_minimized = False
# Clean old PDF backup files on startup
self.cleanup_old_pdfs()
# Clean old log files on startup
self.cleanup_old_logs()
def _shorten_printer_name(self, name, max_len=20):
"""Shorten printer name for display (max 20 chars).
For network printers like \\\\server\\printer, show just the printer part."""
if name.startswith('\\\\'):
# Network printer: \\server\printer -> extract printer name
parts = name.strip('\\').split('\\')
if len(parts) >= 2:
short = parts[-1] # Just the printer name
else:
short = name
else:
short = name
# Truncate to max_len
if len(short) > max_len:
short = short[:max_len]
return short
def _get_full_printer_name(self, display_name):
"""Resolve display name back to full printer name for printing."""
return self.printer_display_map.get(display_name, display_name)
def initialize_conf_folder(self):
"""
Initialize conf folder with default templates and configuration.
Creates folder and files if they don't exist.
"""
conf_dir = 'conf'
# Create conf folder if it doesn't exist
if not os.path.exists(conf_dir):
os.makedirs(conf_dir, exist_ok=True)
print(f"Created conf folder: {conf_dir}")
# Define default SVG template (minimal 35mm x 25mm with placeholders)
default_svg_template = '''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="35mm" height="25mm" viewBox="0 0 35 25">
<defs>
<style type="text/css">
.text { font-family: Arial; font-weight: bold; font-size: 3.5px; fill: #000000; }
.checkmark { fill: #008000; }
</style>
</defs>
<g id="content">
<text x="2" y="7" class="text">Nr. Comanda: {Article}</text>
<text x="2" y="12" class="text">Nr. Art.: {NrArt}</text>
<text x="2" y="17" class="text">Serial No.: {Serial}</text>
<circle cx="31" cy="7" r="2.5" class="checkmark"/>
<path d="M30,7 L30.8,7.8 L32,6" stroke="white" stroke-width="0.5" fill="none"/>
</g>
</svg>'''
# OK template (green checkmark)
ok_svg_template = '''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="35mm" height="25mm" viewBox="0 0 35 25">
<defs>
<style type="text/css">
.text { font-family: Arial; font-weight: bold; font-size: 3.5px; fill: #000000; }
.checkmark { fill: #008000; }
.ok-text { font-family: Arial; font-weight: bold; font-size: 4px; fill: #008000; }
</style>
</defs>
<g id="content">
<text x="2" y="7" class="text">Nr. Comanda: {Article}</text>
<text x="2" y="12" class="text">Nr. Art.: {NrArt}</text>
<text x="2" y="17" class="text">Serial No.: {Serial}</text>
<text x="29" y="5" class="ok-text">OK</text>
<circle cx="31" cy="12" r="2.5" class="checkmark"/>
<path d="M30,12 L30.8,12.8 L32,11" stroke="white" stroke-width="0.5" fill="none"/>
</g>
</svg>'''
# NOK template (red text and checkmark)
nok_svg_template = '''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="35mm" height="25mm" viewBox="0 0 35 25">
<defs>
<style type="text/css">
.text { font-family: Arial; font-weight: bold; font-size: 3.5px; fill: #CC0000; }
.checkmark { fill: #CC0000; }
.nok-text { font-family: Arial; font-weight: bold; font-size: 4px; fill: #CC0000; }
</style>
</defs>
<g id="content">
<text x="2" y="7" class="text">Nr. Comanda: {Article}</text>
<text x="2" y="12" class="text">Nr. Art.: {NrArt}</text>
<text x="2" y="17" class="text">Serial No.: {Serial}</text>
<text x="27" y="5" class="nok-text">NOK</text>
<circle cx="31" cy="12" r="2.5" class="checkmark"/>
<path d="M30,12 L30.8,12.8 L32,11" stroke="white" stroke-width="0.5" fill="none"/>
</g>
</svg>'''
# Create SVG template files if they don't exist
templates = {
'label_template.svg': default_svg_template,
'label_template_ok.svg': ok_svg_template,
'label_template_nok.svg': nok_svg_template,
}
for filename, content in templates.items():
file_path = os.path.join(conf_dir, filename)
if not os.path.exists(file_path):
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Created template: {file_path}")
except Exception as e:
print(f"Failed to create {file_path}: {e}")
# Create default app.conf if it doesn't exist
config_path = os.path.join(conf_dir, 'app.conf')
if not os.path.exists(config_path):
default_config = '''[Settings]
file_path = C:\\Users\\Public\\Documents\\check.txt
printer = PDF
'''
try:
with open(config_path, 'w', encoding='utf-8') as f:
f.write(default_config)
print(f"Created config: {config_path}")
except Exception as e:
print(f"Failed to create {config_path}: {e}")
def cleanup_old_pdfs(self, days=5):
"""
Delete PDF files older than specified days from pdf_backup folder.
Args:
days (int): Delete files older than this many days (default: 5)
"""
pdf_backup_dir = 'pdf_backup'
# Create folder if it doesn't exist
if not os.path.exists(pdf_backup_dir):
os.makedirs(pdf_backup_dir, exist_ok=True)
return
try:
current_time = time.time()
cutoff_time = current_time - (days * 24 * 3600) # Convert days to seconds
for filename in os.listdir(pdf_backup_dir):
file_path = os.path.join(pdf_backup_dir, filename)
# Only process PDF files
if not filename.endswith('.pdf'):
continue
# Check if file is older than cutoff
if os.path.isfile(file_path):
file_mtime = os.path.getmtime(file_path)
if file_mtime < cutoff_time:
try:
os.remove(file_path)
print(f"Deleted old PDF: {filename}")
except Exception as e:
print(f"Failed to delete {filename}: {e}")
except Exception as e:
print(f"Error during PDF cleanup: {e}")
def cleanup_old_logs(self, days=5):
"""
Delete log files older than specified days from logs folder.
Args:
days (int): Delete files older than this many days (default: 5)
"""
logs_dir = 'logs'
# Create folder if it doesn't exist
if not os.path.exists(logs_dir):
os.makedirs(logs_dir, exist_ok=True)
return
try:
current_time = time.time()
cutoff_time = current_time - (days * 24 * 3600) # Convert days to seconds
for filename in os.listdir(logs_dir):
file_path = os.path.join(logs_dir, filename)
# Process both .log and .csv files
if not (filename.endswith('.log') or filename.endswith('.csv')):
continue
# Check if file is older than cutoff
if os.path.isfile(file_path):
file_mtime = os.path.getmtime(file_path)
if file_mtime < cutoff_time:
try:
os.remove(file_path)
print(f"Deleted old log: {filename}")
except Exception as e:
print(f"Failed to delete {filename}: {e}")
except Exception as e:
print(f"Error during log cleanup: {e}")
def load_config(self):
"""
Load configuration from conf/app.conf file.
Returns dict with 'file_path' and 'printer' keys.
"""
config = {'file_path': '', 'printer': ''}
if not os.path.exists(self.config_file):
return config
try:
parser = configparser.ConfigParser()
parser.read(self.config_file, encoding='utf-8')
if 'Settings' in parser:
config['file_path'] = parser['Settings'].get('file_path', '')
config['printer'] = parser['Settings'].get('printer', '')
print(f"Configuration loaded from {self.config_file}")
except Exception as e:
print(f"Error loading config: {e}")
return config
def save_config(self):
"""
Save current configuration to conf/app.conf file.
Saves file_path and selected printer.
"""
# Ensure conf folder exists
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
try:
parser = configparser.ConfigParser()
parser['Settings'] = {
'file_path': self.file_input.text.strip(),
'printer': self.printer_spinner.text
}
with open(self.config_file, 'w', encoding='utf-8') as f:
parser.write(f)
print(f"Configuration saved to {self.config_file}")
except Exception as e:
print(f"Error saving config: {e}")
def log_print_action(self, article, nr_art, serial, printer, pdf_filename, success):
"""
Log the print action to a CSV log file (table format).
Args:
article (str): Article/Comanda number
nr_art (str): Nr. Art. value
serial (str): Serial number
printer (str): Printer name
pdf_filename (str): Path to the generated PDF file
success (bool): Whether the print was successful
"""
logs_dir = 'logs'
# Create logs folder if it doesn't exist
os.makedirs(logs_dir, exist_ok=True)
try:
# Create log filename with date
log_date = datetime.datetime.now().strftime("%Y%m%d")
log_filename = os.path.join(logs_dir, f"print_log_{log_date}.csv")
# Create log entry values
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
status = "SUCCESS" if success else "FAILED"
# Escape CSV values (handle commas and quotes)
def escape_csv(value):
"""Escape CSV special characters"""
value = str(value)
if ',' in value or '"' in value or '\n' in value:
value = '"' + value.replace('"', '""') + '"'
return value
# Create CSV line
log_line = (
f"{timestamp},{status},"
f"{escape_csv(article)},"
f"{escape_csv(nr_art)},"
f"{escape_csv(serial)},"
f"{escape_csv(printer)},"
f"{escape_csv(pdf_filename)}\n"
)
# Check if file exists to add header on first entry
file_exists = os.path.isfile(log_filename)
# Write to log file
with open(log_filename, 'a', encoding='utf-8') as f:
# Add header if file is new
if not file_exists:
f.write("Timestamp,Status,Article,Nr Art,Serial No,Printer,PDF File\n")
# Append log entry
f.write(log_line)
print(f"Log entry saved to: {log_filename}")
except Exception as e:
print(f"Error saving log entry: {e}")
def build(self):
"""Build the simplified file monitoring UI"""
self.title = "Label Printing - File Monitor"
# Main container - single column layout
main_layout = BoxLayout(orientation='vertical', spacing=8, padding=12)
# Title
title = Label(
text='[b]Label Printing - File Monitor[/b]',
markup=True,
size_hint_y=0.08,
font_size='18sp',
color=(1, 1, 1, 1)
)
main_layout.add_widget(title)
# Scroll view for form fields
scroll = ScrollView(size_hint_y=0.60)
form_layout = GridLayout(cols=1, spacing=8, size_hint_y=None, padding=8)
form_layout.bind(minimum_height=form_layout.setter('height'))
# File path input
file_label = Label(
text='Monitor File Path:',
size_hint_y=None,
height=30,
font_size='12sp'
)
form_layout.add_widget(file_label)
# File path row with input and browse button
file_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=45, spacing=5)
self.file_input = TextInput(
multiline=False,
size_hint_x=0.75,
font_size='12sp',
background_color=(0.95, 0.95, 0.95, 1),
padding=(10, 10),
hint_text='Enter file path to monitor'
)
file_row.add_widget(self.file_input)
browse_button = Button(
text='Browse',
size_hint_x=0.25,
font_size='11sp',
background_color=(0.3, 0.5, 0.7, 1),
background_normal=''
)
browse_button.bind(on_press=self.browse_file)
file_row.add_widget(browse_button)
form_layout.add_widget(file_row)
# Monitoring status
self.status_label = Label(
text='Status: Not monitoring',
size_hint_y=None,
height=30,
font_size='11sp',
color=(1, 0.5, 0, 1)
)
form_layout.add_widget(self.status_label)
# Add spacing
form_layout.add_widget(Label(text='', size_hint_y=None, height=20))
# Printer selection
printer_label = Label(
text='Select Printer:',
size_hint_y=None,
height=30,
font_size='12sp'
)
form_layout.add_widget(printer_label)
printer_spinner = Spinner(
text=self.available_printers[0] if self.available_printers else "No Printers",
values=self.available_printers,
size_hint_y=None,
height=45,
font_size='12sp',
sync_height=True,
)
self.printer_spinner = printer_spinner
# Save config when printer changes
printer_spinner.bind(text=self.on_printer_changed)
form_layout.add_widget(printer_spinner)
scroll.add_widget(form_layout)
main_layout.add_widget(scroll)
# Buttons layout
buttons_layout = BoxLayout(orientation='vertical', size_hint_y=0.30, spacing=5)
# Start/Stop monitoring button
self.monitor_button = Button(
text='START MONITORING',
size_hint_y=0.7,
font_size='14sp',
background_color=(0.2, 0.5, 0.8, 1),
background_normal='',
bold=True
)
self.monitor_button.bind(on_press=self.toggle_monitoring)
buttons_layout.add_widget(self.monitor_button)
# Minimize to tray button
minimize_button = Button(
text='MINIMIZE TO TRAY',
size_hint_y=0.3,
font_size='11sp',
background_color=(0.5, 0.5, 0.5, 1),
background_normal=''
)
minimize_button.bind(on_press=self.minimize_to_tray)
buttons_layout.add_widget(minimize_button)
main_layout.add_widget(buttons_layout)
# Load configuration after UI is built
Clock.schedule_once(lambda dt: self.apply_config(), 0.5)
return main_layout
def apply_config(self):
"""
Load and apply saved configuration.
Auto-start monitoring if both file and printer are configured.
"""
config = self.load_config()
# Apply file path
if config['file_path']:
self.file_input.text = config['file_path']
print(f"Loaded file path: {config['file_path']}")
# Apply printer selection
if config['printer'] and config['printer'] in self.available_printers:
self.printer_spinner.text = config['printer']
print(f"Loaded printer: {config['printer']}")
# Auto-start monitoring if both are configured and file exists
if config['file_path'] and config['printer']:
if os.path.exists(config['file_path']) and os.path.isfile(config['file_path']):
print("Auto-starting monitoring with saved configuration...")
self.start_monitoring()
else:
print(f"Configured file not found: {config['file_path']}")
def on_printer_changed(self, spinner, text):
"""
Called when printer selection changes.
Save configuration.
"""
self.save_config()
def create_tray_icon_image(self):
"""
Create a simple icon for the system tray.
Returns PIL Image.
"""
# Create a 64x64 icon with a simple printer symbol
width = 64
height = 64
image = Image.new('RGB', (width, height), color='white')
draw = ImageDraw.Draw(image)
# Draw a simple printer icon (rectangle with lines)
draw.rectangle([10, 20, 54, 40], fill='blue', outline='black', width=2)
draw.rectangle([15, 40, 49, 50], fill='lightblue', outline='black', width=2)
draw.rectangle([20, 10, 44, 20], fill='lightgray', outline='black', width=1)
return image
def minimize_to_tray(self, instance=None):
"""
Minimize the application to system tray.
"""
if not PYSTRAY_AVAILABLE:
print("System tray not available - window will minimize normally")
return
if self.is_minimized:
return
# Hide the window
Window.hide()
self.is_minimized = True
# Create tray icon if not exists
if not self.tray_icon:
icon_image = self.create_tray_icon_image()
menu = pystray.Menu(
item('Restore', self.restore_from_tray),
item('Exit', self.exit_from_tray)
)
self.tray_icon = pystray.Icon(
'Label Printer',
icon_image,
'Label Printer - Monitoring',
menu
)
# Run tray icon in separate thread
tray_thread = threading.Thread(target=self.tray_icon.run, daemon=True)
tray_thread.start()
def restore_from_tray(self, icon=None, item=None):
"""
Restore the application from system tray.
"""
if not self.is_minimized:
return
# Stop tray icon
if self.tray_icon:
self.tray_icon.stop()
self.tray_icon = None
# Show the window
Clock.schedule_once(lambda dt: Window.show(), 0)
self.is_minimized = False
def exit_from_tray(self, icon=None, item=None):
"""
Exit the application from system tray.
"""
# Stop tray icon
if self.tray_icon:
self.tray_icon.stop()
self.tray_icon = None
# Stop monitoring
if self.observer:
self.observer.stop()
self.observer.join()
# Stop the app
Clock.schedule_once(lambda dt: self.stop(), 0)
def browse_file(self, instance):
"""Open file browser to select file to monitor"""
# On Windows, always use native dialog to avoid Kivy FileChooser win32timezone issues in packaged apps
if os.name == 'nt':
selected_file = self._browse_file_windows_native()
if selected_file:
self.file_input.text = selected_file
self.save_config()
return
from kivy.uix.filechooser import FileChooserListView
content = BoxLayout(orientation='vertical', spacing=10, padding=10)
# File chooser
file_chooser = FileChooserListView(
path=os.path.expanduser('~'),
size_hint=(1, 0.9)
)
content.add_widget(file_chooser)
# Buttons
buttons = BoxLayout(size_hint_y=0.1, spacing=10)
popup = Popup(
title='Select File to Monitor',
content=content,
size_hint=(0.9, 0.9)
)
def select_file(instance):
if file_chooser.selection:
self.file_input.text = file_chooser.selection[0]
# Save config when file is selected
self.save_config()
popup.dismiss()
select_btn = Button(text='Select')
select_btn.bind(on_press=select_file)
buttons.add_widget(select_btn)
cancel_btn = Button(text='Cancel')
cancel_btn.bind(on_press=popup.dismiss)
buttons.add_widget(cancel_btn)
content.add_widget(buttons)
popup.open()
def _browse_file_windows_native(self):
"""Open a native Windows file dialog via PowerShell/.NET and return selected file path."""
ps_script = (
"Add-Type -AssemblyName System.Windows.Forms; "
"$dialog = New-Object System.Windows.Forms.OpenFileDialog; "
"$dialog.Title = 'Select File to Monitor'; "
"$dialog.Filter = 'Text files (*.txt)|*.txt|All files (*.*)|*.*'; "
"$dialog.Multiselect = $false; "
"if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { "
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; "
"Write-Output $dialog.FileName "
"}"
)
try:
creationflags = getattr(subprocess, 'CREATE_NO_WINDOW', 0)
result = subprocess.run(
['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps_script],
capture_output=True,
text=True,
check=False,
creationflags=creationflags
)
selected = result.stdout.strip()
if selected and os.path.isfile(selected):
return selected
if result.stderr.strip():
print(f"Windows file dialog error: {result.stderr.strip()}")
except Exception as e:
print(f"Native Windows browse failed: {e}")
# Fallback: manual path entry popup (no external dependencies)
self.show_manual_path_popup()
return None
def show_manual_path_popup(self):
"""Fallback popup for manually entering monitored file path."""
content = BoxLayout(orientation='vertical', spacing=8, padding=10)
input_box = TextInput(
text=self.file_input.text.strip(),
multiline=False,
hint_text='Enter full file path, e.g. C:\\Users\\Public\\Documents\\check.txt'
)
content.add_widget(Label(text='Browse unavailable. Enter file path manually:'))
content.add_widget(input_box)
buttons = BoxLayout(size_hint_y=0.35, spacing=8)
popup = Popup(title='Set Monitor File', content=content, size_hint=(0.9, 0.4))
def apply_path(_instance):
path = input_box.text.strip()
if path and os.path.isfile(path):
self.file_input.text = path
self.save_config()
popup.dismiss()
self.show_popup('Success', f'File set:\n{path}', auto_dismiss_after=2)
else:
self.show_popup('Error', 'Invalid file path', auto_dismiss_after=3)
ok_btn = Button(text='Apply')
ok_btn.bind(on_press=apply_path)
buttons.add_widget(ok_btn)
cancel_btn = Button(text='Cancel')
cancel_btn.bind(on_press=popup.dismiss)
buttons.add_widget(cancel_btn)
content.add_widget(buttons)
popup.open()
def toggle_monitoring(self, instance):
"""Start or stop file monitoring"""
if not self.monitoring_active:
self.start_monitoring()
else:
self.stop_monitoring()
def start_monitoring(self):
"""Start monitoring the specified file"""
file_path = self.file_input.text.strip()
if not file_path:
self.show_popup("Error", "Please enter a file path to monitor", auto_dismiss_after=3)
return
if not os.path.exists(file_path):
self.show_popup("Error", "File does not exist", auto_dismiss_after=3)
return
if not os.path.isfile(file_path):
self.show_popup("Error", "Path is not a file", auto_dismiss_after=3)
return
try:
# Stop existing observer if any
if self.observer:
self.observer.stop()
self.observer.join()
# Create and start new observer
self.monitored_file = os.path.abspath(file_path)
event_handler = FileMonitorHandler(self, self.monitored_file)
self.observer = Observer()
self.observer.schedule(event_handler, os.path.dirname(self.monitored_file), recursive=False)
self.observer.start()
self.monitoring_active = True
self.monitor_button.text = 'STOP MONITORING'
self.monitor_button.background_color = (0.8, 0.2, 0.2, 1)
self.status_label.text = f'Status: Monitoring {os.path.basename(file_path)}'
self.status_label.color = (0, 1, 0, 1)
self.show_popup("Success", f"Started monitoring:\n{file_path}", auto_dismiss_after=2)
except Exception as e:
self.show_popup("Error", f"Failed to start monitoring:\n{str(e)}", auto_dismiss_after=3)
def stop_monitoring(self):
"""Stop file monitoring"""
try:
if self.observer:
self.observer.stop()
self.observer.join()
self.observer = None
self.monitoring_active = False
self.monitor_button.text = 'START MONITORING'
self.monitor_button.background_color = (0.2, 0.5, 0.8, 1)
self.status_label.text = 'Status: Not monitoring'
self.status_label.color = (1, 0.5, 0, 1)
self.show_popup("Success", "Stopped monitoring", auto_dismiss_after=2)
except Exception as e:
self.show_popup("Error", f"Failed to stop monitoring:\n{str(e)}", auto_dismiss_after=3)
def on_file_changed(self):
"""Called when monitored file changes"""
if not self.monitoring_active or not self.monitored_file:
return
# Automatically print label when file changes
self.print_label(None)
def clear_file_after_print(self):
"""Clear the monitored file and write '-' to prevent re-printing"""
if not self.monitored_file or not os.path.exists(self.monitored_file):
return
try:
with open(self.monitored_file, 'w', encoding='utf-8') as f:
f.write('-')
print(f"Cleared file: {self.monitored_file}")
except Exception as e:
print(f"Error clearing file: {e}")
def read_file_variables(self):
"""Read variables from monitored file
Expected format: article;nr_art;serial;status;count
status: 1=OK, 0=NOK
count: number of labels to print (default=1)
"""
if not self.monitored_file or not os.path.exists(self.monitored_file):
return None, None, None, '1', 1
try:
with open(self.monitored_file, 'r', encoding='utf-8') as f:
content = f.read().strip()
# Skip if file is empty or only contains "-" (cleared marker)
if not content or content == '-':
return None, None, None, '1', 1
# Parse file content - expecting format: article;nr_art;serial;template_type;count
# or key=value pairs on separate lines
lines = content.split('\n')
article = ""
nr_art = ""
serial = ""
status_flag = "1"
count = 1 # Default to 1 label
# Try semicolon-separated format first
if ';' in content:
parts = content.split(';')
if len(parts) >= 1:
article = parts[0].strip()
if len(parts) >= 2:
nr_art = parts[1].strip()
if len(parts) >= 3:
serial = parts[2].strip()
if len(parts) >= 4:
status_flag = parts[3].strip()
if len(parts) >= 5:
try:
count = int(parts[4].strip())
if count < 1:
count = 1
if count > 100: # Safety limit
count = 100
except ValueError:
count = 1
else:
# Try key=value format
for line in lines:
if '=' in line:
key, value = line.split('=', 1)
key = key.strip().lower()
value = value.strip()
if key in ['article', 'articol', 'comanda', 'nr_comanda']:
article = value
elif key in ['nr_art', 'nr-art', 'nrart']:
nr_art = value
elif key in ['serial', 'serial_no', 'serial-no', 'serialno']:
serial = value
elif key in ['status', 'result', 'quality', 'ok', 'is_ok', 'nok']:
status_flag = value
elif key in ['count', 'quantity', 'copies']:
try:
count = int(value)
if count < 1:
count = 1
if count > 100:
count = 100
except ValueError:
count = 1
# Normalize status flag: 1 = OK label, 0 = NOK label
normalized_status = str(status_flag).strip().lower()
if normalized_status in ['0', 'nok', 'no', 'false', 'rejected', 'refused', 'fail']:
status_flag = '0'
else:
status_flag = '1'
return article, nr_art, serial, status_flag, count
except Exception as e:
print(f"Error reading file: {e}")
return None, None, None, '1', 1
def print_label(self, instance):
"""Handle print button press - read from file and print"""
# Read variables from file including status and count
article, nr_art, serial, status_flag, count = self.read_file_variables()
# Resolve display name to full printer name
printer = self._get_full_printer_name(self.printer_spinner.text)
# Validate input
if not article and not nr_art and not serial:
self.show_popup("Error", "No data in file or file not set", auto_dismiss_after=3)
return
# Select template based on status_flag (1=OK, 0=NOK)
if status_flag == '0':
template_path = os.path.join('conf', 'label_template_nok.svg')
template_name = "NOK"
else:
template_path = os.path.join('conf', 'label_template_ok.svg')
template_name = "OK"
# Verify template exists, fallback to default if not
if not os.path.exists(template_path):
template_path = os.path.join('conf', 'label_template.svg')
template_name = "DEFAULT"
# Create combined label text using semicolon separator
# status_flag: 1 = OK label, 0 = NOK label
label_text = f"{article};{nr_art};{serial};{status_flag}"
# Show loading popup with count info
popup = Popup(
title='Printing',
content=BoxLayout(
orientation='vertical',
padding=10,
spacing=10
),
size_hint=(0.8, 0.3)
)
popup.content.add_widget(Label(text=f'Processing {count} label(s)...\nTemplate: {template_name}\nPlease wait'))
popup.open()
# Print in background thread (using PDF by default)
def print_thread():
pdf_filename = None
success = False
all_success = True
try:
# Send all copies in ONE print job to prevent blank labels
# being ejected between separate jobs on thermal printers.
success = print_label_standalone(
label_text, printer, preview=0,
svg_template=template_path, copies=count
)
all_success = success
# Get the PDF filename that was created
# Files are saved to pdf_backup/ with timestamp
pdf_files = glob.glob('pdf_backup/final_label_*.pdf')
if pdf_files:
# Get the most recently created PDF file
pdf_filename = max(pdf_files, key=os.path.getctime)
if all_success:
# Log the successful print action
self.log_print_action(article, nr_art, serial, printer, pdf_filename or "unknown", True)
# Clear the file and write "-" to prevent re-printing
Clock.schedule_once(lambda dt: self.clear_file_after_print(), 0)
# Use Clock.schedule_once to update UI from main thread
Clock.schedule_once(lambda dt: popup.dismiss(), 0)
Clock.schedule_once(lambda dt: self.show_popup("Success", f"{count} label(s) printed successfully!", auto_dismiss_after=3), 0.1)
else:
# Log the failed print action
self.log_print_action(article, nr_art, serial, printer, pdf_filename or "unknown", False)
Clock.schedule_once(lambda dt: popup.dismiss(), 0)
Clock.schedule_once(lambda dt: self.show_popup("Error", "Failed to print label", auto_dismiss_after=3), 0.1)
except Exception as e:
# Log the error
self.log_print_action(article, nr_art, serial, printer, pdf_filename or "unknown", False)
Clock.schedule_once(lambda dt: popup.dismiss(), 0)
Clock.schedule_once(lambda dt: self.show_popup("Error", f"Print error: {str(e)}", auto_dismiss_after=3), 0.1)
thread = threading.Thread(target=print_thread)
thread.daemon = True
thread.start()
def show_popup(self, title, message, auto_dismiss_after=None):
"""Show a popup message
Args:
title (str): Popup title
message (str): Popup message
auto_dismiss_after (float): Seconds after which to auto-dismiss (None = manual dismiss only)
"""
popup = Popup(
title=title,
content=BoxLayout(
orientation='vertical',
padding=10,
spacing=10
),
size_hint=(0.8, 0.4)
)
popup.content.add_widget(Label(text=message))
close_button = Button(text='OK', size_hint_y=0.3)
close_button.bind(on_press=popup.dismiss)
popup.content.add_widget(close_button)
popup.open()
# Auto-dismiss if specified
if auto_dismiss_after:
Clock.schedule_once(lambda dt: popup.dismiss(), auto_dismiss_after)
if __name__ == '__main__':
app = LabelPrinterApp()
app.run()