From ea10cdd1ed836191e7ad4adca65594527b0b922e Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 5 May 2018 19:34:01 +0200 Subject: [PATCH] Nice spectrum plot looking as it should, including photodiode response compensation. --- firmware/Spectrum Measurement.ipynb | 4928 ++++++++++++++++++++++++++- firmware/measure_spectrum.py | 19 +- firmware/spectra.sqlite3 | Bin 208896 -> 294912 bytes firmware/spectrum_progress.py | 10 +- firmware/stepper_test.py | 9 +- 5 files changed, 4945 insertions(+), 21 deletions(-) diff --git a/firmware/Spectrum Measurement.ipynb b/firmware/Spectrum Measurement.ipynb index 57d4339..5d76e1d 100644 --- a/firmware/Spectrum Measurement.ipynb +++ b/firmware/Spectrum Measurement.ipynb @@ -12,6 +12,7 @@ "import time\n", "from IPython import display\n", "from datetime import datetime\n", + "import scipy.interpolate as inter\n", "\n", "import numpy as np\n", "from matplotlib import pyplot as plt\n", @@ -1690,14 +1691,12 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 72, "metadata": { "collapsed": false }, "outputs": [], "source": [ - "import scipy.interpolate as inter\n", - "\n", "def live_plot_rgb_splines(id_r, id_g, id_b, max_stdev=0.05, spline_s=1, interval=1, live=True):\n", " fig, ax = plt.subplots(1, 1)\n", " fig.suptitle('Runs {}, {}, {} at {:%y-%m-%d %H:%M:%S}'.format(id_r, id_g, id_b, datetime.now()))\n", @@ -1705,15 +1704,18 @@ " while True:\n", " ax.clear()\n", " colors = [\n", - " ((1,0,0), (1,0.5,0.5)),\n", - " ((0,1,0), (0.5,1,0.5)),\n", - " ((0,0,1), (0.5,0.5,1))\n", + " ((1,0,0), (1,0.8,0.8)),\n", + " ((0,1,0), (0.8,1,0.8)),\n", + " ((0,0,1), (0.8,0.8,1))\n", " ]\n", " for run_id, (color_dark, color_bright) in zip([id_r, id_g, id_b], colors):\n", " steps, values, stdev = load_run_zero_cal(run_id, max_stdev)\n", - " spline = inter.UnivariateSpline(steps, values, s=spline_s)\n", " ax.errorbar(steps, values, yerr=stdev, color=color_bright)\n", - " ax.plot(steps, spline(steps), color=color_dark)\n", + " try:\n", + " spline = inter.UnivariateSpline(steps, values, s=spline_s)\n", + " ax.plot(steps, spline(steps), color=color_dark)\n", + " except:\n", + " pass\n", " fig.canvas.draw()\n", " if not live:\n", " break\n", @@ -2517,6 +2519,4916 @@ "* Decrease amplification to avoid clipping. Maybe change amplification midway for green channel. Currenlty set to 5GOhm using 10M transimp feedback R with 1:10 T feedback and ~1:50 gain voltage amp stage. Maybe go back to plain transimp amp with 10M gain, for a total gain of 500M\n", "* Decrease VGND bias to allow for more headroom" ] + }, + { + "cell_type": "code", + "execution_count": 96, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support.' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " this.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width);\n", + " canvas.attr('height', height);\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
')\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " event.shiftKey = false;\n", + " // Send a \"J\" for go to next cell\n", + " event.which = 74;\n", + " event.keyCode = 74;\n", + " manager.command_mode();\n", + " manager.handle_keydown(event);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "live_plot_rgb_splines(45, 46, 44, spline_s=0.08, max_stdev=1.0, live=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 169, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def plot_rgb_foo(data_rgb, ids_rgb, spline_s=1):\n", + " fig, ax = plt.subplots(1, 1)\n", + " fig.suptitle('Runs {}(R), {}(G), {}(B) at {:%y-%m-%d %H:%M:%S}'.format(*ids_rgb, datetime.now()))\n", + "\n", + " colors = [\n", + " ((1,0,0), (1,0.8,0.8)),\n", + " ((0,1,0), (0.8,1,0.8)),\n", + " ((0,0,1), (0.8,0.8,1))\n", + " ]\n", + " for (steps, values, stdev), (color_dark, color_bright) in zip(data_rgb, colors):\n", + " ax.errorbar(steps, values, yerr=stdev, color=color_bright)\n", + " \n", + " spline = inter.UnivariateSpline(steps, values, s=spline_s)\n", + " ax.plot(steps, spline(steps), color=color_dark)" + ] + }, + { + "cell_type": "code", + "execution_count": 171, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Poly for run 45: 2\n", + "2.282e-06 x - 0.001576 x + 0.4019\n", + "Poly for run 46: 2\n", + "6.886e-07 x - 0.0005388 x + 0.1561\n", + "Poly for run 44: 2\n", + "1.258e-06 x - 0.001514 x + 0.5252\n" + ] + }, + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support.' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " this.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width);\n", + " canvas.attr('height', height);\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
')\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " event.shiftKey = false;\n", + " // Send a \"J\" for go to next cell\n", + " event.which = 74;\n", + " event.keyCode = 74;\n", + " manager.command_mode();\n", + " manager.handle_keydown(event);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "(380, 720)" + ] + }, + "execution_count": 226, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "λ_sfh2701 = [ 300, 400, 500, 600, 700, 800, 900, 1000, 1100]\n", + "S_sfh2701 = [0.00, 0.20, 0.57, 0.76, 0.90, 1.00, 0.85, 0.37, 0.00]\n", + "Λ_sfh2701 = np.poly1d(np.polyfit(λ_sfh2701, S_sfh2701, 5))\n", + "r = np.arange(380, 720)\n", + "fig, ax = plt.subplots(1, 1)\n", + "ax.plot(r, Λ_sfh2701(r))\n", + "ax.set_xlim([380, 720])" + ] + }, + { + "cell_type": "code", + "execution_count": 257, + "metadata": { + "collapsed": false, + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Poly for run 45:\n", + " 2\n", + "2.282e-06 x - 0.001576 x + 0.4019\n", + "Poly for run 46:\n", + " 2\n", + "6.886e-07 x - 0.0005388 x + 0.1561\n", + "Poly for run 44:\n", + " 2\n", + "1.258e-06 x - 0.001514 x + 0.5252\n", + "[297, 228, 117] [339, 270, 210]\n", + "[330, 228, 159]\n" + ] + }, + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support.' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " this.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width);\n", + " canvas.attr('height', height);\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
')\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " event.shiftKey = false;\n", + " // Send a \"J\" for go to next cell\n", + " event.which = 74;\n", + " event.keyCode = 74;\n", + " manager.command_mode();\n", + " manager.handle_keydown(event);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ids = (45, 46, 44) # Run IDs of runs for R, G and B channel\n", + "\n", + "# Approximate bands of interest for R, G and B channelsfor offset and stray light correction.\n", + "bands = [(260,410), (150,330), (100,260)] # [step]\n", + "\n", + "# The wavelengths are from a random RGB LED datasheet and are just preliminary starting values.\n", + "# https://www.sparkfun.com/datasheets/Components/YSL-R596CR3G4B5C-C10.pdf\n", + "λ_led = [623, 518, 466] # [nm] Assumed wavelengths of R, G and B spectral peaks.\n", + "λ_be = 400 # [nm] Approximate short-λ edge of blue band\n", + "y_edge_min = 0.5\n", + "\n", + "poly_degree = 2 # degree of polynomial for stray light and offset correction. Should be 1 or 2.\n", + "\n", + "max_stdev = 1.0 # [V] Nerved value that has been used for outlier removal in earlier tries\n", + "\n", + "remove_thresh = 0.05 # [V] standard deviation delta threshold for outlier removal\n", + "\n", + "# ---\n", + "data_rgb = []\n", + "for run_id, (l, r) in zip(ids, bands):\n", + " # Load this channel from the database\n", + " steps, values, stdev = load_run_zero_cal(run_id, max_stdev)\n", + " \n", + " # Remove outlier values whose standard deviation is much larger than that of their right and left neighbors\n", + " idxs = (np.abs(stdev[1:-1] - stdev[0:-2]) < remove_thresh) |\\\n", + " (np.abs(stdev[1:-1] - stdev[2:]) < remove_thresh)\n", + " idxs = np.hstack([np.array([True]), idxs, np.array([True])])\n", + " steps, values, stdev = steps[idxs], values[idxs], stdev[idxs]\n", + " \n", + " # Remove offset and stray light by fitting a second-order polynomial over the parts of the curve\n", + " # that are clearly *not* part of the primary peak.\n", + " idxs = (steps < l) | (steps > r)\n", + " poly = np.poly1d(np.polyfit(steps[idxs], values[idxs], poly_degree))\n", + " print('Poly for run {}:'.format(run_id))\n", + " print(poly)\n", + " values -= poly(steps)\n", + " \n", + " data_rgb.append((steps, values, stdev))\n", + "\n", + "\n", + "# Calibrate wavelength axis using assumed peaks for r, g and b. Use least-squares polyfit for getting coefficients.\n", + "peaks = [ x[np.argmax(y)] for x, y, σ2 in data_rgb ]\n", + "edgesl = [ x[np.argmax(y > y_edge_min)] for x, y, σ2 in data_rgb ]\n", + "print(edgesl, peaks)\n", + "\n", + "Λ_est = np.poly1d(np.polyfit([edgesl[2], peaks[0]], [λ_be, λ_led[0]], 1))\n", + "\n", + "data_tmp = [ (x, Λ_est(x), y, σ2) for x, y, σ2 in data_rgb ]\n", + "data_tmp = [ (x, λ, y/Λ_sfh2701(λ), σ2) for x, λ, y, σ2 in data_tmp ]\n", + "# Limit wavelength range\n", + "data_tmp = [ (x[λ > 380], λ[λ > 380], y[λ > 380], σ2[λ > 380]) for x, λ, y, σ2 in data_tmp ]\n", + "peaks = [ x[np.argmax(y)] for x, λ, y, σ2 in data_tmp ]\n", + "Λ = np.poly1d(np.polyfit(peaks, λ_led, 1))\n", + "\n", + "data_rgb = [ (Λ(x), y, σ2) for x, y, σ2 in data_rgb ]\n", + "data_rgb = [ (λ, y/Λ_sfh2701(λ), σ2) for λ, y, σ2 in data_rgb ]\n", + "\n", + "# Limit wavelength range to slightly-larger-than visible range. We're getting improbably large values in the\n", + "# utraviolet region that are probably caused by stray light.\n", + "data_rgb = [ (λ[λ > 380], y[λ > 380], σ2[λ > 380]) for λ, y, σ2 in data_rgb ]\n", + "\n", + "# Normalize amplitude data to brightest channel for ease of reading\n", + "max_val = max(np.max(y) for λ, y, σ2 in data_rgb)\n", + "data_rgb = [ (λ, y/max_val, σ2/max_val) for λ, y, σ2 in data_rgb ]\n", + "\n", + "plot_rgb_bar(data_rgb, ids, spline_s=0.005)" + ] } ], "metadata": { diff --git a/firmware/measure_spectrum.py b/firmware/measure_spectrum.py index 0e7dc22..471d1a8 100644 --- a/firmware/measure_spectrum.py +++ b/firmware/measure_spectrum.py @@ -14,10 +14,12 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('run_name', nargs='?', default='auto') parser.add_argument('buspirate_port', nargs='?', default='/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AD01W1RF-if00-port0') - parser.add_argument('-s', '--steps', type=int, nargs='?', default=100, help='Steps to run through') + parser.add_argument('-s', '--steps', type=int, nargs='?', default=400, help='Steps to run through') + parser.add_argument('-k', '--skip', type=int, nargs='?', default=2, help='Steps skip between measurements for shorter runtime') parser.add_argument('-d', '--database', default='spectra.sqlite3', help='sqlite3 database file to store results in') - parser.add_argument('-w', '--wait', type=float, default=0.1, help='time to wait between samples in seconds') - parser.add_argument('-o', '--oversample', type=int, default=16, help='oversampling ratio') + parser.add_argument('-w', '--wait', type=float, default=2.0, help='time to wait between samples in seconds') + parser.add_argument('-o', '--oversample', type=int, default=32, help='oversampling ratio') + parser.add_argument('-c', '--comment', help='run comment') args = parser.parse_args() db = sqlite3.connect(args.database) @@ -80,8 +82,8 @@ if __name__ == '__main__': run_name += str(1+max(int(n) if str.isnumeric(n) else 0 for n in names)) with db: cur = db.cursor() - cur.execute('INSERT INTO runs(name, timestamp) VALUES (?, ?)', - (run_name, time.time())) + cur.execute('INSERT INTO runs(name, comment, timestamp) VALUES (?, ?, ?)', + (run_name, args.comment, time.time())) run_id = cur.lastrowid print('Starting run {} "{}" at {:%y-%m-%d %H:%M:%S:%f}'.format(run_id, run_name, datetime.now())) @@ -92,8 +94,7 @@ if __name__ == '__main__': bp.step() bp.stepper_direction('up') - for step in range(args.steps): - bp.step() + for step in range(0, args.steps+args.skip, args.skip): # Run one skip past end to capture both interval boundaries for led_val in [0, 1]: try: bp.led(led_val) @@ -115,8 +116,10 @@ if __name__ == '__main__': raise except TypeError as e: print('Buspirate hiccup, ignoring:', e) + for _ in range(args.skip): + bp.step() bp.stepper_direction('down') - for _ in range(args.steps): + for _ in range(args.steps+args.skip): bp.step() diff --git a/firmware/spectra.sqlite3 b/firmware/spectra.sqlite3 index c82c2fed2559b4dd581d449dd9d7599645a85826..09e23c22bfcf741ae2aa92d3c47dedf93f05c2ec 100644 GIT binary patch delta 85067 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.A|`i8&Dpb|zW%xC0??kJyoPvgGo>}) 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 E0FusbeEgG1NIz|qC xzUK@eb(47%;-E5Hn_ua*ztUse{z{KY>Laro>zv8#3B4N&Ias&PQDsiy0sz^xASwU= diff --git a/firmware/spectrum_progress.py b/firmware/spectrum_progress.py index a4eb4b2..35a9d65 100644 --- a/firmware/spectrum_progress.py +++ b/firmware/spectrum_progress.py @@ -17,7 +17,7 @@ if __name__ == '__main__': step, = db.execute( 'SELECT MAX(step) FROM measurements WHERE run_id = (SELECT MAX(run_id) FROM runs)' ).fetchone() - return int(step)+1 + return int(step) def step_gen(): while True: @@ -28,5 +28,9 @@ if __name__ == '__main__': time.sleep(args.update_delay) bar = tqdm.tqdm(total=args.max_step) - for step in step_gen(): - bar.update(step - bar.n) + while True: + try: + for step in step_gen(): + bar.update(step - bar.n) + except: + time.sleep(args.update_delay) diff --git a/firmware/stepper_test.py b/firmware/stepper_test.py index 919fc02..23486fb 100644 --- a/firmware/stepper_test.py +++ b/firmware/stepper_test.py @@ -27,27 +27,32 @@ if __name__ == '__main__': def stepper_step(): bp.cs = 1 - time.sleep(0.005) + #time.sleep(0.005) bp.cs = 0 - time.sleep(0.005) + #time.sleep(0.005) import curses screen = curses.initscr() curses.noecho() curses.cbreak() screen.keypad(True) + i = 0 try: while True: key = screen.getch() if key == ord('q'): break + screen.addstr('{: 4}'.format(i)) + if key == curses.KEY_DOWN: stepper_direction_down() stepper_step() + i -= 1 elif key == curses.KEY_UP: stepper_direction_up() stepper_step() + i += 1 finally: curses.nocbreak()