Files
membank/auto-check/pkg/report/print.go

352 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package report
import (
"bufio"
"fmt"
"io"
"net"
"os"
"os/exec"
"runtime"
"strings"
"time"
"auto-check/pkg/config"
)
// Printed 打印状态表IP → 上次打印的状态),包级公开
// 用于"状态变化才重新打印"策略
var Printed = make(map[string]string)
// ============================
// Printer 接口 — 打印后端抽象
// ============================
// Printer 报告打印机
type Printer interface {
Print(text string) error
// PrintQR 在打印流末尾追加二维码ESC/POS GS ( k 指令)
// 后端不支持时返回 nil不打印二维码
PrintQR(url string) error
Close() error
}
// ============================
// 打印配置
// ============================
// DefaultPrintConfig 默认打印配置80mm 热敏)
func DefaultPrintConfig() config.PrintConfig {
return config.PrintConfig{
Backend: "system",
Width: 42,
}
}
// ============================
// ESC/POS 指令生成USB 与网络后端共用)
// ============================
// writeEscpos 将文本转为 ESC/POS 指令流写入 writer
func writeEscpos(w io.Writer, text string) error {
bw := bufio.NewWriter(w)
// 初始化打印机
bw.WriteString("\x1b@") // ESC @ 复位
bw.WriteString("\x1b3\x18") // 行距 24点
bw.WriteString("\x1bM\x00") // 标准字体
bw.WriteString("\x1ba\x00") // 左对齐
// 文本内容UTF-8 直发,打印机需支持)
for _, line := range strings.Split(text, "\n") {
bw.WriteString(line)
bw.WriteString("\n")
}
// 走纸 + 切纸
bw.WriteString("\n\n") // 尾部空行
bw.WriteString("\x1di") // 切纸(部分机型)
return bw.Flush()
}
// ============================
// ESC/POS 二维码GS ( k 指令)
// ============================
//
// 适配 EPSON / Star / 芯烨(Xprinter) / 佳博 等主流 ESC/POS 热敏打印机。
// 指令格式GS ( k pL pH cn fn [n...]
// cn = 49 (0x31) QR Code
// fn = 65 (A) 选择模型 / 67 (C) 模块大小 / 69 (E) 纠错 / 80 (P) 数据 / 81 (Q) 打印
//
// 参考EPSON TM-T20 QR Code 规范
// 数据长度限制Model 2 + Level M 纠错URL 场景 < 512 字节完全够用。
// escposQR 写入 ESC/POS 二维码指令流(不切纸,由调用方决定结尾)
// size: 1-16模块像素数默认 6
// ecc : 48/49/50/51 (L/M/Q/H)
func escposQR(w io.Writer, data string, size byte, ecc byte) error {
if size < 1 {
size = 1
}
if size > 16 {
size = 16
}
if ecc < 48 || ecc > 51 {
ecc = 49 // 默认 M
}
if len(data) == 0 {
return nil
}
bw := bufio.NewWriter(w)
// 1) 选择模型Model 2cn=49 fn=65 n=50
bw.WriteString("\x1d(k\x04\x00\x31\x41\x32")
// 2) 模块大小cn=49 fn=67 n=size
bw.WriteString("\x1d(k\x03\x00\x31\x43")
bw.WriteByte(size)
// 3) 纠错级别cn=49 fn=69 n=ecc
bw.WriteString("\x1d(k\x03\x00\x31\x45")
bw.WriteByte(ecc)
// 4) 存储数据GS ( k pL pH 49 80 48 <data>
// pL pH = (data_len + 3) 低/高字节
pL := byte((len(data) + 3) & 0xff)
pH := byte(((len(data) + 3) >> 8) & 0xff)
bw.WriteString("\x1d(k")
bw.WriteByte(pL)
bw.WriteByte(pH)
bw.WriteString("\x31\x50\x30")
bw.WriteString(data)
// 5) 打印cn=49 fn=81 n=48 不叠加文字)
bw.WriteString("\x1d(k\x03\x00\x31\x51\x30")
return bw.Flush()
}
// ============================
// 后端实现
// ============================
// SystemPrinter 系统打印后端lp/lpr/print
type SystemPrinter struct {
printerName string
command string
}
// NewSystemPrinter 创建系统打印机
// printerName 为空使用系统默认打印机
func NewSystemPrinter(printerName string) *SystemPrinter {
command := "lpr"
if runtime.GOOS == "windows" {
command = "print"
}
return &SystemPrinter{printerName: printerName, command: command}
}
// Print 发送文本到系统打印机
func (p *SystemPrinter) Print(text string) error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
// 临时文件方式print 命令需要文件)
tmp, err := os.CreateTemp("", "auto-check-*.txt")
if err != nil {
return err
}
name := tmp.Name()
if _, err := tmp.WriteString(text); err != nil {
tmp.Close()
return err
}
tmp.Close()
defer os.Remove(name)
if p.printerName != "" {
cmd = exec.Command("cmd", "/c", "print", "/D:"+p.printerName, name)
} else {
cmd = exec.Command("cmd", "/c", "print", name)
}
default:
args := []string{}
if p.printerName != "" {
args = append(args, "-P", p.printerName)
}
cmd = exec.Command(p.command, append(args, "-")...)
cmd.Stdin = strings.NewReader(text)
}
return cmd.Run()
}
// PrintQR 系统打印后端无二维码指令能力no-op
func (p *SystemPrinter) PrintQR(url string) error { return nil }
// Close 系统打印无需关闭
func (p *SystemPrinter) Close() error { return nil }
// ============================
// ESCPOSPrinter — 网络热敏直连9100
// ============================
// ESCPOSPrinter 网络 ESC/POS 打印机
type ESCPOSPrinter struct {
conn net.Conn
}
// NewESCPOSPrinter 连接网络 ESC/POS 打印机
// address 格式: 192.168.1.100:9100
func NewESCPOSPrinter(address string) (*ESCPOSPrinter, error) {
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
if err != nil {
return nil, fmt.Errorf("连接打印机 %s 失败: %w", address, err)
}
return &ESCPOSPrinter{conn: conn}, nil
}
// Print 发送文本(自动转 ESC/POS 指令)
func (p *ESCPOSPrinter) Print(text string) error {
return writeEscpos(p.conn, text)
}
// PrintQR 发送 ESC/POS 二维码指令到网络打印机
// 走纸居中追加一行提示,模块大小 6纠错级别 M
func (p *ESCPOSPrinter) PrintQR(url string) error {
if p.conn == nil {
return nil
}
// 居中ESC a 1后写一行"扫码查看详细报告",然后发二维码
center := "\x1ba\x01———— 扫码查看详细报告 ————\n\x1ba\x00"
if _, err := p.conn.Write([]byte(center)); err != nil {
return err
}
return escposQR(p.conn, url, 6, 49)
}
// Close 关闭连接
func (p *ESCPOSPrinter) Close() error {
if p.conn != nil {
return p.conn.Close()
}
return nil
}
// ============================
// USBPrinter — USB 热敏直连
// ============================
// USBPrinter USB ESC/POS 打印机
// Windows: \\.\usb001或打印机端口名
// Linux: /dev/usb/lp0或 lp1...
type USBPrinter struct {
dev *os.File
}
// defaultUSBDevice 默认 USB 设备路径
func defaultUSBDevice() string {
if runtime.GOOS == "windows" {
return `\\.\usb001`
}
return "/dev/usb/lp0"
}
// NewUSBPrinter 打开 USB 打印机设备
// device 为空时自动探测Linux /dev/usb/lp0Windows \\.\usb001
func NewUSBPrinter(device string) (*USBPrinter, error) {
if device == "" || device == "auto" {
device = defaultUSBDevice()
}
f, err := os.OpenFile(device, os.O_WRONLY, 0)
if err != nil {
return nil, fmt.Errorf("打开 USB 打印机 %s 失败: %w", device, err)
}
return &USBPrinter{dev: f}, nil
}
// Print 发送文本(自动转 ESC/POS 指令)
func (p *USBPrinter) Print(text string) error {
return writeEscpos(p.dev, text)
}
// PrintQR 发送 ESC/POS 二维码指令到 USB 打印机
func (p *USBPrinter) PrintQR(url string) error {
if p.dev == nil {
return nil
}
center := "\x1ba\x01———— 扫码查看详细报告 ————\n\x1ba\x00"
if _, err := p.dev.Write([]byte(center)); err != nil {
return err
}
return escposQR(p.dev, url, 6, 49)
}
// Close 关闭设备
func (p *USBPrinter) Close() error {
if p.dev != nil {
return p.dev.Close()
}
return nil
}
// ============================
// FilePrinter — 文件后端(调试/重定向)
// ============================
// FilePrinter 文件打印后端
type FilePrinter struct {
path string
f *os.File
}
// NewFilePrinter 创建文件打印机
func NewFilePrinter(path string) (*FilePrinter, error) {
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
return &FilePrinter{path: path, f: f}, nil
}
// Print 追加写入文件
func (p *FilePrinter) Print(text string) error {
_, err := p.f.WriteString(text + "\n\n")
return err
}
// PrintQR 文件后端:将"扫码查看详细报告"提示行 + URL 写入文件(便于在文件中复制链接)
func (p *FilePrinter) PrintQR(url string) error {
_, err := fmt.Fprintf(p.f, "\n———— 扫码查看详细报告 ————\nURL: %s\n\n", url)
return err
}
// Close 关闭文件
func (p *FilePrinter) Close() error {
return p.f.Close()
}
// ============================
// 工厂
// ============================
// NewPrinter 按配置创建打印机
func NewPrinter(cfg config.PrintConfig) (Printer, error) {
if !cfg.Enabled {
return nil, nil
}
switch cfg.Backend {
case "system":
return NewSystemPrinter(cfg.Device), nil
case "escpos":
return NewESCPOSPrinter(cfg.Device)
case "usb":
return NewUSBPrinter(cfg.Device)
case "file":
return NewFilePrinter(cfg.Device)
default:
return nil, fmt.Errorf("未知打印后端: %s", cfg.Backend)
}
}