platform.py

Add rfcomm for bluetooth radios - Angus Ainslie, 06/21/2020 09:32 am

Download (15.1 kB)

 
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()