diff --git a/CLAUDE.md b/CLAUDE.md index 6df8bff..5e113c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,7 @@ 現在の進捗: Step 1〜5 完了(撮影・保存・一覧・削除の実装完了).初期バージョンのアプリが動作する状態. ※ 撮影→保存の高速化完了(YUV→PNG 変換を Android ネイティブに移行,非圧縮 PNG + 回転統合により 15 秒 → 約 2 秒に短縮) +※ カメラ診断機能のインフラ追加完了(`DiagnosticsProvider` / `RawCaptureService.getCameraDiagnostics` / `FileService.saveDiagnosticsJson`).UI は非表示.AQUOS sense3 のフロントカメラは **HW Level 3 + RAW (DNG) + MANUAL_SENSOR + MANUAL_POST_PROCESSING** 対応を確認.次は定量モード撮影機能(DNG 経路)の実装. ※ ステップ完了時にここを更新すること. ## 必須ルール(コード実装時) diff --git a/android/app/src/main/kotlin/com/example/mini_tias/RawCapturePlugin.kt b/android/app/src/main/kotlin/com/example/mini_tias/RawCapturePlugin.kt index 9324ef1..589a200 100644 --- a/android/app/src/main/kotlin/com/example/mini_tias/RawCapturePlugin.kt +++ b/android/app/src/main/kotlin/com/example/mini_tias/RawCapturePlugin.kt @@ -77,6 +77,7 @@ result.error("SOUND_ERROR", "保存完了音の再生に失敗: ${e.message}", null) } } + "getCameraDiagnostics" -> getCameraDiagnostics(result) else -> result.notImplemented() } } @@ -443,6 +444,92 @@ out.write(ByteBuffer.allocate(4).putInt(crc.value.toInt()).array()) } + /** + * フロントカメラの Camera2 API 能力を読み出して Map で返す. + * + * CameraCharacteristics から各種パラメータを取得し,null セーフに処理する. + * フロントカメラが見つからない場合は error を返す. + */ + private fun getCameraDiagnostics(result: MethodChannel.Result) { + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + + val cameraId = cameraManager.cameraIdList.firstOrNull { id -> + val characteristics = cameraManager.getCameraCharacteristics(id) + characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT + } + + if (cameraId == null) { + result.error("NO_CAMERA", "フロントカメラが見つかりません", null) + return + } + + val c = cameraManager.getCameraCharacteristics(cameraId) + val streamConfigMap = c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + + // rawOutputSizes + val rawOutputSizes = streamConfigMap + ?.getOutputSizes(ImageFormat.RAW_SENSOR) + ?.map { "${it.width}x${it.height}" } + ?: emptyList() + + // yuvOutputSizes + val yuvOutputSizes = streamConfigMap + ?.getOutputSizes(ImageFormat.YUV_420_888) + ?.map { "${it.width}x${it.height}" } + ?: emptyList() + + // jpegOutputSizes + val jpegOutputSizes = streamConfigMap + ?.getOutputSizes(ImageFormat.JPEG) + ?.map { "${it.width}x${it.height}" } + ?: emptyList() + + val diagnostics = mapOf( + "cameraId" to cameraId, + "hardwareLevel" to c.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL), + "capabilities" to c.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)?.toList(), + "exposureTimeRangeNs" to c.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE) + ?.let { listOf(it.lower, it.upper) }, + "sensitivityRange" to c.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) + ?.let { listOf(it.lower, it.upper) }, + "lscMapSize" to null, // 実キャプチャ後に CaptureResult から取得する必要があるため診断段階では null + "availableLensShadingMapModes" to c.get( + CameraCharacteristics.STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES + )?.map { it }, + "availableNoiseReductionModes" to c.get( + CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES + )?.toList(), + "availableEdgeModes" to c.get(CameraCharacteristics.EDGE_AVAILABLE_EDGE_MODES)?.toList(), + "availableTonemapModes" to c.get( + CameraCharacteristics.TONEMAP_AVAILABLE_TONE_MAP_MODES + )?.toList(), + "availableShadingModes" to c.get(CameraCharacteristics.SHADING_AVAILABLE_MODES)?.toList(), + "availableAwbModes" to c.get( + CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES + )?.toList(), + "availableAfModes" to c.get( + CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES + )?.toList(), + "minFocusDistance" to c.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE), + "hyperfocalDistance" to c.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE), + "rawOutputSizes" to rawOutputSizes, + "yuvOutputSizes" to yuvOutputSizes, + "jpegOutputSizes" to jpegOutputSizes, + "colorFilterArrangement" to c.get( + CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT + ), + "blackLevelPattern" to c.get(CameraCharacteristics.SENSOR_BLACK_LEVEL_PATTERN)?.toString(), + "whiteLevel" to c.get(CameraCharacteristics.SENSOR_INFO_WHITE_LEVEL), + "physicalSize" to c.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE) + ?.let { listOf(it.width, it.height) }, + "pixelArraySize" to c.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE) + ?.let { listOf(it.width, it.height) }, + "activeArraySize" to c.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)?.toString(), + ) + + result.success(diagnostics) + } + private fun cleanup() { cameraDevice?.close() cameraDevice = null diff --git a/lib/main.dart b/lib/main.dart index 87089bf..d024a8a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:mini_tias/app.dart'; import 'package:mini_tias/providers/camera_provider.dart'; +import 'package:mini_tias/providers/diagnostics_provider.dart'; import 'package:mini_tias/providers/gallery_provider.dart'; void main() { @@ -15,6 +16,7 @@ providers: [ ChangeNotifierProvider(create: (_) => CameraProvider()), ChangeNotifierProvider(create: (_) => GalleryProvider()), + ChangeNotifierProvider(create: (_) => DiagnosticsProvider()), ], child: const MiniTiasApp(), ), diff --git a/lib/providers/diagnostics_provider.dart b/lib/providers/diagnostics_provider.dart new file mode 100644 index 0000000..05011bd --- /dev/null +++ b/lib/providers/diagnostics_provider.dart @@ -0,0 +1,55 @@ +import 'package:flutter/foundation.dart'; + +import 'package:mini_tias/services/file_service.dart'; +import 'package:mini_tias/services/raw_capture_service.dart'; + +/// カメラ診断機能の状態管理. +/// Camera2 API の能力を読み出し JSON ファイルに保存する. +class DiagnosticsProvider extends ChangeNotifier { + final RawCaptureService _rawCaptureService; + final FileService _fileService; + + DiagnosticsProvider({ + RawCaptureService? rawCaptureService, + FileService? fileService, + }) : _rawCaptureService = rawCaptureService ?? RawCaptureService(), + _fileService = fileService ?? FileService(); + + bool _isRunning = false; + String? _lastSavedPath; + String? _errorMessage; + + bool get isRunning => _isRunning; + String? get lastSavedPath => _lastSavedPath; + String? get errorMessage => _errorMessage; + + /// 診断を実行し,結果を JSON ファイルとして保存する. + Future runDiagnostics() async { + if (_isRunning) return; + + _isRunning = true; + _errorMessage = null; + _lastSavedPath = null; + notifyListeners(); + + try { + final diagnostics = await _rawCaptureService.getCameraDiagnostics(); + if (diagnostics == null) { + _errorMessage = 'カメラ診断情報を取得できませんでした'; + _isRunning = false; + notifyListeners(); + return; + } + + final savedPath = await _fileService.saveDiagnosticsJson(diagnostics); + await _rawCaptureService.scanFile(savedPath); + + _lastSavedPath = savedPath; + } catch (e) { + _errorMessage = '診断の実行に失敗しました: $e'; + } + + _isRunning = false; + notifyListeners(); + } +} diff --git a/lib/services/file_service.dart b/lib/services/file_service.dart index 1f9765a..37b0d9f 100644 --- a/lib/services/file_service.dart +++ b/lib/services/file_service.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -52,6 +53,36 @@ return '${baseName}_$suffix.png'; } + /// カメラ診断情報を JSON ファイルとして保存する. + /// + /// ファイル名: `camera_diagnostics_YYYYMMDD_HHmmss.json` + /// 保存先: `/storage/emulated/0/Pictures/MiniTIAS` + /// 戻り値は保存したファイルのフルパス. + Future saveDiagnosticsJson(Map diagnostics) async { + final directory = Directory(_basePath); + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + final now = DateTime.now(); + final timestamp = + '${now.year}' + '${now.month.toString().padLeft(2, '0')}' + '${now.day.toString().padLeft(2, '0')}' + '_' + '${now.hour.toString().padLeft(2, '0')}' + '${now.minute.toString().padLeft(2, '0')}' + '${now.second.toString().padLeft(2, '0')}'; + + final fileName = 'camera_diagnostics_$timestamp.json'; + final filePath = '$_basePath/$fileName'; + + const encoder = JsonEncoder.withIndent(' '); + await File(filePath).writeAsString(encoder.convert(diagnostics)); + + return filePath; + } + /// テスト用: タイムスタンプとファイル名一覧からファイル名を生成する. static String generateFileNameSync( String timestamp, diff --git a/lib/services/raw_capture_service.dart b/lib/services/raw_capture_service.dart index 9614251..e19e1cf 100644 --- a/lib/services/raw_capture_service.dart +++ b/lib/services/raw_capture_service.dart @@ -51,6 +51,17 @@ return result; } + /// フロントカメラの Camera2 API 能力を診断情報として取得する. + /// + /// 戻り値はカメラの各種パラメータを含む Map.取得できない場合は null を返す. + Future?> getCameraDiagnostics() async { + final result = await _channel.invokeMethod>( + 'getCameraDiagnostics', + ); + if (result == null) return null; + return result.cast(); + } + /// MediaStore にファイルを登録し,PC から MTP で見えるようにする. Future scanFile(String path) async { await _channel.invokeMethod('scanFile', {'path': path});