R4T1NH0 – um micro rover LEGO

Sozinho em casa com os miúdos, usei o SBrick para um micro rover telecomandado:

É uma montagem frágil mas estou certo que é possível fazer ainda mais pequeno e ainda assim mais robusto, é uma questão de tempo e engenho.



[actualizado no dia seguinte]

Uma versão ligeiramente melhor em que a ligação das rodas aos micro-motores não gera tanto atrito e onde experimentei utilizar 3 pilhas de lítio CR2430 para fornecer os 9 Volt. As pilhas portaram-se melhor do que eu esperava.


Assim quase do nada tenho aqui um robot de mesa bastante interessante para experiências com o Snap!

[nova actualização]

O consumo do robot com 3 pilhas CR2430 novas varia entre 60 mA (movendo-se para a frente) e 90 mA (rodando sobre si próprio).

Controlo de SBrick com wiimote

Descobri que é muito fácil usar um wiimote em Linux por isso na sequência de ‘SBrick – controlo remoto com um gamepad‘ segue agora como controlar o SBrick com um wiimote (na verdade um clone barato, um N-Play Remote Plus, comprado fora de horas numa Worten – infelizmente não havia a versão com Motion Plus).

O wiimote usa Bluetooth, apesar de não seguir estritamente as normas. Se tivermos bluetooth no nosso PC podemos confirmar que estamos em condições de utilizar o wiimote carregando em simultâneo nos botões ‘1’ e ‘2’ do wiimote para que este fique visível durante uns segundos:

$ hcitool -i hci0 scan
Scanning ...
    04:02:16:01:1C:E7    Nintendo RVL-CNT-01

Como não segue as normas não é possível emparelhar com ele mas existem ferramentas para isso como o cwiid. Como vou usar pyhton fui buscar a library correspondente:

$ sudo apt-get install python-cwiid

existem vários exemplos básicos que não vou apresentar aqui, sigo directamente para o resultado final:

 

O script utilizado no video acima:

# apt-get install python-cwiid
import cwiid
from time import sleep
from subprocess import call
from math import log10

# macros for the SBrick
DRIVE_A="gatttool -b 00:07:80:7F:28:E1 -i hci0 --char-write --handle=0x0025 --value=0102"
DRIVE_B="gatttool -b 00:07:80:7F:28:E1 -i hci0 --char-write --handle=0x0025 --value=0103"
COAST_A="gatttool -b 00:07:80:7F:28:E1 -i hci0 --char-write --handle=0x0025 --value=01020000"
COAST_B="gatttool -b 00:07:80:7F:28:E1 -i hci0 --char-write --handle=0x0025 --value=01030000"
BREAK_A="gatttool -b 00:07:80:7F:28:E1 -i hci0 --char-write --handle=0x0025 --value=0002"
BREAK_B="gatttool -b 00:07:80:7F:28:E1 -i hci0 --char-write --handle=0x0025 --value=0003"


# connecting to the wiimote. This allows several attempts
# as first few often fail.

print 'Press 1+2 on your Wiimote now...'
wm = None
i=1
while not wm:
    try:
        wm=cwiid.Wiimote()
    except RuntimeError:
        if (i>5):
            print("cannot create connection")
            quit()
        print "Error opening wiimote connection"
        print "attempt " + str(i)
        i +=1

#set wiimote to report button presses and accelerometer state
wm.rpt_mode = cwiid.RPT_BTN | cwiid.RPT_ACC

#turn on all led and ruble to show connected
wm.led=15
wm.rumble=True
sleep(0.5)
wm.rumble=False
wm.led=0
sleep(1.5)

# roll = accelerometer[0], standby ~125
# pitch = accelerometer[1], standby ~125

while True:
    buttons = wm.state['buttons']

    #only pay attention when button '1' pressed
    if (buttons & cwiid.BTN_1):

        roll=(wm.state['acc'][0]-125)
        pitch=(wm.state['acc'][1]-125)
        
        if (roll<0):
            if (roll<-4):
                if (roll<-25):
                    roll=-100
                else:
                    roll=-50*log10(-4*roll)
            else:
                roll=0

        if (roll>0):
            if (roll>4):
                if (roll>25):
                    roll=100
                else:
                    roll=50*log10(4*roll)
            else:
                roll=0

        if (pitch>0):
            if (pitch>4):
                if (pitch>25):
                    pitch=100
                else:
                    pitch=50*log10(4*pitch)

            else:
                pitch=0

        if (pitch<0):
            if (pitch<-4):
                if(pitch<-25):
                    pitch=-100
                else:
                    pitch=-50*log10(-4*pitch)
            else:
                pitch=0

        if ((pitch<>0)or(roll<>0)):

            roll=2.5*roll
            pitch=2.5*pitch

            if(pitch<>0):
                if(roll>0):
                    # turn right
                    motor_L=pitch
                    motor_R=-pitch-roll/2

                else:
                    # turn left
                    motor_R=-pitch
                    motor_L=pitch+roll/2

            elif(roll<>0):
                #just rotate
                motor_R=motor_L=roll;

            else:
                # does nothing
                motor_R=motor_L=0

            if((motor_R<>0)or(motor_L<>0)):

                if(motor_R<0):
                    duty=str(hex(int(-motor_R)))
                    command_A=DRIVE_A+"00"+duty[2:]                    
                else:
                    duty=str(hex(int(motor_R)))
                    command_A=DRIVE_A+"01"+duty[2:]

                if(motor_L<0):
                    duty=str(hex(int(-motor_L)))
                    command_B=DRIVE_B+"00"+duty[2:]                    
                else:
                    duty=str(hex(int(motor_L)))
                    command_B=DRIVE_B+"01"+duty[2:]

                #send motors commands to SBrick
                call(command_A, shell=True);
                call(command_B, shell=True);
                sleep(0.1)

                #send COAST commands to SBrick
                call(COAST_A, shell=True);
                call(COAST_B, shell=True);

    else:
        # inactive
        sleep(0.01)

1-wire LEGO LED stripe

Este artigo é a parte 2 de 3 da série  Rede de controlo 1-wire

E agora um primeira experiência com múltiplos DS2413 numa mesma microLAN…

Cada dispositivo 1-wire tem um ID distinto gravado em fábrica:

$sudo ls /mnt/1wire/
3A.4C2B13000000  81.086D33000000  settings    structure  uncached
3A.B0E216000000  bus.1          statistics  system

Vemos que temos 3 dispositivos de 2 classes diferentes.  A classe 81 é a do Master Controller (o adaptador USB DS9490R) e a classe  3A é a do DS2413. Temos portanto dois dispositivos DS2413 na rede, cada um com o seu ID:

  • 4C2B13000000
  • B0E216000000

Na forma como o owfs lida com 1-wire o par “Classe.ID” funciona como um endereço. É possível definir alias mas funcionam apenas localmente (isto é: se movermos a microLAN do meu Ubuntu para o LEGO ev3dev tẽm de ser redefinidos) e na minha primeira experiência com alias perdi o acesso aos dispositivos por isso deixo para outra altura.

Sendo da mesma classe, estes dois dispositivos partilham os mesmos atributos (PIO.A, PIO.B e PIO.ALL por exemplo). Se tiver um LED em cada PIO destes devices podemos por exemplo acender todos os LEDs percorrendo todos os dispositivos da classe 3A e escrevendo “1,1” na subpasta PIO.ALL.

O programa abaixo acende cada LED em sequência, primeiro num sentido depois no outro:

import sys, traceback
from time import sleep
from subprocess import call

# B0E216000000/PIO.A = primeiro LED
# B0E216000000/PIO.B = segundo LED
# 4C2B13000000/PIO.A = terceiro LED
# 4C2B13000000/PIO.B = quarto LED

def main():
    try:

# apagar tudo ao comecar

    call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
    call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);

        while True:
        # apaga tudo
        call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
        call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);

                call("echo 1 > /mnt/1wire/3A.B0E216000000/PIO.A", shell=True); # LED1
                sleep(0.25);

        # apaga tudo
        call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
        call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);

                call("echo 1 > /mnt/1wire/3A.B0E216000000/PIO.B", shell=True); # LED2
                sleep(0.25);

        # apaga tudo
        call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
        call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);

        call("echo 1 > /mnt/1wire/3A.4C2B13000000/PIO.A", shell=True); # LED3
                sleep(0.25);

        # apaga tudo
        call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
        call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);

        call("echo 1 > /mnt/1wire/3A.4C2B13000000/PIO.B", shell=True); # LED4
                sleep(0.25);

        # apaga tudo
        call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
        call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);

        call("echo 1 > /mnt/1wire/3A.4C2B13000000/PIO.A", shell=True); # LED3
                sleep(0.25);

        # apaga tudo
        call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
        call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);

                call("echo 1 > /mnt/1wire/3A.B0E216000000/PIO.B", shell=True); # LED2
                sleep(0.25);


    except (KeyboardInterrupt, SystemExit):
        print "Exiting...";
    except Exception:
        traceback.print_exc(file=sys.stdout);

# apagar tudo antes de sair

    call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
    call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);
    sys.exit(0);

if __name__ == "__main__":
    main()

 

E o programa abaixo acende as luzes aleatoriamente, mais próprio para um concerto ou uma discoteca (com tempo hei-de estudar a forma de usar as rotinas PyAudio para medir a intensidade de uma música e controlar as luzes ao ritmo desta embora duvide que o EV3 tenha CPU para tal).

 

 

import sys, traceback
from time import sleep
from subprocess import call
from random import randrange

# B0E216000000/PIO.A = primeiro LED
# B0E216000000/PIO.B = segundo LED
# 4C2B13000000/PIO.A = terceiro LED
# 4C2B13000000/PIO.B = quarto LED


def main():
    try:

# apagar tudo ao comecar

    call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
    call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);

        while True:
        # apaga tudo
        call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
        call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);

        if (randrange(2) == 1):
                    call("echo 1 > /mnt/1wire/3A.B0E216000000/PIO.A", shell=True); # LED1

        if (randrange(2) == 1):
                    call("echo 1 > /mnt/1wire/3A.B0E216000000/PIO.B", shell=True); # LED2

        if (randrange(2) == 1):
            call("echo 1 > /mnt/1wire/3A.4C2B13000000/PIO.A", shell=True); # LED3

        if (randrange(2) == 1):
            call("echo 1 > /mnt/1wire/3A.4C2B13000000/PIO.B", shell=True); # LED4
                
                sleep(0.25);


    except (KeyboardInterrupt, SystemExit):
        print "Exiting...";
    except Exception:
        traceback.print_exc(file=sys.stdout);

# apagar tudo antes de sair

    call("echo 0,0 > /mnt/1wire/3A.B0E216000000/PIO.ALL", shell=True);
    call("echo 0,0 > /mnt/1wire/3A.4C2B13000000/PIO.ALL", shell=True);
    sys.exit(0);

if __name__ == "__main__":
    main()

 

1-wire LED

Este artigo é a parte 1 de 3 da série  Rede de controlo 1-wire

Estou a reproduzir em LEGO um concerto dos Xutos&Pontapés. A posição dos elementos da banda em palco vai ser controlada por um controlador (ainda não decidi se um Raspberry Pi ou um LEGO EV3) e quero poder controlar também as luzes dos projectores:

Se as luzes acendessem todas em simultâneo as ligações eléctricas seriam simples e seriam necessários passar apenas 2 cabos (positivo e negativo, comuns a todos os LEDs) pela estrutura por cima do palco. Mas eu quero um controlo individual dos projectores e pelo menos 10 deles. Isso significa pelo menos 11 cabos (10 positivos e 1 negativo comum) e apesar de ser possível (consigo passar 4 ou 5 cabos de jumper por entre um furo Technic por isso bem arrumados os cabos até nem dariam muito nas vistas) obriga-me a ter pelo menos 10 portas digitais para o controlo (o que também é possível com circuitos adicionais mas não me cativa). E se quiser aumentar o número de projectores a coisa começa a complicar-se.

Comecei logo a pensar num bus de controlo e pendurar micro-circuitos de controlo ao longo do bus. O ideal mesmo seriam LEDs I2C mas não encontrei nada suficientemente pequeno ou funcional por isso optei por uma microLAN 1-wire.

O termo “1-wire” é enganador: são necessários pelo menos 2 cabos num bus 1-wire (dados e massa) e na maioria das aplicações práticas será necessário um terceiro para alimentar os dispositivos (a norma prevê um modo “parasita” em que os dispositivos retiram alimentação da ligação prevista para dados mas isso apenas se aplica a dispositivos de muito baixo consumo como sensores de temperatura). Mesmo assim 3 cabos é muito bom para controlar quantos LEDs quiser (se decidir mais 2 ou 3 projectores não preciso passar mais 2 ou 3 cabos nem desencantar mais 2 ou 3 portas digitais no controlador).

A Maxim fabrica o DS2413, um circuito extremamente reduzido que implementa 2 portas digitais bidireccionais. É um chip SMD mas a Adafruit fornece um kit com o chip já montado numa placa de circuito impresso sendo só necessário soldar 4 headers e eventualmente uma resistência (tudo fornecido com o kit) para podermos utilizar na nossa microLAN.

Uhmmm… qual microLAN?

É possível implementar uma microLAN com o Raspberry Pi como master utilizando uma porta GPIO e uma resistência de pull-up. Mas é algo específico para o Raspberry, prefiro algo que possa replicar no meu PC ou no LEGO EV3. Por isso encomendei à RS Online um Maxim DS9490R que é um dispositivo USB que funciona como master controller numa microLAN e é suportado pelo Linux.

No meu portátil (Ubuntu 14.04) foi só ligar:

$ dmesg
(...)
[18011.890734] usb 2-1.3: new full-speed USB device number 7 using ehci-pci
[18011.983566] usb 2-1.3: New USB device found, idVendor=04fa, idProduct=2490
[18011.983576] usb 2-1.3: New USB device strings: Mfr=0, Product=0, SerialNumber=0
[18012.021996] Driver for 1-wire Dallas network protocol.
[18012.026144] usbcore: registered new interface driver DS9490R

Não fui ver que módulos foram carregados na altura mas penso que foram ‘ds2490’ e ‘wire’ (existe também um módulo ‘ds9490r’ mas não está presente no meu Ubuntu).

Após detectar uma microLAN o kernel cria uma pasta ‘/sys/bus/w1’ para dispositivos 1-wire:

$ ls /sys/bus/w1/devices
 81-000000336d08  w1_bus_master1

“81-” identifica o DS9490

A ligação do DS2413 à microLAN é muito simples: o DS9490R tem uma ficha RJ12, idêntica às RJ11 dos telefones mas com 6 contactos. Felizmente dos 6 contactos apenas os 2 centrais são necessários pelo que podemos usar um cabo normal de telefone só com dois condutores:

  • 3 – OW (OW 1-Wire Data)
  • 4 – GND_OW (1-Wire Return)
Wire D

Apesar da datasheet da MAXIM indicar o pinout achei esta foto menos dada a confusões:

Sheepwalk Electronics: Pinout DS9490R

A Sheepwalk Electronics vende alguns produtos 1-wire (inclusive o DS9490R) e publica no seu site alguma informação útil relacionada com 1-wire.

Depois de soldar o header à placa da Adafruit foi só fazer as ligações (com cabos de jumper ou crocodilos, o que houver à mão):

1wire-led

Com 3 baterias AA NiMH (cerca de 3.8V) e um LED branco de alto brilho (cerca de 3.2V) temos 0.6V na resistência por isso se esta for de 1KΩ o consumo do LED será de 0.6 mA de corrente, bastante abaixo dos 20 mA tolerados por cada porta e suficiente para um teste (no palco LEGO irei puxar mais por cada LED, pelo menos 10 mA cada e alimentação será de outro tipo).

NOTA: com o kit da Adafruit vem uma resistência de 4.7KΩ para pull-up da linha ‘1-Wire Data’. Deveria ligar esta resistência ao pino 1 da ficha RJ12 (+5V vindos do bus USB do host onde estiver ligado o controlador, neste caso o PC) mas não me agradou muito a ideia e como suspeito que internamente o controlador já preveja isso optei deliberadamente por não usar o pull-up… aparentemente sem consequências.

Pouco depois de ligar o cabo telefónico ao controlador USB o nosso circuito é imediatamente reconhecido:

 [  706.356777] 0x81: count=17, status: 01 00 20 40 05 04 04 00 20 53 00 00 00 01 00 00 a5
 [  706.356800]
 [  706.356808]                                   enable flag:        1
 [  706.356812]                                  1-wire speed:        0
 [  706.356816]                        strong pullup duration:       20
 [  706.356819]                    programming pulse duration:       40
 [  706.356822]                    pulldown slew rate control:        5
 [  706.356825]                              write-1 low time:        4
 [  706.356829]      data sample offset/write-0 recovery time:        4
 [  706.356832]                      reserved (test register):        0
 [  706.356835]                           device status flags:       20
 [  706.356839]                  communication command byte 1:       53
 [  706.356842]                  communication command byte 2:        0
 [  706.356845]           communication command buffer status:        0
 [  706.356848]              1-wire data output buffer status:        0
 [  706.356852]               1-wire data input buffer status:        1
 [  706.356855]                                      reserved:        0
 [  706.356858]                                      reserved:        0
 [  706.356861]                             new device detect:       a5
 [  707.897674] w1_master_driver w1_bus_master1: Family 3a for 3a.00000012fdf4.44 is not registered.

Infelizmente diz que a famíla 3A não está registrada – o suporte nativo do kernel a dispositivos 1-wire não se extende [ainda?] ao DS2413. É pena, assim não temos nada útil dentro de ‘/sys/bus/w1/devices/3a-00000012fdf4’. Mas tudo bem, já esperava isso, existe o projecto owfs dedicado especificamente ao 1-wire:

sudo apt-get install owfs
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following extra packages will be installed:
  libow-2.8-15 owfs-common owfs-fuse owftpd owhttpd owserver
Suggested packages:
  owfs-doc
The following NEW packages will be installed:
  libow-2.8-15 owfs owfs-common owfs-fuse owftpd owhttpd owserver
0 upgraded, 7 newly installed, 0 to remove and 0 not upgraded.
Need to get 296 kB of archives.
After this operation, 1,364 kB of additional disk space will be used.
Do you want to continue? [Y/n]

Após a instalação descobrimos que temos 3 serviços a correr:

  • owserver
  • owhttpd
  • owftpd

Embora não me pareçam necessários, nesta fase vou deixá-los a correr. Descobrimos também que o owfs impede o kernel de carregar os drivers que referi acima de modo a não ocorrerem conflitos:

$ cat /etc/modprobe.d/libow-2.8-15.conf
blacklist ds9490r
blacklist ds2490
blacklist wire

Para aceder à microLAN usamos o comando ‘owfs’ mas primeiro é necessário definir um mountpoint onde o owfs vai criar uma estrutura de pastas virtuais que mapeiam cada dispositivo (escolhi ‘/mnt/1wire’):

sudo mkdir /mnt/1wire
sudo owfs -u -m /mnt/1wire

Para confirmar que funciona:

$ sudo ls /mnt/1wire
81.086D33000000 bus.1 settings statistics structure system uncached

temos 1 dispositivo na microLAN (o master controller DS9490R). Vamos agora ligar o DS2413 à microLAN – passadas algumas dezenas de segundos temos:

$ sudo ls /mnt/1wire
3A.F4FD12000000  bus.1       statistics  system
81.086D33000000  settings  structure   uncached

agora já temos algo útil dentro do device ‘3A.F4FD12000000’:

$ sudo ls /mnt/1wire/3A.F4FD12000000
address  family   PIO.A    PIO.BYTE   r_locator   sensed.B
alias     id      PIO.ALL  r_address  sensed.A      sensed.BYTE
crc8     locator  PIO.B    r_id       sensed.ALL  type

Ligando um LED a PIO.A podemos acendê-lo:

$ sudo sh -c  "echo 1 > /mnt/1wire/3A.F4FD12000000/PIO.A"

e voltar a apagá-lo:

$ sudo sh -c  "echo 0 > /mnt/1wire/3A.F4FD12000000/PIO.A"

(os comandos são muito feios porque o owfs tem de correr com previlégios de root e eu não sou root no meu Ubuntu; em Raspberry Pi ou ev3dev bastaria:

echo 1 > /mnt/1wire/3A.F4FD12000000/PIO.A
echo 0 > /mnt/1wire/3A.F4FD12000000/PIO.A

)

E agora um script python para piscar o LED a cada meio segundo:

import sys, traceback
from time import sleep
from subprocess import call

def main():
    try:
        while True:
                call("echo 1 > /mnt/1wire/3A.F4FD12000000/PIO.A", shell=True);
                sleep(0.5);
                call("echo 0 > /mnt/1wire/3A.F4FD12000000/PIO.A", shell=True);
                sleep(0.5);

    except (KeyboardInterrupt, SystemExit):
        print "Exiting...";
    except Exception:
        traceback.print_exc(file=sys.stdout);
    call("echo 0 > /mnt/1wire/3A.F4FD12000000/PIO.A", shell=True);
    sys.exit(0);

if __name__ == "__main__":
    main()

Os meus agradecimentos à PTRobotics por incluírem o Adafruit DS2413 na sua lista de produtos a meu pedido.

 

 

 

 

Computer Vision com LEGO

Com a webcam a funcionar e com tanta coisa em comum com a comunidade Raspberry Pi ocorreu-me experimentar o OpenCV e testar as capacidades do EV3  no campo da Computer Vision.

E afinal nem é muito difícil começar: segundo o artigo ‘Face Recognition With Python, in Under 25 Lines of Code‘ são mesmo suficientes menos de 25 linhas de código em Python.

Em vez de instalar o opencv todo, instalei apenas o módulo para python:

root@ev3dev:~# apt-get install python-opencv

Depois foi só criar o script ‘teste-opencv.py’:

import numpy as np
import cv2

imagePath = "/root/teste-face.jpg"
cascPath = "/root/haarcascade_frontalface_default.xml"
resultPath ="/root/resultado.jpg"

faceCascade = cv2.CascadeClassifier(cascPath)

image = cv2.imread(imagePath)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

faces = faceCascade.detectMultiScale(
    gray,
    scaleFactor=1.1,
    minNeighbors=5,
    minSize=(30, 30),
    flags = cv2.cv.CV_HAAR_SCALE_IMAGE
)

print "Found {0} faces!".format(len(faces))

# Draw a rectangle around the faces
for (x, y, w, h) in faces:
    cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 2)

cv2.imwrite(resultPath,image)

Como não instalei o opencv não tenho os ficheiros xml com regras de classificação. Para este exemplo o autor do artigo referido disponibiliza também o ficheiro ‘haarcascade_frontalface_default.xml‘.

Para esta foto ‘teste-face.jpg’:

teste-face02O resultado do script, ao fim de 3 a 5 minutos de CPU a 80% foi este:

resultado02Abre um mundo de possibilidades, não abre?

ev3dev – keypad USB

Ter o LEGO Mindstorms EV3 a correr uma versão quase standard de Debian Linux começa agora a revelar as suas vantagens: podemos atirar-lhe com quase tudo o que tivermos na nossa gaveta de sucata… como por exemplo um teclado numérico USB:

USB Keypad (roline)

O sistema operativo reconhece-o como um teclado convencional:

root@ev3dev:~# dmesg
usb 1-1.4.1: new low-speed USB device number 7 using ohci
input: USB-compliant keyboard as /devices/platform/ohci.0/usb1/1-1/1-1.4/1-1.4.1/1-1.4.1:1.0/0003:0B38:0003.0001/input/input2
hid-generic 0003:0B38:0003.0001: input: USB HID v1.10 Keyboard [USB-compliant keyboard] on usb-ohci.0-1.4.1/input0
input: USB-compliant keyboard as /devices/platform/ohci.0/usb1/1-1/1-1.4/1-1.4.1/1-1.4.1:1.1/0003:0B38:0003.0002/input/input3
hid-generic 0003:0B38:0003.0002: input: USB HID v1.10 Mouse [USB-compliant keyboard] on usb-ohci.0-1.4.1/input1
root@ev3dev:~# lsusb
Bus 001 Device 007: ID 0b38:0003 Gear Head Keyboard
Bus 001 Device 006: ID 058f:9254 Alcor Micro Corp. Hub

(o teclado tem embutido um hub USB de 2 portas daí o segundo device USB reportado)

O sistema operativo cria entradas do tipo ‘event#’ em ‘/dev/input’ assim como apontadores com descritivos mais legíveis:

ls /dev/input/by-id/ -la
lrwxrwxrwx 1 root root  10 Aug 13 23:38 usb-0b38_USB-compliant_keyboard-event-if01 -> ../event2
lrwxrwxrwx 1 root root  10 Aug 13 23:38 usb-0b38_USB-compliant_keyboard-event-kbd -> ../event1

De modo que o teclado pode ser referido por um destes dois ficheiros:

  • /dev/input/event1
  • /dev/input/usb-0b38_USB-compliant_keyboard-event-kbd

Enquanto que o sufixo “event1” é variável, o descritivo “by-id” não é – no meu laptop Ubuntu o mesmo comando retorna:

~$ ls /dev/input/by-id/ -la
lrwxrwxrwx 1 root root  10 Aug 13 23:38 usb-0b38_USB-compliant_keyboard-event-if01 -> ../event19
lrwxrwxrwx 1 root root  10 Aug 13 23:38 usb-0b38_USB-compliant_keyboard-event-kbd -> ../event18

por isso é preferível utilizar o descritivo “by-id”.

Não é suficiente ler os valores deste ficheiro – o primir de uma tecla gera gera um código de vários caracteres aparentemente ininteligiveis:

~$ cat /dev/input/event1
w��S�VSw��S�VEw��S�Vw��S�Ww��S�Ww��S��Sw��S��Ew��S��^C

A solução é usar o comando ‘showkey -s’ para descodificar. Por exemplo premindo a tecla ‘4’ com e sem ‘NumLock’ activo resulta:

cat /dev/input/by-id/usb-0b38_USB-compliant_keyboard-event-kbd > showkey -s
  • Sem Numlock: “^[[D”
  • Com NumLock: “4”

Felizmente isto só é necessário se quisermos ler directamente o teclado. Em situações normais, quando os nossos programas estão a correr, o próprio sistema operativo pode fazer esse trabalho por nós. No caso do ev3dev podemos ler uma única tecla a partir da consola com o seguinte comando bash:

read -s -n 1 Tecla < /dev/tty1

O parâmetro ‘-s’ serve para correr em modo silencioso (i.e. a tecla não é simultâneamente escrita na consola) e o parâmetro ‘-n 1’ força a leitura de um único caracter (quanto ao pipe ‘< /dev/tty1’ descobri entretanto que tanto faz referir /dev/tty0 como /dev/tty1 portanto a partir daqui refiro a consola do ev3dev como /dev/tty1… porque sim)

O script abaixo junta esta informação com a obtida anterior a respeito dos motores e permite controlar a rotação do motor com as teclas ‘4’ (LEFT) e ‘6’ (RIGHT), terminado com a tecla ‘ENTER’.

#!/bin/bash

#chmod +x controlkey.sh

tput clear > /dev/tty1
setfont /usr/share/consolefonts/Lat15-TerminusBold32x16.psf.gz
echo -n "Prima tecla" > /dev/tty1

while true; do

  read -s -n 1 char < /dev/tty1
  if [[ "$char" = "" ]]; then
    break
  fi

  case "$char" in

  4) echo "LEFT" > /dev/tty1

     echo   0 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
     echo   1 > /sys/class/tacho-motor/tacho-motor0/run
     echo   50 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
     sleep 1s
     echo   0 > /sys/class/tacho-motor/tacho-motor0/run
     echo   0 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp

    ;;

  6) echo "RIGHT" > /dev/tty1

     echo   0 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
     echo   1 > /sys/class/tacho-motor/tacho-motor0/run
     echo   -50 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
     sleep 1s
     echo   0 > /sys/class/tacho-motor/tacho-motor0/run
     echo   0 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp

    ;;
   esac

done

echo "Bye!" > /dev/tty1

 

 

ev3dev – controlando motores

A utilização básica do LEGO Mindstorms EV3 com ev3dev fica completa com a utilização de motores.

Ligando um motor (45503 EV3 Medium Servo Motor) na ficha A, o sistema operativo acusa o evento:

root@ev3dev:~# dmesg
(...)
tacho-motor tacho-motor0: Tacho motor registered.
ev3-tacho-motor outA:motor: A Tacho Motor connected to port outA gpio 91 irq 192

Uma vez mais é necessário consultar a wiki do projecto ev3dev para entender como aceder aos motores. Estes também são mapeados pelo sistema operativo, desta vez em “/sys/bus/legoev3/devices/” e em ” /sys/class/tacho-motor/”:

root@ev3dev:~# ls /sys/bus/legoev3/devices/
in2  in3  in4  outA  outA:motor  outB  outC  outD
root@ev3dev:~# ls /sys/class/tacho-motor/
tacho-motor0
root@ev3dev:~# ls /sys/class/tacho-motor/tacho-motor0
device           port_name      pulses_per_second     reset        speed_regulation_K  subsystem
duty_cycle     position       pulses_per_second_sp  run            speed_regulation_P  time_sp
duty_cycle_sp  position_mode  ramp_down_sp        run_mode        state            type
estop           position_sp    ramp_up_sp        speed_regulation_D    stop_mode        uevent
polarity_mode  power          regulation_mode        speed_regulation_I    stop_modes

Muito informação para processar mas para começar podemos confirmar apenas o tipo de motor:

root@ev3dev:~# cat /sys/class/tacho-motor/tacho-motor0/type
minitacho

Os motores Mindstorms são inicializados no modo “run forever”, podemos também confirmar isso:

root@ev3dev:~# cat /sys/class/tacho-motor/tacho-motor0/run_mode
forever

Neste modo podemos controlar o motor controlando 2 parâmetros:

  • /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
  • /sys/class/tacho-motor/tacho-motor0/run

o shell script abaixo faz o motor rodar 5 segundos num sentido e 5 segundos no sentido inverso, com um duty factor de 50%:

 #!/bin/bash
 echo   0 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
 echo   1 > /sys/class/tacho-motor/tacho-motor0/run
 echo   50 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
 sleep 5s
 echo   0 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
 sleep 1s
 echo   -50 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
 sleep 5s
 echo   0 > /sys/class/tacho-motor/tacho-motor0/duty_cycle_sp
 echo   0 > /sys/class/tacho-motor/tacho-motor0/run

 

 

ev3dev – usando o LCD do EV3

Usando o ev3dev há duas formas de aceder ao LCD do LEGO Mindstorms EV3:

  • modo de texto: terminal via /dev/tty0
  • modo gráfico: framebuffer via /dev/fb0

Ambos os modos estão ainda a sofrer desenvolvimentos mas o segundo modo é seguramente o mais complexo pelo que fico-me por enquanto só pelo modo de texto.

Assim para escrever qualquer coisa basta por exemplo redireccionar o comando echo para /dev/tty0:

echo "Hello world!" > /dev/tty0

Já é qualquer coisa mas para limpar o ecran sem ter de enviar vários “echo” existem formas melhores. Sendo um terminal tty convencional, a maioria dos comandos para terminal devem funcionar (inclusive algumas sequencias de escape). Experimentei o tput:

Para limpar o ecran:

tput clear > /dev/tty0

Para posicionar o cursor:

tput cup linha coluna > /dev/tty0

(tput cup 0 0 envia para o canto superior esquerdo e já agora com tput cols ficamos a saber que o LCD tem 44 colunas)

Para escrever branco sobre negro:

tput smso > /dev/tty0

Para voltar ao negro sobre branco:

tput rmso > /dev/tty0

Muito bem, já escrevo o que quiser em qualquer lado do ecran… mas a fonte é tão pequena que só com uma lupa é que se lê. Como aumentar o texto?

A primeira hipótese que me ocorreu foi instalar o figlet que usa caracteres ASCII para fingir fontes maiores:

apt-get install figlet
figlet -f banner "Teste" > /dev/tty0
figlet -f big "Teste" > /dev/tty0

teste01mas é uma forma um bocado coxa. O melhor mesmo é poder trocar o tamanho dos caracteres e para isso existe o comando “setfont” que felizmente já existe no ev3dev e disponibiliza umas quantas fontes em “/usr/share/consolefonts/”, vejamos só quantas há na variante Lat15 (que serve à maioria das linguas ocidentais incluindo a nossa):

root@ev3dev:~# ls /usr/share/consolefonts/Lat15*
/usr/share/consolefonts/Lat15-Fixed13.psf.gz
/usr/share/consolefonts/Lat15-Fixed14.psf.gz
/usr/share/consolefonts/Lat15-Fixed15.psf.gz
/usr/share/consolefonts/Lat15-Fixed16.psf.gz
/usr/share/consolefonts/Lat15-Fixed18.psf.gz
/usr/share/consolefonts/Lat15-Terminus12x6.psf.gz
/usr/share/consolefonts/Lat15-Terminus14.psf.gz
/usr/share/consolefonts/Lat15-Terminus16.psf.gz
/usr/share/consolefonts/Lat15-Terminus20x10.psf.gz
/usr/share/consolefonts/Lat15-Terminus22x11.psf.gz
/usr/share/consolefonts/Lat15-Terminus24x12.psf.gz
/usr/share/consolefonts/Lat15-Terminus28x14.psf.gz
/usr/share/consolefonts/Lat15-Terminus32x16.psf.gz
/usr/share/consolefonts/Lat15-TerminusBold14.psf.gz
/usr/share/consolefonts/Lat15-TerminusBold16.psf.gz
/usr/share/consolefonts/Lat15-TerminusBold20x10.psf.gz
/usr/share/consolefonts/Lat15-TerminusBold22x11.psf.gz
/usr/share/consolefonts/Lat15-TerminusBold24x12.psf.gz
/usr/share/consolefonts/Lat15-TerminusBold28x14.psf.gz
/usr/share/consolefonts/Lat15-TerminusBold32x16.psf.gz
/usr/share/consolefonts/Lat15-TerminusBoldVGA14.psf.gz
/usr/share/consolefonts/Lat15-TerminusBoldVGA16.psf.gz
/usr/share/consolefonts/Lat15-TomThumb4x6.psf.gz
/usr/share/consolefonts/Lat15-VGA14.psf.gz
/usr/share/consolefonts/Lat15-VGA16.psf.gz
/usr/share/consolefonts/Lat15-VGA28x16.psf.gz
/usr/share/consolefonts/Lat15-VGA32x16.psf.gz
/usr/share/consolefonts/Lat15-VGA8.psf.gz

Bold e 32×16 parece-me bem por isso:

tput clear > /dev/tty0
setfont /usr/share/consolefonts/Lat7-TerminusBold32x16.psf.gz
echo "Hello World" > /dev/tty0

helloworldOs 11 caracteres de “Hello World” encheram uma linha (onde antes, com a fonte default, cabiam 44). Pelo menos já não é precisa a lupa.

ev3dev com USB Hub e USB Audio

Depois de ter Wi-Fi decidi testar mais alguns dispositivos USB que uso com o Raspberry Pi.

Para começar, uma vez que o EV3 apenas tem uma porta USB, imprescindível um hub, de preferência pequeno:

Hub KUNFT 4 Portas USB 2.0 H-088 (na Worten por €6,99)

Com o EV3 ainda desligado liguei-o e a ele o dongle Wi-Fi (ThePiHut).

Da primeira vez não obteve rede, não sei se não esperei tempo suficiente. Retirei o hub, liguei novamente (sem o dongle Wi-Fi) esperei um pouco e depois liguei o dongle e desta vez obtive rede. E a partir daqui cada boot/reboot com tudo ligado funcionou sempre bem.

root@ev3dev:~# lsusb
(...)
Bus 001 Device 002: ID 05e3:0608 Genesys Logic, Inc. USB-2.0 4-Port HUB

Agora o audio:

[Nota: o EV3 já tem uma placa de audio interna, suportada pelo ALSA, e um altifalante interno também. Mas tanto quanto percebi funciona de modo semelhante ao jack audio do Raspberry Pi, por PWM, consumindo CPU… além de ser mono e não permitir ligação externa a um amplificador]

DIY USB to Audio Module for Raspberry PI / Arduino / MAC / PC + More – Green (SKU 315287 na DX – DealExtreme, US$10.88)

root@ev3dev:~# dmesg
(...)
input: Burr-Brown from TI               USB Audio DAC    as /devices/platform/ohci.0/usb1/1-1/1-1.4/1-1.4:1.2/0003:08BB:2704.0001/input/input2
hid-generic 0003:08BB:2704.0001: input: USB HID v1.00 Device [Burr-Brown from TI USB Audio DAC   ] on usb-ohci.0-1.4/input2
(...)
root@ev3dev:~# lsusb
(...)
Bus 001 Device 004: ID 08bb:2704 Texas Instruments Audio Codec
(...)

O ALSA reconhece de imediato a nova placa de som:

root@ev3dev:~# aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: legoev3 [LEGO Mindstorms EV3 speaker], device 0: LEGO Mindstorms EV3 [LEGO Mindstorms EV3]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: DAC [USB Audio DAC], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
root@ev3dev:~# alsamixer
┌───────────────────────────── AlsaMixer v1.0.28 ──────────────────────────────┐
│ Card: LEGO Mindstorms EV3 speaker                    F1:  Help               │
│ Chip:                                                F2:  System information │
│ View: F3:[Playback] F4: Capture  F5: All             F6:  Select sound card  │
│ Item: Playback                                       Esc: Exit               │
│                                                                              │
│                                     ┌──┐                                     │
│                                     │  │                                     │
│                                     │  │                                     │
│                       ┌───────── Sound Card ─────────┐                       │
│                       │-  (default)                  │                       │
│                       │0  LEGO Mindstorms EV3 speaker│                       │
│                       │1  USB Audio DAC              │                       │
│                       │   enter device name...       │                       │
│                       └──────────────────────────────┘                       │
│                                     │▒▒│                                     │
│                                     │▒▒│                                     │
│                                     │▒▒│                                     │
│                                     │▒▒│                                     │
│                                     └──┘                                     │
│                                      75                                      │
│                                  <Playback>                                  │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

E para tocar um ficheiro mp3 (previamente trasnferido por ssh):

root@ev3dev:~# apt-get install mpg321
root@ev3dev:~# mpg321 -a hw:1,0 Thunderstruck.mp3

Rock’n Roll!!!

Durante a execução o mpg321 consome entre 22 a 25% de CPU. É um bocado alto (quando se utiliza a rede em simultâneo, por exemplo um update, ocorrem soluços) e como dizem que o SoX consome menos CPU…

root@ev3dev:~# apt-get install sox libsox-fmt-all
root@ev3dev:~# export AUDIODEV=hw:1,0
root@ev3dev:~# play Thunderstruck.mp3

Durante a execução o play consome  20 a 22% de CPU, apenas marginalmente melhor.
Mas como SoX permite vários formatos (wav, ogg…) permite o redireccionamento (pipe) de outros comandos como o espeak:

root@ev3dev:~# espeak -v pt-pt “olá” –stdout | play -t wav –

E temos o nosso EV3 a falar português 🙂

EV3 wireless com ev3dev

Depois de instalar a última versão do ev3dev (ev3dev-jessie-2014-07-12) experimentei ligar 2 dos dongles Wi-FI que uso com o Raspberry Pi:

  • ThePiHut
  • Wi-Pi

Para o kernel são o mesmo dispositivo:

root@ev3dev:~#lsusb
ID 148f:5370 Ralink Technology, Corp. RT5370 Wireless Adapter

Mas não foram carregados nenhuns drivers por isso actualizei:

root@ev3dev:~#apt-get update
root@ev3dev:~#apt-get upgrade
root@ev3dev:~#apt-get dist-upgrade
root@ev3dev:~#reboot

Agora o Debian já tem os drivers:

root@ev3dev:~#dmesg
(...)
usbcore: registered new interface driver rt2800usb
(...

mas surge uma outra mensagem sobre não carregar o firmware por isso

root@ev3dev:~#dmesg
apt-get install firmware-ralink
ieee80211 phy1: rt2x00_set_rt: Info - RT chipset 5390, rev 0502 detected
ieee80211 phy1: rt2x00_set_rf: Info - RF chipset 5370 detected
ieee80211 phy1: Selected rate control algorithm 'minstrel_ht'
ieee80211 phy1: rt2x00lib_request_firmware: Info - Loading firmware file 'rt2870.bin'
ieee80211 phy1: rt2x00lib_request_firmware: Info - Firmware detected - version: 0.29

E para a ligação wireless, à semelhança do Raspbian no Pi, editei o ficheiro ‘interfaces’:

root@ev3dev:~#nano /etc/network/interfaces
# interfaces(5) file used by ifup(8) and ifdown(8)
# Include files from /etc/network/interfaces.d:
source-directory /etc/network/interfaces.d

### acrescentar as linhas abaixo ###
auto lo
iface lo inet loopback
allow-hotplug wlan0
iface wlan0 inet dhcp
   wpa-ssid "WI-Fi SID"
   wpa-psk "Wi-Fi Password"

iface default inet dhcp

Confirma-se:

root@ev3dev:~#ifconfig
wlan0     Link encap:Ethernet  HWaddr 00:0f:55:a8:ac:0a  
          inet addr:192.168.1.69  Bcast:192.168.1.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:11 errors:0 dropped:0 overruns:0 frame:0
          TX packets:5 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:1447 (1.4 KiB)  TX bytes:1074 (1.0 KiB)

Só não percebo porque motivo só funciona um dos dongles (o ThePiHut) quando aparentam ser iguais. Talvez o firmware não o seja completamente.