C#: lưu và nạp recipe trạm vision bằng System.Text.Json
Lưu thông số từng mã sản phẩm ra JSON với System.Text.Json: required, enum dạng chữ, từ chối trường gõ sai, bẫy số thực ở biên dung sai và ghi file an toàn.
Nội dung bài viết
Một trạm kiểm tra thường chạy nhiều mã sản phẩm. Mỗi mã cần thông số riêng: thời gian phơi sáng, chế độ đèn, vùng ROI, kích thước danh nghĩa và dung sai. Bộ thông số đó gọi là recipe. Nếu ghi cứng trong code, mỗi lần đổi mã hàng lại phải biên dịch và cài lại phần mềm. Cách làm chuẩn là lưu recipe ra file JSON và nạp khi chuyển mã. Bài này dùng System.Text.Json, thư viện JSON có sẵn trong .NET, không cần cài thêm gói nào.
Nếu bạn mới học C#, hãy đọc trước C# cơ bản cho kỹ sư tự động hóa. Code trong bài đã chạy thử với .NET 8 (dùng required, chuỗi thô """ của C# 11 và tùy chọn UnmappedMemberHandling có từ .NET 8).
1. Mô hình hóa recipe bằng lớp C#
Tạo dự án: dotnet new console -n Recipe. Đặt các kiểu dữ liệu ở cuối file Program.cs (sau các câu lệnh chính):
public enum CheDoDen { Tat, Lien, Strobe }
public record Roi(int X, int Y, int W, int H);
public record DungSai(double DanhNghia, double CongTru)
{
private const double Eps = 1e-9; // nhỏ hơn rất nhiều so với độ phân giải đo
public bool Dat(double giaTri) => Math.Abs(giaTri - DanhNghia) <= CongTru + Eps;
}
public class CaiDatCamera
{
public int PhoiSangUs { get; set; } = 1000;
public double Gain { get; set; } = 0;
}
public class Recipe
{
public int PhienBan { get; set; } = 1;
public required string MaSanPham { get; set; }
public CaiDatCamera Camera { get; set; } = new();
public CheDoDen CheDoDen { get; set; } = CheDoDen.Lien;
public required Roi Roi { get; set; }
public required DungSai KichThuocMm { get; set; }
}
Vài điểm thiết kế đáng chú ý:
required: những trường không có giá trị mặc định hợp lý (mã sản phẩm, ROI, dung sai) bắt buộc phải có trong file. Thiếu là báo lỗi ngay khi nạp, không để máy chạy với ROI rỗng.- Giá trị mặc định cho các trường "an toàn" (phơi sáng 1000 µs, gain 0). File cũ thiếu trường mới vẫn nạp được.
PhienBan: số phiên bản định dạng. Sau này đổi cấu trúc, bạn biết file nào cần chuyển đổi.recordcho dữ liệu nhỏ, bất biến như ROI và dung sai. System.Text.Json tự dùng constructor của record khi đọc.
2. Tùy chọn serializer
Đặt ở đầu Program.cs:
using System.Text.Json;
using System.Text.Json.Serialization;
var tuyChon = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter() }
};
| Tùy chọn | Tác dụng |
|---|---|
WriteIndented | Xuống dòng, thụt lề cho người đọc (mặc định JSON viết liền một dòng) |
PropertyNamingPolicy = CamelCase | MaSanPham thành maSanPham, đúng thói quen đặt tên trong JSON |
JsonStringEnumConverter | Ghi enum dạng chữ "Strobe" thay vì số 2. Người vận hành đọc được, và thêm giá trị enum mới không làm lệch nghĩa file cũ |
3. Lưu và nạp recipe
// 1. Tạo recipe và lưu ra file
var recipe = new Recipe
{
MaSanPham = "SP-001",
Camera = new CaiDatCamera { PhoiSangUs = 1200, Gain = 2.0 },
CheDoDen = CheDoDen.Strobe,
Roi = new Roi(120, 80, 400, 300),
KichThuocMm = new DungSai(25.00, 0.05)
};
string json = JsonSerializer.Serialize(recipe, tuyChon);
File.WriteAllText("SP-001.json", json);
Console.WriteLine(json);
// 2. Nạp lại và dùng
var napLai = JsonSerializer.Deserialize<Recipe>(File.ReadAllText("SP-001.json"), tuyChon)!;
Console.WriteLine($"Nạp {napLai.MaSanPham}: phơi sáng {napLai.Camera.PhoiSangUs} µs, đèn {napLai.CheDoDen}");
Console.WriteLine($"25.03 mm -> {(napLai.KichThuocMm.Dat(25.03) ? "OK" : "NG")}");
Console.WriteLine($"25.06 mm -> {(napLai.KichThuocMm.Dat(25.06) ? "OK" : "NG")}");
Kết quả dotnet run:
{
"phienBan": 1,
"maSanPham": "SP-001",
"camera": {
"phoiSangUs": 1200,
"gain": 2
},
"cheDoDen": "Strobe",
"roi": {
"x": 120,
"y": 80,
"w": 400,
"h": 300
},
"kichThuocMm": {
"danhNghia": 25,
"congTru": 0.05
}
}
Nạp SP-001: phơi sáng 1200 µs, đèn Strobe
25.03 mm -> OK
25.06 mm -> NG
Lưu ý 2.0 được ghi thành 2 và 25.00 thành 25: JSON không phân biệt số nguyên và số thực, đọc lại vào double vẫn đúng.
4. Bắt lỗi recipe thiếu trường
Người vận hành sửa tay file và xóa nhầm ROI. Nhờ required, lỗi được phát hiện ngay lúc nạp:
// 3. File thiếu trường bắt buộc
string thieu = """{ "maSanPham": "SP-002", "cheDoDen": "Lien" }""";
try
{
JsonSerializer.Deserialize<Recipe>(thieu, tuyChon);
}
catch (JsonException ex)
{
Console.WriteLine($"Lỗi recipe: {ex.Message}");
}
Lỗi recipe: JSON deserialization for type 'Recipe' was missing required properties, including the following: roi, kichThuocMm
Trong phần mềm thật, khi gặp lỗi này hãy giữ nguyên recipe đang chạy, hiện thông báo rõ tên file và tên trường thiếu, và không cho chuyển mã. Chạy tiếp với thông số rỗng nguy hiểm hơn nhiều so với dừng máy.
5. Bẫy số thực ở đúng biên dung sai
Bạn có để ý hằng số Eps trong DungSai? Hãy xem điều gì xảy ra ở đúng biên 25,05 mm:
// 4. Bẫy số thực ở đúng biên dung sai
Console.WriteLine($"25.05 - 25.00 = {25.05 - 25.00:R}");
Console.WriteLine($"25.05 mm -> {(napLai.KichThuocMm.Dat(25.05) ? "OK" : "NG")}");
25.05 - 25.00 = 0.05000000000000071
25.05 mm -> OK
Số 25,05 không biểu diễn chính xác được ở dạng nhị phân, nên hiệu số lớn hơn 0,05 một chút xíu. Nếu viết <= CongTru không có Eps, sản phẩm đúng biên dung sai bị đánh NG (chúng tôi đã chạy thử để kiểm chứng). Cộng thêm một sai số rất nhỏ như 1e-9 mm, nhỏ hơn rất nhiều độ phân giải của bất kỳ hệ đo quang học nào, giải quyết vấn đề. Một cách khác là làm tròn kết quả đo về số chữ số có nghĩa trước khi so sánh.
6. Chế độ đọc "chặt" cho file sửa tay
File recipe hay được sửa tay, nên ta muốn: cho phép chú thích //, chấp nhận dấu phẩy thừa ở cuối, nhưng báo lỗi khi gặp tên trường lạ (thường là do gõ sai). Mặc định System.Text.Json lặng lẽ bỏ qua trường lạ, nên một lỗi gõ phoiSang thay vì phoiSangUs sẽ khiến máy âm thầm chạy với giá trị mặc định.
// 5. Chấp nhận chú thích, dấu phẩy thừa; từ chối trường lạ (gõ sai tên)
var tuyChonChat = new JsonSerializerOptions(tuyChon)
{
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow
};
string goSai = """
{
// recipe sản phẩm SP-003
"maSanPham": "SP-003",
"roi": { "x": 0, "y": 0, "w": 100, "h": 100 },
"kichThuocMm": { "danhNghia": 12.5, "congTru": 0.02 },
"phoiSang": 800,
}
""";
try
{
JsonSerializer.Deserialize<Recipe>(goSai, tuyChonChat);
}
catch (JsonException ex)
{
Console.WriteLine($"Lỗi recipe: {ex.Message}");
}
Lỗi recipe: The JSON property 'phoiSang' could not be mapped to any .NET member contained in type 'Recipe'.
new JsonSerializerOptions(tuyChon) sao chép toàn bộ tùy chọn cũ (camelCase, enum dạng chữ) rồi bổ sung thêm. Chú thích và dấu phẩy thừa được chấp nhận; chỉ trường gõ sai bị bắt.
7. Ghi file an toàn
Nếu mất điện đúng lúc đang ghi, file recipe có thể bị cắt nửa chừng. Cách phổ biến: ghi ra file tạm rồi thay thế:
static void LuuAnToan(string duongDan, string noiDung)
{
string tam = duongDan + ".tmp";
File.WriteAllText(tam, noiDung);
File.Move(tam, duongDan, overwrite: true); // thay thế file cũ sau khi ghi xong
}
Kết hợp thêm: lưu bản sao có ngày giờ mỗi lần sửa (ví dụ SP-001_20261010_0830.json) để truy vết ai đổi gì, rất có ích khi nghiệm thu và khi khách hàng hỏi "hôm đó máy chạy thông số nào".
Tổng kết quy tắc recipe
- Mỗi mã sản phẩm một file JSON, có trường
phienBan. - Trường quan trọng dùng
required; trường phụ có giá trị mặc định an toàn. - Enum ghi dạng chữ; từ chối trường lạ khi nạp file sửa tay.
- Nạp lỗi thì giữ recipe cũ và báo rõ; không bao giờ chạy với thông số rỗng.
- So sánh dung sai số thực có sai số nhỏ
Eps.
Ý tưởng tương tự trong Python có ở bài tổ chức pipeline bằng dataclass và cấu hình JSON. Recipe này có thể dùng ngay trong ứng dụng WinForms hiển thị camera với OpenCvSharp.
Nguồn tham khảo: Microsoft Learn: tùy biến tên thuộc tính và enum, Microsoft Learn: required properties, Microsoft Learn: xử lý trường không khớp.


