WiFi no Raspberry Pi

Agora que o Raspberry Pi está com as configurações básicas prontas, (veja o post anterior), o próximo passo é simplificar a conexão dele com a internet.

Quero deixá-lo em um lugar onde não atrapalhe. Quanto menos fios e cabos precisar melhor, portanto WiFi é uma opção melhor do que Ethernet.

Esse Raspberry Pi é bem antigo e não tem WiFi integrado na placa e precisa de um adaptador USB-WiFi.

Mas vamos lá para a configuração. Para quem não sabe, o sistema operacional do Raspberry OS (ou Raspbian) é baseado no Debian Linux e podemos usar muito da documentação deste.

O link abaixo explica como configurar o WiFi no Debian:

https://wiki.debian.org/WiFi/HowToUse#Using_ifupdown

Primeiro precisamos determinar qual é o nome do adaptador WiFi (seja ele integrado na placa ou via USB). Para isso logamos usando SSH e usamos o comando ip a, que lista as interfaces de rede.

$ ip a
> 1: lo: ...
> 2: eth0: ...
> 3: wlan0: ...

A saída do comando tem um tanto a mais de informação do que estou mostrando. Porém podemos identificar "wlan0" como a interface WiFi.

Agora vamos criar um arquivo de configuração para a rede sem fio:

  1. Usamos sudo su para mudar para o usuário root;
  2. Mudamos a máscara de criação de arquivos para 077, que faz com que apenas o dono do arquivo tenha acesso. Isso é importante para evitar os usuários possam ler o arquivo abaixo, que vai conter a senha do WiFi
  3. Criamos o arquivo de configuração usando o comando touch; e
  4. Editamos o arquivo (com o editor nano) adicionando a configuração mostrada em seguida.
$ sudo su
$ uname 077
$ touch /etc/network/interfaces.d/wlan0.conf
$ nano /etc/network/interfaces.d/wlan0.conf

O arquivo de configuração pode ter qualquer nome. Para facilitar coloco o nome da interface que ele configura.

O conteúdo do arquivo de configuração é mostrado abaixo, configurando a interface para ser detectada automaticamente, usar DHCP para obter um endereço de IP e também configura o SSID da rede (nome da rede sem fio) e a sua senha.

# Arquivo: /etc/network/interfaces.d/wlan0.conf mode=0600
allow-hotplug wlan0
iface wlan0 inet dhcp
    wpa-ssid NOME_DA_REDE
    wpa-psk SENHA_DA_REDE

Note que no arquivo de configuração precisamos especificar corretamente o nome da interface de rede duas vezes, "wlan0" no meu caso.

Raspberry Pi modelo B com WiFi

Agora é só reiniciar o Raspberry Pi, desconectar todos os cabos desnecessários e curtir ele através da rede sem fio. Agora, a única coisa que ele precisa é uma tomada.

Revivendo um Raspberry Pi B (modelo antigo)

Tenho um Raspberry Pi antigo, modelo B, e quero fazer ele voltar à vida para rodar testes de projetos multiplataforma.

Gravando o Cartão SD

Entrando no site do Raspberry Pi, encontramos a página de downloads. Optei por escolher a imagem manualmente na página abaixo.

https://www.raspberrypi.com/software/operating-systems/

Como pretendo criar um servidor, não preciso da imagem desktop, podendo utilizar a imagem "lite" (leve), que tem apenas a linha de comando.

Fiz o download da imagem "Raspberry Pi OS Lite (32-bit)", obtendo o arquivo "2022-09-22-raspios-bullseye-armhf-lite.img.xz".

Se você tiver algum modelo mais novo do Raspberry Pi, é possível usar uma imagem "Raspberry Pi OS Lite (64-bit)". Há uma lista dos modelos que suportam o sistema 64-bits.

Para extrair a imagem usamos o comando xz. Isso remove o arquivo compactado .img.xz cria um arquivo descompactado .img.

$ xz -d 2022-09-22-raspios-bullseye-armhf-lite.img.xz

Então, para gravá-la usamos o comando dd, enviando os dados para o cartão SD no caminho /dev/mmcblk0.

$ dd if=2022-09-22-raspios-bullseye-armhf-lite.img of=/dev/mmcblk0

Se tiver dúvidas sobre qual é o seu cartão de memória, o comando ls -lrt /dev/ vai te mostrar uma lista com os dispositivos detectados no sistema, listados do mais velho para o mais novo.

Se quiser também é possível escrever no cartão SD enquanto extrai, sem criar o arquivo descomprimido.

$ xz -dc 2022-09-22-raspios-bullseye-armhf-lite.img.xz | dd of=/dev/mmcblk0

Antes do Primeiro Boot

Habilitando SSH

Para realizar algumas modificações montei a partição de root e de boot do Raspberry Pi.

$ mount /dev/mmcblk0p2 /mnt
$ mount /dev/mmcblk0p1 /mnt/boot

Na pasta /mnt/boot (/boot do Raspberry Pi) criei um arquivo vazio chamado ssh, que faz o servidor SSH ser habilitado no primeiro boot.

touch /mnt/boot/ssh

Obsoleto: Isso evita ter que acessar o mini computador com tela e teclado só para habilitar o SSH. Assim posso ir direto para o acesso remoto.

Infelizmente, agora é preciso configurar usuário e senha no primeiro boot. Então parece ser obrigatório conectar uma tela e teclado. Deve haver algum jeito de evitar isso.

Agora basta desmontar a partição e iniciar o Raspberry Pi com o cartão SD.

$ umount /dev/mmcblk0p1
$ umount /dev/mmcblk0p2

Raspberry Pi modelo B

Primeiro Boot

As primeiras coisas a fazer são:

  • Escolher o usuário
  • Escolher a senha
  • Identifique o IP no roteador (no meu caso é 192.168.0.101)
  • Instalar chave SSH (opcional)
    $ ssh-copy-id [email protected]
    
  • Fazer o login pelo SSH
    $ ssh [email protected]
    
  • Atualizar
    pi$ sudo apt update
    pi$ sudo apt upgrade
    
  • Reduzir a memória da GPU
    pi$ sudo raspi-config
    
    • 4 Performance Options
      • P2 GPU Memory
        • 8 > OK
    • Finish
  • Reiniciar
    pi$ sudo reboot
    

Ansible: Criptografando Senhas de Usuários

As vezes queremos gerar uma senha fixa para usuários, a qual vamos reutilizar em várias máquinas.

Um exemplo típico é criar usuário administrador, capaz de realizar login com senha. Dessa forma, caso ocorra algum problema com o servidor SSH ou perca a sua chave privada junto com o seu HD, ainda é possível fazer login direto na máquina, por VNC ou por outros meios de acesso remoto.

Porém essa é a senha de administrador de muitos computadores e não queremos colocá-la diretamente em scripts de configuração, como os do Ansible, onde podem ser lidas diretamente.

Podemos, no entanto, criar uma versão encriptada da senha (a mesma usada no arquivo /etc/shadow) e aí sim colocá-la no script.

O código abaixo cria uma senha segura 24 caracteres utilizando pwgen. A opção -s indica que a senha deve ser completamente aleatória.

$ pwgen -s 24 1
> VONga6jUFoXCMQNhDlHgTHQY

Por fim usamos openssl passwd para encriptar a senha gerada Usamos a opção -6 para criptografar a senha com SHA512.

$ openssl passwd -6 'VONga6jUFoXCMQNhDlHgTHQY'
> $6$80Ll7KOwC/XDQVgZ$NwTjYG8cXcMTr9NI1EbtaX7OEAIBq3ULUSJPmpjtk/vUiyB9duYO3aNqmWmBnS.mjgaN37/mo34R8COzNrFBh0

Dessa forma, a preciosa senha está segura, longe de olhos curiosos, e podemos colocá-la no script de criação de usuários.

# Arquivo:  tasks/default-tasks.yml
---
- name: Criar usuário admin com senha
  ansible.builtin.user:
    name: admin
    comment: Usuário Administrador
    password: "$6$80Ll7KOwC/XDQVgZ$NwTjYG8cXcMTr9NI1EbtaX7OEAIBq3ULUSJPmpjtk/vUiyB9duYO3aNqmWmBnS.mjgaN37/mo34R8COzNrFBh0"

Ansible: Testando Playbooks

Ansible é uma excelente ferramenta para automatização de tarefas.

Cria-se uma lista de computadores e instalando neles um servidor SSH e Python 3, é possível automatizar a instalação de programas, alteração de configurações e, o meu favorito, atualização dos computadores.

No maior estilo do TDD (Test Driven Development), muitas vezes, queremos ter certeza que algo está funcionando antes de prosseguir com a execução do playbook.

Exemplos destes casos são:

  • A nova configuração funciona?
  • O usuário realmente pode (não pode) usar usar sudo?
  • Login do usuário root está mesmo desativado?
  • O firewall liberou/bloqueou a porta HTTP?

Podemos criar testes como esses com certa facilidade e inclusive podemos escolher entre a máquina remota ou local para executar os comandos, o que é essencial para alguns testes.

Abaixo mostro dois exemplos, testando as permissões de um usuário do servidor remoto e testando se o computador local pode fazer login como root.

# Arquivo: tasks/testes.yml
---
- name: TESTE Verificar se o usuário 'admin' pode usar SUDO sem senha
  # Teste no servidor remoto:
  # - su admin -c "xxx": executa o comando xxx como admin;
  # - whoami: imprime o nome do usuário; e
  # - Esperamos que imprima 'admin' (sem o sudo) e em seguida
  #   imprima 'root' (com sudo).
  ansible.builtin.command: |
    su admin -c "whoami; sudo whoami"
  register: result
  changed_when: false
  failed_when: >
    result.stdout_lines[0] != 'admin'
    or result.stdout_lines[1] != 'root'
  timeout: 5

- name: TESTE Negar login do usuário 'root' port SSH
  # Teste no computador local:
  # - SSH, ativando senha e desativando chave pública;
  # - whoami: um comando qualquer para ser executado; e
  # - Esperamos que a tentativa de login seja negada.
  ansible.builtin.command: >
    ssh -oPasswordAuthentication=yes -oPubkeyAuthentication=no
    -p{{ ansible_port }} root@{{ ansible_host }} whoami
  register: result
  changed_when: false
  failed_when: "'Permission denied (publickey)' not in result.stderr"
  timeout: 5
  # Esta parte final definie que o teste é executado no computador local
  # (que executa o Ansible).
  # Desativamos ansible_become para que use o usuário atual, em vez de
  # usar sudo para se tornar root.
  delegate_to: localhost
  vars:
    ansible_become: false

Ansible: Variáveis Dependentes do Sistema Operacional

Ansible é uma excelente ferramenta para automatização de tarefas.

Cria-se uma lista de computadores e instalando neles um servidor SSH e Python 3, é possível automatizar a instalação de programas, alteração de configurações e, o meu favorito, atualização dos computadores.

No entanto, há coisas que são diferentes de um sistema para outro, por exemplo caminhos de arquivos de configuração e nomes de pacotes.

Para evitar criar várias e várias tarefas que fazem a mesma coisa ajustadas para cada sistema (when: ansible_distribution == ...), enchendo o playbook com código repetido, Ansible permite a importação de arquivos de variáveis com o módulo include_vars, onde podemos escolher dinamicamente o arquivo de variáveis correspondente ao sistema.

Então podemos criar um arquivo para cada um dos sistemas em uso e seguir em frente. Porém isso acaba por se tornar um pesadelo de manutenção: cada nova variável precisa ser adicionada em TODOS os arquivos de variáveis e, ao criar um arquivo de variáveis novo, é preciso atualizar todas as variáveis possíveis.

Uma alternativa interessante seria uma criar estrutura hierárquica, onde definimos valores padrão para todos, em seguida substituímos com os valores para a família, então com os valores para a distribuição e quem sabe até com valores para certas versões.

Padrão          default
                 /   \
Família      Debian  FreeBSD
              / \
Distro  Debian   Ubuntu

Além disso, é importante que a inexistência de algum desses arquivos de especialização não seja um empecilho.

Com um pouco de pesquisa e um pouco de tentativa-e-erro, e com "um pouco" quero dizer interminável, cheguei no código abaixo:

# Arquivo: tasks/default-tasks.yml
---
- name: Carrega variáveis padrão, da família e da distro
  ansible.builtin.include_vars: "{{ item }}"
  with_fileglob:
    - vars/default.yml
    - vars/family_{{ ansible_os_family }}.yml
    - vars/distro_{{ ansible_distribution }}.yml

Usamos include_vars, como de costume, porém junto com with_fileglob. Este geralmente é usado para encontrar vários arquivos usando coringas (wildcards) como vars/*.yml. Mas aqui não usamos coringas, damos direto o nome do arquivo. Se o arquivo não é encontrado ele é ignorado.

Abaixo estão exemplos dos arquivos de variáveis padrão e especializado.

# Arquivo: vars/default.yml
---
doas_config_file: /etc/doas.conf
# Arquivo: vars/family_FreeBSD.yml
---
doas_config_file: /usr/local/etc/doas.conf

Copiando a suas chaves SSH do GitHub ou GitLab

As vezes queremos copiar uma chave pública SSH para um computador.

Esse é um jeito fácil de adicionar a sua própria chave ou de adicionar a chave de alguém para te ajudar com algum problema.

Tendo uma conexão com a internet, é possível fazer o download das chaves públicas SSH de qualquer usuário do GitHub ou GitLab.

Com os comandos abaixo é possível fazer o download da chave adicioná-las nas chaves permitidas para acesso SSH. Basta trocar USUARIO pelo nome do usuário desejado.

  1. Configure a máscara de permissões para 077: leitura e escrita apenas para o usuário dono.
    • umask 077
  2. Crie a pasta ~/.ssh, se ela não existir.
    • mkdir -p ~/.ssh
  3. Faça o download das chaves:
    • GitHub: curl https://github.com/USUARIO.keys >>~/.ssh/authorized_keys
    • GitLab: curl https://gitlab.com/USUARIO.keys >>~/.ssh/authorized_keys

Também podemos usar wget em vez de curl, substituindo curl por wget -O-.

Mudei um arquivo no Yocto mas o Bitbake não recompila

Quando compilando uma imagem no Yocto, às vezes o Bitbake se confunde e não consegue determinar que um arquivo mudou na receita.

Nesse ponto, recomendo limpar toda as pastas tmp e sstate-cache e recompilar do zero. No entanto, recompilar do zero pode demorar muito e podemos deixar isso para o fim do dia.

Para resolver o problema sem tomar muito tempo, basta limpar o diretório de trabalho e o cache. Digamos que esteja trabalhando no arquivo NOME.bb, utilizamos os comandos abaixo.

$ bitbake NOME -c clean
$ bitbake NOME -c cleansstate

Então basta compilar a imagem novamente.

$ bitbake IMAGEM

Minhas Preferências de Formatação

Abaixo estão algumas das minhas preferências e suas razões.

1. Endentação e aspecto geral

  • Endentar com 4 espaços;
  • Limite de 80 caracteres por linha; e
  • Tabulações proibidas.

Para mim 2 espaços é pouco e 8 espaços é muito na endentação. Quatro espaços parecem ser o meio termo adequado.

Linhas longas atrapalham a leitura do código: eu desejo um código tão legível quanto livros e jornais. Além disso, o limite de 80 caracteres permite abrir arquivos lado a lado sem necessitar de rolagem horizontal e ainda facilita revisão do código anterior e atual abertos lado a lado.

Com respeito a proibir tabulações, existe o argumento que tabulações são mais flexíveis, pois o programador pode escolher o tamanho de sua preferência. O argumento é válido, porém somente até se misturarem as tabulações e os espaços: nesse instante, a única pessoa que vê alinhado é o próprio autor do código; e todo o resto do mundo vê uma bagunça desalinhada. Usar apenas um tipo de espaço em branco elimina esse problema.

2. Alinhamento das chaves

Abertura de chaves na linha abaixo.

Isso ajuda a identificar onde acontece a abertura do bloco. Chave abrindo na própria linha permite determinar o início do bloco sem precisar procurar por ela no fim de alguma linha.

unsigned long int funcao_h(
    unsigned long int parametro1,
    unsigned long int parametro2,
    unsigned long int parametro3)
{
    return parametro1 + parametro2 - parametro3;
}

Compare abaixo a mesma função abrindo as chaves na linha. Não é tão óbvio como no caso acima.

unsigned long int funcao_h(
    unsigned long int parametro1,
    unsigned long int parametro2,
    unsigned long int parametro3) {
    return parametro1 + parametro2 - parametro3;
}

Abrindo as chaves na linha abaixo, fica óbvio onde os parâmetros terminam e onde inicia o corpo da função. O mesmo acontece em blocos de controle como if, for, etc.

Mesmo se eliminarmos qualquer informação sobre os caracteres, como é mostrado abaixo, facilmente identificamos a lista de parâmetros e o início do corpo da função.

XXXXXXXX XXXX XXX XXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
X
    XXXXXX XXXXXXXXXX X XXXXXXXXXX X XXXXXXXXXXX
X

XXXXXXXX XXXX XXX XXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX
    XXXXXXXX XXXX XXX XXXXXXXXXXX X
    XXXXXX XXXXXXXXXX X XXXXXXXXXX X XXXXXXXXXXX
X

A maior parte do tempo que passamos programando é utilizada, na verdade, lendo o código. Esta pequena escolha ajuda a encontrar a parte que importa no momento.

3. Parâmetros de funções

A lista de parâmetros de funções e a lista de argumentos de chamada de uma função devem estar todos em uma linha ou cada um em sua linha.

Dessa forma, a lista de parâmetros (ou argumentos) realmente se parece com uma lista. É fácil de contar os parâmetros e é fácil de encontrá-los.

Nesse ponto, há três possibilidades avaliadas na ordem seguinte:

  • Protótipo todo na mesma linha;
  • Lista de parâmetros toda na segunda linha; e
  • Lista de parâmetros com um parâmetro por linha.

Exemplos abaixo:

/* Protótipo inteiro cabe em uma linha: */

int funcao_f(int parametro1, int parametro2)
{
    return parametro1 + parametro2;
}

/*
 * Protótipo não cabe em uma única linha, porém
 * a lista de parâmetros cabe inteira na segunda linha:
 */

unsigned long int funcao_g(
    unsigned long int parametro1, unsigned long int parametro2)
{
    return parametro1 + parametro2;
}

/*
 * Parâmetros não cabem todos na segunda linha, então
 * coloca-se um parâmetro por linha:
 */

unsigned long int funcao_h(
    unsigned long int parametro1,
    unsigned long int parametro2,
    unsigned long int parametro3)
{
    return parametro1 + parametro2 - parametro3;
}

4. Comentários

  • Usar o mínimo de comentários possível; e
  • Comentários multilinha devem ter um caractere inicial de alinhamento.

Comentários não são analisados pelo compilador e não precisam fazer sentido para o código funcionar. Por isso não são atualizados em conjunto com o código. Consequentemente, com o passar do tempo a semântica (o significado) do código se desvia dos comentários presentes.

Na maioria dos casos é possível fazer um código autoexplicativo através da refatoração e da melhoria de nomes de funções e nomes de variáveis.

/* executa_loop1 */

void executa_loop1(void)
{
    // Lê o valor do sinal do sensor
    int16_t val = ADC0;

    // Calcula tensão do sensor em volts
    // a partir do offset e do ganho
    float valf = (val - 512) * (24.0 / 512.0);

    // Escreve na serial
    Serial_EscreveFloat(valf);
}

/* ---------------------------------------- */

/* executa_loop2 */

#define SENSOR_OFFSET 512
#define SENSOR_GANHO  (24.0 / 512.0)

float TensaoSensor_Volts(void);

void executa_loop2(void)
{
    Serial_EscreveFloat(TensaoSensor_Volts());
}

int16_t SinalSensor_ADC(void)
{
    return ADC0;
}

float TensaoSensor_Volts(void)
{
    return (SinalSensor_ADC() - SENSOR_OFFSET) * SENSOR_GANHO;
}
  • O primeiro comentário é removido com uma chamada da função SinalSensor_ADC().
  • O segundo comentário é removido com uma chamada da função TensaoSensor_Volts() e pelo uso das constantes SENSOR_OFFSET e SENSOR_GANHO.
  • O terceiro comentário é redundante e desnecessário.

Qual versão da função executa_loopX() você prefere ler?

Alguns comentários, por sua vez, são impossíveis de serem transformados em código. Por exemplo, o comentário a seguir mostra um diagrama de um LCD 16×2 que mostra a tensão elétrica e a corrente medida.

/*
 *   0123456789012345
 * 0 Tensão   XX,XX V 0
 * 1 Corrente Y,YYY A 1
 *   0123456789012345
 */

Sobre o caractere inicial dos comentários multilinha, alguns editores e formatadores eliminam espaços em branco iniciais, alinhando o primeiro caractere não branco de cada linha.

Dessa forma, todo o esforço de alinhamento abaixo é perdido em um piscar de olhos:

/*  0123456789012345
  0 Display 16x2     0
  1 Linha 2          1
    0123456789012345 */

O comentário acima acaba por se transformar em alguma dessas atrocidades:

/* 0123456789012345
   0 Display 16x2     0
   1 Linha 2          1
   0123456789012345 */

/* 0123456789012345
0 Display 16x2     0
1 Linha 2          1
0123456789012345 */

Colocando os asteriscos de alinhamento o problema deixa de existir.

/*
 *   0123456789012345
 * 0 Display 16x2     0
 * 1 Linha 2          1
 *   0123456789012345
 */

Considerações finais

Essas são as algumas das minhas preferências, ao menos as que eu acho mais importantes.

Todas essas já mudaram algumas vezes desde que comecei a programar e, com a experiência, são as que me parecem mais naturais e que facilitam a leitura do código.

Além disso, a maioria delas (exceto as sobre comentários), não me dão trabalho algum, pois utilizo um formatador automático. Assim posso focar no código em si e em como melhorá-lo para eliminar comentários.