########### Visão geral ########### Este documento procura oferecer uma visão geral sobre o funcionamento do código deste projeto, útil para aqueles que desejam modificá-lo ou simplesmente endendê-lo. Estrutura de arquivos ********************* Segue uma breve descrição dos arquivos/diretórios presentes na pasta raiz do projeto: .. code-block:: shell arduino-meteorolog/ ├── data/ # contém dados gerados pelo logger ├── docs/ # arquivos de documentação ├── logger/ # software que faz coleta de dados e envio para o servidor ├── meteorolog/ # projeto ".ino" do firmware (compilável pela Arduino Toolchain) ├── scripts/ # scripts utilizados pelo Makefile ├── settings.ini # configurações do logger └── Makefile # proporciona diversos comandos para fácil utilização O arquivo ``Makefile`` contém diversos comandos [1]_ curtos para facilitar a realização de diversas operações de instalação e manutenção de todos os softwares do projeto. Assim, basta estar presente nesta pasta raiz e executar: .. code-block:: shell $ make para realizar alguma tarefa. Experimente ``make help`` para listar todos os comandos possíveis. Funcionamento ************* Essa estrutura existe para comportar os dois seguintes softwares: * **Firmware**: Executado no processador do Arduino, é responsável por ler os sensores conectados de acordo com solicitações enviadas à porta serial. * **Logger**: Executado em uma máquina Linux (PC, Raspberry, etc), é responsável por coletar dados da placa através de uma leitura serial, fazer armazenamento local e também remoto (envio de dados para [dados.cta.if.ufrgs.br/emm](dados.cta.if.ufrgs.br/emm)). Ambos encontram-se no mesmo repositório pois o logger está preparado para enviar comandos pela serial cujo formato o firmware está preparado para receber. Por exemplo, considere a seguinte string enviada pelo logger à porta serial onde está a placa Arduino:: readSensors,LDR,DHT22_TEMP Ao receber esses caracteres, o firmware determinará que trata-se de um comando para leitura de sensores e que os sensores a serem lidos são, nessa ordem: o ``LDR`` e o ``DHT22_TEMP`` (luminosidade e temperatura, respectivamente). O firmware retorna pela serial uma resposta com números separados por vírgula, algo como:: 84.1,24.5 indicando 84,1 % de luminosidade e 24,5 ºC de temperatura. De acordo com o comando enviado, o logger estará preparado para receber uma resposta com dois valores **CSV** (*comma-separated-values*), que serão guardados juntamente com a hora do sistema em um arquivo de log local. Adicionamente, o logger também fará uma tentativa de envio ao servidor remoto. Caso falhe, será guardado em um arquivo ``outgoing.json`` para tentativas futuras. ######## Firmware ######## Escrito na linguagem C++ suportada pela Arduino Toolchain, pode ser compilado utilizando a IDE do Arduino ou pelo terminal através de ``$ make firmware``. Estrutura de arquivos ********************* .. code-block:: shell meteorolog/ ├── libs/ # bibliotecas de terceiros │   ├── Adafruit_BMP085.cpp │   ├── Adafruit_BMP085.h # sensor BMP085 (licença BSD) │   ├── DHT.cpp │   ├── DHT.h # sensor DHT11 e 22 (licença MIT) │   ├── RTClib.cpp │   └── RTClib.h # relógio RTC DS1307 (domínio público) ├── meteorolog.ino # setup() e loop() da Arduino Toolchain ├── mysensors.cpp ├── mysensors.h # leitura dos sensores disponíveis ├── boardcommands.cpp ├── boardcommands.h # execução de comandos para a placa ├── utils.cpp └── utils.h # utilidades suplentes da Arduino Toolchain .. note:: Os arquivos ``.h`` (cabeçalhos) contém os protótipos juntamente com a documentação do código implementado nos ``.cpp``. O ponto de entrada é o arquivo ``meteorolog.ino``, pois ele define as duas seguintes funções padrões de um Sketch Arduino: * ``setup()`` Executada uma vez quando a placa é ligada, inicializa a comunicação serial e chama ``mysensors_setup()``, que fará inicialização dos sensores. * ``loop()`` Executada enquanto a placa estiver ligada, verifica constantemente se há caracteres disponíveis na porta serial. Caso afirmativo, lê a string ali presente e encaminha ela para ``execute_board_command()``. Essa função, por sua vez, interpreta o comando presente na string recebida e retorna uma string como resposta, que é então devolvida para a porta serial e o loop recomeça. boardcommands.h *************** Os comandos esperados pelo firmware constituem strings no seguinte formato CSV:: nomeDoComando,arg1,arg2,...,argN Essa string contendo o comando e seus argumentos é enviada para ``execute_board_command()``, que interpretará a parte inicial ``nomeDoComando`` para delegar uma ação apropriada. O retorno de ``execute_board_command()`` é uma string contendo a resposta do comando ou, em caso de erros (comando inexistente, argumentos insuficientes, etc):: readSensors =========== A leitura dos sensores é feita pelo seguinte comando da placa:: readSensors,nome1,nome2,...,nomeN onde os argumentos ``nome1,nome2,...,nomeN`` são transmitidos para ``read_sensors()``, que fará as solicitações de leitura. Essa função itera sobre cada nome/apelido, passando o mesmo para ``call_read_sensor()`` de modo que a função correta de leitura seja invocada. Por exemplo, sejam os dois seguintes sensores passados como argumento:: LDR,p O primeiro deve levar à execução da função ``read_LDR()`` e o segundo, à execução de ``read_BMP085_PRESSURE()`` (pois ``"p"`` é um apelido para ``BMP085_PRESSURE``). Ambas funções não recebem nenhum argumento e retornam uma string contendo, presumivelmente, o número medido ou um indicador de erro conforme programado em `my_sensors.h `_. A operação de ``call_read_sensor()`` depende então de mapear-se uma string como ``"LDR"`` para um ponteiro da função ``read_LDR()``. Isso é alcançado em `boardcommands.cpp `_ através dos três seguintes vetores globais: * ``_sensor_names[]``: Contém o nome de todos os sensores disponíveis. * ``_sensor_nicknames[]``: Contém todos os respectivos apelidos. * ``_fp_array_read_sensor[]``: Contém os ponteiros de função das ``read_X()``, onde ``X`` é o nome de um sensor -- por exemplo, ``read_LDR()``. Percorrendo-se os dois primeiros, ``call_read_sensor()`` busca por um nome/apelido válido. Caso encontre, o índice é utilizado para acessar ``_fp_array_read_sensor[]``, obter o ponteiro da função e finalmente executá-la. Os vetores são incializados com as respectivas constantes declaradas em `my_sensors.h `_. setRTC ====== A configuração do relógio (se presente) na placa é feita com o comando:: setRTC,ano,mes,dia,hora,minuto,segundo onde os argumentos ``ano,mes,dia,hora,minuto,segundo`` são repassados para ``set_time_from_csv()`` (`my_sensors.h `_), cujo funcionamento depende do RTC em questão (ver seção sobre o ``RTC_DS1307`` e suas funções). Exemplo:: setRTC,2015,8,17,14,43,10 caso bem sucedido deverá retornar a string:: done: 2015-08-17 14:43:10 mysensors.h *********** Esse módulo contém: * Funções ``read_X()`` onde ``X`` é o nome de um sensor disponível; * Função ``mysensors_setup()`` para inicialização programada de todos os sensores ao ligar a placa; * Constantes a serem usadas por `boardcommands.cpp `_ nos vetores de lookup das funções ``read_X()`` : * ``__SENSOR_COUNT``: total de sensores; * ``__SENSOR_NAMES``: vetor de strings de nomes de todos os sensores; * ``__SENSOR_NICKNAMES``: vetor de strings de apelidos de todos os sensores; * ``__FP_ARRAY_READ_SENSOR``: vetor de ponteiros de função das ``read_X()``. .. note:: Entende-se aqui **sensor** por um elemento de software capaz de proporcionar um valor medido. Ou seja, ainda que um único componente eletrônico possa oferecer diversas medições (como temperatura e umidade do ar pelo DHT22), em termos do software cada medição é devida a um sensor, conforme cadastrado em http://dados.cta.if.ufrgs.br/emm. De um modo geral as funções ``read_X()`` são bem simples, pois apenas invocam funções externas para obteção da medição numérica que é então convertida para string -- o tipo de retorno esperado. Por exemplo, sensores que atuam diretamente em pinos analógicos podem fazer uso de ``analogRead()`` (biblioteca do Arduino) e alguma matemática para calibração diretamente nas ``read_X()`` (a exemplo de ``read_LDR()``). Já sensores que possuem controladoras Wire ou I2C em gearl acabam fazendo uso de bibliotecas separadas para melhor organização do código. .. note:: Bibliotecas de terceiros são mantidas no subdiretório ``libs/``. Inserindo novos sensores ======================== O software do repositório contém o código básico para os sensores suportados oficialmente, mas nada impede que novos sensores sejam adicionados. Para isso, siga os seguintes passos: **1. (opcional) Disponibilize uma biblioteca dentro de libs/** Caso o código de leitura seja complexo demais, considere criar uma nova biblioteca:: libs/novo_sensor.h libs/novo_sensor.cpp Dica: utilize orientação a objetos para melhor organização. **2. Registre o protótipo da nova função read_X() em mysensors.h** .. code-block:: cpp #include "libs/novo_sensor.h" String read_NOVO_NOME(); onde ``NOVO_NOME`` será o nome do novo sensor. **3. Registre novo nome e apelido** * Incremente ``__SENSOR_COUNT``; * Inclua ``NOVO_NOME`` no vetor ``__SENSOR_NAMES``; * Inclua um apelido curto qualquer no vetor ``__SENSOR_NICKNAMES``, na mesma posição utilizada por ``NOVO_NOME`` anteriormente; * Inclua o ponteiro de função ``&read_NOVO_NOME`` no vetor ``__FP_ARRAY_READ_SENSOR``, na mesma posição utilizada por ``NOVO_NOME`` anteriormente. **4. Implemente o código em mysensors.cpp** Exemplo : .. code-block:: cpp // === NOVO_NOME SETUP ======================================= #define NOVO_NOME_PIN 8 // digital String read_NOVO_NOME() { return FloatToString(...); } // =========================================================== ###### Logger ###### Software responsável por solicitar leitura dos sensores pela placa, guardar os dados localmente e também enviá-los ao servidor. Foi pensado para execução initerrupta em background através de um **daemon** registrado no gerenciador `supervisor `_. Seguem abaixo algumas das filosofias do software: * Configuração amigável ao usuário leigo; * Sistema completo de logging (dados e execução); * Sincronismo de dados locais e remotos; Estrutura de arquivos ********************* Este software está escrito na linguagem Python 3 e apresenta a seguinte estrutura de arquivos: .. code-block:: shell logger/ ├── app/ # Python package contendo a aplicação │   ├── __init__.py # torna essa pasta um package e faz inicializações │   ├── config.py # gerencia configurações do programa │   └── main.py # implementa a classe Meterologger ├── logs/ # guarda logs de execução ├── deploy.py # script para registrar o daemon do logger ├── init_serial.py # script para obter uma conexão serial ├── requirements.pip # bibliotecas Python de terceiros └── run.py # ponto de entrada para excução do logger .. note:: Normalmente em projetos Python, o arquivo de configuração fica presente no nível superior da pasta package ao lado de ``run.py``. No caso deste projeto, optamos por mantê-lo na raíz do repositório, na posição de destaque ao lado do ``Makefile``. Projetos Python multi-arquivos fazem uso do conceito de **package**: pasta que contém um arquivo `__init__.py `_ tornando-se acessível exteriormente como um módulo. Assim, tanto um interpretador em `logger/ `_ como o arquivo `run.py `_ fora do package podem fazer:: from app.main import Meteorologger Módulos internos do package podem acessar uns aos outros por importação relativa, como acontece em `app/main.py `_:: from .config import Config onde o operador ``.`` refere-se ao nível atual (`main.py `_ e `config.py `_ estão na mesma pasta), ``..`` indica nível superior e assim por diante. Em resumo, o código do aplicativo logger está todo na pasta ``app/``, onde ``Meteorologger`` é a classe principal, e sua execução se dá pelo arquivo ``run.py`` com o seguinte ponto de entrada:: Meteorologger().run() .. note:: A instanciação ``Meteorologger()`` é responsável principalmente pelo carregamento do arquivo de configurações e sua validação. Já o método ``run()`` contém o loop infinito que consiste na execução do logger. Dependências ************ Além da linguagem Python 3 o logger depende das seguintes bibliotecas de terceiros: * ``pyserial`` Possibilita comunicação entre Python e portas seriais. Aqui é utilizada para enviar comandos à placa Arduino e ler as respostas obtidas. * ``requests`` Alternativa à biblioteca padrão ``urllib``, usada para comunicação HTTP. Aqui é utilizada para enviar requests para a API do servidor em http://dados.cta.if.ufrgs.br/emm. .. note:: As bibliotecas e suas versões estão listadas no arquivo `requirements.pip `_ para instalação automatizada através do gerenciador de pacotes **pip3** (vem por padrão com Python 3.4+). Afim de evitar comprometer a instalação global do Python do usuário, optamos aqui pelo uso da ferramenta `virtualenv `_ para a criação de um ambiente virtual contendo uma cópia isolada do interpretador Python. Todo o processo é automatizado pelo ``Makefile`` principal do projeto através do comando: .. code-block:: shell $ make setup O resultado é a criação de uma pasta ``.venv`` contendo uma instalação isolada de Python 3 e as bibliotecas mencionadas acima. A execução correta desse comando depende dos seguintes programas no sistema: * **python3** : interpretador da linguagem Python 3.x (recomenda-se versão 3.4); * Pacote Debian: ``python3`` * **pip3** : gerenciador de pacotes do Python 3; * Pacote Debian: ``python3-pip`` * **virtualenv** : criação de ambientes virtuais de Python; * Instale via **pip3**: ``$ sudo pip3 install virtualenv`` Adicionamente, para que o logger possa ser executado em background (ver seção `deploy.py`) esse projeto também requer a seguinte ferramenta: * **supervisor** : gerenciador de daemons (processos background); * Pacote Debian: ``supervisor`` .. note:: Algumas distribuições podem possuir as versões 3.x do interpretador Python registradas em comandos diferentes de ``python3``, como por exemplo, ``python-3.x``, o mesmo valendo para o **pip** (ex: ``pip-3.x``). Nesse caso, você precisa fornecer o nome correto através da variável ``PYBIN``:: $ make setup PYBIN=python-3.x .. note:: Os pacotes Debian podem ser instalados com ``$ sudo apt-get install ``. Usuários de outras distribuições deverão procurar os equivalentes para o seu gerenciador de pacotes. run.py ****** Este arquivo consiste no ponto de entrada da aplicação, permitindo a execução do logger por um interpretador Python: ``$ python3 run.py [options]``. Entretanto, conforme descrito na seção anterior sobre dependências, deve ser utilizado o interpretador do ambiente virtual. Isso é alcançado pelo seguinte comando do ``Makefile``: .. code-block:: shell $ make logger-run que deve ser executado após a criação do ambiente virtual com ``make setup``. Parâmetros ========== * ``--verbose`` Se presente, resulta em ``Meteorologger.verbose = True``, o que coloca o log de execução em nível debug. * ``--sync-rtc`` Se presente, ao invés de executar o logger, simplesmente executa o método ``Meterologger().sync_rtc()`` para realizar sincronização do relógio da placa (``RTC_DS1307``) com o da máquina. Essa operação está disponibilizada no seguinte comando do ``Makefile``:: $ make sync-rtc deploy.py ********* Conforme mencionado na introdução, o logger foi pensado como um programa para ser executado em background. Por exemplo, as menssagens do log de execução são, por padrão, escritas em um arquivo dentro de ``logger/logs`` através da biblioteca padrão ``logging`` de Python. O script `deploy.py `_ é responsável por registrar um novo processo **daemon** no `supervisor `_ atendendo pelo nome ``meteorologger``. A execução se dá pelo seguinte comando do ``Makefile`` (permissões de administrador serão solicitadas): .. code-block:: shell $ make deploy O registro de um *daemon* no supervisor consiste na criação de um arquivo de configuração em ``/etc/supervisor/conf.d/`` e a subsquente execução de ``supervisorctl update``. É exatamente isso que faz a função ``deploy_supervisor()`` do script `deploy.py `_. O arquivo de configuração utiliza o seguinte template (string ``TEMPLATE_SUPERVISOR_CONF``):: [program:{PROCESS_NAME}] command={BASE_DIR}/.venv/bin/python {BASE_DIR}/logger/run.py directory={BASE_DIR} user=root autostart=true autorestart=true redirect_stderr=true stdout_logfile={BASE_DIR}/logger/logs/stdout.log logfile={BASE_DIR}/logger/logs/supervisor-{PROCESS_NAME}.log Os valores substituídos nesse template estão declarados nas constantes globais: * ``PROCESS_NAME``: apelido para o *daemon* dentro do supervisor. No caso, ``meteorologger``. * ``BASE_DIR``: diretório raiz do projeto, que contém o ``Makefile``. Obtido pelo cálculo relativo da posição do arquivo ``deploy.py``. Sobre as configurações, vale destacar: * ``redirect_stderr``: menssagens de erro serão escritas na saída padrão. * ``stdout_logfile``: além das menssagens da saída padrão, o traceback aparecerá nesse arquivo caso o programa falhe. Por fim, o mesmo script `deploy.py `_ é utilizado também para *undeployment*, isto é, remoção do *daemon* no supervisor. Isso é feito passando-se o argumento ``-u`` para o script, operação disponível pelo seguinte comando do ``Makefile``: .. code-block:: shell $ make undeploy app/config.py ************* O arquivo de configuração utilizado pelo logger chama-se `settings.ini `_ e encontra-se na pasta raiz do projeto, ao lado do `Makefile `_ por ser uma posição de destaque. Foi concebido para ser configurado por um usuário leigo. Existem várias opções de sintaxe para arquivos de configuração no universo Python: *XML*, *JSON*, *YAML*, *INI*, etc. Apesar de que sintaticamente o *YAML* seja mais interessante para projetos Python por levar em conta a identação, esse mesmo motivo dificultaria a configuração por usuários leigos em computação. Assim, a flexibilidade do formato *INI* tratado pela biblioteca padrão ``configparser`` determinou sua escolha para esse projeto. O módulo `app/config.py `_ é responsável pela leitura e validação do arquivo de configuração através da classe ``Config``, que deverá se comportar como um dicionário para obteção das seções e chaves. Como todos valores lidos e armazenados pelo objeto ``configparser.ConfigParser`` são strings, optamos aqui por utilizar e manipular uma cópia em dicionário das configurações através do atributo ``_sections`` deste objeto. Assim, quando uma seção de configuração é acessada dentro de ``Config`` com o operador ``[]``, o método mágico ``__getitem__()`` retorna um dicionário dentro de ``_sections`` podendo conter qualquer tipo de dados como chaves e valores. Além disso, a classe ``Config`` implementa diversos métodos iniciando por ``validate_``, dedicados a validar seções e chaves específicas do arquivo de configuração, por vezes introduzindo novas chaves úteis à classe ``Meteorologger``. A organização foi feita assim para evitar um único método com mais de 20 linhas de código. Segue um resumo das validações realizadas: Validações ========== * ``validate_server_url()`` Utiliza valores da seção ``[server]`` para compor a URL utilizada para postagem de dados. O valor ``URL`` consiste na base do endereço do servidor -- opção disponibilizada para o caso de o usuário desejar utilizar outro servidor que não o nosso (por exemplo, um servidor local como ``http://localhost``). O valor ``BOARD_ID`` é utilizado pela URL e também pela API do site ao validar o usuário no momento da postagem. * ``validate_reading_sensors()`` Utiliza os valores da seção ``[reading]`` para determinar quais sensores terão a leitura solicitada pelo logger e também se deverá ser lido o relógio da placa (visto como um sensor de nome ``RTC_DS1307``). A ordem dos sensores na chave ``SENSORS`` determinará as colunas do arquivo *datalog.csv* (guarda dados localmente). Também introduz a nova chave ``reading/command`` contendo a linha de comando a ser enviada para a porta serial. Essa linha vai conter todos os sensores da chave ``SENSORS``, e também o ``RTC_DS1307`` caso a chave ``RTC_DS1304`` seja ``true``. * ``validate_reading_interval()`` Valida a chave ``INTERVAL`` da seção ``[reading]``, transformando a string ``h:m:s`` em um dicionário ``{'H': hh, 'M': mm, 'S': ss}`` para fácil acesso posterior a esses valores. Também introduz a nova chave ``reading/interval_seconds`` contendo o valor do intervalo de leitura já convertido para segundos. * ``validate_datalog_csv_sep()`` Valida o caracetere utilizado como separador CSV do arquivo *datalog.csv*, configurado na chave ``CSV_SEP`` da seção ``[datalog]``. Além de eliminar opções inválidas, decodifica o caractere para uso ASCII correto posteriormente. * ``validate_arduino_serial_port()`` Valida a chave ``SERIAL_PORT`` da seção ``[arduino]``. O usuário pode especificar uma ou mais portas separadas por vírgula para que o logger tente conexão caso uma delas falhe. Adicionalmente, essa chave pode ser deixada em branco, caso em que será gerada a seguinte lista de portas: ``['/dev/ttyACM0', '/dev/ttyUSB0', ..., '/dev/tty/ACM4', '/dev/ttyUSB4']`` para que o logger tente buscar sozinho a porta onde está a placa. app/main.py *********** .. [1] Requer que o programa ``make`` esteja instalado no sistema Linux. Felizmente ele vem por padrão nas principais distribuições.