diff --git a/.gitignore b/.gitignore index 6eb7e1d..4a5310e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ __pycache__/ app/__pycache__/ tray/ jdk-11.0.20-full/ +jdk-11.0.21+9-jre/ +jre-11-windows.zip # Ignore generated or local files -py_app/instance/pairing_keys.json -py_app/app/forked_tray.py -backup/print_module.html +backup/ \ No newline at end of file diff --git a/py_app/PRINT_PROGRESS_FEATURE.md b/py_app/PRINT_PROGRESS_FEATURE.md new file mode 100644 index 0000000..246a1c6 --- /dev/null +++ b/py_app/PRINT_PROGRESS_FEATURE.md @@ -0,0 +1,125 @@ +# Print Progress Modal Feature + +## Overview +Added a visual progress modal that displays during label printing operations via QZ Tray. The modal shows real-time progress, updates the database upon completion, and refreshes the table view automatically. + +## Features Implemented + +### 1. Progress Modal UI +- **Modal Overlay**: Full-screen semi-transparent overlay to focus user attention +- **Progress Bar**: Animated progress bar showing percentage completion +- **Status Messages**: Real-time status updates during printing +- **Label Counter**: Shows "X / Y" format for current progress (e.g., "5 / 10") + +### 2. Print Flow Improvements +The printing process now follows these steps: + +1. **Validation**: Check QZ Tray connection and printer selection +2. **Modal Display**: Show progress modal immediately +3. **Sequential Printing**: Print each label one by one with progress updates + - Update progress bar after each successful print + - Show current label number being printed + - 500ms delay between labels for printer processing +4. **Database Update**: Call `/update_printed_status/` endpoint + - Marks the order as printed in the database + - Handles errors gracefully (prints still succeed even if DB update fails) +5. **Table Refresh**: Automatically click "Load Orders" button to refresh the view +6. **Modal Close**: Hide modal after completion +7. **Notification**: Show success notification to user + +### 3. Progress Updates +The modal displays different status messages: +- "Preparing to print..." (initial) +- "Printing label X of Y..." (during printing) +- "✅ All labels printed! Updating database..." (after prints complete) +- "✅ Complete! Refreshing table..." (after DB update) +- "⚠️ Labels printed but database update failed" (on DB error) + +### 4. Error Handling +- Modal automatically closes on any error +- Error notifications shown to user +- Database update failures don't prevent successful printing +- Graceful degradation if DB update fails + +## Technical Details + +### CSS Styling +- **Modal**: Fixed position, z-index 9999, centered layout +- **Content Card**: White background, rounded corners, shadow +- **Progress Bar**: Linear gradient blue, smooth transitions +- **Responsive**: Min-width 400px, max-width 500px + +### JavaScript Functions Modified + +#### `handleQZTrayPrint(selectedRow)` +**Changes:** +- Added modal element references +- Show modal before printing starts +- Update progress bar and counter in loop +- Call database update endpoint after printing +- Handle database update errors +- Refresh table automatically +- Close modal on completion or error + +### Backend Integration + +#### Endpoint Used: `/update_printed_status/` +- **Method**: POST +- **Purpose**: Mark order as printed in database +- **Authentication**: Requires superadmin, warehouse_manager, or etichete role +- **Response**: JSON with success/error message + +## User Experience Flow + +1. User selects an order row in the table +2. User clicks "Print Label (QZ Tray)" button +3. Modal appears showing "Preparing to print..." +4. Progress bar fills as each label prints +5. Counter shows current progress (e.g., "7 / 10") +6. After all labels print: "✅ All labels printed! Updating database..." +7. Database updates with printed status +8. Modal shows "✅ Complete! Refreshing table..." +9. Modal closes automatically +10. Success notification appears +11. Table refreshes showing updated order status + +## Benefits + +✅ **Visual Feedback**: Users see real-time progress instead of a frozen UI +✅ **Status Clarity**: Clear messages about what's happening +✅ **Automatic Updates**: Database and UI update without manual intervention +✅ **Error Recovery**: Graceful handling of database update failures +✅ **Professional UX**: Modern, polished user interface +✅ **Non-Blocking**: Progress modal doesn't interfere with printing operation + +## Files Modified + +1. **print_module.html** + - Added modal HTML structure + - Added modal CSS styles + - Updated `handleQZTrayPrint()` function + - Added database update API call + - Added automatic table refresh + +## Testing Checklist + +- [ ] Modal appears when printing starts +- [ ] Progress bar animates smoothly +- [ ] Counter updates correctly (1/10, 2/10, etc.) +- [ ] All labels print successfully +- [ ] Database updates after printing +- [ ] Table refreshes automatically +- [ ] Modal closes after completion +- [ ] Success notification appears +- [ ] Error handling works (if DB update fails) +- [ ] Modal closes on printing errors + +## Future Enhancements + +Potential improvements: +- Add "Cancel" button to stop printing mid-process +- Show estimated time remaining +- Add sound notification on completion +- Log printing history with timestamps +- Add printer status monitoring +- Show print queue if multiple orders selected diff --git a/py_app/app/__pycache__/routes.cpython-312.pyc b/py_app/app/__pycache__/routes.cpython-312.pyc index ef12fd9..fd1d11c 100644 Binary files a/py_app/app/__pycache__/routes.cpython-312.pyc and b/py_app/app/__pycache__/routes.cpython-312.pyc differ diff --git a/py_app/app/forked_tray.py b/py_app/app/forked_tray.py new file mode 100644 index 0000000..5b64d6f --- /dev/null +++ b/py_app/app/forked_tray.py @@ -0,0 +1,44 @@ +import os +import json +import uuid +from datetime import datetime, timedelta +from flask import Blueprint, request, render_template, redirect, url_for + +bp_tray = Blueprint('forked_tray', __name__) +PAIRING_FILE = os.path.join(os.path.dirname(__file__), '../instance/pairing_keys.json') + +# Utility to load pairing keys from file +def load_pairing_keys(): + if not os.path.exists(PAIRING_FILE): + return [] + with open(PAIRING_FILE, 'r') as f: + return json.load(f) + +# Utility to save pairing keys to file +def save_pairing_keys(keys): + with open(PAIRING_FILE, 'w') as f: + json.dump(keys, f, indent=2) + +@bp_tray.route('/download_extension', methods=['GET']) +def download_extension(): + pairing_keys = load_pairing_keys() + return render_template('download_extension.html', pairing_keys=pairing_keys) + +@bp_tray.route('/generate_pairing_key', methods=['POST']) +def generate_pairing_key(): + printer_name = request.form.get('printer_name') + if not printer_name: + return redirect(url_for('forked_tray.download_extension')) + # Generate key and warranty + pairing_key = str(uuid.uuid4()) + warranty_days = 30 + warranty_until = (datetime.utcnow() + timedelta(days=warranty_days)).strftime('%Y-%m-%d') + # Load, append, and save + keys = load_pairing_keys() + keys.append({ + 'printer_name': printer_name, + 'pairing_key': pairing_key, + 'warranty_until': warranty_until + }) + save_pairing_keys(keys) + return render_template('download_extension.html', pairing_key=pairing_key, printer_name=printer_name, warranty_until=warranty_until, pairing_keys=keys) diff --git a/py_app/app/routes.py b/py_app/app/routes.py index e212815..ab34f6a 100644 --- a/py_app/app/routes.py +++ b/py_app/app/routes.py @@ -1,17 +1,4 @@ import json -from flask import Blueprint -bp = Blueprint('main', __name__) -@bp.route('/get_pairing_keys') -def get_pairing_keys(): - """Return all pairing keys as JSON for client selection.""" - keys_path = os.path.join(current_app.instance_path, 'pairing_keys.json') - try: - with open(keys_path, 'r') as f: - keys = json.load(f) - except Exception as e: - print(f"Error loading pairing keys: {e}") - return jsonify({'success': False, 'error': str(e), 'pairing_keys': []}), 500 - return jsonify({'success': True, 'pairing_keys': keys}) import os import mariadb from datetime import datetime, timedelta @@ -61,6 +48,21 @@ def format_cell_data(cell): def store_articles(): return render_template('store_articles.html') +@bp.route('/get_pairing_keys') +def get_pairing_keys(): + """Return all pairing keys as JSON for client selection.""" + keys_path = os.path.join(current_app.instance_path, 'pairing_keys.json') + try: + if os.path.exists(keys_path): + with open(keys_path, 'r') as f: + keys = json.load(f) + else: + keys = [] + except Exception as e: + print(f"Error loading pairing keys: {e}") + return jsonify([]), 200 + return jsonify(keys) + @bp.route('/warehouse_reports') def warehouse_reports(): return render_template('warehouse_reports.html') diff --git a/py_app/app/static/qz-tray-PATCH-NOTES.txt b/py_app/app/static/qz-tray-PATCH-NOTES.txt new file mode 100644 index 0000000..2d7fd27 --- /dev/null +++ b/py_app/app/static/qz-tray-PATCH-NOTES.txt @@ -0,0 +1,35 @@ +QZ TRAY LIBRARY PATCH NOTES +=========================== +Version: 2.2.4 (patched for custom QZ Tray with pairing key authentication) +Date: October 2, 2025 + +CHANGES MADE: +------------- + +1. Line ~387: Commented out certificate sending + - Original: _qz.websocket.connection.sendData({ certificate: cert, promise: openPromise }); + - Patched: openPromise.resolve(); (resolves immediately without sending certificate) + +2. Line ~391-403: Bypassed certificate retrieval + - Original: Called _qz.security.callCert() to get certificate from user + - Patched: Directly calls sendCert(null) without trying to get certificate + +3. Comments added to indicate patches + +REASON FOR PATCHES: +------------------ +The custom QZ Tray server has certificate validation COMPLETELY DISABLED. +It uses ONLY pairing key (HMAC) authentication instead of certificates. +The original qz-tray.js library expects certificate-based authentication and +fails when the server doesn't respond to certificate requests. + +COMPATIBILITY: +------------- +- Works with custom QZ Tray server (forked version with certificate validation disabled) +- NOT compatible with standard QZ Tray servers +- Connects to both ws://localhost:8181 and wss://localhost:8182 +- Authentication handled by server-side pairing keys + +BACKUP: +------- +Original unpatched version saved as: qz-tray.js.backup diff --git a/py_app/app/static/qz-tray.js b/py_app/app/static/qz-tray.js new file mode 100644 index 0000000..71146c8 --- /dev/null +++ b/py_app/app/static/qz-tray.js @@ -0,0 +1,2871 @@ +'use strict'; + +/** + * @version 2.2.4 + * @overview QZ Tray Connector + * @license LGPL-2.1-only + *

+ * Connects a web client to the QZ Tray software. + * Enables printing and device communication from javascript. + */ +var qz = (function() { + +///// POLYFILLS ///// + + if (!Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; + } + + if (!Number.isInteger) { + Number.isInteger = function(value) { + return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; + }; + } + +///// PRIVATE METHODS ///// + + var _qz = { + VERSION: "2.2.4", //must match @version above + DEBUG: false, + + log: { + /** Debugging messages */ + trace: function() { if (_qz.DEBUG) { console.log.apply(console, arguments); } }, + /** General messages */ + info: function() { console.info.apply(console, arguments); }, + /** General warnings */ + warn: function() { console.warn.apply(console, arguments); }, + /** Debugging errors */ + allay: function() { if (_qz.DEBUG) { console.warn.apply(console, arguments); } }, + /** General errors */ + error: function() { console.error.apply(console, arguments); } + }, + + + //stream types + streams: { + serial: 'SERIAL', usb: 'USB', hid: 'HID', printer: 'PRINTER', file: 'FILE', socket: 'SOCKET' + }, + + + websocket: { + /** The actual websocket object managing the connection. */ + connection: null, + /** Track if a connection attempt is being cancelled. */ + shutdown: false, + + /** Default parameters used on new connections. Override values using options parameter on {@link qz.websocket.connect}. */ + connectConfig: { + host: ["localhost", "localhost.qz.io"], //hosts QZ Tray can be running on + hostIndex: 0, //internal var - index on host array + usingSecure: true, //boolean use of secure protocol + protocol: { + secure: "wss://", //secure websocket + insecure: "ws://" //insecure websocket + }, + port: { + secure: [8181, 8282, 8383, 8484], //list of secure ports QZ Tray could be listening on + insecure: [8182, 8283, 8384, 8485], //list of insecure ports QZ Tray could be listening on + portIndex: 0 //internal var - index on active port array + }, + keepAlive: 60, //time between pings to keep connection alive, in seconds + retries: 0, //number of times to reconnect before failing + delay: 0 //seconds before firing a connection + }, + + setup: { + /** Loop through possible ports to open connection, sets web socket calls that will settle the promise. */ + findConnection: function(config, resolve, reject) { + if (_qz.websocket.shutdown) { + reject(new Error("Connection attempt cancelled by user")); + return; + } + + //force flag if missing ports + if (!config.port.secure.length) { + if (!config.port.insecure.length) { + reject(new Error("No ports have been specified to connect over")); + return; + } else if (config.usingSecure) { + _qz.log.error("No secure ports specified - forcing insecure connection"); + config.usingSecure = false; + } + } else if (!config.port.insecure.length && !config.usingSecure) { + _qz.log.trace("No insecure ports specified - forcing secure connection"); + config.usingSecure = true; + } + + var deeper = function() { + if (_qz.websocket.shutdown) { + //connection attempt was cancelled, bail out + reject(new Error("Connection attempt cancelled by user")); + return; + } + + config.port.portIndex++; + + if ((config.usingSecure && config.port.portIndex >= config.port.secure.length) + || (!config.usingSecure && config.port.portIndex >= config.port.insecure.length)) { + if (config.hostIndex >= config.host.length - 1) { + //give up, all hope is lost + reject(new Error("Unable to establish connection with QZ")); + return; + } else { + config.hostIndex++; + config.port.portIndex = 0; + } + } + + // recursive call until connection established or all ports are exhausted + _qz.websocket.setup.findConnection(config, resolve, reject); + }; + + var address; + if (config.usingSecure) { + address = config.protocol.secure + config.host[config.hostIndex] + ":" + config.port.secure[config.port.portIndex]; + } else { + address = config.protocol.insecure + config.host[config.hostIndex] + ":" + config.port.insecure[config.port.portIndex]; + } + + try { + _qz.log.trace("Attempting connection", address); + _qz.websocket.connection = new _qz.tools.ws(address); + } + catch(err) { + _qz.log.error(err); + deeper(); + return; + } + + if (_qz.websocket.connection != null) { + _qz.websocket.connection.established = false; + + //called on successful connection to qz, begins setup of websocket calls and resolves connect promise after certificate is sent + _qz.websocket.connection.onopen = function(evt) { + if (!_qz.websocket.connection.established) { + _qz.log.trace(evt); + _qz.log.info("Established connection with QZ Tray on " + address); + + _qz.websocket.setup.openConnection({ resolve: resolve, reject: reject }); + + if (config.keepAlive > 0) { + var interval = setInterval(function() { + if (!_qz.tools.isActive() || _qz.websocket.connection.interval !== interval) { + clearInterval(interval); + return; + } + + _qz.websocket.connection.send("ping"); + }, config.keepAlive * 1000); + + _qz.websocket.connection.interval = interval; + } + } + }; + + //called during websocket close during setup + _qz.websocket.connection.onclose = function() { + // Safari compatibility fix to raise error event + if (_qz.websocket.connection && typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { + _qz.websocket.connection.onerror(); + } + }; + + //called for errors during setup (such as invalid ports), reject connect promise only if all ports have been tried + _qz.websocket.connection.onerror = function(evt) { + _qz.log.trace(evt); + + _qz.websocket.connection = null; + + deeper(); + }; + } else { + reject(new Error("Unable to create a websocket connection")); + } + }, + + /** Finish setting calls on successful connection, sets web socket calls that won't settle the promise. */ + openConnection: function(openPromise) { + _qz.websocket.connection.established = true; + + //called when an open connection is closed + _qz.websocket.connection.onclose = function(evt) { + _qz.log.trace(evt); + + _qz.websocket.connection = null; + _qz.websocket.callClose(evt); + _qz.log.info("Closed connection with QZ Tray"); + + for(var uid in _qz.websocket.pendingCalls) { + if (_qz.websocket.pendingCalls.hasOwnProperty(uid)) { + _qz.websocket.pendingCalls[uid].reject(new Error("Connection closed before response received")); + } + } + + //if this is set, then an explicit close call was made + if (this.promise != undefined) { + this.promise.resolve(); + } + }; + + //called for any errors with an open connection + _qz.websocket.connection.onerror = function(evt) { + _qz.websocket.callError(evt); + }; + + //send JSON objects to qz + _qz.websocket.connection.sendData = function(obj) { + _qz.log.trace("Preparing object for websocket", obj); + + if (obj.timestamp == undefined) { + obj.timestamp = Date.now(); + if (typeof obj.timestamp !== 'number') { + obj.timestamp = new Date().getTime(); + } + } + if (obj.promise != undefined) { + obj.uid = _qz.websocket.setup.newUID(); + _qz.websocket.pendingCalls[obj.uid] = obj.promise; + } + + // track requesting monitor + obj.position = { + x: typeof screen !== 'undefined' ? ((screen.availWidth || screen.width) / 2) + (screen.left || screen.availLeft || 0) : 0, + y: typeof screen !== 'undefined' ? ((screen.availHeight || screen.height) / 2) + (screen.top || screen.availTop || 0) : 0 + }; + + try { + if (obj.call != undefined && obj.signature == undefined && _qz.security.needsSigned(obj.call)) { + var signObj = { + call: obj.call, + params: obj.params, + timestamp: obj.timestamp + }; + + //make a hashing promise if not already one + var hashing = _qz.tools.hash(_qz.tools.stringify(signObj)); + if (!hashing.then) { + hashing = _qz.tools.promise(function(resolve) { + resolve(hashing); + }); + } + + hashing.then(function(hashed) { + return _qz.security.callSign(hashed); + }).then(function(signature) { + _qz.log.trace("Signature for call", signature); + obj.signature = signature || ""; + obj.signAlgorithm = _qz.security.signAlgorithm; + + _qz.signContent = undefined; + _qz.websocket.connection.send(_qz.tools.stringify(obj)); + }).catch(function(err) { + _qz.log.error("Signing failed", err); + + if (obj.promise != undefined) { + obj.promise.reject(new Error("Failed to sign request")); + delete _qz.websocket.pendingCalls[obj.uid]; + } + }); + } else { + _qz.log.trace("Signature for call", obj.signature); + + //called for pre-signed content and (unsigned) setup calls + _qz.websocket.connection.send(_qz.tools.stringify(obj)); + } + } + catch(err) { + _qz.log.error(err); + + if (obj.promise != undefined) { + obj.promise.reject(err); + delete _qz.websocket.pendingCalls[obj.uid]; + } + } + }; + + //receive message from qz + _qz.websocket.connection.onmessage = function(evt) { + var returned = JSON.parse(evt.data); + + if (returned.uid == null) { + if (returned.type == null) { + //incorrect response format, likely connected to incompatible qz version + _qz.websocket.connection.close(4003, "Connected to incompatible QZ Tray version"); + + } else { + //streams (callbacks only, no promises) + switch(returned.type) { + case _qz.streams.serial: + if (!returned.event) { + returned.event = JSON.stringify({ portName: returned.key, output: returned.data }); + } + + _qz.serial.callSerial(JSON.parse(returned.event)); + break; + case _qz.streams.socket: + _qz.socket.callSocket(JSON.parse(returned.event)); + break; + case _qz.streams.usb: + if (!returned.event) { + returned.event = JSON.stringify({ vendorId: returned.key[0], productId: returned.key[1], output: returned.data }); + } + + _qz.usb.callUsb(JSON.parse(returned.event)); + break; + case _qz.streams.hid: + _qz.hid.callHid(JSON.parse(returned.event)); + break; + case _qz.streams.printer: + _qz.printers.callPrinter(JSON.parse(returned.event)); + break; + case _qz.streams.file: + _qz.file.callFile(JSON.parse(returned.event)); + break; + default: + _qz.log.allay("Cannot determine stream type for callback", returned); + break; + } + } + + return; + } + + _qz.log.trace("Received response from websocket", returned); + + var promise = _qz.websocket.pendingCalls[returned.uid]; + if (promise == undefined) { + _qz.log.allay('No promise found for returned response'); + } else { + if (returned.error != undefined) { + promise.reject(new Error(returned.error)); + } else { + promise.resolve(returned.result); + } + } + + delete _qz.websocket.pendingCalls[returned.uid]; + }; + + + //send up the certificate before making any calls + //also gives the user a chance to deny the connection + // PATCHED: Skip certificate sending for custom QZ Tray with pairing key authentication + function sendCert(cert) { + if (cert === undefined) { cert = null; } + + //websocket setup, query what version is connected + // PATCHED: Don't send certificate - custom server uses pairing keys only + qz.api.getVersion().then(function(version) { + _qz.websocket.connection.version = version; + _qz.websocket.connection.semver = version.toLowerCase().replace(/-rc\./g, "-rc").split(/[\\+\\.-]/g); + for(var i = 0; i < _qz.websocket.connection.semver.length; i++) { + try { + if (i == 3 && _qz.websocket.connection.semver[i].toLowerCase().indexOf("rc") == 0) { + // Handle "rc1" pre-release by negating build info + _qz.websocket.connection.semver[i] = -(_qz.websocket.connection.semver[i].replace(/\D/g, "")); + continue; + } + _qz.websocket.connection.semver[i] = parseInt(_qz.websocket.connection.semver[i]); + } + catch(ignore) {} + + if (_qz.websocket.connection.semver.length < 4) { + _qz.websocket.connection.semver[3] = 0; + } + } + + //algorithm can be declared before a connection, check for incompatibilities now that we have one + _qz.compatible.algorithm(true); + }).then(function() { + // PATCHED: Don't send certificate to server - custom QZ Tray uses pairing keys + // Original: _qz.websocket.connection.sendData({ certificate: cert, promise: openPromise }); + // Just resolve the promise immediately without sending certificate + openPromise.resolve(); + }); + } + + // PATCHED: Skip certificate retrieval for custom QZ Tray with pairing keys + // Just call sendCert with null immediately + sendCert(null); + + // Original code below (commented out): + /* + _qz.security.callCert().then(sendCert).catch(function(error) { + _qz.log.warn("Failed to get certificate:", error); + + if (_qz.security.rejectOnCertFailure) { + openPromise.reject(error); + } else { + sendCert(null); + } + }); + */ + }, + + /** Generate unique ID used to map a response to a call. */ + newUID: function() { + var len = 6; + return (new Array(len + 1).join("0") + (Math.random() * Math.pow(36, len) << 0).toString(36)).slice(-len) + } + }, + + dataPromise: function(callName, params, signature, signingTimestamp) { + return _qz.tools.promise(function(resolve, reject) { + var msg = { + call: callName, + promise: { resolve: resolve, reject: reject }, + params: params, + signature: signature, + timestamp: signingTimestamp + }; + + _qz.websocket.connection.sendData(msg); + }); + }, + + /** Library of promises awaiting a response, uid -> promise */ + pendingCalls: {}, + + /** List of functions to call on error from the websocket. */ + errorCallbacks: [], + /** Calls all functions registered to listen for errors. */ + callError: function(evt) { + if (Array.isArray(_qz.websocket.errorCallbacks)) { + for(var i = 0; i < _qz.websocket.errorCallbacks.length; i++) { + _qz.websocket.errorCallbacks[i](evt); + } + } else { + _qz.websocket.errorCallbacks(evt); + } + }, + + /** List of function to call on closing from the websocket. */ + closedCallbacks: [], + /** Calls all functions registered to listen for closing. */ + callClose: function(evt) { + if (Array.isArray(_qz.websocket.closedCallbacks)) { + for(var i = 0; i < _qz.websocket.closedCallbacks.length; i++) { + _qz.websocket.closedCallbacks[i](evt); + } + } else { + _qz.websocket.closedCallbacks(evt); + } + } + }, + + + printing: { + /** Default options used for new printer configs. Can be overridden using {@link qz.configs.setDefaults}. */ + defaultConfig: { + //value purposes are explained in the qz.configs.setDefaults docs + + bounds: null, + colorType: 'color', + copies: 1, + density: 0, + duplex: false, + fallbackDensity: null, + interpolation: 'bicubic', + jobName: null, + legacy: false, + margins: 0, + orientation: null, + paperThickness: null, + printerTray: null, + rasterize: false, + rotation: 0, + scaleContent: true, + size: null, + units: 'in', + + forceRaw: false, + encoding: null, + spool: null + } + }, + + + serial: { + /** List of functions called when receiving data from serial connection. */ + serialCallbacks: [], + /** Calls all functions registered to listen for serial events. */ + callSerial: function(streamEvent) { + if (Array.isArray(_qz.serial.serialCallbacks)) { + for(var i = 0; i < _qz.serial.serialCallbacks.length; i++) { + _qz.serial.serialCallbacks[i](streamEvent); + } + } else { + _qz.serial.serialCallbacks(streamEvent); + } + } + }, + + + socket: { + /** List of functions called when receiving data from network socket connection. */ + socketCallbacks: [], + /** Calls all functions registered to listen for network socket events. */ + callSocket: function(socketEvent) { + if (Array.isArray(_qz.socket.socketCallbacks)) { + for(var i = 0; i < _qz.socket.socketCallbacks.length; i++) { + _qz.socket.socketCallbacks[i](socketEvent); + } + } else { + _qz.socket.socketCallbacks(socketEvent); + } + } + }, + + + usb: { + /** List of functions called when receiving data from usb connection. */ + usbCallbacks: [], + /** Calls all functions registered to listen for usb events. */ + callUsb: function(streamEvent) { + if (Array.isArray(_qz.usb.usbCallbacks)) { + for(var i = 0; i < _qz.usb.usbCallbacks.length; i++) { + _qz.usb.usbCallbacks[i](streamEvent); + } + } else { + _qz.usb.usbCallbacks(streamEvent); + } + } + }, + + + hid: { + /** List of functions called when receiving data from hid connection. */ + hidCallbacks: [], + /** Calls all functions registered to listen for hid events. */ + callHid: function(streamEvent) { + if (Array.isArray(_qz.hid.hidCallbacks)) { + for(var i = 0; i < _qz.hid.hidCallbacks.length; i++) { + _qz.hid.hidCallbacks[i](streamEvent); + } + } else { + _qz.hid.hidCallbacks(streamEvent); + } + } + }, + + + printers: { + /** List of functions called when receiving data from printer connection. */ + printerCallbacks: [], + /** Calls all functions registered to listen for printer events. */ + callPrinter: function(streamEvent) { + if (Array.isArray(_qz.printers.printerCallbacks)) { + for(var i = 0; i < _qz.printers.printerCallbacks.length; i++) { + _qz.printers.printerCallbacks[i](streamEvent); + } + } else { + _qz.printers.printerCallbacks(streamEvent); + } + } + }, + + + file: { + /** List of functions called when receiving info regarding file changes. */ + fileCallbacks: [], + /** Calls all functions registered to listen for file events. */ + callFile: function(streamEvent) { + if (Array.isArray(_qz.file.fileCallbacks)) { + for(var i = 0; i < _qz.file.fileCallbacks.length; i++) { + _qz.file.fileCallbacks[i](streamEvent); + } + } else { + _qz.file.fileCallbacks(streamEvent); + } + } + }, + + + security: { + /** Function used to resolve promise when acquiring site's public certificate. */ + certHandler: function(resolve, reject) { reject(); }, + /** Called to create new promise (using {@link _qz.security.certHandler}) for certificate retrieval. */ + callCert: function() { + if (typeof _qz.security.certHandler.then === 'function') { + //already a promise + return _qz.security.certHandler; + } else if (_qz.security.certHandler.constructor.name === "AsyncFunction") { + //already callable as a promise + return _qz.security.certHandler(); + } else { + //turn into a promise + return _qz.tools.promise(_qz.security.certHandler); + } + }, + + /** Function used to create promise resolver when requiring signed calls. */ + signatureFactory: function() { return function(resolve) { resolve(); } }, + /** Called to create new promise (using {@link _qz.security.signatureFactory}) for signed calls. */ + callSign: function(toSign) { + if (_qz.security.signatureFactory.constructor.name === "AsyncFunction") { + //use directly + return _qz.security.signatureFactory(toSign); + } else { + //use in a promise + return _qz.tools.promise(_qz.security.signatureFactory(toSign)); + } + }, + + /** Signing algorithm used on signatures */ + signAlgorithm: "SHA1", + + rejectOnCertFailure: false, + + needsSigned: function(callName) { + const undialoged = [ + "printers.getStatus", + "printers.stopListening", + "usb.isClaimed", + "usb.closeStream", + "usb.releaseDevice", + "hid.stopListening", + "hid.isClaimed", + "hid.closeStream", + "hid.releaseDevice", + "file.stopListening", + "getVersion" + ]; + + return callName != null && undialoged.indexOf(callName) === -1; + } + }, + + + tools: { + /** Create a new promise */ + promise: function(resolver) { + //prefer global object for historical purposes + if (typeof RSVP !== 'undefined') { + return new RSVP.Promise(resolver); + } else if (typeof Promise !== 'undefined') { + return new Promise(resolver); + } else { + _qz.log.error("Promise/A+ support is required. See qz.api.setPromiseType(...)"); + } + }, + + /** Stub for rejecting with an Error from withing a Promise */ + reject: function(error) { + return _qz.tools.promise(function(resolve, reject) { + reject(error); + }); + }, + + stringify: function(object) { + //old versions of prototype affect stringify + var pjson = Array.prototype.toJSON; + delete Array.prototype.toJSON; + + function skipKeys(key, value) { + if (key === "promise") { + return undefined; + } + + return value; + } + + var result = JSON.stringify(object, skipKeys); + + if (pjson) { + Array.prototype.toJSON = pjson; + } + + return result; + }, + + hash: function(data) { + //prefer global object for historical purposes + if (typeof Sha256 !== 'undefined') { + return Sha256.hash(data); + } else { + return _qz.SHA.hash(data); + } + }, + + ws: typeof WebSocket !== 'undefined' ? WebSocket : null, + + absolute: function(loc) { + if (typeof window !== 'undefined' && typeof document.createElement === 'function') { + var a = document.createElement("a"); + a.href = loc; + return a.href; + } else if (typeof exports === 'object') { + //node.js + require('path').resolve(loc); + } + return loc; + }, + + relative: function(data) { + for(var i = 0; i < data.length; i++) { + if (data[i].constructor === Object) { + var absolute = false; + + if (data[i].data && data[i].data.search && data[i].data.search(/data:image\/\w+;base64,/) === 0) { + //upgrade from old base64 behavior + data[i].flavor = "base64"; + data[i].data = data[i].data.replace(/^data:image\/\w+;base64,/, ""); + } else if (data[i].flavor) { + //if flavor is known, we can directly check for absolute flavor types + if (["FILE", "XML"].indexOf(data[i].flavor.toUpperCase()) > -1) { + absolute = true; + } + } else if (data[i].format && ["HTML", "IMAGE", "PDF", "FILE", "XML"].indexOf(data[i].format.toUpperCase()) > -1) { + //if flavor is not known, all valid pixel formats default to file flavor + //previous v2.0 data also used format as what is now flavor, so we check for those values here too + absolute = true; + } else if (data[i].type && ((["PIXEL", "IMAGE", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && !data[i].format) + || (["HTML", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && (!data[i].format || data[i].format.toUpperCase() === "FILE")))) { + //if all we know is pixel type, then it is image's file flavor + //previous v2.0 data also used type as what is now format, so we check for those value here too + absolute = true; + } + + if (absolute) { + //change relative links to absolute + data[i].data = _qz.tools.absolute(data[i].data); + } + if (data[i].options && typeof data[i].options.overlay === 'string') { + data[i].options.overlay = _qz.tools.absolute(data[i].options.overlay); + } + } + } + }, + + /** Performs deep copy to target from remaining params */ + extend: function(target) { + //special case when reassigning properties as objects in a deep copy + if (typeof target !== 'object') { + target = {}; + } + + for(var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + if (!source) { continue; } + + for(var key in source) { + if (source.hasOwnProperty(key)) { + if (target === source[key]) { continue; } + + if (source[key] && source[key].constructor && source[key].constructor === Object) { + var clone; + if (Array.isArray(source[key])) { + clone = target[key] || []; + } else { + clone = target[key] || {}; + } + + target[key] = _qz.tools.extend(clone, source[key]); + } else if (source[key] !== undefined) { + target[key] = source[key]; + } + } + } + } + + return target; + }, + + versionCompare: function(major, minor, patch, build) { + if (_qz.tools.assertActive()) { + var semver = _qz.websocket.connection.semver; + if (semver[0] != major) { + return semver[0] - major; + } + if (minor != undefined && semver[1] != minor) { + return semver[1] - minor; + } + if (patch != undefined && semver[2] != patch) { + return semver[2] - patch; + } + if (build != undefined && semver.length > 3 && semver[3] != build) { + return Number.isInteger(semver[3]) && Number.isInteger(build) ? semver[3] - build : semver[3].toString().localeCompare(build.toString()); + } + return 0; + } + }, + + isVersion: function(major, minor, patch, build) { + return _qz.tools.versionCompare(major, minor, patch, build) == 0; + }, + + isActive: function() { + return !_qz.websocket.shutdown && _qz.websocket.connection != null + && (_qz.websocket.connection.readyState === _qz.tools.ws.OPEN + || _qz.websocket.connection.readyState === _qz.tools.ws.CONNECTING); + }, + + assertActive: function() { + if (_qz.tools.isActive()) { + return true; + } + // Promise won't reject on throw; yet better than 'undefined' + throw new Error("A connection to QZ has not been established yet"); + }, + + uint8ArrayToHex: function(uint8) { + return Array.from(uint8) + .map(function(i) { return i.toString(16).padStart(2, '0'); }) + .join(''); + }, + + uint8ArrayToBase64: function(uint8) { + /** + * Adapted from Egor Nepomnyaschih's code under MIT Licence (C) 2020 + * see https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 + */ + var map = [ + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", + "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/" + ]; + + var result = '', i, l = uint8.length; + for (i = 2; i < l; i += 3) { + result += map[uint8[i - 2] >> 2]; + result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; + result += map[((uint8[i - 1] & 0x0F) << 2) | (uint8[i] >> 6)]; + result += map[uint8[i] & 0x3F]; + } + if (i === l + 1) { // 1 octet yet to write + result += map[uint8[i - 2] >> 2]; + result += map[(uint8[i - 2] & 0x03) << 4]; + result += "=="; + } + if (i === l) { // 2 octets yet to write + result += map[uint8[i - 2] >> 2]; + result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; + result += map[(uint8[i - 1] & 0x0F) << 2]; + result += "="; + } + return result; + }, + }, + + compatible: { + /** Converts message format to a previous version's */ + data: function(printData) { + // special handling for Uint8Array + for(var i = 0; i < printData.length; i++) { + if (printData[i].constructor === Object && printData[i].data instanceof Uint8Array) { + if (printData[i].flavor) { + var flavor = printData[i].flavor.toString().toUpperCase(); + switch(flavor) { + case 'BASE64': + printData[i].data = _qz.tools.uint8ArrayToBase64(printData[i].data); + break; + case 'HEX': + printData[i].data = _qz.tools.uint8ArrayToHex(printData[i].data); + break; + default: + throw new Error("Uint8Array conversion to '" + flavor + "' is not supported."); + } + } + } + } + + if(_qz.tools.versionCompare(2, 2, 4) < 0) { + for(var i = 0; i < printData.length; i++) { + if (printData[i].constructor === Object) { + // dotDensity: "double-legacy|single-legacy" since 2.2.4. Fallback to "double|single" + if (printData[i].options && typeof printData[i].options.dotDensity === 'string') { + printData[i].options.dotDensity = printData[i].options.dotDensity.toLowerCase().replace("-legacy", ""); + } + } + } + } + + if (_qz.tools.isVersion(2, 0)) { + /* + 2.0.x conversion + ----- + type=pixel -> use format as 2.0 type (unless 'command' format, which forces 2.0 'raw' type) + type=raw -> 2.0 type has to be 'raw' + if format is 'image' -> force 2.0 'image' format, ignore everything else (unsupported in 2.0) + + flavor translates straight to 2.0 format (unless forced to 'raw'/'image') + */ + _qz.log.trace("Converting print data to v2.0 for " + _qz.websocket.connection.version); + for(var i = 0; i < printData.length; i++) { + if (printData[i].constructor === Object) { + if (printData[i].type && printData[i].type.toUpperCase() === "RAW" && printData[i].format && printData[i].format.toUpperCase() === "IMAGE") { + if (printData[i].flavor && printData[i].flavor.toUpperCase() === "BASE64") { + //special case for raw base64 images + printData[i].data = "data:image/compat;base64," + printData[i].data; + } + printData[i].flavor = "IMAGE"; //forces 'image' format when shifting for conversion + } + if ((printData[i].type && printData[i].type.toUpperCase() === "RAW") || (printData[i].format && printData[i].format.toUpperCase() === "COMMAND")) { + printData[i].format = "RAW"; //forces 'raw' type when shifting for conversion + } + + printData[i].type = printData[i].format; + printData[i].format = printData[i].flavor; + delete printData[i].flavor; + } + } + } + }, + + /* Converts config defaults to match previous version */ + config: function(config, dirty) { + if (_qz.tools.isVersion(2, 0)) { + if (!dirty.rasterize) { + config.rasterize = true; + } + } + if(_qz.tools.versionCompare(2, 2) < 0) { + if(config.forceRaw !== 'undefined') { + config.altPrinting = config.forceRaw; + delete config.forceRaw; + } + } + if(_qz.tools.versionCompare(2, 1, 2, 11) < 0) { + if(config.spool) { + if(config.spool.size) { + config.perSpool = config.spool.size; + delete config.spool.size; + } + if(config.spool.end) { + config.endOfDoc = config.spool.end; + delete config.spool.end; + } + delete config.spool; + } + } + return config; + }, + + /** Compat wrapper with previous version **/ + networking: function(hostname, port, signature, signingTimestamp, mappingCallback) { + // Use 2.0 + if (_qz.tools.isVersion(2, 0)) { + return _qz.tools.promise(function(resolve, reject) { + _qz.websocket.dataPromise('websocket.getNetworkInfo', { + hostname: hostname, + port: port + }, signature, signingTimestamp).then(function(data) { + if (typeof mappingCallback !== 'undefined') { + resolve(mappingCallback(data)); + } else { + resolve(data); + } + }, reject); + }); + } + // Wrap 2.1 + return _qz.tools.promise(function(resolve, reject) { + _qz.websocket.dataPromise('networking.device', { + hostname: hostname, + port: port + }, signature, signingTimestamp).then(function(data) { + resolve({ ipAddress: data.ip, macAddress: data.mac }); + }, reject); + }); + }, + + /** Check if QZ version supports chosen algorithm */ + algorithm: function(quiet) { + //if not connected yet we will assume compatibility exists for the time being + if (_qz.tools.isActive()) { + if (_qz.tools.isVersion(2, 0)) { + if (!quiet) { + _qz.log.warn("Connected to an older version of QZ, alternate signature algorithms are not supported"); + } + return false; + } + } + + return true; + } + }, + + /** + * Adapted from Chris Veness's code under MIT Licence (C) 2002 + * see http://www.movable-type.co.uk/scripts/sha256.html + */ + SHA: { + //@formatter:off - keep this block compact + hash: function(msg) { + // add trailing '1' bit (+ 0's padding) to string [§5.1.1] + msg = _qz.SHA._utf8Encode(msg) + String.fromCharCode(0x80); + + // constants [§4.2.2] + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]; + // initial hash value [§5.3.1] + var H = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ]; + + // convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1] + var l = msg.length / 4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length + var N = Math.ceil(l / 16); // number of 16-integer-blocks required to hold 'l' ints + var M = new Array(N); + + for(var i = 0; i < N; i++) { + M[i] = new Array(16); + for(var j = 0; j < 16; j++) { // encode 4 chars per integer, big-endian encoding + M[i][j] = (msg.charCodeAt(i * 64 + j * 4) << 24) | (msg.charCodeAt(i * 64 + j * 4 + 1) << 16) | + (msg.charCodeAt(i * 64 + j * 4 + 2) << 8) | (msg.charCodeAt(i * 64 + j * 4 + 3)); + } // note running off the end of msg is ok 'cos bitwise ops on NaN return 0 + } + // add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1] + // note: most significant word would be (len-1)*8 >>> 32, but since JS converts + // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators + M[N-1][14] = ((msg.length - 1) * 8) / Math.pow(2, 32); + M[N-1][14] = Math.floor(M[N-1][14]); + M[N-1][15] = ((msg.length - 1) * 8) & 0xffffffff; + + // HASH COMPUTATION [§6.1.2] + var W = new Array(64); var a, b, c, d, e, f, g, h; + for(var i = 0; i < N; i++) { + // 1 - prepare message schedule 'W' + for(var t = 0; t < 16; t++) { W[t] = M[i][t]; } + for(var t = 16; t < 64; t++) { W[t] = (_qz.SHA._dev1(W[t-2]) + W[t-7] + _qz.SHA._dev0(W[t-15]) + W[t-16]) & 0xffffffff; } + // 2 - initialise working variables a, b, c, d, e, f, g, h with previous hash value + a = H[0]; b = H[1]; c = H[2]; d = H[3]; e = H[4]; f = H[5]; g = H[6]; h = H[7]; + // 3 - main loop (note 'addition modulo 2^32') + for(var t = 0; t < 64; t++) { + var T1 = h + _qz.SHA._sig1(e) + _qz.SHA._ch(e, f, g) + K[t] + W[t]; + var T2 = _qz.SHA._sig0(a) + _qz.SHA._maj(a, b, c); + h = g; g = f; f = e; e = (d + T1) & 0xffffffff; + d = c; c = b; b = a; a = (T1 + T2) & 0xffffffff; + } + // 4 - compute the new intermediate hash value (note 'addition modulo 2^32') + H[0] = (H[0]+a) & 0xffffffff; H[1] = (H[1]+b) & 0xffffffff; H[2] = (H[2]+c) & 0xffffffff; H[3] = (H[3]+d) & 0xffffffff; + H[4] = (H[4]+e) & 0xffffffff; H[5] = (H[5]+f) & 0xffffffff; H[6] = (H[6]+g) & 0xffffffff; H[7] = (H[7]+h) & 0xffffffff; + } + + return _qz.SHA._hexStr(H[0]) + _qz.SHA._hexStr(H[1]) + _qz.SHA._hexStr(H[2]) + _qz.SHA._hexStr(H[3]) + + _qz.SHA._hexStr(H[4]) + _qz.SHA._hexStr(H[5]) + _qz.SHA._hexStr(H[6]) + _qz.SHA._hexStr(H[7]); + }, + + // Rotates right (circular right shift) value x by n positions + _rotr: function(n, x) { return (x >>> n) | (x << (32 - n)); }, + // logical functions + _sig0: function(x) { return _qz.SHA._rotr(2, x) ^ _qz.SHA._rotr(13, x) ^ _qz.SHA._rotr(22, x); }, + _sig1: function(x) { return _qz.SHA._rotr(6, x) ^ _qz.SHA._rotr(11, x) ^ _qz.SHA._rotr(25, x); }, + _dev0: function(x) { return _qz.SHA._rotr(7, x) ^ _qz.SHA._rotr(18, x) ^ (x >>> 3); }, + _dev1: function(x) { return _qz.SHA._rotr(17, x) ^ _qz.SHA._rotr(19, x) ^ (x >>> 10); }, + _ch: function(x, y, z) { return (x & y) ^ (~x & z); }, + _maj: function(x, y, z) { return (x & y) ^ (x & z) ^ (y & z); }, + // note can't use toString(16) as it is implementation-dependant, and in IE returns signed numbers when used on full words + _hexStr: function(n) { var s = "", v; for(var i = 7; i >= 0; i--) { v = (n >>> (i * 4)) & 0xf; s += v.toString(16); } return s; }, + // implementation of deprecated unescape() based on https://cwestblog.com/2011/05/23/escape-unescape-deprecated/ (and comments) + _unescape: function(str) { + return str.replace(/%(u[\da-f]{4}|[\da-f]{2})/gi, function(seq) { + if (seq.length - 1) { + return String.fromCharCode(parseInt(seq.substring(seq.length - 3 ? 2 : 1), 16)) + } else { + var code = seq.charCodeAt(0); + return code < 256 ? "%" + (0 + code.toString(16)).slice(-2).toUpperCase() : "%u" + ("000" + code.toString(16)).slice(-4).toUpperCase() + } + }); + }, + _utf8Encode: function(str) { + return _qz.SHA._unescape(encodeURIComponent(str)); + } + //@formatter:on + }, + }; + + +///// CONFIG CLASS //// + + /** Object to handle configured printer options. */ + function Config(printer, opts) { + + this.config = _qz.tools.extend({}, _qz.printing.defaultConfig); //create a copy of the default options + this._dirtyOpts = {}; //track which config options have changed from the defaults + + /** + * Set the printer assigned to this config. + * @param {string|Object} newPrinter Name of printer. Use object type to specify printing to file or host. + * @param {string} [newPrinter.name] Name of printer to send printing. + * @param {string} [newPrinter.file] DEPRECATED: Name of file to send printing. + * @param {string} [newPrinter.host] IP address or host name to send printing. + * @param {string} [newPrinter.port] Port used by <printer.host>. + */ + this.setPrinter = function(newPrinter) { + if (typeof newPrinter === 'string') { + newPrinter = { name: newPrinter }; + } + this.printer = newPrinter; + }; + + /** + * @returns {Object} The printer currently assigned to this config. + */ + this.getPrinter = function() { + return this.printer; + }; + + /** + * Alter any of the printer options currently applied to this config. + * @param newOpts {Object} The options to change. See qz.configs.setDefaults docs for available values. + * + * @see qz.configs.setDefaults + */ + this.reconfigure = function(newOpts) { + for(var key in newOpts) { + if (newOpts[key] !== undefined) { + this._dirtyOpts[key] = true; + } + } + + _qz.tools.extend(this.config, newOpts); + }; + + /** + * @returns {Object} The currently applied options on this config. + */ + this.getOptions = function() { + return _qz.compatible.config(this.config, this._dirtyOpts); + }; + + // init calls for new config object + this.setPrinter(printer); + this.reconfigure(opts); + } + + /** + * Shortcut method for calling qz.print with a particular config. + * @param {Array} data Array of data being sent to the printer. See qz.print docs for available values. + * @param {boolean} [signature] Pre-signed signature of JSON string containing call, params, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @example + * qz.print(myConfig, ...); // OR + * myConfig.print(...); + * + * @see qz.print + */ + Config.prototype.print = function(data, signature, signingTimestamp) { + qz.print(this, data, signature, signingTimestamp); + }; + + +///// PUBLIC METHODS ///// + + /** @namespace qz */ + var qz = { + + /** + * Calls related specifically to the web socket connection. + * @namespace qz.websocket + */ + websocket: { + /** + * Check connection status. Active connection is necessary for other calls to run. + * + * @returns {boolean} If there is an active connection with QZ Tray. + * + * @see connect + * + * @memberof qz.websocket + */ + isActive: function() { + return _qz.tools.isActive(); + }, + + /** + * Call to setup connection with QZ Tray on user's system. + * + * @param {Object} [options] Configuration options for the web socket connection. + * @param {string|Array} [options.host=['localhost', 'localhost.qz.io']] Host running the QZ Tray software. + * @param {Object} [options.port] Config options for ports to cycle. + * @param {Array} [options.port.secure=[8181, 8282, 8383, 8484]] Array of secure (WSS) ports to try + * @param {Array} [options.port.insecure=[8182, 8283, 8384, 8485]] Array of insecure (WS) ports to try + * @param {boolean} [options.usingSecure=true] If the web socket should try to use secure ports for connecting. + * @param {number} [options.keepAlive=60] Seconds between keep-alive pings to keep connection open. Set to 0 to disable. + * @param {number} [options.retries=0] Number of times to reconnect before failing. + * @param {number} [options.delay=0] Seconds before firing a connection. Ignored if options.retries is 0. + * + * @returns {Promise} + * + * @memberof qz.websocket + */ + connect: function(options) { + return _qz.tools.promise(function(resolve, reject) { + if (_qz.websocket.connection) { + const state = _qz.websocket.connection.readyState; + + if (state === _qz.tools.ws.OPEN) { + reject(new Error("An open connection with QZ Tray already exists")); + return; + } else if (state === _qz.tools.ws.CONNECTING) { + reject(new Error("The current connection attempt has not returned yet")); + return; + } else if (state === _qz.tools.ws.CLOSING) { + reject(new Error("Waiting for previous disconnect request to complete")); + return; + } + } + + if (!_qz.tools.ws) { + reject(new Error("WebSocket not supported by this browser")); + return; + } else if (!_qz.tools.ws.CLOSED || _qz.tools.ws.CLOSED == 2) { + reject(new Error("Unsupported WebSocket version detected: HyBi-00/Hixie-76")); + return; + } + + //ensure some form of options exists for value checks + if (options == undefined) { options = {}; } + + //disable secure ports if page is not secure + if (typeof location === 'undefined' || location.protocol !== 'https:') { + //respect forcing secure ports if it is defined, otherwise disable + if (typeof options.usingSecure === 'undefined') { + _qz.log.trace("Disabling secure ports due to insecure page"); + options.usingSecure = false; + } + } + + //ensure any hosts are passed to internals as an array + if (typeof options.host !== 'undefined' && !Array.isArray(options.host)) { + options.host = [options.host]; + } + + _qz.websocket.shutdown = false; //reset state for new connection attempt + var attempt = function(count) { + var tried = false; + var nextAttempt = function() { + if (!tried) { + tried = true; + + if (options && count < options.retries) { + attempt(count + 1); + } else { + _qz.websocket.connection = null; + reject.apply(null, arguments); + } + } + }; + + var delayed = function() { + var config = _qz.tools.extend({}, _qz.websocket.connectConfig, options); + _qz.websocket.setup.findConnection(config, resolve, nextAttempt) + }; + if (count == 0) { + delayed(); // only retries will be called with a delay + } else { + setTimeout(delayed, options.delay * 1000); + } + }; + + attempt(0); + }); + }, + + /** + * Stop any active connection with QZ Tray. + * + * @returns {Promise} + * + * @memberof qz.websocket + */ + disconnect: function() { + return _qz.tools.promise(function(resolve, reject) { + if (_qz.websocket.connection != null) { + if (_qz.tools.isActive()) { + // handles closing both 'connecting' and 'connected' states + _qz.websocket.shutdown = true; + _qz.websocket.connection.promise = { resolve: resolve, reject: reject }; + _qz.websocket.connection.close(); + } else { + reject(new Error("Current connection is still closing")); + } + } else { + reject(new Error("No open connection with QZ Tray")); + } + }); + }, + + /** + * List of functions called for any connections errors outside of an API call.

+ * Also called if {@link websocket#connect} fails to connect. + * + * @param {Function|Array} calls Single or array of Function({Event} event) calls. + * + * @memberof qz.websocket + */ + setErrorCallbacks: function(calls) { + _qz.websocket.errorCallbacks = calls; + }, + + /** + * List of functions called for any connection closing event outside of an API call.

+ * Also called when {@link websocket#disconnect} is called. + * + * @param {Function|Array} calls Single or array of Function({Event} event) calls. + * + * @memberof qz.websocket + */ + setClosedCallbacks: function(calls) { + _qz.websocket.closedCallbacks = calls; + }, + + /** + * @deprecated Since 2.1.0. Please use qz.networking.device() instead + * + * @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] Port to use with custom hostname, defaults to 443 + * @param {string} [signature] Pre-signed signature of hashed JSON string containing call='websocket.getNetworkInfo', params object, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @returns {Promise|Error>} Connected system's network information. + * + * @memberof qz.websocket + */ + getNetworkInfo: _qz.compatible.networking, + + /** + * @returns {Object<{socket: String, host: String, port: Number}>} Details of active websocket connection + * + * @memberof qz.websocket + */ + getConnectionInfo: function() { + if (_qz.tools.assertActive()) { + var url = _qz.websocket.connection.url.split(/[:\/]+/g); + return { socket: url[0], host: url[1], port: +url[2] }; + } + } + }, + + + /** + * Calls related to getting printer information from the connection. + * @namespace qz.printers + */ + printers: { + /** + * @param {string} [signature] Pre-signed signature of hashed JSON string containing call='printers.getDefault, params, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @returns {Promise} Name of the connected system's default printer. + * + * @memberof qz.printers + */ + getDefault: function(signature, signingTimestamp) { + return _qz.websocket.dataPromise('printers.getDefault', null, signature, signingTimestamp); + }, + + /** + * @param {string} [query] Search for a specific printer. All printers are returned if not provided. + * @param {string} [signature] Pre-signed signature of hashed JSON string containing call='printers.find', params, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @returns {Promise|string|Error>} The matched printer name if query is provided. + * Otherwise an array of printer names found on the connected system. + * + * @memberof qz.printers + */ + find: function(query, signature, signingTimestamp) { + return _qz.websocket.dataPromise('printers.find', { query: query }, signature, signingTimestamp); + }, + + /** + * Provides a list, with additional information, for each printer available to QZ. + * + * @returns {Promise|Object|Error>} + * + * @memberof qz.printers + */ + details: function() { + return _qz.websocket.dataPromise('printers.detail'); + }, + + /** + * Start listening for printer status events, such as paper_jam events. + * Reported under the ACTION type in the streamEvent on callbacks. + * + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.printers.setPrinterCallbacks + * + * @param {null|string|Array} printers Printer or list of printers to listen to, null listens to all. + * @param {Object|null} [options] Printer listener options + * @param {null|boolean} [options.jobData=false] Flag indicating if raw spool file content should be return as well as status information (Windows only) + * @param {null|number} [options.maxJobData=-1] Maximum number of bytes to returns for raw spooled file content (Windows only) + * @param {null|string} [options.flavor="plain"] Flavor of data format returned. Valid flavors are [base64 | hex | plain*] (Windows only) + * + * @memberof qz.printers + */ + startListening: function(printers, options) { + if (!Array.isArray(printers)) { + printers = [printers]; + } + var params = { + printerNames: printers + }; + if (options && options.jobData == true) params.jobData = true; + if (options && options.maxJobData) params.maxJobData = options.maxJobData; + if (options && options.flavor) params.flavor = options.flavor; + return _qz.websocket.dataPromise('printers.startListening', params); + }, + + /** + * Clear the queue of a specified printer or printers. Does not delete retained jobs. + * + * @param {string|Object} [options] Name of printer to clear + * @param {string} [options.printerName] Name of printer to clear + * @param {number} [options.jobId] Cancel a job of a specific JobId instead of canceling all. Must include a printerName. + * + * @returns {Promise} + * @since 2.2.4 + * + * @memberof qz.printers + */ + clearQueue: function(options) { + if (typeof options !== 'object') { + options = { + printerName: options + }; + } + return _qz.websocket.dataPromise('printers.clearQueue', options); + }, + + /** + * Stop listening for printer status actions. + * + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.printers.setPrinterCallbacks + * + * @memberof qz.printers + */ + stopListening: function() { + return _qz.websocket.dataPromise('printers.stopListening'); + }, + + /** + * Retrieve current printer status from any active listeners. + * + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.printers.startListening + * + * @memberof qz.printers + */ + getStatus: function() { + return _qz.websocket.dataPromise('printers.getStatus'); + }, + + /** + * List of functions called for any printer status change. + * Event data will contain {string} printerName and {string} status for all types. + * For RECEIVE types, {Array} output (in hexadecimal format). + * For ERROR types, {string} exception. + * For ACTION types, {string} actionType. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * @since 2.1.0 + * + * @memberof qz.printers + */ + setPrinterCallbacks: function(calls) { + _qz.printers.printerCallbacks = calls; + } + }, + + /** + * Calls related to setting up new printer configurations. + * @namespace qz.configs + */ + configs: { + /** + * Default options used by new configs if not overridden. + * Setting a value to NULL will use the printer's default options. + * Updating these will not update the options on any created config. + * + * @param {Object} options Default options used by printer configs if not overridden. + * + * @param {Object} [options.bounds=null] Bounding box rectangle. + * @param {number} [options.bounds.x=0] Distance from left for bounding box starting corner + * @param {number} [options.bounds.y=0] Distance from top for bounding box starting corner + * @param {number} [options.bounds.width=0] Width of bounding box + * @param {number} [options.bounds.height=0] Height of bounding box + * @param {string} [options.colorType='color'] Valid values [color | grayscale | blackwhite | default] + * @param {number} [options.copies=1] Number of copies to be printed. + * @param {number|Array|Object|Array|string} [options.density=0] Pixel density (DPI, DPMM, or DPCM depending on [options.units]). + * If provided as an array, uses the first supported density found (or the first entry if none found). + * If provided as a string, valid values are [best | draft], corresponding to highest or lowest reported density respectively. + * @param {number} [options.density.cross=0] Asymmetric pixel density for the cross feed direction. + * @param {number} [options.density.feed=0] Asymmetric pixel density for the feed direction. + * @param {boolean|string} [options.duplex=false] Double sided printing, Can specify duplex style by passing a string value: [one-sided | duplex | long-edge | tumble | short-edge] + * @param {number} [options.fallbackDensity=null] Value used when default density value cannot be read, or in cases where reported as "Normal" by the driver, (in DPI, DPMM, or DPCM depending on [options.units]). + * @param {string} [options.interpolation='bicubic'] Valid values [bicubic | bilinear | nearest-neighbor]. Controls how images are handled when resized. + * @param {string} [options.jobName=null] Name to display in print queue. + * @param {boolean} [options.legacy=false] If legacy style printing should be used. + * @param {Object|number} [options.margins=0] If just a number is provided, it is used as the margin for all sides. + * @param {number} [options.margins.top=0] + * @param {number} [options.margins.right=0] + * @param {number} [options.margins.bottom=0] + * @param {number} [options.margins.left=0] + * @param {string} [options.orientation=null] Valid values [portrait | landscape | reverse-landscape | null]. + * If set to null, orientation will be determined automatically. + * @param {number} [options.paperThickness=null] + * @param {string|number} [options.printerTray=null] Printer tray to pull from. The number N assumes string equivalent of 'Tray N'. Uses printer default if NULL. + * @param {boolean} [options.rasterize=false] Whether documents should be rasterized before printing. + * Specifying [options.density] for PDF print formats will set this to true. + * @param {number} [options.rotation=0] Image rotation in degrees. + * @param {boolean} [options.scaleContent=true] Scales print content to page size, keeping ratio. + * @param {Object} [options.size=null] Paper size. + * @param {number} [options.size.width=null] Page width. + * @param {number} [options.size.height=null] Page height. + * @param {string} [options.units='in'] Page units, applies to paper size, margins, and density. Valid value [in | cm | mm] + * + * @param {boolean} [options.forceRaw=false] Print the specified raw data using direct method, skipping the driver. Not yet supported on Windows. + * @param {string|Object} [options.encoding=null] Character set for commands. Can be provided as an object for converting encoding types for RAW types. + * @param {string} [options.encoding.from] If this encoding type is provided, RAW type commands will be parsed from this for the purpose of being converted to the encoding.to value. + * @param {string} [options.encoding.to] Encoding RAW type commands will be converted into. If encoding.from is not provided, this will be treated as if a string was passed for encoding. + * @param {string} [options.endOfDoc=null] DEPRECATED Raw only: Character(s) denoting end of a page to control spooling. + * @param {number} [options.perSpool=1] DEPRECATED: Raw only: Number of pages per spool. + * @param {boolean} [options.retainTemp=false] Retain any temporary files used. Ignored unless forceRaw true. + * @param {Object} [options.spool=null] Advanced spooling options. + * @param {number} [options.spool.size=null] Number of pages per spool. Default is no limit. If spool.end is provided, defaults to 1 + * @param {string} [options.spool.end=null] Raw only: Character(s) denoting end of a page to control spooling. + * + * @memberof qz.configs + */ + setDefaults: function(options) { + _qz.tools.extend(_qz.printing.defaultConfig, options); + }, + + /** + * Creates new printer config to be used in printing. + * + * @param {string|object} printer Name of printer. Use object type to specify printing to file or host. + * @param {string} [printer.name] Name of printer to send printing. + * @param {string} [printer.file] Name of file to send printing. + * @param {string} [printer.host] IP address or host name to send printing. + * @param {string} [printer.port] Port used by <printer.host>. + * @param {Object} [options] Override any of the default options for this config only. + * + * @returns {Config} The new config. + * + * @see configs.setDefaults + * + * @memberof qz.configs + */ + create: function(printer, options) { + return new Config(printer, options); + } + }, + + + /** + * Send data to selected config for printing. + * The promise for this method will resolve when the document has been sent to the printer. Actual printing may not be complete. + *

+ * Optionally, print requests can be pre-signed: + * Signed content consists of a JSON object string containing no spacing, + * following the format of the "call" and "params" keys in the API call, with the addition of a "timestamp" key in milliseconds + * ex. '{"call":"","params":{...},"timestamp":1450000000}' + * + * @param {Object|Array>} configs Previously created config object or objects. + * @param {Array|Array>} data Array of data being sent to the printer.
+ * String values are interpreted as {type: 'raw', format: 'command', flavor: 'plain', data: <string>}. + * @param {string} data.data + * @param {string} data.type Printing type. Valid types are [pixel | raw*]. *Default + * @param {string} data.format Format of data type used. *Default per type

+ * For [pixel] types, valid formats are [html | image* | pdf].

+ * For [raw] types, valid formats are [command* | html | image | pdf]. + * @param {string} data.flavor Flavor of data format used. *Default per format

+ * For [command] formats, valid flavors are [base64 | file | hex | plain* | xml].

+ * For [html] formats, valid flavors are [file* | plain].

+ * For [image] formats, valid flavors are [base64 | file*].

+ * For [pdf] formats, valid flavors are [base64 | file*]. + * @param {Object} [data.options] + * @param {string} [data.options.language] Required with [raw] type + [image] format. Printer language. + * @param {number} [data.options.x] Optional with [raw] type + [image] format. The X position of the image. + * @param {number} [data.options.y] Optional with [raw] type + [image] format. The Y position of the image. + * @param {string|number} [data.options.dotDensity] Optional with [raw] type + [image] format. + * @param {number} [data.precision=128] Optional with [raw] type [image] format. Bit precision of the ribbons. + * @param {boolean|string|Array>} [data.options.overlay=false] Optional with [raw] type [image] format. + * Boolean sets entire layer, string sets mask image, Array sets array of rectangles in format [x1,y1,x2,y2]. + * @param {string} [data.options.xmlTag] Required with [xml] flavor. Tag name containing base64 formatted data. + * @param {number} [data.options.pageWidth] Optional with [html | pdf] formats. Width of the rendering. + * Defaults to paper width. + * @param {number} [data.options.pageHeight] Optional with [html | pdf] formats. Height of the rendering. + * Defaults to paper height for [pdf], or auto sized for [html]. + * @param {string} [data.options.pageRanges] Optional with [pdf] formats. Comma-separated list of page ranges to include. + * @param {boolean} [data.options.ignoreTransparency=false] Optional with [pdf] formats. Instructs transparent PDF elements to be ignored. + * Transparent PDF elements are known to degrade performance and quality when printing. + * @param {boolean} [data.options.altFontRendering=false] Optional with [pdf] formats. Instructs PDF to be rendered using PDFBOX 1.8 techniques. + * Drastically improves low-DPI PDF print quality on Windows. + * @param {...*} [arguments] Additionally three more parameters can be specified:

+ * {boolean} [resumeOnError=false] Whether the chain should continue printing if it hits an error on one the the prints.

+ * {string|Array} [signature] Pre-signed signature(s) of the JSON string for containing call, params, and timestamp.

+ * {number|Array} [signingTimestamps] Required to match with signature. Timestamps for each of the passed pre-signed content. + * + * @returns {Promise} + * + * @see qz.configs.create + * + * @memberof qz + */ + print: function(configs, data) { + var resumeOnError = false, + signatures = [], + signaturesTimestamps = []; + + //find optional parameters + if (arguments.length >= 3) { + if (typeof arguments[2] === 'boolean') { + resumeOnError = arguments[2]; + + if (arguments.length >= 5) { + signatures = arguments[3]; + signaturesTimestamps = arguments[4]; + } + } else if (arguments.length >= 4) { + signatures = arguments[2]; + signaturesTimestamps = arguments[3]; + } + + //ensure values are arrays for consistency + if (signatures && !Array.isArray(signatures)) { signatures = [signatures]; } + if (signaturesTimestamps && !Array.isArray(signaturesTimestamps)) { signaturesTimestamps = [signaturesTimestamps]; } + } + + if (!Array.isArray(configs)) { configs = [configs]; } //single config -> array of configs + if (!Array.isArray(data[0])) { data = [data]; } //single data array -> array of data arrays + + //clean up data formatting + for(var d = 0; d < data.length; d++) { + _qz.tools.relative(data[d]); + _qz.compatible.data(data[d]); + } + + var sendToPrint = function(mapping) { + var params = { + printer: mapping.config.getPrinter(), + options: mapping.config.getOptions(), + data: mapping.data + }; + + return _qz.websocket.dataPromise('print', params, mapping.signature, mapping.timestamp); + }; + + //chain instead of Promise.all, so resumeOnError can collect each error + var chain = []; + for(var i = 0; i < configs.length || i < data.length; i++) { + (function(i_) { + var map = { + config: configs[Math.min(i_, configs.length - 1)], + data: data[Math.min(i_, data.length - 1)], + signature: signatures[i_], + timestamp: signaturesTimestamps[i_] + }; + + chain.push(function() { return sendToPrint(map) }); + })(i); + } + + //setup to catch errors if needed + var fallThrough = null; + if (resumeOnError) { + var fallen = []; + fallThrough = function(err) { fallen.push(err); }; + + //final promise to reject any errors as a group + chain.push(function() { + return _qz.tools.promise(function(resolve, reject) { + fallen.length ? reject(fallen) : resolve(); + }); + }); + } + + var last = null; + chain.reduce(function(sequence, link) { + last = sequence.catch(fallThrough).then(link); //catch is ignored if fallThrough is null + return last; + }, _qz.tools.promise(function(r) { r(); })); //an immediately resolved promise to start off the chain + + //return last promise so users can chain off final action or catch when stopping on error + return last; + }, + + + /** + * Calls related to interaction with serial ports. + * @namespace qz.serial + */ + serial: { + /** + * @returns {Promise|Error>} Communication (RS232, COM, TTY) ports available on connected system. + * + * @memberof qz.serial + */ + findPorts: function() { + return _qz.websocket.dataPromise('serial.findPorts'); + }, + + /** + * List of functions called for any response from open serial ports. + * Event data will contain {string} portName for all types. + * For RECEIVE types, {string} output. + * For ERROR types, {string} exception. + * + * @param {Function|Array} calls Single or array of Function({object} streamEvent) calls. + * + * @memberof qz.serial + */ + setSerialCallbacks: function(calls) { + _qz.serial.serialCallbacks = calls; + }, + + /** + * Opens a serial port for sending and receiving data + * + * @param {string} port Name of serial port to open. + * @param {Object} [options] Serial port configurations. + * @param {number} [options.baudRate=9600] Serial port speed. Set to 0 for auto negotiation. + * @param {number} [options.dataBits=8] Serial port data bits. Set to 0 for auto negotiation. + * @param {number} [options.stopBits=1] Serial port stop bits. Set to 0 for auto negotiation. + * @param {string} [options.parity='NONE'] Serial port parity. Set to AUTO for auto negotiation. Valid values [NONE | EVEN | ODD | MARK | SPACE | AUTO] + * @param {string} [options.flowControl='NONE'] Serial port flow control. Set to AUTO for auto negotiation. Valid values [NONE | XONXOFF | XONXOFF_OUT | XONXOFF_IN | RTSCTS | RTSCTS_OUT | RTSCTS_IN | AUTO] + * @param {string} [options.encoding='UTF-8'] Character set for communications. + * @param {string} [options.start=0x0002] DEPRECATED: Legacy character denoting start of serial response. Use options.rx.start instead. + * @param {string} [options.end=0x000D] DEPRECATED: Legacy character denoting end of serial response. Use options.rx.end instead. + * @param {number} [options.width] DEPRECATED: Legacy use for fixed-width response serial communication. Use options.rx.width instead. + * @param {Object} [options.rx] Serial communications response definitions. If an object is passed but no options are defined, all response data will be sent back as it is received unprocessed. + * @param {string|Array} [options.rx.start] Character(s) denoting start of response bytes. Used in conjunction with `end`, `width`, or `lengthbit` property. + * @param {string} [options.rx.end] Character denoting end of response bytes. Used in conjunction with `start` property. + * @param {number} [options.rx.width] Fixed width size of response bytes (not including header if `start` is set). Used alone or in conjunction with `start` property. + * @param {boolean} [options.rx.untilNewline] Returns data between newline characters (`\n` or `\r`) Truncates empty responses. Overrides `start`, `end`, `width`. + * @param {number|Object} [options.rx.lengthBytes] If a number is passed it is treated as the length index. Other values are left as their defaults. + * @param {number} [options.rx.lengthBytes.index=0] Position of the response byte (not including response `start` bytes) used to denote the length of the remaining response data. + * @param {number} [options.rx.lengthBytes.length=1] Length of response length bytes after response header. + * @param {string} [options.rx.lengthBytes.endian='BIG'] Byte endian for multi-byte length values. Valid values [BIG | LITTLE] + * @param {number|Object} [options.rx.crcBytes] If a number is passed it is treated as the crc length. Other values are left as their defaults. + * @param {number} [options.rx.crcBytes.index=0] Position after the response data (not including length or data bytes) used to denote the crc. + * @param {number} [options.rx.crcBytes.length=1] Length of response crc bytes after the response data length. + * @param {boolean} [options.rx.includeHeader=false] Whether any of the header bytes (`start` bytes and any length bytes) should be included in the processed response. + * @param {string} [options.rx.encoding] Override the encoding used for response data. Uses the same value as options.encoding otherwise. + * + * @returns {Promise} + * + * @memberof qz.serial + */ + openPort: function(port, options) { + var params = { + port: port, + options: options + }; + return _qz.websocket.dataPromise('serial.openPort', params); + }, + + /** + * Send commands over a serial port. + * Any responses from the device will be sent to serial callback functions. + * + * @param {string} port An open serial port to send data. + * @param {string|Array|Object} data Data to be sent to the serial device. + * @param {string} [data.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @param {string|Array} data.data Data to be sent to the serial device. + * @param {Object} options Serial port configuration updates. See qz.serial.openPort `options` docs for available values. + * For best performance, it is recommended to only set these values on the port open call. + * + * @returns {Promise} + * + * @see qz.serial.setSerialCallbacks + * + * @memberof qz.serial + */ + sendData: function(port, data, options) { + if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) { + if (typeof data !== 'object') { + data = { + data: data, + type: "PLAIN" + } + } + + if (data.type && data.type.toUpperCase() == "FILE") { + data.data = _qz.tools.absolute(data.data); + } + } + + var params = { + port: port, + data: data, + options: options + }; + return _qz.websocket.dataPromise('serial.sendData', params); + }, + + /** + * @param {string} port Name of port to close. + * + * @returns {Promise} + * + * @memberof qz.serial + */ + closePort: function(port) { + return _qz.websocket.dataPromise('serial.closePort', { port: port }); + } + }, + + /** + * Calls related to interaction with communication sockets. + * @namespace qz.socket + */ + socket: { + /** + * Opens a network port for sending and receiving data. + * + * @param {string} host The connection hostname. + * @param {number} port The connection port number. + * @param {Object} [options] Network socket configuration. + * @param {string} [options.encoding='UTF-8'] Character set for communications. + * + * @memberof qz.socket + */ + open: function(host, port, options) { + var params = { + host: host, + port: port, + options: options + }; + return _qz.websocket.dataPromise("socket.open", params); + }, + + /** + * @param {string} host The connection hostname. + * @param {number} port The connection port number. + * + * @memberof qz.socket + */ + close: function(host, port) { + var params = { + host: host, + port: port + }; + return _qz.websocket.dataPromise("socket.close", params); + }, + + /** + * Send data over an open socket. + * + * @param {string} host The connection hostname. + * @param {number} port The connection port number. + * @param {string|Object} data Data to be sent over the port. + * @param {string} [data.type='PLAIN'] Valid values [PLAIN] + * @param {string} data.data Data to be sent over the port. + * + * @memberof qz.socket + */ + sendData: function(host, port, data) { + if (typeof data !== 'object') { + data = { + data: data, + type: "PLAIN" + }; + } + + var params = { + host: host, + port: port, + data: data + }; + return _qz.websocket.dataPromise("socket.sendData", params); + }, + + /** + * List of functions called for any response from open network sockets. + * Event data will contain {string} host and {number} port for all types. + * For RECEIVE types, {string} response. + * For ERROR types, {string} exception. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * + * @memberof qz.socket + */ + setSocketCallbacks: function(calls) { + _qz.socket.socketCallbacks = calls; + } + }, + + /** + * Calls related to interaction with USB devices. + * @namespace qz.usb + */ + usb: { + /** + * List of available USB devices. Includes (hexadecimal) vendor ID, (hexadecimal) product ID, and hub status. + * If supported, also returns manufacturer and product descriptions. + * + * @param includeHubs Whether to include USB hubs. + * @returns {Promise|Error>} Array of JSON objects containing information on connected USB devices. + * + * @memberof qz.usb + */ + listDevices: function(includeHubs) { + return _qz.websocket.dataPromise('usb.listDevices', { includeHubs: includeHubs }); + }, + + /** + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @returns {Promise|Error>} List of available (hexadecimal) interfaces on a USB device. + * + * @memberof qz.usb + */ + listInterfaces: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('usb.listInterfaces', deviceInfo); + }, + + /** + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.iface Hex string of interface on the USB device to search. + * @returns {Promise|Error>} List of available (hexadecimal) endpoints on a USB device's interface. + * + * @memberof qz.usb + */ + listEndpoints: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + interface: arguments[2] + }; + } + + return _qz.websocket.dataPromise('usb.listEndpoints', deviceInfo); + }, + + /** + * List of functions called for any response from open usb devices. + * Event data will contain {string} vendorId and {string} productId for all types. + * For RECEIVE types, {Array} output (in hexadecimal format). + * For ERROR types, {string} exception. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * + * @memberof qz.usb + */ + setUsbCallbacks: function(calls) { + _qz.usb.usbCallbacks = calls; + }, + + /** + * Claim a USB device's interface to enable sending/reading data across an endpoint. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.interface Hex string of interface on the USB device to claim. + * @returns {Promise} + * + * @memberof qz.usb + */ + claimDevice: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + interface: arguments[2] + }; + } + + return _qz.websocket.dataPromise('usb.claimDevice', deviceInfo); + }, + + /** + * Check the current claim state of a USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @returns {Promise} + * + * @since 2.0.2 + * @memberOf qz.usb + */ + isClaimed: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('usb.isClaimed', deviceInfo); + }, + + /** + * Send data to a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @param deviceInfo.data Bytes to send over specified endpoint. + * @param {string} [deviceInfo.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @returns {Promise} + * + * @memberof qz.usb + */ + sendData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2], + data: arguments[3] + }; + } + + if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) { + if (typeof deviceInfo.data !== 'object') { + deviceInfo.data = { + data: deviceInfo.data, + type: "PLAIN" + } + } + + if (deviceInfo.data.type && deviceInfo.data.type.toUpperCase() == "FILE") { + deviceInfo.data.data = _qz.tools.absolute(deviceInfo.data.data); + } + } + + return _qz.websocket.dataPromise('usb.sendData', deviceInfo); + }, + + /** + * Read data from a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @returns {Promise|Error>} List of (hexadecimal) bytes received from the USB device. + * + * @memberof qz.usb + */ + readData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2], + responseSize: arguments[3] + }; + } + + return _qz.websocket.dataPromise('usb.readData', deviceInfo); + }, + + /** + * Provides a continuous stream of read data from a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @param deviceInfo.interval=100 Frequency to send read data back, in milliseconds. + * @returns {Promise} + * + * @see qz.usb.setUsbCallbacks + * + * @memberof qz.usb + */ + openStream: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2], + responseSize: arguments[3], + interval: arguments[4] + }; + } + + return _qz.websocket.dataPromise('usb.openStream', deviceInfo); + }, + + /** + * Stops the stream of read data from a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @returns {Promise} + * + * @memberof qz.usb + */ + closeStream: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2] + }; + } + + return _qz.websocket.dataPromise('usb.closeStream', deviceInfo); + }, + + /** + * Release a claimed USB device to free resources after sending/reading data. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @returns {Promise} + * + * @memberof qz.usb + */ + releaseDevice: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('usb.releaseDevice', deviceInfo); + } + }, + + + /** + * Calls related to interaction with HID USB devices
+ * Many of these calls can be accomplished from the qz.usb namespace, + * but HID allows for simpler interaction + * @namespace qz.hid + * @since 2.0.1 + */ + hid: { + /** + * List of available HID devices. Includes (hexadecimal) vendor ID and (hexadecimal) product ID. + * If available, also returns manufacturer and product descriptions. + * + * @returns {Promise|Error>} Array of JSON objects containing information on connected HID devices. + * @since 2.0.1 + * + * @memberof qz.hid + */ + listDevices: function() { + return _qz.websocket.dataPromise('hid.listDevices'); + }, + + /** + * Start listening for HID device actions, such as attach / detach events. + * Reported under the ACTION type in the streamEvent on callbacks. + * + * @returns {Promise} + * @since 2.0.1 + * + * @see qz.hid.setHidCallbacks + * + * @memberof qz.hid + */ + startListening: function() { + return _qz.websocket.dataPromise('hid.startListening'); + }, + + /** + * Stop listening for HID device actions. + * + * @returns {Promise} + * @since 2.0.1 + * + * @see qz.hid.setHidCallbacks + * + * @memberof qz.hid + */ + stopListening: function() { + return _qz.websocket.dataPromise('hid.stopListening'); + }, + + /** + * List of functions called for any response from open usb devices. + * Event data will contain {string} vendorId and {string} productId for all types. + * For RECEIVE types, {Array} output (in hexadecimal format). + * For ERROR types, {string} exception. + * For ACTION types, {string} actionType. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * @since 2.0.1 + * + * @memberof qz.hid + */ + setHidCallbacks: function(calls) { + _qz.hid.hidCallbacks = calls; + }, + + /** + * Claim a HID device to enable sending/reading data across. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + claimDevice: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.claimDevice', deviceInfo); + }, + + /** + * Check the current claim state of a HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * + * @since 2.0.2 + * @memberOf qz.hid + */ + isClaimed: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.isClaimed', deviceInfo); + }, + + /** + * Send data to a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.data Bytes to send over specified endpoint. + * @param deviceInfo.endpoint=0x00 First byte of the data packet signifying the HID report ID. + * Must be 0x00 for devices only supporting a single report. + * @param deviceInfo.reportId=0x00 Alias for deviceInfo.endpoint. Not used if endpoint is provided. + * @param {string} [deviceInfo.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + sendData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + data: arguments[2], + endpoint: arguments[3] + }; + } + + if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) { + if (typeof deviceInfo.data !== 'object') { + deviceInfo.data = { + data: deviceInfo.data, + type: "PLAIN" + } + } + + if (deviceInfo.data.type && deviceInfo.data.type.toUpperCase() == "FILE") { + deviceInfo.data.data = _qz.tools.absolute(deviceInfo.data.data); + } + } else { + if (typeof deviceInfo.data === 'object') { + if (deviceInfo.data.type.toUpperCase() !== "PLAIN" + || typeof deviceInfo.data.data !== "string") { + return _qz.tools.reject(new Error("Data format is not supported with connected QZ Tray version " + _qz.websocket.connection.version)); + } + + deviceInfo.data = deviceInfo.data.data; + } + } + + return _qz.websocket.dataPromise('hid.sendData', deviceInfo); + }, + + /** + * Read data from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @returns {Promise|Error>} List of (hexadecimal) bytes received from the HID device. + * @since 2.0.1 + * + * @memberof qz.hid + */ + readData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + responseSize: arguments[2] + }; + } + + return _qz.websocket.dataPromise('hid.readData', deviceInfo); + }, + + /** + * Send a feature report to a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.data Bytes to send over specified endpoint. + * @param deviceInfo.endpoint=0x00 First byte of the data packet signifying the HID report ID. + * Must be 0x00 for devices only supporting a single report. + * @param deviceInfo.reportId=0x00 Alias for deviceInfo.endpoint. Not used if endpoint is provided. + * @param {string} [deviceInfo.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @returns {Promise} + * + * @memberof qz.hid + */ + sendFeatureReport: function(deviceInfo) { + return _qz.websocket.dataPromise('hid.sendFeatureReport', deviceInfo); + }, + + /** + * Get a feature report from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @returns {Promise|Error>} List of (hexadecimal) bytes received from the HID device. + * + * @memberof qz.hid + */ + getFeatureReport: function(deviceInfo) { + return _qz.websocket.dataPromise('hid.getFeatureReport', deviceInfo); + }, + + /** + * Provides a continuous stream of read data from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @param deviceInfo.interval=100 Frequency to send read data back, in milliseconds. + * @returns {Promise} + * @since 2.0.1 + * + * @see qz.hid.setHidCallbacks + * + * @memberof qz.hid + */ + openStream: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + responseSize: arguments[2], + interval: arguments[3] + }; + } + + return _qz.websocket.dataPromise('hid.openStream', deviceInfo); + }, + + /** + * Stops the stream of read data from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + closeStream: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.closeStream', deviceInfo); + }, + + /** + * Release a claimed HID device to free resources after sending/reading data. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + releaseDevice: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.releaseDevice', deviceInfo); + } + }, + + + /** + * Calls related to interactions with the filesystem + * @namespace qz.file + * @since 2.1 + */ + file: { + /** + * List of files available at the given directory.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute directory path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @returns {Promise|Error>} Array of files at the given path + * + * @memberof qz.file + */ + list: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.list', param); + }, + + /** + * Reads contents of file at the given path.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @param {string} [params.flavor='plain'] Flavor of data format used, valid flavors are [base64 | hex | plain]. + * @returns {Promise} String containing the file contents + * + * @memberof qz.file + */ + read: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.read', param); + }, + + /** + * Writes data to the file at the given path.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location. + * @param {Object} params Object containing file access parameters + * @param {string} params.data File data to be written + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @param {boolean} [params.append=false] Appends to the end of the file if set, otherwise overwrites existing contents + * @param {string} [params.flavor='plain'] Flavor of data format used, valid flavors are [base64 | file | hex | plain]. + * @returns {Promise} + * + * @memberof qz.file + */ + write: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.write', param); + }, + + /** + * Deletes a file at given path.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @returns {Promise} + * + * @memberof qz.file + */ + remove: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.remove', param); + }, + + /** + * Provides a continuous stream of events (and optionally data) from a local file. + * + * @param {string} path Relative or absolute directory path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @param {Object} [params.listener] If defined, file data will be returned on events + * @param {number} [params.listener.bytes=-1] Number of bytes to return or -1 for all + * @param {number} [params.listener.lines=-1] Number of lines to return or -1 for all + * @param {boolean} [params.listener.reverse] Controls whether data should be returned from the bottom of the file. Default value is true for line mode and false for byte mode. + * @param {string|Array} [params.include] File patterns to match. Blank values will be ignored. + * @param {string|Array} [params.exclude] File patterns to exclude. Blank values will be ignored. Takes priority over params.include. + * @param {boolean} [params.ignoreCase=true] Whether params.include or params.exclude are case-sensitive. + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.file.setFileCallbacks + * + * @memberof qz.file + */ + startListening: function(path, params) { + if (params && typeof params.include !== 'undefined' && !Array.isArray(params.include)) { + params.include = [params.include]; + } + if (params && typeof params.exclude !== 'undefined' && !Array.isArray(params.exclude)) { + params.exclude = [params.exclude]; + } + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.startListening', param); + }, + + /** + * Closes listeners with the provided settings. Omitting the path parameter will result in all listeners closing. + * + * @param {string} [path] Previously opened directory path of listener to close, or omit to close all. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @returns {Promise} + * + * @memberof qz.file + */ + stopListening: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.stopListening', param); + }, + + /** + * List of functions called for any response from a file listener. + * For ERROR types event data will contain, {string} message. + * For ACTION types event data will contain, {string} file {string} eventType {string} [data]. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * @since 2.1.0 + * + * @memberof qz.file + */ + setFileCallbacks: function(calls) { + _qz.file.fileCallbacks = calls; + } + }, + + /** + * Calls related to networking information + * @namespace qz.networking + * @since 2.1.0 + */ + networking: { + /** + * @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] Port to use with custom hostname, defaults to 443 + * @returns {Promise} Connected system's network information. + * + * @memberof qz.networking + * @since 2.1.0 + */ + device: function(hostname, port) { + // Wrap 2.0 + if (_qz.tools.isVersion(2, 0)) { + return _qz.compatible.networking(hostname, port, null, null, function(data) { + return { ip: data.ipAddress, mac: data.macAddress }; + }); + } + // Use 2.1 + return _qz.websocket.dataPromise('networking.device', { + hostname: hostname, + port: port + }); + }, + + /** + * Get computer hostname + * + * @param {string} [hostname] DEPRECATED Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] DEPRECATED Port to use with custom hostname, defaults to 443 + * @returns {Promise} Connected system's hostname. + * + * @memberof qz.networking + * @since 2.2.2 + */ + hostname: function(hostname, port) { + // Wrap < 2.2.2 + if (_qz.tools.versionCompare(2, 2, 2) < 0) { + return _qz.tools.promise(function(resolve, reject) { + _qz.websocket.dataPromise('networking.device', { hostname: hostname, port: port }).then(function(device) { + console.log(device); + resolve(device.hostname); + }); + }); + } else { + return _qz.websocket.dataPromise('networking.hostname'); + } + }, + + /** + * @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] Port to use with custom hostname, defaults to 443 + * @returns {Promise|Error>} Connected system's network information. + * + * @memberof qz.networking + * @since 2.1.0 + */ + devices: function(hostname, port) { + // Wrap 2.0 + if (_qz.tools.isVersion(2, 0)) { + return _qz.compatible.networking(hostname, port, null, null, function(data) { + return [{ ip: data.ipAddress, mac: data.macAddress }]; + }); + } + // Use 2.1 + return _qz.websocket.dataPromise('networking.devices', { + hostname: hostname, + port: port + }); + } + }, + + + /** + * Calls related to signing connection requests. + * @namespace qz.security + */ + security: { + /** + * Set promise resolver for calls to acquire the site's certificate. + * + * @param {Function|AsyncFunction|Promise} promiseHandler Either a function that will be used as a promise resolver (of format Function({function} resolve, {function}reject)), + * an async function, or a promise. Any of which should return the public certificate via their respective resolve call. + * @param {Object} [options] Configuration options for the certificate resolver + * @param {boolean} [options.rejectOnFailure=[false]] Overrides default behavior to call resolve with a blank certificate on failure. + * @memberof qz.security + */ + setCertificatePromise: function(promiseHandler, options) { + _qz.security.certHandler = promiseHandler; + _qz.security.rejectOnCertFailure = !!(options && options.rejectOnFailure); + }, + + /** + * Set promise factory for calls to sign API calls. + * + * @param {Function|AsyncFunction} promiseFactory Either a function that accepts a string parameter of the data to be signed + * and returns a function to be used as a promise resolver (of format Function({function} resolve, {function}reject)), + * or an async function that can take a string parameter of the data to be signed. Either of which should return the signed contents of + * the passed string parameter via their respective resolve call. + * + * @example + * qz.security.setSignaturePromise(function(dataToSign) { + * return function(resolve, reject) { + * $.ajax("/signing-url?data=" + dataToSign).then(resolve, reject); + * } + * }) + * + * @memberof qz.security + */ + setSignaturePromise: function(promiseFactory) { + _qz.security.signatureFactory = promiseFactory; + }, + + /** + * Set which signing algorithm QZ will check signatures against. + * + * @param {string} algorithm The algorithm used in signing. Valid values: [SHA1 | SHA256 | SHA512] + * @since 2.1.0 + * + * @memberof qz.security + */ + setSignatureAlgorithm: function(algorithm) { + //warn for incompatibilities if known + if (!_qz.compatible.algorithm()) { + return; + } + + if (["SHA1", "SHA256", "SHA512"].indexOf(algorithm.toUpperCase()) < 0) { + _qz.log.error("Signing algorithm '" + algorithm + "' is not supported."); + } else { + _qz.security.signAlgorithm = algorithm; + } + }, + + /** + * Get the signing algorithm QZ will be checking signatures against. + * + * @returns {string} The algorithm used in signing. + * @since 2.1.0 + * + * @memberof qz.security + */ + getSignatureAlgorithm: function() { + return _qz.security.signAlgorithm; + } + }, + + /** + * Calls related to compatibility adjustments + * @namespace qz.api + */ + api: { + /** + * Show or hide QZ api debugging statements in the browser console. + * + * @param {boolean} show Whether the debugging logs for QZ should be shown. Hidden by default. + * @returns {boolean} Value of debugging flag + * @memberof qz.api + */ + showDebug: function(show) { + return (_qz.DEBUG = show); + }, + + /** + * Get version of connected QZ Tray application. + * + * @returns {Promise} Version number of QZ Tray. + * + * @memberof qz.api + */ + getVersion: function() { + return _qz.websocket.dataPromise('getVersion'); + }, + + /** + * Checks for the specified version of connected QZ Tray application. + * + * @param {string|number} [major] Major version to check + * @param {string|number} [minor] Minor version to check + * @param {string|number} [patch] Patch version to check + * + * @memberof qz.api + */ + isVersion: _qz.tools.isVersion, + + /** + * Checks if the connected QZ Tray application is greater than the specified version. + * + * @param {string|number} major Major version to check + * @param {string|number} [minor] Minor version to check + * @param {string|number} [patch] Patch version to check + * @param {string|number} [build] Build version to check + * @returns {boolean} True if connected version is greater than the version specified. + * + * @memberof qz.api + * @since 2.1.0-4 + */ + isVersionGreater: function(major, minor, patch, build) { + return _qz.tools.versionCompare(major, minor, patch, build) > 0; + }, + + /** + * Checks if the connected QZ Tray application is less than the specified version. + * + * @param {string|number} major Major version to check + * @param {string|number} [minor] Minor version to check + * @param {string|number} [patch] Patch version to check + * @param {string|number} [build] Build version to check + * @returns {boolean} True if connected version is less than the version specified. + * + * @memberof qz.api + * @since 2.1.0-4 + */ + isVersionLess: function(major, minor, patch, build) { + return _qz.tools.versionCompare(major, minor, patch, build) < 0; + }, + + /** + * Change the promise library used by QZ API. + * Should be called before any initialization to avoid possible errors. + * + * @param {Function} promiser Function({function} resolver) called to create new promises. + * + * @memberof qz.api + */ + setPromiseType: function(promiser) { + _qz.tools.promise = promiser; + }, + + /** + * Change the SHA-256 hashing function used by QZ API. + * Should be called before any initialization to avoid possible errors. + * + * @param {Function} hasher Function({function} message) called to create hash of passed string. + * + * @memberof qz.api + */ + setSha256Type: function(hasher) { + _qz.tools.hash = hasher; + }, + + /** + * Change the WebSocket handler. + * Should be called before any initialization to avoid possible errors. + * + * @param {Function} ws Function({function} WebSocket) called to override the internal WebSocket handler. + * + * @memberof qz.api + */ + setWebSocketType: function(ws) { + _qz.tools.ws = ws; + } + }, + + /** + * Version of this JavaScript library + * + * @constant {string} + * + * @memberof qz + */ + version: _qz.VERSION + }; + + return qz; +})(); + + +(function() { + if (typeof define === 'function' && define.amd) { + define(qz); + } else if (typeof exports === 'object') { + module.exports = qz; + } else { + window.qz = qz; + } +})(); diff --git a/py_app/app/static/qz-tray.js.backup b/py_app/app/static/qz-tray.js.backup new file mode 100644 index 0000000..46633a1 --- /dev/null +++ b/py_app/app/static/qz-tray.js.backup @@ -0,0 +1,2859 @@ +'use strict'; + +/** + * @version 2.2.4 + * @overview QZ Tray Connector + * @license LGPL-2.1-only + *

+ * Connects a web client to the QZ Tray software. + * Enables printing and device communication from javascript. + */ +var qz = (function() { + +///// POLYFILLS ///// + + if (!Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; + } + + if (!Number.isInteger) { + Number.isInteger = function(value) { + return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; + }; + } + +///// PRIVATE METHODS ///// + + var _qz = { + VERSION: "2.2.4", //must match @version above + DEBUG: false, + + log: { + /** Debugging messages */ + trace: function() { if (_qz.DEBUG) { console.log.apply(console, arguments); } }, + /** General messages */ + info: function() { console.info.apply(console, arguments); }, + /** General warnings */ + warn: function() { console.warn.apply(console, arguments); }, + /** Debugging errors */ + allay: function() { if (_qz.DEBUG) { console.warn.apply(console, arguments); } }, + /** General errors */ + error: function() { console.error.apply(console, arguments); } + }, + + + //stream types + streams: { + serial: 'SERIAL', usb: 'USB', hid: 'HID', printer: 'PRINTER', file: 'FILE', socket: 'SOCKET' + }, + + + websocket: { + /** The actual websocket object managing the connection. */ + connection: null, + /** Track if a connection attempt is being cancelled. */ + shutdown: false, + + /** Default parameters used on new connections. Override values using options parameter on {@link qz.websocket.connect}. */ + connectConfig: { + host: ["localhost", "localhost.qz.io"], //hosts QZ Tray can be running on + hostIndex: 0, //internal var - index on host array + usingSecure: true, //boolean use of secure protocol + protocol: { + secure: "wss://", //secure websocket + insecure: "ws://" //insecure websocket + }, + port: { + secure: [8181, 8282, 8383, 8484], //list of secure ports QZ Tray could be listening on + insecure: [8182, 8283, 8384, 8485], //list of insecure ports QZ Tray could be listening on + portIndex: 0 //internal var - index on active port array + }, + keepAlive: 60, //time between pings to keep connection alive, in seconds + retries: 0, //number of times to reconnect before failing + delay: 0 //seconds before firing a connection + }, + + setup: { + /** Loop through possible ports to open connection, sets web socket calls that will settle the promise. */ + findConnection: function(config, resolve, reject) { + if (_qz.websocket.shutdown) { + reject(new Error("Connection attempt cancelled by user")); + return; + } + + //force flag if missing ports + if (!config.port.secure.length) { + if (!config.port.insecure.length) { + reject(new Error("No ports have been specified to connect over")); + return; + } else if (config.usingSecure) { + _qz.log.error("No secure ports specified - forcing insecure connection"); + config.usingSecure = false; + } + } else if (!config.port.insecure.length && !config.usingSecure) { + _qz.log.trace("No insecure ports specified - forcing secure connection"); + config.usingSecure = true; + } + + var deeper = function() { + if (_qz.websocket.shutdown) { + //connection attempt was cancelled, bail out + reject(new Error("Connection attempt cancelled by user")); + return; + } + + config.port.portIndex++; + + if ((config.usingSecure && config.port.portIndex >= config.port.secure.length) + || (!config.usingSecure && config.port.portIndex >= config.port.insecure.length)) { + if (config.hostIndex >= config.host.length - 1) { + //give up, all hope is lost + reject(new Error("Unable to establish connection with QZ")); + return; + } else { + config.hostIndex++; + config.port.portIndex = 0; + } + } + + // recursive call until connection established or all ports are exhausted + _qz.websocket.setup.findConnection(config, resolve, reject); + }; + + var address; + if (config.usingSecure) { + address = config.protocol.secure + config.host[config.hostIndex] + ":" + config.port.secure[config.port.portIndex]; + } else { + address = config.protocol.insecure + config.host[config.hostIndex] + ":" + config.port.insecure[config.port.portIndex]; + } + + try { + _qz.log.trace("Attempting connection", address); + _qz.websocket.connection = new _qz.tools.ws(address); + } + catch(err) { + _qz.log.error(err); + deeper(); + return; + } + + if (_qz.websocket.connection != null) { + _qz.websocket.connection.established = false; + + //called on successful connection to qz, begins setup of websocket calls and resolves connect promise after certificate is sent + _qz.websocket.connection.onopen = function(evt) { + if (!_qz.websocket.connection.established) { + _qz.log.trace(evt); + _qz.log.info("Established connection with QZ Tray on " + address); + + _qz.websocket.setup.openConnection({ resolve: resolve, reject: reject }); + + if (config.keepAlive > 0) { + var interval = setInterval(function() { + if (!_qz.tools.isActive() || _qz.websocket.connection.interval !== interval) { + clearInterval(interval); + return; + } + + _qz.websocket.connection.send("ping"); + }, config.keepAlive * 1000); + + _qz.websocket.connection.interval = interval; + } + } + }; + + //called during websocket close during setup + _qz.websocket.connection.onclose = function() { + // Safari compatibility fix to raise error event + if (_qz.websocket.connection && typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { + _qz.websocket.connection.onerror(); + } + }; + + //called for errors during setup (such as invalid ports), reject connect promise only if all ports have been tried + _qz.websocket.connection.onerror = function(evt) { + _qz.log.trace(evt); + + _qz.websocket.connection = null; + + deeper(); + }; + } else { + reject(new Error("Unable to create a websocket connection")); + } + }, + + /** Finish setting calls on successful connection, sets web socket calls that won't settle the promise. */ + openConnection: function(openPromise) { + _qz.websocket.connection.established = true; + + //called when an open connection is closed + _qz.websocket.connection.onclose = function(evt) { + _qz.log.trace(evt); + + _qz.websocket.connection = null; + _qz.websocket.callClose(evt); + _qz.log.info("Closed connection with QZ Tray"); + + for(var uid in _qz.websocket.pendingCalls) { + if (_qz.websocket.pendingCalls.hasOwnProperty(uid)) { + _qz.websocket.pendingCalls[uid].reject(new Error("Connection closed before response received")); + } + } + + //if this is set, then an explicit close call was made + if (this.promise != undefined) { + this.promise.resolve(); + } + }; + + //called for any errors with an open connection + _qz.websocket.connection.onerror = function(evt) { + _qz.websocket.callError(evt); + }; + + //send JSON objects to qz + _qz.websocket.connection.sendData = function(obj) { + _qz.log.trace("Preparing object for websocket", obj); + + if (obj.timestamp == undefined) { + obj.timestamp = Date.now(); + if (typeof obj.timestamp !== 'number') { + obj.timestamp = new Date().getTime(); + } + } + if (obj.promise != undefined) { + obj.uid = _qz.websocket.setup.newUID(); + _qz.websocket.pendingCalls[obj.uid] = obj.promise; + } + + // track requesting monitor + obj.position = { + x: typeof screen !== 'undefined' ? ((screen.availWidth || screen.width) / 2) + (screen.left || screen.availLeft || 0) : 0, + y: typeof screen !== 'undefined' ? ((screen.availHeight || screen.height) / 2) + (screen.top || screen.availTop || 0) : 0 + }; + + try { + if (obj.call != undefined && obj.signature == undefined && _qz.security.needsSigned(obj.call)) { + var signObj = { + call: obj.call, + params: obj.params, + timestamp: obj.timestamp + }; + + //make a hashing promise if not already one + var hashing = _qz.tools.hash(_qz.tools.stringify(signObj)); + if (!hashing.then) { + hashing = _qz.tools.promise(function(resolve) { + resolve(hashing); + }); + } + + hashing.then(function(hashed) { + return _qz.security.callSign(hashed); + }).then(function(signature) { + _qz.log.trace("Signature for call", signature); + obj.signature = signature || ""; + obj.signAlgorithm = _qz.security.signAlgorithm; + + _qz.signContent = undefined; + _qz.websocket.connection.send(_qz.tools.stringify(obj)); + }).catch(function(err) { + _qz.log.error("Signing failed", err); + + if (obj.promise != undefined) { + obj.promise.reject(new Error("Failed to sign request")); + delete _qz.websocket.pendingCalls[obj.uid]; + } + }); + } else { + _qz.log.trace("Signature for call", obj.signature); + + //called for pre-signed content and (unsigned) setup calls + _qz.websocket.connection.send(_qz.tools.stringify(obj)); + } + } + catch(err) { + _qz.log.error(err); + + if (obj.promise != undefined) { + obj.promise.reject(err); + delete _qz.websocket.pendingCalls[obj.uid]; + } + } + }; + + //receive message from qz + _qz.websocket.connection.onmessage = function(evt) { + var returned = JSON.parse(evt.data); + + if (returned.uid == null) { + if (returned.type == null) { + //incorrect response format, likely connected to incompatible qz version + _qz.websocket.connection.close(4003, "Connected to incompatible QZ Tray version"); + + } else { + //streams (callbacks only, no promises) + switch(returned.type) { + case _qz.streams.serial: + if (!returned.event) { + returned.event = JSON.stringify({ portName: returned.key, output: returned.data }); + } + + _qz.serial.callSerial(JSON.parse(returned.event)); + break; + case _qz.streams.socket: + _qz.socket.callSocket(JSON.parse(returned.event)); + break; + case _qz.streams.usb: + if (!returned.event) { + returned.event = JSON.stringify({ vendorId: returned.key[0], productId: returned.key[1], output: returned.data }); + } + + _qz.usb.callUsb(JSON.parse(returned.event)); + break; + case _qz.streams.hid: + _qz.hid.callHid(JSON.parse(returned.event)); + break; + case _qz.streams.printer: + _qz.printers.callPrinter(JSON.parse(returned.event)); + break; + case _qz.streams.file: + _qz.file.callFile(JSON.parse(returned.event)); + break; + default: + _qz.log.allay("Cannot determine stream type for callback", returned); + break; + } + } + + return; + } + + _qz.log.trace("Received response from websocket", returned); + + var promise = _qz.websocket.pendingCalls[returned.uid]; + if (promise == undefined) { + _qz.log.allay('No promise found for returned response'); + } else { + if (returned.error != undefined) { + promise.reject(new Error(returned.error)); + } else { + promise.resolve(returned.result); + } + } + + delete _qz.websocket.pendingCalls[returned.uid]; + }; + + + //send up the certificate before making any calls + //also gives the user a chance to deny the connection + function sendCert(cert) { + if (cert === undefined) { cert = null; } + + //websocket setup, query what version is connected + qz.api.getVersion().then(function(version) { + _qz.websocket.connection.version = version; + _qz.websocket.connection.semver = version.toLowerCase().replace(/-rc\./g, "-rc").split(/[\\+\\.-]/g); + for(var i = 0; i < _qz.websocket.connection.semver.length; i++) { + try { + if (i == 3 && _qz.websocket.connection.semver[i].toLowerCase().indexOf("rc") == 0) { + // Handle "rc1" pre-release by negating build info + _qz.websocket.connection.semver[i] = -(_qz.websocket.connection.semver[i].replace(/\D/g, "")); + continue; + } + _qz.websocket.connection.semver[i] = parseInt(_qz.websocket.connection.semver[i]); + } + catch(ignore) {} + + if (_qz.websocket.connection.semver.length < 4) { + _qz.websocket.connection.semver[3] = 0; + } + } + + //algorithm can be declared before a connection, check for incompatibilities now that we have one + _qz.compatible.algorithm(true); + }).then(function() { + _qz.websocket.connection.sendData({ certificate: cert, promise: openPromise }); + }); + } + + _qz.security.callCert().then(sendCert).catch(function(error) { + _qz.log.warn("Failed to get certificate:", error); + + if (_qz.security.rejectOnCertFailure) { + openPromise.reject(error); + } else { + sendCert(null); + } + }); + }, + + /** Generate unique ID used to map a response to a call. */ + newUID: function() { + var len = 6; + return (new Array(len + 1).join("0") + (Math.random() * Math.pow(36, len) << 0).toString(36)).slice(-len) + } + }, + + dataPromise: function(callName, params, signature, signingTimestamp) { + return _qz.tools.promise(function(resolve, reject) { + var msg = { + call: callName, + promise: { resolve: resolve, reject: reject }, + params: params, + signature: signature, + timestamp: signingTimestamp + }; + + _qz.websocket.connection.sendData(msg); + }); + }, + + /** Library of promises awaiting a response, uid -> promise */ + pendingCalls: {}, + + /** List of functions to call on error from the websocket. */ + errorCallbacks: [], + /** Calls all functions registered to listen for errors. */ + callError: function(evt) { + if (Array.isArray(_qz.websocket.errorCallbacks)) { + for(var i = 0; i < _qz.websocket.errorCallbacks.length; i++) { + _qz.websocket.errorCallbacks[i](evt); + } + } else { + _qz.websocket.errorCallbacks(evt); + } + }, + + /** List of function to call on closing from the websocket. */ + closedCallbacks: [], + /** Calls all functions registered to listen for closing. */ + callClose: function(evt) { + if (Array.isArray(_qz.websocket.closedCallbacks)) { + for(var i = 0; i < _qz.websocket.closedCallbacks.length; i++) { + _qz.websocket.closedCallbacks[i](evt); + } + } else { + _qz.websocket.closedCallbacks(evt); + } + } + }, + + + printing: { + /** Default options used for new printer configs. Can be overridden using {@link qz.configs.setDefaults}. */ + defaultConfig: { + //value purposes are explained in the qz.configs.setDefaults docs + + bounds: null, + colorType: 'color', + copies: 1, + density: 0, + duplex: false, + fallbackDensity: null, + interpolation: 'bicubic', + jobName: null, + legacy: false, + margins: 0, + orientation: null, + paperThickness: null, + printerTray: null, + rasterize: false, + rotation: 0, + scaleContent: true, + size: null, + units: 'in', + + forceRaw: false, + encoding: null, + spool: null + } + }, + + + serial: { + /** List of functions called when receiving data from serial connection. */ + serialCallbacks: [], + /** Calls all functions registered to listen for serial events. */ + callSerial: function(streamEvent) { + if (Array.isArray(_qz.serial.serialCallbacks)) { + for(var i = 0; i < _qz.serial.serialCallbacks.length; i++) { + _qz.serial.serialCallbacks[i](streamEvent); + } + } else { + _qz.serial.serialCallbacks(streamEvent); + } + } + }, + + + socket: { + /** List of functions called when receiving data from network socket connection. */ + socketCallbacks: [], + /** Calls all functions registered to listen for network socket events. */ + callSocket: function(socketEvent) { + if (Array.isArray(_qz.socket.socketCallbacks)) { + for(var i = 0; i < _qz.socket.socketCallbacks.length; i++) { + _qz.socket.socketCallbacks[i](socketEvent); + } + } else { + _qz.socket.socketCallbacks(socketEvent); + } + } + }, + + + usb: { + /** List of functions called when receiving data from usb connection. */ + usbCallbacks: [], + /** Calls all functions registered to listen for usb events. */ + callUsb: function(streamEvent) { + if (Array.isArray(_qz.usb.usbCallbacks)) { + for(var i = 0; i < _qz.usb.usbCallbacks.length; i++) { + _qz.usb.usbCallbacks[i](streamEvent); + } + } else { + _qz.usb.usbCallbacks(streamEvent); + } + } + }, + + + hid: { + /** List of functions called when receiving data from hid connection. */ + hidCallbacks: [], + /** Calls all functions registered to listen for hid events. */ + callHid: function(streamEvent) { + if (Array.isArray(_qz.hid.hidCallbacks)) { + for(var i = 0; i < _qz.hid.hidCallbacks.length; i++) { + _qz.hid.hidCallbacks[i](streamEvent); + } + } else { + _qz.hid.hidCallbacks(streamEvent); + } + } + }, + + + printers: { + /** List of functions called when receiving data from printer connection. */ + printerCallbacks: [], + /** Calls all functions registered to listen for printer events. */ + callPrinter: function(streamEvent) { + if (Array.isArray(_qz.printers.printerCallbacks)) { + for(var i = 0; i < _qz.printers.printerCallbacks.length; i++) { + _qz.printers.printerCallbacks[i](streamEvent); + } + } else { + _qz.printers.printerCallbacks(streamEvent); + } + } + }, + + + file: { + /** List of functions called when receiving info regarding file changes. */ + fileCallbacks: [], + /** Calls all functions registered to listen for file events. */ + callFile: function(streamEvent) { + if (Array.isArray(_qz.file.fileCallbacks)) { + for(var i = 0; i < _qz.file.fileCallbacks.length; i++) { + _qz.file.fileCallbacks[i](streamEvent); + } + } else { + _qz.file.fileCallbacks(streamEvent); + } + } + }, + + + security: { + /** Function used to resolve promise when acquiring site's public certificate. */ + certHandler: function(resolve, reject) { reject(); }, + /** Called to create new promise (using {@link _qz.security.certHandler}) for certificate retrieval. */ + callCert: function() { + if (typeof _qz.security.certHandler.then === 'function') { + //already a promise + return _qz.security.certHandler; + } else if (_qz.security.certHandler.constructor.name === "AsyncFunction") { + //already callable as a promise + return _qz.security.certHandler(); + } else { + //turn into a promise + return _qz.tools.promise(_qz.security.certHandler); + } + }, + + /** Function used to create promise resolver when requiring signed calls. */ + signatureFactory: function() { return function(resolve) { resolve(); } }, + /** Called to create new promise (using {@link _qz.security.signatureFactory}) for signed calls. */ + callSign: function(toSign) { + if (_qz.security.signatureFactory.constructor.name === "AsyncFunction") { + //use directly + return _qz.security.signatureFactory(toSign); + } else { + //use in a promise + return _qz.tools.promise(_qz.security.signatureFactory(toSign)); + } + }, + + /** Signing algorithm used on signatures */ + signAlgorithm: "SHA1", + + rejectOnCertFailure: false, + + needsSigned: function(callName) { + const undialoged = [ + "printers.getStatus", + "printers.stopListening", + "usb.isClaimed", + "usb.closeStream", + "usb.releaseDevice", + "hid.stopListening", + "hid.isClaimed", + "hid.closeStream", + "hid.releaseDevice", + "file.stopListening", + "getVersion" + ]; + + return callName != null && undialoged.indexOf(callName) === -1; + } + }, + + + tools: { + /** Create a new promise */ + promise: function(resolver) { + //prefer global object for historical purposes + if (typeof RSVP !== 'undefined') { + return new RSVP.Promise(resolver); + } else if (typeof Promise !== 'undefined') { + return new Promise(resolver); + } else { + _qz.log.error("Promise/A+ support is required. See qz.api.setPromiseType(...)"); + } + }, + + /** Stub for rejecting with an Error from withing a Promise */ + reject: function(error) { + return _qz.tools.promise(function(resolve, reject) { + reject(error); + }); + }, + + stringify: function(object) { + //old versions of prototype affect stringify + var pjson = Array.prototype.toJSON; + delete Array.prototype.toJSON; + + function skipKeys(key, value) { + if (key === "promise") { + return undefined; + } + + return value; + } + + var result = JSON.stringify(object, skipKeys); + + if (pjson) { + Array.prototype.toJSON = pjson; + } + + return result; + }, + + hash: function(data) { + //prefer global object for historical purposes + if (typeof Sha256 !== 'undefined') { + return Sha256.hash(data); + } else { + return _qz.SHA.hash(data); + } + }, + + ws: typeof WebSocket !== 'undefined' ? WebSocket : null, + + absolute: function(loc) { + if (typeof window !== 'undefined' && typeof document.createElement === 'function') { + var a = document.createElement("a"); + a.href = loc; + return a.href; + } else if (typeof exports === 'object') { + //node.js + require('path').resolve(loc); + } + return loc; + }, + + relative: function(data) { + for(var i = 0; i < data.length; i++) { + if (data[i].constructor === Object) { + var absolute = false; + + if (data[i].data && data[i].data.search && data[i].data.search(/data:image\/\w+;base64,/) === 0) { + //upgrade from old base64 behavior + data[i].flavor = "base64"; + data[i].data = data[i].data.replace(/^data:image\/\w+;base64,/, ""); + } else if (data[i].flavor) { + //if flavor is known, we can directly check for absolute flavor types + if (["FILE", "XML"].indexOf(data[i].flavor.toUpperCase()) > -1) { + absolute = true; + } + } else if (data[i].format && ["HTML", "IMAGE", "PDF", "FILE", "XML"].indexOf(data[i].format.toUpperCase()) > -1) { + //if flavor is not known, all valid pixel formats default to file flavor + //previous v2.0 data also used format as what is now flavor, so we check for those values here too + absolute = true; + } else if (data[i].type && ((["PIXEL", "IMAGE", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && !data[i].format) + || (["HTML", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && (!data[i].format || data[i].format.toUpperCase() === "FILE")))) { + //if all we know is pixel type, then it is image's file flavor + //previous v2.0 data also used type as what is now format, so we check for those value here too + absolute = true; + } + + if (absolute) { + //change relative links to absolute + data[i].data = _qz.tools.absolute(data[i].data); + } + if (data[i].options && typeof data[i].options.overlay === 'string') { + data[i].options.overlay = _qz.tools.absolute(data[i].options.overlay); + } + } + } + }, + + /** Performs deep copy to target from remaining params */ + extend: function(target) { + //special case when reassigning properties as objects in a deep copy + if (typeof target !== 'object') { + target = {}; + } + + for(var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + if (!source) { continue; } + + for(var key in source) { + if (source.hasOwnProperty(key)) { + if (target === source[key]) { continue; } + + if (source[key] && source[key].constructor && source[key].constructor === Object) { + var clone; + if (Array.isArray(source[key])) { + clone = target[key] || []; + } else { + clone = target[key] || {}; + } + + target[key] = _qz.tools.extend(clone, source[key]); + } else if (source[key] !== undefined) { + target[key] = source[key]; + } + } + } + } + + return target; + }, + + versionCompare: function(major, minor, patch, build) { + if (_qz.tools.assertActive()) { + var semver = _qz.websocket.connection.semver; + if (semver[0] != major) { + return semver[0] - major; + } + if (minor != undefined && semver[1] != minor) { + return semver[1] - minor; + } + if (patch != undefined && semver[2] != patch) { + return semver[2] - patch; + } + if (build != undefined && semver.length > 3 && semver[3] != build) { + return Number.isInteger(semver[3]) && Number.isInteger(build) ? semver[3] - build : semver[3].toString().localeCompare(build.toString()); + } + return 0; + } + }, + + isVersion: function(major, minor, patch, build) { + return _qz.tools.versionCompare(major, minor, patch, build) == 0; + }, + + isActive: function() { + return !_qz.websocket.shutdown && _qz.websocket.connection != null + && (_qz.websocket.connection.readyState === _qz.tools.ws.OPEN + || _qz.websocket.connection.readyState === _qz.tools.ws.CONNECTING); + }, + + assertActive: function() { + if (_qz.tools.isActive()) { + return true; + } + // Promise won't reject on throw; yet better than 'undefined' + throw new Error("A connection to QZ has not been established yet"); + }, + + uint8ArrayToHex: function(uint8) { + return Array.from(uint8) + .map(function(i) { return i.toString(16).padStart(2, '0'); }) + .join(''); + }, + + uint8ArrayToBase64: function(uint8) { + /** + * Adapted from Egor Nepomnyaschih's code under MIT Licence (C) 2020 + * see https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 + */ + var map = [ + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", + "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/" + ]; + + var result = '', i, l = uint8.length; + for (i = 2; i < l; i += 3) { + result += map[uint8[i - 2] >> 2]; + result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; + result += map[((uint8[i - 1] & 0x0F) << 2) | (uint8[i] >> 6)]; + result += map[uint8[i] & 0x3F]; + } + if (i === l + 1) { // 1 octet yet to write + result += map[uint8[i - 2] >> 2]; + result += map[(uint8[i - 2] & 0x03) << 4]; + result += "=="; + } + if (i === l) { // 2 octets yet to write + result += map[uint8[i - 2] >> 2]; + result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; + result += map[(uint8[i - 1] & 0x0F) << 2]; + result += "="; + } + return result; + }, + }, + + compatible: { + /** Converts message format to a previous version's */ + data: function(printData) { + // special handling for Uint8Array + for(var i = 0; i < printData.length; i++) { + if (printData[i].constructor === Object && printData[i].data instanceof Uint8Array) { + if (printData[i].flavor) { + var flavor = printData[i].flavor.toString().toUpperCase(); + switch(flavor) { + case 'BASE64': + printData[i].data = _qz.tools.uint8ArrayToBase64(printData[i].data); + break; + case 'HEX': + printData[i].data = _qz.tools.uint8ArrayToHex(printData[i].data); + break; + default: + throw new Error("Uint8Array conversion to '" + flavor + "' is not supported."); + } + } + } + } + + if(_qz.tools.versionCompare(2, 2, 4) < 0) { + for(var i = 0; i < printData.length; i++) { + if (printData[i].constructor === Object) { + // dotDensity: "double-legacy|single-legacy" since 2.2.4. Fallback to "double|single" + if (printData[i].options && typeof printData[i].options.dotDensity === 'string') { + printData[i].options.dotDensity = printData[i].options.dotDensity.toLowerCase().replace("-legacy", ""); + } + } + } + } + + if (_qz.tools.isVersion(2, 0)) { + /* + 2.0.x conversion + ----- + type=pixel -> use format as 2.0 type (unless 'command' format, which forces 2.0 'raw' type) + type=raw -> 2.0 type has to be 'raw' + if format is 'image' -> force 2.0 'image' format, ignore everything else (unsupported in 2.0) + + flavor translates straight to 2.0 format (unless forced to 'raw'/'image') + */ + _qz.log.trace("Converting print data to v2.0 for " + _qz.websocket.connection.version); + for(var i = 0; i < printData.length; i++) { + if (printData[i].constructor === Object) { + if (printData[i].type && printData[i].type.toUpperCase() === "RAW" && printData[i].format && printData[i].format.toUpperCase() === "IMAGE") { + if (printData[i].flavor && printData[i].flavor.toUpperCase() === "BASE64") { + //special case for raw base64 images + printData[i].data = "data:image/compat;base64," + printData[i].data; + } + printData[i].flavor = "IMAGE"; //forces 'image' format when shifting for conversion + } + if ((printData[i].type && printData[i].type.toUpperCase() === "RAW") || (printData[i].format && printData[i].format.toUpperCase() === "COMMAND")) { + printData[i].format = "RAW"; //forces 'raw' type when shifting for conversion + } + + printData[i].type = printData[i].format; + printData[i].format = printData[i].flavor; + delete printData[i].flavor; + } + } + } + }, + + /* Converts config defaults to match previous version */ + config: function(config, dirty) { + if (_qz.tools.isVersion(2, 0)) { + if (!dirty.rasterize) { + config.rasterize = true; + } + } + if(_qz.tools.versionCompare(2, 2) < 0) { + if(config.forceRaw !== 'undefined') { + config.altPrinting = config.forceRaw; + delete config.forceRaw; + } + } + if(_qz.tools.versionCompare(2, 1, 2, 11) < 0) { + if(config.spool) { + if(config.spool.size) { + config.perSpool = config.spool.size; + delete config.spool.size; + } + if(config.spool.end) { + config.endOfDoc = config.spool.end; + delete config.spool.end; + } + delete config.spool; + } + } + return config; + }, + + /** Compat wrapper with previous version **/ + networking: function(hostname, port, signature, signingTimestamp, mappingCallback) { + // Use 2.0 + if (_qz.tools.isVersion(2, 0)) { + return _qz.tools.promise(function(resolve, reject) { + _qz.websocket.dataPromise('websocket.getNetworkInfo', { + hostname: hostname, + port: port + }, signature, signingTimestamp).then(function(data) { + if (typeof mappingCallback !== 'undefined') { + resolve(mappingCallback(data)); + } else { + resolve(data); + } + }, reject); + }); + } + // Wrap 2.1 + return _qz.tools.promise(function(resolve, reject) { + _qz.websocket.dataPromise('networking.device', { + hostname: hostname, + port: port + }, signature, signingTimestamp).then(function(data) { + resolve({ ipAddress: data.ip, macAddress: data.mac }); + }, reject); + }); + }, + + /** Check if QZ version supports chosen algorithm */ + algorithm: function(quiet) { + //if not connected yet we will assume compatibility exists for the time being + if (_qz.tools.isActive()) { + if (_qz.tools.isVersion(2, 0)) { + if (!quiet) { + _qz.log.warn("Connected to an older version of QZ, alternate signature algorithms are not supported"); + } + return false; + } + } + + return true; + } + }, + + /** + * Adapted from Chris Veness's code under MIT Licence (C) 2002 + * see http://www.movable-type.co.uk/scripts/sha256.html + */ + SHA: { + //@formatter:off - keep this block compact + hash: function(msg) { + // add trailing '1' bit (+ 0's padding) to string [§5.1.1] + msg = _qz.SHA._utf8Encode(msg) + String.fromCharCode(0x80); + + // constants [§4.2.2] + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]; + // initial hash value [§5.3.1] + var H = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ]; + + // convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1] + var l = msg.length / 4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length + var N = Math.ceil(l / 16); // number of 16-integer-blocks required to hold 'l' ints + var M = new Array(N); + + for(var i = 0; i < N; i++) { + M[i] = new Array(16); + for(var j = 0; j < 16; j++) { // encode 4 chars per integer, big-endian encoding + M[i][j] = (msg.charCodeAt(i * 64 + j * 4) << 24) | (msg.charCodeAt(i * 64 + j * 4 + 1) << 16) | + (msg.charCodeAt(i * 64 + j * 4 + 2) << 8) | (msg.charCodeAt(i * 64 + j * 4 + 3)); + } // note running off the end of msg is ok 'cos bitwise ops on NaN return 0 + } + // add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1] + // note: most significant word would be (len-1)*8 >>> 32, but since JS converts + // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators + M[N-1][14] = ((msg.length - 1) * 8) / Math.pow(2, 32); + M[N-1][14] = Math.floor(M[N-1][14]); + M[N-1][15] = ((msg.length - 1) * 8) & 0xffffffff; + + // HASH COMPUTATION [§6.1.2] + var W = new Array(64); var a, b, c, d, e, f, g, h; + for(var i = 0; i < N; i++) { + // 1 - prepare message schedule 'W' + for(var t = 0; t < 16; t++) { W[t] = M[i][t]; } + for(var t = 16; t < 64; t++) { W[t] = (_qz.SHA._dev1(W[t-2]) + W[t-7] + _qz.SHA._dev0(W[t-15]) + W[t-16]) & 0xffffffff; } + // 2 - initialise working variables a, b, c, d, e, f, g, h with previous hash value + a = H[0]; b = H[1]; c = H[2]; d = H[3]; e = H[4]; f = H[5]; g = H[6]; h = H[7]; + // 3 - main loop (note 'addition modulo 2^32') + for(var t = 0; t < 64; t++) { + var T1 = h + _qz.SHA._sig1(e) + _qz.SHA._ch(e, f, g) + K[t] + W[t]; + var T2 = _qz.SHA._sig0(a) + _qz.SHA._maj(a, b, c); + h = g; g = f; f = e; e = (d + T1) & 0xffffffff; + d = c; c = b; b = a; a = (T1 + T2) & 0xffffffff; + } + // 4 - compute the new intermediate hash value (note 'addition modulo 2^32') + H[0] = (H[0]+a) & 0xffffffff; H[1] = (H[1]+b) & 0xffffffff; H[2] = (H[2]+c) & 0xffffffff; H[3] = (H[3]+d) & 0xffffffff; + H[4] = (H[4]+e) & 0xffffffff; H[5] = (H[5]+f) & 0xffffffff; H[6] = (H[6]+g) & 0xffffffff; H[7] = (H[7]+h) & 0xffffffff; + } + + return _qz.SHA._hexStr(H[0]) + _qz.SHA._hexStr(H[1]) + _qz.SHA._hexStr(H[2]) + _qz.SHA._hexStr(H[3]) + + _qz.SHA._hexStr(H[4]) + _qz.SHA._hexStr(H[5]) + _qz.SHA._hexStr(H[6]) + _qz.SHA._hexStr(H[7]); + }, + + // Rotates right (circular right shift) value x by n positions + _rotr: function(n, x) { return (x >>> n) | (x << (32 - n)); }, + // logical functions + _sig0: function(x) { return _qz.SHA._rotr(2, x) ^ _qz.SHA._rotr(13, x) ^ _qz.SHA._rotr(22, x); }, + _sig1: function(x) { return _qz.SHA._rotr(6, x) ^ _qz.SHA._rotr(11, x) ^ _qz.SHA._rotr(25, x); }, + _dev0: function(x) { return _qz.SHA._rotr(7, x) ^ _qz.SHA._rotr(18, x) ^ (x >>> 3); }, + _dev1: function(x) { return _qz.SHA._rotr(17, x) ^ _qz.SHA._rotr(19, x) ^ (x >>> 10); }, + _ch: function(x, y, z) { return (x & y) ^ (~x & z); }, + _maj: function(x, y, z) { return (x & y) ^ (x & z) ^ (y & z); }, + // note can't use toString(16) as it is implementation-dependant, and in IE returns signed numbers when used on full words + _hexStr: function(n) { var s = "", v; for(var i = 7; i >= 0; i--) { v = (n >>> (i * 4)) & 0xf; s += v.toString(16); } return s; }, + // implementation of deprecated unescape() based on https://cwestblog.com/2011/05/23/escape-unescape-deprecated/ (and comments) + _unescape: function(str) { + return str.replace(/%(u[\da-f]{4}|[\da-f]{2})/gi, function(seq) { + if (seq.length - 1) { + return String.fromCharCode(parseInt(seq.substring(seq.length - 3 ? 2 : 1), 16)) + } else { + var code = seq.charCodeAt(0); + return code < 256 ? "%" + (0 + code.toString(16)).slice(-2).toUpperCase() : "%u" + ("000" + code.toString(16)).slice(-4).toUpperCase() + } + }); + }, + _utf8Encode: function(str) { + return _qz.SHA._unescape(encodeURIComponent(str)); + } + //@formatter:on + }, + }; + + +///// CONFIG CLASS //// + + /** Object to handle configured printer options. */ + function Config(printer, opts) { + + this.config = _qz.tools.extend({}, _qz.printing.defaultConfig); //create a copy of the default options + this._dirtyOpts = {}; //track which config options have changed from the defaults + + /** + * Set the printer assigned to this config. + * @param {string|Object} newPrinter Name of printer. Use object type to specify printing to file or host. + * @param {string} [newPrinter.name] Name of printer to send printing. + * @param {string} [newPrinter.file] DEPRECATED: Name of file to send printing. + * @param {string} [newPrinter.host] IP address or host name to send printing. + * @param {string} [newPrinter.port] Port used by <printer.host>. + */ + this.setPrinter = function(newPrinter) { + if (typeof newPrinter === 'string') { + newPrinter = { name: newPrinter }; + } + this.printer = newPrinter; + }; + + /** + * @returns {Object} The printer currently assigned to this config. + */ + this.getPrinter = function() { + return this.printer; + }; + + /** + * Alter any of the printer options currently applied to this config. + * @param newOpts {Object} The options to change. See qz.configs.setDefaults docs for available values. + * + * @see qz.configs.setDefaults + */ + this.reconfigure = function(newOpts) { + for(var key in newOpts) { + if (newOpts[key] !== undefined) { + this._dirtyOpts[key] = true; + } + } + + _qz.tools.extend(this.config, newOpts); + }; + + /** + * @returns {Object} The currently applied options on this config. + */ + this.getOptions = function() { + return _qz.compatible.config(this.config, this._dirtyOpts); + }; + + // init calls for new config object + this.setPrinter(printer); + this.reconfigure(opts); + } + + /** + * Shortcut method for calling qz.print with a particular config. + * @param {Array} data Array of data being sent to the printer. See qz.print docs for available values. + * @param {boolean} [signature] Pre-signed signature of JSON string containing call, params, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @example + * qz.print(myConfig, ...); // OR + * myConfig.print(...); + * + * @see qz.print + */ + Config.prototype.print = function(data, signature, signingTimestamp) { + qz.print(this, data, signature, signingTimestamp); + }; + + +///// PUBLIC METHODS ///// + + /** @namespace qz */ + var qz = { + + /** + * Calls related specifically to the web socket connection. + * @namespace qz.websocket + */ + websocket: { + /** + * Check connection status. Active connection is necessary for other calls to run. + * + * @returns {boolean} If there is an active connection with QZ Tray. + * + * @see connect + * + * @memberof qz.websocket + */ + isActive: function() { + return _qz.tools.isActive(); + }, + + /** + * Call to setup connection with QZ Tray on user's system. + * + * @param {Object} [options] Configuration options for the web socket connection. + * @param {string|Array} [options.host=['localhost', 'localhost.qz.io']] Host running the QZ Tray software. + * @param {Object} [options.port] Config options for ports to cycle. + * @param {Array} [options.port.secure=[8181, 8282, 8383, 8484]] Array of secure (WSS) ports to try + * @param {Array} [options.port.insecure=[8182, 8283, 8384, 8485]] Array of insecure (WS) ports to try + * @param {boolean} [options.usingSecure=true] If the web socket should try to use secure ports for connecting. + * @param {number} [options.keepAlive=60] Seconds between keep-alive pings to keep connection open. Set to 0 to disable. + * @param {number} [options.retries=0] Number of times to reconnect before failing. + * @param {number} [options.delay=0] Seconds before firing a connection. Ignored if options.retries is 0. + * + * @returns {Promise} + * + * @memberof qz.websocket + */ + connect: function(options) { + return _qz.tools.promise(function(resolve, reject) { + if (_qz.websocket.connection) { + const state = _qz.websocket.connection.readyState; + + if (state === _qz.tools.ws.OPEN) { + reject(new Error("An open connection with QZ Tray already exists")); + return; + } else if (state === _qz.tools.ws.CONNECTING) { + reject(new Error("The current connection attempt has not returned yet")); + return; + } else if (state === _qz.tools.ws.CLOSING) { + reject(new Error("Waiting for previous disconnect request to complete")); + return; + } + } + + if (!_qz.tools.ws) { + reject(new Error("WebSocket not supported by this browser")); + return; + } else if (!_qz.tools.ws.CLOSED || _qz.tools.ws.CLOSED == 2) { + reject(new Error("Unsupported WebSocket version detected: HyBi-00/Hixie-76")); + return; + } + + //ensure some form of options exists for value checks + if (options == undefined) { options = {}; } + + //disable secure ports if page is not secure + if (typeof location === 'undefined' || location.protocol !== 'https:') { + //respect forcing secure ports if it is defined, otherwise disable + if (typeof options.usingSecure === 'undefined') { + _qz.log.trace("Disabling secure ports due to insecure page"); + options.usingSecure = false; + } + } + + //ensure any hosts are passed to internals as an array + if (typeof options.host !== 'undefined' && !Array.isArray(options.host)) { + options.host = [options.host]; + } + + _qz.websocket.shutdown = false; //reset state for new connection attempt + var attempt = function(count) { + var tried = false; + var nextAttempt = function() { + if (!tried) { + tried = true; + + if (options && count < options.retries) { + attempt(count + 1); + } else { + _qz.websocket.connection = null; + reject.apply(null, arguments); + } + } + }; + + var delayed = function() { + var config = _qz.tools.extend({}, _qz.websocket.connectConfig, options); + _qz.websocket.setup.findConnection(config, resolve, nextAttempt) + }; + if (count == 0) { + delayed(); // only retries will be called with a delay + } else { + setTimeout(delayed, options.delay * 1000); + } + }; + + attempt(0); + }); + }, + + /** + * Stop any active connection with QZ Tray. + * + * @returns {Promise} + * + * @memberof qz.websocket + */ + disconnect: function() { + return _qz.tools.promise(function(resolve, reject) { + if (_qz.websocket.connection != null) { + if (_qz.tools.isActive()) { + // handles closing both 'connecting' and 'connected' states + _qz.websocket.shutdown = true; + _qz.websocket.connection.promise = { resolve: resolve, reject: reject }; + _qz.websocket.connection.close(); + } else { + reject(new Error("Current connection is still closing")); + } + } else { + reject(new Error("No open connection with QZ Tray")); + } + }); + }, + + /** + * List of functions called for any connections errors outside of an API call.

+ * Also called if {@link websocket#connect} fails to connect. + * + * @param {Function|Array} calls Single or array of Function({Event} event) calls. + * + * @memberof qz.websocket + */ + setErrorCallbacks: function(calls) { + _qz.websocket.errorCallbacks = calls; + }, + + /** + * List of functions called for any connection closing event outside of an API call.

+ * Also called when {@link websocket#disconnect} is called. + * + * @param {Function|Array} calls Single or array of Function({Event} event) calls. + * + * @memberof qz.websocket + */ + setClosedCallbacks: function(calls) { + _qz.websocket.closedCallbacks = calls; + }, + + /** + * @deprecated Since 2.1.0. Please use qz.networking.device() instead + * + * @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] Port to use with custom hostname, defaults to 443 + * @param {string} [signature] Pre-signed signature of hashed JSON string containing call='websocket.getNetworkInfo', params object, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @returns {Promise|Error>} Connected system's network information. + * + * @memberof qz.websocket + */ + getNetworkInfo: _qz.compatible.networking, + + /** + * @returns {Object<{socket: String, host: String, port: Number}>} Details of active websocket connection + * + * @memberof qz.websocket + */ + getConnectionInfo: function() { + if (_qz.tools.assertActive()) { + var url = _qz.websocket.connection.url.split(/[:\/]+/g); + return { socket: url[0], host: url[1], port: +url[2] }; + } + } + }, + + + /** + * Calls related to getting printer information from the connection. + * @namespace qz.printers + */ + printers: { + /** + * @param {string} [signature] Pre-signed signature of hashed JSON string containing call='printers.getDefault, params, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @returns {Promise} Name of the connected system's default printer. + * + * @memberof qz.printers + */ + getDefault: function(signature, signingTimestamp) { + return _qz.websocket.dataPromise('printers.getDefault', null, signature, signingTimestamp); + }, + + /** + * @param {string} [query] Search for a specific printer. All printers are returned if not provided. + * @param {string} [signature] Pre-signed signature of hashed JSON string containing call='printers.find', params, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @returns {Promise|string|Error>} The matched printer name if query is provided. + * Otherwise an array of printer names found on the connected system. + * + * @memberof qz.printers + */ + find: function(query, signature, signingTimestamp) { + return _qz.websocket.dataPromise('printers.find', { query: query }, signature, signingTimestamp); + }, + + /** + * Provides a list, with additional information, for each printer available to QZ. + * + * @returns {Promise|Object|Error>} + * + * @memberof qz.printers + */ + details: function() { + return _qz.websocket.dataPromise('printers.detail'); + }, + + /** + * Start listening for printer status events, such as paper_jam events. + * Reported under the ACTION type in the streamEvent on callbacks. + * + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.printers.setPrinterCallbacks + * + * @param {null|string|Array} printers Printer or list of printers to listen to, null listens to all. + * @param {Object|null} [options] Printer listener options + * @param {null|boolean} [options.jobData=false] Flag indicating if raw spool file content should be return as well as status information (Windows only) + * @param {null|number} [options.maxJobData=-1] Maximum number of bytes to returns for raw spooled file content (Windows only) + * @param {null|string} [options.flavor="plain"] Flavor of data format returned. Valid flavors are [base64 | hex | plain*] (Windows only) + * + * @memberof qz.printers + */ + startListening: function(printers, options) { + if (!Array.isArray(printers)) { + printers = [printers]; + } + var params = { + printerNames: printers + }; + if (options && options.jobData == true) params.jobData = true; + if (options && options.maxJobData) params.maxJobData = options.maxJobData; + if (options && options.flavor) params.flavor = options.flavor; + return _qz.websocket.dataPromise('printers.startListening', params); + }, + + /** + * Clear the queue of a specified printer or printers. Does not delete retained jobs. + * + * @param {string|Object} [options] Name of printer to clear + * @param {string} [options.printerName] Name of printer to clear + * @param {number} [options.jobId] Cancel a job of a specific JobId instead of canceling all. Must include a printerName. + * + * @returns {Promise} + * @since 2.2.4 + * + * @memberof qz.printers + */ + clearQueue: function(options) { + if (typeof options !== 'object') { + options = { + printerName: options + }; + } + return _qz.websocket.dataPromise('printers.clearQueue', options); + }, + + /** + * Stop listening for printer status actions. + * + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.printers.setPrinterCallbacks + * + * @memberof qz.printers + */ + stopListening: function() { + return _qz.websocket.dataPromise('printers.stopListening'); + }, + + /** + * Retrieve current printer status from any active listeners. + * + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.printers.startListening + * + * @memberof qz.printers + */ + getStatus: function() { + return _qz.websocket.dataPromise('printers.getStatus'); + }, + + /** + * List of functions called for any printer status change. + * Event data will contain {string} printerName and {string} status for all types. + * For RECEIVE types, {Array} output (in hexadecimal format). + * For ERROR types, {string} exception. + * For ACTION types, {string} actionType. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * @since 2.1.0 + * + * @memberof qz.printers + */ + setPrinterCallbacks: function(calls) { + _qz.printers.printerCallbacks = calls; + } + }, + + /** + * Calls related to setting up new printer configurations. + * @namespace qz.configs + */ + configs: { + /** + * Default options used by new configs if not overridden. + * Setting a value to NULL will use the printer's default options. + * Updating these will not update the options on any created config. + * + * @param {Object} options Default options used by printer configs if not overridden. + * + * @param {Object} [options.bounds=null] Bounding box rectangle. + * @param {number} [options.bounds.x=0] Distance from left for bounding box starting corner + * @param {number} [options.bounds.y=0] Distance from top for bounding box starting corner + * @param {number} [options.bounds.width=0] Width of bounding box + * @param {number} [options.bounds.height=0] Height of bounding box + * @param {string} [options.colorType='color'] Valid values [color | grayscale | blackwhite | default] + * @param {number} [options.copies=1] Number of copies to be printed. + * @param {number|Array|Object|Array|string} [options.density=0] Pixel density (DPI, DPMM, or DPCM depending on [options.units]). + * If provided as an array, uses the first supported density found (or the first entry if none found). + * If provided as a string, valid values are [best | draft], corresponding to highest or lowest reported density respectively. + * @param {number} [options.density.cross=0] Asymmetric pixel density for the cross feed direction. + * @param {number} [options.density.feed=0] Asymmetric pixel density for the feed direction. + * @param {boolean|string} [options.duplex=false] Double sided printing, Can specify duplex style by passing a string value: [one-sided | duplex | long-edge | tumble | short-edge] + * @param {number} [options.fallbackDensity=null] Value used when default density value cannot be read, or in cases where reported as "Normal" by the driver, (in DPI, DPMM, or DPCM depending on [options.units]). + * @param {string} [options.interpolation='bicubic'] Valid values [bicubic | bilinear | nearest-neighbor]. Controls how images are handled when resized. + * @param {string} [options.jobName=null] Name to display in print queue. + * @param {boolean} [options.legacy=false] If legacy style printing should be used. + * @param {Object|number} [options.margins=0] If just a number is provided, it is used as the margin for all sides. + * @param {number} [options.margins.top=0] + * @param {number} [options.margins.right=0] + * @param {number} [options.margins.bottom=0] + * @param {number} [options.margins.left=0] + * @param {string} [options.orientation=null] Valid values [portrait | landscape | reverse-landscape | null]. + * If set to null, orientation will be determined automatically. + * @param {number} [options.paperThickness=null] + * @param {string|number} [options.printerTray=null] Printer tray to pull from. The number N assumes string equivalent of 'Tray N'. Uses printer default if NULL. + * @param {boolean} [options.rasterize=false] Whether documents should be rasterized before printing. + * Specifying [options.density] for PDF print formats will set this to true. + * @param {number} [options.rotation=0] Image rotation in degrees. + * @param {boolean} [options.scaleContent=true] Scales print content to page size, keeping ratio. + * @param {Object} [options.size=null] Paper size. + * @param {number} [options.size.width=null] Page width. + * @param {number} [options.size.height=null] Page height. + * @param {string} [options.units='in'] Page units, applies to paper size, margins, and density. Valid value [in | cm | mm] + * + * @param {boolean} [options.forceRaw=false] Print the specified raw data using direct method, skipping the driver. Not yet supported on Windows. + * @param {string|Object} [options.encoding=null] Character set for commands. Can be provided as an object for converting encoding types for RAW types. + * @param {string} [options.encoding.from] If this encoding type is provided, RAW type commands will be parsed from this for the purpose of being converted to the encoding.to value. + * @param {string} [options.encoding.to] Encoding RAW type commands will be converted into. If encoding.from is not provided, this will be treated as if a string was passed for encoding. + * @param {string} [options.endOfDoc=null] DEPRECATED Raw only: Character(s) denoting end of a page to control spooling. + * @param {number} [options.perSpool=1] DEPRECATED: Raw only: Number of pages per spool. + * @param {boolean} [options.retainTemp=false] Retain any temporary files used. Ignored unless forceRaw true. + * @param {Object} [options.spool=null] Advanced spooling options. + * @param {number} [options.spool.size=null] Number of pages per spool. Default is no limit. If spool.end is provided, defaults to 1 + * @param {string} [options.spool.end=null] Raw only: Character(s) denoting end of a page to control spooling. + * + * @memberof qz.configs + */ + setDefaults: function(options) { + _qz.tools.extend(_qz.printing.defaultConfig, options); + }, + + /** + * Creates new printer config to be used in printing. + * + * @param {string|object} printer Name of printer. Use object type to specify printing to file or host. + * @param {string} [printer.name] Name of printer to send printing. + * @param {string} [printer.file] Name of file to send printing. + * @param {string} [printer.host] IP address or host name to send printing. + * @param {string} [printer.port] Port used by <printer.host>. + * @param {Object} [options] Override any of the default options for this config only. + * + * @returns {Config} The new config. + * + * @see configs.setDefaults + * + * @memberof qz.configs + */ + create: function(printer, options) { + return new Config(printer, options); + } + }, + + + /** + * Send data to selected config for printing. + * The promise for this method will resolve when the document has been sent to the printer. Actual printing may not be complete. + *

+ * Optionally, print requests can be pre-signed: + * Signed content consists of a JSON object string containing no spacing, + * following the format of the "call" and "params" keys in the API call, with the addition of a "timestamp" key in milliseconds + * ex. '{"call":"","params":{...},"timestamp":1450000000}' + * + * @param {Object|Array>} configs Previously created config object or objects. + * @param {Array|Array>} data Array of data being sent to the printer.
+ * String values are interpreted as {type: 'raw', format: 'command', flavor: 'plain', data: <string>}. + * @param {string} data.data + * @param {string} data.type Printing type. Valid types are [pixel | raw*]. *Default + * @param {string} data.format Format of data type used. *Default per type

+ * For [pixel] types, valid formats are [html | image* | pdf].

+ * For [raw] types, valid formats are [command* | html | image | pdf]. + * @param {string} data.flavor Flavor of data format used. *Default per format

+ * For [command] formats, valid flavors are [base64 | file | hex | plain* | xml].

+ * For [html] formats, valid flavors are [file* | plain].

+ * For [image] formats, valid flavors are [base64 | file*].

+ * For [pdf] formats, valid flavors are [base64 | file*]. + * @param {Object} [data.options] + * @param {string} [data.options.language] Required with [raw] type + [image] format. Printer language. + * @param {number} [data.options.x] Optional with [raw] type + [image] format. The X position of the image. + * @param {number} [data.options.y] Optional with [raw] type + [image] format. The Y position of the image. + * @param {string|number} [data.options.dotDensity] Optional with [raw] type + [image] format. + * @param {number} [data.precision=128] Optional with [raw] type [image] format. Bit precision of the ribbons. + * @param {boolean|string|Array>} [data.options.overlay=false] Optional with [raw] type [image] format. + * Boolean sets entire layer, string sets mask image, Array sets array of rectangles in format [x1,y1,x2,y2]. + * @param {string} [data.options.xmlTag] Required with [xml] flavor. Tag name containing base64 formatted data. + * @param {number} [data.options.pageWidth] Optional with [html | pdf] formats. Width of the rendering. + * Defaults to paper width. + * @param {number} [data.options.pageHeight] Optional with [html | pdf] formats. Height of the rendering. + * Defaults to paper height for [pdf], or auto sized for [html]. + * @param {string} [data.options.pageRanges] Optional with [pdf] formats. Comma-separated list of page ranges to include. + * @param {boolean} [data.options.ignoreTransparency=false] Optional with [pdf] formats. Instructs transparent PDF elements to be ignored. + * Transparent PDF elements are known to degrade performance and quality when printing. + * @param {boolean} [data.options.altFontRendering=false] Optional with [pdf] formats. Instructs PDF to be rendered using PDFBOX 1.8 techniques. + * Drastically improves low-DPI PDF print quality on Windows. + * @param {...*} [arguments] Additionally three more parameters can be specified:

+ * {boolean} [resumeOnError=false] Whether the chain should continue printing if it hits an error on one the the prints.

+ * {string|Array} [signature] Pre-signed signature(s) of the JSON string for containing call, params, and timestamp.

+ * {number|Array} [signingTimestamps] Required to match with signature. Timestamps for each of the passed pre-signed content. + * + * @returns {Promise} + * + * @see qz.configs.create + * + * @memberof qz + */ + print: function(configs, data) { + var resumeOnError = false, + signatures = [], + signaturesTimestamps = []; + + //find optional parameters + if (arguments.length >= 3) { + if (typeof arguments[2] === 'boolean') { + resumeOnError = arguments[2]; + + if (arguments.length >= 5) { + signatures = arguments[3]; + signaturesTimestamps = arguments[4]; + } + } else if (arguments.length >= 4) { + signatures = arguments[2]; + signaturesTimestamps = arguments[3]; + } + + //ensure values are arrays for consistency + if (signatures && !Array.isArray(signatures)) { signatures = [signatures]; } + if (signaturesTimestamps && !Array.isArray(signaturesTimestamps)) { signaturesTimestamps = [signaturesTimestamps]; } + } + + if (!Array.isArray(configs)) { configs = [configs]; } //single config -> array of configs + if (!Array.isArray(data[0])) { data = [data]; } //single data array -> array of data arrays + + //clean up data formatting + for(var d = 0; d < data.length; d++) { + _qz.tools.relative(data[d]); + _qz.compatible.data(data[d]); + } + + var sendToPrint = function(mapping) { + var params = { + printer: mapping.config.getPrinter(), + options: mapping.config.getOptions(), + data: mapping.data + }; + + return _qz.websocket.dataPromise('print', params, mapping.signature, mapping.timestamp); + }; + + //chain instead of Promise.all, so resumeOnError can collect each error + var chain = []; + for(var i = 0; i < configs.length || i < data.length; i++) { + (function(i_) { + var map = { + config: configs[Math.min(i_, configs.length - 1)], + data: data[Math.min(i_, data.length - 1)], + signature: signatures[i_], + timestamp: signaturesTimestamps[i_] + }; + + chain.push(function() { return sendToPrint(map) }); + })(i); + } + + //setup to catch errors if needed + var fallThrough = null; + if (resumeOnError) { + var fallen = []; + fallThrough = function(err) { fallen.push(err); }; + + //final promise to reject any errors as a group + chain.push(function() { + return _qz.tools.promise(function(resolve, reject) { + fallen.length ? reject(fallen) : resolve(); + }); + }); + } + + var last = null; + chain.reduce(function(sequence, link) { + last = sequence.catch(fallThrough).then(link); //catch is ignored if fallThrough is null + return last; + }, _qz.tools.promise(function(r) { r(); })); //an immediately resolved promise to start off the chain + + //return last promise so users can chain off final action or catch when stopping on error + return last; + }, + + + /** + * Calls related to interaction with serial ports. + * @namespace qz.serial + */ + serial: { + /** + * @returns {Promise|Error>} Communication (RS232, COM, TTY) ports available on connected system. + * + * @memberof qz.serial + */ + findPorts: function() { + return _qz.websocket.dataPromise('serial.findPorts'); + }, + + /** + * List of functions called for any response from open serial ports. + * Event data will contain {string} portName for all types. + * For RECEIVE types, {string} output. + * For ERROR types, {string} exception. + * + * @param {Function|Array} calls Single or array of Function({object} streamEvent) calls. + * + * @memberof qz.serial + */ + setSerialCallbacks: function(calls) { + _qz.serial.serialCallbacks = calls; + }, + + /** + * Opens a serial port for sending and receiving data + * + * @param {string} port Name of serial port to open. + * @param {Object} [options] Serial port configurations. + * @param {number} [options.baudRate=9600] Serial port speed. Set to 0 for auto negotiation. + * @param {number} [options.dataBits=8] Serial port data bits. Set to 0 for auto negotiation. + * @param {number} [options.stopBits=1] Serial port stop bits. Set to 0 for auto negotiation. + * @param {string} [options.parity='NONE'] Serial port parity. Set to AUTO for auto negotiation. Valid values [NONE | EVEN | ODD | MARK | SPACE | AUTO] + * @param {string} [options.flowControl='NONE'] Serial port flow control. Set to AUTO for auto negotiation. Valid values [NONE | XONXOFF | XONXOFF_OUT | XONXOFF_IN | RTSCTS | RTSCTS_OUT | RTSCTS_IN | AUTO] + * @param {string} [options.encoding='UTF-8'] Character set for communications. + * @param {string} [options.start=0x0002] DEPRECATED: Legacy character denoting start of serial response. Use options.rx.start instead. + * @param {string} [options.end=0x000D] DEPRECATED: Legacy character denoting end of serial response. Use options.rx.end instead. + * @param {number} [options.width] DEPRECATED: Legacy use for fixed-width response serial communication. Use options.rx.width instead. + * @param {Object} [options.rx] Serial communications response definitions. If an object is passed but no options are defined, all response data will be sent back as it is received unprocessed. + * @param {string|Array} [options.rx.start] Character(s) denoting start of response bytes. Used in conjunction with `end`, `width`, or `lengthbit` property. + * @param {string} [options.rx.end] Character denoting end of response bytes. Used in conjunction with `start` property. + * @param {number} [options.rx.width] Fixed width size of response bytes (not including header if `start` is set). Used alone or in conjunction with `start` property. + * @param {boolean} [options.rx.untilNewline] Returns data between newline characters (`\n` or `\r`) Truncates empty responses. Overrides `start`, `end`, `width`. + * @param {number|Object} [options.rx.lengthBytes] If a number is passed it is treated as the length index. Other values are left as their defaults. + * @param {number} [options.rx.lengthBytes.index=0] Position of the response byte (not including response `start` bytes) used to denote the length of the remaining response data. + * @param {number} [options.rx.lengthBytes.length=1] Length of response length bytes after response header. + * @param {string} [options.rx.lengthBytes.endian='BIG'] Byte endian for multi-byte length values. Valid values [BIG | LITTLE] + * @param {number|Object} [options.rx.crcBytes] If a number is passed it is treated as the crc length. Other values are left as their defaults. + * @param {number} [options.rx.crcBytes.index=0] Position after the response data (not including length or data bytes) used to denote the crc. + * @param {number} [options.rx.crcBytes.length=1] Length of response crc bytes after the response data length. + * @param {boolean} [options.rx.includeHeader=false] Whether any of the header bytes (`start` bytes and any length bytes) should be included in the processed response. + * @param {string} [options.rx.encoding] Override the encoding used for response data. Uses the same value as options.encoding otherwise. + * + * @returns {Promise} + * + * @memberof qz.serial + */ + openPort: function(port, options) { + var params = { + port: port, + options: options + }; + return _qz.websocket.dataPromise('serial.openPort', params); + }, + + /** + * Send commands over a serial port. + * Any responses from the device will be sent to serial callback functions. + * + * @param {string} port An open serial port to send data. + * @param {string|Array|Object} data Data to be sent to the serial device. + * @param {string} [data.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @param {string|Array} data.data Data to be sent to the serial device. + * @param {Object} options Serial port configuration updates. See qz.serial.openPort `options` docs for available values. + * For best performance, it is recommended to only set these values on the port open call. + * + * @returns {Promise} + * + * @see qz.serial.setSerialCallbacks + * + * @memberof qz.serial + */ + sendData: function(port, data, options) { + if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) { + if (typeof data !== 'object') { + data = { + data: data, + type: "PLAIN" + } + } + + if (data.type && data.type.toUpperCase() == "FILE") { + data.data = _qz.tools.absolute(data.data); + } + } + + var params = { + port: port, + data: data, + options: options + }; + return _qz.websocket.dataPromise('serial.sendData', params); + }, + + /** + * @param {string} port Name of port to close. + * + * @returns {Promise} + * + * @memberof qz.serial + */ + closePort: function(port) { + return _qz.websocket.dataPromise('serial.closePort', { port: port }); + } + }, + + /** + * Calls related to interaction with communication sockets. + * @namespace qz.socket + */ + socket: { + /** + * Opens a network port for sending and receiving data. + * + * @param {string} host The connection hostname. + * @param {number} port The connection port number. + * @param {Object} [options] Network socket configuration. + * @param {string} [options.encoding='UTF-8'] Character set for communications. + * + * @memberof qz.socket + */ + open: function(host, port, options) { + var params = { + host: host, + port: port, + options: options + }; + return _qz.websocket.dataPromise("socket.open", params); + }, + + /** + * @param {string} host The connection hostname. + * @param {number} port The connection port number. + * + * @memberof qz.socket + */ + close: function(host, port) { + var params = { + host: host, + port: port + }; + return _qz.websocket.dataPromise("socket.close", params); + }, + + /** + * Send data over an open socket. + * + * @param {string} host The connection hostname. + * @param {number} port The connection port number. + * @param {string|Object} data Data to be sent over the port. + * @param {string} [data.type='PLAIN'] Valid values [PLAIN] + * @param {string} data.data Data to be sent over the port. + * + * @memberof qz.socket + */ + sendData: function(host, port, data) { + if (typeof data !== 'object') { + data = { + data: data, + type: "PLAIN" + }; + } + + var params = { + host: host, + port: port, + data: data + }; + return _qz.websocket.dataPromise("socket.sendData", params); + }, + + /** + * List of functions called for any response from open network sockets. + * Event data will contain {string} host and {number} port for all types. + * For RECEIVE types, {string} response. + * For ERROR types, {string} exception. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * + * @memberof qz.socket + */ + setSocketCallbacks: function(calls) { + _qz.socket.socketCallbacks = calls; + } + }, + + /** + * Calls related to interaction with USB devices. + * @namespace qz.usb + */ + usb: { + /** + * List of available USB devices. Includes (hexadecimal) vendor ID, (hexadecimal) product ID, and hub status. + * If supported, also returns manufacturer and product descriptions. + * + * @param includeHubs Whether to include USB hubs. + * @returns {Promise|Error>} Array of JSON objects containing information on connected USB devices. + * + * @memberof qz.usb + */ + listDevices: function(includeHubs) { + return _qz.websocket.dataPromise('usb.listDevices', { includeHubs: includeHubs }); + }, + + /** + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @returns {Promise|Error>} List of available (hexadecimal) interfaces on a USB device. + * + * @memberof qz.usb + */ + listInterfaces: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('usb.listInterfaces', deviceInfo); + }, + + /** + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.iface Hex string of interface on the USB device to search. + * @returns {Promise|Error>} List of available (hexadecimal) endpoints on a USB device's interface. + * + * @memberof qz.usb + */ + listEndpoints: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + interface: arguments[2] + }; + } + + return _qz.websocket.dataPromise('usb.listEndpoints', deviceInfo); + }, + + /** + * List of functions called for any response from open usb devices. + * Event data will contain {string} vendorId and {string} productId for all types. + * For RECEIVE types, {Array} output (in hexadecimal format). + * For ERROR types, {string} exception. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * + * @memberof qz.usb + */ + setUsbCallbacks: function(calls) { + _qz.usb.usbCallbacks = calls; + }, + + /** + * Claim a USB device's interface to enable sending/reading data across an endpoint. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.interface Hex string of interface on the USB device to claim. + * @returns {Promise} + * + * @memberof qz.usb + */ + claimDevice: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + interface: arguments[2] + }; + } + + return _qz.websocket.dataPromise('usb.claimDevice', deviceInfo); + }, + + /** + * Check the current claim state of a USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @returns {Promise} + * + * @since 2.0.2 + * @memberOf qz.usb + */ + isClaimed: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('usb.isClaimed', deviceInfo); + }, + + /** + * Send data to a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @param deviceInfo.data Bytes to send over specified endpoint. + * @param {string} [deviceInfo.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @returns {Promise} + * + * @memberof qz.usb + */ + sendData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2], + data: arguments[3] + }; + } + + if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) { + if (typeof deviceInfo.data !== 'object') { + deviceInfo.data = { + data: deviceInfo.data, + type: "PLAIN" + } + } + + if (deviceInfo.data.type && deviceInfo.data.type.toUpperCase() == "FILE") { + deviceInfo.data.data = _qz.tools.absolute(deviceInfo.data.data); + } + } + + return _qz.websocket.dataPromise('usb.sendData', deviceInfo); + }, + + /** + * Read data from a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @returns {Promise|Error>} List of (hexadecimal) bytes received from the USB device. + * + * @memberof qz.usb + */ + readData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2], + responseSize: arguments[3] + }; + } + + return _qz.websocket.dataPromise('usb.readData', deviceInfo); + }, + + /** + * Provides a continuous stream of read data from a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @param deviceInfo.interval=100 Frequency to send read data back, in milliseconds. + * @returns {Promise} + * + * @see qz.usb.setUsbCallbacks + * + * @memberof qz.usb + */ + openStream: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2], + responseSize: arguments[3], + interval: arguments[4] + }; + } + + return _qz.websocket.dataPromise('usb.openStream', deviceInfo); + }, + + /** + * Stops the stream of read data from a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @returns {Promise} + * + * @memberof qz.usb + */ + closeStream: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2] + }; + } + + return _qz.websocket.dataPromise('usb.closeStream', deviceInfo); + }, + + /** + * Release a claimed USB device to free resources after sending/reading data. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @returns {Promise} + * + * @memberof qz.usb + */ + releaseDevice: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('usb.releaseDevice', deviceInfo); + } + }, + + + /** + * Calls related to interaction with HID USB devices
+ * Many of these calls can be accomplished from the qz.usb namespace, + * but HID allows for simpler interaction + * @namespace qz.hid + * @since 2.0.1 + */ + hid: { + /** + * List of available HID devices. Includes (hexadecimal) vendor ID and (hexadecimal) product ID. + * If available, also returns manufacturer and product descriptions. + * + * @returns {Promise|Error>} Array of JSON objects containing information on connected HID devices. + * @since 2.0.1 + * + * @memberof qz.hid + */ + listDevices: function() { + return _qz.websocket.dataPromise('hid.listDevices'); + }, + + /** + * Start listening for HID device actions, such as attach / detach events. + * Reported under the ACTION type in the streamEvent on callbacks. + * + * @returns {Promise} + * @since 2.0.1 + * + * @see qz.hid.setHidCallbacks + * + * @memberof qz.hid + */ + startListening: function() { + return _qz.websocket.dataPromise('hid.startListening'); + }, + + /** + * Stop listening for HID device actions. + * + * @returns {Promise} + * @since 2.0.1 + * + * @see qz.hid.setHidCallbacks + * + * @memberof qz.hid + */ + stopListening: function() { + return _qz.websocket.dataPromise('hid.stopListening'); + }, + + /** + * List of functions called for any response from open usb devices. + * Event data will contain {string} vendorId and {string} productId for all types. + * For RECEIVE types, {Array} output (in hexadecimal format). + * For ERROR types, {string} exception. + * For ACTION types, {string} actionType. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * @since 2.0.1 + * + * @memberof qz.hid + */ + setHidCallbacks: function(calls) { + _qz.hid.hidCallbacks = calls; + }, + + /** + * Claim a HID device to enable sending/reading data across. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + claimDevice: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.claimDevice', deviceInfo); + }, + + /** + * Check the current claim state of a HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * + * @since 2.0.2 + * @memberOf qz.hid + */ + isClaimed: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.isClaimed', deviceInfo); + }, + + /** + * Send data to a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.data Bytes to send over specified endpoint. + * @param deviceInfo.endpoint=0x00 First byte of the data packet signifying the HID report ID. + * Must be 0x00 for devices only supporting a single report. + * @param deviceInfo.reportId=0x00 Alias for deviceInfo.endpoint. Not used if endpoint is provided. + * @param {string} [deviceInfo.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + sendData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + data: arguments[2], + endpoint: arguments[3] + }; + } + + if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) { + if (typeof deviceInfo.data !== 'object') { + deviceInfo.data = { + data: deviceInfo.data, + type: "PLAIN" + } + } + + if (deviceInfo.data.type && deviceInfo.data.type.toUpperCase() == "FILE") { + deviceInfo.data.data = _qz.tools.absolute(deviceInfo.data.data); + } + } else { + if (typeof deviceInfo.data === 'object') { + if (deviceInfo.data.type.toUpperCase() !== "PLAIN" + || typeof deviceInfo.data.data !== "string") { + return _qz.tools.reject(new Error("Data format is not supported with connected QZ Tray version " + _qz.websocket.connection.version)); + } + + deviceInfo.data = deviceInfo.data.data; + } + } + + return _qz.websocket.dataPromise('hid.sendData', deviceInfo); + }, + + /** + * Read data from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @returns {Promise|Error>} List of (hexadecimal) bytes received from the HID device. + * @since 2.0.1 + * + * @memberof qz.hid + */ + readData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + responseSize: arguments[2] + }; + } + + return _qz.websocket.dataPromise('hid.readData', deviceInfo); + }, + + /** + * Send a feature report to a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.data Bytes to send over specified endpoint. + * @param deviceInfo.endpoint=0x00 First byte of the data packet signifying the HID report ID. + * Must be 0x00 for devices only supporting a single report. + * @param deviceInfo.reportId=0x00 Alias for deviceInfo.endpoint. Not used if endpoint is provided. + * @param {string} [deviceInfo.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @returns {Promise} + * + * @memberof qz.hid + */ + sendFeatureReport: function(deviceInfo) { + return _qz.websocket.dataPromise('hid.sendFeatureReport', deviceInfo); + }, + + /** + * Get a feature report from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @returns {Promise|Error>} List of (hexadecimal) bytes received from the HID device. + * + * @memberof qz.hid + */ + getFeatureReport: function(deviceInfo) { + return _qz.websocket.dataPromise('hid.getFeatureReport', deviceInfo); + }, + + /** + * Provides a continuous stream of read data from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @param deviceInfo.interval=100 Frequency to send read data back, in milliseconds. + * @returns {Promise} + * @since 2.0.1 + * + * @see qz.hid.setHidCallbacks + * + * @memberof qz.hid + */ + openStream: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + responseSize: arguments[2], + interval: arguments[3] + }; + } + + return _qz.websocket.dataPromise('hid.openStream', deviceInfo); + }, + + /** + * Stops the stream of read data from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + closeStream: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.closeStream', deviceInfo); + }, + + /** + * Release a claimed HID device to free resources after sending/reading data. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + releaseDevice: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.releaseDevice', deviceInfo); + } + }, + + + /** + * Calls related to interactions with the filesystem + * @namespace qz.file + * @since 2.1 + */ + file: { + /** + * List of files available at the given directory.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute directory path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @returns {Promise|Error>} Array of files at the given path + * + * @memberof qz.file + */ + list: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.list', param); + }, + + /** + * Reads contents of file at the given path.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @param {string} [params.flavor='plain'] Flavor of data format used, valid flavors are [base64 | hex | plain]. + * @returns {Promise} String containing the file contents + * + * @memberof qz.file + */ + read: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.read', param); + }, + + /** + * Writes data to the file at the given path.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location. + * @param {Object} params Object containing file access parameters + * @param {string} params.data File data to be written + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @param {boolean} [params.append=false] Appends to the end of the file if set, otherwise overwrites existing contents + * @param {string} [params.flavor='plain'] Flavor of data format used, valid flavors are [base64 | file | hex | plain]. + * @returns {Promise} + * + * @memberof qz.file + */ + write: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.write', param); + }, + + /** + * Deletes a file at given path.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @returns {Promise} + * + * @memberof qz.file + */ + remove: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.remove', param); + }, + + /** + * Provides a continuous stream of events (and optionally data) from a local file. + * + * @param {string} path Relative or absolute directory path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @param {Object} [params.listener] If defined, file data will be returned on events + * @param {number} [params.listener.bytes=-1] Number of bytes to return or -1 for all + * @param {number} [params.listener.lines=-1] Number of lines to return or -1 for all + * @param {boolean} [params.listener.reverse] Controls whether data should be returned from the bottom of the file. Default value is true for line mode and false for byte mode. + * @param {string|Array} [params.include] File patterns to match. Blank values will be ignored. + * @param {string|Array} [params.exclude] File patterns to exclude. Blank values will be ignored. Takes priority over params.include. + * @param {boolean} [params.ignoreCase=true] Whether params.include or params.exclude are case-sensitive. + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.file.setFileCallbacks + * + * @memberof qz.file + */ + startListening: function(path, params) { + if (params && typeof params.include !== 'undefined' && !Array.isArray(params.include)) { + params.include = [params.include]; + } + if (params && typeof params.exclude !== 'undefined' && !Array.isArray(params.exclude)) { + params.exclude = [params.exclude]; + } + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.startListening', param); + }, + + /** + * Closes listeners with the provided settings. Omitting the path parameter will result in all listeners closing. + * + * @param {string} [path] Previously opened directory path of listener to close, or omit to close all. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @returns {Promise} + * + * @memberof qz.file + */ + stopListening: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.stopListening', param); + }, + + /** + * List of functions called for any response from a file listener. + * For ERROR types event data will contain, {string} message. + * For ACTION types event data will contain, {string} file {string} eventType {string} [data]. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * @since 2.1.0 + * + * @memberof qz.file + */ + setFileCallbacks: function(calls) { + _qz.file.fileCallbacks = calls; + } + }, + + /** + * Calls related to networking information + * @namespace qz.networking + * @since 2.1.0 + */ + networking: { + /** + * @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] Port to use with custom hostname, defaults to 443 + * @returns {Promise} Connected system's network information. + * + * @memberof qz.networking + * @since 2.1.0 + */ + device: function(hostname, port) { + // Wrap 2.0 + if (_qz.tools.isVersion(2, 0)) { + return _qz.compatible.networking(hostname, port, null, null, function(data) { + return { ip: data.ipAddress, mac: data.macAddress }; + }); + } + // Use 2.1 + return _qz.websocket.dataPromise('networking.device', { + hostname: hostname, + port: port + }); + }, + + /** + * Get computer hostname + * + * @param {string} [hostname] DEPRECATED Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] DEPRECATED Port to use with custom hostname, defaults to 443 + * @returns {Promise} Connected system's hostname. + * + * @memberof qz.networking + * @since 2.2.2 + */ + hostname: function(hostname, port) { + // Wrap < 2.2.2 + if (_qz.tools.versionCompare(2, 2, 2) < 0) { + return _qz.tools.promise(function(resolve, reject) { + _qz.websocket.dataPromise('networking.device', { hostname: hostname, port: port }).then(function(device) { + console.log(device); + resolve(device.hostname); + }); + }); + } else { + return _qz.websocket.dataPromise('networking.hostname'); + } + }, + + /** + * @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] Port to use with custom hostname, defaults to 443 + * @returns {Promise|Error>} Connected system's network information. + * + * @memberof qz.networking + * @since 2.1.0 + */ + devices: function(hostname, port) { + // Wrap 2.0 + if (_qz.tools.isVersion(2, 0)) { + return _qz.compatible.networking(hostname, port, null, null, function(data) { + return [{ ip: data.ipAddress, mac: data.macAddress }]; + }); + } + // Use 2.1 + return _qz.websocket.dataPromise('networking.devices', { + hostname: hostname, + port: port + }); + } + }, + + + /** + * Calls related to signing connection requests. + * @namespace qz.security + */ + security: { + /** + * Set promise resolver for calls to acquire the site's certificate. + * + * @param {Function|AsyncFunction|Promise} promiseHandler Either a function that will be used as a promise resolver (of format Function({function} resolve, {function}reject)), + * an async function, or a promise. Any of which should return the public certificate via their respective resolve call. + * @param {Object} [options] Configuration options for the certificate resolver + * @param {boolean} [options.rejectOnFailure=[false]] Overrides default behavior to call resolve with a blank certificate on failure. + * @memberof qz.security + */ + setCertificatePromise: function(promiseHandler, options) { + _qz.security.certHandler = promiseHandler; + _qz.security.rejectOnCertFailure = !!(options && options.rejectOnFailure); + }, + + /** + * Set promise factory for calls to sign API calls. + * + * @param {Function|AsyncFunction} promiseFactory Either a function that accepts a string parameter of the data to be signed + * and returns a function to be used as a promise resolver (of format Function({function} resolve, {function}reject)), + * or an async function that can take a string parameter of the data to be signed. Either of which should return the signed contents of + * the passed string parameter via their respective resolve call. + * + * @example + * qz.security.setSignaturePromise(function(dataToSign) { + * return function(resolve, reject) { + * $.ajax("/signing-url?data=" + dataToSign).then(resolve, reject); + * } + * }) + * + * @memberof qz.security + */ + setSignaturePromise: function(promiseFactory) { + _qz.security.signatureFactory = promiseFactory; + }, + + /** + * Set which signing algorithm QZ will check signatures against. + * + * @param {string} algorithm The algorithm used in signing. Valid values: [SHA1 | SHA256 | SHA512] + * @since 2.1.0 + * + * @memberof qz.security + */ + setSignatureAlgorithm: function(algorithm) { + //warn for incompatibilities if known + if (!_qz.compatible.algorithm()) { + return; + } + + if (["SHA1", "SHA256", "SHA512"].indexOf(algorithm.toUpperCase()) < 0) { + _qz.log.error("Signing algorithm '" + algorithm + "' is not supported."); + } else { + _qz.security.signAlgorithm = algorithm; + } + }, + + /** + * Get the signing algorithm QZ will be checking signatures against. + * + * @returns {string} The algorithm used in signing. + * @since 2.1.0 + * + * @memberof qz.security + */ + getSignatureAlgorithm: function() { + return _qz.security.signAlgorithm; + } + }, + + /** + * Calls related to compatibility adjustments + * @namespace qz.api + */ + api: { + /** + * Show or hide QZ api debugging statements in the browser console. + * + * @param {boolean} show Whether the debugging logs for QZ should be shown. Hidden by default. + * @returns {boolean} Value of debugging flag + * @memberof qz.api + */ + showDebug: function(show) { + return (_qz.DEBUG = show); + }, + + /** + * Get version of connected QZ Tray application. + * + * @returns {Promise} Version number of QZ Tray. + * + * @memberof qz.api + */ + getVersion: function() { + return _qz.websocket.dataPromise('getVersion'); + }, + + /** + * Checks for the specified version of connected QZ Tray application. + * + * @param {string|number} [major] Major version to check + * @param {string|number} [minor] Minor version to check + * @param {string|number} [patch] Patch version to check + * + * @memberof qz.api + */ + isVersion: _qz.tools.isVersion, + + /** + * Checks if the connected QZ Tray application is greater than the specified version. + * + * @param {string|number} major Major version to check + * @param {string|number} [minor] Minor version to check + * @param {string|number} [patch] Patch version to check + * @param {string|number} [build] Build version to check + * @returns {boolean} True if connected version is greater than the version specified. + * + * @memberof qz.api + * @since 2.1.0-4 + */ + isVersionGreater: function(major, minor, patch, build) { + return _qz.tools.versionCompare(major, minor, patch, build) > 0; + }, + + /** + * Checks if the connected QZ Tray application is less than the specified version. + * + * @param {string|number} major Major version to check + * @param {string|number} [minor] Minor version to check + * @param {string|number} [patch] Patch version to check + * @param {string|number} [build] Build version to check + * @returns {boolean} True if connected version is less than the version specified. + * + * @memberof qz.api + * @since 2.1.0-4 + */ + isVersionLess: function(major, minor, patch, build) { + return _qz.tools.versionCompare(major, minor, patch, build) < 0; + }, + + /** + * Change the promise library used by QZ API. + * Should be called before any initialization to avoid possible errors. + * + * @param {Function} promiser Function({function} resolver) called to create new promises. + * + * @memberof qz.api + */ + setPromiseType: function(promiser) { + _qz.tools.promise = promiser; + }, + + /** + * Change the SHA-256 hashing function used by QZ API. + * Should be called before any initialization to avoid possible errors. + * + * @param {Function} hasher Function({function} message) called to create hash of passed string. + * + * @memberof qz.api + */ + setSha256Type: function(hasher) { + _qz.tools.hash = hasher; + }, + + /** + * Change the WebSocket handler. + * Should be called before any initialization to avoid possible errors. + * + * @param {Function} ws Function({function} WebSocket) called to override the internal WebSocket handler. + * + * @memberof qz.api + */ + setWebSocketType: function(ws) { + _qz.tools.ws = ws; + } + }, + + /** + * Version of this JavaScript library + * + * @constant {string} + * + * @memberof qz + */ + version: _qz.VERSION + }; + + return qz; +})(); + + +(function() { + if (typeof define === 'function' && define.amd) { + define(qz); + } else if (typeof exports === 'object') { + module.exports = qz; + } else { + window.qz = qz; + } +})(); diff --git a/py_app/app/templates/print_module.html b/py_app/app/templates/print_module.html index 766dab4..5e73eeb 100644 --- a/py_app/app/templates/print_module.html +++ b/py_app/app/templates/print_module.html @@ -35,6 +35,71 @@ background-color: #007bff !important; color: white !important; } + +/* Print Progress Modal Styles */ +.print-progress-modal { + display: none; + position: fixed; + z-index: 9999; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + justify-content: center; + align-items: center; +} + +.print-progress-content { + background-color: white; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + min-width: 400px; + max-width: 500px; + text-align: center; +} + +.print-progress-content h3 { + margin: 0 0 20px 0; + color: #333; + font-size: 24px; +} + +.progress-info { + margin-bottom: 15px; + color: #666; + font-size: 16px; +} + +.progress-bar-container { + width: 100%; + height: 30px; + background-color: #f0f0f0; + border-radius: 15px; + overflow: hidden; + margin-bottom: 15px; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.progress-bar { + height: 100%; + background: linear-gradient(90deg, #007bff 0%, #0056b3 100%); + width: 0%; + transition: width 0.3s ease; + border-radius: 15px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; +} + +.progress-details { + font-size: 18px; + font-weight: bold; + color: #007bff; +} {% endblock %} @@ -44,6 +109,20 @@

Label View
+ + + + +
+ + +
+ + +
+
@@ -209,11 +288,19 @@
Professional printing solution • ZPL thermal labels • Direct hardware access
- +
+ + +
+
📥 Download QZ Tray (Free)
- Setup: 1. Install QZ Tray → 2. Start the service → 3. Refresh this page + Setup: 1. Install QZ Tray → 2. Start the service → 3. Click "Reconnect"
@@ -253,10 +340,28 @@ + + + - + + + + +{% endblock %} + +{% block content %} +
+ +
+
Label View
+ + + + + +
+ + +
+ + +
+ + +
+ +
+ +
+ INNOFA ROMANIA SRL +
+ + +
+ +
+ + +
+
+
+
+
+
+
+
+ + +
+ + +
+ Quantity ordered +
+
+ +
+ + +
+ Customer order +
+
+ +
+ + +
+ Delivery date +
+
+ +
+ + +
+ Product description +
+
+ +
+ + +
+ Size +
+
+ +
+ + +
+ Article code +
+
+ +
+ + +
+ Prod. order +
+
+ +
+
+ + +
+ + + + +
+ +
+
+ + +
+ + + + +
+ +
+
+
+ + +
+ +
+ + +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+ + +
+
Creates sequential labels based on quantity
+ (e.g., CP00000711-001 to CP00000711-063) +
+ + + +
+
+ + +
+

Data Preview (Unprinted Orders)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/print_module.html.backup2 b/py_app/app/templates/print_module.html.backup2 new file mode 100644 index 0000000..4913228 --- /dev/null +++ b/py_app/app/templates/print_module.html.backup2 @@ -0,0 +1,1374 @@ +{% extends "base.html" %} + +{% block head %} + + + +{% endblock %} + +{% block content %} +
+ +
+
Label View
+ + + + + +
+ + +
+ + +
+ + +
+ +
+ +
+ INNOFA ROMANIA SRL +
+ + +
+ +
+ + +
+
+
+
+
+
+
+
+ + +
+ + +
+ Quantity ordered +
+
+ +
+ + +
+ Customer order +
+
+ +
+ + +
+ Delivery date +
+
+ +
+ + +
+ Product description +
+
+ +
+ + +
+ Size +
+
+ +
+ + +
+ Article code +
+
+ +
+ + +
+ Prod. order +
+
+ +
+
+ + +
+ + + + +
+ +
+
+ + +
+ + + + +
+ +
+
+
+ + +
+ +
+ + +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+ + +
+
Creates sequential labels based on quantity
+ (e.g., CP00000711-001 to CP00000711-063) +
+ + + +
+
+ + +
+

Data Preview (Unprinted Orders)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/instance/pairing_keys.json b/py_app/instance/pairing_keys.json new file mode 100644 index 0000000..a52d568 --- /dev/null +++ b/py_app/instance/pairing_keys.json @@ -0,0 +1,7 @@ +[ + { + "printer_name": "test 1", + "pairing_key": "fanlCYDshm8exebw5gP1Se_l0BR37mwV6FbogxZUE2w", + "warranty_until": "2026-09-30" + } +] \ No newline at end of file