在美国,即使是自己直接寄信,不贴任何条码,USPS 在邮局里也会喷上 IMB 条码。有些 商业客户寄信的时候,会在信封上面直接印刷 IMB 条码。IMB 条码是有 trace 功能的 (他不提供 tracking 功能,无法确认信件是否确实投妥)。这个 trace 功能可以使用 USPS API 直接进行跟踪,这也是为什么有些银行虽然使用 First Class Mail,但是可以 告诉你信用卡哪天寄到。当然,实现这样的功能的前提是打印信封的时候粘上去条码。
打印条码本身很简单,连 API 都不需要,只要做好 IMB 的编码工作即可。我自己申请了个 Mailer ID,但是也有免费的工具可以用 他们的 Mail ID。https://envelopetracker.com/ 就是一个很好的工具。
我自己我使用了 Gprinter 写了个小工具,可以自动解码地址,查找邮编后打印信封。由于我的 Mercurial 服务器还没修好,所以在文后附上打印机驱动的源码。等哪天有时间了,我会把自动解码 地址的代码部署一个云服务提供给大家使用。
获取条码,打印条码都非常简单,麻烦的地方主要是如何方便的在线追踪,以及获得更新后的通知。 https://envelopetracker.com/ 已经有了追踪功能,但是没有办法推送。想得到推送通知, 就需要自己注册 Mailer ID 后,登 USPS IV 后台,设置一个 JSON API endpoint,USPS 会定期向这个 endpoint 发送 更新。
接受更新最方便的方法是 lambda 函数(或者是类似的云函数)。考虑到这是 AI 时代,我就 vibe code 了一个 lambda 函数,凑合也能用。这里的主要挑战是 USPS 根本没写 payload 的格式,我只好 直接 log 了请求。下面附上数据结构,供各位 vibe code 的时候作为 prompt 输入。改改总能改出来凑合能用的东西的。
另外注意,scanLocaleKey 很有意思,可以通过查询 Excel 表格,找到对应的 Facility 的地址。我把数据转换成了 parquet 文件,方便 lambda 里 load. 这玩意儿实在有点大,用 JSON 扛不住。
对应的数据字典文件和参考资料有:
除了 lambda 函数,我还做了个 Telegram bot,可以发送 IMB 的更新: t.me/usps_iv_bot 。这个 Telegram bot 用了 @yegle 负责的 GCP Cloud Run Functions 服务。Cloud Run Functions 的好处主要是它不需要单独开个 VPC 以及对应的 NAT Gateway,比用 AWS 省一些钱,免费 tier 完全够用。当然,另一个好处是知道挂了以后找谁去喷。
USPS POST 过来的数据结构如下:
{ "events": [ { "mailShapeDescription": null, "startTheClockFacilityAddress": null, "recipientCridOfMidOnPiece": "<Recipient Crid>", "idTag": null, "recipientRoutingCodeAuthorizedCrid": null, "pieceId": null, "machineName": "DIOSS-006", "ldeTriggerMethod": null, "parentEdocContainerImcb": null, "routingCodeImbMatchingPortion": "ZIP+9", "edocSubmitterCrid": null, "ldeDeliveryMode": null, "parentContainerEdocContainerId": null, "startTheClockFacilityCity": null, "scanFacilityState": "WA", "recipientMailOwnerCrid": null, "scanEventCode": "<Scan Event Code, See IVMTR_OperationCodesList_v9.1_06252025.xlsx>", "scanDatetime": "2025-01-01 01:00:00-0700", "impb": null, "parentEdocTrayImtb": null, "scanFacilityZip": "<5 digit zip code>", "imbTrackingCode": "<IMB without ZIP+9>", "imbRoutingCode": "ZIP+9", "machineId": "006", "startTheClockFacilityLocaleKey": null, "recipientCridOfMidOnPieceDelegator": null, "recipientMailOwnerDelegatorCrid": null, "startTheClockFacilityZip": null, "edocJobId": null, "mailClassDescription": null, "imb": "<IMB>", "scanLocaleKey": "<Locale Key in FACILITY.xlsx>", "mailPhase": "Phase 2a - Destination MMP Processing", "parentTrayEdocContainerId": null, "edocMailingGroupId": null, "predictedDeliveryDate": null, "startTheClockFacilityState": null, "scanFacilityCity": "<City>", "anticipatedDeliveryDate": null, "scannerType": null, "imbStid": "270", "imbSerialNumber": "<IMB Serial Number>", "startTheClockDate": null, "expectedDeliveryDate": null, "handlingEventTypeDescription": "Actual", "imbMid": "<Mailer ID>", "ldeInventoryMethod": null, "handlingEventType": "A", "startTheClockFacilityName": null, "scanFacilityName": "<City>" } ], "msgGrpId": "<Message Group ID>", "msgSerNbr": 1, "totMsgCnt": 1, "recCnt": 1, "totRecCnt": 1 }
Gprinter 打印标签的源代码 (print_label.py) 如下:
import pathlib import sys import typing from absl import app from absl import flags from absl import logging from escpos import printer from PIL import Image import usb.core # --- Flag Definitions --- FLAGS = flags.FLAGS flags.DEFINE_string("png_file", None, "Path to the PNG label file to reprint.") flags.DEFINE_integer("count", 1, "Number of copies to print.") flags.DEFINE_integer("vid", 0x0471, "USB Vendor ID of the label printer.") flags.DEFINE_integer("pid", 0x0055, "USB Product ID of the label printer.") flags.DEFINE_integer("in_ep", 0x82, "USB IN endpoint for the printer.") flags.DEFINE_integer("out_ep", 0x02, "USB OUT endpoint for the printer.") # --- Label Dimension Flags --- # These are needed to send the correct SIZE and GAP commands to the printer. flags.DEFINE_float("paper_width_mm", 100.0, "The width of the label paper in mm.") flags.DEFINE_float("paper_height_mm", 50.0, "The height of the label paper in mm.") flags.DEFINE_float("gap_mm", 3.0, "The gap between labels in mm.") flags.DEFINE_integer("direction", 1, "The print direction (0 or 1).") flags.mark_flag_as_required("png_file") class TSPLPrinterDriver: """A driver to send TSPL commands to a USB label printer.""" def __init__(self, vid: int, pid: int, in_ep: int, out_ep: int): """Initializes the printer driver. Args: vid: The USB Vendor ID of the printer. pid: The USB Product ID of the printer. in_ep: The USB IN endpoint for the printer. out_ep: The USB OUT endpoint for the printer. """ self.vid = vid self.pid = pid self.in_ep = in_ep self.out_ep = out_ep self._device = None def _get_device(self) -> printer.Usb: """Finds and initializes the USB printer device. Returns: An escpos.printer.Usb object for communication. Raises: ConnectionError: If the printer cannot be found or connected to. """ try: self._device = printer.Usb( self.vid, self.pid, in_ep=self.in_ep, out_ep=self.out_ep ) logging.info("Printer found: VID=%#06x, PID=%#06x", self.vid, self.pid) return self._device except usb.core.NoBackendError as e: raise ConnectionError("libusb backend not found.") from e except Exception as e: raise ConnectionError( f"Printer not found (VID={self.vid:#06x}, PID={self.pid:#06x}). " "Check connection/permissions." ) from e def _image_to_bitmap_bytes(self, img: Image.Image) -> tuple[bytes, int, int]: """Converts a PIL Image to a monochrome bitmap byte array for TSPL.""" img = img.convert("1") # Ensure monochrome with 1-bit pixels. width, height = img.size # Use the standard '1' decoder for MSB-first bitmap data. return img.tobytes("raw", "1"), width, height def print_image( self, img: Image.Image, count: int, label_params: dict[str, typing.Any] ): """Constructs and sends the full TSPL payload to print an image. Args: img: The PIL Image to print. count: The number of copies to print. label_params: A dictionary with label specs (paper_width_mm, etc.). """ bitmap_data, width_dots, height_dots = self._image_to_bitmap_bytes(img) # Width in bytes for the BITMAP command (must be a multiple of 8). width_bytes = (width_dots + 7) // 8 payload = bytearray() payload.extend( f"SIZE {label_params['paper_width_mm']} mm, {label_params['paper_height_mm']} mm\r\n".encode() ) payload.extend(f"GAP {label_params.get('gap_mm', 2.0)} mm, 0 mm\r\n".encode()) payload.extend(f"DIRECTION {label_params.get('direction', 1)}\r\n".encode()) payload.extend(b"CLS\r\n") # Clear image buffer before sending new data. # Use mode 0 (OVERWRITE) for the BITMAP command. payload.extend(f"BITMAP 0,0,{width_bytes},{height_dots},0,".encode()) payload.extend(bitmap_data) payload.extend(b"\r\n") payload.extend(f"PRINT {count},1\r\n".encode()) printer_device = self._get_device() try: logging.info("Sending %d bytes to printer.", len(payload)) printer_device._raw(payload) logging.info("Print command sent successfully.") finally: printer_device.close() def main(argv: list[str]): """Main application entry point.""" del argv # Unused. try: # 1. Load the PNG image file. png_path = pathlib.Path(FLAGS.png_file) if not png_path.exists(): raise FileNotFoundError(f"PNG file not found at: {png_path}") logging.info("Loading label from: %s", png_path) label_image = Image.open(png_path) # 2. Prepare label parameters from flags. label_params = { "paper_width_mm": FLAGS.paper_width_mm, "paper_height_mm": FLAGS.paper_height_mm, "gap_mm": FLAGS.gap_mm, "direction": FLAGS.direction, } # 3. Initialize the printer driver and print. driver = TSPLPrinterDriver( vid=FLAGS.vid, pid=FLAGS.pid, in_ep=FLAGS.in_ep, out_ep=FLAGS.out_ep ) driver.print_image(label_image, FLAGS.count, label_params) except (FileNotFoundError, ConnectionError, IOError) as e: logging.error("Reprint operation failed: %s", e, exc_info=True) sys.exit(1) if __name__ == "__main__": app.run(main)
Comments