Programação de veículos autônomos com linguagem BZR (Parte 1)

O Heptagon é um software para a modelagem de sistemas de eventos discretos (SEDs) que utiliza a linguagem BZR para a verificação das propriedades dos SEDs e síntese de controladores para SEDs. Neste post apresentaremos em duas partes o desenvolvimento de um veículo autônomo utilizando o Heptagon/BZR, sintetizando automaticamente o controlador e integrando o código gerado ao Arduino.

Essa proposta envolve uma importante teoria para os SEDs: a teoria de controle supervisório. Nessa teoria assume-se que o veículo (sistema) é composto por um conjunto de sub-sistemas (componentes) e que cada sub-sistema apresenta seu próprio comportamento. Nessa abordagem, um supervisor deve atuar sobre os componentes para garantir o comportamento desejado para o veículo como um todo.

Ao longo do primeiro post, apresentaremos a modelagem dos principais componentes do veículo na linguagem BZR e como sintetizar automaticamente o controlador utilizando o Heptagon. Nesse caso, é importante destacar que é necessário ter o Heptagon/BZR instalado e um tutorial para instalação do Heptagon já foi visto aqui no blog. Por fim, veremos em um segundo post como o código gerado pode ser integrado ao Arduino.

1 Modelagem

O processo de modelagem do veículo consiste em definir o comportamento esperado considerando todas as variáveis de entrada e as ações associadas a essas entradas. Assim, queremos um veículo autônomo que seja capaz de detectar obstáculos e desviar sua rota para evitar a colisão. Isso definido, podemos pensar nos sub-sistemas que compõem o veículo. Consideramos então os sub-sistemas:

  • Para que ele possa detectar obstáculos podemos assumir a existência um sensor de distância.
  • Necessitaremos também de motores que controlam as rodas do veículo, definindo a direção e sentido.

Definimos então dois sub-sistemas: um sub-sistema que considera o sensor de distância como entrada e que responde se há ou não obstáculo, e outro sub-sistema que define o sentido de giro e velocidade dos motores. Com base nisso, iremos definir as seguintes regras:

  1. A distância mínima do veículo para um obstáculo será de 45 cm. Caso a distância seja menor ou igual a 45 cm, ele deverá mudar de direção;
  2. O veículo possuirá quatro rodas, logo, quatro motores. Considerando quatro motores, ele poderá mudar a sua direção mudando o sentido do giro de duas dessas rodas: enquanto duas giram para frente, duas giram para trás.

A partir dessas definições, podemos iniciar a modelagem utilizando a linguagem BZR. Nessa abordagem, cada componente deve ser descrito no formalismo de autômatos, usando o conceito de máquinas de estados. Para facilitar a compreensão, os diagramas de cada um dos autômatos acompanham a modelagem de cada sub-sistema.

1.1 Sensor de obstáculos

O autômato que define o sensor de obstáculos pode ser representado por dois estados: há um obstáculo e não há um obstáculo. Assim, podemos assumir que existe uma variável de entrada (dist) que fornece ao sub-sistema o valor da distância entre o veículo e o obstáculo mais próximo. As transições entre os estados acontecem com base nas condições: se a distância é menor ou igual a 45 cm, então há obstáculo. Caso contrário, não há. A resposta do autômato será sempre um valor  associado ao estado atual, verdadeiro (há obstáculo) ou falso (não há obstáculo). Graficamente, podemos representar através da Figura 1.

obstacle
Figura 1. Autômato para o sensor de obstáculos

Com base na Figura 1, podemos implementar o autômato utilizando a linguagem BZR, conforme o código que segue:

node obstacle(dist: int) returns (obs: bool)
let
automaton
state OBSTACLE do
obs = true;
unless (dist > 45) then NO_OBSTACLE
state NO_OBSTACLE do
obs = false;
unless (dist <= 45) then OBSTACLE
end
tel
view raw obstacle.mli hosted with ❤ by GitHub

Observe que a implementação é extremamente simples e que a informação gerada pode ser interpretada como um sinal. Assim, enquanto o veículo se move, um sinal é sempre atualizado quanto a existência de um obstáculo.

1.2 Motor

Como previsto, a modelagem de quatro motores é necessária para definir o movimento do veículo. Podemos modelar cada um dos motores com um único autômato já que eles terão exatamente o mesmo comportamento.

A modelagem do motor apresenta dois estados possíveis: para frente (FORWARD) ou para trás (BACKWARD). Note que a transição é definida pela variável de entrada c, sendo uma entrada definida pelo controlador que será sintetizado. Essa característica define esse automato como um autômato controlável, diferente do sensor de obstáculos. Dessa forma, as transições entre esses estados poderão ocorrer somente se elas não ferirem as regras definidas. Outras duas variáveis, mode e vel, representam uma constante que define o sentido do giro do motor e a velocidade do giro, respectivamente.

motor
Figura 2. Autômato do motor

A implementação na linguagem BZR é definida pelo código abaixo.

node motor(c: bool) returns (mode, velocity: int)
let
automaton
state FORWARD do
mode = 1;
velocity = 255;
unless not c then BACKWARD
state BACKWARD do
mode = 2;
velocity = 255;
unless c then FORWARD
end
tel
view raw motor.mli hosted with ❤ by GitHub

1.3 Comportamento do veículo

Além desses, um outro autômato deve ser modelado para definir o comportamento do veículo. O autômato Movement também é um automato controlável, logo, as transições dependem da variável c definida pelo controlador para que as regras sejam satisfeitas. O autômato apresenta 3 estados possíveis: MOVING (em movimento), STURNING (iniciar uma rotação) ou TURNING (em rotação). A variável turning define se o veículo está desviando de um obstáculo ou não. Quando o movimento de rotação é iniciado, a variável mbc é incrementada durante doze iterações. Enquanto o mbc for menor que doze, o estado será TURNING. Essa estratégia faz com que o veículo dê uma volta de 120 graus, evitando que ele ainda colida com o obstáculo que o sensor não foi capaz de detectar. Ao retornar para o estado STURNING, a variável mbc assume o valor zero e o veículo pode mover-se para frente ou voltar para o estado TURNING, caso ainda exista obstáculo. A Figura 3 apresenta todos os estados desse autômato.

movement
Figura 3. Autômato que define os estados de movimento do veículo

A implementação em BZR do autômato é apresentado abaixo.

node movement(c: bool) returns (turning: bool; ombc:int)
var last mbc:int = 0;
let
ombc = mbc;
automaton
state MOVING do
mbc = 0;
turning = false;
unless not c then STURNING
state STURNING do
mbc = 0;
turning = true;
until not c then TURNING | c then MOVING
state TURNING do
mbc = last mbc + 1;
turning = true;
unless c & (12 = last mbc) then STURNING
end
tel
view raw movement.mli hosted with ❤ by GitHub

1.4 O controlador

Após definir todos os autômatos, podemos definir o node principal que indicará como os componentes estarão conectados bem como as regras que deverão ser garantidas pelo controlador. Iremos agora definir como cada autômato deverá interagir com o outro para garantir que o veículo tenha o comportamento desejado.

Note que o código abaixo define como entrada o valor da distância obtida pelo sensor (distance). Como variáveis de saída, são esperadas aquelas que definem a existência de um obstáculo (obs), se o veículo está rotacionando (turning), quatro variáveis para definir o estado de cada motor (motor1, …, motor4), quatro variáveis que definem a velocidade de cada motor (vel1, …, vel4) e um contador (mbc).

node controller(distance: int) returns (obs, turning: bool; motor1, motor2, motor3, motor4, vel1, vel2, vel3, vel4, cont: int)
contract
var
rule: bool;
let
rule = not obs & not turning or (turning & (motor1=1 & motor2=2 & motor3=1 & motor4=2));
tel
enforce rule
with (c_move, c_motor1, c_motor2, c_motor3, c_motor4: bool)
let
obs = inlined obstacle(distance);
(turning, cont) = inlined movement(c_move);
(motor1, vel1) = inlined motor(c_motor1);
(motor2, vel2) = inlined motor(c_motor2);
(motor3, vel3) = inlined motor(c_motor3);
(motor4, vel4) = inlined motor(c_motor4);
tel
view raw controller.mli hosted with ❤ by GitHub

Observe na linha 6 que as regras 1 e 2 definidas no início da modelagem foram condensadas em uma única regra (rule). Em outras palavras temos: se há obstáculo e o veículo está no estado de rotação (STURNING ou TURNING), então o estado continua definido como TURNING e os motores devem ser ajustados da seguinte forma: motor1 e motor3 giram para frente e motor2 e motor4 giram para trás. O complementar dessa regra diz que: se não houver obstáculo, então todos os motores devem girar para frente e, consequentemente, o veículo deve andar para frente.

Nas linhas 8 e 9 há a declaração enforce com a regra (rule) e a declaração with que indica as variáveis controláveis c_move, c_motor1, c_motor2, c_motor3, c_motor4. O objetivo é fazer com que essas variáveis sejam utilizadas pelo controlador para que a regra seja obedecida. Observe que essas variáveis são argumentos para a chamada dos autômatos entre as linhas 12 e 16. Assim, a partir dessas variáveis, o controlador poderá ajustar o estado de cada autômato para atender a regra.

Por fim, um arquivo nomeado car.ept com todo o código BZR da modelagem deve ser salvo em um diretório exclusivo. O arquivo fica estruturado da seguinte maneira:

node obstacle(dist: int) returns (obs: bool)
let
automaton
state OBSTACLE do
obs = true;
unless (dist > 45) then NO_OBSTACLE
state NO_OBSTACLE do
obs = false;
unless (dist <= 45) then OBSTACLE
end
tel
node motor(c: bool) returns (mode, velocity: int)
let
automaton
state FORWARD do
mode = 1;
velocity = 255;
unless not c then BACKWARD
state BACKWARD do
mode = 2;
velocity = 255;
unless c then FORWARD
end
tel
node movement(c: bool) returns (turning: bool; ombc:int)
var last mbc:int = 0;
let
ombc = mbc;
automaton
state MOVING do
mbc = 0;
turning = false;
unless not c then STURNING
state STURNING do
mbc = 0;
turning = true;
until not c then TURNING | c then MOVING
state TURNING do
mbc = last mbc + 1;
turning = true;
unless c & (12 = last mbc) then STURNING
end
tel
node controller(distance: int) returns (obs, turning: bool; motor1, motor2, motor3, motor4, vel1, vel2, vel3, vel4, cont: int)
contract
var
rule: bool;
let
rule = not obs & not turning or (turning & (motor1=1 & motor2=1 & motor3=2 & motor4=2));
tel
enforce rule
with (c_move, c_motor1, c_motor2, c_motor3, c_motor4: bool)
let
obs = inlined obstacle(distance);
(turning, cont) = inlined movement(c_move);
(motor1, vel1) = inlined motor(c_motor1);
(motor2, vel2) = inlined motor(c_motor2);
(motor3, vel3) = inlined motor(c_motor3);
(motor4, vel4) = inlined motor(c_motor4);
tel
view raw car.mli hosted with ❤ by GitHub

2 Sintetizando o controlador

A síntese do controlador propriamente dita depende do Heptagon/BZR e do Sigali, que é capaz de verificar se a lógica das regras está correta, evitando inconsistências e garantindo que o controlador não tenha que lidar com situações de erros em tempo de execução.

Para a síntese, faremos uso do arquivo de Makefile apresentado abaixo para definir as etapas de compilação. Isso permite que a compilação seja feita de modo automático.

SIM_PROG=car
SIM_NODE=controller
CONTRACT_NODE=controller
CONTRACT_PROG=$(SIM_PROG)
CC=gcc
HEPTC=heptc
HEPTS=hepts
SIGALI=/home/osboxes/tmc/Sigali-2.4/bin/sigali
HEPT_LIB_C=/usr/local/lib/heptagon/c
CTRLR_PROG=$(CONTRACT_NODE)_controller
HEPT_PATH=$(shell pwd)
# Path to C headers
HEPT_CINCLUDES=
CFLAGS= $(HEPT_CINCLUDES) -I $(CTRLR_PROG)_c -I $(HEPT_LIB_C)
HEPT_SOURCES = $(wildcard *.ept)
HEPT_MODNAMES = $(patsubst %.ept,%,$(HEPT_SOURCES))
HEPT_C_FILES = $(shell echo $(HEPT_SOURCES) | sed -r 's+([^ ]*).ept+\1_c/\1.c+g')
HEPT_C_DIRS = $(patsubst %.ept,%_c,$(HEPT_SOURCES))
HEPT_Z3Z_DIRS = $(patsubst %.ept,%_z3z,$(HEPT_SOURCES))
HEPT_OBJS = $(HEPT_C_FILES:.c=.o)
CFLAGS += $(patsubst %, -I %,$(HEPT_C_DIRS))
CTRLR_C_FILES = $(CTRLR_PROG)_c/$(CTRLR_PROG).c
CTRLR_OBJS = $(CTRLR_C_FILES:.c=.o)
all: sim
sim: $(SIM_NODE)_sim $(SIM_PROG).epci
$(SIM_NODE)_sim: $(SIM_PROG)_c/_main.o $(HEPT_OBJS) $(CTRLR_OBJS)
$(CC) $(LDFLAGS) -o $(SIM_NODE)_sim $^
$(CONTRACT_PROG)_z3z/$(CONTRACT_NODE).z3z: $(CONTRACT_PROG).ept
$(HEPTC) -target c -target z3z -s $(SIM_NODE) $(CONTRACT_PROG).ept
$(CONTRACT_PROG).epci \
$(CONTRACT_PROG)_c/$(CONTRACT_PROG).c \
$(CONTRACT_PROG)_c/_main.c: $(CONTRACT_PROG)_z3z/$(CONTRACT_NODE).z3z
%.epci: %.epi
$(HEPTC) $<
$(CTRLR_PROG).ept: $(CONTRACT_PROG)_z3z/$(CONTRACT_NODE).z3z
$(SIGALI) < $(CONTRACT_PROG)_z3z/$(CONTRACT_NODE).z3z > /dev/null
$(CTRLR_PROG).epci $(CTRLR_PROG)_c/$(CTRLR_PROG).c: $(CTRLR_PROG).ept
$(HEPTC) -target c $(CTRLR_PROG).ept
%.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
clean:
rm -fr $(HEPT_C_DIRS) $(HEPT_Z3Z_DIRS) $(HEPT_JAVA_DIRS)
rm -f $(SIM_NODE)_sim main.*
rm -f *.epci *.mls *.ml *.obc ./*~
rm -f $(CTRLR_PROG).ept
# Dependences
$(HEPT_OBJS) $(SIM_PROG)_c/_main.o: $(CTRLR_C_FILES)
view raw Makefile hosted with ❤ by GitHub

As variáveis SIM_PROG, SIM_NODE e CONTRACT_NODE no Makefile definem o nome do arquivo *.ept criado no processo de modelagem, o nome do arquivo para o controlador a ser compilado e o nome do node principal, respectivamente. As linhas 11 e 13 definem o caminho para o Sigali e para o Heptagon, respectivamente. Ao executar o Makefile, o Heptagon deverá gerar arquivos com os formatos *.c e *.z3z, conforme a linha:

$(HEPTC) -target c -target z3z -s $(SIM_NODE) $(CONTRACT_PROG).ept
view raw Makefile hosted with ❤ by GitHub

A variável $(HEPTC) é definida anteriormente como o heptc, comando para compilar um arquivo de extensão *.ept em C ou Java. No Makefile, definimos a linguagem C passando o c como parâmetro -target. Além disso, geramos o arquivo *.z3z contendo o nome do controlador controller passando como parâmetro a extensão *.z3z. O parâmetro -s indica o nome do node principal, neste caso, controller (o controlador). Por fim, o último parâmetro é o nome do arquivo a ser compilado, o car.ept.

O heptc cria então dois diretórios: car_c (estão os arquivos em *.c) e car_z3z, onde está o arquivo controller.z3z. O arquivo controller.z3z será utilizado como argumento ao Sigali para que a análise das regras seja feita, confome a linha 54 do Makefile:

$(SIGALI) < $(CONTRACT_PROG)_z3z/$(CONTRACT_NODE).z3z > /dev/null
view raw Makefile hosted with ❤ by GitHub

Após a compilação do Sigali, será gerado o arquivo controller_controller.ept que, após ser novamente compilado pelo heptc (linha 57 do Makefile), irá gerar (finalmente!) o simulador chamado controller_sim.

Com os arquivos car.ept e Makefile em um mesmo diretório, é necessário executar o comando make no terminal para iniciar a compilação. Após esse processo, o controlador é sintetizado e pode ser testado a partir do executável controller_sim. Caso exista algum erro lógico nas regras definidas o Sigali irá sinalizar e o processo de síntese será interrompido. Considerando que todo o processo ocorreu com sucesso, para executar o controlador basta entrar com o comando abaixo no terminal.

$./controller_sim
view raw Makefile hosted with ❤ by GitHub

3 Continua…

É isso! O processo de modelagem e síntese do controlador utilizando o Heptagon/BZR foi concluído com sucesso. No próximo post apresentaremos com detalhes os passos necessários para integrar ao Arduino o controlador que geramos neste post.

Publicidade

Um comentário em “Programação de veículos autônomos com linguagem BZR (Parte 1)

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s