python實現串口通信

因爲店家只給了我兩頁紙的vc0706通信協議,許多細節我還是不清楚,寫得也沒有特別優雅,大家就湊合看吧。

目錄

1 硬件設備

2 serial安裝

3 實現串口通信

3.1 發現端口

3.2 發送命令

3.2.1 協議格式

3.2.2 serial傳送的方式

3.3 獲取版本號(hello world)

3.4 復位

3.5 照相

3.5.1 停止當前幀刷新

3.5.2 獲娶圖片長度

3.5.3 恢復幀更新

3.5.4 拍照

4 反思


 

1 硬件設備

  1. TTL串口攝像頭(VC0706
  2. USB轉TTL燒錄器

2 serial安裝

第一次安裝的是serial的包導包的時候發現下載錯了,正確應該是pyserial。安裝後直接import就可以了。

3 實現串口通信

3.1 發現端口

Windows下爲COM(N, N=1、2...), Ubuntu下爲‘/dev/ttyS0Windows初學者,可以給您一下兩種方式確定端口號。

方法一:輸入在終端(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對象的代碼,不管你是window還是linux都可以不會報錯:

​
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.2 serial傳送的方式

serial傳送的方式有:

  1. 串行端口對象。
  2. 只傳單個字節。
  3. 字符串。
  4. 字節數組+字節數組長度。

所以直接選用數組傳數據,這裏會遇到一個問題就是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'', VERSION))
        self.assertFalse(ser.isreply(GET_VERSION_CMD, ''))
        self.assertFalse(ser.isreply(GET_VERSION_CMD, None))
        self.assertFalse(ser.isreply(b'', ''))
        self.assertFalse(ser.isreply(b'\x56\x00\xAA\x00', VERSION))
        self.assertFalse(ser.isreply(GET_VERSION_CMD, '\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,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name, left))
                return True
        return False

*測試和運行結果不一樣。

花了一點時間找到原因了,單元測我都是點擊前面綠色的小箭頭,以爲只是運行當前的測試函數的內容,但是我發現它把其他的函數都運行了。所以要把之前的測試函數註釋掉得到的結果就一樣了。

測試通過。

3.5 照相

  1. 停止當前幀刷新
  2. 獲娶圖片長度
  3. 獲取圖片
  4. 恢復幀更新

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,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name, left))
            return True
        return False

通過測試

    def test_stoprefresh(self):
        self.assertTrue(ser.stoprefresh())

3.5.2 獲娶圖片長度

此時讀完後還是要小心會有後序的內容沒有讀完也會影響後序的讀buffer。

    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,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name, 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,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name, 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)

現象:

  1. 現象是運行一直不停都是手動stop console,或者沒有stop console會一直打印lines爲空,就此可以猜測read()不是阻塞的。
  2. 是圖片字節總數不斷增多。
  3. 每次遍歷完後滿足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,The function'{}' is running. res:{}".format(sys._getframe().f_code.co_name, res))
            self.savephoto(res)

        # 恢復刷新
        self.recover_refresh()

 輸出圖片結果(拍的是導線沒有聚焦)

# 這個測試應該怎麼寫? 有圖片就輸出並且可以打開就可以了惹?有人能教教我?
    def test_getphoto(self):
        pass  


更新2月13日 

5 連續兩天解決運行不停止

通過無數種方式打印各種信息,納悶了沒有錯啊,一個進程,單線程,都打印了saosao進程結束了啊!!沒有沒關閉的進程或者是線程,with不是自帶close嗎?我不放心也加了close(),我調用完getphoto以後連串口都關閉了,到底是什麼地方需要輸入?明明沒有一個地方需要輸入的地方。最後我做了一個大膽的猜想,我新建了一個demo.py試一下運行一句helloworld發現helloworld也是停不了,我猜想是pycharm自帶輸入導致程序不停止,果然我解決了這個問題,我的九尾狐奶奶太惡毒了!

 

4 反思

  1. 犯了很多‘我覺得’的錯誤,我覺得這個值是什麼,多打斷點看清楚,那一段演示代碼裏面因爲協議寫了是五個字節,isreply我已經讀了4個字節再讀一個一定是0x00,後來打印那一行返回的值是0x04這才爲猜想讀到上一張圖作下鋪墊。
  2. 當你寫的很複雜超過20行的邏輯代碼就知道一定是錯了。--UnyieldingL
  3. 編碼方面的內容耗費了很長時間,就在反思的時候發現了decode("hex")。惹?
  4. list='aabbccddee'  
    hexer=list.decode("hex")  
    print  hexer 
  5. 打印日誌要詳細。 每一個變量涉及的變量長度函數名,此時哪個函數運行。
  6. 還有一個問題,pycharm還是沒有自己停止,打印某個線程的堆棧。
  7. 當你發現無論發了什麼的命令read的時候都是空就是表示沒有恢復幀刷新,相反發現無論發了什麼命令read都是亂七八糟的東西就是沒有停止幀刷新
  8. 耗時兩天的“bug”都是出於自己對pycharm不熟悉惹的禍,pycharm更新的太快了甚至有點不熟悉,尤其是debugstep over和step into經常混淆。大膽猜想小心求證。

5 鳴謝

技術指導:UnyieldingL

程序媛鼓勵師:風向晚。

發佈了47 篇原創文章 · 獲贊 81 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章