更新UCD-API库及文档

This commit is contained in:
xinzhu.yin
2026-07-02 17:16:18 +08:00
parent a500751d85
commit 9fa811a9eb
290 changed files with 9558 additions and 2306 deletions

View File

@@ -0,0 +1,102 @@
import re
class ReportContentExtractor:
"""
Class `ReportContentExtractor` allows to extract content from test report.
- Extracted EDIDs, DisplayIDs from test report.
"""
__EDID_LINE_PATTERN = re.compile(r"\d{4}.\d{3}.\d{3}:\s+[0-9a-f]{4}:\s+([0-9a-f]{2}\s+)+")
def __init__(self) -> None:
self.__extracting = False
self.__edid_addr = 0
self.__edid_size = 0
self.__edid_ready = False
self.__data = bytearray()
def __start_new_edid(self):
self.__extracting = True
self.__edid_addr = 0
self.__edid_size = 0
self.__edid_ready = False
self.__data = bytearray()
def extract_content_from_report(self, report):
edids = []
for log_line in report.fw_logs.splitlines():
self.__parse_line(log_line)
if self.__edid_ready and self.__edid_size > 0:
edids.append(self.__pop_edid())
return edids
def __parse_line(self, line: str):
if not self.__extracting:
if ("Reference Source reads Sink DUT EDID" in line
or "EDID contents:" in line):
self.__start_new_edid()
return
if len(line) < 18:
return
view = line[18:]
if "I2C AUX WR" in line:
sub_strs = view.split()
if len(sub_strs) <= 3:
return
el = sub_strs[-3]
if "30h" in el:
val = int(sub_strs[-1], 16)
self.__edid_addr = val * 0x100
elif "50h" in el:
val = int(sub_strs[-1], 16)
self.__edid_addr = val
return
if "I2C AUX RD" in line:
sub_strs = view.split()
if len(sub_strs) <= 3:
return
count = int(sub_strs[4])
for i in range(count):
byte_hex = sub_strs[5 + i]
self.__data.append(int(byte_hex, 16))
self.__edid_addr += 1
self.__edid_size = max(self.__edid_size, self.__edid_addr)
return
if self.__EDID_LINE_PATTERN.search(line):
if len(line) < 20:
return
view = line[line.rfind(':') + 1:]
sub_strs = view.split()
for byte_hex in sub_strs:
self.__data.append(int(byte_hex, 16))
self.__edid_addr += 1
self.__edid_size = max(self.__edid_size, self.__edid_addr)
return
self.__edid_ready = True
self.__extracting = False
def __pop_edid(self):
if self.__edid_ready and self.__edid_size > 0:
edid = self.__data[:self.__edid_size].hex()
self.__edid_ready = False
self.__edid_size = 0
self.__data = bytearray()
self.__edid_addr = 0
return edid
return bytearray()

View File

@@ -1,8 +1,10 @@
import os
import platform
from datetime import datetime
from enum import IntEnum
from .report_template import *
from .report_edid_extractor import ReportContentExtractor
class TestResult(IntEnum):
@@ -59,12 +61,48 @@ def replace_additional_info(test_additional_info: TestAdditionalInfo):
return result
def to_intel_hex(edid_hex: str) -> str:
data = bytes.fromhex(edid_hex)
lines = []
for offset in range(0, len(data), 16):
chunk = data[offset:offset + 16]
byte_count = len(chunk)
addr_hi = (offset >> 8) & 0xFF
addr_lo = offset & 0xFF
record = [byte_count, addr_hi, addr_lo, 0x00] + list(chunk)
checksum = (~sum(record) + 1) & 0xFF
hex_str = ''.join(f'{b:02X}' for b in record) + f'{checksum:02X}'
lines.append(f':{hex_str}')
lines.append(':00000001FF')
return ('\r\n'.join(lines) + '\r\n').encode('utf-8').hex().upper()
def generate_html_report(path: str, test_additional_info: TestAdditionalInfo, reports: list):
result = str()
head = ReportHead
head = head.replace("#count#", str((len(reports) + 2)))
for index, report in enumerate(reports):
extractor = ReportContentExtractor()
edid_displaid_content = extractor.extract_content_from_report(report)
if edid_displaid_content is None or len(edid_displaid_content) <= 0:
continue
formatted_edids = '["' + '",\n "'.join(edid_displaid_content).upper() + '"]'
formatted_edids_intel_hex = '["' + '",\n "'.join(to_intel_hex(edid) for edid in edid_displaid_content) + '"]'
test_name_formatted = report.test_name.split(' ')[0].replace('.', '_')
if len(edid_displaid_content) > 0:
head = head.replace("#edid_fill#", f"testsEdidData.set({index}, {{name: \"{test_name_formatted}\", data: {formatted_edids}, dataIntelHex: {formatted_edids_intel_hex}}}); \n#edid_fill#")
break
head = head.replace("#edid_fill#", "")
head = head.replace("#jsCode#", report_js)
head = head.replace("#count#", str(len(reports) + 2))
head = head.replace("#fileNamePrefix#", os.path.splitext(os.path.basename(path))[0])
body = ReportBody
body = body.replace("#device_description#", replace_additional_info(test_additional_info))
@@ -105,7 +143,7 @@ def generate_html_report(path: str, test_additional_info: TestAdditionalInfo, re
line = OptionListLine
line = line.replace("#number#", str(i + 2))
line = line.replace("#test_name#", f"{reports[i].group_name} / {reports[i].test_name}")
line = line.replace("#test_result#", dict_result_2.get(reports[i].test_result))
line = line.replace("#test_result#", dict_result_2.get(reports[i].test_result, "UNKNOWN"))
options_list += line + "\n"
body = body.replace("#optionList#", options_list)
@@ -117,7 +155,7 @@ def generate_html_report(path: str, test_additional_info: TestAdditionalInfo, re
line = line.replace("#handler_number#", str(i + 2))
line = line.replace("#test_name#", reports[i].test_name)
line = line.replace("#test_group#", reports[i].group_name)
line = line.replace("#test_result#", dict_result_2.get(reports[i].test_result))
line = line.replace("#test_result#", dict_result_2.get(reports[i].test_result, "UNKNOWN"))
line = line.replace("#error#", f"{reports[i].error_code}" if reports[i].error_code != 0 else "-")
line = line.replace("#ending#", "2" if i % 2 else "")
test_result = dict_result_2.get(reports[i].test_result)
@@ -153,14 +191,17 @@ def generate_html_report(path: str, test_additional_info: TestAdditionalInfo, re
line = line.replace("#test_number#", f"{i + 1}")
line = line.replace("#test_group#", reports[i].group_name)
line = line.replace("#test_name#", reports[i].test_name)
line = line.replace("#result#", dict_result_2.get(reports[i].test_result))
line = line.replace("#result#", dict_result_2.get(reports[i].test_result, "UNKNOWN"))
line = line.replace("#test_delay#", f"{reports[i].test_delay * 1000}")
line = line.replace("#test_parameters#", reports[i].config_info)
line = line.replace("#warning#", "<b>Debug options enabled!</b><br><br>" if reports[i].debug else "")
if reports[i].debug:
if "demo" in reports[i].group_name.lower():
warnings = "<b>Demo Report Only - Not valid for DP Logo Certification.</b><br><br>"
elif reports[i].debug:
warnings = "<b>Debug options enabled!</b><br><br>"
line = line.replace("#warning#", warnings)
line = line.replace("#test_log#", escape_html(reports[i].fw_logs))
tables_list += line + "\n"

View File

@@ -14,23 +14,29 @@ ReportHead = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\
" PRE.TESTBODY{margin-top: 0;margin-bottom: 0;margin-left: 1.0cm;font-weight: medium;font-family: \"Courier New\";font-size: 11pt;color: #000000;}\n" \
" P.BODYDETAILTEXT{margin-top: 0;margin-bottom: 0;margin-left: 1.4cm;font-weight: medium;font-family: \"Arial\";font-size: 9pt;color: #000000;}\n" \
" P.CONTENTS SELECT{font-weight: normal;font-family: \"Arial\";font-size: 11pt;width: 98%;margin-top: 0;text-align: left;}\n" \
"P.TABLE_HEADERS{background-color: #DCFABC;margin-top: 0;margin-bottom: 0;margin-left: 1.0cm;font-weight: " \
"medium;font-family: \"Arial\";font-size: 12pt;color: #000000;}\n" \
" P.TABLE_HEADERS{background-color: #DCFABC;margin-top: 0;margin-bottom: 0;margin-left: 1.0cm;font-weight: medium;font-family: \"Arial\";font-size: 12pt;color: #000000;}\n" \
" P.TABLE_LINKS{margin-top: 0;margin-bottom: 0;margin-left: 1.0cm;font-weight: medium;font-family: \"Arial\";font-size: 11pt;}\n" \
" P.TABLE_LINKS2{margin-top: 0;margin-bottom: 0;margin-left: 1.0cm;font-weight: medium;font-family: \"Arial\";font-size: 11pt; background-color: #DDDDDD;}\n" \
" P.FAILED{margin-top: 0;margin-bottom: 0;background-color: #FFBCBC; font-weight: bold;font-family: \"Arial\";font-size: 11pt;color: #000000;}\n" \
" P.PASSED{margin-top: 0;margin-bottom: 0;background-color: #BCFFBC; font-weight: bold;font-family: \"Arial\";font-size: 11pt;color: #000000;}\n" \
" P.SKIPPED{margin-top: 0;margin-bottom: 0;background-color: #BCBCBC; font-weight: bold;font-family: \"Arial\";font-size: 11pt;color: #000000;}\n" \
" P.ERROR_CODE{margin-top: 0;margin-bottom: 0;background-color: #FFFFFF; font-weight: bold;font-family: \"Arial\";font-size: 11pt;color: #000000;}\n" \
" P.ABORTED{margin-top: 0;margin-bottom: 0;background-color: #FFFFFF; font-weight: bold;font-family: \"Arial\";font-size: 11pt;color: #000000;}\n" \
" .TOPBUTTON{display: none; position: fixed; bottom: 20px; right: 30px; z-index: 99; font-size: 18px; border: solid black 1px; outline: none; background-color: #DCFABC; cursor: pointer; padding: 15px; border-radius: 5px;}\n" \
" .DBG_HIGHLIGHT { color: #ff0000;}\n" \
" .flex-container {\n" \
" display: flex;\n" \
" gap: 5px;\n" \
" }\n" \
"</style>\n" \
"<script src=\"https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js\"></script>" \
"<script type=\"text/javascript\">\n" \
"var base64Content = #configInfo#;\n"\
"var base64Content = #configInfo#;\n" \
"window.current_index = 2;\n" \
"window.min_value = 2;\n" \
"window.max_value = #count# - 1;\n" \
"window.onscroll = function() {scrollFunction()};\n" \
"let testsEdidData = new Map();\n" \
"#edid_fill#\n\n" \
"function Hide(iElementIndex) { document.getElementById(\"ID\"+iElementIndex).style.display = 'none'; }\n" \
"function Show(iElementIndex) { document.getElementById(\"ID\"+iElementIndex).style.display = 'inline'; }\n" \
"function HideAll(iDivCount) { for(var i=0;i<iDivCount;i++) { Hide(i); }}\n" \
@@ -38,15 +44,26 @@ ReportHead = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\
"function SetIndex(value) { window.current_index = value; }\n" \
"function GetIndex() { return parseInt(window.current_index, 10); }\n" \
"function HideButtons() { document.getElementById(\"top_buttons\").style.display = 'none'; document.getElementById(\"bottom_buttons\").style.display = 'none'; }\n" \
"function ShowButtons() { document.getElementById(\"top_buttons\").style.display = 'inline'; document.getElementById(\"bottom_buttons\").style.display = 'inline'; }\n" \
"function UpdateButtonsState() { GetIndex() >= window.min_value && window.max_value ? ShowButtons() : HideButtons(); }\n" \
"function ShowButtons() { document.getElementById(\"top_buttons\").style.display = 'inline'; document.getElementById(\"bottom_buttons\").style.display = 'inline';} \n" \
"function ShowHideEdidBtn() {\n" \
" let index = GetIndex();\n" \
" let edidBtnVisibility = 'none';\n" \
" if (index < window.min_value || index > window.max_value) {\n" \
" if (testsEdidData.size > 0) edidBtnVisibility = 'inline';\n }" \
" else if (testsEdidData.has(index)) edidBtnVisibility = 'inline';\n" \
" document.getElementById(\"save_edid_btn\").style.display = edidBtnVisibility;\n" \
" document.getElementById(\"save_edid_intel_hex_btn\").style.display = edidBtnVisibility;\n" \
"}\n" \
"function UpdateButtonsState() { GetIndex() >= window.min_value && window.max_value ? ShowButtons() : HideButtons();\n" \
" ShowHideEdidBtn(); }\n" \
"function ListHandler (iOptionValue) { if(iOptionValue==-1) { ShowAll(#count#); Hide(1); SetIndex(1); UpdateButtonsState(); return; } HideAll(#count#); Show(iOptionValue); SetIndex(iOptionValue > 1 ? iOptionValue : 1); UpdateButtonsState();}\n" \
"function NextTest() { var index = GetIndex(); ListHandler ((index + 1 > window.max_value) ? (window.min_value) : (index + 1)); }\n" \
"function PreviousTest() { var index = GetIndex(); ListHandler ((index - 1 < window.min_value) ? (window.max_value) : (index - 1)); }\n" \
"function scrollFunction() { let topButton = document.getElementById('topBtn'); if (document.body.scrollTop > 50 || document.documentElement.scrollTop > 50) {topButton.style.display = 'block';} else {topButton.style.display = 'none';} }\n" \
"function TopFunction() { document.body.scrollTop = 0; document.documentElement.scrollTop = 0;}\n" \
"function SaveConfig() { var jsonData = atob(base64Content[GetIndex()-2]); var blob = new Blob([jsonData], { type: 'application/json' }); var link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); var currentPageURL = window.location.href; var decodedURL = decodeURIComponent(currentPageURL); link.download = getText() + \".json\"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }\n"\
"function getText() { var index = GetIndex(); let a = document.getElementById(\"testlist\"); let option = a.options[index]; return option.text.replace('/', '').replace('>', '').replace(':', '') }\n"\
"function SaveConfig() { var jsonData = atob(base64Content[GetIndex()-2]); var blob = new Blob([jsonData], { type: 'application/json' }); var link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); var currentPageURL = window.location.href; var decodedURL = decodeURIComponent(currentPageURL); link.download = getText() + \".json\"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }\n" \
"function getText() { var index = GetIndex(); let a = document.getElementById(\"testlist\"); let option = a.options[index]; return option.text.replace('/', '').replace('>', '').replace(':', '') }\n" \
"#jsCode#" \
"</script>\n" \
"</head>\n"
@@ -102,7 +119,7 @@ TestTable = "<br><br></p></div><div id=\"ID#number#\">\n" \
"\n"
ReportBody = "<body onload='ListHandler(0);'>\n" \
" <button class=\"TOPBUTTON\" onclick=\"TopFunction()\" id=\"topBtn\" title=\"Go to top\">&#8593;</button>\n" \
"<button class=\"TOPBUTTON\" onclick=\"TopFunction()\" id=\"topBtn\" title=\"Go to top\">&#8593;</button>\n" \
" <div><p class=\"HEADING\" id=\"top\">&nbsp;&nbsp;Unigraf Test Report</p></div>\n" \
" <div>\n" \
" <table class=\"DOCUMENT\" cellpadding=\"0\" cellspacing=\"0\">\n" \
@@ -115,16 +132,22 @@ ReportBody = "<body onload='ListHandler(0);'>\n" \
" <option value=\"0\">Report Summary</option>\n" \
" <option value=\"1\">Test Summary</option>\n" \
"#optionList#\n" \
" <option value=\"-1\">Show everything</option>\n" \
" <option value=\"-1\">Show All</option>\n" \
" </select>\n" \
" </td>\n" \
" </table>\n" \
"<div id=\"top_buttons\" style='margin-left: 12'>\n" \
"<button type=\"button\" onclick=\"PreviousTest()\">Previous</button>\n" \
"<button type=\"button\" onclick=\"ListHandler(1)\">Summary</button>\n" \
"<button type=\"button\" onclick=\"SaveConfig()\">Save Test Config</button>\n" \
"<button type=\"button\" onclick=\"NextTest()\">Next</button>\n" \
"<br>\n" \
"<div class='flex-container'>" \
" <div id=\"top_buttons\">\n" \
" <button type=\"button\" onclick=\"PreviousTest()\">Previous</button>\n" \
" <button type=\"button\" onclick=\"ListHandler(1)\">Summary</button>\n" \
" <button type=\"button\" onclick=\"SaveConfig()\">Save Test Config</button>\n" \
" <button type=\"button\" onclick=\"NextTest()\">Next</button>\n" \
" </div>\n" \
" <div>\n" \
" <button type=\"button\" id=\"save_edid_btn\" onclick=\"SaveEdid()\">Save EDID as bin</button>\n" \
" <button type=\"button\" id=\"save_edid_intel_hex_btn\" onclick=\"SaveEdidIntelHex()\">Save EDID as intel hex</button>\n" \
" <br>\n" \
" </div>\n" \
"</div>\n" \
"<br>\n" \
"<div id=\"ID0\">\n" \
@@ -151,3 +174,134 @@ ReportFooter = "<br><br></pre></div>\n" \
" </p>\n" \
"</body>\n" \
"</html>\n"
report_js = """
function hexToUint8Array(hex) {
const clean = hex.replace(/\s+/g, '');
const bytes = new Uint8Array(clean.length / 2);
for (let i = 0; i < clean.length; i += 2) {
bytes[i / 2] = parseInt(clean.substr(i, 2), 16);
}
return bytes;
}
function saveBlob(blob, fileName) {
const a = document.createElement('a');
const url = URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
setTimeout(() => {
URL.revokeObjectURL(url);
a.remove();
}, 2000);
}
async function saveZip(files, zipName) {
const zip = new JSZip();
for (const file of files) {
zip.file(file.name, file.data, {
binary: true
});
}
const uint8 = await zip.generateAsync({
type: 'uint8array',
compression: 'STORE'
});
const blob = new Blob([uint8], { type: 'application/zip' });
saveBlob(blob, zipName);
}
async function saveFiles(getFileList, zipName, mimeType) {
const files = getFileList();
if (files.length === 1) {
saveBlob(
new Blob([files[0].data], { type: mimeType }),
files[0].name
);
} else if (files.length > 1) {
await saveZip(files, zipName);
}
}
function getEdidBinFiles() {
let index = GetIndex();
const files = [];
if (index < window.min_value || index > window.max_value) {
for (const [key, value] of testsEdidData) {
for (let i = 0; i < value.data.length; ++i) {
files.push({
name: `#fileNamePrefix#_Test${key}_${value.name}_EDID_${i}.bin`,
data: hexToUint8Array(value.data[i])
});
}
}
} else if (testsEdidData.has(index)) {
const testData = testsEdidData.get(index);
for (let i = 0; i < testData.data.length; ++i) {
files.push({
name: `#fileNamePrefix#_Test_${testData.name}_EDID_${i}.bin`,
data: hexToUint8Array(testData.data[i])
});
}
}
return files;
}
function getEdidIntelHexFiles() {
let index = GetIndex();
const files = [];
if (index < window.min_value || index > window.max_value) {
for (const [key, value] of testsEdidData) {
for (let i = 0; i < value.dataIntelHex.length; ++i) {
files.push({
name: `#fileNamePrefix#_Test${key}_${value.name}_EDID_${i}.hex`,
data: hexToUint8Array(value.dataIntelHex[i])
});
}
}
} else if (testsEdidData.has(index)) {
const testData = testsEdidData.get(index);
for (let i = 0; i < testData.dataIntelHex.length; ++i) {
files.push({
name: `#fileNamePrefix#_Test_${testData.name}_EDID_${i}.hex`,
data: hexToUint8Array(testData.dataIntelHex[i])
});
}
}
return files;
}
async function SaveEdid() {
await saveFiles(
getEdidBinFiles,
'#fileNamePrefix#_EDID_BIN.zip',
'application/octet-stream'
);
}
async function SaveEdidIntelHex() {
await saveFiles(
getEdidIntelHexFiles,
'#fileNamePrefix#_EDID_HEX.zip',
'application/octet-stream'
);
}
"""