· compliance· 约 26 分钟精读

Tagworks Status Tag Record Selection Legal Scale — Technical Analysis | KeyRotate Technology

核心要点速览 · TL;DR 概要

TagWorks 的状态标签不是界面的截图,而是从一条记录投影出来的独立产物。未解决失败印红标,最近一次合格检验印绿标,两者都没有就不出标;而 AS/NZS 3760 对标签的五项最低要求、800×420 的画布与 400×210 的 PDF 页面,又各自把这张纸推向不同的尺寸。

Tagworks Status Tag Record Selection Legal Scale — Technical Analysis | KeyRotate Technology

地下车库的临时检测台上并排躺着两台角磨机。第一台的接地电阻是 62 mΩ,绝缘电阻 48 MΩ,目视没有缺陷;第二台的电源线护套在插头根部裂开,金属外壳上有一道明显的磕痕。检验员在 TagWorks 里录完两条记录,把手机递给旁边的助手,打印机吐出两张纸:一张绿色横幅,写着 RECORDED TEST PASS;一张红色横幅,写着 DANGER — DO NOT USE。

这两张纸是整条链路里唯一离开手机的东西。台账留在沙盒里,报告留在 PDF 里,只有标签会被贴在设备外壳上,跟着设备进仓库、上工地、被油污覆盖、被下一个班组看见。它的读者不是检验员,而是三个月后路过这台机器、准备插电的人。

也正因为如此,标签不是界面的截图。它有自己的记录选择规则、自己的法定要素、自己的物理尺度。本文按源码核对这三份契约,看看一张 800×420 的画布,最后怎样落到纸上。

一张标签,三份契约记录选择未解决失败 → 红标 DANGER最近合格 → 绿标 PASS两者都没有 → 不出标签800 × 420 pt 画布RECORDED TEST PASSTAG ID: TG-4821APPLIANCE: Angle GrinderNEXT DUEQR 110 × 110,仅绿标绘制红标缺陷框 440 × 96 点AS/NZS 3760 cl. 2.5.2.1检验人姓名有企业名称缺测试日期有下次到期有标准引用缺标签不可复用:无序列号同一个标签,三个物理尺寸界面徽章100 × 50 mm生成器注释100 × 52.5 mmPDF 页面141 × 74 mm三份契约里,只有一份写在纸上

左右滑动查看完整图表

一、谁被印在标签上:红标优先于绿标

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 × 50mm100 × 50宽高比 2.00
生成器注释800 x 400 pt282 × 141与实际画布不符
生成器实际画布CGSize 800 × 420282 × 148800×420 点,不是像素
PDF 注释100mm x 52.5mm100 × 52.5宽高比 1.905
PDF 实际页面CGRect 400 × 210141.1 × 74.1宽高比 1.905

先把 PDF 这一行算清楚。PDF 的用户空间单位是点,一点等于 1/72 英寸:

400 pt÷72×25.4=141.1 mm400\ \mathrm{pt} \div 72 \times 25.4 = 141.1\ \mathrm{mm} 210 pt÷72×25.4=74.1 mm210\ \mathrm{pt} \div 72 \times 25.4 = 74.1\ \mathrm{mm}

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 pt16 pt11.3 pt
标签 ID28 pt14 pt9.9 pt
下次到期月份20 pt10 pt7.1 pt
器具名称 / 地点16 pt8 pt5.7 pt
字段标签 / 缺陷原因13 pt6.5 pt4.6 pt
页脚声明11 pt5.5 pt3.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、器具、地点、检验人、日期
QR480, 114, 110 × 110仅 PASS 标签绘制
下次到期框605, 114, 165 × 110月份 20 pt,完整日期 13 pt
页脚480, 246, 290 × 13611 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 上
缺陷原因分页/缩字原因超过四行时降字号或续到第二张关键原因被裁掉
红标补 QRFAIL 分支复用 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. 文档

Share:
Back to Blog