Loading docs/source/conf.py +53 −38 Original line number Diff line number Diff line Loading @@ -17,32 +17,34 @@ import sys # Load module from relative imports module_path = os.path.abspath('../..') module_path = os.path.abspath("../..") sys.path.insert(0, module_path) # Handle mock imports for non-platform-agnostic modules from unittest.mock import MagicMock class Mock(MagicMock): @classmethod def __getattr__(cls, name): return MagicMock() mock_imports = ['picamera', 'picamera.array', 'picamera.mmalobj'] mock_imports = ["picamera", "picamera.array", "picamera.mmalobj"] sys.modules.update((mod_name, Mock()) for mod_name in mock_imports) # -- Project information ----------------------------------------------------- project = 'OpenFlexure Microscope Software' copyright = '2018, Bath Open Instrumentation Group' author = 'Bath Open Instrumentation Group' project = "OpenFlexure Microscope Software" copyright = "2018, Bath Open Instrumentation Group" author = "Bath Open Instrumentation Group" # The short X.Y version version = '' version = "" # The full version, including alpha/beta/rc tags release = '' release = "" # -- General configuration --------------------------------------------------- Loading @@ -55,32 +57,32 @@ release = '' # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages', 'sphinx.ext.ifconfig', 'sphinxcontrib.httpdomain', 'sphinxcontrib.autohttp.flask', 'sphinxcontrib.autohttp.flaskqref' "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx.ext.githubpages", "sphinx.ext.ifconfig", "sphinxcontrib.httpdomain", "sphinxcontrib.autohttp.flask", "sphinxcontrib.autohttp.flaskqref", ] # Override ordering autodoc_member_order = 'bysource' autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' source_suffix = ".rst" # The master toctree document. master_doc = 'index' master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. Loading Loading @@ -114,7 +116,7 @@ html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. Loading @@ -130,7 +132,7 @@ html_static_path = ['_static'] # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'OpenFlexureMicroscopeSoftwaredoc' htmlhelp_basename = "OpenFlexureMicroscopeSoftwaredoc" # -- Options for LaTeX output ------------------------------------------------ Loading @@ -139,15 +141,12 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', Loading @@ -157,8 +156,13 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'OpenFlexureMicroscopeSoftware.tex', 'OpenFlexure Microscope Software Documentation', 'Bath Open Instrumentation Group', 'manual'), ( master_doc, "OpenFlexureMicroscopeSoftware.tex", "OpenFlexure Microscope Software Documentation", "Bath Open Instrumentation Group", "manual", ) ] Loading @@ -167,8 +171,13 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'openflexuremicroscopesoftware', 'OpenFlexure Microscope Software Documentation', [author], 1) ( master_doc, "openflexuremicroscopesoftware", "OpenFlexure Microscope Software Documentation", [author], 1, ) ] Loading @@ -178,9 +187,15 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'OpenFlexureMicroscopeSoftware', 'OpenFlexure Microscope Software Documentation', author, 'OpenFlexureMicroscopeSoftware', 'One line description of project.', 'Miscellaneous'), ( master_doc, "OpenFlexureMicroscopeSoftware", "OpenFlexure Microscope Software Documentation", author, "OpenFlexureMicroscopeSoftware", "One line description of project.", "Miscellaneous", ) ] Loading @@ -199,7 +214,7 @@ epub_title = project # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- Loading @@ -208,8 +223,8 @@ epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'openflexure_stage': ('https://openflexure-stage.readthedocs.io/en/latest/', None), 'picamera': ('https://picamera.readthedocs.io/en/release-1.13/', None) "openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None), "picamera": ("https://picamera.readthedocs.io/en/release-1.13/", None), } # -- Options for todo extension ---------------------------------------------- Loading docs/source/plugins/example/plugin.py +27 −26 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ FORM_PATH = os.path.join(HERE, "forms.json") ### MICROSCOPE PLUGIN ### class MyPluginClass(MicroscopePlugin): """ A set of default plugins Loading @@ -20,13 +21,13 @@ class MyPluginClass(MicroscopePlugin): global FORM_PATH with open(FORM_PATH, 'r') as sc: with open(FORM_PATH, "r") as sc: api_form = json.load(sc) api_views = { '/identify': IdentifyAPI, '/hello': HelloWorldAPI, '/timelapse': TimelapseAPI, "/identify": IdentifyAPI, "/hello": HelloWorldAPI, "/timelapse": TimelapseAPI, } def identify(self): Loading @@ -34,8 +35,9 @@ class MyPluginClass(MicroscopePlugin): 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) response = "My parent camera is {}, and my parent stage is {}.".format( self.microscope.camera, self.microscope.stage ) return response def hello_world(self): Loading @@ -58,13 +60,11 @@ class MyPluginClass(MicroscopePlugin): # Create a data stream to capture to capture_data = self.microscope.camera.new_image( write_to_file=True, temporary=False) 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) self.microscope.camera.capture(capture_data, use_video_port=True) # Append the capture data to our list capture_array.append(capture_data) Loading @@ -75,10 +75,12 @@ class MyPluginClass(MicroscopePlugin): ### 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. Loading @@ -99,13 +101,11 @@ class HelloWorldAPI(MicroscopeViewPlugin): """ # If the microscope does not already contain our plugin_string attribute if not hasattr(self.microscope, 'plugin_string'): 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 }) json_response = jsonify({"plugin_string": self.microscope.plugin_string}) return Response(json_response) Loading @@ -118,18 +118,17 @@ class HelloWorldAPI(MicroscopeViewPlugin): payload = JsonResponse(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) 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 }) json_response = jsonify({"plugin_string": self.microscope.plugin_string}) return Response(json_response) class TimelapseAPI(MicroscopeViewPlugin): def post(self): Loading @@ -137,10 +136,12 @@ class TimelapseAPI(MicroscopeViewPlugin): payload = JsonResponse(request) # Extract the "n_images" parameter if it was passed. Otherwise, default to 10. n_images = payload.param('n_images', default=10, convert=int) 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) 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 tests/api_client.py +29 −33 Original line number Diff line number Diff line Loading @@ -30,63 +30,61 @@ class APIconnection: return r.json() def set_overlay(self, message="", size=50): json = { "text": message, "size": size } return self.post('/camera/overlay', json=json) json = {"text": message, "size": size} return self.post("/camera/overlay", json=json) def get_overlay(self): return self.get('/camera/overlay') return self.get("/camera/overlay") def get_config(self): return self.get('/config') return self.get("/config") def set_config(self, config_dict): return self.post('/config', json=config_dict) return self.post("/config", json=config_dict) def get_state(self): return self.get('/state') return self.get("/state") def start_preview(self): return self.post('/camera/preview/start') return self.post("/camera/preview/start") def stop_preview(self): return self.post('/camera/preview/stop') return self.post("/camera/preview/stop") def move_by(self, x=0, y=0, z=0): json = { "x": x, "y": y, "z": z } return self.post('/stage/position', json=json) json = {"x": x, "y": y, "z": z} return self.post("/stage/position", json=json) def new_capture(self, use_video_port=True, keep_on_disk=False, resize=None): json = { "keep_on_disk": keep_on_disk, "use_video_port": use_video_port } json = {"keep_on_disk": keep_on_disk, "use_video_port": use_video_port} if resize: json['size'] = {'width': resize[0], 'height': resize[1]} json["size"] = {"width": resize[0], "height": resize[1]} return self.post('/camera/capture', json=json) return self.post("/camera/capture", json=json) def get_capture(self, capture_id): uri_route = '/camera/capture/{}/download'.format(capture_id) uri_route = "/camera/capture/{}/download".format(capture_id) r = self.get(uri_route, json=False) img = Image.open(BytesIO(r.content)) array = np.asarray(img, dtype=np.int32) return array def del_capture(self, capture_id): uri_route = '/camera/capture/{}'.format(capture_id) uri_route = "/camera/capture/{}".format(capture_id) return self.delete(uri_route) def capture(self, use_video_port=True, keep_on_disk=False, delete_after_use=True, resize=None): p = self.new_capture(use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize) capture_id = p['metadata']['id'] def capture( self, use_video_port=True, keep_on_disk=False, delete_after_use=True, resize=None, ): p = self.new_capture( use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize ) capture_id = p["metadata"]["id"] img_array = self.get_capture(capture_id) if delete_after_use: Loading @@ -95,10 +93,8 @@ class APIconnection: return img_array def set_zoom(self, zoom_value=1.0): json = { "zoom_value": zoom_value, } return self.post('/camera/zoom', json=json) json = {"zoom_value": zoom_value} return self.post("/camera/zoom", json=json) def get_zoom(self): return self.get('/camera/zoom') return self.get("/camera/zoom") tests/test_api.py +25 −32 Original line number Diff line number Diff line Loading @@ -6,27 +6,23 @@ import unittest import logging import sys logging.basicConfig(stream=sys.stderr, level=logging.INFO) class TestCapture(unittest.TestCase): def test_capture_config(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") config = connection.get_config() expected_keys = [ 'image_resolution', 'stream_resolution', 'numpy_resolution', ] expected_keys = ["image_resolution", "stream_resolution", "numpy_resolution"] for key in expected_keys: self.assertTrue(key in config) def test_capture_videoport(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") resolution = connection.get_config()['stream_resolution'] resolution = connection.get_config()["stream_resolution"] for resize in [None, (640, 480)]: Loading @@ -37,13 +33,14 @@ class TestCapture(unittest.TestCase): use_video_port=True, keep_on_disk=False, delete_after_use=True, resize=resize) resize=resize, ) self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3)) def test_capture_full(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") resolution = connection.get_config()['image_resolution'] resolution = connection.get_config()["image_resolution"] for resize in [None, (640, 480)]: Loading @@ -54,20 +51,18 @@ class TestCapture(unittest.TestCase): use_video_port=False, keep_on_disk=False, delete_after_use=True, resize=resize) resize=resize, ) self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3)) class TestStage(unittest.TestCase): def test_stage_config(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") config = connection.get_config() expected_keys = [ 'backlash', ] expected_keys = ["backlash"] for key in expected_keys: self.assertTrue(key in config) Loading @@ -76,14 +71,12 @@ class TestStage(unittest.TestCase): connection = APIconnection(host="localhost", port=5000, api_ver="v1") state = connection.get_state() self.assertTrue('stage' in state) self.assertTrue("stage" in state) expected_keys = [ 'position', ] expected_keys = ["position"] for key in expected_keys: self.assertTrue(key in state['stage']) self.assertTrue(key in state["stage"]) def test_stage_movement(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") Loading @@ -91,16 +84,16 @@ class TestStage(unittest.TestCase): move_distance = 500 for axis in range(3): for direction in [1, -1]: pos_i_dict = connection.get_state()['stage']['position'] pos_i = [pos_i_dict['x'], pos_i_dict['y'], pos_i_dict['z']] pos_i_dict = connection.get_state()["stage"]["position"] pos_i = [pos_i_dict["x"], pos_i_dict["y"], pos_i_dict["z"]] move = [0, 0, 0] move[axis] = move_distance * direction connection.move_by(*move) pos_f_dict = connection.get_state()['stage']['position'] pos_f = [pos_f_dict['x'], pos_f_dict['y'], pos_f_dict['z']] pos_f_dict = connection.get_state()["stage"]["position"] pos_f = [pos_f_dict["x"], pos_f_dict["y"], pos_f_dict["z"]] diff = np.subtract(pos_f, pos_i) logging.debug("{} > {}".format(pos_i, pos_f)) Loading @@ -108,7 +101,7 @@ class TestStage(unittest.TestCase): self.assertTrue(np.array_equal(diff, move)) if __name__ == '__main__': if __name__ == "__main__": suites = [ unittest.TestLoader().loadTestsFromTestCase(TestCapture), Loading tests/test_camera.py +11 −31 Original line number Diff line number Diff line Loading @@ -12,11 +12,11 @@ import unittest import logging import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) class TestCaptureMethods(unittest.TestCase): def test_still_capture(self): """Tests capturing still images to a BytesIO stream.""" global camera Loading @@ -30,11 +30,7 @@ class TestCaptureMethods(unittest.TestCase): # Capture to a context (auto-deletes files when done) with camera.new_image(write_to_file=False) as output: camera.capture( output, use_video_port=use_video_port, resize=resize ) camera.capture(output, use_video_port=use_video_port, resize=resize) # Ensure file deletion fails and returns False self.assertFalse(output.delete_file()) Loading @@ -43,14 +39,8 @@ class TestCaptureMethods(unittest.TestCase): # BEFORE DELETE: Ensure StreamObject 'stream' has # a valid BytesIO object and byte string self.assertTrue(isinstance( output.data, io.IOBase )) self.assertTrue(isinstance( output.binary, (bytes, bytearray) )) self.assertTrue(isinstance(output.data, io.IOBase)) self.assertTrue(isinstance(output.binary, (bytes, bytearray))) # Save capture to file output.save_file() Loading @@ -65,10 +55,7 @@ class TestCaptureMethods(unittest.TestCase): # AFTER DELETE: Ensure StreamObject 'stream' has # a valid BytesIO object and byte string self.assertTrue(isinstance(output.data, io.IOBase)) self.assertTrue(isinstance( output.binary, (bytes, bytearray) )) self.assertTrue(isinstance(output.binary, (bytes, bytearray))) # Create a PIL image from stream image = Image.open(output.data) Loading Loading @@ -102,7 +89,8 @@ class TestCaptureMethods(unittest.TestCase): output = camera.capture( camera.new_image(write_to_file=True), use_video_port=use_video_port, resize=resize) resize=resize, ) # Check file got saved self.assertTrue(os.path.isfile(output.file)) Loading @@ -122,7 +110,6 @@ class TestCaptureMethods(unittest.TestCase): class TestUnencodedMethods(unittest.TestCase): def test_yuv_array(self): """Tests capturing unencoded YUV data to a Numpy array.""" global camera Loading @@ -136,9 +123,7 @@ class TestUnencodedMethods(unittest.TestCase): camera.wait_for_camera() # Capture RGB array yuv = camera.yuv( use_video_port=use_video_port, resize=resize) yuv = camera.yuv(use_video_port=use_video_port, resize=resize) # Ensure capture output is a valid numpy array self.assertTrue(isinstance(yuv, np.ndarray)) Loading Loading @@ -168,9 +153,7 @@ class TestUnencodedMethods(unittest.TestCase): camera.wait_for_camera() # Capture RGB array rgb = camera.array( use_video_port=use_video_port, resize=resize) rgb = camera.array(use_video_port=use_video_port, resize=resize) # Ensure capture output is a valid numpy array self.assertTrue(isinstance(rgb, np.ndarray)) Loading @@ -189,7 +172,6 @@ class TestUnencodedMethods(unittest.TestCase): class TestRecordMethods(unittest.TestCase): def test_video_record(self): """Tests recording videos to BytesIO stream, and to file on disk.""" global camera Loading @@ -201,9 +183,7 @@ class TestRecordMethods(unittest.TestCase): # Wait for camera camera.wait_for_camera() with camera.new_video( write_to_file=write_to_file ) as output: with camera.new_video(write_to_file=write_to_file) as output: # Start recording camera.start_recording(output) Loading Loading @@ -278,7 +258,7 @@ class TestThreadStarting(unittest.TestCase): self.assertIsInstance(camera.stream, io.IOBase) if __name__ == '__main__': if __name__ == "__main__": with PiCameraStreamer() as camera: suites = [ Loading Loading
docs/source/conf.py +53 −38 Original line number Diff line number Diff line Loading @@ -17,32 +17,34 @@ import sys # Load module from relative imports module_path = os.path.abspath('../..') module_path = os.path.abspath("../..") sys.path.insert(0, module_path) # Handle mock imports for non-platform-agnostic modules from unittest.mock import MagicMock class Mock(MagicMock): @classmethod def __getattr__(cls, name): return MagicMock() mock_imports = ['picamera', 'picamera.array', 'picamera.mmalobj'] mock_imports = ["picamera", "picamera.array", "picamera.mmalobj"] sys.modules.update((mod_name, Mock()) for mod_name in mock_imports) # -- Project information ----------------------------------------------------- project = 'OpenFlexure Microscope Software' copyright = '2018, Bath Open Instrumentation Group' author = 'Bath Open Instrumentation Group' project = "OpenFlexure Microscope Software" copyright = "2018, Bath Open Instrumentation Group" author = "Bath Open Instrumentation Group" # The short X.Y version version = '' version = "" # The full version, including alpha/beta/rc tags release = '' release = "" # -- General configuration --------------------------------------------------- Loading @@ -55,32 +57,32 @@ release = '' # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages', 'sphinx.ext.ifconfig', 'sphinxcontrib.httpdomain', 'sphinxcontrib.autohttp.flask', 'sphinxcontrib.autohttp.flaskqref' "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx.ext.githubpages", "sphinx.ext.ifconfig", "sphinxcontrib.httpdomain", "sphinxcontrib.autohttp.flask", "sphinxcontrib.autohttp.flaskqref", ] # Override ordering autodoc_member_order = 'bysource' autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' source_suffix = ".rst" # The master toctree document. master_doc = 'index' master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. Loading Loading @@ -114,7 +116,7 @@ html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. Loading @@ -130,7 +132,7 @@ html_static_path = ['_static'] # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'OpenFlexureMicroscopeSoftwaredoc' htmlhelp_basename = "OpenFlexureMicroscopeSoftwaredoc" # -- Options for LaTeX output ------------------------------------------------ Loading @@ -139,15 +141,12 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', Loading @@ -157,8 +156,13 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'OpenFlexureMicroscopeSoftware.tex', 'OpenFlexure Microscope Software Documentation', 'Bath Open Instrumentation Group', 'manual'), ( master_doc, "OpenFlexureMicroscopeSoftware.tex", "OpenFlexure Microscope Software Documentation", "Bath Open Instrumentation Group", "manual", ) ] Loading @@ -167,8 +171,13 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'openflexuremicroscopesoftware', 'OpenFlexure Microscope Software Documentation', [author], 1) ( master_doc, "openflexuremicroscopesoftware", "OpenFlexure Microscope Software Documentation", [author], 1, ) ] Loading @@ -178,9 +187,15 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'OpenFlexureMicroscopeSoftware', 'OpenFlexure Microscope Software Documentation', author, 'OpenFlexureMicroscopeSoftware', 'One line description of project.', 'Miscellaneous'), ( master_doc, "OpenFlexureMicroscopeSoftware", "OpenFlexure Microscope Software Documentation", author, "OpenFlexureMicroscopeSoftware", "One line description of project.", "Miscellaneous", ) ] Loading @@ -199,7 +214,7 @@ epub_title = project # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- Loading @@ -208,8 +223,8 @@ epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'openflexure_stage': ('https://openflexure-stage.readthedocs.io/en/latest/', None), 'picamera': ('https://picamera.readthedocs.io/en/release-1.13/', None) "openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None), "picamera": ("https://picamera.readthedocs.io/en/release-1.13/", None), } # -- Options for todo extension ---------------------------------------------- Loading
docs/source/plugins/example/plugin.py +27 −26 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ FORM_PATH = os.path.join(HERE, "forms.json") ### MICROSCOPE PLUGIN ### class MyPluginClass(MicroscopePlugin): """ A set of default plugins Loading @@ -20,13 +21,13 @@ class MyPluginClass(MicroscopePlugin): global FORM_PATH with open(FORM_PATH, 'r') as sc: with open(FORM_PATH, "r") as sc: api_form = json.load(sc) api_views = { '/identify': IdentifyAPI, '/hello': HelloWorldAPI, '/timelapse': TimelapseAPI, "/identify": IdentifyAPI, "/hello": HelloWorldAPI, "/timelapse": TimelapseAPI, } def identify(self): Loading @@ -34,8 +35,9 @@ class MyPluginClass(MicroscopePlugin): 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) response = "My parent camera is {}, and my parent stage is {}.".format( self.microscope.camera, self.microscope.stage ) return response def hello_world(self): Loading @@ -58,13 +60,11 @@ class MyPluginClass(MicroscopePlugin): # Create a data stream to capture to capture_data = self.microscope.camera.new_image( write_to_file=True, temporary=False) 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) self.microscope.camera.capture(capture_data, use_video_port=True) # Append the capture data to our list capture_array.append(capture_data) Loading @@ -75,10 +75,12 @@ class MyPluginClass(MicroscopePlugin): ### 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. Loading @@ -99,13 +101,11 @@ class HelloWorldAPI(MicroscopeViewPlugin): """ # If the microscope does not already contain our plugin_string attribute if not hasattr(self.microscope, 'plugin_string'): 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 }) json_response = jsonify({"plugin_string": self.microscope.plugin_string}) return Response(json_response) Loading @@ -118,18 +118,17 @@ class HelloWorldAPI(MicroscopeViewPlugin): payload = JsonResponse(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) 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 }) json_response = jsonify({"plugin_string": self.microscope.plugin_string}) return Response(json_response) class TimelapseAPI(MicroscopeViewPlugin): def post(self): Loading @@ -137,10 +136,12 @@ class TimelapseAPI(MicroscopeViewPlugin): payload = JsonResponse(request) # Extract the "n_images" parameter if it was passed. Otherwise, default to 10. n_images = payload.param('n_images', default=10, convert=int) 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) 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
tests/api_client.py +29 −33 Original line number Diff line number Diff line Loading @@ -30,63 +30,61 @@ class APIconnection: return r.json() def set_overlay(self, message="", size=50): json = { "text": message, "size": size } return self.post('/camera/overlay', json=json) json = {"text": message, "size": size} return self.post("/camera/overlay", json=json) def get_overlay(self): return self.get('/camera/overlay') return self.get("/camera/overlay") def get_config(self): return self.get('/config') return self.get("/config") def set_config(self, config_dict): return self.post('/config', json=config_dict) return self.post("/config", json=config_dict) def get_state(self): return self.get('/state') return self.get("/state") def start_preview(self): return self.post('/camera/preview/start') return self.post("/camera/preview/start") def stop_preview(self): return self.post('/camera/preview/stop') return self.post("/camera/preview/stop") def move_by(self, x=0, y=0, z=0): json = { "x": x, "y": y, "z": z } return self.post('/stage/position', json=json) json = {"x": x, "y": y, "z": z} return self.post("/stage/position", json=json) def new_capture(self, use_video_port=True, keep_on_disk=False, resize=None): json = { "keep_on_disk": keep_on_disk, "use_video_port": use_video_port } json = {"keep_on_disk": keep_on_disk, "use_video_port": use_video_port} if resize: json['size'] = {'width': resize[0], 'height': resize[1]} json["size"] = {"width": resize[0], "height": resize[1]} return self.post('/camera/capture', json=json) return self.post("/camera/capture", json=json) def get_capture(self, capture_id): uri_route = '/camera/capture/{}/download'.format(capture_id) uri_route = "/camera/capture/{}/download".format(capture_id) r = self.get(uri_route, json=False) img = Image.open(BytesIO(r.content)) array = np.asarray(img, dtype=np.int32) return array def del_capture(self, capture_id): uri_route = '/camera/capture/{}'.format(capture_id) uri_route = "/camera/capture/{}".format(capture_id) return self.delete(uri_route) def capture(self, use_video_port=True, keep_on_disk=False, delete_after_use=True, resize=None): p = self.new_capture(use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize) capture_id = p['metadata']['id'] def capture( self, use_video_port=True, keep_on_disk=False, delete_after_use=True, resize=None, ): p = self.new_capture( use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize ) capture_id = p["metadata"]["id"] img_array = self.get_capture(capture_id) if delete_after_use: Loading @@ -95,10 +93,8 @@ class APIconnection: return img_array def set_zoom(self, zoom_value=1.0): json = { "zoom_value": zoom_value, } return self.post('/camera/zoom', json=json) json = {"zoom_value": zoom_value} return self.post("/camera/zoom", json=json) def get_zoom(self): return self.get('/camera/zoom') return self.get("/camera/zoom")
tests/test_api.py +25 −32 Original line number Diff line number Diff line Loading @@ -6,27 +6,23 @@ import unittest import logging import sys logging.basicConfig(stream=sys.stderr, level=logging.INFO) class TestCapture(unittest.TestCase): def test_capture_config(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") config = connection.get_config() expected_keys = [ 'image_resolution', 'stream_resolution', 'numpy_resolution', ] expected_keys = ["image_resolution", "stream_resolution", "numpy_resolution"] for key in expected_keys: self.assertTrue(key in config) def test_capture_videoport(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") resolution = connection.get_config()['stream_resolution'] resolution = connection.get_config()["stream_resolution"] for resize in [None, (640, 480)]: Loading @@ -37,13 +33,14 @@ class TestCapture(unittest.TestCase): use_video_port=True, keep_on_disk=False, delete_after_use=True, resize=resize) resize=resize, ) self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3)) def test_capture_full(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") resolution = connection.get_config()['image_resolution'] resolution = connection.get_config()["image_resolution"] for resize in [None, (640, 480)]: Loading @@ -54,20 +51,18 @@ class TestCapture(unittest.TestCase): use_video_port=False, keep_on_disk=False, delete_after_use=True, resize=resize) resize=resize, ) self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3)) class TestStage(unittest.TestCase): def test_stage_config(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") config = connection.get_config() expected_keys = [ 'backlash', ] expected_keys = ["backlash"] for key in expected_keys: self.assertTrue(key in config) Loading @@ -76,14 +71,12 @@ class TestStage(unittest.TestCase): connection = APIconnection(host="localhost", port=5000, api_ver="v1") state = connection.get_state() self.assertTrue('stage' in state) self.assertTrue("stage" in state) expected_keys = [ 'position', ] expected_keys = ["position"] for key in expected_keys: self.assertTrue(key in state['stage']) self.assertTrue(key in state["stage"]) def test_stage_movement(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") Loading @@ -91,16 +84,16 @@ class TestStage(unittest.TestCase): move_distance = 500 for axis in range(3): for direction in [1, -1]: pos_i_dict = connection.get_state()['stage']['position'] pos_i = [pos_i_dict['x'], pos_i_dict['y'], pos_i_dict['z']] pos_i_dict = connection.get_state()["stage"]["position"] pos_i = [pos_i_dict["x"], pos_i_dict["y"], pos_i_dict["z"]] move = [0, 0, 0] move[axis] = move_distance * direction connection.move_by(*move) pos_f_dict = connection.get_state()['stage']['position'] pos_f = [pos_f_dict['x'], pos_f_dict['y'], pos_f_dict['z']] pos_f_dict = connection.get_state()["stage"]["position"] pos_f = [pos_f_dict["x"], pos_f_dict["y"], pos_f_dict["z"]] diff = np.subtract(pos_f, pos_i) logging.debug("{} > {}".format(pos_i, pos_f)) Loading @@ -108,7 +101,7 @@ class TestStage(unittest.TestCase): self.assertTrue(np.array_equal(diff, move)) if __name__ == '__main__': if __name__ == "__main__": suites = [ unittest.TestLoader().loadTestsFromTestCase(TestCapture), Loading
tests/test_camera.py +11 −31 Original line number Diff line number Diff line Loading @@ -12,11 +12,11 @@ import unittest import logging import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) class TestCaptureMethods(unittest.TestCase): def test_still_capture(self): """Tests capturing still images to a BytesIO stream.""" global camera Loading @@ -30,11 +30,7 @@ class TestCaptureMethods(unittest.TestCase): # Capture to a context (auto-deletes files when done) with camera.new_image(write_to_file=False) as output: camera.capture( output, use_video_port=use_video_port, resize=resize ) camera.capture(output, use_video_port=use_video_port, resize=resize) # Ensure file deletion fails and returns False self.assertFalse(output.delete_file()) Loading @@ -43,14 +39,8 @@ class TestCaptureMethods(unittest.TestCase): # BEFORE DELETE: Ensure StreamObject 'stream' has # a valid BytesIO object and byte string self.assertTrue(isinstance( output.data, io.IOBase )) self.assertTrue(isinstance( output.binary, (bytes, bytearray) )) self.assertTrue(isinstance(output.data, io.IOBase)) self.assertTrue(isinstance(output.binary, (bytes, bytearray))) # Save capture to file output.save_file() Loading @@ -65,10 +55,7 @@ class TestCaptureMethods(unittest.TestCase): # AFTER DELETE: Ensure StreamObject 'stream' has # a valid BytesIO object and byte string self.assertTrue(isinstance(output.data, io.IOBase)) self.assertTrue(isinstance( output.binary, (bytes, bytearray) )) self.assertTrue(isinstance(output.binary, (bytes, bytearray))) # Create a PIL image from stream image = Image.open(output.data) Loading Loading @@ -102,7 +89,8 @@ class TestCaptureMethods(unittest.TestCase): output = camera.capture( camera.new_image(write_to_file=True), use_video_port=use_video_port, resize=resize) resize=resize, ) # Check file got saved self.assertTrue(os.path.isfile(output.file)) Loading @@ -122,7 +110,6 @@ class TestCaptureMethods(unittest.TestCase): class TestUnencodedMethods(unittest.TestCase): def test_yuv_array(self): """Tests capturing unencoded YUV data to a Numpy array.""" global camera Loading @@ -136,9 +123,7 @@ class TestUnencodedMethods(unittest.TestCase): camera.wait_for_camera() # Capture RGB array yuv = camera.yuv( use_video_port=use_video_port, resize=resize) yuv = camera.yuv(use_video_port=use_video_port, resize=resize) # Ensure capture output is a valid numpy array self.assertTrue(isinstance(yuv, np.ndarray)) Loading Loading @@ -168,9 +153,7 @@ class TestUnencodedMethods(unittest.TestCase): camera.wait_for_camera() # Capture RGB array rgb = camera.array( use_video_port=use_video_port, resize=resize) rgb = camera.array(use_video_port=use_video_port, resize=resize) # Ensure capture output is a valid numpy array self.assertTrue(isinstance(rgb, np.ndarray)) Loading @@ -189,7 +172,6 @@ class TestUnencodedMethods(unittest.TestCase): class TestRecordMethods(unittest.TestCase): def test_video_record(self): """Tests recording videos to BytesIO stream, and to file on disk.""" global camera Loading @@ -201,9 +183,7 @@ class TestRecordMethods(unittest.TestCase): # Wait for camera camera.wait_for_camera() with camera.new_video( write_to_file=write_to_file ) as output: with camera.new_video(write_to_file=write_to_file) as output: # Start recording camera.start_recording(output) Loading Loading @@ -278,7 +258,7 @@ class TestThreadStarting(unittest.TestCase): self.assertIsInstance(camera.stream, io.IOBase) if __name__ == '__main__': if __name__ == "__main__": with PiCameraStreamer() as camera: suites = [ Loading