python實現串列埠通訊的示例程式碼
1 硬體裝置
- TTL串列埠攝像頭(VC0706)
- USB轉TTL燒錄器
2 serial安裝
第一次安裝的是serial的包導包的時候發現下載錯了,正確應該是pyserial。安裝後直接import就可以了。
3 實現串列埠通訊
3.1 發現埠
Windows下為COM(N,N=1、2...),Ubuntu下為‘/dev/ttyS0
'。
Windows初學者,可以給您一下兩種方式確定埠號。
方法一:輸入在終端(cmd)中輸入
python -m serial.tools.list_ports
輸出結果:
COM5 1 ports found
方法二:搜尋電腦上的裝置管理器,開啟以後然後插入燒錄器,自動就會彈出。如果沒有彈出就可能是驅動沒有安裝,安裝好以後不好使,重啟一下電腦,到了工作的時候大家都知道程式設計師會跟你說,你重啟一下,清一下快取,這兩句話。也有可能是驅動安裝的不對。
方法三:直接找一個有埠掃描的上位機,點選掃描就可以了。大部分上位機都是你一插進去就會檢測到你的埠。
----->
注意:當串列埠被佔用的時候也有可能導致失敗,例如你在編譯器有兩個程序執行下面的測試程式碼,第二個程序就會因為端口占用而失效。也有的上位機是因為同時打開了兩個上位機的緣故(實驗課的時候同學遇到過情況),可以用工作管理員kill掉。
測試:
import serial #Windows ser = serial.Serial(port='COM5',baudrate=115200,timeout=0.5) print(ser.name)
控制檯列印結果:
COM5 Process finished with exit code 0
建立ser物件的程式碼:
class PicSerial: __ser = None # ser的單例 __isinit = False @staticmethod def get_available_port(): """ 檢測可以使用的埠號 :return->str: 埠號的名稱 """ port = list(list_ports.comports()) if len(port) > 0: port_name = port[0].device print(port_name) return port_name # logging.info("Available port:",ports) else: print("There is no available port.") # logging.error("There is no available port.") def __new__(cls,*args,**kwargs): if PicSerial.__ser is None: cls.__ser = object.__new__(cls) return cls.__ser def __init__(self): if not PicSerial.__isinit: self.sername = self.get_available_port() self.ser = serial.Serial(port=self.sername,baudrate=BAUDRATE) PicSerial.__isinit = False print("PicSerial init.")
3.2 傳送命令
3.2.1 協議格式
3.2.2serial傳送的方式
serial傳送的方式有:
串列埠物件。只傳單個位元組。字串。位元組陣列+位元組陣列長度。
所以直接選用陣列傳資料,這裡會遇到一個問題就是python的list會自動把十六進位制數轉換為整形。
所以要進行轉換你可以直接寫成b“/x56/x00/x17/x00”。假如你不需要傳十進位制也可以轉成list,直接map(chr,x)或map(ord,x)也是可以的。讀的時候也要注意只要你放進list裡面就會自動轉成整形。
【我覺得這樣寫很降智,但是又不得不這樣寫】
#在PicSerial中 def isreply(self,cmd: bytes,option: str) -> bool: """ 檢測是否有回覆 :return:回覆的內容 :param cmd: :param option: :return: True則有回覆 """ if isinstance(cmd,bytes) and isinstance(option,str) and len(cmd) > 0 and len(option) > 0: self.ser.write(cmd) reply = self.ser.read(4) reply = list(map(chr,list(reply))) print("49h,The function'{}' is running. reply:{}".format(sys._getframe().f_code.co_name,reply)) if len(reply) >= 4 and reply[0] == 'v' and reply[1] == SERIAL_NUM and reply[2] == option and reply[3] == STATUS: return True return False
測試:
#在test檔案中 class TestSerial(unittest.TestCase): def test_isreply(self): self.assertTrue(ser.isreply(GET_VERSION_CMD,VERSION)) self.assertFalse(ser.isreply('\x56\x00\x11\x00',VERSION)) self.assertFalse(ser.isreply(GET_VERSION_CMD,b'\x11')) self.assertFalse(ser.isreply(123456,b'\x11')) self.assertFalse(ser.isreply('',VERSION)) self.assertFalse(ser.isreply(b'','')) self.assertFalse(ser.isreply(GET_VERSION_CMD,None)) self.assertFalse(ser.isreply(b'','')) self.assertFalse(ser.isreply(b'\x56\x00\xAA\x00','\xAA')) #之後就省略不寫了 if __name__ == '__main__': unittest.main()
結果:
3.3 獲取版本號(hello world)
按照協議一步一步操作
主 機 發:56 00 11 00 攝像頭回:76 00 11 00 0B 56 43 30 37 30 33 20 31 2E 30 30 (VC0703 1.00)
#在PicSerial中 def getversion(self) -> str: """ 獲取版本號 :return: """ cmd = GET_VERSION_CMD option = VERSION if self.isreply(cmd,option): left = self.ser.readall() print("75h,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name,left)) return self.ser.read(12).decode()[1:]
測試:
#在test檔案中 def test_getversion(self): self.assertEqual(ser.getversion(),'VC0703 1.00')
結果:通過測試
3.4 復位
主 機 發: 56 00 26 00
攝像頭回: 76 00 26 00 00
#在PicSerial中 def reset(self): """ 復位 :return: """ cmd = REST_CMD option = RESET if self.isreply(cmd,option): if self.ser.read(1) == b'': left = self.ser.readall() print("75h,left)) return True return False
*測試和執行結果不一樣。
花了一點時間找到原因了,單元測我都是點選前面綠色的小箭頭,以為只是運行當前的測試函式的內容,但是我發現它把其他的函式都運行了。所以要把之前的測試函式註釋掉得到的結果就一樣了。
測試通過。
3.5 照相
- 停止當前幀重新整理
- 獲娶圖片長度
- 獲取圖片
- 恢復幀更新
3.5.1停止當前幀重新整理
這一步執行一次就夠了。因為讀命令的時候會出現麻煩。但是這一步是有意義的,就是當你發現圖片很大,的時候正常大小就兩個byte可以表示完了(排除你的圖片面積十分大或十分清晰),又或者是突然讀空了。假如數值非常的大,可以使用該函式,再不行就要選擇復位。
def stoprefresh(self): """ 停止重新整理當前幀 :return: """ cmd = STOP_REFRESH_CMD option = TAKE_PHOTO self.ser.write(cmd) if self.isreply(cmd,option) and self.ser.read(1) == b"\x00": left = self.ser.readall() print("87h,left)) return True return False
通過測試
def test_stoprefresh(self): self.assertTrue(ser.stoprefresh())
3.5.2獲娶圖片長度
def getlength_bytes(self) -> bytes: """ 獲取圖片的長度 :return: """ cmd = GET_LENGTH_CMD option_pic = '4' self.ser.write(cmd) if self.isreply(cmd,option_pic): if self.ser.read(1) == b'\x04': res = self.ser.read(4) left = self.ser.readall() print("103h,left)) return res return b'\x00\x00\x00\x00'
測試通過
def test_getlength(self): self.assertEqual(ser.getlength(),b'\x00\x00\x12\x34')
3.5.3恢復幀更新
def recover_refresh(self): """ 恢復幀重新整理 :return: """ cmd = RECOVER_REFRESH_CMD option = TAKE_PHOTO self.ser.write(cmd) if self.isreply(cmd,option): # 讀出剩餘的位元組 left = self.ser.readall() print("142h,left)) return True return False
測試並通過:
def test_recover_refresh(self): self.assertTrue(ser.recover_refresh())
3.5.4 拍照
在這裡卡了很長時間,不知道為什麼長度是不確定的,每一次讀的長度都沒讀完,看程式碼。
下面程式碼只是演示
#下面程式碼只是演示不在最終版本中 def savephoto(self,cmd,option,len): """ 儲存圖片 :param cmd: :param option: :param len: 照片的長度 :return: """ with open('write_pic/serialpic/photo.jpg','wb') as f: if self.isreply(cmd,option): print(self.ser.read(1)) countofread_complete_byte = 0 # 用於計算當前已經寫入的長度 while countofread_complete_byte != len + 10: # read()是有上限的,不可以把全部都讀取 lines = self.ser.read(len + 10 - countofread_complete_byte) countofread_complete_byte += lines.__len__() f.write(lines) print("142h,countofread_complete_byte:",countofread_complete_byte,"lines",lines.__len__()) left = self.ser.readall() print("146h,少讀內容:",left,"共",left.__len__(),"個位元組") res = self.ser.readall() print(res)
現象:
現象是執行一直不停都是手動stop console,或者沒有stop console會一直列印lines為空,就此可以猜測read()不是阻塞的。是圖片位元組總數不斷增多。每次遍歷完後滿足self.ser.read(len + 10 - countofread_complete_byte)後再readall()還是有剩餘的內容。
我發現此時readall一共讀出了4049個位元組,圖片資料4030個位元組+首尾兩部分共10個位元組,那多出來的9個位元組是什麼火眼金睛的Unyielding ● L發現了正確的開始位置為上圖紅色方塊處,碰巧多出來的是九個位元組,所以多出來的就不是這一張圖片的內容,所以可以猜想程式沒有停止的原因是上一次圖片還沒讀完我就手動停止,所以留下了資料,上一次沒有讀完的內容,這一次讀到了。
位元組串和字串都可以切片。直接切出來儲存。
def getphoto(self): """ 拍照並且儲存圖片 :return: """ # self.reset() # 1、停止幀重新整理 # self.stoprefresh() # 獲取圖片長度 # 返回位元組長度用於整合命令,表示圖片的總位元組數 length = self.getlength_bytes() # 返回整形,表示圖片的總位元組數 len = self.bytesToInt(length) print("158h,len:",len) # 拍照 cmd = GET_PHOTO_START_CMD + length + GET_PHOTO_END_CMD print("159hcmd",cmd) self.ser.write(cmd) readall = self.ser.readall() readall_len = readall.__len__() differ = readall_len - len - 10 if differ != 0: res = readall[differ + 5:-5] print("172h:",res) self.savephoto(res) else: res = readall[5:-5] print("175h:",res) self.savephoto(res) # 關閉串列埠 self.ser.close() # 恢復重新整理 # self.recover_refresh()
成功輸出結果:
為了方便debug,停幀回覆幀都是手動傳送的,剩下的問題就是把註釋開啟試一試能不能成功組合成一個函式,發現有的命令會讀空,所以可以推斷:一定又是前一個命令裡面又留下來什麼還沒有被讀取的位元組造成讀到的內容篡位了。
每一次執行完命令後看一看還有沒有遺留位元組,把剩餘位元組都取出來,然後differ的判斷都不需要了。【讀者看到的程式碼都是最新版本的,此處我添加了left和print到對應函式中】,處理結果列印到控制檯:
def getphoto(self): """ 拍照並且儲存圖片 :return: """ # 1、停止幀重新整理 self.stoprefresh() # 獲取圖片長度 # 返回位元組長度用於整合命令,表示圖片的總位元組數 length = self.getlength_bytes() # 返回整形,表示圖片的總位元組數 len = self.bytesToInt(length) print("161h,The function'{}' is running. len:{}".format(sys._getframe().f_code.co_name,len)) # 拍照 cmd = GET_PHOTO_START_CMD + length + GET_PHOTO_END_CMD print("165h,The function'{}' is running. cmd:{}".format(sys._getframe().f_code.co_name,cmd)) self.ser.write(cmd) readall = self.ser.readall() readall_len = readall.__len__() differ = readall_len - len - 10 if differ != 0: res = readall[differ + 5:-5] print("161h,The function'{}' is running. res:{}".format(sys._getframe().f_code.co_name,res)) self.savephoto(res) else: res = readall[5:-5] print("161h,res)) self.savephoto(res) # 恢復重新整理 self.recover_refresh()
輸出圖片結果(拍的是導線沒有聚焦)
# 這個測試應該怎麼寫? 有圖片就輸出並且可以開啟就可以了惹?有人能教教我? def test_getphoto(self): pass
4 反思犯了很多
‘我覺得'的錯誤,我覺得這個值是什麼,多打斷點看清楚,那一段演示程式碼裡面因為協議寫了是五個位元組,isreply我已經讀了4個位元組再讀一個一定是0x00,後來列印那一行返回的值是0x04這才為猜想讀到上一張圖作下鋪墊。
當你寫的很複雜超過20行的邏輯程式碼就知道一定是錯了。--UnyieldingL
編碼方面的內容耗費了很長時間,就在反思的時候發現了decode("hex")。惹?
list='aabbccddee' hexer=list.decode("hex") print hexer
列印日誌要詳細。 每一個變數涉及的變數長度函式名,此時哪個函式執行。
還有一個問題,pycharm還是沒有自己停止,列印某個執行緒的堆疊。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。