· compliance· 约 26 分钟精读
800×420 落到纸上:设备状态标签的记录选择、法定要素与物理尺度
TagWorks 的状态标签不是界面的截图,而是从一条记录投影出来的独立产物。未解决失败印红标,最近一次合格检验印绿标,两者都没有就不出标;而 AS/NZS 3760 对标签的五项最低要求、800×420 的画布与 400×210 的 PDF 页面,又各自把这张纸推向不同的尺寸。
地下车库的临时检测台上并排躺着两台角磨机。第一台的接地电阻是 62 mΩ,绝缘电阻 48 MΩ,目视没有缺陷;第二台的电源线护套在插头根部裂开,金属外壳上有一道明显的磕痕。检验员在 TagWorks 里录完两条记录,把手机递给旁边的助手,打印机吐出两张纸:一张绿色横幅,写着 RECORDED TEST PASS;一张红色横幅,写着 DANGER — DO NOT USE。
这两张纸是整条链路里唯一离开手机的东西。台账留在沙盒里,报告留在 PDF 里,只有标签会被贴在设备外壳上,跟着设备进仓库、上工地、被油污覆盖、被下一个班组看见。它的读者不是检验员,而是三个月后路过这台机器、准备插电的人。
也正因为如此,标签不是界面的截图。它有自己的记录选择规则、自己的法定要素、自己的物理尺度。本文按源码核对这三份契约,看看一张 800×420 的画布,最后怎样落到纸上。
左右滑动查看完整图表
一、谁被印在标签上:红标优先于绿标
AssetDetailView 里有一个计算属性 printableRecord,它决定打印按钮拿到哪一条记录。逻辑只有三层:
if let unresolvedFailure { return unresolvedFailure }
guard let latest = ReportExporter.latestInspection(for: current, records: store.records),
latest.establishesPass(for: current.applianceClass) else { return nil }
return latest第一层是未解决的失败记录。ReportExporter.unresolvedFailure 按时间顺序重放整段历史:遇到 test 或 retest 且判定为失败,就把这条记录记为活动失败;遇到 repair 直接跳过;只有遇到一条满足完整条件的 release,才把活动失败清空。条件包括:放行记录指向的必须是一条有效的复试记录、复试日期早于放行日期、放行结论为 pass,放行记录的检验人、备注与授权人字段都不为空。任何一项缺失,失败都留在账上。
第二层是最近一次检验。latestInspection 只取 kind 为 test 或 retest 的记录,按日期取最大值;如果这条记录不能 establishesPass,就返回空。第三层是空值:既没有未解决失败,也没有最近一次合格检验,打印按钮不可用。
这三层投影出三种标签,而不是两种:
| 台账状态 | 取到的记录 | 标签与依据 |
|---|---|---|
| 存在未解决失败 | 失败记录本身 | 红标 DANGER;unresolvedFailure 非空 |
| 无未解决失败,最近一次检验合格 | 最近一次合格检验 | 绿标 PASS;establishesPass 为真 |
| 无未解决失败,最近一次检验不完整或从未检验 | 空 | 不生成标签;canPrintStatusTag 为假 |
establishesPass 不是“读数在门限内”的同义词。它调用 evidenceState,而 evidenceState 对记录类型有硬前提:只有 test 与 retest 参与证据判定;repair 与 release 一律返回 notMeasured。在检验记录内部,顺序是:结论为 fail、目视发现缺陷、任一读数越界,三者任一成立即判 fail;目视结果不是“无缺陷”则返回 visualNotRecorded;最后才看读数是否全部合格。也就是说,一条只填了绝缘电阻、目视栏留空的记录,不会被印成绿标。
这个顺序带来一个容易被忽略的后果:一条失败的复试记录通过、但还没有走完放行链时,标签仍然是红的。红标依据的是 unresolvedFailure,不是“最近一次读数”。printableRecord 在存在活动失败时直接返回失败记录,根本不会走到 latestInspection。所以现场看到的红标,日期是当初失败的那一天,不是复试的那一天;它表达的是“这台设备还没有被放行”,而不是“这台设备最后一次测量不合格”。
从安全角度看,这个方向是对的:宁可让一台已经复试合格的设备继续挂红标,也不要让一台还没走完放行链的设备提前变绿。但它对标签的读者提出了要求:红标上的日期不能单独当作“最后一次检测日期”使用,必须回到台账里看失败、修复、复试、放行四条记录的顺序。标签只承载结论,不承载过程。
红标也不是“只要不合格就一定有”。如果最近一次检验是“未测量”或目视未记录,unresolvedFailure 不会把它当成失败,latestInspection 又无法 establishesPass,结果是空值,标签根本不会生成。现场看到的是一台没有标签的设备,而不是一台挂红标的设备。这两种状态在管理上并不等价:红标至少是一个明确的“停用”信号,无标签则可能被误读为“还没轮到贴”。应用把“不完整”留在了台账里,没有把它投影到物理世界。
二、五项法定要素:标签上少了两项
AS/NZS 3760:2022 的第 2.5.2.1 条规定了通过检验的设备标签最低要写什么:执行检验的人与企业的名称、检验日期、复检日期,以及对 AS/NZS 3760 的引用。第 2.5.2 条则规定了标签本身的属性:耐久、可读、不可重复使用、非金属、不需要特殊技术即可读取。前者是内容,后者是载体。
TagWorks 的生成器把内容画在一张 800×420 的画布上。逐项核对,结果如下:
| 法定要素 | 标签上的实际内容 | 是否具备 |
|---|---|---|
| 执行检验的人 | TESTED BY 行 + 姓名与执照号 | 具备,姓名缺失时回落为 — |
| 执行检验的企业 | 无 | 缺失 |
| 检验日期 | TEST DATE 行 | 具备 |
| 复检日期 | NEXT DUE 月份年份 + 完整日期 | 具备 |
| 对 AS/NZS 3760 的引用 | 无,页脚只写 “Verify applicable inspection and testing requirements.” | 缺失 |
企业名称不是没有数据。InspectorSignatureManager 里有 companyName,设置页有 “Company (on reports)” 输入框,报告导出时会把它写进抬头,登记册页眉也会读它。但 TagWorksLabelGenerator 的函数签名只接收 asset、record 与 inspectorLicense,没有接收企业名称;生成器的全部绘制字符串里也没有 companyName。报告有抬头,标签没有。
标准引用也是同样的情况。全工程检索 3760,命中集中在设置页、标准速查手册、报告导出器与记录模型;TagWorksLabelGenerator.swift 与 PrintableTagView.swift 里一次都没有出现。标签上写的是 “VERIFY APPLICABLE REQUIREMENTS”,这句话本身没有错,但它没有回答“适用的是哪一本”。如果现场同时执行 AS/NZS 3760 与其他属地标准,读者无法从标签上判断这次检验依据的是哪一套。
还有一处承诺与实现的落差。SignatureCaptureView 的说明写着:“Your signature and accreditation number are included in exported operator records and tags.” 执照号确实进了标签,但签名没有。生成器没有读取 InspectorSignatureManager.loadSignature(),也没有任何绘制 UIImage 的调用。标签上的 TESTED BY 是文本,不是签名位图。对一份需要现场归属的文书来说,文本姓名与手写签名不是同一种证据。
“不可重复使用”这一项也不成立。标签没有独立的标签序列号,没有签发时间,没有与记录 UUID 绑定的短码;同一台设备、同一条记录,今天打印和下周打印得到的是两张外观相同的纸。撕下来贴到另一台同型号设备上,标签本身没有任何抵抗能力。应用能做的只是印上 “DO NOT REMOVE”,这是一句请求,不是一条控制。
载体属性同样落在应用之外。生成器输出的是位图与 PDF,耐久、可读、非金属取决于用户装进打印机的是哪一种标签纸。应用无法知道纸是聚酯还是铜版纸,也无法知道打印机的碳带是蜡基还是树脂基。它能控制的是版面与字号,而这两项在下一节会变得比想象中更麻烦。
三、三个尺寸:100×50、100×52.5 与 141×74
同一张标签,代码里出现了五个不同的尺寸口径:
| 出处 | 写法 | 换算成毫米 | 说明 |
|---|---|---|---|
| 预览界面徽章 | 100mm × 50mm | 100 × 50 | 宽高比 2.00 |
| 生成器注释 | 800 x 400 pt | 282 × 141 | 与实际画布不符 |
| 生成器实际画布 | CGSize 800 × 420 | 282 × 148 | 800×420 点,不是像素 |
| PDF 注释 | 100mm x 52.5mm | 100 × 52.5 | 宽高比 1.905 |
| PDF 实际页面 | CGRect 400 × 210 | 141.1 × 74.1 | 宽高比 1.905 |
先把 PDF 这一行算清楚。PDF 的用户空间单位是点,一点等于 1/72 英寸:
400×210 点就是 141.1×74.1 毫米,正好是 100×52.5 毫米的 1.411 倍。注释写的是 100×52.5,页面实际是 141×74;两者宽高比相同,尺寸不同。界面徽章的 100×50 又是另一个宽高比,和画布的 800×420(1.905)对不上。
800×420 这个数字本身有它的来历。203 dpi 是热敏与热转印标签机最常见的分辨率之一,800 像素除以 203 再乘 25.4,正好是 100.1 毫米;420 像素是 52.6 毫米。也就是说,如果画布真的是一张 800×420 像素的位图,它在 203 dpi 下就是一张 100×52.5 毫米的标签,和 PDF 注释的意图一致。
问题在于,UIGraphicsImageRenderer(size: CGSize(width: 800, height: 420)) 的 size 单位是点,不是像素。渲染器的默认 format.scale 取主屏的 scale:在 2 倍屏上得到 1600×840 像素,在 3 倍屏上得到 2400×1260 像素。它从来不是一张固定的 800×420 像素画布,而是一张 800×420 点的画布,像素数随设备变化。注释里的 “high-res for thermal/label printers” 只在点的意义上成立;真到打印机上,像素密度取决于导出它的那台手机。
PDF 路径把这张 800×420 点的图绘制到 400×210 点的页面里,缩放系数是 0.5。所有字号减半:
| 元素 | 画布字号 | PDF 字号 | 若再缩到 100 mm 宽 |
|---|---|---|---|
| 横幅标题 | 32 pt | 16 pt | 11.3 pt |
| 标签 ID | 28 pt | 14 pt | 9.9 pt |
| 下次到期月份 | 20 pt | 10 pt | 7.1 pt |
| 器具名称 / 地点 | 16 pt | 8 pt | 5.7 pt |
| 字段标签 / 缺陷原因 | 13 pt | 6.5 pt | 4.6 pt |
| 页脚声明 | 11 pt | 5.5 pt | 3.9 pt |
“若再缩到 100 mm 宽”这一列,是把 141.1 毫米的 PDF 页面按 100 毫米宽放进标签纸时的比例(0.709)。很多标签打印机会按纸张尺寸缩放,这一列并非必然发生,但它是现场最常见的使用方式。即使不缩放,页脚的 5.5 pt 也已经很小:5.5 pt 的字身约 1.94 毫米,正文的 6.5 pt 约 2.29 毫米。标准要求标签“可读”,这两个数字是否足够,取决于读者站在多远的距离、光线如何、标签纸的表面是否反光。
AirPrint 路径更直接:printController.printingItem = img,传的是一张 UIImage,不是 PDF。代码没有设置 printPaper,没有设置 printFormatter,也没有设置缩放系数。物理尺寸由打印控制器与打印机共同决定:打印机可能按 100×50 毫米的标签纸缩放,也可能按 A4 纸张的实际点数输出。应用在界面上承诺了 100×50,但没有一条代码把这个承诺写进打印任务。
所以一张标签有三个尺寸:界面说 100×50,注释说 100×52.5,PDF 实际 141×74;而 AirPrint 的尺寸根本不在应用手里。三者之中,只有 PDF 的页面盒是确定的,偏偏它的数值与注释不一致。
四、版面:右侧的 QR 与下方的原因框
把画布拆开,PASS 标签的几何是这样的:
| 元素 | 位置与尺寸(点) | 说明 |
|---|---|---|
| 外框 | 12, 12, 776 × 396 | 圆角 16,边线 4 |
| 横幅 | 12, 12, 776 × 86 | 绿底,标题 32 pt |
| 左栏 | x=32,宽 448 | 标签 ID、器具、地点、检验人、日期 |
| QR | 480, 114, 110 × 110 | 仅 PASS 标签绘制 |
| 下次到期框 | 605, 114, 165 × 110 | 月份 20 pt,完整日期 13 pt |
| 页脚 | 480, 246, 290 × 136 | 11 pt,三行声明 |
左栏的 448 点是固定的。器具名称用 16 pt 系统字体绘制在 x=32,没有换行、没有截断、没有 boundingRect 预测量。16 pt 下,448 点大约能容纳 52 个拉丁字符或 28 个汉字;超过这个长度,文字会继续向右画,直接盖到 480 点处的 QR 上。地点字段同理。draw(at:) 不会裁剪,也不会省略,它只是把整串字画出去。现场的设备名称经常带型号、电压、工位号,超过 52 个字符并不罕见。
QR 的载荷是七段竖线分隔的字符串:
TAGWORKS|ID:<标识>|NAME:<名称>|CLASS:<类别>|TEST:<日期>|DUE:<日期>|RESULT:<结论>
日期用 yyyy-MM-dd,结论取 evidenceState 的显示名。载荷长度随名称与标识增长。以标识 7 个字符、名称从 12 个字符增至 60 个字符为例,按 ISO/IEC 18004 的字节模式与 M 级纠错估算:一个 94 字节的载荷落在版本 6(41×41 模块),106 字节仍是版本 6,121 字节升到版本 7(45×45),141 字节升到版本 8(49×49)。含 4 模块静区后,110 点的 QR 框在版本 6 下每模块约 2.24 点,版本 8 下约 1.93 点;按 100 毫米标签宽折算,模块边长从约 0.28 毫米降到约 0.24 毫米。手机在近距离还能读,但余量已经不多。
生成 QR 的代码先做了一次 10 倍放大:CGAffineTransform(scaleX: 10, y: 10),再把结果画进 110×110 的框。放大后的图像远大于目标框,绘制时被缩小,模块边缘经过插值,可能产生灰边。对二维码来说,清晰的二值边缘比高分辨率更重要;先放大再缩小,并不会增加信息,只会让边缘变软。
红标没有 QR。FAIL 分支只画了标签 ID、器具名称、缺陷原因框、检验人与日期、右侧的锁定警告框;generateQRCode 在 FAIL 分支里一次都没有调用。也就是说,合格设备可以被扫码找回,不合格设备只能靠人眼读 ID 再手工输入。对于一件已经被隔离、需要尽快定位的设备,这个不对称并不合理。
缺陷原因框是 440×96 点,内边距 12/10,实际可绘区域 416×76 点。13 pt 字的行高约 15.6 点,76 点大约能放 4.8 行。原因是按顺序追加的:接地电阻越界、绝缘电阻越界、目视发现缺陷、目视未记录、记录备注。前四项最多四条,加上 “DEFECT REASON:” 标题与项目符号,已经超过 4.8 行的上限;如果备注再长一点,后面的原因就会被 draw(in:) 裁掉。裁剪没有省略号,读者看到的是一个在句子中间结束的缺陷说明。
绝缘电阻那一条还写死了一个数字:比较用的是 TestThresholds.insulationMinMegaohms,显示却写 Min: 1.00 MΩ。接地那一条用的是 Int(TestThresholds.earthBondMaxMilliohms),会跟着常量走;绝缘这一条不会。只要常量将来调整,标签上的门限就会和判定层的门限说两套话。
红标还少了地点。PASS 标签有 LOCATION,FAIL 标签只有 TAG ID 与 APPLIANCE。一台在两层车库之间搬动的角磨机,红标上不会写它在哪个工位;隔离动作要靠台账里的站点字段,而标签的读者往往只有这张纸。
五、标签之后:可复核性留在哪里
一张标签要能被复核,至少需要回答三个问题:它是从哪条记录生成的、什么时候生成的、有没有被换过。当前实现只回答了第一个问题的一半。
QR 载荷里有 TEST、DUE、RESULT,但 TagWorks 的扫码入口只做一件事:从 TAGWORKS| 开头的字符串里取出 ID: 段,规范化后去登记册里找资产。TEST、DUE、RESULT 三个字段在扫码路径上不会被读取,也不会与台账里的记录比对。标签上写了结论,扫码却只认身份;结论仍然是给人看的,不是给系统校验的。
标签没有序列号,也没有签发记录。PrintableTagView 不写台账、不记打印时间、不记打印人;同一台设备重复打印,得到的是两张外观相同的标签。台账里没有“这张纸是什么时候贴上去的”这一条,只有资产、记录与报告。复核者拿到一张脱落的标签,无法判断它是不是当前有效的那一张,只能凭日期推断。
这不意味着标签没有价值。它仍然是现场唯一不依赖电池、不依赖网络、不依赖应用是否被打开的证据载体。但它能承担的是“提示”和“标识”,不是“证明”。证明仍然在台账的记录链里:失败、修复、复试、放行四条记录的顺序,以及报告的导出。标签把结论带到现场,台账把过程留在设备里。
如果要让这张纸更接近一份可复核的文书,有几件事是可以在现有结构里做的,不需要改变离线架构:
| 改动 | 具体做法 | 解决的问题 |
|---|---|---|
| 补齐法定要素 | 把 companyName 传进生成器,页脚加 AS/NZS 3760 引用 | 五项最低要求缺两项 |
| 固定物理尺寸 | PDF 页面改为 283.46×148.82 点(100×52.5 mm),或按 203 dpi 的 800×420 像素画布输出 | 141×74 与 100×52.5 的口径冲突 |
| 固定像素画布 | 给渲染器格式设 scale = 1,或直接按目标 dpi 计算像素 | 像素数随设备 scale 变化 |
| 文本测量与换行 | 用 boundingRect 预测量器具名与地点,超宽时换行或截断 | 文字压到 QR 上 |
| 缺陷原因分页/缩字 | 原因超过四行时降字号或续到第二张 | 关键原因被裁掉 |
| 红标补 QR | FAIL 分支复用 generateQRCode | 不合格设备无法扫码定位 |
| 标签序列号 | 在页脚印记录 UUID 的短码或签发序号,并写入台账 | 无法判断标签是否当前有效 |
这些是工程上的收敛方向,不是已经实现的功能。文章能核对的是代码现在做了什么,不能替产品宣布它已经做了什么。
六、一张纸的三份契约
回到地下车库。绿标被贴在角磨机的机身侧面,红标被贴在另一台的电源线根部。三个月后,绿标上的 NEXT DUE 会被下一个检验员看到;红标上的 DANGER 会被准备插电的人看到。两张纸的设计宽度都只有 100 毫米,字都很小,但它们承担的是同一件事:把设备里的记录翻译成现场能读懂的一句话。
这句话要成立,需要三份契约同时成立。记录选择决定这句话是“合格”还是“停用”;法定要素决定这句话是否包含标准要求的最低信息;物理尺度决定这句话在油污、反光和两米距离之外还能不能被读出来。任何一份契约断了,标签就退化成一张有颜色的纸。
TagWorks 的记录选择是清楚的:未解决失败优先,最近合格检验其次,两者都没有就不出标。法定要素缺了企业名称与标准引用,签名承诺与实现也不一致。物理尺度有三个数字,PDF 的页面盒与注释对不上,AirPrint 的尺寸不在应用手里。这些都不是界面上的小瑕疵,而是标签作为现场文书时会被直接问到的问题。
一张标签的价值,不在于横幅是绿色还是红色,而在于它能不能让一个没有参加过检验的人,在三米外做出正确的动作。做到这一点,靠的不是颜色,是那五项要素、那一条记录链,以及印在纸上的字是否还看得清。
参考资料
[1] Standards Australia & Standards New Zealand. AS/NZS 3760:2022 — In-service safety inspection and testing of electrical equipment and RCDs. 标准页 [2] Field Efficient. Test & tag intervals: the AS/NZS 3760 schedule. 指南 [3] Metrotest. Electrical Test Tag Compliance in NZ. 说明 [4] ISO/IEC 18004:2024. QR code bar code symbology specification. 标准页 [5] Apple. UIGraphicsImageRenderer. 文档 [6] Apple. UIGraphicsPDFRenderer. 文档 [7] Apple. UIPrintInteractionController. 文档