diff --git a/docs/source/conf.py b/docs/source/conf.py index 1c914c2c..49b86271 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -64,7 +64,7 @@ extensions = [ 'sphinx.ext.ifconfig', 'sphinxcontrib.httpdomain', 'sphinxcontrib.autohttp.flask', - 'sphinxcontrib.autohttp.flaskqref', + 'sphinxcontrib.autohttp.flaskqref' ] # Override ordering diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 64b93eb8..b7134ad0 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -8,5 +8,6 @@ Developing Plugins ./plugins/structure.rst ./plugins/routes.rst ./plugins/hardware.rst + ./plugins/schema.rst ./plugins/example.rst ./plugins/class.rst \ No newline at end of file diff --git a/docs/source/plugins/example.rst b/docs/source/plugins/example.rst index 185ad642..5d0ba18b 100644 --- a/docs/source/plugins/example.rst +++ b/docs/source/plugins/example.rst @@ -1,135 +1,20 @@ Example plugin -------------- -.. code-block:: python +plugin.py ++++++++++ - from openflexure_microscope.plugins import MicroscopePlugin - from openflexure_microscope.api.v1.views import MicroscopeViewPlugin - from openflexure_microscope.api.utilities import JsonPayload - - from flask import request, Response, escape +.. literalinclude:: example/plugin.py + :language: python +schema.json ++++++++++++ - ### MICROSCOPE PLUGIN ### +.. literalinclude:: example/schema.json + :language: JSON - class MyPluginClass(MicroscopePlugin): - """ - A set of default plugins - """ - - api_views = { - '/identify': IdentifyAPI, - '/hello': HelloWorldAPI, - '/timelapse': TimelapseAPI, - } - - def identify(self): - """ - Demonstrate access to Microscope.camera, and Microscope.stage - """ - - response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera, - self.microscope.stage) - return response - - def hello_world(self): - """ - Demonstrate passive method - """ - - return "Hello world!" - - def timelapse(self, n_images): - """ - Demonstrate a long-running method that requires microscope hardware - """ - print("Starting timelapse...") - capture_array = [] # Empty list to store captures in - - # Acquire locks. Exception is raised if lock is in use by another thread. - with self.microscope.camera.lock, self.microscope.stage.lock: - for _ in range(n_images): - - # Create a data stream to capture to - capture_data = self.microscope.camera.new_image( - write_to_file=True, - temporary=False) - - # Capture a still image from the Pi camera, into the data stream - self.microscope.camera.capture( - capture_data, - use_video_port=True) - - # Append the capture data to our list - capture_array.append(capture_data) - - # Wait for 1 minute - time.sleep(60) - - - ### API VIEWS ### - - class IdentifyAPI(MicroscopeViewPlugin): - """ - A simple example API plugin, attached through the main microscope plugin. - """ - def get(self): - """ - Method to call when an HTTP GET request is made. - """ - # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut - data = self.plugin.identify() - return Response(escape(data)) - - - class HelloWorldAPI(MicroscopeViewPlugin): - """ - A method to create, set, and return a new microscope parameter. - """ - - def get(self): - """ - Method to call when an HTTP GET request is made. - """ - - # If the microscope does not already contain our plugin_string attribute - if not hasattr(self.microscope, 'plugin_string'): - # Make a string, using the MicroscopeViewPlugin.plugin shortcut - self.microscope.plugin_string = self.plugin.hello_world() - - return Response(self.microscope.plugin_string) - - def post(self): - """ - Method to call when an HTTP POST request is made. - Assumes request will include a JSON payload. - """ - # Get payload JSON - payload = JsonPayload(request) - - # Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty. - new_plugin_string = payload.param('plugin_string', default='', convert=str) - - if new_plugin_string: # If not None or empty - # Set microscope attribute to the specified string - self.microscope.plugin_string = new_plugin_string - - return Response(self.microscope.plugin_string) - - class TimelapseAPI(MicroscopeViewPlugin): - def post(self): - - # Get any JSON data in the body of the POST request - payload = JsonPayload(request) - - # Extract the "n_images" parameter if it was passed. Otherwise, default to 10. - n_images = payload.param('n_images', default=10, convert=int) - - # Attach the long-running method as a microscope task - self.timelapse_task = self.microscope.task.start(self.plugin.timelapse, n_images) - - # Return the state of the task (will show ID, start time, and status before the task has finished) - return jsonify(self.timelapse_task.state), 202 +Notes ++++++ In this example, if the package or file were named ``my_plugin``, the three microscope plugin methods would be accessible from ``.plugin.my_plugin.identify()``, ``.plugin.my_plugin.timelapse()``, and ``.plugin.my_plugin.hello_world()``. diff --git a/docs/source/plugins/example/plugin.py b/docs/source/plugins/example/plugin.py new file mode 100644 index 00000000..4a3baea5 --- /dev/null +++ b/docs/source/plugins/example/plugin.py @@ -0,0 +1,146 @@ +from openflexure_microscope.plugins import MicroscopePlugin +from openflexure_microscope.api.v1.views import MicroscopeViewPlugin +from openflexure_microscope.api.utilities import JsonPayload + +import os +import time +import json + +from flask import request, Response, escape, jsonify + +HERE = os.path.dirname(os.path.realpath(__file__)) +SCHEMA_PATH = os.path.join(HERE, "schema.json") + +### MICROSCOPE PLUGIN ### + +class MyPluginClass(MicroscopePlugin): + """ + A set of default plugins + """ + + global SCHEMA_PATH + + with open(SCHEMA_PATH, 'r') as sc: + api_schema = json.load(sc) + + api_views = { + '/identify': IdentifyAPI, + '/hello': HelloWorldAPI, + '/timelapse': TimelapseAPI, + } + + def identify(self): + """ + Demonstrate access to Microscope.camera, and Microscope.stage + """ + + response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera, + self.microscope.stage) + return response + + def hello_world(self): + """ + Demonstrate passive method + """ + + return "Hello world!" + + def timelapse(self, n_images): + """ + Demonstrate a long-running method that requires microscope hardware + """ + print("Starting timelapse...") + capture_array = [] # Empty list to store captures in + + # Acquire locks. Exception is raised if lock is in use by another thread. + with self.microscope.camera.lock, self.microscope.stage.lock: + for _ in range(n_images): + + # Create a data stream to capture to + capture_data = self.microscope.camera.new_image( + write_to_file=True, + temporary=False) + + # Capture a still image from the Pi camera, into the data stream + self.microscope.camera.capture( + capture_data, + use_video_port=True) + + # Append the capture data to our list + capture_array.append(capture_data) + + # Wait for 1 minute + time.sleep(60) + + +### API VIEWS ### + +class IdentifyAPI(MicroscopeViewPlugin): + """ + A simple example API plugin, attached through the main microscope plugin. + """ + def get(self): + """ + Method to call when an HTTP GET request is made. + """ + # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut + data = self.plugin.identify() + return Response(escape(data)) + + +class HelloWorldAPI(MicroscopeViewPlugin): + """ + A method to create, set, and return a new microscope parameter. + """ + + def get(self): + """ + Method to call when an HTTP GET request is made. + """ + + # If the microscope does not already contain our plugin_string attribute + if not hasattr(self.microscope, 'plugin_string'): + # Make a string, using the MicroscopeViewPlugin.plugin shortcut + self.microscope.plugin_string = self.plugin.hello_world() + + json_response = jsonify({ + 'plugin_string': self.microscope.plugin_string + }) + + return Response(json_response) + + def post(self): + """ + Method to call when an HTTP POST request is made. + Assumes request will include a JSON payload. + """ + # Get payload JSON + payload = JsonPayload(request) + + # Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty. + new_plugin_string = payload.param('plugin_string', default='', convert=str) + + if new_plugin_string: # If not None or empty + # Set microscope attribute to the specified string + self.microscope.plugin_string = new_plugin_string + + json_response = jsonify({ + 'plugin_string': self.microscope.plugin_string + }) + + return Response(json_response) + +class TimelapseAPI(MicroscopeViewPlugin): + def post(self): + + # Get any JSON data in the body of the POST request + payload = JsonPayload(request) + + # Extract the "n_images" parameter if it was passed. Otherwise, default to 10. + n_images = payload.param('n_images', default=10, convert=int) + + # Attach the long-running method as a microscope task + self.timelapse_task = self.microscope.task.start(self.plugin.timelapse, n_images) + + # Return the state of the task (will show ID, start time, and status before the task has finished) + return jsonify(self.timelapse_task.state), 202 \ No newline at end of file diff --git a/docs/source/plugins/example/schema.json b/docs/source/plugins/example/schema.json new file mode 100644 index 00000000..58364150 --- /dev/null +++ b/docs/source/plugins/example/schema.json @@ -0,0 +1,33 @@ +{ + "id": "example-plugin", + "icon": "pets", + "forms": [ + { + "name": "Hello world", + "isTask": false, + "selfUpdate": true, + "route": "/hello", + "schema": [ + { + "fieldType": "textInput", + "placeholder": "Enter a string", + "label": "Plugin string", + "name": "plugin_string" + } + ] + }, + { + "name": "Timelapse", + "isTask": true, + "route": "/timelapse", + "submitLabel": "Start timelapse", + "schema": [ + { + "fieldType": "numberInput", + "name": "n_images", + "label": "Number of images" + } + ] + } + ] + } \ No newline at end of file diff --git a/docs/source/plugins/schema.rst b/docs/source/plugins/schema.rst new file mode 100644 index 00000000..ef0375f4 --- /dev/null +++ b/docs/source/plugins/schema.rst @@ -0,0 +1,192 @@ +Adding a GUI +===================== + +Introduction +------------ + +In order to bind user-interface elements to your plugin, an ``api_schema`` variable must be included in your microscope plugin, similar to your ``api_views``. + +This variable must be in the form of a dictionary, with all data able to be parsed into JSON. Because of this requirement, it is suggested that you create your API schema as a JSON file, and load that file as a dictionary into your plugin. For example: + +.. code-block:: python + + import json + + HERE = os.path.dirname(os.path.realpath(__file__)) # Find the full path of your plugin Python file + SCHEMA_PATH = os.path.join(HERE, "schema.json") # Find the full path of the adjascent JSON file + + class MyPluginClass(MicroscopePlugin): + + with open(SCHEMA_PATH, 'r') as sc: # Open the JSON file + api_schema = json.load(sc) # Load the JSON file into the api_schema dictionary + +Throughout this documentation, all example of ``api_schema`` sections will be in JSON format. Keep in mind however that it is possible to directly define your ``api_schema`` as a dictionary, without loading an external file. + + +Forms from ``api_schema`` +------------------------- + +The ``api_schema`` object essentially describes HTML forms, which it is up to the client to render. The form is constructed by specifying a set of components, and their values. A form can update it's values by sending a GET request to the API route bound to that form, and can send it's current values via a POST request to *this same API route*. + +Each component in the form has a ``name`` property, which must match up to a property your API route expects in JSON POST requests, and returns in JSON GET requests. + +Structure of ``api_schema`` +--------------------------- + +Root level +++++++++++ + +The root of your ``api_schema`` expects 3 properties: + +``id`` - A unique ID to give your client-side plugin + +``icon`` - The name of a Material Design icon to use for your plugin + +``forms`` - An array of forms as described below + +Form level +++++++++++ + +Your plugin can contain multiple forms. For example, if your plugin creates several API routes, you will need a separate form for each route. + +Each form is described by a JSON object, with the following properties: + +``name`` - A human-readable name for the form + +``route`` - String of the corresponding API route. *Must* match a route defined in your ``api_views`` dictionary + +``selfUpdate`` *(optional)* - Whether the client should automatically update form component values with a GET request to your API route + +``isTask`` *(optional)* - Whether the client should treat your API route as a long-running task + +``submitLabel`` *(optional)* - String to place inside of the form's submit button + +``schema`` - An array of form components as described below + +Component level ++++++++++++++++ + +Each form can (and probably should) contain multiple components. For example, if your API route expects several properties in POST requests, each property can be bound to a form component. + +Upon form submission, the form data will be converted into a JSON object of key-value pairs, where the key is the components ``name``, and the value is it's current value. + +On load, the form will make a GET request to it's API route, an expects a JSON object of key-value pairs in the same format (keys will be matched to component names). + +An overview of available components, and their properties, can be found below. + +Arranging components +^^^^^^^^^^^^^^^^^^^^ + +You can request that the client render several components in a horizontal grid by placing them in an array. You cannot nest arrays however. Each component in the array will be rendered with equal width as far as possible. + +Overview of components +---------------------- + +.. list-table:: + :widths: 10 10 40 20 + :header-rows: 1 + :stub-columns: 1 + + * - fieldType + - Data type + - Properties + - Example + * - checkList + - [str, str,...] + - **name** (str) Unique name of the component + + **label** (str) Friendly label for the component + + **value** ([str, str,...]) List of selected options + + **options** ([str, str,...]) List of all options + + - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/checkList.png + * - htmlBlock + - N/A + - **name** (str) Unique name of the component + + **label** (str) Friendly label for the component + + **content** (str) HTML string to be rendered + + - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/htmlBlock.png + * - keyvalList + - dict + - **name** (str) Unique name of the component + + **value** (dict) Dictionary of key-value pairs + + - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/keyvalList.png + * - labelInput + - str + - **name** (str) Unique name of the component + + **label** (str) Friendly label for the component + + **value** (str) Value of the editable label text + + - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/labelInput.png + * - numberInput + - int + - **name** (str) Unique name of the component + + **label** (str) Friendly label for the component + + **value** (int) Value of the input + + **placeholder** (int) Placeholder value + + - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/numberInput.png + * - radioList + - String + - **name** (str) Unique name of the component + + **label** (str) Friendly label for the component + + **value** (str) Currently selected option + + **options** ([str, str,...]) List of all options + + - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/radioList.png + * - selectList + - str + - **name** (str) Unique name of the component + + **label** (str) Friendly label for the component + + **value** (str) Currently selected option + + **options** ([str, str,...]) List of all options + + - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/selectList.png + * - tagList + - [str, str,...] + - **name** (str) Unique name of the component + + **value** ([str, str,...]) List of tag strings + + - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/tagList.png + * - textInput + - str + - **name** (str) Unique name of the component + + **label** (str) Friendly label for the component + + **value** (int) Value of the input + + **placeholder** (str) Placeholder value + + - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/textInput.png + +JSON form example +----------------- + +.. literalinclude:: schema_example.json + :language: JSON + + +.. toctree:: + :maxdepth: 1 + + ./schema/json_schema.rst \ No newline at end of file diff --git a/docs/source/plugins/schema/json_schema.rst b/docs/source/plugins/schema/json_schema.rst new file mode 100644 index 00000000..c7cf118e --- /dev/null +++ b/docs/source/plugins/schema/json_schema.rst @@ -0,0 +1,5 @@ +Full JSON Schema +================ + +.. literalinclude:: schema.json + :language: JSON diff --git a/docs/source/plugins/schema/schema.json b/docs/source/plugins/schema/schema.json new file mode 100644 index 00000000..063e63cd --- /dev/null +++ b/docs/source/plugins/schema/schema.json @@ -0,0 +1,187 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://example.com/root.json", + "type": "object", + "title": "The Root Schema", + "required": [ + "id", + "icon", + "forms" + ], + "properties": { + "id": { + "$id": "#/properties/id", + "type": "string", + "title": "Unique ID", + "description": "A unique name for the plugin UI", + "default": "", + "examples": [ + "test-plugin" + ], + "pattern": "^(.*)$" + }, + "icon": { + "$id": "#/properties/icon", + "type": "string", + "title": "Icon name", + "description": "Name of the Material Design icon to use for the plugin tab", + "default": "", + "examples": [ + "pets" + ], + "pattern": "^(.*)$" + }, + "forms": { + "$id": "#/properties/forms", + "type": "array", + "title": "Forms array", + "description": "Array of form descriptions to be displayed", + "default": null, + "items": { + "$id": "#/properties/forms/items", + "type": "object", + "title": "The Items Schema", + "required": [ + "name", + "route", + "schema" + ], + "properties": { + "name": { + "$id": "#/properties/forms/items/properties/name", + "type": "string", + "title": "Form name", + "description": "Human-readable form name", + "default": "", + "examples": [ + "Simple request" + ], + "pattern": "^(.*)$" + }, + "isCollapsible": { + "$id": "#/properties/forms/items/properties/isCollapsible", + "type": "boolean", + "title": "The isCollapsible Schema", + "description": "Should the form be rendered as a collapsible item", + "default": false, + "examples": [ + true + ] + }, + "isTask": { + "$id": "#/properties/forms/items/properties/isTask", + "type": "boolean", + "title": "The isTask Schema", + "description": "Should the form's submit function be treated as a long-running task", + "default": false, + "examples": [ + false + ] + }, + "selfUpdate": { + "$id": "#/properties/forms/items/properties/selfUpdate", + "type": "boolean", + "title": "The selfUpdate Schema", + "description": "Should the form's component values be automatically updated with a GET request", + "default": false, + "examples": [ + true + ] + }, + "route": { + "$id": "#/properties/forms/items/properties/route", + "type": "string", + "title": "The Route Schema", + "description": "Form submit POST request's corresponding api_views route", + "default": "", + "examples": [ + "/do" + ], + "pattern": "^(.*)$" + }, + "submitLabel": { + "$id": "#/properties/forms/items/properties/submitLabel", + "type": "string", + "title": "The submitLabel Schema", + "description": "String to place in submit button", + "default": "Submit", + "examples": [ + "Do my function" + ], + "pattern": "^(.*)$" + }, + "schema": { + "$id": "#/properties/forms/items/properties/schema", + "type": "array", + "title": "Form component array", + "items": { + "$id": "#/properties/forms/items/properties/schema/items", + "type": "object", + "title": "A form component", + "required": [ + "fieldType", + "name" + ], + "properties": { + "fieldType": { + "$id": "#/properties/forms/items/properties/schema/items/properties/fieldType", + "type": "string", + "title": "Component type", + "default": "", + "examples": [ + "numberInput", + "textInput", + "radioList", + "checkList", + "htmlBlock", + "selectList", + "textInput", + "keyvalList", + "taglList" + ], + "pattern": "^(.*)$" + }, + "placeholder": { + "$id": "#/properties/forms/items/properties/schema/items/properties/placeholder", + "type": "string", + "title": "Component placeholder value", + "default": "", + "examples": [ + "Enter your name" + ], + "pattern": "^(.*)$" + }, + "name": { + "$id": "#/properties/forms/items/properties/schema/items/properties/name", + "type": "string", + "title": "Unique component name", + "examples": [ + "my_form_component_1" + ], + "pattern": "^(.*)$" + }, + "label": { + "$id": "#/properties/forms/items/properties/schema/items/properties/label", + "type": "string", + "title": "Component label (if applicable)", + "default": "", + "examples": [ + "My form component" + ], + "pattern": "^(.*)$" + }, + "value": { + "$id": "#/properties/forms/items/properties/schema/items/properties/value", + "type": ["array", "boolean", "integer", "number", "object", "string"], + "title": "Component value", + "pattern": "^(.*)$" + } + } + } + } + } + } + } + } + } \ No newline at end of file diff --git a/docs/source/plugins/schema_example.json b/docs/source/plugins/schema_example.json new file mode 100644 index 00000000..07b348d2 --- /dev/null +++ b/docs/source/plugins/schema_example.json @@ -0,0 +1,87 @@ +{ + "id": "test-plugin", + "icon": "pets", + "forms": [ + { + "name": "Simple request", + "isCollapsible": false, + "isTask": false, + "selfUpdate": true, + "route": "/do", + "submitLabel": "Do things", + "schema": [ + { + "fieldType": "numberInput", + "placeholder": "Some integer", + "name": "val_int", + "label": "Number value" + }, + { + "fieldType": "textInput", + "placeholder": "Some string", + "label": "String value", + "name": "val_str" + }, + { + "fieldType": "radioList", + "name": "val_radio", + "label": "Radio value", + "options": ["First", "Second", "Third"] + }, + { + "fieldType": "checkList", + "name": "val_check", + "label": "Checklist values", + "options": ["Foo", "Bar", "Baz"] + }, + { + "fieldType": "htmlBlock", + "name": "html_block", + "content": "This is a block of HTML in a plugin!" + }, + { + "fieldType": "selectList", + "name": "val_select", + "multi": false, + "label": "Some selection", + "options": ["Most", "Average", "Least"] + }, + { + "fieldType": "textInput", + "placeholder": "Some string", + "label": "Non-persistent string", + "name": "val_disposable" + }, + { + "fieldType": "keyvalList", + "label": "A key-val object", + "name": "val_keyval", + "value": { + "key1": "foo", + "key2": "bar" + } + }, + { + "fieldType": "tagList", + "label": "A tag list", + "name": "val_tags", + "value": ["tag2", "tag2", "squidward"] + } + ] + }, + { + "name": "Task form", + "isTask": true, + "selfUpdate": true, + "route": "/task", + "submitLabel": "Start task", + "schema": [ + { + "fieldType": "numberInput", + "name": "run_time", + "label": "Run time (seconds)" + } + ] + } + ] + } \ No newline at end of file diff --git a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py index f0426d09..eeccdaa7 100644 --- a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py +++ b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py @@ -54,7 +54,7 @@ class ExtensibleSerialInstrument(object): ignore_echo = False port_settings = {} - def __init__(self, port, **kwargs): + def __init__(self, port=None, **kwargs): """ Set up the serial port and so on. """ @@ -64,7 +64,7 @@ class ExtensibleSerialInstrument(object): self.open(port, False) # Eventually this shouldn't rely on init... logging.info("Opened ESI connection to port {}".format(port)) - def open(self, port, quiet=True): + def open(self, port=None, quiet=True): """Open communications with the serial port. If no port is specified, it will attempt to autodetect. If quiet=True @@ -74,6 +74,8 @@ class ExtensibleSerialInstrument(object): if hasattr(self,'_ser') and self._ser.isOpen(): if not quiet: logging.warning("Attempted to open an already-open port!") return + if port is None: + port=self.find_port() assert port is not None, "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?" self._ser = serial.Serial(port,**self.port_settings) #the block above wraps the serial IO layer with a text IO layer @@ -199,15 +201,15 @@ class ExtensibleSerialInstrument(object): response_regex = response_string noop = lambda x: x #placeholder null parse function placeholders = [ #tuples of (regex matching placeholder, regex to replace it with, parse function) - (r"%c",r".", noop), - (r"%(\d+)c",r".{\1}", noop), #TODO support %cn where n is a number of chars - (r"%d",r"[-+]?\d+", int), - (r"%[eEfg]",r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?", float), - (r"%i",r"[-+]?(?:0[xX][\dA-Fa-f]+|0[0-7]*|\d+)", lambda x: int(x, 0)), #0=autodetect base - (r"%o",r"[-+]?[0-7]+", lambda x: int(x, 8)), #8 means octal - (r"%s",r"\S+",noop), - (r"%u",r"\d+",int), - (r"%[xX]",r"[-+]?(?:0[xX])?[\dA-Fa-f]+",lambda x: int(x, 16)), #16 forces hexadecimal + (r"%c", r".", noop), + (r"%(\d+)c", r".{\1}", noop), #TODO support %cn where n is a number of chars + (r"%d", r"[-+]?\\d+", int), + (r"%[eEfg]", r"[-+]?(?:\\d+(?:\.\\d*)?|\.\\d+)(?:[eE][-+]?\\d+)?", float), + (r"%i", r"[-+]?(?:0[xX][\\dA-Fa-f]+|0[0-7]*|\\d+)", lambda x: int(x, 0)), #0=autodetect base + (r"%o", r"[-+]?[0-7]+", lambda x: int(x, 8)), #8 means octal + (r"%s", r"\\s+", noop), + (r"%u", r"\\d+", int), + (r"%[xX]", r"[-+]?(?:0[xX])?[\\dA-Fa-f]+", lambda x: int(x, 16)), #16 forces hexadecimal ] matched_placeholders = [] for placeholder, regex, parse_fun in placeholders: @@ -261,7 +263,32 @@ class ExtensibleSerialInstrument(object): Usually this function sends a command and checks for a known reply.""" with self.communications_lock: return True - + + def find_port(self): + """Iterate through the available serial ports and query them to see + if our instrument is there.""" + with self.communications_lock: + success = False + for port_name, _, _ in serial.tools.list_ports.comports(): #loop through serial ports, apparently 256 is the limit?! + try: + logging.info("Trying port {}".format(port_name)) + self.open(port_name) + success = True + logging.info("Success!") + except: + pass + finally: + try: + self.close() + except: + pass #we don't care if there's an error closing the port... + if success: + break #again, make sure this happens *after* closing the port + if success: + return port_name + else: + return None + class OptionalModule(object): """This allows a `ExtensibleSerialInstrument` to have optional features. diff --git a/openflexure_microscope/stage/sangaboard/sangaboard.py b/openflexure_microscope/stage/sangaboard/sangaboard.py index 7c3cc8ee..b755d44b 100644 --- a/openflexure_microscope/stage/sangaboard/sangaboard.py +++ b/openflexure_microscope/stage/sangaboard/sangaboard.py @@ -81,13 +81,8 @@ class Sangaboard(ExtensibleSerialInstrument): it doesn't need to be named. """ - # If no port is specified - if not port: - # Scan all available ports, and check for valid firmware - scanned_port = self.scan_ports() - # Initialise basic serial instrument with specified - ExtensibleSerialInstrument.__init__(self, scanned_port, **kwargs) + ExtensibleSerialInstrument.__init__(self, port, **kwargs) try: # Make absolutely sure that whatever port we're on is valid @@ -119,44 +114,18 @@ class Sangaboard(ExtensibleSerialInstrument): logging.error("You may need to update the firmware running on the Sangaboard.") raise e - def scan_ports(self): - """Iterate through the available serial ports and query them to see - if our instrument is there.""" - logging.debug("Running Sangaboard port scanner") - with self.communications_lock: - success = False - for port_name, _, _ in serial.tools.list_ports.comports(): #loop through serial ports, apparently 256 is the limit?! - try: - logging.info("Trying port {}".format(port_name)) - self.open(port_name) - success = True - - logging.info("Checking firmware") - fw = self.check_valid_firmware() - if not fw: - success = False - except Exception as e: - logging.warning("Error on port {}".format(port_name)) - logging.warning(e) - pass - finally: - try: - self.close() - except: - pass #we don't care if there's an error closing the port... - if success: - break #again, make sure this happens *after* closing the port - if success: - return port_name - else: - return None + def test_communications(self): + """ + Overrides superclass, used in self.open(), and port scanning + """ + return self.check_valid_firmware() def check_valid_firmware(self): logging.debug("Running firmware checks") # Request firmware version from the board self.firmware = self.query("version",timeout=2).rstrip() - + logging.info("Firmware response: {}".format(self.firmware)) # Check for valid firmware string if self.firmware: match = re.match(r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware)