buyme_on_device

Flutter SDK chạy model AI trên thiết bị (on-device) để kiểm định / chụp ảnh xe theo thời gian thực qua camera — nhận diện góc xe, bộ phận và biển số.


Mục lục


Cài đặt

Thêm vào pubspec.yaml của app:

dependencies:
  buyme_on_device: ^0.1.1

Sau đó import:

import 'package:buyme_on_device/buyme_on_device.dart';

Yêu cầu Flutter >=3.38.0 / Dart >=3.10 (YOLO native + Native Assets hooks cho OpenCV).


OpenCV (opencv_dart)

SDK dùng opencv_dart (^2.2.1) để kiểm tra chất lượng ảnh real-time trên Dart layer (blur / độ sáng / chói — glare). Native camera chỉ sample + encode JPEG nhỏ; thuật toán OpenCV chạy hoàn toàn phía Dart.

Bạn không cần thêm opencv_dart thủ công vào app — dependency đã nằm trong package. Host app chỉ cần đủ Flutter/Dart version hỗ trợ Native Assets / hooks.

Yêu cầu

Mục Giá trị
Flutter >=3.38.0
Dart SDK >=3.10.0
Package opencv_dart: ^2.2.1+4 (transitive)
Modules dùng imgproc, imgcodecs (core luôn có sẵn)

Hooks đã cấu hình sẵn trong package

Trong pubspec.yaml của buyme_on_device:

hooks:
  user_defines:
    dartcv4:
      include_modules:
        - imgproc
        - imgcodecs

Từ opencv_dart 2.2 trở đi, mặc định build OpenCV từ source (không còn prebuilt static binary). Lần build đầu có thể lâu và cần toolchain native (CMake / NDK trên Android, Xcode trên iOS). Các lần sau dùng cache.

Lưu ý khi integrate app

  1. Giữ Flutter >=3.38 — opencv_dart 2.x không chạy trên Flutter cũ (v1.x là nhánh riêng).
  2. Lần flutter run / flutter build đầu tiên trên máy có thể mất nhiều phút để biên dịch OpenCV.
  3. Không cần copy thêm permission riêng cho OpenCV; nó dùng frame JPEG đã có từ camera pipeline của SDK.
  4. Nếu app cũng dùng opencv_dart / dartcv4 trực tiếp, có thể khai báo thêm hooks.user_defines.dartcv4 ở pubspec.yaml của app — tránh exclude_modules làm mất imgproc / imgcodecs vì SDK phụ thuộc hai module này.

Cấu hình nền tảng (Permissions)

SDK sử dụng camera, vị trí (geolocation) và lưu ảnh vào gallery. Cần khai báo quyền cho từng nền tảng.

iOS — ios/Runner/Info.plist

<key>NSCameraUsageDescription</key>
<string>Ứng dụng cần camera để chụp ảnh kiểm định xe</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Ứng dụng cần vị trí để gắn toạ độ ảnh chụp</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Ứng dụng cần lưu ảnh đã chụp vào thư viện</string>

Android — android/app/src/main/AndroidManifest.xml

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

SDK tự yêu cầu quyền (camera, vị trí) khi cần.


Bắt đầu nhanh

import 'package:buyme_on_device/buyme_on_device.dart';
import 'package:flutter/material.dart';

void openInspection(BuildContext context) {
  final config = BuyMeConfig(
    generalConfig: GeneralConfig(
      apiToken: '<API_TOKEN>', // bắt buộc — liên hệ để được cấp
      documentId: '<DOCUMENT_ID>', // bắt buộc — ID hồ sơ (so_id_hs)
      organization: AiCycleOrg.aicycle,
      environment: BuyMeEnvironment.stage,
    ),
    carInformation: CarInformation(
      companyName: 'toyota',
      modelName: 'vios',
      licensePlate: '30A12345',
    ),
    modelConfig: ModelConfig(), // dùng ngưỡng mặc định
    validateConfig: const ValidateConfig(
      angleMode: CaptureAngleMode.sixAngles, // hoặc CaptureAngleMode.eightAngles (8 góc có thêm 2 góc sườn)
      requireFullAnglePanoramicPhotos: false,
      requireLicensePlate: true, // mặc định: bắt buộc đọc được biển trước khi auto-capture
    ),
  );

  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (_) => BuyMeOnDeviceCamera(
        buyMeConfig: config,
        onComplete: () => Navigator.pop(context),
        onError: (error) => debugPrint('SDK Error: $error'),
        onImageUploaded: (data) => debugPrint('Uploaded: $data'),
      ),
    ),
  );
}

Hai cách sử dụng: BuyMeOnDevice vs BuyMeOnDeviceCamera

SDK cung cấp 2 widget điểm vào (entry point). Cả hai đều nhận buyMeConfig, onComplete, onError, onImageUploaded. Khác nhau ở luồng chuẩn bị model:

Tiêu chí BuyMeOnDevice BuyMeOnDeviceCamera
Màn hình quản lý / chọn model trước khi mở camera ✅ Có (ModelManagerScreen) ❌ Không
Tự tải & chuẩn bị model Người dùng chọn model rồi mới tải Tự tải toàn bộ model cần thiết
Phù hợp khi Muốn người dùng xem/chọn model đã tải Muốn vào thẳng camera, đơn giản nhất
Cách dùng Push widget vào Navigator Push widget vào Navigator

Bên trong, BuyMeOnDevice sau khi người dùng chọn model sẽ tự push sang BuyMeOnDeviceCamera. Nếu không cần màn chọn model, dùng thẳng BuyMeOnDeviceCamera.

1. BuyMeOnDevice (widget)

Hiển thị màn Model Manager để người dùng xem/chọn model trước, sau đó vào camera.

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => BuyMeOnDevice(
      buyMeConfig: config,
      onComplete: () => Navigator.pop(context),
      onError: (error) => showError(error),
      onImageUploaded: (data) { /* xử lý từng ảnh upload thành công */ },
    ),
  ),
);

Tham số:

Tên Kiểu Bắt buộc Mô tả
buyMeConfig BuyMeConfig ✅ Toàn bộ cấu hình SDK (xem bên dưới).
onComplete void Function()? ❌ Gọi khi hoàn tất luồng (sau khi upload xong).
onError void Function(String error)? ❌ Gọi khi có lỗi (khởi tạo hồ sơ, tải model, …).
onImageUploaded OnImageUploaded? ❌ Gọi mỗi khi một ảnh upload thành công, kèm JSON server trả về.

Nút back trên màn Model Manager được điều khiển bằng DisplayConfig.showBackButton.

2. BuyMeOnDeviceCamera (widget)

Điểm vào trực tiếp: tự tạo/lấy hồ sơ (folder), tự tải & chuẩn bị model rồi vào thẳng camera. Đây là cách dùng phổ biến nhất.

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => BuyMeOnDeviceCamera(
      buyMeConfig: config,
      onComplete: () => Navigator.pop(context),
      onError: (error) => showError(error),
      onImageUploaded: (data) { /* ... */ },
    ),
  ),
);

Tham số:

Tên Kiểu Bắt buộc Mô tả
buyMeConfig BuyMeConfig ✅ Toàn bộ cấu hình SDK.
onComplete void Function()? ❌ Gọi khi hoàn tất luồng (sau khi upload ảnh).
onError void Function(String error)? ❌ Gọi khi có lỗi.
onImageUploaded OnImageUploaded? ❌ Gọi mỗi khi một ảnh upload thành công.
carCornerModelPath String? ❌ Đường dẫn model "góc xe" có sẵn (bỏ trống → SDK tự tải).
carPartModelPath String? ❌ Đường dẫn model "bộ phận xe" có sẵn (bỏ trống → SDK tự tải).
carQualityModelPath String? ❌ Đường dẫn model "chất lượng ảnh" (hiện tạm tắt trên UI).
licensePlateModelPath String? ❌ Đường dẫn model OCR biển số (bỏ trống → SDK tự tải).

Các tham số *ModelPath chủ yếu dùng nội bộ (khi đã có model từ màn chọn model). Bình thường không cần truyền — SDK tự tải.

Model đang bật: carCorner, carPart, licensePlate (carQuality tạm tắt).


Chi tiết các tham số cấu hình

BuyMeConfig

Đối tượng gốc gom toàn bộ cấu hình.

Trường Kiểu Bắt buộc Mặc định Mô tả
generalConfig GeneralConfig ✅ — Cấu hình chung (token, hồ sơ, môi trường…).
modelConfig ModelConfig ✅ — Ngưỡng confidence / IOU cho từng model.
carInformation CarInformation ✅ — Thông tin xe được kiểm định.
displayConfig DisplayConfig ❌ DisplayConfig() Tuỳ chỉnh hiển thị.
validateConfig ValidateConfig ❌ ValidateConfig() Tuỳ chỉnh ràng buộc chụp ảnh (chế độ 6 góc / 8 góc, biển số…).
vbiConfig VBIConfig? ⚠️ null Bắt buộc khi organization == AiCycleOrg.vbi.
initialPhotos Map<int, List<String>>? ❌ null Map các góc đã chụp trước đó (angleId -> [filePath]) để khôi phục phiên chụp và tiếp tục chụp các góc còn lại (0..5 với 6 góc, 0..7 với 8 góc).
final config = BuyMeConfig(
  generalConfig: GeneralConfig(...),
  modelConfig: ModelConfig(),
  carInformation: CarInformation(...),
  displayConfig: const DisplayConfig(showBackButton: true),
  validateConfig: const ValidateConfig(
    angleMode: CaptureAngleMode.sixAngles, // hoặc CaptureAngleMode.eightAngles
    requireFullAnglePanoramicPhotos: true,
    requireLicensePlate: true,
    minCarSpanThreshold: 0.35,
    maxCarSpanThreshold: 0.85,
  ),
  initialPhotos: {
    0: ['/path/to/front.jpg'], // Góc 0 đã có ảnh -> SDK bỏ qua góc này và chụp tiếp
  },
  vbiConfig: null, // hoặc VBIConfig(...) nếu organization là vbi
);

GeneralConfig (bắt buộc)

Trường Kiểu Bắt buộc Mặc định Mô tả
apiToken String ✅ — Token API (liên hệ để được cấp).
documentId String ✅ — ID hồ sơ (so_id_hs).
organization AiCycleOrg ✅ — Tổ chức dùng SDK: aicycle, vbi, others.
environment BuyMeEnvironment ❌ develop Môi trường: develop, stage, production.
documentName String? ❌ null Tên hồ sơ.
loggingEnabled bool ❌ false Bật/tắt log của SDK.
savePhotoAfterShot bool ❌ true Có lưu ảnh đã chụp vào gallery của máy không.
alwaysCache bool ❌ false Khi thoát màn camera, giữ cache ảnh đã chụp và không hiện dialog xác nhận xoá cache.
aicBaseUrl String? ❌ null Base URL API theo domain đối tác (on-premise).
GeneralConfig(
  apiToken: '<API_TOKEN>',
  documentId: '<DOCUMENT_ID>',
  organization: AiCycleOrg.aicycle,
  environment: BuyMeEnvironment.production,
  documentName: 'Hồ sơ kiểm định xe',
  loggingEnabled: false,
  savePhotoAfterShot: true,
  alwaysCache: false,
  // Ví dụ override domain API cho môi trường đối tác.
  // aicBaseUrl: 'https://partner-api.example.com',
);

⚠️ Với organization: AiCycleOrg.vbi, aicBaseUrl là bắt buộc và phải khác rỗng.

CarInformation (bắt buộc)

Tất cả các trường đều tùy chọn (có giá trị mặc định là chuỗi rỗng / null), nhưng nên điền đầy đủ để hồ sơ chính xác.

Trường Kiểu Mặc định Mô tả
companyName String '' Hãng xe, ví dụ "toyota".
modelName String '' Dòng/hiệu xe, ví dụ "vios".
manufacturingYear int? null Năm sản xuất, ví dụ 2022.
vehicleVersionName String '' Phiên bản xe, ví dụ "1.5C".
licensePlate String '' Biển số xe, ví dụ "30A12345".
vehicleType String? null Loại xe, ví dụ "sedan", "pickup".
color String? null Màu xe dạng hex #RRGGBB, ví dụ "#A2A8A1".
garageId String '' ID garage.
vehicleBrandId String '' ID brand.
CarInformation(
  companyName: 'toyota',
  modelName: 'vios',
  manufacturingYear: 2022,
  vehicleVersionName: '1.5C',
  licensePlate: '30A12345',
  vehicleType: 'sedan',
  color: '#A2A8A1',
  garageId: '',
  vehicleBrandId: '',
);

ModelConfig (bắt buộc)

Ngưỡng cho từng model AI và thuật toán kiểm tra chất lượng ảnh OpenCV. Các giá trị phải tuân thủ điều kiện chặn biên (vi phạm sẽ ném assert).

Trường Kiểu Mặc định Mô tả
carPartConfThreshold double 0.5 Confidence threshold model nhận diện bộ phận xe (0.0–1.0).
carPartIouThreshold double 0.45 IOU threshold model nhận diện bộ phận xe (0.0–1.0).
carCornerConfThreshold double 0.3 Confidence threshold model phân loại góc xe (0.0–1.0).
carQualityConfThreshold double 0.3 Confidence threshold model đánh giá chất lượng ảnh (0.0–1.0).
licensePlateConfThreshold double 0.5 Confidence threshold model OCR biển số (0.0–1.0).
blurThreshold double 30.0 Ngưỡng Laplacian variance cơ bản để xác định ảnh mờ (>= 0.0).
blurAdvancedThresholds List<double> [3.0, 10.0] Cặp ngưỡng [sharpnessGrade, blurScore] nhận diện ảnh mờ nâng cao (>= 0.0).
brightThreshold double 85.0 Ngưỡng độ sáng trung bình tối thiểu (0.0–255.0, dưới ngưỡng này là ảnh tối).
glareThreshold double 0.18 Ngưỡng tỷ lệ điểm trắng/lóa tối đa (0.0–1.0, vượt ngưỡng này là ảnh chói sáng).
// Dùng mặc định
ModelConfig();

// Hoặc tinh chỉnh
ModelConfig(
  carPartConfThreshold: 0.5,
  carPartIouThreshold: 0.45,
  carCornerConfThreshold: 0.3,
  carQualityConfThreshold: 0.3,
  licensePlateConfThreshold: 0.5,
  blurThreshold: 30.0,
  blurAdvancedThresholds: const [3.0, 10.0],
  brightThreshold: 85.0,
  glareThreshold: 0.18,
);

Conf threshold càng thấp → bắt được nhiều đối tượng hơn nhưng dễ nhiễu. IOU threshold điều chỉnh việc gộp các bounding box trùng nhau. Image quality thresholds điều chỉnh độ nhạy khi phát hiện ảnh mờ, tối hoặc chói sáng.

DisplayConfig (tùy chọn)

Trường Kiểu Mặc định Mô tả
loadingWidget Widget? null Widget loading tùy chỉnh.
showBackButton bool false Hiển thị nút back trên màn quản lý model.
const DisplayConfig(
  showBackButton: true,
);

ValidateConfig (tùy chọn)

Trường Kiểu Mặc định Mô tả
angleMode CaptureAngleMode CaptureAngleMode.sixAngles Chế độ góc chụp toàn cảnh: sixAngles (6 góc truyền thống), sixAnglesWithSides (6 góc sườn & chéo), hoặc eightAngles (8 góc có thêm 2 sườn).
requireFullAnglePanoramicPhotos bool false Bắt buộc chụp ảnh toàn cảnh đủ tất cả các góc theo angleMode trước khi xem kết quả. Khi false: chỉ cần ít nhất một góc đã chụp.
require6AnglePanoramicPhotos bool false (Tương thích ngược) Tương đương requireFullAnglePanoramicPhotos khi ở chế độ 6 góc.
requireLicensePlate bool true Bắt buộc nhận diện / đọc được biển số trước khi auto-capture ảnh toàn cảnh (tự động miễn trừ với các góc sườn).
minCarSpanThreshold double 0.35 Ngưỡng chiều dài tối thiểu của xe trong khung hình (từ 0.0 đến 1.0). Nếu xe nhỏ hơn ngưỡng này khi thiếu bộ phận -> nhắc tiến lại gần thay vì lùi ra xa.
maxCarSpanThreshold double 0.85 Ngưỡng chiều dài tối đa của xe trong khung hình (từ minCarSpanThreshold đến 1.0).
const ValidateConfig(
  angleMode: CaptureAngleMode.sixAngles, // sixAngles, sixAnglesWithSides, hoặc eightAngles
  requireFullAnglePanoramicPhotos: true,
  requireLicensePlate: true,
  minCarSpanThreshold: 0.35,
  maxCarSpanThreshold: 0.85,
);

Chế độ góc chụp (CaptureAngleMode)

SDK hỗ trợ 3 chế độ góc chụp toàn cảnh thông qua enum CaptureAngleMode:

Chế độ Số góc Danh sách các góc (angleId) Mô tả
CaptureAngleMode.sixAngles (mặc định) 6 0: Đầu xe
1: Đầu phụ (45° phải trước)
2: Đuôi phụ (45° phải sau)
3: Đuôi xe
4: Đuôi lái (45° trái sau)
5: Đầu lái (45° trái trước)
6 góc truyền thống: Trước, Sau và 4 góc chéo 45°.
CaptureAngleMode.sixAnglesWithSides 6 0: Đầu phụ (45° phải trước)
1: Sườn phải (90° bên phải)
2: Đuôi phụ (45° phải sau)
3: Đuôi lái (45° trái sau)
4: Sườn trái (90° bên trái)
5: Đầu lái (45° trái trước)
6 góc sườn & chéo: Loại bỏ góc Trước/Sau, radar chia vạch tại đầu/đuôi xe và 2 cung sườn hông.
CaptureAngleMode.eightAngles 8 0: Đầu xe
1: Đầu phụ (45° phải trước)
2: Sườn phải (90° bên phải)
3: Đuôi phụ (45° phải sau)
4: Đuôi xe
5: Đuôi lái (45° trái sau)
6: Sườn trái (90° bên trái)
7: Đầu lái (45° trái trước)
8 góc toàn diện: Trước, Sau, 2 góc sườn và 4 góc chéo 45°.
Đặc điểm chụp của các góc sườn xe (sixAnglesWithSides và eightAngles):
  • Bộ phận nhận diện bắt buộc: Khung hình camera cần nhận diện đủ 3 nhóm bộ phận: Cánh cửa, Tai (vè trước) xe, Hông (vè sau) xe / thùng xe.
  • Cơ chế miễn trừ biển số (requiresPlate = false): Ở góc chụp trực diện 90° vào sườn xe, biển số trước và sau sẽ bị che khuất hoặc nghiêng góc lớn. SDK tự động bỏ qua kiểm tra biển số cho các góc sườn, ngay cả khi requireLicensePlate: true.
  • Tooltip điều hướng: Cung cấp tooltip chỉ dẫn trực quan tương ứng: "Vui lòng di chuyển sang sườn phải xe" và "Vui lòng di chuyển sang sườn trái xe".
  • Vòng tròn tiến độ (CarProgressRing): Tự động chia các cung tròn và vạch ngăn tương ứng theo angleMode. Ở chế độ sixAnglesWithSides, vạch ngăn ranh giới nằm chính giữa đầu xe và đuôi xe, 2 cung sườn nằm ở hông xe.

Cơ chế hoạt động của minCarSpanThreshold:

  • Định nghĩa: Tỷ lệ chiều dài của toàn bộ chiếc xe (bao gồm mọi bộ phận xe nhận diện được) so với chiều dài màn hình (từ 0.0 đến 1.0). Mặc định là 0.35 (35% màn hình).
  • Mục đích: Khắc phục tình trạng khi người dùng đứng quá xa xe, hệ thống bị thiếu các bộ phận nhỏ nhưng lại nhắc sai thành "Vui lòng lùi ra xa xe hơn" (do lầm tưởng xe bị tràn khung).
  • Hành vi:
    • Nếu xe đứng xa và chiếm dưới minCarSpanThreshold màn hình mà vẫn thiếu bộ phận $\rightarrow$ Tooltip sẽ thông minh hướng dẫn: "Vui lòng tiến lại gần xe hơn".
    • Chỉ khi xe đủ lớn nhưng bị cắt mép khung hình $\rightarrow$ Mới nhắc: "Vui lòng lùi ra xa xe hơn".

VBIConfig (bắt buộc khi organization = vbi)

Chỉ dùng khi generalConfig.organization == AiCycleOrg.vbi. SDK thực hiện upload ảnh lên endpoint chuẩn hóa: POST {apiUploadBaseUrl}/api/{apiVersionCode}/Aicycle/buy-me/upload

Phân định trách nhiệm dữ liệu:

  • SDK tự động xử lý:
    • Img: File ảnh nhị phân vừa chụp từ Camera.
    • DirectionSlug: SDK tự động ánh xạ dựa trên góc chụp hiện tại (angleId: 0..5) sang slug Aicycle, hoặc fallback về defaultDirectionSlug (mặc định: '45-trai-truoc-C1xM02').
    • RequestedTime: SDK tự sinh thời gian upload hiện tại (DateTime.now().toIso8601String()).
  • App Host truyền cấu hình động qua VBIConfig:
Trường Kiểu Bắt buộc Mặc định Mô tả
apiUploadBaseUrl String ✅ — Base URL upload VBI (VD: https://uat-api-mobile.evbi.vn).
apiVersionCode String ❌ 'v1' Version code API VBI.
authorityId String? ❌ null Mã Authority đính kèm header Authority (tùy chọn).
signatureKey String? ❌ null Khóa chữ ký đính kèm header Signature (tùy chọn).
soId String? ❌ documentId Mã hồ sơ VBI (SoId), tự fallback về documentId nếu không truyền.
soIdDt String? ❌ externalSessionId Mã đối tượng / phiên phụ (SoIdDt).
externalSessionId String? ❌ soIdDt ID phiên bên ngoài (ExternalSessionId).
isValidate bool ❌ true Cờ validate (IsValidate), khi true thì DirectionSlug là bắt buộc.
defaultDirectionSlug String ❌ '45-trai-truoc-C1xM02' Slug góc chụp mặc định khi không xác định góc.
angleSlugMap Map<int, String>? ❌ null Cho phép App Host ghi đè slug riêng cho từng góc angleId (0..5 với 6 góc, 0..7 với 8 góc).
carCompany String? ❌ carInfo.companyName Hãng xe (CarCompany).
licensePlate String? ❌ carInfo.licensePlate Biển số xe (LicensePlate).
hasLicensePlate bool? ❌ licensePlate != '' Cờ có biển số hay không (HasLicensePlate).
externalUsedCar String? ❌ '' Cờ xe cũ / đã qua sử dụng (ExternalUsedCar).
location String? ❌ lat,long GPS máy Toạ độ GPS (Location), nếu để trống SDK sẽ tự lấy toạ độ của thiết bị.

Danh sách Slug góc chụp mặc định theo angleId:

SDK tự động ánh xạ theo angleMode được cấu hình trong ValidateConfig:

  • Chế độ 6 góc truyền thống (CaptureAngleMode.sixAngles):

    • 0 (Đầu xe): truoc-sT9qgX
    • 1 (Đầu phụ): 45-phai-truoc-UoYzs6
    • 2 (Đuôi phụ): 45-phai-sau-fRzY3r
    • 3 (Đuôi xe): sau-htBwjB
    • 4 (Đuôi lái): 45-trai-sau-1q3G3J
    • 5 (Đầu lái): 45-trai-truoc-C1xM02
  • Chế độ 6 góc gồm sườn (CaptureAngleMode.sixAnglesWithSides):

    • 0 (Đầu phụ): 45-phai-truoc-UoYzs6
    • 1 (Sườn phải): suon-phai
    • 2 (Đuôi phụ): 45-phai-sau-fRzY3r
    • 3 (Đuôi lái): 45-trai-sau-1q3G3J
    • 4 (Sườn trái): suon-trai
    • 5 (Đầu lái): 45-trai-truoc-C1xM02
  • Chế độ 8 góc (CaptureAngleMode.eightAngles):

    • 0 (Đầu xe): truoc-sT9qgX
    • 1 (Đầu phụ): 45-phai-truoc-UoYzs6
    • 2 (Sườn phải): suon-phai
    • 3 (Đuôi phụ): 45-phai-sau-fRzY3r
    • 4 (Đuôi xe): sau-htBwjB
    • 5 (Đuôi lái): 45-trai-sau-1q3G3J
    • 6 (Sườn trái): suon-trai
    • 7 (Đầu lái): 45-trai-truoc-C1xM02
  • Các góc khác / Không xác định: Fallback về defaultDirectionSlug ('45-trai-truoc-C1xM02'). Có thể tùy biến toàn bộ qua angleSlugMap.

final config = BuyMeConfig(
  generalConfig: GeneralConfig(
    apiToken: '<API_TOKEN>',
    documentId: '<DOCUMENT_ID>', // SoId
    organization: AiCycleOrg.vbi, // ⚠️ bắt buộc vbiConfig
    aicBaseUrl: 'https://uat-api-mobile.evbi.vn', // ⚠️ bắt buộc với VBI
  ),
  modelConfig: ModelConfig(),
  carInformation: CarInformation(
    companyName: 'Toyota',
    licensePlate: '30A12345',
  ),
  vbiConfig: VBIConfig(
    apiUploadBaseUrl: 'https://uat-api-mobile.evbi.vn',
    apiVersionCode: 'v1',
    authorityId: '<AUTHORITY_ID>',
    signatureKey: '<SIGNATURE_KEY>',
    soId: '202609230001',
    soIdDt: '202609230002',
    externalSessionId: '202609230002',
    isValidate: true,
    defaultDirectionSlug: '45-trai-truoc-C1xM02', // hoặc tuỳ chỉnh
    externalUsedCar: '',
  ),
);

⚠️ Nếu organization == AiCycleOrg.vbi mà vbiConfig == null, constructor BuyMeConfig sẽ ném assert.


Callback sự kiện: onPhotoCaptured, onComplete, onError, onImageUploaded

Cả BuyMeOnDevice và BuyMeOnDeviceCamera đều hỗ trợ các callback sau:

Tên Callback Kiểu dữ liệu / Signature Thời điểm gọi
onPhotoCaptured void Function(CapturedPhotoData photo) Realtime ngay khi chụp xong một ảnh thành công và lưu file xuống đĩa. Dùng để App Host lưu đường dẫn và quản lý ảnh local.
onComplete void Function() Sau khi hoàn tất chụp, người dùng bấm "Xem kết quả" và upload ảnh xong.
onError void Function(String error) Khi xảy ra lỗi (tạo hồ sơ thất bại, tải model lỗi, xác thực token lỗi…).
onImageUploaded void Function(Map<String, dynamic> data) Gọi mỗi lần một ảnh upload thành công lên server.

Cấu trúc dữ liệu CapturedPhotoData

class CapturedPhotoData {
  final String documentId;     // Mã hồ sơ (documentId)
  final int angleId;           // ID góc chụp (0..5 ở chế độ 6 góc, hoặc 0..7 ở chế độ 8 góc)
  final String filePath;       // Đường dẫn file ảnh gốc tuyệt đối trên đĩa
  final String thumbnailPath;  // Đường dẫn file thumbnail tuyệt đối trên đĩa
  final int photoIndex;        // Thứ tự ảnh trong góc đó (thường là 0)
  final DateTime capturedAt;   // Thời gian chụp
}
BuyMeOnDeviceCamera(
  buyMeConfig: config,
  onPhotoCaptured: (CapturedPhotoData photo) {
    debugPrint('Đã chụp góc ${photo.angleId}: ${photo.filePath}');
    // App Host lưu lại thông tin để hiển thị hoặc chụp tiếp
  },
  onComplete: () {
    Navigator.pop(context);
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Hoàn tất')),
    );
  },
  onError: (error) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Lỗi SDK: $error')),
    );
  },
  onImageUploaded: (data) {
    debugPrint('Ảnh upload thành công: $data');
  },
);

Quản lý phiên chụp & Khôi phục (Session Recovery)

SDK hỗ trợ cơ chế cho phép App Host toàn quyền quản lý thời gian và vòng đời file ảnh (ví dụ: hồ sơ còn hạn trong 30 phút thì cho phép chụp tiếp, quá 30 phút thì chụp từ đầu).

1. Lấy ảnh realtime qua onPhotoCaptured

Mỗi khi chụp xong một góc, SDK lưu file vào bộ nhớ thiết bị và bắn callback onPhotoCaptured. App Host nhận đối tượng photo và có thể render ngay lên giao diện:

// Hiển thị ảnh bằng Image.file:
final file = File(photo.thumbnailPath.isNotEmpty ? photo.thumbnailPath : photo.filePath);
if (file.existsSync()) {
  Image.file(file, fit: BoxFit.cover);
}

2. Tiếp tục chụp các góc còn thiếu qua initialPhotos

Nếu người dùng thoát camera hoặc tắt ứng dụng, khi mở lại SDK:

  • Nếu muốn chụp tiếp: App Host gom danh sách ảnh hiện có thành Map<int, List<String>> và truyền vào BuyMeConfig(initialPhotos: ...):

    final initialPhotos = <int, List<String>>{
      0: ['/path/to/angle_0.jpg'], // Góc 0 đã có
      1: ['/path/to/angle_1.jpg'], // Góc 1 đã có
    };
    
    final config = BuyMeConfig(
      generalConfig: GeneralConfig(documentId: 'doc_123', ...),
      initialPhotos: initialPhotos, // 👈 Truyền map ảnh đã chụp
      // ...
    );
    

    Hành vi SDK khi nhận initialPhotos:

    • Tự động kiểm tra các file ảnh còn tồn tại trên máy.
    • Vòng tròn tiến độ tự động đánh dấu xanh các góc đã có ảnh.
    • Bỏ qua màn hình hướng dẫn ban đầu (Guide Sheet).
    • Tự động hướng dẫn người dùng di chuyển máy sang góc còn thiếu tiếp theo (ví dụ: Góc 2).
  • Nếu muốn chụp lại từ đầu: App Host truyền initialPhotos: null (hoặc map rỗng), SDK sẽ bắt đầu một phiên hoàn toàn mới từ góc 0.

3. Dọn dẹp & Xoá ảnh local

  • Tự động dọn dẹp khi "Kết thúc chụp ảnh" thành công: Khi người dùng hoàn tất quá trình chụp và bấm "Kết thúc chụp ảnh" (upload toàn bộ ảnh lên server thành công), SDK sẽ tự động xoá sạch toàn bộ ảnh local và thumbnail của documentId đó trên máy để giải phóng bộ nhớ.
  • Nếu chưa bấm kết thúc (thoát ngang / back camera): Ảnh vẫn được giữ nguyên vẹn trên máy để phục vụ chụp tiếp.
  • App host tự xoá thủ công từng file:
    final file = File(photo.filePath);
    if (file.existsSync()) await file.delete();
    
  • Hoặc chủ động xoá sạch qua helper SDK:
    await BuyMeOnDevice.clearSessionPhotos(documentId);
    

Cache ảnh phiên chụp

SDK lưu ảnh đã chụp theo documentId (session) trên disk để sống sót khi app bị kill / thoát giữa chừng.

  • Ảnh được ghi cache ngay sau mỗi lần chụp.
  • Nếu lần trước thoát khi đang upload (upload pending), lần mở lại SDK có thể khôi phục và tiếp tục màn upload.
GeneralConfig(
  apiToken: '<API_TOKEN>',
  documentId: '<DOCUMENT_ID>',
  organization: AiCycleOrg.aicycle,
  alwaysCache: true, // giữ ảnh đã chụp khi thoát camera
);

Libraries

buyme_on_device
The public API for the buyme_on_device package.