# -*- coding:utf-8 -*- import os from BaseLog import CBaseLog from ExtraData import CExtraData from OptionExcel import COptionExcel from OptionConfig import COptionConfig from OptionFocus import COptionFocus from OptionOCR import COptionOCR from ssat_sdk.tv_detect import * from ssat_sdk.device_manage.capturecard_manager import CCardManager from ssat_sdk.utils.string_util import strcmp g_level = ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh', 'Eighth', 'Ninth', 'Tenth', 'Eleventh', 'Twelfth'] def strSplit(text): ret = [] str_int = '' str_ch = '' ch_last = ' ' for ch in text: if 47 < ord(ch) < 58: str_int += ch if str_ch.__len__(): ret.append(str_ch) str_ch = '' else: if 47 < ord(ch_last) < 58 and ch == '.': str_int += ch if str_ch.__len__(): ret.append(str_ch) str_ch = '' else: str_ch += ch if str_int.__len__(): ret.append(str_int) str_int = '' ch_last = ch if str_ch.__len__(): ret.append(str_ch) if str_int.__len__(): ret.append(str_int) return ret # 注意:所有不对外暴露的变量和函数需要私有化,以明确哪些接口和参数是对外的。 # 这样便于后期维护时,根据对外的变量和函数来做处理。 class COptionAction(CBaseLog): # ==============设备对象============== # # 红老鼠遥控对象; __redRat3 = TvOperator() # 创建视频采集对象 __ccard = CCardManager() # 图片切割对象 __imgCMP = ImageCMP() def __init__(self, optionName, optionValue, optionConfig, optionExcel): CBaseLog.__init__(self) # 层级位置; self.__pos = 0 # 目标option; self.__optionName = optionName # __optionValue可空; self.__optionValue = optionValue self.__optionExcel = optionExcel self.__optionConfig = optionConfig # 焦点定位及文字识别; self.__optionFocus = COptionFocus(optionConfig) self.__optionOCR = COptionOCR(optionConfig, optionExcel) if self.__optionExcel is None: self.error(u"表格对象空") # ==============常用对象数据==============; self.__optionPaths = self.__optionExcel.getOptionPaths(self.__optionName) # 如果__optionValue空则不取value表任何内容; if self.__optionValue != "": self.__optionValueInfo = self.__optionExcel.getOptionValueInfo(self.__optionName, self.__optionValue) self.__optionInfo = self.__optionExcel.getOptionInfo(self.__optionName) # 当前状态下的变量,与__pos对应; self.__curOptionName = '' self.__curOptionInfo = None # 到达value节点后,记录value值(一般只用于range() 数值记录,其他值无意义); self.__optionValueText = "" # 获取一次当前层级信息; self.getCurOptionInfo() @property def pos(self): return self.__pos @property def curOptionName(self): return self.__curOptionName @property def curOptionInfo(self): return self.__curOptionInfo @property def optionValueText(self): return self.__optionValueText ''' 函数:截图; 参数:无 返回:无 ''' def takePicture(self): img = os.path.join(getSATTmpDIR(), "menutree_runpath.png") COptionAction.__ccard.takePicture(img) if os.path.exists(img): self.error(u"截图失败:%s" % img) return img ''' 函数:调用根节点快捷键(中间节点不需要快捷键;); 参数: 返回: ''' def callFirstOptionShortCutKey(self): if 'shortcut_key' in self.__optionPaths['First']: self.sendKey(self.__optionPaths['First']['shortcut_key']) else: self.sendKey(self.__optionPaths['First']['parent']) self.warn(u"表格没有shortcut_key字段,执行默认的parent按键:%s" % self.__optionPaths['First']['parent']) ''' 函数:调用当前结点的toparent_key 参数: 返回: ''' def callCurOptionBackKey(self, curOptionName): curOptionInfo = self.__optionExcel.getOptionInfo(curOptionName) if 'toparent_key' in self.__optionPaths[curOptionInfo['level']]: self.sendKey(self.__optionPaths[curOptionInfo['level']]['toparent_key']) else: self.error(u"表格没有toparent_key字段,执行默认按键return") self.sendKey('return') ''' 函数:是否在父节点菜单上。一般在执行了callFirstOptionShortCutKey后调用; 参数: 返回: 注意:由于所有父节点上的子项都共用一个图片定位参数,所以只要随意一个父节点的子项option即可获取定位参数; 示例: 测试:。 ''' def isOnFirstOption(self): pic = self.takePicture() return self.__optionFocus.findFocusByIcon(pic, self.__optionPaths['First']['option'])[0] ''' 函数:是否在当前节点(移动后,判断是否移动到下一目标节点)上. 说明: 每次移动到下一目标节点(option)上时,self.__pos + 1,表示移动到下一层路径。 当self.__pos >= self.__optionPaths.__len__()时,表示到达value表格; 所以,该类的重点在self.__pos的移动; 参数: 返回:Boolean, 识别的文本/数字; 示例: 测试:。 ''' def isOnTargetNode(self): # 是否在value表中; isValueSheet = self.isOnValueSheet() self.info(u"当前层级在:%s" % ("value表" if isValueSheet else "路径表")) # 析出参数; if isValueSheet: curLevel = 'value' curParent = self.__optionPaths[g_level[self.__pos-1]]['parent'] curOption = self.__optionValueInfo['option'] curOthers = self.__optionValueInfo['others'] else: curLevel = g_level[self.__pos] curParent = self.__optionPaths[curLevel]['parent'] curOption = self.__optionPaths[curLevel]['option'] curOthers = self.__optionPaths[curLevel]['others'] self.info("当前[%s]others=[%s]" % (curOption, curOthers)) if curOthers.__len__() == 0: curOthers = {} else: curOthers = json.loads(curOthers) firstParent = self.__optionPaths['First']['parent'] # 获取文本识别的参数; ocrConfigList = self.__optionConfig.getOptionOCRConfig(curOption) ocrThreshold = self.__optionConfig.getThresholdDict(firstParent) # 注意,此处使用firstParent; # 获取当前option的ocr值/value name下所有ocr值; if isValueSheet: if curOption.lower() == self.__optionName.lower(): optionTextList = self.__optionExcel.getOptionValueText(curOption, self.__optionValue) else: optionTextList = self.__optionExcel.getOptionValueText(curOption) else: optionTextList = self.__optionExcel.getOptionText(curOption) # 获取option下所有兄弟项的ocr:option字典内容; siblingTextDict = self.__optionExcel.getOptionAllSiblingItemDict(curOption, not isValueSheet) # 获取所有option兄弟项(包括自己)的ocr值; siblingTextList = list(siblingTextDict.keys()) # 是否获取数值文本; isNumberText = False # 如果是value表,且兄弟项文本为range # 注:value表中的option实际并没有兄弟项,取的是所有value项 if isValueSheet and siblingTextList.__len__(): if siblingTextList[0].startswith('range('): self.info(u"识别的内容是value表数字内容(range(0,xx)类型)") isNumberText = True # 清除之前的value值; self.__optionValueText = "" # 是否为静态焦点识别(动态则为跑马灯); if curOthers.__len__() and 'marquee' in curOthers: return self.__getDynamicPicText(curOption, optionTextList, siblingTextList, ocrConfigList, ocrThreshold, curOthers['marquee'], isNumberText, isValueSheet) else: return self.__getStaticPicText(self.takePicture(), curOption, optionTextList, siblingTextList, ocrConfigList, ocrThreshold, isNumberText, isValueSheet) # endif ''' 函数:是否移到目标节点上(在isOnOption后,判断__pos位置是否在__paths最后); 参数: 返回:Boolean. ''' def isOnTargetOption(self): return True if self.__pos == (self.__optionPaths.__len__() - 1) else False ''' 函数:当前节点是否在value sheet层级中; 参数:无 返回:Boolean ''' def isOnValueSheet(self): if self.__optionValue == "": # 该值空,表明不会移动到value sheet中; return False else: return True if self.__pos >= self.__optionPaths.__len__() else False ''' 函数:移动到下一兄弟节点; 参数:无 返回:无 注意: sendKey的等待时间太短,会导致画面未响应,截图时还是截上一状态的,比如0.1秒就很经常出现这问题。 同时,按键等待时间,应该有所区分。 如果不截图,可以不考虑sendKey的等待时间. ''' def move2NextSiblingNode(self): # 获取当前option信息; if self.getCurOptionInfo(): # 析出move key; optionMoveKey = self.__curOptionInfo['option_move_key'] if optionMoveKey.__len__() == 0: self.sendKey(self.__curOptionInfo['move_key'][1], 1, 1) else: self.sendKey(optionMoveKey[1], 1, 1) else: valueMoveKey = self.__optionValueInfo['move_key'] self.sendKey(valueMoveKey[1], 1, 1) ''' 函数:移动到上一兄弟节点 参数:无 返回:无 注意: sendKey的等待时间太短,会导致画面未响应,截图时还是截上一状态的,比如0.1秒就很经常出现这问题。 同时,按键等待时间,应该有所区分。 如果不截图,可以不考虑sendKey的等待时间. ''' def move2PrevSiblingNode(self): # 获取当前option信息; if self.getCurOptionInfo(): # 析出move key; optionMoveKey = self.__curOptionInfo['option_move_key'] if optionMoveKey.__len__() == 0: self.sendKey(self.__curOptionInfo['move_key'][0], 1, 1) else: self.sendKey(optionMoveKey[0], 1, 1) else: valueMoveKey = self.__optionValueInfo['move_key'] self.sendKey(valueMoveKey[0], 1, 1) ''' 函数:返回到父节点 参数:无 返回:无 注意: sendKey的等待时间太短,会导致画面未响应,截图时还是截上一状态的,比如0.1秒就很经常出现这问题。 同时,按键等待时间,应该有所区分。 如果不截图,可以不考虑sendKey的等待时间. ''' def back2ParentNode(self): # 获取当前option信息; if self.getCurOptionInfo(): backKey = self.__curOptionInfo['back_key'] else: backKey = self.__optionValueInfo['back_key'] if backKey.__len__() == 0: self.sendKey('return', 1, 1) else: self.sendKey(backKey, 1, 1) # 返回,自减; self.__pos -= 1 ''' 函数:进入当前节点,只对路径节点有效,value节点不处理; 参数:无 返回:无 ''' def enterNode(self): # 获取当前option信息; if self.getCurOptionInfo(): # 是否有等待时间 waitTime = self.__optionConfig.getParentWaitTime(self.__curOptionInfo['parent']) # 析出enter key; optionEnterKey = self.__curOptionInfo['option_enter_key'] if optionEnterKey.__len__() == 0: self.sendKey(self.__curOptionInfo['enter_key'], 1, waitTime) else: self.sendKey(optionEnterKey, 1, waitTime) # 进入下层,自增 self.__pos += 1 else: self.info(u"节点已在value上,无法再进入") ''' 函数:设置当前节点位置; 参数: pos: 外部节点位置值。 返回:无 注意:此函数用于外部创建两个【路径具体子集关系】的COptionAction对象,才可以使用此函数。 假设, a对象路径{p,p1,p2,p3,p4, v1},b = {p, p1, p2, p3, p4, p5, p6, v2}, c = {p, p2, p5, p6, v3} 其中, v表示value值,不属于路径。那么,a和b具有子集关系, a与c或b与c都不具有子集关系。 a移动到p4上,完成了v1设置后,a.back2ParentNode()后,此时如果要操作b并设置v2,就要b.SetCurPos(a.pos). ''' def setCurPos(self, pos): if pos < 0 or pos > self.__optionPaths.__len__(): self.error(u"pos值[%d]超出路径范围:[0-%d]" % (pos, self.__optionPaths.__len__())) return self.__pos = pos ''' 函数:设置目标option的值, 只设置数值型value和输入型value(选择型value不需要此步骤). 参数: 返回: 注意:此函数必须是已聚焦在目标value节点上,否则无效。 重要: 在此函数enter后,UI是否返回到上一层父节点上,还是停留在本层节点不返回。 建议在excel中配置这个关键信息,以便此函数可以正确更改self.__pos的值。 ''' def setOptionValue(self): self.info(u"【在此函数enter后,UI是否返回到上一层父节点上,还是停留在本层节点不返回。\ 建议在excel中配置这个关键信息,以便此函数可以正确更改self.__pos的值。】") if type(self.__optionValue) == str and self.__optionValue.__len__() == 0: self.error(u"[%s]的值为空,没有设置的值" % self.__optionName) return enterKey = self.__optionValueInfo['enter_key'] moveKey = self.__optionValueInfo['move_key'] valueText = self.__optionValueInfo['value_for_ocr'] others = self.__optionValueInfo['others'] if others.__len__(): others = json.loads(others) else: others = {} # 是否有按键延时值; duration = float(others['duration']) if "duration" in others else 0.1 # 是否为数字文本(特指:range(0, 100)); isNumberText = self.isNumberText(valueText) # 数值型value; if isNumberText: if moveKey[0] == 'input': # 将数值转成字符; optionValue = self.__optionValue if type(optionValue) == int or type(optionValue) == float: optionValue = str(self.__optionValue) # 再将字符转成list; chList = list(optionValue) self.sendKey(chList, 1, duration) else: # 相差值; num = int(self.__optionValue) - int(self.__optionValueText) # 正->往右或下,负->往左或上; self.sendKey(moveKey[1] if num > 0 else moveKey[0], abs(num), duration) elif moveKey[0] == 'input': # 将字符转成list; chList = list(self.__optionValue) self.sendKey(chList, 1, duration) # 最后,如果有进入键执行; if enterKey != 'default': self.sendKey(enterKey, 1, 0.1) ''' 函数: 参数: 返回: ''' def getCurOptionInfo(self): if self.__optionPaths.__len__() == 0: self.error(u"paths路径空") return False if self.__pos >= self.__optionPaths.__len__(): self.warn(u"已到达value节点,无法获取路径信息") return False # 只有第一次或层级移动了才需要更新; if self.__curOptionInfo is None or self.__pos != self.__curOptionInfo['layers']: self.__curOptionName = self.__optionPaths[g_level[self.__pos]]['option'] outResult, outData = self.__optionExcel.getOptionInfo(self.__curOptionName, self.__optionPaths) if outResult is False: return False self.__curOptionInfo = outData return True ''' 函数:检测路径是否有效; 参数: 返回:Boolean ''' def checkRunOptionPath(self): outData = self.__optionExcel.checkOptionPaths(self.__optionPaths) if str(outData[1]) == 'NoExit': self.error(u"表格中不存在到达Option:[%s]的路径,在表格中排查到达该Option路径" % self.__optionName) return False if str(outData[1]) == 'Fail': self.error(u"表格中到达Option:[%s]的路径出现数据断层找不到First层级,在表格中排查到达该Option路径" % self.__optionName) return False return True ''' 函数: 参数: 返回: ''' def isNumberText(self, textList): # 是否获取数值文本; isNumberText = False # 如果是value表,且兄弟项文本为range # 注:value表中的option实际并没有兄弟项,取的是所有value项 if self.isOnValueSheet() and textList.__len__(): if textList[0].startswith('range('): self.info(u"识别的内容是value表数字内容(range(0,xx)类型)") isNumberText = True return isNumberText ''' 函数:获取静态图片文本内容 参数: 注意: 返回:Boolean、文本识别内容 测试:。 ''' def __getStaticPicText(self, pic, optionName, optionTextList, siblingTextList, ocrConfigList, ocrThreshold, isNumberText, isValueSheet, aliveKey=None): # 获取图片焦点框; found, focusBox = self.__optionFocus.findFocusByIcon(pic, optionName, isValueSheet) if found is False: self.debug(u"未找到[%s]聚集框" % optionName) return False, None # 如果有鲜活键; self.sendAliveKey(aliveKey) # 获取文本区域框; textBox = self.__optionFocus.getFocusTextBox(optionName, focusBox, isValueSheet) # 配置文本图片路径,保存文本区域截图; text_pic = os.path.join(getSATTmpDIR(), "meuttree_area_text.png") self.__imgCMP.saveCropPic(pic, text_pic, textBox) if not os.path.exists(text_pic): self.error(u"%s截取文本图片失败:%s" % (optionName, text_pic)) return False, None # 是否在某个兄弟项中; isOnSibling = False # 遍历所有ocr识别选项; for ocrConfig in ocrConfigList: # 如果有鲜活键; self.sendAliveKey(aliveKey) # 识别出当前聚焦文本; curFocusText = self.__optionOCR.getImageText(text_pic, ocrConfig, ocrThreshold) # 判断识别文本是来正常; if curFocusText == "ERR" or curFocusText.__len__() == 0: continue # 转成小写; curFocusText = curFocusText.lower() self.info("[%s]当前识别出的文本=%s" % (optionName, curFocusText)) # 是否取数字文本(肯定在value节点上); if isNumberText is True: # 特殊情况处理:某些情况下,会将包含数字以外的区域一起识别; curFocusText = curFocusText.strip('>') # 将数字分组 numberTextList = strSplit(curFocusText) # 只判断最后一位是否为数字; if numberTextList.__len__() < 1: self.error(u"当前识别的文本不是数字文本:%s" % curFocusText) continue try: numberText = float(numberTextList[numberTextList.__len__() - 1]) # 记录value值; self.__optionValueText = numberText return True, numberText except Exception: continue else: # 当前option识别的文本与所有兄弟项文本比较; for siblingText in siblingTextList: # 转为小写,保证所有比较都是小写; siblingText = siblingText.lower() # 兄弟项文本是否被包含在curFocusText中或相同; if siblingText in curFocusText or strcmp(siblingText, curFocusText): isOnSibling = True self.info(u"当前焦点在[%s], 目标焦点为[%s]" % (siblingText, optionName)) # 再判断,该兄弟项是否为目标节点(curOption); for optionText in optionTextList: optionText = optionText.lower() # 若当前兄弟项为目标option返回True、文本; if strcmp(optionText, siblingText): return True, curFocusText # endif # endfor # 在兄弟项中,退出循环; break # endif # endfor if isOnSibling is False: self.error(u"未聚集到任何[%s]的兄弟项中" % optionName) else: self.info("未聚集到目标节点[%s],当前文本=%s" % (optionName, curFocusText)) return False, curFocusText # endif # endfor # 默认返回; return False, 0 if isNumberText else "" ''' 函数:获取动态图片文本内容 参数: 返回:返回:Boolean、文本识别内容 ''' def __getDynamicPicText(self, optionName, optionTextList, siblingTextList, ocrConfigList, ocrThreshold, marqueeDict, isNumberText, isValueSheet): # 判断图片是否动态:截图两次,判断两次文本内容是否相同; firstRetsult, firstText = self.__getStaticPicText(self.takePicture(), optionName, optionTextList, siblingTextList, ocrConfigList, ocrThreshold, isNumberText, isValueSheet) if firstRetsult is False: self.error(u"[%s]第一次截图未识别出聚焦框" % optionName) return False, None # 发送鲜活键, 保证界面鲜活; self.sendAliveKey(marqueeDict['alive_key']) # 第二次截图; secondRetsult, secondText = self.__getStaticPicText(self.takePicture(), optionName, optionTextList, siblingTextList, ocrConfigList, ocrThreshold, isNumberText, isValueSheet) if secondRetsult is False: self.error(u"[%s]第二次截图未识别出聚焦框" % optionName) return False, None # 发送鲜活键, 保证界面鲜活; self.sendAliveKey(marqueeDict['alive_key']) # 比较两文本是否相同; if firstText.__len__() and firstText == secondText: self.info(u"截图两次,文本(%s)识别相同,聚焦的不是跑马灯Option" % firstText) return False, firstText # 文本不相同,为动态图片; menuList = marqueeDict['menu'] # 如果只有一项跑马灯,且目标option亦在跑马灯列表中,则直接返回成功结果 if menuList.__len__() == 1 and (optionName in menuList): self.info(u"该层菜单只有一项跑马灯, 找到即成功返回") return True, firstText picList = [] # 如果有多项同级option都是跑马灯, 要成功识别文本需要间隔截图5次(大概会成功截图到最全的文本); for i in range(0, 5): picList.append(self.takePicture()) # 间隔多久截图; time.sleep(marqueeDict['sleep_time']) # 发送鲜活键; self.sendAliveKey(marqueeDict['alive_key']) ocrTextList = [] # 对截图进行文本识别分析; for pic in picList: result, text = self.__getStaticPicText(pic, optionName, optionTextList, siblingTextList, ocrConfigList, ocrThreshold, isNumberText, isValueSheet, marqueeDict['alive_key']) if result is True: ocrTextList.append(text) # 发送鲜活键; self.sendAliveKey(marqueeDict['alive_key']) # 过滤重复的字符; ocrTextList = self.__removeDuplicateString(ocrTextList) self.info(u"识别到的跑马灯ocr文字列表:%s" % ocrTextList) # 获取动态文本的option字典; dynamicOptionOcrDict = self.__getOptionInfoDict(menuList) self.info(u"获取到的跑马灯Option对应的ocr字典:%s" % dynamicOptionOcrDict) # 遍历:识别结果与xls结果进行比较; for dynamicOption in dynamicOptionOcrDict: dynamicOptionOcrList = dynamicOptionOcrDict[dynamicOption] for dynamicOptionOcr in dynamicOptionOcrList: # 只要有3张满足,判断找到option; count = 0 for ocrText in ocrTextList: if ocrText.lower() in dynamicOptionOcr: count += 1 if count >= 3 and optionName == dynamicOption: return True, ocrText else: self.info(u"当前聚焦的跑马灯实际为:%s" % dynamicOption) return False, ocrText # endfor # endfor self.info("未能识别到当前聚焦的跑马灯Option") return False, 0 if isNumberText else None ''' 函数: 参数: 返回: ''' def __getOptionInfoDict(self, optionNameList): OptionInfoDict = {} for optionName in optionNameList: found, optionDict = self.__optionExcel(optionName) if found: OptionInfoDict[optionName] = optionDict # endif # endfor return OptionInfoDict ''' 函数:找到两个字符串左边或者右边相同的部分 参数: 返回: ''' def __findDuplicateString(self, str1, str2, direction="right"): index = 0 if direction == "right": while True: index -= 1 if abs(index) > str1.__len__() or abs(index) > str2.__len__(): break if not str1[index] == str2[index]: break if index == -1: self.info(u"没有找到重复文本") return "" return str1[index + 1:] elif direction == "left": while True: if abs(index) >= str1.__len__() or abs(index) >= str2.__len__(): break if not str1[index] == str2[index]: break index += 1 return str1[:index] ''' 函数:去掉字符串数组中每个字符串 左边或右边相同的部分 参数: 返回: ''' def __removeDuplicateString(self, strList): finishedList = strList directionList = ["left", "right"] for direction in directionList: same_str = self.__findDuplicateString(strList[0], strList[1], direction) if same_str == "": continue else: for i in range(2, strList.__len__()): same_str = self.__findDuplicateString(same_str, strList[i], direction) if same_str == "": break if same_str != "": finishedList = [] for text in strList: if direction == "left": text = str[same_str.__len__():] else: text = str[:-same_str.__len__()] finishedList.append(text) # endfor # endif # endif # endfor return finishedList ''' 函数:发送红老鼠按键; 参数: key 1、如果是字符串时,当作单个按键; 2、如果是list时,当作多个按键; count 执行多少次key; wait 1、执行单个key后,等待时长(因为电视机响应遥控需要时间); 2、执行list多个key后,每个key的等待时间; 返回:无 ''' def sendKey(self, key, count=1, wait=1): if key is not None and key.__len__() > 0: if type(key) == list: for k in key: # 清除前后空格; k = k.lstrip() k = k.rstrip() COptionAction.__redRat3.sendKey(k, 1, wait) else: key = str(key) # 清除前后空格; key = key.lstrip() key = key.rstrip() COptionAction.__redRat3.sendKey(key, count, wait) else: self.error(u"error:按键内容空") ''' 函数:发送鲜活键; 参数: aliveKey 鲜活按键; 返回:无 注意:鲜活键等待时间是0.1秒,因为不考虑截图。 ''' def sendAliveKey(self, aliveKey): self.sendKey(aliveKey, 1, 0.1) if __name__ == "__main__": exData = CExtraData() optionExcel = COptionExcel(exData) optionConfig = COptionConfig(exData, optionExcel) optionAction = COptionAction('picture', '', optionConfig, optionExcel) # ====================================== # optionAction.callFirstOptionShortCutKey()