-
Notifications
You must be signed in to change notification settings - Fork 9
/
xmlconfig.py
265 lines (213 loc) · 9.12 KB
/
xmlconfig.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
'''
.. 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
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()
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 {!r}'.format(self.root.tag))
for child in self.root:
self.settings[str(child.tag).lower()] = child.text
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
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 = ''.join(
'{} : {}\n'.format(key, value)
for key, value in self.settings.items()
)
return s
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
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
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)
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
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 %s...', 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)
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
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('%s changed from %r to %r', key.upper(), value, new[key])
except KeyError:
pass # TBD
self.settings = new
self.mtime = mtime