diff --git a/local_backend/routers/settings.py b/local_backend/routers/settings.py index 7cb3890..d11de9a 100644 --- a/local_backend/routers/settings.py +++ b/local_backend/routers/settings.py @@ -37,6 +37,9 @@ VALID_SETTINGS = { "print.font_ingredient": "Font for removed ingredient lines (- marker): SIZE:BOLD:CAPS", "print.font_item_note": "Font for per-item note lines: SIZE:BOLD:CAPS", "print.font_order_note": "Font for order-level notes: SIZE:BOLD:CAPS", + # Beep settings + "print.beep_on_ticket": "Play beep when a kitchen ticket prints: 'true' | 'false'", + "print.beep_pattern": "Beep pattern: 'single' | 'double' | 'triple' | 'long' | 'custom:n1:n2:n3'", } DEFAULTS = { @@ -63,6 +66,8 @@ DEFAULTS = { "print.font_ingredient": "0:0:0", "print.font_item_note": "0:0:0", "print.font_order_note": "0:1:0", + "print.beep_on_ticket": "true", + "print.beep_pattern": "double", } diff --git a/local_backend/routers/system.py b/local_backend/routers/system.py index c2e9fdd..516ffff 100644 --- a/local_backend/routers/system.py +++ b/local_backend/routers/system.py @@ -133,6 +133,15 @@ def test_order_print(printer_id: int, db: Session = Depends(get_db), user: User return {"success": success, "error": error} +@router.post("/printers/test-beep") +def test_beep(printer_id: int, n1: int = 2, n2: int = 2, n3: int = 1, db: Session = Depends(get_db), user: User = Depends(require_manager)): + printer = db.query(Printer).filter(Printer.id == printer_id).first() + if not printer: + raise HTTPException(status_code=404, detail="Printer not found") + success, error = printer_service.send_test_beep(printer.ip_address, printer.port, n1, n2, n3) + return {"success": success, "error": error} + + @router.put("/printers/{printer_id}", response_model=PrinterOut) def update_printer(printer_id: int, body: PrinterUpdate, db: Session = Depends(get_db), user: User = Depends(require_manager)): printer = db.query(Printer).filter(Printer.id == printer_id).first() diff --git a/local_backend/services/printer_service.py b/local_backend/services/printer_service.py index 7a730de..2aa8341 100644 --- a/local_backend/services/printer_service.py +++ b/local_backend/services/printer_service.py @@ -66,6 +66,8 @@ _PRINT_SETTING_KEYS = [ "print.font_ingredient", "print.font_item_note", "print.font_order_note", + "print.beep_on_ticket", + "print.beep_pattern", ] _PRINT_SETTING_DEFAULTS = { @@ -80,6 +82,17 @@ _PRINT_SETTING_DEFAULTS = { "print.font_ingredient": "0:0:0", "print.font_item_note": "0:0:0", "print.font_order_note": "0:1:0", + "print.beep_on_ticket": "true", + "print.beep_pattern": "double", +} + +# Beep patterns: (n1=on×100ms, n2=off×100ms, n3=count) +# Using ESC BEL n1 n2 n3 (0x1B 0x07 n1 n2 n3) +_BEEP_PATTERNS = { + "single": (2, 2, 1), # one 200ms beep + "double": (1, 1, 2), # two short beeps + "triple": (1, 1, 3), # three short beeps + "long": (5, 2, 1), # one 500ms beep } # SIZE byte values (ESC ! base, no bold bit): @@ -179,6 +192,21 @@ def is_spoof_mode() -> bool: db.close() +def send_test_beep(ip: str, port: int, n1: int, n2: int, n3: int) -> Tuple[bool, str]: + """Send a standalone beep-only job. n1=on-time×100ms, n2=off-time×100ms, n3=count.""" + if is_spoof_mode(): + logger.info("Spoof printing ON — dropping test beep") + return True, "" + try: + p = _get_printer(ip, port) + p._raw(bytes([0x1b, 0x07, n1, n2, n3])) + p.close() + return True, "" + except Exception as e: + logger.error("Test beep failed for %s:%s — %s", ip, port, e) + return False, str(e) + + def send_test_print(ip: str, port: int, name: str) -> Tuple[bool, str]: if is_spoof_mode(): logger.info("Spoof printing ON — dropping test print for %s", name) @@ -563,6 +591,22 @@ def _print_kitchen_ticket(p: Network, order: Order, items: List[OrderItem], db: p._raw(b'\x1b\x21\x00') p._raw(b'\n\n\n') + + # Beep before cut so the buzzer fires as the paper advances + beep_enabled = cfg.get("print.beep_on_ticket", "true") == "true" + if beep_enabled: + pattern = cfg.get("print.beep_pattern", "double") + if pattern.startswith("custom:"): + # custom:n1:n2:n3 + try: + _, n1s, n2s, n3s = pattern.split(":") + beep_bytes = (int(n1s), int(n2s), int(n3s)) + except (ValueError, TypeError): + beep_bytes = _BEEP_PATTERNS["double"] + else: + beep_bytes = _BEEP_PATTERNS.get(pattern, _BEEP_PATTERNS["double"]) + p._raw(bytes([0x1b, 0x07, beep_bytes[0], beep_bytes[1], beep_bytes[2]])) + p.cut() diff --git a/manager_dashboard/src/pages/Settings/tabs/PrintFontsTab.jsx b/manager_dashboard/src/pages/Settings/tabs/PrintFontsTab.jsx index f42333b..ed90a86 100644 --- a/manager_dashboard/src/pages/Settings/tabs/PrintFontsTab.jsx +++ b/manager_dashboard/src/pages/Settings/tabs/PrintFontsTab.jsx @@ -42,6 +42,8 @@ const FONT_DEFAULTS = { 'print.font_order_note': '0:1:0', 'print.divider_style': 'dash', 'print.ticket_mode': 'detailed', + 'print.beep_on_ticket': 'true', + 'print.beep_pattern': 'double', } // ── Preview ──────────────────────────────────────────────────────────────── @@ -347,6 +349,189 @@ function TicketModeSection({ value, onChange, isPending, printers }) { ) } +// ── Beep section ─────────────────────────────────────────────────────────── + +const BEEP_PRESETS = [ + { id: 'off', label: 'Χωρίς Ήχο', desc: 'Χωρίς beep κατά την εκτύπωση' }, + { id: 'single', label: 'Μονό', desc: '1× παλμός 200ms' }, + { id: 'double', label: 'Διπλό', desc: '2× σύντομοι παλμοί (προτεινόμενο)' }, + { id: 'triple', label: 'Τριπλό', desc: '3× σύντομοι παλμοί — πιο εντονο' }, + { id: 'long', label: 'Μακρύ', desc: '1× παλμός 500ms' }, + { id: 'custom', label: 'Προσαρμοσμένο', desc: 'Ορίστε τη διάρκεια χειροκίνητα' }, +] + +// Parse beep settings → { preset, n1, n2, n3 } +function decodeBeep(patternVal, enabledVal) { + if (enabledVal === 'false') return { preset: 'off', n1: 2, n2: 2, n3: 1 } + if (!patternVal || patternVal === 'double') return { preset: 'double', n1: 1, n2: 1, n3: 2 } + if (patternVal.startsWith('custom:')) { + const [, n1, n2, n3] = patternVal.split(':') + return { preset: 'custom', n1: parseInt(n1) || 2, n2: parseInt(n2) || 2, n3: parseInt(n3) || 1 } + } + const presetMap = { single: [2,2,1], double: [1,1,2], triple: [1,1,3], long: [5,2,1] } + const [n1, n2, n3] = presetMap[patternVal] ?? [1,1,2] + return { preset: patternVal, n1, n2, n3 } +} + +function BeepSection({ beepEnabled, beepPattern, onChange, isPending, printers }) { + const { preset: initPreset, n1: iN1, n2: iN2, n3: iN3 } = decodeBeep(beepPattern, beepEnabled) + const [preset, setPreset] = useState(initPreset) + const [n1, setN1] = useState(iN1) + const [n2, setN2] = useState(iN2) + const [n3, setN3] = useState(iN3) + const [testPrinter, setTestPrinter] = useState(null) + const [testing, setTesting] = useState(false) + + useEffect(() => { + if (printers.length > 0 && !testPrinter) { + setTestPrinter((printers.find(p => p.is_active) ?? printers[0]).id) + } + }, [printers]) + + // Sync local state if parent settings change (e.g. after save) + useEffect(() => { + const decoded = decodeBeep(beepPattern, beepEnabled) + setPreset(decoded.preset) + setN1(decoded.n1); setN2(decoded.n2); setN3(decoded.n3) + }, [beepPattern, beepEnabled]) + + function handlePresetChange(id) { + setPreset(id) + if (id === 'off') { + onChange('print.beep_on_ticket', 'false') + } else if (id === 'custom') { + onChange('print.beep_on_ticket', 'true') + onChange('print.beep_pattern', `custom:${n1}:${n2}:${n3}`) + } else { + onChange('print.beep_on_ticket', 'true') + onChange('print.beep_pattern', id) + } + } + + function handleCustomChange(field, raw) { + const v = Math.max(1, Math.min(30, parseInt(raw) || 1)) + const next = { n1, n2, n3, [field]: v } + if (field === 'n1') setN1(v) + if (field === 'n2') setN2(v) + if (field === 'n3') setN3(v) + onChange('print.beep_pattern', `custom:${next.n1}:${next.n2}:${next.n3}`) + } + + // Compute beep bytes from current state for test + function getBeepBytes() { + if (preset === 'custom') return { n1, n2, n3 } + const presetMap = { single: [2,2,1], double: [1,1,2], triple: [1,1,3], long: [5,2,1] } + const [bn1, bn2, bn3] = presetMap[preset] ?? [1,1,2] + return { n1: bn1, n2: bn2, n3: bn3 } + } + + async function handleTestBeep() { + if (!testPrinter || preset === 'off') return + const { n1: b1, n2: b2, n3: b3 } = getBeepBytes() + setTesting(true) + try { + const res = await client.post(`/api/system/printers/test-beep?printer_id=${testPrinter}&n1=${b1}&n2=${b2}&n3=${b3}`) + if (res.data.success) toast.success('Beep στάλθηκε!') + else toast.error(`Σφάλμα: ${res.data.error}`) + } catch { + toast.error('Σφάλμα επικοινωνίας') + } finally { + setTesting(false) + } + } + + return ( +
+ Ο εκτυπωτής χτυπά για να ειδοποιεί την κουζίνα σε κάθε νέα παραγγελία. +
+