{%hackmd @neko-yoru/darktheme %} <style> pre > code > div, #doc > ol > li > pre { background-color: var(--background-primary-alt); } #doc > ol > li > pre::-webkit-scrollbar-thumb { background-color: #55586b; } #doc > ol > li > pre::-webkit-scrollbar-track { background-color: #333649 } </style> # Raspberry Pi 1 教學 ## Week 1 系統安裝及VNC連線 ### 系統安裝 :::info 推薦使用 [<font class="hyperlink-font">Raspberry Pi Imager</font>](https://www.raspberrypi.com/software) 進行安裝,新版的 Raspberry Pi OS 因安全性考量已取消預設帳號及密碼。若安裝舊版系統則預設帳號為 <font class="pink-font">pi</font> 密碼為 <font class="pink-font">raspberry</font>,以下僅使用 Raspberry Pi Imager 示範。 ::: 1. 點選 <font class="pink-font">CHOOSE OS</font> 後選擇 <font class="pink-font">Raspberry Pi OS(32-bit)</font>。<div class="parallel-two-img"><img src="https://hackmd.io/_uploads/BkadD6-Ja.png"><img src="https://hackmd.io/_uploads/S1rNPpZyT.png"> </div> 2. 點選 <font class="pink-font">CHOOSE STORAGE</font> 後選取欲安裝系統的儲存裝置。 3. 點選右下角的設定(藍色框)後勾選 <font class="pink-font">Set username and password</font> 並設定系統登入帳號(<font class="alert-font">**不要使用中文**</font>),設定完後點選 <font class="pink-font">SAVE</font>。<div class="parallel-two-img"> <img src="https://hackmd.io/_uploads/r1W4Yp-k6.png"><img src="https://hackmd.io/_uploads/BynR5pW16.png"> </div> 4. 點擊 <font class="pink-font">WRITE</font> 開始燒錄系統檔案。 ### VNC設定 1. 將燒錄完成的 SD Card 插入 Raspberry Pi。 2. 連接UART模組,板子的TX接模組的RX,RX則接TX,Raspberry Pi 先不要接上電源。 ![Pinout](https://hackmd.io/_uploads/SJuHrRby6.png "圖片來源:eTechnophiles") 圖片來源: [<font class="hyperlink-font">eTechnophiles</font>](https://www.etechnophiles.com/raspberry-pi-1-gpio-pinout-schematic-and-specs-in-detail/) 3. 將UART模組接上電腦,並在電腦上打開裝置管理員(<kbd>WIN</kbd>+<kbd>x</kbd>或對 Windows 符號按右鍵即可找到),點選序列埠(Ports)確認序列埠的編號,如我的為<font class="pink-font">COM3</font>。 ![Device Manager](https://hackmd.io/_uploads/B1swYRW16.png) 4. 打開序列埠連接軟體(以下使用PuTTY示範),設定剛剛查看的編號,連線速度使用<font class="pink-font">115200</font>,點選Open以開啟序列埠。 ![PuTTY Setup](https://hackmd.io/_uploads/SJz3qRZ1T.png) 5. 將 Raspberry Pi 接上網路線及電源(網路線另一端接電腦即可,若接路由器則須注意電腦及 Raspberry Pi 位於同一區域網路)。 6. 待出現 ```raspberrypi login:``` 後,輸入剛剛燒錄時所設定的帳號並按下 <kbd>Enter</kbd> 後,出現 ```Password:``` 後再輸入密碼,輸入完成後再次按下 <kbd>Enter</kbd>。 :::info 在輸入密碼的時候沒有出現任何字元是正常的,這是 Linux 系統基於安全性考量所做的設定。 ::: ![Login](https://hackmd.io/_uploads/BkAbykGJa.png) 7. 輸入 ```sudo raspi-config``` 並按下 <kbd>Enter</kbd> 後會出現以下畫面。 ![raspi-config](https://hackmd.io/_uploads/S1AK1yzJ6.png) 8. 使用鍵盤的上下鍵可以選擇選項,左右鍵可以選擇 ```<select>``` 或 ```<Finish>``` 按下 <kbd>Enter</kbd> 可以進入或選擇。 9. 進入 ```3 Interface Options``` 並選擇 ```I3 VNC```,然後選擇 ```<Yes>```。<div class="parallel-two-img"><img src="https://hackmd.io/_uploads/H17ae1zJp.png"><img src="https://hackmd.io/_uploads/BycWW1Mka.png"></div> 10. 出現 ```The VNC Server is enabled``` 後按下 <kbd>Enter</kbd> 確認。 11. 可以進入 ```Display Options``` 設定待會 VNC 連線的解析度,設定方式並不複雜,且不更改亦可運行,故此處不再贅述。 12. 選擇 ```<Finish>``` 完成設定,若提示需要重啟就選擇OK,待重啟後重新登入。 13. 輸入 ```vncserver``` 以啟動 VNC,並記下 VNC Server 的 IP 位置。 ![vncserver](https://hackmd.io/_uploads/r10VrxzkT.png) 15. 在電腦上安裝 VNC Viewer 軟體,以下使用 [<font class="hyperlink-font">RealVNC Viewer</font>](https://www.realvnc.com/en/connect/download/viewer/)。 16. 在 VNC Viewer 中輸入剛剛所記下的 IP 位置。 ![VNC Viewer](https://hackmd.io/_uploads/Syw2N1M1T.png) 17. 同樣使用燒錄時所建立的系統帳號進行登入。 ![VNC Connection](https://hackmd.io/_uploads/SJhnSJz1a.jpg) ### 透過網路線共享網路 1. 按下 <kbd>WIN</kbd>+<kbd>x</kbd> 開啟<font class="pink-font">執行</font>視窗,輸入 ```ncpa.cpl``` 並按下 <kbd>Enter</kbd> 打開介面卡設定頁面。 2. 將 Wi-Fi 關閉。 3. 按住 <kbd>Ctrl</kbd> 並點選 Wi-Fi 及乙太網路(<font class="alert-font">**先點選乙太網路再點選 Wi-Fi**</font>)兩個介面,選取後對其按下右鍵,並選擇橋接器連線。 ![Bridge Connections](https://hackmd.io/_uploads/H1gNAmQ16.png) 4. 待新建的網路橋接器辨識完成後,對其點擊右鍵並點選<font class="pink-font">內容</font>,在新出現的視窗中確認 Wi-Fi 及乙太網路都有被勾選(如下圖紅框處)。 ![Network Bridge](https://hackmd.io/_uploads/Sy9iUrXya.png) 5. 雙擊<font class="pink-font">網際網路通訊協定第4版(TCP/IPv4)</font>,並確認 IP 位址及 DNS 伺服器皆為自動取得。<div class="parallel-two-img"><img src="https://hackmd.io/_uploads/r1BvwQjkp.png"><img src="https://hackmd.io/_uploads/r1b4O7jk6.png"></div> 6. 重新連接 Wi-Fi。 7. 雙擊網路橋接器介面並點選詳細資料查看 IPv4 子網路遮罩,本例中為 ```255.255.255.0```。 *IP 位址其實是二進位編碼,使用十進位是為了方便查看。子網路遮罩是用來判斷不同 IP 是否位於同一子網路的設定,在判斷時需將 IP 位址與子網路遮罩做 AND 運算,得到的結果相同者即為同一子網路。* *如本例中 IPv4 位址為 ```192.168.0.239```,將其與子網路遮罩做 AND 運算後得到 ```192.168.0.0```,因此只要格式為 ```192.168.0.xxx``` 的 IP 位址皆與本機位於同一子網路。* ![netmask](https://hackmd.io/_uploads/SyJdeAiJT.png) 8. 在序列埠連接軟體中輸入 ```sudo ifconfig eth0 192.168.0.240 netmask 255.255.255.0```,```192.168.0.240``` 請替換為與你電腦相同子網路但未被其他裝置使用的 IP 位址(在 Windows 中的命令提示字元中輸入 ```arp -a``` 可以查看被其他裝置使用的 IP 位址),```255.255.255.0``` 則替換為方才查看的子網路遮罩。 9. 輸入 ```ping 8.8.8.8``` 確認網路連線正常。 ![Ping](https://hackmd.io/_uploads/ryTTUS7yT.png) 10. 若稍早已經開啟 VNC Server 則須將其重啟,先輸入 ```vncserver -kill :1``` 將其關閉後再使用 ```vncserver``` 開啟,並使用新的 IP 進行連線即可。 ## Week 2 使用 Python 控制 GPIO ### 使用 GPIO :::info - 以下教學皆於序列埠連接軟體操作。 - 在 Raspberry Pi 上用 Python 對 GPIO 進行操作主要有兩種常見套件:RPi.GPIO 以及 GPIO Zero,GPIO Zero 是基於 RPi.GPIO 建構的,以下會使用 RPi.GPIO 實作。 - 有關 RPi.GPIO 的詳細資訊,可以參考[<font class="hyperlink-font">官方文件</font>](https://sourceforge.net/p/raspberry-gpio-python/wiki/Home/) ::: :::danger ***不論在使用甚麼開發板、晶片或是零件,請務必查看文檔確認其電器特性,配置電路時須嚴格遵守限制。*** ::: 1. 使用指令 ```pinout``` 可以查看腳位等相關資訊,標示為綠色的即為我們可以使用的 GPIO 腳位。 ![Pinout](https://hackmd.io/_uploads/HJXqma6y6.png) 2. 輸入 ```python``` 開啟直譯器互動模式,並輸入以下指令匯入套件以及設定引腳編號模式。 ```python= import RPi.GPIO as GPIO # 有兩種模式 GPIO.BCM 及 GPIO.BOARD,在使用 GPIO 之前需要先進行設定。 # GPIO.BCM 使用的編號為晶片腳位(上一步驟圖示中綠色字後的編號) # GPIO.BOARD 使用的編號為端子頭的編號(上一步驟圖示中括號內的數字) GPIO.setmode(GPIO.BCM) ``` 3. 以下分別針對 GPIO 的輸入、輸出以及 PWM 模式進行介紹 - 使用 ```GPIO.setup(<channel>, <direction>)``` 設定模式,<channel\>為腳位編號,可以傳入單一腳位編號或使用串列或元組傳入多個腳位,<direction\>則為 <font class="pink-font">GPIO.IN</font> 或是 <font class="pink-font">GPIO.OUT</font>,此函數還有兩個 Keyword Arguments 將於稍後介紹。 - 如果忘記函數的使用方式,可以到官方文件查詢,或是使用 help 函數 (e.g. ```help(GPIO.setup)``` 可以查詢 GPIO.setup 的使用方式)。 1. 輸出模式 - 使用 ```GPIO.setup(<channel>, GPIO.OUT, initial=<initial>)``` 將設定<channel\>引腳為輸出模式且初始輸出為<initial\>,其中<initial\>可以設定為 <font class="pink-font">GPIO.HIGH</font> 或是 <font class="pink-font">GPIO.LOW</font>。 - 使用 ```GPIO.output(<channel>, <value>)``` 設定輸出值。 - 範例: ```python= # 以下程式可以使 GPIO17 一秒變換一次狀態,起始輸出高電位。 from time import sleep import RPi.GPIO as GPIO output_pin = 17 state = 1 GPIO.setmode(GPIO.BCM) GPIO.setup(output_pin, GPIO.OUT, initial=1) while 1: state = state ^ 1 GPIO.output(output_pin, state) sleep(1) ``` ![GPIO Output](https://hackmd.io/_uploads/BkEyET616.png) 2. 輸入模式 - 使用 ```GPIO.setup(<channel>, GPIO.IN, pull_up_down=<pull_up_down>)``` 將設定<channel\>引腳為輸入模式,<pull_up_down\>可以設定上拉電阻 <font class="pink-font">GPIO.PUD_UP</font> 或下拉電阻 <font class="pink-font">GPIO.PUD_DOWN</font> ,如未設定則使用預設值 <font class="pink-font">GPIO.PUD_OFF</font>,即關閉上下拉電阻。 - 使用 ```GPIO.input(<channel>)``` 可以讀取<channel\>腳位的輸入電位。 - 使用 ```GPIO.wait_for_edge(<channel>, <edge>)``` 會等待<channel\>引腳有正緣訊號或是負緣訊號時才繼續執行下一行指令。<edge\>可以設定為正緣觸發 <font class="pink-font">GPIO.RISING</font>、負緣觸發 <font class="pink-font">GPIO.FALLING</font> 或是正負緣觸發 <font class="pink-font">GPIO.BOTH</font>。另有兩個關鍵字引數[bouncetime]及[timeout]請參考官方文件。 - 上面那種方式稱作輪詢(Polling),但因該方式需要停下來等待所以較少使用。較常使用的方式為中斷(Interrupt)。在 RPi.GPIO 中需要先呼叫 ```GPIO.add_event_detect(<channel>, <edge>, [bouncetime], [timeout])``` 設定需要在何時產生中斷,再呼叫 ```GPIO.add_event_callback(<channel>, <callback>)``` 設置產生中斷時所要執行的<callback\>函數。 - 範例: ```python= import RPi.GPIO as GPIO def callback_func(channel): print(f"Channel {channel} generated interrupt") input_pin = 17 GPIO.setmode(GPIO.BCM) GPIO.setup(input_pin, GPIO.IN, GPIO.PUD_DOWN) GPIO.add_event_detect(input_pin, GPIO.RISING) GPIO.add_event_callback(input_pin, callback_func) # 此無限迴圈是為了防止程式結束,程式結束會清除所有 GPIO 設定。 while True: pass ``` 3. PWM 模式 :::info PWM(Pulse Width Modulation) 是一種使用數位訊號來生成類比訊號的方式。Raspberry Pi 的 GPIO 僅能輸出數位訊號(0V 和 3.3V),我們可以透過 PWM 來達成輸出類比電壓的目的。關於 PWM 的詳細介紹,請參考[<font class="hyperlink-font"> Wikipedia</font>](https://zh.wikipedia.org/zh-tw/%E8%84%88%E8%A1%9D%E5%AF%AC%E5%BA%A6%E8%AA%BF%E8%AE%8A)。 ::: - 使用 PWM Mode 需先將 GPIO 設為輸出模式。 - 用 ```pwm_var = GPIO.PWM(<channel>, <frequency>)``` 設定 PWM 模式及輸出頻率,該函數會回傳一個物件可用於稍後的 PWM 操作。 - ```pwm_var.start(<duty cycle>)``` 會開始產生佔空比為<duty cycle\>%的訊號,```pwm_var.stop()``` 可以停止輸出。 - ```pwm_var.ChangeFrequency(<frequency>)``` 可以變更頻率,```pwm_var.ChangeDutyCycle(<duty cycle>)``` 可以改變佔空比。 - 範例: ```python= from time import sleep import RPi.GPIO as GPIO output_pin = 17 GPIO.setmode(GPIO.BCM) GPIO.setup(output_pin, GPIO.OUT) pwm_var = GPIO.PWM(output_pin, 10) pwm_var.start(10) sleep(1) pwm_var.ChangeDutyCycle(50) sleep(1) pwm_var.stop() ``` ![PWM](https://hackmd.io/_uploads/ryaEVTa1p.png) ### 使用 CRON 定期運行任務 :::info 使用 cron 可以讓電腦自動運行指令,也可以在開機的時候自動運行。以下僅對常用功能進行介紹,如要深入了解可以使用 ```man cron``` 與 ```man crontab``` 指令查看詳細資訊。 ::: 1. 在 Raspberry Pi 的終端上輸入 ```crontab -e``` 可以開啟 cron 的設定腳本並進行編輯,以 <font class="pink-font">#</font> 字號為開頭的是註解,建議編輯腳本時不要把註解刪掉,若忘記語法可以查看註解。 2. crontab 的語法為 ```分 時 日 月 週 指令```。 3. 範例: ```bash! * * * * * python ~/Documents/demo.py # 每分鐘運行 10 * * * * python ~/Documents/demo.py # 每小時的 10 分運行 * 12 * * 1,2 python ~/Documents/demo.py # 每週一、二 12 點運行 * 1 * 6-12 * python ~/Documents/demo.py # 6~12 月每日 1 點運行 */5 * * * * python ~/Documents/demo.py # 每五分鐘執行一次 @reboot python ~/Documents/demo.py # 每次開機運行一次。 ``` ## Week 3 連接觸控面板 ### 軟體設定 1. 使用 ```sudo apt install -y onboard``` 指令安裝虛擬鍵盤。 2. 使用 ```sudo nano /boot/config.txt``` 開啟並編輯開機設定檔,找到 ```dtoverlay=vc4-fkms-v3d```(若找到兩個,請編輯第一個而非 pi4 底下那個) 並在該行的開頭加上<font class="pink-font">#</font>字號將其註解,然後在檔案的最後一行加上 ```ignore_lcd=0```。 4. 使用 ```sudo raspi-config``` 指令開啟 Raspberry Pi 配置頁面,選擇<font class="pink-font">1 System Options</font>,然後選擇<font class="pink-font">S5 Boot / Auto Login</font>,最後選擇<font class="pink-font">B4 Desktop Autologin</font>,待其設定完成後回到配置主畫面,選擇<font class="pink-font">Finish</font>並重新啟動以完成設定。 ### 硬體設定 1. 斷開 Raspberry Pi 上面的電源,使用杜邦線將螢幕後面的5V、SCL、SDA、GND分別接到 Raspberry Pi 上對應的腳位,排線一端插螢幕後方,另一端插入 Raspberry Pi 的 DSI 接口(靠近 Micro B 接口的那個),最後將 Micro USB 線接到螢幕後的 PWR IN 插座上,等待 Raspberry Pi 完成開機即可使用。 2. 若要使用虛擬鍵盤,點擊畫面左上角的 Logo 並選<font class="pink-font">Universal Access</font>,然後點選<font class="pink-font">Onboard</font>即可。 ## Week 4 I2C & SPI & 相機模組設置 ### 前置作業 1. 使用 ```sudo raspi-config``` 指令開啟 Raspberry Pi 配置頁面,選擇<font class="pink-font">3 Interface Options</font>,然後分別進入<font class="pink-font">SPI</font>以及<font class="pink-font">I2C</font>的頁面並將它們啟用。 2. 開啟<font class="pink-font">/etc/modules</font>檔案,在檔案最後新增一行並輸入 ```i2c-dev```(如果已經存在則無需新增),存檔後將 Raspberry Pi 重新開機。 ### I2C 1. 在 [<font class="hyperlink-font">pinout.xyz</font>](https://pinout.xyz/) 網站中查看腳位,可以看到<font class="pink-font">GPIO 2</font>及<font class="pink-font">GPIO 3</font>可以當作<font class="pink-font">I2C1</font>使用,其中<font class="pink-font">I2C1</font>中的<font class="pink-font">1</font>表示它的 I2C Bus ID 為 1,在 Raspberry Pi 的終端中使用指令 ```i2cdetect -y 1``` 可以查看目前連接在 I2C Bus 1 上的 I2C 裝置的位址,例如下圖表示 I2C Bus 1 上有一個 I2C Address 為 0x3C 的裝置。 ![i2cdetect](https://hackmd.io/_uploads/ryJ29DWf6.png) 2. 以下程式使用 SMBus 的 Python Package 示範如何操作 MPU6050 裝置。 :::info - 操作 I2C 裝置前須先使用 ```bus = smbus.SMBus(<busid>)``` 初始化,並且使用該函數傳回的物件操作裝置。 - 使用 ```bus.write_byte_data(<i2c_addr>, <reg_addr or cmd>, <val>)``` 寫入單一位元組,其中 ```<i2c_addr>``` 為裝置位置,```<reg_addr or cmd>``` 為欲寫入的內部位址或是指令,```<val>``` 為欲寫入的值。若要寫入一個區塊的資料,可以使用 ```write_i2c_block_data(<addr>, <reg_addr or cmd>, <[vals]>)```,```<i2c_addr>``` 及 ```<reg_addr or cmd>``` 與上述相同,唯 ```<[vals]>``` 須為一串列,另須注意該串列的長度不得大於 32。 - 使用 ```bus.read_byte_data(<i2c_addr>, <reg_addr or cmd>)``` 可以讀取 ```<i2c_addr>``` 上 ```<reg_addr or cmd>``` 的資料。欲讀取一個區塊之資料可以使用 ```read_i2c_block_data(<i2c_addr>, <reg_addr or cmd>, <len>)```,若無指定 ```len``` 則預設讀取 32 位元組的資料,另須注意 ```len``` 的值不得大於 32。 ::: - 查看 MPU6050 的手冊可以發現該裝置啟動時預設會進入睡眠狀態,因此要先將其啟動,啟動方式為對下圖的暫存器寫入 0x00。 ![mpu6050 reg 0x6B](https://hackmd.io/_uploads/rkXc5czz6.png) - 讀取加速度計的相關暫存器如下圖,因單一暫存器僅有一個位元組,所以一個軸需要讀取兩個暫存器(共 16 位元)。 ![mpu6050 reg accelarator](https://hackmd.io/_uploads/HJY1o5Gzp.png) <br /> ```python= import smbus from time import sleep bus = smbus.SMBus(1) bus.write_byte_data(0x68, 0x6B, 0x00) # 計算二補數的函數 twos_complement = lambda value, bitWidth: value - int((value << 1) & 2**bitWidth) # 讀取兩個暫存器(高位及低位)並計算其二補數 def read(addr): values = bus.read_i2c_block_data(0x68, addr, 2) return twos_complement((values[0] << 8) + values[1], 16) while 1: print(f"X: {read(0x3C)}, Y: {read(0x3E)}, Z: {read(0x3F)}") sleep(0.5) ``` ### SPI 1. 一樣在 [<font class="hyperlink-font">pinout.xyz</font>](https://pinout.xyz/) 網站查看腳位(Raspberry Pi 1B 僅有 26 個腳位,故僅有 SPI1 可供使用)。SPI1 的 1 表示其 SPI Bus ID 1 為 1。 2. 以下程式使用 spidev 的 Python Package 示範如何操作 SPI 裝置。 :::info - 操作 SPI 裝置前須先使用 ```spi = spidev.SpiDev()``` 初始化,使用該函數之傳回值來操作 SPI。 - 用 ```spi.open(<bus>, <device>)``` 開啟裝置,```<bus>``` 為 SPI Bus ID,```<device>``` 為 CS(CE)的編號。 - 設定 ```spi.max_speed_hz``` 以限制 SCLK 的最大值。 - 使用 ```spi.xfer(<[val]>)``` 傳送 ```<[val]>```(須為一串列)中的資料至 MOSI,並且同時接收 MISO 上的資料並傳回(傳送幾個位元組就會收到幾個位元組)。 - ```xfer``` 函數另有 ```xfer2``` 及 ```xfer3``` 兩個版本,```xfer``` 在傳送每個位元組之間會恢復 CS(CE)引腳,```xfer2``` 及 ```xfer3``` 則否,而 ```xfer3``` 可以傳送比 ```xfer2``` 更長的串列。 - spidev 中另有其他函數可供使用,不過多數時候使用 xfer 系列函數即可。如有其餘需求,可以參考 spidev 的[<font class="hyperlink-font">官網</font>](https://github.com/doceme/py-spidev)。 ::: - 為簡化範例,以下使用我自己設計的 SPI 裝置,該裝置在收到 0x01 後會輸出四個按鈕的狀態,如下圖。使用 Raspberry Pi 上的 SPI0 及 CE0,故 Bus ID 為 0、Device ID 為 0。 ![SPI Device](https://hackmd.io/_uploads/r1mehWVG6.png) <br /> ```python= import spidev import time spi = spidev.SpiDev() spi.open(0, 0) spi.max_speed_hz = 1000000 while True: resp = spi.xfer2([0x01, 0x00]) print(f"First byte: {resp[0]:08b}, Second byte: {resp[1]:08b}") time.sleep(0.5) ``` ### 相機模組 1. 使用 ```sudo raspi-config``` 指令,選擇<font class="pink-font">Interface Options</font>,並將相機功能啟用(若無此選項,請跳至第二步,並將步驟 4 的指令改為 ```libcamera-still -o output.jpg```)。回到 raspi-config 的主頁,選擇<font class="pink-font">Advanced Options</font>,並將<font class="pink-font">Glamor</font>啟用。 2. 編輯 ```/boot/config.txt```,在檔案末加入一行 ```gpu_mem=144```(若本來就有此行僅數值不同,則更改數值即可)。另外,若先前設定螢幕時將 ```dtoverlay=vc4-fkms-v3d``` 註解,請將其取消註解(將<font class="pink-font">#</font>字號刪除)。 3. 將 Raspberry Pi 關機,然後將相機模組的排線接上 Raspberry Pi 的 CSI 接口上,CSI 接口為 HDMI 及 RJ45 接口中間那個。 4. 使用 ```raspistill -o output.jpg``` 拍攝一張相片,若能順利拍攝即可。日後可以與 OpenCV 等影像處理相關套件配合。 ## Week 5 Robot Operating System (ROS) ### 前置作業 1. 用 ```sudo docker pull neko0725/ros:debian_bullseye``` 將映像檔拉下來。 2. 使用 ```sudo docker run -it --privileged neko0725/ros:debian_bullseye``` 運行容器,可以配合 ```-v``` 或 ```--mount``` 選項將資料夾或卷宗掛載到容器上,以免關閉容器後資料一起被刪除(例如:```sudo docker run -it --mount src="/home/neko/Downloads/ros_tutorial_ws/",target=/root/neko_ws,type=bind --privileged neko0725/ros:debian_bullseye```) 3. 運行 ```source /opt/ros/noetic/setup.bash``` 將 ROS 的套件及執行檔加入至環境中。 ### 建立套件 1. 在啟動容器時掛載到容器內的資料夾中建立一個 ```src``` 資料夾,並 ```cd``` 到該資料夾內。運行 ```catkin_create_pkg topic_tutorial std_msgs rospy``` 建立套件,其中 ```topic_tutorial``` 為套件名稱可自行命名,```rospy``` 及 ```std_msgs``` 為依賴套件,請視需求自行更改,完成後會出現一個與套件同名的套件資料夾,而上層的資料夾稱為工作區資料夾。 2. 以下使用 Python 撰寫程式,通常 Python 的程式碼會放在 ```scripts``` 資料夾而非 ```src``` 資料夾,故先在套件資料夾中建立一個 ```scripts``` 資料夾,之後的程式碼皆放在此資料夾下。目前的資料夾結構如下圖,```ros_tutorial_ws``` 為工作區、```topic_tutorial``` 為套件資料夾,一個 Workspace 的 src 資料夾底下可以有多個套件資料夾。 ![dir structure](https://hackmd.io/_uploads/S1z-wVAMa.png) ### ROS 通信方式 1. Topic ![topic](https://hackmd.io/_uploads/ryO7JK0fa.gif) i. 建立兩個檔案分別對應 Publisher 和 Subscriber。 ii. Publisher 的程式範例如下。 ```python= #!/usr/bin/python3 import rospy from std_msgs.msg import Bool import RPi.GPIO as gp gp.setmode(gp.BCM) gp.setup(7, gp.IN) def publisher_main(): rospy.init_node("publisher") publisher = rospy.Publisher("gpio7", Bool, queue_size = 10) rate = rospy.Rate(10) while not rospy.is_shutdown(): rospy.loginfo("Send: " + str(gp.input(7))) publisher.publish(gp.input(7)) rate.sleep() if __name__ == "__main__": try: publisher_main() except rospy.ROSInterruptException: pass ``` :::info 1. ```#!/usr/bin/python3``` 用來指定執行時所要用的直譯器。 2. ```rospy.init_node``` 用來初始化節點,參數為節點名稱。 3. ```publisher = rospy.Publisher("gpio7", Bool, queue_size = 10)``` 創建一個 Publisher,其所發布的 Topic 名稱為 gpio7、型態為 std_msgs 的 Bool,最大佇列長度為 10。 4. ```rate = rospy.Rate(10)``` 設定用於週期執行的物件,參數為頻率(Hz)。 5. ```rospy.is_shutdown()``` 檢查節點是否關閉。 6. ```rospy.loginfo(<string>)``` 會記錄 info 等級的 log。 7. ```publisher.publish()``` 用來發送資料,其形態需與先前定義的型態相同,如此處為 Bool。 8. ```rate.sleep()``` 會暫停直到下一個週期。 ::: iii. Subscriber 的程式範例如下。 ```python= #!/usr/bin/python3 import rospy from std_msgs.msg import Bool def callback(data): rospy.loginfo(f"Recive: {data.data}") def subscriber(): rospy.init_node("subscriber", anonymous=True) rospy.Subscriber("gpio7", Bool, callback) rospy.spin() if __name__ == "__main__": subscriber() ``` :::info 1. ```rospy.Subscriber``` 中的 callback 為訂閱的 Topic 有新的資料時所要執行的函數(函數的定義在 5、6 行) 2. ```rospy.spin()``` 為一無窮迴圈,該迴圈會在訂閱的 Topic 有新的資料時執行回調函數。 ::: 2. Service ![Service-SingleServiceClient.gif](https://hackmd.io/_uploads/BkJm45M7T.gif) i. Server 的程式範例如下。 ```python= #!/usr/bin/python3 import rospy import math from service_tutorial.srv import CalculateDistance, CalculateDistanceRequest, CalculateDistanceResponse def callback(request): response = CalculateDistanceResponse() response.distance = math.sqrt((request.position1.x - request.position2.x) ** 2 + (request.position1.y - request.position2.y) ** 2) print(f"Got Request: \n{request}\n") return response def server(): rospy.init_node("server") calculate_distance_server = rospy.Service("CalculateDistance", CalculateDistance, callback) print("\033[93mServer Started\033[0m") rospy.spin(); if __name__ == '__main__': server() ``` ii. Client 的程式範例如下。 ```python= #!/usr/bin/python3 import sys import rospy from service_tutorial.srv import * from service_tutorial.msg import * def client(): rospy.wait_for_service("CalculateDistance") pos1 = Coordinate2D(1.0, 1.0) pos2 = Coordinate2D(2.0, 2.0) calculate_distance = rospy.ServiceProxy("CalculateDistance", CalculateDistance) print(f"Got Response: {calculate_distance(pos1, pos2)}") if __name__ == '__main__': rospy.init_node("client") rate = rospy.Rate(0.5) while not rospy.is_shutdown(): client() rate.sleep() ``` iii. srv/CalculateDistance.srv ``` service_tutorial/Coordinate2D position1 service_tutorial/Coordinate2D position2 --- float32 distance ``` iv. msg/Coordinate2D.msg ``` float32 x float32 y ``` ### 編譯 1. 編輯套件資料夾底下的 ```CMakeLists.txt``` 檔,在檔案末加入以下指令(把所有節點的檔案都打上去): ``` catkin_install_python(PROGRAMS scripts/publisher.py scripts/subscriber.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} ) ``` 2. 如果有自訂的 msg、srv,則須在 ```package.xml``` 中加入以下內容: ``` <build_depend>message_generation</build_depend> <build_export_depend>message_generation</build_export_depend> <exec_depend>message_runtime</exec_depend> ``` 並在 ```CMakeLists.txt``` 中加入以下內容(如指令已經存在,則加入缺少的內容即可。例如 ```find_package``` 中加入 ```message_generation``` 即可,請勿重複輸入相同指令): ``` find_package(catkin REQUIRED COMPONENTS rospy std_msgs message_generation ) add_message_files( FILES Coordinate2D.msg ) add_service_files( FILES CalculateDistance.srv ) generate_messages( DEPENDENCIES std_msgs # Or other packages containing msgs ) ``` 自訂的 msg、srv 須分別放置在 msg、srv 資料夾,資料夾結構如下。 ![service tutorial dir structure](https://hackmd.io/_uploads/SJw-i9z76.png) 3. 編譯前請先確定在 Workspace 資料夾下(如本例中為 ros_tutorial_ws)。 4. 執行 ```catkin_make``` 即可編譯該 Workspace 底下的所有套件。 5. 完成後的資料夾結構如下。 ![Compiled Dir](https://hackmd.io/_uploads/Hkn2W1lXT.png) ### 執行 1. 使用 ```roscore``` 指令開啟 ROS 的主節點。 2. 開啟一個新的終端(可以使用 TMUX 終端管理工具,不過要注意上個步驟的 roscore 盡量不要在 TMUX 下運行,否則可能會有 URI 錯誤,此錯誤只會出現在部分版本),先輸入 ```source ~/neko_ws/devel/setup.bash``` 設定套件及 ROS 環境(請自行依據個人路徑更改,每次開啟一個終端都要 source 一次,如果輸入指令時顯示無法找到指令或無法找到套件,那八成是因為還沒設定環境),再輸入 ```rosrun topic_tutorial publisher.py``` 開始運行 Publisher,```topic_tutorial``` 及 ```publisher.py``` 請自行根據套件名稱及檔案名稱更改。 3. 再開啟一個新的終端,並輸入 ```rosrun topic_tutorial subscriber.py``` 運行 Subscriber。