如何通过 Epson TM-T88V POS printer 打印 Arecibo message
结论
不必把每个 dot 当成独立数字发送。先将 23×73 的 0/1 矩阵按比例放大,再把每行的黑白点按照 8 个 dots 打包为 1 byte,最后通过 ESC/POS 光栅位图命令发送。
使用 GS v 0(Print raster bit image)时,命令格式如下:
GS v 0 m xL xH yL yH d1...dk
参数对应的图像尺寸和数据格式如下:
- 图像宽度为
23 × scaledots - 图像高度为
73 × scaledots scale=8时,图像尺寸为184×584dotsscale=16时,图像尺寸为368×1168dots- 每个 byte 的最高位对应最左侧 dot
1表示黑点,0表示白点
需要注意,GS v 0 命令中的 xL/xH 表示每行的 byte 数,而不是 dot 数。不同 ESC/POS 图像命令对参数的定义并不完全相同,不能因为都使用了 nL + nH × 256,就把它们混用。
nL + nH × 256 是什么意思
这是将大于 255 的整数拆成两个 byte 的通用方式:
value = nL + nH × 256
nL = value % 256
nH = value // 256
例如,某条命令明确要求用两个 byte 表示水平方向的 dot 数时,可以这样计算。
放大 8 倍
width = 23 × 8 = 184 dots
nL = 184 % 256 = 184
nH = 184 // 256 = 0
需要发送:
184, 0
放大 16 倍
width = 23 × 16 = 368 dots
nL = 368 % 256 = 112
nH = 368 // 256 = 1
因为:
112 + 1 × 256 = 368
所以需要发送:
112, 1
本文使用的 GS v 0 命令有所不同,它的水平参数是每行的 byte 数:
bytes_per_row = ceil(width_dots / 8)
实际参数如下:
| 比例 | 宽度 | 高度 | 每行 bytes | xL,xH | yL,yH |
|---|---|---|---|---|---|
| 8×8 | 184 dots | 584 dots | 23 | 23,0 | 72,2 |
| 16×16 | 368 dots | 1168 dots | 46 | 46,0 | 144,4 |
其中:
584 = 72 + 2 × 256
1168 = 144 + 4 × 256
位图数据如何打包
假设放大后,某一行开头的 8 个 dots 是:
00101000
将其视为一个二进制 byte:
00101000₂ = 40
此时应向打印机发送一个值为 40 的 byte。
各位的顺序如下:
bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
左侧 右侧
对应的 Python 写法是:
byte_value |= 1 << (7 - bit_position)
将每个原始元素放大为 8×8 时:
- 水平方向将该元素重复 8 次
- 垂直方向将整行重复 8 次
放大为 16×16 时采用同样的方法。
可直接使用的 Python 示例
下面的代码使用 GS v 0,命令字节为:
1D 76 30 m xL xH yL yH
其中,m=0 表示按正常尺寸打印。图像已经由程序完成放大,因此不再使用打印机内置的倍宽或倍高模式。
# -*- coding: utf-8 -*-
import socket
def split_word(value):
"""将非负整数拆成低位 byte 和高位 byte。"""
if not 0 <= value <= 0xFFFF:
raise ValueError("value must fit in two bytes")
return value & 0xFF, (value >> 8) & 0xFF
def parse_arecibo(text):
rows = [
line.strip()
for line in text.splitlines()
if line.strip()
]
if not rows:
raise ValueError("No bitmap data found")
width = len(rows[0])
if any(len(row) != width for row in rows):
raise ValueError("All rows must have the same width")
if any(ch not in "01" for row in rows for ch in row):
raise ValueError("Bitmap may contain only '0' and '1'")
return rows
def expand_and_pack(rows, scale=8):
"""
将 0/1 矩阵放大,并按 GS v 0 所需格式打包。
每个输出 byte 包含从左到右的 8 个 dots:
bit 7 是最左侧 dot,bit 0 是最右侧 dot。
"""
if scale <= 0:
raise ValueError("scale must be greater than zero")
source_width = len(rows[0])
source_height = len(rows)
width_dots = source_width * scale
height_dots = source_height * scale
bytes_per_row = (width_dots + 7) // 8
image_data = bytearray()
for source_row in rows:
expanded_bits = []
for pixel in source_row:
bit = 1 if pixel == "1" else 0
expanded_bits.extend([bit] * scale)
packed_row = bytearray(bytes_per_row)
for x, bit in enumerate(expanded_bits):
if bit:
byte_index = x // 8
bit_index = 7 - (x % 8)
packed_row[byte_index] |= 1 << bit_index
# 垂直方向重复 scale 次
for _ in range(scale):
image_data.extend(packed_row)
return width_dots, height_dots, bytes_per_row, bytes(image_data)
def make_raster_command(rows, scale=8):
width_dots, height_dots, bytes_per_row, image_data = (
expand_and_pack(rows, scale)
)
xL, xH = split_word(bytes_per_row)
yL, yH = split_word(height_dots)
# GS v 0 m xL xH yL yH
header = bytes([
0x1D, 0x76, 0x30, 0x00,
xL, xH,
yL, yH,
])
expected_size = bytes_per_row * height_dots
if len(image_data) != expected_size:
raise RuntimeError("Unexpected raster data size")
return header + image_data
def print_arecibo(host, bitmap_text, scale=8, port=9100):
rows = parse_arecibo(bitmap_text)
if len(rows) != 73 or len(rows[0]) != 23:
raise ValueError(
f"Expected a 23x73 bitmap, got "
f"{len(rows[0])}x{len(rows)}"
)
raster = make_raster_command(rows, scale)
with socket.create_connection((host, port), timeout=10) as sock:
# ESC @:初始化打印机
sock.sendall(b"\x1b\x40")
# ESC a 1:居中,可按需要删除
sock.sendall(b"\x1b\x61\x01")
# 发送完整的 GS v 0 命令及图像数据
sock.sendall(raster)
# 恢复左对齐,并走纸
sock.sendall(b"\x1b\x61\x00")
sock.sendall(b"\n\n\n")
# GS V 0:切纸
sock.sendall(b"\x1d\x56\x00")
ARECIBO = """\
00000010101010000000000
00101000001010000000100
10001000100010010110010
10101010101010100100100
00000000000000000000000
00000000000011000000000
00000000001101000000000
00000000001101000000000
00000000010101000000000
00000000011111000000000
00000000000000000000000
11000011100011000011000
10000000000000110010000
11010001100011000011010
11111011111011111011111
"""
# 使用时必须将 ARECIBO 补全为全部 73 行:
# print_arecibo("192.168.1.15", ARECIBO, scale=8)
题目只展示了部分数据,因此示例中的 ARECIBO 并不完整。实际打印前必须补全全部 73 行,否则尺寸校验会报错。
8×8 模式的实际数据量
原始尺寸为:
23 × 73 elements
放大后:
width_dots = 23 × 8 = 184
height_dots = 73 × 8 = 584
每行的 byte 数为:
184 / 8 = 23 bytes
图像数据总长度为:
23 × 584 = 13432 bytes
命令头为:
1D 76 30 00 17 00 48 02
对应的十进制值为:
29, 118, 48, 0, 23, 0, 72, 2
其中:
0x17 0x00表示每行 23 bytes0x48 0x02表示高度为 584 dots- 命令头之后紧跟 13432 bytes 的图像数据
16×16 模式的实际数据量
放大后:
width_dots = 23 × 16 = 368
height_dots = 73 × 16 = 1168
每行的 byte 数为:
368 / 8 = 46 bytes
图像数据总长度为:
46 × 1168 = 53728 bytes
命令头为:
1D 76 30 00 2E 00 90 04
对应的十进制值为:
29, 118, 48, 0, 46, 0, 144, 4
368 dots 通常能够容纳在 TM-T88V 的打印宽度内,但仍需以当前纸宽、打印区域设置及对应固件的 ESC/POS 技术手册为准。
原程序中需要修正的问题
原发送函数使用了:
struct.pack('h', x)
h 表示一个通常占 2 bytes 的有符号短整数,并且默认采用本机字节序。发送 ESC/POS 指令时,每个指令值通常必须是一个准确的 byte。例如,27 应发送为:
1B
而不是:
1B 00
原程序还使用了:
mySocket.send(data, 1)
这里的第二个参数是 socket flags,并不表示“发送一个 byte”。同时,send() 也不能保证一次发送完整的缓冲区。
应直接使用:
sock.sendall(bytes([27, 33, 48]))
例如:
def send_instructions(sock, values):
sock.sendall(bytes(values))
发送文本时可以继续使用:
def send_text(sock, text):
sock.sendall(text.encode("utf-8"))
打印机能否直接解释 UTF-8,取决于它的字符集配置,但这不影响前面的二进制位图发送。
注意事项
- 确认当前 TM-T88V 固件支持所使用的
GS v 0命令。如果手册描述的是ESC *或GS ( L,参数含义和数据排列方式会有所不同。 - 使用
sendall()发送完整的命令和数据,避免 TCP 只发送部分缓冲区。 - 图像数据不能经过 UTF-8 编码,也不能转换为十进制数字字符串,必须保持为原始
bytes。 - 如果位图每行不足 8 dots,应在右侧补白点。本例放大后的宽度 184 和 368 都能被 8 整除,不需要填充。
- 如果打印结果出现黑白反转,可以将打包条件从
if bit:改为对白点置位,也可以先确认所用命令是否规定1代表打印 dot。 - 如果一次发送较大的 16×16 图像时发生中断,应先检查打印机接收缓冲区、接口超时设置和命令允许的最大光栅高度。除非能在各段之间正确处理走纸和连续打印位置,否则不要擅自拆分图像。
备注:内容仅供参考。
