#!/data/data/com.termux/files/usr/bin/bash # ============================================ # PARSING DES PARAMÈTRES # ============================================ DOMAIN="" PORT="8899" SECRET="" ENABLE_BOOT=true while [[ $# -gt 0 ]]; do case $1 in --domain) DOMAIN="$2"; shift 2 ;; --port) PORT="$2"; shift 2 ;; --secret) SECRET="$2"; shift 2 ;; --no-boot) ENABLE_BOOT=false; shift ;; *) echo "❌ Paramètre inconnu: $1"; exit 1 ;; esac done if [ -z "$DOMAIN" ]; then echo "" echo "Usage: bash install-auto-v22.sh --domain ladarka.fr [--port 8899] [--secret mysecret] [--no-boot]" echo "" exit 1 fi clear echo "" echo "════════════════════════════════════════════════════════════" echo " 🖨️ INSTALLATION SERVEUR v21 - CORRECTIONS SOCKET 🖨️" echo "════════════════════════════════════════════════════════════" echo " Version v22 - Fixes : socket destroy + garde concurrence" echo " DPI: 203dpi - Zone: 576 points (comme RawBT)" echo " Domaine: $DOMAIN" echo " Port: $PORT" echo "════════════════════════════════════════════════════════════" echo "" # ============================================ # 1. MISE À JOUR TERMUX # ============================================ echo "📦 Mise à jour Termux..." pkg update -y -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" &1 | grep -E "upgraded|installed" || true pkg upgrade -y -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" &1 | grep -E "upgraded|installed" || true # ============================================ # 2. INSTALLATION NODE.JS # ============================================ echo "" echo "📦 Installation Node.js..." if ! command -v node &> /dev/null; then pkg install -y nodejs -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" &1 | grep -E "installed|Setting up" || true fi if ! command -v node &> /dev/null; then echo " Tentative avec nodejs-lts..." pkg install -y nodejs-lts -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" &1 | grep -E "installed|Setting up" || true fi if ! command -v node &> /dev/null; then echo "❌ Erreur: Node.js non installé" echo " Essayez manuellement: pkg install nodejs-lts" exit 1 fi NODE_VERSION=$(node -v) echo "✅ Node.js $NODE_VERSION installé" # ============================================ # 3. NETTOYAGE # ============================================ echo "" echo "🧹 Nettoyage..." pkill -9 node 2>/dev/null || true # Sauvegarder la config heartbeat si elle existe if [ -f ~/printer-server/heartbeat.json ]; then mkdir -p ~/tmp cp ~/printer-server/heartbeat.json ~/tmp/heartbeat_backup.json echo " 💾 Config heartbeat sauvegardée" fi cd ~ rm -rf printer-server print-server server.js 2>/dev/null echo "✅ Nettoyage terminé" # ============================================ # 4. CRÉATION DOSSIER # ============================================ echo "" mkdir -p ~/printer-server cd ~/printer-server # Restaurer la config heartbeat si sauvegardée if [ -f ~/tmp/heartbeat_backup.json ]; then cp ~/tmp/heartbeat_backup.json ~/printer-server/heartbeat.json rm ~/tmp/heartbeat_backup.json echo "✅ Config heartbeat restaurée" fi echo "✅ Dossier créé" # ============================================ # 5. INSTALLATION MODULES NPM # ============================================ echo "" echo "📦 Installation packages npm..." npm install express cors escpos escpos-network axios ws --no-save --silent 2>&1 | grep -E "added" || true echo "✅ Packages installés" # ============================================ # 5b. INSTALLATION TERMUX-API (pour WiFi info) # ============================================ echo "" echo "📦 Vérification termux-api..." if ! command -v termux-wifi-connectioninfo &> /dev/null; then echo " Installation termux-api..." pkg install -y termux-api -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" &1 | grep -E "installed|Setting up" || true echo "✅ termux-api installé" else echo "✅ termux-api déjà présent" fi # ============================================ # 6. CRÉATION SERVEUR v21 # ============================================ echo "" echo "📝 Création serveur v21..." cat > server.js << 'ENDOFSERVER' const express = require('./node_modules/express'); const cors = require('./node_modules/cors'); const escpos = require('./node_modules/escpos'); const escposNetwork = require('./node_modules/escpos-network'); const axios = require('axios'); const os = require('os'); const fs = require('fs'); const path = require('path'); const app = express(); const PORT = 8899; const LOG_FILE = path.join(__dirname, 'server.log'); const LOG_MAX_LINES = 200; // Écriture dans le fichier log (rotation automatique) function writeLog(message) { const line = '[' + new Date().toISOString() + '] ' + message + '\n'; try { fs.appendFileSync(LOG_FILE, line); // Rotation : garder seulement les 200 dernières lignes const content = fs.readFileSync(LOG_FILE, 'utf8'); const lines = content.split('\n').filter(l => l.trim()); if (lines.length > LOG_MAX_LINES) { fs.writeFileSync(LOG_FILE, lines.slice(-LOG_MAX_LINES).join('\n') + '\n'); } } catch(e) {} } // Surcharge console.log pour écrire aussi dans le fichier const _origLog = console.log; console.log = function() { _origLog.apply(console, arguments); const msg = Array.from(arguments).join(' ').replace(/\x1b\[[0-9;]*m/g, ''); // strip ANSI writeLog(msg); }; function getLocalIP() { const interfaces = os.networkInterfaces(); for (const name of Object.keys(interfaces)) { for (const iface of interfaces[name]) { if (iface.family === 'IPv4' && !iface.internal) { return iface.address; } } } return '0.0.0.0'; } app.use(cors()); app.use(express.json({ limit: '10mb' })); // Couleurs ANSI const GREEN = '\x1b[32m\x1b[1m'; const YELLOW = '\x1b[33m\x1b[1m'; const ORANGE = '\x1b[38;5;208m\x1b[1m'; const BLUE = '\x1b[36m\x1b[1m'; const RED = '\x1b[31m\x1b[1m'; const CYAN = '\x1b[96m\x1b[1m'; const RESET = '\x1b[0m'; let printCount = 0; let lastPrintTime = null; let isPrinting = false; let printQueue = []; // file d'attente des impressions // Traite la prochaine impression dans la file function processNextInQueue() { if (isPrinting || printQueue.length === 0) return; const next = printQueue.shift(); next(); } // ─── FIX v21 : fermeture agressive de la socket ─────────────────────────── function forceCloseDevice(device) { try { device.close(); } catch(e) {} try { // escpos-network expose le socket sous ._client ou ._socket selon version const sock = device._client || device._socket || null; if (sock && typeof sock.destroy === 'function') { sock.destroy(); } } catch(e) {} } // ────────────────────────────────────────────────────────────────────────── function cleanLogs() { if (isPrinting) { console.log(YELLOW + '⏳ Nettoyage différé (impression en cours)' + RESET); return; } console.clear(); displayServerInfo(); console.log(CYAN + '🧹 Logs nettoyés à ' + new Date().toLocaleTimeString('fr-FR') + RESET); console.log(''); } function displayServerInfo() { const localIP = getLocalIP(); console.log(''); console.log(GREEN + '═══════════════════════════════════════════════════' + RESET); console.log(GREEN + '🖨️ SERVEUR IMPRESSION v22 - ' + GREEN + 'ONLINE' + RESET); console.log(GREEN + '═══════════════════════════════════════════════════' + RESET); console.log(BLUE + '📡 IP: ' + RESET + localIP); console.log(BLUE + '📡 Port: ' + RESET + PORT); console.log(BLUE + '🌐 URL: ' + RESET + 'http://' + localIP + ':' + PORT); console.log(''); console.log(ORANGE + '🎯 CONFIGURATION v22' + RESET); console.log(ORANGE + '✅ Images PNG complètes (sans fractionnement)' + RESET); console.log(ORANGE + '✅ DPI: 203dpi (comme RawBT)' + RESET); console.log(ORANGE + '✅ Zone: 576 points' + RESET); console.log(ORANGE + '✅ Timeout: 30 secondes' + RESET); console.log(ORANGE + '✅ Socket destroy après chaque impression' + RESET); console.log(ORANGE + '✅ Garde anti-impression simultanée' + RESET); console.log(ORANGE + '🧹 Nettoyage auto: toutes les 2 heures' + RESET); console.log(''); console.log(CYAN + '📊 Stats: ' + printCount + ' impressions - Dernière: ' + (lastPrintTime || 'Aucune') + RESET); console.log(GREEN + '✅ SYSTÈME v22 OPÉRATIONNEL' + RESET); console.log(GREEN + '═══════════════════════════════════════════════════' + RESET); console.log(''); } setInterval(() => { cleanLogs(); }, 2 * 60 * 60 * 1000); // Translitération function transliterate(text) { if (!text) return ''; const map = { 'à':'a','á':'a','â':'a','ã':'a','ä':'a','å':'a', 'è':'e','é':'e','ê':'e','ë':'e', 'ì':'i','í':'i','î':'i','ï':'i', 'ò':'o','ó':'o','ô':'o','õ':'o','ö':'o', 'ù':'u','ú':'u','û':'u','ü':'u', 'ý':'y','ÿ':'y','ñ':'n','ç':'c', 'À':'A','Á':'A','Â':'A','Ã':'A','Ä':'A','Å':'A', 'È':'E','É':'E','Ê':'E','Ë':'E', 'Ì':'I','Í':'I','Î':'I','Ï':'I', 'Ò':'O','Ó':'O','Ô':'O','Õ':'O','Ö':'O', 'Ù':'U','Ú':'U','Û':'U','Ü':'U', 'Ý':'Y','Ñ':'N','Ç':'C', 'æ':'ae','œ':'oe','Æ':'AE','Œ':'OE', '€':'EUR' }; return text.toString().split('').map(c => map[c] || c).join(''); } app.get('/status', (req, res) => { console.log('✅ Status check'); res.json({ status: 'ok', version: '21', localIP: getLocalIP(), features: ['image-complete', 'no-split', 'socket-fix', 'concurrency-guard'], timestamp: new Date().toISOString(), stats: { printCount, lastPrint: lastPrintTime } }); }); // POST /status : même réponse que GET, pour compatibilité PWA (service worker bloque les GET HTTP) app.post('/status', (req, res) => { console.log('✅ Status check (POST)'); res.json({ status: 'ok', version: '21', localIP: getLocalIP(), features: ['image-complete', 'no-split', 'socket-fix', 'concurrency-guard'], timestamp: new Date().toISOString(), stats: { printCount, lastPrint: lastPrintTime } }); }); // POST /logs : retourne les dernières lignes du fichier log (POST pour bypasser service worker) app.post('/logs', (req, res) => { const n = parseInt((req.body && req.body.lines)) || 50; try { if (!fs.existsSync(LOG_FILE)) { return res.json({ lines: ['(aucun log disponible)'] }); } const content = fs.readFileSync(LOG_FILE, 'utf8'); const lines = content.split('\n').filter(l => l.trim()); res.json({ lines: lines.slice(-n), total: lines.length }); } catch(e) { res.status(500).json({ error: e.message }); } }); // POST /restart : redémarre le process Node (nécessite boucle de surveillance) app.post('/restart', (req, res) => { console.log('🔄 Redémarrage demandé depuis l\'interface...'); res.json({ success: true, message: 'Redémarrage en cours...' }); setTimeout(() => { console.log('🔄 Arrêt du process pour redémarrage...'); process.exit(0); }, 500); }); // POST /network-info : infos réseau WiFi via termux-wifi-connectioninfo app.post('/network-info', (req, res) => { const { exec } = require('child_process'); exec('termux-wifi-connectioninfo', { timeout: 5000 }, (error, stdout, stderr) => { if (error) { // termux-api non installé ou erreur return res.json({ available: false, error: error.message, localIP: getLocalIP(), }); } try { const data = JSON.parse(stdout); res.json({ available: true, ssid: data.ssid || '', bssid: data.bssid || '', ip: data.ip || getLocalIP(), rssi: data.rssi || null, frequency_mhz: data.frequency_mhz || null, link_speed_mbps: data.link_speed_mbps || null, supplicant_state: data.supplicant_state || '', }); } catch(e) { res.json({ available: false, error: 'Parse error: ' + e.message, localIP: getLocalIP() }); } }); }); app.get('/test', (req, res) => { console.log(GREEN + '✅ Test réussi' + RESET); res.json({ status: 'ok', version: '21', timestamp: new Date().toISOString() }); }); /** * Vérifie la connectivité TCP d'une imprimante * Body: { ip, port } * FIX v21 : utilisé par WordPress pour afficher le statut en ligne/hors ligne */ app.post('/printer-status', (req, res) => { const { ip, port } = req.body || {}; if (!ip) return res.status(400).json({ online: false, error: 'IP manquante' }); const net = require('net'); const socket = new net.Socket(); let responded = false; const finish = (online, reason) => { if (responded) return; responded = true; socket.destroy(); console.log((online ? GREEN + '🟢' : RED + '🔴') + ' Statut ' + ip + ':' + (port || 9100) + ' → ' + (online ? 'en ligne' : reason) + RESET); res.json({ online, ip, port: port || 9100, reason: reason || null }); }; socket.setTimeout(3000); socket.connect(port || 9100, ip, () => finish(true, 'reachable')); socket.on('timeout', () => finish(false, 'timeout')); socket.on('error', (e) => finish(false, e.code || e.message)); }); app.post('/print', async (req, res) => { // Si une impression est en cours, mettre en file d'attente if (isPrinting) { if (printQueue.length >= 5) { console.log(RED + '⚠️ File d\'attente pleine (5 max) - requête rejetée' + RESET); return res.status(429).json({ success: false, error: 'File d\'attente pleine' }); } console.log(YELLOW + '⏳ Impression en cours - mise en file d\'attente (' + (printQueue.length + 1) + ')' + RESET); printQueue.push(() => handlePrint(req, res)); return; } handlePrint(req, res); }); async function handlePrint(req, res) { isPrinting = true; try { const { type, imageUrl, data } = req.body; console.log(''); console.log('─────────────────────────────────────────'); console.log(BLUE + '📥 Nouvelle requête d\'impression' + RESET); console.log(' Type: ' + (type || 'text')); if (!data || !data.printerConfig) { isPrinting = false; processNextInQueue(); return res.status(400).json({ success: false, error: 'Config manquante' }); } const config = data.printerConfig; console.log(' Imprimante: ' + config.ip + ':' + (config.port || 9100)); const device = new escposNetwork(config.ip, config.port || 9100); const printerDevice = new escpos.Printer(device); await new Promise((resolve, reject) => { device.open(async (error) => { if (error) { console.log(RED + ' ❌ Erreur connexion: ' + error.message + RESET); isPrinting = false; processNextInQueue(); reject(error); return; } try { if (type === 'image' && imageUrl) { console.log(' Mode: ' + YELLOW + 'IMAGE PNG (TIG)' + RESET); console.log(' URL: ' + imageUrl); await printImage(printerDevice, imageUrl); } else { console.log(' Mode: ' + YELLOW + 'TEXTE ESC/POS' + RESET); await generateTicket(printerDevice, data, config.width || 80); } forceCloseDevice(device); printCount++; lastPrintTime = new Date().toISOString(); console.log(GREEN + ' ✅ Impression réussie (#' + printCount + ')' + RESET); console.log('─────────────────────────────────────────'); console.log(''); resolve(); } catch (e) { forceCloseDevice(device); console.log(RED + ' ❌ Erreur impression: ' + e.message + RESET); console.log('─────────────────────────────────────────'); console.log(''); reject(e); } finally { isPrinting = false; processNextInQueue(); } }); }); res.json({ success: true }); } catch (error) { isPrinting = false; processNextInQueue(); console.error(RED + '❌ Erreur: ' + error.message + RESET); res.status(500).json({ success: false, error: error.message }); } } /** * Impression image PNG * FIX v21 : double sécurité setTimeout fallback si printer.close() ne rappelle jamais */ async function printImage(printer, imageUrl) { return new Promise(async (resolve, reject) => { try { console.log(' 📥 Téléchargement image...'); const response = await axios.get(imageUrl, { responseType: 'arraybuffer', timeout: 60000 }); const imageBuffer = Buffer.from(response.data); console.log(' ✅ Image téléchargée (' + (imageBuffer.length / 1024).toFixed(2) + ' KB)'); const tmpFile = path.join('/data/data/com.termux/files/usr/tmp', 'ticket_' + Date.now() + '.png'); fs.writeFileSync(tmpFile, imageBuffer); console.log(' 🖨️ Envoi à l\'imprimante...'); escpos.Image.load(tmpFile, function(image) { printer.align('CT'); printer.image(image); console.log(' 📄 Avance papier...'); printer.feed(6); console.log(' ✂️ Envoi commande découpe'); printer.cut(); console.log(' 💾 Envoi des données à l\'imprimante...'); printer.flush(); let settled = false; // FIX v21 : timeout de sécurité si printer.close() ne rappelle jamais const fallbackTimer = setTimeout(() => { if (settled) return; settled = true; try { fs.unlinkSync(tmpFile); } catch (e) {} console.log(' ✅ Image imprimée (timeout fallback)'); resolve(); }, 12000); setTimeout(() => { printer.close(() => { if (settled) return; settled = true; clearTimeout(fallbackTimer); try { fs.unlinkSync(tmpFile); } catch (e) {} console.log(' ✅ Image imprimée avec découpe'); resolve(); }); }, 10000); }); } catch (err) { console.error(RED + '❌ Erreur image: ' + err.message + RESET); reject(err); } }); } /** * Génération ticket ESC/POS texte */ async function generateTicket(printer, data, width) { return new Promise(async (resolve, reject) => { try { const chars = width === 58 ? 32 : (width === 80 ? 48 : 42); printer.encode('CP858'); printer.align('ct'); printer.style('normal'); // EN-TÊTE if (data.establishment) { if (data.establishment.name) { printer.style('b').size(1, 1); printer.text(transliterate(data.establishment.name)); printer.style('normal').size(0, 0); } if (data.establishment.address) printer.text(transliterate(data.establishment.address)); if (data.establishment.phone) printer.text('Tel: ' + data.establishment.phone); printer.text(''); } // NUMÉRO COMMANDE if (data.orderNumber) { printer.text('='.repeat(chars)); printer.style('b').size(1, 1); printer.text('COMMANDE N' + data.orderNumber); printer.style('normal').size(0, 0); printer.text('='.repeat(chars)); printer.text(''); } // TYPE LIVRAISON if (data.deliveryType) { printer.style('b').size(1, 0); printer.text(data.deliveryType.toUpperCase()); printer.style('normal').size(0, 0); } if (data.deliveryTime) printer.text('Heure: ' + data.deliveryTime); printer.text('='.repeat(chars)); printer.text(''); // CLIENT if (data.customer) { printer.align('lt').text('CLIENT:'); if (data.customer.name) { const nameLine = transliterate(data.customer.name); printer.style('b').size(1, 0).text(nameLine); printer.style('normal').size(0, 0); } if (data.customer.address) { const addressLine = transliterate(data.customer.address); printer.style('b').size(1, 0).text(addressLine); printer.style('normal').size(0, 0); const cityLine = transliterate((data.customer.postcode || '') + ' ' + (data.customer.city || '')).trim(); printer.style('b').size(1, 0).text(cityLine); printer.style('normal').size(0, 0); printer.text(''); } if (data.customer.phone) { printer.size(1, 0).text('Tel: ' + data.customer.phone); printer.size(0, 0); } printer.text('-'.repeat(chars)); printer.text(''); printer.align('ct'); } // ARTICLES if (data.items && data.items.length > 0) { printer.align('lt'); data.items.forEach(item => { const productLine = item.quantity + ' x ' + transliterate(item.name); const priceStr = parseFloat(item.price).toFixed(2).replace('.', ',') + ' EUR'; const spaces = chars - productLine.length - priceStr.length; printer.raw(Buffer.from([0x1B, 0x45, 0x01])); printer.raw(Buffer.from(productLine + ' '.repeat(Math.max(0, spaces)))); printer.raw(Buffer.from([0x1B, 0x45, 0x00])); printer.text(priceStr); if (item.options && item.options.length > 0) { item.options.forEach(opt => { printer.text(''); printer.text(' ' + transliterate(opt.label).toUpperCase()); if (opt.items && opt.items.length > 0) { opt.items.forEach(sub => { const subLine = ' ' + sub.quantity + 'x ' + transliterate(sub.name); const subPrice = parseFloat(sub.price).toFixed(2).replace('.', ',') + ' EUR'; const subSpaces = chars - subLine.length - subPrice.length; printer.text(subLine + ' '.repeat(Math.max(1, subSpaces)) + subPrice); }); } }); } if (item.comment) printer.text('Note: ' + transliterate(item.comment)); printer.text(''); }); printer.text('-'.repeat(chars)); printer.text(''); printer.align('ct'); } // TOTAUX if (data.totals) { printer.align('rt'); if (data.totals.subtotal) { printer.text('Total HT: ' + parseFloat(data.totals.subtotal).toFixed(2).replace('.', ',') + ' EUR'); } if (data.totals.tax) { printer.text('TVA: ' + parseFloat(data.totals.tax).toFixed(2).replace('.', ',') + ' EUR'); } printer.text('='.repeat(chars)); printer.style('b').size(1, 1); printer.text('TOTAL TTC: ' + parseFloat(data.totals.total).toFixed(2).replace('.', ',') + ' EUR'); printer.style('normal').size(0, 0); } else if (data.total) { printer.align('rt'); printer.text('='.repeat(chars)); printer.style('b').size(1, 1); printer.text('TOTAL: ' + parseFloat(data.total).toFixed(2).replace('.', ',') + ' EUR'); printer.style('normal').size(0, 0); } printer.align('ct'); if (data.date || data.printDate) printer.text(data.printDate || data.date); printer.text('='.repeat(chars)); if (data.establishment && data.establishment.name) { printer.text(''); printer.style('b').text(transliterate(data.establishment.name) + ' vous remercie !'); printer.style('normal'); } printer.text(''); printer.feed(4); printer.cut(); printer.close(); resolve(); } catch (err) { console.error(RED + '❌ Erreur ticket: ' + err.message + RESET); reject(err); } }); } app.listen(PORT, '0.0.0.0', () => { displayServerInfo(); // Démarrer le heartbeat WordPress après 10s setTimeout(startHeartbeat, 10000); // Connexion WebSocket vers le VPS après 8s (après chargement heartbeat.json) setTimeout(connectVPS, 8000); }); // ── WEBSOCKET CLIENT → VPS ToHome ───────────────────────────────────────── const WS_VPS_URL = 'wss://ws.tohome.fr'; let wsClient = null; let wsReady = false; let wsRetryTimer = null; function connectVPS() { if (wsClient) { try { wsClient.terminate(); } catch(e) {} } const WebSocket = require('ws'); // Extraire le domaine depuis heartbeatUrl pour le siteId const siteId = heartbeatUrl ? heartbeatUrl.replace(/^https?:\/\//, '').replace(/\/.*$/, '').replace(/^www\./, '') : getLocalIP(); console.log(CYAN + '🔌 Connexion VPS WebSocket: ' + WS_VPS_URL + ' (siteId: ' + siteId + ')' + RESET); wsClient = new WebSocket(WS_VPS_URL); wsClient.on('open', () => { wsReady = true; console.log(GREEN + '✅ VPS WebSocket connecté' + RESET); // S'identifier auprès du VPS wsClient.send(JSON.stringify({ type: 'identify', siteId })); // Le heartbeat sera envoyé après réception de 'identified' // Keepalive ping toutes les 30s pour maintenir la connexion active if (wsClient._pingTimer) clearInterval(wsClient._pingTimer); wsClient._pingTimer = setInterval(() => { if (wsClient && wsClient.readyState === 1) { try { wsClient.ping(); } catch(e) {} } }, 30000); }); wsClient.on('message', (message) => { try { const data = JSON.parse(message); console.log(CYAN + '📨 Commande VPS: ' + data.type + RESET); if (data.type === 'identified') { // Envoyer un heartbeat immédiat après identification confirmée sendWsHeartbeat(siteId); } else if (data.type === 'restart') { console.log(ORANGE + '🔄 Redémarrage demandé depuis le VPS...' + RESET); setTimeout(() => process.exit(0), 500); } else if (data.type === 'print' && data.printId && data.payload) { console.log(CYAN + '🖨️ Impression demandée via VPS: ' + data.printId + RESET); // Traiter l'impression localement const fakeReq = { body: data.payload }; const fakeRes = { json: (result) => { // Renvoyer le résultat au VPS if (wsClient && wsReady) { wsClient.send(JSON.stringify({ type: 'print_result', printId: data.printId, result: result, })); } }, status: (code) => ({ json: (result) => { if (wsClient && wsReady) { wsClient.send(JSON.stringify({ type: 'print_result', printId: data.printId, result: { ...result, httpCode: code }, })); } }}) }; handlePrint(fakeReq, fakeRes); // Reconfigurer le heartbeat seulement si wpUrl est fourni dans ce message if (data.wpUrl) { heartbeatUrl = data.wpUrl; heartbeatSecret = data.secret || heartbeatSecret; try { fs.writeFileSync(HEARTBEAT_CONFIG_FILE, JSON.stringify({ wpUrl: heartbeatUrl, secret: heartbeatSecret, printers: heartbeatPrinters })); } catch(e) {} console.log(CYAN + '💓 Heartbeat reconfiguré via VPS → ' + heartbeatUrl + RESET); sendHeartbeat(); } } } catch(e) {} }); wsClient.on('close', () => { wsReady = false; if (wsClient._pingTimer) { clearInterval(wsClient._pingTimer); wsClient._pingTimer = null; } console.log(YELLOW + '📴 VPS WebSocket déconnecté — reconnexion dans 30s...' + RESET); wsRetryTimer = setTimeout(connectVPS, 30000); }); wsClient.on('error', (err) => { wsReady = false; console.log(YELLOW + '⚠️ VPS WebSocket erreur: ' + err.message + RESET); }); } async function sendWsHeartbeat(siteId) { if (!wsClient || !wsReady) return; try { // Vérifier les imprimantes en parallèle let printersStatus = []; if (heartbeatPrinters.length > 0) { const net = require('net'); printersStatus = await Promise.all(heartbeatPrinters.map(p => { if (!p.ip) return Promise.resolve({ name: p.name||p.type, type: p.type, ip: '', online: null }); return new Promise(resolve => { const sock = new net.Socket(); let done = false; const finish = (online) => { if (done) return; done = true; sock.destroy(); resolve({ name: p.name||p.type, type: p.type, ip: p.ip, port: p.port||9100, online }); }; sock.setTimeout(3000); sock.connect(p.port||9100, p.ip, () => finish(true)); sock.on('timeout', () => finish(false)); sock.on('error', () => finish(false)); }); })); } // Récupérer les infos réseau WiFi let networkInfo = { available: false }; try { await new Promise((resolve) => { const { exec } = require('child_process'); exec('termux-wifi-connectioninfo', { timeout: 4000 }, (error, stdout) => { if (!error && stdout) { try { const d = JSON.parse(stdout); networkInfo = { available: true, ssid: d.ssid || '', ip: d.ip || getLocalIP(), rssi: d.rssi || null, frequency_mhz: d.frequency_mhz || null, link_speed_mbps: d.link_speed_mbps || null, }; } catch(e) {} } resolve(); }); }); } catch(e) {} wsClient.send(JSON.stringify({ type: 'heartbeat', siteId, status: 'online', version: '21', localIP: getLocalIP(), port: PORT, printCount, lastPrint: lastPrintTime, isPrinting, queueLength: printQueue.length, uptime: Math.floor(process.uptime()), network: networkInfo, printers_status: printersStatus, timestamp: new Date().toISOString(), })); } catch(e) { console.log(YELLOW + '⚠️ Erreur envoi WS heartbeat: ' + e.message + RESET); } } // ────────────────────────────────────────────────────────────────────────── // Envoie le statut toutes les 60s vers WordPress // Lit aussi les commandes en attente (restart, etc.) let heartbeatUrl = ''; let heartbeatSecret = ''; let heartbeatPrinters = []; let heartbeatTimer = null; // Charger la config heartbeat depuis fichier si elle existe const HEARTBEAT_CONFIG_FILE = path.join(__dirname, 'heartbeat.json'); try { if (fs.existsSync(HEARTBEAT_CONFIG_FILE)) { const hbCfg = JSON.parse(fs.readFileSync(HEARTBEAT_CONFIG_FILE, 'utf8')); heartbeatUrl = hbCfg.wpUrl || ''; heartbeatSecret = hbCfg.secret || ''; heartbeatPrinters = hbCfg.printers || []; console.log('💓 Config heartbeat chargée: ' + heartbeatUrl + ' (' + heartbeatPrinters.length + ' imprimantes)'); } } catch(e) {} app.post('/heartbeat-config', (req, res) => { const { wpUrl, secret, printers } = req.body || {}; if (!wpUrl) return res.status(400).json({ error: 'wpUrl manquant' }); heartbeatUrl = wpUrl; heartbeatSecret = secret || ''; if (Array.isArray(printers)) heartbeatPrinters = printers; try { fs.writeFileSync(HEARTBEAT_CONFIG_FILE, JSON.stringify({ wpUrl, secret: heartbeatSecret, printers: heartbeatPrinters })); } catch(e) {} console.log('💓 Heartbeat configuré → ' + wpUrl + ' (' + heartbeatPrinters.length + ' imprimantes)'); sendHeartbeat(); res.json({ success: true }); }); function startHeartbeat() { if (heartbeatTimer) clearInterval(heartbeatTimer); heartbeatTimer = setInterval(sendHeartbeat, 60000); console.log(CYAN + '💓 Heartbeat démarré (toutes les 60s)' + RESET); } async function sendHeartbeat() { if (!heartbeatUrl) return; try { // Lire les dernières lignes de log let recentLogs = []; try { if (fs.existsSync(LOG_FILE)) { const content = fs.readFileSync(LOG_FILE, 'utf8'); recentLogs = content.split('\n').filter(l => l.trim()).slice(-20); } } catch(e) {} // Récupérer les infos réseau WiFi via termux-api let networkInfo = { available: false }; try { await new Promise((resolve) => { const { exec } = require('child_process'); exec('termux-wifi-connectioninfo', { timeout: 4000 }, (error, stdout) => { if (!error && stdout) { try { const d = JSON.parse(stdout); networkInfo = { available: true, ssid: d.ssid || '', ip: d.ip || getLocalIP(), rssi: d.rssi || null, frequency_mhz: d.frequency_mhz || null, link_speed_mbps: d.link_speed_mbps || null, }; } catch(e) {} } resolve(); }); }); } catch(e) {} // Vérifier le statut TCP de chaque imprimante configurée let printersStatus = []; if (heartbeatPrinters.length > 0) { const net = require('net'); printersStatus = await Promise.all(heartbeatPrinters.map(p => { if (!p.ip) return Promise.resolve({ name: p.name||p.type, type: p.type, ip: '', online: null }); return new Promise(resolve => { const sock = new net.Socket(); let done = false; const finish = (online) => { if (done) return; done = true; sock.destroy(); resolve({ name: p.name||p.type, type: p.type, ip: p.ip, port: p.port||9100, online }); }; sock.setTimeout(3000); sock.connect(p.port||9100, p.ip, () => finish(true)); sock.on('timeout', () => finish(false)); sock.on('error', () => finish(false)); }); })); } const payload = { action: 'thermal_heartbeat_receive', secret: heartbeatSecret, status: 'online', version: '21', localIP: getLocalIP(), port: PORT, printCount, lastPrint: lastPrintTime, isPrinting, queueLength: printQueue.length, uptime: Math.floor(process.uptime()), logs: recentLogs, network: networkInfo, printers_status: printersStatus, timestamp: new Date().toISOString(), }; // Utiliser URLSearchParams pour wp_remote_post compatible WordPress const params = new URLSearchParams(); Object.keys(payload).forEach(k => { const v = payload[k]; params.append(k, (Array.isArray(v) || typeof v === 'object') ? JSON.stringify(v) : v); }); const response = await axios.post(heartbeatUrl, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 }); const data = response.data; // Lire la commande en attente if (data && data.command) { console.log(CYAN + '💓 Commande reçue: ' + data.command + RESET); if (data.command === 'restart') { console.log(ORANGE + '🔄 Redémarrage demandé depuis le Hub...' + RESET); setTimeout(() => process.exit(0), 500); } else if (data.command === 'config_heartbeat' && data.command_data && data.command_data.wpUrl) { const newUrl = data.command_data.wpUrl; const newSecret = data.command_data.secret || heartbeatSecret; const newPrinters = data.command_data.printers || heartbeatPrinters; console.log(CYAN + '💓 Reconfiguration heartbeat → ' + newUrl + ' (' + newPrinters.length + ' imprimantes)' + RESET); heartbeatUrl = newUrl; heartbeatSecret = newSecret; heartbeatPrinters = newPrinters; try { fs.writeFileSync(HEARTBEAT_CONFIG_FILE, JSON.stringify({ wpUrl: newUrl, secret: newSecret, printers: newPrinters })); console.log(CYAN + '💓 heartbeat.json mis à jour' + RESET); } catch(e) { console.log(YELLOW + '⚠️ Erreur écriture heartbeat.json: ' + e.message + RESET); } } } else { console.log(CYAN + '💓 Heartbeat envoyé ✓' + RESET); // Synchroniser aussi vers le VPS WebSocket const siteId = heartbeatUrl ? heartbeatUrl.replace(/^https?:\/\//, '').replace(/\/.*$/, '') : getLocalIP(); sendWsHeartbeat(siteId); } } catch(e) { console.log(YELLOW + '💓 Heartbeat échoué: ' + e.message + RESET); } } // ────────────────────────────────────────────────────────────────────────── process.on('uncaughtException', (error) => { console.log(''); console.log(' Statut: ' + RED + '● OFFLINE' + RESET); console.log(' Erreur: ' + error.message); console.log(''); }); process.on('SIGINT', () => { console.log(''); console.log(' Statut: ' + RED + '● OFFLINE' + RESET); console.log(' Serveur arrêté'); console.log(''); process.exit(0); }); ENDOFSERVER # Injecter le port choisi (le heredoc ci-dessus est en quotes, donc on patche après coup) sed -i "s/const PORT = 8899;/const PORT = ${PORT};/" server.js echo "✅ Serveur v21 créé" # ============================================ # 7. DÉMARRAGE AUTOMATIQUE # ============================================ echo "" cat > ~/.bashrc_printer << 'ENDBASHRC' cd ~/printer-server && node server.js & ENDBASHRC if ! grep -q "bashrc_printer" ~/.bashrc 2>/dev/null; then echo "" >> ~/.bashrc echo "[ -f ~/.bashrc_printer ] && source ~/.bashrc_printer" >> ~/.bashrc echo "✅ Démarrage auto activé" else echo "✅ Démarrage auto déjà configuré" fi # ============================================ # 8. DÉTECTION IP # ============================================ echo "" echo "🔍 Détection de l'IP..." IP=$(ifconfig wlan0 2>/dev/null | grep 'inet ' | awk '{print $2}' | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1) if [ -z "$IP" ]; then IP=$(ip addr show wlan0 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1) fi if [ -z "$IP" ]; then IP=$(hostname -I 2>/dev/null | awk '{print $1}') fi if [ ! -z "$IP" ]; then echo "✅ IP détectée: $IP" else echo "⚠️ IP non détectée automatiquement" fi # ============================================ # 9. CONFIGURATION HEARTBEAT # ============================================ echo "" echo "📝 Configuration heartbeat..." WP_URL="https://${DOMAIN}/wp-admin/admin-ajax.php" cat > ~/printer-server/heartbeat.json << ENDHB { "wpUrl": "$WP_URL", "secret": "$SECRET", "printers": [] } ENDHB echo "✅ Heartbeat configuré: $WP_URL" # ============================================ # 10. CONFIGURATION TERMUX:BOOT # ============================================ if [ "$ENABLE_BOOT" = true ]; then echo "" echo "🚀 Configuration boot automatique..." mkdir -p ~/.termux/boot cat > ~/.termux/boot/start-printer.sh << 'ENDBOOT' #!/data/data/com.termux/files/usr/bin/bash cd ~/printer-server while true; do node server.js EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then sleep 1 else sleep 3 fi done & ENDBOOT chmod +x ~/.termux/boot/start-printer.sh echo "✅ Boot automatique activé" else echo "" echo "⏭️ Boot automatique désactivé (--no-boot)" fi # ============================================ # 11. RÉSUMÉ # ============================================ echo "" echo "════════════════════════════════════════" echo "✅ INSTALLATION v21 TERMINÉE" echo "════════════════════════════════════════" echo "" if [ ! -z "$IP" ]; then echo "URL: http://$IP:$PORT" fi echo "Port: $PORT" echo "Domaine: $DOMAIN" echo "Heartbeat: $WP_URL" echo "" echo "Corrections v21 :" echo " 🔌 Socket destroy() après chaque impression" echo " 🚦 Garde anti-impression simultanée (HTTP 429)" echo " ⏱️ Timeout fallback si printer.close() bloque" echo " 🎨 Images PNG complètes (SANS fractionnement)" echo " 📐 DPI: 203dpi - Zone: 576 points" echo "" echo "🚀 Démarrage avec surveillance automatique..." echo "" cd ~/printer-server # Boucle de surveillance : redémarre automatiquement si le process s'arrête # Nécessaire pour que POST /restart fonctionne while true; do cd ~/printer-server && node server.js EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then echo "🔄 $(date '+%H:%M:%S') - Redémarrage du serveur..." sleep 1 else echo "❌ $(date '+%H:%M:%S') - Arrêt inattendu (code $EXIT_CODE), redémarrage dans 3s..." sleep 3 fi done