|
29 | 29 | <i class="fas fa-filter"></i> Filter |
30 | 30 | </label> |
31 | 31 | </div> |
| 32 | + <button class="btn btn-secondary" id="connect-printer"> |
| 33 | + Connect printer |
| 34 | + </button> |
32 | 35 | <div class="card mt-3{% if not hasFilters %} d-none{% endif %}" id="filter-card"> |
33 | 36 | <div class="card-body"> |
34 | 37 | <div class="row"> |
|
83 | 86 | {% block extrafooter %} |
84 | 87 | {% if current_contest is not null %} |
85 | 88 | <script> |
| 89 | + let port = null; |
| 90 | + let isConnected = false; |
| 91 | +
|
| 92 | + async function checkForPreviousPermissions() { |
| 93 | + try { |
| 94 | + // Get previously authorized ports |
| 95 | + const ports = await navigator.serial.getPorts(); |
| 96 | +
|
| 97 | + if (ports.length > 0) { |
| 98 | + console.log('Found previously authorized port'); |
| 99 | + return ports[0]; // Return the first available port |
| 100 | + } |
| 101 | +
|
| 102 | + return null; |
| 103 | + } catch (error) { |
| 104 | + console.error('Error checking permissions:', error); |
| 105 | + return null; |
| 106 | + } |
| 107 | + } |
| 108 | +
|
| 109 | + $(document).ready(async function() { |
| 110 | + if (!navigator.serial) { |
| 111 | + $('#connect-printer').hide(); |
| 112 | + return; |
| 113 | + } |
| 114 | +
|
| 115 | + $('#connect-printer').text('Checking for previous printer connection...'); |
| 116 | +
|
| 117 | + try { |
| 118 | + // Check for previously granted permissions |
| 119 | + const previousPort = await checkForPreviousPermissions(); |
| 120 | +
|
| 121 | + if (previousPort) { |
| 122 | + // Attempt to reconnect automatically |
| 123 | + port = previousPort; |
| 124 | +
|
| 125 | + try { |
| 126 | + // Open the port with TM-88II settings |
| 127 | + await port.open({ |
| 128 | + baudRate: 9600, // TM-88II default |
| 129 | + dataBits: 8, |
| 130 | + stopBits: 1, |
| 131 | + parity: 'none', |
| 132 | + flowControl: 'none' |
| 133 | + }); |
| 134 | +
|
| 135 | + $('#connect-printer').text('Reconnected to printer'); |
| 136 | + isConnected = true; |
| 137 | + } catch (error) { |
| 138 | + $('#status').text('Failed to reconnect'); |
| 139 | + } |
| 140 | + } else { |
| 141 | + $('#status').text('Connect printer'); |
| 142 | + } |
| 143 | + } catch (error) { |
| 144 | + console.error('Auto-reconnect failed:', error); |
| 145 | + $('#status').text('Connect printer'); |
| 146 | + } |
| 147 | + }); |
| 148 | +
|
| 149 | + async function connectPrinter() { |
| 150 | + try { |
| 151 | + // Request port access |
| 152 | + port = await navigator.serial.requestPort(); |
| 153 | +
|
| 154 | + // Open the port with TM-88II settings |
| 155 | + await port.open({ |
| 156 | + baudRate: 9600, // TM-88II default |
| 157 | + dataBits: 8, |
| 158 | + stopBits: 1, |
| 159 | + parity: 'none', |
| 160 | + flowControl: 'none' |
| 161 | + }); |
| 162 | +
|
| 163 | + console.log('Printer connected!'); |
| 164 | + $('#connect-printer').text('Connected to printer'); |
| 165 | + return true; |
| 166 | + } catch (error) { |
| 167 | + console.error('Connection failed:', error); |
| 168 | + return false; |
| 169 | + } |
| 170 | + } |
| 171 | +
|
| 172 | + async function sendCommand(command) { |
| 173 | + if (!port) { |
| 174 | + console.error('Printer not connected'); |
| 175 | + return; |
| 176 | + } |
| 177 | +
|
| 178 | + const writer = port.writable.getWriter(); |
| 179 | + try { |
| 180 | + await writer.write(new Uint8Array(command)); |
| 181 | + // Small delay to ensure command is processed |
| 182 | + await new Promise(resolve => setTimeout(resolve, 10)); |
| 183 | + } catch (error) { |
| 184 | + console.error('Write failed:', error); |
| 185 | + } finally { |
| 186 | + writer.releaseLock(); |
| 187 | + } |
| 188 | + } |
| 189 | +
|
| 190 | + // Map German umlauts to CP850 encoding |
| 191 | + function convertToCP850(text) { |
| 192 | + const charMap = { |
| 193 | + '<C3><A4>': String.fromCharCode(0x84), // <C3><A4> -> 132 |
| 194 | + '<C3><B6>': String.fromCharCode(0x94), // <C3><B6> -> 148 |
| 195 | + '<C3><BC>': String.fromCharCode(0x81), // <C3><BC> -> 129 |
| 196 | + '<C3><84>': String.fromCharCode(0x8E), // <C3><84> -> 142 |
| 197 | + '<C3><96>': String.fromCharCode(0x99), // <C3><96> -> 153 |
| 198 | + '<C3><9C>': String.fromCharCode(0x9A), // <C3><9C> -> 154 |
| 199 | + '<C3><9F>': String.fromCharCode(0xE1), // <C3><9F> -> 225 |
| 200 | + '<E2><82><AC>': String.fromCharCode(0xD5), // Euro symbol (for CP858) |
| 201 | +
|
| 202 | + // French characters |
| 203 | + '<C3><A9>': String.fromCharCode(0x82), // <C3><A9> -> 130 |
| 204 | + '<C3><A8>': String.fromCharCode(0x8A), // <C3><A8> -> 138 |
| 205 | + '<C3><A0>': String.fromCharCode(0x85), // <C3><A0> -> 133 |
| 206 | + '<C3><A7>': String.fromCharCode(0x87), // <C3><A7> -> 135 |
| 207 | +
|
| 208 | + // Spanish characters |
| 209 | + '<C3><B1>': String.fromCharCode(0xA4), // <C3><B1> -> 164 |
| 210 | + '<C3><A1>': String.fromCharCode(0xA0), // <C3><A1> -> 160 |
| 211 | + '<C3><AD>': String.fromCharCode(0xA1), // <C3><AD> -> 161 |
| 212 | + '<C3><B3>': String.fromCharCode(0xA2), // <C3><B3> -> 162 |
| 213 | + '<C3><BA>': String.fromCharCode(0xA3) // <C3><BA> -> 163 |
| 214 | + }; |
| 215 | +
|
| 216 | + return text.replace(/[<C3><A4><C3><B6><C3><BC><C3><84><C3><96><C3><9C><C3><9F><C3><A9><C3><A8><C3><A0><C3><A7><C3><B1><C3><A1><C3><AD><C3><B3><C3><BA><E2><82<82><AC>]/g, match => charMap[match] || match); |
| 217 | + } |
| 218 | +
|
| 219 | + // Send text with automatic encoding |
| 220 | + async function sendText(text) { |
| 221 | + const convertedText = convertToCP850(text); |
| 222 | + const textBytes = new Uint8Array([...convertedText].map(char => char.charCodeAt(0))); |
| 223 | + await sendCommand([...textBytes]); |
| 224 | + } |
| 225 | +
|
| 226 | + // ESC/POS command constants |
| 227 | + const ESC = 0x1B; |
| 228 | + const GS = 0x1D; |
| 229 | + const FS = 0x1C; |
| 230 | +
|
| 231 | + // Working commands for TM-88II |
| 232 | + const commands = { |
| 233 | + // Code page commands for TM-88II |
| 234 | + codePage: { |
| 235 | + cp850: [ESC, 0x74, 0x02], // Western Europe (supports umlauts) |
| 236 | + cp858: [ESC, 0x74, 0x13], // Western Europe + Euro symbol |
| 237 | + cp437: [ESC, 0x74, 0x00], // US (default, no umlauts) |
| 238 | + iso8859_1: [ESC, 0x74, 0x06] // Latin-1 |
| 239 | + }, |
| 240 | +
|
| 241 | + // Initialization |
| 242 | + init: [ESC, 0x40], |
| 243 | +
|
| 244 | + // Text emphasis (these work better than bold) |
| 245 | + emphasis: { |
| 246 | + on: [ESC, 0x45, 0x01], // Bold on |
| 247 | + off: [ESC, 0x45, 0x00] // Bold off |
| 248 | + }, |
| 249 | +
|
| 250 | + // Double size (more reliable than bold) |
| 251 | + doubleWidth: { |
| 252 | + on: [ESC, 0x21, 0x20], // Double width on |
| 253 | + off: [ESC, 0x21, 0x00] // Normal width |
| 254 | + }, |
| 255 | +
|
| 256 | + doubleHeight: { |
| 257 | + on: [ESC, 0x21, 0x10], // Double height on |
| 258 | + off: [ESC, 0x21, 0x00] // Normal height |
| 259 | + }, |
| 260 | +
|
| 261 | + // Both double width and height |
| 262 | + doubleSize: { |
| 263 | + on: [ESC, 0x21, 0x30], // Double both |
| 264 | + off: [ESC, 0x21, 0x00] // Normal |
| 265 | + }, |
| 266 | +
|
| 267 | + // Alignment (these should work) |
| 268 | + align: { |
| 269 | + left: [ESC, 0x61, 0x30], // or 0x00 |
| 270 | + center: [ESC, 0x61, 0x31], // or 0x01 |
| 271 | + right: [ESC, 0x61, 0x32] // or 0x02 |
| 272 | + }, |
| 273 | +
|
| 274 | + // Alternative alignment commands |
| 275 | + justification: { |
| 276 | + left: [ESC, 0x61, 0x00], |
| 277 | + center: [ESC, 0x61, 0x01], |
| 278 | + right: [ESC, 0x61, 0x02] |
| 279 | + }, |
| 280 | +
|
| 281 | + // Paper handling |
| 282 | + feedLines: (n) => [ESC, 0x64, n], |
| 283 | + cutPaper: [GS, 0x56, 0x00], // Full cut |
| 284 | + cutPaperPartial: [GS, 0x56, 0x01], // Partial cut |
| 285 | +
|
| 286 | + // Line feed and carriage return |
| 287 | + lf: [0x0A], |
| 288 | + cr: [0x0D], |
| 289 | + crlf: [0x0D, 0x0A] |
| 290 | + }; |
| 291 | +
|
| 292 | + async function printReceipt(balloon, team, color) { |
| 293 | + try { |
| 294 | + // Initialize printer |
| 295 | + await sendCommand(commands.init); |
| 296 | + await sendCommand(commands.codePage.cp850); |
| 297 | +
|
| 298 | + // Center-aligned header with double size |
| 299 | + await sendCommand(commands.align.center); |
| 300 | + await sendCommand(commands.doubleSize.on); |
| 301 | + await sendText('NWERC 2025'); |
| 302 | + await sendCommand(commands.crlf); |
| 303 | + await sendCommand(commands.doubleSize.off); |
| 304 | +
|
| 305 | + // Normal size subtitle |
| 306 | + await sendText(`Balloon #${balloon.balloonid}`); |
| 307 | + await sendCommand(commands.crlf); |
| 308 | + await sendCommand(commands.crlf); |
| 309 | +
|
| 310 | + // Left align for items |
| 311 | + await sendCommand(commands.align.left); |
| 312 | + await sendText('Date: ' + new Date().toLocaleDateString()); |
| 313 | + await sendCommand(commands.crlf); |
| 314 | + await sendText('Time: ' + new Date().toLocaleTimeString()); |
| 315 | + await sendCommand(commands.crlf); |
| 316 | + await sendCommand(commands.crlf); |
| 317 | +
|
| 318 | + // Items with emphasis |
| 319 | + await sendCommand(commands.emphasis.on); |
| 320 | + await sendText(` Location: Problem: `); |
| 321 | + await sendCommand(commands.emphasis.off); |
| 322 | + await sendCommand(commands.crlf); |
| 323 | + await sendCommand(commands.doubleSize.on); |
| 324 | + await sendText(` ${balloon.location || 'Z7'} ${balloon.problem} `); |
| 325 | + await sendCommand(commands.doubleSize.off); |
| 326 | + await sendCommand(commands.crlf); |
| 327 | + await sendCommand(commands.crlf); |
| 328 | + await sendCommand(commands.emphasis.on); |
| 329 | + await sendText('Team Details:'); |
| 330 | + await sendCommand(commands.emphasis.off); |
| 331 | + await sendCommand(commands.crlf); |
| 332 | + await sendText(`Name: ${team}`); |
| 333 | + await sendCommand(commands.crlf); |
| 334 | + await sendText(`Location: ${balloon.location || 'Z7'}`); |
| 335 | + await sendCommand(commands.crlf); |
| 336 | + await sendText(`Affiliation: ${balloon.affiliation}`); |
| 337 | + await sendCommand(commands.crlf); |
| 338 | + await sendText(`Category: ${balloon.category}`); |
| 339 | + await sendCommand(commands.crlf); |
| 340 | + await sendText(`#Balloons: ${Object.keys(balloon.total).length}`); |
| 341 | + await sendCommand(commands.crlf); |
| 342 | + await sendCommand(commands.crlf); |
| 343 | + await sendCommand(commands.emphasis.on); |
| 344 | + await sendText('Balloon Details:'); |
| 345 | + await sendCommand(commands.emphasis.off); |
| 346 | + await sendCommand(commands.crlf); |
| 347 | + await sendText(`Problem: ${balloon.problem}`); |
| 348 | + await sendCommand(commands.crlf); |
| 349 | + await sendText(`Color: ${color}`); |
| 350 | + await sendCommand(commands.crlf); |
| 351 | +
|
| 352 | + // Separator line |
| 353 | + await sendText('------------------------'); |
| 354 | + await sendCommand(commands.crlf); |
| 355 | +
|
| 356 | + // Feed and cut |
| 357 | + await sendCommand(commands.feedLines(3)); |
| 358 | + await sendCommand(commands.cutPaper); |
| 359 | +
|
| 360 | + console.log('Receipt printed successfully'); |
| 361 | + } catch (error) { |
| 362 | + console.error('Print failed:', error); |
| 363 | + } |
| 364 | + } |
| 365 | +
|
| 366 | + async function printThenRedirect(balloon, team, color, url) { |
| 367 | + event.preventDefault(); |
| 368 | + console.log(balloon, team, color); |
| 369 | + console.log(JSON.stringify(balloon)); |
| 370 | + console.log(JSON.stringify(team)); |
| 371 | + console.log(JSON.stringify(color)); |
| 372 | + await printReceipt(balloon, team, color); |
| 373 | + window.location.href = url; |
| 374 | + } |
| 375 | +
|
86 | 376 | $(function () { |
| 377 | + $('#connect-printer').on('click', async function () { |
| 378 | + await connectPrinter(); |
| 379 | + }); |
| 380 | +
|
87 | 381 | $('#filter-toggle').on('change', function () { |
88 | 382 | if ($(this).is(':checked')) { |
89 | 383 | $('#filter-card').removeClass('d-none'); |
|
0 commit comments