Improve NFC history readers and prepare production build

This commit is contained in:
2026-05-08 05:40:52 +07:00
parent 1dc293c697
commit bd34467ddc
14 changed files with 688 additions and 182 deletions

View File

@ -21,6 +21,7 @@ extension Dictionary where Key == String, Value == Any {
@available(iOS 13.0, *)
public class UnifiedNfcApi {
var stationMap: [Int: Station] = [:]
var felicaHistoryRetryCount = 0
func parseData() {
stationMap = [
@ -87,7 +88,7 @@ public class UnifiedNfcApi {
var apduRunner = ApduRunner()
public init() {
langCode = Locale.current.languageCode
langCode = Locale.current.languageCode ?? "en"
parseData()
}
@ -124,9 +125,16 @@ public class UnifiedNfcApi {
public func checkFelicaCard(tag: NFCFeliCaTag){
readFelicaCard(tag: tag)
}
private func logFelica(_ message: String) {
_ = message
// print("[FeliCa] \(message)")
// debugLog("[FeliCa] \(message)")
}
func readFelicaCard(tag: NFCFeliCaTag){
let kmt = Emoney()
logFelica("Start read | currentIDm=\(tag.currentIDm.map { String(format: "%02X", $0) }.joined()) | currentSystemCode=\(tag.currentSystemCode.map { String(format: "%02X", $0) }.joined())")
let serviceCode = Data([0x0B, 0x30])
let blockList = Data([0x80, 0x00])
@ -137,21 +145,27 @@ public class UnifiedNfcApi {
) { (status1, status2, blockData, error) in
if let error = error {
self.logFelica("Card info read failed | error=\(error.localizedDescription)")
self.apduRunner.nfcApi?.stopCheckCard(message: "Gagal membaca: \(error.localizedDescription)")
return
}
// Cek status keberhasilan dari kartu (0x00 0x00 berarti sukses)
guard status1 == 0x00 && status2 == 0x00 else {
self.logFelica("Card info read failed | status1=\(status1) | status2=\(status2)")
self.apduRunner.nfcApi?.stopCheckCard(message: "Error Status: \(status1) \(status2)")
return
}
for (index, data) in blockData.enumerated() {
debugLog("Data Blok \(index): \(data.map { String(format: "%02X", $0) }.joined())")
let hex = data.map { String(format: "%02X", $0) }.joined()
self.logFelica("Card info block \(index) raw=\(hex)")
if let cardNumberString = String(data: data, encoding: .utf8) {
self.logFelica("Card number parsed=\(cardNumberString)")
kmt.setCardLabel("KMT")
kmt.setCardNumber(cardNumberString)
self.readFelicaBalance(tag: tag, kmt: kmt)
} else {
self.logFelica("Card number parse failed for block \(index)")
}
}
}
@ -166,12 +180,18 @@ public class UnifiedNfcApi {
blockList: [blockList]
) { (status1, status2, blockData, error) in
if let error = error {
self.logFelica("Balance read failed | error=\(error.localizedDescription)")
self.apduRunner.nfcApi?.stopCheckCard(message: "Gagal membaca: \(error.localizedDescription)")
return
}
if status1 == 0x00 && status2 == 0x00 {
let cardBalance = [UInt8](blockData[0])
guard let firstBlock = blockData.first, firstBlock.count >= 4 else {
self.logFelica("Balance block missing or too short")
return
}
self.logFelica("Balance raw=\(firstBlock.map { String(format: "%02X", $0) }.joined())")
let cardBalance = [UInt8](firstBlock)
var y: Int = 0
for x in 0..<4 {
y += Int(cardBalance[x]) << (x * 8)
@ -180,12 +200,14 @@ public class UnifiedNfcApi {
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "id_ID")
formatter.numberStyle = .decimal
debugLog("balance")
kmt.setBalance(y)
if let balance = formatter.string(from: NSNumber(value: y)) {
debugLog("Saldo: \(balance)") // Hasil contoh: "67.305.985"
self.logFelica("Balance parsed=\(y) | formatted=\(balance)")
}
self.felicaHistoryRetryCount = 0
self.readFelicaCardHistory(tag: tag, kmt: kmt)
} else {
self.logFelica("Balance read failed | status1=\(status1) | status2=\(status2)")
// kmt.setTampilRiwayat(false)
// self.apduRunner.callback?.complete(emoney: kmt)
// self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
@ -210,34 +232,38 @@ public class UnifiedNfcApi {
) { (status1, status2, blockData, error) in
var riwayatList: [RiwayatCard] = []
if let error = error {
debugLog("Error: \(error.localizedDescription)")
self.logFelica("History read failed | error=\(error.localizedDescription)")
self.retryFelicaHistoryRead(tag: tag, kmt: kmt, reason: "error \(error.localizedDescription)")
return
}
if status1 == 0x00 && status2 == 0x00 {
debugLog("Berhasil membaca 15 blok!")
self.logFelica("History read success | blocks=\(blockData.count)")
// blockData akan berisi array of Data, masing-masing 16 byte
for (index, data) in blockData.enumerated() {
let riwayat = RiwayatCard()
var normal = true
let rawHex = data.map { String(format: "%02X", $0) }.joined()
self.logFelica("History block \(index) raw=\(rawHex)")
guard data.count >= 12 else {
self.logFelica("History block \(index) skipped | reason=too_short | count=\(data.count)")
continue
}
let subId = data.subdata(in: 8..<10)
let uid = self.convert(bytes: [UInt8](subId))
debugLog("station: \(uid)")
debugLog("Blok \(index): \(data.map { String(format: "%02X", $0) }.joined())")
self.logFelica("History block \(index) stationId=\(uid)")
if (uid == 0){
normal = false
}
if data.count >= 13 {
let transactionKind = self.felicaTransactionKind(for: [UInt8](data))
let transactionKind = self.felicaTransactionKind(for: [UInt8](data), stationId: uid)
riwayat.setProsesTipe(transactionKind.prosesTipe)
riwayat.setTitle(transactionKind.title.localizeString(string: self.langCode!))
debugLog("signature: \(transactionKind.signature)")
debugLog(transactionKind.logLabel)
self.logFelica("History block \(index) signature=\(transactionKind.signature) | kind=\(transactionKind.logLabel)")
}
if let station = self.stationMap[uid]{
debugLog("station", station.name)
self.logFelica("History block \(index) stationName=\(station.name)")
riwayat.setLocationName(station.name.uppercased(with: .autoupdatingCurrent))
}
// let station = self.stationMap[uid]
@ -247,14 +273,17 @@ public class UnifiedNfcApi {
let subData = data.subdata(in: 0..<4)
let date = self.getDate(data: [UInt8](subData))
riwayat.setTransactionTime(date!)
if let date = self.getDate(data: [UInt8](subData)) {
riwayat.setTransactionTime(date)
}
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "id_ID") // Format Indonesia
formatter.dateFormat = "dd MMMM yyyy, HH:mm"
let dateString = formatter.string(from: date!)
debugLog("Hasil Konversi: \(dateString)")
if let date = riwayat.getTransationTime() {
let dateString = formatter.string(from: date)
self.logFelica("History block \(index) timestamp=\(dateString)")
}
let amn = data.subdata(in: 4..<8)
let amount = amn.withUnsafeBytes { $0.load(as: Int32.self).bigEndian }
@ -283,11 +312,10 @@ public class UnifiedNfcApi {
nformatter.locale = Locale(identifier: "id_ID")
nformatter.numberStyle = .decimal
if let balance = nformatter.string(from: NSNumber(value: amount)) {
debugLog("amount: \(balance)") // Hasil contoh: "67.305.985"
self.logFelica("History block \(index) amount=\(amount) | formatted=\(balance)")
}
debugLog("")
} else {
debugLog("RESKA PARKIR")
self.logFelica("History block \(index) mode=RESKA_PARKIR")
let stringData = data.map { String(format: "%02X", $0) }.joined()
@ -304,9 +332,9 @@ public class UnifiedNfcApi {
let result = outputFormatter.string(from: date)
riwayat.setTransactionTime(date)
debugLog(result) // Hasil: 29-01-2026 16:00:44
self.logFelica("History block \(index) timestamp=\(result)")
} else {
debugLog("Format string tidak cocok")
self.logFelica("History block \(index) timestamp_parse_failed")
}
let amn = data.subdata(in: 8..<12)
let amount = amn.withUnsafeBytes { $0.load(as: Int32.self).bigEndian }
@ -318,26 +346,42 @@ public class UnifiedNfcApi {
nformatter.locale = Locale(identifier: "id_ID")
nformatter.numberStyle = .decimal
if let balance = nformatter.string(from: NSNumber(value: amount)) {
debugLog("amount: \(balance)") // Hasil contoh: "67.305.985"
self.logFelica("History block \(index) amount=\(amount) | formatted=\(balance)")
}
}
debugLog("")
let title = riwayat.getTitle() ?? "-"
let location = riwayat.getLocationName() ?? "-"
self.logFelica("History block \(index) summary | title=\(title) | prosesTipe=\(riwayat.getProsesTipe()) | location=\(location)")
riwayatList.append(riwayat)
}
kmt.setRiwayatList(riwayatList)
kmt.setTampilRiwayat(true)
self.logFelica("History parsed total=\(riwayatList.count)")
self.apduRunner.callback?.complete(emoney: kmt)
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
} else {
debugLog("Gagal. Status: \(status1), \(status2)")
self.logFelica("History read failed | status1=\(status1) | status2=\(status2)")
self.retryFelicaHistoryRead(tag: tag, kmt: kmt, reason: "status \(status1)-\(status2)")
}
}
}
private func retryFelicaHistoryRead(tag: NFCFeliCaTag, kmt: Emoney, reason: String) {
if felicaHistoryRetryCount < 1 {
felicaHistoryRetryCount += 1
logFelica("Retrying history read | reason=\(reason)")
readFelicaCardHistory(tag: tag, kmt: kmt)
} else {
apduRunner.callback?.complete(emoney: kmt)
apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
apduRunner.invalidateSession()
}
}
func getDate(data: [UInt8]) -> Date? {
// 1. Tentukan TimeZone Jakarta
let timeZone = TimeZone(identifier: "Asia/Jakarta")!
let timeZone = TimeZone(identifier: "Asia/Jakarta") ?? .current
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
@ -364,7 +408,7 @@ public class UnifiedNfcApi {
func convert(bytes: [UInt8]) -> Int {
switch bytes.count {
case 0:
fatalError("Data kosong")
return 0
case 1:
return Int(bytes[0])
case 2:
@ -379,12 +423,17 @@ public class UnifiedNfcApi {
}
}
private func felicaTransactionKind(for bytes: [UInt8]) -> (prosesTipe: Int, title: String, logLabel: String, signature: String) {
private func felicaTransactionKind(for bytes: [UInt8], stationId: Int) -> (prosesTipe: Int, title: String, logLabel: String, signature: String) {
let signatureBytes = Array(bytes[10...12])
let signature = signatureBytes.map { String(format: "%02X", $0) }.joined(separator: " ")
if stationId == 0 {
return (1, "payment", "Parkir", signature)
}
switch signatureBytes {
case [0x00, 0x02, 0x00],
[0x02, 0x01, 0x00],
[0x03, 0x01, 0x00]:
return (0, "topup", "Topup", signature)
case [0x01, 0x01, 0x01],
@ -392,14 +441,12 @@ public class UnifiedNfcApi {
[0x03, 0x61, 0x01]:
return (1, "payment", "Pembayaran", signature)
default:
let type = bytes[10]
switch type {
let terminalMarker = bytes[12]
switch terminalMarker {
case 0x00:
return (0, "topup", "Topup (fallback)", signature)
return (0, "topup", "Topup (fallback sig[2])", signature)
case 0x01:
return (1, "payment", "Pembayaran (fallback)", signature)
case 0x03:
return (1, "payment", "Pembayaran (fallback 0x03)", signature)
return (1, "payment", "Pembayaran (fallback sig[2])", signature)
default:
return (1, "payment", "Other", signature)
}