diff --git a/README.md b/README.md index 1c722cd..1100cb3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ dash 顶部已加入低压电池电压 `lvbat`(两位小数),来源是 SDC - [AMI.dbc](AMI.dbc):当前 AMI CAN 接口,10 条报文、33 个信号,含标准/扩展帧。 - [DBC 对应说明](docs/AMI_CAN_DBC说明.md):信号布局、枚举、字节序、样例与复现检查。 +- [CAN 报文说明 PDF](output/pdf/AMI_CAN报文说明.pdf):当前 v2.0 协议的中文打印版,包含全部 10 条报文。 - [联调报告](docs/SDC_BMS联调报告.md):真实 CAN1 / MCU / COM4 到 HMI 调试器的证据。 - [已联调固件 BIN](output/firmware/AMI_Controller.bin):STM32G0B1CBT6,配套源码在 firmware。 - [AMI.HMI](AMI.HMI):最新版屏幕工程,打开后编译并下载到对应型号屏幕。 @@ -54,3 +55,15 @@ dash.lvbat.val=1260 灰色数值表示未知或失效。现有资料缺少电机/逆变器温度协议、Hz 到 km/h 标定参数、SDC PD0..PD14 到具名安全信号的映射,对应显示保持无效/未知。VCU 任务裁决、执行反馈和 EBS 安全输出仍需车辆控制协议,画面 EBS 文案不能作为安全状态证明。 许可证沿用仓库中的 [AGPL-3.0](LICENSE)。 + +## 导出 CAN 报文 PDF + +PDF 与 `docs/AMI_CAN_DBC说明.md` 使用同一份内容,按 A4 横向排版并嵌入中文字体。在仓库根目录运行: + +```powershell +python -m pip install -r scripts/requirements-pdf.txt +python scripts/export_dbc_docs.py +python scripts/export_can_pdf.py +``` + +默认使用 Windows 黑体 `C:/Windows/Fonts/simhei.ttf`;其他环境可通过 `--font` 指定支持中文的 TrueType 字体。输出为 `output/pdf/AMI_CAN报文说明.pdf`。修改 DBC 后,需重新导出 Markdown 和 PDF。 diff --git a/output/pdf/AMI_CAN报文说明.pdf b/output/pdf/AMI_CAN报文说明.pdf new file mode 100644 index 0000000..9ca93e5 Binary files /dev/null and b/output/pdf/AMI_CAN报文说明.pdf differ diff --git a/scripts/export_can_pdf.py b/scripts/export_can_pdf.py new file mode 100644 index 0000000..3cd91f0 --- /dev/null +++ b/scripts/export_can_pdf.py @@ -0,0 +1,147 @@ +"""Render the verified Chinese CAN Markdown document as a printable PDF.""" +import argparse +import html +import re +from pathlib import Path + +from reportlab.lib import colors +from reportlab.lib.enums import TA_LEFT +from reportlab.lib.pagesizes import A4, landscape +from reportlab.lib.styles import ParagraphStyle +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.platypus import ( + KeepTogether, PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle, +) + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / 'docs/AMI_CAN_DBC说明.md' +OUTPUT = ROOT / 'output/pdf/AMI_CAN报文说明.pdf' +REMOTE = 'https://git.acetaffy.ltd/acetaffy/AMI/src/branch/main/' + + +def inline(text): + """Escape Markdown and retain useful links in the printed document.""" + text = html.escape(text) + text = re.sub(r'`([^`]+)`', r'\1', text) + + def link(match): + label, target = match.groups() + if not target.startswith(('http://', 'https://')): + target = REMOTE + ('AMI.dbc' if target == '../AMI.dbc' else 'docs/' + target) + return f'{label}' + + return re.sub(r'\[([^\]]+)\]\(([^)]+)\)', link, text) + + +def export(font_path): + pdfmetrics.registerFont(TTFont('AMIChinese', str(font_path), subfontIndex=0)) + page_width, page_height = landscape(A4) + width = page_width - 80 + body = ParagraphStyle('Body', fontName='AMIChinese', fontSize=9.3, + leading=15, spaceAfter=8, wordWrap='CJK', alignment=TA_LEFT) + heading = ParagraphStyle('Heading', parent=body, fontSize=16, leading=23, + textColor=colors.HexColor('#12485E'), spaceBefore=3, + spaceAfter=12, keepWithNext=True) + subheading = ParagraphStyle('Subheading', parent=body, fontSize=11, + leading=17, textColor=colors.HexColor('#12485E'), + spaceBefore=8, spaceAfter=6, keepWithNext=True) + title = ParagraphStyle('Title', parent=heading, fontSize=25, leading=32) + cell = ParagraphStyle('Cell', parent=body, fontSize=8.5, leading=12, spaceAfter=0) + code = ParagraphStyle('Code', parent=body, fontSize=8, leading=13, + backColor=colors.HexColor('#F2F5F7'), borderPadding=8) + lines = SOURCE.read_text(encoding='utf-8').splitlines() + story = [] + i = 0 + while i < len(lines): + line = lines[i] + if not line.strip(): + i += 1 + continue + if line.startswith('# '): + story.append(Paragraph('AMI CAN 报文说明', title)) + elif line.startswith('## '): + section = line[3:] + if section != '使用范围': + story.append(PageBreak()) + story.append(Paragraph(inline(section), heading)) + elif line.startswith('### '): + story.append(Paragraph(inline(line[4:]), subheading)) + elif line.startswith('|'): + rows = [] + while i < len(lines) and lines[i].startswith('|'): + row = [x.strip() for x in lines[i].strip().strip('|').split('|')] + if not all(re.fullmatch(r':?-+:?', x) for x in row): + rows.append([Paragraph(inline(x), cell) for x in row]) + i += 1 + count = len(rows[0]) + proportions = { + 2: [0.15, 0.85], + 3: [0.17, 0.40, 0.43], + 6: [0.11, 0.055, 0.04, 0.28, 0.365, 0.15], + 8: [0.32, 0.055, 0.055, 0.10, 0.09, 0.08, 0.06, 0.24], + }[count] + total = sum(proportions) + table = Table(rows, colWidths=[width*x/total for x in proportions], + repeatRows=1, hAlign='LEFT') + table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#DDECF2')), + ('ROWBACKGROUNDS', (0, 1), (-1, -1), + [colors.white, colors.HexColor('#F5F8FA')]), + ('LINEBELOW', (0, 0), (-1, 0), 0.8, colors.HexColor('#7DABB9')), + ('LINEBELOW', (0, 1), (-1, -1), 0.3, colors.HexColor('#D7E0E5')), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('LEFTPADDING', (0, 0), (-1, -1), 7), + ('RIGHTPADDING', (0, 0), (-1, -1), 7), + ('TOPPADDING', (0, 0), (-1, -1), 6), + ('BOTTOMPADDING', (0, 0), (-1, -1), 6), + ])) + if i+1 < len(lines) and not lines[i].strip() and lines[i+1].startswith('`'): + group = [table, Spacer(1, 10), Paragraph(inline(lines[i+1]), body)] + if story and isinstance(story[-1], Paragraph) and story[-1].style is subheading: + group.insert(0, story.pop()) + story.append(KeepTogether(group)) + i += 2 + else: + story.extend([table, Spacer(1, 10)]) + continue + elif line.startswith('```'): + block = [] + i += 1 + while i < len(lines) and not lines[i].startswith('```'): + block.append(html.escape(lines[i])) + i += 1 + story.append(KeepTogether([Paragraph('
'.join(block), code)])) + else: + paragraph = [line] + while i+1 < len(lines) and lines[i+1].strip() and not lines[i+1].startswith(('#', '|', '```')): + i += 1 + paragraph.append(lines[i]) + story.append(Paragraph(inline(' '.join(paragraph)), body)) + i += 1 + + def footer(canvas, doc): + canvas.saveState() + canvas.setStrokeColor(colors.HexColor('#C4D8E1')) + canvas.line(40, 31, page_width-40, 31) + canvas.setFont('AMIChinese', 8) + canvas.setFillColor(colors.HexColor('#506775')) + canvas.drawString(40, 19, 'AMI-2.0 | 2026-10-03 | CAN 接口说明') + canvas.drawRightString(page_width-40, 19, f'{doc.page}') + canvas.restoreState() + + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + document = SimpleDocTemplate(str(OUTPUT), pagesize=landscape(A4), + leftMargin=40, rightMargin=40, + topMargin=36, bottomMargin=44, + title='AMI CAN 报文说明', author='AMI / YMR', + subject='AMI CAN DBC v2.0: SDC, BMS and task requests') + document.build(story, onFirstPage=footer, onLaterPages=footer) + print(OUTPUT) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--font', type=Path, default=Path('C:/Windows/Fonts/simhei.ttf'), + help='CJK TrueType font path (default: Windows SimHei)') + export(parser.parse_args().font) diff --git a/scripts/requirements-pdf.txt b/scripts/requirements-pdf.txt new file mode 100644 index 0000000..b1df017 --- /dev/null +++ b/scripts/requirements-pdf.txt @@ -0,0 +1 @@ +reportlab==5.0.1