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

This commit is contained in:
jeremygan2021
2026-03-05 19:59:56 +08:00
parent efbe08f2cd
commit c66f80d0eb
6 changed files with 537 additions and 30 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -4,41 +4,49 @@ from micropython import const
from machine import Pin, SPI
from time import sleep_ms
# ----------------------------打印机引脚配置-------------------------------------------------
# TTL 引脚配置
ttl_tx = Pin(2) # TTL TX 连接到引脚22
ttl_rx = Pin(1) # TTL RX 连接到引脚23
ttl_Dtr = Pin(6) # TTL TX 连接到引脚22
# ----------------------------epaper配置-------------------------------------------------
# SPI引脚配置
sck = Pin(47) # SCK pin47
miso = Pin(46) # MISO pin46
mosi = Pin(21) # SDI/MOSI pin21
# sck = Pin(47) # SCK pin47
# miso = Pin(46) # MISO pin46
# mosi = Pin(21) # SDI/MOSI pin21
# 控制引脚配置
dc = Pin(40) # D/C pin40
cs = Pin(45) # CS pin45
rst = Pin(41) # RES pin41
busy = Pin(42) # BUSY pin42
# # 控制引脚配置
# dc = Pin(40) # D/C pin40
# cs = Pin(45) # CS pin45
# rst = Pin(41) # RES pin41
# busy = Pin(42) # BUSY pin42
# 按钮引脚配置
btn1 = Pin(46, Pin.IN, Pin.PULL_UP) # 按钮1连接到引脚46
btn2 = Pin(20, Pin.IN, Pin.PULL_UP) # 按钮2连接到引脚20
btn3 = Pin(12, Pin.IN, Pin.PULL_UP) # 按钮3连接到引脚12
btn4 = Pin(11, Pin.IN, Pin.PULL_UP) # 按钮4连接到引脚11
# # 按钮引脚配置
# btn1 = Pin(46, Pin.IN, Pin.PULL_UP) # 按钮1连接到引脚46
# btn2 = Pin(20, Pin.IN, Pin.PULL_UP) # 按钮2连接到引脚20
# btn3 = Pin(12, Pin.IN, Pin.PULL_UP) # 按钮3连接到引脚12
# btn4 = Pin(11, Pin.IN, Pin.PULL_UP) # 按钮4连接到引脚11
# 蜂鸣器引脚配置
buzzer_pin = 14 # 蜂鸣器连接到引脚14
# # 蜂鸣器引脚配置
# buzzer_pin = 14 # 蜂鸣器连接到引脚14
# epaper屏幕尺寸
WIDTH = 400
HEIGHT = 300
# # epaper屏幕尺寸
# WIDTH = 400
# HEIGHT = 300
# 初始化 SPI2HSPI/VSPI 视固件而定)
spi = SPI(2, baudrate=2_000_000, polarity=0, phase=0,
sck=sck, miso=miso, mosi=mosi)
# # 初始化 SPI2HSPI/VSPI 视固件而定)
# spi = SPI(2, baudrate=2_000_000, polarity=0, phase=0,
# sck=sck, miso=miso, mosi=mosi)
# 如果你板子上真有单独的 EPD 电源控制 FET就按实际 IO 改;
# 若只是直接 3.3V 供电,可以把下面这一段去掉。
epd_power = Pin(2, Pin.OUT)
epd_power.on()
sleep_ms(10)
# # 如果你板子上真有单独的 EPD 电源控制 FET就按实际 IO 改;
# # 若只是直接 3.3V 供电,可以把下面这一段去掉。
# epd_power = Pin(2, Pin.OUT)
# epd_power.on()
# sleep_ms(10)
# ----------------------------epaper配置-------------------------------------------------
@@ -122,3 +130,4 @@ SERVER_IP = "118.196.74.38"
SERVER_PORT = 8811
SERVER_PATH = "/ws/audio"
SERVER_URL = f"ws://{SERVER_IP}:{SERVER_PORT}{SERVER_PATH}"

257
convert_img.py Normal file
View File

@@ -0,0 +1,257 @@
import os
from PIL import Image
def generate_micropython_printer_script(image_path, output_py_path):
"""
将图片转换为 TSPL BITMAP 指令数据,并生成 MicroPython 脚本
"""
if not os.path.exists(image_path):
print(f"错误: 找不到图片 {image_path}")
return
# 1. 加载并预处理图片
try:
img = Image.open(image_path)
except Exception as e:
print(f"无法打开图片: {e}")
return
# 处理透明背景 (将透明部分变为白色)
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
# 保持比例缩放
w_percent = (target_width / float(img.size[0]))
target_height = int((float(img.size[1]) * float(w_percent)))
img = img.resize((target_width, target_height), Image.Resampling.LANCZOS)
# 转为二值图 (0=黑, 255=白)
# 优化: 使用 Floyd-Steinberg 抖动算法 (convert('1') 默认行为)
# 这样可以保留更多细节,而不是简单的阈值切断
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 数据格式
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):
pixel = img.getpixel((x, y))
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 should_print:
byte_index = x // 8
bit_index = 7 - (x % 8)
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 设置。
hex_data = data.hex()
script_content = f'''from machine import UART
import time
from config import ttl_tx, ttl_rx
from printer_driver import TsplPrinter
import ubinascii
# ==============================================================================
# 图片打印脚本 (自动生成)
# ==============================================================================
# 图片来源: {image_path}
# ==============================================================================
# 1. 初始化
uart = UART(1, baudrate=115200, tx=ttl_tx, rx=ttl_rx)
printer = TsplPrinter(uart)
def print_image():
print("=== 开始打印图片 ===")
# 2. 设置标签
# 修正: 严格匹配标签纸尺寸,防止浪费
printer.cls()
printer.size(48, 30) # 强制 48mm x 30mm
printer.gap(2, 0)
printer.home()
# 3. 准备图片数据
img_hex = "{hex_data}"
img_data = ubinascii.unhexlify(img_hex)
# 4. 发送 BITMAP 指令
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')
uart.write(cmd)
chunk_size = 128
for i in range(0, len(img_data), chunk_size):
uart.write(img_data[i : i + chunk_size])
time.sleep(0.005)
uart.write(b'\\r\\n')
# 5. 打印
printer.print_out(1)
print("打印完成")
if __name__ == "__main__":
try:
print_image()
except Exception as e:
print(f"错误: {{e}}")
'''
with open(output_py_path, 'w', encoding='utf-8') as f:
f.write(script_content)
print(f"成功生成 MicroPython 脚本: {output_py_path}")
if __name__ == "__main__":
generate_micropython_printer_script(
"/Users/jeremygan/Desktop/python_dev/V2_micropython/test_image.png",
"/Users/jeremygan/Desktop/python_dev/V2_micropython/printer_image_print.py"
)

185
printer_driver.py Normal file
View File

@@ -0,0 +1,185 @@
import time
class TsplPrinter:
"""
基于 TSPL (TSC Printer Language) 的标签打印机驱动
适用于 HPRT (汉印), TSC, 佳博等支持 TSPL 指令集的打印机
参考 Android SDK: Serialport_Factory
"""
def __init__(self, uart, encoding='gbk'):
"""
:param uart: machine.UART 对象
:param encoding: 文本编码,默认 'gbk' (国产打印机通用)
"""
self.uart = uart
self.encoding = encoding
def send_raw(self, data):
"""发送原始数据 (bytes 或 str)"""
if isinstance(data, str):
try:
data = data.encode(self.encoding)
except Exception:
# 如果无法用指定编码,尝试 utf-8 或直接发送 ascii
data = data.encode('utf-8')
self.uart.write(data)
def send_command(self, cmd):
"""发送 TSPL 指令 (自动添加换行)"""
self.send_raw(cmd + "\r\n")
# =================================================================
# 基础设置指令
# =================================================================
def size(self, width_mm, height_mm):
"""
设置标签尺寸
:param width_mm: 宽度 (mm)
:param height_mm: 高度 (mm)
"""
# TSPL 标准: SIZE m, n (单位 mm)
# 如果打印机只支持 dots可能需要修改为 "SIZE 384 dot, 240 dot"
# SDK 注释: 384*0.125=48mm
self.send_command(f"SIZE {width_mm} mm, {height_mm} mm")
def gap(self, gap_mm=2, offset_mm=0):
"""
设置标签间隙 (定位用)
:param gap_mm: 间隙高度 (mm),通常为 2mm
:param offset_mm: 偏移量 (mm)
"""
self.send_command(f"GAP {gap_mm} mm, {offset_mm} mm")
def cls(self):
"""清除图像缓冲区 (每次打印新标签前调用)"""
self.send_command("CLS")
def feed(self, len_mm=None):
"""进纸"""
if len_mm:
self.send_command(f"FEED {len_mm} mm")
else:
self.send_command("FORMFEED") # 进纸到下一张标签开头
def home(self):
"""寻找标签原点 (自动测纸后定位)"""
self.send_command("HOME")
# =================================================================
# 绘图与文本指令
# =================================================================
def text(self, x, y, content, font="TSS24.BF2", rotation=0, x_mul=1, y_mul=1):
"""
打印文本
:param x: x 坐标 (dots)
:param y: y 坐标 (dots)
:param content: 文本内容
:param font: 字体名称。
"TSS24.BF2" 是常用的内置简体中文字体。
"1"~"5" 是内置英文字体。
:param rotation: 旋转角度 (0, 90, 180, 270)
:param x_mul: 横向放大倍数 (1-10)
:param y_mul: 纵向放大倍数 (1-10)
"""
# TEXT x,y,"font",rotation,x_mul,y_mul,"content"
# 注意: content 需要用双引号包围,且内部双引号需要转义
safe_content = content.replace('"', '\\"')
cmd = f'TEXT {x},{y},"{font}",{rotation},{x_mul},{y_mul},"{safe_content}"'
self.send_command(cmd)
def barcode(self, x, y, content, type="128", height=100, human=1, rotation=0, narrow=2, wide=2):
"""
打印条码
:param x: x 坐标
:param y: y 坐标
:param content: 条码内容
:param type: 条码类型 ("128", "39", "EAN13", "QRCODE"等) - 注意 TSPL 有专门的 QRCODE 指令
:param height: 条码高度 (dots)
:param human: 是否打印人眼可读字符 (0:不可见, 1:可见)
:param rotation: 旋转 (0, 90, 180, 270)
:param narrow: 窄条宽度 (dots)
:param wide: 宽条宽度 (dots)
"""
# BARCODE x,y,"type",height,human,rotation,narrow,wide,"content"
safe_content = content.replace('"', '\\"')
cmd = f'BARCODE {x},{y},"{type}",{height},{human},{rotation},{narrow},{wide},"{safe_content}"'
self.send_command(cmd)
def qrcode(self, x, y, content, ecc="L", cell_width=5, mode="A", rotation=0):
"""
打印二维码
:param x: x 坐标
:param y: y 坐标
:param content: 二维码内容
:param ecc: 纠错级别 (L, M, Q, H)
:param cell_width: 单元格宽度 (dots, 1-10),控制二维码大小
:param mode: 模式 (A:自动, M:手动)
:param rotation: 旋转 (0, 90, 180, 270)
"""
# QRCODE x,y,ECC,cell_width,mode,rotation,"content"
safe_content = content.replace('"', '\\"')
cmd = f'QRCODE {x},{y},{ecc},{cell_width},{mode},{rotation},"{safe_content}"'
self.send_command(cmd)
def box(self, x, y, x_end, y_end, thickness=1):
"""绘制矩形框"""
self.send_command(f"BOX {x},{y},{x_end},{y_end},{thickness}")
def line(self, x, y, width, height):
"""绘制直线 (TSPL 中 BAR 命令用于画线)"""
# BAR x,y,width,height
self.send_command(f"BAR {x},{y},{width},{height}")
# =================================================================
# 执行打印
# =================================================================
def print_out(self, quantity=1):
"""
开始打印
:param quantity: 打印份数
"""
self.send_command(f"PRINT {quantity}")
# =================================================================
# SDK 特殊指令兼容 (参考 Android SDK)
# =================================================================
def set_special_mode(self, enabled=True):
"""
发送 SDK 中出现的神秘指令 0x12 0x43
可能是某种私有模式开关(如加粗或特殊字体)
"""
if enabled:
self.send_raw(b'\x12\x43\x01')
self.send_raw(b'\x12\x45\x01')
else:
self.send_raw(b'\x12\x43\x00')
self.send_raw(b'\x12\x45\x00')
# =================================================================
# 快捷组合方法 (仿 Android SDK 逻辑)
# =================================================================
def label_begin(self, width_dots, height_dots):
"""
开始标签任务 (仿 SDK 接口)
自动将 dots 转换为 mm (假设 203dpi, 8 dots/mm)
"""
width_mm = int(width_dots / 8)
height_mm = int(height_dots / 8)
# 1. 清除缓冲区
self.cls()
# 2. 设置尺寸
self.size(width_mm, height_mm)
# 3. 设置间隙 (默认 2mm)
self.gap(2, 0)
# 4. 寻原点 (可选,防止跑偏)
# self.home()
def label_end(self):
"""结束标签任务并打印 (仿 SDK 接口)"""
self.print_out(1)

56
printer_image_print.py Normal file

File diff suppressed because one or more lines are too long

View File

@@ -526,11 +526,11 @@ def optimize_prompt(asr_text, progress_callback=None):
if progress_callback:
progress_callback(0, "正在准备优化提示词...")
system_prompt = """你是一个AI图像提示词优化专家。将用户简短的语音识别结果转化为详细的、适合AI图像生成的文提示词。
system_prompt = """你是一个AI图像提示词优化专家。将用户简短的语音识别结果转化为详细的、适合AI图像生成的文提示词。
要求:
1. 保留核心内容和主要元素
2. 添加适合AI绘画的描述词(风格、光线、氛围等)
3. 用英文输出
1. 用于热敏打印机的中文提示词图片
2. 添加适合AI绘画的描述词尺寸宽48mm, 高30mm 的线稿图片,线稿要存粗方便热敏打印
3. 适合热敏打印机打印图片还可以是icon方便这个标签打印机打印效果
4. 简洁但描述详细
5. 不要添加多余解释,直接输出优化后的提示词"""