Manual

do

Maker

.

com

Manipulação de arquivos: dados binários

Manipulação de arquivos: dados binários

Já pensou em transferir uma imagem via serial para uma MCU ou transferir uma imagem entre rádios? Essa transferência de dados binários poderia ser tanto um arquivo binário de qualquer tipo como uma imagem jpg, bmp etc. Existem casos muito específicos onde esse tipo de transferência é fundamental, mas para fazê-la de forma adequada é necessário ter um mínimo de entendimento sobre os bastidores da coisa.

Nesse artigo veremos os conceitos envolvidos e seguiremos com o estudo em uma próxima oportunidade. Mas não se preocupe, já dará para brincar um bocado com o que será disposto aqui.

Experimentação em Python

Para esse exemplo, estou utilizando o Python 3 e para escrever o código para execução em fluxo, utilizo o bpython3. Tudo o que faço é em Linux, se estiver utilizando Windows, use seu editor preferido. Sugiro o PyCharm.

Instale o bpython3, caso já não o tenha no sistema:

sudo apt-get install bpython3

Criação de um bytearray

O bytearray em Python é uma forma de criar um objeto mutável, onde seus dados podem ser modificados a qualquer momento. Para exemplificar um dado binário, o utilizaremos no editor bpython3. Abra o programa em um terminal, simplesmente digitando bpython3. Depois crie um array vazio ou com dados iniciais.

Um byte corresponde a 8 bits, cujo valor pode variar entre 0 e 255. Esse valor é representado em hexadecimal, usando 2 bytes literais para representar o valor, variando entre 00FF. O formato apresentado (tanto na saída do print quanto para a especificação de um valor) é \x00\xFF para os valores anteriormente citados.

Para criar um bytearray, podemos fazer algo como:

immutable_bytes = bytes(4)

Desse modo, definimos um array inicial de 4 bytes, que serão preenchidos com os valores \x00. Se não especificarmos o número de bytes, será criado um array vazio. Vejamos:

empty_bytes = bytes()
print(empty_bytes)

Resultando em um mero b''. Preenchido:

empty_bytes = bytes(4)
print(empty_bytes)

Resultando em `b'\x00\x00\x00\x00'

Esse artigo não é sobre Python, mas é bom deixar claro o uso dos recursos envolvidos no exemplo. Podemos mudar um valor do bytearray do mesmo modo que mudamos um caractere em uma string, simplesmente passando sua posição. Para mudar o valor da posição 1 (lembrando que a primeira posição é sempre 0), bastaria fazer:

bytearray-binary_transfer-example2-300x139.webp

Quando se cria o valor com a função bytes(), não é possível mudar os valores mais. No segundo exemplo utilizei a função bytearray(), que me permite fazer todo o tipo de manipulação nos valores assinalados. Portanto, para manipular os dados, utiliza-se a segunda função, enquanto para apenas atribuir, utiliza-se a primeira função. Nada impede que se use apenas bytearray().

Lendo e escrevendo dados binários

O Python oferece a biblioteca io, que contém recursos para uma leitura, escrita e conversão de forma direta. Como o intuito é interagir com MCUs e rádios, precisamos ter um pouco de material introdutório para compreender o que precisa ser feito. Utilizar a io.BytesIO() em Python facilitará a interação por parte do computador, mas ainda voltaremos a um nível mais baixo, ao interagir em C/C++.

io.BytesIO()

Em Python tudo vira objeto, é uma linguagem simples de aprender e, em minha humilde opinião, deveria ser a linguagem base para quem está iniciando.

Primeiramente, vamos criar um objeto para manipulação. Tudo o que escrevermos aqui pode ser colocado em um arquivo qualquer contendo a extensão .py e então executado com a precedência do interpretador na linha de comando. Se quiser ver resultados na hora, utilize o bpython3ou use o interpretador nativo, digitando python3 sem parâmetros. A vantagem de utilizar bpython é que ele tem auto-complete, o que dá acesso a recursos das bibliotecas e facilita a redigitação de variáveis.

Ao código:

import io

streaming = io.BytesIO()
streaming.write("Manual do Maker".encode('ascii'))
streaming.write("Manual do Maker".encode('utf-8'))

O método write retorna o número de bytes escritos. Em ambos os casos, deve retornar 14, considerando os espaços.

O cursor é a posição em que se encontra o próximo byte a ser escrito. Podemos mover o cursor para qualquer posição do buffer, por exemplo, posição 5:

streaming.seek(5)

Então podemos exibir o resultado assim:

result = streaming.read()

E o resultado:

bytesio.webp

Mudar os dados de um buffer existente

Como o conteúdo do objeto streaming é imutável, para modificar um dado do buffer devemos primeiramente copiá-lo. O método getbuffer() deverá ser utilizado para tal:

mutable_buf = streaming.getbuffer()
mutable_buf[0] = 0xAB #Ou um valor representado em decimal

Não se esqueça de fazer o reposicionamento do cursos após a modificação do buffer, senão retornará um valor vazio. Exemplo completo:

bytesio-streaming.webp

Como escrever e ler dados binários

A primeira coisa a se atentar é que, para escrever arquivos binários, o arquivo deve ser aberto em modo binário, senão teremos apenas um monte de sujeira. No exemplo anterior mostrei uma string codificada em UTF-8. Agora escreveremos apenas dados binários.

Abrir arquivo em modo binário para escrita

Em Python devemos fazer simplesmente:

with open("binfile.bin","wb") as binfile:
    num_bytes = binfile.write(b'\xAB\xCD\xEF')
    print("Wrote: %d bytes." % num_bytes)

Esse modo é convencional em Python porque dispensa a necessidade de abrir e fechar o arquivo. Como programo bastante em C/C++, prefiro o método tradicional. Seria algo como:

binfile = open("binfile.bin","wb")
binfile.write(b'\xFF')
binfile.close()

Nesse caso, em uma implementação real será necessário fazer tratamento de exceções; antes de abrir o arquivo, deve-se verificar se já não está aberto etc.

Abrir arquivo em modo binário para leitura

Às vezes podemos ter arquivos binários maiores do que a memória disponivel e carregá-lo todo de uma vez não seria possível nesse caso. Para essa questão, utilizamos a leitura linha a linha.

with open("binfile.bin","rb") as f:
    for line in f:
        #tratamento que quiser dar.
        print(line)

Para fazer uma transferência binária, também é melhor optar por ler e tirar o hash da linha, enviar e aguardar a resposta do hash para então enviar a próxima linha após validar a atual. Veremos mais detalhes sobre isso.

Pegando o tamanho do arquivo

Pode ser utilizado tanto para validar a forma que a leitura será feita (linha a linha ou total, conforme o tamanho) ou para checar o tamanho final do arquivo transferido via rádio ou serial.

Para isso, usamos um recurso da biblioteca os:

import os
length_in_bytes = os.path.getesize("binfile.bin")
print(length_in_bytes)

Inteiros para bytes

Às vezes precisamos escrever bytes para comunicação com determinados dispositivos. Peguei uma série de possibilidades em um exemplo da ddungeon:

i = 16

# Create one byte from the integer 16
single_byte = i.to_bytes(1, byteorder='big', signed=True) 
print(single_byte)

# Create four bytes from the integer
four_bytes = i.to_bytes(4, byteorder='big', signed=True)
print(four_bytes)

# Compare the difference to little endian
print(i.to_bytes(4, byteorder='little', signed=True))

# Create bytes from a list of integers with values from 0-255
bytes_from_list = bytes([255, 254, 253, 252])
print(bytes_from_list)

# Create a byte from a base 2 integer
one_byte = int('11110000', 2)
print(one_byte)

# Print out binary string (e.g. 0b010010)
print(bin(22))

Bytes para inteiros

A operação reversa também é possível, claro:

some_bytes = b'\x00\xF0'
i = int.from_bytes(some_bytes, byteorder='big')
print(i)

# Create a signed int
i = int.from_bytes(b'\x00\x0F', byteorder='big', signed=True)
print(i)

# Use a list of integers 0-255 as a source of byte values
i = int.from_bytes([255, 0, 0, 0], byteorder='big')
print(i)

Tem mais uma montanha de coisas que é possível fazer, mas vou citar em outros artigos, conforme a necessidade. Vamos agora ver um pouco de manipulação de dados binários em C/C++.

Experimentação em C/C++ Arduino

Em C++ podemos utilizar fstream para leitura  e escrita de dados binários, mas não podemos utilizá-la em Arduino. Não tem. O Arduino utiliza a herança da classe Stream para isso, e por sua vez, não é chamada diretamente. Seu uso se dá através de bibliotecas como a SD, Wire e Serial. Quando chamamos read() ou write(), estamos utilizando intrinsecamente a classe Stream.

Em C utilizaríamos algo como:

unsigned char buffer[10];
FILE *ptr;
ptr = fopen("test.bin","rb");
fread(buffer,sizeof(buffer),1,ptr);

Bem, o Arduino não tem sistema de arquivos, portanto não podemos contar com nenhum dos recursos anteriores. Já se estivéssemos usando ESP8266 ou ESP32, ambos seriam reconhecidos pelo respectivo compilador. Se o intuito for gravar em uma EEPROM, daí a biblioteca correspondente já oferece os recursos para a gravação do byte X em um endereço int Y.

Para um próximo artigo

No próximo artigo relacionado  devo mostrar mais algumas manipulações e acredito que você, leitor, gostará bastante, porque vamos fazer pelo menos 1 brincadeira interessante.

Até a próxima!

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.