مقایسه کامیتها
3
کامیتها
c9d72c6319
...
3c470a8fea
| مولف | SHA1 | تاریخ | |
|---|---|---|---|
|
|
3c470a8fea | ||
|
|
b5adbec417 | ||
|
|
106084d6a4 |
@@ -0,0 +1,15 @@
|
||||
name: Configure Boot Command for Detected Flash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
set-bootcmd:
|
||||
runs-on: serial-runner
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect flash, set bootcmd and verify
|
||||
run: /usr/bin/python3 scripts/serial_command.py set-bootcmd
|
||||
@@ -1,5 +1,5 @@
|
||||
name: flash-aminpa-V1.5
|
||||
# Production flash for Amin Pardaz Asia device v1.5
|
||||
name: flash-aminpa-V1.5.yml
|
||||
# Production hardware flashing for Amin Pardaz Asia device v1.5
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -12,7 +12,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
flash-aminpa:
|
||||
name: Production flash - Amin Pardaz Asia v1.5
|
||||
name: Hardware flashing - Amin Pardaz Asia v1.5
|
||||
runs-on: serial-runner
|
||||
|
||||
steps:
|
||||
@@ -63,3 +63,9 @@ jobs:
|
||||
|
||||
- name: Inspect boot configuration
|
||||
run: /usr/bin/python3 scripts/serial_command.py boot-config
|
||||
|
||||
- name: Configure and verify bootcmd for detected flash
|
||||
run: /usr/bin/python3 scripts/serial_command.py set-bootcmd
|
||||
|
||||
- name: Reboot hardware after successful flashing
|
||||
run: /usr/bin/python3 scripts/serial_command.py reboot
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Software test - Amin Pardaz Asia v1.5
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- flash-aminpa-V1.5.yml
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ping-device:
|
||||
if: ${{ gitea.event_name == 'workflow_dispatch' || gitea.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: serial-runner
|
||||
|
||||
steps:
|
||||
- name: Checkout tested revision
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ gitea.event.workflow_run.head_sha || gitea.sha }}
|
||||
|
||||
- name: Wait up to three minutes for device ping
|
||||
env:
|
||||
TEST_INTERFACE: ${{ vars.TEST_INTERFACE || 'eno1' }}
|
||||
TEST_HOST_IP: ${{ vars.TEST_HOST_IP || '192.168.75.15' }}
|
||||
TEST_DEVICE_IP: ${{ vars.TEST_DEVICE_IP }}
|
||||
run: |
|
||||
/usr/bin/python3 scripts/software_test.py --interface "$TEST_INTERFACE" --host-ip "$TEST_HOST_IP" --device-ip "$TEST_DEVICE_IP"
|
||||
@@ -0,0 +1,39 @@
|
||||
# فلش و تست دستگاه Amin Pardaz Asia v1.5
|
||||
|
||||
workflow اصلی `flash-aminpa-V1.5.yml` با push به شاخهٔ `main` اجرا میشود.
|
||||
workflow تست نرمافزاری برای اجرا پس از موفقیت آن و همچنین اجرای دستی تعریف شده است.
|
||||
|
||||
## متغیرهای Gitea برای تست نرمافزاری
|
||||
|
||||
متغیرها را در تنظیمات مخزن Gitea، بخش **Actions → Variables** تعریف کنید.
|
||||
نام متغیرها باید دقیقاً مطابق جدول باشد.
|
||||
|
||||
| نام متغیر | کاربرد | پیشفرض | الزامی |
|
||||
| --- | --- | --- | --- |
|
||||
| `TEST_INTERFACE` | نام کارت شبکهٔ میزبان runner برای تست | `eno1` | خیر |
|
||||
| `TEST_HOST_IP` | آدرس IPv4 میزبان runner روی کارت شبکهٔ تست | `192.168.75.15` | خیر |
|
||||
| `TEST_DEVICE_IP` | آدرس IPv4 دستگاه فلششده که باید به ping پاسخ دهد | ندارد | بله |
|
||||
|
||||
`TEST_HOST_IP` آدرس میزبان است، نه دستگاه هدف. مقدار `TEST_DEVICE_IP` باید
|
||||
با آن متفاوت باشد و با IP دستگاه پس از بوت سیستمعامل مطابقت داشته باشد؛
|
||||
این مقدار لزوماً همان `ipaddr` موقت در U-Boot نیست.
|
||||
|
||||
IP میزبان باید از قبل روی کارت شبکهٔ انتخابشده تنظیم شده باشد و مسیر رسیدن
|
||||
به دستگاه از همان کارت شبکه عبور کند. اسکریپت تنظیمات شبکهٔ میزبان را تغییر نمیدهد.
|
||||
مقادیر IP بهصورت آدرس IPv4 وارد میشوند، نه رنج یا CIDR.
|
||||
|
||||
اسکریپت با پایتون سیستم و ابزارهای سیستم `ip` و `ping` اجرا میشود.
|
||||
پس از بررسی تنظیمات میزبان، حداکثر ۱۸۰ ثانیه منتظر پاسخ ping دستگاه میماند؛
|
||||
اولین پاسخ موفق نتیجهٔ `PASS` دارد. نبود پاسخ تا پایان مهلت یا خطای تنظیمات
|
||||
باعث `FAIL` و شکست workflow میشود.
|
||||
|
||||
نمونهٔ اجرای دستی روی runner با جایگزینکردن IP واقعی دستگاه:
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 scripts/software_test.py \
|
||||
--interface eno1 \
|
||||
--host-ip 192.168.75.15 \
|
||||
--device-ip <DEVICE_IP>
|
||||
```
|
||||
|
||||
برای محل فایلهای firmware و نقشهٔ فلش، [راهنمای firmware](firmware/README.md) را ببینید.
|
||||
@@ -184,6 +184,20 @@ def erase_flash(cli):
|
||||
print("\nFull-flash erase command succeeded; first 256 bytes verified as FF.")
|
||||
|
||||
|
||||
def set_bootcmd(cli):
|
||||
capacity = detect_flash(cli)
|
||||
dtb_offset, dtb_size = LAYOUTS[capacity]["dtb"]
|
||||
kernel_offset, kernel_size = LAYOUTS[capacity]["kernel"]
|
||||
bootcmd = (
|
||||
f"sf probe 0; sf read 0x41800000 {dtb_offset:#x} {dtb_size:#x}; "
|
||||
f"sf read 0x42000000 {kernel_offset:#x} {kernel_size:#x}; "
|
||||
"bootz 0x42000000 - 0x41800000"
|
||||
)
|
||||
cli.command("setenv bootcmd '" + bootcmd + "'")
|
||||
cli.command("printenv bootcmd", r"^bootcmd=" + re.escape(bootcmd) + r"$")
|
||||
print("\nDynamic bootcmd verified in RAM; environment not saved and bootcmd not executed.")
|
||||
|
||||
|
||||
def boot_config(cli, confirmed=False):
|
||||
cli.command(
|
||||
"printenv bootcmd bootargs", allow_missing=("bootcmd", "bootargs"),
|
||||
@@ -199,7 +213,7 @@ def boot_config(cli, confirmed=False):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("mode", choices=("check", "configure", "transfer", "transfer-dtb", "transfer-kernel", "transfer-rootfs", "erase", "boot-config", "preflight"), default="check", nargs="?")
|
||||
parser.add_argument("mode", choices=("check", "configure", "transfer", "transfer-dtb", "transfer-kernel", "transfer-rootfs", "erase", "boot-config", "preflight", "set-bootcmd", "reboot"), default="check", nargs="?")
|
||||
parser.add_argument("--port", default="/dev/ttyUSB0")
|
||||
parser.add_argument("--firmware", type=Path, default=None)
|
||||
parser.add_argument("--rootfs-size", type=lambda value: int(value, 0), help="Actual RootFS partition size in bytes (decimal or 0x-prefixed hex)")
|
||||
@@ -218,6 +232,12 @@ def main():
|
||||
cli.detect()
|
||||
if args.mode == "check":
|
||||
return
|
||||
if args.mode == "reboot":
|
||||
print("\n>>> reset", flush=True)
|
||||
port.write(b"reset\r")
|
||||
port.flush()
|
||||
print("Reset command sent; reboot completion has not been verified.", flush=True)
|
||||
return
|
||||
if args.mode == "preflight":
|
||||
capacity = detect_flash(cli)
|
||||
files = {"uboot": "u-boot-sunxi-with-spl.bin", "dtb": "sun8i-v3s-licheepi-zero-dock.dtb",
|
||||
@@ -236,6 +256,9 @@ def main():
|
||||
if args.mode == "boot-config":
|
||||
boot_config(cli, confirmed=args.confirm_squashfs_mtdblock3)
|
||||
return
|
||||
if args.mode == "set-bootcmd":
|
||||
set_bootcmd(cli)
|
||||
return
|
||||
if args.mode in ("transfer", "transfer-dtb", "transfer-kernel", "transfer-rootfs"):
|
||||
transfer(cli, args.firmware, dtb=args.mode == "transfer-dtb", kernel=args.mode == "transfer-kernel", rootfs=args.mode == "transfer-rootfs", rootfs_size=args.rootfs_size)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Wait up to three minutes for the flashed device to answer ICMP."""
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def test_device(interface, host_ip, device_ip):
|
||||
host = str(ipaddress.IPv4Address(host_ip))
|
||||
device = str(ipaddress.IPv4Address(device_ip))
|
||||
if host == device:
|
||||
raise ValueError('Device IP must differ from the test host IP')
|
||||
if not interface or interface.startswith('-'):
|
||||
raise ValueError('Invalid network interface')
|
||||
result = subprocess.run(
|
||||
['ip', '-j', '-4', 'addr', 'show', 'dev', interface],
|
||||
check=True, capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
addresses = json.loads(result.stdout)
|
||||
if not any(info.get('local') == host for entry in addresses for info in entry.get('addr_info', [])):
|
||||
raise ValueError(f'Host IP {host} is not configured on {interface}')
|
||||
result = subprocess.run(
|
||||
['ip', '-j', '-4', 'route', 'get', device, 'from', host],
|
||||
check=True, capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
routes = json.loads(result.stdout)
|
||||
if not routes or any(route.get('dev') != interface for route in routes):
|
||||
raise ValueError(f'Route to {device} does not use {interface}')
|
||||
print(f'Waiting up to 180 seconds: {host} ({interface}) -> {device}', flush=True)
|
||||
# -c 1 exits on the first reply; -w bounds the entire ping run.
|
||||
result = subprocess.run(
|
||||
['ping', '-4', '-n', '-B', '-I', host, '-c', '1', '-i', '1', '-W', '1', '-w', '180', device],
|
||||
timeout=180,
|
||||
)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f'FAIL: {device} did not pass the ping test (exit {result.returncode})')
|
||||
print(f'PASS: device {device} answered ping', flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--interface', default='eno1')
|
||||
parser.add_argument('--host-ip', default='192.168.75.15')
|
||||
parser.add_argument('--device-ip', required=True)
|
||||
args = parser.parse_args()
|
||||
test_device(args.interface, args.host_ip, args.device_ip)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except (ValueError, RuntimeError, OSError, subprocess.SubprocessError) as exc:
|
||||
print(f'FAIL: {exc}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
مرجع در شماره جدید
مسدود کردن کاربر