1. 程式人生 > >第 2 課 讓你的 EV3 做點什麼

第 2 課 讓你的 EV3 做點什麼

介紹

上一課我們編寫了類 EV3,它可以用於與 LEGO EV3 裝置通訊。我們通過什麼也不做的 opNop 操作測試它。這一課是關於帶有引數的真實指令的。這將使你的 EV3 裝置成為你程式的活動部分。目前,我們不從我們的 EV3 接收資料。這個主題需要等稍後的一些課程。我們選取了如下這些種類的操作:

  • 設定 EV3 的名稱
  • 播放聲音和音調
  • 控制它的LED
  • 顯示影象
  • 定時器
  • 啟動程式
  • 模擬按鈕動作

請拿出 LEGO EV3 操作集的官方文件 EV3 Firmware Developer Kit,並閱讀它。LEGO 的官方文件 EV3 Communication Developer Kit 還包含一些直接命令的例子。

如果你沒有編寫 EV3 類,但想要執行本課的程式,你可以自由地從 ev3-python3 下載模組 ev3。程式中僅有的需要你修改的地方是 MAC 地址。把 00:16:53:42:2B:99 替換為你的 EV3 裝置的值。

設定 EV3 的名字

程式設計藝術的一個重要部分是選擇好的名字。我們思考事物的方式強烈依賴於我們為它使用的名字。因此我們從設定名字開始。為了把你的 EV3 的名字修改為 myEV3,你需要傳送如下的直接命令:

-------------------------------------------------                
 \ len \ cnt \ty\ hd  \op\cd\ Name               \               
  -------------------------------------------------              
0x|0E:00|2A:00|00|00:00|D4|08|84:6D:79:45:56:33:00|              
  -------------------------------------------------              
   \ 14  \ 42  \Re\ 0,0 \C \S \ "myEV3"            \             
    \     \     \  \     \o \E \                    \            
     \     \     \  \     \m \T \                    \           
      \     \     \  \     \_ \_ \                    \          
       \     \     \  \     \S \B \                    \         
        \     \     \  \     \e \R \                    \        
         \     \     \  \     \t \I \                    \       
          \     \     \  \     \  \C \                    \      
           \     \     \  \     \  \K \                    \     
            \     \     \  \     \  \N \                    \    
             \     \     \  \     \  \A \                    \   
              \     \     \  \     \  \M \                    \  
               \     \     \  \     \  \E \                    \ 
                -------------------------------------------------

響應是:

----------------    
 \ len \ cnt \rs\   
  ----------------  
0x|03:00|2A:00|02|  
  ----------------  
   \ 3   \ 42  \ok\ 
    ----------------

這說明,直接命令被成功執行了。你可以通過檢視 brick 顯示器來檢查操作的結果,在它的第一行應該顯示新名稱。此外,如果某些藍芽裝置搜尋裝置並找到了你的 EV3,它將以新的名字顯示。

幾點備註:

  • 我們使用了一個新操作,它執行一些設定:opCom_Set = 0x|D4|
  • 由於操作 opCom_Set 用於執行不同的設定,因而它的後面總是跟一個指定操作內容的 CMD。CMD 告訴我們,具體是哪一個。你可以把它們想成是一個兩位元組操作。但在 EV3 的術語中,它是一個操作(即 指令)和它的 CMD。
  • opCom_Set 的 CMD SET_BRICKNAME = 0x|08| 需要一個引數:NAME。在 LEGO 的操作描述中,你可以讀到:(DATA8) NAME – First character in character string。但事實上,我們傳送 0x|84:6D:79:45:56:33:00| 作為引數 NAME 的值。這需要一些解釋:
    • 0x|6D:79:45:56:33| 是字串 myEV3 的 ASCII 碼,其中 0x|6D| = “m”0x|79| = “y” 等等。
    • 0x|00| 終止字串(這被稱為零終止字串)。
    • 0x|84| 是 LCS 字串的前導標識位元組(以二進位制表示是:0b 1000 0100)。

結論是,你傳送給你的 EV3 作為操作的常量引數的每個字串,必須包含前導 0x|84| 和字尾 0x|00|。在我的情況中,連線的結果是LCS("myEV3") = 0x|84:6D:79:45:56:33:00|,作為引數 NAME 的值。

請給你的類 EV3 新增一個靜態類方法(在 Python 中,是一個模組級的函式):

  • LCS(value: str) 以 LCS 格式返回一個表示字串值的位元組陣列。

然後新增兩個常量 opCom_Set = 0x|D4|SET_BRICKNAME = 0x|08|

可以編寫一個小程式來修改名稱。我通過如下的程式碼來完成:

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1
ops = b''.join([
    ev3.opCom_Set,
    ev3.SET_BRICKNAME,
    ev3.LCS("myEV3")
])
my_ev3.send_direct_cmd(ops)

它的輸出是:

09:12:31.011558 Sent 0x|0E:00|2A:00|80|00:00|D4:08:84:6D:79:45:56:33:00|

請看一下你 EV3 的顯示器,它的名字改變了。

常量整數引數

字串是一種引數型別,但還有其它的。共同的是,引數的型別由前導位元組,標識位元組 標識。在這一課,我們集中於常量引數和區域性變數。術語 常量引數 並不是很精確,但它意味著引數具有如下兩個特性:

  • 它們是操作的引數。
  • 它們總是儲存值而永遠沒有地址。

EV3 直接命令支援如下格式的常量引數:

  • 字串(你已經學習了它們中的一個)
  • 整數值

字串的長度可變,整數是有符號的,且可以包含 5 位,8 位,16 位和 32 位。也許你沒有看到浮點數,但是沒有操作需要以浮點數作為引數。事實上,只有 5 種常量引數。

你應該特別專注於第一個位元組,即標識位元組,它定義了變數的型別和長度。標識位元組的位 0 (最高有效位) 代表長或短格式:

  • 0b 0... .... 短格式(只有一個位元組,標識位元組包含值)
  • 0b 1... .... 長格式(標識位元組不包含任何值的位)

位 5 (在長格式的情況下)代表長度型別

  • 0b .... .0.. 意味著固定長度。
  • 0b .... .1.. 意味著以零結束的字串。

位 6 和 7 (僅長格式)代表後續的整數的長度

  • 0b .... ..00 意味著可變長度,
  • 0b .... ..01 意味著後面有一個位元組,
  • 0b .... ..10 是說,後面有兩個位元組,
  • 0b .... ..11 是說,後面有四個位元組。

現在我們寫 5 個常量作為二進位制掩碼,其中 S 代表符號(0 是正的,1 是負的),V 代表值的一位。

  • LC00b 00SV VVVV,5 位整數值,範圍:-32 - 31,長度:1 位元組,由 2 個前導位 00 標識。整數值其實是 6 位的。
  • LC10b 1000 0001 SVVV VVVV,8 位整數值,範圍:-127 - 127,長度:2 位元組,由前導位元組 0x|81| 標識。值 0x|80|NaN
  • LC20b 1000 0010 VVVV VVVV SVVV VVVV,16 位整數值,範圍:-32,767 – 32,767,長度:3 位元組,由前導位元組 0x|82| 標識。值 0x|80:00|NaN
  • LC40b 1000 0011 VVVV VVVV VVVV VVVV VVVV VVVV SVVV VVVV,32 位整數值,範圍:-2,147,483,647 – 2,147,483,647,長度:5 位元組,由前導位元組 0x|83| 標識。值 0x|80:00:00:00|NaN
  • LCS0b 1000 0100 VVVV VVVV ... 0000 0000,以零結尾的字串,長度:可變,由前導位元組 0x|84| 標識。

LC2 和 LC4 的位元組序列是小尾端的。這意味著,正如你從第 1 課學到的那樣,標識位元組是頭部,後面的位元組與你習慣的順序相反。如果操作以整數常量作為引數,則可以在 LC0,LC1,LC2 或 LC4 之間進行選擇。對於小值(範圍在 -32 到 31 之間),用 LC0,對於非常大的值,用 LC4。直接命令從左到右讀取。當解釋一個引數的第一個位元組時,哪個附加位元組屬於它以及在哪裡找到該值是清楚的。總是使用最短的可能變體以消除通訊流量,並因此加速直接命令的操作,但這種影響很小。更多關於引數的標識位元組的細節可以在 LEGO 的 EV3 Firmware Developer Kit 的 3.4 節找到。

請給你的 EV3 類新增另外的靜態類方法(或模組方法):

  • LCX(value: int) 以格式 LC0,LC1,LC2 或 LC4 返回一個依賴於值的範圍的位元組陣列。

播放聲音檔案

我們想要我們的 EV3 brick 播放聲音檔案 /home/root/lms2012/sys/ui/DownloadSucces.rsf,這通過如下操作完成:

  • opSound = 0x|94|CMD PLAY = 0x|02|,且引數為:
    • VOLUME:百分比 [0 - 100]
    • NAME:聲音檔案的絕對路徑,或相對於 /home/root/lms2012/sys/ (不包含副檔名 “.rsf”)

程式:

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1

ops = b''.join([
    ev3.opSound,
    ev3.PLAY,
    ev3.LCX(100),                  # VOLUME
    ev3.LCS('./ui/DownloadSucces') # NAME
])
my_ev3.send_direct_cmd(ops)

輸出:

09:42:03.575103 Sent 0x|1E:00|2A:00|80|00:00|94:02:81:64:84:2E:2F:75:69:2F:44:6F:77:6E:6C:6F:61:64:53:75:63:63:65:73:00|

EV3 brick 的檔案系統不是本節課的主題。更多資訊請參考 Folder Structure

重複播放聲音檔案

操作 opSound 具有一個 CMD REPEAT,它以無限迴圈播放聲音檔案,這可以由操作 opSound 的 CMD BREAK 中斷。有兩個額外的操作:

  • opSound = 0x|94| 的 CMD REPEAT = 0x|03|,且具有引數:
    • VOLUME:百分比 [0 - 100]
    • NAME:聲音檔案的絕對路徑,或相對於 /home/root/lms2012/sys/ 的相對路徑 (不包含副檔名 “.rsf”)
  • opSound = 0x|94| 的 CMD BREAK = 0x|00|,沒有引數。

我們用如下的程式來測試它:

#!/usr/bin/env python3

import ev3, time

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1

ops = b''.join([
    ev3.opSound,
    ev3.REPEAT,
    ev3.LCX(100),                  # VOLUME
    ev3.LCS('./ui/DownloadSucces') # NAME
])
my_ev3.send_direct_cmd(ops)
time.sleep(5)
ops = b''.join([
    ev3.opSound,
    ev3.BREAK
])
my_ev3.send_direct_cmd(ops)

它播放聲音檔案 5 秒,然後停止播放。輸出為:

09:55:28.814320 Sent 0x|1E:00|2A:00|80|00:00|94:03:81:64:84:2E:2F:75:69:2F:44:6F:77:6E:6C:6F:61:64:53:75:63:63:65:73:00|
09:55:33.822352 Sent 0x|07:00|2B:00|80|00:00|94:00|

播放音調

我們想要我們的 EV3 brick 播放音調,這通過如下操作完成:

  • opSound = 0x|94|CMD TONE = 0x|01|,且引數為:
    • VOLUME:百分比 [0 - 100]
    • FREQUENCY:單位為 Hz,[250 - 10000]
    • DURATION:單位為毫秒(0 表示無限制)

播放一個 a’ 一秒的直接命令:

-------------------------------------------------        
 \ len \ cnt \ty\ hd  \op\cd\vo\ fr     \ du     \       
  -------------------------------------------------      
0x|0E:00|2A:00|80|00:00|94|01|01|82:B8:01|82:E8:03|      
  -------------------------------------------------      
   \ 14  \ 42  \no\ 0,0 \S \T \1 \ 440    \ 1000   \      
    \     \     \  \     \o \O \  \        \        \     
     \     \     \  \     \u \N \  \        \        \    
      \     \     \  \     \n \E \  \        \        \   
       \     \     \  \     \d \  \  \        \        \  
        -------------------------------------------------

程式傳送它:

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),    # VOLUME
    ev3.LCX(440),  # FREQUENCY
    ev3.LCX(1000), # DURATION
])
my_ev3.send_direct_cmd(ops)

儘管我們很自豪,但我們希望我們的 EV3 以 c’ 播放三和絃:

  • c’ (262 Hz)
  • e’ (330 Hz)
  • g’ (392 Hz)
  • c’’ (523 Hz)

我們把我們的程式修改為:

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(262),
    ev3.LCX(500),
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(330),
    ev3.LCX(500),
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(392),
    ev3.LCX(500),
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(2),
    ev3.LCX(523),
    ev3.LCX(1000)
])
my_ev3.send_direct_cmd(ops)

但我們只聽到了一個音調,即最後的那個 (c’’)。為什麼?

這是因為操作彼此中斷。你必須將操作視為不耐煩且表現不佳的角色。中斷是他們的標準。如果想要避免中斷,則必須明確告訴它。在播放聲音的場景中,可以通過如下操作完成:

  • opSound_Ready = 0x|96|

它將一直等到聲音結束。我們再次修改程式:

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
ops = b''.join([
        opSound,
        cmdSound_PlayTone,
        LCX(1),
        LCX(262),
        LCX(500),
        opSound_Ready,

        opSound,
        cmdSound_PlayTone,
        LCX(1),
        LCX(330),
        LCX(500),
        opSound_Ready,

        opSound,
        cmdSound_PlayTone,
        LCX(1),
        LCX(392),
        LCX(500),
        opSound_Ready,

        opSound,
        cmdSound_PlayTone,
        LCX(2),
        LCX(523),
        LCX(1000),
])
my_ev3.send_direct_cmd(ops)

現在我們聽到了我們期望的!

修改 LEDs 的顏色

我們的 EV3 永遠不會達到真正的自動點唱機的質量,但為什麼不新增一些燈光效果?這需要一個新操作:

  • opUI_Write = 0x|82|CMD LED = 0x|1B|,且引數為:
    • PATTERNGREEN = 0x|01|RED = 0x|02|,等等。

LED Patterns 可以取的值如下:

  • 0x00 : Led off
  • 0x01 : Led green
  • 0x02 : Led red
  • 0x03 : Led orange
  • 0x04 : Led green flashing
  • 0x05 : Led red flashing
  • 0x06 : Led orange flashing
  • 0x07 : Led green pulse
  • 0x08 : Led red pulse
  • 0x09 : Led orange pulse

我們再次給我們的程式新增一些程式碼:

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1
ops = b''.join([
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_RED,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(262),
    ev3.LCX(500),
    ev3.opSound_Ready,
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_GREEN,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(330),
    ev3.LCX(500),
    ev3.opSound_Ready,
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_RED,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(392),
    ev3.LCX(500),
    ev3.opSound_Ready,
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_RED_FLASH,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(2),
    ev3.LCX(523),
    ev3.LCX(2000),
    ev3.opSound_Ready,
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_GREEN
])
my_ev3.send_direct_cmd(ops)

我們傳送的是一個 60 位元組長的直接命令:

11:39:49.039902 Sent 0x|3C:00|2A:00|80|00:00|82:1B:02:94:01:01:82:06:01:82:F4:01:96:82:1B:01:94:01:01:82:4A:01:82:F4:01:96:82:1B:02:...

這小於它最大長度的 6%。

顯示影象

EV3 的顯示屏是單色的,解析度為 180 x 128 畫素。這聽起來有點過時,但允許顯示圖示和表情符號或繪製圖片。操作 opUI_Draw 具有大量不同的操作顯示器的 CMD。這裡我們使用其中四個:

  • opUI_Draw = 0x|84| 的 CMD UPDATE = 0x|00|,沒有引數。
  • opUI_Draw = 0x|84|CMD TOPLINE = 0x|12|,具有引數:
    • (Data8) ENABLE:啟用或禁用頂部狀態行, [0:禁用,1:啟用]
  • opUI_Draw = 0x|84| 的 CMD FILLWINDOW = 0x|13|,具有引數:
    • (Data8) COLOR:指定黑色或白色,[0:白色,1:黑色]
    • (Data16) Y0:指定 Y 起始點,[0 - 127]
    • (Data16) Y1:指定 Y 大小
  • opUI_Draw = 0x|84| 的 CMD BMPFILE = 0x|1C|,具有引數:
    • (Data8) COLOR:指定黑色或白色,[0:白色,1:黑色]
    • (Data16) X0:指定 X 起始點,[0 - 127]
    • (Data16) Y0:指定 Y 起始點,[0 - 127]
    • NAME:影象檔案的絕對路徑,或相對於 /home/root/lms2012/sys/(具有副檔名 “.rgf”)。該命令的名稱具有誤導性。該檔案的副檔名必須是 .rgf(代表 機器人圖形格式(robot graphic format))而不是 bmp 圖形的檔案。

我們執行這個程式:

#!/usr/bin/env python3

import ev3, time

my_ev3 = ev3.EV3(
    protocol=ev3.USB,
    host='00:16:53:42:2B:99'
)
my_ev3.verbosity = 1

ops = b''.join([
    ev3.opUI_Draw,
    ev3.TOPLINE,
    ev3.LCX(0),                                      # ENABLE
    ev3.opUI_Draw,
    ev3.BMPFILE,
    ev3.LCX(1),                                      # COLOR
    ev3.LCX(0),                                      # X0
    ev3.LCX(0),                                      # Y0
    ev3.LCS("../apps/Motor Control/MotorCtlAD.rgf"), # NAME
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
time.sleep(5)
ops = b''.join([
    ev3.opUI_Draw,
    ev3.TOPLINE,
    ev3.LCX(1),     # ENABLE
    ev3.opUI_Draw,
    ev3.FILLWINDOW,
    ev3.LCX(0),     # COLOR
    ev3.LCX(0),     # Y0
    ev3.LCX(0),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)

輸出為:

12:01:00.253855 Sent 0x|35:00|2A:00|80|00:00|84:12:00:84:1C:01:00:00:84:2E:2E:2F:61:70:70:...
12:01:05.265584 Sent 0x|0F:00|2B:00|80|00:00|84:12:01:84:13:00:00:00:84:00|

顯示屏顯示影象 MotorCtlAD.rgf 五秒鐘,然後顯示屏變為空,除了頂線。 一些註釋:

  • 繪製需要畫布。這是顯示的實際影象。我們新增一些元素,然後呼叫 UPDATE 使畫布可見。如果你更喜歡以空白畫布開始,則必須明確清除畫布的內容。
  • CMD TOPLINE 允許開啟或關閉頂線。
  • CMD FILLWINDOW 允許填充或擦除視窗的一部分。如果引數 Y0Y1 都為零,則表示整個顯示屏。
  • 將 CMD BMPFILE 的引數 COLOR 設定為值 0 會反轉影象的顏色。
  • 操作 opUI_Draw 允許儲存和恢復影象(CMD STORERESTORE)。 但是當實際的直接命令執行結束時,儲存的影象將丟失。

歡迎你測試更多操作 opUI_Draw 的 CMD。

區域性記憶體

在第 1 課我們讀到,本地記憶體是儲存中間資訊的地址空間。現在我們學習如何使用它,我們再次討論標識位元組,它定義變數的型別和長度。我們將編寫另一個函式 LVX,它返回本地記憶體的地址。如你所知,標識位元組的第 0 位代表短格式或長格式:

  • 0b 0... .... 短格式(只有一個位元組,標識位元組包含值)
  • 0b 1... .... 長格式(標識位元組不包含任何值的位)

如果位 1 和 2 是 0b .10. ....,,它們代表區域性變數,它們是區域性記憶體的地址。

位 6 和 7 代表後續的值的長度

  • 0b .... ..00 意味著可變長度,
  • 0b .... ..01 意味著後面有一個位元組,
  • 0b .... ..10 是說,後面有兩個位元組,
  • 0b .... ..11 是說,後面有四個位元組。

這允許將 4 個區域性變數寫為二進位制掩碼,我們不需要符號,因為地址總是正數。 V 代表地址(值)的一位。

  • LV00b 010V VVVV,5 位地址,範圍:0 - 31,長度:1 位元組,由 3 個前導位 010 標識。
  • LV10b 1100 0001 VVVV VVVV,8 位地址,範圍:0 - 255,長度:2 位元組,由前導位元組 0x|C1| 標識。
  • LV20b 1100 0010 VVVV VVVV VVVV VVVV,16 位地址,範圍:0 – 65, 535,長度:3 位元組,由前導位元組 0x|C2| 標識。
  • LV40b 1100 0011 VVVV VVVV VVVV VVVV VVVV VVVV VVVV VVVV,32 位地址,範圍:0 – 4,294,967,296,長度:5 位元組,由前導位元組 0x|C3| 標識。

一些說明:

  • 在直接命令中,不需要 LV2 和 LV4!你記得區域性記憶體最多有 63 個位元組。
  • 必須正確放置區域性記憶體的地址。 如果將 4 位元組值寫入區域性記憶體,則其地址必須為0,4,8,…(4的倍數)。對於 2 位元組值也是一樣,它們的地址必須是 2 的倍數。
  • 你需要將區域性記憶體拆分為所需長度的段,然後使用每個段的第一個位元組的地址。
  • 頭位元組包含區域性記憶體的總長度(有關詳細內容,請參閱第 1 課)。 不要忘記正確傳送頭位元組!

一個新的模組函式:LVX

請將函式 LVX(value) 新增到模組 ev3 中,它取決於實際值,返回 LV0,LV1,LV2 或 LV4 型別中最短的。 我已經完成了,現在我的 ev3 模組的文件如下:

FUNCTIONS
    LCS(value:str) -> bytes
        pack a string into a LCS
    
    LCX(value:int) -> bytes
        create a LC0, LC1, LC2, LC4, dependent from the value
    
    LVX(value:int) -> bytes
        create a LV0, LV1, LV2, LV4, dependent from the value

計時器

控制時間是實時程式的一個重要方面。我們已經看到如何等待音調結束,我們在本地程式中等待,直到我們停止重複播放的聲音檔案。EV3 的操作集包含計時器操作,它們允許在直接命令的執行中等待。我們使用以下兩個操作:

  • opTimer_Wait = 0x|85|,具有引數:

    • (Data32) TIME:等待的時間(單位為毫秒)
    • (Data32) TIMER:用於計時的變數
      這個操作向區域性或全域性記憶體中寫入 4 個位元組的時間戳
  • opTimer_Ready = 0x|86|,具有引數:

    • (Data32) TIMER:用於計時的變數
      這個操作讀取時間戳並等待直到實際時間到達這個時間戳的值。

我們用一個繪製三角形的程式測試計時器操作。這需要操作 opUI_Draw 的另一個 CMD:

  • opUI_Draw = 0x|84| 的 CMD LINE = 0x|03|,具有引數:
    • (Data8) COLOR:指定黑色或白色,[0:白色,1:黑色]
    • (Data16) X0:指定 X 起始點,[0 - 177]
    • (Data16) Y0:指定 Y 起始點,[0 - 127]
    • (Data16) X1:指定 X 結束點
    • (Data16) Y1:指定 Y 結束點

程式:

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opUI_Draw,
    ev3.TOPLINE,
    ev3.LCX(0),     # ENABLE
    ev3.opUI_Draw,
    ev3.FILLWINDOW,
    ev3.LCX(0),     # COLOR
    ev3.LCX(0),     # Y0
    ev3.LCX(0),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE,
    ev3.opTimer_Wait,
    ev3.LCX(1000),
    ev3.LVX(0),
    ev3.opTimer_Ready,
    ev3.LVX(0),
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(2),     # X0
    ev3.LCX(125),   # Y0
    ev3.LCX(88),    # X1
    ev3.LCX(2),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE,
    ev3.opTimer_Wait,
    ev3.LCX(500),
    ev3.LVX(0),
    ev3.opTimer_Ready,
    ev3.LVX(0),
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(88),    # X0
    ev3.LCX(2),     # Y0
    ev3.LCX(175),   # X1
    ev3.LCX(125),   # Y1
    ev3.opUI_Draw,
    ev3.UPDATE,
    ev3.opTimer_Wait,
    ev3.LCX(500),
    ev3.LVX(0),
    ev3.opTimer_Ready,
    ev3.LVX(0),
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(175),   # X0
    ev3.LCX(125),   # Y0
    ev3.LCX(2),     # X1
    ev3.LCX(125),   # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops, local_mem=4)

這個程式清除顯示屏,然後等待一秒,繪製一條線,等待半秒,繪製第二條線,等待並最終繪製第三條線。它需要 4 個位元組的本地記憶體,可以多次寫入和讀出。

顯然,計時可以在本地程式或直接命令中完成。 我們修改程式:

#!/usr/bin/env python3

import ev3, time

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opUI_Draw,
    ev3.TOPLINE,
    ev3.LCX(0),     # ENABLE
    ev3.opUI_Draw,
    ev3.FILLWINDOW,
    ev3.LCX(0),     # COLOR
    ev3.LCX(0),     # Y0
    ev3.LCX(0),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
time.sleep(1)
ops = b''.join([
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(2),     # X0
    ev3.LCX(125),   # Y0
    ev3.LCX(88),    # X1
    ev3.LCX(2),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
time.sleep(0.5)
ops = b''.join([
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(88),    # X0
    ev3.LCX(2),     # Y0
    ev3.LCX(175),   # X1
    ev3.LCX(125),   # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
time.sleep(0.5)
ops = b''.join([
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(175),   # X0
    ev3.LCX(125),   # Y0
    ev3.LCX(2),     # X1
    ev3.LCX(125),   # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)

兩種方案下,顯示器具有相同的行為,但又有些不同。 第一個版本所需的通訊量較少,但它會阻塞 EV3,直到直接命令執行結束。第二個版本需要四個直接命令,但允許在繪圖休眠時傳送其它直接命令。

啟動程式

直接命令可以啟動程式。通常你通過按下 EV3 裝置的按鈕完成。程式是一個副檔名為 “.rbf” 的檔案,它存放在 EV3 的檔案系統上。我們將啟動程式 /home/root/lms2012/apps/Motor Control/Motor Control.rbf。這需要兩個新操作:

  • opFile = 0x|C0| 的 CMD LOAD_IMAGE = 0x|08|,具有引數:

    • (Data16) PRGID:程式執行的 Slot。值 0x|01| 用於執行使用者工程,apps 和工具。
    • (Data8) NAME:可執行檔案的完整路徑,或相對於 /home/root/lms2012/sys/(具有副檔名 “.rbf”)的路徑
      返回:
    • (Data32) SIZE:映象的位元組大小
    • (Data32) *IP:映象的地址
      這個操作是 載入器。它把程式載入進記憶體中,並準備執行。
  • opProgram_Start = 0x|C0|,具有引數:

    • (Data16) PRGID:程式執行的 Slot。
    • (Data32) SIZE:映象的位元組大小
    • (Data32) *IP:映象的地址
    • (Data8) DEBUG:除錯模式,值 0 代表普通模式

程式:

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1

ops = b''.join([
    ev3.opFile,
    ev3.LOAD_IMAGE,
    ev3.LCX(1),                                         # SLOT
    ev3.LCS('../apps/Motor Control/Motor Control.rbf'), # NAME
    ev3.LVX(0),                                         # SIZE
    ev3.LVX(4),                                         # IP*
    ev3.opProgram_Start,
    ev3.LCX(1),                                         # SLOT
    ev3.LVX(0),                                         # SIZE
    ev3.LVX(4),                                         # IP*
    ev3.LCX(0)                                          # DEBUG
])
my_ev3.send_direct_cmd(ops, local_mem=8)

第一個操作的返回值是 SIZEIP*。我們把它們寫入區域性記憶體的地址 0 和 4。第二個操作從區域性記憶體讀取它的引數 SIZEIP*。它的引數 SLOTDEBUG 是給定的常量值。程式的輸出是:

12:50:45.332826 Sent 0x|38:00|2A:00|80|00:20|C0:08:01:84:2E:2E:2F:61:70:70:73:2F:4D:6F:74:6F:...

它真的啟動了程式 /home/root/lms2012/apps/Motor Control/Motor Control.rbf

譯者注:

  • 關於操作命令的引數和返回值。要傳遞引數時,將引數值直接附加到操作命令的後面,並進行適當的編碼即可。對於操作命令的返回值,EV3 的直接命令虛擬機器的處理方式,非常類似與傳出引數。即需要針對每一個返回值,傳入一個指標,告訴操作命令把返回值放在指標所指向的位置。EV3 中有兩種型別的指標,分別是全域性記憶體指標和區域性記憶體指標。兩者的主要差異在於,全域性記憶體中的資料,在命令執行之後,我們的程式可以全部通過讀操作,從 EV3 裝置中讀取出來,而區域性記憶體則不會。全域性記憶體和區域性記憶體的分配,則是通過直接命令的頭部,告訴 EV3 中的直接命令虛擬機器各為它們分配多少位元組。儲存返回值內容的記憶體的指標的具體值,需要開發者自己根據傳出引數的型別長度進行手動計算。因此,EV3 的直接命令的操作命令,沒有我們寫程式碼時一般意義上的那種函式或方法,或者可以說,EV3 的操作命令的原型都是下面這樣的:
void opXXX_cmdYYY(arg1, arg2, arg3, ..., *ret1, *ret2, ...)

如上面,這裡的小程式,指定通過區域性記憶體 LVX(0) / LVX(4) 接收操作命令 opFile/LOAD_IMAGE 的返回值。上面那段程式,也可以寫為通過全域性記憶體來接收操作命令 opFile/LOAD_IMAGE 的返回值,如:

    def start_program(self, exe_file_path: str):
        ops = b''.join([
            opFile,
            cmdFile_LoadImage,
            PRGID_USER,
            LCS(exe_file_path),
            GVX(0),
            GVX(4)
        ])

        ret = self.send_direct_cmd(ops=ops, global_mem=8)

        ops = b''.join([
            opProgram_Start,
            PRGID_USER,
            GVX(0),
            GVX(4),
            debugMode_Normal
        ])
        ret = self.send_direct_cmd(ops = ops)

上面這段程式碼使用全域性記憶體接收操作命令 opFile/LOAD_IMAGE 的返回值。第一個 send_direct_cmd() 呼叫的返回值 ret 中包含了操作命令 opFile/LOAD_IMAGE 的返回值。但注意操作 opProgram_Start 的說明。直接把操作命令 opFile/LOAD_IMAGE 的返回值傳給 opProgram_Start 是不行的,如下面這樣:

    def start_program(self, exe_file_path: str):
        ops = b''.join([
            opFile,
            cmdFile_LoadImage,
            PRGID_USER,
            LCS(exe_file_path),
            GVX(0),
            GVX(4)
        ])

        ret = self.send_direct_cmd(ops=ops, global_mem=8)

        ops = b''.join([
            opProgram_Start,
            PRGID_USER,
            ret[5:],
            debugMode_Normal
        ])
        ret = self.send_direct_cmd(ops = ops)

以這種直接傳值的方式,程式無法如預期執行。

模擬按鈕按下

在這個例子中,我們通過模擬如下的按鈕按下事件關閉 EV3 brick:

  • BACK_BUTTON = 0x|06|
  • RIGHT_BUTTON = 0x|04|
  • ENTER_BUTTON = 0x|02|

我們需要等待直到初始化操作完成。這可以通過操作 opUI_Button 的 CMD WAIT_FOR_PRESS 完成,這再次預防了中斷。使用下面的新操作:

  • opUI_Button = 0x|83| 的 CMD PRESS = 0x|05|,具有引數:
    • BUTTON:Up Button = 0x|01|,Enter Button = 0x|02|,等等。
  • opUI_Button = 0x|83| 的 CMD WAIT_FOR_PRESS = 0x|03|

直接命令具有如下的結構:

-------------------------------------------------------------                 
 \ len \ cnt \ty\ hd  \op\cd\bu\op\cd\op\cd\bu\op\cd\op\cd\bu\                
  -------------------------------------------------------------               
0x|12:00|2A:00|80|00:00|83|05|06|83|03|83|05|04|83|03|83|05|02|               
  -------------------------------------------------------------               
   \ 18  \ 42  \no\ 0,0 \U \P \B \U \W \U \P \R \U \W \U \P \E \              
    \     \     \  \     \I \R \A \I \A \I \R \I \I \A \I \R \N \             
     \     \     \  \     \_ \E \C \_ \I \_ \E \G \_ \I \_ \E \T \            
      \     \     \  \     \B \S \K \B \T \B \S \H \B \T \B \S \E \           
       \     \     \  \     \U \S \_ \U \_ \U \S \T \U \_ \U \S \R \          
        \     \     \  \     \T \  \B \T \F \T \  \_ \T \F \T \  \_ \         
         \     \     \  \     \T \  \U \T \O \T \  \B \T \O \T \  \B \        
          \     \     \  \     \O \  \T \O \R \O \  \U \O \R \O \  \U \       
           \     \     \  \     \N \  \T \N \_ \N \  \T \N \_ \N \  \T \      
            \     \     \  \     \  \  \O \  \P \  \  \T \  \P \  \  \T \     
             \     \     \  \     \  \  \N \  \R \  \  \O \  \R \  \  \O \    
              \     \     \  \     \  \  \  \  \E \  \  \N \  \E \  \  \N \   
               \     \     \  \     \  \  \  \  \S \  \  \  \  \S \  \  \  \  
                \     \     \  \     \  \  \  \  \S \  \  \  \  \S \  \  \  \ 
                 -------------------------------------------------------------

我的對應的程式是:

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.BACK_BUTTON,
    ev3.opUI_Button,
    ev3.WAIT_FOR_PRESS,
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.RIGHT_BUTTON,
    ev3.opUI_Button,
    ev3.WAIT_FOR_PRESS,
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.ENTER_BUTTON
])
my_ev3.send_direct_cmd(ops)

這真的關閉了EV3裝置!

沒有必要回復,但我是一個好奇的人。我的問題是:EV3會在它關閉之前回復還是不回覆?

#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1
my_ev3.sync_mode = ev3.SYNC
ops = b''.join([
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.BACK_BUTTON,
    ev3.opUI_Button,
    ev3.WAIT_FOR_PRESS,
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.RIGHT_BUTTON,
    ev3.opUI_Button,
    ev3.WAIT_FOR_PRESS,
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.ENTER_BUTTON
])
my_ev3.send_direct_cmd(ops)

在我按下另一個按鈕之前沒有任何反應,然後它回覆並關閉。這並不令人驚訝,這是不一致的。 關機和回覆不合適一起,EV3 裝置無法完成命令然後傳送回覆!

我們學到了什麼

  • 直接命令由操作序列組成。當我們給 brick 傳送直接命令時,一個操作接一個操作的執行。但它們彼此互相打斷,如果想要它們等待的話需要特殊的操作。
  • 大多數操作需要引數,它們可以以格式 LC0,LC1,LC2 和 LC4 傳送,這些都包含有符號整數,但具有不同的範圍。另一種格式是 LCS,用於字串。它以0x|84| 開頭,然後是零終止的 ASCII 碼串。
  • 區域性變數(LV0,LV1,LV2 和 LV4)允許定址儲存中間資料的區域性儲存器。
  • 一些操作具有許多 CMD,它們使用不同的引數集定義不同的任務。
  • 我們已經看過很多操作並知道他們的引數的含義,但這只是 EV3 操作集的一小部分。

結論

我們關於直接命令的知識增長了,我們的類 EV3 也是。新增我們需要的所有常量需要一些耐心。隨著運算元量的增加,直接命令的參考文件 EV3 Firmware Developer Kit 需要更加仔細地閱讀。

這是我的函式和資料的實際狀態:

Help on module ev3:

NAME
    ev3 - LEGO EV3 direct commands

CLASSES
    builtins.object
        EV3
    
    class EV3(builtins.object)
     ...

FUNCTIONS
    LCS(value:str) -> bytes
        pack a string into a LCS
    
    LCX(value:int) -> bytes
        create a LC0, LC1, LC2, LC4, dependent from the value
    
    LVX(value:int) -> bytes
        create a LV0, LV1, LV2, LV4, dependent from the value

DATA
    ASYNC = 'ASYNC'
    BACK_BUTTON = b'\x06'
    BLUETOOTH = 'Bluetooth'
    BMPFILE = b'\x1c'
    BREAK = b'\x00'
    ENTER_BUTTON = b'\x02'
    FILLWINDOW = b'\x13'
    LED = b'\x1b'
    LED_OFF = b'\x00'
    LED_GREEN = b'\x01'
    LED_GREEN_FLASH = b'\x04'
    LED_GREEN_PULSE = b'\x07'
    LED_ORANGE = b'\x03'
    LED_ORANGE_FLASH = b'\x06'
    LED_ORANGE_PULSE = b'\t'
    LED_RED = b'\x02'
    LED_RED_FLASH = b'\x05'
    LED_RED_PULSE = b'\x08'
    LINE = b'\x03'
    LOAD_IMAGE = b'\x08'
    PLAY = b'\x02'
    PRESS = b'\x53'
    REPEAT = b'\x02'
    RIGHT_BUTTON = b'\x04'
    SET_BRICKNAME = b'\x08'
    STD = 'STD'
    SYNC = 'SYNC'
    TONE = b'\x01'
    TOPLINE = b'\x12'
    USB = 'Usb'
    UPDATE = b'\x00'
    WAIT_FOR_PRESS = b'\x03'
    WIFI = 'Wifi'
    opCom_Set = b'\xd4'
    opFile = b'\xc0'
    opNop = b'\x01'
    opProgram_Start = b'\x03'
    opSound = b'\x94'
    opSound_Ready = b'\x96'
    opTimer_Wait = b'\x85'
    opTimer_Ready = b'\x86'
    opUI_Button = b'\x83'
    opUI_Draw = b'\x84'
    opUI_Write = b'\x82'

真正的機器人從其感測器讀取資料並通過其電機進行運動。目前我們的 EV3 裝置都沒有。我也知道,存在更酷的聲音或光效的電子裝置。現在,你可以測試在 EV3 Firmware Developer Kit 中找到的其他一些操作了。

保持聯絡,下一課將是關於電機的。我希望,我們將更接近你真正感興趣的話題。

原文