"面向?qū)ο笫?quot;開發(fā)串口協(xié)議的SDK

背景

用戶在訪問我們的網(wǎng)站時(shí)斋日,可以選擇使用線下POS機(jī)進(jìn)行支付,因此我們需要集成并控制POS機(jī)完成刷卡操作和返回?cái)?shù)據(jù)。
然而噪窘,POS機(jī)提供方郵件發(fā)送過來的并不是我們預(yù)想的Http接口或是SDK吏廉,而是150多頁的一份串口集成文檔...

(文中涉及的代碼泞遗、編號(hào)、枚舉值都已經(jīng)過模糊處理)

令人頭暈的二進(jìn)制

不同于我們?nèi)粘K褂玫腍TTP協(xié)議:具有標(biāo)準(zhǔn)結(jié)構(gòu)和完備的SDK席覆;可以很容易的構(gòu)建起Server-Client進(jìn)行數(shù)據(jù)傳輸史辙;無需關(guān)注應(yīng)用層(ISO七層)以下的實(shí)現(xiàn)。而串口更像是物理層佩伤,通過指定頻率的高低電平(0/1)來傳輸數(shù)據(jù)聊倔。

因此在使用串口通信時(shí),通常需要設(shè)計(jì)一套自有協(xié)議表達(dá)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)生巡。例如:

image-20211127144504102.png
  • 主要部分為消息頭(Header)和消息數(shù)據(jù)(Data)耙蔑,消息數(shù)據(jù)可以有多個(gè)
  • 通過Field Code可區(qū)別不同的數(shù)據(jù),是一個(gè)變長的數(shù)據(jù)
  • 數(shù)據(jù)的主要類型為Hex(十六進(jìn)制)孤荣、BCD(二進(jìn)制化整數(shù))甸陌、ASC(asiic碼)

對(duì)照著上述結(jié)構(gòu)來構(gòu)造一個(gè)消息并不是一件困難的事,然而不同的類型的功能指令(Function)會(huì)包含大量不同的消息數(shù)據(jù)(Field Data)盐股。如果我們面向一個(gè)一個(gè)功能指令開發(fā)钱豁,效率低且代碼難以維護(hù) —— 從我們拿到的一份Legacy的代碼中,也能看出這點(diǎn):

code-demo1.png

在一個(gè)類中疯汁,既有對(duì)業(yè)務(wù)字段的賦值牲尺,又有對(duì)底層數(shù)據(jù)格式的序列化。再加上大量的魔法字符,讓我抄都不知道從何抄起...

于是在“修改這份代碼使其適應(yīng)新版本協(xié)議”“重寫谤碳,重寫溃卡,重寫!”中蜒简,我毅然選擇了后者...

“封裝瘸羡,他使用了封裝!”

那如何開發(fā)一個(gè)適配底層協(xié)議的SDK呢臭蚁?

遇事不決最铁,量子力學(xué)(不是) / 遇事不決,面向?qū)ο?/strong>(還行)

面向?qū)ο蟮囊淮髢?yōu)勢(shì)是利用封裝垮兑,使邏輯高內(nèi)聚低耦合冷尉。

首先,我將三個(gè)字段類型進(jìn)行了封裝:BCD系枪、ASC雀哨、Hex,使其實(shí)現(xiàn)Attribute接口以實(shí)現(xiàn)toBytes()方法私爷。此時(shí)業(yè)務(wù)所會(huì)用到的數(shù)據(jù)類型都和Bytes沒有了直接關(guān)系雾棺,BCD、ASC衬浑、Hex成為了實(shí)質(zhì)上的的基本類型捌浩。

同理,Token工秩、分隔符尸饺、長度這些和功能指令(業(yè)務(wù)側(cè))沒有直接關(guān)聯(lián)的數(shù)據(jù)類型也被我抽取了出來。

此時(shí)的Message和0101已完全解耦助币,變成了只含三個(gè)字段類型的POJO類浪听。

image-20211127150158827.png

一層一層又一層

但是直接使用Message時(shí)還是有一些困難

  • ASC對(duì)于上層業(yè)務(wù)指令來說還是太細(xì)節(jié),而且無法很好的表達(dá)業(yè)務(wù)含義
    • 對(duì)于指令功能(Function)眉菱,他們不關(guān)心下層如何序列化我的數(shù)據(jù)迹栓,只關(guān)心業(yè)務(wù)數(shù)據(jù)是否正確的被設(shè)置和接收
    • 對(duì)于消息數(shù)據(jù)(Message & Message Data),他們不關(guān)心上層數(shù)據(jù)有什么業(yè)務(wù)含義俭缓,也不關(guān)心下層這些數(shù)據(jù)如何被發(fā)送

多重施法克伊! —— 就像Attribute一樣,使用Field接口將上層的業(yè)務(wù)字段進(jìn)行了隔離华坦。此后愿吹,當(dāng)你想要發(fā)起一個(gè)指令,你只需要用富含業(yè)務(wù)信息的Field組建你的數(shù)據(jù)季春,接口會(huì)幫你完成剩下所有的事洗搂。

val request = MakePaymentRequest(ID = "000001", amount = 200.00)
val response = client.send(request)
val isSuccess = response.responseCode == ResponseCode.APPROVED
image-20211127160249771.png

當(dāng)然還得加上串口連接的部分消返,里面會(huì)使用Blocking隊(duì)列來將異步操作同步化

各個(gè)組件通過接口Request/Response, Message, Field, Attribute進(jìn)行協(xié)作载弄,大大增加了靈活性和可擴(kuò)展性

也使用了注解耘拇、反射來統(tǒng)一實(shí)現(xiàn)對(duì)象的序列化操作

image-20211127160401744.png

測試...

Of cause,為了避免破壞已經(jīng)構(gòu)建好的功能宇攻,測試也是開發(fā)過程中需要慎重對(duì)待的環(huán)節(jié)(前面錯(cuò)一個(gè)bit惫叛,后面讀出來的消息能跟鬼畫符一樣...)。對(duì)于協(xié)議(protocol)層來說逞刷,TDD用起來是非常爽的且高效的嘉涌,但是到了數(shù)據(jù)傳輸部分就難搞起來了。

  • 串口的讀寫操作是異步的夸浅,讀操作是通過注冊(cè)監(jiān)聽器實(shí)現(xiàn)的
  • 因?yàn)轭愃崎L鏈接仑最,在傳輸過程中遇到錯(cuò)誤會(huì)發(fā)送ACK/NACK的握手信息,并且會(huì)觸發(fā)重試

Option 1:構(gòu)造多線程測試環(huán)境

  • 創(chuàng)建Stub Server:使用了PipedInputStream,PipedOutputStream將Client的讀寫流給包裝起來帆喇,通過另一個(gè)線程來模擬Server操作里面的數(shù)據(jù)警医,實(shí)現(xiàn)接收請(qǐng)求、返回?cái)?shù)據(jù)坯钦。

    val serverInputStream = PipedInputStream()
    val serverOutputStream = PipedOutputStream()
    val clientInputStream = PipedInputStream(serverOutputStream)  // server output -> client input
    val clientOutputStream = PipedOutputStream(serverInputStream) // client output -> server input
    val connection = StreamSerialChannel(clientInputStream, clientOutputStream)
    
    val mockServer = Thread {
        Thread.sleep(50)                                                                  // 1.  wait for client
        serverInputStream.read(ByteArray(requestLength))  // 2. read request in server side
        serverOutputStream.write(ACK.getBytes())                  // 3. send ack to client
        connection.onDataAvailable()                                          // 4. notify client - simulate comm listener
        
        serverOutputStream.write(responseBytes)                       // 5. send response to client
        connection.onDataAvailable()                                          // 6. notify client - simulate comm listener
        
        Thread.sleep(50)                                                                  // 7.  wait for client
        serverInputStream.read(ByteArray(1))                          // 8. read ack in server side
    }
    val client = Client(connection)
    ....
    

Option 2:使用Fake的外部程序

  • 虛擬串口:Windows和Linux上都有現(xiàn)成的串口調(diào)試工具预皇,Win上的有界面更方便

    我使用的是Windows Virtual Serial Port Driver,因?yàn)樘摂M串口不好操作寫數(shù)據(jù)(或者是我太菜)婉刀,我創(chuàng)建了2個(gè)虛擬串口A - B吟温,將他們Pair起來;

    還是如上起2個(gè)線程突颊,這時(shí)候Client通過串口A寫入的數(shù)據(jù)鲁豪,會(huì)被正在監(jiān)聽串口B的Server捕獲;

    Server如期返回?cái)?shù)據(jù)后洋丐,Client又能夠從串口A接收到Response呈昔;

    而且所有數(shù)據(jù)操作和控制都在Test代碼里,一鍵操作還是不錯(cuò)的友绝;

  • USB轉(zhuǎn)串口芯片(稍微硬核)

    從某寶網(wǎng)購一塊USB轉(zhuǎn)TTL的串口芯片堤尾,裝上驅(qū)動(dòng)就能用;

    把read和write引腳短接迁客,可以驗(yàn)證串口連接和數(shù)據(jù)傳輸是否正確郭宝;

    但是想要走通整個(gè)流程,還得控制write的數(shù)據(jù) —— write/read接上樹莓派掷漱,然后(一頓操作)就可以了粘室;

Option 3:連接測試機(jī)

  • (主要是我們的測試機(jī)器到的太晚,而且還是遠(yuǎn)程調(diào)試卜范,所以才想了上面的辦法先保證基本功能沒問題)

    (不過由于前期的良好測試衔统,拿到測試機(jī)后直接run test case沒太大問題)

后記(腦補(bǔ))

此文僅以筆者經(jīng)驗(yàn),闡述使用面向?qū)ο笫址ǚ庋b串口協(xié)議的一種方式。

雖然分析實(shí)現(xiàn)過程以“面向?qū)ο蟆睘橹鹘蹙簦庋b舱殿、基礎(chǔ)、多態(tài)均有险掀,但都是抽象的一種手法 —— 抽象沪袭,即是編程的本質(zhì),對(duì)問題域和解決方案域的提煉樟氢。選擇合適的角度和層級(jí)分析問題冈绊、找尋共性、得出答案埠啃,將過程抽象為模型死宣、方法論、原則碴开,最后用何種代碼十电、何種工具進(jìn)行實(shí)現(xiàn)就不再是問題了。這也是架構(gòu)師的職責(zé)所在 —— 將混沌叹螟、繁雜的問題通過分析變?yōu)閺?fù)雜鹃骂、簡單的知識(shí)(Cynefin)。

雖然此處只是對(duì)串口協(xié)議做了一層封裝罢绽,但相關(guān)的分析方式畏线、分層模型一樣可以套用到其他領(lǐng)域:

  • 高級(jí)語言對(duì)匯編指令的抽象封裝
  • kubctl對(duì)K8S組件的抽象封裝
  • 云服務(wù)對(duì)軟硬件服務(wù)的抽象抽象
  • ...

Learning:在如今的云原生時(shí)代,抽象的層級(jí)又被拔高了一些良价,但軟件工程的核心理論(短期)不會(huì)改變寝殴。如何將既有的分析模式、架構(gòu)設(shè)計(jì)擴(kuò)展到云原生領(lǐng)域...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末明垢,一起剝皮案震驚了整個(gè)濱河市蚣常,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌痊银,老刑警劉巖抵蚊,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異溯革,居然都是意外死亡贞绳,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門致稀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冈闭,“玉大人,你說我怎么就攤上這事抖单∥埽” “怎么了遇八?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長耍休。 經(jīng)常有香客問我押蚤,道長,這世上最難降的妖魔是什么羹应? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮次屠,結(jié)果婚禮上园匹,老公的妹妹穿的比我還像新娘。我一直安慰自己劫灶,他們只是感情好裸违,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著本昏,像睡著了一般供汛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涌穆,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天怔昨,我揣著相機(jī)與錄音,去河邊找鬼宿稀。 笑死趁舀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的祝沸。 我是一名探鬼主播矮烹,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼罩锐!你這毒婦竟也來了奉狈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤涩惑,失蹤者是張志新(化名)和其女友劉穎仁期,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竭恬,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蟀拷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了萍聊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片问芬。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖寿桨,靈堂內(nèi)的尸體忽然破棺而出此衅,到底是詐尸還是另有隱情强戴,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布挡鞍,位于F島的核電站骑歹,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏墨微。R本人自食惡果不足惜道媚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望翘县。 院中可真熱鬧最域,春花似錦、人聲如沸锈麸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忘伞。三九已至薄翅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氓奈,已是汗流浹背翘魄。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舀奶,地道東北人熟丸。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像伪节,于是被迫代替她去往敵國和親光羞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

推薦閱讀更多精彩內(nèi)容