Skip to content

Commit

Permalink
#2
Browse files Browse the repository at this point in the history
  • Loading branch information
slomkowski committed Jul 17, 2020
1 parent 90d0537 commit 0857f15
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 160 deletions.
19 changes: 19 additions & 0 deletions host/geiger-manager/geiger-manager-tray-application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env python3
import sys

from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMenu
from PyQt5.QtGui import QIcon

app = QApplication(sys.argv)
tray_icon = QSystemTrayIcon(QIcon("icons8-nuclear-64.png"), parent=app)
tray_icon.setToolTip("test trool tip")
tray_icon.show()

menu = QMenu()
exit_action = menu.addAction("Exit")

exit_action.triggered.connect(app.quit)

tray_icon.setContextMenu(menu)

sys.exit(app.exec())
47 changes: 13 additions & 34 deletions host/geiger-manager/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
import configparser
import logging
import os
import signal
import sys
import time

import usbcomm

__version__ = '1.2.0'
__version__ = '2.0.0'
__author__ = 'Michał Słomkowski'
__copyright__ = 'GNU GPL v.3.0'

Expand All @@ -27,29 +26,16 @@

CONFIG_PATH = [os.path.realpath(os.path.join(directory, CONFIG_FILE_NAME)) for directory in CONFIG_PATH]


def signal_handler(signum, frame):
"""Handles stopping signals, closes all updaters and threads and exits."""
if args.monitor:
global logger
logger.info("Catched signal no. %d, stopping.", signum)
global monitor
monitor.stop()
logging.shutdown()
sys.exit(1)


# parse command-line arguments
description = "Geiger manager v. " + __version__ + ', ' + __author__ + ". "
description += """This program is a daemon which monitors constantly the radiation measured by USB Geiger device
and sends the results to cosm.com, MySQL database or CSV file. All configuration is stored in the file '"""
and sends the results to radmon.org, MySQL database or CSV file. All configuration is stored in the file '"""
description += CONFIG_FILE_NAME
description += """' which is essential to run. Program uses pyusb library and MySQLdb, if MySQL is enabled."""

parser = argparse.ArgumentParser(description=description)
parser.add_argument("-c", "--config", nargs=1, help="reads given configuration file")
parser.add_argument("-v", "--verbose", action='store_true', help="shows additional information")
parser.add_argument("-b", "--background", action='store_true', help="runs as background process")
group = parser.add_mutually_exclusive_group()
group.add_argument("-m", "--monitor", action='store_true', help="starts program in monitor mode")
group.add_argument("-s", "--status", action='store_true',
Expand All @@ -60,11 +46,6 @@ def signal_handler(signum, frame):
if args.verbose and not args.background:
print("Geiger manager v. " + __version__ + ', ' + __author__)

# become a daemon and fork
if args.background:
if os.fork() != 0:
os._exit(0)

if args.config:
CONFIG_PATH = [args.config[0]]
if args.verbose and not args.background:
Expand Down Expand Up @@ -102,10 +83,9 @@ def signal_handler(signum, frame):
else:
logger.setLevel(logging.ERROR)

if not args.background:
consoleLog = logging.StreamHandler(sys.stdout)
consoleLog.setFormatter(logFormatter)
logger.addHandler(consoleLog)
consoleLog = logging.StreamHandler(sys.stdout)
consoleLog.setFormatter(logFormatter)
logger.addHandler(consoleLog)

if not args.monitor:
loggingEnabled = False
Expand All @@ -127,20 +107,19 @@ def signal_handler(signum, frame):
logger.critical("Error at initializing USB device: %s", str(exp))
sys.exit(1)

# register SIGINT (Ctrl-C) signal handler
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

# start monitor mode
if args.monitor:
import monitor

monitor = monitor.Monitor(configuration=conf, usbcomm=comm)
monitor.start()
updaters = monitor.find_updaters(conf)

monitor = monitor.Monitor(conf, comm, updaters)

while not monitor.error:
time.sleep(1)

while True:
time.sleep(5)
logger.critical("Error in monitor: %s", monitor.error)
sys.exit(2)

# default behavior: display values from Geiger device and leave
print(comm)
sys.exit()
101 changes: 39 additions & 62 deletions host/geiger-manager/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,78 +6,55 @@
"""

import configparser
import importlib
import logging
import os
import sys
import threading
import time
from collections import Iterator

import updaters
import usbcomm
from updaters.csvfile import CsvFileUpdater
from updaters.email import EmailNotificationUpdater
from updaters.mysql import MySQLUpdater
from updaters.radmon_org import RadmonOrgUpdater


def find_updaters(configuration: configparser.ConfigParser) -> [updaters.BaseUpdater]:
# todo replace with dynamic module walking
return filter(lambda u: u.is_enabled(),
[EmailNotificationUpdater(configuration),
CsvFileUpdater(configuration),
MySQLUpdater(configuration),
RadmonOrgUpdater(configuration)])


class Monitor(object):
_interval = None
_usbcomm = None
_log = False
_configuration = None
_conf_file_section = "monitor"
_log = logging.getLogger("geiger.monitor")

error: Exception = None

_timer = None
def __init__(self,
configuration: configparser.ConfigParser,
comm: usbcomm.Connector,
updaters_list: [updaters.BaseUpdater]):
self._usbcomm = comm
self._updaters = updaters_list

_updaters = []
if not self._updaters:
raise Exception("At least one updater has to be enabled.")

def __init__(self, configuration, usbcomm):
self._log = logging.getLogger("geiger.monitor")
self._usbcomm = usbcomm
self._configuration = configuration
conf_file_section = 'monitor'
try:
self._interval = configuration.getint(conf_file_section, 'interval')
self._interval = configuration.getint(self._conf_file_section, 'interval')
except configparser.Error as e:
self._log.critical("Measuring interval wrong or not provided: %s.", str(e))
sys.exit(1)
raise Exception("Measuring interval wrong or not provided:", e)

self._log.info("Setting programmed voltage and interval to %d seconds.", self._interval)

usbcomm.set_voltage_from_config_file()
usbcomm.set_interval(self._interval)

# initialize all updater modules in the directory
for files in os.listdir(os.path.join(os.path.dirname(__file__), "updaters")):
if files.endswith(".py"):
self._initialize_updater(files[:-3])

if len(self._updaters) == 0:
self._log.critical("At least one updater has to be enabled.")
sys.exit(1)

def _initialize_updater(self, import_name):
importlib.import_module("updaters." + import_name, "updaters")
self._usbcomm.set_voltage_from_config_file()
self._usbcomm.set_interval(self._interval)

module = sys.modules["updaters." + import_name]

for elem in getattr(module, '__dict__'):
if elem.endswith('Updater'):
class_name = elem

try:
name = getattr(module, 'IDENTIFIER')
except:
return
try:
u = getattr(module, class_name)(self._configuration)
if u.is_enabled():
self._updaters.append(u)
self._log.info("%s updater enabled.", name)
except updaters.updaters.UpdaterException as e:
self._log.error("Error at initializing %s updater: %s. Disabling.", name, str(e))

def start(self):
"""Enables cyclic monitoring. The first measurement cycle has the 1.5 length of the given interval in order
to collect data by the device. """

self._timer = threading.Timer(1.5 * self._interval, self._update)
self._timer = threading.Timer(1.5 * self._interval, self._perform_update_every_tick)
self._timer.setDaemon(True)
self._timer.start()

Expand All @@ -91,12 +68,12 @@ def stop(self):
if updater.is_enabled():
updater.close()

def _update(self):
def _perform_update_every_tick(self):
"""This method is called by the internal timer every 'interval' time to gather measurements and send them to
specified updaters. The first cycle has 1.5*interval length to give the Geiger device time to collect counts.
Then, update takes place in the middle of the next measuring cycle. """
Then, update takes place in the middle of the next measuring cycle."""

self._timer = threading.Timer(self._interval, self._update)
self._timer = threading.Timer(self._interval, self._perform_update_every_tick)
self._timer.setDaemon(True)
# start new cycle here to prevent shifting next update time stamp
self._timer.start()
Expand All @@ -116,19 +93,19 @@ def _update(self):
except usbcomm.CommException as e:
self._log.critical("Error at reinitializing device: %s", str(e))
self.stop()
# close entire application
thread.interrupt_main()
self.error = e
return

self._timer.cancel()
self._timer = threading.Timer(1.5 * self._interval, self._update)
self._timer = threading.Timer(1.5 * self._interval, self._perform_update_every_tick)
self._timer.setDaemon(True)
self._timer.start()
return

self._log.info("pushing data: %f CPM, %f uSv/h", cpm, radiation)
self._log.info("Pushing data: %f CPM, %f uSv/h", cpm, radiation)

for updater in self._updaters:
try:
updater.update(timestamp=timestamp, radiation=radiation, cpm=cpm)
except updaters.updaters.UpdaterException as exp:
except updaters.UpdaterException as exp:
self._log.error("Updater error: %s", str(exp))
9 changes: 5 additions & 4 deletions host/geiger-manager/tests/test_monitor.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import configparser
import unittest
import time
import updaters
import unittest
from unittest import TestCase
from unittest.mock import patch

import monitor
import usbcomm
import updaters


class DummyUpdater(updaters.BaseUpdater):
pass
full_name = "updater used in tests only"


class TestMonitor(TestCase):

Expand Down
8 changes: 8 additions & 0 deletions host/geiger-manager/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from unittest import TestCase

import utils


class UtilsTest(TestCase):
def test_find_updaters(self):
utils.find_updaters()
4 changes: 4 additions & 0 deletions host/geiger-manager/updaters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ def update(self, timestamp: time.struct_time, radiation: float = None, cpm: floa
"""Sends data wherever they are sent. Both parameters are optional. Raises exception if no data was
delivered. """
raise NotImplementedError

@property
def full_name(self):
raise NotImplementedError
49 changes: 25 additions & 24 deletions host/geiger-manager/updaters/csvfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@

import updaters

IDENTIFIER = 'CSV file'


class CsvFileException(updaters.UpdaterException):
pass
Expand All @@ -22,51 +20,54 @@ class CsvFileUpdater(updaters.BaseUpdater):
"""Writes the CPM and radiation data to CSV file. Each row contains information: date, time, radiation in uSv/h
and CPM (counts per minute) value. """

conf_file_section = 'csvfile'
full_name = "CSV file"

def __init__(self, configuration: configparser.ConfigParser):
"""Reads configuration and opens the file to read."""
super().__init__(configuration)
conf_file_section = 'csvfile'
try:
self._enabled = configuration.getboolean(conf_file_section, 'enabled')
self._enabled = configuration.getboolean(self.conf_file_section, 'enabled')
except Exception:
pass

if self._enabled is False:
return

try:
file_name = configuration.get(conf_file_section, 'file_name')
self._dateFormat = configuration.get(conf_file_section, 'date_format')
self._timeFormat = configuration.get(conf_file_section, 'time_format')
self._decimalSep = configuration.get(conf_file_section, 'decimal_separator', fallback='.')
delimiter = configuration.get(conf_file_section, 'delimiter')
self._file_name = configuration.get(self.conf_file_section, 'file_name')
self._dateFormat = configuration.get(self.conf_file_section, 'date_format')
self._timeFormat = configuration.get(self.conf_file_section, 'time_format')
self._decimalSep = configuration.get(self.conf_file_section, 'decimal_separator', fallback='.')
self._delimiter = configuration.get(self.conf_file_section, 'delimiter')
except configparser.Error as e:
self._enabled = False
raise CsvFileException("could not load all needed settings from the config file: " + str(e))

try:
self._fileHandle = open(file_name, 'ab')
self._csv = csv.writer(self._fileHandle, delimiter=delimiter)

# write header
if self._fileHandle.tell() == 0:
print("Adding header to CSV file.")
self._csv.writerow(("Date:", "Time:", "Radiation [uSv/h]:", "CPM:"))
with open(self._file_name, 'a', newline='') as fp:
# write header
if fp.tell() == 0:
print("Adding header to CSV file.")
wr = csv.writer(fp, delimiter=self._delimiter)
wr.writerow(("Date:",
"Time:",
"Radiation [uSv/h]:",
"CPM:"))
except IOError as e:
self._enabled = False
raise CsvFileException("could not open log file to write: " + str(e))

def close(self):
"""Closes the file."""
self._enabled = False
self._fileHandle.close()

def update(self, timestamp, radiation=None, cpm=None):
timestamp = self.local_time(timestamp)
curr_date = time.strftime(self._dateFormat, timestamp)
curr_time = time.strftime(self._timeFormat, timestamp)
try:
self._csv.writerow((curr_date, curr_time, str(radiation).replace('.', self._decimalSep),
str(cpm).replace('.', self._decimalSep)))
self._fileHandle.flush()
with open(self._file_name, 'a', newline='') as fp:
wr = csv.writer(fp, delimiter=self._delimiter)
wr.writerow((curr_date,
curr_time,
str(radiation).replace('.', self._decimalSep),
str(cpm).replace('.', self._decimalSep)))
except IOError as e:
raise CsvFileException("could not write row to the CSV file: " + str(e))
Loading

0 comments on commit 0857f15

Please sign in to comment.