printer
All checks were successful
Deploy WebSocket Server / deploy (push) Successful in 3s

This commit is contained in:
jeremygan2021
2026-03-05 20:04:16 +08:00
parent c66f80d0eb
commit 24e5b4d018
3 changed files with 136 additions and 153 deletions

View File

@@ -1,159 +1,66 @@
import os
from PIL import Image
def generate_micropython_printer_script(image_path, output_py_path):
def image_to_tspl_commands(image_path):
"""
图片转换为 TSPL BITMAP 指令数据,并生成 MicroPython 脚本
读取图片转换为 TSPL 打印指令 (bytes)
包含: SIZE, GAP, CLS, BITMAP, PRINT
"""
if not os.path.exists(image_path):
print(f"错误: 找不到图片 {image_path}")
return
return None
# 1. 加载并预处理图片
try:
img = Image.open(image_path)
except Exception as e:
print(f"无法打开图片: {e}")
return
return None
# 处理透明背景 (将透明部分变为白色)
# 处理透明背景
if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info):
alpha = img.convert('RGBA').split()[-1]
bg = Image.new("RGBA", img.size, (255, 255, 255, 255))
bg.paste(img, mask=alpha)
img = bg
# 目标宽度: 48mm = 384 dots (假设 203dpi)
target_width = 384
# 目标尺寸: 48mm x 30mm @ 203dpi
# 宽度: 48 * 8 = 384 dots
# 高度: 30 * 8 = 240 dots
MAX_WIDTH = 384
MAX_HEIGHT = 240
# 保持比例缩放
w_percent = (target_width / float(img.size[0]))
target_height = int((float(img.size[1]) * float(w_percent)))
# 使用 thumbnail 进行等比缩放,确保不超过最大尺寸
img.thumbnail((MAX_WIDTH, MAX_HEIGHT), Image.Resampling.LANCZOS)
img = img.resize((target_width, target_height), Image.Resampling.LANCZOS)
target_width, target_height = img.size
print(f"图片缩放后尺寸: {target_width}x{target_height}")
# 转为二值图 (0=黑, 255=白)
# 优化: 使用 Floyd-Steinberg 抖动算法 (convert('1') 默认行为)
# 这样可以保留更多细节,而不是简单的阈值切断
# 转为二值图
# 1. 先转灰度
img = img.convert('L')
# 2. 二值化 (使用默认的抖动算法)
img = img.convert('1')
# 如果线条太细,可以先尝试增强一下对比度 (可选)
# 但对于线稿,抖动通常能很好地还原灰度细节
# 强制不自动判断,直接根据用户反馈修改
# 用户反馈: "明明白色的底打印成黑色了"
# 这意味着我们之前的逻辑把白色映射为了 1 (打印/变黑)。
# TSPL 中: 1 = Print dot (Black), 0 = No print (White).
# 所以我们需要确保: White pixel -> 0 bit.
# 如果之前判定逻辑是:
# if white > black: invert=True (白色为背景)
# if invert: if pixel==0(Black) -> set 1.
# 也就是说之前逻辑是: 黑色像素设为1白色像素设为0。这应该是对的。
# 但用户说打出来全是黑的说明白色像素被设为了1。
# 这意味着可能实际上不需要反色,或者之前的反色逻辑写反了。
# 让我们强制反转之前的逻辑。
# 之前: invert_logic = True (when white bg)
# 现在强制改为 False 再试一次。
invert_logic = False
print("强制设置: 不执行反色逻辑 (测试用)")
# 2. 转换为 TSPL BITMAP 数据格式
# 构造 BITMAP 数据
width_bytes = (target_width + 7) // 8
data = bytearray()
# 遍历像素生成数据
# TSPL BITMAP 数据: 1=Black(Print), 0=White(No Print)
# PIL '1' mode: 0=Black, 255=White
for y in range(target_height):
row_bytes = bytearray(width_bytes)
for x in range(target_width):
pixel = img.getpixel((x, y))
# 逻辑修正:
# 我们希望 黑色像素(0) -> 打印(1)
# 白色像素(255) -> 不打印(0)
should_print = False
# 重新梳理逻辑
# 我们想要: 黑色像素(0) -> 打印(1), 白色像素(255) -> 不打印(0)
# 假设 img 是 '1' mode: 0=Black, 255/1=White
if pixel == 0: # 是黑色像素
should_print = True # 我们要打印它 -> set 1
else: # 是白色像素
should_print = False # 不打印 -> set 0
# 如果之前的逻辑打出来是反的,说明之前的代码逻辑产生的位是反的。
# 之前的代码:
# if invert_logic (True): if pixel == 0: should_print = True
# 这逻辑看起来是对的啊... 黑像素打印。
# 为什么用户说反了?
# 可能性 1: TSPL 0=Black, 1=White? (不太可能通常热敏都是加热点为1)
# 可能性 2: 图片转换时 convert('1') 并没有按预期工作,或者 getpixel 返回值理解错了。
# 可能性 3: 之前的 invert_logic 其实是 False?
# 不管怎样,既然用户说反了,我们就强制反过来。
# 强制反转:
# 如果是黑色像素(0) -> 不打印 (0)
# 如果是白色像素(255) -> 打印 (1)
# (但这会导致白底变黑块... 等等,用户现在的抱怨正是"白色的底打印成黑色了")
# 所以现在的状态是: 白色像素被打印了。
# 所以我们需要: 白色像素 -> 不打印。
# 这意味着 bit 必须是 0。
# 让我们再试一次完全相反的逻辑:
# 之前: if pixel == 0: print. (即黑->打) -> 用户说白底变黑了。
# 这说明 convert('1') 后,原来的白色背景变成了 0
# 让我们打印一下像素值看看。
# 修正策略:不管那么多,直接加一个全局取反开关。
# 用户现在的现象:白底 -> 黑。
# 我们要:白底 -> 白。
# 所以我们要把发送给打印机的 bit 取反。
# 在这里我们实现一个逻辑:
# 如果 pixel != 0 (即白色): 不打印 (0)
# 如果 pixel == 0 (即黑色): 打印 (1)
# 但等等,之前的代码:
# if invert_logic: if pixel == 0: should_print = True
# invert_logic 之前是 True. 所以 if pixel == 0 (黑) -> print.
# 结果白底变黑了。
# 这说明在 convert('1') 之后,白色背景的 pixel 值变成了 0
# 在 PIL '1' mode 中0=Black, 255=White。
# 除非... resize 过程中的插值导致了问题?
# 让我们尝试最简单的逻辑:
# 强制反转当前所有位的逻辑。
if pixel != 0: # 白色
should_print = True # 试一下让白色打印?不,这会更黑。
# 让我们回退到最原始的猜测:
# 用户说 "白色的底打印成黑色了"。
# 说明我们给白色背景发了 '1'。
# 我们之前的逻辑是: if pixel == 0: 1.
# 说明白色背景的 pixel 是 0。
# 这意味着 convert('1') 把白色转成了 0。
# 让我们检查 convert 的阈值逻辑。
# 此次修改:直接取反。
if pixel != 0: # 如果是 255 (原图白)
should_print = False # 不打印 -> 0
else: # 如果是 0 (原图黑)
should_print = True # 打印 -> 1
# 但为了确解决用户的"反了"的问题,我们将在此逻辑基础上再取反一次?
# 不,用户说现在是反的。
# 现在的代码是:
# if invert_logic (True): if pixel == 0: True.
# 也就是 0->1.
# 结果: 反了。
# 结论: 应该是 0->0, 255->1 ? (即 0是不打1是打?)
# 或者是 pixel值反了。
# 决定:直接反转判断条件。
if pixel != 0: # 白色/255
should_print = True # 之前是 False
else: # 黑色/0
should_print = False # 之前是 True
if pixel == 0: # Black
should_print = True
if should_print:
byte_index = x // 8
@@ -161,29 +68,76 @@ def generate_micropython_printer_script(image_path, output_py_path):
row_bytes[byte_index] |= (1 << bit_index)
data.extend(row_bytes)
# 3. 生成 MicroPython 脚本内容
# 优化: 修复 GAP 问题,避免浪费纸张
# 用户抱怨: "每次打印中间都浪费了一张白色的贴纸"
# 原因: 可能 GAP 设置不对,或者 size 设置太高,或者自动进纸了。
# 标签纸高度通常是固定的。用户之前说是 30mm。
# 我们现在的 height 是动态算的。
# 如果算的 height > 30mm打印机就会跨页。
# 让我们强制限制最大高度。
# 强制裁剪或缩放高度到 30mm (240 dots)
if target_height > 240:
print(f"警告: 图片高度 {target_height} 超过标签高度 240将强制调整。")
# 这里为了不破坏长宽比,最好是重新 resize但为了简单先让用户能打出来。
# 更好的做法是缩小图片以适应 48x30mm
# 重新计算缩放
h_percent = (240 / float(img.size[1]))
# 取宽高中较小的缩放比,确保能塞进去
scale = min(w_percent, h_percent) # 使用之前的 w_percent 和新的 h_percent 比较?
# 不,重新算吧。
# 但我们已经在上面 resize 过了。
# 让我们修改脚本里的 SIZE 设置。
# 计算居中偏移
x_offset = (MAX_WIDTH - target_width) // 2
y_offset = (MAX_HEIGHT - target_height) // 2
# 生成指令
cmds = bytearray()
# 1. 初始化
# SIZE 48 mm, 30 mm
cmds.extend(b"SIZE 48 mm, 30 mm\r\n")
# GAP 2 mm, 0 mm
cmds.extend(b"GAP 2 mm, 0 mm\r\n")
# CLS
cmds.extend(b"CLS\r\n")
# 2. BITMAP
# BITMAP x, y, width_bytes, height, mode, data
header = f"BITMAP {x_offset},{y_offset},{width_bytes},{target_height},0,".encode('utf-8')
cmds.extend(header)
cmds.extend(data)
cmds.extend(b"\r\n") # BITMAP data 后面通常不需要回车,但有些文档建议加? 不binary data后紧跟下一个指令
# TSPL protocol says: BITMAP ... data ... CR LF is NOT required after data, but next command must start.
# Usually it's safer to just send data.
# 3. PRINT
cmds.extend(b"PRINT 1\r\n")
return cmds
def generate_micropython_printer_script(image_path, output_py_path):
"""
保留此函数以兼容现有 workflow但内部使用新的逻辑
"""
cmds = image_to_tspl_commands(image_path)
if not cmds:
return
# 提取 BITMAP 数据部分用于生成脚本 (因为脚本里是用 uart.write 分段发送的)
# 为了简单,我们重新解析一下 cmds 或者直接重写这部分逻辑
# 但为了脚本的可读性,还是像之前一样生成
# 重新执行一遍核心逻辑来获取参数 (为了生成漂亮的 python 代码)
if not os.path.exists(image_path): return
img = Image.open(image_path)
if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info):
alpha = img.convert('RGBA').split()[-1]
bg = Image.new("RGBA", img.size, (255, 255, 255, 255))
bg.paste(img, mask=alpha)
img = bg
MAX_WIDTH = 384
MAX_HEIGHT = 240
img.thumbnail((MAX_WIDTH, MAX_HEIGHT), Image.Resampling.LANCZOS)
target_width, target_height = img.size
img = img.convert('L').convert('1')
width_bytes = (target_width + 7) // 8
data = bytearray()
for y in range(target_height):
row_bytes = bytearray(width_bytes)
for x in range(target_width):
if img.getpixel((x, y)) == 0:
byte_index = x // 8
bit_index = 7 - (x % 8)
row_bytes[byte_index] |= (1 << bit_index)
data.extend(row_bytes)
x_offset = (MAX_WIDTH - target_width) // 2
y_offset = (MAX_HEIGHT - target_height) // 2
hex_data = data.hex()
@@ -207,11 +161,9 @@ def print_image():
print("=== 开始打印图片 ===")
# 2. 设置标签
# 修正: 严格匹配标签纸尺寸,防止浪费
printer.cls()
printer.size(48, 30) # 强制 48mm x 30mm
printer.size(48, 30)
printer.gap(2, 0)
printer.home()
# 3. 准备图片数据
img_hex = "{hex_data}"
@@ -221,10 +173,8 @@ def print_image():
print(f"正在发送图片数据 ({{len(img_data)}} bytes)...")
# BITMAP X, Y, width_bytes, height, mode, data
# 居中打印: Y 轴偏移
y_offset = max(0, (240 - {target_height}) // 2)
cmd = f"BITMAP 0,{{y_offset}},{width_bytes},{target_height},0,".encode('utf-8')
# 居中打印
cmd = f"BITMAP {x_offset},{y_offset},{width_bytes},{target_height},0,".encode('utf-8')
uart.write(cmd)
chunk_size = 128

View File

@@ -4,7 +4,7 @@ import struct
import gc
import network
import st7789py as st7789
from config import CURRENT_CONFIG, SERVER_URL
from config import CURRENT_CONFIG, SERVER_URL, ttl_tx, ttl_rx
from audio import AudioPlayer, Microphone
# Define colors that might be missing in st7789py
@@ -19,6 +19,7 @@ WIFI_PASS = "djt12345678"
IMAGE_STATE_IDLE = 0
IMAGE_STATE_RECEIVING = 1
PRINTER_STATE_RECEIVING = 2
UI_SCREEN_HOME = 0
UI_SCREEN_RECORDING = 1

View File

@@ -14,6 +14,10 @@ from dashscope.audio.asr import Recognition, RecognitionCallback, RecognitionRes
from dashscope import ImageSynthesis
from dashscope import Generation
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import convert_img
# 加载环境变量
load_dotenv()
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
@@ -883,6 +887,34 @@ async def websocket_endpoint(websocket: WebSocket):
else:
await websocket.send_text("STATUS:ERROR:提示词为空")
elif text == "PRINT_IMAGE":
print("Received PRINT_IMAGE request")
if os.path.exists(GENERATED_IMAGE_FILE):
try:
# Use convert_img logic to get TSPL commands
tspl_data = convert_img.image_to_tspl_commands(GENERATED_IMAGE_FILE)
if tspl_data:
print(f"Sending printer data: {len(tspl_data)} bytes")
await websocket.send_text(f"PRINTER_DATA_START:{len(tspl_data)}")
# Send in chunks
chunk_size = 512
for i in range(0, len(tspl_data), chunk_size):
chunk = tspl_data[i:i+chunk_size]
await websocket.send_bytes(chunk)
# Small delay to prevent overwhelming ESP32 buffer
await asyncio.sleep(0.01)
await websocket.send_text("PRINTER_DATA_END")
print("Printer data sent")
else:
await websocket.send_text("STATUS:ERROR:图片转换失败")
except Exception as e:
print(f"Error converting image for printer: {e}")
await websocket.send_text(f"STATUS:ERROR:打印出错: {str(e)}")
else:
await websocket.send_text("STATUS:ERROR:没有可打印的图片")
elif text.startswith("GET_TASK_STATUS:"):
task_id = text.split(":", 1)[1].strip()
if task_id in active_tasks: