Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e31c357
connection recovery feature
harutiro Nov 4, 2025
4c1963f
feat: Add functionality to clear data for specific tag and navigate b…
harutiro Nov 10, 2025
bc76b39
feat: Update tag navigation logic to allow returning to the last comp…
harutiro Nov 10, 2025
8585672
feat: Implement antenna pairing management and integrate with Connect…
harutiro Nov 11, 2025
5143e8c
feat: Add antenna ID management and coordinate transformation for rea…
harutiro Nov 11, 2025
3792426
Refactor FloorMap and Antenna Positioning Logic
harutiro Nov 20, 2025
66ef89f
refactor: Remove ProjectProgress model and related repository methods
harutiro Nov 20, 2025
db4ad67
fix: Adjust azimuth angle conversion for polar to Cartesian transform…
harutiro Nov 20, 2025
bc615d7
feat: Add sessionId to PersistentRealtimeData and update related func…
harutiro Nov 24, 2025
179e4d5
feat: 複数アンテナ対応のデータ管理機能を追加し、CSVエクスポート機能を強化
harutiro Nov 24, 2025
429fa3f
fix: 修正されたプロパティアクセスを使用してメッセージ送信とデータマップ更新のロジックを改善
harutiro Nov 24, 2025
d3f1cc3
feat: NLOS検出機能を追加し、UIに警告表示を実装
harutiro Nov 24, 2025
d8de131
feat: createSessionDirectoryのcustomName引数を必須に変更し、ファイル名の保存とエクスポート処理を改善
harutiro Nov 24, 2025
c290c12
feat: リアルタイムデータ表示のトグル機能を追加し、UIのアニメーションを改善
harutiro Nov 24, 2025
fb493fa
feat: navigateToメソッドにスタックリセットオプションを追加し、データコレクション画面に戻るボタンを実装
harutiro Nov 24, 2025
01b1c43
fix: iOSと他のプラットフォームでのナビゲーションバーのボタン配置を条件付きで修正
harutiro Nov 24, 2025
3deccdf
feat: ZIP圧縮機能を持つZipFileManagerとセッションデータエクスポート用のUseCaseを追加し、データ表示画面に共有…
harutiro Nov 24, 2025
99398bf
feat: セッションデータ削除機能を追加し、データ表示画面に削除確認アラートを実装
harutiro Nov 24, 2025
74d1ae3
refactor: 不要なファイル管理機能を削除し、表示モードを簡素化
harutiro Nov 24, 2025
6e7a3f4
refactor: NavigationViewをVStackに変更し、SettingsViewのレイアウトを簡素化
harutiro Nov 24, 2025
845bbef
fix: フロアマップ情報の保存処理を修正し、SensingFlowNavigatorの検証に必要な情報を保存するように変更
harutiro Nov 24, 2025
fddd23d
feat: タグ表示モードを追加し、リアルタイム位置表示を個別表示と統合表示に切り替え可能に
harutiro Dec 11, 2025
2153560
fix: アンテナの回転角度を修正し、表示用の回転角度を追加
harutiro Dec 11, 2025
d4da821
fix: アンテナ回転コントロールの表示角度計算を修正し、ボタンアクションを調整
harutiro Dec 11, 2025
9e1e104
feat: 座標変換と重心計算のロジックを改善し、ローパスフィルターを追加
harutiro Dec 12, 2025
0c333fc
feat: Enhance Sensor Data Processing with IQR Outlier Detection
harutiro Dec 15, 2025
83f3139
feat: アンテナに紐づいたデバイスのデータ収集ロジックを改善し、エラーハンドリングを追加
harutiro Dec 16, 2025
a79be6e
キャリブレションをするときに動作が不安定になる 問題を解決。キャリブレーションをする端末だけをセンシングするように変更
harutiro Dec 16, 2025
c5ce0cd
Implement automatic reconnection handling in AutoAntennaCalibration, …
harutiro Dec 17, 2025
8ca782c
feat: 統合座標履歴の管理とCSVエクスポート機能を追加
harutiro Dec 17, 2025
39e4415
feat: 接続の安定化とペアリング情報の検証・復元機能を追加
harutiro Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions UWBViewerSystem/Devices/File/ZipFileManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//
// ZipFileManager.swift
// UWBViewerSystem
//
// Created by Claude Code on 2025/11/25.
//

import Foundation

/// ZIP圧縮処理を担当するファイルマネージャー
/// Devices層のFile管理機能として、ディレクトリのZIP圧縮を提供
class ZipFileManager {
static let shared = ZipFileManager()

private init() {}

/// ディレクトリをZIP圧縮する
/// - Parameters:
/// - sourceURL: 圧縮するディレクトリのURL
/// - zipFileName: 作成するZIPファイルの名前
/// - destinationDirectory: ZIPファイルを配置するディレクトリ(デフォルトは一時ディレクトリ)
/// - Returns: 作成されたZIPファイルのURL
/// - Throws: ファイル操作に関連するエラー
func zipDirectory(
at sourceURL: URL,
to zipFileName: String,
in destinationDirectory: URL? = nil
) throws -> URL {
let destDir = destinationDirectory ?? FileManager.default.temporaryDirectory
let zipURL = destDir.appendingPathComponent(zipFileName)

// 既存のZIPファイルを削除
try? FileManager.default.removeItem(at: zipURL)

// ZIP圧縮を実行
let coordinator = NSFileCoordinator()
var coordinationError: NSError?

coordinator.coordinate(
readingItemAt: sourceURL,
options: .forUploading,
error: &coordinationError
) { zipFileURL in
do {
try FileManager.default.copyItem(at: zipFileURL, to: zipURL)
print("📦 ZIP圧縮成功: \(zipURL.path)")
} catch {
print("❌ ZIP作成エラー: \(error)")
}
}

if let error = coordinationError {
throw error
}

return zipURL
}
Comment on lines +39 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

zipDirectoryでZIP作成に失敗してもURLを返してしまう可能性があります

NSFileCoordinator内のcopyItemでエラーが発生してもprintするだけで、zipDirectory自体は常にzipURLを返しています。その結果、実際には存在しない(または壊れた)ZIPファイルのURLを上位が扱うリスクがあります。

coordinationErrorとは別にcopyErrorを捕捉してthrowする、あるいはFileManager.default.fileExists(atPath: zipURL.path)で存在確認して、存在しない場合はエラーを投げるようにすると堅牢になります。

-        coordinator.coordinate(
+        var copyError: Error?
+        coordinator.coordinate(
             readingItemAt: sourceURL,
             options: .forUploading,
             error: &coordinationError
         ) { zipFileURL in
             do {
                 try FileManager.default.copyItem(at: zipFileURL, to: zipURL)
                 print("📦 ZIP圧縮成功: \(zipURL.path)")
             } catch {
-                print("❌ ZIP作成エラー: \(error)")
+                print("❌ ZIP作成エラー: \(error)")
+                copyError = error
             }
         }
 
         if let error = coordinationError {
             throw error
         }
+
+        if let error = copyError {
+            throw error
+        }
+
+        // 必要であれば存在確認も追加
+        // guard FileManager.default.fileExists(atPath: zipURL.path) else { throw SomeError() }


---

🏁 Script executed:

```shell
#!/bin/bash
# Examine ZipFileManager.swift to verify error handling claims
fd -e swift -p "ZipFileManager.swift" --exec cat {} \;

Length of output: 5446


🏁 Script executed:

#!/bin/bash
# Also search for how zipDirectory is called/used in codebase
rg -n "zipDirectory" --type swift -C 5

Length of output: 5143


🏁 Script executed:

#!/bin/bash
# Search for zipDirectoryContents to compare error handling approach
rg -n "zipDirectoryContents" --type swift -A 20

Length of output: 3707


zipDirectoryでZIP作成に失敗してもURLを返してしまう可能性があります

NSFileCoordinator内のcopyItemでエラーが発生しても、44~47行目のcatchブロックでprintするだけで、エラーが捕捉されず、zipDirectory自体は常にzipURLを返しています。その結果、実際には存在しない(または壊れた)ZIPファイルのURLを上位(zipFileszipDirectoryContentsなど)が受け取るリスクがあります。

coordinationErrorと同様にcopyErrorを捕捉し、ブロック後にthrowする必要があります。

         var coordinationError: NSError?
+        var copyError: Error?
 
         coordinator.coordinate(
             readingItemAt: sourceURL,
             options: .forUploading,
             error: &coordinationError
         ) { zipFileURL in
             do {
                 try FileManager.default.copyItem(at: zipFileURL, to: zipURL)
                 print("📦 ZIP圧縮成功: \(zipURL.path)")
             } catch {
                 print("❌ ZIP作成エラー: \(error)")
+                copyError = error
             }
         }
 
         if let error = coordinationError {
             throw error
         }
+
+        if let error = copyError {
+            throw error
+        }
 
         return zipURL
🤖 Prompt for AI Agents
In UWBViewerSystem/Devices/File/ZipFileManager.swift around lines 39 to 57, the
NSFileCoordinator closure currently catches copy errors only with a print, so
zipDirectory may still return the zipURL even if copy failed; declare a local
var copyError: Error? before calling coordinator, assign copyError = error in
the catch block inside the closure, and after the coordinator call check both
coordinationError and copyError and throw the first non-nil error instead of
returning zipURL on failure so the caller does not receive a non-existent or
corrupted URL.


/// 一時ディレクトリにファイルをコピーし、ZIP圧縮する
/// - Parameters:
/// - files: コピーするファイルのURL配列
/// - zipFileName: 作成するZIPファイルの名前
/// - tempDirectoryName: 一時ディレクトリの名前(デフォルトはランダムUUID)
/// - Returns: 作成されたZIPファイルのURL
/// - Throws: ファイル操作に関連するエラー
func zipFiles(
_ files: [URL],
to zipFileName: String,
tempDirectoryName: String? = nil
) throws -> URL {
// 一時ディレクトリを作成
let tempDirName = tempDirectoryName ?? UUID().uuidString
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(tempDirName, isDirectory: true)

try FileManager.default.createDirectory(
at: tempDir,
withIntermediateDirectories: true
)

// ファイルをコピー
for fileURL in files {
let fileName = fileURL.lastPathComponent
let destinationURL = tempDir.appendingPathComponent(fileName)
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
print("📄 ファイルコピー: \(fileName)")
}

// ZIP圧縮
let zipURL = try zipDirectory(at: tempDir, to: zipFileName)

// 一時ディレクトリを削除
try? FileManager.default.removeItem(at: tempDir)

return zipURL
}

/// ディレクトリ内の全ファイルをZIP圧縮する
/// - Parameters:
/// - directoryURL: 圧縮するディレクトリのURL
/// - zipFileName: 作成するZIPファイルの名前
/// - includeMetadata: メタデータJSONを含めるかどうか
/// - metadata: 含めるメタデータ(includeMetadataがtrueの場合)
/// - Returns: 作成されたZIPファイルのURL、ディレクトリが存在しない場合はnil
/// - Throws: ファイル操作に関連するエラー
func zipDirectoryContents(
at directoryURL: URL,
to zipFileName: String,
includeMetadata: Bool = false,
metadata: [String: Any]? = nil
) throws -> URL? {
// ディレクトリの存在確認
guard FileManager.default.fileExists(atPath: directoryURL.path) else {
print("⚠️ 指定されたディレクトリが存在しません: \(directoryURL.path)")
return nil
}

// 一時ディレクトリを作成
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)

try FileManager.default.createDirectory(
at: tempDir,
withIntermediateDirectories: true
)

// ディレクトリ内の全ファイルをコピー
let fileManager = FileManager.default
let files = try fileManager.contentsOfDirectory(
at: directoryURL,
includingPropertiesForKeys: nil
)

var copiedFileCount = 0
for fileURL in files {
let fileName = fileURL.lastPathComponent
let destinationURL = tempDir.appendingPathComponent(fileName)
try fileManager.copyItem(at: fileURL, to: destinationURL)
print("📄 ファイルコピー: \(fileName)")
copiedFileCount += 1
}

print("✅ \(copiedFileCount)個のファイルをコピーしました")

// メタデータJSONを追加
if includeMetadata, let metadata {
let jsonData = try JSONSerialization.data(
withJSONObject: metadata,
options: .prettyPrinted
)
let metadataURL = tempDir.appendingPathComponent("metadata.json")
try jsonData.write(to: metadataURL)
print("📄 メタデータJSON作成完了: \(metadataURL.path)")
}

// ZIP圧縮
print("🗜️ ZIP圧縮開始")
let zipURL = try zipDirectory(at: tempDir, to: zipFileName)
print("🗜️ ZIP圧縮完了: \(zipURL.path)")

// ZIPファイルの存在確認
if FileManager.default.fileExists(atPath: zipURL.path) {
let attributes = try? FileManager.default.attributesOfItem(atPath: zipURL.path)
let fileSize = attributes?[.size] as? Int64 ?? 0
print("✅ ZIPファイル確認OK: サイズ=\(fileSize)バイト")
}

// 一時ディレクトリを削除
try? FileManager.default.removeItem(at: tempDir)
print("🗑️ 一時ディレクトリ削除完了")

return zipURL
}
}
70 changes: 58 additions & 12 deletions UWBViewerSystem/Devices/NearByConnection/NearByConnectionApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,38 @@ import Foundation
return
}

// 既存の広告を停止してからリトライ付きで開始
advertiser.stopAdvertising()
self.startAdvertiseInternal(advertiser: advertiser, retryCount: 0)
}

/// Advertise開始の内部実装(リトライ対応)
private func startAdvertiseInternal(advertiser: Advertiser, retryCount: Int) {
let maxRetries = 3
let context = Data(nickName.utf8)

advertiser.startAdvertising(using: context) { [weak self] error in
Task { @MainActor [weak self] in
guard let self else { return }

if let error {
self?.notifyCallbacks {
$0.onConnectionStateChanged(state: "広告開始エラー: \(error.localizedDescription)")
print("📢 Advertise開始エラー (試行 \(retryCount + 1)/\(maxRetries + 1)): \(error.localizedDescription)")

// リトライ可能な場合はリトライ
if retryCount < maxRetries {
advertiser.stopAdvertising()
let delay = Double(retryCount + 1) * 0.5
print("📢 \(delay)秒後にAdvertiseをリトライします...")

try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
self.startAdvertiseInternal(advertiser: advertiser, retryCount: retryCount + 1)
} else {
self.notifyCallbacks {
$0.onConnectionStateChanged(state: "広告開始エラー: \(error.localizedDescription)")
}
}
} else {
self?.notifyCallbacks { $0.onConnectionStateChanged(state: "広告開始成功") }
self.notifyCallbacks { $0.onConnectionStateChanged(state: "広告開始成功") }
}
}
}
Expand All @@ -161,23 +184,46 @@ import Foundation
return
}

// 既にDiscovery中の場合は何もしない
// 既にDiscovery中の場合は一度停止してから再開
if self.isDiscovering {
self.notifyCallbacks { $0.onConnectionStateChanged(state: "既に検索中です") }
return
print("📡 既にDiscovery中のため、一度停止します")
discoverer.stopDiscovery()
self.isDiscovering = false
}

self.startDiscoveryInternal(discoverer: discoverer, retryCount: 0)
}

/// Discovery開始の内部実装(リトライ対応)
private func startDiscoveryInternal(discoverer: Discoverer, retryCount: Int) {
let maxRetries = 3

discoverer.startDiscovery { [weak self] error in
Task { @MainActor [weak self] in
guard let self else { return }

if let error {
self?.isDiscovering = false
self?.notifyCallbacks {
$0.onConnectionStateChanged(state: "発見開始エラー: \(error.localizedDescription)")
self.isDiscovering = false
print("📡 Discovery開始エラー (試行 \(retryCount + 1)/\(maxRetries + 1)): \(error.localizedDescription)")

// リトライ可能な場合はリトライ
if retryCount < maxRetries {
// 一度停止してから遅延後にリトライ
discoverer.stopDiscovery()
let delay = Double(retryCount + 1) * 0.5 // 0.5秒、1秒、1.5秒
print("📡 \(delay)秒後にDiscoveryをリトライします...")

try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
self.startDiscoveryInternal(discoverer: discoverer, retryCount: retryCount + 1)
} else {
self.notifyCallbacks {
$0.onConnectionStateChanged(state: "発見開始エラー: \(error.localizedDescription)")
}
}
} else {
self?.isDiscovering = true
self?.notifyCallbacks { $0.onConnectionStateChanged(state: "発見開始成功") }
self?.notifyCallbacks { $0.onDiscoveryStateChanged(isDiscovering: true) }
self.isDiscovering = true
self.notifyCallbacks { $0.onConnectionStateChanged(state: "発見開始成功") }
self.notifyCallbacks { $0.onDiscoveryStateChanged(isDiscovering: true) }
}
}
}
Expand Down
Loading
Loading