Zenn
🐔

iOSでType-VのNFCへwriteNDEFで書き込む前にフォーマットが必要

2025/03/15に公開

Type-VのNFCタグにNDEFを書き込もうとwriteNDEFを利用したらエラーが出た。

どうやらType-V(iso15693)の場合、書き込む前にCC(Capability Container)領域の初期化が必要で、このあたりはCoreNFCはやってくれないらしいので、フォーマット部分を自前した。

探してもあまり正式な情報が見つからず、他のNFC書き込みアプリで書き込んだメモリ結果を読み取りながらギリギリ合わせたので、対応しないタグなどがあるかも。

フォーマット処理の全体像

formatISO15693TagToNDEF関数でタグからシステム情報を取得し、CCブロックの構築とNDEFデータ領域の確保を行う。

func formatISO15693TagToNDEF(tag: NFCISO15693Tag, session: NFCTagReaderSession) async throws {
    let info = try await getSystemInfo(tag: tag)
    
    let blockSize = info.blockSize
    let ndefMagicNumber: [UInt8] = [0xE1, 0x40] // NFCフォーラム Type-V タグを示す
    let versionNumber: UInt8 = 0x01
    let ccBlock: [UInt8] = ndefMagicNumber + [0x30, versionNumber]
    
    let ndefTagFlag: UInt8 = 0x03  // NDEFメッセージTLVを示す
    let length: [UInt8] = encodeTLVLength(info.blockSize)
    let startTLV: [UInt8] = [ndefTagFlag] + length
    
    let header: [UInt8] = ccBlock + startTLV
    // メッセージを書き込めるように埋める
    var paddedMessage: [UInt8] = header
    let remainder = header.count % blockSize
    if remainder != 0 {
        paddedMessage += Array(repeating: 0, count: blockSize - remainder)
    }
    let blocks = splitMessageIntoBlocks(message: paddedMessage)
    try await writeBlocksSequentially(tag: tag, blocks: blocks)
}

formatISO15693TagToNDEF関数

処理の概要

NDEFメッセージ書き込みの前処理としてType-V NFCタグを初期化する関数。処理は以下の5ステップで構成される。

  1. タグからシステム情報を取得
let info = try await getSystemInfo(tag: tag)

ブロックサイズなど以降の処理に必要な情報がここに格納される。

  1. CCブロック(Capability Container)の構築
let blockSize = info.blockSize
let ndefMagicNumber: [UInt8] = [0xE1, 0x40] // NFCフォーラム Type-V タグを示す
let versionNumber: UInt8 = 0x01
let ccBlock: [UInt8] = ndefMagicNumber + [0x30, versionNumber]

CCブロックの各バイトの役割:

  • [0xE1, 0x40]: Type-Vタグのマジックナンバー
  • 0x30: 読み書き可能を示すアクセス権限バイト
  • 0x01: NDEFマッピングバージョン
  1. TLV(Type-Length-Value)構造の構築
let ndefTagFlag: UInt8 = 0x03  // NDEFメッセージTLVを示す
let length: [UInt8] = encodeTLVLength(info.blockSize)
let startTLV: [UInt8] = [ndefTagFlag] + length
  • Type: 0x03(NDEFメッセージ識別子)
  • Length: タグ容量から計算
  • Value: 後続のNDEFメッセージ格納領域
  1. ブロックサイズ調整のためのパディング処理
let header: [UInt8] = ccBlock + startTLV
var paddedMessage: [UInt8] = header
let remainder = header.count % blockSize
if remainder != 0 {
    paddedMessage += Array(repeating: 0, count: blockSize - remainder)
}

Type-Vタグはブロック単位での書き込みが必須のため、データをブロックサイズに調整する。

  1. データの分割と書き込み
let blocks = splitMessageIntoBlocks(message: paddedMessage)
try await writeBlocksSequentially(tag: tag, blocks: blocks)

調整済みデータをブロックサイズで分割し、タグに順次書き込む。writeBlocksSequentiallyについては後述
これによりwriteNDEFが利用可能な状態となる。

各関数の実装

システム情報の取得

コールバック地獄は避けたかったので、withCheckedThrowingContinuationで包んでasync/awaitにした。

func getSystemInfo(tag: NFCISO15693Tag) async throws -> NFCISO15693SystemInfo {
    try await withCheckedThrowingContinuation { continuation in
        tag.getSystemInfo(requestFlags: [.highDataRate]) { result in
            switch result {
            case .success(let systemInfo):
                continuation.resume(returning: systemInfo)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

メッセージのブロック分割

タグに書き込むデータはブロックサイズに合わせて分割する必要があるのでその処理

private func splitMessageIntoBlocks(message: [UInt8], blockSize: Int = 4) -> [Data] {
    var blocks: [Data] = []
    for i in stride(from: 0, to: message.count, by: blockSize) {
        let block = Array(message[i..<i + blockSize])
        blocks.append(Data(block))
    }
    return blocks
}

ブロックの書き込み

分割したブロックを順番にタグに書き込んでいく。
tag.writeSingleBlockで直接書き込みの力業が必要

private func writeBlocksSequentially(tag: NFCISO15693Tag, blocks: [Data]) async throws {
    for (index, block) in blocks.enumerated() {
        let blockNumber = UInt8(index)
        try await tag.writeSingleBlock(
            requestFlags: [.highDataRate], blockNumber: blockNumber, dataBlock: block)
    }
}

TLV長のエンコード処理

255バイト以上だと3バイト構造に変える必要があったので、その部分を実装

func encodeTLVLength(_ length: Int) -> [UInt8] {
    if length < 255 {
        // 1バイト形式: 0x00 〜 0xFE
        return [UInt8(length)]
    } else {
        // 3バイト形式: 0xFF + 2バイト(ビッグエンディアン)
        let highByte = UInt8((length >> 8) & 0xFF)  // 上位バイト
        let lowByte = UInt8(length & 0xFF)  // 下位バイト
        return [0xFF, highByte, lowByte]
    }
}

使用例

NFCセッションを作って実際に書き込む

class NFCWriter: NSObject, NFCTagReaderSessionDelegate {
  // ...
    func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
        guard let firstTag = tags.first else { return }
        
        switch firstTag {
          // ...
        case .iso15693(let tag):
            Task {
                do {
                    try await session.connect(to: firstTag)
                    try await formatISO15693TagToNDEF(tag: tag, session: session)
                    
                    let payload = NFCNDEFPayload.wellKnownTypeTextPayload(
                        string: "Hello",
                        locale: Locale(identifier: "en")
                    )!
                    let message = NFCNDEFMessage(records: [payload])
                    
                    let status = try await tag.queryNDEFStatus()
                    try await tag.writeNDEF(message)
                    
                    self.message = "「Hello」を書き込みました"
                    session.invalidate()
                } catch {
                    self.message = "エラー: \(error.localizedDescription)"
                    session.invalidate()
                }
            }
        }
    }
}

writeNDEFの前にformatISO15693TagToNDEFを呼び出すのがキモ。これでNDEFメッセージが書き込める。

GitHubで編集を提案

Discussion

ログインするとコメントできます