EV3 直接命令 - 第 5 课 从 EV3 的传感器读取数据
讀取傳感器的類型和模式
我們從 EV3 設(shè)備的一些自反映開始并詢問它:
請給你的 EV3 發(fā)送如下的直接命令:
---------------------------------------- \ len \ cnt \ty\ hd \op\cd\la\no\ty\mo\ ---------------------------------------- 0x|0B:00|2A:00|00|02:00|99|05|00|10|60|61| ---------------------------------------- \ 11 \ 42 \re\ 0,2 \I \G \0 \16\0 \1 \ \ \ \ \ \n \E \ \ \ \ \ \ \ \ \ \p \T \ \ \ \ \ \ \ \ \ \u \_ \ \ \ \ \ \ \ \ \ \t \T \ \ \ \ \ \ \ \ \ \_ \Y \ \ \ \ \ \ \ \ \ \D \P \ \ \ \ \ \ \ \ \ \e \E \ \ \ \ \ \ \ \ \ \v \M \ \ \ \ \ \ \ \ \ \i \O \ \ \ \ \ \ \ \ \ \c \D \ \ \ \ \ \ \ \ \ \e \E \ \ \ \ \ ----------------------------------------一些說明:
- 我們以 CMD GET_TYPEMODE 使用操作 opInput_device
- 如果被用作傳感器端口,電機(jī)端口 A 的編號(hào)為 16。
- 我們將獲得兩個(gè)數(shù)作為應(yīng)答,類型和模式。
- 類型需要一個(gè)字節(jié)且將占據(jù)字節(jié) 0 的位置,模式也需要一個(gè)字節(jié)且被放在字節(jié) 1 的位置。
我獲得了如下的回答:
---------------------- \ len \ cnt \rs\ty\mo\ ---------------------- 0x|05:00|2A:00|02|07|00| ---------------------- \ 5 \ 42 \ok\7 \0 \ ----------------------它是說:電機(jī)端口 A 處的傳感器具有類型 7 且實(shí)際處于模式 0 中。如果你看一下文檔 EV3 Firmware Developer Kit 的第 5 章,其標(biāo)題為 Device type list,你發(fā)現(xiàn)類型 7 和模式 0 代表 EV3-Large-Motor-Degree。 ### EV3 Firmware Developer Kit。
讀取電機(jī)的實(shí)際位置
我們來到一個(gè)非常有趣的問題:電機(jī)端口 A 的電機(jī)實(shí)際位置是多少?我發(fā)送了這個(gè)命令:
---------------------------------------------- \ len \ cnt \ty\ hd \op\cd\la\no\ty\mo\va\v1\ ---------------------------------------------- 0x|0D:00|2A:00|00|04:00|99|1C|00|10|07|00|01|60| ---------------------------------------------- \ 13 \ 42 \re\ 0,4 \I \R \0 \16\E \D \1 \0 \ \ \ \ \ \n \E \ \ \V \e \ \ \ \ \ \ \ \p \A \ \ \3 \g \ \ \ \ \ \ \ \u \D \ \ \- \r \ \ \ \ \ \ \ \t \Y \ \ \L \e \ \ \ \ \ \ \ \_ \_ \ \ \a \e \ \ \ \ \ \ \ \D \R \ \ \r \ \ \ \ \ \ \ \ \e \A \ \ \g \ \ \ \ \ \ \ \ \v \W \ \ \e \ \ \ \ \ \ \ \ \i \ \ \ \- \ \ \ \ \ \ \ \ \c \ \ \ \M \ \ \ \ \ \ \ \ \e \ \ \ \o \ \ \ \ \ \ \ \ \ \ \ \ \t \ \ \ \ \ \ \ \ \ \ \ \ \o \ \ \ \ \ \ \ \ \ \ \ \ \r \ \ \ \ ----------------------------------------------我得到了答復(fù):
---------------------------- \ len \ cnt \rs\ degrees \ ---------------------------- 0x|07:00|2A:00|02|00:00:00:00| ---------------------------- \ 7 \ 42 \ok\ 0 \ ----------------------------然后我用手移動(dòng)電機(jī)并再次發(fā)送相同的直接命令。這次回復(fù)是:
---------------------------- \ len \ cnt \rs\ degrees \ ---------------------------- 0x|07:00|2A:00|02|50:07:00:00| ---------------------------- \ 7 \ 42 \ok\ 1872 \ ----------------------------那是說:電機(jī)移動(dòng)了 1, 872 度(5.2 周)。這似乎是對的!
技術(shù)細(xì)節(jié)
是時(shí)候看一下幕后的東西了!你需要理解:
- 端口編號(hào)的系統(tǒng),
- 我們使用的操作的參數(shù),和
- 如何定位并解包全局內(nèi)存。
端口編號(hào)的系統(tǒng)
傳感器有四個(gè)端口,電機(jī)有四個(gè)端口。傳感器端口的編號(hào)是 1 到 4:
- 端口 1: PORT = 0x|00| 或 LCX(0)
- 端口 2: PORT = 0x|01| 或 LCX(1)
- 端口 3: PORT = 0x|02| 或 LCX(2)
- 端口 4: PORT = 0x|03| 或 LCX(3)
這似乎有點(diǎn)滑稽,但計(jì)算機(jī)通常從數(shù)字 0 開始計(jì)數(shù),人類從數(shù)字 1 開始計(jì)數(shù)。我們剛剛了解到,電機(jī)也是傳感器,我們可以從中讀取電機(jī)的實(shí)際位置。電機(jī)端口標(biāo)為字母 A 到 D,但通過如下方式定位:
- 端口 A: PORT = 0x|10| 或 LCX(16)
- 端口 B: PORT = 0x|11| 或 LCX(17)
- 端口 C: PORT = 0x|12| 或 LCX(18)
- 端口 D: PORT = 0x|13| 或 LCX(19)
我在我的模塊 ev3.py 中添加了一個(gè)小函數(shù):
def port_motor_input(port_output: int) -> bytes:"""get corresponding input motor port (from output motor port)"""if port_output == PORT_A:return LCX(16)elif port_output == PORT_B:return LCX(17)elif port_output == PORT_C:return LCX(18)elif port_output == PORT_D:return LCX(19)else:raise ValueError("port_output needs to be one of the port numbers [1, 2, 4, 8]")從電機(jī)輸出端口轉(zhuǎn)換到輸入端口。
操作 opInput_Device
opInput_Device 的兩個(gè)變體的簡短描述,我們已經(jīng)使用了:
- opInput_Device = 0x|99| 的 CMD GET_TYPEMODE = 0x|05|:
參數(shù)- (Data8) LAYER:鏈 layer 號(hào)
- (Data8) NO:端口編號(hào)
返回值
- (Data8) TYPE:設(shè)備類型
- (Data8) MODE:設(shè)備模式
- opInput_Device = 0x|99| 的 CMD READY_RAW = 0x|1C|:
參數(shù)- (Data8) LAYER:鏈 layer 號(hào)
- (Data8) NO:端口編號(hào)
- (Data8) TYPE:設(shè)備類型
- (Data8) MODE:設(shè)備模式
- (Data8) VALUES:返回值的個(gè)數(shù)
返回值 - (Data32) VALUE1:以特定模式從傳感器接收的第一個(gè)值
這里 Data32 是說這是一個(gè) 32 位有符號(hào)整數(shù)。 返回的數(shù)據(jù)是值,但請記住,返回參數(shù)如 VALUE1 是引用。引用是局部或全局內(nèi)存的地址。閱讀下一部分了解詳情。
尋址全局內(nèi)存
在第 2 課中,我們介紹了常量參數(shù)和局部變量。你將記得,我們已經(jīng)看到了 LCS,LC0,LC1,LC2,LC4,LV0,LV1,LV2 和 LV4,并寫了三個(gè)函數(shù) LCX(value:int),LVX(value:int) 和 LCS(value:str):
FUNCTIONSLCS(value:str) -> bytespack a string into a LCSLCX(value:int) -> bytescreate a LC0, LC1, LC2, LC4, dependent from the valueLVX(value:int) -> bytescreate a LV0, LV1, LV2, LV4, dependent from the value我們討論了標(biāo)識(shí)字節(jié),它定義了變量的類型和長度:
現(xiàn)在我們編寫另一個(gè)函數(shù) GVX,它返回全局內(nèi)存的地址。如你已經(jīng)知道的那樣,標(biāo)識(shí)字節(jié)的位 0 代表短格式或長格式:
- 0b 0... .... 短格式(只有一個(gè)字節(jié),標(biāo)識(shí)字節(jié)包含值)
- 0b 1... .... 長格式(標(biāo)識(shí)字節(jié)不包含任何值的位)
如果位 1 和 2 是 0b .11. ....,它們代表全局變量,它們是全局內(nèi)存的地址。
位 6 和 7 代表后續(xù)的值的長度
- 0b .... ..00 意味著可變長度,
- 0b .... ..01 意味著后面有一個(gè)字節(jié),
- 0b .... ..10 是說,后面有兩個(gè)字節(jié),
- 0b .... ..11 是說,后面有四個(gè)字節(jié)。
現(xiàn)在我們寫 4 個(gè)全局變量作為二進(jìn)制掩碼,我們不需要符號(hào),因?yàn)榈刂房偸钦龜?shù)。 V 代表地址(值)的一位。
- GV0: 0b 011V VVVV,5 位地址,范圍:0 - 31,長度:1 字節(jié),由前導(dǎo)位 011 標(biāo)識(shí)。
- GV1:0b 1110 0001 VVVV VVVV,8 位地址,范圍:0 - 255,長度:2 字節(jié),由前導(dǎo)字節(jié) 0x|E1| 標(biāo)識(shí)。
- GV2:0b 1110 0010 VVVV VVVV VVVV VVVV,16 位地址,范圍:0 – 65.536,長度:3 字節(jié),由前導(dǎo)字節(jié) 0x|E2| 標(biāo)識(shí)。
- GV4:0b 1110 0011 VVVV VVVV VVVV VVVV VVVV VVVV VVVV VVVV,32 位地址,范圍:0 – 4,294,967,296,長度:5 字節(jié),由前導(dǎo)字節(jié) 0x|E3| 標(biāo)識(shí)。
一些說明:
- 在直接命令中,不需要 GV4!你記得全局內(nèi)存最多有 1019 個(gè)字節(jié) (1024 - 5)。
- 必須正確放置全局內(nèi)存的地址。 如果將 4 字節(jié)值寫入全局內(nèi)存,則其地址必須為 0,4,8,…(4的倍數(shù))。 對于 2 字節(jié)值也是一樣,它們的地址必須是 2 的倍數(shù)。
- 你將需要將全局內(nèi)存拆分為所需長度的段,然后使用每個(gè)段的第一個(gè)字節(jié)的地址。我們的第一個(gè)例子中,我們需要兩個(gè)段(類型和模式),每個(gè)段一個(gè)字節(jié)。因此我們使用 GV0(0) 和 GV0(1) 作為地址。
- 頭字節(jié)包含全局內(nèi)存的總長度(有關(guān)詳細(xì)內(nèi)容,請參閱第 1 課)。在我們的例子中,這些是2個(gè)字節(jié) resp. 4字節(jié)。不要忘記正確發(fā)送頭字節(jié)!
- 不要在段之間留下空隙!標(biāo)準(zhǔn)的工具諸如 struct.unpack 不喜歡它們。把 4 字節(jié)類型放在前面,然后是 2 字節(jié)類型以此類推。這使得對拆包進(jìn)行編碼比較方便。
一個(gè)新模塊函數(shù):GVX
請給你的 ev3 模塊添加一個(gè)函數(shù) GVX(value),依賴于值,它返回 GV0,GV1,GV2或 GV4 中最短的類型。我已經(jīng)完成了,現(xiàn)在我的模塊 ev3 的文檔如下:
FUNCTIONSGVX(value:int) -> bytescreate a GV0, GV1, GV2, GV4, dependent from the valueLCS(value:str) -> bytespack a string into a LCSLCX(value:int) -> bytescreate a LC0, LC1, LC2, LC4, dependent from the valueLVX(value:int) -> bytescreate a LV0, LV1, LV2, LV4, dependent from the valueport_motor_input(port_output:int) -> bytesget corresponding input motor port (from output motor port)解包全局內(nèi)存
我已經(jīng)提到,已經(jīng)有了解包全局內(nèi)存的好工具了。在 Python 3 中,這個(gè)工具是 struct — Interpret bytes as packed binary data 。
一字節(jié)無符號(hào)整數(shù)
我的從電機(jī)端口 A 讀取模式和類型的程序:
#!/usr/bin/env python3import ev3, structmy_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99') my_ev3.verbosity = 1ops = b''.join([ev3.opInput_Device,ev3.GET_TYPEMODE,ev3.LCX(0), # LAYERev3.port_motor_input(PORT_A), # NOev3.GVX(0), # TYPEev3.GVX(1) # MODE ]) reply = my_ev3.send_direct_cmd(ops, global_mem=2) (type, mode) = struct.unpack('BB', reply[5:]) print("type: {}, mode: {}".format(type, mode))模式 ‘BB’ 把全局內(nèi)存分為兩個(gè) 1 字節(jié)的無符號(hào)整數(shù)值。這個(gè)程序的輸出是:
08:08:13.477998 Sent 0x|0B:00|2A:00|00|02:00|99:05:00:10:60:61| 08:08:13.558793 Recv 0x|05:00|2A:00|02|07:00| type: 7, mode: 0四個(gè)字節(jié)的浮點(diǎn)數(shù)和四個(gè)字節(jié)的有符號(hào)整數(shù)
我的讀取端口 A 和端口 D 上的電機(jī)的電機(jī)位置的程序:
#!/usr/bin/env python3import ev3, structmy_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99') my_ev3.verbosity = 1ops = b''.join([ev3.opInput_Device,ev3.READY_SI,ev3.LCX(0), # LAYERev3.port_motor_input(PORT_A), # NOev3.LCX(7), # TYPEev3.LCX(0), # MODEev3.LCX(1), # VALUESev3.GVX(0), # VALUE1ev3.opInput_Device,ev3.READY_RAW,ev3.LCX(0), # LAYERev3.port_motor_input(PORT_D), # NOev3.LCX(7), # TYPEev3.LCX(0), # MODEev3.LCX(1), # VALUESev3.GVX(4) # VALUE1 ]) reply = my_ev3.send_direct_cmd(ops, global_mem=8) (pos_a, pos_d) = struct.unpack('<fi', reply[5:]) print("positions in degrees (ports A and D): {} and {}".format(pos_a, pos_d))格式 ‘<fi’ 將全局內(nèi)存分為一個(gè) 4 字節(jié)的浮點(diǎn)數(shù)和一個(gè) 4 字節(jié)的有符號(hào)整數(shù),都是小尾端的。輸出是:
08:32:32.865522 Sent 0x|15:00|2A:00|00|08:00|99:1D:00:10:07:00:01:60:99:1C:00:13:07:00:01:64| 08:32:32.949266 Recv 0x|0B:00|2A:00|02|00:80:6C:C4:54:04:00:00| positions in degrees (ports A and D): -946.0 and 1108字符串
我們讀取 EV3 設(shè)備的名字:
#!/usr/bin/env python3import ev3, structmy_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99') my_ev3.verbosity = 1ops = b''.join([ev3.opCom_Get,ev3.GET_BRICKNAME,ev3.LCX(16), # LENGTHev3.GVX(0) # NAME ]) reply = my_ev3.send_direct_cmd(ops, global_mem=16) (brickname,) = struct.unpack('16s', reply[5:]) brickname = brickname.split(b'\x00')[0] brickname = brickname.decode("ascii") print("Brickname:", brickname)說明:
- 格式 ‘16s’ 描述了一個(gè) 16 字節(jié)的字符串。
- brickname = brickname.split(b’\x00’)[0] 占據(jù)了以 0 結(jié)尾的字符串的第一部分。你需要那樣做是因?yàn)?EV3 設(shè)備不清除全局內(nèi)存。在字符串的右端部分也許有一些垃圾。等一會(huì)兒,然后我將演示這個(gè)問題。
- brickname = brickname.decode(“ascii”) 從字節(jié)類型創(chuàng)建一個(gè)字符串類型。
這個(gè)程序的輸出是:
08:55:00.098825 Sent 0x|0A:00|2B:00|00|10:00|D3:0D:81:20:60| 08:55:00.138258 Recv 0x|13:00|2B:00|02|6D:79:45:56:33:00:00:00:00:00:00:00:00:00:00:00| Brickname: myEV3帶有垃圾的字符串
我們發(fā)送兩個(gè)直接命令,第二個(gè)讀取一個(gè)字符串:
#!/usr/bin/env python3import ev3, structmy_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99') my_ev3.verbosity = 1ops = b''.join([ev3.opInput_Device,ev3.READY_SI,ev3.LCX(0), # LAYERev3.port_motor_input(PORT_A), # NOev3.LCX(7), # TYPEev3.LCX(0), # MODEev3.LCX(1), # VALUESev3.GVX(0), # VALUE1ev3.opInput_Device,ev3.READY_RAW,ev3.LCX(0), # LAYERev3.port_motor_input(PORT_D), # NOev3.LCX(7), # TYPEev3.LCX(0), # MODEev3.LCX(1), # VALUESev3.GVX(4) # VALUE1 ]) reply = my_ev3.send_direct_cmd(ops, global_mem=8) (pos_a, pos_d) = struct.unpack('<fi', reply[5:]) print("positions in degrees (ports A and D): {} and {}".format(pos_a, pos_d))ops = b''.join([ev3.opCom_Get,ev3.GET_BRICKNAME,ev3.LCX(16), # LENGTHev3.GVX(0) # NAME ]) reply = my_ev3.send_direct_cmd(ops, global_mem=16)這個(gè)程序的輸出是:
09:13:30.379771 Sent 0x|15:00|2A:00|00|08:00|99:1D:00:10:07:00:01:60:99:1C:00:13:07:00:01:64| 09:13:30.433495 Recv 0x|0B:00|2A:00|02|00:08:90:C5:FE:F0:FF:FF| positions in degrees (ports A and D): -4609.0 and -3842 09:13:30.433932 Sent 0x|0A:00|2B:00|00|10:00|D3:0D:81:20:60| 09:13:30.502499 Recv 0x|13:00|2B:00|02|6D:79:45:56:33:00:FF:FF:00:00:00:00:00:00:00:00|以 0 結(jié)尾的字符串 ‘myEV3’ (0x|6D:79:45:56:33:00|) 的長度為 6 個(gè)字節(jié)。接下來的兩個(gè)字節(jié) (0x|FF:FF|) 是來自于第一個(gè)直接命令的垃圾。
最快的拇指
觸屏傳感器的類型編號(hào)為 16,且有兩個(gè)模式,0: EV3-Touch 和 1: EV3-Bump。第一個(gè)測試,如果傳感器實(shí)際被觸摸了,第二個(gè)從上次清除傳感器開始計(jì)算觸摸。我們通過一個(gè)小程序演示這些模式。它計(jì)數(shù),摸傳感器在五秒鐘內(nèi)撞擊的頻率(請?jiān)诙丝?2 插入你的觸摸傳感器):
#!/usr/bin/env python3import ev3, struct, timemy_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')def change_color(color) -> bytes:return b''.join([ev3.opUI_Write,ev3.LED,color])def play_sound(vol: int, freq: int, dur:int) -> bytes:return b''.join([ev3.opSound,ev3.TONE,ev3.LCX(vol),ev3.LCX(freq),ev3.LCX(dur)])def ready() -> None:ops = change_color(ev3.LED_RED)my_ev3.send_direct_cmd(ops)time.sleep(3)def steady() -> None:ops_color = change_color(ev3.LED_ORANGE)ops_sound = play_sound(1, 200, 60)my_ev3.send_direct_cmd(ops_color + ops_sound)time.sleep(0.25)for i in range(3):my_ev3.send_direct_cmd(ops_sound)time.sleep(0.25)def go() -> None:ops_clear = b''.join([ev3.opInput_Device,ev3.CLR_CHANGES,ev3.LCX(0), # LAYERev3.LCX(1) # NO])ops_color = change_color(ev3.LED_GREEN_FLASH)ops_sound = play_sound(10, 200, 100)my_ev3.send_direct_cmd(ops_clear + ops_color + ops_sound)time.sleep(5)def stop() -> None:ops_read = b''.join([ev3.opInput_Device,ev3.READY_SI,ev3.LCX(0), # LAYERev3.LCX(1), # NOev3.LCX(16), # TYPE - EV3-Touchev3.LCX(0), # MODE - Touchev3.LCX(1), # VALUESev3.GVX(0), # VALUE1ev3.opInput_Device,ev3.READY_SI,ev3.LCX(0), # LAYERev3.LCX(1), # NOev3.LCX(16), # TYPE - EV3-Touchev3.LCX(1), # MODE - Bumpev3.LCX(1), # VALUESev3.GVX(4) # VALUE1])ops_sound = play_sound(10, 200, 100)reply = my_ev3.send_direct_cmd(ops_sound + ops_read, global_mem=8)(touched, bumps) = struct.unpack('<ff', reply[5:])if touched == 1:bumps += 0.5print(bumps, "bumps")for i in range(3):ready()steady()go()stop() ops_color = change_color(ev3.LED_GREEN) my_ev3.send_direct_cmd(ops_color) print("**** Game over ****")我們使用了一個(gè)新操作:opInput_Device = 0x|99| 的 CMD CLR_CHANGES = 0x|1A|,有這些參數(shù):
- (Data8) LAYER:鏈 layer 號(hào)
- (Data8) NO:端口編號(hào)
它清除傳感器,所有它的內(nèi)部數(shù)據(jù)被設(shè)置為初始值。
郁悶的長頸鹿
讓我們編寫一個(gè)程序,它使用類 TwoWheelVehicle 和紅外傳感器。紅外傳感器的類型編號(hào)為 33,它的模式 0 讀取傳感器前方的自由距離。我們使用它來探測小車前方的障礙和坑洞。轉(zhuǎn)換你的小車并放置紅外傳感器,使其看向前方,但從上到下(向下約30 - 60°)。傳感器讀取小車前方的區(qū)域并在遇到意外狀況時(shí)停止運(yùn)動(dòng):
#!/usr/bin/env python3import ev3, ev3_vehicle, struct, randomvehicle = ev3_vehicle.TwoWheelVehicle(0.02128, # radius_wheel0.1175, # treadprotocol=ev3.BLUETOOTH,host='00:16:53:42:2B:99' )def distance() -> float:ops = b''.join([ev3.opInput_Device,ev3.READY_SI,ev3.LCX(0), # LAYERev3.LCX(0), # NOev3.LCX(33), # TYPE - EV3-IRev3.LCX(0), # MODE - Proximityev3.LCX(1), # VALUESev3.GVX(0) # VALUE1])reply = vehicle.send_direct_cmd(ops, global_mem=4)return struct.unpack('<f', reply[5:])[0]speed = 25 vehicle.move(speed, 0) for i in range(10):while True:dist = distance()if dist < 15 or dist > 20:breakvehicle.stop()vehicle.sync_mode = ev3.SYNCangle = 135 + 45 * random.random()if random.random() > 0.5:vehicle.drive_turn(speed, 0, angle)else:vehicle.drive_turn(speed, 0, angle, right_turn=True)vehicle.sync_mode = ev3.STDspeed -= 2vehicle.move(speed, 0) vehicle.stop()一些注釋:
- 如果你從 ev3-python3 下載了模塊 ev3_vehicle.py,請消除屬性 sync_mode 的設(shè)置(vehicle.sync_mode = ev3.SYNC 或 vehicle.sync_mode = ev3.STD)
- 算法的核心部分是:
這個(gè)代碼在自由距離小于 15 cm 或大于 20 cm 時(shí)(具體值依賴于對象的構(gòu)造)停止運(yùn)動(dòng)。這是說:如果小車到了桌子的邊緣(距離變大),它將停止,以及如果它到了一個(gè)障礙物處(小距離),它也將停止。
- 停止后,車輛以隨機(jī)方向及隨機(jī)角度開啟(范圍在 135 到 180°)。sync_mode 設(shè)置為 SYNC,我們想要程序等待直到轉(zhuǎn)彎完成:
- 然后速度減小,小車向前移動(dòng),循環(huán)再次開始:
- 循環(huán)數(shù)限制為10。
- 我的傳感器放在一個(gè)裝配長頸鹿頸部的結(jié)構(gòu)上。這個(gè)以及越來越慢的運(yùn)動(dòng)就成了這個(gè)名字。
- 一個(gè)缺點(diǎn)是傳感器直接向前聚焦。如果車輛以小角度移動(dòng)到桌子的邊緣或靠著障礙物,它將會(huì)識(shí)別它太晚。
技術(shù)上會(huì)發(fā)生什么?
- vehicle.move(speed, 0) 啟動(dòng)一個(gè)無限的運(yùn)動(dòng),它不阻塞 EV3 設(shè)備。
- 這允許在小車運(yùn)動(dòng)時(shí)從傳感器讀取自由距離。
- 與第 3 課和第 4 課的遠(yuǎn)程控制的相似性非常重要,傳感器取代了人類的思維。
- 僅有的阻塞 EV3 設(shè)備的行為是方法 drive_turn。這個(gè)命令需要 sync_mode = SYNC。幸運(yùn)的是,在它執(zhí)行時(shí),我們不需要任何傳感器數(shù)據(jù)。
現(xiàn)在是時(shí)候適配你的程序來滿足你的需要和你的小車的構(gòu)造了。我發(fā)現(xiàn)將小車放在桌面上,其中桌面的一部分被屏障隔開,是最令人印象深刻的。
導(dǎo)引頭
紅外傳感器有另一種有趣的模式:seeker。這個(gè)模式讀取 EV3 紅外信標(biāo)的方向和距離。信標(biāo)允許在四個(gè)信號(hào)通道中選一個(gè)。請?jiān)诙丝?2 插入 IR 傳感器,打開信標(biāo),選擇一個(gè)通道,把它放在紅外傳感器的前方,然后運(yùn)行這個(gè)程序:
#!/usr/bin/env python3import ev3, structmy_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')ops_read = b''.join([ev3.opInput_Device,ev3.READY_RAW,ev3.LCX(0), # LAYERev3.LCX(1), # NOev3.LCX(33), # TYPE - IRev3.LCX(1), # MODE - Seekerev3.LCX(8), # VALUESev3.GVX(0), # VALUE1 - heading channel 1ev3.GVX(4), # VALUE2 - proximity channel 1ev3.GVX(8), # VALUE3 - heading channel 2ev3.GVX(12), # VALUE4 - proximity channel 2ev3.GVX(16), # VALUE5 - heading channel 3ev3.GVX(20), # VALUE6 - proximity channel 3ev3.GVX(24), # VALUE5 - heading channel 4ev3.GVX(28) # VALUE6 - proximity channel 4 ]) reply = my_ev3.send_direct_cmd(ops_read, global_mem=32) (h1, p1,h2, p2,h3, p3,h4, p4, ) = struct.unpack('8i', reply[5:]) print("heading1: {}, proximity1: {}".format(h1, p1)) print("heading2: {}, proximity2: {}".format(h2, p2)) print("heading3: {}, proximity3: {}".format(h3, p3)) print("heading4: {}, proximity4: {}".format(h4, p4))朝向的范圍為 [-25 - 25],負(fù)值代表左,0 代表直行,正的代表右邊。接近性的范圍為 [0 - 100],且以 cm 計(jì)。這個(gè)操作讀取所有 4 個(gè)通道,每個(gè)通道兩個(gè)值。這個(gè)程序的輸出是(seeker 通道是 2):
heading1: 0, proximity1: -2147483648 heading2: -21, proximity2: 27 heading3: 0, proximity3: -2147483648 heading4: 0, proximity4: -2147483648信標(biāo)放在紅外傳感器的左前方,距離為 27 cm。通道 1,3,和 4 返回一個(gè)距離值 -2147483648,它是 0x|00:00:00:80|(小尾端,最高位為 1,所有其它的為 0),表示 沒信號(hào)。
PID 控制器
PID 控制器 持續(xù)計(jì)算錯(cuò)誤值,作為所需設(shè)定值和測量過程變量之間的差值。控制器嘗試通過控制變量的調(diào)整隨時(shí)間最小化誤差。這是一個(gè)偉大的算法,它修改一個(gè)過程的參數(shù)直到過程達(dá)到它的目的狀態(tài)。最好的是,你不需要知道你的參數(shù)的精確依賴以及過程的狀態(tài)。一個(gè)典型的例子是加熱房間的暖氣片。過程變量是房間的溫度,控制器改變暖氣片閥的位置直到房間溫度穩(wěn)定在設(shè)置的點(diǎn)。我們將使用 PID 控制器調(diào)整小車移動(dòng)的參數(shù) speed 和 turn。我們給模塊 ev3 添加一個(gè)類 PID:
class PID():"""object to implement a PID controller"""def __init__(self,setpoint: float,gain_prop: float,gain_der: float=None,gain_int: float=None,half_life: float=None):self._setpoint = setpointself._gain_prop = gain_propself._gain_int = gain_intself._gain_der = gain_derself._half_life = half_lifeself._error = Noneself._time = Noneself._int = Noneself._value = Nonedef control_signal(self, actual_value: float) -> float:if self._value is None:self._value = actual_valueself._time = time.time()self._int = 0self._error = self._setpoint - actual_valuereturn self._gain_prop * self._errorelse:time_act = time.time()delta_time = time_act - self._timeself._time = time_actif self._half_life is None:self._value = actual_valueelse:fact1 = math.log(2) / self._half_lifefact2 = math.exp(-fact1 * delta_time)self._value = fact2 * self._value + actual_value * (1 - fact2)error = self._setpoint - self._valueif self._gain_int is None:signal_int = 0else:self._int += error * delta_timesignal_int = self._gain_int * self._intif self._gain_der is None:signal_der = 0else:signal_der = self._gain_der * (error - self._error) / delta_timeself._error = errorreturn self._gain_prop * error + signal_int + signal_der這實(shí)現(xiàn)了一個(gè)PID控制器,只有一個(gè)修改:half_life。實(shí)際值可能有噪聲或通過離散步驟改變,我們對它們進(jìn)行平滑,因?yàn)楫?dāng)實(shí)際值隨機(jī)或離散變化時(shí),導(dǎo)數(shù)部分將顯示峰值。half_life 的維度 [s] 為時(shí)間,并且是阻尼的半衰期。但請記住:平滑控制器使其變得遲緩!
它的文檔為:
class PID(builtins.object)| object to implement a PID controller| | Methods defined here:| | __init__(self, setpoint:float, gain_prop:float, gain_der:float=None, gain_int:float=None, half_life:float=None)| Parametrizes a new PID controller| | Arguments:| setpoint: ideal value of the process variable| gain_prop: proportional gain,| high values result in fast adaption, but too high values produce oscillations or instabilities| | Keyword Arguments:| gain_der: gain of the derivative part [s], decreases overshooting and settling time| gain_int: gain of the integrative part [1/s], eliminates steady-state error, slower and smoother response| half_life: used for discrete or noisy systems, smooths actual values [s]| | control_signal(self, actual_value:float) -> float| calculates the control signal from the actual value| | Arguments:| actual_value: actual measured process variable (will be compared to setpoint)| | Returns:| control signal, which will be sent to the process保持專注
請將紅外傳感器放在車輛上,水平放在前面。將其插入端口 2,選擇信標(biāo)通道 1,激活信標(biāo),然后啟動(dòng)這個(gè)程序:
#!/usr/bin/env python3import ev3, ev3_vehicle, structvehicle = ev3_vehicle.TwoWheelVehicle(0.02128, # radius_wheel0.1175, # treadprotocol=ev3.BLUETOOTH,host='00:16:53:42:2B:99' ) ops_read = b''.join([ev3.opInput_Device,ev3.READY_RAW,ev3.LCX(0), # LAYERev3.LCX(1), # NOev3.LCX(33), # TYPE - IRev3.LCX(1), # MODE - Seekerev3.LCX(2), # VALUESev3.GVX(0), # VALUE1 - heading channel 1ev3.GVX(4) # VALUE2 - proximity channel 1 ]) speed_ctrl = ev3.PID(0, 2, half_life=0.1, gain_der=0.2) while True:reply = vehicle.send_direct_cmd(ops_read, global_mem=8)(heading, proximity) = struct.unpack('2i', reply[5:])if proximity == -2147483648:print("**** lost connection ****")breakturn = 200speed = round(speed_ctrl.control_signal(heading))speed = max(-100, min(100, speed))vehicle.move(speed, turn) vehicle.stop()說明:
- 我們選擇了通道 1,這只允許讀取該通道的值。
- 控制器不是一個(gè) PID,它的 PD 帶有平滑的值。
- 如果你移動(dòng)信標(biāo),你的小車將改變它的方向并保持信標(biāo)在它的眼鏡的焦點(diǎn)上。
- 這個(gè)程序在關(guān)閉信標(biāo)時(shí)停止。
- 前進(jìn)方向是過程變量,其設(shè)定值為 0(直行)。通過將車輛轉(zhuǎn)動(dòng)到位來完成調(diào)整。
- 請改變 PD 控制器的參數(shù)以了解控制機(jī)制。
- 沒有穩(wěn)定狀態(tài)錯(cuò)誤,因?yàn)?control_signal == 0 將進(jìn)程保持在穩(wěn)定狀態(tài)并且是唯一的穩(wěn)定狀態(tài)。
跟我來
我們稍微改變了程序的代碼,但從根本上改變了它的含義:
#!/usr/bin/env python3import ev3, ev3_vehicle, structvehicle = ev3_vehicle.TwoWheelVehicle(0.02128, # radius_wheel0.1175, # treadprotocol=ev3.BLUETOOTH,host='00:16:53:42:2B:99' ) ops_read = b''.join([ev3.opInput_Device,ev3.READY_RAW,ev3.LCX(0), # LAYERev3.LCX(1), # NOev3.LCX(33), # TYPE - IRev3.LCX(1), # MODE - Seekerev3.LCX(2), # VALUESev3.GVX(0), # VALUE1 - heading channel 1ev3.GVX(4) # VALUE2 - proximity channel 1 ]) speed_ctrl = ev3.PID(10, 4, half_life=0.1, gain_der=0.2) turn_ctrl = ev3.PID(0, 8, half_life=0.1, gain_der=0.3) while True:reply = vehicle.send_direct_cmd(ops_read, global_mem=8)(heading, proximity) = struct.unpack('2i', reply[5:])if proximity == -2147483648:print("**** lost connection ****")breakturn = round(turn_ctrl.control_signal(heading))turn = max(-200, min(200, turn))speed = round(-speed_ctrl.control_signal(proximity))speed = max(-100, min(100, speed))vehicle.move(speed, turn) vehicle.stop()這個(gè)程序使用 heading 來控制移動(dòng)參數(shù) turn 和 proximity 繼而控制它的 speed。speed_ctrl 的設(shè)定點(diǎn)是一個(gè)距離 (10 cm)。如果距離增長,控制器增加小車的速度。你可以減少 10 厘米以下的距離,然后車輛向后移動(dòng)。控制器總是試圖保持或達(dá)到信標(biāo)和紅外傳感器距離為 10 厘米的狀態(tài)。請改變兩個(gè)傳感器的參數(shù)。
如果信標(biāo)穩(wěn)定向前移動(dòng)并且車輛跟隨信標(biāo)會(huì)發(fā)生什么?這就像駕駛車隊(duì)一樣,可以研究穩(wěn)態(tài)誤差。則 speed = gain_prop * error,即 speed = gain_prop * (proximity - setpoint)。這是說:proximity = speed / gain_prop + setpoint。信標(biāo)和傳感器之間的穩(wěn)定距離隨著速度從 10 cm (speed == 0) 到 35 cm (speed == 100) 的增加而增加。如果我們模擬車輛車隊(duì),這正是我們想要的。
我們可以設(shè)置 gain_int 為一個(gè)正值。甚至非常小的值將消除穩(wěn)態(tài)誤差。巡航將保持在 10 cm 的距離,甚至在高速的情況下。
結(jié)論
這一課是關(guān)于傳感器值的。我們已經(jīng)看到,電機(jī)也是傳感器,它允許我們讀取實(shí)際的電機(jī)位置。我們寫了一些小程序,使用紅外傳感器來控制帶有兩個(gè)驅(qū)動(dòng)輪的車輛的運(yùn)動(dòng)。這是我們第一個(gè)真正的機(jī)器人程序。讀取傳感器值的機(jī)器,可以對環(huán)境作出反應(yīng)。
我們獲得了一些 PID 控制器方面的經(jīng)驗(yàn),PID 控制器是受控過程的行業(yè)標(biāo)準(zhǔn)。調(diào)整它們的參數(shù)取代了復(fù)雜算法的編碼。我們的程序,使用PID控制器是驚人的緊湊和驚人的統(tǒng)一。PID控制器似乎功能強(qiáng)大且通用。
下一課將改進(jìn)我們的類 TwoWheelVehicle,并為多任務(wù)做好準(zhǔn)備。我期待著再次見到你。
原文
總結(jié)
以上是生活随笔為你收集整理的EV3 直接命令 - 第 5 课 从 EV3 的传感器读取数据的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: EV3 直接命令 - 第 4 课 用两个
- 下一篇: QUIC 之路