diff --git a/convert_img.py b/convert_img.py index 564a94c..54ca50a 100644 --- a/convert_img.py +++ b/convert_img.py @@ -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 diff --git a/main.py b/main.py index 65a6e7d..589d773 100644 --- a/main.py +++ b/main.py @@ -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 diff --git a/websocket_server/server.py b/websocket_server/server.py index dc92e56..70ccae4 100644 --- a/websocket_server/server.py +++ b/websocket_server/server.py @@ -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: