Commit 27cccac5 authored by Nelso Jost's avatar Nelso Jost

FIX: refactor finished and documentation improved

parent 8074a13f
PYVER := 3
PYBIN := python3
VENV := .venv
VENVPY := ${VENV}/bin/python
INO_DIR := .ino
USE := ino
......@@ -74,8 +75,8 @@ setup: clean-venv create-venv
create-venv:
@ echo "-------------------------------------------------------"
virtualenv -v --python='python${PYVER}' ${VENV}
@ echo "Virtualenv with 'python${PYVER}' interpreter was created at ${VENV}"
virtualenv -v --python='${PYBIN}' ${VENV}
@ echo "Virtualenv with '${PYBIN}' interpreter was created at ${VENV}"
@ echo "-------------------------------------------------------"
${VENV}/bin/pip install --upgrade pip
@ echo "-------------------------------------------------------"
......@@ -90,26 +91,26 @@ clean-venv:
rm -rf ${VENV}
serial:
${VENV}/bin/python -i logger/init_serial.py
${VENVPY} -i logger/init_serial.py
firmware: ${USE}-install
chmod +x scripts/ino-build.sh
./scripts/ino-build.sh ${USE} ${INO_DIR} ${ARDUINO}
sync-rtc:
${VENV}/bin/python${PYVER} logger/run.py --syncrtc
${VENVPY} logger/run.py --syncrtc
run:
${VENV}/bin/python logger/run.py
logger-run:
${VENVPY} logger/run.py --verbose
deploy: undeploy
mkdir -p logger/logs
sudo ${VENV}/bin/python${PYVER} logger/deploy.py
sudo ${VENVPY} logger/deploy.py
undeploy:
sudo ${VENV}/bin/python${PYVER} logger/deploy.py -u
sudo ${VENVPY} logger/deploy.py -u
tail-log:
logger-tail:
$(eval TMP := $(shell ls -t -I "pid*|stdout*" logger/logs | head -n 1))
@ echo "Last log file updated: logger/logs/$(TMP)"
@ echo "File size: `du -h logger/logs/$(TMP) | cut -f1`"
......@@ -131,7 +132,7 @@ plot-data:
@ cd tools && gnuplot -persist -e "config='config.plt'; col=${col}" loop.plt
clean-data:
rm -rfv data/*.csv data/outgoing/*.json
rm -rfv data/*
clean-logs:
rm -rfv logger/logs/*
......
......@@ -153,13 +153,13 @@ Esse módulo contém:
* ``__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:: Entenderemos 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 cadastrado em http://dados.cta.if.ufrgs.br/emm.
.. 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.
Em particular, relógio RTC DS1307 também é visto como um sensor ainda que não seja cadastrado no servidor.
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.
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 -- 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.
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, como o ``LDR``. Já sensores que possuem controladoras Wire ou I2C, acabam fazendo uso de bibliotecas separadas para melhor organização do código. Ainda que sejam bibliotecas de terceiros, optamos por mantê-las aqui dentro do subdiretório ``libs/``.
.. note:: Bibliotecas de terceiros são mantidas no subdiretório ``libs/``.
Inserindo novos sensores
========================
......@@ -213,6 +213,208 @@ Exemplo :
Logger
######
Escrito na linguagem Python, é o software responsável por solicitar leitura dos sensores conectados na placa, guardar os dados localmente e também enviá-los ao servidor.
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 <https://supervisor.readthedocs.org/en/latest/>`_.
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 <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger/app/__init__.py>`_ tornando-se acessível exteriormente como um módulo. Assim, tanto um interpretador em `logger/ <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger>`_ como o arquivo `run.py <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger/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 <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger/app/main.py>`_::
from .config import Config
onde o operador ``.`` refere-se ao nível atual (`main.py <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger/app/main.py>`_ e `config.py <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger/app/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 <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger/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 <https://virtualenv.pypa.io/en/latest/>`_ 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 <pacote>``. 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 <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger/deploy.py>`_ é responsável por registrar um novo processo **daemon** no `supervisor <https://supervisor.readthedocs.org/en/latest/>`_ 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 <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger/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 <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/logger/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 <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/settings.ini>`_ e encontra-se na pasta raiz do projeto, ao lado do `Makefile <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/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 <https://git.cta.if.ufrgs.br/meteorolog/arduino-meteorolog/blob/master/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.
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
......@@ -8,9 +7,9 @@ PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
# $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
# endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
......@@ -19,7 +18,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext setup
help:
@echo "Please use \`make <target>' where <target> is one of"
......
from .main import Meteorologger
import configparser
import sys
import os
from pprint import pprint
DEFAULT_SETTINGS =\
DEFAULT_INI=\
"""\
[server]
; ID of the board to which data will be uploaded to
;
; base URL of the server
URL = http://dados.cta.if.ufrgs.br/emm
; board identification number (auto generated when a new board is created)
BOARD_ID =
; authentication token for the board's user
;
USER_HASH =
; wep app base URL
;
URL = http://dados.cta.if.ufrgs.br/emm
[logger]
; comma-separated list of sensor nicknames that will be read
; the order reflect columns of the local DATALOG csv file
;
[reading]
; CSV list of sensor names/nicknames (order reflect columns on datalog files)
SENSORS = DHT22_TEMP, DHT22_AH, BMP085_PRESSURE, LDR
; time between readings attempts (cycles of the logger execution)
; format: hours:minutes:seconds
;
; time between logger cycles -- format: hours:minutes:seconds
INTERVAL = 0:5:0
; True for try reading RTC_DS1307 or False for use the system time
; if the RTC reading fails, the system time will be used instead
;
USE_RTC_DS1307 = False
; expected time stamp of the RTC (see Python's datetime module documentation)
;
RTC_DATETIME_FORMAT = %Y-%m-%d %H:%M:%S
; true for read board clock; if fail, or false, system's time will be used
RTC_DS1307 = true
[datalog]
; CSV delimiter (sugestions: ',' or ';' or '\t')
;
; CSV delimiter for local log files (sugestions: ',' or ';' or '\t')
CSV_SEP = '\t'
; format of the datetime column (see Python docs on datetime module)
;
DATETIME_FORMAT = %Y-%m-%d-%H-%M-%S
[arduino]
; USB port that have the board plugged in
; keep blank for automatic search on /dev/ttyACM* and /dev/ttyUSB*
;
; CSV list of ports to attempt or blank for automatic search on
; /dev/ttyACM* and /dev/ttyUSB* (where * varies from 0 to 10)
SERIAL_PORT =
"""
def make_path_here(filename):
''' Append filename to the current __file__ path. '''
return os.path.join(os.path.abspath(os.path.dirname(__file__)), filename)
class Config(dict):
class RTCDateTime:
SENSOR_NAME = 'RTC_DS1307'
READ_TIMESTAMP = '%Y-%m-%d %H:%M:%S'
__qualname__ = "RTCDateTime fmt='{}'".format(READ_TIMESTAMP)
def __init__(self, s):
self.dt = datetime.strptime(s, self.RTC_DT_FMT)
def __str__(self):
return self.dt.strftime('%Y%m%d%H%M%S')
class Config:
SETTINGS_FILENAME = make_path_here('../../settings.ini')
DATALOG_PATH = make_path_here('../../data/')
EXECUTION_LOG_PATH = make_path_here('../logs/')
OUTGOING_FILENAME = make_path_here('../../data/outgoing.json')
URL_POST_RAWSENSORDATA = '{base}api/post/rawsensordata/{bid}'
URL_API_POST_RAWSENSORDATA = '{base}api/post/rawsensordata/{bid}'
SERIAL_CSV_SEP = ','
def __init__(self):
super().__init__()
self.load_settings()
self.default = configparser.ConfigParser()
self.default.read_string(DEFAULT_INI)
try:
self.load_settings()
except Exception as e:
print('\nConfigurationError:', e)
def __getitem__(self, key):
return self._parser._sections[key]
def ask_restore_settings_file(self):
a = input("\nRestore default file now and overwrite all
a = input("\nRestore default file now and overwrite all"
"current values? [y/N] ")
if 'y' in a.lowercase()
if 'y' in a.lowercase():
try:
with open(self.SETTINGS_FILENAME, 'w') as f:
f.write(DEFAULT_SETTINGS)
......@@ -84,53 +99,94 @@ class Config(dict):
sys.exit(1)
def get(self, section, key, validate, ask_restore=True):
try:
r = self.config[section]
except:
print("Config file\n {}\n\nis missing the section [{}]!"
.format(self.SETTINGS_FILENAME, section))
if ask_restore:
self.ask_restore_settings_file()
try:
r = self.config[section][key]
print("On the config file\n {}\n\nsection [{}] is missing the "
"key '{}'!")
if ask_restore:
self.ask_restore_settings_file()
try:
r = validate(self.config[section][key])
except:
print("On the config file\n {}\n\ninvalid value for '{}' for "
"key the [{}][{}]!".format(""))
if ask_restore:
self.ask_restore_settings_file()
def load_settings(self, filename):
def load_settings(self):
'''
Load the configuration file onto the self.CFG attribute.
Some keys will be tested and filenames will be normalized.
Load the INI configuration file onto the self._parser attribute.
'''
self.config = configparser.ConfigParser()
self._parser = configparser.ConfigParser()
try:
self.config.read(self.SETTINGS_FILENAME)
self._parser.read(self.SETTINGS_FILENAME)
except:
print("Unable to open configuration file at\n {}".format(
self.SETTINGS_FILENAME))
self.ask_restore_settings_file()
self.URL_POST_RAWSENSORDATA = self.URL_POST_RAWSENSORDATA.format(
base=self.CFG('server', 'URL',
lambda x: x if x.endswith('/') else x + '/'),
bid=self.CFG('server', 'BOARD_ID', int)
pprint(self._parser._sections)
self.validate_server_url()
self.validate_reading_sensors()
self.validate_reading_interval()
self.validate_datalog_csv_sep()
self.validate_arduino_serial_port()
pprint(self._parser._sections)
self.LOGGER_INTERVAL_SECONDS =\
self.CFG['LOGGER']['INTERVAL']['seconds']\
+ 60 * self.CFG['LOGGER']['INTERVAL']['minutes']\
+ 3600 * self.CFG['LOGGER']['INTERVAL']['hours']
self.DATALOG_CSV_SEP = bytes(self.CFG['DATALOG']['CSV_SEP'],
'utf8').decode('unicode_escape')
def validate_server_url(self):
try:
bid = int(self['server']['board_id'])
except:
raise Exception(
"Invalid numeric value for the server/BOARD_ID key.\n"
"Expected to be of type integer.\n"
"Was given:\n {}".format(self['server']['board_id']))
self['server']['api_post_url'] = self.URL_API_POST_RAWSENSORDATA\
.format(base=self['server']['url'] +
('' if self['server']['url'].endswith('/') else '/'),
bid=bid)
def validate_reading_sensors(self):
self['reading']['sensors'] = [x.strip() for x in
self['reading']['sensors'].split(',') if x.strip() != '']
if len(self['reading']['sensors']) == 0:
raise Exception(
"At least one sensor must be present on the reading/"
"SENSORS key.")
if 'true' in self['reading']['rtc_ds1307'].lower():
self['reading']['sensors'].append(RTCDateTime.SENSOR_NAME)
self['reading']['command'] = 'readSensors,' + ','.join(
self['reading']['sensors'])
def validate_reading_interval(self):
try:
numbers = self['reading']['interval'].split(':')
if len(numbers) != 3: raise Exception
self['reading']['interval'] = {k: int(v) for k, v in
zip(['H', 'M', 'S'], numbers)}
self['reading']['interval_seconds'] =\
3600 * self['reading']['interval']['H'] + \
60 * self['reading']['interval']['M'] + \
self['reading']['interval']['S']
except:
raise Exception(
"Invalid time value for the reading/INTERVAL key.\n"
"Expected format:\n"
" hours:minutes:seconds (integer numbers)\n"
"Was given:\n {}".format(self['reading']['interval']))
def validate_datalog_csv_sep(self):
value = self['datalog']['csv_sep']
self['datalog']['csv_sep'] = bytes(value, 'utf8').decode(
'unicode_escape')
if not value in (',', ';', r'\t'):
raise Exception(
"Invalid character value for the datalog/CSV_SEP key.\n"
"Supported values:\n ',' ';' '\\t'\n"
"Was given:\n {}".format(value))
def validate_arduino_serial_port(self):
self['arduino']['serial_port'] = [x.strip() for x in
self['arduino']['serial_port'].split(',') if x.strip() != '']
if len(self['arduino']['serial_port']) == 0:
self['arduino']['serial_port'] = []
for i in range(5):
self['arduino']['serial_port'].append('/dev/ttyACM{}'.format(i))
self['arduino']['serial_port'].append('/dev/ttyUSB{}'.format(i))
#-------------------------------------------------------------------------------
# Author: Nelso G. Jost (nelsojost@gmail.com)
# License: GPL
# License: GPLv2
# Purpose: Get data from the board via serial and send it to the server.
#-------------------------------------------------------------------------------
from __future__ import print_function, division
from .config import Config
from .config import Config, RTCDateTime
from datetime import datetime
from pprint import pprint
......@@ -19,47 +19,24 @@ import sys
import time
import json
def make_path_here(filename):
''' Append filename to the current __file__ path. '''
return os.path.join(os.path.abspath(os.path.dirname(__file__)), filename)
class RTCDateTime:
RTC_DT_FMT = '%Y-%m-%d %H:%M:%S'
__qualname__ = "RTCDateTime fmt='{}'".format(RTC_DT_FMT)
def __init__(self, s):
self.dt = datetime.strptime(s, self.RTC_DT_FMT)
def __str__(self):
return self.dt.strftime('%Y%m%d%H%M%S')
class Meteorologger:
'''
Provides a series of mechanisms to collect sensor data from the board
via serial port, save locally on the machine and upload to the server.
The functionality of this class relies heavily on the config file given
by the SETTINGS_FILENAME attribute, which uses YAML syntax.
The functionality of this class relies heavily on the configuration file
specified and managed by the config module, which is validated on the
class instantiation and stored on self.config attribute.
Call the run() method to start the logging process.
Call the run() method to start the logging processs.
'''
DATALOG_DIR = 'data/'
EXECUTION_LOG_PATH = 'logger/logs/'
OUTGOING_BASENAME = 'outgoing.json'
FILE_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M-%S'
SERIAL_CSV_SEP = ','
DATA_FORMATS = {'int': int, 'float': float, 'str': str,
'datetime': RTCDateTime}
FIND_PORT_TIMEOUT = 0.5 # seconds
ARDUINO_BAUD_RATE = 9600
SERIAL_READ_TIMEOUT = 1.5 # seconds
FIND_PORT_TIMEOUT = 5 # seconds
BOARD_RESET_TIMEOUT = 2 # seconds
BOARD_RESPONSE_DELAY = 3 # seconds
verbose = True
def __init__(self, verbose=False):
self.verbose = verbose
self.config = Config()
......@@ -73,292 +50,218 @@ class Meteorologger:
print("Invalid bytes!")
return result
def create_json(self, raw_line):
def setup_logging(self):
'''
Given the raw serial line response (CSV string), builds and returns
a JSON dict with validated, server-ready, sensor data.
Prepares the execution log file mechanism, which uses the standard
library logging. This way de app is prepared for background execution.
'''
d = {'datetime': {'format': self.CFG['DATALOG']['DATETIME_FORMAT']},
'sensors': {}}
rtc = self.CFG['LOGGER']['USE_RTC_DATETIME']
using_rtc = rtc and rtc in self.CFG['LOGGER']['SENSORS']
if using_rtc:
d['datetime']['source'] = rtc
rtc_datetime_fmt = self.CFG['LOGGER']['RTC_DATETIME_FORMAT']
for i, v in enumerate(raw_line.split(self.SERIAL_CSV_SEP)):
nickname = self.CFG['LOGGER']['SENSORS'][i]
type_name = self.CFG['SENSORS_AVAILABLE'][nickname]['data_format']
if type_name == 'datetime':
if using_rtc and not 'not_found' in v.strip():
d['datetime']['value'] = datetime.strptime(
v, rtc_datetime_fmt).strftime(d['datetime']['format'])
continue
try:
v = self.DATA_FORMATS[type_name](v.strip())
except:
logging.warning("[{}]: '{}' is not a valid {}"
.format(nickname, v, type_name))
d['sensors'][nickname] = 'NaN'
continue
d['sensors'][nickname] = v
if not 'value' in d['datetime']:
d['datetime']['source'] = 'logger'
d['datetime']['value'] = datetime.now().strftime(
d['datetime']['format'])
logging.info("Validated JSON: {}".format(d))
logging.basicConfig(
level=logging.INFO,
filename=self.SESSION_EXECUTION_LOG_FILENAME,
format='%(asctime)s : %(levelname)s : %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
return d
if self.verbose:
root = logging.getLogger('')
root.setLevel(logging.INFO)
console = logging.StreamHandler()
console.setFormatter(logging.Formatter(
fmt='%(asctime)s : %(levelname)s : %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'))
root.addHandler(console)
def write_datalog(self, json_data):
def setup_session_files(self):
'''
For backup purposes, write the given JSON data onto the file
DATALOG_CSV as specficied on self.SETTINGS_FILENAME.
Prepare directory structure and files for execution and data logging
by configuring proper FILENAME constants with current system time.
'''
# convert raw str into normal escaped str (e.g., r'\\t' --> '\t')
csv_line = json_data['datetime']['value'] + self.DATALOG_CSV_SEP
session_datetime = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
for nickname in self.CFG['LOGGER']['SENSORS']:
if nickname in json_data['sensors']:
csv_line += str(json_data['sensors'][nickname])
csv_line += self.DATALOG_CSV_SEP
csv_line = csv_line[:-1]
self.SESSION_DATALOG_FILENAME = self.config.DATALOG_PATH \
+ 'datalog-' + session_datetime + '.csv'
self.SESSION_EXECUTION_LOG_FILENAME = self.config.EXECUTION_LOG_PATH \
+ 'exec-' + session_datetime + '.log'
try:
datalog_filename = self.SESSION_DATALOG_FILENAME
with open(datalog_filename, 'a') as f:
f.write(csv_line + '\n')
logging.info("Updated datalog file: '{}'".format(datalog_filename))
with open(self.SESSION_DATALOG_FILENAME, 'w') as f:
# initiate the file by writing the CSV header columns