崙?zhàn):Track2/3軌道數(shù)據(jù)解析與串口通信源碼詳解)
簡(jiǎn)介23軌道磁卡讀寫程序含源碼是一套面向.NET、VB、VC開(kāi)發(fā)者的磁卡讀寫學(xué)習(xí)與二次開(kāi)發(fā)資料聚焦23位編碼軌道的磁卡數(shù)據(jù)讀取和寫入適用于金融交易、交通系統(tǒng)、門禁控制等需要操作磁卡硬件的場(chǎng)景。壓縮包共113個(gè)文件大小10.28MB涵蓋exe可執(zhí)行程序、dll動(dòng)態(tài)庫(kù)、h頭文件、cpp/cs/VB等源碼文件以及sln/csproj/vbp等工程配置和doc/txt說(shuō)明文檔目錄結(jié)構(gòu)清晰便于對(duì)照學(xué)習(xí)。已有310人學(xué)習(xí)資料通過(guò)VB、VC、.NET多語(yǔ)言示例將磁卡讀寫過(guò)程代碼級(jí)拆解并提供了可供復(fù)用的動(dòng)態(tài)庫(kù)讓開(kāi)發(fā)者能夠快速理解軌道數(shù)據(jù)格式、讀寫流程、錯(cuò)誤處理機(jī)制以及底層硬件交互邏輯。對(duì)于需要深入掌握磁卡讀寫技術(shù)或進(jìn)行行業(yè)項(xiàng)目二次開(kāi)發(fā)的人員來(lái)說(shuō)是一份兼具教學(xué)性與實(shí)用性的寶貴源碼包。 最近在給公司的會(huì)員系統(tǒng)做數(shù)據(jù)遷移工具核心需求是把老卡上的磁道數(shù)據(jù)完整讀出來(lái)再按新系統(tǒng)的規(guī)則重新寫卡。一開(kāi)始我覺(jué)得這活兒應(yīng)該不算難直到真正拿到項(xiàng)目需求才發(fā)現(xiàn)光是搞清楚Track2、Track3也就是標(biāo)題里說(shuō)的23軌道的編碼格式、設(shè)備上報(bào)協(xié)議和校驗(yàn)算法就足夠?qū)懸黄L(zhǎng)文了。今天把這套軌道磁卡讀寫程序的設(shè)計(jì)思路和源碼實(shí)現(xiàn)整理出來(lái)給后面要做同類工具的同學(xué)做個(gè)參考。這個(gè)方案適合哪些人看如果你正在做會(huì)員卡、門禁卡、考勤卡、公交卡相關(guān)的讀寫程序或者手上有一臺(tái)串口磁卡讀寫器不知道怎么調(diào)通這篇文章可以直接幫你把底層邏輯串起來(lái)。我會(huì)先用一張表講清楚三個(gè)磁道的差異再聊程序架構(gòu)和串口協(xié)議最后給出可用的源碼片段和調(diào)試經(jīng)驗(yàn)。1. 先搞清楚23軌道背后的磁卡三軌結(jié)構(gòu)1.1 三個(gè)軌道的規(guī)格差異磁卡上的磁條一般劃分成三個(gè)獨(dú)立軌道每一條軌道都有自己的編碼規(guī)則、字符集和數(shù)據(jù)容量。很多開(kāi)發(fā)者在拿到項(xiàng)目時(shí)只看設(shè)備手冊(cè)里的指令格式卻不了解磁道本身的定義導(dǎo)致解析數(shù)據(jù)時(shí)一頭霧水。我把三個(gè)軌道的核心參數(shù)整理成了表格方便對(duì)照軌道標(biāo)準(zhǔn)/別名起始符結(jié)束符字符類型最大容量編碼方式常見(jiàn)用途Track1IATA%?字母數(shù)字部分符號(hào)79字符7位/字符含1位奇偶校驗(yàn)航司會(huì)員、部分國(guó)際會(huì)員卡Track2ABA;?純數(shù)字40字符5位/字符含1位奇偶校驗(yàn)銀行卡、會(huì)員卡、門禁卡Track3ISO/Thrift;?純數(shù)字107字符5位/字符含1位奇偶校驗(yàn)金融交易輔助數(shù)據(jù)、預(yù)付費(fèi)卡Track1 因?yàn)槟艽孀帜赋S糜谛枰涗浶彰?、卡?hào)、有效期的場(chǎng)景。Track2 和 Track3 都是純數(shù)字軌道Track2 記錄的是核心賬號(hào)和校驗(yàn)信息Track3 的容量大得多但實(shí)際啟用率反而不如 Track2 高。開(kāi)發(fā)時(shí)如果設(shè)備手冊(cè)里寫“讀23軌道”指的就是同時(shí)讀取 Track2 和 Track3 這兩條數(shù)字軌道。1.2 為什么很多業(yè)務(wù)場(chǎng)景只用Track2和Track3我在做這個(gè)項(xiàng)目時(shí)同樣只需要 Track2 和 Track3因?yàn)槔蠒?huì)員系統(tǒng)在發(fā)卡時(shí)只往這兩條軌道寫了數(shù)據(jù)。這種情況在行業(yè)中非常普遍國(guó)內(nèi)不少會(huì)員卡、儲(chǔ)值卡、停車場(chǎng)卡發(fā)卡時(shí)只寫 Track2連 Track3 都留空。銀行體系雖然標(biāo)準(zhǔn)上允許三軌都使用但很多實(shí)際業(yè)務(wù)只依賴 Track2 的賬號(hào)主信息和 Track3 的輔助數(shù)據(jù)區(qū)域。從開(kāi)發(fā)角度看選擇 Track2、Track3 還有一個(gè)現(xiàn)實(shí)原因它們的編碼方式一致都是 5位編碼4位數(shù)據(jù) 1位奇偶校驗(yàn)解析邏輯可以復(fù)用同一套代碼。Track1 則是 7位編碼字符集也完全不同需要單獨(dú)寫一套解碼器。如果項(xiàng)目只涉及數(shù)字類卡片優(yōu)先實(shí)現(xiàn) Track2/3 的讀寫能覆蓋絕大多數(shù)場(chǎng)景這也是“23軌道讀寫程序”這個(gè)命名在業(yè)界的通用含義。2. 讀寫程序的整體設(shè)計(jì)與硬件選型2.1 讀卡器選型與通信參數(shù)磁卡讀寫程序的核心依賴是讀卡器硬件。市面上常見(jiàn)的串口磁卡讀寫器內(nèi)部其實(shí)已經(jīng)完成了很多底層工作磁頭讀取磁條信號(hào)、放大整形、F/2F解碼、奇偶校驗(yàn)過(guò)濾最后把軌道數(shù)據(jù)以ASCII字符或十六進(jìn)制幀的形式從串口輸出。這就意味著我們不需要自己寫信號(hào)解碼算法重點(diǎn)放在串口通信和數(shù)據(jù)幀解析上。我這次用的是常見(jiàn)的USB轉(zhuǎn)串口磁卡讀寫器驅(qū)動(dòng)識(shí)別為虛擬COM口通信參數(shù)是 9600 波特率、8位數(shù)據(jù)位、無(wú)校驗(yàn)、1位停止位簡(jiǎn)寫 9600 8N1。需要提醒的是不同廠商的設(shè)備默認(rèn)波特率可能不同有的出廠是9600有的是38400最好先看設(shè)備手冊(cè)確認(rèn)否則刷卡后程序收到的全是亂碼。Python的串口操作我用的是 pyserial它跨平臺(tái)支持好代碼量也少。如果是做嵌入式方向可以參考同樣的協(xié)議邏輯用STM32的USART實(shí)現(xiàn)主框架基本是一致的。2.2 程序模塊怎么劃分一個(gè)能正常交付的磁卡讀寫程序至少應(yīng)該拆成四個(gè)層次串口通信層負(fù)責(zé)打開(kāi)、關(guān)閉串口配置波特率等參數(shù)提供讀寫字節(jié)的接口。協(xié)議解析層識(shí)別設(shè)備上報(bào)的幀格式提取出每一軌道的原始數(shù)據(jù)這一步需要嚴(yán)格按照設(shè)備手冊(cè)的報(bào)文結(jié)構(gòu)來(lái)解析。磁道數(shù)據(jù)解析層處理軌道里的起始符、結(jié)束符、LRC校驗(yàn)字符返回干凈的卡號(hào)等業(yè)務(wù)數(shù)據(jù)。業(yè)務(wù)應(yīng)用層根據(jù)項(xiàng)目需求做數(shù)據(jù)落庫(kù)、界面展示、寫卡邏輯或者對(duì)接第三方系統(tǒng)。我見(jiàn)過(guò)不少新手把全部邏輯寫在一個(gè)腳本里串口讀取、協(xié)議解析、業(yè)務(wù)處理混在一起最后調(diào)試時(shí)根本分不清是哪一層出了問(wèn)題。模塊化劃分看起來(lái)多寫了幾個(gè)函數(shù)但后續(xù)維護(hù)和問(wèn)題定位會(huì)輕松很多特別是項(xiàng)目從一開(kāi)始就打算交付源碼給別人用的話清晰的模塊結(jié)構(gòu)比“功能能跑”重要得多。3. 核心源碼實(shí)現(xiàn)解析3.1 串口數(shù)據(jù)接收刷卡后設(shè)備主動(dòng)上報(bào)我使用的這款讀卡器采用“主動(dòng)上報(bào)”模式設(shè)備上電后處于監(jiān)聽(tīng)狀態(tài)用戶刷一下卡讀卡器把三軌數(shù)據(jù)打包成一幀主動(dòng)通過(guò)串口發(fā)出來(lái)。程序不需要主動(dòng)發(fā)送讀卡指令只需要在串口上等待并接收數(shù)據(jù)即可。這種模式的好處是邏輯簡(jiǎn)單刷卡即觸發(fā)非常適合做數(shù)據(jù)采集場(chǎng)景。串口初始化和數(shù)據(jù)讀取的基礎(chǔ)代碼import serial ser serial.Serial( portCOM3, # 設(shè)備管理器里確認(rèn)實(shí)際串口號(hào) baudrate9600, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout1 ) def read_frame(ser: serial.Serial) - bytes: 讀取一幀完整數(shù)據(jù)按幀頭0x02開(kāi)始幀尾0x03結(jié)束 frame bytearray() while True: chunk ser.read(1) if not chunk: continue if chunk[0] 0x02: frame bytearray() break while True: chunk ser.read(1) if not chunk: continue if chunk[0] 0x03: break frame.append(chunk[0]) return bytes(frame)這里的關(guān)鍵點(diǎn)是刷卡后設(shè)備上報(bào)的幀頭是固定字節(jié)0x02幀尾是0x03中間是整個(gè)數(shù)據(jù)體。read_frame函數(shù)先等待幀頭然后依次接收字節(jié)直到幀尾這樣可以保證拿到的數(shù)據(jù)是完整的一幀不會(huì)因?yàn)榇诮邮諘r(shí)序問(wèn)題導(dǎo)致數(shù)據(jù)錯(cuò)位。3.2 磁道數(shù)據(jù)解析起始符、結(jié)束符、LRC校驗(yàn)?zāi)玫揭粠瑪?shù)據(jù)后還需要按照設(shè)備協(xié)議解析出每個(gè)軌道的原始內(nèi)容。我用的設(shè)備上報(bào)幀格式大致是狀態(tài)碼1字節(jié) Track1長(zhǎng)度1字節(jié) Track1數(shù)據(jù) Track2長(zhǎng)度1字節(jié) Track2數(shù)據(jù) Track3長(zhǎng)度1字節(jié) Track3數(shù)據(jù) LRC校驗(yàn)字節(jié)。LRC縱向冗余校驗(yàn)是一種非常簡(jiǎn)單的校驗(yàn)算法對(duì)數(shù)據(jù)體的每個(gè)字節(jié)做異或最終得到一個(gè)校驗(yàn)字節(jié)。異或校驗(yàn)的好處是計(jì)算量小在C語(yǔ)言和Python里都很好實(shí)現(xiàn)。我封裝了一個(gè)校驗(yàn)函數(shù)def lrc_check(data: bytes) - int: 計(jì)算數(shù)據(jù)的LRC校驗(yàn)值所有字節(jié)依次異或 lrc 0 for byte in data: lrc ^ byte return lrc解析整幀數(shù)據(jù)的核心邏輯def parse_frame(frame: bytes): 解析設(shè)備上報(bào)的軌道數(shù)據(jù)幀 if len(frame) 7: return None status frame[0] if status ! 0x00: print(f設(shè)備上報(bào)錯(cuò)誤狀態(tài)碼: {status:#x}) return None offset 1 tracks {} for track_no in (1, 2, 3): length frame[offset] offset 1 raw frame[offset:offset length] offset length tracks[fTrack{track_no}] raw # 校驗(yàn)幀的LRC # 這里按設(shè)備協(xié)議校驗(yàn)幀頭到LRC前的全部字節(jié) body bytes([0x02]) frame[:-1] calc_lrc lrc_check(body) if calc_lrc ! frame[-1]: print(LRC校驗(yàn)失敗) return None return tracks實(shí)際開(kāi)發(fā)時(shí)需要注意一點(diǎn)不同廠商的協(xié)議在“LRC校驗(yàn)范圍”上有差異有的是從狀態(tài)碼開(kāi)始算有的是從幀頭開(kāi)始算。最穩(wěn)妥的做法是把設(shè)備返回的完整一幀打印成hex自己對(duì)著手冊(cè)算一遍確認(rèn)校驗(yàn)范圍后再寫進(jìn)代碼。3.3 一個(gè)Track2數(shù)據(jù)的完整解碼過(guò)程把原始軌道數(shù)據(jù)從幀里解析出來(lái)后還需要再做一步軌道內(nèi)格式處理。以常見(jiàn)的Track2為例標(biāo)準(zhǔn)數(shù)據(jù)格式是; 開(kāi)始符 卡號(hào)等信息 分隔符 附加數(shù)據(jù) LRC校驗(yàn)符 ? 結(jié)束符舉個(gè)例子如果掃描到的原始數(shù)據(jù)是;6234567890123456180512345678?其中;是軌道開(kāi)始標(biāo)記?是結(jié)束標(biāo)記是主賬號(hào)與附加數(shù)據(jù)之間的分隔符而結(jié)束符前面的一個(gè)字符也就是這里的8是LRC校驗(yàn)字符。解析函數(shù)要做的事情就是把開(kāi)始符、結(jié)束符、尾部校驗(yàn)字符識(shí)別出來(lái)返回中間真正有用的卡號(hào)和數(shù)據(jù)段def decode_track2(raw: bytes): 解析Track2原始數(shù)據(jù)返回卡號(hào)主體 if not raw.startswith(b;) or not raw.endswith(b?): return None body raw[1:-1] # 去掉;和? # 去掉最后一個(gè)LRC校驗(yàn)字符 body body[:-1] if b in body: card_no, extra body.split(b, 1) elif bD in body: card_no, extra body.split(bD, 1) else: card_no, extra body, b return { card_no: card_no.decode(ascii), extra: extra.decode(ascii) }這里有個(gè)很值得注意的細(xì)節(jié)Track2的數(shù)據(jù)里主賬號(hào)和附加信息之間的分隔符常用表示但有些設(shè)備的驅(qū)動(dòng)在底層已經(jīng)做過(guò)字符轉(zhuǎn)換輸出來(lái)的是D。我在調(diào)試過(guò)程中遇到過(guò)同一種卡在A設(shè)備上是, 在B設(shè)備上是D的情況所以解析時(shí)兩側(cè)都判斷一下最保險(xiǎn)。4. 調(diào)試記錄與常見(jiàn)問(wèn)題速查4.1 刷了卡卻沒(méi)數(shù)據(jù)的排查順序這個(gè)現(xiàn)象在項(xiàng)目初期幾乎一定遇到我的排查順序是這樣的先打開(kāi)設(shè)備管理器確認(rèn)虛擬串口號(hào)有些USB轉(zhuǎn)串口設(shè)備每次插拔后串口號(hào)會(huì)變程序?qū)懰懒薈OM3就可能找不到設(shè)備。然后檢查串口參數(shù)波特率不一致是刷卡后完全沒(méi)反應(yīng)或收到亂碼的最常見(jiàn)原因我反復(fù)強(qiáng)調(diào)要先看設(shè)備手冊(cè)確認(rèn)默認(rèn)波特率。再檢查刷卡方向磁卡讀寫器的磁頭位置不同刷卡方向也不同有的要求磁條朝里快速劃過(guò)有的是把卡插進(jìn)去再抽出方向反了讀卡器什么都不會(huì)上報(bào)。最后觀察設(shè)備指示燈如果刷卡瞬間指示燈有反應(yīng)說(shuō)明硬件鏈路是通的問(wèn)題在串口參數(shù)或軟件解析如果指示燈根本沒(méi)反應(yīng)多半是卡的問(wèn)題或刷卡方式不對(duì)。4.2 亂碼與奇偶校驗(yàn)處理的坑亂碼問(wèn)題有兩種典型表現(xiàn)。第一種是接收到的數(shù)據(jù)全是不可讀的十六進(jìn)制字節(jié)這通常是串口參數(shù)不匹配尤其是數(shù)據(jù)位或校驗(yàn)位配置錯(cuò)誤導(dǎo)致字節(jié)錯(cuò)位。第二種是數(shù)據(jù)能讀出來(lái)但軌道里的字符偶爾不對(duì)這可能是磁條臟污或刷卡速度不均勻設(shè)備解碼磁信號(hào)時(shí)產(chǎn)生了位錯(cuò)誤。關(guān)于奇偶校驗(yàn)我使用的讀卡器固件已經(jīng)完成了軌道數(shù)據(jù)的奇偶校驗(yàn)過(guò)濾輸出給我的就是干凈的ASCII字符所以在應(yīng)用層代碼里不需要處理位級(jí)校驗(yàn)。但如果你買的模塊是裸磁頭方案只給原始模擬信號(hào)或位流那就需要自己實(shí)現(xiàn)5選4位解碼、奇偶校驗(yàn)和F/2F解碼工作量會(huì)大一個(gè)層級(jí)。選型時(shí)務(wù)必要確認(rèn)清楚設(shè)備輸出的是“解析后的ASCII數(shù)據(jù)”還是“原始未解碼數(shù)據(jù)”。4.3 常見(jiàn)問(wèn)題速查表現(xiàn)象可能原因處理辦法打開(kāi)串口報(bào)錯(cuò)串口號(hào)錯(cuò)誤、驅(qū)動(dòng)未安裝重新拔插設(shè)備確認(rèn)COM號(hào)安裝CH340或CP210x驅(qū)動(dòng)刷了卡完全沒(méi)數(shù)據(jù)波特率不對(duì)、刷卡方向錯(cuò)誤、磁條損壞核對(duì)波特率按設(shè)備圖示方向刷換一張已知正常的卡測(cè)試收到數(shù)據(jù)是亂碼數(shù)據(jù)位/校驗(yàn)位設(shè)置錯(cuò)了確認(rèn)8N1配置對(duì)照設(shè)備手冊(cè)核對(duì)串口參數(shù)LRC校驗(yàn)總是失敗校驗(yàn)范圍搞錯(cuò)了、把幀頭也算進(jìn)去了打印完整hex按手冊(cè)逐字節(jié)核對(duì)校驗(yàn)范圍Track3讀取為空這張卡本身就沒(méi)有寫Track3用全軌道測(cè)試卡驗(yàn)證讀寫器能力數(shù)據(jù)里偶爾亂一個(gè)字符磁條臟、刷卡速度不均勻清潔磁條勻速刷卡再試4.4 調(diào)試時(shí)最有用的一個(gè)竅門這個(gè)項(xiàng)目做下來(lái)我覺(jué)得最值得分享的調(diào)試經(jīng)驗(yàn)就是第一時(shí)間把串口收到的原始數(shù)據(jù)用hex和ASCII雙格式打印出來(lái)。不要直接去看解析后的業(yè)務(wù)數(shù)據(jù)因?yàn)橐坏┙馕鲦溌防锬硞€(gè)假設(shè)是錯(cuò)的你看到的卡號(hào)可能永遠(yuǎn)是錯(cuò)的但你找不到錯(cuò)在哪一層。先打印原始幀和手冊(cè)上的報(bào)文結(jié)構(gòu)做對(duì)照確認(rèn)設(shè)備上報(bào)格式正確再驗(yàn)證LRC算法、軌道數(shù)據(jù)解析問(wèn)題就能一步步縮小到具體的函數(shù)里。我在調(diào)試Track2分隔符問(wèn)題時(shí)就是這么定位的程序解析出的卡號(hào)總是多出兩位打印ASCII原始數(shù)據(jù)后發(fā)現(xiàn)設(shè)備上報(bào)的軌道數(shù)據(jù)里分隔符是D而不是導(dǎo)致split邏輯沒(méi)生效卡號(hào)把附加信息也吞進(jìn)去了。如果一開(kāi)始就盯著業(yè)務(wù)界面看恐怕還得排查很久。最后說(shuō)說(shuō)我個(gè)人對(duì)這套方案的使用感受磁卡讀寫這個(gè)方向看起來(lái)不算新潮但實(shí)際做一遍下來(lái)會(huì)發(fā)現(xiàn)很多細(xì)節(jié)都是文檔里不會(huì)明說(shuō)的不同設(shè)備的幀格式五花八門、LRC校驗(yàn)范圍各有差異、軌道分隔符在不同驅(qū)動(dòng)下輸出不一致。把這些問(wèn)題一個(gè)個(gè)踩過(guò)去之后再做類似的身份識(shí)別卡、會(huì)員卡對(duì)接項(xiàng)目就會(huì)順暢很多。開(kāi)發(fā)時(shí)我強(qiáng)烈建議準(zhǔn)備幾張自己寫的全軌道空白測(cè)試卡把Track1、Track2、Track3都寫滿已知數(shù)據(jù)這樣調(diào)試解析程序時(shí)就能拿著一份“標(biāo)準(zhǔn)答案”逐字節(jié)比對(duì)比拿一張未知數(shù)據(jù)內(nèi)容的舊卡盲猜高效太多。本文還有配套的精品資源點(diǎn)擊獲取