Transmita imagens por código morse

Transmita imagens por código morse Deixe um comentário

Neste post veremos como é possível codificar, transmitir e receber imagens através de código morse

O código morse foi desenvolvido por Samuel Morse em 1835 e utilizado na representação de mensagens transmitidas pelo telégrafo elétrico, sendo o único modo de modulação desenvolvido para ser facilmente compreendido por humanos, sem ajuda de computadores. O código morse foi eventualmente substituído por outros formatos de codificação como o código ASCII, mais adequado à comunicação automatizada.

Apesar de ser desenvolvido para representação de letras, números e alguns poucos sinais especiais, é possível utilizar o código morse para representar e transmitir dados mais complexos, como por exemplo, uma imagem.

Ficou curioso para saber como tudo isso funciona? Veja abaixo!

Material necessário

Para transmitir imagens por código morse vamos precisar de:

OBS:  A Raspberry Pi deve estar configurada para rodar o sistema operacional Raspberry Pi OS. O post Primeiros passos com Raspberry Pi e Linux ensina como configurar e instalar o sistema.

Montagem do circuito

Eu utilizei a Raspberry Pi 3 B+, porém deve ser possível utilizar outras versões. O display que possuo é um pouco menor que o disponível na loja MakerHero, com 2.8 polegadas, mas utiliza a mesma interface e biblioteca.

A Raspberry Pi e o Arduino Uno utilizam níveis de tensão diferentes: 3.3v para a Raspberry e 5v para o Arduino. Certifique-se que a GPIO do arduino esteja configurada como entrada antes de conectar a Raspberry, caso contrário os 5v do arduino podem danificar as GPIOs da Raspberry. Se não quiser correr riscos, utilize um conversor de nível lógico.

O Shield LCD nos deixa com poucos pinos disponíveis, além de possuir um design que dificulta a conexão de terminais. Felizmente precisamos de apenas um: utilizei dois fios de cabo de internet e os enrolei entre os conectores do LCD para ter acesso aos terminais A5 e GND do Arduino Uno. Veja as imagens abaixo:

Requisitos do Projeto

O código morse não foi desenvolvido para codificar imagens e, por esse motivo, utilizá-lo para transmitir-las é ineficiente e lento. Portanto, vamos simplificar os requisitos para facilitar o desenvolvimento do projeto.

Os requisitos nada mais são que capacidades ou condições que devem ser atendidas pelo projeto. Neste caso, vamos considerar os seguintes requisitos:

  • O projeto deve transmitir imagens através de código morse;
  • Vamos considerar apenas imagens quadradas;
  • As imagens devem ter um tamanho máximo de 16 x 16 pixels, com isso não podemos enviar imagens de alta resolução (nem pretendemos), mas ainda podemos enviar algumas pixel arts;
  • As imagens enviadas tem uma profundidade de cor de 8 bpp (Explicarei isso mais a frente).

Código Morse

O código morse internacional é composto por seis elementos:

  • Sinal curto, chamado de ponto ou dit (.)
  • Sinal longo, chamado de traço ou dah (-)
  • Intervalo entre caracteres (entre dit’s e dah’s)
  • Intervalo curto (entre letras)
  • Intervalo médio (entre palavras)
  • Intervalo longo (entre frases)

A duração de cada sinal e intervalo é comumente definida em função de Unidades de Tempo:

  • Um dit  e um intervalo entre caracteres tem duração de uma unidade de tempo
  • Um dah e um intervalo curto tem duração de três unidades de tempo
  • Um intervalo médio tem duração de sete unidades de tempo

A combinação de dit’s e dah’s formam letras, número e símbolos. A imagem abaixo é representação em árvore das ‘letras’ codificadas em morse. Começando em start, para cada caractere recebido, move-se à esquerda no caso de um dit e à direita no caso de um dah.

Observe que cada símbolo tem um comprimento variável: o zero é o mais longo, composto por cinco dah’s e o E, o mais curto, composto por apenas um dit. O comprimento variável de cada símbolo é um dos fatores que dificultam a adaptação do código morse a comunicação automatizada, o que o fez ser substituído por formatos mais regulares.

Codificando as Imagens em Morse 

Como visto anteriormente o código morse codifica apenas letras, números e alguns símbolos, para transmitir imagens precisamos convertê-la para uma sequência de dados que possam ser representados em morse.

A quantidade de caracteres que podem ser codificados em morse varia de acordo com a versão utilizada. No entanto, em todas as versões são codificadas as 26 letras do alfabeto latino (não há distinção entre letras maiúsculas e minúsculas) e os algarismos indo-arábicos (0 a 9). Se nos limitarmos a esses dois conjuntos, há um total de 36 símbolos que podemos utilizar.

Imagens digitais nada mais são que sequências de bits que, em imagens bitmap, armazenam as cores de cada pixel. Se consideramos as imagens como uma string binária poderíamos utilizar os caracteres 0 e 1 para representar cada bit na imagem. No entanto, a transmissão de uma string binária é extremamente lenta, não só por uma imagem conter muitos bits, mas 0 é o símbolo mais longo para ser transmitido, composto por cinco dah’s, com um comprimento de vinte unidades de tempo.

Com apenas 36 símbolos não podemos realizar uma representação byte a símbolo (para isso precisaríamos de 256 símbolos), mas podemos fazer uma representação nibble a símbolo. Um nibble é um conjunto de quatro bits, cada byte contém dois nibbles e cada nibble pode ser correspondido por um único dígito hexadecimal. Se considerarmos uma imagem como uma string hex, podemos utilizar os números 0 a 9 e as letras A a F para codificar e transmitir as imagens.

Profundidade de cor

Um elemento importante em imagens digitais é a profundidade de cor, a qual refere-se à quantidade de bits utilizados para representar ou armazenar a cor de um único pixel em um bitmap.

Geralmente representamos imagens coloridas em uma profundidade de 24 bpp (bits por pixel), ou três bytes por pixel, onde cada byte representa um canal RGB. Em imagens em escala de cinza, no entanto, utiliza-se apenas 8 bpp, já que os três canais RGB tem sempre o mesmo valor. Quanto maior a profundidade de cor mais cores podem ser representadas, mas a imagem ocupa mais memória.

4bpp para 24bpp em imagens para código morse

Em uma representação colorida tradicional, em 24 bpp, uma imagem de 16 por 16 pixels ocupa 6144 bits de memória. Considerando que codificamos cada byte em duas letras em morse, isso resulta em 1536 letras, o que levaria muito tempo para ser transmitido (fora o fato que o display utiliza uma profundidade de apenas 16 bpp).

Ao invés de 24 vamos utilizar uma profundidade de 8 bpp, com isso reduzimos a quantidade a um terço do original. Obviamente há uma perda de dados nessa redução, uma vez que 24 bits podem representar mais de 16 milhões de cores e 8 bits apenas 256, porém devido à natureza das imagens que pretendemos enviar, isso não deve fazer grande diferença.

diferença de bits para a imagem do código morse

Note que em 8 bpp não podemos distribuir um número igual de bits para cada canal, um deles ficará com um bit a menos. Atribui-se menos bits ao canal B (Azul) pois o olho humano é menos sensível à cor azul do que ao verde e vermelho. Se ao invés de faltar, sobrar um bit, o bit extra deve ser adicionado ao canal G (Verde) pois o olho humano é mais sensível ao verde do que ao vermelho e azul.

Código

O código deste projeto é dividido em duas partes: o transmissor, implementado no Arduino Uno e o receptor, implementado na Raspberry Pi. Vamos ver primeiro o código do receptor no Arduino Uno.

Código do Receptor

O código a ser gravado no Arduino Uno está representado abaixo:

/*======================================================================/
    Morse Image
/======================================================================*/
// Macros de Configuração
#define ENABLE_SERIAL 1
#define USE_MASK_AS_ALPHA 1 
#define COLOR_ALPHA_MASK 0xEE
#define BACKGROUND_COLOR 0x0000 //0xFB74
#define DISPLAY_H_OFFSET 40
#define DISPLAY_V_OFFSET 0
//=======================================================================
// Macros do LCD, podem variar de acordo com o modelo utilizado
#define LCD_RESET A4
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
//=======================================================================
// Bibliotecas do LCD, podem variar de acordo com o modelo utilizado
#include <Adafruit_TFTLCD.h> 
#include <Adafruit_GFX.h>
#include <MCUFRIEND_kbv.h>
/*======================================================================/
	Constantes Gloabais
/======================================================================*/
// Arvore binaria de codigo morse
// Folha Esquerda (2n+1)
// Folha Direira  (2n+2)
const char MorseTree[] = {
	'\0', 'E', 'T', 'I', 'A', 'N', 'M', 'S',
	'U', 'R', 'W', 'D', 'K', 'G', 'O', 'H',
	'V', 'F', 'U', 'L', 'A', 'P', 'J', 'B',
	'X', 'C', 'Y', 'Z', 'Q', '\0','\0','5',
	'4', '\0','3', '\0','\0','\0','2', '\0',
	'\0','+', '\0','\0','\0','\0','1', '6',
	'=', '/', '\0','\0','\0','(', '\0','7',
	'\0','\0','\0','8', '\0','9', '0', '\0',
	'\0','\0','\0','\0','\0','\0','\0','\0',
	'\0','\0','\0','?', '_', '\0','\0','\0',
	'\0','"', '\0','\0','.', '\0','\0','\0',
	'\0','@', '\0','\0','\0','\0','\0','\0',
	'-', '\0','\0','\0','\0','\0','\0','\0',
	'\0',';', '!', '\0',')', '\0','\0','\0',
	'\0','\0',',', '\0','\0','\0','\0',':',
	'\0','\0','\0','\0','\0','\0','\0'
};
// Duração de um dit (.) em ms
const int TIME_UNIT = 50;
// Pino utilizado para receber código morse
const int MORSE_PIN = A5;
// Timeout (Intervalo entre palavras)
const int IDLE_TIMEOUT = 7 * TIME_UNIT;
// Tamanho máximo da imagem (imagens devem ser quadradas)
const int MAX_IMG_SIZE = 16;
// Tamanho do buffer de imagem
const int IMG_BUFFER_SIZE = MAX_IMG_SIZE * MAX_IMG_SIZE;
/*======================================================================/
	Variáveis Globais
/======================================================================*/
// Buffer utilizado para armazenar a imagem durante a transmissão
unsigned char img_buffer[IMG_BUFFER_SIZE];
// Index da próxima posição livre no buffe
unsigned char img_buffer_idx = 0;
// Index do symbolo morse na árvore binária
unsigned int morse_ptr = 0;
// Significancia do nibble recebido, alterna para cada nibble recebido
bool high_nibble = true;
// True enquanto uma transmissão estiver sendo realizada
bool transmitting = false;
// Estado lógico atual de MORSE_PIN
bool current_state = LOW;
// Momento do última transição lógica de morse pin (segundos desde ligar o arduino)
unsigned long ltime = 0;
// Tempo em que MORSE_PIN permaneceu em Idle (LOW)
unsigned long elapsed_idle_time = 0;
// Tempo em que MORSE_PIN permaneceu em Active (HIGH)
unsigned long elapsed_active_time = 0;
// Contador de pixels, indica quantos pixels foram recebidos na última transmissão
unsigned int px_cnt = 0;

// Gerenciador do display LCD, pode variar de acordo com o modelo utilizado
MCUFRIEND_kbv tft;
/*======================================================================/
	Funções Auxiliares
/======================================================================*/
/**
    Converte um caractere ascii 0-9 ou A-F para seu valor hexadecimal
*/
char to_hex(char sym) {
	if(('0' <= sym) && (sym <= '9')) {
		return sym - '0';
	}
	else if (('A' <= sym) && (sym <= 'F')) {
		return sym - 'A' + 10;
	}
	else if (('a' <= sym) && (sym <= 'f')) {
		return sym - 'a' + 10;
	}
	return 0;
}
/*======================================================================/
	Funções do Arduino
/======================================================================*/
/**
 * Setup
 */
void setup() {
    begin_TFT();	// Inicializa display LCD TFT
    #if defined(ENABLE_SERIAL) and (ENABLE_SERIAL != 0)
        Serial.begin(9600);     // Inicializa Serial
    #endif
}
//=====================================================================//
/**
 *	Loop
 */
void loop() {
	bool last_state = current_state;
	// Verifica estado lógico em MORSE_PIN
	current_state = digitalRead(MORSE_PIN);
	if(last_state != current_state) {
	// Se houve uma mudança de estado
		if((!transmitting) && (current_state==HIGH)) {
		// Se uma transmissão morse ainda não foi iniciada
			// Inicia transmissão morse
			start_transmission();
		}
        // Função de callback para mudança de estado
		signal_transition_to(current_state);
	}
	else if (transmitting && (current_state==LOW)){
	// Se uma transmissão morse foi iniciada e MORSE_PIN está em idle
		unsigned long etime = millis() - ltime;
		if(etime > IDLE_TIMEOUT) {
		// Se MORSE_PIN esta em idle por tempo maior que TIMEOUT
			// Pseudo transição para lidar com último caractere
			signal_transition_to(HIGH);
			// Finaliza transmissão
			end_transmission();
		}
	}
}
/*======================================================================/
	Funções do receptor morse
/======================================================================*/
/**
    Inicia uma transmissão morse, seta variáveis utilizadas para valores
	apropriados
*/
void start_transmission() {
    if(!transmitting) {
    	elapsed_idle_time = elapsed_active_time = 0;
		img_buffer_idx = px_cnt = 0;
    	ltime = millis();
    	transmitting = true;
    	high_nibble = true;
        #if defined(ENABLE_SERIAL) and (ENABLE_SERIAL != 0)
            Serial.println("--- Start ---\n");
        #endif
    }
}
//=======================================================================
/**
    Finaliza uma transmissão morse e desenha dados do buffer no display
*/
void end_transmission() {
    clear_TFT();
    unsigned int sz = min(ceil(sqrt(px_cnt)), MAX_IMG_SIZE);
    unsigned int ps = 240 / sz;
	for(int y = 0; y < sz; ++y) {
        for(int x = 0; x < sz; ++x) {
            draw_to_display(img_buffer[sz * y + x], x, y, ps);
            px_cnt -= 1;
            if(px_cnt == 0) {
                break;
            }
        }
        if(px_cnt == 0) {
            break;
        }
    }
    #if defined(ENABLE_SERIAL) and (ENABLE_SERIAL != 0)
        Serial.println("--- End ---");
    #endif
    transmitting = false;
}
//=====================================================================//
/**
    Função de callback para transições no estado lógico de MORSE_PIN
*/
void signal_transition_to(bool state) {
    // Calcula tempo decorrido da ultima transição
	unsigned long etime = millis() - ltime;
	if (state == HIGH) {
	// Se houve transição LOW para HIGH, intervalo
		elapsed_idle_time += etime;
        if(elapsed_idle_time > (3 * TIME_UNIT / 2)){
		// Intervalo Curto (Entre caracteres)
			// converte morse para ascii
            char letter = MorseTree[morse_ptr];
            #if defined(ENABLE_SERIAL) and (ENABLE_SERIAL != 0)
                Serial.print(" ");
                Serial.println(letter);
            #endif
			// Reinicia ponteiro morse
            morse_ptr = 0;
            if(high_nibble) {
			// Se for o caractere representa o nibble mais significativo
                img_buffer[img_buffer_idx] = ((to_hex(letter) << 4) & 0xF0);
            }
            else {
			// Se for o caractere representa o nibble menos significativo
                img_buffer[img_buffer_idx] |= (to_hex(letter) & 0x0F);
                #if defined(ENABLE_SERIAL) and (ENABLE_SERIAL != 0)
                    Serial.println("");
                #endif
                img_buffer_idx = (img_buffer_idx + 1) % IMG_BUFFER_SIZE;
				// Incrementa contador de pixels
                px_cnt = min(px_cnt + 1, IMG_BUFFER_SIZE);
            }
            // Alterna significancia do proximo nibble
            high_nibble = !high_nibble;
        }
		elapsed_active_time = 0;
	}
	else {
	// Se houve transição HIGH para LOW, sinal
        elapsed_active_time += etime;
		if((TIME_UNIT / 2) < elapsed_active_time
		&& elapsed_active_time < (3 * TIME_UNIT / 2)) {
		// Sinal curto, Dit (.)
			morse_ptr = (2 * morse_ptr) + 1;
            #if defined(ENABLE_SERIAL) and (ENABLE_SERIAL != 0)
                Serial.print('.');
            #endif
		}
		else if((5 * TIME_UNIT / 2) < elapsed_active_time
		&& elapsed_active_time < (7 * TIME_UNIT / 2)) {
		// Sinal longo, Dah (-)
			morse_ptr = (2 * morse_ptr) + 2;
            #if defined(ENABLE_SERIAL) and (ENABLE_SERIAL != 0)
                Serial.print('-');
            #endif
		}
		elapsed_idle_time = 0;
	}
	ltime = millis();
}
//=====================================================================//
/**
    Converte uma representação de cor de 8 bits para 16 bits
    8bpp:
        RRRGGGBB
            3 bits para Red
            3 bits para Green
            2 bits para Blue
    16bpp:
        RRRRRGGGGGGBBBBB
            5 bits para Red
            6 bits para Green
            5 bits para Blue
*/
uint16_t color8to16bpp(uint8_t color) {
	uint16_t color_16 = 0;
	// Red
    color_16 |= (map((color & 0xE0) >> 5, 0, 7, 0, 31) << 11) & 0xF800;
	 // Green
    color_16 |= (map((color & 0x1C) >> 2, 0, 7, 0, 63) <<  5) & 0x07E0;
	// Blue
    color_16 |=  map((color & 0x03), 0, 3, 0, 31) & 0x001F;
	return color_16;
}
/*======================================================================/
	Funções do LCD TFT

    Se necessário, reescreva estas funções para corresponder ao modelo
    de LCD utilizado
/======================================================================*/
/**
    Desenha um pixel no display
        color - cor em 8bpp
        x_pos - posição horizontal no display
        y_pos - posição vertical no display
        pixel_size - tamanho da representação de um pixel
            1 - quadrado de 1 pixel
            2 - quadrado de 4 pixel
            3 - quadrado de 9 pixels
*/
void draw_to_display(uint8_t color, int x_pos, int y_pos,
		unsigned int pixel_size) {
	#if defined(USE_MASK_AS_ALPHA) && (USE_MASK_AS_ALPHA != 0)
		if (color == COLOR_ALPHA_MASK) {
			return;
		}
	#endif
   // == Reescreva o código abaixo desta linha ==
	tft.fillRect(
		x_pos * pixel_size + DISPLAY_H_OFFSET,
		y_pos * pixel_size + DISPLAY_V_OFFSET,
		pixel_size,
		pixel_size,
		color8to16bpp(color)
	);
    // == Reescreva o código acima desta linha ==
}
//=====================================================================//
/**
    Inicialida display LCD TFT
*/
void begin_TFT(void) {
    // == Reescreva o código abaixo desta linha ==
	static uint16_t g_identifier;
	pinMode(MORSE_PIN, INPUT);      // Inicializa Pino do código morse
    g_identifier = tft.readID();    // Identifica id do TFT
    
    if (g_identifier == 0x00D3 || g_identifier == 0xD3D3)
        g_identifier = 0x9481; 		// write-only shield
    else if (g_identifier == 0xFFFF)
        g_identifier = 0x9341;
  
	tft.begin(g_identifier);        // Inicializa TFT
	tft.setRotation(1);             // Modo paisagem
	tft.fillScreen(BACKGROUND_COLOR);         // Pinta background
    // == Reescreva o código acima desta linha ==
}
//=====================================================================//
/**
    Limpa tela do display, pintando-o com BACKGROUND_COLOR
*/
void clear_TFT(void) {
    // == Reescreva o código abaixo desta linha ==
    tft.fillScreen(BACKGROUND_COLOR);         // Pinta background
    // == Reescreva o código acima desta linha ==
}
//=====================================================================//

A comunicação com o shield LCD TFT é feita através da biblioteca MCUFriend_kbv.h que pode ser instalada através do gerenciador de bibliotecas da Arduino IDE. A biblioteca depende das bibliotecas Adafruit_TFTLCD.h e Adafruit_GFX.h, as mesmas devem ser instaladas automaticamente junto à MCUFriend_kbv.h. Caso utilize um modelo de LCD diferente, instale e importe as bibliotecas correspondentes.

biblioteca para o display do código morse

Na função setup inicializa-mos o display LCD e a porta Serial. Note que a inicialização do display é feita através da função begin_TFT que chama as funções da biblioteca MCUFriend_kvb, deste modo, caso deseje utilizar um display incompatível com a biblioteca, basta reescrever as funções begin_TFT, clear_TFT e draw_to_display, o restante do código deve funcionar corretamente.

Na função loop monitoramos o pino utilizado para recepção morse (A5) e detectamos toda vez que ocorre uma transição no nível lógico (Alto para Baixo, ou Baixo para Alto). Quando uma transição é detectada verificamos se há uma transmissão iniciada, caso não iniciamos uma nova transmissão, setando os as variáveis utilizadas na decodificação para os valores iniciais e passamos a decodificar os símbolos recebidos.

Quando uma transição Alto para Baixo ocorre, o software calcula quanto tempo o sinal permaneceu em Alto e identifica a duração como um sinal curto (dit) ou sinal longo (dah). Utilizamos uma árvore binária para identificar o símbolo transmitido, a variável morse_ptr armazena a posição da árvore que representa o símbolo atual, seu valor é atualizado para cada dit ou dah recebido, a partir do topo (raíz) nos movemos para a folha esquerda se recebermos um dit, ou para folha direita se recebermos um dah.

Quando uma transição Baixo para Alto ocorre, o software calcula quanto tempo o sinal permaneceu em Baixo e identifica a duração como um intervalo. Se for detectado um intervalo maior ou igual um intervalo curto, significa que um símbolo inteiro já foi transmitido. Verificamos para qual símbolo morse_ptr está apontando, o convertemos para seu valor hexadecimal e armazenamos o valor no buffer para ser posteriormente pintado no LCD. Como cada byte é representado por dois algarismos hexadecimais, usamos a variável high_nibble para indicar qual o nibble recebido (mais significativo ou menos significativo), a variável é invertida a cada nibble recebido.

Note que a conversão e armazenamento em buffer dos valores recebidos ocorre apenas quando uma transição Baixo para Alto é detectada. Isso causa um problema ao receber o último caractere, pois o sinal alterna de Alto para Baixo e permanece assim até a próxima transmissão. Por isso, na função loop, quando uma transmissão já tiver sido iniciada, monitoramos quanto tempo o sinal permanece em Baixo, caso ultrapasse um timeout (duração de um intervalo médio), lidamos com o último símbolo transmitido e finalizamos a transmissão (Consideramos que cada imagem é uma única ‘palavra’, portanto não precisamos detectar intervalos longos). 

Código do Transmissor

O código do transmissor foi escrito em Python 3. O interpretador Python e a maioria das bibliotecas utilizadas já estão inclusas na instalação do Raspberry Pi OS, no entanto duas bibliotecas adicionais são necessárias.

Vamos usar a biblioteca OpenCV, uma biblioteca de código livre para visão computacional, para ler e manipular as imagens. Escolhi esta biblioteca pois, além de já ter uma experiência com a mesma, possui diversas funções otimizadas para processamento de imagens, vamos utilizar algumas delas.

A versão Python do OpenCV requer a biblioteca Numpy, que suporta matrizes e arrays multidimensionais. Ambas devem ser instaladas na Raspberry. Para isso abra um terminal e digite o seguinte código:

sudo apt-get install python3-numpy python3-opencv

O script Python, que deve ser salvo como um arquivo executável na Raspberry Pi está descrito abaixo:

#!/usr/bin/env python3
# coding: utf-8

# ## Instalar bibliotecas
# Requer bibliotecas numpy e opencv

#! sudo apt-get install python3-numpy python3-opencv


# Importamos as bibliotecas necessárias. Além do numpy e opencv instaladas anteriormente, vamos utilizar:
# - gpiozero : Acessar as GPIOs do RaspberryPi
# - argparse : Checar e obter os argumentos
# - time : Calcular tempo e causar delay
# - math : Funções matemáticas


import numpy as np
import cv2
import gpiozero
import argparse
import time
import math


# ## Constantes

# ### TO MORSE
# Dicionário utilizado para mapear valores hex para sua representação Morse.


__TO_MORSE = {
    '0': "-----",
    '1': ".----",
    '2': "..---",
    '3': "...--",
    '4': "....-",
    '5': ".....",
    '6': "-....",
    '7': "--...",
    '8': "---..",
    '9': "----.",
    'A': ".-",
    'B': "-...",
    'C': "-.-.",
    'D': "-..",
    'E': ".",
    'F': "..-.",
    'a': ".-",
    'b': "-...",
    'c': "-.-.",
    'd': "-..",
    'e': ".",
    'f': "..-.",
}


# ### Default Time Unit
# 
# Valor padrão para duração de 1 dit, utilizada como base de tempo para a transmissão Morse


DEFAULT_TIME_UNIT = 50e-3


# ### Default GPIO
# GPIO padrão para transmissão morse


DEFAULT_GPIO = 21


# ## Funções

# ### Funções para manipulação de Imagens


def img_clamp(img, max_size = 16):
    """
        Assegura que o maior lado da imagem seja menor ou igual à max_size, mantendo a proporção
    """
    if (img.shape[0] > max_size) or (img.shape[1] > max_size):
        img_max_size = max(img.shape[0], img.shape[1])
        factor = max_size / img_max_size
        img = cv2.resize(img, None, fx=factor, fy=factor, interpolation=cv2.INTER_AREA)
    return img



def img_to_square(img):
    """
        Faz com que a imagem seja um quadrado, adicionando margens se necessário
    """
    if img.shape[0] != img.shape[1]:
        s = max(img.shape[0:2])
        f = np.zeros((s, s, img.shape[2]), np.float32)
        f[:,:] = np.asarray([2/3, 3/7, 1])
        ax, ay = (s - img.shape[1])//2, (s - img.shape[0])//2
        f[ay:img.shape[0]+ay, ax:ax+img.shape[1]] = img
        return f
    return img



def img_to_hexstring(img):
    """
        Converte uma imagem para uma string hexadecimal onde cada 2 caracteres representam um pixel (8bpp)
            img - Imagem a ser convertida (3 floats BGR)
    """
    hex_str = ""
    for y in range(img.shape[0]):
        for x in range(img.shape[1]):
            B,G,R = img[y, x]
            px_val  = int(R * 7) << 5
            px_val |= int(G * 7) << 2
            px_val |= int(B * 3)
            hex_str += f"{px_val:02x}"
    return hex_str


# ### Funções para conversão e transmissão morse


def to_morse(char):
    """
        Converte um caractere (hexadecimal) para sua representação morse
    """
    if char in __TO_MORSE:
        return __TO_MORSE[char]
    return ""



def send_morse(message, morse_out, time_unit, report=True):
    it = 0
    mem_size = len(message)
    for char in message.upper():
        if report:
            print(f'    {char} => ', end='')
        for morse_char in to_morse(char):
            if morse_char == '.':
                # Dit
                morse_out.on()
                time.sleep(time_unit)
                morse_out.off()
                if report:
                    print('.', end='')
            elif morse_char == '-':
                # Dah
                morse_out.on()
                time.sleep(3 * time_unit)
                morse_out.off()
                if report:
                    print('-', end='')
            else:
                continue
            # Intervalo entre caracteres [1 Unidade de tempo]
            time.sleep(time_unit)
        # Intervalo Curto [3 Unidades de tempo, 1 Unidade já passou desde o último caractere]
        time.sleep(2 * time_unit)
        it += 1
        if report:
            print(f'\t[{it}/{mem_size}]({int(100.0 * it / mem_size)}%)')


# ### Main


def main(args):
    try:
        img = cv2.imread(args.img, cv2.IMREAD_COLOR)
        if img is None:
            print(f'Erro ao abrir imagem {args.img}.')
            exit()
        img = img.astype(np.float32) / 255.0
        img = img_clamp(img, 16)
        img = img_to_square(img)
        hex_str = img_to_hexstring(img)

        morse_out = gpiozero.LED(args.gpio)

        print("Iniciando transmissão...")
        start_time = time.time()
        send_morse(hex_str, morse_out, args.time_unit, args.report)
        end_time = time.time()
        print(f"Transmissão Finalizada em {math.floor(end_time - start_time)} segundos!")
    except KeyboardInterrupt:
        print("\nAbort!")
    exit()



if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("img", metavar="IMG", type=str, help="Image path")
    parser.add_argument("--gpio", "-g", metavar="GPIO", type=int, help=f"GPIO Number (default {DEFAULT_GPIO})", default=DEFAULT_GPIO)
    parser.add_argument("--time-unit", "-t", metavar="TIME-UNIT", type=float, help=f"Time Unit in seconds (default {DEFAULT_TIME_UNIT})", default=DEFAULT_TIME_UNIT)
    parser.add_argument("--report", "-r", metavar="REPORT", type=bool, help="Enable Reports (default True)", default=True)
    args = parser.parse_args()
    main(args)

Após salvar o script no Raspberry não se esqueça de dar-lhe permissão de execução, para isso utilize o seguinte comando (substitua toMorse pelo nome do arquivo):

chmod u+x toMorse

No script acima, após carregar uma imagem na memória, realizamos alguns pré processamentos para facilitar a transmissão:

  • Convertemos a imagem de 24 bits por pixel para 3 floats por pixel, onde cada float representa um canal, sendo 0 o valor mínimo e 1 o máximo. Essa conversão é muito comum em softwares de processamento de imagem e evita problemas de overflow e precisão limitada ao manipular imagens;
  • Garantimos que a maior dimensão da imagem é 16 pixels, reduzindo a imagem, se necessário, mas mantendo a proporção. Não é recomendado utilizar imagens muito maiores que 16 x 16, pois perdemos muita informação durante a redução;
  • Garantimos que a imagem seja um quadrado, adicionando margens se necessário. A cor da margem foi escolhida de tal forma que é codificada para 0xEE em 8 bpp (lembre-se que a letra E possui a transmissão mais rápida em morse);
  • Por fim convertemos a imagem para um string hexadecimal onde cada pixel da imagem é codificado para dois dígitos hexadecimais (em 8 bpp).

Cada caractere na string hex é codificado em morse e então transmitido através da GPIO, onde um sinal baixo (0v) representa um intervalo e um sinal alto (3.3V) representa um sinal.

Ao utilizar o script devemos fornecer-lhe alguns argumentos:

IMG Caminho da imagem (obrigatório)
-g GPIO Número da GPIO a ser utilizada (opcional, padrão 21)
-t TIME-UNIT Unidade de tempo em segundos (opcional, padrão 0.05)
-r REPORT Exibe relatório da transmissão (opcional, padrão True)

Também é possível utilizar o argumento -h ou –help para exibir a lista de argumentos suportados:

Funcionamento

O vídeo abaixo mostra o protótipo em funcionamento. Nele é possível ver o terminal da Raspberry, o monitor serial do Arduino além do display LCD e um pequeno LED que mostra os sinais recebidos.

YouTube video

No vídeo acima foi utilizado uma versão antiga do script Python. Note que os argumentos passados são um pouco diferentes. As duas versões funcionam da mesma forma, porém a nova versão tem um código mais legível e algumas funções foram simplificadas.

Conclusão

Apesar de ser possível transmitir dados complexos, como imagens, através do código morse, a transmissão está longe de ser eficiente e confiável. As taxas de transmissão são muito baixas, levando cerca de 3 minutos para transmitir uma imagem 16 x 16, o que pode ser feito em milissegundos em outras formas de transmissão.

Além disso, como não foi implementado nenhum feedback, não é possível ter certeza que os dados chegaram intactos ao receptor. Durante os testes, duas vezes ocorreram erros de transmissão que fizeram o receptor exibir uma imagem com pixels de cores diferentes do esperado. Isso é ainda mais grave considerando que o receptor e transmissor estavam menos de 10 cm um do outro, quanto maior a distância mais provável se torna a ocorrência de interferências e erros de transmissão.

Gostou do post Transmita imagens por código morse? Deixe seu comentário logo abaixo. 

Faça seu comentário

Acesse sua conta e participe