4 октября 2015 г.

Исправляем проблему сборки модулей VirtualBox под Ubuntu

Который раз уже сталкиваюсь с проблемой, что срочно нужна виртуальная машина с WinXP, но быстро выясняется, что после обновления ядра DKMS не собрал необходимые модули автоматически. Поэтому приходилось вручную собирать модули следующей командой:
sudo /etc/init.d/vboxdrv setup
По сообщениям в консоли, стало понятно, что регистрация модулей через DKMS не срабатывает, поэтому после установки обновлений ядра автоматическая установка не происходит, что подтвердил вывод команды:
dkms status | grep vboxhost
Немного погуглив по английским сайтам, нашел рабочее решение, чтобы восстановить сборку через dkms нужно сделать следующее:
sudo rm -rf /var/lib/dkms/vboxhost/
sudo dpkg-reconfigure virtualbox-5.0
Первой командой мы удаляем настройки от старых версий virtualbox (а у меня их была целая куча), затем перенастраиваем пакет с virtualbox (здесь нужно будет указать свою версию virtualbox, если вы пользуетесь отличной от моей). После чего вас могут попросить закрыть виртуалбокс, а так же спросят, нужно ли установить драйвер прямо сейчас - отвечаем ДА. После чего через dkms соберется драйвер для текущей версии ядра. Убедиться в том, что все прошло успешно, можно командой:
dkms status | grep vboxhost
При успехе получим следующее:
vboxhost, 5.0.4, 3.16.0-50-generic, x86_64: installed
Обратите внимание, что версия ядра должна совпадать с текущим. На этом все, надеюсь кому-то эта информация пригодится.

Arduino + Raspberry Pi = термометр + гигрометр с отправкой данных на сервер (часть 2)

В предыдущей части статьи я рассказывал про часть, которая отвечала за измерение температуры и влажности, теперь пора рассказать о части, отвечающей за сбор данных с Arduino и отправку их на сервер.

Для сбора данных с Arduino используется программа на языке Python, который читает данные в формате CSV с серийного порта ардуины, затем обрабатывает их и отправляет на веб-сервер, FTP и OpenWeatherMap. Так же я предусмотрел возможность записи в журнал сообщений о действии программы или об возникающих ошибках, поэтому можно легко понять, из-за чего данные могли не дойти до серверов.

Теперь рассмотрим поподробнее работу нашего сборщика данных. Удобнее всего будет давать объяснения прямо по тексту программы, поэтому начнем:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# тут мы импортируем все необходимые библиотеки и модули для дальнейшей работы
import serial
from serial import SerialException
import datetime
import re
import ftplib
import socket
import os
import sys
import time
import signal
import logging
from logging.handlers import WatchedFileHandler
import threading
import requests
from requests.exceptions import *

# тут у нас указан путь к файлу журнала, куда будут записываться все сообщения
LOG_PATH = '/var/log/mstation.log'
# формат сообщения, в конце статьи приведу ссылку на описание, что тут что означает
LOG_FORMAT = '[%(asctime)s] %(levelname)s: %(funcName)s: %(message)s'
# путь к серийному порту (наша ардуина)
SERIAL_DEV = '/dev/ttyACM0'
# здесь указываем имя пользователя, логин и пароль для ftp сервера
FTP_SERV = 'ftp_serv'
FTP_NAME = 'ftp_name'
FTP_PASS = 'ftp_password'
# путь к директории, в которой будут храниться полученные данные (на Raspberry Pi)
DATA_PATH = '/usr/local/MStation/data/'
# количество попыток установить соединение с ардуиной
CONNECT_TRYOUT = 10
# путь к php файлу на сервере, который обрабатывает отправленные данные
UPLOAD_URL = 'upload_handler'
# OpenWeatherMap
# данные для проекта OpenWeatherMap (логин, пароль, название станции и ее координаты)
URL_OPENW = 'http://openweathermap.org/data/post'
LATITUDE = 56.3294
LONGITUDE = 37.7532
ALTITUDE = 215
USER_NAME = 'user_name'
PASSW = 'password'
STATION_NAME = 'station_name'
# OpenWeatherMap

# блокировки для корректной работы потоков, подробности будут ниже
write_lock = threading.Lock()
upload_lock = threading.Lock()

# очередь с пакетами, которые не удалось отправить
failed_upload = []

# тут мы настраиваем работу логгера
log = logging.getLogger('main_log')
log.setLevel(logging.INFO)
log_handler = WatchedFileHandler(LOG_PATH)
log_fmt = logging.Formatter(fmt=LOG_FORMAT)
log_handler.setFormatter(log_fmt)
log.addHandler(log_handler)


# основная функция, отсюда начинается выполнение
def main():
    signal.signal(signal.SIGINT, ctrl_c_handler)
    # генерируем имя файла, в который будут записаны полученные от Arduino данные
    f_name = datetime.datetime.now().strftime('%Y-%m-%d.cvs')
    # получаем полный путь к файлу
    f_path = DATA_PATH + f_name
    # открываем файл в режиме присоединения, т.е. если файл уже есть на диске, то данные будут в него дописываться
    f_table = open(f_path, 'a', 0)

    # открываем соединение с Arduino
    ser = serial_connect(SERIAL_DEV, 115200)

    # паттерн для отсеивания строки инициализации
    init_patt = re.compile(r'MStation')
    # запоминаем текущую дату
    curr_date = datetime.date.today()

    # считывание данных у нас происходит в бесконечном цикле
    while True:
        try:
            # считываем строку из порта пока не встретится символ окончания строки
            curr = ser.readline()
        except SerialException as e:
            # если возникла ошибка, то открываем соединение заново
            log.error('Serial error: ' + str(e))
            time.sleep(120)
            ser = serial_connect(SERIAL_DEV, 115200)
            continue

        # проверяем, считанная строка - строка инициализации ?
        init_match = re.search(init_patt, curr)

        if init_match:
            # если да, то отправляем ее в журнал
            log.info(curr)
            continue
        # если у нас изменилась дата, то меняем имя файла и открываем новый файл
        if curr_date.day != datetime.date.today().day:
            f_table.close()
            f_name = datetime.datetime.now().strftime('%Y-%m-%d.cvs')
            f_path = DATA_PATH + f_name
            f_table = open(f_path, 'a', 0)
            curr_date = datetime.date.today()

        # приписываем к строки с данными текущие дату и время
        date_str = datetime.datetime.now().strftime('%Y/%m/%d %H:%M')
        final_str = date_str + ',' + curr

        # создаем поток, который отсылает данные на ftp сервер
        ftp_thread = threading.Thread(
            target=upload_to_ftp,
            args=(FTP_SERV, FTP_NAME, FTP_PASS, f_name)
        )
        # создаем поток, который отсылает данные на веб сервер
        upl_thread = threading.Thread(
            target=upload_to_site,
            args=(UPLOAD_URL, final_str)
        )
        # создаем поток, который отсылает данные на проект OpenWeatherMap сервер
        openw_thread = threading.Thread(
            target=upload_to_openweathermap,
            args=[final_str]
        )

        # записываем данные в файл
        write_lock.acquire()
        f_table.write(final_str)
        write_lock.release()
        # запускаем работу потоков
        ftp_thread.start()
        upl_thread.start()
        openw_thread.start()

    sys.exit(0)


# функция для создания соединения с Arduino через серийный порт
def serial_connect(dev, speed):
    try_count = 1

    while try_count <= CONNECT_TRYOUT:
        try:
            ser = serial.Serial(dev, speed)
        except SerialException as e:
            log.error('Serial Connect Error: ' + str(e))
            print 'Connection Error'
            time.sleep(120)
        else:
            log.info('Connected after ' + str(try_count) + ' tryouts')
            return ser
        finally:
            try_count += 1

    log.critical('FAIL to connect. Exit.')
    sys.exit(-1)


# функция для отправки данных на ftp сервер
def upload_to_ftp(host, name, passw, f_name):
    # генерируем полный путь к файлу
    path = DATA_PATH + f_name
    # открываем файл
    upl_file = open(path, 'rb')
    # к файлу на сервере при загрузке дописывается .new для того, чтобы в случае проблем с соединением, на сервере не оказался испорченный файл, поэтому файл заливается с приписанным к нему .new, а при успехе файл переименовывается
    name_new = f_name + '.new'
    try:
        # устанавливаем соединение и загружаем файл в папку public_html
        ftp_serv = ftplib.FTP(host, name, passw, '', 30)
        ftp_serv.cwd('public_html')
        ftp_serv.voidcmd('TYPE I')
        ftp_serv.voidcmd('PASV')
        write_lock.acquire()
        ftp_serv.storbinary('STOR '+name_new, upl_file)
        # смотрим размер залитого файла
        upl_size = ftp_serv.size(name_new)
        if upl_size:
            # сравниваем с файлом на диске, при совпадении переименовываем на сервере
            f_size = os.path.getsize(path)
            if upl_size == f_size:
                ftp_serv.rename(name_new, f_name)
            else:
                raise uplFail('sizes not match')
        else:
            raise uplFail('unknown error')
        ftp_serv.close()
    # обработка потенциальных ошибок
    except (socket.error, socket.gaierror) as e:
        log.error('FTP Socket Error: ' + str(e))
    except ftplib.error_temp as e:
        log.error('FTP Temporary Error: ' + str(e))
    except ftplib.error_perm as e:
        log.error('FTP Permanent Error: ' + str(e))
    except ftplib.error_reply as e:
        log.error('FTP Unexpected Error: ' + str(e))
    except uplFail as e:
        log.error('FTP Upload Failed: ' + str(e))
    else:
        log.info('FTP Upload Success')
    finally:
        if write_lock.locked():
            write_lock.release()

    upl_file.close()


# необходим для работы загрузки по FTP
class uplFail(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


# отправка данных на веб-сервер
# при запуске отправляет один пакет на сервер, при успехе смотрит, не остались ли неотправленные данные, если остались, то пытается их отослать, если отправка одного пакета оказалась неудачной - добавляет пакет в список неотправленных пакетов
def upload_to_site(url, data_str):
    if not upload_to_url(url, data_str):
        failed_upload.append(data_str)
    else:
        if upload_lock.acquire(False):
            while len(failed_upload) != 0:
                data = failed_upload[0]
                if upload_to_url(url, data):
                    failed_upload.pop(0)
            upload_lock.release()


# отправляет один пакет на сервер
def upload_to_url(url, data_str):
    # подготавливаем данные к отправке
    data_send = data_str.split(',')
    data_req = {
        'date': data_send[0],
        'temp1': data_send[1],
        'temp2': data_send[2],
        'humid': data_send[3]
    }

    try:
        # пытаемся отправить данные на веб-сервер
        req = requests.post(url, data=data_req, timeout=60)
    # обрабатываем ошибки, если они были
    except (ConnectionError, HTTPError, URLRequired, Timeout) as e:
        log.error(str(e))
        return False
    else:
        # проверяем, правильно ли обработан наш POST запрос
        if (req.status_code == requests.codes['ok']):
            log.info('Upload success.')
            return True
        else:
            log.error('Upload failed with code ' + str(req.status_code))
            return False


# отправка данных на проект OpenWeatherMap, работает аналогично предыдущей функции
def upload_to_openweathermap(data_str):
    data_send = data_str.split(',')
    data_post = {
        'temp': data_send[1],
        'humidity': data_send[3],
        'lat': LATITUDE,
        'long': LONGITUDE,
        'alt': ALTITUDE,
        'name': STATION_NAME
    }
    try:
        req = requests.post(
            URL_OPENW,
            auth=(USER_NAME, PASSW),
            data=data_post,
            timeout=60
        )
    except (ConnectionError, HTTPError, URLRequired, Timeout) as e:
        log.error(str(e))
    else:
        if (req.status_code == requests.codes['ok']):
            log.info('Upload to OpenWeather success.')
        else:
            log.error(
                'Upload to OpenWeather failed with code '
                +
                str(req.status_code)
            )


# обработчик сигнала об завершении работы программы
def ctrl_c_handler(signum, frame):
    print 'Exit.'
    log.info('Exit.')
    logging.shutdown()
    sys.exit(0)

if __name__ == '__main__':
    main()

Теперь необходимо рассказать о некоторых аспектах работы всей этой штуковины. Как уже говорилось в предыдущей части статьи, данные с ардуины приходят в формате CSV, здесь же к этим данным дописываются текущие дата и время, естественно с условием соблюдения этого формата. Все эти данные хранятся на диске (в случае с Raspberry Pi на sd карточке), и при каждом изменении файла отправляются на FTP-сервер. Для чего ? Все довольно просто, изначально данные на сервере брались из файла, но впоследствии я реализовал хранение в бд MySQL, а реализованную загрузку на ftp решил не удалять. Если вам она не нужна, то просто удалите код, связанный с FTP. И да, один файл содержит данные, собранные за сутки, поэтому называется, к примеру, 2015-10-01.cvs, а его содержимое выглядит примерно вот так:
2014/09/05 14:35,20.57,21.90,50.80
2014/09/05 14:40,20.66,21.90,50.00
2014/09/05 14:45,20.51,22.00,49.90
2014/09/05 14:50,20.98,22.00,50.00
2014/09/05 14:55,20.98,22.00,50.00
Данные на сервер отправляются POST запросом (для собственного веб-сервера и для OpenWeatherMap). О том, что этот запрос из себя представляет, почитайте вот тут: http://www.myfirstsite.ru/articles/get-and-post 
Так как я пользуюсь потоками для отправки данных на сервера, то возникла необходимость ввести примитивы синхронизации потоков, такие как блокировки:
write_lock = threading.Lock()
upload_lock = threading.Lock()
Первая блокировка нужна для того, чтобы у нас не возникло ситуации, что мы записываем данные в файл, а в этот момент у нас происходит его отправка на сервер, без блокировки мы бы получили файл, в конце которого был бы кусок данных, что не очень то хорошо. Вторая блокировка нужна, чтобы у нас не возникло ситуации, что у нас одновременно работают два потока и отправляют на сервер одни и те же данные. Подробнее об этих механизмах потоков и блокировках почитайте вот тут: https://www.ibm.com/developerworks/ru/library/l-python_part_9/

В конце скажу, что автозапуск программы осуществляется через upstart, подробно об этом я писал у себя вот в этой статье: Raspberry Pi + Raspbian = автозапуск программы при помощи upstart после синхронизации с NTP

На этом, пожалуй, и остановимся, в следующей части статьи расскажу о той части, которая работает на стороне сервера и обрабатывает запросу на прием и выдачу данных. Если возникнут какие-то вопросы, пишите мне в G+ или в комментарии.

Всего вам доброго.

Ссылка на исходный код: https://github.com/edgar-ch/mstation/blob/stable/mstation_serv.py
Предыдущая часть статьи: http://blog.edtex.ru/2014/09/arduino-raspberry-pi-1.html