'''
.. TODO:: Currently, the 'save' functionality does not work. To preserve comments and other formatting, a bit of work
will be needed to write a custom regex-based string subset modifier.
'''
import os
import sys
import time
import signal
import logging
import threading
from xml.etree import ElementTree
from xml.sax.saxutils import escape
[docs]class XmlConfig(object):
'''Placeholder for fldigi.prefs config read/write
:param config_dir: The directory in which fldigi reads and writes its config files. e.g. 'C:/Users/njansen/fldigi.files/'
:type config_dir: str
:param read_now: If True, read() is called immediately at the end of the constructor. Otherwise, a read is not done until explicitly called.
:type read_now: bool
:Example:
>>> import pyfldigi
>>> xc = pyfldigi.XmlConfig(config_dir)
.. automethod:: pyfldigi.xmlconfig.XmlConfig.__getitem__
.. automethod:: pyfldigi.xmlconfig.XmlConfig.__setitem__
.. automethod:: pyfldigi.xmlconfig.XmlConfig.__str__
'''
def __init__(self, config_dir, read_now=True):
# TODO: Check to make sure that the location is valid, and that the name is fldigi_def.xml
self.config_dir = config_dir
self.location = os.path.join(config_dir, 'fldigi_def.xml')
self.settings = {}
self.dirty = False
if read_now is True:
self.read()
[docs] def read(self):
'''Open and parse the config XML file.
:Example:
>>> import pyfldigi
>>> xc = pyfldigi.XmlConfig(config_dir, read_now=False)
>>> # do some other stuff
>>> xc.read()
>>> xc['baz'] # Read some random setting named 'baz'
42
'''
self.settings = {}
self.dirty = False
self.tree = ElementTree.parse(self.location)
self.root = self.tree.getroot()
if not self.root.tag == 'FLDIGI_DEFS':
raise Exception('Expected root tag to be \'FLDIGI_DEFS\' but got {}'.format(self.root.tag))
for child in self.root:
self.settings[str(child.tag).lower()] = child.text
[docs] def save(self):
'''Save the config. Will only write to the file if changes have been made (self.dirty is True)
:Example:
>>> import pyfldigi
>>> xc = pyfldigi.XmlConfig(config_dir)
>>> xc['baz'] = 42
>>> xc.save()
'''
if self.dirty is True:
# Delete the xml-old, if present
pass
[docs] def __str__(self):
'''Prints out all of the settings in a manner of: '{name} : {value}\n'
:Example:
>>> import pyfldigi
>>> xc = pyfldigi.XmlConfig(config_dir)
>>> str(xc)
SETTING1 : 0
SETTING2 : 1
# and so on
'''
s = ''
for key, value in self.settings.items():
s += ('{} : {}\n'.format(key, value))
return s
[docs] def __getitem__(self, key):
'''Get a setting from the XML config, by its XML tag name
:Example:
>>> import pyfldigi
>>> xc = pyfldigi.XmlConfig(config_dir)
>>> xc['XMLRIGDEVICE']
'COM1'
'''
return self.settings[str(key).lower()] # case insensitive
[docs] def __setitem__(self, key, value):
'''Update a setting value.
:param key: The XML tag name
:type key: str
:param value: description
:type value: str, bool, int, or float.
.. note:: Settings aren't written until save() is called!
.. note:: Strings will be escaped because this is an XML file. '<', '>', etc. will be replaced as required.
.. todo:: boolean values will be encoded as '1' or '0'
:Example:
>>> import pyfldigi
>>> xc = pyfldigi.XmlConfig(config_dir)
>>> xc['XMLRIGDEVICE']
'COM1'
>>> xc['XMLRIGDEVICE'] = 'COM5'
>>> xc['XMLRIGDEVICE']
'COM5'
>>> xc.save() # Settings aren't written until save() is called!
'''
if isinstance(value, str):
value = escape(value)
elif isinstance(value, bool):
value = str(int(value))
elif isinstance(value, (int, float)):
value = str(value)
else:
raise TypeError('Types supported are bool, str, float, or int. If you are purposely setting another type, please cast it to one of these.')
try:
self.settings[key] = value
self.dirty = True
except KeyError:
raise KeyError('Not a valid configuration item')
# #####################################################################################
# Highly used items have their own methods below
[docs] def set_serial_port(self, comport):
'''Sets the serial port device in the config.
:param comport: The com port, e.g. 'COM1' or '/dev/ttys1'
:type comport: str
.. note::
There are two COM ports in the config (XMLRIGDEVICE and HAMRIGDEVICE).
One for XML-RPC (flrig) and one for HamRig.
Set both because they're mutually exclusive.
'''
self['XMLRIGDEVICE'] = str(comport)
self['HAMRIGDEVICE'] = str(comport)
[docs] def set_sound_card(self):
'''
PORTINDEVICE is None if not defined. Or a str (e.g. 'Microphone (USB Audio Codec)' if defined)
PORTININDEX is -1 if not defined. Or the current index of the sound card (9)
PORTOUTDEVICE is None if not defined. Or a str (e.g. 'Speakers (High Definition Audio to Speakers (USB Audio Codec)')
AUDIOIO ?? changed from 3 to 1
PORTOUTINDEX -1 or a valid index.
'''
pass
[docs]class XmlMonitor(object):
'''A useful tool for figuring out which XML parameter is correlated with a particular
setting in the FLDIGI GUI. It will monitor for changes to the config file, and print
any changes to the console as they happen. Make sure to press 'save settings' after
making your setting! The actual monitoring happens in a thread, so it is non-blocking.
:param config_dir: The directory in which fldigi reads and writes its config files. e.g. 'C:/Users/njansen/fldigi.files/'
:type config_dir: str (path to folder)
:param start: If True, the monitoring will start immediately. if False, start() must be called.
:type start: bool
.. note::
All changed settings are logged by default using the Python logger framework, to the
configuration directory, as 'XmlMonitor.log'
.. note:: Press Ctrl-C to stop, or set a timeout before running.
:Example:
>>> import pyfldigi
>>> xc = pyfldigi.XmlMonitor(config_dir)
2017-02-05 16:36:29,129 : XmlMonitor : Monitoring C:/Users/jeff/fldigi.files/fldigi_def.xml...
2017-02-05 16:36:47,252 : XmlMonitor : MYANTENNA changed from 'dipole' to 'yagi'
2017-02-05 16:36:51,279 : XmlMonitor : MYANTENNA changed from 'yagi' to 'magloop'
'''
def __init__(self, config_dir, start=True):
self.config_dir = config_dir
self.location = os.path.join(config_dir, 'fldigi_def.xml')
self.mtime = os.path.getmtime(self.location) # File modification time
# Load the XML and get the settings
self.settings = XmlConfig(config_dir).settings
# Setup the logger
self.logger = logging.getLogger('pyfldigi.config.XmlMonitor')
self.logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s : XmlMonitor : %(message)s')
self.fh = logging.FileHandler(os.path.join(config_dir, 'XmlMonitor.log'))
self.sh = logging.StreamHandler()
self.fh.setLevel(logging.INFO)
self.sh.setLevel(logging.INFO)
self.fh.setFormatter(formatter)
self.sh.setFormatter(formatter)
self.logger.addHandler(self.fh)
self.logger.addHandler(self.sh)
self.logger.info('Monitoring {}...'.format(self.location))
# Setup the thread
self._timer = None
self.interval = 2
self.is_running = False
if start is True:
self.start()
# register SIGINT signal
signal.signal(signal.SIGINT, self.stop)
[docs] def start(self):
'''Start monitoring the XML. This launches a monitoring thread, therefore start() is non-blocking.'''
if not self.is_running:
self._timer = threading.Timer(self.interval, self._threadworker)
self._timer.start()
self.is_running = True
[docs] def stop(self):
'''Stop monitoring the XML. Stops the thread. Can be restarted with start().'''
self._timer.cancel()
self.is_running = False
def _threadworker(self):
self.is_running = False
self.start()
mtime = os.path.getmtime(self.location)
if mtime != self.mtime:
# The file has been modified. Parse the new settings
new = XmlConfig(self.config_dir).settings
old = self.settings
for key, value in old.items():
try:
if new[key] != value:
self.logger.info('{} changed from {} to {}'.format(key.upper(), value, new[key]))
except KeyError:
pass # TBD
self.settings = new
self.mtime = mtime