⭐ Vision Engineer · Chặng 14Bài 4/5Bài học~45 phút

Viết tài liệu dự án Vision: README, đặc tả, SOP, phiên bản mô hình

README, đặc tả đo được, SOP cho người vận hành, hướng dẫn bảo trì, model card, changelog theo SemVer và checklist bàn giao, kèm script kiểm tra tài liệu.

Viết tài liệu dự án Vision: README, đặc tả, SOP, phiên bản mô hình

Học xong bài này, bạn sẽ

  • Viết README, đặc tả, hướng dẫn vận hành
  • Ghi lại phiên bản mô hình và cấu hình
  • Bàn giao cho người khác bảo trì

Nên học trước: Capstone 3: kiểm tra lỗi bề mặt bằng AI, trả kết quả PLC

Nội dung bài viết
  1. 1. Bộ tài liệu của một dự án vision: ai đọc, đọc khi nào
  2. 2. README: kỹ sư mới chạy được hệ thống trong 30 phút
  3. 3. Đặc tả yêu cầu: đo được, thử được, không cãi nhau
  4. 4. Hướng dẫn vận hành (SOP): viết cho người đứng máy
  5. 5. Hướng dẫn bảo trì: để người khác sửa được trạm
  6. 6. Quản lý phiên bản: phần mềm, mô hình, cấu hình
  7. 7. Sinh khung tài liệu và kiểm tra README bằng script
  8. 8. Bàn giao: để người khác bảo trì được
  9. 9. Bài tập
  10. Tóm tắt

Ba capstone vừa rồi (trạm đo kích thước, trạm đọc DataMatrix và số seri, trạm kiểm tra lỗi bề mặt bằng AI) đã cho bạn những hệ thống chạy được và có biên bản nghiệm thu. Nhưng một trạm vision chỉ thực sự "xong" khi người khác vận hành, sửa chữa và nâng cấp được nó mà không cần gọi bạn lúc 2 giờ sáng. Ở nhà máy, người đọc tài liệu rất khác nhau: công nhân vận hành cần biết bấm gì khi đèn đỏ sáng, kỹ thuật viên bảo trì cần biết lau ống kính thế nào và hiệu chuẩn lại ra sao, kỹ sư kế nhiệm cần biết mô hình nào đang chạy, huấn luyện từ dữ liệu nào, ngưỡng đặt bao nhiêu và vì sao. Bài này trình bày bộ tài liệu tối thiểu cho một dự án vision, mẫu cho từng loại, cách quản lý phiên bản mô hình và cấu hình, danh sách bàn giao, cùng vài script Python nhỏ giúp tài liệu không bị bỏ quên. Bài tiếp theo "Xây portfolio để ứng tuyển Vision Engineer" sẽ dùng chính các README này để thuyết phục nhà tuyển dụng.

Mục tiêu cuối bàiViết được README, đặc tả yêu cầu và hướng dẫn vận hành (SOP) cho đúng người đọc; ghi phiên bản phần mềm, mô hình và cấu hình theo semantic versioning, có model card và changelog; bàn giao dự án bằng checklist; dùng script để sinh khung tài liệu, kiểm tra README và sinh changelog từ dữ liệu.

1. Bộ tài liệu của một dự án vision: ai đọc, đọc khi nào

Tài liệuNgười đọcTrả lời câu hỏiCập nhật khi
READMEKỹ sư (bạn sau 6 tháng, đồng nghiệp)Hệ thống làm gì, cài và chạy thế nào, code nằm đâuMỗi lần đổi cách cài/chạy
Đặc tả yêu cầu (specification)Khách hàng, QA, quản lý dự ánPhải đạt gì, đo bằng gì, cái gì ngoài phạm viKhi yêu cầu thay đổi (có duyệt)
Hướng dẫn vận hành (SOP)Công nhân vận hành, trưởng caKhởi động, kiểm tra đầu ca, đổi mã hàng, xử lý báo lỗiKhi giao diện hoặc quy trình đổi
Hướng dẫn bảo trìKỹ thuật viên bảo trì, kỹ sư hiện trườngVệ sinh, thay camera/đèn, hiệu chuẩn lại, sao lưu, khắc phục sự cốSau mỗi sự cố có bài học
Model cardKỹ sư AI, QAMô hình học từ dữ liệu nào, ngưỡng, kết quả, giới hạn đã biếtMỗi phiên bản mô hình
ChangelogMọi ngườiBản nào đang chạy, khác bản trước ở đâuMỗi lần phát hành
Biên bản nghiệm thuKhách hàng, QAĐã chứng minh đạt yêu cầu bằng số liệu nàoFAT/SAT, sau thay đổi lớn

Nguyên tắc chung: một tài liệu, một người đọc. SOP viết cho kỹ sư sẽ không ai trong xưởng đọc; README viết cho công nhân sẽ thiếu thông tin cho kỹ sư. Tài liệu kỹ thuật nằm cạnh mã nguồn (thư mục docs/, định dạng Markdown, quản lý bằng git cùng code); SOP cho xưởng thường in ra, ép plastic, dán cạnh trạm, kèm bản PDF có số phiên bản.

2. README: kỹ sư mới chạy được hệ thống trong 30 phút

Tiêu chí kiểm tra README rất đơn giản: đưa cho một kỹ sư chưa biết dự án, máy sạch, họ có chạy được phần mềm (ít nhất ở chế độ mô phỏng) trong 30 phút không? Mẫu README cho một trạm vision:

# Trạm kiểm tra lỗi bề mặt nắp nhôm – Line 3

Kiểm tra xước/đốm trên nắp nhôm xước sợi, 30 nắp/phút, trả OK/NG cho PLC qua Modbus TCP.
Phiên bản phần mềm 1.3.0 · mô hình surface_cnn 2.1.0 · cấu hình line3.toml rev 7

## Mục đích và phạm vi
- Phát hiện: xước dài ≥ 0,5 mm, đốm ≥ 0,3 mm trên mặt trên của nắp.
- KHÔNG kiểm tra: mặt bên, ren, màu anod (xem SPEC.md mục 2.3).

## Kết quả nghiệm thu (SAT 2026-09-20, xem docs/bien_ban_SAT.pdf)
| Chỉ số | Yêu cầu | Đạt |
|---|---|---|
| Escape (lọt lỗi) | 0 / 300 nắp NG | 0 |
| Overkill (loại nhầm) | ≤ 2 % | 1,9 % |
| Thời gian xử lý p99 | ≤ 50 ms | 31 ms |

## Phần cứng
- Camera 5 MP mono GigE, IP 192.168.10.11; ống kính 25 mm, f/5.6
- Đèn vòm (dome) trắng + bộ điều khiển strobe; PC công nghiệp Linux, 8 lõi
- PLC: IP 192.168.10.1, Modbus TCP cổng 502 (bảng địa chỉ: docs/PLC_MAP.md)

## Cài đặt
1. Cài SDK camera theo docs/MAINTENANCE.md mục 1
2. `python -m venv .venv && source .venv/bin/activate`
3. `pip install -r requirements.txt`

## Chạy
- Mô phỏng (không cần camera/PLC): `python -m tram --config config/sim.toml`
- Trên trạm: dịch vụ `tram-vision` (systemd), xem docs/SOP.md

## Cấu hình
`config/line3.toml`: ngưỡng, ROI, địa chỉ PLC. Đổi cấu hình → tăng rev, ghi CHANGELOG.md.

## Cấu trúc thư mục
src/  config/  models/  tests/  docs/  tools/

## Tài liệu khác
SPEC.md · docs/SOP.md · docs/MAINTENANCE.md · models/MODEL_CARD.md · CHANGELOG.md

## Liên hệ
Bộ phận Kỹ thuật tự động hóa – người phụ trách hiện tại ghi trong docs/HANDOVER.md

Những điểm đáng học trong mẫu: dòng đầu nói bài toán bằng con số (30 nắp/phút, OK/NG, Modbus TCP); dòng phiên bản cho biết ba thứ đang chạy (phần mềm, mô hình, cấu hình); phạm vi nói rõ cả điều không làm; kết quả nghiệm thu là bảng có yêu cầu và giá trị đạt; có đường chạy mô phỏng để người mới thử mà không cần trạm thật. Số liệu trong mẫu là ví dụ: thay bằng số trong biên bản của bạn.

README không phải nơi chứa bí mậtKhông ghi mật khẩu PLC, tài khoản MES, khóa API vào README hay bất kỳ file nào trong git. Ghi chỗ lấy chúng (ví dụ "file tram.env trên máy trạm, quyền đọc: nhóm vận hành") và tên người có quyền cấp.

3. Đặc tả yêu cầu: đo được, thử được, không cãi nhau

Đặc tả (specification, SPEC) là "hợp đồng kỹ thuật" giữa bạn và khách hàng. Tranh cãi lúc nghiệm thu gần như luôn đến từ câu chữ mơ hồ. Ba quy tắc:

  1. Từ khóa mức độ: "phải" (shall) là bắt buộc, kiểm tra khi nghiệm thu; "nên" (should) là mong muốn; "có thể" (may) là tùy chọn. Không dùng "cố gắng", "tốt", "nhanh".
  2. Mỗi yêu cầu có mã, tiêu chí bằng số và cách thử, đúng như bảng R1, R2… trong các capstone.
  3. Ghi rõ ngoài phạm vi và giả định: lỗi nào không kiểm tra, điều kiện ánh sáng, tốc độ băng tải, vật liệu nào đã thử.
Câu mơ hồViết lại đo được
Hệ thống phát hiện tốt các vết xướcR2: Hệ thống phải phát hiện 100% vết xước dài ≥ 0,5 mm trên bộ 300 nắp NG chuẩn (escape = 0)
Ít loại nhầmR3: Tỉ lệ loại nhầm phải ≤ 2% trên 1000 nắp OK của 2 lô vật liệu khác nhau
Chạy đủ nhanhR5: Thời gian từ trigger tới khi PLC nhận kết quả phải có p99 ≤ 300 ms, đo trên 2000 chu kỳ liên tục
Đo chính xácR2 (đo): %GRR thành phần lặp lại ≤ 10% dung sai; |bias| so với CMM ≤ 10% dung sai

Khung một đặc tả 2–4 trang: 1. Mục đích; 2. Phạm vi (sản phẩm, loại lỗi, ngoài phạm vi); 3. Điều kiện vận hành (tốc độ, môi trường, ánh sáng xung quanh); 4. Bảng yêu cầu (mã, nội dung, tiêu chí, cách thử); 5. Giao tiếp (bảng tín hiệu PLC, dữ liệu gửi MES); 6. Dữ liệu và truy vết (lưu gì, bao lâu); 7. Giả định và rủi ro; 8. Lịch sử phiên bản đặc tả.

4. Hướng dẫn vận hành (SOP): viết cho người đứng máy

SOP (Standard Operating Procedure) dành cho công nhân và trưởng ca. Họ không cần biết CNN là gì; họ cần biết làm gì, theo thứ tự nào, khi thấy gì. Quy tắc viết: câu ngắn bắt đầu bằng động từ; mỗi bước một hành động, đánh số; ảnh chụp màn hình và ảnh thật của trạm; tên nút, tên đèn đúng như trên máy; mỗi báo lỗi có hành động cụ thể và người cần gọi.

SOP-VIS-03  Kiểm tra đầu ca – Trạm nắp nhôm Line 3          Phiên bản 2 · 2026-10-01
──────────────────────────────────────────────────────────────────────────────────
1. Kiểm tra đèn tháp: XANH = sẵn sàng. Nếu VÀNG hoặc ĐỎ: sang bảng xử lý báo lỗi.
2. Lấy hộp MẪU CHUẨN (tủ cạnh trạm, nhãn "MC-L3"): 2 nắp OK, 2 nắp NG.
3. Bấm nút "KIỂM TRA MẪU" trên màn hình.
4. Đặt lần lượt 4 nắp lên băng tải, đúng chiều mũi tên.
5. Màn hình phải hiện: OK, OK, NG, NG (đúng thứ tự trên nhãn).
6. Đúng cả 4: ký sổ đầu ca, bấm "BẮT ĐẦU SẢN XUẤT".
   Sai bất kỳ nắp nào: KHÔNG chạy sản xuất. Gọi kỹ thuật Vision (máy lẻ 214).
Báo lỗi trên màn hìnhÝ nghĩaNgười vận hành làmGọi ai
E01 Mất cameraKhông nhận được ảnhKiểm tra đèn LINK trên camera; bấm "KẾT NỐI LẠI" một lầnKỹ thuật Vision nếu còn lỗi
E04 Ảnh quá tốiĐộ sáng vùng tham chiếu ngoài dảiKiểm tra đèn vòm có sáng; lau kính bảo vệ bằng khăn trong hộpBảo trì nếu đèn tắt
NG liên tiếp ≥ 5 nắpLỗi hàng loạt hoặc trạm lỗiDừng băng tải, lấy 2 nắp NG đưa trưởng ca kiểm tra bằng mắtTrưởng ca + QA
E09 Mô hình không hợp lệFile mô hình sai mã bămKhông tự xử lýKỹ thuật Vision ngay

Hãy thử SOP với một công nhân thật, đứng cạnh nhìn họ làm mà không giải thích. Mỗi chỗ họ hỏi lại là một chỗ phải sửa.

5. Hướng dẫn bảo trì: để người khác sửa được trạm

  • Lịch bảo trì định kỳ: lau kính bảo vệ (hằng ca hoặc hằng ngày, tùy bụi), kiểm tra đèn và cáp (hằng tuần), kiểm tra hiệu chuẩn bằng mẫu chuẩn (hằng ca), sao lưu dữ liệu (hằng tuần).
  • Quy trình thay thế: thay camera, ống kính, đèn; sau mỗi lần thay phải làm gì (đặt lại khẩu độ, lấy nét, hiệu chuẩn mm/px, chạy bộ mẫu vàng). Ghi rõ thông số cơ khí: khoảng cách làm việc, vị trí khóa vòng nét, ảnh chụp vị trí lắp.
  • Sao lưu và khôi phục: những file nào (cấu hình, mô hình kèm thẻ mô hình, hệ số hiệu chuẩn, CSDL kết quả), ở đâu, lệnh khôi phục.
  • Khắc phục sự cố: bảng triệu chứng → nguyên nhân → cách kiểm tra → cách sửa; vị trí file log và cách đọc.
  • Phụ tùng: mã phụ tùng, nhà cung cấp, thời gian giao hàng; phụ tùng nào nên có sẵn trong kho (camera dự phòng, cáp, đèn).

6. Quản lý phiên bản: phần mềm, mô hình, cấu hình

Ở trạm AI vision, kết quả OK/NG phụ thuộc vào ba thứ thay đổi độc lập: mã nguồn, mô hình, và cấu hình (ngưỡng, ROI, hệ số hiệu chuẩn). Mỗi thứ cần số phiên bản riêng, và mỗi bản ghi kết quả trong CSDL nên lưu cả ba, để khi QA hỏi "nắp lô 2610 bị loại ngày 12 chạy bằng gì?" bạn trả lời được.

Semantic versioning (SemVer) đặt phiên bản dạng MAJOR.MINOR.PATCH. Áp vào trạm vision:

TăngKhi nào (phần mềm trạm)Ví dụ
MAJOR (2.0.0)Thay đổi không tương thích ngược: bảng địa chỉ PLC, định dạng dữ liệu gửi MES, cấu trúc file cấu hìnhThêm thanh ghi mã lỗi làm dịch địa chỉ cũ; PLC phải sửa theo
MINOR (1.4.0)Thêm tính năng tương thích ngược; mô hình mới thay đổi quyết địnhThêm cảnh báo drift; dùng mô hình 2.1.0
PATCH (1.3.1)Sửa lỗi không đổi hành vi mong đợiSửa rò bộ nhớ khi mất camera

Mô hình có số phiên bản riêng (surface_cnn 2.1.0), gắn với: phiên bản dataset, mã băm SHA-256 của file ONNX, ngưỡng, và kết quả kiểm định. Đổi trọng số luôn là phiên bản mới và phải chạy lại bộ kiểm định; không có chuyện "chép đè file model.onnx". Cấu hình dùng số rev đơn giản (rev 7, rev 8) cộng changelog, vì thay đổi ngưỡng cũng làm đổi quyết định.

Model card (thẻ mô hình) là một trang mô tả mô hình, đi cùng file ONNX. Mẫu tối thiểu, dạng JSON như bài "Xuất ONNX và kiểm tra kết quả khớp PyTorch" hoặc Markdown:

# MODEL_CARD – surface_cnn 2.1.0
- File: surface_cnn_2.1.0.onnx · SHA-256: 9f2c…e41a · opset 18 · đầu vào images [N,3,128,128] float32
- Tiền xử lý: crop ô 128×128, BGR→RGB, /255, mean/std ImageNet (R,G,B)
- Dữ liệu: dataset v4 (lô 2601–2614, 12 400 ô OK, 1 870 ô NG); val: lô 2615–2616; test: lô 2617–2618
- Ngưỡng: 0,31, chọn trên val sao cho escape = 0 với biên an toàn
- Kết quả test: escape 0/412 ô NG; overkill 1,9 % ô OK; p99 31 ms/nắp trên PC trạm
- Giới hạn đã biết: xước song song vân xước sợi dưới 0,5 mm khó thấy; chưa thử nắp màu đen
- Huấn luyện lại khi: đổi nhà cung cấp phôi, đổi đèn/camera, overkill tuần > 3 %

Changelog ghi theo phiên bản, mới nhất ở trên, nhóm theo loại thay đổi, có số liệu khi thay đổi ảnh hưởng chất lượng. Thay vì viết tay, giữ danh sách phát hành ở dạng dữ liệu (JSON) và sinh changelog bằng script; script đồng thời kiểm tra phiên bản tăng đúng SemVer và ngày tháng theo thứ tự:

import json
from pathlib import Path


def tach(v):
    return tuple(int(x) for x in v.split("."))


def tang(v, loai):
    """Phiên bản kế tiếp theo SemVer: loai là 'major', 'minor' hoặc 'patch'."""
    ma, mi, pa = tach(v)
    return {"major": f"{ma + 1}.0.0", "minor": f"{ma}.{mi + 1}.0", "patch": f"{ma}.{mi}.{pa + 1}"}[loai]


def sinh_changelog(ds):
    """ds: danh sách bản phát hành, cũ -> mới. Trả về (markdown, danh sách lỗi)."""
    loi = []
    for truoc, sau in zip(ds, ds[1:]):
        if tang(truoc["version"], sau["loai"]) != sau["version"]:
            loi.append(f"{sau['version']}: '{sau['loai']}' sau {truoc['version']} phải là "
                       f"{tang(truoc['version'], sau['loai'])}")
        if sau["date"] < truoc["date"]:
            loi.append(f"{sau['version']}: ngày {sau['date']} trước bản {truoc['version']}")
    dong = ["# Changelog", ""]
    for b in reversed(ds):                                   # mới nhất ở trên
        dong.append(f"## {b['version']} – {b['date']}")
        if b.get("model"):
            dong.append(f"Mô hình: {b['model']} · Cấu hình: {b['config']}")
        for nhom in ("Thêm", "Thay đổi", "Sửa lỗi"):
            for muc in b.get(nhom, []):
                dong.append(f"- **{nhom}**: {muc}")
        dong.append("")
    return "\n".join(dong), loi


phat_hanh = [
    {"version": "1.2.0", "loai": "minor", "date": "2026-08-14", "model": "surface_cnn 1.4.0",
     "config": "rev 5", "Thêm": ["Lưu ảnh NG kèm điểm vào tên file"]},
    {"version": "1.2.1", "loai": "patch", "date": "2026-08-30", "model": "surface_cnn 1.4.0",
     "config": "rev 5", "Sửa lỗi": ["Rò bộ nhớ khi camera mất kết nối (E01) nhiều lần"]},
    {"version": "1.3.0", "loai": "minor", "date": "2026-10-05", "model": "surface_cnn 2.1.0",
     "config": "rev 7",
     "Thêm": ["Cảnh báo drift độ nét (EWMA), hiện E04 trên màn hình"],
     "Thay đổi": ["Mô hình 2.1.0 (dataset v4, +600 ảnh ca đêm): overkill 2,1 % → 1,9 % trên 1000 nắp, escape 0/300",
                  "Ngưỡng 0,35 → 0,31 (rev 7), chọn lại trên tập val mới"]},
]
md, loi = sinh_changelog(phat_hanh)
Path("CHANGELOG.md").write_text(md, encoding="utf-8")
print(md)
print("Lỗi:", loi or "không")

sai = phat_hanh + [{"version": "1.5.0", "loai": "minor", "date": "2026-09-01", "Sửa lỗi": ["..."]}]
print("Lỗi khi thêm bản 1.5.0:", sinh_changelog(sai)[1])
print("Thay bảng địa chỉ PLC sau 1.3.0 →", tang("1.3.0", "major"))
# Changelog

## 1.3.0 – 2026-10-05
Mô hình: surface_cnn 2.1.0 · Cấu hình: rev 7
- **Thêm**: Cảnh báo drift độ nét (EWMA), hiện E04 trên màn hình
- **Thay đổi**: Mô hình 2.1.0 (dataset v4, +600 ảnh ca đêm): overkill 2,1 % → 1,9 % trên 1000 nắp, escape 0/300
- **Thay đổi**: Ngưỡng 0,35 → 0,31 (rev 7), chọn lại trên tập val mới

## 1.2.1 – 2026-08-30
Mô hình: surface_cnn 1.4.0 · Cấu hình: rev 5
- **Sửa lỗi**: Rò bộ nhớ khi camera mất kết nối (E01) nhiều lần

## 1.2.0 – 2026-08-14
Mô hình: surface_cnn 1.4.0 · Cấu hình: rev 5
- **Thêm**: Lưu ảnh NG kèm điểm vào tên file

Lỗi: không
Lỗi khi thêm bản 1.5.0: ["1.5.0: 'minor' sau 1.3.0 phải là 1.4.0", '1.5.0: ngày 2026-09-01 trước bản 1.3.0']
Thay bảng địa chỉ PLC sau 1.3.0 → 2.0.0

Bản 1.5.0 bị bắt hai lỗi: nhảy cóc qua 1.4.0, và ngày phát hành sớm hơn bản trước (thường do chép nhầm). Trong dự án thật, chạy script này trong CI hoặc trước mỗi lần phát hành; số phiên bản trong changelog phải khớp số phiên bản phần mềm in ra lúc khởi động.

7. Sinh khung tài liệu và kiểm tra README bằng script

Tài liệu hay bị bỏ vì bắt đầu từ trang trắng. Script sau tạo khung docs/ với các mục bắt buộc cho từng tài liệu; bạn chỉ việc điền. Mục nào chưa điền giữ chữ TODO để script kiểm tra phát hiện:

KHUNG = {
    "README.md": ["Mục đích và phạm vi", "Kết quả nghiệm thu", "Phần cứng", "Cài đặt", "Chạy",
                  "Cấu hình", "Cấu trúc thư mục", "Tài liệu khác", "Liên hệ"],
    "SPEC.md": ["Mục đích", "Phạm vi", "Điều kiện vận hành", "Bảng yêu cầu", "Giao tiếp PLC/MES",
                "Dữ liệu và truy vết", "Giả định và rủi ro", "Lịch sử phiên bản"],
    "docs/SOP.md": ["Khởi động và tắt", "Kiểm tra đầu ca", "Đổi mã sản phẩm", "Xử lý báo lỗi"],
    "docs/MAINTENANCE.md": ["Lịch bảo trì", "Thay thế linh kiện", "Hiệu chuẩn lại",
                            "Sao lưu và khôi phục", "Khắc phục sự cố", "Phụ tùng"],
    "models/MODEL_CARD.md": ["File và mã băm", "Tiền xử lý", "Dữ liệu", "Ngưỡng", "Kết quả", "Giới hạn"],
    "docs/HANDOVER.md": ["Người bàn giao và nhận", "Tài khoản và quyền truy cập", "Vấn đề còn mở"],
}


def tao_khung(goc, ten_du_an):
    goc = Path(goc)
    for ten, muc in KHUNG.items():
        p = goc / ten
        if p.exists():                                    # không ghi đè tài liệu đã viết
            continue
        p.parent.mkdir(parents=True, exist_ok=True)
        tieu_de = ten.split("/")[-1].removesuffix(".md")
        noi_dung = [f"# {tieu_de} – {ten_du_an}", ""] + [f"## {m}\n\nTODO\n" for m in muc]
        p.write_text("\n".join(noi_dung), encoding="utf-8")
    return sorted(str(p.relative_to(goc)) for p in goc.rglob("*.md"))


print(tao_khung("du_an_nap_nhom", "Trạm nắp nhôm Line 3"))
print("\n".join(Path("du_an_nap_nhom/docs/SOP.md").read_text(encoding="utf-8").splitlines()[:5]))
['README.md', 'SPEC.md', 'docs/HANDOVER.md', 'docs/MAINTENANCE.md', 'docs/SOP.md', 'models/MODEL_CARD.md']
# SOP – Trạm nắp nhôm Line 3

## Khởi động và tắt

TODO

Hàm kiểm tra README: đủ mục bắt buộc, không còn TODO, có dòng phiên bản, có số liệu kết quả, mọi file được nhắc tới (ảnh, tài liệu khác) tồn tại, và không có dấu hiệu mật khẩu:

import re


def kiem_tra_readme(duong_dan, muc_bat_buoc=KHUNG["README.md"]):
    p = Path(duong_dan)
    van_ban = p.read_text(encoding="utf-8")
    muc = {m.strip() for m in re.findall(r"^##\s+(.+)$", van_ban, re.M)}
    loi = [f"thiếu mục '{m}'" for m in muc_bat_buoc if m not in muc]
    if "TODO" in van_ban:
        loi.append(f"còn {van_ban.count('TODO')} chỗ TODO")
    if not re.search(r"[Pp]hiên bản.*\d+\.\d+\.\d+", van_ban):
        loi.append("không có dòng phiên bản dạng X.Y.Z")
    if not re.search(r"\d+(,\d+)?\s*(%|ms|mm|µm)", van_ban):
        loi.append("không có số liệu kết quả (%, ms, mm…)")
    for f in re.findall(r"(?:docs|models)/[\w./-]+\.(?:md|pdf|png|gif|jpg)", van_ban):
        if not (p.parent / f).exists():
            loi.append(f"nhắc tới {f} nhưng file không tồn tại")
    if re.search(r"(password|mật khẩu|passwd)\s*[:=]\s*\S+", van_ban, re.I):
        loi.append("có vẻ chứa mật khẩu")
    return loi


print("Khung mới tạo:", kiem_tra_readme("du_an_nap_nhom/README.md"))
Khung mới tạo: ['còn 9 chỗ TODO', 'không có dòng phiên bản dạng X.Y.Z', 'không có số liệu kết quả (%, ms, mm…)']

Kết quả đúng như mong đợi với khung chưa điền: có đủ mục nhưng còn TODO, chưa có phiên bản, chưa có số liệu. Bài tập 1 dùng hàm này để chấm README bạn viết cho capstone.

8. Bàn giao: để người khác bảo trì được

Bàn giao không phải là gửi một thư mục qua email. Đó là một buổi làm việc có checklist, kết thúc khi người nhận tự làm được các thao tác chính trong lúc bạn đứng nhìn:

NhómHạng mụcCách xác nhận
Mã nguồnRepo đầy đủ, nhánh chính khớp bản đang chạy (tag phiên bản)Người nhận build lại được đúng phiên bản trên máy của họ
Mô hìnhFile ONNX + model card + mã băm; dataset và script huấn luyện ở đâuNgười nhận chạy bộ kiểm định, ra đúng số trong model card
Cấu hìnhBản cấu hình đang chạy, sao lưu, changelog cấu hìnhSo mã băm cấu hình trên trạm với bản trong repo
Tài khoảnDanh sách tài khoản (PC trạm, PLC, MES, git) và người cấp quyền; đổi mật khẩu sau bàn giaoNgười nhận đăng nhập được mọi nơi
Vận hànhSOP in, dán cạnh trạm; công nhân đã được đào tạoMột trưởng ca tự làm kiểm tra đầu ca theo SOP
Bảo trìHướng dẫn bảo trì; phụ tùng dự phòngNgười nhận tự hiệu chuẩn lại và chạy bộ mẫu vàng
Sự cốDanh sách vấn đề còn mở, rủi ro đã biết, sự cố đã xảy ra và cách xử lýGhi trong HANDOVER.md, hai bên ký

9. Bài tập

Bài 1 (thực hành chính). Viết README cho một capstone của bạn theo mẫu mục 2 (gợi ý: Capstone 1, trạm đo kích thước), đặt trong thư mục dự án cùng các tài liệu được nhắc tới, rồi chạy kiem_tra_readme cho đến khi không còn lỗi.

Xem đáp án

Một README đạt yêu cầu cho Capstone 1 (số liệu là ví dụ; dùng số trong biên bản tự sinh của bạn). Đoạn code tạo luôn các file tài liệu được nhắc tới để kiểm tra liên kết; trong dự án thật đó là các file bạn đã viết:

goc = Path("capstone1")
for f in ["docs/bien_ban_nghiem_thu.md", "docs/PLC_MAP.md", "docs/ket_qua_do.png"]:
    (goc / f).parent.mkdir(parents=True, exist_ok=True)
    (goc / f).write_text("(nội dung)", encoding="utf-8")

README = """# Trạm đo kích thước tấm đế dập

Đo 4 kích thước tấm đế (L, W, P, D), dung sai ±0,05 mm, 100% sản phẩm, 2 s/sản phẩm; trả OK/NG và số đo cho PLC qua Modbus TCP.
Phiên bản phần mềm 1.0.0 · cấu hình hiệu chuẩn calib_2026-10-02 · không dùng mô hình AI

![Kết quả đo](docs/ket_qua_do.png)

## Mục đích và phạm vi
- Đo: chiều dài L, chiều rộng W, khoảng cách lỗ P, đường kính lỗ D trên ảnh đèn nền.
- KHÔNG đo: độ phẳng, ba via mặt cắt, chiều dày.

## Kết quả nghiệm thu
| Chỉ số | Yêu cầu | Đạt |
|---|---|---|
| %GRR lặp lại (D) | ≤ 10 % | 4,2 % |
| Escape mẫu vàng NG | 0 | 0 / 30 |
| Thời gian xử lý p99 | ≤ 150 ms | 62 ms |
Chi tiết: docs/bien_ban_nghiem_thu.md

## Phần cứng
Camera mono 5 MP, ống kính telecentric, đèn nền LED đỏ; PLC Modbus TCP (bảng địa chỉ docs/PLC_MAP.md).

## Cài đặt
`pip install -r requirements.txt`

## Chạy
`python -m tram_do --mo-phong` chạy toàn bộ kịch bản thử với ảnh tổng hợp và PLC giả, sinh biên bản.

## Cấu hình
`config/tram.toml`: dung sai, ROI, địa chỉ PLC. Hệ số hiệu chuẩn trong `calib/`.

## Cấu trúc thư mục
src/ config/ calib/ tests/ docs/

## Tài liệu khác
docs/bien_ban_nghiem_thu.md · docs/PLC_MAP.md

## Liên hệ
Tác giả: (tên bạn) – qua mục Issues của repo
"""
(goc / "README.md").write_text(README, encoding="utf-8")
print(kiem_tra_readme(goc / "README.md") or "README đạt")
README đạt

Tự hỏi thêm ba câu mà script không kiểm tra được: (1) đọc riêng dòng đầu, người ngoài có hiểu trạm làm gì không? (2) lệnh "Chạy" có thật sự chạy được trên máy sạch không (nhờ bạn bè thử)? (3) giới hạn của dự án đã được nói trung thực chưa?

Bài 2. Viết lại các câu trong đặc tả sau thành yêu cầu đo được: (a) "Camera đọc mã nhanh và chính xác." (b) "Hệ thống hoạt động ổn định." (c) "Kết quả được lưu lại để truy vết."

Xem đáp án
  • (a) R1: Trạm phải đọc đúng 100% mã DataMatrix trên bộ 300 nhãn chuẩn (gồm 18 nhãn in mờ); R7: thời gian xử lý p99 phải ≤ 150 ms trên PC của trạm, đo trên mọi chu kỳ của bộ thử.
  • (b) R10: Trạm phải chạy liên tục 72 giờ trong thử nghiệm SAT không cần can thiệp thủ công; khi mất camera, trạm phải báo E01 cho PLC trong ≤ 2 s và tự kết nối lại khi camera có lại; sau mất điện, phần mềm phải tự khởi động và sẵn sàng trong ≤ 3 phút.
  • (c) R6: Mỗi chu kỳ phải có một bản ghi gồm thời điểm, mã sản phẩm, kết quả, lý do NG, phiên bản phần mềm, phiên bản mô hình, rev cấu hình; ảnh NG lưu ≥ 90 ngày; kiểm tra bằng truy vấn CSDL sau khi chạy bộ thử.

Bài 3. Phiên bản hiện tại 1.3.0. Với từng thay đổi sau, phiên bản kế tiếp là gì? (a) Sửa lỗi log ghi sai múi giờ. (b) Đổi ngưỡng 0,31 → 0,29 sau khi QA yêu cầu giảm escape. (c) Thêm thanh ghi "mã lỗi chi tiết" vào giữa bảng địa chỉ PLC, làm dịch các địa chỉ sau nó. (d) Mô hình mới 2.2.0, huấn luyện thêm lô nắp màu đen.

Xem đáp án
for mo_ta, loai in [("(a) sửa log múi giờ", "patch"), ("(c) dịch địa chỉ PLC", "major"),
                    ("(d) mô hình 2.2.0", "minor")]:
    print(f"{mo_ta:22s} {loai:6s} → {tang('1.3.0', loai)}")
(a) sửa log múi giờ    patch  → 1.3.1
(c) dịch địa chỉ PLC   major  → 2.0.0
(d) mô hình 2.2.0      minor  → 1.4.0

(a) PATCH: sửa lỗi, không đổi hành vi mong đợi. (b) Ngưỡng thuộc cấu hình: tăng rev cấu hình (rev 7 → rev 8), ghi changelog có lý do và số liệu (escape/overkill trên tập val với ngưỡng mới), phiên bản phần mềm giữ nguyên. (c) MAJOR: PLC phải sửa chương trình theo, không tương thích ngược; tốt hơn là thêm thanh ghi mới vào cuối bảng để chỉ cần MINOR. (d) MINOR cho phần mềm nếu bản phát hành đóng gói mô hình mới (quyết định thay đổi, phải kiểm định lại); bản thân mô hình có số 2.2.0 riêng, ghi trong model card cùng mã băm.

Tóm tắt

  • Mỗi tài liệu một người đọc: README cho kỹ sư, đặc tả cho khách hàng/QA, SOP cho người vận hành, hướng dẫn bảo trì cho kỹ thuật viên, model card cho người làm AI, changelog cho mọi người.
  • README: bài toán bằng số, phiên bản (phần mềm, mô hình, cấu hình), phạm vi và ngoài phạm vi, bảng kết quả nghiệm thu, đường chạy mô phỏng; người mới chạy được trong 30 phút.
  • Đặc tả: "phải/nên/có thể", mỗi yêu cầu có mã, tiêu chí bằng số và cách thử, ghi rõ ngoài phạm vi.
  • SOP: câu ngắn bắt đầu bằng động từ, đánh số bước, ảnh thật, bảng báo lỗi kèm hành động và người cần gọi; thử với công nhân thật.
  • Phiên bản: SemVer cho phần mềm (MAJOR khi đổi giao tiếp PLC/MES), số riêng cho mô hình (kèm mã băm, model card), rev cho cấu hình; mỗi bản ghi kết quả lưu cả ba. Changelog sinh từ dữ liệu và được kiểm tra tự động.
  • Bàn giao bằng checklist, kết thúc khi người nhận tự làm được các thao tác chính.

Bài tiếp theo: Xây portfolio để ứng tuyển Vision Engineer, biến ba capstone và các README này thành hồ sơ thuyết phục nhà tuyển dụng.

Bài tập thực hành

  1. Viết README cho một capstone của bạn theo mẫu trong bài.

Làm xong bài tập mới chuyển sang bài tiếp theo: tự viết code là cách duy nhất để nhớ lâu.

Bài tiếp theo
Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.