1
|
# Copyright 2008 Dan Smith <dsmith@danplanet.com>
|
2
|
#
|
3
|
# This program is free software: you can redistribute it and/or modify
|
4
|
# it under the terms of the GNU General Public License as published by
|
5
|
# the Free Software Foundation, either version 3 of the License, or
|
6
|
# (at your option) any later version.
|
7
|
#
|
8
|
# This program is distributed in the hope that it will be useful,
|
9
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
# GNU General Public License for more details.
|
12
|
#
|
13
|
# You should have received a copy of the GNU General Public License
|
14
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
|
16
|
import os
|
17
|
import sys
|
18
|
import glob
|
19
|
import re
|
20
|
import logging
|
21
|
from subprocess import Popen
|
22
|
|
23
|
import six
|
24
|
|
25
|
LOG = logging.getLogger(__name__)
|
26
|
|
27
|
|
28
|
def win32_comports_bruteforce():
|
29
|
import win32file
|
30
|
import win32con
|
31
|
|
32
|
ports = []
|
33
|
for i in range(1, 257):
|
34
|
portname = "\\\\.\\COM%i" % i
|
35
|
try:
|
36
|
mode = win32con.GENERIC_READ | win32con.GENERIC_WRITE
|
37
|
port = \
|
38
|
win32file.CreateFile(portname,
|
39
|
mode,
|
40
|
win32con.FILE_SHARE_READ,
|
41
|
None,
|
42
|
win32con.OPEN_EXISTING,
|
43
|
0,
|
44
|
None)
|
45
|
if portname.startswith("\\"):
|
46
|
portname = portname[4:]
|
47
|
ports.append((portname, "Unknown", "Serial"))
|
48
|
win32file.CloseHandle(port)
|
49
|
port = None
|
50
|
except Exception as e:
|
51
|
pass
|
52
|
|
53
|
return ports
|
54
|
|
55
|
|
56
|
try:
|
57
|
from serial.tools.list_ports import comports
|
58
|
except:
|
59
|
comports = win32_comports_bruteforce
|
60
|
|
61
|
|
62
|
def _find_me():
|
63
|
return sys.modules["chirp.platform"].__file__
|
64
|
|
65
|
|
66
|
def natural_sorted(l):
|
67
|
def convert(text):
|
68
|
return int(text) if text.isdigit() else text.lower()
|
69
|
|
70
|
def natural_key(key):
|
71
|
return [convert(c) for c in re.split('([0-9]+)', key)]
|
72
|
|
73
|
return sorted(l, key=natural_key)
|
74
|
|
75
|
|
76
|
class Platform:
|
77
|
"""Base class for platform-specific functions"""
|
78
|
|
79
|
def __init__(self, basepath):
|
80
|
self._base = basepath
|
81
|
self._last_dir = self.default_dir()
|
82
|
|
83
|
def get_last_dir(self):
|
84
|
"""Return the last directory used"""
|
85
|
return self._last_dir
|
86
|
|
87
|
def set_last_dir(self, last_dir):
|
88
|
"""Set the last directory used"""
|
89
|
self._last_dir = last_dir
|
90
|
|
91
|
def config_dir(self):
|
92
|
"""Return the preferred configuration file directory"""
|
93
|
return self._base
|
94
|
|
95
|
def log_dir(self):
|
96
|
"""Return the preferred log file directory"""
|
97
|
logdir = os.path.join(self.config_dir(), "logs")
|
98
|
if not os.path.isdir(logdir):
|
99
|
os.mkdir(logdir)
|
100
|
|
101
|
return logdir
|
102
|
|
103
|
def filter_filename(self, filename):
|
104
|
"""Filter @filename for platform-forbidden characters"""
|
105
|
return filename
|
106
|
|
107
|
def log_file(self, filename):
|
108
|
"""Return the full path to a log file with @filename"""
|
109
|
filename = self.filter_filename(filename + ".txt").replace(" ", "_")
|
110
|
return os.path.join(self.log_dir(), filename)
|
111
|
|
112
|
def config_file(self, filename):
|
113
|
"""Return the full path to a config file with @filename"""
|
114
|
return os.path.join(self.config_dir(),
|
115
|
self.filter_filename(filename))
|
116
|
|
117
|
def open_text_file(self, path):
|
118
|
"""Spawn the necessary program to open a text file at @path"""
|
119
|
raise NotImplementedError("The base class can't do that")
|
120
|
|
121
|
def open_html_file(self, path):
|
122
|
"""Spawn the necessary program to open an HTML file at @path"""
|
123
|
raise NotImplementedError("The base class can't do that")
|
124
|
|
125
|
def list_serial_ports(self):
|
126
|
"""Return a list of valid serial ports"""
|
127
|
return []
|
128
|
|
129
|
def default_dir(self):
|
130
|
"""Return the default directory for this platform"""
|
131
|
return "."
|
132
|
|
133
|
def gui_open_file(self, start_dir=None, types=[]):
|
134
|
"""Prompt the user to pick a file to open"""
|
135
|
import gtk
|
136
|
|
137
|
if not start_dir:
|
138
|
start_dir = self._last_dir
|
139
|
|
140
|
dlg = gtk.FileChooserDialog("Select a file to open",
|
141
|
None,
|
142
|
gtk.FILE_CHOOSER_ACTION_OPEN,
|
143
|
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
|
144
|
gtk.STOCK_OPEN, gtk.RESPONSE_OK))
|
145
|
if start_dir and os.path.isdir(start_dir):
|
146
|
dlg.set_current_folder(start_dir)
|
147
|
|
148
|
for desc, spec in types:
|
149
|
ff = gtk.FileFilter()
|
150
|
ff.set_name(desc)
|
151
|
ff.add_pattern(spec)
|
152
|
dlg.add_filter(ff)
|
153
|
|
154
|
res = dlg.run()
|
155
|
fname = dlg.get_filename()
|
156
|
dlg.destroy()
|
157
|
|
158
|
if res == gtk.RESPONSE_OK:
|
159
|
self._last_dir = os.path.dirname(fname)
|
160
|
return fname
|
161
|
else:
|
162
|
return None
|
163
|
|
164
|
def gui_save_file(self, start_dir=None, default_name=None, types=[]):
|
165
|
"""Prompt the user to pick a filename to save"""
|
166
|
import gtk
|
167
|
|
168
|
if not start_dir:
|
169
|
start_dir = self._last_dir
|
170
|
|
171
|
dlg = gtk.FileChooserDialog("Save file as",
|
172
|
None,
|
173
|
gtk.FILE_CHOOSER_ACTION_SAVE,
|
174
|
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
|
175
|
gtk.STOCK_SAVE, gtk.RESPONSE_OK))
|
176
|
if start_dir and os.path.isdir(start_dir):
|
177
|
dlg.set_current_folder(start_dir)
|
178
|
|
179
|
if default_name:
|
180
|
dlg.set_current_name(default_name)
|
181
|
|
182
|
extensions = {}
|
183
|
for desc, ext in types:
|
184
|
ff = gtk.FileFilter()
|
185
|
ff.set_name(desc)
|
186
|
ff.add_pattern("*.%s" % ext)
|
187
|
extensions[desc] = ext
|
188
|
dlg.add_filter(ff)
|
189
|
|
190
|
res = dlg.run()
|
191
|
|
192
|
fname = dlg.get_filename()
|
193
|
ext = extensions[dlg.get_filter().get_name()]
|
194
|
if fname and not fname.endswith(".%s" % ext):
|
195
|
fname = "%s.%s" % (fname, ext)
|
196
|
|
197
|
dlg.destroy()
|
198
|
|
199
|
if res == gtk.RESPONSE_OK:
|
200
|
self._last_dir = os.path.dirname(fname)
|
201
|
return fname
|
202
|
else:
|
203
|
return None
|
204
|
|
205
|
def gui_select_dir(self, start_dir=None):
|
206
|
"""Prompt the user to pick a directory"""
|
207
|
import gtk
|
208
|
|
209
|
if not start_dir:
|
210
|
start_dir = self._last_dir
|
211
|
|
212
|
dlg = gtk.FileChooserDialog("Choose folder",
|
213
|
None,
|
214
|
gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
|
215
|
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
|
216
|
gtk.STOCK_SAVE, gtk.RESPONSE_OK))
|
217
|
if start_dir and os.path.isdir(start_dir):
|
218
|
dlg.set_current_folder(start_dir)
|
219
|
|
220
|
res = dlg.run()
|
221
|
fname = dlg.get_filename()
|
222
|
dlg.destroy()
|
223
|
|
224
|
if res == gtk.RESPONSE_OK and os.path.isdir(fname):
|
225
|
self._last_dir = fname
|
226
|
return fname
|
227
|
else:
|
228
|
return None
|
229
|
|
230
|
def os_version_string(self):
|
231
|
"""Return a string that describes the OS/platform version"""
|
232
|
return "Unknown Operating System"
|
233
|
|
234
|
def executable_path(self):
|
235
|
"""Return a full path to the program executable"""
|
236
|
def we_are_frozen():
|
237
|
return hasattr(sys, "frozen")
|
238
|
|
239
|
if we_are_frozen():
|
240
|
# Win32, find the directory of the executable
|
241
|
return os.path.dirname(six.text_type(sys.executable,
|
242
|
sys.getfilesystemencoding()))
|
243
|
else:
|
244
|
# UNIX: Find the parent directory of this module
|
245
|
return os.path.dirname(os.path.abspath(os.path.join(_find_me(),
|
246
|
"..")))
|
247
|
|
248
|
def find_resource(self, filename):
|
249
|
"""Searches for files installed to a share/ prefix."""
|
250
|
execpath = self.executable_path()
|
251
|
share_candidates = [
|
252
|
os.path.join(execpath, "share"),
|
253
|
os.path.join(sys.prefix, "share"),
|
254
|
"/usr/local/share",
|
255
|
"/usr/share",
|
256
|
]
|
257
|
pkgshare_candidates = [os.path.join(i, "chirp")
|
258
|
for i in share_candidates]
|
259
|
search_paths = [execpath] + pkgshare_candidates + share_candidates
|
260
|
for path in search_paths:
|
261
|
candidate = os.path.join(path, filename)
|
262
|
if os.path.exists(candidate):
|
263
|
return candidate
|
264
|
return ""
|
265
|
|
266
|
|
267
|
def _unix_editor():
|
268
|
macos_textedit = "/Applications/TextEdit.app/Contents/MacOS/TextEdit"
|
269
|
|
270
|
if os.path.exists(macos_textedit):
|
271
|
return macos_textedit
|
272
|
else:
|
273
|
return "gedit"
|
274
|
|
275
|
|
276
|
class UnixPlatform(Platform):
|
277
|
"""A platform module suitable for UNIX systems"""
|
278
|
def __init__(self, basepath):
|
279
|
if not basepath:
|
280
|
basepath = os.path.abspath(os.path.join(self.default_dir(),
|
281
|
".chirp"))
|
282
|
|
283
|
if not os.path.isdir(basepath):
|
284
|
os.mkdir(basepath)
|
285
|
|
286
|
Platform.__init__(self, basepath)
|
287
|
|
288
|
# This is a hack that needs to be properly fixed by importing the
|
289
|
# latest changes to this module from d-rats. In the interest of
|
290
|
# time, however, I'll throw it here
|
291
|
if sys.platform == "darwin":
|
292
|
if "DISPLAY" not in os.environ:
|
293
|
LOG.info("Forcing DISPLAY for MacOS")
|
294
|
os.environ["DISPLAY"] = ":0"
|
295
|
|
296
|
os.environ["PANGO_RC_FILE"] = "../Resources/etc/pango/pangorc"
|
297
|
|
298
|
def default_dir(self):
|
299
|
return os.path.abspath(os.getenv("HOME"))
|
300
|
|
301
|
def filter_filename(self, filename):
|
302
|
return filename.replace("/", "")
|
303
|
|
304
|
def open_text_file(self, path):
|
305
|
pid1 = os.fork()
|
306
|
if pid1 == 0:
|
307
|
pid2 = os.fork()
|
308
|
if pid2 == 0:
|
309
|
editor = _unix_editor()
|
310
|
LOG.debug("calling `%s %s'" % (editor, path))
|
311
|
os.execlp(editor, editor, path)
|
312
|
else:
|
313
|
sys.exit(0)
|
314
|
else:
|
315
|
os.waitpid(pid1, 0)
|
316
|
LOG.debug("Exec child exited")
|
317
|
|
318
|
def open_html_file(self, path):
|
319
|
os.system("firefox '%s'" % path)
|
320
|
|
321
|
def list_serial_ports(self):
|
322
|
ports = ["/dev/ttyS*",
|
323
|
"/dev/ttyUSB*",
|
324
|
"/dev/ttyAMA*",
|
325
|
"/dev/ttyACM*",
|
326
|
"/dev/cu.*",
|
327
|
"/dev/cuaU*",
|
328
|
"/dev/cua0*",
|
329
|
"/dev/term/*",
|
330
|
"/dev/tty.KeySerial*",
|
331
|
"/dev/rfcom*"]
|
332
|
return natural_sorted(sum([glob.glob(x) for x in ports], []))
|
333
|
|
334
|
def os_version_string(self):
|
335
|
try:
|
336
|
issue = file("/etc/issue.net", "r")
|
337
|
ver = issue.read().strip().replace("\r", "").replace("\n", "")[:64]
|
338
|
issue.close()
|
339
|
ver = "%s - %s" % (os.uname()[0], ver)
|
340
|
except Exception:
|
341
|
ver = " ".join(os.uname())
|
342
|
|
343
|
return ver
|
344
|
|
345
|
|
346
|
class Win32Platform(Platform):
|
347
|
"""A platform module suitable for Windows systems"""
|
348
|
def __init__(self, basepath=None):
|
349
|
if not basepath:
|
350
|
appdata = os.getenv("APPDATA")
|
351
|
if not appdata:
|
352
|
appdata = "C:\\"
|
353
|
basepath = os.path.abspath(os.path.join(appdata, "CHIRP"))
|
354
|
|
355
|
if not os.path.isdir(basepath):
|
356
|
os.mkdir(basepath)
|
357
|
|
358
|
Platform.__init__(self, basepath)
|
359
|
|
360
|
def default_dir(self):
|
361
|
return os.path.abspath(os.path.join(os.getenv("USERPROFILE"),
|
362
|
"Desktop"))
|
363
|
|
364
|
def filter_filename(self, filename):
|
365
|
for char in "/\\:*?\"<>|":
|
366
|
filename = filename.replace(char, "")
|
367
|
|
368
|
return filename
|
369
|
|
370
|
def open_text_file(self, path):
|
371
|
Popen(["notepad", path])
|
372
|
return
|
373
|
|
374
|
def open_html_file(self, path):
|
375
|
os.system("explorer %s" % path)
|
376
|
|
377
|
def list_serial_ports(self):
|
378
|
try:
|
379
|
ports = list(comports())
|
380
|
except Exception as e:
|
381
|
if comports != win32_comports_bruteforce:
|
382
|
LOG.error("Failed to detect win32 serial ports: %s" % e)
|
383
|
ports = win32_comports_bruteforce()
|
384
|
return natural_sorted([port for port, name, url in ports])
|
385
|
|
386
|
def gui_open_file(self, start_dir=None, types=[]):
|
387
|
import win32gui
|
388
|
|
389
|
typestrs = ""
|
390
|
for desc, spec in types:
|
391
|
typestrs += "%s\0%s\0" % (desc, spec)
|
392
|
if not typestrs:
|
393
|
typestrs = None
|
394
|
|
395
|
try:
|
396
|
fname, _, _ = win32gui.GetOpenFileNameW(Filter=typestrs)
|
397
|
except Exception as e:
|
398
|
LOG.error("Failed to get filename: %s" % e)
|
399
|
return None
|
400
|
|
401
|
return str(fname)
|
402
|
|
403
|
def gui_save_file(self, start_dir=None, default_name=None, types=[]):
|
404
|
import win32gui
|
405
|
import win32api
|
406
|
|
407
|
(pform, _, _, _, _) = win32api.GetVersionEx()
|
408
|
|
409
|
typestrs = ""
|
410
|
custom = "%s\0*.%s\0" % (types[0][0], types[0][1])
|
411
|
for desc, ext in types[1:]:
|
412
|
typestrs += "%s\0%s\0" % (desc, "*.%s" % ext)
|
413
|
|
414
|
if pform > 5:
|
415
|
typestrs = "%s\0%s\0" % (types[0][0], "*.%s" % types[0][1]) + \
|
416
|
typestrs
|
417
|
|
418
|
if not typestrs:
|
419
|
typestrs = custom
|
420
|
custom = None
|
421
|
|
422
|
def_ext = "*.%s" % types[0][1]
|
423
|
try:
|
424
|
fname, _, _ = win32gui.GetSaveFileNameW(File=default_name,
|
425
|
CustomFilter=custom,
|
426
|
DefExt=def_ext,
|
427
|
Filter=typestrs)
|
428
|
except Exception as e:
|
429
|
LOG.error("Failed to get filename: %s" % e)
|
430
|
return None
|
431
|
|
432
|
return str(fname)
|
433
|
|
434
|
def gui_select_dir(self, start_dir=None):
|
435
|
from win32com.shell import shell
|
436
|
|
437
|
try:
|
438
|
pidl, _, _ = shell.SHBrowseForFolder()
|
439
|
fname = shell.SHGetPathFromIDList(pidl)
|
440
|
except Exception as e:
|
441
|
LOG.error("Failed to get directory: %s" % e)
|
442
|
return None
|
443
|
|
444
|
return str(fname)
|
445
|
|
446
|
def os_version_string(self):
|
447
|
import win32api
|
448
|
|
449
|
vers = {4: "Win2k",
|
450
|
5: "WinXP",
|
451
|
6: "WinVista/7",
|
452
|
}
|
453
|
|
454
|
(pform, sub, build, _, _) = win32api.GetVersionEx()
|
455
|
|
456
|
return vers.get(pform,
|
457
|
"Win32 (Unknown %i.%i:%i)" % (pform, sub, build))
|
458
|
|
459
|
|
460
|
def _get_platform(basepath):
|
461
|
if os.name == "nt":
|
462
|
return Win32Platform(basepath)
|
463
|
else:
|
464
|
return UnixPlatform(basepath)
|
465
|
|
466
|
PLATFORM = None
|
467
|
|
468
|
|
469
|
def get_platform(basepath=None):
|
470
|
"""Return the platform singleton"""
|
471
|
global PLATFORM
|
472
|
|
473
|
if not PLATFORM:
|
474
|
PLATFORM = _get_platform(basepath)
|
475
|
|
476
|
return PLATFORM
|
477
|
|
478
|
|
479
|
def _do_test():
|
480
|
__pform = get_platform()
|
481
|
|
482
|
print("Config dir: %s" % __pform.config_dir())
|
483
|
print("Default dir: %s" % __pform.default_dir())
|
484
|
print("Log file (foo): %s" % __pform.log_file("foo"))
|
485
|
print("Serial ports: %s" % __pform.list_serial_ports())
|
486
|
print("OS Version: %s" % __pform.os_version_string())
|
487
|
# __pform.open_text_file("d-rats.py")
|
488
|
|
489
|
# print "Open file: %s" % __pform.gui_open_file()
|
490
|
# print "Save file: %s" % __pform.gui_save_file(default_name="Foo.txt")
|
491
|
print("Open folder: %s" % __pform.gui_select_dir("/tmp"))
|
492
|
|
493
|
if __name__ == "__main__":
|
494
|
_do_test()
|