Cleer Arc5耳机Current Time当前时间特征值

AI助手已提取文章相关产品:

Cleer Arc5耳机Current Time当前时间特征值技术解析

你有没有想过,为什么你的Cleer Arc5耳机能“知道”现在是早上还是晚上?甚至在你听歌两小时后,温柔地提醒你:“该让耳朵休息啦~” 🎧💤

这背后可不只是简单的定时器在工作——它其实悄悄从手机里“偷看”了系统时间。而实现这一切的关键,正是蓝牙世界里的一个不起眼但极其聪明的标准设计: Current Time Service(CTS) ,以及它的核心成员—— 0x2A2B 特征值。

别小看这个10字节的数据包,它让耳机从“被动播放器”进化成了“会思考的智能伴侣”。今天我们就来扒一扒,它是如何在低功耗、高兼容的前提下,默默支撑起一堆“贴心功能”的。


从一次久听提醒说起 ⏳

想象这样一个场景:

你戴着Cleer Arc5通勤听播客,一路听到下班,中间摘下耳机吃饭、开会,断连了好几次。结果刚戴上继续听,没几分钟耳机突然说:“已连续使用90分钟,请适当休息。”

咦?我明明断开了快一个小时啊,怎么还在算?

如果这是靠传统计时器实现的,那大概率会出错。但Cleer Arc5不会。因为它不是靠“内部倒计时”,而是每次恢复连接时,都会先去问一句手机:“现在几点?”
👉 然后结合上次结束播放的时间戳,判断是否属于“新的聆听会话”。

而这一步的核心,就是读取那个叫做 Current Time 的GATT特征值。

是不是有点意思了?我们先来看看这个“时间信使”到底长什么样。


它是谁?—— Current Time特征值的本质

在蓝牙SIG(Special Interest Group)制定的GATT规范中,有一个叫 Current Time Service (CTS) 的标准服务,UUID为:

00002A2B-0000-1000-8000-00805F9B34FB

这个服务里最关键的,就是 Current Time 这个特征值。它不像厂商自定义协议那样五花八门,而是统一格式、全球通用,iOS、Android、Windows全支持,即插即用 ✅

它的数据结构固定为 10字节 ,按Little-Endian排列如下:

字段 长度(字节) 说明
Year 2 年份(如2024)
Month 1 1~12
Day 1 1~31
Hours 1 0~23
Minutes 1 0~59
Seconds 1 0~59
Day of Week 1 1=周一, …, 7=周日
Fractional Seconds 1 毫秒级精度(1/256秒)
Adjust Reason & Time Zone Offset 2 包含时区偏移、夏令时标志等

最后两个字节尤其重要:它们不仅告诉你“当前时区”,还能标明时间来源(比如是来自NTP网络授时,还是用户手动设置),帮助耳机判断可信度。

🤔 小知识:推荐传输UTC时间而非本地时间!这样即使你飞越太平洋,耳机也能通过本地存储的时区规则自动校准,避免“时间错乱”。


它是怎么工作的?—— 一场轻量级的通信对话

当Cleer Arc5和手机建立BLE连接后,并不会立刻开始放音乐。相反,它会先做一件事: 服务发现(Service Discovery)

就像进超市前先看地图一样,耳机会扫描手机端开放了哪些GATT服务。一旦发现对方支持 0x2A2B ,就知道:“哦,这家伙有标准时间可以同步。”

接下来有两种方式获取时间:

  • Read Request :一次性拉取当前时间(适合开机同步)
  • Enable Notify :监听变化事件,手机时间一改,立马推送(适合跨时区飞行后的自动校准)

整个过程走的是ATT协议,数据包极小,通常只需一次BLE帧就能完成,对功耗几乎无影响 ⚡️

收到原始字节流后,耳机固件会立即解析成结构化时间对象,再交给系统时间管理模块处理:

typedef struct {
    uint16_t year;
    uint8_t  month;
    uint8_t  day;
    uint8_t  hour;
    uint8_t  minute;
    uint8_t  second;
    uint8_t  day_of_week;      
    uint8_t  fractional_sec;  
    int16_t  adjust_info;     
} current_time_t;

然后调用类似 system_set_real_time_clock() 的接口更新内部RTC或共享内存,最后触发一系列“时间敏感型策略”刷新。

整个流程就像一条流水线:

[手机] 
 → BLE Write(0x2A2B)
   → [耳机 GATT Server]
     → [Time Manager 解析]
       → [策略引擎调度]
         ├─ 昼夜模式切换
         ├─ 健康提醒触发
         └─ 戴戴检测灵敏度调整

完全事件驱动,松耦合,效率极高!


为什么不用私有协议?—— 标准化的力量 💡

你可能会问:我自己定义个“时间同步命令”不也行吗?

当然可以,但代价不小。来看一组对比:

对比项 私有方案 使用CTS标准服务
开发成本 高(需两端协同开发) 极低(系统原生支持)
功耗 可能需轮询查询 支持Notify,零轮询
兼容性 仅限自家APP iOS/Android开箱即用
智能潜力 有限 可接入AI调度系统

更关键的是, CTS是蓝牙认证必测项目之一 。只要设备通过BQB认证,就默认支持这项服务。这意味着Cleer Arc5无需依赖特定APP,哪怕你是用第三方蓝牙管理工具配对,照样能拿到时间!

✅ 总结一句话: 标准化 = 更少代码 + 更低功耗 + 更强生态


实战案例拆解:那个“久听提醒”是怎么炼成的?

我们再回到开头的问题:如何精准识别“新会话”?

假设你在中午12:00开始听歌,13:30暂停;下午17:00再次戴上。虽然总播放时间超过90分钟,但中间间隔远大于设定阈值(比如30分钟),应视为两次独立使用。

传统做法可能直接累加时间,导致误提醒。而Cleer Arc5的做法更聪明:

  1. 每次恢复音频播放前,向手机发起 Read Request 获取当前时间;
  2. 固件解析出UTC时间,并与Flash中缓存的“上次播放结束时间”比较;
  3. 计算差值:
    - > 30分钟 → 新会话,重置计时器
    - ≤ 30分钟 → 继续累计
  4. 若累计达90分钟,触发TTS语音提示。

这样一来,既避免了关机断连后计时丢失的问题,又防止了跨天、跨设备场景下的逻辑混乱。

而且全程不需要维持长连接,也不需要频繁唤醒主控MCU——真正做到了 低功耗 + 高准确率


工程实践中的那些坑 🚧

听起来很美好,但在实际落地时,有几个细节必须小心处理:

🔹 时间有效性校验不能少!

试想一下:如果手机发来一个年份为 1970 9999 的时间包,耳机直接拿来用,岂不是整个系统时间崩了?

所以解析时一定要加防护:

if (ct->year < 2020 || ct->month == 0 || ct->month > 12 ||
    ct->day == 0 || ct->day > 31) {
    LOG_ERROR("Invalid time received");
    return;
}

建议设置合理范围(如2020–2050),防止异常数据注入。

🔹 时区信息要持久化保存

虽然CTS允许携带时区偏移,但并非每次都会发送。因此耳机应在首次同步后,将有效的时区偏移量写入非易失存储(如EEPROM或Flash分区),下次启动时优先使用本地记录。

否则可能出现“UTC转本地时间失败”的尴尬情况。

🔹 能不用Polling就不用!

有些人图省事,每隔几分钟主动去读一次时间。这种轮询方式看似简单,实则严重拖累续航。

正确姿势是: 启用CCC描述符,打开Notify功能 ,让手机在时间变更时主动推过来。这才是真正的“事件驱动”。

🔹 多设备切换怎么办?

当你从iPhone切到iPad时,两台设备时间可能略有差异。此时应以“最后连接设备”的时间为权威源,并在APP端给出提示:“请保持设备时间同步”,引导用户养成良好习惯。


它的意义,远不止“显示时间”那么简单 🌟

很多人以为Current Time只是用来做“时间同步”的工具,但实际上,它正在成为 智能音频决策系统的基石

在Cleer Arc5这样的高端耳机中,它支撑起了多个高级功能:

  • 🌞 昼夜模式自动切换 :白天开启通透模式,夜晚增强降噪强度
  • 基于时间段的佩戴检测优化 :夜间降低传感器灵敏度,减少误唤醒
  • 🧠 AI自适应算法输入 :为未来OTA升级的个性化音效提供时间维度特征
  • 📅 健康行为分析建模 :统计每日高频使用时段,生成听力保护建议

换句话说, 时间是一个上下文信号 。有了它,耳机才能真正理解“你现在处于什么情境”,从而做出更人性化的响应。


写在最后:小特征,大智慧 🎯

0x2A2B 只是一个小小的GATT特征值,没有炫酷的名字,也没有复杂的加密机制。但它代表了一种工程哲学: 善用标准,少造轮子,专注体验

在TWS耳机越来越卷音质、降噪、续航的今天,Cleer Arc5选择在“看不见的地方”下功夫——用一个标准化的时间服务,打通设备协同的毛细血管,构建真正懂你的智慧听觉体验。

也许未来的某一天,当你走进办公室耳机自动切换至“会议模式”,或者傍晚跑步时主动推荐运动歌单,背后的起点,就是这一次轻量级的 Read Request

🎧 技术的魅力,往往藏在最安静的通信里。

而我们要做的,就是让这些“安静的力量”,被更多人听见。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

def cleanup_short_polylines(self, job, step, backup_layer, min_length=8.0): """ 删除总长度 < min_length 的整根 polyline(由多个 line 与arc 组成) 利用 feature['length'] 避免重复计算几何距离 """ features = self.ico.GetFeatures(job, step, backup_layer) # 提取所有 line 和 arc valid_features = [f for f in features if f['type'] in ('line', 'arc')] if not valid_features: return # #######构建拓扑图 端点->线段#### graph = defaultdict(list) # 坐标 -> 相邻坐标 coord_to_feats = defaultdict(list) # 坐标 -> (index, feature) for idx, feat in enumerate(valid_features): xs = round(feat['xs'], 6) ys = round(feat['ys'], 6) xe = round(feat['xe'], 6) ye = round(feat['ye'], 6) start = (xs, ys) end = (xe, ye) graph[start].append(end) graph[end].append(start) coord_to_feats[start].append((idx, feat)) coord_to_feats[end].append((idx, feat)) visited_coords = set() short_polyline_seeds = [] # 存储每根短 polyline 的一个起点用于删除 ##########BFS 遍历每个连通组件########### for coord in list(graph.keys()): if coord in visited_coords: continue queue = deque([coord]) current_visited_coords = set() current_feat_indices = set() while queue: curr = queue.popleft() if curr in current_visited_coords: continue current_visited_coords.add(curr) for neighbor in graph[curr]: # 查找连接 curr和neighbor 的 feature found = False for feat_idx, feat in coord_to_feats[curr]: if feat_idx in current_feat_indices: continue # 判断是否连接 curr 和 neighbor s = (round(feat['xs'],6), round(feat['ys'],6)) e = (round(feat['xe'],6), round(feat['ye'],6)) connected = (curr == s and neighbor == e) or (curr == e and neighbor == s) if connected: current_feat_indices.add(feat_idx) found = True break if not found: continue # 没有可用边 if neighbor not in current_visited_coords: queue.append(neighbor) # 标记这些点已被全局访问 visited_coords.update(current_visited_coords) if not current_feat_indices: continue ################计算当前 polyline 总长度########### total_length = sum(valid_features[i]['length'] for i in current_feat_indices) if total_length < min_length: # 找一个端点作为选择依据(度为1的点) endpoints = [pt for pt in current_visited_coords if len(graph[pt]) == 1] seed = endpoints[0] if endpoints else next(iter(current_visited_coords)) short_polyline_seeds.append(seed) ##########删除:选中所有短 polyline######### if short_polyline_seeds: self.incam.COM("sel_clear_feat") for x, y in short_polyline_seeds: self.incam.COM(f"sel_polyline_feat,operation=select,x={x:.6f},y={y:.6f},tol=1") self.incam.COM("sel_delete") 你重新处理了run函数中删除线的做法,但是上面这个函数没有考虑到,这个函数是否也有类似的隐患呢?如果有,改正
11-02
#!/bin/env python # 必须写在第一行 # -*- coding: utf-8 -*- ################################################# # Author: songwenhua # Function:客户阻抗线连续性检查 # Date: 2025-11-05 # v1.00 songwenhua 用户需求号: 2482 任务ID:2100 # /home/incam/Desktop/scripts/shendan/check_Y_zk_line # LOAD_MODE__ import os import re import sys from py39COM import Gateway, InCAM from py39Tools import TableWidget from ICO import ICO from ICNET import ICNET from XMLParse import XMLParsePlus from messageBox import messageBox from EqHelper import EqHelper from PyQt5.QtWidgets import QWidget, QApplication, QComboBox from collections import defaultdict, deque from functools import partial import math # from img import apprcc_rc from pprint import pprint import time class chk_Conti_zkline: def __init__(self): # self.setWindowFlags(Qt.WindowStaysOnTopHint) self.JOB = os.environ.get('JOB', None) self.STEP = os.environ.get('STEP', None) # self.chklist = os.environ.get('chklist', None) # --启动pycharm.sh时,里面有export INCAM_DEBUG=yes设置此环境变量 INCAM_DEBUG = os.getenv('INCAM_DEBUG', None) # 接口定义 if INCAM_DEBUG == 'yes': # 通过genesis gateway命令连结pid进行会话,不用在genesis环境下运行,直接用gateway的方式,可在pycharm环境下直接debug self.incam = Gateway() # 方法genesis_connect通过查询log-genesis文件获取的料号名 self.JOB = self.incam.job_name self.STEP = self.incam.step_name self.pid = self.incam.pid else: self.incam = InCAM() self.pid = os.getpid() self.ico = ICO(incam=self.incam) self.icNet = ICNET(incam=self.incam) self.jobName = self.ico.SimplifyJobName(jobName=self.JOB) self.dbSite = self.ico.GetDBSite(JOB=self.JOB) self.SITE = self.ico.GetSite(JOB=self.JOB) self.getinfomation_dict = defaultdict(defaultdict) self.ballPadIndexList = defaultdict(list) self.layerMatrix = self.ico.GetLayerMatrix() self.zkLineIndex = defaultdict(defaultdict) self.step_list = self.ico.GetStepList() layer_list = self.ico.GetLayerList() self.zk_siglay = {} self.zkLayerList = self.getZKLayer() # 获取阻抗线层别 self.run() def getZKLayer(self): """ 获取阻抗线层别 :return: 阻抗线层别 """ zkLayerList = list() layerList = self.layerMatrix['allLay'] # self.ico.GetLayerList() for lay in layerList: for t in ('s', 'ss', 'gsg', 'gssg'): # pattern_tmp = re.compile(r'^((?:un-)?(\d+|[tb])zk)-(\d+\.?\d*)-(%s)-?(\d+\.?\d*)?-?(\d+\.?\d*)?-?(\d+\.?\d*)?$' % t)#存在un开头的阻抗层 pattern_tmp = re.compile(r'^((\d+|[tb])zk)-(\d+\.?\d*)-(%s)-?(\d+\.?\d*)?-?(\d+\.?\d*)?-?(\d+\.?\d*)?$' % t) matchObj = re.match(pattern_tmp, lay) if matchObj: matchList = matchObj.groups() zkLayerList.append(lay) self.getinfomation_dict[lay]['layer'] = matchList[0] print(zkLayerList) if len(zkLayerList) > 0: SignalLayer_list = self.layerMatrix['sigAllLay'] for i in range(len(SignalLayer_list)): if i == 0: self.zk_siglay['tzk'] = SignalLayer_list[0] elif i == (len(SignalLayer_list) - 1): self.zk_siglay['bzk'] = SignalLayer_list[-1] else: self.zk_siglay[str(i + 1) + 'zk'] = SignalLayer_list[i] # self.zk_siglay['un-' + str(i + 1) + 'zk'] = SignalLayer_list[i]#还要加一个un-开头的对应线路层! print("zk_siglay : %s" % self.zk_siglay) for lay in self.getinfomation_dict.keys(): for key in self.zk_siglay: if self.getinfomation_dict[lay]['layer'] == key: self.getinfomation_dict[lay]['sigLayerNum'] = str(SignalLayer_list.index(self.zk_siglay[key]) + 1) self.getinfomation_dict[lay]['sigLayerName'] = str(self.zk_siglay[key]) break else: messageBox.showMessage(message='没有识别到阻抗线层别,请规范命名(eg:tzk-gssg-100)', bitmap='critical') exit() return zkLayerList def get_unique_endpoints_single_layer(self, step, backup_layer): """ 仅从单个 backup_layer 提取端点(仅本层内统计) """ self.ico.ClearLayer() self.ico.DispWork(backup_layer) features = self.ico.GetFeatureFullInfo(step, layer=backup_layer) if not features: return [] layer_endpoints = [] for feat in features: ftype = feat['type'] if ftype in ('line', 'arc'): xs, ys = feat['x0'], feat['y0'] xe, ye = feat['x1'], feat['y1'] layer_endpoints.append((round(xs, 6), round(ys, 6))) layer_endpoints.append((round(xe, 6), round(ye, 6))) point_count = defaultdict(int) for pt in layer_endpoints: point_count[pt] += 1 unique_ends = [pt for pt, cnt in point_count.items() if cnt == 1] unique_ends.sort(key=lambda p: (p[0], p[1])) print(f"{backup_layer}: 找到 {len(unique_ends)} 个唯一端点") return unique_ends def selectun_separate(self, backup_layer, zkLay, target_temp_layer): """ 在原始信号层上选出与 backup_layer 接触的所有非 line/arc 图形, 复制到指定的 target_temp_layer 并合并成 surface。 """ try: zk2SigLay = self.getinfomation_dict[zkLay]['sigLayerName'] except KeyError as e: return False self.ico.ClearAll() self.incam.COM("adv_filter_reset") self.incam.COM(f"display_layer, name={zk2SigLay}, display=yes") self.incam.COM(f"work_layer, name={zk2SigLay}") self.incam.COM("set_filter_type,filter_name=,lines=no,pads=yes,surfaces=yes,arcs=no,text=yes") self.incam.COM("set_filter_polarity,filter_name=,positive=yes,negative=yes") self.incam.COM(f"sel_ref_feat,layers={backup_layer},use=filter,mode=touch,pads_as=shape," "f_types=line;pad;surface;arc;text,polarity=positive;negative,include_syms=,exclude_syms=") self.incam.COM("get_select_count") count = int(self.incam.COMANS) if count == 0: return False # 复制到专属临时层 self.incam.COM(f"sel_copy_other, dest=layer_name, target_layer={target_temp_layer}, " "invert=no, dx=0, dy=0, size=0, x_anchor=0, y_anchor=0") # 切换到目标临时层并合并 self.ico.ClearAll() self.ico.DispWork(target_temp_layer) self.ico.TrySelContResize([2.54, 5, 10]) return True def selectBallPad(self, zkLay, x, y): """ 判断指定坐标点是否与背面ball pad导通 :param zkLay: 阻抗层名称 :param x: 检查点的x坐标 :param y: 检查点的y坐标 :return: bool 是否导通 """ # 阻抗层对应的线路层 zk2SigLay = self.getinfomation_dict[zkLay]['sigLayerName']#self.getinfomation_dict得到的是原始阻抗层,而不是_bak备份阻抗层 sigBot = self.layerMatrix['sigAllLay'][-1] #线路层最后一层,背面 # 设置工作环境 self.ico.ClearAll() self.ico.ResetFilter() self.incam.COM(f'affected_layer, name={sigBot}, mode=single, affected=yes') self.incam.COM(f'display_layer, name={zk2SigLay}, display=yes') self.incam.COM(f'work_layer, name={zk2SigLay}') # 执行选择 self.incam.COM("sel_clear_feat") #清除之前的选择 self.incam.COM("clear_highlight") self.incam.COM(f'sel_board_net_feat, operation=select, x={x}, y={y}, tol=1, use_ffilter=no') # 获取选中特征 featureFullInfo_sigBot = self.ico.GetFeatureFullInfo(step=self.STEP, layer=sigBot, mode='select') # 检查背面ball pad # 对于背面sig选到的物体,做个筛选,如果是pad且是.smd属性,说明选到了ball pad selectPadList = [info for info in featureFullInfo_sigBot if info['type'] == 'pad' and (info['attr'] == '.smd' or info['attr'] == '.smd,.bga' or info['attr'] == '.smd,.lga' or info['attr'] == '.smd,.smt_pad' or info['attr'] == '.smd,.test_pad')] # 20241209 新增ball pad属性是.smd.bga和.smd.lga return len(selectPadList) > 0 # 如果找到ball pad则返回True def cleanup_short_polylines(self, step, backup_layer, min_length=8.0): """ 删除总长度 < min_length 的整根 polyline(由多个 line 与 arc 组成) 利用 feature['length'] 避免重复计算几何距离 """ features = self.ico.GetFeatures(step, backup_layer) # 提取所有 line 和 arc valid_features = [f for f in features if f['type'] in ('line', 'arc')] if not valid_features: return # 构建拓扑图 graph = defaultdict(list) # 坐标 -> 相邻坐标 edge_map = {} # (start, end) -> feature_index for idx, feat in enumerate(valid_features): xs = round(feat['xs'], 3) ys = round(feat['ys'], 3) xe = round(feat['xe'], 3) ye = round(feat['ye'], 3) start = (xs, ys) end = (xe, ye) graph[start].append(end) graph[end].append(start) edge_map[(start, end)] = idx edge_map[(end, start)] = idx visited_coords = set() short_polyline_seeds = [] # 存储每根短 polyline 的一个起点用于删除 # BFS 遍历每个连通组件 for coord in graph: if coord in visited_coords: continue queue = deque([coord]) current_visited = set() current_feature_indices = set() while queue: curr = queue.popleft() if curr in current_visited: continue current_visited.add(curr) for neighbor in graph[curr]: edge_key = (curr, neighbor) if edge_key in edge_map: feat_idx = edge_map[edge_key] if feat_idx not in current_feature_indices: current_feature_indices.add(feat_idx) if neighbor not in current_visited: queue.append(neighbor) visited_coords.update(current_visited) if not current_feature_indices: continue # 计算当前 polyline 总长度 total_length = sum(valid_features[i]['length'] for i in current_feature_indices) if total_length < min_length: # 找一个端点作为选择依据(度为1的点) endpoints = [pt for pt in current_visited if len(graph[pt]) == 1] seed = endpoints[0] if endpoints else next(iter(current_visited)) short_polyline_seeds.append(seed) # 删除所有短 polyline if short_polyline_seeds: self.incam.COM("sel_clear_feat") for x, y in short_polyline_seeds: self.incam.COM(f"sel_polyline_feat,operation=select,x={x:.3f},y={y:.3f},tol=1") self.incam.COM("get_select_count") current_count = int(self.incam.COMANS) # 一次性删除所有选中的图形 if current_count > 0: self.incam.COM("sel_delete") def delete_surfaces_covering_points(self, points, layer_new): """ 删除临时层中包含给定点的 surface :param points: list of (x, y) """ self.ico.ClearLayer() self.ico.DispWork(layer_new) self.incam.COM("sel_clear_feat") # 初始清空 tol = 0.05 # 匹配点的 filter 容差 selected_count = 0 for x, y in points: #用 FilterAreaXY 查找落在点附近的图形 # self.incam.COM("sel_clear_feat") # 临时清空 # x1, y1 = x - tol, y - tol # x2, y2 = x + tol, y + tol # count_in_area = self.ico.FilterAreaXY( # isInside='yes', # intersect='yes', # x1=x1, # y1=y1, # x2=x2, # y2=y2 # ) self.incam.COM(f"sel_single_feat, operation=select, x={x}, y={y}, tol=0.1") # self.incam.COM(f"sel_single_feat,operation=select,x={x},y={y},tol=35.775,cyclic=yes,clear_prev=no") self.incam.COM("get_select_count") current_count = int(self.incam.COMANS) # 一次性删除所有选中的图形 if current_count > 0: self.incam.COM("sel_delete") # print(f"成功删除了 {current_count} 个覆盖末端点的 surface") # else: # print("未找到覆盖末端点的 surface") @staticmethod def extract_start_points(data): """ 支持输入为 str 或 list[str],统一处理并提取每个图形的 #OB 起点 """ if isinstance(data, list): # 如果是列表,用换行符拼接成字符串 content = "\n".join(data) elif isinstance(data, str): content = data else: raise TypeError("Input must be str or list of str") start_points = [] # 按 '#数字 #S' 分割出各个图形块 blocks = re.split(r'#(\d+)\s+#S', content)[1:] # 每两项:编号 + 内容 for i in range(0, len(blocks), 2): block_content = blocks[i+1] match = re.search(r'#OB\s+([-\d.]+)\s+([-\d.]+)', block_content) if match: x = float(match.group(1)) y = float(match.group(2)) start_points.append((x, y)) return start_points def get_coords(self, feature): """ 从任意图元中快速提取一个坐标点 (x, y) 特别处理 surface 的 orig 字段 """ # 处理 surface 的 orig if feature.get('type') == 'surface' and isinstance(feature.get('orig'), list): pattern = r'#O[BS]\s+([-\d.]+)\s+([-\d.]+)' for line in feature['orig']: match = re.search(pattern, line) if match: x = float(match.group(1)) y = float(match.group(2)) return round(x, 3), round(y, 3) # 返回第一个有效坐标即可 return None def run(self): """ 主执行函数:客户阻抗线连续性检查(分层处理 + 四步清理) 改进版:不合并到 tmp_total,逐层独立判断 temp_layer_n 是否为空 并且:仅将实际处理过的层纳入结果提示 """ job = self.JOB step = 'edit' unthrough_lay = [] # 存在与 ball pad 导通风险的层 backup_layers = [] temp_layers = [] self.ico.ClearAll() self.zkLineIndex = defaultdict(lambda: defaultdict(list)) ######## 1. 创建备份层 ################## for idx, zklay in enumerate(self.zkLayerList): zkLay_bak = f'{zklay}_bak' layer_n = f'tmp_{zklay}' temp_layers.append(layer_n) self.zkLineIndex[zklay]['zkLaybak'] = zkLay_bak self.zkLineIndex[zklay]['tempLayer'] = layer_n self.ico.DelLayer(layer_list=[zkLay_bak, layer_n]) self.ico.DispWork(zklay) self.incam.COM(f'sel_copy_other,dest=layer_name,target_layer={zkLay_bak},' 'invert=no,dx=0,dy=0,size=0,x_anchor=0,y_anchor=0') backup_layers.append(zkLay_bak) ######## 2. 分层处理:复制非line/arc图形到独立临时层,并删末端覆盖surface ######## for backup_layer in backup_layers: original_zklay = backup_layer.replace('_bak', '') temp_layer_n = self.zkLineIndex[original_zklay]['tempLayer'] print(f"处理 {backup_layer} => 使用临时层 {temp_layer_n}") has_copied = self.selectun_separate(backup_layer, original_zklay, temp_layer_n) if not has_copied: continue unique_ends = self.get_unique_endpoints_single_layer(step, backup_layer) if not unique_ends: print(f"{backup_layer}: 无唯一端点,跳过 surface 删除") else: self.delete_surfaces_covering_points(unique_ends, temp_layer_n) ######## 3. 检查是否连接到背面 Ball Pad(逐层判断)######## for backup_layer in backup_layers: original_zklay = backup_layer.replace('_bak', '') temp_layer_n = self.zkLineIndex[original_zklay]['tempLayer'] self.ico.ClearAll() self.ico.DispWork(backup_layer) self.ico.DispLayer(temp_layer_n) # 设置过滤器:只选 line self.incam.COM("adv_filter_reset") self.incam.COM("set_filter_type, lines=yes, pads=no, surfaces=no, arcs=no, text=no") self.incam.COM("set_filter_polarity, positive=yes, negative=yes") # 选择与 final_tmp_layer 接触的 line self.incam.COM(f"sel_ref_feat,layers={temp_layer_n},use=filter,mode=touch,pads_as=shape," "f_types=line;pad;surface;arc;text,polarity=positive;negative,include_syms=,exclude_syms=") features = self.ico.GetFeatureFullInfo(step, layer=backup_layer, mode='select') if not features: continue connection_to_ballpad = False problem_coords = [] test_points = [(round(feat['x0'], 6), round(feat['y0'], 6)) for feat in features if feat['type'] in ('line', 'arc')] test_points = list(set(test_points)) for xs, ys in test_points: if self.selectBallPad(original_zklay, xs, ys): connection_to_ballpad = True problem_coords.append((xs, ys)) if connection_to_ballpad: unthrough_lay.append({ 'layer': backup_layer, 'coords': problem_coords }) ######## 统一弹窗提示 ###### if unthrough_lay: # 提取所有出问题的原始层名(去掉 '_bak' 后缀) problem_layers = [item['layer'].replace('_bak', '') for item in unthrough_lay] layer_list_str = ', '.join(problem_layers) messageBox.showDialog( title='警告', text=f'{layer_list_str}层存在阻抗线不连续,此阻抗线与背面 Ball Pad 导通:\n\n' '1. 人为确认是否为分流设计;\n' '2. 如为分流设计,需与客户确认是否设计异常,与客户EQ此组阻抗通过测量科邦控阻值,如客户不同意,内部策划列难点评估测科邦', bitmap='warning', buttons=['OK'], defaultButton='OK' ) sys.exit(0) # sys.exit(0) ######### 4. 清理阶段:三大删除操作(仅对非空 temp_layer_n 执行)######## self.ico.ClearAll() processed_empty = [] # 被处理过且清空的层 processed_valid = [] # 被处理过但仍保留线路的层 skipped_layers = [] # temp_layer_n 为空,跳过的层 for backup_layer in backup_layers: original_zklay = backup_layer.replace('_bak', '') temp_layer_n = self.zkLineIndex[original_zklay]['tempLayer'] current_step = 'edit' # --- 判断是否跳过 --- temp_info = self.ico.GetFeatureFullInfo(current_step, temp_layer_n) if not temp_info or len(temp_info) == 0: print(f"跳过处理:{backup_layer} 对应 {temp_layer_n} 为空") skipped_layers.append(original_zklay) continue # 完全跳过,不执行任何操作,也不做判断 ############################################################# # 从现在开始:只有 temp_layer_n 非空的层才会进入下面流程 ############################################################# self.ico.ClearAll() self.incam.COM("sel_clear_feat") self.ico.DispWork(backup_layer) self.ico.DispLayer(temp_layer_n) # --- 步骤1: 删除与 temp_layer_n 接触的所有整根 polyline --- print(f"[{backup_layer}] 步骤1: 删除与 {temp_layer_n} 接触的整根 polyline...") self.ico.ResetFilter() self.incam.COM(f"sel_ref_feat,layers={temp_layer_n},use=filter,mode=touch,pads_as=shape," "f_types=line;pad;surface;arc;text,polarity=positive;negative") features_touch = self.ico.GetFeatureFullInfo(current_step, layer=backup_layer, mode='select') touch_start_points = set() for feat in features_touch: if feat['type'] in ('line', 'arc'): xs, ys = round(feat['x0'], 6), round(feat['y0'], 6) touch_start_points.add((xs, ys)) if touch_start_points: self.incam.COM("sel_clear_feat") # print("11111111111") for x, y in touch_start_points: self.incam.COM(f"sel_polyline_feat,operation=select,x={x:.3f},y={y:.3f},tol=1") self.incam.COM("get_select_count") selected_count = int(self.incam.COMANS) if selected_count > 0: self.incam.COM("sel_delete") print(f"{backup_layer}: 删除了 {selected_count} 根与 {temp_layer_n} 接触的 polyline") # sys.exit(0) # ''' # --- 步骤2: 删除长度 < 8mm 的短 polyline --- print(f"[{backup_layer}] 步骤2: 删除长度 < 8mm 的短 polyline...") self.cleanup_short_polylines(step=current_step, backup_layer=backup_layer, min_length=8.0) # --- 步骤3: 删除两端都不连接 ball pad 的孤立段 --- print(f"[{backup_layer}] 步骤3: 删除孤立段(不连 ball pad)...") remaining_features = self.ico.GetFeatureFullInfo(step=current_step, layer=backup_layer) delete_seeds = [] for feat in remaining_features: if feat['type'] not in ('line', 'arc'): continue xs, ys = round(feat['x0'], 6), round(feat['y0'], 6) xe, ye = round(feat['x1'], 6), round(feat['y1'], 6) start_ok = self.selectBallPad(original_zklay, xs, ys) end_ok = self.selectBallPad(original_zklay, xe, ye) if not (start_ok or end_ok): delete_seeds.append((xs, ys)) if delete_seeds: self.incam.COM("sel_clear_feat") for x, y in delete_seeds: self.incam.COM(f"sel_polyline_feat,operation=select,x={x:.3f},y={y:.3f},tol=1") self.incam.COM("get_select_count") current_count = int(self.incam.COMANS) if current_count > 0: self.incam.COM("sel_delete") print(f"{backup_layer}: 删除了 {len(delete_seeds)} 根孤立段") # --- 步骤4: 仅当该层被实际处理过时,才判断其清理后状态 --- remaining_after = self.ico.GetFeaturesPro(job=job, step=current_step, layer=backup_layer) has_valid_line = any(f['type'] in ('line', 'arc') for f in remaining_after) if has_valid_line: processed_valid.append(original_zklay) else: processed_empty.append(original_zklay) # 注意:这里不在 continue 路径上 -> 只有处理过的层才走到这里 ######## 5. 输出分类提示信息(仅包含实际处理过的层)######## msg_parts = [] if processed_empty: msg_parts.append( f'{", ".join(processed_empty)} 存在阻抗线不连续,同组无可测阻抗线,与客户EQ此组阻抗通过测量科邦卡控阻值,如客户不同意,内部策划列难点评估测科邦' ) if processed_valid: msg_parts.append( f'{", ".join(processed_valid)} 存在阻抗线不连续,同组存在可测阻抗线,EQ客户Y型阻抗线follow同组可测阻抗线进行调整' ) if not msg_parts: messageBox.showMessage( message="本次未处理任何阻抗层(所有 tmp_zklay 均为空)", bitmap='warning' ) else: messageBox.showDialog( title='结果', text='\n\n'.join(msg_parts), bitmap='warning', buttons=['OK'], defaultButton='OK' ) sys.exit() # ''' if __name__ == "__main__": app = QApplication(sys.argv) analyzer = chk_Conti_zkline() 从开发的角度说明是如何实现这个代码功能的,使用什么功能判断、什么功能实现。一定要有逻辑
11-26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值