diff --git a/Arduino.sublime-syntax b/Arduino.sublime-syntax new file mode 100644 index 0000000..b8e76fc --- /dev/null +++ b/Arduino.sublime-syntax @@ -0,0 +1,22 @@ +%YAML 1.2 +--- +# http://www.sublimetext.com/docs/3/syntax.html +name: Arduino +file_extensions: [ino, pde] +scope: source.arduino + +contexts: + main: + - match: '' + push: Packages/C++/C++.sublime-syntax + with_prototype: + - match: \b(HIGH|LOW|INPUT|OUTPUT|INPUT_PULLUP|LED_BUILTIN)\b + scope: constant.language.arduino + - match: \b(boolean|word|String|string|array)\b + scope: storage.type.arduino + - match: PROGRAM + scope: storage.modifier.arduino + - match: \b(Serial|Stream|Keyboard|Mouse)\b + scope: entity.name.class.arduino + - match: \b(pinMode|digitalWrite|digitalRead|analogReference|analogRead|analogWrite|analogReadResolution|analogWriteResolution|tone|noTone|shiftOut|shiftIn|pulseIn|millis|micros|delay|delayMicroseconds|min|max|constrain|map|pow|sqrt|sin|cos|tan|isAlphaNumeric|isAlpha|isAscii|isWhitespace|isControl|isDigit|isGraph|isLowerCase|isPrintable|isPunct|isSpace|isUpperCase|isHexadecimalDigit|randomSeed|random|lowByte|highByte|bitRead|bitWrite|bitSet|bitClear|bit|attachInterrupt|detachInterrupt|interrupts|noInterrupts)\b + scope: entity.name.function.arduino diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94ffc6a --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Stino +A Sublime Text Plugin for Arduino + +Copyright (C) 2012-2017 Sen . + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/Main.sublime-menu b/Main.sublime-menu new file mode 100644 index 0000000..c1a8809 --- /dev/null +++ b/Main.sublime-menu @@ -0,0 +1,421 @@ +[ + { + "caption": "Preferences", + "children": [ + { + "caption": "Package Settings", + "children": [ + { + "caption": "Stino (Arduino Like IDE)", + "children": [ + { + "args": { + "file": "${packages}/User/Stino/app_dir.stino-settings" + }, + "caption": "App Dir Setting", + "command": "open_file" + }, + { + "caption": "-" + }, + { + "args": { + "file": "${packages}/User/Stino/Default (OSX).sublime-keymap", + "platform": "OSX" + }, + "caption": "Key Bindings - User", + "command": "open_file" + }, + { + "args": { + "file": "${packages}/User/Stino/Default (Linux).sublime-keymap", + "platform": "Linux" + }, + "caption": "Key Bindings - User", + "command": "open_file" + }, + { + "args": { + "file": "${packages}/User/Stino/Default (Windows).sublime-keymap", + "platform": "Windows" + }, + "caption": "Key Bindings - User", + "command": "open_file" + }, + { + "caption": "-" + } + ] + } + ], + "id": "package-settings", + "mnemonic": "P" + } + ], + "id": "preferences", + "mnemonic": "n" + }, + { + "caption": "Arduino", + "mnemonic": "A", + "id": "arduino", + "children": + [ + { + "caption": "Open Sketch", + "id": "stino_sketchbook", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_sketchbook", + "command": "stino_refresh_sketchbook" + }, + { + "caption": "Change Location...", + "id": "stino_change_sketchbook_location", + "command": "stino_change_sketchbook_location" + }, + { + "caption": "In New Window", + "id": "stino_open_in_new_win", + "command": "stino_open_in_new_win", + "checkbox": true + }, + {"caption": "-"}, + { + "caption": "New Sketch...", + "id": "stino_new_sketch", + "command": "stino_new_sketch" + }, + {"caption": "-"} + ] + }, + { + "caption": "Show Sketch Folder", + "id": "stino_show_sketch_dir", + "command": "stino_show_sketch_dir" + }, + {"caption": "-"}, + { + "caption": "Open Example", + "id": "stino_examples", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_examples", + "command": "stino_refresh_examples" + }, + {"caption": "-"} + ] + }, + { + "caption": "Import Library", + "id": "stino_import_library", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_libraries", + "command": "stino_refresh_libraries" + }, + {"caption": "-"} + ] + }, + { + "caption": "Install Library", + "id": "stino_install_library", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_install_library", + "command": "stino_refresh_install_library" + }, + {"caption": "-"} + ] + }, + {"caption": "-"}, + { + "id": "stino_platform_info", + "command": "stino_platform_info" + }, + { + "caption": "Install Platform", + "id": "stino_install_platform", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_install_platform", + "command": "stino_refresh_install_platform" + }, + { + "caption": "Add Package", + "id": "stino_add_package", + "command": "stino_add_package" + }, + { + "caption": "Add Arduino IDE", + "id": "stino_add_ide", + "command": "stino_add_ide" + }, + {"caption": "-"} + ] + }, + { + "caption": "Platform", + "id": "stino_platform", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_platforms", + "command": "stino_refresh_platforms" + }, + {"caption": "-"} + ] + }, + { + "caption": "Version", + "id": "stino_platform_version", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_platform_versions", + "command": "stino_refresh_platform_versions" + }, + { + "caption": "Check Toolchain", + "id": "stino_check_tools", + "command": "stino_check_tools" + }, + {"caption": "-"} + ] + }, + { + "caption": "Open Platform Example", + "id": "stino_platform_examples", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_platform_examples", + "command": "stino_refresh_platform_examples" + }, + {"caption": "-"} + ] + }, + { + "caption": "Import Platform Library", + "id": "stino_import_platform_library", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_platform_libraries", + "command": "stino_refresh_platform_libraries" + }, + {"caption": "-"} + ] + }, + {"caption": "-"}, + { + "id": "stino_board_info", + "command": "stino_board_info" + }, + { + "caption": "Build", + "id": "stino_build", + "command": "stino_build" + }, + { + "caption": "Board", + "id": "stino_board", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_boards", + "command": "stino_refresh_boards" + }, + {"caption": "-"} + ] + }, + { + "caption": "Board Options", + "id": "board_options", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_board_options", + "command": "stino_refresh_board_options" + }, + {"caption": "-"} + ] + }, + { + "caption": "Build Options", + "id": "build_options", + "children": + [ + { + "caption": "Extra Flags", + "id": "stino_set_extra_flag", + "command": "stino_set_extra_flag" + }, + { + "caption": "Save sketch before build", + "id": "stino_save_before_build", + "command": "stino_save_before_build", + "checkbox": true + }, + { + "caption": "Full Build", + "id": "stino_full_build", + "command": "stino_toggle_full_build", + "checkbox": true + }, + { + "caption": "Show Build Output", + "id": "stino_show_build_output", + "command": "stino_show_build_output", + "checkbox": true + }, + { + "caption": "Show Upload Output", + "id": "stino_show_upload_output", + "command": "stino_show_upload_output", + "checkbox": true + }, + { + "caption": "Verify Code after Upload", + "id": "stino_verify_code", + "command": "stino_verify_code", + "checkbox": true + } + ] + }, + { + "caption": "Show Build Folder", + "id": "stino_show_build_dir", + "command": "stino_show_build_dir" + }, + {"caption": "-"}, + { + "id": "stino_serial_info", + "command": "stino_serial_info" + }, + { + "caption": "Upload", + "id": "stino_upload", + "command": "stino_upload" + }, + { + "caption": "Serial Port", + "id": "serial_port", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_serials", + "command": "stino_refresh_serials" + }, + {"caption": "-"} + ] + }, + { + "caption": "Get Port Info", + "id": "stino_get_port_info", + "command": "stino_get_port_info" + }, + {"caption": "-"}, + { + "id": "stino_programmer_info", + "command": "stino_programmer_info" + }, + { + "caption": "Upload by Using Programmer", + "id": "stino_upload_using_programmer", + "command": "stino_upload_using_programmer" + }, + { + "caption": "Programmer", + "id": "stino_programmer", + "children": + [ + { + "caption": "Refresh", + "id": "stino_refresh_programmers", + "command": "stino_refresh_programmers" + }, + {"caption": "-"} + ] + }, + {"caption": "-"}, + { + "caption": "Burn Bootloader", + "id": "stino_burn_bootloader", + "command": "stino_burn_bootloader" + }, + { + "caption": "Auto Format", + "id": "stino_auto_format", + "command": "stino_auto_format" + }, + {"caption": "-"}, + { + "caption": "Show Message Panel", + "id": "stino_show_panel", + "command": "stino_show_panel" + }, + { + "caption": "Language", + "id": "stino_language", + "children": + [ + { + "caption": "Install", + "id": "stino_install_languages", + "children": + [ + {"caption": "-"} + ] + }, + { + "caption": "Refresh", + "id": "stino_refresh_languages", + "command": "stino_refresh_languages" + }, + {"caption": "-"} + ] + }, + { + "caption": "Help", + "id": "stino_help", + "children": + [ + { + "caption": "Arduino Documents", + "id": "stino_documents", + "command": "open_url", + "args": {"url": "https://www.arduino.cc/en/Reference/HomePage"} + }, + { + "caption": "Platform Documents", + "id": "stino_platform_documents", + "command": "stino_open_platform_documents" + }, + { + "caption": "About Stino", + "id": "stino_about", + "command": "stino_about" + } + ] + } + ] + } +] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d20cb03 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Stino2017 + +Stino is a [Sublime Text](http://www.sublimetext.com) plugin that provides an [Arduino](http://arduino.cc)-like environment for editing, compiling and uploading sketches. The plugin was written by @Robot-Will in 2012-2017. If you have any ideas or suggestions, please leave messages at [Issues](https://github.com/Robot-Will/Stino/issues). Thanks. + +### Current State +Stino2017 is a totally new software and it is under development, please be patient. Thanks a lot! + +Currently it works for Arduino Avr Boards and still a lot of work is left to do to complete and test this plugin. I am working on Windows 10 x64 currently, however on other platforms you may encounter errors. You can press ctrl+` to open the SublimeText console and find the error messages. The error messages will help to improve this plugin. + +### Requirements +[Sublime Text](http://www.sublimetext.com) 3.0 (developing under Build 3126) + +![Screenshot](https://github.com/Robot-Will/Stino/blob/Wiki-Images/images/stino_menu01.jpg) + +### Installation + +1. Download the .zip file from [github](https://github.com/Robot-Will/Stino) + +2. Open SublimeText Packages Folder + +3. Unzip the .zip file and copy the unzipped folder to the SublimeText Packages Folder + +![Installation](https://github.com/Robot-Will/Stino/blob/Wiki-Images/images/s004.jpg) + +### How to use +#### 1. Add Package and Library Index files +This software does not need the Arduino IDE and it will download everything from the internet. By default it has [Arduino Package Index File](http://downloads.arduino.cc/packages/package_index.json) and [Arduino Library Index File](http://downloads.arduino.cc/libraries/library_index.json), and you can add your index files into the list. This software will check new index files every 30 minutes by default. Some package index files' link can be found in the [Unofficial list of 3rd party boards support urls](https://github.com/arduino/Arduino/wiki/Unofficial-list-of-3rd-party-boards-support-urls) page. + +![Add Indexes](https://github.com/Robot-Will/Stino/blob/Wiki-Images/images/s002.jpg) + +#### 2. Set the folders +This software uses 3 folders: Arduino App Folder, Sketchbook Folder and Arduino IDE folder. + +Arduino App Folder is the folder where the packages folder (cores and toolchains), build folder, download folder (staging) and setting files are. By default it is ~/Arduino15 (set to {$default}). + +Sketchbook Folder is folder where sketches, examples and libraries are. By default it is [Documents Folder]/Arduino (set to {$default}). You can put examples and libraries into the 'examples' and 'libraries' folders respectively. + +If you want integrate SublimeText for portable use, you can set the above two folders to {$sublime}, and it will use the [Sublime Packages]/User/Stino folder. + +Arduino IDE folder is where is the Arduino IDE is. This software supports the Arduino IDE, but it does not need it. You do not need to provide this folder and can leave this option blank. + +![Folders](https://github.com/Robot-Will/Stino/blob/Wiki-Images/images/s001.jpg) + +Then you can install packages and libraries from the internet and speed up your work by choosing your platform, version and board. + +![Select Board](https://github.com/Robot-Will/Stino/blob/Wiki-Images/images/s003.jpg) + +### License + +Copyright (C) 2012-2017 Sen . + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/StinoCommands.py b/StinoCommands.py new file mode 100644 index 0000000..9b77c3f --- /dev/null +++ b/StinoCommands.py @@ -0,0 +1,961 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Doc.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import os +import sys + +import sublime +import sublime_plugin + + +def get_relative_path(relative_dir): + """.""" + cur_path = os.path.dirname(os.path.realpath(__file__)) + dir_path = os.path.normpath(os.path.join(cur_path, relative_dir)) + return dir_path + + +def add_relative_dir_to_sys_path(relative_dir): + """.""" + dir_path = get_relative_path(relative_dir) + sys.path.append(dir_path) + + +stino = None +add_relative_dir_to_sys_path('libs') +st_version = 2 if sys.version_info < (3,) else 3 + + +def plugin_loaded(): + """.""" + global stino + import stino_runtime as stino + +# if st_version == 2: +# plugin_loaded() + + +############################################# +# Sketch Commands +############################################# +class ViewMonitor(sublime_plugin.EventListener): + """.""" + + def on_close(self, view): + """.""" + if stino.arduino_info['init_done']: + file_path = view.file_name() + if file_path in stino.arduino_info.get('phantoms', {}): + stino.arduino_info['phantoms'].pop(file_path) + + +############################################# +# Sketch Commands +############################################# +class StinoRefreshSketchbookCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.st_menu.update_sketchbook_menu, + stino.arduino_info) + + +class StinoNewSketchCommand(sublime_plugin.WindowCommand): + """New Sketch.""" + + def run(self): + """New Sketch.""" + if stino.arduino_info['init_done']: + caption = stino.translate('Sketch Name:') + self.window.show_input_panel(caption, '', self.on_done, None, None) + + def on_done(self, sketch_name): + """New Sketch.""" + stino.new_sketch(sketch_name, self.window) + + +class StinoChangeSketchbookLocationCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + sketchbook_path = stino.arduino_info['sketchbook_path'] + caption = stino.translate('Sketchbook Path:') + self.window.show_input_panel(caption, sketchbook_path, + self.on_done, None, None) + stino.do_action.put(stino.st_menu.update_sketchbook_menu, + stino.arduino_info) + stino.do_action.put(stino.st_menu.update_example_menu, + stino.arduino_info) + stino.do_action.put(stino.st_menu.update_library_menu, + stino.arduino_info) + + def on_done(self, sketchbook_path): + """New Sketch.""" + sketchbook_path = sketchbook_path.replace('\\', '/') + stino.arduino_info['sketchbook_path'] = sketchbook_path + stino.arduino_info['app_dir_settings'].set('sketchbook_path', + sketchbook_path) + + +class StinoOpenInNewWinCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + state = \ + bool(stino.arduino_info['settings'].get('open_in_new_window')) + stino.arduino_info['settings'].set('open_in_new_window', not state) + + def is_checked(self): + """.""" + state = False + if stino.arduino_info['init_done']: + state = \ + bool(stino.arduino_info['settings'].get('open_in_new_window')) + return state + + +class StinoOpenSketchCommand(sublime_plugin.WindowCommand): + """Open Sketch.""" + + def run(self, sketch_path): + """Open Sketch.""" + if stino.arduino_info['init_done']: + in_new = \ + bool(stino.arduino_info['settings'].get('open_in_new_window')) + win = self.window + if in_new: + sublime.run_command('new_window') + win = sublime.windows()[-1] + stino.open_project(sketch_path, win) + + +class StinoShowSketchDirCommand(sublime_plugin.TextCommand): + """Show Sketch Folder.""" + + def run(self, edit): + """Show Sketch Folder.""" + file_path = self.view.file_name() + if file_path: + dir_path = os.path.dirname(file_path) + url = 'file://' + dir_path + sublime.run_command('open_url', {'url': url}) + + +############################################# +# Example and Library Commands +############################################# +class StinoRefreshExamplesCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.st_menu.update_example_menu, + stino.arduino_info) + + +class StinoOpenExampleCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, example_path): + """.""" + stino.open_project(example_path, self.window) + + +class StinoRefreshLibrariesCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.st_menu.update_library_menu, + stino.arduino_info) + + +class StinoImportLibraryCommand(sublime_plugin.TextCommand): + """Import Library.""" + + def run(self, edit, library_path): + """Import Library.""" + if stino.arduino_info['init_done']: + stino.import_lib(self.view, edit, library_path) + + def is_enabled(self): + """Import Library.""" + state = False + if stino.arduino_info['init_done']: + file_path = self.view.file_name() + if file_path: + if stino.c_file.is_cpp_file(file_path): + state = True + return state + + +class StinoRefreshInstallLibraryCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.init_libs_info) + stino.do_action.put(stino.st_menu.update_install_library_menu, + stino.arduino_info) + + +class StinoInstallLibCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, category, name, version): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.install_library, category, name, version) + + +############################################# +# Package and Platform Commands +############################################# +class StinoPlatformInfoCommand(sublime_plugin.WindowCommand): + """.""" + + def is_enabled(self): + """.""" + return False + + def description(self): + """.""" + caption = '--' + if stino.arduino_info['init_done']: + caption += '[%s] ' % stino.arduino_info['selected'].get('package') + caption += '%s ' % stino.arduino_info['selected'].get('platform') + caption += '%s' % stino.arduino_info['selected'].get('version') + caption += '--' + return caption + + +class StinoRefreshInstallPlatformCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.init_pkgs_info) + stino.do_action.put(stino.st_menu.update_install_platform_menu, + stino.arduino_info) + + +class StinoAddPackageCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + arduino_app_path = stino.arduino_info.get('arduino_app_path') + file_path = \ + os.path.join(arduino_app_path, 'packages.stino-settings') + self.window.open_file(file_path) + + +class StinoAddIdeCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + ide_path = stino.arduino_info['ext_app_path'] + caption = stino.translate('Arduino IDE Path:') + self.window.show_input_panel(caption, ide_path, self.on_done, + None, None) + stino.do_action.put(stino.init_inst_pkgs_info) + stino.do_action.put(stino.st_menu.update_install_platform_menu, + stino.arduino_info) + stino.do_action.put(stino.st_menu.update_example_menu, + stino.arduino_info) + stino.do_action.put(stino.st_menu.update_library_menu, + stino.arduino_info) + + def on_done(self, ide_path): + """New Sketch.""" + ide_path = ide_path.replace('\\', '/') + stino.arduino_info['ext_app_path'] = ide_path + stino.arduino_info['app_dir_settings'].set('additional_app_path', + ide_path) + + +class StinoImportAvrPlatformCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + caption = stino.translate('Arduino IDE Path:') + self.window.show_input_panel(caption, '', self.on_done, None, None) + + def on_done(self, ide_path): + """New Sketch.""" + stino.ide_importer.put(ide_path) + + +class StinoInstallPlatformCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, package_name, platform_name, version): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.install_platform, package_name, + platform_name, version) + + def is_enabled(self, package_name, platform_name, version): + """.""" + state = True + if stino.arduino_info['init_done']: + pkgs_info = stino.arduino_info['installed_packages'] + vers = stino.selected.get_platform_versions(pkgs_info, + package_name, + platform_name) + if version in vers: + state = False + return state + + +class StinoRefreshPlatformsCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.init_inst_pkgs_info) + stino.do_action.put(stino.st_menu.update_platform_menu, + stino.arduino_info) + + +class StinoSelectPlatformCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, package_name, platform_name): + """.""" + if stino.arduino_info['init_done']: + if not self.is_checked(package_name, platform_name): + stino.do_action.put(stino.on_platform_select, package_name, + platform_name) + + def is_checked(self, package_name, platform_name): + """.""" + state = False + if stino.arduino_info['init_done']: + c1 = stino.arduino_info['selected'].get('package') == package_name + c2 = \ + stino.arduino_info['selected'].get('platform') == platform_name + state = c1 and c2 + return state + + +class StinoRefreshPlatformVersionsCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.init_inst_pkgs_info) + stino.do_action.put(stino.st_menu.update_version_menu, + stino.arduino_info) + + +class StinoCheckToolsCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.check_platform_dep) + + +class StinoSelectVersionCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, version): + """.""" + if stino.arduino_info['init_done']: + if not self.is_checked(version): + stino.do_action.put(stino.on_version_select, version) + + def is_checked(self, version): + """.""" + state = False + if stino.arduino_info['init_done']: + state = stino.arduino_info['selected'].get('version') == version + return state + + +class StinoRefreshPlatformExamplesCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.init_inst_pkgs_info) + stino.do_action.put(stino.st_menu.update_platform_example_menu, + stino.arduino_info) + + +class StinoRefreshPlatformLibrariesCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.init_inst_pkgs_info) + stino.do_action.put(stino.st_menu.update_platform_library_menu, + stino.arduino_info) + + +############################################# +# Board and Build Commands +############################################# +class StinoBoardInfoCommand(sublime_plugin.WindowCommand): + """.""" + + def is_enabled(self): + """.""" + return False + + def description(self): + """.""" + caption = '----' + if stino.arduino_info['init_done']: + caption = '--%s--' % stino.arduino_info['selected'].get('board') + return caption + + +class StinoBuildCommand(sublime_plugin.TextCommand): + """.""" + + def run(self, edit): + """.""" + if stino.arduino_info['init_done']: + file_path = self.view.file_name() + dir_path = os.path.dirname(file_path) + build_info = {'path': dir_path} + stino.sketch_builder.put(build_info) + + def is_enabled(self): + """.""" + state = False + if stino.arduino_info['init_done']: + file_path = self.view.file_name() + if file_path: + if stino.c_file.is_cpp_file(file_path): + info = \ + stino.selected.get_sel_board_info(stino.arduino_info) + if info: + state = True + return state + + +class StinoRefreshBoardsCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.init_inst_pkgs_info) + stino.do_action.put(stino.init_boards_info) + stino.do_action.put(stino.st_menu.update_board_menu, + stino.arduino_info) + + +class StinoSelectBoardCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, board_name): + """.""" + if stino.arduino_info['init_done']: + if not self.is_checked(board_name): + stino.do_action.put(stino.on_board_select, board_name) + + def is_checked(self, board_name): + """.""" + state = False + if stino.arduino_info['init_done']: + state = stino.arduino_info['selected'].get('board') == board_name + return state + + +class StinoRefreshBoardOptionsCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.init_inst_pkgs_info) + stino.do_action.put(stino.init_boards_info) + stino.do_action.put(stino.st_menu.update_board_options_menu, + stino.arduino_info) + + +class StinoSelectBoardOptionCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, option, value): + """.""" + if stino.arduino_info['init_done']: + if not self.is_checked(option, value): + stino.on_board_option_select(option, value) + + def is_checked(self, option, value): + """.""" + state = False + if stino.arduino_info['init_done']: + key = 'option_%s' % option + state = stino.arduino_info['selected'].get(key) == value + return state + + +class StinoSetExtraFlagCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + arduino_app_path = stino.arduino_info.get('arduino_app_path') + file_path = os.path.join(arduino_app_path, 'config.stino-settings') + self.window.open_file(file_path) + + +class StinoSaveBeforeBuildCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + state = \ + bool(stino.arduino_info['settings'].get('save_before_build')) + stino.arduino_info['settings'].set('save_before_build', not state) + + def is_checked(self): + """.""" + state = False + if stino.arduino_info['init_done']: + state = \ + bool(stino.arduino_info['settings'].get('save_before_build')) + return state + + +class StinoToggleFullBuildCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + state = bool(stino.arduino_info['settings'].get('full_build')) + stino.arduino_info['settings'].set('full_build', not state) + + def is_checked(self): + """.""" + state = False + if stino.arduino_info['init_done']: + state = bool(stino.arduino_info['settings'].get('full_build')) + return state + + +class StinoShowBuildOutputCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + state = bool(stino.arduino_info['settings'].get('verbose_build')) + stino.arduino_info['settings'].set('verbose_build', not state) + + def is_checked(self): + """.""" + state = False + if stino.arduino_info['init_done']: + state = bool(stino.arduino_info['settings'].get('verbose_build')) + return state + + +class StinoShowUploadOutputCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + state = bool(stino.arduino_info['settings'].get('verbose_upload')) + stino.arduino_info['settings'].set('verbose_upload', not state) + + def is_checked(self): + """.""" + state = False + if stino.arduino_info['init_done']: + state = bool(stino.arduino_info['settings'].get('verbose_upload')) + return state + + +class StinoVerifyCodeCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + state = bool(stino.arduino_info['settings'].get('verify_code')) + stino.arduino_info['settings'].set('verify_code', not state) + + def is_checked(self): + """.""" + state = False + if stino.arduino_info['init_done']: + state = bool(stino.arduino_info['settings'].get('verify_code')) + return state + + +class StinoShowBuildDirCommand(sublime_plugin.TextCommand): + """Show Sketch Folder.""" + + def run(self, edit): + """Show Sketch Folder.""" + if stino.arduino_info['init_done']: + file_path = self.view.file_name() + if file_path: + prj_path = os.path.dirname(file_path) + prj_name = os.path.basename(prj_path) + arduino_app_path = stino.arduino_info['arduino_app_path'] + build_path = os.path.join(arduino_app_path, 'build') + prj_build_path = os.path.join(build_path, prj_name) + if os.path.isdir(prj_build_path): + url = 'file://' + prj_build_path + sublime.run_command('open_url', {'url': url}) + + def is_enabled(self): + """.""" + state = False + if stino.arduino_info['init_done']: + file_path = self.view.file_name() + if file_path: + if stino.c_file.is_cpp_file(file_path): + state = True + return state + + +############################################# +# Serial and Upload Commands +############################################# +class StinoSerialInfoCommand(sublime_plugin.WindowCommand): + """.""" + + def is_enabled(self): + """.""" + return False + + def description(self): + """.""" + caption = '----' + if stino.arduino_info['init_done']: + key = 'serial_port' + caption = '--%s--' % stino.arduino_info['selected'].get(key) + return caption + + +class StinoUploadCommand(sublime_plugin.TextCommand): + """.""" + + def run(self, edit): + """.""" + if stino.arduino_info['init_done']: + file_path = self.view.file_name() + dir_path = os.path.dirname(file_path) + build_info = {'path': dir_path} + build_info['upload_mode'] = 'upload' + stino.sketch_builder.put(build_info) + + def is_enabled(self): + """.""" + state = False + if stino.arduino_info['init_done']: + file_path = self.view.file_name() + sel_serial = stino.arduino_info['selected'].get('serial_port') + if sel_serial and file_path and \ + stino.c_file.is_cpp_file(file_path): + info = stino.selected.get_sel_board_info(stino.arduino_info) + if info: + state = True + return state + + +class StinoRefreshSerialsCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.st_menu.update_serial_menu, + stino.arduino_info) + + +class StinoSelectSerialCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, serial_port): + """.""" + if stino.arduino_info['init_done']: + if not self.is_checked(serial_port): + stino.on_serial_select(serial_port) + + def is_checked(self, serial_port): + """.""" + state = False + if stino.arduino_info['init_done']: + key = 'serial_port' + state = stino.arduino_info['selected'].get(key) == serial_port + return state + + +class StinoGetPortInfoCommand(sublime_plugin.TextCommand): + """.""" + + def run(self, edit): + """.""" + if stino.arduino_info['init_done']: + serials_info = stino.serial_port.get_serials_info() + sel_serial = stino.arduino_info['selected'].get('serial_port') + if sel_serial: + info = serials_info.get(sel_serial, {}) + if info: + port = info.get('port') + desc = info.get('description') + hwid = info.get('hwid') + stino.message_queue.put(port) + stino.message_queue.put(desc) + stino.message_queue.put(hwid) + + board_info = \ + stino.selected.get_sel_board_info(stino.arduino_info) + board_name = board_info.get('name') + vid = board_info.get('build.vid', 'None') + pid = board_info.get('build.pid', 'None') + stino.message_queue.put(board_name) + stino.message_queue.put('VID:PID=%s:%s' % (vid, pid)) + + def is_enabled(self): + """.""" + state = False + if stino.arduino_info['init_done']: + sel_serial = stino.arduino_info['selected'].get('serial_port') + if sel_serial: + state = True + return state + + +############################################# +# Programmer Commands +############################################# +class StinoProgrammerInfoCommand(sublime_plugin.WindowCommand): + """.""" + + def is_enabled(self): + """.""" + return False + + def description(self): + """.""" + caption = '----' + if stino.arduino_info['init_done']: + key = 'programmer' + caption = '--%s--' % stino.arduino_info['selected'].get(key) + return caption + + +class StinoUploadUsingProgrammerCommand(sublime_plugin.TextCommand): + """Upload Using Programmer.""" + + def run(self, edit): + """Upload Using Programmer.""" + if stino.arduino_info['init_done']: + file_path = self.view.file_name() + dir_path = os.path.dirname(file_path) + build_info = {'path': dir_path} + build_info['upload_mode'] = 'program' + stino.sketch_builder.put(build_info) + + def is_enabled(self): + """.""" + state = False + if stino.arduino_info['init_done']: + file_path = self.view.file_name() + sel_prog = stino.arduino_info['selected'].get('programmer') + if file_path and stino.c_file.is_cpp_file(file_path) and sel_prog: + cmd = stino.selected.get_upload_command(stino.arduino_info, + mode='program') + if cmd: + info = \ + stino.selected.get_sel_board_info(stino.arduino_info) + if info: + state = True + return state + + +class StinoRefreshProgrammersCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.init_inst_pkgs_info) + stino.do_action.put(stino.init_programmers_info) + stino.do_action.put(stino.st_menu.update_programmer_menu, + stino.arduino_info) + + +class StinoSelectProgrammerCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, programmer_name): + """.""" + if stino.arduino_info['init_done']: + if not self.is_checked(programmer_name): + stino.on_programmer_select(programmer_name) + + def is_checked(self, programmer_name): + """.""" + state = False + if stino.arduino_info['init_done']: + key = 'programmer' + state = stino.arduino_info['selected'].get(key) == programmer_name + return state + + +############################################# +# Tools Commands +############################################# +class StinoBurnBootloaderCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + msg = 'Beware: Please check your board type! Continue?' + result = sublime.yes_no_cancel_dialog(msg) + if result == sublime.DIALOG_YES: + stino.bootloader.put() + + def is_enabled(self): + """.""" + state = False + if stino.arduino_info['init_done']: + sel_prog = stino.arduino_info['selected'].get('programmer') + if sel_prog: + cmds = \ + stino.selected.get_bootloader_commands(stino.arduino_info) + if cmds and cmds[0] and cmds[1]: + info = \ + stino.selected.get_sel_board_info(stino.arduino_info) + if info: + state = True + return state + + +class StinoAutoFormatCommand(sublime_plugin.TextCommand): + """Auto Format Src.""" + + def run(self, edit): + """Auto Format Src.""" + if self.view.is_dirty(): + self.view.run_command('save') + file_path = self.view.file_name() + stino.beautify_src(self.view, edit, file_path) + + def is_enabled(self): + """Auto Format Src.""" + state = False + file_path = self.view.file_name() + if file_path and stino.c_file.is_cpp_file(file_path): + state = True + return state + + +############################################# +# Help Commands +############################################# +class StinoShowPanelCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + panel_name = 'stino_panel' + if not self.window.find_output_panel(panel_name): + stino.message_queue.put() + out_panel_name = 'output.stino_panel' + self.window.run_command("show_panel", {"panel": out_panel_name}) + + +class StinoRefreshLangsCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.do_action.put(stino.st_menu.update_language_menu, + stino.arduino_info) + + +class StinoSelectLanguageCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self, language): + """.""" + if stino.arduino_info['init_done']: + if self.is_checked(language): + stino.do_action.put(stino.on_language_select, language) + + def is_checked(self, language): + """.""" + state = False + if stino.arduino_info['init_done']: + key = 'language' + state = stino.arduino_info['selected'].get(key) == language + return state + + +class StinoOpenPlatformDocumentsCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + if stino.arduino_info['init_done']: + stino.open_platform_documents() + + +class StinoAboutCommand(sublime_plugin.WindowCommand): + """.""" + + def run(self): + """.""" + stino.message_queue.put('Stino 2017') + + +class StinoPanelWriteCommand(sublime_plugin.TextCommand): + """.""" + + def run(self, edit, text, mode='insert', do_scroll=True): + """.""" + point = self.view.size() + if mode == 'insert': + self.view.insert(edit, point, text) + else: + region = sublime.Region(0, point) + self.view.replace(edit, region, text) + + if do_scroll: + point = self.view.size() + self.view.show(point) diff --git a/libs/base_utils/__init__.py b/libs/base_utils/__init__.py new file mode 100644 index 0000000..f520b09 --- /dev/null +++ b/libs/base_utils/__init__.py @@ -0,0 +1 @@ +"""__init__.""" diff --git a/libs/base_utils/c_file.py b/libs/base_utils/c_file.py new file mode 100644 index 0000000..86239ca --- /dev/null +++ b/libs/base_utils/c_file.py @@ -0,0 +1,1087 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Doc.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import os +import re +from . import file + +MAX_LINE_LENGTH = 80 + +INO_EXTS = ['.ino', '.pde'] +H_EXTS = ['.h', '.hh', '.hpp'] +C_EXTS = ['.c', '.cc'] +CPP_EXTS = ['.cpp', '.cxx'] +S_EXTS = ['.S'] +CC_EXTS = CPP_EXTS + C_EXTS + S_EXTS +INOC_EXTS = INO_EXTS + CC_EXTS +SRC_EXTS = INOC_EXTS + H_EXTS +chars = '_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +num_chars = '0123456789.' +var_chars = chars + num_chars +none_operator_chars = var_chars + +whitespace = r'\s+' +preprocessor_directive = r'\s*#.*?$' +multi_line_comment = r'/\*[^*]*(?:\*(?!/)[^*]*)*\*/' +single_line_comment = r'//.*?$' +double_quoted_string = r'"(?:[^"\\]|\\.)*"' +include = r'#include\s*[<"](\S+)[">]' + + +def is_cpp_file(file_name): + """.""" + state = False + ext = os.path.splitext(file_name)[1] + if ext in SRC_EXTS: + state = True + return state + + +def strip_back_slash(lines): + """Doc.""" + new_lines = [] + is_last_break_line = False + last_line = '' + + for line in lines: + line = line.strip() + + if not line: + continue + + if not is_last_break_line: + cur_line = line + else: + cur_line = last_line + line + + if line.endswith('\\'): + is_last_break_line = True + last_line = cur_line[:-1] + continue + + is_last_break_line = False + new_lines.append(cur_line) + return new_lines + + +def break_lines(lines): + """Doc.""" + new_lines = [] + + in_multi_line_comment = False + in_single_line_comment = False + + in_single_quoted_character = False + in_double_quoted_string = False + + for line in lines: + line = line.strip() + if not line: + continue + + if not in_multi_line_comment: + in_single_line_comment = False + in_single_quoted_character = False + in_double_quoted_string = False + + text = '' + for index, char in enumerate(line): + in_str = (in_multi_line_comment or in_single_line_comment or + in_single_quoted_character or in_double_quoted_string) + + if in_str: + text += char + if index - 1 >= 0: + pre_char = line[index - 1] + if (char == '/' and pre_char == '*' and + in_multi_line_comment): + in_multi_line_comment = False + text += '\n' + elif (char == '\'' and pre_char != '\\' and + in_single_quoted_character): + in_single_quoted_character = False + elif (char == '"' and pre_char != '\\' and + in_double_quoted_string): + in_double_quoted_string = False + else: + if char == '/': + if index + 1 < len(line): + next_char = line[index + 1] + if next_char == '*': + in_multi_line_comment = True + text += '\n' + text += char + elif next_char == '/': + in_single_line_comment = True + text += char + else: + text += char + else: + text += char + elif char == "'": + in_single_quoted_character = True + text += char + elif char == '"': + in_double_quoted_string = True + text += char + elif char == '#': + text += '\n' + text += char + elif char not in none_operator_chars: + if char == '{' or char == '}': + delimeter = '\n' + else: + delimeter = ' ' + text += (delimeter + char + delimeter) + else: + text += char + new_lines += text.split('\n') + + lines = new_lines + new_lines = [] + for line in lines: + if line.startswith('#'): + line = '#' + line[1:].strip() + new_lines.append(line) + return new_lines + + +def split_line_by_str(line): + """Doc.""" + indeces = [0] + in_single_quoted_character = False + in_double_quoted_string = False + in_single_line_comment = False + for index, char in enumerate(line): + in_str = (in_single_quoted_character or in_double_quoted_string or + in_single_line_comment) + if in_str: + if index - 1 >= 0: + pre_char = line[index - 1] + if (char == '\'' and pre_char != '\\' and + in_single_quoted_character): + in_single_quoted_character = False + indeces.append(index + 1) + elif (char == '"' and pre_char != '\\' and + in_double_quoted_string): + in_double_quoted_string = False + indeces.append(index + 1) + else: + if char == '\'': + in_single_quoted_character = True + indeces.append(index) + elif char == '"': + in_double_quoted_string = True + indeces.append(index) + elif char == '/' and index + 1 < len(line): + next_char = line[index + 1] + if next_char == '/': + in_single_line_comment = True + indeces.append(index) + indeces.append(len(line)) + + start_indeces = indeces[:-1] + end_indeces = indeces[1:] + index_pairs = zip(start_indeces, end_indeces) + line_slices = [] + for index_pair in index_pairs: + line_slice = line[index_pair[0]:index_pair[1]].strip() + if line_slice: + line_slices.append(line_slice) + return line_slices + + +def split_line_to_words(line): + """Doc.""" + words = [] + line_slices = split_line_by_str(line) + for line_slice in line_slices: + if not line_slice: + continue + if (line_slice.startswith('\'') or line_slice.startswith('"') or + line_slice.startswith('//')): + words.append(line_slice) + else: + words += line_slice.split() + return words + + +def insert_semicolon_break(words_list): + """Doc.""" + new_words_list = [] + for words in words_list: + new_words = [] + in_for = False + semicolon_counter = 0 + for index, word in enumerate(words): + next_word = '' + if index + 1 < len(words): + next_word = words[index + 1] + + new_words.append(word) + if not in_for: + if word == ';' and next_word[:2] != '//': + new_words_list.append(new_words) + new_words = [] + if word == 'for': + in_for = True + else: + if word == ';': + semicolon_counter += 1 + if semicolon_counter == 2: + in_for = False + semicolon_counter = 0 + new_words_list.append(new_words) + return new_words_list + + +def insert_right_parenthesis_break(words_list): + """Doc.""" + new_words_list = [] + for words in words_list: + in_condition = False + indent_level = 0 + need_break = False + for index, word in enumerate(words): + if not in_condition: + if word in ('if', 'while', 'for', 'switch'): + in_condition = True + continue + else: + if word == '(': + indent_level += 1 + elif word == ')': + indent_level -= 1 + if indent_level == 0: + need_break = True + break + if need_break: + index = index + 1 + new_words_list.append(words[:index]) + new_words_list.append(words[index:]) + else: + new_words_list.append(words) + return new_words_list + + +def insert_else_break(words_list): + """Doc.""" + new_words_list = [] + for words in words_list: + if not words: + continue + if words[0] == 'else': + if len(words) >= 2 and words[1].startswith('//'): + new_words_list.append(words) + else: + new_words_list.append([words[0]]) + new_words_list.append(words[1:]) + else: + new_words_list.append(words) + return new_words_list + + +def insert_colon_break(words_list): + """Doc.""" + keys = ('case', 'default', 'private', 'public', 'protect') + new_words_list = [] + for words in words_list: + if ':' in words and words[-1][:2] != '//' and words[0] in keys: + index = words.index(':') + 1 + new_words_list.append(words[:index]) + new_words_list.append(words[index:]) + else: + new_words_list.append(words) + return new_words_list + + +def remove_blank_words(words_list): + """Doc.""" + new_words_list = [] + for words in words_list: + new_words = [] + for word in words: + if not word: + continue + else: + new_words.append(word) + if new_words: + new_words_list.append(new_words) + return new_words_list + + +def remove_break_before_semicolon(words_list): + """Doc.""" + new_words_list = [] + words_list = remove_blank_words(words_list) + for index, words in enumerate(words_list): + state = False + if words[0] in ',;' and index - 1 >= 0: + pre_words = words_list[index - 1] + if not pre_words[-1].startswith('//'): + if pre_words[-1][-1] in ')}': + state = True + + if state: + pre_words = words_list[index - 1] + pre_words += words + new_words_list.pop() + new_words_list.append(pre_words) + else: + new_words_list.append(words) + return new_words_list + + +def remove_parenthesis_break(words_list): + """Doc.""" + new_words_list = [] + parenthesis_num = 0 + new_words = [] + is_line_end = True + for words in words_list: + if not words: + continue + + new_words += words + if not words[-1].startswith('//'): + for word in words: + if word == '(': + parenthesis_num += 1 + elif word == ')': + parenthesis_num -= 1 + if parenthesis_num <= 0: + is_line_end = True + else: + is_line_end = False + else: + is_line_end = True + + if is_line_end: + new_words_list.append(new_words) + new_words = [] + return new_words_list + + +def regular_chars(words): + """Doc.""" + new_words = [] + new_word = '' + + for index, word in enumerate(words): + pre_word = '' + if index - 1 >= 0: + pre_word = words[index - 1] + + if pre_word in '([!~^:' or word[0] in '[]]);,:?.^': + new_word += word + + elif word == '(': + if pre_word in ('if', 'for', 'while', 'switch'): + new_words.append(new_word) + new_word = word + elif pre_word[-1] in '+-*/%<>!=&|^': + new_words.append(new_word) + new_word = word + elif words[0] == '#define' and index == 2: + new_words.append(new_word) + new_word = word + else: + new_word += word + + elif word == '=': + if pre_word in '+-*/%<>!=&|^': + new_word += word + else: + new_words.append(new_word) + new_word = word + + elif pre_word == '+': + if word in '+': + new_word += word + else: + new_words.append(new_word) + new_word = word + + elif pre_word == '-': + if word in '->': + new_word += word + else: + is_negtive = False + pre_2_index = index - 2 + if pre_2_index >= 0: + pre_2_word = words[index - 2] + if pre_2_word[-1] not in (none_operator_chars + ')]'): + is_negtive = True + if is_negtive: + new_word += word + else: + new_words.append(new_word) + new_word = word + + elif pre_word == '<': + if words[0] == '#include' or word in '<=': + new_word += word + else: + new_words.append(new_word) + new_word = word + + elif pre_word == '>': + if word in '>=': + new_word += word + else: + new_words.append(new_word) + new_word = word + + elif pre_word == '&': + if word in '&': + new_word += word + else: + new_words.append(new_word) + new_word = word + + elif pre_word == '|': + if word in '|': + new_word += word + else: + new_words.append(new_word) + new_word = word + + elif word == '>': + if words[0] == '#include': + new_word += word + else: + new_words.append(new_word) + new_word = word + + elif word.startswith('//'): + new_words.append(new_word) + new_word = '// ' + word[2:].strip() + + else: + new_words.append(new_word) + new_word = word + new_words.append(new_word) + return new_words + + +def regular_pp_mm(words): + """Doc.""" + new_words = [] + new_word = '' + for index, word in enumerate(words): + pre_word = '' + if index - 1 >= 0: + pre_word = words[index - 1] + + if index == 0: + new_word += word + elif pre_word[-2:] in ('++', '--'): + if word[0] in chars: + new_word += word + else: + new_words.append(new_word) + new_word = word + elif word[:2] in ('++', '--'): + if pre_word[-1] in var_chars: + new_word += word + else: + new_words.append(new_word) + new_word = word + else: + new_words.append(new_word) + new_word = word + new_words.append(new_word) + return new_words + + +def regular_blanks(words_list): + """Doc.""" + new_words_list = [] + for words in words_list: + words = regular_chars(words) + words = regular_pp_mm(words) + new_words_list.append(words) + return new_words_list + + +def regular_none_comment_lines(lines): + """Doc.""" + words_list = [] + for line in lines: + words = split_line_to_words(line) + words_list.append(words) + words_list = insert_semicolon_break(words_list) + words_list = insert_right_parenthesis_break(words_list) + words_list = insert_colon_break(words_list) + words_list = insert_else_break(words_list) + words_list = remove_break_before_semicolon(words_list) + words_list = regular_blanks(words_list) + + new_lines = [] + for words in words_list: + line = ' '.join(words) + new_lines.append(line) + return new_lines + + +def split_lines_by_comment(lines): + """Doc.""" + lines_list = [] + + new_lines = [] + in_multi_line_comment = False + for line in lines: + line = line.strip() + if not line: + continue + + if not in_multi_line_comment: + if line.startswith('/*'): + in_multi_line_comment = True + lines_list.append(new_lines) + new_lines = [] + elif line.startswith('//'): + line = '// ' + line[2:].strip() + lines_list.append(new_lines) + lines_list.append([line]) + new_lines = [] + else: + new_lines.append(line) + + if in_multi_line_comment: + new_lines.append(line) + if line.endswith('*/'): + in_multi_line_comment = False + lines_list.append(new_lines) + new_lines = [] + lines_list.append(new_lines) + return lines_list + + +def regular_lines(lines): + """Doc.""" + new_lines_list = [] + lines_list = split_lines_by_comment(lines) + for lines in lines_list: + if not lines: + continue + + if not (lines[0].startswith('/*') or lines[0].startswith('//')): + lines = regular_none_comment_lines(lines) + new_lines_list.append(lines) + return new_lines_list + + +def break_long_line(line, indent_level): + """Doc.""" + words = split_line_to_words(line) + level = indent_level + 1 + words_list = [] + new_words = [] + for word in words: + new_words.append(word) + line = ' '.join(new_words) + if (4 * level + len(line)) > MAX_LINE_LENGTH: + if not word.startswith('//'): + new_words.pop() + new_words.append('\\') + words_list.append(new_words) + new_words = ['\t' + word] + words_list.append(new_words) + lines = [' '.join(words) for words in words_list] + lines = ['\t' * indent_level + line for line in lines] + new_line = '\n'.join(lines) + return new_line + + +def indent_lines(lines): + """Doc.""" + new_lines = [] + indent_flags = [] + lines_list = regular_lines(lines) + + for lines in lines_list: + if not lines: + continue + + if lines[0].startswith('/*') or lines[0].startswith('//'): + indent_level = len(indent_flags) - indent_flags.count('#') + for line in lines: + new_lines.append('\t' * indent_level + line) + continue + + for line_index, line in enumerate(lines): + line = line.strip() + if not line: + continue + + no_indent_once = False + parenthesis_indent_once = False + macro_indent_once = False + macro_no_indent_once = False + + line_slices = split_line_by_str(line) + last_slice = line_slices[-1] + if last_slice.startswith('//'): + last_slice = line_slices[-2] + + if line.startswith('{'): + indent_flags.append('{') + no_indent_once = True + elif line.endswith(':'): + if indent_flags and ':' in indent_flags: + temp_flags = indent_flags[:] + flag = '#' + while(flag == '#'): + flag = temp_flags.pop() + if flag == ':': + index = len(temp_flags) + indent_flags.pop(index) + if line_index + 1 < len(lines): + next_line = lines[line_index + 1] + if not next_line.startswith('{'): + indent_flags.append(':') + no_indent_once = True + elif ((last_slice.endswith(')') or last_slice == 'else') and + not line.startswith('#')): + parenthesis_indent_once = True + if line_index + 1 < len(lines): + next_line = lines[line_index + 1] + if not next_line.startswith('{'): + no_indent_once = True + indent_flags.append(')') + elif line.startswith('}'): + if '{' in indent_flags: + index = indent_flags[::-1].index('{') + before_flags = indent_flags[::-1][index + 1:][::-1] + after_flags = indent_flags[::-1][:index + 1][::-1] + else: + before_flags = [] + after_flags = indent_flags + sharp_num = after_flags.count('#') + indent_flags = before_flags + ['#'] * sharp_num + + elif line.startswith('#if'): + indent_flags.append('#') + macro_indent_once = True + macro_no_indent_once = True + elif line.startswith('#else') or line.startswith('#elif'): + macro_indent_once = True + macro_no_indent_once = True + if '#' in indent_flags: + index = indent_flags[::-1].index('#') + indent_flags = indent_flags[::-1][index:][::-1] + elif line.startswith('#endif'): + macro_indent_once = True + if '#' in indent_flags: + index = indent_flags[::-1].index('#') + index = len(indent_flags) - 1 - index + indent_flags.pop(index) + + if macro_indent_once: + indent_level = indent_flags.count('#') + if macro_no_indent_once: + indent_level -= 1 + + new_line = '' + if line.startswith('#if'): + new_line += '\n' + if (4 * indent_level + len(line)) < MAX_LINE_LENGTH: + new_line += '\t' * indent_level + line + else: + new_line += break_long_line(line, indent_level) + if line.startswith('#endif'): + new_line += '\n' + else: + indent_level = len(indent_flags) - indent_flags.count('#') + if no_indent_once: + indent_level -= 1 + if (4 * indent_level + len(line)) < MAX_LINE_LENGTH: + new_line = '\t' * indent_level + line + else: + new_line = break_long_line(line, indent_level) + if line.startswith('}') and indent_flags.count('{') == 0: + new_line += '\n' + new_lines.append(new_line) + + if not parenthesis_indent_once: + if indent_flags: + flag = '?' + temp_flags = indent_flags[:] + while(temp_flags and flag not in '{:'): + flag = temp_flags.pop() + + index = len(temp_flags) + if flag in '{:': + index += 1 + + before_flags = indent_flags[:index] + after_flags = indent_flags[index:] + else: + before_flags = [] + after_flags = indent_flags + sharp_num = after_flags.count('#') + indent_flags = before_flags + ['#'] * sharp_num + return new_lines + + +def scrub_comments(lines): + """Replace commented portions of a given source text as spaces.""" + lines = strip_back_slash(lines) + + new_lines = [] + in_multi_line_comment = False + in_single_line_comment = False + + in_single_quoted_character = False + in_double_quoted_string = False + + for line in lines: + line = line.strip() + if not line: + continue + + if not in_multi_line_comment: + in_single_line_comment = False + in_single_quoted_character = False + in_double_quoted_string = False + + new_line = '' + for index, char in enumerate(line): + not_char = False + in_str = (in_multi_line_comment or in_single_line_comment or + in_single_quoted_character or in_double_quoted_string) + + if in_str: + if index - 1 >= 0: + pre_char = line[index - 1] + if (char == '/' and pre_char == '*' and + in_multi_line_comment): + in_multi_line_comment = False + elif (char == '\'' and pre_char != '\\' and + in_single_quoted_character): + in_single_quoted_character = False + elif (char == '"' and pre_char != '\\' and + in_double_quoted_string): + in_double_quoted_string = False + else: + if char == '/': + if index + 1 < len(line): + next_char = line[index + 1] + if next_char == '*': + in_multi_line_comment = True + not_char = True + elif next_char == '/': + in_single_line_comment = True + not_char = True + elif char == "'": + in_single_quoted_character = True + not_char = True + elif char == '"': + if not line.startswith('#include'): + in_double_quoted_string = True + not_char = True + + if not not_char: + if char == '{' or char == '}': + new_line += ('\n' + char + '\n') + elif char == '#': + new_line += ('\n' + char) + else: + new_line += char + new_lines += new_line.split('\n') + return new_lines + + +def collapse_braces(lines): + """Doc.""" + lines = scrub_comments(lines) + + new_lines = [] + indent_flags = [] + macro_flags = [] + line_off_level = 100 + + line_on = True + for line in lines: + line = line.strip() + if not line: + continue + if line_on: + if line.startswith('{'): + indent_flags.append('{') + elif line.startswith('}'): + if '{' in indent_flags: + index = indent_flags[::-1].index('{') + index = len(indent_flags) - 1 - index + indent_flags.pop(index) + + if '{' not in indent_flags: + if line.startswith('#if'): + macro_flags.append('#') + elif line.startswith('#else') or line.startswith('#elif'): + if line_on: + line_on = False + line_off_level = len(macro_flags) + elif line.startswith('#endif'): + if len(macro_flags) == line_off_level: + line_on = True + if macro_flags: + macro_flags.pop() + if line_on and not (line.startswith('#if') or + line.startswith('#endif') or + line.startswith('#define')): + new_lines.append(line) + else: + if line.startswith('#if'): + indent_flags.append('#') + elif line.startswith('#else') or line.startswith('#elif'): + index = indent_flags[::-1].index('#') + index = len(indent_flags) - index + indent_flags = indent_flags[:index] + elif line.startswith('#endif'): + if '#' in indent_flags: + index = indent_flags[::-1].index('#') + index = len(indent_flags) - 1 - index + indent_flags.pop(index) + + if indent_flags.count('{') == 1 and line.startswith('{'): + new_lines.append(line) + return new_lines + + +def simplify_to_one_line(lines): + """Doc.""" + new_lines = [] + in_one_lines = [] + is_line_end = False + for line in lines: + is_start_break = False + if line.startswith('void') or line.startswith('#'): + is_line_end = True + is_start_break = True + if is_line_end: + new_lines.append(' '.join(in_one_lines)) + in_one_lines = [] + is_line_end = False + in_one_lines.append(line) + + if not is_start_break: + if line.endswith(';') or line.endswith('}'): + is_line_end = True + if is_line_end: + new_lines.append(' '.join(in_one_lines)) + in_one_lines = [] + is_line_end = False + if not is_line_end: + new_lines.append(' '.join(in_one_lines)) + return new_lines + + +def remove_none_func_lines(lines): + """Doc.""" + new_lines = [] + for line in lines: + if line.startswith('#') and '.h' in line: + new_lines.append(line) + elif line.endswith(');') or line.endswith('}'): + if '(' in line and ')' in line and '::' not in line: + new_lines.append(line) + return new_lines + + +def simplify_lines(lines): + """Doc.""" + lines = break_lines(lines) + lines = collapse_braces(lines) + lines = regular_none_comment_lines(lines) + lines = simplify_to_one_line(lines) + lines = remove_none_func_lines(lines) + return lines + + +def beautify_lines(lines): + """Doc.""" + lines = strip_back_slash(lines) + lines = break_lines(lines) + lines = indent_lines(lines) + return lines + + +def is_main_ino_file(file_path): + """.""" + state = False + f = CFile(file_path) + funcs = f.list_function_definitions() + funcs = [f.split('(')[0] for f in funcs] + + count = 0 + for func in funcs: + if ' setup' in func or ' loop' in func: + count += 1 + if count == 2: + break + + if count == 2: + state = True + return state + + +def is_main_cpp_file(file_path): + """.""" + state = False + f = CFile(file_path) + funcs = f.list_function_definitions() + funcs = [f.split('(')[0] for f in funcs] + for func in funcs: + if ' main' in func: + state = True + break + return state + + +def get_index_of_first_statement(src_text): + """ + Return the index of the first character. + + that's not whitespace a comment or a pre-processor directive. + + Args: + src_text: The source code. + + Returns: + index: The index of first statement. + """ + pattern_text = preprocessor_directive + pattern_text += '|' + multi_line_comment + pattern_text += '|' + single_line_comment + pattern_text += '|' + whitespace + + pattern = re.compile(pattern_text, re.M | re.S) + match_iter = pattern.finditer(src_text) + + index = 0 + for match in match_iter: + if match.start() != index: + break + index = match.end() + return index + + +class CFile(file.File): + """A c/c++ source file.""" + + def __init__(self, file_path): + """Initiate the source file.""" + super(CFile, self).__init__(file_path) + self._last_mtime = 0 + self._text = self.read() + self._lines = self._text.split('\n') + self._beautified_lines = [] + self._simplified_lines = [] + + def _is_modified(self): + """Doc.""" + return self.get_mtime() != self._last_mtime + + def is_cpp_file(self): + """Doc.""" + return self.get_ext() in SRC_EXTS + + def _update(self): + """Doc.""" + self._last_mtime = self.get_mtime() + self._text = self.read() + self._lines = self._text.split('\n') + + def _check_modified(self): + """Doc.""" + if self.is_cpp_file() and self._is_modified(): + self._update() + + def get_lines(self): + """Doc.""" + self._check_modified() + return self._lines + + def get_beautified_lines(self): + """Doc.""" + self._check_modified() + return self._beautified_lines + + def get_beautified_text(self): + """Doc.""" + self._check_modified() + if not self._beautified_lines: + self._beautified_lines = beautify_lines(self._lines) + beautified_text = '\n'.join(self._beautified_lines) + beautified_text = beautified_text.replace('\n\n\n', '\n\n') + beautified_text = beautified_text.replace('\n;', ';\n') + return beautified_text + + def get_simplified_lines(self): + """Doc.""" + self._check_modified() + if not self._simplified_lines: + self._simplified_lines = simplify_lines(self._lines) + return self._simplified_lines + + def get_simplified_text(self): + """Doc.""" + self._check_modified() + if not self._simplified_lines: + self._simplified_lines = simplify_lines(self._lines) + return '\n'.join(self._simplified_lines) + + def list_function_declarations(self): + """Doc.""" + self._check_modified() + function_declarations = [] + if not self._simplified_lines: + self._simplified_lines = simplify_lines(self._lines) + for line in self._simplified_lines: + if line.endswith(');'): + function_declarations.append(line[:-1]) + return function_declarations + + def list_function_definitions(self): + """Doc.""" + self._check_modified() + function_definitions = [] + if not self._simplified_lines: + self._simplified_lines = simplify_lines(self._lines) + for line in self._simplified_lines: + if line.endswith('}'): + function_definitions.append(line.split('{')[0].strip()) + return function_definitions + + def list_inclde_headers(self): + """Doc.""" + self._check_modified() + pattern_text = multi_line_comment + pattern_text += '|' + single_line_comment + + pattern = re.compile(pattern_text, re.M | re.S) + text = pattern.sub('', self._text) + + pattern = re.compile(include) + headers = pattern.findall(text) + return headers + + def get_undeclar_func_defs(self): + """.""" + func_defs = self.list_function_definitions() + func_declars = self.list_function_declarations() + + undeclar_func_defs = [] + for func_def in func_defs: + if func_def not in func_declars: + undeclar_func_defs.append(func_def) + return undeclar_func_defs diff --git a/libs/base_utils/c_project.py b/libs/base_utils/c_project.py new file mode 100644 index 0000000..288d674 --- /dev/null +++ b/libs/base_utils/c_project.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Doc.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import os +import glob +import codecs +from . import file +from . import c_file + + +def list_files_of_extension(dir_path, ext='', mode='norecursion'): + """.""" + paths = [] + if mode == 'recursion': + sub_file_paths = glob.glob(dir_path + '/*') + sub_dir_paths = [p for p in sub_file_paths if os.path.isdir(p)] + for sub_dir_path in sub_dir_paths: + paths += list_files_of_extension(sub_dir_path, ext, 'recursion') + paths += glob.glob(dir_path + '/*' + ext) + return paths + + +def list_files_of_extensions(dir_path, exts, mode='norecursion'): + """.""" + paths = [] + for ext in exts: + paths += list_files_of_extension(dir_path, ext, mode) + return paths + + +def get_file_info_of_extension(dir_path, ext='', + mode='norecursion', excludes=[]): + """.""" + info = {} + if mode == 'recursion': + sub_file_paths = glob.glob(dir_path + '/*') + sub_dir_paths = [p for p in sub_file_paths if os.path.isdir(p)] + for sub_dir_path in sub_dir_paths: + dir_name = os.path.basename(sub_dir_path) + if dir_name.lower() not in excludes: + sub_info = get_file_info_of_extension(sub_dir_path, ext, + 'recursion', excludes) + info.update(sub_info) + + paths = glob.glob(dir_path + '/*' + ext) + for path in paths: + name = os.path.basename(path) + info[name] = dir_path + return info + + +def get_file_info_of_extensions(dir_path, exts, + mode='norecursion', excludes=[]): + """.""" + info = {} + for ext in exts: + info.update(get_file_info_of_extension(dir_path, ext, mode, excludes)) + return info + + +def combine_ino_files(ino_file_paths, target_file_path): + """.""" + need_combine = False + + build_path = os.path.dirname(target_file_path) + last_inos_path = os.path.join(build_path, + 'last_inos.stino-settings') + last_inos_info = file.SettingsFile(last_inos_path) + f_paths = [p.replace('\\', '/') for p in ino_file_paths] + + if f_paths: + if not os.path.isfile(target_file_path): + need_combine = True + else: + for ino_file_path in f_paths: + mtime = os.path.getmtime(ino_file_path) + last_mtime = last_inos_info.get(ino_file_path) + if mtime and mtime != last_mtime: + last_inos_info.set(ino_file_path, mtime) + need_combine = True + + if need_combine: + func_prototypes = [] + for ino_file_path in f_paths: + ino_file = c_file.CFile(ino_file_path) + prototypes = ino_file.get_undeclar_func_defs() + for prototype in prototypes: + if prototype not in func_prototypes: + func_prototypes.append(prototype) + + with codecs.open(f_paths[0], 'r', 'utf-8') as source_f: + src_text = source_f.read() + index = c_file.get_index_of_first_statement(src_text) + header_text = src_text[:index] + footer_text = src_text[index:] + + with codecs.open(target_file_path, 'w', 'utf-8') as target_f: + cur_path = f_paths[0] + footer_start_line = len(header_text.split('\n')) + text = '#line 1 "%s"\n' % cur_path + text += header_text + text += '\n#include \n' + if func_prototypes: + text += ';\n'.join(func_prototypes) + text += ';\n\n' + text += '#line %d "%s"\n' % (footer_start_line, cur_path) + text += footer_text + target_f.write(text) + + for ino_file_path in f_paths[1:]: + first_line = '#line 1 "%s"\n' % ino_file_path + target_f.write(first_line) + with codecs.open(target_file_path, + 'r', 'utf-8') as source_f: + target_f.write(source_f.read()) + else: + with codecs.open(target_file_path, 'w', 'utf-8') as target_f: + text = '#include \n' + target_f.write(text) + + +def check_main_file(file_paths, prj_type='arduino'): + """.""" + has_main_file = False + + if prj_type == 'arduino': + is_main_file = c_file.is_main_ino_file + else: + is_main_file = c_file.is_main_cpp_file + + for file_path in file_paths: + if is_main_file(file_path): + has_main_file = True + break + return has_main_file + + +class CProject(object): + """.""" + + def __init__(self, project_path, build_dir_path): + """.""" + self._path = project_path + self._name = os.path.basename(project_path) + self.set_build_path(build_dir_path) + + self._ino_file_paths = list_files_of_extensions(self._path, + c_file.INO_EXTS) + self._cpp_file_paths = list_files_of_extensions(self._path, + c_file.CC_EXTS) + self._src_file_paths = self._ino_file_paths + self._cpp_file_paths + + self._is_arduino_project = False + self._is_cpp_project = self.check_is_c_project() + if not self._is_cpp_project: + self._is_arduino_project = self.check_is_arduino_project() + + def get_name(self): + """.""" + return self._name + + def get_path(self): + """.""" + return self._path + + def get_build_path(self): + """.""" + return self._build_path + + def get_cpp_files(self): + """.""" + return self._cpp_file_paths + + def set_build_path(self, build_dir_path): + """.""" + self._build_path = os.path.join(build_dir_path, self._name) + if not os.path.isdir(self._build_path): + os.makedirs(self._build_path) + + def check_is_arduino_project(self): + """.""" + has_main_file = check_main_file(self._src_file_paths) + return has_main_file + + def check_is_c_project(self): + """.""" + has_main_file = check_main_file(self._src_file_paths, 'cpp') + return has_main_file + + def is_arduino_project(self): + """.""" + return self._is_arduino_project + + def is_cpp_project(self): + """.""" + return self._is_cpp_project + + def gen_arduino_tmp_file(self): + """.""" + tmp_cpp_name = self._name + '.ino.cpp' + tmp_file_path = os.path.join(self._build_path, tmp_cpp_name) + combine_ino_files(self._ino_file_paths, tmp_file_path) diff --git a/libs/base_utils/decos.py b/libs/base_utils/decos.py new file mode 100644 index 0000000..d72a64b --- /dev/null +++ b/libs/base_utils/decos.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""".""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + + +def singleton(cls): + """From PEP-318 http://www.python.org/dev/peps/pep-0318/#examples.""" + _instances = {} + + def get_instance(*args, **kwargs): + if cls not in _instances: + _instances[cls] = cls(*args, **kwargs) + return _instances[cls] + return get_instance diff --git a/libs/base_utils/default_arduino_dirs.py b/libs/base_utils/default_arduino_dirs.py new file mode 100644 index 0000000..a710c7b --- /dev/null +++ b/libs/base_utils/default_arduino_dirs.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Package Docs.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import os +from . import sys_info +from . import sys_dirs + + +def arduino_app_path(): + """Function Docs.""" + app_path = os.path.join(sys_dirs.get_user_config_path(), 'Arduino15') + if sys_info.get_os_name() == 'linux': + home = os.getenv('HOME') + app_path = os.path.join(home, '.arduino15') + return app_path + + +def arduino_sketchbook_path(): + """Function Docs.""" + doc_path = sys_dirs.get_document_path() + sketchbook_path = os.path.join(doc_path, 'Arduino') + return sketchbook_path diff --git a/libs/base_utils/default_st_dirs.py b/libs/base_utils/default_st_dirs.py new file mode 100644 index 0000000..5496b28 --- /dev/null +++ b/libs/base_utils/default_st_dirs.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Package Docs.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import os +import sublime + +from . import file + + +def get_user_path(): + """.""" + packages_path = sublime.packages_path() + user_path = os.path.join(packages_path, 'User') + return user_path + + +def get_plugin_config_path(plugin_name): + """.""" + user_path = get_user_path() + config_path = os.path.join(user_path, plugin_name) + file.check_dir(config_path) + return config_path + + +def get_plugin_menu_path(plugin_name): + """.""" + config_path = get_plugin_config_path(plugin_name) + menu_path = os.path.join(config_path, 'menu') + file.check_dir(menu_path) + return menu_path diff --git a/libs/base_utils/downloader.py b/libs/base_utils/downloader.py new file mode 100644 index 0000000..c37e6c8 --- /dev/null +++ b/libs/base_utils/downloader.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Doc.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import os +import sys +import http.client +import ssl +import socket +import urllib.error +import urllib.request +import operator + +from . import task_queue + + +def get_remote_file_info(url): + """.""" + info = {} + if url.startswith('https'): + opener = urllib.request.build_opener(HTTPSHandlerV3()) + urllib.request.install_opener(opener) + try: + response = urllib.request.urlopen(url) + except (urllib.error.URLError, urllib.error.HTTPError) as e: + print(url, e) + else: + info = dict(response.headers) + response.close() + return info + + +def get_remote_etag(url): + """.""" + info = get_remote_file_info(url) + if 'ETag' in info: + etag = info.get('ETag') + elif 'Last-Modified' in info: + etag = info.get('Last-Modified') + elif 'Content-Length' in info: + etag = info.get('Content-Length') + else: + etag = '' + + if etag.endswith('"'): + etag = etag[:-1] + if '"'in etag: + index = etag.index('"') + etag = etag[index + 1:] + return etag + + +def download(url, target_dir, + message_consumer=sys.stdout.write, mode='resume'): + """.""" + is_done = False + trunk_size = 1024 + done_size = 0 + is_msg_quiet = True + + file_name = os.path.basename(url) + target_file_path = os.path.join(target_dir, file_name) + tmp_file_path = target_file_path + '.stino-down' + + remote_info = get_remote_file_info(url) + remote_size = int(remote_info.get('Content-Length', '0')) + + if remote_size > 0: + if mode == 'resume': + if os.path.isfile(target_file_path): + if os.path.getsize(target_file_path) == remote_size: + is_done = True + + if not is_done: + if url.startswith('https'): + opener = urllib.request.build_opener(HTTPSHandlerV3()) + urllib.request.install_opener(opener) + req = urllib.request.Request(url) + if os.path.isfile(tmp_file_path): + if mode == 'resume': + done_size = os.path.getsize(tmp_file_path) + req.add_header('Range', 'bytes=%d-' % done_size) + else: + os.remove(tmp_file_path) + + try: + remote_f = urllib.request.urlopen(req) + except (ValueError, urllib.error.HTTPError, urllib.error.URLError): + message_consumer('[Error] Can not fetch %s\n' % url, + is_msg_quiet) + else: + message_consumer('[%s] Download started.\n' % url, + is_msg_quiet) + block = b'' + retry_counter = 0 + + if not os.path.isdir(target_dir): + os.makedirs(target_dir) + f = open(tmp_file_path, 'ab') + while True: + try: + trunk = remote_f.read(trunk_size) + except (urllib.error.HTTPError, urllib.error.URLError): + retry_counter += 1 + if retry_counter < 20: + continue + else: + break + else: + if not trunk: + is_done = True + break + + block += trunk + if len(block) > remote_size / 10: + done_size += len(block) + f.write(block) + block = b'' + + percent = done_size / remote_size * 100 + text = '[%s] %.0f%%' % (url, percent) + text += ' (' + text += '%.2f' % (done_size / 1024 / 1024) + text += ' M / ' + text += '%.2f' % (remote_size / 1024 / 1024) + text += ' M)\n' + message_consumer(text, is_msg_quiet) + + if done_size < remote_size: + f.write(block) + + remote_f.close() + f.close() + + if is_done: + if os.path.isfile(target_file_path): + os.remove(target_file_path) + os.rename(tmp_file_path, target_file_path) + message_consumer('[%s] Download completed.\n' % url, + is_msg_quiet) + else: + message_consumer('[%s] Download failed.\n' % url, + is_msg_quiet) + return is_done + + +class HTTPSConnectionV3(http.client.HTTPSConnection): + """.""" + + def __init__(self, *args, **kwargs): + """.""" + super(HTTPSConnectionV3, self).__init__(*args, **kwargs) + + def connect(self): + """.""" + sock = socket.create_connection((self.host, self.port), self.timeout) + try: + protocol = ssl.PROTOCOL_SSLv23 + self.sock = ssl.SSLContext(protocol).wrap_socket(sock) + except ssl.SSLError: + try: + protocol = ssl.PROTOCOL_SSLv3 + self.sock = ssl.SSLContext(protocol).wrap_socket(sock) + except ssl.SSLError: + try: + protocol = ssl.PROTOCOL_TLSv1 + self.sock = ssl.SSLContext(protocol).wrap_socket(sock) + except ssl.SSLError: + try: + protocol = ssl.PROTOCOL_SSLv2 + self.sock = ssl.SSLContext(protocol).wrap_socket(sock) + except ssl.SSLError: + self.sock = sock + + +class HTTPSHandlerV3(urllib.request.HTTPSHandler): + """.""" + + def https_open(self, req): + """.""" + return self.do_open(HTTPSConnectionV3, req) + + +class DownloadQueue(task_queue.TaskQueue): + """.""" + + def put(self, down_info): + """.""" + if self._callable: + in_queue = False + for info in self._queue: + if operator.eq(info, down_info): + in_queue = True + break + if not in_queue: + self._queue.append(down_info) + self._start() diff --git a/libs/base_utils/file.py b/libs/base_utils/file.py new file mode 100644 index 0000000..6a0c42c --- /dev/null +++ b/libs/base_utils/file.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Package Docs.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import os +import codecs +import json +import glob + + +class AbstractFile(object): + """Class Docs.""" + + def __init__(self, path): + """Method Docs.""" + path = os.path.realpath(path) + self.set_path(path) + self._is_readonly = False + + def set_path(self, path): + """Method Docs.""" + self._path = path + self._dir = os.path.dirname(self._path) + self._name = os.path.basename(self._path) + + def __str__(self): + """Method Docs.""" + return '%s (%s)' % (self._name, self._path) + + def get_path(self): + """Method Docs.""" + return self._path + + def get_dir(self): + """Method Docs.""" + return self._dir + + def get_name(self): + """Method Docs.""" + return self._name + + def get_ctime(self): + """Method Docs.""" + ctime = 0 + if os.path.exists(self._path): + ctime = os.path.getctime(self._path) + return ctime + + def get_mtime(self): + """Method Docs.""" + mtime = 0 + if os.path.exists(self._path): + mtime = os.path.getmtime(self._path) + return mtime + + def is_file(self): + """Method Docs.""" + return os.path.isfile(self._path) + + def is_dir(self): + """Method Docs.""" + return os.path.isdir(self._path) + + def is_temp_file(self): + """Method Docs.""" + state = False + lower_name = self._name.lower() + if lower_name == 'cvs': + state = True + elif lower_name.startswith('$') or lower_name.startswith('.'): + state = True + elif lower_name.endswith('.tmp') or lower_name.endswith('.bak'): + state = True + return state + + def change_name(self, new_name): + """Method Docs.""" + os.chdir(self._dir) + os.rename(self._name, new_name) + new_path = os.path.join(self._dir, new_name) + self.set_path(new_path) + + def is_readonly(self): + """Method Docs.""" + return self._is_readonly + + def set_readonly(self, state): + """Method Docs.""" + if state: + self._is_readonly = True + else: + self._is_readonly = False + + +class File(AbstractFile): + """Class Docs.""" + + def __init__(self, path, encoding='utf-8'): + """Method Docs.""" + super(File, self).__init__(path) + self.set_encoding(encoding) + + def has_ext(self, extension): + """Method Docs.""" + return self.get_ext() == extension + + def get_ext(self): + """Method Docs.""" + return os.path.splitext(self._name)[1] + + def get_basename(self): + """Method Docs.""" + return os.path.splitext(self._name)[0] + + def get_encoding(self): + """Method Docs.""" + return self._encoding + + def set_encoding(self, encoding='utf-8'): + """Method Docs.""" + self._encoding = encoding + + def read(self): + """Method Docs.""" + text = '' + try: + with codecs.open(self._path, 'r', self._encoding) as f: + text = f.read() + except (IOError, UnicodeError) as e: + print(e) + return text + + def write(self, text, append=False): + """Method Docs.""" + if self._is_readonly: + return + + mode = 'w' + if append: + mode = 'a' + + if not os.path.isdir(self._dir): + os.makedirs(self._dir) + try: + with codecs.open(self._path, mode, self._encoding) as f: + f.write(text) + except (IOError, UnicodeError): + pass + + +class JSONFile(File): + """Class Docs.""" + + def __init__(self, path): + """Method Docs.""" + super(JSONFile, self).__init__(path) + self._data = {} + self.load() + + def set_data(self, data): + """Method Docs.""" + self._data = data + self.save() + + def get_data(self): + """Method Docs.""" + return self._data + + def load(self): + """Method Docs.""" + text = self.read() + try: + self._data = json.loads(text) + except ValueError: + pass + # print('Error while loading Json file %s.' % self._path) + + def save(self): + """Method Docs.""" + text = json.dumps(self._data, sort_keys=True, indent=4) + self.write(text) + + +class SettingsFile(JSONFile): + """Class Docs.""" + + def __init__(self, path): + """Method Docs.""" + super(SettingsFile, self).__init__(path) + + def get(self, key, default_value=None): + """Method Docs.""" + value = self._data.get(key, default_value) + return value + + def set(self, key, value): + """Method Docs.""" + self._data[key] = value + self.save() + + def get_keys(self): + """.""" + return self._data.keys() + + +class Dir(AbstractFile): + """Class Docs.""" + + def __init__(self, path): + """Method Docs.""" + super(Dir, self).__init__(path) + + def create(self): + """Method Docs.""" + if self.is_file(): + cur_file = File(self._path) + new_name = 'old_file_' + cur_file.name + cur_file.change_name(new_name) + if not self.is_dir(): + os.makedirs(self._path) + + def list_all(self, pattern='*'): + """Method Docs.""" + all_files = [] + paths = glob.glob(os.path.join(self._path, pattern)) + all_files = (AbstractFile(path) for path in paths) + all_files = [f for f in all_files if not f.is_temp_file()] + all_files.sort(key=lambda f: f.get_name().lower()) + return all_files + + def list_dirs(self, pattern='*'): + """Method Docs.""" + all_files = self.list_all(pattern) + dirs = [Dir(f._path) for f in all_files if f.is_dir()] + return dirs + + def list_files(self, pattern='*'): + """Method Docs.""" + all_files = self.list_all(pattern) + files = [File(f._path) for f in all_files if f.is_file()] + return files + + def list_files_of_extension(self, ext=''): + """Method Docs.""" + return self.list_files('*' + ext) + + def list_files_of_extensions(self, exts=['']): + """Method Docs.""" + all_files = [] + for ext in exts: + all_files += self.list_files_of_extension(ext) + return all_files + + def recursive_list_files(self, exts=[''], exclude_dirs=[]): + """Method Docs.""" + all_files = self.list_files_of_extensions(exts) + dirs = self.list_dirs() + for cur_dir in dirs: + if cur_dir.get_name() not in exclude_dirs: + all_files += cur_dir.recursive_list_files(exts) + return all_files + + def has_file(self, file_name): + """Method Docs.""" + file_path = os.path.join(self._path, file_name) + return os.path.isfile(file_path) + + +def check_dir(dir_path): + """.""" + if not os.path.isdir(dir_path): + os.makedirs(dir_path) diff --git a/libs/base_utils/index_file.py b/libs/base_utils/index_file.py new file mode 100644 index 0000000..2c81bdd --- /dev/null +++ b/libs/base_utils/index_file.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Package Docs.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +from . import file + +LIB_KEYS = ['category', 'name', 'version'] + + +def get_item_info(parent_item, items_id): + """ + . + + { + 'packages' : + { + 'names': [$names], + $name: + { + 'platforms': + { + 'names': [$names], + 'arches': [$arches] + $name: + { + + } + } + 'tools': + { + 'names': [$names], + $name: + { + + } + } + } + } + } + """ + info = {'names': [], + 'arches': []} + + name_items_info = {} + items = parent_item.get(items_id, []) + for item in items: + name = item.get('name', '') + if name not in info['names']: + info['names'].append(name) + name_items_info[name] = [item] + else: + name_items_info[name].append(item) + + arch = item.get('architecture', '') + if arch not in info['arches']: + info['arches'].append(arch) + + for name in info['names']: + info[name] = {} + info[name]['versions'] = [] + items = name_items_info[name] + for item in items: + version = item.get('version', '') + if version not in info[name]['versions']: + info[name]['versions'].append(version) + info[name][version] = item + return info + + +def classify_infos_by_key(infos, key, do_classify=True): + """.""" + classified_info = {} + values_name = key + 's' + values = [] + for info in infos: + if key in info: + value = info[key] + if do_classify: + if value not in values: + values.append(value) + classified_info[value] = [info] + else: + classified_info[value].append(info) + else: + if value not in values: + values.append(value) + classified_info[value] = info + values.sort(key=str.lower) + classified_info[values_name] = values + return classified_info + + +def classfy_infos_of_levels(infos, keys, level=0): + """.""" + if level == len(keys) - 1: + all_info = classify_infos_by_key(infos, keys[level], False) + else: + all_info = {} + c_info = classify_infos_by_key(infos, keys[level]) + sub_values = c_info.get(keys[level] + 's') + for sub_value in sub_values: + sub_infos = c_info[sub_value] + sub_all_info = classfy_infos_of_levels(sub_infos, keys, level + 1) + all_info[sub_value] = sub_all_info + all_info[keys[level] + 's'] = sub_values + return all_info + + +class PackageIndexFile(file.JSONFile): + """Class Docs.""" + + def __init__(self, path): + """Method Docs.""" + super(PackageIndexFile, self).__init__(path) + self._is_readonly = True + self._info = {'packages': {}} + self._info['packages']['names'] = [] + + package_infos = self._data.get('packages', []) + for package_info in package_infos: + package_name = package_info.get('name', '') + platform_info = get_item_info(package_info, 'platforms') + tool_info = get_item_info(package_info, 'tools') + package_info['platforms'] = platform_info + package_info['tools'] = tool_info + self._info['packages']['names'].append(package_name) + self._info['packages'][package_name] = package_info + self._info['packages']['names'].sort(key=str.lower) + + def get_info(self): + """.""" + return self._info + + +class PackageIndexFiles(): + """Class Docs.""" + + def __init__(self, paths): + """Method Docs.""" + all_packages_info = {'names': []} + for path in paths: + index_file = PackageIndexFile(path) + index_file_info = index_file.get_info() + + packages_info = index_file_info.get('packages') + names = packages_info.pop('names') + all_packages_info['names'] += names + all_packages_info.update(packages_info) + all_packages_info['names'].sort(key=str.lower) + self._info = {'packages': all_packages_info} + + def get_info(self): + """.""" + return self._info + + +class LibIndexFile(file.JSONFile): + """Class Docs.""" + + def __init__(self, path): + """Method Docs.""" + super(LibIndexFile, self).__init__(path) + self._is_readonly = True + lib_infos = self._data.get('libraries', []) + info = classfy_infos_of_levels(lib_infos, LIB_KEYS) + self._info = {'libraries': info} + + def get_info(self): + """.""" + return self._info + + +class LibIndexFiles(): + """Class Docs.""" + + def __init__(self, paths): + """Method Docs.""" + infos = [] + for path in paths: + lib_index_file = file.JSONFile(path) + infos += lib_index_file.get_data().get('libraries', []) + info = classfy_infos_of_levels(infos, LIB_KEYS) + self._info = {'libraries': info} + + def get_info(self): + """.""" + return self._info diff --git a/libs/base_utils/language_file.py b/libs/base_utils/language_file.py new file mode 100644 index 0000000..0025df0 --- /dev/null +++ b/libs/base_utils/language_file.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +#-*- coding: utf-8 -*- + +# 1. Copyright +# 2. Lisence +# 3. Author + +""" +Documents +""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +from . import abs_file + + +class LanguageFile(abs_file.File): + def __init__(self, path): + super(LanguageFile, self).__init__(path) + text = self.read() + self.trans_dict = load_trans_dict(text) + + def get_trans_dict(self): + return self.trans_dict + + +def load_trans_dict(text): + trans_dict = {} + lines = text.split('\n') + lines = [line.strip() for line in lines if lines if line.strip() and + not line.strip().startswith('#')] + blocks = split_lines(lines) + for block in blocks: + key, value = load_trans_pair(block) + trans_dict[key] = value + return trans_dict + + +def split_lines(lines): + blocks = [] + block = [] + for line in lines: + if line.startswith('msgid'): + blocks.append(block) + block = [] + block.append(line) + blocks.append(block) + blocks.pop(0) + return blocks + + +def load_trans_pair(block): + is_key = True + key = '' + value = '' + for line in block: + index = line.index('"') + cur_str = line[index + 1: -1] + if line.startswith('msgstr'): + is_key = False + if is_key: + key += cur_str + else: + value += cur_str + return (key, value) diff --git a/libs/base_utils/plain_params_file.py b/libs/base_utils/plain_params_file.py new file mode 100644 index 0000000..c88f273 --- /dev/null +++ b/libs/base_utils/plain_params_file.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""".""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +from . import sys_info +from . import file + + +def get_key_value(line): + """.""" + if '=' in line: + index = line.index('=') + key = line[:index].strip() + value = line[index + 1:].strip() + else: + key = line.strip() + value = '' + return key, value + + +def get_lines_with_head(lines, head): + """.""" + block = [] + for line in lines: + if line.startswith(head): + block.append(line) + return block + + +def value_is_name(line, name): + """.""" + state = False + key, value = get_key_value(line) + if value == name: + state = True + return state + + +def get_names(lines): + """.""" + names = [] + for line in lines: + if '.name' in line: + key, name = get_key_value(line) + if name not in names: + names.append(name) + return names + + +def get_heads(lines): + """.""" + heads = [] + for line in lines: + key, value = get_key_value(line) + if '.' in key: + index = key.index('.') + head = key[:index] + else: + head = key + if head not in heads: + heads.append(head) + return heads + + +def get_lines_with_name(lines, name): + """.""" + new_lines = [] + for line in lines: + if value_is_name(line, name): + head = line.split('.')[0] + new_lines = get_lines_with_head(lines, head) + break + return new_lines + + +def get_blocks_by_names(lines, names): + """.""" + for name in names: + block = get_lines_with_name(lines, name) + yield block + + +def get_blocks_by_heads(lines, heads): + """.""" + for head in heads: + block = get_lines_with_head(lines, head) + yield block + + +def remove_block_head(block, head): + """.""" + new_block = [] + for line in block: + if line.startswith(head): + index = len(head) + line = line[index:] + if line.startswith('.'): + line = line[1:] + new_block.append(line) + return new_block + + +def get_generic_info(block, category): + """.""" + generic_info = {} + generic_block = get_generic_block(block) + for line in generic_block: + key, value = get_key_value(line) + if key == 'name': + key = category + '.' + key + generic_info[key] = value + return generic_info + + +def get_generic_block(block): + """.""" + new_block = [] + for line in block: + if not line.startswith('menu.'): + new_block.append(line) + return new_block + + +def get_option_block_info(block): + """.""" + os_name = sys_info.get_os_name() + + block_info = {'names': []} + heads = get_heads(block) + item_blocks = get_blocks_by_heads(block, heads) + for item_block in item_blocks: + head = get_heads(item_block)[0] + item_block = remove_block_head(item_block, head) + item_info = {} + item_name = '' + for line in item_block: + key, value = get_key_value(line) + if not key: + item_name = value + elif key == 'windows': + if os_name == 'windows': + item_name = value + elif key == 'linux': + if os_name == 'linux': + item_name = value + elif key == 'macosx': + if os_name == 'osx': + item_name = value + else: + item_info[key] = value + if item_name: + if item_name not in block_info['names']: + block_info['names'].append(item_name) + block_info[item_name] = item_info + + return block_info + + +def get_menu_blocks_info(block, menu_info): + """.""" + blocks_info = {'options': []} + menu_names = menu_info['sub_menus'].get('names', []) + for menu_name in menu_names: + head = menu_info['sub_menus'].get(menu_name) + option_block = get_lines_with_head(block, head) + + if option_block: + blocks_info['options'].append(menu_name) + option_block = remove_block_head(option_block, head) + block_info = get_option_block_info(option_block) + blocks_info[menu_name] = block_info + return blocks_info + + +class PlainParamsFile(file.File): + """.""" + + def __init__(self, path): + """.""" + super(PlainParamsFile, self).__init__(path) + lines = self.read().split('\n') + lines = (l.strip() for l in lines) + self._lines = [l for l in lines if l and not l.startswith('#')] + self._names = get_names(self._lines) + # self._names.sort(key=str.lower) + + def get_info(self): + """.""" + info = {} + for line in self._lines: + key, value = get_key_value(line) + info[key] = value + return info + + +class BoardsFile(PlainParamsFile): + """.""" + + def __init__(self, path): + """.""" + super(BoardsFile, self).__init__(path) + + def get_menu_info(self): + """.""" + menu_info = {'sub_menus': {}} + menu_info['sub_menus']['names'] = [] + + lines = get_lines_with_head(self._lines, 'menu.') + for line in lines: + key, value = get_key_value(line) + if value not in menu_info['sub_menus']['names']: + menu_info['sub_menus']['names'].append(value) + menu_info['sub_menus'][value] = key + menu_info['sub_menus']['names'].sort(key=str.lower) + return menu_info + + def get_boards_info(self): + """.""" + boards_info = {'boards': {}} + boards_info['boards']['names'] = self._names + + sub_menu_info = self.get_menu_info() + boards_info.update(sub_menu_info) + + for name in self._names: + boards_info['boards'][name] = {} + block = get_lines_with_name(self._lines, name) + head = get_heads(block)[0] + block = remove_block_head(block, head) + generic_info = get_generic_info(block, 'board') + menu_blocks_info = get_menu_blocks_info(block, sub_menu_info) + boards_info['boards'][name]['generic'] = generic_info + boards_info['boards'][name].update(menu_blocks_info) + return boards_info + + +class ProgrammersFile(PlainParamsFile): + """.""" + + def __init__(self, path): + """.""" + super(ProgrammersFile, self).__init__(path) + + def get_programmers_info(self): + """.""" + programmers_info = {'programmers': {}} + programmers_info['programmers']['names'] = self._names + + for name in self._names: + block = get_lines_with_name(self._lines, name) + head = get_heads(block)[0] + block = remove_block_head(block, head) + generic_info = get_generic_info(block, 'programmer') + programmers_info['programmers'][name] = generic_info + return programmers_info diff --git a/libs/base_utils/serial_port.py b/libs/base_utils/serial_port.py new file mode 100644 index 0000000..3f32b69 --- /dev/null +++ b/libs/base_utils/serial_port.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""".""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import time +import threading +import glob +import serial +from serial.tools.list_ports import comports +from . import decos +from . import sys_info + + +def list_serial_ports(): + """.""" + serial_ports = [port for port, d, h in comports() if port] + os_name = sys_info.get_os_name() + if not serial_ports and os_name == "osx": + for port in glob("/dev/tty.*"): + serial_ports.append(port) + return serial_ports + + +def get_serials_info(): + """.""" + serials_info = {'ports': []} + for port, desc, hwid in comports(): + if port: + serials_info['ports'].append(port) + info = {'port': port, 'description': desc, 'hwid': hwid} + serials_info[port] = info + + os_name = sys_info.get_os_name() + if not serials_info['ports'] and os_name == "osx": + for port in glob("/dev/tty.*"): + serials_info['ports'].append(port) + info = {"port": port, "description": "", "hwid": ""} + serials_info[port] = info + return serials_info + + +@decos.singleton +class SerialListener(object): + """.""" + + def __init__(self, call_back=None): + """.""" + self.is_alive = False + self.call_back = call_back + + def start(self): + """.""" + if not self.is_alive: + self.is_alive = True + listener_thread = threading.Thread(target=self.update) + listener_thread.start() + + def update(self): + """.""" + pre_serial_ports = [] + while self.is_alive: + serial_ports = list_serial_ports() + if serial_ports != pre_serial_ports: + pre_serial_ports = serial_ports + if callable(self.call_back): + self.call_back(serial_ports) + time.sleep(1) + + def stop(self): + """.""" + self.is_alive = False + + +def flush_serial_buffer(serial_port): + """.""" + ser = serial.Serial(serial_port) + ser.flushInput() + ser.setDTR(False) + ser.setRTS(False) + time.sleep(0.1) + ser.setDTR(True) + ser.setRTS(True) + ser.close() + + +def touch_port(serial_port, baudrate): + """.""" + ser = serial.Serial() + ser.port = serial_port + ser.baudrate = baudrate + ser.bytesize = serial.EIGHTBITS + ser.stopbits = serial.STOPBITS_ONE + ser.parity = serial.PARITY_NONE + try: + ser.open() + except serial.SerialException: + pass + else: + ser.setDTR(True) + time.sleep(0.022) + ser.setDTR(False) + ser.close() + time.sleep(0.4) + + +def auto_reset(serial_port): + """.""" + ser = serial.Serial() + ser.port = serial_port + try: + ser.open() + except serial.SerialException: + pass + else: + ser.setRTS(False) + ser.setDTR(False) + ser.setDTR(True) + time.sleep(0.05) + ser.setDTR(False) + ser.setRTS(True) + ser.setDTR(True) + time.sleep(0.05) + ser.setDTR(False) + time.sleep(0.05) + ser.write('1EAF') + time.sleep(0.05) + ser.close() + + +def wait_for_new_port(upload_port, before_ports): + """.""" + new_port = None + elapsed = 0 + os_name = sys_info.get_os_name() + while elapsed < 10: + now_ports = list_serial_ports() + diff = list(set(now_ports) - set(before_ports)) + if diff: + new_port = diff[0] + break + + before_ports = now_ports + time.sleep(0.25) + elapsed += 0.25 + + if upload_port in now_ports: + if elapsed >= 5 and os_name != 'windows': + new_port = upload_port + break + + if not new_port: + text = "Couldn't find a Leonardo on the selected port. " + text += 'Check that you have the correct port selected. ' + text += "If it is correct, try pressing the board's reset " + text += 'button after initiating the upload.' + print(text) + return new_port + + +def auto_detect_upload_port(board_info): + """.""" + upload_port = None + vid = board_info.get('build.vid', '') + pid = board_info.get('build.pid', '') + board_hwid = ("%s:%s" % (vid, pid)).replace('0x', '') + + serials_info = get_serials_info() + ports = serials_info.get('ports', []) + for port in ports: + port_info = serials_info.get(port, {}) + port_hwid = port_info.get('hwid', '') + if 'VID:PID' in port_hwid: + if board_hwid in port_hwid: + upload_port = port + break + return upload_port + + +def check_do_touch(board_info): + """.""" + do_touch = False + bootloader_file = board_info.get('bootloader.file', '') + if 'caterina' in bootloader_file.lower(): + do_touch = True + elif board_info.get('upload.use_1200bps_touch') == 'true': + do_touch = True + return do_touch + + +def checke_do_reset(board_info): + """.""" + return board_info.get('upload.auto_reset', '') == 'true' + + +def prepare_upload_port(upload_port, do_touch=False, do_reset=False): + """.""" + if do_touch: + before_ports = list_serial_ports() + if upload_port in before_ports: + text = 'Forcing reset using 1200bps open/close ' + text += 'on port %s.' % upload_port + print(text) + touch_port(upload_port, 1200) + + if sys_info.get_os_name() != 'osx': + time.sleep(0.4) + upload_port = wait_for_new_port(upload_port, before_ports) + + if do_reset: + text = 'Resetting to bootloader via DTR pulse\\n' + print(text) + auto_reset(upload_port) + return upload_port + + +def restore_serial_port(upload_port, baudrate, timeout=4): + """.""" + time.sleep(0.1) + before_time = time.time() + while time.time() - before_time < timeout: + ports = list_serial_ports() + if upload_port in ports: + touch_port(upload_port, baudrate) + else: + break + time.sleep(0.25) + + +def get_serial_file(port): + """.""" + serial_file = port + if serial_file and '/dev/' in serial_file: + serial_file = serial_file[5:] + return serial_file diff --git a/libs/base_utils/sys_dirs.py b/libs/base_utils/sys_dirs.py new file mode 100644 index 0000000..d8a69b7 --- /dev/null +++ b/libs/base_utils/sys_dirs.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Package Docs.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import os +from . import sys_info + + +def get_document_path(): + """Function Docs.""" + _os_name = sys_info.get_os_name() + if _os_name == 'windows': + if sys_info.get_python_version() < 3: + import _winreg as winreg + else: + import winreg + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows' + + r'\CurrentVersion\Explorer\Shell Folders',) + document_path = winreg.QueryValueEx(key, 'Personal')[0] + elif _os_name == 'osx': + home_path = os.getenv('HOME') + document_path = os.path.join(home_path, 'Documents') + else: + document_path = os.getenv('HOME') + return document_path + + +def get_tmp_path(): + """Function Docs.""" + tmp_path = '/tmp' + if sys_info.get_os_name() == 'windows': + tmp_path = os.environ['tmp'] + return tmp_path + + +def get_user_config_path(): + """Function Docs.""" + _os_name = sys_info.get_os_name() + home = os.getenv('HOME') + if _os_name == 'windows': + user_config_path = os.getenv('LOCALAPPDATA') + if not user_config_path: + user_config_path = os.getenv('APPDATA') + elif _os_name == 'linux': + user_config_path = os.path.join(home, '.config') + elif _os_name == 'osx': + user_config_path = os.path.join(home, 'Library') + return user_config_path diff --git a/libs/base_utils/sys_info.py b/libs/base_utils/sys_info.py new file mode 100644 index 0000000..0a21a56 --- /dev/null +++ b/libs/base_utils/sys_info.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Package Docs.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import sys +import codecs +import locale + + +def get_python_version(): + """Function Docs.""" + python_version = sys.version_info[0] + return python_version + + +def get_os_name(): + """Function Docs.""" + name = sys.platform + if name == 'win32': + os_name = 'windows' + elif name == 'darwin': + os_name = 'osx' + elif 'linux' in name: + os_name = 'linux' + else: + os_name = 'other' + return os_name + + +def get_host(): + """Function Docs.""" + machine = 'pc' + ext = 'x32' + + if is_x64(): + ext = 'x64' + host = '-'.join((machine, get_os_name(), ext)) + return host + + +def get_sys_encoding(): + """Function Docs.""" + if get_os_name() == 'osx': + sys_encoding = 'utf-8' + else: + sys_encoding = codecs.lookup(locale.getpreferredencoding()).name + return sys_encoding + + +def get_sys_language(): + """Function Docs.""" + sys_language = locale.getdefaultlocale()[0] + if not sys_language: + sys_language = 'en' + else: + sys_language = sys_language.lower() + return sys_language + + +def is_x64(): + """Function Docs.""" + return sys.maxsize > 2**32 + + +def is_in_submlimetext(): + """Function Docs.""" + state = False + try: + import sublime + except ImportError: + pass + else: + state = True + return state diff --git a/libs/base_utils/task_listener.py b/libs/base_utils/task_listener.py new file mode 100644 index 0000000..29ca88d --- /dev/null +++ b/libs/base_utils/task_listener.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Doc.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import time +import threading + + +class TaskListener(object): + """.""" + + def __init__(self, task, response=None, delay=0.01): + """.""" + self._is_alive = False + self._task = task + self._response = response + self._delay = delay + + def start(self): + """.""" + if not self._is_alive: + self._is_alive = True + thread = threading.Thread(target=self._loop) + thread.start() + + def _loop(self): + """.""" + while True: + self._run() + time.sleep(self._delay) + + def _run(self): + """.""" + if callable(self._task): + state = self._task() + if state is True and callable(self._response): + self._response() + + def stop(self): + """.""" + self._is_alive = False diff --git a/libs/base_utils/task_queue.py b/libs/base_utils/task_queue.py new file mode 100644 index 0000000..1528aae --- /dev/null +++ b/libs/base_utils/task_queue.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Doc.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +import time +import sys +import threading + + +class ActionQueue(object): + """.""" + + def __init__(self, delay=0): + """.""" + self._queue = [] + self._is_alive = False + self._delay = delay + + def put(self, action, *args, **kwargs): + """.""" + if callable(action): + self._queue.append((action, args, kwargs)) + self._start() + + def _start(self): + """.""" + if not self._is_alive: + self._is_alive = True + thread = threading.Thread(target=self._run) + thread.start() + + def _run(self): + """.""" + while self._queue: + params = self._queue.pop(0) + action = params[0] + args = params[1] + kwargs = params[2] + if args and kwargs: + action(*args, **kwargs) + elif args: + action(*args) + elif kwargs: + action(**kwargs) + else: + action() + time.sleep(self._delay) + self._is_alive = False + + +class TaskQueue(object): + """.""" + + def __init__(self, consumer=sys.stdout.write, delay=0): + """.""" + self._queue = [] + self._is_alive = False + self._consumer = consumer + self._delay = delay + self._callable = callable(self._consumer) + + def put(self, *args): + """.""" + if self._callable: + self._queue.append(args) + self._start() + + def _start(self): + """.""" + if not self._is_alive: + self._is_alive = True + thread = threading.Thread(target=self._run) + thread.start() + + def _run(self): + """.""" + while self._queue: + args = self._queue.pop(0) + if isinstance(args, dict): + self._consumer(args) + else: + self._consumer(*args) + time.sleep(self._delay) + self._is_alive = False diff --git a/libs/serial/LICENSE.txt b/libs/serial/LICENSE.txt new file mode 100644 index 0000000..22a93d0 --- /dev/null +++ b/libs/serial/LICENSE.txt @@ -0,0 +1,39 @@ +Copyright (c) 2001-2016 Chris Liechti +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--------------------------------------------------------------------------- +Note: +Individual files contain the following tag instead of the full license text. + + SPDX-License-Identifier: BSD-3-Clause + +This enables machine processing of license information based on the SPDX +License Identifiers that are here available: http://spdx.org/licenses/ diff --git a/libs/serial/README.rst b/libs/serial/README.rst new file mode 100644 index 0000000..6636b0b --- /dev/null +++ b/libs/serial/README.rst @@ -0,0 +1,48 @@ +================================= + pySerial |build-status| |docs| +================================= + +Overview +======== +This module encapsulates the access for the serial port. It provides backends +for Python_ running on Windows, OSX, Linux, BSD (possibly any POSIX compliant +system) and IronPython. The module named "serial" automatically selects the +appropriate backend. + +- Project Homepage: https://github.com/pyserial/pyserial +- Download Page: https://pypi.python.org/pypi/pyserial + +BSD license, (C) 2001-2016 Chris Liechti + + +Documentation +============= +For API documentation, usage and examples see files in the "documentation" +directory. The ".rst" files can be read in any text editor or being converted to +HTML or PDF using Sphinx_. A HTML version is online at +https://pythonhosted.org/pyserial/ + +Examples +======== +Examples and unit tests are in the directory examples_. + + +Installation +============ +``pip install pyserial`` should work for most users. + +Detailed information can be found in `documentation/pyserial.rst`_. + +The usual setup.py for Python_ libraries is used for the source distribution. +Windows installers are also available (see download link above). + +.. _`documentation/pyserial.rst`: https://github.com/pyserial/pyserial/blob/master/documentation/pyserial.rst#installation +.. _examples: https://github.com/pyserial/pyserial/blob/master/examples +.. _Python: http://python.org/ +.. _Sphinx: http://sphinx-doc.org/ +.. |build-status| image:: https://travis-ci.org/pyserial/pyserial.svg?branch=master + :target: https://travis-ci.org/pyserial/pyserial + :alt: Build status +.. |docs| image:: https://readthedocs.org/projects/pyserial/badge/?version=latest + :target: http://pyserial.readthedocs.io/ + :alt: Documentation diff --git a/libs/serial/__init__.py b/libs/serial/__init__.py new file mode 100644 index 0000000..4cd3a25 --- /dev/null +++ b/libs/serial/__init__.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# +# This is a wrapper module for different platform implementations +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2016 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +import sys +import importlib + +from serial.serialutil import * +#~ SerialBase, SerialException, to_bytes, iterbytes + +__version__ = '3.2.1' + +VERSION = __version__ + +# pylint: disable=wrong-import-position +if sys.platform == 'cli': + from serial.serialcli import Serial +else: + import os + # chose an implementation, depending on os + if os.name == 'nt': # sys.platform == 'win32': + from serial.serialwin32 import Serial + elif os.name == 'posix': + from serial.serialposix import Serial, PosixPollSerial, VTIMESerial # noqa + elif os.name == 'java': + from serial.serialjava import Serial + else: + raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name)) + + +protocol_handler_packages = [ + 'serial.urlhandler', +] + + +def serial_for_url(url, *args, **kwargs): + """\ + Get an instance of the Serial class, depending on port/url. The port is not + opened when the keyword parameter 'do_not_open' is true, by default it + is. All other parameters are directly passed to the __init__ method when + the port is instantiated. + + The list of package names that is searched for protocol handlers is kept in + ``protocol_handler_packages``. + + e.g. we want to support a URL ``foobar://``. A module + ``my_handlers.protocol_foobar`` is provided by the user. Then + ``protocol_handler_packages.append("my_handlers")`` would extend the search + path so that ``serial_for_url("foobar://"))`` would work. + """ + # check and remove extra parameter to not confuse the Serial class + do_open = not kwargs.pop('do_not_open', False) + # the default is to use the native implementation + klass = Serial + try: + url_lowercase = url.lower() + except AttributeError: + # it's not a string, use default + pass + else: + # if it is an URL, try to import the handler module from the list of possible packages + if '://' in url_lowercase: + protocol = url_lowercase.split('://', 1)[0] + module_name = '.protocol_{}'.format(protocol) + for package_name in protocol_handler_packages: + try: + importlib.import_module(package_name) + handler_module = importlib.import_module(module_name, package_name) + except ImportError: + continue + else: + if hasattr(handler_module, 'serial_class_for_url'): + url, klass = handler_module.serial_class_for_url(url) + else: + klass = handler_module.Serial + break + else: + raise ValueError('invalid URL, protocol {!r} not known'.format(protocol)) + # instantiate and open when desired + instance = klass(None, *args, **kwargs) + instance.port = url + if do_open: + instance.open() + return instance diff --git a/libs/serial/rfc2217.py b/libs/serial/rfc2217.py new file mode 100644 index 0000000..dee5c2b --- /dev/null +++ b/libs/serial/rfc2217.py @@ -0,0 +1,1342 @@ +#! python +# +# This module implements a RFC2217 compatible client. RF2217 descibes a +# protocol to access serial ports over TCP/IP and allows setting the baud rate, +# modem control lines etc. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +# TODO: +# - setting control line -> answer is not checked (had problems with one of the +# severs). consider implementing a compatibility mode flag to make check +# conditional +# - write timeout not implemented at all + +# ########################################################################### +# observations and issues with servers +# =========================================================================== +# sredird V2.2.1 +# - http://www.ibiblio.org/pub/Linux/system/serial/ sredird-2.2.2.tar.gz +# - does not acknowledge SET_CONTROL (RTS/DTR) correctly, always responding +# [105 1] instead of the actual value. +# - SET_BAUDRATE answer contains 4 extra null bytes -> probably for larger +# numbers than 2**32? +# - To get the signature [COM_PORT_OPTION 0] has to be sent. +# - run a server: while true; do nc -l -p 7000 -c "sredird debug /dev/ttyUSB0 /var/lock/sredir"; done +# =========================================================================== +# telnetcpcd (untested) +# - http://ftp.wayne.edu/kermit/sredird/telnetcpcd-1.09.tar.gz +# - To get the signature [COM_PORT_OPTION] w/o data has to be sent. +# =========================================================================== +# ser2net +# - does not negotiate BINARY or COM_PORT_OPTION for his side but at least +# acknowledges that the client activates these options +# - The configuration may be that the server prints a banner. As this client +# implementation does a flushInput on connect, this banner is hidden from +# the user application. +# - NOTIFY_MODEMSTATE: the poll interval of the server seems to be one +# second. +# - To get the signature [COM_PORT_OPTION 0] has to be sent. +# - run a server: run ser2net daemon, in /etc/ser2net.conf: +# 2000:telnet:0:/dev/ttyS0:9600 remctl banner +# ########################################################################### + +# How to identify ports? pySerial might want to support other protocols in the +# future, so lets use an URL scheme. +# for RFC2217 compliant servers we will use this: +# rfc2217://:[?option[&option...]] +# +# options: +# - "logging" set log level print diagnostic messages (e.g. "logging=debug") +# - "ign_set_control": do not look at the answers to SET_CONTROL +# - "poll_modem": issue NOTIFY_MODEMSTATE requests when CTS/DTR/RI/CD is read. +# Without this option it expects that the server sends notifications +# automatically on change (which most servers do and is according to the +# RFC). +# the order of the options is not relevant + +import logging +import socket +import struct +import threading +import time +try: + import urlparse +except ImportError: + import urllib.parse as urlparse +try: + import Queue +except ImportError: + import queue as Queue + +import serial +from serial.serialutil import SerialBase, SerialException, to_bytes, \ + iterbytes, portNotOpenError, Timeout + +# port string is expected to be something like this: +# rfc2217://host:port +# host may be an IP or including domain, whatever. +# port is 0...65535 + +# map log level names to constants. used in from_url() +LOGGER_LEVELS = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, +} + + +# telnet protocol characters +SE = b'\xf0' # Subnegotiation End +NOP = b'\xf1' # No Operation +DM = b'\xf2' # Data Mark +BRK = b'\xf3' # Break +IP = b'\xf4' # Interrupt process +AO = b'\xf5' # Abort output +AYT = b'\xf6' # Are You There +EC = b'\xf7' # Erase Character +EL = b'\xf8' # Erase Line +GA = b'\xf9' # Go Ahead +SB = b'\xfa' # Subnegotiation Begin +WILL = b'\xfb' +WONT = b'\xfc' +DO = b'\xfd' +DONT = b'\xfe' +IAC = b'\xff' # Interpret As Command +IAC_DOUBLED = b'\xff\xff' + +# selected telnet options +BINARY = b'\x00' # 8-bit data path +ECHO = b'\x01' # echo +SGA = b'\x03' # suppress go ahead + +# RFC2217 +COM_PORT_OPTION = b'\x2c' + +# Client to Access Server +SET_BAUDRATE = b'\x01' +SET_DATASIZE = b'\x02' +SET_PARITY = b'\x03' +SET_STOPSIZE = b'\x04' +SET_CONTROL = b'\x05' +NOTIFY_LINESTATE = b'\x06' +NOTIFY_MODEMSTATE = b'\x07' +FLOWCONTROL_SUSPEND = b'\x08' +FLOWCONTROL_RESUME = b'\x09' +SET_LINESTATE_MASK = b'\x0a' +SET_MODEMSTATE_MASK = b'\x0b' +PURGE_DATA = b'\x0c' + +SERVER_SET_BAUDRATE = b'\x65' +SERVER_SET_DATASIZE = b'\x66' +SERVER_SET_PARITY = b'\x67' +SERVER_SET_STOPSIZE = b'\x68' +SERVER_SET_CONTROL = b'\x69' +SERVER_NOTIFY_LINESTATE = b'\x6a' +SERVER_NOTIFY_MODEMSTATE = b'\x6b' +SERVER_FLOWCONTROL_SUSPEND = b'\x6c' +SERVER_FLOWCONTROL_RESUME = b'\x6d' +SERVER_SET_LINESTATE_MASK = b'\x6e' +SERVER_SET_MODEMSTATE_MASK = b'\x6f' +SERVER_PURGE_DATA = b'\x70' + +RFC2217_ANSWER_MAP = { + SET_BAUDRATE: SERVER_SET_BAUDRATE, + SET_DATASIZE: SERVER_SET_DATASIZE, + SET_PARITY: SERVER_SET_PARITY, + SET_STOPSIZE: SERVER_SET_STOPSIZE, + SET_CONTROL: SERVER_SET_CONTROL, + NOTIFY_LINESTATE: SERVER_NOTIFY_LINESTATE, + NOTIFY_MODEMSTATE: SERVER_NOTIFY_MODEMSTATE, + FLOWCONTROL_SUSPEND: SERVER_FLOWCONTROL_SUSPEND, + FLOWCONTROL_RESUME: SERVER_FLOWCONTROL_RESUME, + SET_LINESTATE_MASK: SERVER_SET_LINESTATE_MASK, + SET_MODEMSTATE_MASK: SERVER_SET_MODEMSTATE_MASK, + PURGE_DATA: SERVER_PURGE_DATA, +} + +SET_CONTROL_REQ_FLOW_SETTING = b'\x00' # Request Com Port Flow Control Setting (outbound/both) +SET_CONTROL_USE_NO_FLOW_CONTROL = b'\x01' # Use No Flow Control (outbound/both) +SET_CONTROL_USE_SW_FLOW_CONTROL = b'\x02' # Use XON/XOFF Flow Control (outbound/both) +SET_CONTROL_USE_HW_FLOW_CONTROL = b'\x03' # Use HARDWARE Flow Control (outbound/both) +SET_CONTROL_REQ_BREAK_STATE = b'\x04' # Request BREAK State +SET_CONTROL_BREAK_ON = b'\x05' # Set BREAK State ON +SET_CONTROL_BREAK_OFF = b'\x06' # Set BREAK State OFF +SET_CONTROL_REQ_DTR = b'\x07' # Request DTR Signal State +SET_CONTROL_DTR_ON = b'\x08' # Set DTR Signal State ON +SET_CONTROL_DTR_OFF = b'\x09' # Set DTR Signal State OFF +SET_CONTROL_REQ_RTS = b'\x0a' # Request RTS Signal State +SET_CONTROL_RTS_ON = b'\x0b' # Set RTS Signal State ON +SET_CONTROL_RTS_OFF = b'\x0c' # Set RTS Signal State OFF +SET_CONTROL_REQ_FLOW_SETTING_IN = b'\x0d' # Request Com Port Flow Control Setting (inbound) +SET_CONTROL_USE_NO_FLOW_CONTROL_IN = b'\x0e' # Use No Flow Control (inbound) +SET_CONTROL_USE_SW_FLOW_CONTOL_IN = b'\x0f' # Use XON/XOFF Flow Control (inbound) +SET_CONTROL_USE_HW_FLOW_CONTOL_IN = b'\x10' # Use HARDWARE Flow Control (inbound) +SET_CONTROL_USE_DCD_FLOW_CONTROL = b'\x11' # Use DCD Flow Control (outbound/both) +SET_CONTROL_USE_DTR_FLOW_CONTROL = b'\x12' # Use DTR Flow Control (inbound) +SET_CONTROL_USE_DSR_FLOW_CONTROL = b'\x13' # Use DSR Flow Control (outbound/both) + +LINESTATE_MASK_TIMEOUT = 128 # Time-out Error +LINESTATE_MASK_SHIFTREG_EMPTY = 64 # Transfer Shift Register Empty +LINESTATE_MASK_TRANSREG_EMPTY = 32 # Transfer Holding Register Empty +LINESTATE_MASK_BREAK_DETECT = 16 # Break-detect Error +LINESTATE_MASK_FRAMING_ERROR = 8 # Framing Error +LINESTATE_MASK_PARTIY_ERROR = 4 # Parity Error +LINESTATE_MASK_OVERRUN_ERROR = 2 # Overrun Error +LINESTATE_MASK_DATA_READY = 1 # Data Ready + +MODEMSTATE_MASK_CD = 128 # Receive Line Signal Detect (also known as Carrier Detect) +MODEMSTATE_MASK_RI = 64 # Ring Indicator +MODEMSTATE_MASK_DSR = 32 # Data-Set-Ready Signal State +MODEMSTATE_MASK_CTS = 16 # Clear-To-Send Signal State +MODEMSTATE_MASK_CD_CHANGE = 8 # Delta Receive Line Signal Detect +MODEMSTATE_MASK_RI_CHANGE = 4 # Trailing-edge Ring Detector +MODEMSTATE_MASK_DSR_CHANGE = 2 # Delta Data-Set-Ready +MODEMSTATE_MASK_CTS_CHANGE = 1 # Delta Clear-To-Send + +PURGE_RECEIVE_BUFFER = b'\x01' # Purge access server receive data buffer +PURGE_TRANSMIT_BUFFER = b'\x02' # Purge access server transmit data buffer +PURGE_BOTH_BUFFERS = b'\x03' # Purge both the access server receive data + # buffer and the access server transmit data buffer + + +RFC2217_PARITY_MAP = { + serial.PARITY_NONE: 1, + serial.PARITY_ODD: 2, + serial.PARITY_EVEN: 3, + serial.PARITY_MARK: 4, + serial.PARITY_SPACE: 5, +} +RFC2217_REVERSE_PARITY_MAP = dict((v, k) for k, v in RFC2217_PARITY_MAP.items()) + +RFC2217_STOPBIT_MAP = { + serial.STOPBITS_ONE: 1, + serial.STOPBITS_ONE_POINT_FIVE: 3, + serial.STOPBITS_TWO: 2, +} +RFC2217_REVERSE_STOPBIT_MAP = dict((v, k) for k, v in RFC2217_STOPBIT_MAP.items()) + +# Telnet filter states +M_NORMAL = 0 +M_IAC_SEEN = 1 +M_NEGOTIATE = 2 + +# TelnetOption and TelnetSubnegotiation states +REQUESTED = 'REQUESTED' +ACTIVE = 'ACTIVE' +INACTIVE = 'INACTIVE' +REALLY_INACTIVE = 'REALLY_INACTIVE' + + +class TelnetOption(object): + """Manage a single telnet option, keeps track of DO/DONT WILL/WONT.""" + + def __init__(self, connection, name, option, send_yes, send_no, ack_yes, + ack_no, initial_state, activation_callback=None): + """\ + Initialize option. + :param connection: connection used to transmit answers + :param name: a readable name for debug outputs + :param send_yes: what to send when option is to be enabled. + :param send_no: what to send when option is to be disabled. + :param ack_yes: what to expect when remote agrees on option. + :param ack_no: what to expect when remote disagrees on option. + :param initial_state: options initialized with REQUESTED are tried to + be enabled on startup. use INACTIVE for all others. + """ + self.connection = connection + self.name = name + self.option = option + self.send_yes = send_yes + self.send_no = send_no + self.ack_yes = ack_yes + self.ack_no = ack_no + self.state = initial_state + self.active = False + self.activation_callback = activation_callback + + def __repr__(self): + """String for debug outputs""" + return "{o.name}:{o.active}({o.state})".format(o=self) + + def process_incoming(self, command): + """\ + A DO/DONT/WILL/WONT was received for this option, update state and + answer when needed. + """ + if command == self.ack_yes: + if self.state is REQUESTED: + self.state = ACTIVE + self.active = True + if self.activation_callback is not None: + self.activation_callback() + elif self.state is ACTIVE: + pass + elif self.state is INACTIVE: + self.state = ACTIVE + self.connection.telnet_send_option(self.send_yes, self.option) + self.active = True + if self.activation_callback is not None: + self.activation_callback() + elif self.state is REALLY_INACTIVE: + self.connection.telnet_send_option(self.send_no, self.option) + else: + raise ValueError('option in illegal state {!r}'.format(self)) + elif command == self.ack_no: + if self.state is REQUESTED: + self.state = INACTIVE + self.active = False + elif self.state is ACTIVE: + self.state = INACTIVE + self.connection.telnet_send_option(self.send_no, self.option) + self.active = False + elif self.state is INACTIVE: + pass + elif self.state is REALLY_INACTIVE: + pass + else: + raise ValueError('option in illegal state {!r}'.format(self)) + + +class TelnetSubnegotiation(object): + """\ + A object to handle subnegotiation of options. In this case actually + sub-sub options for RFC 2217. It is used to track com port options. + """ + + def __init__(self, connection, name, option, ack_option=None): + if ack_option is None: + ack_option = option + self.connection = connection + self.name = name + self.option = option + self.value = None + self.ack_option = ack_option + self.state = INACTIVE + + def __repr__(self): + """String for debug outputs.""" + return "{sn.name}:{sn.state}".format(sn=self) + + def set(self, value): + """\ + Request a change of the value. a request is sent to the server. if + the client needs to know if the change is performed he has to check the + state of this object. + """ + self.value = value + self.state = REQUESTED + self.connection.rfc2217_send_subnegotiation(self.option, self.value) + if self.connection.logger: + self.connection.logger.debug("SB Requesting {} -> {!r}".format(self.name, self.value)) + + def is_ready(self): + """\ + Check if answer from server has been received. when server rejects + the change, raise a ValueError. + """ + if self.state == REALLY_INACTIVE: + raise ValueError("remote rejected value for option {!r}".format(self.name)) + return self.state == ACTIVE + # add property to have a similar interface as TelnetOption + active = property(is_ready) + + def wait(self, timeout=3): + """\ + Wait until the subnegotiation has been acknowledged or timeout. It + can also throw a value error when the answer from the server does not + match the value sent. + """ + timeout_timer = Timeout(timeout) + while not timeout_timer.expired(): + time.sleep(0.05) # prevent 100% CPU load + if self.is_ready(): + break + else: + raise SerialException("timeout while waiting for option {!r}".format(self.name)) + + def check_answer(self, suboption): + """\ + Check an incoming subnegotiation block. The parameter already has + cut off the header like sub option number and com port option value. + """ + if self.value == suboption[:len(self.value)]: + self.state = ACTIVE + else: + # error propagation done in is_ready + self.state = REALLY_INACTIVE + if self.connection.logger: + self.connection.logger.debug("SB Answer {} -> {!r} -> {}".format(self.name, suboption, self.state)) + + +class Serial(SerialBase): + """Serial port implementation for RFC 2217 remote serial ports.""" + + BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200) + + def __init__(self, *args, **kwargs): + super(Serial, self).__init__(*args, **kwargs) + self._thread = None + self._socket = None + self._linestate = 0 + self._modemstate = None + self._modemstate_timeout = Timeout(-1) + self._remote_suspend_flow = False + self._write_lock = None + self.logger = None + self._ignore_set_control_answer = False + self._poll_modem_state = False + self._network_timeout = 3 + self._telnet_options = None + self._rfc2217_port_settings = None + self._rfc2217_options = None + self._read_buffer = None + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened. + """ + self.logger = None + self._ignore_set_control_answer = False + self._poll_modem_state = False + self._network_timeout = 3 + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + try: + self._socket = socket.create_connection(self.from_url(self.portstr), timeout=5) # XXX good value? + self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except Exception as msg: + self._socket = None + raise SerialException("Could not open port {}: {}".format(self.portstr, msg)) + + # use a thread save queue as buffer. it also simplifies implementing + # the read timeout + self._read_buffer = Queue.Queue() + # to ensure that user writes does not interfere with internal + # telnet/rfc2217 options establish a lock + self._write_lock = threading.Lock() + # name the following separately so that, below, a check can be easily done + mandadory_options = [ + TelnetOption(self, 'we-BINARY', BINARY, WILL, WONT, DO, DONT, INACTIVE), + TelnetOption(self, 'we-RFC2217', COM_PORT_OPTION, WILL, WONT, DO, DONT, REQUESTED), + ] + # all supported telnet options + self._telnet_options = [ + TelnetOption(self, 'ECHO', ECHO, DO, DONT, WILL, WONT, REQUESTED), + TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED), + TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, REQUESTED), + TelnetOption(self, 'they-BINARY', BINARY, DO, DONT, WILL, WONT, INACTIVE), + TelnetOption(self, 'they-RFC2217', COM_PORT_OPTION, DO, DONT, WILL, WONT, REQUESTED), + ] + mandadory_options + # RFC 2217 specific states + # COM port settings + self._rfc2217_port_settings = { + 'baudrate': TelnetSubnegotiation(self, 'baudrate', SET_BAUDRATE, SERVER_SET_BAUDRATE), + 'datasize': TelnetSubnegotiation(self, 'datasize', SET_DATASIZE, SERVER_SET_DATASIZE), + 'parity': TelnetSubnegotiation(self, 'parity', SET_PARITY, SERVER_SET_PARITY), + 'stopsize': TelnetSubnegotiation(self, 'stopsize', SET_STOPSIZE, SERVER_SET_STOPSIZE), + } + # There are more subnegotiation objects, combine all in one dictionary + # for easy access + self._rfc2217_options = { + 'purge': TelnetSubnegotiation(self, 'purge', PURGE_DATA, SERVER_PURGE_DATA), + 'control': TelnetSubnegotiation(self, 'control', SET_CONTROL, SERVER_SET_CONTROL), + } + self._rfc2217_options.update(self._rfc2217_port_settings) + # cache for line and modem states that the server sends to us + self._linestate = 0 + self._modemstate = None + self._modemstate_timeout = Timeout(-1) + # RFC 2217 flow control between server and client + self._remote_suspend_flow = False + + self.is_open = True + self._thread = threading.Thread(target=self._telnet_read_loop) + self._thread.setDaemon(True) + self._thread.setName('pySerial RFC 2217 reader thread for {}'.format(self._port)) + self._thread.start() + + try: # must clean-up if open fails + # negotiate Telnet/RFC 2217 -> send initial requests + for option in self._telnet_options: + if option.state is REQUESTED: + self.telnet_send_option(option.send_yes, option.option) + # now wait until important options are negotiated + timeout = Timeout(self._network_timeout) + while not timeout.expired(): + time.sleep(0.05) # prevent 100% CPU load + if sum(o.active for o in mandadory_options) == sum(o.state != INACTIVE for o in mandadory_options): + break + else: + raise SerialException( + "Remote does not seem to support RFC2217 or BINARY mode {!r}".format(mandadory_options)) + if self.logger: + self.logger.info("Negotiated options: {}".format(self._telnet_options)) + + # fine, go on, set RFC 2271 specific things + self._reconfigure_port() + # all things set up get, now a clean start + if not self._dsrdtr: + self._update_dtr_state() + if not self._rtscts: + self._update_rts_state() + self.reset_input_buffer() + self.reset_output_buffer() + except: + self.close() + raise + + def _reconfigure_port(self): + """Set communication parameters on opened port.""" + if self._socket is None: + raise SerialException("Can only operate on open ports") + + # if self._timeout != 0 and self._interCharTimeout is not None: + # XXX + + if self._write_timeout is not None: + raise NotImplementedError('write_timeout is currently not supported') + # XXX + + # Setup the connection + # to get good performance, all parameter changes are sent first... + if not 0 < self._baudrate < 2 ** 32: + raise ValueError("invalid baudrate: {!r}".format(self._baudrate)) + self._rfc2217_port_settings['baudrate'].set(struct.pack(b'!I', self._baudrate)) + self._rfc2217_port_settings['datasize'].set(struct.pack(b'!B', self._bytesize)) + self._rfc2217_port_settings['parity'].set(struct.pack(b'!B', RFC2217_PARITY_MAP[self._parity])) + self._rfc2217_port_settings['stopsize'].set(struct.pack(b'!B', RFC2217_STOPBIT_MAP[self._stopbits])) + + # and now wait until parameters are active + items = self._rfc2217_port_settings.values() + if self.logger: + self.logger.debug("Negotiating settings: {}".format(items)) + timeout = Timeout(self._network_timeout) + while not timeout.expired(): + time.sleep(0.05) # prevent 100% CPU load + if sum(o.active for o in items) == len(items): + break + else: + raise SerialException("Remote does not accept parameter change (RFC2217): {!r}".format(items)) + if self.logger: + self.logger.info("Negotiated settings: {}".format(items)) + + if self._rtscts and self._xonxoff: + raise ValueError('xonxoff and rtscts together are not supported') + elif self._rtscts: + self.rfc2217_set_control(SET_CONTROL_USE_HW_FLOW_CONTROL) + elif self._xonxoff: + self.rfc2217_set_control(SET_CONTROL_USE_SW_FLOW_CONTROL) + else: + self.rfc2217_set_control(SET_CONTROL_USE_NO_FLOW_CONTROL) + + def close(self): + """Close port""" + self.is_open = False + if self._socket: + try: + self._socket.shutdown(socket.SHUT_RDWR) + self._socket.close() + except: + # ignore errors. + pass + if self._thread: + self._thread.join(7) # XXX more than socket timeout + self._thread = None + # in case of quick reconnects, give the server some time + time.sleep(0.3) + self._socket = None + + def from_url(self, url): + """\ + extract host and port from an URL string, other settings are extracted + an stored in instance + """ + parts = urlparse.urlsplit(url) + if parts.scheme != "rfc2217": + raise SerialException( + 'expected a string in the form ' + '"rfc2217://:[?option[&option...]]": ' + 'not starting with rfc2217:// ({!r})'.format(parts.scheme)) + try: + # process options now, directly altering self + for option, values in urlparse.parse_qs(parts.query, True).items(): + if option == 'logging': + logging.basicConfig() # XXX is that good to call it here? + self.logger = logging.getLogger('pySerial.rfc2217') + self.logger.setLevel(LOGGER_LEVELS[values[0]]) + self.logger.debug('enabled logging') + elif option == 'ign_set_control': + self._ignore_set_control_answer = True + elif option == 'poll_modem': + self._poll_modem_state = True + elif option == 'timeout': + self._network_timeout = float(values[0]) + else: + raise ValueError('unknown option: {!r}'.format(option)) + if not 0 <= parts.port < 65536: + raise ValueError("port not in range 0...65535") + except ValueError as e: + raise SerialException( + 'expected a string in the form ' + '"rfc2217://:[?option[&option...]]": {}'.format(e)) + return (parts.hostname, parts.port) + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of bytes currently in the input buffer.""" + if not self.is_open: + raise portNotOpenError + return self._read_buffer.qsize() + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise portNotOpenError + data = bytearray() + try: + while len(data) < size: + if self._thread is None: + raise SerialException('connection failed (reader thread died)') + data += self._read_buffer.get(True, self._timeout) + except Queue.Empty: # -> timeout + pass + return bytes(data) + + def write(self, data): + """\ + Output the given byte string over the serial port. Can block if the + connection is blocked. May raise SerialException if the connection is + closed. + """ + if not self.is_open: + raise portNotOpenError + with self._write_lock: + try: + self._socket.sendall(to_bytes(data).replace(IAC, IAC_DOUBLED)) + except socket.error as e: + raise SerialException("connection failed (socket error): {}".format(e)) + return len(data) + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.is_open: + raise portNotOpenError + self.rfc2217_send_purge(PURGE_RECEIVE_BUFFER) + # empty read buffer + while self._read_buffer.qsize(): + self._read_buffer.get(False) + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and + discarding all that is in the buffer. + """ + if not self.is_open: + raise portNotOpenError + self.rfc2217_send_purge(PURGE_TRANSMIT_BUFFER) + + def _update_break_state(self): + """\ + Set break: Controls TXD. When active, to transmitting is + possible. + """ + if not self.is_open: + raise portNotOpenError + if self.logger: + self.logger.info('set BREAK to {}'.format('active' if self._break_state else 'inactive')) + if self._break_state: + self.rfc2217_set_control(SET_CONTROL_BREAK_ON) + else: + self.rfc2217_set_control(SET_CONTROL_BREAK_OFF) + + def _update_rts_state(self): + """Set terminal status line: Request To Send.""" + if not self.is_open: + raise portNotOpenError + if self.logger: + self.logger.info('set RTS to {}'.format('active' if self._rts_state else 'inactive')) + if self._rts_state: + self.rfc2217_set_control(SET_CONTROL_RTS_ON) + else: + self.rfc2217_set_control(SET_CONTROL_RTS_OFF) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready.""" + if not self.is_open: + raise portNotOpenError + if self.logger: + self.logger.info('set DTR to {}'.format('active' if self._dtr_state else 'inactive')) + if self._dtr_state: + self.rfc2217_set_control(SET_CONTROL_DTR_ON) + else: + self.rfc2217_set_control(SET_CONTROL_DTR_OFF) + + @property + def cts(self): + """Read terminal status line: Clear To Send.""" + if not self.is_open: + raise portNotOpenError + return bool(self.get_modem_state() & MODEMSTATE_MASK_CTS) + + @property + def dsr(self): + """Read terminal status line: Data Set Ready.""" + if not self.is_open: + raise portNotOpenError + return bool(self.get_modem_state() & MODEMSTATE_MASK_DSR) + + @property + def ri(self): + """Read terminal status line: Ring Indicator.""" + if not self.is_open: + raise portNotOpenError + return bool(self.get_modem_state() & MODEMSTATE_MASK_RI) + + @property + def cd(self): + """Read terminal status line: Carrier Detect.""" + if not self.is_open: + raise portNotOpenError + return bool(self.get_modem_state() & MODEMSTATE_MASK_CD) + + # - - - platform specific - - - + # None so far + + # - - - RFC2217 specific - - - + + def _telnet_read_loop(self): + """Read loop for the socket.""" + mode = M_NORMAL + suboption = None + try: + while self.is_open: + try: + data = self._socket.recv(1024) + except socket.timeout: + # just need to get out of recv form time to time to check if + # still alive + continue + except socket.error as e: + # connection fails -> terminate loop + if self.logger: + self.logger.debug("socket error in reader thread: {}".format(e)) + break + if not data: + break # lost connection + for byte in iterbytes(data): + if mode == M_NORMAL: + # interpret as command or as data + if byte == IAC: + mode = M_IAC_SEEN + else: + # store data in read buffer or sub option buffer + # depending on state + if suboption is not None: + suboption += byte + else: + self._read_buffer.put(byte) + elif mode == M_IAC_SEEN: + if byte == IAC: + # interpret as command doubled -> insert character + # itself + if suboption is not None: + suboption += IAC + else: + self._read_buffer.put(IAC) + mode = M_NORMAL + elif byte == SB: + # sub option start + suboption = bytearray() + mode = M_NORMAL + elif byte == SE: + # sub option end -> process it now + self._telnet_process_subnegotiation(bytes(suboption)) + suboption = None + mode = M_NORMAL + elif byte in (DO, DONT, WILL, WONT): + # negotiation + telnet_command = byte + mode = M_NEGOTIATE + else: + # other telnet commands + self._telnet_process_command(byte) + mode = M_NORMAL + elif mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following + self._telnet_negotiate_option(telnet_command, byte) + mode = M_NORMAL + finally: + self._thread = None + if self.logger: + self.logger.debug("read thread terminated") + + # - incoming telnet commands and options + + def _telnet_process_command(self, command): + """Process commands other than DO, DONT, WILL, WONT.""" + # Currently none. RFC2217 only uses negotiation and subnegotiation. + if self.logger: + self.logger.warning("ignoring Telnet command: {!r}".format(command)) + + def _telnet_negotiate_option(self, command, option): + """Process incoming DO, DONT, WILL, WONT.""" + # check our registered telnet options and forward command to them + # they know themselves if they have to answer or not + known = False + for item in self._telnet_options: + # can have more than one match! as some options are duplicated for + # 'us' and 'them' + if item.option == option: + item.process_incoming(command) + known = True + if not known: + # handle unknown options + # only answer to positive requests and deny them + if command == WILL or command == DO: + self.telnet_send_option((DONT if command == WILL else WONT), option) + if self.logger: + self.logger.warning("rejected Telnet option: {!r}".format(option)) + + def _telnet_process_subnegotiation(self, suboption): + """Process subnegotiation, the data between IAC SB and IAC SE.""" + if suboption[0:1] == COM_PORT_OPTION: + if suboption[1:2] == SERVER_NOTIFY_LINESTATE and len(suboption) >= 3: + self._linestate = ord(suboption[2:3]) # ensure it is a number + if self.logger: + self.logger.info("NOTIFY_LINESTATE: {}".format(self._linestate)) + elif suboption[1:2] == SERVER_NOTIFY_MODEMSTATE and len(suboption) >= 3: + self._modemstate = ord(suboption[2:3]) # ensure it is a number + if self.logger: + self.logger.info("NOTIFY_MODEMSTATE: {}".format(self._modemstate)) + # update time when we think that a poll would make sense + self._modemstate_timeout.restart(0.3) + elif suboption[1:2] == FLOWCONTROL_SUSPEND: + self._remote_suspend_flow = True + elif suboption[1:2] == FLOWCONTROL_RESUME: + self._remote_suspend_flow = False + else: + for item in self._rfc2217_options.values(): + if item.ack_option == suboption[1:2]: + #~ print "processing COM_PORT_OPTION: %r" % list(suboption[1:]) + item.check_answer(bytes(suboption[2:])) + break + else: + if self.logger: + self.logger.warning("ignoring COM_PORT_OPTION: {!r}".format(suboption)) + else: + if self.logger: + self.logger.warning("ignoring subnegotiation: {!r}".format(suboption)) + + # - outgoing telnet commands and options + + def _internal_raw_write(self, data): + """internal socket write with no data escaping. used to send telnet stuff.""" + with self._write_lock: + self._socket.sendall(data) + + def telnet_send_option(self, action, option): + """Send DO, DONT, WILL, WONT.""" + self._internal_raw_write(to_bytes([IAC, action, option])) + + def rfc2217_send_subnegotiation(self, option, value=b''): + """Subnegotiation of RFC2217 parameters.""" + value = value.replace(IAC, IAC_DOUBLED) + self._internal_raw_write(to_bytes([IAC, SB, COM_PORT_OPTION, option] + list(value) + [IAC, SE])) + + def rfc2217_send_purge(self, value): + """\ + Send purge request to the remote. + (PURGE_RECEIVE_BUFFER / PURGE_TRANSMIT_BUFFER / PURGE_BOTH_BUFFERS) + """ + item = self._rfc2217_options['purge'] + item.set(value) # transmit desired purge type + item.wait(self._network_timeout) # wait for acknowledge from the server + + def rfc2217_set_control(self, value): + """transmit change of control line to remote""" + item = self._rfc2217_options['control'] + item.set(value) # transmit desired control type + if self._ignore_set_control_answer: + # answers are ignored when option is set. compatibility mode for + # servers that answer, but not the expected one... (or no answer + # at all) i.e. sredird + time.sleep(0.1) # this helps getting the unit tests passed + else: + item.wait(self._network_timeout) # wait for acknowledge from the server + + def rfc2217_flow_server_ready(self): + """\ + check if server is ready to receive data. block for some time when + not. + """ + #~ if self._remote_suspend_flow: + #~ wait--- + + def get_modem_state(self): + """\ + get last modem state (cached value. If value is "old", request a new + one. This cache helps that we don't issue to many requests when e.g. all + status lines, one after the other is queried by the user (getCTS, getDSR + etc.) + """ + # active modem state polling enabled? is the value fresh enough? + if self._poll_modem_state and self._modemstate_timeout.expired(): + if self.logger: + self.logger.debug('polling modem state') + # when it is older, request an update + self.rfc2217_send_subnegotiation(NOTIFY_MODEMSTATE) + timeout = Timeout(self._network_timeout) + while not timeout.expired(): + time.sleep(0.05) # prevent 100% CPU load + # when expiration time is updated, it means that there is a new + # value + if not self._modemstate_timeout.expired(): + break + else: + if self.logger: + self.logger.warning('poll for modem state failed') + # even when there is a timeout, do not generate an error just + # return the last known value. this way we can support buggy + # servers that do not respond to polls, but send automatic + # updates. + if self._modemstate is not None: + if self.logger: + self.logger.debug('using cached modem state') + return self._modemstate + else: + # never received a notification from the server + raise SerialException("remote sends no NOTIFY_MODEMSTATE") + + +############################################################################# +# The following is code that helps implementing an RFC 2217 server. + +class PortManager(object): + """\ + This class manages the state of Telnet and RFC 2217. It needs a serial + instance and a connection to work with. Connection is expected to implement + a (thread safe) write function, that writes the string to the network. + """ + + def __init__(self, serial_port, connection, logger=None): + self.serial = serial_port + self.connection = connection + self.logger = logger + self._client_is_rfc2217 = False + + # filter state machine + self.mode = M_NORMAL + self.suboption = None + self.telnet_command = None + + # states for modem/line control events + self.modemstate_mask = 255 + self.last_modemstate = None + self.linstate_mask = 0 + + # all supported telnet options + self._telnet_options = [ + TelnetOption(self, 'ECHO', ECHO, WILL, WONT, DO, DONT, REQUESTED), + TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED), + TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, INACTIVE), + TelnetOption(self, 'we-BINARY', BINARY, WILL, WONT, DO, DONT, INACTIVE), + TelnetOption(self, 'they-BINARY', BINARY, DO, DONT, WILL, WONT, REQUESTED), + TelnetOption(self, 'we-RFC2217', COM_PORT_OPTION, WILL, WONT, DO, DONT, REQUESTED, self._client_ok), + TelnetOption(self, 'they-RFC2217', COM_PORT_OPTION, DO, DONT, WILL, WONT, INACTIVE, self._client_ok), + ] + + # negotiate Telnet/RFC2217 -> send initial requests + if self.logger: + self.logger.debug("requesting initial Telnet/RFC 2217 options") + for option in self._telnet_options: + if option.state is REQUESTED: + self.telnet_send_option(option.send_yes, option.option) + # issue 1st modem state notification + + def _client_ok(self): + """\ + callback of telnet option. It gets called when option is activated. + This one here is used to detect when the client agrees on RFC 2217. A + flag is set so that other functions like check_modem_lines know if the + client is OK. + """ + # The callback is used for we and they so if one party agrees, we're + # already happy. it seems not all servers do the negotiation correctly + # and i guess there are incorrect clients too.. so be happy if client + # answers one or the other positively. + self._client_is_rfc2217 = True + if self.logger: + self.logger.info("client accepts RFC 2217") + # this is to ensure that the client gets a notification, even if there + # was no change + self.check_modem_lines(force_notification=True) + + # - outgoing telnet commands and options + + def telnet_send_option(self, action, option): + """Send DO, DONT, WILL, WONT.""" + self.connection.write(to_bytes([IAC, action, option])) + + def rfc2217_send_subnegotiation(self, option, value=b''): + """Subnegotiation of RFC 2217 parameters.""" + value = value.replace(IAC, IAC_DOUBLED) + self.connection.write(to_bytes([IAC, SB, COM_PORT_OPTION, option] + list(value) + [IAC, SE])) + + # - check modem lines, needs to be called periodically from user to + # establish polling + + def check_modem_lines(self, force_notification=False): + """\ + read control lines from serial port and compare the last value sent to remote. + send updates on changes. + """ + modemstate = ( + (self.serial.getCTS() and MODEMSTATE_MASK_CTS) | + (self.serial.getDSR() and MODEMSTATE_MASK_DSR) | + (self.serial.getRI() and MODEMSTATE_MASK_RI) | + (self.serial.getCD() and MODEMSTATE_MASK_CD)) + # check what has changed + deltas = modemstate ^ (self.last_modemstate or 0) # when last is None -> 0 + if deltas & MODEMSTATE_MASK_CTS: + modemstate |= MODEMSTATE_MASK_CTS_CHANGE + if deltas & MODEMSTATE_MASK_DSR: + modemstate |= MODEMSTATE_MASK_DSR_CHANGE + if deltas & MODEMSTATE_MASK_RI: + modemstate |= MODEMSTATE_MASK_RI_CHANGE + if deltas & MODEMSTATE_MASK_CD: + modemstate |= MODEMSTATE_MASK_CD_CHANGE + # if new state is different and the mask allows this change, send + # notification. suppress notifications when client is not rfc2217 + if modemstate != self.last_modemstate or force_notification: + if (self._client_is_rfc2217 and (modemstate & self.modemstate_mask)) or force_notification: + self.rfc2217_send_subnegotiation( + SERVER_NOTIFY_MODEMSTATE, + to_bytes([modemstate & self.modemstate_mask])) + if self.logger: + self.logger.info("NOTIFY_MODEMSTATE: {}".format(modemstate)) + # save last state, but forget about deltas. + # otherwise it would also notify about changing deltas which is + # probably not very useful + self.last_modemstate = modemstate & 0xf0 + + # - outgoing data escaping + + def escape(self, data): + """\ + This generator function is for the user. All outgoing data has to be + properly escaped, so that no IAC character in the data stream messes up + the Telnet state machine in the server. + + socket.sendall(escape(data)) + """ + for byte in iterbytes(data): + if byte == IAC: + yield IAC + yield IAC + else: + yield byte + + # - incoming data filter + + def filter(self, data): + """\ + Handle a bunch of incoming bytes. This is a generator. It will yield + all characters not of interest for Telnet/RFC 2217. + + The idea is that the reader thread pushes data from the socket through + this filter: + + for byte in filter(socket.recv(1024)): + # do things like CR/LF conversion/whatever + # and write data to the serial port + serial.write(byte) + + (socket error handling code left as exercise for the reader) + """ + for byte in iterbytes(data): + if self.mode == M_NORMAL: + # interpret as command or as data + if byte == IAC: + self.mode = M_IAC_SEEN + else: + # store data in sub option buffer or pass it to our + # consumer depending on state + if self.suboption is not None: + self.suboption += byte + else: + yield byte + elif self.mode == M_IAC_SEEN: + if byte == IAC: + # interpret as command doubled -> insert character + # itself + if self.suboption is not None: + self.suboption += byte + else: + yield byte + self.mode = M_NORMAL + elif byte == SB: + # sub option start + self.suboption = bytearray() + self.mode = M_NORMAL + elif byte == SE: + # sub option end -> process it now + self._telnet_process_subnegotiation(bytes(self.suboption)) + self.suboption = None + self.mode = M_NORMAL + elif byte in (DO, DONT, WILL, WONT): + # negotiation + self.telnet_command = byte + self.mode = M_NEGOTIATE + else: + # other telnet commands + self._telnet_process_command(byte) + self.mode = M_NORMAL + elif self.mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following + self._telnet_negotiate_option(self.telnet_command, byte) + self.mode = M_NORMAL + + # - incoming telnet commands and options + + def _telnet_process_command(self, command): + """Process commands other than DO, DONT, WILL, WONT.""" + # Currently none. RFC2217 only uses negotiation and subnegotiation. + if self.logger: + self.logger.warning("ignoring Telnet command: {!r}".format(command)) + + def _telnet_negotiate_option(self, command, option): + """Process incoming DO, DONT, WILL, WONT.""" + # check our registered telnet options and forward command to them + # they know themselves if they have to answer or not + known = False + for item in self._telnet_options: + # can have more than one match! as some options are duplicated for + # 'us' and 'them' + if item.option == option: + item.process_incoming(command) + known = True + if not known: + # handle unknown options + # only answer to positive requests and deny them + if command == WILL or command == DO: + self.telnet_send_option((DONT if command == WILL else WONT), option) + if self.logger: + self.logger.warning("rejected Telnet option: {!r}".format(option)) + + def _telnet_process_subnegotiation(self, suboption): + """Process subnegotiation, the data between IAC SB and IAC SE.""" + if suboption[0:1] == COM_PORT_OPTION: + if self.logger: + self.logger.debug('received COM_PORT_OPTION: {!r}'.format(suboption)) + if suboption[1:2] == SET_BAUDRATE: + backup = self.serial.baudrate + try: + (baudrate,) = struct.unpack(b"!I", suboption[2:6]) + if baudrate != 0: + self.serial.baudrate = baudrate + except ValueError as e: + if self.logger: + self.logger.error("failed to set baud rate: {}".format(e)) + self.serial.baudrate = backup + else: + if self.logger: + self.logger.info("{} baud rate: {}".format('set' if baudrate else 'get', self.serial.baudrate)) + self.rfc2217_send_subnegotiation(SERVER_SET_BAUDRATE, struct.pack(b"!I", self.serial.baudrate)) + elif suboption[1:2] == SET_DATASIZE: + backup = self.serial.bytesize + try: + (datasize,) = struct.unpack(b"!B", suboption[2:3]) + if datasize != 0: + self.serial.bytesize = datasize + except ValueError as e: + if self.logger: + self.logger.error("failed to set data size: {}".format(e)) + self.serial.bytesize = backup + else: + if self.logger: + self.logger.info("{} data size: {}".format('set' if datasize else 'get', self.serial.bytesize)) + self.rfc2217_send_subnegotiation(SERVER_SET_DATASIZE, struct.pack(b"!B", self.serial.bytesize)) + elif suboption[1:2] == SET_PARITY: + backup = self.serial.parity + try: + parity = struct.unpack(b"!B", suboption[2:3])[0] + if parity != 0: + self.serial.parity = RFC2217_REVERSE_PARITY_MAP[parity] + except ValueError as e: + if self.logger: + self.logger.error("failed to set parity: {}".format(e)) + self.serial.parity = backup + else: + if self.logger: + self.logger.info("{} parity: {}".format('set' if parity else 'get', self.serial.parity)) + self.rfc2217_send_subnegotiation( + SERVER_SET_PARITY, + struct.pack(b"!B", RFC2217_PARITY_MAP[self.serial.parity])) + elif suboption[1:2] == SET_STOPSIZE: + backup = self.serial.stopbits + try: + stopbits = struct.unpack(b"!B", suboption[2:3])[0] + if stopbits != 0: + self.serial.stopbits = RFC2217_REVERSE_STOPBIT_MAP[stopbits] + except ValueError as e: + if self.logger: + self.logger.error("failed to set stop bits: {}".format(e)) + self.serial.stopbits = backup + else: + if self.logger: + self.logger.info("{} stop bits: {}".format('set' if stopbits else 'get', self.serial.stopbits)) + self.rfc2217_send_subnegotiation( + SERVER_SET_STOPSIZE, + struct.pack(b"!B", RFC2217_STOPBIT_MAP[self.serial.stopbits])) + elif suboption[1:2] == SET_CONTROL: + if suboption[2:3] == SET_CONTROL_REQ_FLOW_SETTING: + if self.serial.xonxoff: + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL) + elif self.serial.rtscts: + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL) + else: + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_NO_FLOW_CONTROL) + elif suboption[2:3] == SET_CONTROL_USE_NO_FLOW_CONTROL: + self.serial.xonxoff = False + self.serial.rtscts = False + if self.logger: + self.logger.info("changed flow control to None") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_NO_FLOW_CONTROL) + elif suboption[2:3] == SET_CONTROL_USE_SW_FLOW_CONTROL: + self.serial.xonxoff = True + if self.logger: + self.logger.info("changed flow control to XON/XOFF") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL) + elif suboption[2:3] == SET_CONTROL_USE_HW_FLOW_CONTROL: + self.serial.rtscts = True + if self.logger: + self.logger.info("changed flow control to RTS/CTS") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL) + elif suboption[2:3] == SET_CONTROL_REQ_BREAK_STATE: + if self.logger: + self.logger.warning("requested break state - not implemented") + pass # XXX needs cached value + elif suboption[2:3] == SET_CONTROL_BREAK_ON: + self.serial.setBreak(True) + if self.logger: + self.logger.info("changed BREAK to active") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_ON) + elif suboption[2:3] == SET_CONTROL_BREAK_OFF: + self.serial.setBreak(False) + if self.logger: + self.logger.info("changed BREAK to inactive") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_OFF) + elif suboption[2:3] == SET_CONTROL_REQ_DTR: + if self.logger: + self.logger.warning("requested DTR state - not implemented") + pass # XXX needs cached value + elif suboption[2:3] == SET_CONTROL_DTR_ON: + self.serial.setDTR(True) + if self.logger: + self.logger.info("changed DTR to active") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_ON) + elif suboption[2:3] == SET_CONTROL_DTR_OFF: + self.serial.setDTR(False) + if self.logger: + self.logger.info("changed DTR to inactive") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_OFF) + elif suboption[2:3] == SET_CONTROL_REQ_RTS: + if self.logger: + self.logger.warning("requested RTS state - not implemented") + pass # XXX needs cached value + #~ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON) + elif suboption[2:3] == SET_CONTROL_RTS_ON: + self.serial.setRTS(True) + if self.logger: + self.logger.info("changed RTS to active") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON) + elif suboption[2:3] == SET_CONTROL_RTS_OFF: + self.serial.setRTS(False) + if self.logger: + self.logger.info("changed RTS to inactive") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_OFF) + #~ elif suboption[2:3] == SET_CONTROL_REQ_FLOW_SETTING_IN: + #~ elif suboption[2:3] == SET_CONTROL_USE_NO_FLOW_CONTROL_IN: + #~ elif suboption[2:3] == SET_CONTROL_USE_SW_FLOW_CONTOL_IN: + #~ elif suboption[2:3] == SET_CONTROL_USE_HW_FLOW_CONTOL_IN: + #~ elif suboption[2:3] == SET_CONTROL_USE_DCD_FLOW_CONTROL: + #~ elif suboption[2:3] == SET_CONTROL_USE_DTR_FLOW_CONTROL: + #~ elif suboption[2:3] == SET_CONTROL_USE_DSR_FLOW_CONTROL: + elif suboption[1:2] == NOTIFY_LINESTATE: + # client polls for current state + self.rfc2217_send_subnegotiation( + SERVER_NOTIFY_LINESTATE, + to_bytes([0])) # sorry, nothing like that implemented + elif suboption[1:2] == NOTIFY_MODEMSTATE: + if self.logger: + self.logger.info("request for modem state") + # client polls for current state + self.check_modem_lines(force_notification=True) + elif suboption[1:2] == FLOWCONTROL_SUSPEND: + if self.logger: + self.logger.info("suspend") + self._remote_suspend_flow = True + elif suboption[1:2] == FLOWCONTROL_RESUME: + if self.logger: + self.logger.info("resume") + self._remote_suspend_flow = False + elif suboption[1:2] == SET_LINESTATE_MASK: + self.linstate_mask = ord(suboption[2:3]) # ensure it is a number + if self.logger: + self.logger.info("line state mask: 0x{:02x}".format(self.linstate_mask)) + elif suboption[1:2] == SET_MODEMSTATE_MASK: + self.modemstate_mask = ord(suboption[2:3]) # ensure it is a number + if self.logger: + self.logger.info("modem state mask: 0x{:02x}".format(self.modemstate_mask)) + elif suboption[1:2] == PURGE_DATA: + if suboption[2:3] == PURGE_RECEIVE_BUFFER: + self.serial.reset_input_buffer() + if self.logger: + self.logger.info("purge in") + self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_RECEIVE_BUFFER) + elif suboption[2:3] == PURGE_TRANSMIT_BUFFER: + self.serial.reset_output_buffer() + if self.logger: + self.logger.info("purge out") + self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_TRANSMIT_BUFFER) + elif suboption[2:3] == PURGE_BOTH_BUFFERS: + self.serial.reset_input_buffer() + self.serial.reset_output_buffer() + if self.logger: + self.logger.info("purge both") + self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_BOTH_BUFFERS) + else: + if self.logger: + self.logger.error("undefined PURGE_DATA: {!r}".format(list(suboption[2:]))) + else: + if self.logger: + self.logger.error("undefined COM_PORT_OPTION: {!r}".format(list(suboption[1:]))) + else: + if self.logger: + self.logger.warning("unknown subnegotiation: {!r}".format(suboption)) + + +# simple client test +if __name__ == '__main__': + import sys + s = Serial('rfc2217://localhost:7000', 115200) + sys.stdout.write('{}\n'.format(s)) + + sys.stdout.write("write...\n") + s.write(b"hello\n") + s.flush() + sys.stdout.write("read: {}\n".format(s.read(5))) + s.close() diff --git a/libs/serial/rs485.py b/libs/serial/rs485.py new file mode 100644 index 0000000..2939350 --- /dev/null +++ b/libs/serial/rs485.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +# RS485 support +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +"""\ +The settings for RS485 are stored in a dedicated object that can be applied to +serial ports (where supported). +NOTE: Some implementations may only support a subset of the settings. +""" + +import time +import serial + + +class RS485Settings(object): + def __init__( + self, + rts_level_for_tx=True, + rts_level_for_rx=False, + loopback=False, + delay_before_tx=None, + delay_before_rx=None): + self.rts_level_for_tx = rts_level_for_tx + self.rts_level_for_rx = rts_level_for_rx + self.loopback = loopback + self.delay_before_tx = delay_before_tx + self.delay_before_rx = delay_before_rx + + +class RS485(serial.Serial): + """\ + A subclass that replaces the write method with one that toggles RTS + according to the RS485 settings. + + NOTE: This may work unreliably on some serial ports (control signals not + synchronized or delayed compared to data). Using delays may be + unreliable (varying times, larger than expected) as the OS may not + support very fine grained delays (no smaller than in the order of + tens of milliseconds). + + NOTE: Some implementations support this natively. Better performance + can be expected when the native version is used. + + NOTE: The loopback property is ignored by this implementation. The actual + behavior depends on the used hardware. + + Usage: + + ser = RS485(...) + ser.rs485_mode = RS485Settings(...) + ser.write(b'hello') + """ + + def __init__(self, *args, **kwargs): + super(RS485, self).__init__(*args, **kwargs) + self._alternate_rs485_settings = None + + def write(self, b): + """Write to port, controlling RTS before and after transmitting.""" + if self._alternate_rs485_settings is not None: + # apply level for TX and optional delay + self.setRTS(self._alternate_rs485_settings.rts_level_for_tx) + if self._alternate_rs485_settings.delay_before_tx is not None: + time.sleep(self._alternate_rs485_settings.delay_before_tx) + # write and wait for data to be written + super(RS485, self).write(b) + super(RS485, self).flush() + # optional delay and apply level for RX + if self._alternate_rs485_settings.delay_before_rx is not None: + time.sleep(self._alternate_rs485_settings.delay_before_rx) + self.setRTS(self._alternate_rs485_settings.rts_level_for_rx) + else: + super(RS485, self).write(b) + + # redirect where the property stores the settings so that underlying Serial + # instance does not see them + @property + def rs485_mode(self): + """\ + Enable RS485 mode and apply new settings, set to None to disable. + See serial.rs485.RS485Settings for more info about the value. + """ + return self._alternate_rs485_settings + + @rs485_mode.setter + def rs485_mode(self, rs485_settings): + self._alternate_rs485_settings = rs485_settings diff --git a/libs/serial/serialcli.py b/libs/serial/serialcli.py new file mode 100644 index 0000000..0727a52 --- /dev/null +++ b/libs/serial/serialcli.py @@ -0,0 +1,251 @@ +#! python +# +# Backend for .NET/Mono (IronPython), .NET >= 2 +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2008-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +import System +import System.IO.Ports +from serial.serialutil import * + +# must invoke function with byte array, make a helper to convert strings +# to byte arrays +sab = System.Array[System.Byte] + + +def as_byte_array(string): + return sab([ord(x) for x in string]) # XXX will require adaption when run with a 3.x compatible IronPython + + +class Serial(SerialBase): + """Serial port implementation for .NET/Mono.""" + + BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200) + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened. + """ + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + try: + self._port_handle = System.IO.Ports.SerialPort(self.portstr) + except Exception as msg: + self._port_handle = None + raise SerialException("could not open port %s: %s" % (self.portstr, msg)) + + # if RTS and/or DTR are not set before open, they default to True + if self._rts_state is None: + self._rts_state = True + if self._dtr_state is None: + self._dtr_state = True + + self._reconfigure_port() + self._port_handle.Open() + self.is_open = True + if not self._dsrdtr: + self._update_dtr_state() + if not self._rtscts: + self._update_rts_state() + self.reset_input_buffer() + + def _reconfigure_port(self): + """Set communication parameters on opened port.""" + if not self._port_handle: + raise SerialException("Can only operate on a valid port handle") + + #~ self._port_handle.ReceivedBytesThreshold = 1 + + if self._timeout is None: + self._port_handle.ReadTimeout = System.IO.Ports.SerialPort.InfiniteTimeout + else: + self._port_handle.ReadTimeout = int(self._timeout * 1000) + + # if self._timeout != 0 and self._interCharTimeout is not None: + # timeouts = (int(self._interCharTimeout * 1000),) + timeouts[1:] + + if self._write_timeout is None: + self._port_handle.WriteTimeout = System.IO.Ports.SerialPort.InfiniteTimeout + else: + self._port_handle.WriteTimeout = int(self._write_timeout * 1000) + + # Setup the connection info. + try: + self._port_handle.BaudRate = self._baudrate + except IOError as e: + # catch errors from illegal baudrate settings + raise ValueError(str(e)) + + if self._bytesize == FIVEBITS: + self._port_handle.DataBits = 5 + elif self._bytesize == SIXBITS: + self._port_handle.DataBits = 6 + elif self._bytesize == SEVENBITS: + self._port_handle.DataBits = 7 + elif self._bytesize == EIGHTBITS: + self._port_handle.DataBits = 8 + else: + raise ValueError("Unsupported number of data bits: %r" % self._bytesize) + + if self._parity == PARITY_NONE: + self._port_handle.Parity = getattr(System.IO.Ports.Parity, 'None') # reserved keyword in Py3k + elif self._parity == PARITY_EVEN: + self._port_handle.Parity = System.IO.Ports.Parity.Even + elif self._parity == PARITY_ODD: + self._port_handle.Parity = System.IO.Ports.Parity.Odd + elif self._parity == PARITY_MARK: + self._port_handle.Parity = System.IO.Ports.Parity.Mark + elif self._parity == PARITY_SPACE: + self._port_handle.Parity = System.IO.Ports.Parity.Space + else: + raise ValueError("Unsupported parity mode: %r" % self._parity) + + if self._stopbits == STOPBITS_ONE: + self._port_handle.StopBits = System.IO.Ports.StopBits.One + elif self._stopbits == STOPBITS_ONE_POINT_FIVE: + self._port_handle.StopBits = System.IO.Ports.StopBits.OnePointFive + elif self._stopbits == STOPBITS_TWO: + self._port_handle.StopBits = System.IO.Ports.StopBits.Two + else: + raise ValueError("Unsupported number of stop bits: %r" % self._stopbits) + + if self._rtscts and self._xonxoff: + self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSendXOnXOff + elif self._rtscts: + self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSend + elif self._xonxoff: + self._port_handle.Handshake = System.IO.Ports.Handshake.XOnXOff + else: + self._port_handle.Handshake = getattr(System.IO.Ports.Handshake, 'None') # reserved keyword in Py3k + + #~ def __del__(self): + #~ self.close() + + def close(self): + """Close port""" + if self.is_open: + if self._port_handle: + try: + self._port_handle.Close() + except System.IO.Ports.InvalidOperationException: + # ignore errors. can happen for unplugged USB serial devices + pass + self._port_handle = None + self.is_open = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of characters currently in the input buffer.""" + if not self.is_open: + raise portNotOpenError + return self._port_handle.BytesToRead + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise portNotOpenError + # must use single byte reads as this is the only way to read + # without applying encodings + data = bytearray() + while size: + try: + data.append(self._port_handle.ReadByte()) + except System.TimeoutException: + break + else: + size -= 1 + return bytes(data) + + def write(self, data): + """Output the given string over the serial port.""" + if not self.is_open: + raise portNotOpenError + #~ if not isinstance(data, (bytes, bytearray)): + #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data))) + try: + # must call overloaded method with byte array argument + # as this is the only one not applying encodings + self._port_handle.Write(as_byte_array(data), 0, len(data)) + except System.TimeoutException: + raise writeTimeoutError + return len(data) + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.is_open: + raise portNotOpenError + self._port_handle.DiscardInBuffer() + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and + discarding all that is in the buffer. + """ + if not self.is_open: + raise portNotOpenError + self._port_handle.DiscardOutBuffer() + + def _update_break_state(self): + """ + Set break: Controls TXD. When active, to transmitting is possible. + """ + if not self.is_open: + raise portNotOpenError + self._port_handle.BreakState = bool(self._break_state) + + def _update_rts_state(self): + """Set terminal status line: Request To Send""" + if not self.is_open: + raise portNotOpenError + self._port_handle.RtsEnable = bool(self._rts_state) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready""" + if not self.is_open: + raise portNotOpenError + self._port_handle.DtrEnable = bool(self._dtr_state) + + @property + def cts(self): + """Read terminal status line: Clear To Send""" + if not self.is_open: + raise portNotOpenError + return self._port_handle.CtsHolding + + @property + def dsr(self): + """Read terminal status line: Data Set Ready""" + if not self.is_open: + raise portNotOpenError + return self._port_handle.DsrHolding + + @property + def ri(self): + """Read terminal status line: Ring Indicator""" + if not self.is_open: + raise portNotOpenError + #~ return self._port_handle.XXX + return False # XXX an error would be better + + @property + def cd(self): + """Read terminal status line: Carrier Detect""" + if not self.is_open: + raise portNotOpenError + return self._port_handle.CDHolding + + # - - platform specific - - - - + # none diff --git a/libs/serial/serialjava.py b/libs/serial/serialjava.py new file mode 100644 index 0000000..7bd5b3e --- /dev/null +++ b/libs/serial/serialjava.py @@ -0,0 +1,249 @@ +#!jython +# +# Backend Jython with JavaComm +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2002-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +from serial.serialutil import * + + +def my_import(name): + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + + +def detect_java_comm(names): + """try given list of modules and return that imports""" + for name in names: + try: + mod = my_import(name) + mod.SerialPort + return mod + except (ImportError, AttributeError): + pass + raise ImportError("No Java Communications API implementation found") + + +# Java Communications API implementations +# http://mho.republika.pl/java/comm/ + +comm = detect_java_comm([ + 'javax.comm', # Sun/IBM + 'gnu.io', # RXTX +]) + + +def device(portnumber): + """Turn a port number into a device name""" + enum = comm.CommPortIdentifier.getPortIdentifiers() + ports = [] + while enum.hasMoreElements(): + el = enum.nextElement() + if el.getPortType() == comm.CommPortIdentifier.PORT_SERIAL: + ports.append(el) + return ports[portnumber].getName() + + +class Serial(SerialBase): + """\ + Serial port class, implemented with Java Communications API and + thus usable with jython and the appropriate java extension. + """ + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened. + """ + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + if type(self._port) == type(''): # strings are taken directly + portId = comm.CommPortIdentifier.getPortIdentifier(self._port) + else: + portId = comm.CommPortIdentifier.getPortIdentifier(device(self._port)) # numbers are transformed to a comport id obj + try: + self.sPort = portId.open("python serial module", 10) + except Exception as msg: + self.sPort = None + raise SerialException("Could not open port: %s" % msg) + self._reconfigurePort() + self._instream = self.sPort.getInputStream() + self._outstream = self.sPort.getOutputStream() + self.is_open = True + + def _reconfigurePort(self): + """Set communication parameters on opened port.""" + if not self.sPort: + raise SerialException("Can only operate on a valid port handle") + + self.sPort.enableReceiveTimeout(30) + if self._bytesize == FIVEBITS: + jdatabits = comm.SerialPort.DATABITS_5 + elif self._bytesize == SIXBITS: + jdatabits = comm.SerialPort.DATABITS_6 + elif self._bytesize == SEVENBITS: + jdatabits = comm.SerialPort.DATABITS_7 + elif self._bytesize == EIGHTBITS: + jdatabits = comm.SerialPort.DATABITS_8 + else: + raise ValueError("unsupported bytesize: %r" % self._bytesize) + + if self._stopbits == STOPBITS_ONE: + jstopbits = comm.SerialPort.STOPBITS_1 + elif self._stopbits == STOPBITS_ONE_POINT_FIVE: + jstopbits = comm.SerialPort.STOPBITS_1_5 + elif self._stopbits == STOPBITS_TWO: + jstopbits = comm.SerialPort.STOPBITS_2 + else: + raise ValueError("unsupported number of stopbits: %r" % self._stopbits) + + if self._parity == PARITY_NONE: + jparity = comm.SerialPort.PARITY_NONE + elif self._parity == PARITY_EVEN: + jparity = comm.SerialPort.PARITY_EVEN + elif self._parity == PARITY_ODD: + jparity = comm.SerialPort.PARITY_ODD + elif self._parity == PARITY_MARK: + jparity = comm.SerialPort.PARITY_MARK + elif self._parity == PARITY_SPACE: + jparity = comm.SerialPort.PARITY_SPACE + else: + raise ValueError("unsupported parity type: %r" % self._parity) + + jflowin = jflowout = 0 + if self._rtscts: + jflowin |= comm.SerialPort.FLOWCONTROL_RTSCTS_IN + jflowout |= comm.SerialPort.FLOWCONTROL_RTSCTS_OUT + if self._xonxoff: + jflowin |= comm.SerialPort.FLOWCONTROL_XONXOFF_IN + jflowout |= comm.SerialPort.FLOWCONTROL_XONXOFF_OUT + + self.sPort.setSerialPortParams(self._baudrate, jdatabits, jstopbits, jparity) + self.sPort.setFlowControlMode(jflowin | jflowout) + + if self._timeout >= 0: + self.sPort.enableReceiveTimeout(int(self._timeout*1000)) + else: + self.sPort.disableReceiveTimeout() + + def close(self): + """Close port""" + if self.is_open: + if self.sPort: + self._instream.close() + self._outstream.close() + self.sPort.close() + self.sPort = None + self.is_open = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of characters currently in the input buffer.""" + if not self.sPort: + raise portNotOpenError + return self._instream.available() + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.sPort: + raise portNotOpenError + read = bytearray() + if size > 0: + while len(read) < size: + x = self._instream.read() + if x == -1: + if self.timeout >= 0: + break + else: + read.append(x) + return bytes(read) + + def write(self, data): + """Output the given string over the serial port.""" + if not self.sPort: + raise portNotOpenError + if not isinstance(data, (bytes, bytearray)): + raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data))) + self._outstream.write(data) + return len(data) + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.sPort: + raise portNotOpenError + self._instream.skip(self._instream.available()) + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and + discarding all that is in the buffer. + """ + if not self.sPort: + raise portNotOpenError + self._outstream.flush() + + def send_break(self, duration=0.25): + """Send break condition. Timed, returns to idle state after given duration.""" + if not self.sPort: + raise portNotOpenError + self.sPort.sendBreak(duration*1000.0) + + def _update_break_state(self): + """Set break: Controls TXD. When active, to transmitting is possible.""" + if self.fd is None: + raise portNotOpenError + raise SerialException("The _update_break_state function is not implemented in java.") + + def _update_rts_state(self): + """Set terminal status line: Request To Send""" + if not self.sPort: + raise portNotOpenError + self.sPort.setRTS(self._rts_state) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready""" + if not self.sPort: + raise portNotOpenError + self.sPort.setDTR(self._dtr_state) + + @property + def cts(self): + """Read terminal status line: Clear To Send""" + if not self.sPort: + raise portNotOpenError + self.sPort.isCTS() + + @property + def dsr(self): + """Read terminal status line: Data Set Ready""" + if not self.sPort: + raise portNotOpenError + self.sPort.isDSR() + + @property + def ri(self): + """Read terminal status line: Ring Indicator""" + if not self.sPort: + raise portNotOpenError + self.sPort.isRI() + + @property + def cd(self): + """Read terminal status line: Carrier Detect""" + if not self.sPort: + raise portNotOpenError + self.sPort.isCD() diff --git a/libs/serial/serialposix.py b/libs/serial/serialposix.py new file mode 100644 index 0000000..01848e9 --- /dev/null +++ b/libs/serial/serialposix.py @@ -0,0 +1,789 @@ +#!/usr/bin/env python +# +# backend for serial IO for POSIX compatible systems, like Linux, OSX +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2016 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +# +# parts based on code from Grant B. Edwards : +# ftp://ftp.visi.com/users/grante/python/PosixSerial.py +# +# references: http://www.easysw.com/~mike/serial/serial.html + +# Collection of port names (was previously used by number_to_device which was +# removed. +# - Linux /dev/ttyS%d (confirmed) +# - cygwin/win32 /dev/com%d (confirmed) +# - openbsd (OpenBSD) /dev/cua%02d +# - bsd*, freebsd* /dev/cuad%d +# - darwin (OS X) /dev/cuad%d +# - netbsd /dev/dty%02d (NetBSD 1.6 testing by Erk) +# - irix (IRIX) /dev/ttyf%d (partially tested) names depending on flow control +# - hp (HP-UX) /dev/tty%dp0 (not tested) +# - sunos (Solaris/SunOS) /dev/tty%c (letters, 'a'..'z') (confirmed) +# - aix (AIX) /dev/tty%d + + +# pylint: disable=abstract-method +import errno +import fcntl +import os +import select +import struct +import sys +import termios + +import serial +from serial.serialutil import SerialBase, SerialException, to_bytes, \ + portNotOpenError, writeTimeoutError, Timeout + + +class PlatformSpecificBase(object): + BAUDRATE_CONSTANTS = {} + + def _set_special_baudrate(self, baudrate): + raise NotImplementedError('non-standard baudrates are not supported on this platform') + + def _set_rs485_mode(self, rs485_settings): + raise NotImplementedError('RS485 not supported on this platform') + + +# some systems support an extra flag to enable the two in POSIX unsupported +# paritiy settings for MARK and SPACE +CMSPAR = 0 # default, for unsupported platforms, override below + +# try to detect the OS so that a device can be selected... +# this code block should supply a device() and set_special_baudrate() function +# for the platform +plat = sys.platform.lower() + +if plat[:5] == 'linux': # Linux (confirmed) # noqa + import array + + # extra termios flags + CMSPAR = 0o10000000000 # Use "stick" (mark/space) parity + + # baudrate ioctls + TCGETS2 = 0x802C542A + TCSETS2 = 0x402C542B + BOTHER = 0o010000 + + # RS485 ioctls + TIOCGRS485 = 0x542E + TIOCSRS485 = 0x542F + SER_RS485_ENABLED = 0b00000001 + SER_RS485_RTS_ON_SEND = 0b00000010 + SER_RS485_RTS_AFTER_SEND = 0b00000100 + SER_RS485_RX_DURING_TX = 0b00010000 + + class PlatformSpecific(PlatformSpecificBase): + BAUDRATE_CONSTANTS = { + 0: 0o000000, # hang up + 50: 0o000001, + 75: 0o000002, + 110: 0o000003, + 134: 0o000004, + 150: 0o000005, + 200: 0o000006, + 300: 0o000007, + 600: 0o000010, + 1200: 0o000011, + 1800: 0o000012, + 2400: 0o000013, + 4800: 0o000014, + 9600: 0o000015, + 19200: 0o000016, + 38400: 0o000017, + 57600: 0o010001, + 115200: 0o010002, + 230400: 0o010003, + 460800: 0o010004, + 500000: 0o010005, + 576000: 0o010006, + 921600: 0o010007, + 1000000: 0o010010, + 1152000: 0o010011, + 1500000: 0o010012, + 2000000: 0o010013, + 2500000: 0o010014, + 3000000: 0o010015, + 3500000: 0o010016, + 4000000: 0o010017 + } + + def _set_special_baudrate(self, baudrate): + # right size is 44 on x86_64, allow for some growth + buf = array.array('i', [0] * 64) + try: + # get serial_struct + fcntl.ioctl(self.fd, TCGETS2, buf) + # set custom speed + buf[2] &= ~termios.CBAUD + buf[2] |= BOTHER + buf[9] = buf[10] = baudrate + + # set serial_struct + fcntl.ioctl(self.fd, TCSETS2, buf) + except IOError as e: + raise ValueError('Failed to set custom baud rate ({}): {}'.format(baudrate, e)) + + def _set_rs485_mode(self, rs485_settings): + buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding + try: + fcntl.ioctl(self.fd, TIOCGRS485, buf) + buf[0] |= SER_RS485_ENABLED + if rs485_settings is not None: + if rs485_settings.loopback: + buf[0] |= SER_RS485_RX_DURING_TX + else: + buf[0] &= ~SER_RS485_RX_DURING_TX + if rs485_settings.rts_level_for_tx: + buf[0] |= SER_RS485_RTS_ON_SEND + else: + buf[0] &= ~SER_RS485_RTS_ON_SEND + if rs485_settings.rts_level_for_rx: + buf[0] |= SER_RS485_RTS_AFTER_SEND + else: + buf[0] &= ~SER_RS485_RTS_AFTER_SEND + if rs485_settings.delay_before_tx is not None: + buf[1] = int(rs485_settings.delay_before_tx * 1000) + if rs485_settings.delay_before_rx is not None: + buf[2] = int(rs485_settings.delay_before_rx * 1000) + else: + buf[0] = 0 # clear SER_RS485_ENABLED + fcntl.ioctl(self.fd, TIOCSRS485, buf) + except IOError as e: + raise ValueError('Failed to set RS485 mode: {}'.format(e)) + + +elif plat == 'cygwin': # cygwin/win32 (confirmed) + + class PlatformSpecific(PlatformSpecificBase): + BAUDRATE_CONSTANTS = { + 128000: 0x01003, + 256000: 0x01005, + 500000: 0x01007, + 576000: 0x01008, + 921600: 0x01009, + 1000000: 0x0100a, + 1152000: 0x0100b, + 1500000: 0x0100c, + 2000000: 0x0100d, + 2500000: 0x0100e, + 3000000: 0x0100f + } + + +elif plat[:6] == 'darwin': # OS X + import array + IOSSIOSPEED = 0x80045402 # _IOW('T', 2, speed_t) + + class PlatformSpecific(PlatformSpecificBase): + osx_version = os.uname()[2].split('.') + # Tiger or above can support arbitrary serial speeds + if int(osx_version[0]) >= 8: + def _set_special_baudrate(self, baudrate): + # use IOKit-specific call to set up high speeds + buf = array.array('i', [baudrate]) + fcntl.ioctl(self.fd, IOSSIOSPEED, buf, 1) + +elif plat[:3] == 'bsd' or \ + plat[:7] == 'freebsd' or \ + plat[:6] == 'netbsd' or \ + plat[:7] == 'openbsd': + + class ReturnBaudrate(object): + def __getitem__(self, key): + return key + + class PlatformSpecific(PlatformSpecificBase): + # Only tested on FreeBSD: + # The baud rate may be passed in as + # a literal value. + BAUDRATE_CONSTANTS = ReturnBaudrate() + +else: + class PlatformSpecific(PlatformSpecificBase): + pass + + +# load some constants for later use. +# try to use values from termios, use defaults from linux otherwise +TIOCMGET = getattr(termios, 'TIOCMGET', 0x5415) +TIOCMBIS = getattr(termios, 'TIOCMBIS', 0x5416) +TIOCMBIC = getattr(termios, 'TIOCMBIC', 0x5417) +TIOCMSET = getattr(termios, 'TIOCMSET', 0x5418) + +# TIOCM_LE = getattr(termios, 'TIOCM_LE', 0x001) +TIOCM_DTR = getattr(termios, 'TIOCM_DTR', 0x002) +TIOCM_RTS = getattr(termios, 'TIOCM_RTS', 0x004) +# TIOCM_ST = getattr(termios, 'TIOCM_ST', 0x008) +# TIOCM_SR = getattr(termios, 'TIOCM_SR', 0x010) + +TIOCM_CTS = getattr(termios, 'TIOCM_CTS', 0x020) +TIOCM_CAR = getattr(termios, 'TIOCM_CAR', 0x040) +TIOCM_RNG = getattr(termios, 'TIOCM_RNG', 0x080) +TIOCM_DSR = getattr(termios, 'TIOCM_DSR', 0x100) +TIOCM_CD = getattr(termios, 'TIOCM_CD', TIOCM_CAR) +TIOCM_RI = getattr(termios, 'TIOCM_RI', TIOCM_RNG) +# TIOCM_OUT1 = getattr(termios, 'TIOCM_OUT1', 0x2000) +# TIOCM_OUT2 = getattr(termios, 'TIOCM_OUT2', 0x4000) +if hasattr(termios, 'TIOCINQ'): + TIOCINQ = termios.TIOCINQ +else: + TIOCINQ = getattr(termios, 'FIONREAD', 0x541B) +TIOCOUTQ = getattr(termios, 'TIOCOUTQ', 0x5411) + +TIOCM_zero_str = struct.pack('I', 0) +TIOCM_RTS_str = struct.pack('I', TIOCM_RTS) +TIOCM_DTR_str = struct.pack('I', TIOCM_DTR) + +TIOCSBRK = getattr(termios, 'TIOCSBRK', 0x5427) +TIOCCBRK = getattr(termios, 'TIOCCBRK', 0x5428) + + +class Serial(SerialBase, PlatformSpecific): + """\ + Serial port class POSIX implementation. Serial port configuration is + done with termios and fcntl. Runs on Linux and many other Un*x like + systems. + """ + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened.""" + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + self.fd = None + # open + try: + self.fd = os.open(self.portstr, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + except OSError as msg: + self.fd = None + raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg)) + #~ fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # set blocking + + try: + self._reconfigure_port(force_update=True) + except: + try: + os.close(self.fd) + except: + # ignore any exception when closing the port + # also to keep original exception that happened when setting up + pass + self.fd = None + raise + else: + self.is_open = True + try: + if not self._dsrdtr: + self._update_dtr_state() + if not self._rtscts: + self._update_rts_state() + except IOError as e: + if e.errno in (errno.EINVAL, errno.ENOTTY): + # ignore Invalid argument and Inappropriate ioctl + pass + else: + raise + self.reset_input_buffer() + self.pipe_abort_read_r, self.pipe_abort_read_w = os.pipe() + self.pipe_abort_write_r, self.pipe_abort_write_w = os.pipe() + fcntl.fcntl(self.pipe_abort_read_r, fcntl.F_SETFL, os.O_NONBLOCK) + fcntl.fcntl(self.pipe_abort_write_r, fcntl.F_SETFL, os.O_NONBLOCK) + + def _reconfigure_port(self, force_update=False): + """Set communication parameters on opened port.""" + if self.fd is None: + raise SerialException("Can only operate on a valid file descriptor") + custom_baud = None + + vmin = vtime = 0 # timeout is done via select + if self._inter_byte_timeout is not None: + vmin = 1 + vtime = int(self._inter_byte_timeout * 10) + try: + orig_attr = termios.tcgetattr(self.fd) + iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr + except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here + raise SerialException("Could not configure port: {}".format(msg)) + # set up raw mode / no echo / binary + cflag |= (termios.CLOCAL | termios.CREAD) + lflag &= ~(termios.ICANON | termios.ECHO | termios.ECHOE | + termios.ECHOK | termios.ECHONL | + termios.ISIG | termios.IEXTEN) # |termios.ECHOPRT + for flag in ('ECHOCTL', 'ECHOKE'): # netbsd workaround for Erk + if hasattr(termios, flag): + lflag &= ~getattr(termios, flag) + + oflag &= ~(termios.OPOST | termios.ONLCR | termios.OCRNL) + iflag &= ~(termios.INLCR | termios.IGNCR | termios.ICRNL | termios.IGNBRK) + if hasattr(termios, 'IUCLC'): + iflag &= ~termios.IUCLC + if hasattr(termios, 'PARMRK'): + iflag &= ~termios.PARMRK + + # setup baud rate + try: + ispeed = ospeed = getattr(termios, 'B{}'.format(self._baudrate)) + except AttributeError: + try: + ispeed = ospeed = self.BAUDRATE_CONSTANTS[self._baudrate] + except KeyError: + #~ raise ValueError('Invalid baud rate: %r' % self._baudrate) + # may need custom baud rate, it isn't in our list. + ispeed = ospeed = getattr(termios, 'B38400') + try: + custom_baud = int(self._baudrate) # store for later + except ValueError: + raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate)) + else: + if custom_baud < 0: + raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate)) + + # setup char len + cflag &= ~termios.CSIZE + if self._bytesize == 8: + cflag |= termios.CS8 + elif self._bytesize == 7: + cflag |= termios.CS7 + elif self._bytesize == 6: + cflag |= termios.CS6 + elif self._bytesize == 5: + cflag |= termios.CS5 + else: + raise ValueError('Invalid char len: {!r}'.format(self._bytesize)) + # setup stop bits + if self._stopbits == serial.STOPBITS_ONE: + cflag &= ~(termios.CSTOPB) + elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE: + cflag |= (termios.CSTOPB) # XXX same as TWO.. there is no POSIX support for 1.5 + elif self._stopbits == serial.STOPBITS_TWO: + cflag |= (termios.CSTOPB) + else: + raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits)) + # setup parity + iflag &= ~(termios.INPCK | termios.ISTRIP) + if self._parity == serial.PARITY_NONE: + cflag &= ~(termios.PARENB | termios.PARODD | CMSPAR) + elif self._parity == serial.PARITY_EVEN: + cflag &= ~(termios.PARODD | CMSPAR) + cflag |= (termios.PARENB) + elif self._parity == serial.PARITY_ODD: + cflag &= ~CMSPAR + cflag |= (termios.PARENB | termios.PARODD) + elif self._parity == serial.PARITY_MARK and CMSPAR: + cflag |= (termios.PARENB | CMSPAR | termios.PARODD) + elif self._parity == serial.PARITY_SPACE and CMSPAR: + cflag |= (termios.PARENB | CMSPAR) + cflag &= ~(termios.PARODD) + else: + raise ValueError('Invalid parity: {!r}'.format(self._parity)) + # setup flow control + # xonxoff + if hasattr(termios, 'IXANY'): + if self._xonxoff: + iflag |= (termios.IXON | termios.IXOFF) # |termios.IXANY) + else: + iflag &= ~(termios.IXON | termios.IXOFF | termios.IXANY) + else: + if self._xonxoff: + iflag |= (termios.IXON | termios.IXOFF) + else: + iflag &= ~(termios.IXON | termios.IXOFF) + # rtscts + if hasattr(termios, 'CRTSCTS'): + if self._rtscts: + cflag |= (termios.CRTSCTS) + else: + cflag &= ~(termios.CRTSCTS) + elif hasattr(termios, 'CNEW_RTSCTS'): # try it with alternate constant name + if self._rtscts: + cflag |= (termios.CNEW_RTSCTS) + else: + cflag &= ~(termios.CNEW_RTSCTS) + # XXX should there be a warning if setting up rtscts (and xonxoff etc) fails?? + + # buffer + # vmin "minimal number of characters to be read. 0 for non blocking" + if vmin < 0 or vmin > 255: + raise ValueError('Invalid vmin: {!r}'.format(vmin)) + cc[termios.VMIN] = vmin + # vtime + if vtime < 0 or vtime > 255: + raise ValueError('Invalid vtime: {!r}'.format(vtime)) + cc[termios.VTIME] = vtime + # activate settings + if force_update or [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] != orig_attr: + termios.tcsetattr( + self.fd, + termios.TCSANOW, + [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) + + # apply custom baud rate, if any + if custom_baud is not None: + self._set_special_baudrate(custom_baud) + + if self._rs485_mode is not None: + self._set_rs485_mode(self._rs485_mode) + + def close(self): + """Close port""" + if self.is_open: + if self.fd is not None: + os.close(self.fd) + self.fd = None + os.close(self.pipe_abort_read_w) + os.close(self.pipe_abort_read_r) + os.close(self.pipe_abort_write_w) + os.close(self.pipe_abort_write_r) + self.pipe_abort_read_r, self.pipe_abort_read_w = None, None + self.pipe_abort_write_r, self.pipe_abort_write_w = None, None + self.is_open = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of bytes currently in the input buffer.""" + #~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str) + s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str) + return struct.unpack('I', s)[0] + + # select based implementation, proved to work on many systems + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise portNotOpenError + read = bytearray() + timeout = Timeout(self._timeout) + while len(read) < size: + try: + ready, _, _ = select.select([self.fd, self.pipe_abort_read_r], [], [], timeout.time_left()) + if self.pipe_abort_read_r in ready: + os.read(self.pipe_abort_read_r, 1000) + break + # If select was used with a timeout, and the timeout occurs, it + # returns with empty lists -> thus abort read operation. + # For timeout == 0 (non-blocking operation) also abort when + # there is nothing to read. + if not ready: + break # timeout + buf = os.read(self.fd, size - len(read)) + # read should always return some data as select reported it was + # ready to read when we get to this point. + if not buf: + # Disconnected devices, at least on Linux, show the + # behavior that they are always ready to read immediately + # but reading returns nothing. + raise SerialException( + 'device reports readiness to read but returned no data ' + '(device disconnected or multiple access on port?)') + read.extend(buf) + except OSError as e: + # this is for Python 3.x where select.error is a subclass of + # OSError ignore EAGAIN errors. all other errors are shown + if e.errno != errno.EAGAIN and e.errno != errno.EINTR: + raise SerialException('read failed: {}'.format(e)) + except select.error as e: + # this is for Python 2.x + # ignore EAGAIN errors. all other errors are shown + # see also http://www.python.org/dev/peps/pep-3151/#select + if e[0] != errno.EAGAIN: + raise SerialException('read failed: {}'.format(e)) + if timeout.expired(): + break + return bytes(read) + + def cancel_read(self): + os.write(self.pipe_abort_read_w, b"x") + + def cancel_write(self): + os.write(self.pipe_abort_write_w, b"x") + + def write(self, data): + """Output the given byte string over the serial port.""" + if not self.is_open: + raise portNotOpenError + d = to_bytes(data) + tx_len = len(d) + timeout = Timeout(self._write_timeout) + while tx_len > 0: + try: + n = os.write(self.fd, d) + if timeout.is_non_blocking: + # Zero timeout indicates non-blocking - simply return the + # number of bytes of data actually written + return n + elif not timeout.is_infinite: + # when timeout is set, use select to wait for being ready + # with the time left as timeout + if timeout.expired(): + raise writeTimeoutError + abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], timeout.time_left()) + if abort: + os.read(self.pipe_abort_write_r, 1000) + break + if not ready: + raise writeTimeoutError + else: + assert timeout.time_left() is None + # wait for write operation + abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], None) + if abort: + os.read(self.pipe_abort_write_r, 1) + break + if not ready: + raise SerialException('write failed (select)') + d = d[n:] + tx_len -= n + except SerialException: + raise + except OSError as v: + if v.errno != errno.EAGAIN: + raise SerialException('write failed: {}'.format(v)) + # still calculate and check timeout + if timeout.expired(): + raise writeTimeoutError + return len(data) + + def flush(self): + """\ + Flush of file like objects. In this case, wait until all data + is written. + """ + if not self.is_open: + raise portNotOpenError + termios.tcdrain(self.fd) + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.is_open: + raise portNotOpenError + termios.tcflush(self.fd, termios.TCIFLUSH) + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and discarding all + that is in the buffer. + """ + if not self.is_open: + raise portNotOpenError + termios.tcflush(self.fd, termios.TCOFLUSH) + + def send_break(self, duration=0.25): + """\ + Send break condition. Timed, returns to idle state after given + duration. + """ + if not self.is_open: + raise portNotOpenError + termios.tcsendbreak(self.fd, int(duration / 0.25)) + + def _update_break_state(self): + """\ + Set break: Controls TXD. When active, no transmitting is possible. + """ + if self._break_state: + fcntl.ioctl(self.fd, TIOCSBRK) + else: + fcntl.ioctl(self.fd, TIOCCBRK) + + def _update_rts_state(self): + """Set terminal status line: Request To Send""" + if self._rts_state: + fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_RTS_str) + else: + fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_RTS_str) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready""" + if self._dtr_state: + fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_DTR_str) + else: + fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_DTR_str) + + @property + def cts(self): + """Read terminal status line: Clear To Send""" + if not self.is_open: + raise portNotOpenError + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I', s)[0] & TIOCM_CTS != 0 + + @property + def dsr(self): + """Read terminal status line: Data Set Ready""" + if not self.is_open: + raise portNotOpenError + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I', s)[0] & TIOCM_DSR != 0 + + @property + def ri(self): + """Read terminal status line: Ring Indicator""" + if not self.is_open: + raise portNotOpenError + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I', s)[0] & TIOCM_RI != 0 + + @property + def cd(self): + """Read terminal status line: Carrier Detect""" + if not self.is_open: + raise portNotOpenError + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I', s)[0] & TIOCM_CD != 0 + + # - - platform specific - - - - + + @property + def out_waiting(self): + """Return the number of bytes currently in the output buffer.""" + #~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str) + s = fcntl.ioctl(self.fd, TIOCOUTQ, TIOCM_zero_str) + return struct.unpack('I', s)[0] + + def fileno(self): + """\ + For easier use of the serial port instance with select. + WARNING: this function is not portable to different platforms! + """ + if not self.is_open: + raise portNotOpenError + return self.fd + + def set_input_flow_control(self, enable=True): + """\ + Manually control flow - when software flow control is enabled. + This will send XON (true) or XOFF (false) to the other device. + WARNING: this function is not portable to different platforms! + """ + if not self.is_open: + raise portNotOpenError + if enable: + termios.tcflow(self.fd, termios.TCION) + else: + termios.tcflow(self.fd, termios.TCIOFF) + + def set_output_flow_control(self, enable=True): + """\ + Manually control flow of outgoing data - when hardware or software flow + control is enabled. + WARNING: this function is not portable to different platforms! + """ + if not self.is_open: + raise portNotOpenError + if enable: + termios.tcflow(self.fd, termios.TCOON) + else: + termios.tcflow(self.fd, termios.TCOOFF) + + def nonblocking(self): + """DEPRECATED - has no use""" + import warnings + warnings.warn("nonblocking() has no effect, already nonblocking", DeprecationWarning) + + +class PosixPollSerial(Serial): + """\ + Poll based read implementation. Not all systems support poll properly. + However this one has better handling of errors, such as a device + disconnecting while it's in use (e.g. USB-serial unplugged). + """ + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise portNotOpenError + read = bytearray() + poll = select.poll() + poll.register(self.fd, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL) + if size > 0: + while len(read) < size: + # print "\tread(): size",size, "have", len(read) #debug + # wait until device becomes ready to read (or something fails) + for fd, event in poll.poll(self._timeout * 1000): + if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL): + raise SerialException('device reports error (poll)') + # we don't care if it is select.POLLIN or timeout, that's + # handled below + buf = os.read(self.fd, size - len(read)) + read.extend(buf) + if ((self._timeout is not None and self._timeout >= 0) or + (self._inter_byte_timeout is not None and self._inter_byte_timeout > 0)) and not buf: + break # early abort on timeout + return bytes(read) + + +class VTIMESerial(Serial): + """\ + Implement timeout using vtime of tty device instead of using select. + This means that no inter character timeout can be specified and that + the error handling is degraded. + + Overall timeout is disabled when inter-character timeout is used. + """ + + def _reconfigure_port(self, force_update=True): + """Set communication parameters on opened port.""" + super(VTIMESerial, self)._reconfigure_port() + fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # clear O_NONBLOCK + + if self._inter_byte_timeout is not None: + vmin = 1 + vtime = int(self._inter_byte_timeout * 10) + elif self._timeout is None: + vmin = 1 + vtime = 0 + else: + vmin = 0 + vtime = int(self._timeout * 10) + try: + orig_attr = termios.tcgetattr(self.fd) + iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr + except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here + raise serial.SerialException("Could not configure port: {}".format(msg)) + + if vtime < 0 or vtime > 255: + raise ValueError('Invalid vtime: {!r}'.format(vtime)) + cc[termios.VTIME] = vtime + cc[termios.VMIN] = vmin + + termios.tcsetattr( + self.fd, + termios.TCSANOW, + [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise portNotOpenError + read = bytearray() + while len(read) < size: + buf = os.read(self.fd, size - len(read)) + if not buf: + break + read.extend(buf) + return bytes(read) + + # hack to make hasattr return false + cancel_read = property() diff --git a/libs/serial/serialutil.py b/libs/serial/serialutil.py new file mode 100644 index 0000000..636a10c --- /dev/null +++ b/libs/serial/serialutil.py @@ -0,0 +1,672 @@ +#! python +# +# Base class and support functions used by various backends. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2016 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +import io +import time + +# ``memoryview`` was introduced in Python 2.7 and ``bytes(some_memoryview)`` +# isn't returning the contents (very unfortunate). Therefore we need special +# cases and test for it. Ensure that there is a ``memoryview`` object for older +# Python versions. This is easier than making every test dependent on its +# existence. +try: + memoryview +except (NameError, AttributeError): + # implementation does not matter as we do not really use it. + # it just must not inherit from something else we might care for. + class memoryview(object): # pylint: disable=redefined-builtin,invalid-name + pass + +try: + unicode +except (NameError, AttributeError): + unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name + +try: + basestring +except (NameError, AttributeError): + basestring = (str,) # for Python 3, pylint: disable=redefined-builtin,invalid-name + + +# "for byte in data" fails for python3 as it returns ints instead of bytes +def iterbytes(b): + """Iterate over bytes, returning bytes instead of ints (python3)""" + if isinstance(b, memoryview): + b = b.tobytes() + i = 0 + while True: + a = b[i:i + 1] + i += 1 + if a: + yield a + else: + break + + +# all Python versions prior 3.x convert ``str([17])`` to '[17]' instead of '\x11' +# so a simple ``bytes(sequence)`` doesn't work for all versions +def to_bytes(seq): + """convert a sequence to a bytes type""" + if isinstance(seq, bytes): + return seq + elif isinstance(seq, bytearray): + return bytes(seq) + elif isinstance(seq, memoryview): + return seq.tobytes() + elif isinstance(seq, unicode): + raise TypeError('unicode strings are not supported, please encode to bytes: {!r}'.format(seq)) + else: + # handle list of integers and bytes (one or more items) for Python 2 and 3 + return bytes(bytearray(seq)) + + +# create control bytes +XON = to_bytes([17]) +XOFF = to_bytes([19]) + +CR = to_bytes([13]) +LF = to_bytes([10]) + + +PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = 'N', 'E', 'O', 'M', 'S' +STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2) +FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS = (5, 6, 7, 8) + +PARITY_NAMES = { + PARITY_NONE: 'None', + PARITY_EVEN: 'Even', + PARITY_ODD: 'Odd', + PARITY_MARK: 'Mark', + PARITY_SPACE: 'Space', +} + + +class SerialException(IOError): + """Base class for serial port related exceptions.""" + + +class SerialTimeoutException(SerialException): + """Write timeouts give an exception""" + + +writeTimeoutError = SerialTimeoutException('Write timeout') +portNotOpenError = SerialException('Attempting to use a port that is not open') + + +class Timeout(object): + """\ + Abstraction for timeout operations. Using time.monotonic() if available + or time.time() in all other cases. + + The class can also be initialized with 0 or None, in order to support + non-blocking and fully blocking I/O operations. The attributes + is_non_blocking and is_infinite are set accordingly. + """ + if hasattr(time, 'monotonic'): + # Timeout implementation with time.monotonic(). This function is only + # supported by Python 3.3 and above. It returns a time in seconds + # (float) just as time.time(), but is not affected by system clock + # adjustments. + TIME = time.monotonic + else: + # Timeout implementation with time.time(). This is compatible with all + # Python versions but has issues if the clock is adjusted while the + # timeout is running. + TIME = time.time + + def __init__(self, duration): + """Initialize a timeout with given duration""" + self.is_infinite = (duration is None) + self.is_non_blocking = (duration == 0) + self.duration = duration + if duration is not None: + self.target_time = self.TIME() + duration + else: + self.target_time = None + + def expired(self): + """Return a boolean, telling if the timeout has expired""" + return self.target_time is not None and self.time_left() <= 0 + + def time_left(self): + """Return how many seconds are left until the timeout expires""" + if self.is_non_blocking: + return 0 + elif self.is_infinite: + return None + else: + delta = self.target_time - self.TIME() + if delta > self.duration: + # clock jumped, recalculate + self.target_time = self.TIME() + self.duration + return self.duration + else: + return max(0, delta) + + def restart(self, duration): + """\ + Restart a timeout, only supported if a timeout was already set up + before. + """ + self.duration = duration + self.target_time = self.TIME() + duration + + +class SerialBase(io.RawIOBase): + """\ + Serial port base class. Provides __init__ function and properties to + get/set port settings. + """ + + # default values, may be overridden in subclasses that do not support all values + BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200, 230400, 460800, 500000, + 576000, 921600, 1000000, 1152000, 1500000, 2000000, 2500000, + 3000000, 3500000, 4000000) + BYTESIZES = (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS) + PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE) + STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO) + + def __init__(self, + port=None, + baudrate=9600, + bytesize=EIGHTBITS, + parity=PARITY_NONE, + stopbits=STOPBITS_ONE, + timeout=None, + xonxoff=False, + rtscts=False, + write_timeout=None, + dsrdtr=False, + inter_byte_timeout=None, + **kwargs): + """\ + Initialize comm port object. If a "port" is given, then the port will be + opened immediately. Otherwise a Serial port object in closed state + is returned. + """ + + self.is_open = False + self.portstr = None + self.name = None + # correct values are assigned below through properties + self._port = None + self._baudrate = None + self._bytesize = None + self._parity = None + self._stopbits = None + self._timeout = None + self._write_timeout = None + self._xonxoff = None + self._rtscts = None + self._dsrdtr = None + self._inter_byte_timeout = None + self._rs485_mode = None # disabled by default + self._rts_state = True + self._dtr_state = True + self._break_state = False + + # assign values using get/set methods using the properties feature + self.port = port + self.baudrate = baudrate + self.bytesize = bytesize + self.parity = parity + self.stopbits = stopbits + self.timeout = timeout + self.write_timeout = write_timeout + self.xonxoff = xonxoff + self.rtscts = rtscts + self.dsrdtr = dsrdtr + self.inter_byte_timeout = inter_byte_timeout + # watch for backward compatible kwargs + if 'writeTimeout' in kwargs: + self.write_timeout = kwargs.pop('writeTimeout') + if 'interCharTimeout' in kwargs: + self.inter_byte_timeout = kwargs.pop('interCharTimeout') + if kwargs: + raise ValueError('unexpected keyword arguments: {!r}'.format(kwargs)) + + if port is not None: + self.open() + + # - - - - - - - - - - - - - - - - - - - - - - - - + + # to be implemented by subclasses: + # def open(self): + # def close(self): + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def port(self): + """\ + Get the current port setting. The value that was passed on init or using + setPort() is passed back. + """ + return self._port + + @port.setter + def port(self, port): + """\ + Change the port. + """ + if port is not None and not isinstance(port, basestring): + raise ValueError('"port" must be None or a string, not {}'.format(type(port))) + was_open = self.is_open + if was_open: + self.close() + self.portstr = port + self._port = port + self.name = self.portstr + if was_open: + self.open() + + @property + def baudrate(self): + """Get the current baud rate setting.""" + return self._baudrate + + @baudrate.setter + def baudrate(self, baudrate): + """\ + Change baud rate. It raises a ValueError if the port is open and the + baud rate is not possible. If the port is closed, then the value is + accepted and the exception is raised when the port is opened. + """ + try: + b = int(baudrate) + except TypeError: + raise ValueError("Not a valid baudrate: {!r}".format(baudrate)) + else: + if b < 0: + raise ValueError("Not a valid baudrate: {!r}".format(baudrate)) + self._baudrate = b + if self.is_open: + self._reconfigure_port() + + @property + def bytesize(self): + """Get the current byte size setting.""" + return self._bytesize + + @bytesize.setter + def bytesize(self, bytesize): + """Change byte size.""" + if bytesize not in self.BYTESIZES: + raise ValueError("Not a valid byte size: {!r}".format(bytesize)) + self._bytesize = bytesize + if self.is_open: + self._reconfigure_port() + + @property + def parity(self): + """Get the current parity setting.""" + return self._parity + + @parity.setter + def parity(self, parity): + """Change parity setting.""" + if parity not in self.PARITIES: + raise ValueError("Not a valid parity: {!r}".format(parity)) + self._parity = parity + if self.is_open: + self._reconfigure_port() + + @property + def stopbits(self): + """Get the current stop bits setting.""" + return self._stopbits + + @stopbits.setter + def stopbits(self, stopbits): + """Change stop bits size.""" + if stopbits not in self.STOPBITS: + raise ValueError("Not a valid stop bit size: {!r}".format(stopbits)) + self._stopbits = stopbits + if self.is_open: + self._reconfigure_port() + + @property + def timeout(self): + """Get the current timeout setting.""" + return self._timeout + + @timeout.setter + def timeout(self, timeout): + """Change timeout setting.""" + if timeout is not None: + try: + timeout + 1 # test if it's a number, will throw a TypeError if not... + except TypeError: + raise ValueError("Not a valid timeout: {!r}".format(timeout)) + if timeout < 0: + raise ValueError("Not a valid timeout: {!r}".format(timeout)) + self._timeout = timeout + if self.is_open: + self._reconfigure_port() + + @property + def write_timeout(self): + """Get the current timeout setting.""" + return self._write_timeout + + @write_timeout.setter + def write_timeout(self, timeout): + """Change timeout setting.""" + if timeout is not None: + if timeout < 0: + raise ValueError("Not a valid timeout: {!r}".format(timeout)) + try: + timeout + 1 # test if it's a number, will throw a TypeError if not... + except TypeError: + raise ValueError("Not a valid timeout: {!r}".format(timeout)) + + self._write_timeout = timeout + if self.is_open: + self._reconfigure_port() + + @property + def inter_byte_timeout(self): + """Get the current inter-character timeout setting.""" + return self._inter_byte_timeout + + @inter_byte_timeout.setter + def inter_byte_timeout(self, ic_timeout): + """Change inter-byte timeout setting.""" + if ic_timeout is not None: + if ic_timeout < 0: + raise ValueError("Not a valid timeout: {!r}".format(ic_timeout)) + try: + ic_timeout + 1 # test if it's a number, will throw a TypeError if not... + except TypeError: + raise ValueError("Not a valid timeout: {!r}".format(ic_timeout)) + + self._inter_byte_timeout = ic_timeout + if self.is_open: + self._reconfigure_port() + + @property + def xonxoff(self): + """Get the current XON/XOFF setting.""" + return self._xonxoff + + @xonxoff.setter + def xonxoff(self, xonxoff): + """Change XON/XOFF setting.""" + self._xonxoff = xonxoff + if self.is_open: + self._reconfigure_port() + + @property + def rtscts(self): + """Get the current RTS/CTS flow control setting.""" + return self._rtscts + + @rtscts.setter + def rtscts(self, rtscts): + """Change RTS/CTS flow control setting.""" + self._rtscts = rtscts + if self.is_open: + self._reconfigure_port() + + @property + def dsrdtr(self): + """Get the current DSR/DTR flow control setting.""" + return self._dsrdtr + + @dsrdtr.setter + def dsrdtr(self, dsrdtr=None): + """Change DsrDtr flow control setting.""" + if dsrdtr is None: + # if not set, keep backwards compatibility and follow rtscts setting + self._dsrdtr = self._rtscts + else: + # if defined independently, follow its value + self._dsrdtr = dsrdtr + if self.is_open: + self._reconfigure_port() + + @property + def rts(self): + return self._rts_state + + @rts.setter + def rts(self, value): + self._rts_state = value + if self.is_open: + self._update_rts_state() + + @property + def dtr(self): + return self._dtr_state + + @dtr.setter + def dtr(self, value): + self._dtr_state = value + if self.is_open: + self._update_dtr_state() + + @property + def break_condition(self): + return self._break_state + + @break_condition.setter + def break_condition(self, value): + self._break_state = value + if self.is_open: + self._update_break_state() + + # - - - - - - - - - - - - - - - - - - - - - - - - + # functions useful for RS-485 adapters + + @property + def rs485_mode(self): + """\ + Enable RS485 mode and apply new settings, set to None to disable. + See serial.rs485.RS485Settings for more info about the value. + """ + return self._rs485_mode + + @rs485_mode.setter + def rs485_mode(self, rs485_settings): + self._rs485_mode = rs485_settings + if self.is_open: + self._reconfigure_port() + + # - - - - - - - - - - - - - - - - - - - - - - - - + + _SAVED_SETTINGS = ('baudrate', 'bytesize', 'parity', 'stopbits', 'xonxoff', + 'dsrdtr', 'rtscts', 'timeout', 'write_timeout', + 'inter_byte_timeout') + + def get_settings(self): + """\ + Get current port settings as a dictionary. For use with + apply_settings(). + """ + return dict([(key, getattr(self, '_' + key)) for key in self._SAVED_SETTINGS]) + + def apply_settings(self, d): + """\ + Apply stored settings from a dictionary returned from + get_settings(). It's allowed to delete keys from the dictionary. These + values will simply left unchanged. + """ + for key in self._SAVED_SETTINGS: + if key in d and d[key] != getattr(self, '_' + key): # check against internal "_" value + setattr(self, key, d[key]) # set non "_" value to use properties write function + + # - - - - - - - - - - - - - - - - - - - - - - - - + + def __repr__(self): + """String representation of the current port settings and its state.""" + return '{name}(port={p.portstr!r}, ' \ + 'baudrate={p.baudrate!r}, bytesize={p.bytesize!r}, parity={p.parity!r}, ' \ + 'stopbits={p.stopbits!r}, timeout={p.timeout!r}, xonxoff={p.xonxoff!r}, ' \ + 'rtscts={p.rtscts!r}, dsrdtr={p.dsrdtr!r})'.format( + name=self.__class__.__name__, id=id(self), p=self) + + # - - - - - - - - - - - - - - - - - - - - - - - - + # compatibility with io library + # pylint: disable=invalid-name,missing-docstring + + def readable(self): + return True + + def writable(self): + return True + + def seekable(self): + return False + + def readinto(self, b): + data = self.read(len(b)) + n = len(data) + try: + b[:n] = data + except TypeError as err: + import array + if not isinstance(b, array.array): + raise err + b[:n] = array.array('b', data) + return n + + # - - - - - - - - - - - - - - - - - - - - - - - - + # context manager + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + self.close() + + # - - - - - - - - - - - - - - - - - - - - - - - - + + def send_break(self, duration=0.25): + """\ + Send break condition. Timed, returns to idle state after given + duration. + """ + if not self.is_open: + raise portNotOpenError + self.break_condition = True + time.sleep(duration) + self.break_condition = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + # backwards compatibility / deprecated functions + + def flushInput(self): + self.reset_input_buffer() + + def flushOutput(self): + self.reset_output_buffer() + + def inWaiting(self): + return self.in_waiting + + def sendBreak(self, duration=0.25): + self.send_break(duration) + + def setRTS(self, value=1): + self.rts = value + + def setDTR(self, value=1): + self.dtr = value + + def getCTS(self): + return self.cts + + def getDSR(self): + return self.dsr + + def getRI(self): + return self.ri + + def getCD(self): + return self.cd + + def setPort(self, port): + self.port = port + + @property + def writeTimeout(self): + return self.write_timeout + + @writeTimeout.setter + def writeTimeout(self, timeout): + self.write_timeout = timeout + + @property + def interCharTimeout(self): + return self.inter_byte_timeout + + @interCharTimeout.setter + def interCharTimeout(self, interCharTimeout): + self.inter_byte_timeout = interCharTimeout + + def getSettingsDict(self): + return self.get_settings() + + def applySettingsDict(self, d): + self.apply_settings(d) + + def isOpen(self): + return self.is_open + + # - - - - - - - - - - - - - - - - - - - - - - - - + # additional functionality + + def read_all(self): + """\ + Read all bytes currently available in the buffer of the OS. + """ + return self.read(self.in_waiting) + + def read_until(self, terminator=LF, size=None): + """\ + Read until a termination sequence is found ('\n' by default), the size + is exceeded or until timeout occurs. + """ + lenterm = len(terminator) + line = bytearray() + while True: + c = self.read(1) + if c: + line += c + if line[-lenterm:] == terminator: + break + if size is not None and len(line) >= size: + break + else: + break + return bytes(line) + + def iread_until(self, *args, **kwargs): + """\ + Read lines, implemented as generator. It will raise StopIteration on + timeout (empty read). + """ + while True: + line = self.read_until(*args, **kwargs) + if not line: + break + yield line + + +# - - - - - - - - - - - - - - - - - - - - - - - - - +if __name__ == '__main__': + import sys + s = SerialBase() + sys.stdout.write('port name: {}\n'.format(s.name)) + sys.stdout.write('baud rates: {}\n'.format(s.BAUDRATES)) + sys.stdout.write('byte sizes: {}\n'.format(s.BYTESIZES)) + sys.stdout.write('parities: {}\n'.format(s.PARITIES)) + sys.stdout.write('stop bits: {}\n'.format(s.STOPBITS)) + sys.stdout.write('{}\n'.format(s)) diff --git a/libs/serial/serialwin32.py b/libs/serial/serialwin32.py new file mode 100644 index 0000000..b275ea3 --- /dev/null +++ b/libs/serial/serialwin32.py @@ -0,0 +1,467 @@ +#! python +# +# backend for Windows ("win32" incl. 32/64 bit support) +# +# (C) 2001-2015 Chris Liechti +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# SPDX-License-Identifier: BSD-3-Clause +# +# Initial patch to use ctypes by Giovanni Bajo + +# pylint: disable=invalid-name,too-few-public-methods +import ctypes +import time +from serial import win32 + +import serial +from serial.serialutil import SerialBase, SerialException, to_bytes, portNotOpenError, writeTimeoutError + + +class Serial(SerialBase): + """Serial port implementation for Win32 based on ctypes.""" + + BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200) + + def __init__(self, *args, **kwargs): + self._port_handle = None + self._overlapped_read = None + self._overlapped_write = None + super(Serial, self).__init__(*args, **kwargs) + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened. + """ + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + # the "\\.\COMx" format is required for devices other than COM1-COM8 + # not all versions of windows seem to support this properly + # so that the first few ports are used with the DOS device name + port = self.name + try: + if port.upper().startswith('COM') and int(port[3:]) > 8: + port = '\\\\.\\' + port + except ValueError: + # for like COMnotanumber + pass + self._port_handle = win32.CreateFile( + port, + win32.GENERIC_READ | win32.GENERIC_WRITE, + 0, # exclusive access + None, # no security + win32.OPEN_EXISTING, + win32.FILE_ATTRIBUTE_NORMAL | win32.FILE_FLAG_OVERLAPPED, + 0) + if self._port_handle == win32.INVALID_HANDLE_VALUE: + self._port_handle = None # 'cause __del__ is called anyway + raise SerialException("could not open port {!r}: {!r}".format(self.portstr, ctypes.WinError())) + + try: + self._overlapped_read = win32.OVERLAPPED() + self._overlapped_read.hEvent = win32.CreateEvent(None, 1, 0, None) + self._overlapped_write = win32.OVERLAPPED() + #~ self._overlapped_write.hEvent = win32.CreateEvent(None, 1, 0, None) + self._overlapped_write.hEvent = win32.CreateEvent(None, 0, 0, None) + + # Setup a 4k buffer + win32.SetupComm(self._port_handle, 4096, 4096) + + # Save original timeout values: + self._orgTimeouts = win32.COMMTIMEOUTS() + win32.GetCommTimeouts(self._port_handle, ctypes.byref(self._orgTimeouts)) + + self._reconfigure_port() + + # Clear buffers: + # Remove anything that was there + win32.PurgeComm( + self._port_handle, + win32.PURGE_TXCLEAR | win32.PURGE_TXABORT | + win32.PURGE_RXCLEAR | win32.PURGE_RXABORT) + except: + try: + self._close() + except: + # ignore any exception when closing the port + # also to keep original exception that happened when setting up + pass + self._port_handle = None + raise + else: + self.is_open = True + + def _reconfigure_port(self): + """Set communication parameters on opened port.""" + if not self._port_handle: + raise SerialException("Can only operate on a valid port handle") + + # Set Windows timeout values + # timeouts is a tuple with the following items: + # (ReadIntervalTimeout,ReadTotalTimeoutMultiplier, + # ReadTotalTimeoutConstant,WriteTotalTimeoutMultiplier, + # WriteTotalTimeoutConstant) + timeouts = win32.COMMTIMEOUTS() + if self._timeout is None: + pass # default of all zeros is OK + elif self._timeout == 0: + timeouts.ReadIntervalTimeout = win32.MAXDWORD + else: + timeouts.ReadTotalTimeoutConstant = max(int(self._timeout * 1000), 1) + if self._timeout != 0 and self._inter_byte_timeout is not None: + timeouts.ReadIntervalTimeout = max(int(self._inter_byte_timeout * 1000), 1) + + if self._write_timeout is None: + pass + elif self._write_timeout == 0: + timeouts.WriteTotalTimeoutConstant = win32.MAXDWORD + else: + timeouts.WriteTotalTimeoutConstant = max(int(self._write_timeout * 1000), 1) + win32.SetCommTimeouts(self._port_handle, ctypes.byref(timeouts)) + + win32.SetCommMask(self._port_handle, win32.EV_ERR) + + # Setup the connection info. + # Get state and modify it: + comDCB = win32.DCB() + win32.GetCommState(self._port_handle, ctypes.byref(comDCB)) + comDCB.BaudRate = self._baudrate + + if self._bytesize == serial.FIVEBITS: + comDCB.ByteSize = 5 + elif self._bytesize == serial.SIXBITS: + comDCB.ByteSize = 6 + elif self._bytesize == serial.SEVENBITS: + comDCB.ByteSize = 7 + elif self._bytesize == serial.EIGHTBITS: + comDCB.ByteSize = 8 + else: + raise ValueError("Unsupported number of data bits: {!r}".format(self._bytesize)) + + if self._parity == serial.PARITY_NONE: + comDCB.Parity = win32.NOPARITY + comDCB.fParity = 0 # Disable Parity Check + elif self._parity == serial.PARITY_EVEN: + comDCB.Parity = win32.EVENPARITY + comDCB.fParity = 1 # Enable Parity Check + elif self._parity == serial.PARITY_ODD: + comDCB.Parity = win32.ODDPARITY + comDCB.fParity = 1 # Enable Parity Check + elif self._parity == serial.PARITY_MARK: + comDCB.Parity = win32.MARKPARITY + comDCB.fParity = 1 # Enable Parity Check + elif self._parity == serial.PARITY_SPACE: + comDCB.Parity = win32.SPACEPARITY + comDCB.fParity = 1 # Enable Parity Check + else: + raise ValueError("Unsupported parity mode: {!r}".format(self._parity)) + + if self._stopbits == serial.STOPBITS_ONE: + comDCB.StopBits = win32.ONESTOPBIT + elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE: + comDCB.StopBits = win32.ONE5STOPBITS + elif self._stopbits == serial.STOPBITS_TWO: + comDCB.StopBits = win32.TWOSTOPBITS + else: + raise ValueError("Unsupported number of stop bits: {!r}".format(self._stopbits)) + + comDCB.fBinary = 1 # Enable Binary Transmission + # Char. w/ Parity-Err are replaced with 0xff (if fErrorChar is set to TRUE) + if self._rs485_mode is None: + if self._rtscts: + comDCB.fRtsControl = win32.RTS_CONTROL_HANDSHAKE + else: + comDCB.fRtsControl = win32.RTS_CONTROL_ENABLE if self._rts_state else win32.RTS_CONTROL_DISABLE + comDCB.fOutxCtsFlow = self._rtscts + else: + # checks for unsupported settings + # XXX verify if platform really does not have a setting for those + if not self._rs485_mode.rts_level_for_tx: + raise ValueError( + 'Unsupported value for RS485Settings.rts_level_for_tx: {!r}'.format( + self._rs485_mode.rts_level_for_tx,)) + if self._rs485_mode.rts_level_for_rx: + raise ValueError( + 'Unsupported value for RS485Settings.rts_level_for_rx: {!r}'.format( + self._rs485_mode.rts_level_for_rx,)) + if self._rs485_mode.delay_before_tx is not None: + raise ValueError( + 'Unsupported value for RS485Settings.delay_before_tx: {!r}'.format( + self._rs485_mode.delay_before_tx,)) + if self._rs485_mode.delay_before_rx is not None: + raise ValueError( + 'Unsupported value for RS485Settings.delay_before_rx: {!r}'.format( + self._rs485_mode.delay_before_rx,)) + if self._rs485_mode.loopback: + raise ValueError( + 'Unsupported value for RS485Settings.loopback: {!r}'.format( + self._rs485_mode.loopback,)) + comDCB.fRtsControl = win32.RTS_CONTROL_TOGGLE + comDCB.fOutxCtsFlow = 0 + + if self._dsrdtr: + comDCB.fDtrControl = win32.DTR_CONTROL_HANDSHAKE + else: + comDCB.fDtrControl = win32.DTR_CONTROL_ENABLE if self._dtr_state else win32.DTR_CONTROL_DISABLE + comDCB.fOutxDsrFlow = self._dsrdtr + comDCB.fOutX = self._xonxoff + comDCB.fInX = self._xonxoff + comDCB.fNull = 0 + comDCB.fErrorChar = 0 + comDCB.fAbortOnError = 0 + comDCB.XonChar = serial.XON + comDCB.XoffChar = serial.XOFF + + if not win32.SetCommState(self._port_handle, ctypes.byref(comDCB)): + raise SerialException( + 'Cannot configure port, something went wrong. ' + 'Original message: {!r}'.format(ctypes.WinError())) + + #~ def __del__(self): + #~ self.close() + + def _close(self): + """internal close port helper""" + if self._port_handle is not None: + # Restore original timeout values: + win32.SetCommTimeouts(self._port_handle, self._orgTimeouts) + if self._overlapped_read is not None: + self.cancel_read() + win32.CloseHandle(self._overlapped_read.hEvent) + self._overlapped_read = None + if self._overlapped_write is not None: + self.cancel_write() + win32.CloseHandle(self._overlapped_write.hEvent) + self._overlapped_write = None + win32.CloseHandle(self._port_handle) + self._port_handle = None + + def close(self): + """Close port""" + if self.is_open: + self._close() + self.is_open = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of bytes currently in the input buffer.""" + flags = win32.DWORD() + comstat = win32.COMSTAT() + if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)): + raise SerialException('call to ClearCommError failed') + return comstat.cbInQue + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise portNotOpenError + if size > 0: + win32.ResetEvent(self._overlapped_read.hEvent) + flags = win32.DWORD() + comstat = win32.COMSTAT() + if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)): + raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError())) + n = min(comstat.cbInQue, size) if self.timeout == 0 else size + if n > 0: + buf = ctypes.create_string_buffer(n) + rc = win32.DWORD() + read_ok = win32.ReadFile( + self._port_handle, + buf, + n, + ctypes.byref(rc), + ctypes.byref(self._overlapped_read)) + if not read_ok and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING): + raise SerialException("ReadFile failed ({!r})".format(ctypes.WinError())) + result_ok = win32.GetOverlappedResult( + self._port_handle, + ctypes.byref(self._overlapped_read), + ctypes.byref(rc), + True) + if not result_ok: + if win32.GetLastError() != win32.ERROR_OPERATION_ABORTED: + raise SerialException("GetOverlappedResult failed ({!r})".format(ctypes.WinError())) + read = buf.raw[:rc.value] + else: + read = bytes() + else: + read = bytes() + return bytes(read) + + def write(self, data): + """Output the given byte string over the serial port.""" + if not self.is_open: + raise portNotOpenError + #~ if not isinstance(data, (bytes, bytearray)): + #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data))) + # convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview + data = to_bytes(data) + if data: + #~ win32event.ResetEvent(self._overlapped_write.hEvent) + n = win32.DWORD() + success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write) + if self._write_timeout != 0: # if blocking (None) or w/ write timeout (>0) + if not success and win32.GetLastError() != win32.ERROR_IO_PENDING: + raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError())) + + # Wait for the write to complete. + #~ win32.WaitForSingleObject(self._overlapped_write.hEvent, win32.INFINITE) + win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), True) + if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED: + return n.value # canceled IO is no error + if n.value != len(data): + raise writeTimeoutError + return n.value + else: + errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError() + if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY, + win32.ERROR_OPERATION_ABORTED): + return 0 + elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING): + # no info on true length provided by OS function in async mode + return len(data) + else: + raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError())) + else: + return 0 + + def flush(self): + """\ + Flush of file like objects. In this case, wait until all data + is written. + """ + while self.out_waiting: + time.sleep(0.05) + # XXX could also use WaitCommEvent with mask EV_TXEMPTY, but it would + # require overlapped IO and it's also only possible to set a single mask + # on the port--- + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.is_open: + raise portNotOpenError + win32.PurgeComm(self._port_handle, win32.PURGE_RXCLEAR | win32.PURGE_RXABORT) + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and discarding all + that is in the buffer. + """ + if not self.is_open: + raise portNotOpenError + win32.PurgeComm(self._port_handle, win32.PURGE_TXCLEAR | win32.PURGE_TXABORT) + + def _update_break_state(self): + """Set break: Controls TXD. When active, to transmitting is possible.""" + if not self.is_open: + raise portNotOpenError + if self._break_state: + win32.SetCommBreak(self._port_handle) + else: + win32.ClearCommBreak(self._port_handle) + + def _update_rts_state(self): + """Set terminal status line: Request To Send""" + if self._rts_state: + win32.EscapeCommFunction(self._port_handle, win32.SETRTS) + else: + win32.EscapeCommFunction(self._port_handle, win32.CLRRTS) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready""" + if self._dtr_state: + win32.EscapeCommFunction(self._port_handle, win32.SETDTR) + else: + win32.EscapeCommFunction(self._port_handle, win32.CLRDTR) + + def _GetCommModemStatus(self): + if not self.is_open: + raise portNotOpenError + stat = win32.DWORD() + win32.GetCommModemStatus(self._port_handle, ctypes.byref(stat)) + return stat.value + + @property + def cts(self): + """Read terminal status line: Clear To Send""" + return win32.MS_CTS_ON & self._GetCommModemStatus() != 0 + + @property + def dsr(self): + """Read terminal status line: Data Set Ready""" + return win32.MS_DSR_ON & self._GetCommModemStatus() != 0 + + @property + def ri(self): + """Read terminal status line: Ring Indicator""" + return win32.MS_RING_ON & self._GetCommModemStatus() != 0 + + @property + def cd(self): + """Read terminal status line: Carrier Detect""" + return win32.MS_RLSD_ON & self._GetCommModemStatus() != 0 + + # - - platform specific - - - - + + def set_buffer_size(self, rx_size=4096, tx_size=None): + """\ + Recommend a buffer size to the driver (device driver can ignore this + value). Must be called before the port is opened. + """ + if tx_size is None: + tx_size = rx_size + win32.SetupComm(self._port_handle, rx_size, tx_size) + + def set_output_flow_control(self, enable=True): + """\ + Manually control flow - when software flow control is enabled. + This will do the same as if XON (true) or XOFF (false) are received + from the other device and control the transmission accordingly. + WARNING: this function is not portable to different platforms! + """ + if not self.is_open: + raise portNotOpenError + if enable: + win32.EscapeCommFunction(self._port_handle, win32.SETXON) + else: + win32.EscapeCommFunction(self._port_handle, win32.SETXOFF) + + @property + def out_waiting(self): + """Return how many bytes the in the outgoing buffer""" + flags = win32.DWORD() + comstat = win32.COMSTAT() + if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)): + raise SerialException('call to ClearCommError failed') + return comstat.cbOutQue + + def _cancel_overlapped_io(self, overlapped): + """Cancel a blocking read operation, may be called from other thread""" + # check if read operation is pending + rc = win32.DWORD() + err = win32.GetOverlappedResult( + self._port_handle, + ctypes.byref(overlapped), + ctypes.byref(rc), + False) + if not err and win32.GetLastError() in (win32.ERROR_IO_PENDING, win32.ERROR_IO_INCOMPLETE): + # cancel, ignoring any errors (e.g. it may just have finished on its own) + win32.CancelIoEx(self._port_handle, overlapped) + + def cancel_read(self): + """Cancel a blocking read operation, may be called from other thread""" + self._cancel_overlapped_io(self._overlapped_read) + + def cancel_write(self): + """Cancel a blocking write operation, may be called from other thread""" + self._cancel_overlapped_io(self._overlapped_write) diff --git a/libs/serial/threaded/__init__.py b/libs/serial/threaded/__init__.py new file mode 100644 index 0000000..74b6924 --- /dev/null +++ b/libs/serial/threaded/__init__.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +# +# Working with threading and pySerial +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015-2016 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +"""\ +Support threading with serial ports. +""" +import serial +import threading + + +class Protocol(object): + """\ + Protocol as used by the ReaderThread. This base class provides empty + implementations of all methods. + """ + + def connection_made(self, transport): + """Called when reader thread is started""" + + def data_received(self, data): + """Called with snippets received from the serial port""" + + def connection_lost(self, exc): + """\ + Called when the serial port is closed or the reader loop terminated + otherwise. + """ + if isinstance(exc, Exception): + raise exc + + +class Packetizer(Protocol): + """ + Read binary packets from serial port. Packets are expected to be terminated + with a TERMINATOR byte (null byte by default). + + The class also keeps track of the transport. + """ + + TERMINATOR = b'\0' + + def __init__(self): + self.buffer = bytearray() + self.transport = None + + def connection_made(self, transport): + """Store transport""" + self.transport = transport + + def connection_lost(self, exc): + """Forget transport""" + self.transport = None + super(Packetizer, self).connection_lost(exc) + + def data_received(self, data): + """Buffer received data, find TERMINATOR, call handle_packet""" + self.buffer.extend(data) + while self.TERMINATOR in self.buffer: + packet, self.buffer = self.buffer.split(self.TERMINATOR, 1) + self.handle_packet(packet) + + def handle_packet(self, packet): + """Process packets - to be overridden by subclassing""" + raise NotImplementedError('please implement functionality in handle_packet') + + +class FramedPacket(Protocol): + """ + Read binary packets. Packets are expected to have a start and stop marker. + + The class also keeps track of the transport. + """ + + START = b'(' + STOP = b')' + + def __init__(self): + self.packet = bytearray() + self.in_packet = False + self.transport = None + + def connection_made(self, transport): + """Store transport""" + self.transport = transport + + def connection_lost(self, exc): + """Forget transport""" + self.transport = None + self.in_packet = False + del self.packet[:] + super(FramedPacket, self).connection_lost(exc) + + def data_received(self, data): + """Find data enclosed in START/STOP, call handle_packet""" + for byte in serial.iterbytes(data): + if byte == self.START: + self.in_packet = True + elif byte == self.STOP: + self.in_packet = False + self.handle_packet(bytes(self.packet)) # make read-only copy + del self.packet[:] + elif self.in_packet: + self.packet.extend(byte) + else: + self.handle_out_of_packet_data(byte) + + def handle_packet(self, packet): + """Process packets - to be overridden by subclassing""" + raise NotImplementedError('please implement functionality in handle_packet') + + def handle_out_of_packet_data(self, data): + """Process data that is received outside of packets""" + pass + + +class LineReader(Packetizer): + """ + Read and write (Unicode) lines from/to serial port. + The encoding is applied. + """ + + TERMINATOR = b'\r\n' + ENCODING = 'utf-8' + UNICODE_HANDLING = 'replace' + + def handle_packet(self, packet): + self.handle_line(packet.decode(self.ENCODING, self.UNICODE_HANDLING)) + + def handle_line(self, line): + """Process one line - to be overridden by subclassing""" + raise NotImplementedError('please implement functionality in handle_line') + + def write_line(self, text): + """ + Write text to the transport. ``text`` is a Unicode string and the encoding + is applied before sending ans also the newline is append. + """ + # + is not the best choice but bytes does not support % or .format in py3 and we want a single write call + self.transport.write(text.encode(self.ENCODING, self.UNICODE_HANDLING) + self.TERMINATOR) + + +class ReaderThread(threading.Thread): + """\ + Implement a serial port read loop and dispatch to a Protocol instance (like + the asyncio.Protocol) but do it with threads. + + Calls to close() will close the serial port but it is also possible to just + stop() this thread and continue the serial port instance otherwise. + """ + + def __init__(self, serial_instance, protocol_factory): + """\ + Initialize thread. + + Note that the serial_instance' timeout is set to one second! + Other settings are not changed. + """ + super(ReaderThread, self).__init__() + self.daemon = True + self.serial = serial_instance + self.protocol_factory = protocol_factory + self.alive = True + self._lock = threading.Lock() + self._connection_made = threading.Event() + self.protocol = None + + def stop(self): + """Stop the reader thread""" + self.alive = False + if hasattr(self.serial, 'cancel_read'): + self.serial.cancel_read() + self.join(2) + + def run(self): + """Reader loop""" + if not hasattr(self.serial, 'cancel_read'): + self.serial.timeout = 1 + self.protocol = self.protocol_factory() + try: + self.protocol.connection_made(self) + except Exception as e: + self.alive = False + self.protocol.connection_lost(e) + self._connection_made.set() + return + error = None + self._connection_made.set() + while self.alive and self.serial.is_open: + try: + # read all that is there or wait for one byte (blocking) + data = self.serial.read(self.serial.in_waiting or 1) + except serial.SerialException as e: + # probably some I/O problem such as disconnected USB serial + # adapters -> exit + error = e + break + else: + if data: + # make a separated try-except for called used code + try: + self.protocol.data_received(data) + except Exception as e: + error = e + break + self.alive = False + self.protocol.connection_lost(error) + self.protocol = None + + def write(self, data): + """Thread safe writing (uses lock)""" + with self._lock: + self.serial.write(data) + + def close(self): + """Close the serial port and exit reader thread (uses lock)""" + # use the lock to let other threads finish writing + with self._lock: + # first stop reading, so that closing can be done on idle port + self.stop() + self.serial.close() + + def connect(self): + """ + Wait until connection is set up and return the transport and protocol + instances. + """ + if self.alive: + self._connection_made.wait() + if not self.alive: + raise RuntimeError('connection_lost already called') + return (self, self.protocol) + else: + raise RuntimeError('already stopped') + + # - - context manager, returns protocol + + def __enter__(self): + """\ + Enter context handler. May raise RuntimeError in case the connection + could not be created. + """ + self.start() + self._connection_made.wait() + if not self.alive: + raise RuntimeError('connection_lost already called') + return self.protocol + + def __exit__(self, exc_type, exc_val, exc_tb): + """Leave context: close port""" + self.close() + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + # pylint: disable=wrong-import-position + import sys + import time + import traceback + + #~ PORT = 'spy:///dev/ttyUSB0' + PORT = 'loop://' + + class PrintLines(LineReader): + def connection_made(self, transport): + super(PrintLines, self).connection_made(transport) + sys.stdout.write('port opened\n') + self.write_line('hello world') + + def handle_line(self, data): + sys.stdout.write('line received: {!r}\n'.format(data)) + + def connection_lost(self, exc): + if exc: + traceback.print_exc(exc) + sys.stdout.write('port closed\n') + + ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1) + with ReaderThread(ser, PrintLines) as protocol: + protocol.write_line('hello') + time.sleep(2) + + # alternative usage + ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1) + t = ReaderThread(ser, PrintLines) + t.start() + transport, protocol = t.connect() + protocol.write_line('hello') + time.sleep(2) + t.close() diff --git a/libs/serial/tools/__init__.py b/libs/serial/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/serial/tools/hexlify_codec.py b/libs/serial/tools/hexlify_codec.py new file mode 100644 index 0000000..1371da2 --- /dev/null +++ b/libs/serial/tools/hexlify_codec.py @@ -0,0 +1,124 @@ +#! python +# +# This is a codec to create and decode hexdumps with spaces between characters. used by miniterm. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015-2016 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +"""\ +Python 'hex' Codec - 2-digit hex with spaces content transfer encoding. + +Encode and decode may be a bit missleading at first sight... + +The textual representation is a hex dump: e.g. "40 41" +The "encoded" data of this is the binary form, e.g. b"@A" + +Therefore decoding is binary to text and thus converting binary data to hex dump. + +""" + +import codecs +import serial + + +try: + unicode +except (NameError, AttributeError): + unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name + + +HEXDIGITS = '0123456789ABCDEF' + + +# Codec APIs + +def hex_encode(data, errors='strict'): + """'40 41 42' -> b'@ab'""" + return (serial.to_bytes([int(h, 16) for h in data.split()]), len(data)) + + +def hex_decode(data, errors='strict'): + """b'@ab' -> '40 41 42'""" + return (unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))), len(data)) + + +class Codec(codecs.Codec): + def encode(self, data, errors='strict'): + """'40 41 42' -> b'@ab'""" + return serial.to_bytes([int(h, 16) for h in data.split()]) + + def decode(self, data, errors='strict'): + """b'@ab' -> '40 41 42'""" + return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))) + + +class IncrementalEncoder(codecs.IncrementalEncoder): + """Incremental hex encoder""" + + def __init__(self, errors='strict'): + self.errors = errors + self.state = 0 + + def reset(self): + self.state = 0 + + def getstate(self): + return self.state + + def setstate(self, state): + self.state = state + + def encode(self, data, final=False): + """\ + Incremental encode, keep track of digits and emit a byte when a pair + of hex digits is found. The space is optional unless the error + handling is defined to be 'strict'. + """ + state = self.state + encoded = [] + for c in data.upper(): + if c in HEXDIGITS: + z = HEXDIGITS.index(c) + if state: + encoded.append(z + (state & 0xf0)) + state = 0 + else: + state = 0x100 + (z << 4) + elif c == ' ': # allow spaces to separate values + if state and self.errors == 'strict': + raise UnicodeError('odd number of hex digits') + state = 0 + else: + if self.errors == 'strict': + raise UnicodeError('non-hex digit found: {!r}'.format(c)) + self.state = state + return serial.to_bytes(encoded) + + +class IncrementalDecoder(codecs.IncrementalDecoder): + """Incremental decoder""" + def decode(self, data, final=False): + return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))) + + +class StreamWriter(Codec, codecs.StreamWriter): + """Combination of hexlify codec and StreamWriter""" + + +class StreamReader(Codec, codecs.StreamReader): + """Combination of hexlify codec and StreamReader""" + + +def getregentry(): + """encodings module API""" + return codecs.CodecInfo( + name='hexlify', + encode=hex_encode, + decode=hex_decode, + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + #~ _is_text_encoding=True, + ) diff --git a/libs/serial/tools/list_ports.py b/libs/serial/tools/list_ports.py new file mode 100644 index 0000000..2271dd1 --- /dev/null +++ b/libs/serial/tools/list_ports.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# +# Serial port enumeration. Console tool and backend selection. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2011-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +"""\ +This module will provide a function called comports that returns an +iterable (generator or list) that will enumerate available com ports. Note that +on some systems non-existent ports may be listed. + +Additionally a grep function is supplied that can be used to search for ports +based on their descriptions or hardware ID. +""" + +import sys +import os +import re + +# chose an implementation, depending on os +#~ if sys.platform == 'cli': +#~ else: +if os.name == 'nt': # sys.platform == 'win32': + from serial.tools.list_ports_windows import comports +elif os.name == 'posix': + from serial.tools.list_ports_posix import comports +#~ elif os.name == 'java': +else: + raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name)) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +def grep(regexp): + """\ + Search for ports using a regular expression. Port name, description and + hardware ID are searched. The function returns an iterable that returns the + same tuples as comport() would do. + """ + r = re.compile(regexp, re.I) + for info in comports(): + port, desc, hwid = info + if r.search(port) or r.search(desc) or r.search(hwid): + yield info + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def main(): + import argparse + + parser = argparse.ArgumentParser(description='Serial port enumeration') + + parser.add_argument( + 'regexp', + nargs='?', + help='only show ports that match this regex') + + parser.add_argument( + '-v', '--verbose', + action='store_true', + help='show more messages') + + parser.add_argument( + '-q', '--quiet', + action='store_true', + help='suppress all messages') + + parser.add_argument( + '-n', + type=int, + help='only output the N-th entry') + + args = parser.parse_args() + + hits = 0 + # get iteraror w/ or w/o filter + if args.regexp: + if not args.quiet: + sys.stderr.write("Filtered list with regexp: {!r}\n".format(args.regexp)) + iterator = sorted(grep(args.regexp)) + else: + iterator = sorted(comports()) + # list them + for n, (port, desc, hwid) in enumerate(iterator, 1): + if args.n is None or args.n == n: + sys.stdout.write("{:20}\n".format(port)) + if args.verbose: + sys.stdout.write(" desc: {}\n".format(desc)) + sys.stdout.write(" hwid: {}\n".format(hwid)) + hits += 1 + if not args.quiet: + if hits: + sys.stderr.write("{} ports found\n".format(hits)) + else: + sys.stderr.write("no ports found\n") + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + main() diff --git a/libs/serial/tools/list_ports_common.py b/libs/serial/tools/list_ports_common.py new file mode 100644 index 0000000..df12939 --- /dev/null +++ b/libs/serial/tools/list_ports_common.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# This is a helper module for the various platform dependent list_port +# implementations. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +import re + + +def numsplit(text): + """\ + Convert string into a list of texts and numbers in order to support a + natural sorting. + """ + result = [] + for group in re.split(r'(\d+)', text): + if group: + try: + group = int(group) + except ValueError: + pass + result.append(group) + return result + + +class ListPortInfo(object): + """Info collection base class for serial ports""" + + def __init__(self, device=None): + self.device = device + self.name = None + self.description = 'n/a' + self.hwid = 'n/a' + # USB specific data + self.vid = None + self.pid = None + self.serial_number = None + self.location = None + self.manufacturer = None + self.product = None + self.interface = None + + def usb_description(self): + """return a short string to name the port based on USB info""" + if self.interface is not None: + return '{} - {}'.format(self.product, self.interface) + elif self.product is not None: + return self.product + else: + return self.name + + def usb_info(self): + """return a string with USB related information about device""" + return 'USB VID:PID={:04X}:{:04X}{}{}'.format( + self.vid or 0, + self.pid or 0, + ' SER={}'.format(self.serial_number) if self.serial_number is not None else '', + ' LOCATION={}'.format(self.location) if self.location is not None else '') + + def apply_usb_info(self): + """update description and hwid from USB data""" + self.description = self.usb_description() + self.hwid = self.usb_info() + + def __eq__(self, other): + return self.device == other.device + + def __lt__(self, other): + return numsplit(self.device) < numsplit(other.device) + + def __str__(self): + return '{} - {}'.format(self.device, self.description) + + def __getitem__(self, index): + """Item access: backwards compatible -> (port, desc, hwid)""" + if index == 0: + return self.device + elif index == 1: + return self.description + elif index == 2: + return self.hwid + else: + raise IndexError('{} > 2'.format(index)) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + print(ListPortInfo('dummy')) diff --git a/libs/serial/tools/list_ports_linux.py b/libs/serial/tools/list_ports_linux.py new file mode 100644 index 0000000..567df6d --- /dev/null +++ b/libs/serial/tools/list_ports_linux.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# +# This is a module that gathers a list of serial ports including details on +# GNU/Linux systems. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2011-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +import glob +import os +from serial.tools import list_ports_common + + +class SysFS(list_ports_common.ListPortInfo): + """Wrapper for easy sysfs access and device info""" + + def __init__(self, device): + super(SysFS, self).__init__(device) + self.name = os.path.basename(device) + self.usb_device_path = None + if os.path.exists('/sys/class/tty/{}/device'.format(self.name)): + self.device_path = os.path.realpath('/sys/class/tty/{}/device'.format(self.name)) + self.subsystem = os.path.basename(os.path.realpath(os.path.join(self.device_path, 'subsystem'))) + else: + self.device_path = None + self.subsystem = None + # check device type + if self.subsystem == 'usb-serial': + self.usb_device_path = os.path.dirname(os.path.dirname(self.device_path)) + elif self.subsystem == 'usb': + self.usb_device_path = os.path.dirname(self.device_path) + else: + self.usb_device_path = None + # fill-in info for USB devices + if self.usb_device_path is not None: + self.vid = int(self.read_line(self.usb_device_path, 'idVendor'), 16) + self.pid = int(self.read_line(self.usb_device_path, 'idProduct'), 16) + self.serial_number = self.read_line(self.usb_device_path, 'serial') + self.location = os.path.basename(self.usb_device_path) + self.manufacturer = self.read_line(self.usb_device_path, 'manufacturer') + self.product = self.read_line(self.usb_device_path, 'product') + self.interface = self.read_line(self.device_path, 'interface') + + if self.subsystem in ('usb', 'usb-serial'): + self.apply_usb_info() + #~ elif self.subsystem in ('pnp', 'amba'): # PCI based devices, raspi + elif self.subsystem == 'pnp': # PCI based devices + self.description = self.name + self.hwid = self.read_line(self.device_path, 'id') + elif self.subsystem == 'amba': # raspi + self.description = self.name + self.hwid = os.path.basename(self.device_path) + + def read_line(self, *args): + """\ + Helper function to read a single line from a file. + One or more parameters are allowed, they are joined with os.path.join. + Returns None on errors.. + """ + try: + with open(os.path.join(*args)) as f: + line = f.readline().strip() + return line + except IOError: + return None + + +def comports(): + devices = glob.glob('/dev/ttyS*') # built-in serial ports + devices.extend(glob.glob('/dev/ttyUSB*')) # usb-serial with own driver + devices.extend(glob.glob('/dev/ttyACM*')) # usb-serial with CDC-ACM profile + devices.extend(glob.glob('/dev/ttyAMA*')) # ARM internal port (raspi) + devices.extend(glob.glob('/dev/rfcomm*')) # BT serial devices + return [info + for info in [SysFS(d) for d in devices] + if info.subsystem != "platform"] # hide non-present internal serial ports + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + for port, desc, hwid in sorted(comports()): + print("{}: {} [{}]".format(port, desc, hwid)) diff --git a/libs/serial/tools/list_ports_osx.py b/libs/serial/tools/list_ports_osx.py new file mode 100644 index 0000000..1d57b96 --- /dev/null +++ b/libs/serial/tools/list_ports_osx.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python +# +# This is a module that gathers a list of serial ports including details on OSX +# +# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools +# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston +# and modifications by cliechti, hoihu, hardkrash +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2013-2015 +# +# SPDX-License-Identifier: BSD-3-Clause + + +# List all of the callout devices in OS/X by querying IOKit. + +# See the following for a reference of how to do this: +# http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/SerialDevices.html#//apple_ref/doc/uid/TP30000384-CIHGEAFD + +# More help from darwin_hid.py + +# Also see the 'IORegistryExplorer' for an idea of what we are actually searching + +import ctypes +import ctypes.util + +from serial.tools import list_ports_common + +iokit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit')) +cf = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation')) + +kIOMasterPortDefault = ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault") +kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault") + +kCFStringEncodingMacRoman = 0 + +iokit.IOServiceMatching.restype = ctypes.c_void_p + +iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] +iokit.IOServiceGetMatchingServices.restype = ctypes.c_void_p + +iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + +iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32] +iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p + +iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] +iokit.IORegistryEntryGetPath.restype = ctypes.c_void_p + +iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p] +iokit.IORegistryEntryGetName.restype = ctypes.c_void_p + +iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p] +iokit.IOObjectGetClass.restype = ctypes.c_void_p + +iokit.IOObjectRelease.argtypes = [ctypes.c_void_p] + + +cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32] +cf.CFStringCreateWithCString.restype = ctypes.c_void_p + +cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32] +cf.CFStringGetCStringPtr.restype = ctypes.c_char_p + +cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p] +cf.CFNumberGetValue.restype = ctypes.c_void_p + +# void CFRelease ( CFTypeRef cf ); +cf.CFRelease.argtypes = [ctypes.c_void_p] +cf.CFRelease.restype = None + +# CFNumber type defines +kCFNumberSInt8Type = 1 +kCFNumberSInt16Type = 2 +kCFNumberSInt32Type = 3 +kCFNumberSInt64Type = 4 + + +def get_string_property(device_type, property): + """ + Search the given device for the specified string property + + @param device_type Type of Device + @param property String to search for + @return Python string containing the value, or None if not found. + """ + key = cf.CFStringCreateWithCString( + kCFAllocatorDefault, + property.encode("mac_roman"), + kCFStringEncodingMacRoman) + + CFContainer = iokit.IORegistryEntryCreateCFProperty( + device_type, + key, + kCFAllocatorDefault, + 0) + output = None + + if CFContainer: + output = cf.CFStringGetCStringPtr(CFContainer, 0) + if output is not None: + output = output.decode('mac_roman') + cf.CFRelease(CFContainer) + return output + + +def get_int_property(device_type, property, cf_number_type): + """ + Search the given device for the specified string property + + @param device_type Device to search + @param property String to search for + @param cf_number_type CFType number + + @return Python string containing the value, or None if not found. + """ + key = cf.CFStringCreateWithCString( + kCFAllocatorDefault, + property.encode("mac_roman"), + kCFStringEncodingMacRoman) + + CFContainer = iokit.IORegistryEntryCreateCFProperty( + device_type, + key, + kCFAllocatorDefault, + 0) + + if CFContainer: + if (cf_number_type == kCFNumberSInt32Type): + number = ctypes.c_uint32() + elif (cf_number_type == kCFNumberSInt16Type): + number = ctypes.c_uint16() + cf.CFNumberGetValue(CFContainer, cf_number_type, ctypes.byref(number)) + cf.CFRelease(CFContainer) + return number.value + return None + + +def IORegistryEntryGetName(device): + pathname = ctypes.create_string_buffer(100) # TODO: Is this ok? + iokit.IOObjectGetClass(device, ctypes.byref(pathname)) + return pathname.value + + +def GetParentDeviceByType(device, parent_type): + """ Find the first parent of a device that implements the parent_type + @param IOService Service to inspect + @return Pointer to the parent type, or None if it was not found. + """ + # First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice. + parent_type = parent_type.encode('mac_roman') + while IORegistryEntryGetName(device) != parent_type: + parent = ctypes.c_void_p() + response = iokit.IORegistryEntryGetParentEntry( + device, + "IOService".encode("mac_roman"), + ctypes.byref(parent)) + # If we weren't able to find a parent for the device, we're done. + if response != 0: + return None + device = parent + return device + + +def GetIOServicesByType(service_type): + """ + returns iterator over specified service_type + """ + serial_port_iterator = ctypes.c_void_p() + + iokit.IOServiceGetMatchingServices( + kIOMasterPortDefault, + iokit.IOServiceMatching(service_type.encode('mac_roman')), + ctypes.byref(serial_port_iterator)) + + services = [] + while iokit.IOIteratorIsValid(serial_port_iterator): + service = iokit.IOIteratorNext(serial_port_iterator) + if not service: + break + services.append(service) + iokit.IOObjectRelease(serial_port_iterator) + return services + + +def location_to_string(locationID): + """ + helper to calculate port and bus number from locationID + """ + loc = ['{}-'.format(locationID >> 24)] + while locationID & 0xf00000: + if len(loc) > 1: + loc.append('.') + loc.append('{}'.format((locationID >> 20) & 0xf)) + locationID <<= 4 + return ''.join(loc) + + +class SuitableSerialInterface(object): + pass + + +def scan_interfaces(): + """ + helper function to scan USB interfaces + returns a list of SuitableSerialInterface objects with name and id attributes + """ + interfaces = [] + for service in GetIOServicesByType('IOSerialBSDClient'): + device = get_string_property(service, "IOCalloutDevice") + if device: + usb_device = GetParentDeviceByType(service, "IOUSBInterface") + if usb_device: + name = get_string_property(usb_device, "USB Interface Name") or None + locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) or '' + i = SuitableSerialInterface() + i.id = locationID + i.name = name + interfaces.append(i) + return interfaces + + +def search_for_locationID_in_interfaces(serial_interfaces, locationID): + for interface in serial_interfaces: + if (interface.id == locationID): + return interface.name + return None + + +def comports(): + # Scan for all iokit serial ports + services = GetIOServicesByType('IOSerialBSDClient') + ports = [] + serial_interfaces = scan_interfaces() + for service in services: + # First, add the callout device file. + device = get_string_property(service, "IOCalloutDevice") + if device: + info = list_ports_common.ListPortInfo(device) + # If the serial port is implemented by IOUSBDevice + usb_device = GetParentDeviceByType(service, "IOUSBDevice") + if usb_device: + # fetch some useful informations from properties + info.vid = get_int_property(usb_device, "idVendor", kCFNumberSInt16Type) + info.pid = get_int_property(usb_device, "idProduct", kCFNumberSInt16Type) + info.serial_number = get_string_property(usb_device, "USB Serial Number") + info.product = get_string_property(usb_device, "USB Product Name") or 'n/a' + info.manufacturer = get_string_property(usb_device, "USB Vendor Name") + locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) + info.location = location_to_string(locationID) + info.interface = search_for_locationID_in_interfaces(serial_interfaces, locationID) + info.apply_usb_info() + ports.append(info) + return ports + +# test +if __name__ == '__main__': + for port, desc, hwid in sorted(comports()): + print("{}: {} [{}]".format(port, desc, hwid)) diff --git a/libs/serial/tools/list_ports_posix.py b/libs/serial/tools/list_ports_posix.py new file mode 100644 index 0000000..6ea4db9 --- /dev/null +++ b/libs/serial/tools/list_ports_posix.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# +# This is a module that gathers a list of serial ports on POSIXy systems. +# For some specific implementations, see also list_ports_linux, list_ports_osx +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2011-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +"""\ +The ``comports`` function is expected to return an iterable that yields tuples +of 3 strings: port name, human readable description and a hardware ID. + +As currently no method is known to get the second two strings easily, they are +currently just identical to the port name. +""" + +import glob +import sys +import os +from serial.tools import list_ports_common + +# try to detect the OS so that a device can be selected... +plat = sys.platform.lower() + +if plat[:5] == 'linux': # Linux (confirmed) # noqa + from serial.tools.list_ports_linux import comports + +elif plat[:6] == 'darwin': # OS X (confirmed) + from serial.tools.list_ports_osx import comports + +elif plat == 'cygwin': # cygwin/win32 + # cygwin accepts /dev/com* in many contexts + # (such as 'open' call, explicit 'ls'), but 'glob.glob' + # and bare 'ls' do not; so use /dev/ttyS* instead + def comports(): + devices = glob.glob('/dev/ttyS*') + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:7] == 'openbsd': # OpenBSD + def comports(): + devices = glob.glob('/dev/cua*') + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:3] == 'bsd' or plat[:7] == 'freebsd': + def comports(): + devices = glob.glob('/dev/cua*[!.init][!.lock]') + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:6] == 'netbsd': # NetBSD + def comports(): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/dty*') + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:4] == 'irix': # IRIX + def comports(): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/ttyf*') + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:2] == 'hp': # HP-UX (not tested) + def comports(): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/tty*p0') + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:5] == 'sunos': # Solaris/SunOS + def comports(): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/tty*c') + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:3] == 'aix': # AIX + def comports(): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/tty*') + return [list_ports_common.ListPortInfo(d) for d in devices] + +else: + # platform detection has failed... + import serial + sys.stderr.write("""\ +don't know how to enumerate ttys on this system. +! I you know how the serial ports are named send this information to +! the author of this module: + +sys.platform = {!r} +os.name = {!r} +pySerial version = {} + +also add the naming scheme of the serial ports and with a bit luck you can get +this module running... +""".format(sys.platform, os.name, serial.VERSION)) + raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name)) + +# test +if __name__ == '__main__': + for port, desc, hwid in sorted(comports()): + print("{}: {} [{}]".format(port, desc, hwid)) diff --git a/libs/serial/tools/list_ports_windows.py b/libs/serial/tools/list_ports_windows.py new file mode 100644 index 0000000..a070559 --- /dev/null +++ b/libs/serial/tools/list_ports_windows.py @@ -0,0 +1,284 @@ +#! python +# +# Enumerate serial ports on Windows including a human readable description +# and hardware information. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2016 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +# pylint: disable=invalid-name,too-few-public-methods +import re +import ctypes +from ctypes.wintypes import BOOL +from ctypes.wintypes import HWND +from ctypes.wintypes import DWORD +from ctypes.wintypes import WORD +from ctypes.wintypes import LONG +from ctypes.wintypes import ULONG +from ctypes.wintypes import HKEY +from ctypes.wintypes import BYTE +import serial +from serial.win32 import ULONG_PTR +from serial.tools import list_ports_common + + +def ValidHandle(value, func, arguments): + if value == 0: + raise ctypes.WinError() + return value + + +NULL = 0 +HDEVINFO = ctypes.c_void_p +LPCTSTR = ctypes.c_wchar_p +PCTSTR = ctypes.c_wchar_p +PTSTR = ctypes.c_wchar_p +LPDWORD = PDWORD = ctypes.POINTER(DWORD) +#~ LPBYTE = PBYTE = ctypes.POINTER(BYTE) +LPBYTE = PBYTE = ctypes.c_void_p # XXX avoids error about types + +ACCESS_MASK = DWORD +REGSAM = ACCESS_MASK + + +class GUID(ctypes.Structure): + _fields_ = [ + ('Data1', DWORD), + ('Data2', WORD), + ('Data3', WORD), + ('Data4', BYTE * 8), + ] + + def __str__(self): + return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format( + self.Data1, + self.Data2, + self.Data3, + ''.join(["{:02x}".format(d) for d in self.Data4[:2]]), + ''.join(["{:02x}".format(d) for d in self.Data4[2:]]), + ) + + +class SP_DEVINFO_DATA(ctypes.Structure): + _fields_ = [ + ('cbSize', DWORD), + ('ClassGuid', GUID), + ('DevInst', DWORD), + ('Reserved', ULONG_PTR), + ] + + def __str__(self): + return "ClassGuid:{} DevInst:{}".format(self.ClassGuid, self.DevInst) + + +PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA) + +PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p + +setupapi = ctypes.windll.LoadLibrary("setupapi") +SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList +SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO] +SetupDiDestroyDeviceInfoList.restype = BOOL + +SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameW +SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD] +SetupDiClassGuidsFromName.restype = BOOL + +SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo +SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA] +SetupDiEnumDeviceInfo.restype = BOOL + +SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsW +SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD] +SetupDiGetClassDevs.restype = HDEVINFO +SetupDiGetClassDevs.errcheck = ValidHandle + +SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyW +SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD] +SetupDiGetDeviceRegistryProperty.restype = BOOL + +SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdW +SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD] +SetupDiGetDeviceInstanceId.restype = BOOL + +SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey +SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM] +SetupDiOpenDevRegKey.restype = HKEY + +advapi32 = ctypes.windll.LoadLibrary("Advapi32") +RegCloseKey = advapi32.RegCloseKey +RegCloseKey.argtypes = [HKEY] +RegCloseKey.restype = LONG + +RegQueryValueEx = advapi32.RegQueryValueExW +RegQueryValueEx.argtypes = [HKEY, LPCTSTR , LPDWORD, LPDWORD, LPBYTE, LPDWORD] +RegQueryValueEx.restype = LONG + + +DIGCF_PRESENT = 2 +DIGCF_DEVICEINTERFACE = 16 +INVALID_HANDLE_VALUE = 0 +ERROR_INSUFFICIENT_BUFFER = 122 +SPDRP_HARDWAREID = 1 +SPDRP_FRIENDLYNAME = 12 +SPDRP_LOCATION_PATHS = 35 +DICS_FLAG_GLOBAL = 1 +DIREG_DEV = 0x00000001 +KEY_READ = 0x20019 + + +def iterate_comports(): + """Return a generator that yields descriptions for serial ports""" + GUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough... + guids_size = DWORD() + if not SetupDiClassGuidsFromName( + "Ports", + GUIDs, + ctypes.sizeof(GUIDs), + ctypes.byref(guids_size)): + raise ctypes.WinError() + + # repeat for all possible GUIDs + for index in range(guids_size.value): + g_hdi = SetupDiGetClassDevs( + ctypes.byref(GUIDs[index]), + None, + NULL, + DIGCF_PRESENT) # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports + + devinfo = SP_DEVINFO_DATA() + devinfo.cbSize = ctypes.sizeof(devinfo) + index = 0 + while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)): + index += 1 + + # get the real com port name + hkey = SetupDiOpenDevRegKey( + g_hdi, + ctypes.byref(devinfo), + DICS_FLAG_GLOBAL, + 0, + DIREG_DEV, # DIREG_DRV for SW info + KEY_READ) + port_name_buffer = ctypes.create_unicode_buffer(250) + port_name_length = ULONG(ctypes.sizeof(port_name_buffer)) + RegQueryValueEx( + hkey, + "PortName", + None, + None, + ctypes.byref(port_name_buffer), + ctypes.byref(port_name_length)) + RegCloseKey(hkey) + + # unfortunately does this method also include parallel ports. + # we could check for names starting with COM or just exclude LPT + # and hope that other "unknown" names are serial ports... + if port_name_buffer.value.startswith('LPT'): + continue + + # hardware ID + szHardwareID = ctypes.create_unicode_buffer(250) + # try to get ID that includes serial number + if not SetupDiGetDeviceInstanceId( + g_hdi, + ctypes.byref(devinfo), + #~ ctypes.byref(szHardwareID), + szHardwareID, + ctypes.sizeof(szHardwareID) - 1, + None): + # fall back to more generic hardware ID if that would fail + if not SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_HARDWAREID, + None, + ctypes.byref(szHardwareID), + ctypes.sizeof(szHardwareID) - 1, + None): + # Ignore ERROR_INSUFFICIENT_BUFFER + if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER: + raise ctypes.WinError() + # stringify + szHardwareID_str = szHardwareID.value + + info = list_ports_common.ListPortInfo(port_name_buffer.value) + + # in case of USB, make a more readable string, similar to that form + # that we also generate on other platforms + if szHardwareID_str.startswith('USB'): + m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(\\(\w+))?', szHardwareID_str, re.I) + if m: + info.vid = int(m.group(1), 16) + if m.group(3): + info.pid = int(m.group(3), 16) + if m.group(5): + info.serial_number = m.group(5) + # calculate a location string + loc_path_str = ctypes.create_unicode_buffer(250) + if SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_LOCATION_PATHS, + None, + ctypes.byref(loc_path_str), + ctypes.sizeof(loc_path_str) - 1, + None): + m = re.finditer(r'USBROOT\((\w+)\)|#USB\((\w+)\)', loc_path_str.value) + location = [] + for g in m: + if g.group(1): + location.append('{:d}'.format(int(g.group(1)) + 1)) + else: + if len(location) > 1: + location.append('.') + else: + location.append('-') + location.append(g.group(2)) + if location: + info.location = ''.join(location) + info.hwid = info.usb_info() + elif szHardwareID_str.startswith('FTDIBUS'): + m = re.search(r'VID_([0-9a-f]{4})\+PID_([0-9a-f]{4})(\+(\w+))?', szHardwareID_str, re.I) + if m: + info.vid = int(m.group(1), 16) + info.pid = int(m.group(2), 16) + if m.group(4): + info.serial_number = m.group(4) + # USB location is hidden by FDTI driver :( + info.hwid = info.usb_info() + else: + info.hwid = szHardwareID_str + + # friendly name + szFriendlyName = ctypes.create_unicode_buffer(250) + if SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_FRIENDLYNAME, + #~ SPDRP_DEVICEDESC, + None, + ctypes.byref(szFriendlyName), + ctypes.sizeof(szFriendlyName) - 1, + None): + info.description = szFriendlyName.value + #~ else: + # Ignore ERROR_INSUFFICIENT_BUFFER + #~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER: + #~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value)) + # ignore errors and still include the port in the list, friendly name will be same as port name + yield info + SetupDiDestroyDeviceInfoList(g_hdi) + + +def comports(): + """Return a list of info objects about serial ports""" + return list(iterate_comports()) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + for port, desc, hwid in sorted(comports()): + print("{}: {} [{}]".format(port, desc, hwid)) diff --git a/libs/serial/tools/miniterm.py b/libs/serial/tools/miniterm.py new file mode 100644 index 0000000..7c68e9d --- /dev/null +++ b/libs/serial/tools/miniterm.py @@ -0,0 +1,930 @@ +#!/usr/bin/env python +# +# Very simple serial terminal +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C)2002-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +import codecs +import os +import sys +import threading + +import serial +from serial.tools.list_ports import comports +from serial.tools import hexlify_codec + +# pylint: disable=wrong-import-order,wrong-import-position + +codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None) + +try: + raw_input +except NameError: + # pylint: disable=redefined-builtin,invalid-name + raw_input = input # in python3 it's "raw" + unichr = chr + + +def key_description(character): + """generate a readable description for a key""" + ascii_code = ord(character) + if ascii_code < 32: + return 'Ctrl+{:c}'.format(ord('@') + ascii_code) + else: + return repr(character) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +class ConsoleBase(object): + """OS abstraction for console (input/output codec, no echo)""" + + def __init__(self): + if sys.version_info >= (3, 0): + self.byte_output = sys.stdout.buffer + else: + self.byte_output = sys.stdout + self.output = sys.stdout + + def setup(self): + """Set console to read single characters, no echo""" + + def cleanup(self): + """Restore default console settings""" + + def getkey(self): + """Read a single key from the console""" + return None + + def write_bytes(self, byte_string): + """Write bytes (already encoded)""" + self.byte_output.write(byte_string) + self.byte_output.flush() + + def write(self, text): + """Write string""" + self.output.write(text) + self.output.flush() + + def cancel(self): + """Cancel getkey operation""" + + # - - - - - - - - - - - - - - - - - - - - - - - - + # context manager: + # switch terminal temporary to normal mode (e.g. to get user input) + + def __enter__(self): + self.cleanup() + return self + + def __exit__(self, *args, **kwargs): + self.setup() + + +if os.name == 'nt': # noqa + import msvcrt + import ctypes + + class Out(object): + """file-like wrapper that uses os.write""" + + def __init__(self, fd): + self.fd = fd + + def flush(self): + pass + + def write(self, s): + os.write(self.fd, s) + + class Console(ConsoleBase): + def __init__(self): + super(Console, self).__init__() + self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP() + self._saved_icp = ctypes.windll.kernel32.GetConsoleCP() + ctypes.windll.kernel32.SetConsoleOutputCP(65001) + ctypes.windll.kernel32.SetConsoleCP(65001) + self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace') + # the change of the code page is not propagated to Python, manually fix it + sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace') + sys.stdout = self.output + self.output.encoding = 'UTF-8' # needed for input + + def __del__(self): + ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp) + ctypes.windll.kernel32.SetConsoleCP(self._saved_icp) + + def getkey(self): + while True: + z = msvcrt.getwch() + if z == unichr(13): + return unichr(10) + elif z in (unichr(0), unichr(0x0e)): # functions keys, ignore + msvcrt.getwch() + else: + return z + + def cancel(self): + # CancelIo, CancelSynchronousIo do not seem to work when using + # getwch, so instead, send a key to the window with the console + hwnd = ctypes.windll.kernel32.GetConsoleWindow() + ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0) + +elif os.name == 'posix': + import atexit + import termios + import select + + class Console(ConsoleBase): + def __init__(self): + super(Console, self).__init__() + self.fd = sys.stdin.fileno() + # an additional pipe is used in getkey, so that the cancel method + # can abort the waiting getkey method + self.pipe_r, self.pipe_w = os.pipe() + self.old = termios.tcgetattr(self.fd) + atexit.register(self.cleanup) + if sys.version_info < (3, 0): + self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin) + else: + self.enc_stdin = sys.stdin + + def setup(self): + new = termios.tcgetattr(self.fd) + new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG + new[6][termios.VMIN] = 1 + new[6][termios.VTIME] = 0 + termios.tcsetattr(self.fd, termios.TCSANOW, new) + + def getkey(self): + ready, _, _ = select.select([self.enc_stdin, self.pipe_r], [], [], None) + if self.pipe_r in ready: + os.read(self.pipe_r, 1) + return + c = self.enc_stdin.read(1) + if c == unichr(0x7f): + c = unichr(8) # map the BS key (which yields DEL) to backspace + return c + + def cancel(self): + os.write(self.pipe_w, b"x") + + def cleanup(self): + termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old) + +else: + raise NotImplementedError( + 'Sorry no implementation for your platform ({}) available.'.format(sys.platform)) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +class Transform(object): + """do-nothing: forward all data unchanged""" + def rx(self, text): + """text received from serial port""" + return text + + def tx(self, text): + """text to be sent to serial port""" + return text + + def echo(self, text): + """text to be sent but displayed on console""" + return text + + +class CRLF(Transform): + """ENTER sends CR+LF""" + + def tx(self, text): + return text.replace('\n', '\r\n') + + +class CR(Transform): + """ENTER sends CR""" + + def rx(self, text): + return text.replace('\r', '\n') + + def tx(self, text): + return text.replace('\n', '\r') + + +class LF(Transform): + """ENTER sends LF""" + + +class NoTerminal(Transform): + """remove typical terminal control codes from input""" + + REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32) if unichr(x) not in '\r\n\b\t') + REPLACEMENT_MAP.update( + { + 0x7F: 0x2421, # DEL + 0x9B: 0x2425, # CSI + }) + + def rx(self, text): + return text.translate(self.REPLACEMENT_MAP) + + echo = rx + + +class NoControls(NoTerminal): + """Remove all control codes, incl. CR+LF""" + + REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32)) + REPLACEMENT_MAP.update( + { + 0x20: 0x2423, # visual space + 0x7F: 0x2421, # DEL + 0x9B: 0x2425, # CSI + }) + + +class Printable(Transform): + """Show decimal code for all non-ASCII characters and replace most control codes""" + + def rx(self, text): + r = [] + for c in text: + if ' ' <= c < '\x7f' or c in '\r\n\b\t': + r.append(c) + elif c < ' ': + r.append(unichr(0x2400 + ord(c))) + else: + r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c))) + r.append(' ') + return ''.join(r) + + echo = rx + + +class Colorize(Transform): + """Apply different colors for received and echo""" + + def __init__(self): + # XXX make it configurable, use colorama? + self.input_color = '\x1b[37m' + self.echo_color = '\x1b[31m' + + def rx(self, text): + return self.input_color + text + + def echo(self, text): + return self.echo_color + text + + +class DebugIO(Transform): + """Print what is sent and received""" + + def rx(self, text): + sys.stderr.write(' [RX:{}] '.format(repr(text))) + sys.stderr.flush() + return text + + def tx(self, text): + sys.stderr.write(' [TX:{}] '.format(repr(text))) + sys.stderr.flush() + return text + + +# other ideas: +# - add date/time for each newline +# - insert newline after: a) timeout b) packet end character + +EOL_TRANSFORMATIONS = { + 'crlf': CRLF, + 'cr': CR, + 'lf': LF, +} + +TRANSFORMATIONS = { + 'direct': Transform, # no transformation + 'default': NoTerminal, + 'nocontrol': NoControls, + 'printable': Printable, + 'colorize': Colorize, + 'debug': DebugIO, +} + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def ask_for_port(): + """\ + Show a list of ports and ask the user for a choice. To make selection + easier on systems with long device names, also allow the input of an + index. + """ + sys.stderr.write('\n--- Available ports:\n') + ports = [] + for n, (port, desc, hwid) in enumerate(sorted(comports()), 1): + sys.stderr.write('--- {:2}: {:20} {}\n'.format(n, port, desc)) + ports.append(port) + while True: + port = raw_input('--- Enter port index or full name: ') + try: + index = int(port) - 1 + if not 0 <= index < len(ports): + sys.stderr.write('--- Invalid index!\n') + continue + except ValueError: + pass + else: + port = ports[index] + return port + + +class Miniterm(object): + """\ + Terminal application. Copy data from serial port to console and vice versa. + Handle special keys from the console to show menu etc. + """ + + def __init__(self, serial_instance, echo=False, eol='crlf', filters=()): + self.console = Console() + self.serial = serial_instance + self.echo = echo + self.raw = False + self.input_encoding = 'UTF-8' + self.output_encoding = 'UTF-8' + self.eol = eol + self.filters = filters + self.update_transformations() + self.exit_character = 0x1d # GS/CTRL+] + self.menu_character = 0x14 # Menu: CTRL+T + self.alive = None + self._reader_alive = None + self.receiver_thread = None + self.rx_decoder = None + self.tx_decoder = None + + def _start_reader(self): + """Start reader thread""" + self._reader_alive = True + # start serial->console thread + self.receiver_thread = threading.Thread(target=self.reader, name='rx') + self.receiver_thread.daemon = True + self.receiver_thread.start() + + def _stop_reader(self): + """Stop reader thread only, wait for clean exit of thread""" + self._reader_alive = False + if hasattr(self.serial, 'cancel_read'): + self.serial.cancel_read() + self.receiver_thread.join() + + def start(self): + """start worker threads""" + self.alive = True + self._start_reader() + # enter console->serial loop + self.transmitter_thread = threading.Thread(target=self.writer, name='tx') + self.transmitter_thread.daemon = True + self.transmitter_thread.start() + self.console.setup() + + def stop(self): + """set flag to stop worker threads""" + self.alive = False + + def join(self, transmit_only=False): + """wait for worker threads to terminate""" + self.transmitter_thread.join() + if not transmit_only: + if hasattr(self.serial, 'cancel_read'): + self.serial.cancel_read() + self.receiver_thread.join() + + def close(self): + self.serial.close() + + def update_transformations(self): + """take list of transformation classes and instantiate them for rx and tx""" + transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f] + for f in self.filters] + self.tx_transformations = [t() for t in transformations] + self.rx_transformations = list(reversed(self.tx_transformations)) + + def set_rx_encoding(self, encoding, errors='replace'): + """set encoding for received data""" + self.input_encoding = encoding + self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors) + + def set_tx_encoding(self, encoding, errors='replace'): + """set encoding for transmitted data""" + self.output_encoding = encoding + self.tx_encoder = codecs.getincrementalencoder(encoding)(errors) + + def dump_port_settings(self): + """Write current settings to sys.stderr""" + sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format( + p=self.serial)) + sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format( + ('active' if self.serial.rts else 'inactive'), + ('active' if self.serial.dtr else 'inactive'), + ('active' if self.serial.break_condition else 'inactive'))) + try: + sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format( + ('active' if self.serial.cts else 'inactive'), + ('active' if self.serial.dsr else 'inactive'), + ('active' if self.serial.ri else 'inactive'), + ('active' if self.serial.cd else 'inactive'))) + except serial.SerialException: + # on RFC 2217 ports, it can happen if no modem state notification was + # yet received. ignore this error. + pass + sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive')) + sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive')) + sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding)) + sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding)) + sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper())) + sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters))) + + def reader(self): + """loop and copy serial->console""" + try: + while self.alive and self._reader_alive: + # read all that is there or wait for one byte + data = self.serial.read(self.serial.in_waiting or 1) + if data: + if self.raw: + self.console.write_bytes(data) + else: + text = self.rx_decoder.decode(data) + for transformation in self.rx_transformations: + text = transformation.rx(text) + self.console.write(text) + except serial.SerialException: + self.alive = False + self.console.cancel() + raise # XXX handle instead of re-raise? + + def writer(self): + """\ + Loop and copy console->serial until self.exit_character character is + found. When self.menu_character is found, interpret the next key + locally. + """ + menu_active = False + try: + while self.alive: + try: + c = self.console.getkey() + except KeyboardInterrupt: + c = '\x03' + if not self.alive: + break + if menu_active: + self.handle_menu_key(c) + menu_active = False + elif c == self.menu_character: + menu_active = True # next char will be for menu + elif c == self.exit_character: + self.stop() # exit app + break + else: + #~ if self.raw: + text = c + for transformation in self.tx_transformations: + text = transformation.tx(text) + self.serial.write(self.tx_encoder.encode(text)) + if self.echo: + echo_text = c + for transformation in self.tx_transformations: + echo_text = transformation.echo(echo_text) + self.console.write(echo_text) + except: + self.alive = False + raise + + def handle_menu_key(self, c): + """Implement a simple menu / settings""" + if c == self.menu_character or c == self.exit_character: + # Menu/exit character again -> send itself + self.serial.write(self.tx_encoder.encode(c)) + if self.echo: + self.console.write(c) + elif c == '\x15': # CTRL+U -> upload file + sys.stderr.write('\n--- File to upload: ') + sys.stderr.flush() + with self.console: + filename = sys.stdin.readline().rstrip('\r\n') + if filename: + try: + with open(filename, 'rb') as f: + sys.stderr.write('--- Sending file {} ---\n'.format(filename)) + while True: + block = f.read(1024) + if not block: + break + self.serial.write(block) + # Wait for output buffer to drain. + self.serial.flush() + sys.stderr.write('.') # Progress indicator. + sys.stderr.write('\n--- File {} sent ---\n'.format(filename)) + except IOError as e: + sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e)) + elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help + sys.stderr.write(self.get_help_text()) + elif c == '\x12': # CTRL+R -> Toggle RTS + self.serial.rts = not self.serial.rts + sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive')) + elif c == '\x04': # CTRL+D -> Toggle DTR + self.serial.dtr = not self.serial.dtr + sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive')) + elif c == '\x02': # CTRL+B -> toggle BREAK condition + self.serial.break_condition = not self.serial.break_condition + sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive')) + elif c == '\x05': # CTRL+E -> toggle local echo + self.echo = not self.echo + sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive')) + elif c == '\x06': # CTRL+F -> edit filters + sys.stderr.write('\n--- Available Filters:\n') + sys.stderr.write('\n'.join( + '--- {:<10} = {.__doc__}'.format(k, v) + for k, v in sorted(TRANSFORMATIONS.items()))) + sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters))) + with self.console: + new_filters = sys.stdin.readline().lower().split() + if new_filters: + for f in new_filters: + if f not in TRANSFORMATIONS: + sys.stderr.write('--- unknown filter: {}\n'.format(repr(f))) + break + else: + self.filters = new_filters + self.update_transformations() + sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters))) + elif c == '\x0c': # CTRL+L -> EOL mode + modes = list(EOL_TRANSFORMATIONS) # keys + eol = modes.index(self.eol) + 1 + if eol >= len(modes): + eol = 0 + self.eol = modes[eol] + sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper())) + self.update_transformations() + elif c == '\x01': # CTRL+A -> set encoding + sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding)) + with self.console: + new_encoding = sys.stdin.readline().strip() + if new_encoding: + try: + codecs.lookup(new_encoding) + except LookupError: + sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding)) + else: + self.set_rx_encoding(new_encoding) + self.set_tx_encoding(new_encoding) + sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding)) + sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding)) + elif c == '\x09': # CTRL+I -> info + self.dump_port_settings() + #~ elif c == '\x01': # CTRL+A -> cycle escape mode + #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode + elif c in 'pP': # P -> change port + with self.console: + try: + port = ask_for_port() + except KeyboardInterrupt: + port = None + if port and port != self.serial.port: + # reader thread needs to be shut down + self._stop_reader() + # save settings + settings = self.serial.getSettingsDict() + try: + new_serial = serial.serial_for_url(port, do_not_open=True) + # restore settings and open + new_serial.applySettingsDict(settings) + new_serial.rts = self.serial.rts + new_serial.dtr = self.serial.dtr + new_serial.open() + new_serial.break_condition = self.serial.break_condition + except Exception as e: + sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e)) + new_serial.close() + else: + self.serial.close() + self.serial = new_serial + sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port)) + # and restart the reader thread + self._start_reader() + elif c in 'bB': # B -> change baudrate + sys.stderr.write('\n--- Baudrate: ') + sys.stderr.flush() + with self.console: + backup = self.serial.baudrate + try: + self.serial.baudrate = int(sys.stdin.readline().strip()) + except ValueError as e: + sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e)) + self.serial.baudrate = backup + else: + self.dump_port_settings() + elif c == '8': # 8 -> change to 8 bits + self.serial.bytesize = serial.EIGHTBITS + self.dump_port_settings() + elif c == '7': # 7 -> change to 8 bits + self.serial.bytesize = serial.SEVENBITS + self.dump_port_settings() + elif c in 'eE': # E -> change to even parity + self.serial.parity = serial.PARITY_EVEN + self.dump_port_settings() + elif c in 'oO': # O -> change to odd parity + self.serial.parity = serial.PARITY_ODD + self.dump_port_settings() + elif c in 'mM': # M -> change to mark parity + self.serial.parity = serial.PARITY_MARK + self.dump_port_settings() + elif c in 'sS': # S -> change to space parity + self.serial.parity = serial.PARITY_SPACE + self.dump_port_settings() + elif c in 'nN': # N -> change to no parity + self.serial.parity = serial.PARITY_NONE + self.dump_port_settings() + elif c == '1': # 1 -> change to 1 stop bits + self.serial.stopbits = serial.STOPBITS_ONE + self.dump_port_settings() + elif c == '2': # 2 -> change to 2 stop bits + self.serial.stopbits = serial.STOPBITS_TWO + self.dump_port_settings() + elif c == '3': # 3 -> change to 1.5 stop bits + self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE + self.dump_port_settings() + elif c in 'xX': # X -> change software flow control + self.serial.xonxoff = (c == 'X') + self.dump_port_settings() + elif c in 'rR': # R -> change hardware flow control + self.serial.rtscts = (c == 'R') + self.dump_port_settings() + else: + sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c))) + + def get_help_text(self): + """return the help text""" + # help text, starts with blank line! + return """ +--- pySerial ({version}) - miniterm - help +--- +--- {exit:8} Exit program +--- {menu:8} Menu escape key, followed by: +--- Menu keys: +--- {menu:7} Send the menu character itself to remote +--- {exit:7} Send the exit character itself to remote +--- {info:7} Show info +--- {upload:7} Upload file (prompt will be shown) +--- {repr:7} encoding +--- {filter:7} edit filters +--- Toggles: +--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK +--- {echo:7} echo {eol:7} EOL +--- +--- Port settings ({menu} followed by the following): +--- p change port +--- 7 8 set data bits +--- N E O S M change parity (None, Even, Odd, Space, Mark) +--- 1 2 3 set stop bits (1, 2, 1.5) +--- b change baud rate +--- x X disable/enable software flow control +--- r R disable/enable hardware flow control +""".format(version=getattr(serial, 'VERSION', 'unknown version'), + exit=key_description(self.exit_character), + menu=key_description(self.menu_character), + rts=key_description('\x12'), + dtr=key_description('\x04'), + brk=key_description('\x02'), + echo=key_description('\x05'), + info=key_description('\x09'), + upload=key_description('\x15'), + repr=key_description('\x01'), + filter=key_description('\x06'), + eol=key_description('\x0c')) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# default args can be used to override when calling main() from an other script +# e.g to create a miniterm-my-device.py +def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None): + """Command line tool, entry point""" + + import argparse + + parser = argparse.ArgumentParser( + description="Miniterm - A simple terminal program for the serial port.") + + parser.add_argument( + "port", + nargs='?', + help="serial port name ('-' to show port list)", + default=default_port) + + parser.add_argument( + "baudrate", + nargs='?', + type=int, + help="set baud rate, default: %(default)s", + default=default_baudrate) + + group = parser.add_argument_group("port settings") + + group.add_argument( + "--parity", + choices=['N', 'E', 'O', 'S', 'M'], + type=lambda c: c.upper(), + help="set parity, one of {N E O S M}, default: N", + default='N') + + group.add_argument( + "--rtscts", + action="store_true", + help="enable RTS/CTS flow control (default off)", + default=False) + + group.add_argument( + "--xonxoff", + action="store_true", + help="enable software flow control (default off)", + default=False) + + group.add_argument( + "--rts", + type=int, + help="set initial RTS line state (possible values: 0, 1)", + default=default_rts) + + group.add_argument( + "--dtr", + type=int, + help="set initial DTR line state (possible values: 0, 1)", + default=default_dtr) + + group.add_argument( + "--ask", + action="store_true", + help="ask again for port when open fails", + default=False) + + group = parser.add_argument_group("data handling") + + group.add_argument( + "-e", "--echo", + action="store_true", + help="enable local echo (default off)", + default=False) + + group.add_argument( + "--encoding", + dest="serial_port_encoding", + metavar="CODEC", + help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s", + default='UTF-8') + + group.add_argument( + "-f", "--filter", + action="append", + metavar="NAME", + help="add text transformation", + default=[]) + + group.add_argument( + "--eol", + choices=['CR', 'LF', 'CRLF'], + type=lambda c: c.upper(), + help="end of line mode", + default='CRLF') + + group.add_argument( + "--raw", + action="store_true", + help="Do no apply any encodings/transformations", + default=False) + + group = parser.add_argument_group("hotkeys") + + group.add_argument( + "--exit-char", + type=int, + metavar='NUM', + help="Unicode of special character that is used to exit the application, default: %(default)s", + default=0x1d) # GS/CTRL+] + + group.add_argument( + "--menu-char", + type=int, + metavar='NUM', + help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s", + default=0x14) # Menu: CTRL+T + + group = parser.add_argument_group("diagnostics") + + group.add_argument( + "-q", "--quiet", + action="store_true", + help="suppress non-error messages", + default=False) + + group.add_argument( + "--develop", + action="store_true", + help="show Python traceback on error", + default=False) + + args = parser.parse_args() + + if args.menu_char == args.exit_char: + parser.error('--exit-char can not be the same as --menu-char') + + if args.filter: + if 'help' in args.filter: + sys.stderr.write('Available filters:\n') + sys.stderr.write('\n'.join( + '{:<10} = {.__doc__}'.format(k, v) + for k, v in sorted(TRANSFORMATIONS.items()))) + sys.stderr.write('\n') + sys.exit(1) + filters = args.filter + else: + filters = ['default'] + + while True: + # no port given on command line -> ask user now + if args.port is None or args.port == '-': + try: + args.port = ask_for_port() + except KeyboardInterrupt: + sys.stderr.write('\n') + parser.error('user aborted and port is not given') + else: + if not args.port: + parser.error('port is not given') + try: + serial_instance = serial.serial_for_url( + args.port, + args.baudrate, + parity=args.parity, + rtscts=args.rtscts, + xonxoff=args.xonxoff, + do_not_open=True) + + if not hasattr(serial_instance, 'cancel_read'): + # enable timeout for alive flag polling if cancel_read is not available + serial_instance.timeout = 1 + + if args.dtr is not None: + if not args.quiet: + sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive')) + serial_instance.dtr = args.dtr + if args.rts is not None: + if not args.quiet: + sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive')) + serial_instance.rts = args.rts + + serial_instance.open() + except serial.SerialException as e: + sys.stderr.write('could not open port {}: {}\n'.format(repr(args.port), e)) + if args.develop: + raise + if not args.ask: + sys.exit(1) + else: + args.port = '-' + else: + break + + miniterm = Miniterm( + serial_instance, + echo=args.echo, + eol=args.eol.lower(), + filters=filters) + miniterm.exit_character = unichr(args.exit_char) + miniterm.menu_character = unichr(args.menu_char) + miniterm.raw = args.raw + miniterm.set_rx_encoding(args.serial_port_encoding) + miniterm.set_tx_encoding(args.serial_port_encoding) + + if not args.quiet: + sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format( + p=miniterm.serial)) + sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format( + key_description(miniterm.exit_character), + key_description(miniterm.menu_character), + key_description(miniterm.menu_character), + key_description('\x08'))) + + miniterm.start() + try: + miniterm.join(True) + except KeyboardInterrupt: + pass + if not args.quiet: + sys.stderr.write("\n--- exit ---\n") + miniterm.join() + miniterm.close() + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +if __name__ == '__main__': + main() diff --git a/libs/serial/urlhandler/__init__.py b/libs/serial/urlhandler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/serial/urlhandler/protocol_alt.py b/libs/serial/urlhandler/protocol_alt.py new file mode 100644 index 0000000..c14a87e --- /dev/null +++ b/libs/serial/urlhandler/protocol_alt.py @@ -0,0 +1,55 @@ +#! python +# +# This module implements a special URL handler that allows selecting an +# alternate implementation provided by some backends. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +# +# URL format: alt://port[?option[=value][&option[=value]]] +# options: +# - class=X used class named X instead of Serial +# +# example: +# use poll based implementation on Posix (Linux): +# python -m serial.tools.miniterm alt:///dev/ttyUSB0?class=PosixPollSerial + +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + +import serial + + +def serial_class_for_url(url): + """extract host and port from an URL string""" + parts = urlparse.urlsplit(url) + if parts.scheme != 'alt': + raise serial.SerialException( + 'expected a string in the form "alt://port[?option[=value][&option[=value]]]": ' + 'not starting with alt:// ({!r})'.format(parts.scheme)) + class_name = 'Serial' + try: + for option, values in urlparse.parse_qs(parts.query, True).items(): + if option == 'class': + class_name = values[0] + else: + raise ValueError('unknown option: {!r}'.format(option)) + except ValueError as e: + raise serial.SerialException( + 'expected a string in the form ' + '"alt://port[?option[=value][&option[=value]]]": {!r}'.format(e)) + if not hasattr(serial, class_name): + raise ValueError('unknown class: {!r}'.format(class_name)) + cls = getattr(serial, class_name) + if not issubclass(cls, serial.Serial): + raise ValueError('class {!r} is not an instance of Serial'.format(class_name)) + return (''.join([parts.netloc, parts.path]), cls) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +if __name__ == '__main__': + s = serial.serial_for_url('alt:///dev/ttyS0?class=PosixPollSerial') + print(s) diff --git a/libs/serial/urlhandler/protocol_hwgrep.py b/libs/serial/urlhandler/protocol_hwgrep.py new file mode 100644 index 0000000..49bbebe --- /dev/null +++ b/libs/serial/urlhandler/protocol_hwgrep.py @@ -0,0 +1,89 @@ +#! python +# +# This module implements a special URL handler that uses the port listing to +# find ports by searching the string descriptions. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2011-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +# +# URL format: hwgrep://&