feat: add AS status feedback for both HMI sizes with verified v2.7 release
This commit is contained in:
@@ -39,7 +39,8 @@ def export(font_path):
|
||||
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)
|
||||
leading=14, spaceAfter=6, wordWrap='CJK', alignment=TA_LEFT,
|
||||
allowWidows=0, allowOrphans=0)
|
||||
heading = ParagraphStyle('Heading', parent=body, fontSize=16, leading=23,
|
||||
textColor=colors.HexColor('#12485E'), spaceBefore=3,
|
||||
spaceAfter=12, keepWithNext=True)
|
||||
@@ -127,7 +128,7 @@ def export(font_path):
|
||||
canvas.line(40, 31, page_width-40, 31)
|
||||
canvas.setFont('AMIChinese', 8)
|
||||
canvas.setFillColor(colors.HexColor('#506775'))
|
||||
canvas.drawString(40, 19, 'AMI-2.5 | 2026-10-04 | CAN1 500K / CAN2 250K')
|
||||
canvas.drawString(40, 19, 'AMI-2.7 | 2026-10-07 | CAN1 500K / CAN2 250K')
|
||||
canvas.drawRightString(page_width-40, 19, f'{doc.page}')
|
||||
canvas.restoreState()
|
||||
|
||||
@@ -136,7 +137,7 @@ def export(font_path):
|
||||
leftMargin=40, rightMargin=40,
|
||||
topMargin=36, bottomMargin=44,
|
||||
title='AMI CAN 报文说明', author='AMI / YMR',
|
||||
subject='AMI CAN DBC v2.5: CAN1 500K / CAN2 250K')
|
||||
subject='AMI CAN DBC v2.7: CAN1 500K / CAN2 250K')
|
||||
document.build(story, onFirstPage=footer, onLaterPages=footer)
|
||||
print(OUTPUT)
|
||||
|
||||
|
||||
+56
-31
@@ -30,12 +30,12 @@ def can2_dbc_text():
|
||||
|
||||
PURPOSE = {
|
||||
0x301: 'AMI -> VCU(仅 CAN1):版本、序号、运行时间',
|
||||
0x183: 'VCU -> AMI:实际当前任务、锁定状态、EBS 状态',
|
||||
0x183: 'VCU -> AMI:实际当前任务、锁定状态、无人状态、EBS 状态',
|
||||
0x182: 'VCU / 测试器 -> AMI:扭矩千分、电机温度、MCU 温度',
|
||||
0x300: 'AMI -> VCU:用户确认后的任务选择请求',
|
||||
0x6A0: 'SDC -> AMI:板标识、版本、序号、上电时间',
|
||||
0x6A1: 'SDC -> AMI:低压电池电压,精确 DLC=2',
|
||||
0x6A2: 'SDC -> AMI:PA0 / PA2 / PA6 / PB14 原始频率',
|
||||
0x6A2: 'SDC -> AMI:左前 PA0 / 右前 PA6 已标定轮速',
|
||||
0x6A4: 'SDC -> AMI:DAC 设置应答,识别但不显示',
|
||||
0x6A6: 'SDC -> AMI:PD0..PD14 原始电平和变化掩码',
|
||||
0x140: 'BMS -> AMI:高压总电压、带偏置的总电流',
|
||||
@@ -67,9 +67,9 @@ def render():
|
||||
'本 DBC 包含 AMI 发送的 `0x300` 任务请求和 `0x301` 周期心跳,以及 AMI 当前识别的 SDC / BMS 和自定义显示输入。'
|
||||
'SDC `0x6A3` DAC 控制、`0x6A5` ASSI 控制、BMS 其他电芯/故障字段不属于此接口子集。'
|
||||
'`0x182` 新增扭矩千分、电机温度和电机控制器 MCU 温度的仪表显示输入;'
|
||||
'发送者负责转换实际电机控制器协议。`0x183` 为新增 VCU 状态反馈,需 VCU 按此自定义接口发送;未定义标定后的车速。'
|
||||
'发送者负责转换实际电机控制器协议。`0x183` 为新增 VCU 状态反馈,需 VCU 按此自定义接口发送;车速取 SDC 双前轮轮速平均值。'
|
||||
'旧 `0x180 / 0x181 / 0x280` 输入已停用。', '',
|
||||
'SDC 字段依据 `SDC_ACQ_DAC.dbc` VERSION 1.1,保持其信号名、位布局、缩放和单位;'
|
||||
'SDC 字段依据 `SDC_ACQ_DAC.dbc` VERSION 1.1,核对远端 master 提交 `7bc2d9f1eaaf19814957037bd097cd879cbf4cbf`(2026-10-05),保持其信号名、位布局、缩放和单位;'
|
||||
'仅将输入接收节点设为 AMI。BMS 字段依据 '
|
||||
'[BmsMonitor-CANTest](https://git.acetaffy.ltd/acetaffy/BmsMonitor-CANTest) '
|
||||
'提交 `e65c95203d1689df05343775982d7e85bd782646` 的 `BmsProtocolParser.cs`。', '',
|
||||
@@ -81,7 +81,7 @@ def render():
|
||||
'| CAN ID | 类型 | DLC | 报文名 | 方向和作用 | 周期 / 触发 |',
|
||||
'| --- | --- | --- | --- | --- | --- |',
|
||||
]
|
||||
triggers = {0x300:'确认任务触发',0x6A4:'DAC 设置后应答',0x6A6:'启动 / 电平变化'}
|
||||
triggers = {0x300:'确认任务触发',0x6A4:'DAC 设置后应答'}
|
||||
for m in db.messages:
|
||||
cycle = f'{m.cycle_time} ms' if m.cycle_time else triggers.get(m.frame_id, '源协议未指定')
|
||||
lines.append(f'| `0x{m.frame_id:X}` | {"扩展" if m.is_extended_frame else "标准"} | '
|
||||
@@ -90,7 +90,7 @@ def render():
|
||||
'', '扩展帧在 DBC 的 `BO_` 十进制 ID 中设置 bit31:'
|
||||
'`0x186040F3 -> 2556444915`,`0x186140F3 -> 2556510451`。'
|
||||
'CAN 工具中使用实际 29 位仲裁 ID 并选择扩展帧;bit31 不是总线上发送的仲裁 ID。', '',
|
||||
'`GenMsgCycleTime` 填写已确认的 SDC 周期 100 / 500 / 20 ms,以及 `0x182`、`0x183` 和 `0x301` 的约定周期 100 ms。'
|
||||
'`GenMsgCycleTime` 填写已确认的 SDC 周期 100 / 500 / 20 / 1000 ms,以及 `0x182`、`0x183` 和 `0x301` 的约定周期 100 ms。'
|
||||
'其余帧未填写周期,默认 0 表示未指定,不据此推断 BMS 周期。', '',
|
||||
'## 信号布局', '',
|
||||
'Start 为 DBC 起始位,位号从 0 开始。Intel `@1` 从最低有效位起算;'
|
||||
@@ -112,6 +112,17 @@ def render():
|
||||
lines += ['', ';'.join(choices) + '。']
|
||||
lines += ['']
|
||||
lines += [
|
||||
'## SDC 轮速与车速显示 0x6A2', '',
|
||||
'2026-10-05 的 SDC master 将旧四路 Hz 改为双前轮已标定轮速:标准 Classic CAN 数据帧,DLC 必须为 4,每 20 ms 一帧。'
|
||||
'字节 0..1 为左前 PA0,字节 2..3 为右前 PA6,均为小端 uint16 mm/s(DBC 0.001 m/s)。'
|
||||
'SDC 已使用每圈 49 脉冲、轮周长 1414 mm,AMI 不再次标定。', '',
|
||||
'AMI 用双前轮平均轮速作为仪表车速,转换为 km/h 后一次四舍五入:'
|
||||
'`dash.speed.val=((left_raw+right_raw)*9+2500)/5000`,全部采用 uint32 整数运算。'
|
||||
'有效时白色;超过 500 ms 没有合规轮速帧时保留历史值并变灰;新帧自动恢复。'
|
||||
'其他 SDC 心跳、GPIO、BMS 或 VCU 流量不延长车速有效期。旧 DLC 8、扩展、远程或 FD 帧拒绝。', '',
|
||||
'该源未提供每轮有效标志,0 为有效停止读数,65535 为有效饱和值;不能据此判断传感器健康。'
|
||||
'SDC 超过 1 秒没有边沿会把对应轮速归零;AMI 仍会把持续收到的零作为有效数值。'
|
||||
'平均轮速不是 VCU 融合车速,单轮丢脉冲或打滑会影响显示。接收长度变更与旧固件不兼容,需配套 SDC 最新版本。', '',
|
||||
'## 自定义动力显示输入 0x182', '',
|
||||
'标准数据帧,DLC 必须为 8;CAN1 500K 或 CAN2 250K 均可接收,建议每 100 ms 发送。'
|
||||
'这是一条 AMI 仪表显示设置接口;扭矩千分以发送者定义的基准扭矩归一化,500 表示 500‰。'
|
||||
@@ -136,44 +147,55 @@ def render():
|
||||
'```', '',
|
||||
'## VCU 状态反馈 0x183', '',
|
||||
'新增自定义标准数据帧,DLC 必须为 8,建议周期 100 ms;CAN1 500K 与 CAN2 250K 共用布局。'
|
||||
'只有该帧更新当前任务、锁定、EBS 主页面显示。AMI 发出任务请求后保留 VCU 上次反馈,等待新的实际状态。', '',
|
||||
'v2.7 扩展此帧为版本 2,只有该帧更新当前任务、锁定、无人状态和 EBS 主页面显示。AMI 发出任务请求后保留 VCU 上次反馈,等待新的实际状态。', '',
|
||||
'| 字节 | 字段 | 编码与规则 |', '| --- | --- | --- |',
|
||||
'| 0 | 当前任务 | 0 无;1 八字绕环;2 直线加速;3 高速循迹;4 车检测试;5 EBS 测试;6 手动驾驶 |',
|
||||
'| 1 | 锁定 | 0 已解锁(红);1 已锁定(绿) |',
|
||||
'| 2 | EBS 正常 | 0 故障(红底);1 正常(绿底) |',
|
||||
'| 3 | 有效标志 | bit0 当前任务,bit1 锁定,bit2 EBS;1 有效,0 未知;bit3..7 为 0 |',
|
||||
'| 3 | 有效标志 | bit0 当前任务,bit1 锁定,bit2 EBS,bit3 无人状态;1 有效,0 未知;bit4..7 为 0 |',
|
||||
'| 4 | 反馈计数 | 0..255,供观察;不去重,不作为请求 ACK |',
|
||||
'| 5..6 | 保留 | 必须为 0 |',
|
||||
'| 7 | 帧版本 | 固定 1 |', '',
|
||||
'例:`02 01 01 07 00 00 00 01` 表示当前直线加速、已锁定、EBS 正常。'
|
||||
'`00 00 00 07 01 00 00 01` 表示无任务、已解锁、EBS 故障。任务 1..6 为绿字,0 无任务为红字。'
|
||||
'各有效标志为 0 时,对应项立即灰色未知;连续超过 500 ms 无合规帧后三项均未知,其他 CAN 流量不能续期。'
|
||||
'| 5 | 无人状态 | 0 AS OFF;1 AS Ready;2 AS Driving;3 AS Finished;4 AS Emergency;5 Manual Driving |',
|
||||
'| 6 | 保留 | 必须为 0 |',
|
||||
'| 7 | 帧版本 | 固定 2;旧版本 1 兼容规则见下文 |', '',
|
||||
'例:`02 01 01 0F 00 01 00 02` 表示当前直线加速、已锁定、AS Ready、EBS 正常。'
|
||||
'`00 00 00 0F 01 00 00 02` 表示无任务、已解锁、AS OFF、EBS 故障。任务 1..6 为绿字,0 无任务为红字。'
|
||||
'各有效标志为 0 时,对应项立即灰色未知;连续超过 500 ms 无合规帧后四项均未知,其他 CAN 流量不能续期。'
|
||||
'错误 DLC、版本、枚举值、保留位或扩展/远程/FD 格式整帧拒绝,不改变状态、不续期。'
|
||||
'该反馈只驱动显示,EBS 安全动作及车辆任务裁决由 VCU 实现。', '',
|
||||
'### 无人状态来源与兼容', '',
|
||||
'状态语义依据用户提供 ASF 的 AMI 操作界面及 AS 状态描述。ASF 没有分配本帧的数值编码,'
|
||||
'上表 0..5 是 AMI 与 VCU 的自定义约定,VCU 需显式实现。Manual Driving 用于显示 ASF 的有人驾驶分支,不能由任务 6 自动推断。', '',
|
||||
'主页面在已锁定/已解锁右侧显示独立状态:AS OFF 白色,AS Ready 黄色,AS Driving 黄色,'
|
||||
'AS Finished 蓝色,AS Emergency 红色,Manual Driving 绿色;未知灰色。文字不闪烁,颜色为仪表约定,不代替 ASSI 灯控制。', '',
|
||||
'旧版本 1(byte7=01)仍接受原三项,要求 byte3 bit3..7=0、byte5=0、byte6=0,'
|
||||
'每次收到后无人状态立即置未知;不能保留上一条版本 2 的 AS 状态。旧 MCU 不能识别版本 2;'
|
||||
'新增状态需配套 v2.7 MCU 和包含 as_state 的屏幕工程。800×480 与 480×272 工程均已更新;小屏 AS=5 显示“有人驾驶”。任务、锁定、EBS 与 AS 独立反馈,AMI 不推断安全状态。', '',
|
||||
'发送工具默认版本 2;指定 --as-state 时默认有效标志 15,未指定时为 7(AS 未知)。'
|
||||
'--valid 可单独清除各字段有效位;兼容预览可用 --version 1,不能同时指定 --as-state。', '',
|
||||
'### 当前任务发送对照', '',
|
||||
'下表第 1、2 字节均为 01,只用于展示已锁定、EBS 正常;VCU 必须填写实际状态。'
|
||||
'每条合规帧同时更新三项及其有效性,不能只改任务有效位而期望另外两项继续保留。', '',
|
||||
'下表第 1、2 字节均为 01,并且 byte5=01,只用于展示已锁定、AS Ready、EBS 正常;VCU 必须填写实际状态。'
|
||||
'每条合规帧同时更新四项及其有效性,不能只改任务有效位而期望其他项继续保留。', '',
|
||||
'| 任务号 | 当前任务 | 标准 0x183 / DLC 8 数据 |', '| --- | --- | --- |',
|
||||
'| 0 | 无(红) | `00 01 01 07 00 00 00 01` |',
|
||||
'| 1 | 八字绕环 | `01 01 01 07 00 00 00 01` |',
|
||||
'| 2 | 直线加速 | `02 01 01 07 00 00 00 01` |',
|
||||
'| 3 | 高速循迹 | `03 01 01 07 00 00 00 01` |',
|
||||
'| 4 | 车检测试 | `04 01 01 07 00 00 00 01` |',
|
||||
'| 5 | EBS 测试 | `05 01 01 07 00 00 00 01` |',
|
||||
'| 6 | 手动驾驶 | `06 01 01 07 00 00 00 01` |', '',
|
||||
'任务未知通过 byte3 的 bit0=0 编码。例如 `00 01 01 06 00 00 00 01`'
|
||||
'显示任务未知、已锁定、EBS 正常;不要发送任务号 7,7 是屏内未知状态而不是 CAN 枚举。'
|
||||
'任务号、锁定、EBS 的数值范围始终校验,有效标志为 0 时也不能发送越界值。', '',
|
||||
'| 0 | 无(红) | `00 01 01 0F 00 01 00 02` |',
|
||||
'| 1 | 八字绕环 | `01 01 01 0F 00 01 00 02` |',
|
||||
'| 2 | 直线加速 | `02 01 01 0F 00 01 00 02` |',
|
||||
'| 3 | 高速循迹 | `03 01 01 0F 00 01 00 02` |',
|
||||
'| 4 | 车检测试 | `04 01 01 0F 00 01 00 02` |',
|
||||
'| 5 | EBS 测试 | `05 01 01 0F 00 01 00 02` |',
|
||||
'| 6 | 手动驾驶 | `06 01 01 0F 00 01 00 02` |', '',
|
||||
'任务未知通过 byte3 的 bit0=0 编码。例如 `00 01 01 0E 00 01 00 02`'
|
||||
'显示任务未知、已锁定、AS Ready、EBS 正常;不要发送任务号 7,7 是屏内未知状态而不是 CAN 枚举。'
|
||||
'任务号、锁定、EBS、无人状态的数值范围始终校验,有效标志为 0 时也不能发送越界值。', '',
|
||||
'发送顺序:AMI 确认按钮 -> UART 任务帧 -> MCU CAN `0x300` 请求 -> VCU 裁决 -> '
|
||||
'VCU 周期 CAN `0x183` 实际状态 -> AMI 显示。确认发送后可继续显示此前实际任务,'
|
||||
'直到 VCU 更新或反馈超时。', '',
|
||||
'发送工具预览和实发示例:', '', '```powershell',
|
||||
'python scripts/send_vcu_status_can.py --task 2 --locked 1 --ebs-normal 1 --dry-run',
|
||||
'python scripts/send_vcu_status_can.py --interface pcan --channel PCAN_USBBUS1 --bitrate 500000 --task 2 --locked 1 --ebs-normal 1 --duration 10',
|
||||
'python scripts/send_vcu_status_can.py --task 2 --locked 1 --ebs-normal 1 --as-state 1 --dry-run',
|
||||
'python scripts/send_vcu_status_can.py --interface pcan --channel PCAN_USBBUS1 --bitrate 500000 --task 2 --locked 1 --ebs-normal 1 --as-state 1 --duration 10',
|
||||
'# CAN2 改为 --bitrate 250000;Windows 适配器需要时添加 --dll-dir 指定 DLL 目录',
|
||||
'```', '',
|
||||
'屏内全局变量:`current_task=0..6`(7 未知)、`lock_state=0/1/2`、`ebs_state=0/1/2`'
|
||||
'(0 未知、1 绿、2 红)。MCU 更新这些变量,dash 后初始化和 100 ms 定时器更新文案、颜色和任务按钮;切页保留反馈。', '',
|
||||
'(0 未知、1 绿、2 红);`as_state=0..5`,6 未知。MCU 更新这些变量,dash 后初始化和 100 ms 定时器更新文案、颜色和任务按钮;切页保留反馈。', '',
|
||||
'## AMI 任务请求语义', '',
|
||||
'| Task_ID | 任务 |', '| --- | --- |',
|
||||
'| 1 | 八字绕环 |', '| 2 | 直线加速 |', '| 3 | 高速循迹 |',
|
||||
@@ -188,6 +210,7 @@ def render():
|
||||
'当前实际任务由 `0x183` 反馈,不按请求立即显示;未定义逐请求 ACK 或运行完成报文,也不把任务号换成 ASSI 灯状态。', '',
|
||||
'## 显示缩放与失效规则', '',
|
||||
'| 显示项 | 输入和显示规则 | 失效条件 |', '| --- | --- | --- |',
|
||||
'| 车速 | `0x6A2 / DLC 4` 双前轮 uint16 mm/s 平均,`((左+右)*9+2500)/5000`,整数 km/h | 500 ms 无有效轮速帧;保留数字变灰 |',
|
||||
'| 低压电池 | `0x6A1` uint16 mV;`lvbat.val=(mV+5)/10`,显示两位小数 V | 1500 ms 无有效 ADC |',
|
||||
'| 高压电压 | BMS 大端 uint16 × 0.1 V;现有控件四舍五入到整数 V | 2000 ms 无有效 BMS 状态 |',
|
||||
'| 高压电流 | BMS 大端 uint16 × 0.1 - 500 A;四舍五入到整数 A | 2000 ms 无有效 BMS 状态 |',
|
||||
@@ -205,17 +228,17 @@ def render():
|
||||
'全部低电平正常、高电平故障,d15 无来源保持未知。心跳维持已有采样,'
|
||||
'用户最新纠正 PD1 / Safe_S 为左电机互锁(d5),屏幕已删除 BSPD/TSMS。'
|
||||
'心跳和 GPIO 均停止超过 500 ms 后全部未知,恢复需要新的 GPIO 采样;'
|
||||
'轮频 Hz 尚无周长和每圈脉冲数,不能直接当 km/h。', '',
|
||||
'当前 0x6A6 每 1000 ms 上报一次,变化掩码累积期间变化;心跳继续维持已有采样。', '',
|
||||
'有效值显示白色;未知、无效、超时显示深灰色,历史数值保留但不作为有效测量。'
|
||||
'各源独立超时,其他流量不续期。每 5 秒重新同步界面,刚连接需等待首轮同步。', '',
|
||||
'## 报文样例', '',
|
||||
'| ID / DLC | 数据(十六进制) | 解码及仪表显示 |', '| --- | --- | --- |',
|
||||
'| `0x300 / 8` | `01 02 00 01 E8 03 00 00` | 直线加速请求、序号 0、来源 AMI、上电 1000 ms |',
|
||||
'| `0x183 / 8` | `02 01 01 07 00 00 00 01` | 当前直线加速、已锁定、EBS 正常,三项有效 |',
|
||||
'| `0x183 / 8` | `02 01 01 0F 00 01 00 02` | 当前直线加速、已锁定、AS Ready、EBS 正常,四项有效 |',
|
||||
'| `0x182 / 8` | `F4 01 C2 01 90 01 07 01` | 500‰、45.0 ℃、40.0 ℃,三个值有效 |',
|
||||
'| `0x182 / 8` | `0C FE 9B FF 33 FF 07 01` | -500‰、-10.1 ℃、-20.5 ℃;显示 -500 / -10 / -21 |',
|
||||
'| `0x6A1 / 2` | `0C 30` | ADC 12300 mV;低压 12.30 V |',
|
||||
'| `0x6A2 / 8` | `32 00 64 00 96 00 C8 00` | PA0 / PA2 / PA6 / PB14:50 / 100 / 150 / 200 Hz |',
|
||||
'| `0x6A2 / 4` | `10 27 F8 2A` | 左前 10.000 / 右前 11.000 m/s;平均 37.8 km/h,显示 38 |',
|
||||
'| `0x140 / 8` | `0F A0 14 03 00 00 00 05` | 400.0 V / +12.3 A;显示 400 / 12 |',
|
||||
'| `0x186040F3 / 8` 扩展 | `10 73 10 D1 00 00 00 05` | 421.1 V / -69.5 A;显示 421 / -70 |',
|
||||
'| `0x156 / 8` | `42 FF 43 47 37 0A 50 41` | 第二路无效,有效最高温度 40 ℃ |',
|
||||
@@ -233,6 +256,8 @@ def render():
|
||||
'六个任务号与计数边界编码、大端偏置电流、无效温度枚举、全负温度和截短 DLC。'
|
||||
'已安装并验证 cantools 44.1.0,接口参考 [cantools 官方文档](https://cantools.readthedocs.io/en/latest/)。', '',
|
||||
'上述是静态及软件编解码验证,另校验两份 DBC 的速率、相同信号布局及源码 / CubeMX 位时序一致。'
|
||||
'2026-10-07 v2.7 已完成 MCU 烧录及 16672 字节回读一致;真实 CAN1 / MCU / COM6 到大小屏官方模拟器的无人状态与触摸回传验收通过,'
|
||||
'见 [无人状态报告](无人状态显示验证报告.md) 与 [小屏报告](小屏无人状态验证报告.md)。'
|
||||
'v2.2 的烧录回读、新帧 CAN2 测试及串口显示核验状态见 [显示 CAN 测试报告](AMI自定义显示CAN测试报告.md)。'
|
||||
'v2.1 已完成烧录、完整 Flash 回读比对、CAN1 500K 与 CAN2 250K 到官方 HMI 调试器的'
|
||||
'实物测试,详见 [双 CAN 测试报告](CAN1_CAN2_v2.1测试报告.md)。'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//以下代码只在上电时运行一次,一般用于全局变量定义和上电初始化数据
|
||||
int sys0=0,sys1=0,sys2=0 //全局变量定义目前仅支持4字节有符号整形(int),不支持其他类型的全局变量声明,如需使用字符串类型可以在页面中使用变量控件来实现
|
||||
int pending_task=0,current_task=7
|
||||
int lock_state=0,ebs_state=0
|
||||
int shown_task=-1,shown_lock=-1,shown_ebs=-1
|
||||
int lock_state=0,ebs_state=0,as_state=6
|
||||
int shown_task=-1,shown_lock=-1,shown_ebs=-1,shown_as=-1
|
||||
int d0=0,d1=0,d2=0,d3=0,d4=0,d5=0,d6=0,d7=0,d8=0,d9=0,d10=0,d11=0,d12=0,d13=0,d14=0,d15=0
|
||||
baud=9600//配置波特率
|
||||
dim=100//配置屏幕背光
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
draw 0,0,799,479,10565
|
||||
line 180,30,180,456,10565
|
||||
line 524,30,524,456,10565
|
||||
line 210,96,484,96,2047
|
||||
shown_task=-1
|
||||
shown_lock=-1
|
||||
shown_ebs=-1
|
||||
shown_as=-1
|
||||
if(current_task!=shown_task)
|
||||
{
|
||||
rw.txt="未知"
|
||||
rw.pco=33840
|
||||
if(current_task==0)
|
||||
{
|
||||
rw.txt="无"
|
||||
rw.pco=63488
|
||||
}
|
||||
if(current_task>=1&¤t_task<=6)
|
||||
{
|
||||
rw.pco=2016
|
||||
}
|
||||
if(current_task==1)
|
||||
{
|
||||
rw.txt="八字绕环"
|
||||
}
|
||||
if(current_task==2)
|
||||
{
|
||||
rw.txt="直线加速"
|
||||
}
|
||||
if(current_task==3)
|
||||
{
|
||||
rw.txt="高速循迹"
|
||||
}
|
||||
if(current_task==4)
|
||||
{
|
||||
rw.txt="车检测试"
|
||||
}
|
||||
if(current_task==5)
|
||||
{
|
||||
rw.txt="EBS测试"
|
||||
}
|
||||
if(current_task==6)
|
||||
{
|
||||
rw.txt="手动驾驶"
|
||||
}
|
||||
bt0.val=0
|
||||
if(current_task==1)
|
||||
{
|
||||
bt0.val=1
|
||||
}
|
||||
bt1.val=0
|
||||
if(current_task==2)
|
||||
{
|
||||
bt1.val=1
|
||||
}
|
||||
bt2.val=0
|
||||
if(current_task==3)
|
||||
{
|
||||
bt2.val=1
|
||||
}
|
||||
bt3.val=0
|
||||
if(current_task==4)
|
||||
{
|
||||
bt3.val=1
|
||||
}
|
||||
bt4.val=0
|
||||
if(current_task==5)
|
||||
{
|
||||
bt4.val=1
|
||||
}
|
||||
bt5.val=0
|
||||
if(current_task==6)
|
||||
{
|
||||
bt5.val=1
|
||||
}
|
||||
shown_task=current_task
|
||||
}
|
||||
if(lock_state!=shown_lock)
|
||||
{
|
||||
sd.txt="锁定未知"
|
||||
sd.pco=33840
|
||||
if(lock_state==1)
|
||||
{
|
||||
sd.txt="已锁定"
|
||||
sd.pco=2016
|
||||
}
|
||||
if(lock_state==2)
|
||||
{
|
||||
sd.txt="已解锁"
|
||||
sd.pco=63488
|
||||
}
|
||||
shown_lock=lock_state
|
||||
}
|
||||
if(ebs_state!=shown_ebs)
|
||||
{
|
||||
EBS.txt="EBS 未知"
|
||||
EBS.pco=33840
|
||||
EBS.bco=0
|
||||
if(ebs_state==1)
|
||||
{
|
||||
EBS.txt="EBS 正常"
|
||||
EBS.pco=65535
|
||||
EBS.bco=2016
|
||||
}
|
||||
if(ebs_state==2)
|
||||
{
|
||||
EBS.txt="EBS 故障"
|
||||
EBS.pco=65535
|
||||
EBS.bco=63488
|
||||
}
|
||||
shown_ebs=ebs_state
|
||||
}
|
||||
|
||||
if(as_state!=shown_as)
|
||||
{
|
||||
xstr 330,379,174,53,4,33840,0,1,1,1,"无人未知"
|
||||
if(as_state==0)
|
||||
{
|
||||
xstr 330,379,174,53,4,65535,0,1,1,1,"AS OFF"
|
||||
}
|
||||
if(as_state==1)
|
||||
{
|
||||
xstr 330,379,174,53,4,65504,0,1,1,1,"AS Ready"
|
||||
}
|
||||
if(as_state==2)
|
||||
{
|
||||
xstr 330,379,174,53,4,65504,0,1,1,1,"AS Driving"
|
||||
}
|
||||
if(as_state==3)
|
||||
{
|
||||
xstr 330,379,174,53,4,31,0,1,1,1,"AS Finished"
|
||||
}
|
||||
if(as_state==4)
|
||||
{
|
||||
xstr 330,379,174,53,4,63488,0,1,1,1,"AS Emergency"
|
||||
}
|
||||
if(as_state==5)
|
||||
{
|
||||
xstr 330,379,174,53,4,2016,0,1,1,1,"Manual Driving"
|
||||
}
|
||||
shown_as=as_state
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
if(current_task!=shown_task)
|
||||
{
|
||||
rw.txt="未知"
|
||||
rw.pco=33840
|
||||
if(current_task==0)
|
||||
{
|
||||
rw.txt="无"
|
||||
rw.pco=63488
|
||||
}
|
||||
if(current_task>=1&¤t_task<=6)
|
||||
{
|
||||
rw.pco=2016
|
||||
}
|
||||
if(current_task==1)
|
||||
{
|
||||
rw.txt="八字绕环"
|
||||
}
|
||||
if(current_task==2)
|
||||
{
|
||||
rw.txt="直线加速"
|
||||
}
|
||||
if(current_task==3)
|
||||
{
|
||||
rw.txt="高速循迹"
|
||||
}
|
||||
if(current_task==4)
|
||||
{
|
||||
rw.txt="车检测试"
|
||||
}
|
||||
if(current_task==5)
|
||||
{
|
||||
rw.txt="EBS测试"
|
||||
}
|
||||
if(current_task==6)
|
||||
{
|
||||
rw.txt="手动驾驶"
|
||||
}
|
||||
bt0.val=0
|
||||
if(current_task==1)
|
||||
{
|
||||
bt0.val=1
|
||||
}
|
||||
bt1.val=0
|
||||
if(current_task==2)
|
||||
{
|
||||
bt1.val=1
|
||||
}
|
||||
bt2.val=0
|
||||
if(current_task==3)
|
||||
{
|
||||
bt2.val=1
|
||||
}
|
||||
bt3.val=0
|
||||
if(current_task==4)
|
||||
{
|
||||
bt3.val=1
|
||||
}
|
||||
bt4.val=0
|
||||
if(current_task==5)
|
||||
{
|
||||
bt4.val=1
|
||||
}
|
||||
bt5.val=0
|
||||
if(current_task==6)
|
||||
{
|
||||
bt5.val=1
|
||||
}
|
||||
shown_task=current_task
|
||||
}
|
||||
if(lock_state!=shown_lock)
|
||||
{
|
||||
sd.txt="锁定未知"
|
||||
sd.pco=33840
|
||||
if(lock_state==1)
|
||||
{
|
||||
sd.txt="已锁定"
|
||||
sd.pco=2016
|
||||
}
|
||||
if(lock_state==2)
|
||||
{
|
||||
sd.txt="已解锁"
|
||||
sd.pco=63488
|
||||
}
|
||||
shown_lock=lock_state
|
||||
}
|
||||
if(ebs_state!=shown_ebs)
|
||||
{
|
||||
EBS.txt="EBS 未知"
|
||||
EBS.pco=33840
|
||||
EBS.bco=0
|
||||
if(ebs_state==1)
|
||||
{
|
||||
EBS.txt="EBS 正常"
|
||||
EBS.pco=65535
|
||||
EBS.bco=2016
|
||||
}
|
||||
if(ebs_state==2)
|
||||
{
|
||||
EBS.txt="EBS 故障"
|
||||
EBS.pco=65535
|
||||
EBS.bco=63488
|
||||
}
|
||||
shown_ebs=ebs_state
|
||||
}
|
||||
if(as_state!=shown_as)
|
||||
{
|
||||
xstr 330,379,174,53,4,33840,0,1,1,1,"无人未知"
|
||||
if(as_state==0)
|
||||
{
|
||||
xstr 330,379,174,53,4,65535,0,1,1,1,"AS OFF"
|
||||
}
|
||||
if(as_state==1)
|
||||
{
|
||||
xstr 330,379,174,53,4,65504,0,1,1,1,"AS Ready"
|
||||
}
|
||||
if(as_state==2)
|
||||
{
|
||||
xstr 330,379,174,53,4,65504,0,1,1,1,"AS Driving"
|
||||
}
|
||||
if(as_state==3)
|
||||
{
|
||||
xstr 330,379,174,53,4,31,0,1,1,1,"AS Finished"
|
||||
}
|
||||
if(as_state==4)
|
||||
{
|
||||
xstr 330,379,174,53,4,63488,0,1,1,1,"AS Emergency"
|
||||
}
|
||||
if(as_state==5)
|
||||
{
|
||||
xstr 330,379,174,53,4,2016,0,1,1,1,"Manual Driving"
|
||||
}
|
||||
shown_as=as_state
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
xstr 13,4,267,42,2,2047,0,1,1,1,"DEBUG 安全回路"
|
||||
xstr 283,4,392,42,2,33840,0,1,1,1,"灰未知 绿正常 红故障"
|
||||
draw 13,60,185,139,10565
|
||||
xstr 15,64,170,32,4,65535,0,1,1,1,"配电箱"
|
||||
draw 213,60,385,139,10565
|
||||
xstr 215,64,170,32,4,65535,0,1,1,1,"左电机互锁"
|
||||
draw 413,60,585,139,10565
|
||||
xstr 415,64,170,32,4,65535,0,1,1,1,"左急停"
|
||||
draw 613,60,785,139,10565
|
||||
xstr 615,64,170,32,4,65535,0,1,1,1,"电池箱"
|
||||
draw 213,148,385,228,10565
|
||||
xstr 215,152,170,32,4,65535,0,1,1,1,"回路末端"
|
||||
draw 413,148,585,228,10565
|
||||
xstr 415,152,170,32,4,65535,0,1,1,1,"驾舱/右急停"
|
||||
draw 613,148,785,228,10565
|
||||
xstr 615,152,170,32,4,65535,0,1,1,1,"行程/惯性"
|
||||
line 187,99,213,99,2047
|
||||
line 207,94,213,99,2047
|
||||
line 207,104,213,99,2047
|
||||
line 387,99,413,99,2047
|
||||
line 407,94,413,99,2047
|
||||
line 407,104,413,99,2047
|
||||
line 587,99,613,99,2047
|
||||
line 607,94,613,99,2047
|
||||
line 607,104,613,99,2047
|
||||
line 787,99,797,99,2047
|
||||
line 797,99,797,187,2047
|
||||
line 797,187,787,187,2047
|
||||
line 587,187,613,187,2047
|
||||
line 593,182,587,187,2047
|
||||
line 593,192,587,187,2047
|
||||
line 387,187,413,187,2047
|
||||
line 393,182,387,187,2047
|
||||
line 393,192,387,187,2047
|
||||
draw 13,242,385,321,10565
|
||||
xstr 15,245,370,34,4,50712,0,1,1,1,"RES"
|
||||
draw 413,242,785,321,10565
|
||||
xstr 415,245,370,34,4,50712,0,1,1,1,"主控盒"
|
||||
draw 13,332,185,404,10565
|
||||
xstr 15,335,170,34,4,2047,0,1,1,1,"EBS 舵机"
|
||||
draw 213,332,385,404,10565
|
||||
xstr 215,335,170,34,4,2047,0,1,1,1,"EBS 电磁阀"
|
||||
draw 413,332,585,404,10565
|
||||
xstr 415,335,170,34,4,2047,0,1,1,1,"EBS 气压"
|
||||
draw 613,332,785,404,10565
|
||||
xstr 615,335,170,34,4,2047,0,1,1,1,"EBS 油压"
|
||||
draw 13,409,185,478,10565
|
||||
xstr 15,413,170,34,4,2047,0,1,1,1,"无人驾驶惯导"
|
||||
draw 213,409,385,478,10565
|
||||
xstr 215,413,170,34,4,2047,0,1,1,1,"无人驾驶轮速"
|
||||
draw 413,409,585,478,10565
|
||||
xstr 415,413,170,34,4,2047,0,1,1,1,"无人驾驶相机"
|
||||
draw 613,409,785,478,10565
|
||||
xstr 615,413,170,34,4,2047,0,1,1,1,"主控盒12V"
|
||||
if(d0==1)
|
||||
{
|
||||
t0.txt="正常"
|
||||
t0.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d0==2)
|
||||
{
|
||||
t0.txt="故障"
|
||||
t0.pco=63488
|
||||
}else
|
||||
{
|
||||
t0.txt="未知"
|
||||
t0.pco=33840
|
||||
}
|
||||
}
|
||||
if(d1==1)
|
||||
{
|
||||
t1.txt="正常"
|
||||
t1.pco=2016
|
||||
t3.txt="正常"
|
||||
t3.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d1==2)
|
||||
{
|
||||
t1.txt="故障"
|
||||
t1.pco=63488
|
||||
t3.txt="故障"
|
||||
t3.pco=63488
|
||||
}else
|
||||
{
|
||||
t1.txt="未知"
|
||||
t1.pco=33840
|
||||
t3.txt="未知"
|
||||
t3.pco=33840
|
||||
}
|
||||
}
|
||||
if(d2==1)
|
||||
{
|
||||
t2.txt="正常"
|
||||
t2.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d2==2)
|
||||
{
|
||||
t2.txt="故障"
|
||||
t2.pco=63488
|
||||
}else
|
||||
{
|
||||
t2.txt="未知"
|
||||
t2.pco=33840
|
||||
}
|
||||
}
|
||||
if(d3==1)
|
||||
{
|
||||
t4.txt="正常"
|
||||
t4.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d3==2)
|
||||
{
|
||||
t4.txt="故障"
|
||||
t4.pco=63488
|
||||
}else
|
||||
{
|
||||
t4.txt="未知"
|
||||
t4.pco=33840
|
||||
}
|
||||
}
|
||||
if(d4==1)
|
||||
{
|
||||
t5.txt="正常"
|
||||
t5.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d4==2)
|
||||
{
|
||||
t5.txt="故障"
|
||||
t5.pco=63488
|
||||
}else
|
||||
{
|
||||
t5.txt="未知"
|
||||
t5.pco=33840
|
||||
}
|
||||
}
|
||||
if(d5==1)
|
||||
{
|
||||
t6.txt="正常"
|
||||
t6.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d5==2)
|
||||
{
|
||||
t6.txt="故障"
|
||||
t6.pco=63488
|
||||
}else
|
||||
{
|
||||
t6.txt="未知"
|
||||
t6.pco=33840
|
||||
}
|
||||
}
|
||||
if(d6==1)
|
||||
{
|
||||
t7.txt="正常"
|
||||
t7.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d6==2)
|
||||
{
|
||||
t7.txt="故障"
|
||||
t7.pco=63488
|
||||
}else
|
||||
{
|
||||
t7.txt="未知"
|
||||
t7.pco=33840
|
||||
}
|
||||
}
|
||||
if(d7==1)
|
||||
{
|
||||
t8.txt="正常"
|
||||
t8.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d7==2)
|
||||
{
|
||||
t8.txt="故障"
|
||||
t8.pco=63488
|
||||
}else
|
||||
{
|
||||
t8.txt="未知"
|
||||
t8.pco=33840
|
||||
}
|
||||
}
|
||||
if(d8==1)
|
||||
{
|
||||
t9.txt="正常"
|
||||
t9.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d8==2)
|
||||
{
|
||||
t9.txt="故障"
|
||||
t9.pco=63488
|
||||
}else
|
||||
{
|
||||
t9.txt="未知"
|
||||
t9.pco=33840
|
||||
}
|
||||
}
|
||||
if(d9==1)
|
||||
{
|
||||
t10.txt="正常"
|
||||
t10.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d9==2)
|
||||
{
|
||||
t10.txt="故障"
|
||||
t10.pco=63488
|
||||
}else
|
||||
{
|
||||
t10.txt="未知"
|
||||
t10.pco=33840
|
||||
}
|
||||
}
|
||||
if(d10==1)
|
||||
{
|
||||
t11.txt="正常"
|
||||
t11.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d10==2)
|
||||
{
|
||||
t11.txt="故障"
|
||||
t11.pco=63488
|
||||
}else
|
||||
{
|
||||
t11.txt="未知"
|
||||
t11.pco=33840
|
||||
}
|
||||
}
|
||||
if(d11==1)
|
||||
{
|
||||
t12.txt="正常"
|
||||
t12.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d11==2)
|
||||
{
|
||||
t12.txt="故障"
|
||||
t12.pco=63488
|
||||
}else
|
||||
{
|
||||
t12.txt="未知"
|
||||
t12.pco=33840
|
||||
}
|
||||
}
|
||||
if(d12==1)
|
||||
{
|
||||
t13.txt="正常"
|
||||
t13.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d12==2)
|
||||
{
|
||||
t13.txt="故障"
|
||||
t13.pco=63488
|
||||
}else
|
||||
{
|
||||
t13.txt="未知"
|
||||
t13.pco=33840
|
||||
}
|
||||
}
|
||||
if(d13==1)
|
||||
{
|
||||
t14.txt="正常"
|
||||
t14.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d13==2)
|
||||
{
|
||||
t14.txt="故障"
|
||||
t14.pco=63488
|
||||
}else
|
||||
{
|
||||
t14.txt="未知"
|
||||
t14.pco=33840
|
||||
}
|
||||
}
|
||||
if(d14==1)
|
||||
{
|
||||
t15.txt="正常"
|
||||
t15.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d14==2)
|
||||
{
|
||||
t15.txt="故障"
|
||||
t15.pco=63488
|
||||
}else
|
||||
{
|
||||
t15.txt="未知"
|
||||
t15.pco=33840
|
||||
}
|
||||
}
|
||||
if(d15==1)
|
||||
{
|
||||
t16.txt="正常"
|
||||
t16.pco=2016
|
||||
}else
|
||||
{
|
||||
if(d15==2)
|
||||
{
|
||||
t16.txt="故障"
|
||||
t16.pco=63488
|
||||
}else
|
||||
{
|
||||
t16.txt="未知"
|
||||
t16.pco=33840
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
shown_task=-1
|
||||
shown_lock=-1
|
||||
shown_ebs=-1
|
||||
shown_as=-1
|
||||
if(current_task!=shown_task)
|
||||
{
|
||||
rw.txt="未知"
|
||||
@@ -105,3 +106,32 @@ if(ebs_state!=shown_ebs)
|
||||
}
|
||||
shown_ebs=ebs_state
|
||||
}
|
||||
if(as_state!=shown_as)
|
||||
{
|
||||
xstr 194,215,108,30,0,33840,0,1,1,1,"无人未知"
|
||||
if(as_state==0)
|
||||
{
|
||||
xstr 194,215,108,30,0,65535,0,1,1,1,"AS OFF"
|
||||
}
|
||||
if(as_state==1)
|
||||
{
|
||||
xstr 194,215,108,30,0,65504,0,1,1,1,"AS Ready"
|
||||
}
|
||||
if(as_state==2)
|
||||
{
|
||||
xstr 194,215,108,30,0,65504,0,1,1,1,"AS Driving"
|
||||
}
|
||||
if(as_state==3)
|
||||
{
|
||||
xstr 194,215,108,30,0,31,0,1,1,1,"AS Finished"
|
||||
}
|
||||
if(as_state==4)
|
||||
{
|
||||
xstr 194,215,108,30,0,63488,0,1,1,1,"AS Emergency"
|
||||
}
|
||||
if(as_state==5)
|
||||
{
|
||||
xstr 194,215,108,30,0,2016,0,1,1,1,"有人驾驶"
|
||||
}
|
||||
shown_as=as_state
|
||||
}
|
||||
|
||||
@@ -102,3 +102,32 @@ if(ebs_state!=shown_ebs)
|
||||
}
|
||||
shown_ebs=ebs_state
|
||||
}
|
||||
if(as_state!=shown_as)
|
||||
{
|
||||
xstr 194,215,108,30,0,33840,0,1,1,1,"无人未知"
|
||||
if(as_state==0)
|
||||
{
|
||||
xstr 194,215,108,30,0,65535,0,1,1,1,"AS OFF"
|
||||
}
|
||||
if(as_state==1)
|
||||
{
|
||||
xstr 194,215,108,30,0,65504,0,1,1,1,"AS Ready"
|
||||
}
|
||||
if(as_state==2)
|
||||
{
|
||||
xstr 194,215,108,30,0,65504,0,1,1,1,"AS Driving"
|
||||
}
|
||||
if(as_state==3)
|
||||
{
|
||||
xstr 194,215,108,30,0,31,0,1,1,1,"AS Finished"
|
||||
}
|
||||
if(as_state==4)
|
||||
{
|
||||
xstr 194,215,108,30,0,63488,0,1,1,1,"AS Emergency"
|
||||
}
|
||||
if(as_state==5)
|
||||
{
|
||||
xstr 194,215,108,30,0,2016,0,1,1,1,"有人驾驶"
|
||||
}
|
||||
shown_as=as_state
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
unicorn==2.1.4
|
||||
pyelftools==0.33
|
||||
@@ -5,12 +5,16 @@ import os
|
||||
import time
|
||||
|
||||
|
||||
def build_payload(task, locked, ebs_normal, valid=7, sequence=0):
|
||||
def build_payload(task, locked, ebs_normal, valid=7, sequence=0, as_state=0, version=2):
|
||||
if task not in range(7) or locked not in (0, 1) or ebs_normal not in (0, 1):
|
||||
raise ValueError('task must be 0..6; locked and ebs-normal must be 0 or 1')
|
||||
if valid not in range(8) or sequence not in range(256):
|
||||
raise ValueError('valid must be 0..7 and sequence must be 0..255')
|
||||
return bytes([task, locked, ebs_normal, valid, sequence, 0, 0, 1])
|
||||
if version not in (1, 2) or as_state not in range(6):
|
||||
raise ValueError('version must be 1 or 2; as-state must be 0..5')
|
||||
if valid not in range(8 if version == 1 else 16) or sequence not in range(256):
|
||||
raise ValueError('valid must be 0..7 (v1) or 0..15 (v2); sequence must be 0..255')
|
||||
if version == 1 and as_state != 0:
|
||||
raise ValueError('v1 has no AS state; byte 5 must be zero')
|
||||
return bytes([task, locked, ebs_normal, valid, sequence, as_state, 0, version])
|
||||
|
||||
|
||||
def main():
|
||||
@@ -18,7 +22,10 @@ def main():
|
||||
parser.add_argument('--task', type=int, required=True, choices=range(7))
|
||||
parser.add_argument('--locked', type=int, required=True, choices=(0, 1))
|
||||
parser.add_argument('--ebs-normal', type=int, required=True, choices=(0, 1))
|
||||
parser.add_argument('--valid', type=int, default=7)
|
||||
parser.add_argument('--valid', type=int, help='default 15 with --as-state, otherwise 7')
|
||||
parser.add_argument('--as-state', type=int, choices=range(6),
|
||||
help='0 OFF, 1 Ready, 2 Driving, 3 Finished, 4 Emergency, 5 Manual')
|
||||
parser.add_argument('--version', type=int, choices=(1, 2), default=2)
|
||||
parser.add_argument('--sequence', type=int, default=0)
|
||||
parser.add_argument('--interface', default='pcan')
|
||||
parser.add_argument('--channel', default='PCAN_USBBUS1')
|
||||
@@ -29,7 +36,12 @@ def main():
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
payload = build_payload(args.task, args.locked, args.ebs_normal, args.valid, args.sequence)
|
||||
if args.version == 1 and args.as_state is not None:
|
||||
raise ValueError('--as-state requires --version 2')
|
||||
valid = args.valid if args.valid is not None else (15 if args.as_state is not None else 7)
|
||||
as_state = args.as_state if args.as_state is not None else 0
|
||||
payload = build_payload(args.task, args.locked, args.ebs_normal, valid, args.sequence,
|
||||
as_state, args.version)
|
||||
except ValueError as exc:
|
||||
parser.error(str(exc))
|
||||
if not (0 < args.period <= .5 and 0 < args.duration and math.isfinite(args.duration)):
|
||||
@@ -50,7 +62,7 @@ def main():
|
||||
deadline = started
|
||||
while time.monotonic() < started + args.duration:
|
||||
payload = build_payload(args.task, args.locked, args.ebs_normal,
|
||||
args.valid, (args.sequence + count) & 255)
|
||||
valid, (args.sequence + count) & 255, as_state, args.version)
|
||||
bus.send(can.Message(arbitration_id=0x183, is_extended_id=False, data=payload), timeout=.2)
|
||||
count += 1
|
||||
deadline += args.period
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Verify actual ARM CAN -> AS/VCU display -> UART code with mocked HAL I/O."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from verify_wheel_speed_firmware import Firmware, ROOT
|
||||
from send_vcu_status_can import build_payload
|
||||
|
||||
def verify(path):
|
||||
checks=[]
|
||||
f=Firmware(path)
|
||||
def states():
|
||||
return list(f.uc.mem_read(f.symbols['vcu_display_state'],4))
|
||||
def flush():
|
||||
f.uc.mem_write(f.symbols['vcu_display_dirty'],b'\x0f')
|
||||
f.commands.clear()
|
||||
f.drain()
|
||||
expected=[f'{prefix}={value}' for prefix,value in zip(
|
||||
('current_task','lock_state','ebs_state','as_state'),states())]
|
||||
assert all(cmd in f.commands for cmd in expected),(expected,f.commands)
|
||||
assert states()==[7,0,0,6]
|
||||
flush()
|
||||
checks.append('boot: all four unknown; complete UART commands end in real FF FF FF')
|
||||
for port in (1,2):
|
||||
for task in range(7):
|
||||
for as_state in range(6):
|
||||
for flags in range(16):
|
||||
f.now=1000
|
||||
f.receive(build_payload(task,task&1,(task+1)&1,flags,255,as_state),port,ident=0x183)
|
||||
expected=[task if flags&1 else 7,
|
||||
(1 if task&1 else 2) if flags&2 else 0,
|
||||
(1 if (task+1)&1 else 2) if flags&4 else 0,
|
||||
as_state if flags&8 else 6]
|
||||
assert states()==expected,(port,task,as_state,flags,states())
|
||||
flush()
|
||||
checks.append(f'CAN{port}: all 7 tasks x 6 AS states x 16 independent validity combinations; UART checked')
|
||||
for flags in range(8):
|
||||
f.receive(build_payload(2,1,1,15,0,4),ident=0x183)
|
||||
f.receive(build_payload(2,1,1,flags,0,version=1),ident=0x183)
|
||||
assert states()[3]==6
|
||||
flush()
|
||||
checks.append('legacy v1 always clears previous v2 AS to unknown; all 8 legacy flags accepted')
|
||||
baseline=build_payload(2,1,1,15,0,1)
|
||||
malformed=[]
|
||||
for index,value in [(0,7),(1,2),(2,2),(3,16),(5,6),(5,255),(6,1),(7,0),(7,3)]:
|
||||
b=bytearray(baseline);b[index]=value;malformed.append((bytes(b),{}))
|
||||
b=bytearray(baseline);b[3]=7;b[5]=6;malformed.append((bytes(b),{}))
|
||||
for index,value in [(3,8),(5,1),(6,1)]:
|
||||
b=bytearray(build_payload(2,1,1,7,0,version=1));b[index]=value;malformed.append((bytes(b),{}))
|
||||
for opts in ({'dlc':7},{'dlc':9},{'id_type':0x40000000},{'remote':0x20000000},{'fd':0x00200000}):
|
||||
malformed.append((baseline,opts))
|
||||
for port in (1,2):
|
||||
for data,opts in malformed:
|
||||
f.now=2000;f.receive(baseline,port,ident=0x183)
|
||||
before=states();f.now=2200;f.receive(data,port,ident=0x183,**opts)
|
||||
assert states()==before
|
||||
assert f.read_u32('vcu_status_last_rx_ms')==2000
|
||||
f.service(2501);assert states()==[7,0,0,6]
|
||||
checks.append('both ports: malformed DLC/version/enum/reserved/extended/remote/FD rejected without timeout renewal')
|
||||
for start,boundary,expired in ((0,500,501),(0xFFFFFF00,244,245)):
|
||||
f=Firmware(path)
|
||||
f.now=start;f.receive(baseline,ident=0x183)
|
||||
f.service(boundary);assert states()==[2,1,1,1]
|
||||
f.service(expired);assert states()==[7,0,0,6]
|
||||
if f.now < 800: f.now=800
|
||||
flush()
|
||||
checks.append('timeout at >500 ms, tick zero and wrap verified; all four unknown transmitted')
|
||||
f=Firmware(path);f.now=1000;f.receive(baseline,ident=0x183)
|
||||
f.now=1400;f.receive(bytes(4),ident=0x6A2)
|
||||
f.service(1501);assert states()==[7,0,0,6]
|
||||
f.now=1502;f.receive(baseline,ident=0x183);flush();assert states()==[2,1,1,1]
|
||||
f.now=6502;f.receive(baseline,ident=0x183)
|
||||
f.commands.clear();f.service();f.drain()
|
||||
assert 'as_state=1' in f.commands
|
||||
checks.append('other CAN cannot renew AS; same-value recovery and periodic resync UART work')
|
||||
return dict(elf=str(path.relative_to(ROOT)),sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
result='passed',checks=checks,evidence='ARM emulation with mocked HAL; no physical CAN/HMI')
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--output',type=Path)
|
||||
a=p.parse_args()
|
||||
result=[verify(ROOT/f'firmware/build/{mode}/AMI_Controller.elf') for mode in ('Debug','Release')]
|
||||
text=json.dumps(result,ensure_ascii=False,indent=2)
|
||||
if a.output:a.output.parent.mkdir(parents=True,exist_ok=True);a.output.write_text(text+'\n',encoding='utf-8')
|
||||
print(text)
|
||||
+28
-7
@@ -74,7 +74,7 @@ def main():
|
||||
identifiers = {int(value, 16) for value in re.findall(
|
||||
r'^#define\s+AMI_CAN_ID_\w+\s+(0x[\dA-Fa-f]+)', header, re.MULTILINE)}
|
||||
expected = {0x182:(False,8), 0x183:(False,8), 0x300:(False,8), 0x301:(False,8), 0x6A0:(False,8), 0x6A1:(False,2),
|
||||
0x6A2:(False,8), 0x6A4:(False,4), 0x6A6:(False,8),
|
||||
0x6A2:(False,4), 0x6A4:(False,4), 0x6A6:(False,8),
|
||||
0x140:(False,8), 0x156:(False,8),
|
||||
0x186040F3:(True,8), 0x186140F3:(True,8)}
|
||||
actual = {m.frame_id:(m.is_extended_frame,m.length) for m in db.messages}
|
||||
@@ -90,12 +90,13 @@ def main():
|
||||
assert m.senders == (['VCU'] if m.frame_id in (0x182,0x183) else
|
||||
['SDC_ACQ_DAC'] if 0x6A0 <= m.frame_id <= 0x6A6 else ['BMS'])
|
||||
assert db.dbc.attributes['Baudrate'].value == 500000
|
||||
for ident, cycle in [(0x301,100), (0x182,100), (0x183,100), (0x6A0,100), (0x6A1,500), (0x6A2,20)]:
|
||||
for ident, cycle in [(0x301,100), (0x182,100), (0x183,100), (0x6A0,100), (0x6A1,500), (0x6A2,20), (0x6A6,1000)]:
|
||||
assert db.get_message_by_frame_id(ident).cycle_time == cycle
|
||||
for ident in [0x300,0x6A4,0x6A6,0x140,0x156,0x186040F3,0x186140F3]:
|
||||
for ident in [0x300,0x6A4,0x140,0x156,0x186040F3,0x186140F3]:
|
||||
assert db.get_message_by_frame_id(ident).cycle_time in (None,0)
|
||||
|
||||
assert len(db2.messages) == 12
|
||||
assert sum(len(m.signals) for m in db2.messages) == 49
|
||||
assert all(m.frame_id != 0x301 for m in db2.messages)
|
||||
heartbeat = db.get_message_by_frame_id(0x301)
|
||||
for sequence, uptime in ((0,100), (65535,4294967295), (0,0)):
|
||||
@@ -119,9 +120,17 @@ def main():
|
||||
assert values['EBS_Normal']==((task_id+1)&1)
|
||||
assert status.encode(values)==raw
|
||||
assert db2.get_message_by_frame_id(0x183).encode(values)==raw
|
||||
assert build_vcu_payload(task_id,task_id&1,(task_id+1)&1,flags,255)==raw
|
||||
assert build_vcu_payload(task_id,task_id&1,(task_id+1)&1,flags,255,version=1)==raw
|
||||
for as_state in range(6):
|
||||
for flags in range(16):
|
||||
raw = build_vcu_payload(2, 1, 1, flags, 255, as_state)
|
||||
values = decode(0x183, raw.hex(' '))
|
||||
assert values['AS_State'] == as_state and values['AS_Valid'] == ((flags >> 3) & 1)
|
||||
assert values['Reserved'] == 0 and values['Protocol_Version'] == 2
|
||||
assert status.encode(values) == raw
|
||||
assert db2.get_message_by_frame_id(0x183).encode(values) == raw
|
||||
for arguments in ((7,1,1,7,0), (2,2,1,7,0), (2,1,2,7,0),
|
||||
(2,1,1,8,0), (2,1,1,7,256)):
|
||||
(2,1,1,16,0), (2,1,1,7,256)):
|
||||
try:
|
||||
build_vcu_payload(*arguments)
|
||||
except ValueError:
|
||||
@@ -177,8 +186,20 @@ def main():
|
||||
assert heartbeat['Board_Identifier']==0xA1 and heartbeat['Protocol_Version']==1
|
||||
assert heartbeat['Heartbeat_Sequence']==0x1234
|
||||
near(heartbeat['Uptime'],1.0)
|
||||
freq=decode(0x6A2,'32 00 64 00 96 00 C8 00')
|
||||
assert list(freq.values()) == [50,100,150,200]
|
||||
wheel = db.get_message_by_frame_id(0x6A2)
|
||||
assert wheel.name == 'SDC_WheelSpeed'
|
||||
for raw, speeds in [('00 00 00 00',(0,0)), ('10 27 F8 2A',(10,11)),
|
||||
('FF FF FF FF',(65.535,65.535)), ('00 00 FF FF',(0,65.535))]:
|
||||
values = decode(0x6A2,raw)
|
||||
for actual, expected_speed in zip(values.values(),speeds,strict=True):
|
||||
near(actual,expected_speed)
|
||||
assert wheel.encode(values) == bytes.fromhex(raw)
|
||||
try:
|
||||
wheel.decode(bytes.fromhex('32 00 64 00 96 00 C8 00'),allow_excess=False)
|
||||
except cantools.database.DecodeError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError('Old DLC 8 wheel-frequency payload accepted')
|
||||
gpio=decode(0x6A6,'AA 55 FF 7F E8 03 00 00')
|
||||
assert gpio['SDC_Input_State']==0x55AA and gpio['SDC_Changed_Mask']==0x7fff
|
||||
near(gpio['Timestamp'],1.0)
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
"""Execute compiled STM32 firmware in Unicorn; HAL I/O is mocked, no hardware used.
|
||||
|
||||
Requires scripts/requirements-test.txt and a built Debug or Release ELF.
|
||||
Tests CAN RX -> display state -> ASCII UART DMA commands using actual ARM code.
|
||||
"""
|
||||
import argparse
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
|
||||
from elftools.elf.elffile import ELFFile
|
||||
from unicorn import Uc, UC_ARCH_ARM, UC_MODE_THUMB, UC_MODE_MCLASS, UC_HOOK_CODE
|
||||
from unicorn.arm_const import (
|
||||
UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3,
|
||||
UC_ARM_REG_SP, UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_XPSR,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REGS = (UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3)
|
||||
|
||||
|
||||
class Firmware:
|
||||
def __init__(self, path):
|
||||
self.uc = Uc(UC_ARCH_ARM, UC_MODE_THUMB | UC_MODE_MCLASS)
|
||||
self.uc.mem_map(0x08000000, 0x20000)
|
||||
self.uc.mem_map(0x20000000, 0x24000)
|
||||
self.uc.mem_map(0x40000000, 0x30000)
|
||||
self.uc.mem_map(0x50000000, 0x10000)
|
||||
with path.open('rb') as file:
|
||||
elf = ELFFile(file)
|
||||
self.symbols = {s.name: s['st_value'] for s in elf.get_section_by_name('.symtab').iter_symbols()}
|
||||
for segment in elf.iter_segments():
|
||||
if segment['p_type'] == 'PT_LOAD' and segment['p_filesz']:
|
||||
self.uc.mem_write(segment['p_vaddr'], segment.data())
|
||||
self.now = 800
|
||||
self.rx = []
|
||||
self.commands = []
|
||||
self.hooks = {self.symbols[name] & ~1: name for name in (
|
||||
'HAL_GetTick', 'HAL_GPIO_WritePin', 'HAL_GPIO_TogglePin',
|
||||
'HAL_FDCAN_GetRxFifoFillLevel', 'HAL_FDCAN_GetRxMessage',
|
||||
'HAL_FDCAN_GetTxFifoFreeLevel', 'HAL_UART_Transmit_DMA',
|
||||
)}
|
||||
self.uc.hook_add(UC_HOOK_CODE, self.hook)
|
||||
for port, instance in ((1,0x40006400), (2,0x40006800)):
|
||||
self.write_u32(f'hfdcan{port}', instance)
|
||||
|
||||
def hook(self, uc, address, size, data):
|
||||
name = self.hooks.get(address)
|
||||
if name is None:
|
||||
return
|
||||
result = 0
|
||||
if name == 'HAL_GetTick':
|
||||
result = self.now
|
||||
elif name == 'HAL_FDCAN_GetRxFifoFillLevel':
|
||||
result = len(self.rx)
|
||||
elif name == 'HAL_FDCAN_GetRxMessage':
|
||||
header, payload = self.rx.pop(0)
|
||||
uc.mem_write(uc.reg_read(UC_ARM_REG_R2), header)
|
||||
uc.mem_write(uc.reg_read(UC_ARM_REG_R3), payload)
|
||||
elif name == 'HAL_UART_Transmit_DMA':
|
||||
command = bytes(uc.mem_read(uc.reg_read(UC_ARM_REG_R1), uc.reg_read(UC_ARM_REG_R2)))
|
||||
assert command.endswith(b'\xff\xff\xff'), command
|
||||
self.commands.append(command[:-3].decode('ascii'))
|
||||
# TX free level is zero: heartbeat never touches hardware in this test.
|
||||
uc.reg_write(UC_ARM_REG_R0, result)
|
||||
uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))
|
||||
|
||||
def call(self, name, *args):
|
||||
for register, value in zip(REGS, args):
|
||||
self.uc.reg_write(register, value)
|
||||
self.uc.reg_write(UC_ARM_REG_SP, 0x20023000)
|
||||
self.uc.reg_write(UC_ARM_REG_LR, 0x0801FF01)
|
||||
self.uc.reg_write(UC_ARM_REG_XPSR, 1 << 24)
|
||||
self.uc.emu_start(self.symbols[name] | 1, 0x0801FF00, count=200000)
|
||||
assert self.uc.reg_read(UC_ARM_REG_PC) == 0x0801FF00, f'{name} exceeded instruction budget'
|
||||
return self.uc.reg_read(UC_ARM_REG_R0)
|
||||
|
||||
def read_u32(self, name, offset=0):
|
||||
return struct.unpack('<I',self.uc.mem_read(self.symbols[name]+offset,4))[0]
|
||||
|
||||
def write_u32(self, name, value):
|
||||
self.uc.mem_write(self.symbols[name],struct.pack('<I',value))
|
||||
|
||||
def valid(self):
|
||||
return bool(self.uc.mem_read(self.symbols['display_state']+32,1)[0] & 1)
|
||||
|
||||
def speed(self):
|
||||
return self.read_u32('display_state')
|
||||
|
||||
def receive(self, payload, port=1, ident=0x6A2, id_type=0, remote=0, fd=0, dlc=None):
|
||||
dlc = len(payload) if dlc is None else dlc
|
||||
header = struct.pack('<10I',ident,id_type,remote,dlc,0,0,fd,0,0,0)
|
||||
self.rx.append((header,payload))
|
||||
self.call('HAL_FDCAN_RxFifo0Callback',self.symbols[f'hfdcan{port}'],1)
|
||||
assert not self.rx
|
||||
|
||||
def service(self, now=None):
|
||||
if now is not None:
|
||||
self.now = now & 0xFFFFFFFF
|
||||
self.call('AMI_App_Service')
|
||||
# Model the completion of the UART DMA transfer, without real UART I/O.
|
||||
self.uc.mem_write(self.symbols['screen_tx_busy'],b'\0')
|
||||
|
||||
def drain(self):
|
||||
for _ in range(40):
|
||||
self.service()
|
||||
|
||||
|
||||
def verify(path):
|
||||
cases = []
|
||||
f = Firmware(path)
|
||||
f.write_u32('display_dirty',0xFFFFFFFF)
|
||||
f.drain()
|
||||
assert not f.valid() and f.speed() == 0
|
||||
assert 'dash.speed.val=0' in f.commands and 'dash.speed.pco=16904' in f.commands
|
||||
cases.append('boot unknown speed: grey zero, UART terminator verified')
|
||||
|
||||
samples = [(0,0), (10000,11000), (11000,10000), (0,20000),
|
||||
(1249,1250), (1250,1250), (1250,1251), (65535,65535), (0,65535)]
|
||||
for port in (1,2):
|
||||
for left,right in samples:
|
||||
f.now = 1000
|
||||
f.commands.clear()
|
||||
f.receive(struct.pack('<HH',left,right),port=port)
|
||||
expected = int(((Decimal(left)+Decimal(right))/2000*Decimal('3.6'))
|
||||
.quantize(Decimal('1'),rounding=ROUND_HALF_UP))
|
||||
assert f.valid() and f.speed() == expected,(port,left,right,f.speed(),expected)
|
||||
raw = struct.unpack('<HH',f.uc.mem_read(f.symbols['sdc_wheel_speed_mm_s'],4))
|
||||
assert raw == (left,right)
|
||||
assert f.read_u32('can_port_last_rx_ms',4*(port-1)) == f.now
|
||||
f.drain()
|
||||
# An unchanged display value may legitimately produce no UART command.
|
||||
assert f'dash.speed.val={expected}' in f.commands or not any(
|
||||
c.startswith('dash.speed.val=') for c in f.commands)
|
||||
cases.append(f'CAN{port}: {len(samples)} zero, asymmetric, rounding and saturation vectors')
|
||||
|
||||
for kwargs,payload in [
|
||||
({},bytes.fromhex('32 00 64 00 96 00 C8 00')),
|
||||
({},bytes(3)), ({},bytes(5)), ({'dlc':9,'fd':0x00200000},bytes(12)),
|
||||
({'id_type':0x40000000},bytes(4)),
|
||||
({'remote':0x20000000},bytes(4)), ({'fd':0x00200000},bytes(4)),
|
||||
({'ident':0x180},bytes(8)),
|
||||
]:
|
||||
f = Firmware(path)
|
||||
f.now = 1000
|
||||
f.receive(struct.pack('<HH',10000,11000))
|
||||
snapshot = bytes(f.uc.mem_read(f.symbols['sdc_wheel_speed_mm_s'],4))
|
||||
f.now = 1400
|
||||
f.receive(payload,**kwargs)
|
||||
assert f.speed() == 38 and f.valid()
|
||||
assert f.read_u32('wheel_speed_last_rx_ms') == 1000
|
||||
assert bytes(f.uc.mem_read(f.symbols['sdc_wheel_speed_mm_s'],4)) == snapshot
|
||||
f.service(1500)
|
||||
assert f.valid(), '500 ms boundary must still be valid'
|
||||
f.service(1501)
|
||||
assert not f.valid() and f.speed() == 38
|
||||
f.drain()
|
||||
assert 'dash.speed.pco=16904' in f.commands
|
||||
cases.append('8 invalid DLC/format/legacy frames reject without overwriting or refreshing expiry')
|
||||
|
||||
f = Firmware(path)
|
||||
f.now = 1000
|
||||
f.receive(struct.pack('<HH',10000,11000))
|
||||
f.drain()
|
||||
assert 'dash.speed.val=38' in f.commands and 'dash.speed.pco=65535' in f.commands
|
||||
f.now = 1501
|
||||
for ident,payload in [(0x6A0,bytes.fromhex('A1 01 00 00 00 00 00 00')),
|
||||
(0x6A6,bytes(8)), (0x6A1,bytes.fromhex('0C 30')),
|
||||
(0x140,bytes(8)), (0x183,bytes.fromhex('02 01 01 07 00 00 00 01'))]:
|
||||
f.receive(payload,ident=ident)
|
||||
f.service()
|
||||
assert not f.valid() and f.speed() == 38
|
||||
f.now = 1600
|
||||
f.commands.clear()
|
||||
f.receive(struct.pack('<HH',10000,11000),port=2)
|
||||
f.drain()
|
||||
assert f.valid() and 'dash.speed.val=38' in f.commands and 'dash.speed.pco=65535' in f.commands
|
||||
f.receive(bytes(4))
|
||||
f.drain()
|
||||
assert f.valid() and f.speed() == 0 and 'dash.speed.val=0' in f.commands
|
||||
cases.append('unrelated SDC/BMS/VCU traffic cannot renew; same-value recovery and white zero work')
|
||||
|
||||
for started,boundary,expired in ((0,500,501),(0xFFFFFF00,244,245)):
|
||||
f = Firmware(path)
|
||||
f.now = started
|
||||
f.receive(struct.pack('<HH',10000,11000))
|
||||
f.service(boundary)
|
||||
assert f.valid()
|
||||
f.service(expired)
|
||||
assert not f.valid() and f.speed() == 38
|
||||
cases.append('tick zero and uint32 wrap: independent timeout boundary works')
|
||||
return {'elf':str(path.relative_to(ROOT)), 'sha256':hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
'result':'passed','checks':cases,'evidence':'ARM emulation with mocked HAL, not physical CAN/HMI'}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--elf',type=Path,action='append')
|
||||
parser.add_argument('--output',type=Path)
|
||||
args = parser.parse_args()
|
||||
paths = args.elf or [ROOT/'firmware/build/Debug/AMI_Controller.elf',
|
||||
ROOT/'firmware/build/Release/AMI_Controller.elf']
|
||||
results = [verify(p.resolve()) for p in paths]
|
||||
text = json.dumps(results,ensure_ascii=False,indent=2)
|
||||
if args.output:
|
||||
args.output.parent.mkdir(parents=True,exist_ok=True)
|
||||
args.output.write_text(text+'\n',encoding='utf-8')
|
||||
print(text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user