Skip to content

Commit

Permalink
support multi platform receipt print
Browse files Browse the repository at this point in the history
  • Loading branch information
pandadtdyy committed Jun 13, 2024
1 parent 91e1db2 commit 5b358ec
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 83 deletions.
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

A tool for CN XCPC contests

- 全平台远程代码打印
- 支持封榜期间发放鼓励气球的小票机
- 代码打印和小票机打印(全平台支持)
- 支持封榜期间发放鼓励气球
- 支持连接 DOMjudge 与 Hydro 系统,同时亦可独立运行
- 选手机赛时数据监控与屏幕监控

TODO Features:

- [ ] 更好的选手机座位绑定
- [ ] 支持多平台参赛系统
- [ ] Windows 小票机支持
- [ ] 优化 UI 顺畅度
- [ ] 使用 WebSocket 返回指令执行情况
- [ ] 支持全考场监视
Expand Down Expand Up @@ -51,7 +50,7 @@ const serverSchema = Schema.intersect([
Schema.const('hydro'),
] as const).required(), // 赛事系统类型
server: Schema.string().role('url').required(), // 赛事系统地址
contestId: Schema.string(), // 赛事ID,如无则自动获取(Domjudge),hydro 请使用 domainId/contestId 作为ID
contestId: Schema.string(), // 赛事ID,如无则自动获取(DOMjudge),hydro 请使用 domainId/contestId 作为ID
token: Schema.string(), // 赛事系统 Token 如无可使用用户名密码登录
username: Schema.string(), // 赛事系统用户名
password: Schema.string(), // 赛事系统密码
Expand All @@ -72,7 +71,7 @@ const serverSchema = Schema.intersect([
`print [file] [original] [language] [username] [teamname] [teamid] [location]` 为打印命令,其中 `file` 为代码文件路径,`original` 为原文件名,`language` 为语言,`username` 为用户名,`teamname` 为队伍名,`teamid` 为队伍ID,`location` 为选手位置。

#### Balloon
服务支持 `Fetch Mode` 下的气球推送,支持 `Domjudge``Hydro` 系统,支持 `Domjudge``Hydro` 系统的 `Balloon` 推送,同时若赛事在封榜后仍然推送气球,则支持自定义鼓励气球数,高于设定值则不推送,为所有队伍打造优质赛场体验。
服务支持 `Fetch Mode` 下的气球推送,支持 `DOMjudge``Hydro` 系统,支持 `DOMjudge``Hydro` 系统的 `Balloon` 推送,同时若赛事在封榜后仍然推送气球,则支持自定义鼓励气球数,高于设定值则不推送,为所有队伍打造优质赛场体验。

#### Monitor
服务支持监控选手机情况和监控服务器桌面,如您需要选手机监控,可通过设置 Systemd 定时执行任务等多种方式定时执行 `monitor` 命令,如需监控服务器桌面,请在选手机上提前运行 `vlc-camera``vlc-desktop` 服务, CAICPC 镜像已经内置了这三两个服务,您只需在选手机上运行即可,如您为自己的镜像,可从 `https://github.com/hydro-dev/xcpc-tools/blob/main/scripts/monitor` 下载 `monitor` 服务。
Expand Down
45 changes: 4 additions & 41 deletions packages/server/client/balloon.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
/* eslint-disable no-await-in-loop */
import { exec } from 'child_process';
import path from 'path';
import EscPosEncoder from '@freedom_sky/esc-pos-encoder';
import superagent from 'superagent';
import { config } from '../config';
import {
convertToChinese,
fs, Logger, sleep,
checkReceiptStatus, convertToChinese, Logger, receiptPrint, sleep,
} from '../utils';

const encoder = new EscPosEncoder();
Expand Down Expand Up @@ -73,31 +70,6 @@ const logger = new Logger('balloon');
let timer = null;
let printer = null;

async function getReceiptStatus(receipt) {
if (process.platform === 'win32') printer = { printer: receipt };
const lp = receipt.split('/').pop();
const oldPrinter = printer;
printer = {
printer: receipt,
info: fs.readFileSync(`/sys/class/usbmisc/${lp}/device/ieee1284_id`, 'utf8').trim(),
};
if (!oldPrinter || oldPrinter.info === printer.info) return;
logger.info('Printer changed:', printer.printer, printer.info);
const usbDevices = fs.readdirSync('/dev/usb');
for (const f of usbDevices) {
if (f.startsWith('lp')) {
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
if (lpid === oldPrinter.info) {
logger.info('Printer found:', f, ':', lpid);
oldPrinter.printer = `/dev/usb/${f}`;
printer = oldPrinter;
break;
}
}
}
if (oldPrinter.info !== printer.info) throw Error('Printer not found, please check the printer connection.');
}

async function printBalloon(doc, lang) {
const bReceipt = receiptText(
doc.balloonid,
Expand All @@ -109,17 +81,8 @@ async function printBalloon(doc, lang) {
doc.total ? Object.keys(doc.total).map((k) => `- ${k}: ${doc.total[k].color}`).join('\n') : 'N/A',
lang,
);
if (printer) {
await getReceiptStatus(printer.printer);
if (process.platform === 'win32') {
fs.writeFileSync(path.resolve(process.cwd(), 'data', 'balloon.txt'), bReceipt);
exec(`COPY /B "${path.resolve(process.cwd(), 'data', 'balloon.txt')}" "${printer.printer}"`, (err, stdout, stderr) => {
if (err) logger.error(err);
if (stdout) logger.info(stdout);
if (stderr) logger.error(stderr);
});
} else fs.writeFileSync(path.resolve(printer.printer), bReceipt);
}
printer = await checkReceiptStatus(printer);
await receiptPrint(printer, bReceipt);
}

async function fetchTask(c) {
Expand All @@ -146,7 +109,7 @@ async function fetchTask(c) {
}

export async function apply() {
await getReceiptStatus(config.balloon);
printer = config.balloon;
if (config.token && config.server && config.balloon) await fetchTask(config);
else logger.error('Config not found, please check the config.yaml');
}
28 changes: 6 additions & 22 deletions packages/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import path from 'path';
import Schema from 'schemastery';
import { version } from './package.json';
import {
fs, getPrinters, getWinReceiptPrinter,
Logger, yaml,
checkReceiptPrinter,
fs, getPrinters, Logger, yaml,
} from './utils';

const logger = new Logger('init');
Expand Down Expand Up @@ -32,32 +32,15 @@ password:
`;
let printers = [];
if (isClient) {
printers = await getPrinters().then((r) => r.map((p) => p.printer)).catch(() => []);
printers = await getPrinters(true).catch(() => []);
logger.info(printers.length, 'printers found:', printers.join(', '));
if (process.platform === 'linux') {
const usbDevices = fs.readdirSync('/dev/usb');
for (const f of usbDevices) {
if (f.startsWith('lp')) {
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
logger.info(`USB Printer ${f} found: ${lpid}`);
logger.info(`If you want to use this printer for balloon print, please set balloon: /dev/usb/${f} in config.yaml.`);
}
}
if (!usbDevices.length) logger.info('If you want to use balloon client, please connect your receipt printer first.');
} else if (process.platform === 'win32') {
const printerList = await getWinReceiptPrinter();
for (const printer of printerList) {
logger.info(`Receipt Printer ${printer.printer}(${printer.device})) found: ${printer.description}`);
logger.info(`If you want to use this printer for balloon print, please set balloon: ${printer.printer} in config.yaml.`);
}
if (!printers.length) logger.info('If you want to use balloon client, please share your receipt printer on settings first.');
} else logger.info('If you want to use balloon client, please run this on Linux/Windows.');
await checkReceiptPrinter(printers);
}
const clientConfigDefault = yaml.dump({
server: '',
balloon: '',
balloonLang: 'zh',
printers,
printers: printers.map((p) => p.printer),
token: '',
});
fs.writeFileSync(configPath, isClient ? clientConfigDefault : serverConfigDefault);
Expand Down Expand Up @@ -90,6 +73,7 @@ const serverSchema = Schema.intersect([
token: Schema.string(),
username: Schema.string(),
password: Schema.string(),
freezeEncourage: Schema.number().default(0),
}).description('Fetcher Config'),
Schema.object({
type: Schema.const('server').required(),
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/xcpc-tools",
"version": "1.0.0",
"version": "1.0.1",
"description": "A tools for XCPC contests",
"main": "index.ts",
"repository": "https://github.com/Hydro-dev/xcpc-tools",
Expand Down
1 change: 1 addition & 0 deletions packages/server/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,4 @@ export function decodeBinary(file: string) {
export * from './commandRunner';
export * from './printers';
export * from './color';
export * from './receipt';
16 changes: 3 additions & 13 deletions packages/server/utils/printers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ export function initWinPrinter() {
});
}

const windowsPrinterStatus = {
export const windowsPrinterStatus = {
3: 'idle',
4: 'printing',
};

export async function getPrinters(): Promise<Printer[]> {
export async function getPrinters(raw = false): Promise<object[]> {
if (process.platform === 'win32') {
const winprinters = await wingetPrinters();
return winprinters.filter((p: any) => p.DeviceID).map((p: any) => ({
return raw ? winprinters : winprinters.filter((p: any) => p.DeviceID).map((p: any) => ({
printer: p.DeviceID,
description: p.Caption,
status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown',
Expand All @@ -63,13 +63,3 @@ export async function print(file: string, printer: string, startPage?: number, e
}
return unixPrint(file, printer, startPage && endPage ? ['-P', `${startPage}-${endPage}`] : []);
}

export async function getWinReceiptPrinter() {
const winprinters = await wingetPrinters();
return winprinters.filter((p: any) => p.DeviceID).filter((p: any) => p.ShareName).map((p: any) => ({
printer: `\\\\${p.SystemName}\\${p.ShareName}`,
device: p.DeviceID,
description: p.Caption,
status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown',
}));
}
74 changes: 74 additions & 0 deletions packages/server/utils/receipt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { exec } from 'child_process';
import fs from 'fs';
import path from 'path';
import { Logger, windowsPrinterStatus } from '.';

const logger = new Logger('receipt');

export async function checkReceiptPrinter(printers: object[]) {
if (process.platform === 'linux') {
const usbDevices = fs.readdirSync('/dev/usb');
for (const f of usbDevices) {
if (f.startsWith('lp')) {
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
logger.info(`USB Printer ${f} found: ${lpid}`);
logger.info(`If you want to use this printer for balloon print, please set balloon: /dev/usb/${f} in config.yaml.`);
}
}
if (!usbDevices.length) logger.info('If you want to use balloon client, please connect your receipt printer first.');
} else if (process.platform === 'win32') {
const shared = printers.filter((p: any) => p.DeviceID).filter((p: any) => p.ShareName).map((p: any) => ({
printer: `\\\\${p.SystemName}\\${p.ShareName}`,
device: p.DeviceID,
description: p.Caption,
status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown',
}));
for (const printer of shared) {
logger.info(`Receipt Shared Printer ${printer.printer}(${printer.device})) found: ${printer.description}`);
logger.info(`If you want to use this printer for balloon print, please set balloon: ${printer.printer} in config.yaml.`);
}
if (!shared.length) logger.info('If you want to use balloon client, please share your receipt printer on settings first.');
} else if (process.platform === 'darwin') {
logger.info('If you want to use balloon client, please set balloon: "{printer name}" in config.yaml.');
} else logger.info('If you want to use balloon client, please run this on Linux/Windows/MacOS');
}

export async function checkReceiptStatus(printer) {
if (process.platform !== 'linux') {
printer = { printer: printer.printer };
return;
}
const lp = printer.printer.split('/').pop();
const oldPrinter = printer;
printer = {
printer: printer.printer,
info: fs.readFileSync(`/sys/class/usbmisc/${lp}/device/ieee1284_id`, 'utf8').trim(),
};
if (!oldPrinter || oldPrinter.info === printer.info) return;
logger.info('Printer changed:', printer.printer, printer.info);
const usbDevices = fs.readdirSync('/dev/usb');
for (const f of usbDevices) {
if (f.startsWith('lp')) {
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
if (lpid === oldPrinter.info) {
logger.info('Printer found:', f, ':', lpid);
oldPrinter.printer = `/dev/usb/${f}`;
printer = oldPrinter;
break;
}
}
}
if (oldPrinter.info !== printer.info) throw Error('Printer not found, please check the printer connection.');
}

export async function receiptPrint(text, printer) {
fs.writeFileSync(path.resolve(process.cwd(), 'data', 'balloon.txt'), text);
if (process.platform === 'win32') {
exec(`COPY /B "${path.resolve(process.cwd(), 'data', 'balloon.txt')}" "${printer.printer}"`, (err, stdout, stderr) => {
if (err) logger.error(err);
if (stdout) logger.info(stdout);
if (stderr) logger.error(stderr);
});
} else if (process.platform === 'darwin') exec(`lpr -P ${printer.printer} -o raw ${path.resolve(process.cwd(), 'data', 'balloon.txt')}`);
else fs.writeFileSync(path.resolve(printer.printer), text);
}

0 comments on commit 5b358ec

Please sign in to comment.