Skip to content

Commit 5b358ec

Browse files
committed
support multi platform receipt print
1 parent 91e1db2 commit 5b358ec

File tree

7 files changed

+94
-83
lines changed

7 files changed

+94
-83
lines changed

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
A tool for CN XCPC contests
44

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

910
TODO Features:
1011

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

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

7776
#### Monitor
7877
服务支持监控选手机情况和监控服务器桌面,如您需要选手机监控,可通过设置 Systemd 定时执行任务等多种方式定时执行 `monitor` 命令,如需监控服务器桌面,请在选手机上提前运行 `vlc-camera``vlc-desktop` 服务, CAICPC 镜像已经内置了这三两个服务,您只需在选手机上运行即可,如您为自己的镜像,可从 `https://github.com/hydro-dev/xcpc-tools/blob/main/scripts/monitor` 下载 `monitor` 服务。

packages/server/client/balloon.ts

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
/* eslint-disable no-await-in-loop */
2-
import { exec } from 'child_process';
3-
import path from 'path';
42
import EscPosEncoder from '@freedom_sky/esc-pos-encoder';
53
import superagent from 'superagent';
64
import { config } from '../config';
75
import {
8-
convertToChinese,
9-
fs, Logger, sleep,
6+
checkReceiptStatus, convertToChinese, Logger, receiptPrint, sleep,
107
} from '../utils';
118

129
const encoder = new EscPosEncoder();
@@ -73,31 +70,6 @@ const logger = new Logger('balloon');
7370
let timer = null;
7471
let printer = null;
7572

76-
async function getReceiptStatus(receipt) {
77-
if (process.platform === 'win32') printer = { printer: receipt };
78-
const lp = receipt.split('/').pop();
79-
const oldPrinter = printer;
80-
printer = {
81-
printer: receipt,
82-
info: fs.readFileSync(`/sys/class/usbmisc/${lp}/device/ieee1284_id`, 'utf8').trim(),
83-
};
84-
if (!oldPrinter || oldPrinter.info === printer.info) return;
85-
logger.info('Printer changed:', printer.printer, printer.info);
86-
const usbDevices = fs.readdirSync('/dev/usb');
87-
for (const f of usbDevices) {
88-
if (f.startsWith('lp')) {
89-
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
90-
if (lpid === oldPrinter.info) {
91-
logger.info('Printer found:', f, ':', lpid);
92-
oldPrinter.printer = `/dev/usb/${f}`;
93-
printer = oldPrinter;
94-
break;
95-
}
96-
}
97-
}
98-
if (oldPrinter.info !== printer.info) throw Error('Printer not found, please check the printer connection.');
99-
}
100-
10173
async function printBalloon(doc, lang) {
10274
const bReceipt = receiptText(
10375
doc.balloonid,
@@ -109,17 +81,8 @@ async function printBalloon(doc, lang) {
10981
doc.total ? Object.keys(doc.total).map((k) => `- ${k}: ${doc.total[k].color}`).join('\n') : 'N/A',
11082
lang,
11183
);
112-
if (printer) {
113-
await getReceiptStatus(printer.printer);
114-
if (process.platform === 'win32') {
115-
fs.writeFileSync(path.resolve(process.cwd(), 'data', 'balloon.txt'), bReceipt);
116-
exec(`COPY /B "${path.resolve(process.cwd(), 'data', 'balloon.txt')}" "${printer.printer}"`, (err, stdout, stderr) => {
117-
if (err) logger.error(err);
118-
if (stdout) logger.info(stdout);
119-
if (stderr) logger.error(stderr);
120-
});
121-
} else fs.writeFileSync(path.resolve(printer.printer), bReceipt);
122-
}
84+
printer = await checkReceiptStatus(printer);
85+
await receiptPrint(printer, bReceipt);
12386
}
12487

12588
async function fetchTask(c) {
@@ -146,7 +109,7 @@ async function fetchTask(c) {
146109
}
147110

148111
export async function apply() {
149-
await getReceiptStatus(config.balloon);
112+
printer = config.balloon;
150113
if (config.token && config.server && config.balloon) await fetchTask(config);
151114
else logger.error('Config not found, please check the config.yaml');
152115
}

packages/server/config.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import path from 'path';
22
import Schema from 'schemastery';
33
import { version } from './package.json';
44
import {
5-
fs, getPrinters, getWinReceiptPrinter,
6-
Logger, yaml,
5+
checkReceiptPrinter,
6+
fs, getPrinters, Logger, yaml,
77
} from './utils';
88

99
const logger = new Logger('init');
@@ -32,32 +32,15 @@ password:
3232
`;
3333
let printers = [];
3434
if (isClient) {
35-
printers = await getPrinters().then((r) => r.map((p) => p.printer)).catch(() => []);
35+
printers = await getPrinters(true).catch(() => []);
3636
logger.info(printers.length, 'printers found:', printers.join(', '));
37-
if (process.platform === 'linux') {
38-
const usbDevices = fs.readdirSync('/dev/usb');
39-
for (const f of usbDevices) {
40-
if (f.startsWith('lp')) {
41-
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
42-
logger.info(`USB Printer ${f} found: ${lpid}`);
43-
logger.info(`If you want to use this printer for balloon print, please set balloon: /dev/usb/${f} in config.yaml.`);
44-
}
45-
}
46-
if (!usbDevices.length) logger.info('If you want to use balloon client, please connect your receipt printer first.');
47-
} else if (process.platform === 'win32') {
48-
const printerList = await getWinReceiptPrinter();
49-
for (const printer of printerList) {
50-
logger.info(`Receipt Printer ${printer.printer}(${printer.device})) found: ${printer.description}`);
51-
logger.info(`If you want to use this printer for balloon print, please set balloon: ${printer.printer} in config.yaml.`);
52-
}
53-
if (!printers.length) logger.info('If you want to use balloon client, please share your receipt printer on settings first.');
54-
} else logger.info('If you want to use balloon client, please run this on Linux/Windows.');
37+
await checkReceiptPrinter(printers);
5538
}
5639
const clientConfigDefault = yaml.dump({
5740
server: '',
5841
balloon: '',
5942
balloonLang: 'zh',
60-
printers,
43+
printers: printers.map((p) => p.printer),
6144
token: '',
6245
});
6346
fs.writeFileSync(configPath, isClient ? clientConfigDefault : serverConfigDefault);
@@ -90,6 +73,7 @@ const serverSchema = Schema.intersect([
9073
token: Schema.string(),
9174
username: Schema.string(),
9275
password: Schema.string(),
76+
freezeEncourage: Schema.number().default(0),
9377
}).description('Fetcher Config'),
9478
Schema.object({
9579
type: Schema.const('server').required(),

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hydrooj/xcpc-tools",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "A tools for XCPC contests",
55
"main": "index.ts",
66
"repository": "https://github.com/Hydro-dev/xcpc-tools",

packages/server/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,4 @@ export function decodeBinary(file: string) {
113113
export * from './commandRunner';
114114
export * from './printers';
115115
export * from './color';
116+
export * from './receipt';

packages/server/utils/printers.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ export function initWinPrinter() {
3636
});
3737
}
3838

39-
const windowsPrinterStatus = {
39+
export const windowsPrinterStatus = {
4040
3: 'idle',
4141
4: 'printing',
4242
};
4343

44-
export async function getPrinters(): Promise<Printer[]> {
44+
export async function getPrinters(raw = false): Promise<object[]> {
4545
if (process.platform === 'win32') {
4646
const winprinters = await wingetPrinters();
47-
return winprinters.filter((p: any) => p.DeviceID).map((p: any) => ({
47+
return raw ? winprinters : winprinters.filter((p: any) => p.DeviceID).map((p: any) => ({
4848
printer: p.DeviceID,
4949
description: p.Caption,
5050
status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown',
@@ -63,13 +63,3 @@ export async function print(file: string, printer: string, startPage?: number, e
6363
}
6464
return unixPrint(file, printer, startPage && endPage ? ['-P', `${startPage}-${endPage}`] : []);
6565
}
66-
67-
export async function getWinReceiptPrinter() {
68-
const winprinters = await wingetPrinters();
69-
return winprinters.filter((p: any) => p.DeviceID).filter((p: any) => p.ShareName).map((p: any) => ({
70-
printer: `\\\\${p.SystemName}\\${p.ShareName}`,
71-
device: p.DeviceID,
72-
description: p.Caption,
73-
status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown',
74-
}));
75-
}

packages/server/utils/receipt.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { exec } from 'child_process';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { Logger, windowsPrinterStatus } from '.';
5+
6+
const logger = new Logger('receipt');
7+
8+
export async function checkReceiptPrinter(printers: object[]) {
9+
if (process.platform === 'linux') {
10+
const usbDevices = fs.readdirSync('/dev/usb');
11+
for (const f of usbDevices) {
12+
if (f.startsWith('lp')) {
13+
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
14+
logger.info(`USB Printer ${f} found: ${lpid}`);
15+
logger.info(`If you want to use this printer for balloon print, please set balloon: /dev/usb/${f} in config.yaml.`);
16+
}
17+
}
18+
if (!usbDevices.length) logger.info('If you want to use balloon client, please connect your receipt printer first.');
19+
} else if (process.platform === 'win32') {
20+
const shared = printers.filter((p: any) => p.DeviceID).filter((p: any) => p.ShareName).map((p: any) => ({
21+
printer: `\\\\${p.SystemName}\\${p.ShareName}`,
22+
device: p.DeviceID,
23+
description: p.Caption,
24+
status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown',
25+
}));
26+
for (const printer of shared) {
27+
logger.info(`Receipt Shared Printer ${printer.printer}(${printer.device})) found: ${printer.description}`);
28+
logger.info(`If you want to use this printer for balloon print, please set balloon: ${printer.printer} in config.yaml.`);
29+
}
30+
if (!shared.length) logger.info('If you want to use balloon client, please share your receipt printer on settings first.');
31+
} else if (process.platform === 'darwin') {
32+
logger.info('If you want to use balloon client, please set balloon: "{printer name}" in config.yaml.');
33+
} else logger.info('If you want to use balloon client, please run this on Linux/Windows/MacOS');
34+
}
35+
36+
export async function checkReceiptStatus(printer) {
37+
if (process.platform !== 'linux') {
38+
printer = { printer: printer.printer };
39+
return;
40+
}
41+
const lp = printer.printer.split('/').pop();
42+
const oldPrinter = printer;
43+
printer = {
44+
printer: printer.printer,
45+
info: fs.readFileSync(`/sys/class/usbmisc/${lp}/device/ieee1284_id`, 'utf8').trim(),
46+
};
47+
if (!oldPrinter || oldPrinter.info === printer.info) return;
48+
logger.info('Printer changed:', printer.printer, printer.info);
49+
const usbDevices = fs.readdirSync('/dev/usb');
50+
for (const f of usbDevices) {
51+
if (f.startsWith('lp')) {
52+
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
53+
if (lpid === oldPrinter.info) {
54+
logger.info('Printer found:', f, ':', lpid);
55+
oldPrinter.printer = `/dev/usb/${f}`;
56+
printer = oldPrinter;
57+
break;
58+
}
59+
}
60+
}
61+
if (oldPrinter.info !== printer.info) throw Error('Printer not found, please check the printer connection.');
62+
}
63+
64+
export async function receiptPrint(text, printer) {
65+
fs.writeFileSync(path.resolve(process.cwd(), 'data', 'balloon.txt'), text);
66+
if (process.platform === 'win32') {
67+
exec(`COPY /B "${path.resolve(process.cwd(), 'data', 'balloon.txt')}" "${printer.printer}"`, (err, stdout, stderr) => {
68+
if (err) logger.error(err);
69+
if (stdout) logger.info(stdout);
70+
if (stderr) logger.error(stderr);
71+
});
72+
} else if (process.platform === 'darwin') exec(`lpr -P ${printer.printer} -o raw ${path.resolve(process.cwd(), 'data', 'balloon.txt')}`);
73+
else fs.writeFileSync(path.resolve(printer.printer), text);
74+
}

0 commit comments

Comments
 (0)