├── .gitignore ├── LICENSE ├── README.md ├── ardw-app ├── .idea │ └── ardw-app.iml ├── README.md ├── application.py ├── boardgeometry │ ├── __init__.py │ ├── geometry.py │ ├── geometry_sympy.py │ └── hitscan.py ├── clean_log.sh ├── config_duo.ini ├── config_redboard.ini ├── config_sounddetector.ini ├── config_uno.ini ├── data │ ├── arduino_Uno_Rev3-02-TH.svg │ ├── pcbdata.json │ └── schdata.json ├── data_due │ ├── DUE_V02g.svg │ ├── DUE_V02g_1-DUE_V02g_1.svg │ ├── DUE_V02g_2-DUE_V02g_2.svg │ ├── pcbdata.json │ └── schdata.json ├── data_olinuxino │ ├── A64-OlinuXino_Rev_G.svg │ ├── NAND Flash , eMMC, T-Card and Audio-NAND Flash , eMMC, T-Card and Audio.svg │ ├── Power Supply, Extensions and MiPi-DSI -Power Supply, Extensions and MiPi-DSI.svg │ ├── USB&HDMI,WiFi&BT,Ethernet,LCD-USB&HDMI,WiFi&BT,Ethernet,LCD.svg │ ├── pcbdata.json │ └── schdata.json ├── data_redboard │ ├── SparkFun RED-V RedBoard.svg │ ├── SparkFun_RED-V_RedBoard_1-SparkFun RED-V RedBoard_1.svg │ ├── SparkFun_RED-V_RedBoard_2-SparkFun RED-V RedBoard_2.svg │ ├── pcbdata.json │ └── schdata.json ├── data_sound_detector │ ├── pcbdata.json │ ├── schdata.json │ └── sound-detector-v11.svg ├── data_uno │ ├── arduino_Uno_Rev3-02-TH.svg │ ├── pcbdata.json │ └── schdata.json ├── example_tool.py ├── instrumentscripts │ ├── DMM6500 │ │ ├── css │ │ │ └── front_panel_styles.css │ │ ├── front_panel.html │ │ ├── images │ │ │ └── fp_background.svg │ │ └── script │ │ │ ├── ajax_inc.js │ │ │ ├── tga_decode.js │ │ │ └── vfplib.js │ ├── __init__.py │ ├── scpi_read.py │ ├── scpi_read_classes.py │ ├── scpi_read_flask.py │ └── templates │ │ └── instrument_panel.html ├── refresh_python_modules.sh ├── requirements.txt ├── save_python_modules.sh ├── static │ ├── capture.wav │ ├── favicon.ico │ ├── index.js │ ├── main.js │ ├── projector.js │ ├── render.js │ ├── selection.js │ ├── socket.io.min.js │ ├── split.min.js │ ├── study.js │ ├── style.css │ ├── tool-test.js │ ├── util.js │ ├── win7fail.wav │ └── win7success.wav ├── templates │ ├── index.html │ ├── instrument_panel.html │ ├── main.html │ ├── projector.html │ ├── study.html │ └── tool-test.html └── tools.py ├── ardw-plugin ├── __init__.py ├── __main__.py ├── ardw_action.py ├── clear_pyc.sh ├── core │ ├── __init__.py │ ├── sch_reader.py │ ├── sexpdata.py │ └── svg_fix.py ├── ibom │ ├── __init__.py │ ├── common.py │ ├── core │ │ ├── __init__.py │ │ ├── config.py │ │ ├── fontparser.py │ │ ├── newstroke_font.py │ │ └── units.py │ ├── kicad_extra │ │ ├── __init__.py │ │ ├── netlistparser.py │ │ ├── parser_base.py │ │ ├── sexpressions.py │ │ └── xmlparser.py │ ├── pcbparser.py │ └── svgpath.py └── icon.png ├── cv └── projector_calibration.py ├── learning ├── calibration │ ├── calibrate.py │ ├── grey_calib.pkl │ └── red_calib.pkl ├── preprocessing │ └── step1_extract_coordinates.py ├── requirements.txt ├── settings.py └── utils.py └── pyboard ├── .idea ├── misc.xml ├── modules.xml ├── pyboard.iml ├── vcs.xml └── workspace.xml ├── NatNetClient.py ├── capturedWorldPoints.pkl ├── checkerBoard_images ├── checkerBoardWithCircle00.png ├── checkerBoardWithCircle01.jpg ├── checkerBoardWithCircle01.png ├── checkerBoardWithCircle02.jpg ├── checkerBoardWithCircle02.png ├── checkerBoardWithCircle03.jpg ├── checkerBoardWithCircle03.png ├── checkerBoardWithCircle10.jpg ├── checkerBoardWithCircle10.png ├── checkerBoardWithCircle11.jpg ├── checkerBoardWithCircle11.png ├── checkerBoardWithCircle12.jpg ├── checkerBoardWithCircle12.png ├── checkerBoardWithCircle13.jpg ├── checkerBoardWithCircle13.png ├── checkerBoardWithCircle20.jpg ├── checkerBoardWithCircle20.png ├── checkerBoardWithCircle21.jpg ├── checkerBoardWithCircle21.png ├── checkerBoardWithCircle22.jpg ├── checkerBoardWithCircle22.png ├── checkerBoardWithCircle23.jpg ├── checkerBoardWithCircle23.png ├── checkerBoardWithCircle30.jpg ├── checkerBoardWithCircle30.png ├── checkerBoardWithCircle31.jpg ├── checkerBoardWithCircle31.png ├── checkerBoardWithCircle32.jpg ├── checkerBoardWithCircle32.png ├── checkerBoardWithCircle33.jpg ├── checkerBoardWithCircle33.png ├── checkerBoardWithCircle40.jpg ├── checkerBoardWithCircle40.png ├── checkerBoardWithCircle41.jpg ├── checkerBoardWithCircle41.png ├── checkerBoardWithCircle42.jpg ├── checkerBoardWithCircle42.png ├── checkerBoardWithCircle43.jpg └── checkerBoardWithCircle43.png ├── collect_data.py ├── controller_lib.py ├── extract_opti_cordinate.py ├── imagePoints.pkl ├── opti_lib.py ├── projectionMatrix.pkl ├── projector_calib.py ├── projector_calibration.py ├── prt_natnet.py ├── settings.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Node 132 | node_modules/ 133 | .DS_Store 134 | 135 | # VS Code 136 | .vscode/ 137 | 138 | # Jetlabs 139 | .idea/ 140 | ardw-app/.idea/ 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ar-debug-workbench 2 | Augmented reality workbench for printed circuit board (PCB) debugging. 3 | 4 | For more details, please see open-access publication: 5 | Ishan Chatterjee, Tadeusz Pforte, Aspen Tng, Farshid Salemi Parizi, Chaoran Chen, and Shwetak Patel. 2022. ARDW: An Augmented Reality Workbench for Printed Circuit Board Debugging. In Proceedings of the 35th Annual ACM Symposium on User Interface Software and Technology (UIST '22). Association for Computing Machinery, New York, NY, USA, Article 37, 1–16. https://doi.org/10.1145/3526113.3545684 6 | 7 | /ardw-plugin/ has the code for the KiCAD 5.X action plugin. To enable it, place the folder in the appropriate /scripting/plugins/ directory (see [IBOM's installation instructions](https://github.com/openscopeproject/InteractiveHtmlBom/wiki/Installation) for details). To use it, open a project in EESchema and File>Plot all pages as svg into the folder ./ardw/ (must create the first time). Then, open the project in pcbnew and click the icon on the toolbar. Two json files will show up in the /ardw/ folder and the svgs will be updated. Copying these files to /ardw-app/data/ will allow the app to run with them. 8 | 9 | /ardw-app/ has the code for the web application. The first time, you must run 'export FLASK_APP=application'. Any time you pull, you also need to refresh the python modules. If using a virtual environment (recommended), run './refresh_python_modules.sh'. Otherwise, manually install the necessary modules with 'pip install -r requirements.txt'. Now, the server can be started with 'python application.py' and accessed at http://localhost:5000 (NOTE: 'flask run' *will not* work anymore, because it does not support sockets). Optionally, you can specify a different port as the next argument (eg. 'python application.py 3000'), or change the default port in 'config.ini'. 10 | 11 | To compile the app into a unix executable, run 'pyinstaller -w -F --add-data "templates:templates" --add-data "static:static" application.py' from /ardw-app/. Ideally this is done from a virtual environment (or with as few modules as possible; see /ardw-app/requirements.txt), to keep the file size small. The executable 'application' will appear in /ardw-app/dist/ and can be run from the terminal (with a populated data/ folder in the same directory). 12 | -------------------------------------------------------------------------------- /ardw-app/.idea/ardw-app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /ardw-app/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This is a quick guide to the web and projector application. 4 | 5 | The application runs as a flask server from the file `application.py`, which is the starting point for everything. The server communicates with the client via socketio after the initial GET requests, and listens on UDP port 8052 for optitrack data. 6 | 7 | There are two main client pages, `main` and `projector`. `main` refers to the web application, while `projector` refers to the page that is intended to be fullscreened and projected onto the board. The html files for both are found in the `templates/` folder. Both files share several js files (`util.js`, `selection.js`, and `render.js`), but also have their own "primary" file that specifies unique behavior (`main.js` and `projector.js` respectively). All these files are found in the `static/` folder, along with the only css file (`style.css`). 8 | 9 | ## Rendering 10 | 11 | Rendering is done differently for the schematic and the layout. The schematic is rendered as a two-layer HTML5 canvas: one background layer, which only contains the svg of the current schematic sheet, and one highlight layer, on which selection highlights, crosshairs, tooltips, and any other annotations are drawn. The primary function for (re-)drawing the schematic highlight layer is `drawSchematicHighlights()` in `render.js`. Note that `drawSchematicHighlights()` needs to be called again whenever you want to update the canvas (for example to update the position of a pointer). 12 | 13 | The layout is rendered as two separate four-layer HTML5 canvases, one for the front and one for the back. The four layers are background, fab, silkscreen, and highlight. The first three should not be modified directly. Like the schematic, selection highlights and all other annotations are drawn on the highlight layers. This happens for each side individually in `drawHighlightsOnLayer()`, which is where you should add any additional elements you would like to draw. `drawHighlights()` is a wrapper function that then calls `drawHighlightsOnLayer()` for both the front and back layout, and should be called whenever you want to update the canvas. 14 | 15 | ## Communication between Server and Client 16 | 17 | The server and web clients communicate using socketio, a library that simplifies a TCP connection into messages consisting of a string label and a dictionary/object of data (python/js respctively). For example, an example selection message has label `"selection"` and data `{"type": "comp", "val": 140}`. Note that `data` can be a layered dictionary, so you can send whatever you want. To send a message from the server, call `socketio.emit(label, data)`. To send a message from the client, call `socket.emit(label, data);`. 18 | 19 | Receiving messages is a little more complicated. The server receives messages using decorated functions: 20 | 21 | @socketio.on("label") 22 | def handle_message(data): 23 | 24 | 25 | This decorated function runs whever the server receives a message from a client with the given label. 26 | 27 | Clients receive messages using callbacks: 28 | 29 | socket.on("label", (data) => { 30 | 31 | }); 32 | 33 | This function runs whenever the client receives a message from the server with the given label. All the socket callbacks are kept together in the primary file of each page (eg. `main.js`) in a function called `initSocket()`. 34 | -------------------------------------------------------------------------------- /ardw-app/boardgeometry/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-app/boardgeometry/__init__.py -------------------------------------------------------------------------------- /ardw-app/boardgeometry/geometry.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import numpy as np 3 | from shapely.geometry import Point, Polygon, box 4 | 5 | 6 | def get_pad_polygon(pad, cache=True): 7 | # TODO ibom caches this, but probably unnecessary 8 | if cache and "poly" in pad: 9 | return pad["poly"] 10 | 11 | if pad["shape"] == "rect": 12 | poly = get_rect(pad["size"]) 13 | elif pad["shape"] == "oval": 14 | poly = get_oblong(pad["size"]) 15 | elif pad["shape"] == "circle": 16 | poly = get_circle(pad["size"][0] / 2) 17 | elif pad["shape"] == "roundrect": 18 | poly = get_chamfered_rect(pad["size"], pad["radius"]) 19 | elif pad["shape"] == "chamfrect": 20 | poly = get_chamfered_rect( 21 | pad["size"], pad["radius"], pad["chamfpos"], pad["chamfratio"]) 22 | else: # "custom" 23 | logging.error("Custom pads currently not supported") 24 | #poly = get_poly(pad) 25 | 26 | if cache: 27 | pad["poly"] = poly 28 | return poly 29 | 30 | 31 | def get_rect(size): 32 | return box(-size[0] / 2, -size[1] / 2, size[0] / 2, size[1] / 2) 33 | 34 | 35 | def get_oblong(size): 36 | return get_chamfered_rect(size, min(*size) / 2) 37 | 38 | 39 | def get_circle(radius): 40 | return Point(0, 0).buffer(radius) 41 | 42 | 43 | # approximates a given arc with number of points 44 | def get_arc(x, y, radius, start_deg, end_deg, precision=50): 45 | theta = np.radians(np.linspace(start_deg, end_deg, precision)) 46 | xs = x + radius * np.cos(theta) 47 | ys = y + radius * np.sin(theta) 48 | return np.column_stack([xs, ys]) 49 | 50 | 51 | # handles chamfered and rounded rectangles 52 | # derived from ibom code 53 | def get_chamfered_rect(size, radius, chamfpos=0, chamfratio=0): 54 | # from ibom: chamfpos is a bitmask, left = 1, right = 2, bottom left = 4, bottom right = 8 55 | # recall that ibom is in render reference frame, ie. bottom is pos y 56 | width = size[0] 57 | height = size[1] 58 | x = -width / 2 59 | y = -height / 2 60 | offset = min(width, height) * chamfratio 61 | 62 | points = np.array([(x, 0)]) 63 | if chamfpos & 4: 64 | points = np.append( 65 | points, 66 | [(x, y + height - offset), (x + offset, y + height)], 67 | axis=0 68 | ) 69 | else: 70 | points = np.append( 71 | points, 72 | get_arc(x + radius, y + height - radius, radius, 180, 90), 73 | axis=0 74 | ) 75 | 76 | if chamfpos & 8: 77 | points = np.append( 78 | points, 79 | [(x + width - offset, y + height), (x + width, y + height - offset)], 80 | axis=0 81 | ) 82 | else: 83 | points = np.append( 84 | points, 85 | get_arc(x + width - radius, y + height - radius, radius, 90, 0), 86 | axis=0 87 | ) 88 | 89 | if chamfpos & 2: 90 | points = np.append( 91 | points, 92 | [(x + width, y + offset), (x + width - offset, y)], 93 | axis=0 94 | ) 95 | else: 96 | points = np.append( 97 | points, 98 | get_arc(x + width - radius, y + radius, radius, 0, -90), 99 | axis=0 100 | ) 101 | 102 | if chamfpos & 1: 103 | points = np.append( 104 | points, 105 | [(x + offset, y), (x, y + offset)], 106 | axis=0 107 | ) 108 | else: 109 | points = np.append( 110 | points, 111 | get_arc(x + radius, y + radius, radius, 270, 180), 112 | axis=0 113 | ) 114 | 115 | return Polygon(points).convex_hull 116 | 117 | 118 | def get_poly(pad): 119 | raise NotImplementedError 120 | -------------------------------------------------------------------------------- /ardw-app/boardgeometry/geometry_sympy.py: -------------------------------------------------------------------------------- 1 | from sympy import Point, Circle, Polygon 2 | 3 | # DEPRECATED 4 | 5 | def get_pad_polygon(pad): 6 | # TODO ibom caches this, but probably unnecessary 7 | if pad["shape"] == "rect": 8 | poly = get_rect(pad["size"]) 9 | elif pad["shape"] == "oval": 10 | poly = get_oblong(pad["size"]) 11 | elif pad["shape"] == "circle": 12 | poly = get_circle(pad["size"][0] / 2) 13 | elif pad["shape"] == "roundrect": 14 | poly = get_chamfered_rect(pad["size"], pad["radius"], 0, 0) 15 | elif pad["shape"] == "chamfrect": 16 | poly = get_chamfered_rect(pad["size"], pad["radius"], pad["chamfpos"], pad["chamfratio"]) 17 | else: # "custom" 18 | poly = get_poly(pad) 19 | return poly 20 | 21 | def get_rect(size): 22 | return Polygon(Point(-size[0] / 2, -size[1] / 2), 23 | Point(-size[0] / 2, size[1] / 2), 24 | Point(size[0] / 2, size[1] / 2), 25 | Point(size[0] / 2, -size[1] / 2)) 26 | 27 | def get_oblong(size): 28 | return get_chamfered_rect(size, min(*size) / 2, 0, 0) 29 | 30 | def get_circle(radius): 31 | return Circle(Point(0, 0), radius) 32 | 33 | def get_chamfered_rect(size, radius, chamfpos, chamfratio): 34 | # from ibom: chamfpos is a bitmask, left = 1, right = 2, bottom left = 4, bottom right = 8 35 | return 36 | 37 | 38 | class ChamferedRect: 39 | def __init__(self, size, radius, chamfpos, chamfratio): 40 | self.rect = get_rect(size) -------------------------------------------------------------------------------- /ardw-app/boardgeometry/hitscan.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from shapely.geometry import Point 3 | 4 | from boardgeometry.geometry import get_pad_polygon 5 | 6 | 7 | def deg2rad(deg): 8 | return deg * np.pi / 180. 9 | 10 | 11 | def rotate_vec(v, angle_deg): 12 | angle_rad = deg2rad(angle_deg) 13 | return (v[0] * np.cos(angle_rad) - v[1] * np.sin(angle_rad), 14 | v[0] * np.sin(angle_rad) + v[1] * np.cos(angle_rad)) 15 | 16 | 17 | def point_in_footprint(x, y, footprint, padding=0): 18 | bbox = footprint["bbox"] 19 | v = (x - bbox["pos"][0], y - bbox["pos"][1]) 20 | v = rotate_vec(v, bbox["angle"]) 21 | return bbox["relpos"][0] - padding <= v[0] <= bbox["relpos"][0] + bbox["size"][0] + padding and \ 22 | bbox["relpos"][1] - padding <= v[1] <= bbox["relpos"][1] + bbox["size"][1] + padding 23 | 24 | 25 | def point_in_pad(x, y, pad, padding=0): 26 | v = [x - pad["pos"][0], y - pad["pos"][1]] 27 | # TODO figure out why in js this is neg but footprint angle is pos 28 | v = rotate_vec(v, -pad["angle"]) 29 | if "offset" in pad: 30 | v = (v[0] - pad["offset"][0], v[1] - pad["offset"][1]) 31 | return Point(*v).within(get_pad_polygon(pad).buffer(padding)) 32 | 33 | 34 | # from ibom, don't know what exactly it does 35 | def point_within_dist_to_arc(x, y, xc, yc, radius, start_deg, end_deg, d): 36 | dx = x - xc 37 | dy = y - yc 38 | r_sq = dx * dx + dy * dy 39 | rmin = max(0, radius - d) 40 | rmax = radius + d 41 | 42 | if r_sq < rmin * rmin or r_sq > rmax * rmax: 43 | return False 44 | 45 | angle1 = deg2rad(start_deg) % (2 * np.pi) 46 | dx1 = xc + radius * np.cos(angle1) - x 47 | dy1 = yc + radius * np.sin(angle1) - y 48 | if dx1 * dx1 + dy1 * dy1 <= d * d: 49 | return True 50 | 51 | angle2 = deg2rad(end_deg) % (2 * np.pi) 52 | dx2 = xc + radius * np.cos(angle2) - x 53 | dy2 = yc + radius * np.sin(angle2) - y 54 | if dx2 * dx2 + dy2 * dy2 <= d * d: 55 | return True 56 | 57 | angle = np.arctan2(dy, dx) % (2 * np.pi) 58 | if angle1 > angle2: 59 | return angle >= angle2 or angle <= angle1 60 | else: 61 | return angle >= angle1 and angle <= angle2 62 | 63 | 64 | # from ibom, don't know what exactly it does 65 | def point_within_dist_to_seg(x, y, x1, y1, x2, y2, d): 66 | a = x - x1 67 | b = y - y1 68 | c = x2 - x1 69 | d = y2 - y1 70 | 71 | dot = a * c + b * d 72 | len_sq = c * c + d * d 73 | if len_sq == 0: 74 | dx = x - x1 75 | dy = y - y1 76 | else: 77 | param = dot / len_sq 78 | if param < 0: 79 | xx = x1 80 | yy = y1 81 | elif param > 1: 82 | xx = x2 83 | yy = y2 84 | else: 85 | xx = x1 + param * c 86 | yy = y1 + param * d 87 | dx = x - xx 88 | dy = y - yy 89 | return dx * dx + dy * dy <= d * d 90 | 91 | 92 | def bbox_hitscan(x, y, pcbdata, layer=None, padding=0): 93 | result = [] 94 | for i, footprint in enumerate(pcbdata["footprints"]): 95 | if (not layer or layer == footprint["layer"]) \ 96 | and point_in_footprint(x, y, footprint, padding): 97 | result.append(i) 98 | return result 99 | 100 | 101 | def pin_hitscan(x, y, pcbdata, pinref_to_idx, layer=None, render_pads=True, pin_padding=0): 102 | if not render_pads: 103 | return [] 104 | result = [] 105 | for footprint in pcbdata["footprints"]: 106 | for pad in footprint["pads"]: 107 | if (not layer or layer in pad["layers"]) \ 108 | and point_in_pad(x, y, pad, pin_padding): 109 | pin_name = f"{footprint['ref']}.{pad['padname']}" 110 | if pin_name in pinref_to_idx: 111 | result.append(pinref_to_idx[pin_name]) 112 | return result 113 | 114 | 115 | def net_hitscan(x, y, pcbdata, layer=None, render_pads=True, render_tracks=False, pin_padding=0): 116 | nets_hit = set() 117 | if "tracks" in pcbdata and layer and render_tracks: 118 | for track in pcbdata["tracks"][layer]: 119 | if "radius" in track and point_within_dist_to_arc(x, y, *track["center"], track["radius"], 120 | track["startangle"], track["endangle"], 121 | track["width"] / 2): 122 | nets_hit.add(track["net"]) 123 | elif point_within_dist_to_seg(x, y, *track["start"], *track["end"], track["width"] / 2): 124 | nets_hit.add(track["net"]) 125 | if render_pads: 126 | for footprint in pcbdata["footprints"]: 127 | for pad in footprint["pads"]: 128 | if (not layer or layer in pad["layers"]) \ 129 | and point_in_pad(x, y, pad, pin_padding) and "net" in pad: 130 | nets_hit.add(pad["net"]) 131 | 132 | return list(nets_hit) 133 | 134 | 135 | def hitscan(x, y, pcbdata, pinref_to_idx, layer=None, render_pads=True, render_tracks=False, padding=0, 136 | types=None): 137 | if types is None: 138 | types = ["comp", "pin", "net"] 139 | hits = [] 140 | if "comp" in types: 141 | hits += [{"type": "comp", "val": hit} 142 | for hit in bbox_hitscan(x, y, pcbdata, layer, padding)] 143 | if "pin" in types: 144 | hits += [{"type": "pin", "val": hit} 145 | for hit in pin_hitscan(x, y, pcbdata, pinref_to_idx, layer, render_pads, padding)] 146 | if "net" in types: 147 | hits += [{"type": "net", "val": hit} 148 | for hit in net_hitscan(x, y, pcbdata, layer, render_pads, render_tracks, padding)] 149 | return hits 150 | -------------------------------------------------------------------------------- /ardw-app/clean_log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | logfile="$1" 3 | outfile="$2" 4 | 5 | if [ -z "$2" ]; then 6 | echo "Usage: ./clean_log.sh " 7 | exit 1 8 | fi 9 | 10 | > "$outfile" 11 | 12 | while IFS= read -r line; do 13 | echo "$line" >> "$outfile" 14 | done < "$logfile" 15 | 16 | sed -ri '' -e 's/^(.*),.* - INFO - Study (.*)$/\1 \2/' "$outfile" 17 | -------------------------------------------------------------------------------- /ardw-app/config_duo.ini: -------------------------------------------------------------------------------- 1 | [Server] 2 | ; port on which the server runs 3 | Port = 5000 4 | 5 | ; name of the folder containing pcbdata.json, schdata.json, and appropriate svgs 6 | DataFolder = data_due 7 | ; data_uno 8 | ; data_due 9 | ; data_redboard 10 | ; data_sound_detector 11 | 12 | ; IP at which the server listens for optitrack UDP 13 | UDPAddress = 127.0.0.1 14 | 15 | ; port on which the server listens for optitrack UDP 16 | UDPPort = 8052 17 | 18 | ; optitrack UDP packet size in bytes 19 | UDPPacketSize = 1024 20 | 21 | ; fps that the server listens for UDP at 22 | ; should be greater than Optitrack fps so we can catch up if we fall behind 23 | UDPFramerate = 60 24 | 25 | 26 | [Optitrack] 27 | ; time the tip or end must be stationary to trigger a dwell event, in seconds 28 | DwellTime = 0.5 29 | DwellTimeEnd = 0.5 30 | 31 | ; radius from mean position since DwellTime where probe tip/end must stay to be dwelling, in pixels 32 | DwellRadiusTip = 5 33 | DwellRadiusEnd = 10 34 | 35 | ; radius of "stable zone" where probe tip must be to stay within disambiguation menu, in pixels 36 | MultiAnchorRadius = 20 37 | 38 | ; sensitivity of disambiguation menu dot (how far probe end must move to move the dot) 39 | ; technically used as the height of a row of the linear disambiguation menu, in pixels 40 | MultiMenuSensitivity = 40 41 | 42 | ; horizontal buffer around the edge cuts that the probe needs to leave before it can reselect, in pixels 43 | ReselectHorizontalBuffer = 20 44 | 45 | ; maximum height value of the safe zone that the probe needs to leave before it can reselect, in real mm 46 | ReselectVerticalMaximum = 21 47 | 48 | ; minimum height value of the safe zone, in real mm 49 | ReselectVerticalMinimum = 0 50 | 51 | ; maximum height value where a deselection event will be registered outside the edge cuts, in real mm 52 | OutsideVerticalBuffer = 5 53 | 54 | ; hitbox padding around components and pins for board selection, in mm 55 | PinPadding = 1.0 56 | 57 | 58 | [Rendering] 59 | ; note: colors can be a name, eg. purple; RGB, eg. #800080; or RGBA, eg. rgba(128, 0, 128, 0.5) 60 | 61 | ; colors for pad and track highlighting on projector 62 | PadColorHighlight = #00FF00 63 | TrackColorHighlight = #00FF00 64 | 65 | ; colors for the probe tip dots/crosshair on the projector/layout 66 | ProbeDotColor = green 67 | DmmPosDotColor = green 68 | DmmNegDotColor = purple 69 | OscDotColor = blue 70 | 71 | ; colors for the probe selection highlights on the projector/layout 72 | ProbeSelectionColor = green 73 | DmmPosSelectionColor = green 74 | DmmNegSelectionColor = purple 75 | OscSelectionColor = blue 76 | 77 | 78 | [Dev] 79 | ; if True, start with probe and dmm connected rather than setting up connection through web interface 80 | AutoconnectTools = True 81 | 82 | ; iff True, projector will track the board constantly rather than sitting in a fixed location 83 | ; if False, you can still do a one-time board track using the "Jump to Board" settings button 84 | TrackBoard = False 85 | 86 | ; iff True, projector will track the board rotation, not just translation 87 | ; this also applies to the "Jump to Board" button 88 | TrackBoardRotation = False 89 | 90 | ; default values of projector calibration 91 | DefaultTX = 0 92 | DefaultTY = 0 93 | DefaultRotation = 0 94 | DefaultZoom = 3.93 95 | 96 | 97 | [Study] 98 | ; list of component names to use in the study (comma-separated) 99 | ComponentList = C1, C2, C3, C4, C5, C7, C8, C9, C11, D2, D3, F1, RN1, RN2, T1, U2, U5, Y2, Z1, Z2 100 | 101 | ; list of component names to be used in the practice set 102 | PracticeList = ZU4, U1, U3, TX1, RX1 103 | 104 | ; list of net names to use in study (comma-separated) 105 | BringupList = MASTER-RESET, +3V3, /DUE_V02g_1/VIN+, +5V, /DUE_V02g_2/VDDANA, /DUE_V02g_2/VDDOUTMI, /DUE_V02g_2/VDDOUT, /DUE_V02g_2/VDDPLL 106 | 107 | ; list of bounds corresponding to the nets of BringupList (comma-separated) 108 | ; ie. the bounds of the ith net are 2i (low) and 2i+1 (high) 109 | BringupBounds = 3.201,3.465 , 3.201,3.465 , 6,16 , 4.85,5.25 , 3.201,3.465 , 3.201,3.465 , 1.746,1.89 , 0,0.05 110 | 111 | ; time between instrument panel value updates, in ms 112 | DmmPanelRefreshFrequency = 600 113 | 114 | ; hitbox padding around components for the study 115 | CompPadding = 0.0 116 | 117 | ; name of the board being used (must match a section name to get the right boardpos offset) 118 | ; when calculating the offset values for a new board, use New Board 119 | ; make sure to also set the projector translation to 0 120 | BoardName = Arduino Uno 121 | 122 | 123 | [New Board] 124 | ; offset values for board tracking 125 | BoardposOffsetX = 0 126 | BoardposOffsetY = 0 127 | BoardposOffsetR = 0 128 | BoardposDefaultZ = 1 129 | BoardCenterX = 0 130 | BoardCenterY = 0 131 | 132 | 133 | [Arduino Uno] 134 | ; offset values for board tracking 135 | BoardposOffsetX = 570.9018 136 | BoardposOffsetY = -410.5205 137 | BoardposOffsetR = -86.9076 138 | BoardposDefaultZ = 3.90 139 | BoardCenterX = 148.50 140 | BoardCenterY = 105.00 141 | 142 | 143 | [Arduino Due] 144 | ; offset values for board tracking 145 | BoardposOffsetX = 0 146 | BoardposOffsetY = 0 147 | BoardposOffsetR = 0 148 | BoardposDefaultZ = 1 149 | BoardCenterX = 0 150 | BoardCenterY = 0 151 | 152 | 153 | [Redboard] 154 | ; offset values for board tracking 155 | BoardposOffsetX = 0 156 | BoardposOffsetY = 0 157 | BoardposOffsetR = 0 158 | BoardposDefaultZ = 1 159 | BoardCenterX = 0 160 | BoardCenterY = 0 161 | 162 | 163 | [Sound Detector] 164 | ; offset values for board tracking 165 | BoardposOffsetX = 0 166 | BoardposOffsetY = 0 167 | BoardposOffsetR = 0 168 | BoardposDefaultZ = 1 169 | BoardCenterX = 0 170 | BoardCenterY = 0 171 | -------------------------------------------------------------------------------- /ardw-app/config_redboard.ini: -------------------------------------------------------------------------------- 1 | [Server] 2 | ; port on which the server runs 3 | Port = 5000 4 | 5 | ; name of the folder containing pcbdata.json, schdata.json, and appropriate svgs 6 | DataFolder = data_redboard 7 | 8 | ; IP at which the server listens for optitrack UDP 9 | UDPAddress = 127.0.0.1 10 | 11 | ; port on which the server listens for optitrack UDP 12 | UDPPort = 8052 13 | 14 | ; optitrack UDP packet size in bytes 15 | UDPPacketSize = 1024 16 | 17 | ; fps that the server listens for UDP at 18 | ; should be greater than Optitrack fps so we can catch up if we fall behind 19 | UDPFramerate = 60 20 | 21 | 22 | [Optitrack] 23 | ; time the tip or end must be stationary to trigger a dwell event, in seconds 24 | DwellTime = 0.5 25 | DwellTimeEnd = 0.5 26 | 27 | ; radius from mean position since DwellTime where probe tip/end must stay to be dwelling, in pixels 28 | DwellRadiusTip = 5 29 | DwellRadiusEnd = 10 30 | 31 | ; radius of "stable zone" where probe tip must be to stay within disambiguation menu, in pixels 32 | MultiAnchorRadius = 20 33 | 34 | ; sensitivity of disambiguation menu dot (how far probe end must move to move the dot) 35 | ; technically used as the height of a row of the linear disambiguation menu, in pixels 36 | MultiMenuSensitivity = 40 37 | 38 | ; horizontal buffer around the edge cuts that the probe needs to leave before it can reselect, in pixels 39 | ReselectHorizontalBuffer = 20 40 | 41 | ; maximum height value of the safe zone that the probe needs to leave before it can reselect, in real mm 42 | ReselectVerticalMaximum = 21 43 | 44 | ; minimum height value of the safe zone, in real mm 45 | ReselectVerticalMinimum = 0 46 | 47 | ; maximum height value where a deselection event will be registered outside the edge cuts, in real mm 48 | OutsideVerticalBuffer = 5 49 | 50 | ; hitbox padding around components and pins for board selection, in mm 51 | PinPadding = 1.0 52 | 53 | 54 | [Rendering] 55 | ; note: colors can be a name, eg. purple; RGB, eg. #800080; or RGBA, eg. rgba(128, 0, 128, 0.5) 56 | 57 | ; colors for pad and track highlighting on projector 58 | PadColorHighlight = #00FF00 59 | TrackColorHighlight = #00FF00 60 | 61 | ; colors for the probe tip dots/crosshair on the projector/layout 62 | ProbeDotColor = green 63 | DmmPosDotColor = green 64 | DmmNegDotColor = purple 65 | OscDotColor = blue 66 | 67 | ; colors for the probe selection highlights on the projector/layout 68 | ProbeSelectionColor = green 69 | DmmPosSelectionColor = green 70 | DmmNegSelectionColor = purple 71 | OscSelectionColor = blue 72 | 73 | 74 | [Dev] 75 | ; if True, start with probe and dmm connected rather than setting up connection through web interface 76 | AutoconnectTools = True 77 | 78 | ; iff True, projector will track the board constantly rather than sitting in a fixed location 79 | ; if False, you can still do a one-time board track using the "Jump to Board" settings button 80 | TrackBoard = False 81 | 82 | ; iff True, projector will track the board rotation, not just translation 83 | ; this also applies to the "Jump to Board" button 84 | TrackBoardRotation = False 85 | 86 | ; default values of projector calibration 87 | DefaultTX = 93 88 | DefaultTY = 40 89 | DefaultRotation = 0 90 | DefaultZoom = 3.95 91 | 92 | 93 | [Study] 94 | ; list of component names to use in the study (comma-separated) 95 | ComponentList = C1, C2, C3, C4, C5, C7, C8, C9, C11, D2, D3, F1, RN1, RN2, T1, U2, U5, Y2, Z1, Z2 96 | 97 | ; list of component names to be used in the practice set 98 | PracticeList = ZU4, U1, U3, TX1, RX1 99 | 100 | ; list of net names to use in study (comma-separated) 101 | BringupList = MASTER-RESET, +3V3, /DUE_V02g_1/VIN+, +5V, /DUE_V02g_2/VDDANA, /DUE_V02g_2/VDDOUTMI, /DUE_V02g_2/VDDOUT, /DUE_V02g_2/VDDPLL 102 | 103 | ; list of bounds corresponding to the nets of BringupList (comma-separated) 104 | ; ie. the bounds of the ith net are 2i (low) and 2i+1 (high) 105 | BringupBounds = 3.201,3.465 , 3.201,3.465 , 6,16 , 4.85,5.25 , 3.201,3.465 , 3.201,3.465 , 1.746,1.89 , 0,0.05 106 | 107 | ; time between instrument panel value updates, in ms 108 | DmmPanelRefreshFrequency = 600 109 | 110 | ; hitbox padding around components for the study 111 | CompPadding = 0.0 112 | 113 | ; name of the board being used (must match a section name to get the right boardpos offset) 114 | ; when calculating the offset values for a new board, use New Board 115 | ; make sure to also set the projector translation to 0 116 | BoardName = Arduino Uno 117 | 118 | 119 | [New Board] 120 | ; offset values for board tracking 121 | BoardposOffsetX = 0 122 | BoardposOffsetY = 0 123 | BoardposOffsetR = 0 124 | BoardposDefaultZ = 1 125 | BoardCenterX = 0 126 | BoardCenterY = 0 127 | 128 | 129 | [Arduino Uno] 130 | ; offset values for board tracking 131 | BoardposOffsetX = 570.9018 132 | BoardposOffsetY = -410.5205 133 | BoardposOffsetR = -86.9076 134 | BoardposDefaultZ = 3.90 135 | BoardCenterX = 148.50 136 | BoardCenterY = 105.00 137 | 138 | 139 | [Arduino Due] 140 | ; offset values for board tracking 141 | BoardposOffsetX = 0 142 | BoardposOffsetY = 0 143 | BoardposOffsetR = 0 144 | BoardposDefaultZ = 1 145 | BoardCenterX = 0 146 | BoardCenterY = 0 147 | 148 | 149 | [Redboard] 150 | ; offset values for board tracking 151 | BoardposOffsetX = 0 152 | BoardposOffsetY = 0 153 | BoardposOffsetR = 0 154 | BoardposDefaultZ = 1 155 | BoardCenterX = 0 156 | BoardCenterY = 0 157 | 158 | 159 | [Sound Detector] 160 | ; offset values for board tracking 161 | BoardposOffsetX = 0 162 | BoardposOffsetY = 0 163 | BoardposOffsetR = 0 164 | BoardposDefaultZ = 1 165 | BoardCenterX = 0 166 | BoardCenterY = 0 167 | -------------------------------------------------------------------------------- /ardw-app/config_sounddetector.ini: -------------------------------------------------------------------------------- 1 | [Server] 2 | ; port on which the server runs 3 | Port = 5000 4 | 5 | ; name of the folder containing pcbdata.json, schdata.json, and appropriate svgs 6 | DataFolder = data_sound_detector 7 | 8 | ; IP at which the server listens for optitrack UDP 9 | UDPAddress = 127.0.0.1 10 | 11 | ; port on which the server listens for optitrack UDP 12 | UDPPort = 8052 13 | 14 | ; optitrack UDP packet size in bytes 15 | UDPPacketSize = 1024 16 | 17 | ; fps that the server listens for UDP at 18 | ; should be greater than Optitrack fps so we can catch up if we fall behind 19 | UDPFramerate = 60 20 | 21 | 22 | [Optitrack] 23 | ; time the tip or end must be stationary to trigger a dwell event, in seconds 24 | DwellTime = 0.5 25 | DwellTimeEnd = 0.5 26 | 27 | ; radius from mean position since DwellTime where probe tip/end must stay to be dwelling, in pixels 28 | DwellRadiusTip = 5 29 | DwellRadiusEnd = 10 30 | 31 | ; radius of "stable zone" where probe tip must be to stay within disambiguation menu, in pixels 32 | MultiAnchorRadius = 20 33 | 34 | ; sensitivity of disambiguation menu dot (how far probe end must move to move the dot) 35 | ; technically used as the height of a row of the linear disambiguation menu, in pixels 36 | MultiMenuSensitivity = 40 37 | 38 | ; horizontal buffer around the edge cuts that the probe needs to leave before it can reselect, in pixels 39 | ReselectHorizontalBuffer = 20 40 | 41 | ; maximum height value of the safe zone that the probe needs to leave before it can reselect, in real mm 42 | ReselectVerticalMaximum = 21 43 | 44 | ; minimum height value of the safe zone, in real mm 45 | ReselectVerticalMinimum = 0 46 | 47 | ; maximum height value where a deselection event will be registered outside the edge cuts, in real mm 48 | OutsideVerticalBuffer = 5 49 | 50 | ; hitbox padding around components and pins for board selection, in mm 51 | PinPadding = 1.0 52 | 53 | 54 | [Rendering] 55 | ; note: colors can be a name, eg. purple; RGB, eg. #800080; or RGBA, eg. rgba(128, 0, 128, 0.5) 56 | 57 | ; colors for pad and track highlighting on projector 58 | PadColorHighlight = #00FF00 59 | TrackColorHighlight = #00FF00 60 | 61 | ; colors for the probe tip dots/crosshair on the projector/layout 62 | ProbeDotColor = green 63 | DmmPosDotColor = green 64 | DmmNegDotColor = purple 65 | OscDotColor = blue 66 | 67 | ; colors for the probe selection highlights on the projector/layout 68 | ProbeSelectionColor = green 69 | DmmPosSelectionColor = green 70 | DmmNegSelectionColor = purple 71 | OscSelectionColor = blue 72 | 73 | 74 | [Dev] 75 | ; if True, start with probe and dmm connected rather than setting up connection through web interface 76 | AutoconnectTools = True 77 | 78 | ; iff True, projector will track the board constantly rather than sitting in a fixed location 79 | ; if False, you can still do a one-time board track using the "Jump to Board" settings button 80 | TrackBoard = False 81 | 82 | ; iff True, projector will track the board rotation, not just translation 83 | ; this also applies to the "Jump to Board" button 84 | TrackBoardRotation = False 85 | 86 | ; default values of projector calibration 87 | DefaultTX = 0 88 | DefaultTY = 0 89 | DefaultRotation = 180 90 | DefaultZoom = 3.93 91 | 92 | 93 | [Study] 94 | ; list of component names to use in the study (comma-separated) 95 | ComponentList = C1, C2, C3, C4, C5, C7, C8, C9, C11, D2, D3, F1, RN1, RN2, T1, U2, U5, Y2, Z1, Z2 96 | 97 | ; list of component names to be used in the practice set 98 | PracticeList = ZU4, U1, U3, TX1, RX1 99 | 100 | ; list of net names to use in study (comma-separated) 101 | BringupList = MASTER-RESET, +3V3, /DUE_V02g_1/VIN+, +5V, /DUE_V02g_2/VDDANA, /DUE_V02g_2/VDDOUTMI, /DUE_V02g_2/VDDOUT, /DUE_V02g_2/VDDPLL 102 | 103 | ; list of bounds corresponding to the nets of BringupList (comma-separated) 104 | ; ie. the bounds of the ith net are 2i (low) and 2i+1 (high) 105 | BringupBounds = 3.201,3.465 , 3.201,3.465 , 6,16 , 4.85,5.25 , 3.201,3.465 , 3.201,3.465 , 1.746,1.89 , 0,0.05 106 | 107 | ; time between instrument panel value updates, in ms 108 | DmmPanelRefreshFrequency = 600 109 | 110 | ; hitbox padding around components for the study 111 | CompPadding = 0.0 112 | 113 | ; name of the board being used (must match a section name to get the right boardpos offset) 114 | ; when calculating the offset values for a new board, use New Board 115 | ; make sure to also set the projector translation to 0 116 | BoardName = Arduino Uno 117 | 118 | 119 | [New Board] 120 | ; offset values for board tracking 121 | BoardposOffsetX = 0 122 | BoardposOffsetY = 0 123 | BoardposOffsetR = 0 124 | BoardposDefaultZ = 1 125 | BoardCenterX = 0 126 | BoardCenterY = 0 127 | 128 | 129 | [Arduino Uno] 130 | ; offset values for board tracking 131 | BoardposOffsetX = 570.9018 132 | BoardposOffsetY = -410.5205 133 | BoardposOffsetR = -86.9076 134 | BoardposDefaultZ = 3.90 135 | BoardCenterX = 148.50 136 | BoardCenterY = 105.00 137 | 138 | 139 | [Arduino Due] 140 | ; offset values for board tracking 141 | BoardposOffsetX = 0 142 | BoardposOffsetY = 0 143 | BoardposOffsetR = 0 144 | BoardposDefaultZ = 1 145 | BoardCenterX = 0 146 | BoardCenterY = 0 147 | 148 | 149 | [Redboard] 150 | ; offset values for board tracking 151 | BoardposOffsetX = 0 152 | BoardposOffsetY = 0 153 | BoardposOffsetR = 0 154 | BoardposDefaultZ = 1 155 | BoardCenterX = 0 156 | BoardCenterY = 0 157 | 158 | 159 | [Sound Detector] 160 | ; offset values for board tracking 161 | BoardposOffsetX = 0 162 | BoardposOffsetY = 0 163 | BoardposOffsetR = 0 164 | BoardposDefaultZ = 1 165 | BoardCenterX = 0 166 | BoardCenterY = 0 167 | -------------------------------------------------------------------------------- /ardw-app/config_uno.ini: -------------------------------------------------------------------------------- 1 | [Server] 2 | ; port on which the server runs 3 | Port = 5000 4 | 5 | ; name of the folder containing pcbdata.json, schdata.json, and appropriate svgs 6 | DataFolder = data_uno 7 | 8 | ; IP at which the server listens for optitrack UDP 9 | UDPAddress = 127.0.0.1 10 | 11 | ; port on which the server listens for optitrack UDP 12 | UDPPort = 8052 13 | 14 | ; optitrack UDP packet size in bytes 15 | UDPPacketSize = 1024 16 | 17 | ; fps that the server listens for UDP at 18 | ; should be greater than Optitrack fps so we can catch up if we fall behind 19 | UDPFramerate = 60 20 | 21 | 22 | [Optitrack] 23 | ; time the tip or end must be stationary to trigger a dwell event, in seconds 24 | DwellTime = 0.5 25 | DwellTimeEnd = 0.5 26 | 27 | ; radius from mean position since DwellTime where probe tip/end must stay to be dwelling, in pixels 28 | DwellRadiusTip = 5 29 | DwellRadiusEnd = 10 30 | 31 | ; radius of "stable zone" where probe tip must be to stay within disambiguation menu, in pixels 32 | MultiAnchorRadius = 20 33 | 34 | ; sensitivity of disambiguation menu dot (how far probe end must move to move the dot) 35 | ; technically used as the height of a row of the linear disambiguation menu, in pixels 36 | MultiMenuSensitivity = 40 37 | 38 | ; horizontal buffer around the edge cuts that the probe needs to leave before it can reselect, in pixels 39 | ReselectHorizontalBuffer = 20 40 | 41 | ; maximum height value of the safe zone that the probe needs to leave before it can reselect, in real mm 42 | ReselectVerticalMaximum = 21 43 | 44 | ; minimum height value of the safe zone, in real mm 45 | ReselectVerticalMinimum = 0 46 | 47 | ; maximum height value where a deselection event will be registered outside the edge cuts, in real mm 48 | OutsideVerticalBuffer = 2 49 | 50 | ; hitbox padding around components and pins for board selection, in mm 51 | PinPadding = 1.0 52 | 53 | 54 | [Rendering] 55 | ; note: colors can be a name, eg. purple; RGB, eg. #800080; or RGBA, eg. rgba(128, 0, 128, 0.5) 56 | 57 | ; colors for pad and track highlighting on projector 58 | PadColorHighlight = #00FF00 59 | TrackColorHighlight = #00FF00 60 | 61 | ; colors for the probe tip dots/crosshair on the projector/layout 62 | ProbeDotColor = green 63 | DmmPosDotColor = green 64 | DmmNegDotColor = purple 65 | OscDotColor = blue 66 | 67 | ; colors for the probe selection highlights on the projector/layout 68 | ProbeSelectionColor = green 69 | DmmPosSelectionColor = green 70 | DmmNegSelectionColor = purple 71 | OscSelectionColor = blue 72 | 73 | 74 | [Dev] 75 | ; if True, start with probe and dmm connected rather than setting up connection through web interface 76 | AutoconnectTools = True 77 | 78 | ; iff True, projector will track the board constantly rather than sitting in a fixed location 79 | ; if False, you can still do a one-time board track using the "Jump to Board" settings button 80 | TrackBoard = False 81 | 82 | ; iff True, projector will track the board rotation, not just translation 83 | ; this also applies to the "Jump to Board" button 84 | TrackBoardRotation = False 85 | 86 | ; default values of projector calibration 87 | DefaultTX = 80 88 | DefaultTY = 40 89 | DefaultRotation = 0 90 | DefaultZoom = 3.95 91 | 92 | 93 | [Study] 94 | ; list of component names to use in the study (comma-separated) 95 | ComponentList = C1, C2, C3, C4, C5, C7, C8, C9, C11, D2, D3, F1, RN1, RN2, T1, U2, U5, Y2, Z1, Z2 96 | 97 | ; list of component names to be used in the practice set 98 | PracticeList = ZU4, U1, U3, TX1, RX1 99 | 100 | ; list of net names to use in study (comma-separated) 101 | BringupList = MASTER-RESET, +3V3, /DUE_V02g_1/VIN+, +5V, /DUE_V02g_2/VDDANA, /DUE_V02g_2/VDDOUTMI, /DUE_V02g_2/VDDOUT, /DUE_V02g_2/VDDPLL 102 | 103 | ; list of bounds corresponding to the nets of BringupList (comma-separated) 104 | ; ie. the bounds of the ith net are 2i (low) and 2i+1 (high) 105 | BringupBounds = 3.201,3.465 , 3.201,3.465 , 6,16 , 4.85,5.25 , 3.201,3.465 , 3.201,3.465 , 1.746,1.89 , 0,0.05 106 | 107 | ; time between instrument panel value updates, in ms 108 | DmmPanelRefreshFrequency = 600 109 | 110 | ; hitbox padding around components for the study 111 | CompPadding = 0.0 112 | 113 | ; name of the board being used (must match a section name to get the right boardpos offset) 114 | ; when calculating the offset values for a new board, use New Board 115 | ; make sure to also set the projector translation to 0 116 | BoardName = Arduino Uno 117 | 118 | 119 | [New Board] 120 | ; offset values for board tracking 121 | BoardposOffsetX = 0 122 | BoardposOffsetY = 0 123 | BoardposOffsetR = 0 124 | BoardposDefaultZ = 1 125 | BoardCenterX = 0 126 | BoardCenterY = 0 127 | 128 | 129 | [Arduino Uno] 130 | ; offset values for board tracking 131 | BoardposOffsetX = 570.9018 132 | BoardposOffsetY = -410.5205 133 | BoardposOffsetR = -86.9076 134 | BoardposDefaultZ = 3.90 135 | BoardCenterX = 148.50 136 | BoardCenterY = 105.00 137 | 138 | 139 | [Arduino Due] 140 | ; offset values for board tracking 141 | BoardposOffsetX = 0 142 | BoardposOffsetY = 0 143 | BoardposOffsetR = 0 144 | BoardposDefaultZ = 1 145 | BoardCenterX = 0 146 | BoardCenterY = 0 147 | 148 | 149 | [Redboard] 150 | ; offset values for board tracking 151 | BoardposOffsetX = 0 152 | BoardposOffsetY = 0 153 | BoardposOffsetR = 0 154 | BoardposDefaultZ = 1 155 | BoardCenterX = 0 156 | BoardCenterY = 0 157 | 158 | 159 | [Sound Detector] 160 | ; offset values for board tracking 161 | BoardposOffsetX = 0 162 | BoardposOffsetY = 0 163 | BoardposOffsetR = 0 164 | BoardposDefaultZ = 1 165 | BoardCenterX = 0 166 | BoardCenterY = 0 167 | -------------------------------------------------------------------------------- /ardw-app/example_tool.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | class ExampleTool: 5 | def __init__(self): 6 | print('Example tool connected!') 7 | self.connected = True 8 | self.position = [0, 0] 9 | 10 | def measure(self): 11 | self.position = [random.randint(0, 100), random.randint(0, 100)] 12 | return self.position 13 | -------------------------------------------------------------------------------- /ardw-app/instrumentscripts/DMM6500/css/front_panel_styles.css: -------------------------------------------------------------------------------- 1 | .normal#contentWrapper 2 | { 3 | width: 1074px; 4 | margin-right: auto; 5 | margin-left: auto; 6 | user-select: none; 7 | -moz-user-select: none; 8 | -webkit-user-select: none; 9 | -ms-user-select: none; 10 | } 11 | 12 | .normal #bumper 13 | { 14 | position: relative; 15 | width: 1074px; 16 | height: 430px; 17 | background-color: rgb(240,240,240); 18 | background-image: url(../images/fp_background.svg); 19 | background-repeat: no-repeat; 20 | background-size: contain; 21 | border-color: rgb(100,100,100); 22 | border-style: solid; 23 | border-width: 2px; 24 | } 25 | 26 | .normal #center 27 | { 28 | position: absolute; 29 | left: 134px; 30 | } 31 | 32 | .normal #Display 33 | { 34 | position: relative; 35 | width: 560px; 36 | border: 15px; 37 | background-color: black; 38 | padding: 3px; 39 | clear: both; 40 | border-color: rgb(140, 140, 140); 41 | border-style: inset; 42 | } 43 | 44 | .fullscreen .canhide 45 | { 46 | display: none; 47 | } 48 | 49 | .fullscreen #Display 50 | { 51 | position: relative; 52 | background-color: black; 53 | width: 98vw; 54 | max-width: 800px; 55 | min-width: 400px; 56 | margin-left: auto; 57 | margin-right: auto; 58 | } 59 | 60 | #DisplayCanvas 61 | { 62 | width: 100%; 63 | display: block; 64 | } 65 | 66 | #DisplayCanvas:hover 67 | { 68 | cursor: pointer; 69 | } 70 | 71 | #left 72 | { 73 | position: absolute; 74 | padding-top: 49px; 75 | padding-left: 31px; 76 | } 77 | 78 | #GreenLabel 79 | { 80 | border-radius: 0 0 4px 4px; 81 | float:right; 82 | text-align:center; 83 | font: 16px "helvetica neue", helvetica, arial, sans-serif; 84 | color:#fff; 85 | padding: 6px; 86 | letter-spacing: 0.5px; 87 | background: rgb(0, 150, 57); 88 | margin-bottom: 3px; 89 | } 90 | 91 | #GreenLabel span 92 | { 93 | font-weight: bold; 94 | } 95 | 96 | #right 97 | { 98 | position: absolute; 99 | left: 762px; 100 | padding-top: 49px; 101 | } 102 | 103 | .buttonSurround 104 | { 105 | border-style: solid; 106 | border-width: 2px; 107 | border-color: rgb(100,100,100); 108 | border-radius: 9px; 109 | padding: 2px; 110 | background-color: rgb(70,70,70); 111 | margin-bottom: 8px; 112 | } 113 | 114 | .button 115 | { 116 | color: white; 117 | background-color:rgb(130,130,130); 118 | font: normal 11px "helvetica neue", helvetica, arial, sans-serif; 119 | width: 66px; 120 | height: 32px; 121 | border-width: 1px; 122 | border-style: outset; 123 | border-color: rgb(180,180,180); 124 | border-radius: 6px; 125 | padding: 0px; 126 | outline: none; 127 | } 128 | 129 | .button:hover 130 | { 131 | text-shadow:0 0 5px #fff; 132 | cursor: pointer; 133 | } 134 | 135 | .button:active 136 | { 137 | border-style: solid; 138 | border-color: rgb(70,70,70); 139 | } 140 | 141 | .led 142 | { 143 | position: absolute; 144 | width: 13px; 145 | height: 13px; 146 | border-radius: 13px; 147 | border-style: none; 148 | } 149 | 150 | #terminals.front .rear 151 | { 152 | background-color: rgb(198, 212, 198); 153 | border-color: rgb(162, 171, 162); 154 | } 155 | 156 | #terminals.rear .front 157 | { 158 | background-color: rgb(198, 212, 198); 159 | border-color: rgb(162, 171, 162); 160 | } 161 | 162 | .led.power 163 | { 164 | left: 37px; 165 | top: 377px; 166 | } 167 | 168 | .led.front 169 | { 170 | left: 82px; 171 | top: 276px; 172 | } 173 | 174 | .led.rear 175 | { 176 | left: 82px; 177 | top: 310px; 178 | } 179 | 180 | .green 181 | { 182 | background-color: rgb(41, 255, 41); 183 | border-color: rgb(57, 150, 57); 184 | border-style: solid; 185 | border-width: 2px; 186 | } 187 | 188 | .orange 189 | { 190 | background-color: rgb(255, 200, 116); 191 | border-color: rgb(216, 133, 17); 192 | border-style: solid; 193 | border-width: 2px; 194 | } 195 | 196 | .contextmenu 197 | { 198 | z-index: 1; 199 | font-family: sans-serif; 200 | white-space: nowrap; 201 | position: absolute; 202 | padding-left: 10px; 203 | padding-right: 10px; 204 | border: 3px solid gray; 205 | border-radius: 5px; 206 | background-color: rgb(240,240,240); 207 | display: none; 208 | } 209 | 210 | .contextmenu a 211 | { 212 | text-decoration: none; 213 | color: inherit; 214 | cursor: pointer; 215 | } 216 | -------------------------------------------------------------------------------- /ardw-app/instrumentscripts/DMM6500/front_panel.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-app/instrumentscripts/DMM6500/front_panel.html -------------------------------------------------------------------------------- /ardw-app/instrumentscripts/DMM6500/script/ajax_inc.js: -------------------------------------------------------------------------------- 1 | //url for ajax calls 2 | var url_str = 'ajax_proc'; 3 | 4 | //constants for buttons 5 | var buttonMap = { 6 | 'Home': 1, 7 | 'Menu': 2, 8 | 'Button3': 3, 9 | 'Help': 4, 10 | 'Enter': 5, 11 | 'Wheel': 5, 12 | 'Exit': 6, 13 | 'Function': 7, 14 | 'Trigger': 8, 15 | 'TerminalSwitch': 9, 16 | 'Output': 10 17 | }; 18 | 19 | //constants for functions 20 | var COMMAND_FUNCTION = 1; 21 | var CLICK_DOWN_FUNCTION = 2; 22 | var CLICK_UP_FUNCTION = 3; 23 | var SWIPE_FUNCTION = 4; 24 | var BUFFER_FUNCTION = 5; 25 | var LED_FUNCTION = 6; 26 | var BUTTON_PRESS_FUNCTION = 7; 27 | var BUTTON_RELEASE_FUNCTION = 8; 28 | var FP_SESSION_FUNCTION = 9; 29 | var BUFFER_SESSION_FUNCTION = 10; 30 | var CLICK_MOVE_FUNCTION = 11; 31 | var INTERRUPT_FUNCTION = 12; 32 | 33 | //constants for evaluating returned strings 34 | var DELIMITER = '$@$'; 35 | var ERROR_HEADER = 'ERR='; 36 | var COMMAND_HEADER = 'CMD='; 37 | var BUFFER_HEADER = 'buff'; 38 | var REMOTE_HEADER = 'rem='; 39 | var LAN_HEADER = 'lan='; 40 | var PTP1588_HEADER = 'ptp1588'; 41 | var TERMINAL_HEADER = 'term='; 42 | var OUTPUT_HEADER = 'op='; 43 | var INTERLOCK_HEADER = 'interl='; 44 | var CAP_STATUS_HEADER = 'cap='; 45 | var SESSION_HEADER = 'session='; 46 | 47 | //constants for swipes 48 | var RESIZE_AMOUNT = 1.55; 49 | var SWIPE_LEFT = 1; 50 | var SWIPE_RIGHT = 2; 51 | var SWIPE_UP = 3; 52 | var SWIPE_DOWN = 4; 53 | var SWIPE_DISTANCE = 20; 54 | //how should I handle diagonal swiping? 55 | 56 | //global variables for swipes on touchscreen 57 | var mouseXPos; 58 | var mouseYPos; 59 | 60 | //polling rate constants 61 | var DISPLAY_POLLING = 100; 62 | var BUFFER_POLLING = 5000; 63 | 64 | //path to where buffers live 65 | var BUFFER_PATH = '/'; 66 | var IMAGE_PATH = '/images/fp.tga'; 67 | var IMAGE_PATH_LOW = '/images/fplow.tga'; 68 | 69 | // Utility functions 70 | 71 | function post(params, resultHandler) { 72 | var pair_count = 0; 73 | var pairs = []; 74 | Object.keys(params).forEach(function (key) { 75 | pairs[pair_count++] = key + '=' + params[key]; 76 | }); 77 | 78 | var xhr = new XMLHttpRequest(); 79 | xhr.open('POST', 'ajax_proc'); 80 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 81 | xhr.setRequestHeader('Cache-Control', 'no-cache'); 82 | 83 | if (resultHandler) { 84 | xhr.onload = function () { 85 | resultHandler(xhr.response); 86 | }; 87 | } 88 | 89 | xhr.send(pairs.join('&')); 90 | return xhr; 91 | } 92 | 93 | function startSession(func_id, onready) { 94 | top.document.title = document.title; 95 | 96 | post({ 97 | function: func_id 98 | }, function (result) { 99 | var resultArray = /session=(\d+)/.exec(result); 100 | if (resultArray) { 101 | session_id = parseInt(resultArray[1]); 102 | onready(); 103 | } 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /ardw-app/instrumentscripts/DMM6500/script/tga_decode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get image information from a TGA-formatted buffer 3 | * @param {ArrayBuffer} buffer 4 | */ 5 | function TGAInfo(buffer) { 6 | var header = new DataView(buffer.slice(0, 18)); 7 | tgaCompatibilityCheck(header); 8 | 9 | this.usesRLE = header.getUint8(2) == 10; 10 | //this.x = header.getUint16(8, true); 11 | //this.y = header.getUint16(10, true); 12 | this.width = header.getUint16(12, true); 13 | this.height = header.getUint16(14, true); 14 | } 15 | 16 | /** 17 | * Write ArrayBuffer data into an ImageData object 18 | * @param {ImageData} img 19 | * @param {ArrayBuffer} buffer 20 | * @param {TGAInfo} info 21 | */ 22 | function tgaImageDataSet(img, buffer, info) { 23 | var byteArray = new Uint8Array(buffer); 24 | var dst_index = 0; 25 | var src_index = 18; //default to end of header 26 | var src_x, src_y; 27 | 28 | if (info.usesRLE) { 29 | var pixelCount = 0; 30 | var repeat = false; 31 | var readNextPixel = true; 32 | var R, G, B; 33 | 34 | for (src_y = info.height - 1; src_y > 0; --src_y) { 35 | for (src_x = 0; src_x < info.width; ++src_x) { 36 | dst_index = (src_y * info.width + src_x) * 4; 37 | 38 | if (pixelCount == 0) { 39 | var cmd = byteArray[src_index++]; 40 | pixelCount = 1 + (cmd & 0x7f); 41 | repeat = (cmd & 0x80) != 0; 42 | readNextPixel = true; 43 | } else if (!repeat) { 44 | readNextPixel = true; 45 | } 46 | 47 | if (readNextPixel) { 48 | B = byteArray[src_index++]; 49 | G = byteArray[src_index++]; 50 | R = byteArray[src_index++]; 51 | readNextPixel = false; 52 | } 53 | 54 | img.data[dst_index] = R; 55 | img.data[dst_index + 1] = G; 56 | img.data[dst_index + 2] = B; 57 | img.data[dst_index + 3] = 255; 58 | 59 | --pixelCount; 60 | } 61 | } 62 | } else { 63 | for (src_y = info.height - 1; src_y >= 0; --src_y) { 64 | for (src_x = 0; src_x < info.width; ++src_x) { 65 | dst_index = (src_y * info.width + src_x) * 4; 66 | 67 | img.data[dst_index + 2] = byteArray[src_index++]; 68 | img.data[dst_index + 1] = byteArray[src_index++]; 69 | img.data[dst_index + 0] = byteArray[src_index++]; 70 | img.data[dst_index + 3] = 255; 71 | } 72 | } 73 | } 74 | 75 | return img; 76 | } 77 | 78 | /** 79 | * Check the image header for compatibility with the decoder. Throws if the 80 | * image is not compatible. 81 | * @param {DataView} header 82 | */ 83 | function tgaCompatibilityCheck(header) { 84 | var issue = 'TGA compatibility issue: '; 85 | 86 | // Check for a non-empty image ID area 87 | if (header.getUint8(0) != 0) { 88 | throw issue + 'image ID must be empty'; 89 | } 90 | 91 | // Check color map type (0 indicates no color map) 92 | if (header.getUint8(1) != 0) { 93 | throw issue + 'color map not supported'; 94 | } 95 | 96 | // Check image type 97 | switch (header.getUint8(2)) { 98 | case 2: // true-color uncompressed 99 | case 10: // true-color RLE compressed 100 | break; 101 | default: 102 | throw issue + 'RLE and uncompressed true-color images only'; 103 | } 104 | 105 | // Check color depth 106 | if (header.getUint8(16) != 24) { 107 | throw issue + 'color depth must be 24-bit'; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /ardw-app/instrumentscripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-app/instrumentscripts/__init__.py -------------------------------------------------------------------------------- /ardw-app/instrumentscripts/scpi_read.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple read through SCPI 3 | 4 | Requires PyVISA, PyVISA-py 5 | pip install pyvisa 6 | pip install pyvisa-py 7 | 8 | Confirm proper PyVISA-py installation with 9 | python -m visa info 10 | 11 | If using a USB resource, requires PyUSB which in turn requires USB driver library such as libusb 1.0 12 | 13 | pip intall pyusb 14 | brew install libusb 15 | may need to restart terminal/computer after libusb installation 16 | """ 17 | 18 | # import package 19 | import pyvisa 20 | #import argparse 21 | 22 | # known instruments 23 | # we expect all SCPI-enabled instruments to accept the same basic commands, but we can use this list to assign an instrument type 24 | supported_dmms = ["MODEL DMM6500", "DMM6500"] 25 | supported_oscs = ["MODEL MSO4104", "MSO4104"] 26 | 27 | dmms = [] 28 | oscs = [] 29 | 30 | #Oscilliscope channel 31 | channel = 2 32 | 33 | # parser = argparse.ArgumentParser('Read value from an Instrument') 34 | # parser.add_argument("--instrument", type=str, help="select an instrument. options: 'dmm', 'osc'") 35 | # parser.add_argument("--function", type=str, help="function for instrument read. examples: voltage, current, resistance") 36 | 37 | rm = pyvisa.ResourceManager() 38 | resources = rm.list_resources() 39 | 40 | def initializeInstruments(): 41 | if len(resources) == 0: 42 | print("No resources found") 43 | return 44 | 45 | for counter in range(len(resources)): 46 | print("Connecting to resource "+str(counter)+": " + resources[counter]) 47 | 48 | if "USB" in resources[counter]: 49 | current_resource = rm.open_resource(resources[counter]) 50 | print("Connected to: " + current_resource.query("*IDN?")) 51 | if current_resource.query("*IDN?").split(",")[1] in supported_dmms: 52 | print("Resource "+str(counter)+" is a supported DMM") 53 | dmms.append(current_resource) 54 | 55 | elif current_resource.query("*IDN?").split(",")[1] in supported_oscs: 56 | print("Resource "+str(counter)+" is a supported oscilliscope") 57 | oscs.append(current_resource) 58 | current_resource.write('VERBOSE ON') 59 | 60 | else: 61 | print("Resource "+ current_resource+ " isn't a supported DMM or oscilliscope") 62 | else: 63 | print(resources[counter]+" isn't a USB device. Can't connect.") 64 | 65 | # inst.write("*rst; status:preset; *cls") 66 | 67 | def queryValue(instrumentType, function): 68 | if instrumentType == "dmm": 69 | 70 | # if function is None: 71 | # state = 72 | 73 | if function == "voltage": 74 | value = float(dmms[0].query(':MEASure:VOLTage:DC?')) 75 | print("Measured value = " + str(value) + " VDC") 76 | return value 77 | 78 | if function == "resistance": 79 | value = float(dmms[0].query(':MEAS:RES?')) 80 | print("Measured value = " + str(value) + " ohms") 81 | return value 82 | 83 | if function == "continuity": 84 | value = float(dmms[0].query(':MEAS:CONT?')) 85 | print("Measured value = " + str(value)) 86 | return value 87 | 88 | if function == "dc_current": 89 | value = float(dmms[0].query(':MEAS:CURR:DC?')) 90 | print("Measured value = " + str(value) + " A") 91 | return value 92 | 93 | elif instrumentType == "osc": 94 | if function == "frequency": 95 | # oscs[0].write('MEASU:FREQ CHAN' + str(channel)) 96 | # oscs[0].write(':MEASUrement:IMMed:SOUrce1 CH' + str(channel)) 97 | # oscs[0].write(':MEASUrement:IMMed:TYPe FREQuency') 98 | # value = str(oscs[0].query(':MEASUrement:IMMed:VALue?')) 99 | oscs[0].write(':MEASUrement:MEAS1:STATE ON') 100 | oscs[0].write(':MEASUrement:MEAS1:SOUrce1 CH' + str(channel)) 101 | oscs[0].write(':MEASUrement:MEAS1:TYPe FREQuency') 102 | value = str(oscs[0].query(':MEASUrement:MEAS1:VALue?')) 103 | print("Measured value = " + str(value) + " Hz") 104 | return value 105 | 106 | if function == "pduty": 107 | oscs[0].write(':MEAS:DUTY CHAN' + str(channel)) 108 | value = float(oscs[0].query(':MEAS:DUTY?')) 109 | print("Measured value = " + str(value) + " %") 110 | return value 111 | 112 | else: 113 | print("Invalid instrument type provided to queryValue") 114 | return 115 | 116 | def main(): 117 | initializeInstruments() 118 | queryValue("dmm", "voltage") 119 | #queryValue("osc", "frequency") 120 | 121 | 122 | 123 | if __name__ == "__main__": 124 | # args = parser.parse_args() 125 | #main(args) 126 | main() 127 | -------------------------------------------------------------------------------- /ardw-app/instrumentscripts/scpi_read_classes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple read through SCPI 3 | 4 | Requires PyVISA, PyVISA-py 5 | pip install pyvisa 6 | pip install pyvisa-py 7 | 8 | Confirm proper PyVISA-py installation with 9 | python -m visa info 10 | 11 | If using a USB resource, requires PyUSB which in turn requires USB driver library such as libusb 1.0 12 | 13 | pip intall pyusb 14 | brew install libusb 15 | may need to restart terminal/computer after libusb installation 16 | """ 17 | 18 | # import package 19 | import pyvisa 20 | #import argparse 21 | 22 | # known instruments 23 | # we expect all SCPI-enabled instruments to accept the same basic commands, but we can use this list to assign an instrument type 24 | supported_dmms = ["MODEL DMM6500", "DMM6500"] 25 | supported_oscs = ["MODEL MSO4104", "MSO4104"] 26 | 27 | open_resources = [] 28 | Instruments = [] 29 | 30 | # parser = argparse.ArgumentParser('Read value from an Instrument') 31 | # parser.add_argument("--instrument", type=str, help="select an instrument. options: 'dmm', 'osc'") 32 | # parser.add_argument("--function", type=str, help="function for instrument read. examples: voltage, current, resistance") 33 | 34 | 35 | class Instrument: 36 | def __init__(self, handle, identifier, type): 37 | self.handle = handle 38 | self.identifier = identifier 39 | self.type = type 40 | 41 | 42 | class DMM(Instrument): 43 | def getVoltage(self): 44 | value = float(self.handle.query(':MEASure:VOLTage:DC?')) 45 | print("Measured value = " + str(value) + " VDC") 46 | return value 47 | 48 | class Oscilliscope(Instrument): 49 | #TODO change this method for oscilliscope 50 | def getVoltageOsc(self): 51 | value = float(self.handle.query(':MEASure:VOLTage:DC?')) 52 | print("Measured value = " + str(value) + " VDC") 53 | return value 54 | 55 | # Grabs open resources from PyVisa Resource Manager and appends them to open_resources array 56 | def getResources(): 57 | rm = pyvisa.ResourceManager() 58 | resources = rm.list_resources() 59 | 60 | if len(resources) == 0: 61 | print("No resources found") 62 | return 63 | 64 | for counter in range(len(resources)): 65 | print("Connecting to resource "+str(counter)+": " + resources[counter]) 66 | open_resources.append(rm.open_resource(resources[counter])) 67 | print("Connected to: " + open_resources[counter].query("*IDN?")) 68 | 69 | # Depending on type on instrument 70 | def assignResources(open_resources): 71 | rm = pyvisa.ResourceManager() 72 | resources = rm.list_resources() 73 | 74 | for counter in range(len(open_resources)): 75 | if open_resources[counter].query("*IDN?").split(",")[1] in supported_dmms: 76 | print("Resource "+str(counter)+" is a DMM") 77 | Instruments.append(DMM(open_resources[counter], open_resources[counter].query("*IDN?").split(",")[1], 'DMM')) 78 | elif open_resources[counter].query("*IDN?").split(",")[1] in supported_oscs: 79 | print("Resource "+str(counter)+" is an oscilliscope") 80 | Instruments.append(Oscilliscope(open_resources[counter], open_resources[counter].query("*IDN?").split(",")[1], 'Oscilliscope')) 81 | else: 82 | print("Resource "+str(counter)+" is unknown / unsupported") 83 | Instruments.append(None) 84 | # inst.write("*rst; status:preset; *cls") 85 | 86 | # def queryValue(instrumentType, function): 87 | # if instrumentType == "dmm": 88 | # if function == "voltage": 89 | # # TODO need to fix later so it doesn't just take the first instrument 90 | # value = float(instruments[0].query(':MEASure:VOLTage:DC?')) 91 | # print("Measured value = " + str(value) + " VDC") 92 | # return value 93 | 94 | # elif instrumentType == "osc": 95 | # if function == "voltage": 96 | # # TODO need to fix later so it doesn't just take the first instrument 97 | # value = float(instruments[0].query(':MEASure:VOLTage:DC?')) 98 | # print("Measured value = " + str(value) + " VDC") 99 | # return value 100 | 101 | # else: 102 | # print("Invalid instrument type provided to queryValue") 103 | # return 104 | 105 | def main(): 106 | getResources() 107 | assignResources(open_resources) 108 | #my_DMM = DMM(rm.open_resource(resources[0]), instruments[0].query("*IDN?").split(",")[1]) 109 | Instruments[0].getVoltage() 110 | #queryValue("dmm", "voltage") 111 | # for i in range(200): 112 | # value = float(instruments[0].query(':MEASure:VOLTage:DC?')) 113 | 114 | 115 | if __name__ == "__main__": 116 | # args = parser.parse_args() 117 | #main(args) 118 | main() 119 | -------------------------------------------------------------------------------- /ardw-app/instrumentscripts/scpi_read_flask.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple read through SCPI 3 | 4 | Requires PyVISA, PyVISA-py 5 | pip install pyvisa 6 | pip install pyvisa-py 7 | 8 | Confirm proper PyVISA-py installation with 9 | python -m visa info 10 | 11 | If using a USB resource, requires PyUSB which in turn requires USB driver library such as libusb 1.0 12 | 13 | pip intall pyusb 14 | brew install libusb 15 | may need to restart terminal/computer after libusb installation 16 | """ 17 | 18 | from flask import Flask, render_template, Response, request, redirect, url_for 19 | # import package 20 | import pyvisa 21 | import logging 22 | 23 | # import argparse 24 | 25 | app = Flask(__name__) 26 | host = "127.0.0.1" 27 | port = 8080 28 | 29 | # known instruments 30 | # we expect all SCPI-enabled instruments to accept the same basic commands, but we can use this list to assign an instrument type 31 | supported_dmms = ["MODEL DMM6500", "DMM6500"] 32 | supported_oscs = ["MODEL MSO4104", "MSO4104"] 33 | 34 | dmms = [] 35 | oscs = [] 36 | 37 | # Oscilliscope channel 38 | channel = 2 39 | 40 | # parser = argparse.ArgumentParser('Read value from an Instrument') 41 | # parser.add_argument("--instrument", type=str, help="select an instrument. options: 'dmm', 'osc'") 42 | # parser.add_argument("--function", type=str, help="function for instrument read. examples: voltage, current, resistance") 43 | 44 | rm = pyvisa.ResourceManager() 45 | resources = rm.list_resources() 46 | 47 | # @app.route("/") 48 | def index(): 49 | return ( 50 | """ 51 | 52 | 53 | """ 54 | ) 55 | 56 | 57 | # rendering the HTML page which has the button 58 | @app.route('/instrument_panel') 59 | def json(): 60 | return render_template('instrument_panel.html') 61 | 62 | def initializeInstruments(): 63 | if len(resources) == 0: 64 | logging.warning("No resources found") 65 | return 66 | 67 | for counter in range(len(resources)): 68 | logging.info(f"Connecting to resource {counter}: {resources[counter]}") 69 | 70 | if "USB" in resources[counter]: 71 | current_resource = rm.open_resource(resources[counter]) 72 | logging.info(f"Connected to: {current_resource.query('*IDN?')}") 73 | if current_resource.query("*IDN?").split(",")[1] in supported_dmms: 74 | logging.info(f"Resource {counter} is a supported DMM") 75 | dmms.append(current_resource) 76 | 77 | elif current_resource.query("*IDN?").split(",")[1] in supported_oscs: 78 | logging.info(f"Resource {counter} is a supported oscilliscope") 79 | oscs.append(current_resource) 80 | current_resource.write('VERBOSE ON') 81 | 82 | else: 83 | logging.info(f"Resource {current_resource} isn't a supported DMM or oscilliscope") 84 | else: 85 | logging.info(f"{resources[counter]} isn't a USB device. Can't connect.") 86 | 87 | # inst.write("*rst; status:preset; *cls") 88 | 89 | def releaseInstruments(): 90 | dmms = [] 91 | oscs = [] 92 | print("attempting to close insturments") 93 | rm.close() 94 | print("closed insturments") 95 | 96 | # inst.write("*rst; status:preset; *cls") 97 | 98 | # @app.route('/voltageMode') 99 | # def voltageMode(): 100 | # MODE = "voltage" 101 | # return MODE 102 | # 103 | # @app.route('/resistanceMode') 104 | # def resistanceMode(): 105 | # MODE = "resistance" 106 | # return MODE 107 | 108 | @app.route('/queryValue/') 109 | def queryValue(instrumentType="dmm", function="no_function"): 110 | if instrumentType == "dmm": 111 | 112 | for attempt in range(5): 113 | if function == "no_function": 114 | # return Response("--------", mimetype='text') 115 | if __name__ == "__main__": 116 | return Response("Please select a function", mimetype='text') 117 | return None 118 | 119 | try: 120 | value = None 121 | if function == "voltage": 122 | value = float(dmms[0].query(':MEASure:VOLTage:DC?')) 123 | # logging.info(f"Measured value = {value} VDC") 124 | # return Response(str(value), mimetype='text') 125 | 126 | if function == "resistance": 127 | value = float(dmms[0].query(':MEAS:RES?')) 128 | # logging.info(f"Measured value = {value} ohms") 129 | # return Response(str(value), mimetype='text') 130 | 131 | if function == "diode": 132 | value = float(dmms[0].query(':MEASure:DIODe?')) 133 | # logging.info(f"Measured value = {value} VDC") 134 | # return Response(str(value), mimetype='text') 135 | 136 | if function == "continuity": 137 | value = float(dmms[0].query(':MEAS:CONT?')) 138 | # logging.info(f"Measured value = {value}") 139 | # return Response(str(value), mimetype='text') 140 | 141 | if function == "dc_current": 142 | value = float(dmms[0].query(':MEAS:CURR:DC?')) 143 | # logging.info(f"Measured value = {value} A") 144 | # return Response(str(value), mimetype='text') 145 | 146 | if value: 147 | if __name__ == "__main__": 148 | return Response(str(value), mimetype='text') 149 | else: 150 | return value 151 | 152 | except: 153 | print("Instrument can't communicate -- resetting") 154 | releaseInstruments() 155 | 156 | print("loading new pyvisa resouce manager") 157 | rm = pyvisa.ResourceManager() 158 | resources = rm.list_resources() 159 | 160 | print("Reinitializing...") 161 | initializeInstruments() 162 | print("Reinitialized...") 163 | 164 | else: 165 | break 166 | 167 | 168 | elif instrumentType == "osc": 169 | if function == "frequency": 170 | # oscs[0].write('MEASU:FREQ CHAN' + str(channel)) 171 | # oscs[0].write(':MEASUrement:IMMed:SOUrce1 CH' + str(channel)) 172 | # oscs[0].write(':MEASUrement:IMMed:TYPe FREQuency') 173 | # value = str(oscs[0].query(':MEASUrement:IMMed:VALue?')) 174 | oscs[0].write(':MEASUrement:MEAS1:STATE ON') 175 | oscs[0].write(':MEASUrement:MEAS1:SOUrce1 CH' + str(channel)) 176 | oscs[0].write(':MEASUrement:MEAS1:TYPe FREQuency') 177 | value = str(oscs[0].query(':MEASUrement:MEAS1:VALue?')) 178 | logging.info(f"Measured value = {value} Hz") 179 | return value 180 | 181 | if function == "pduty": 182 | oscs[0].write(':MEASUrement:MEAS1:STATE ON') 183 | oscs[0].write(':MEASUrement:MEAS1:SOUrce1 CH' + str(channel)) 184 | oscs[0].write(':MEASUrement:MEAS1:TYPe PDUTY') 185 | value = str(oscs[0].query(':MEASUrement:MEAS1:VALue?')) 186 | logging.info(f"Measured value = {value} %") 187 | return value 188 | 189 | else: 190 | logging.info("Invalid instrument type provided to queryValue") 191 | return 192 | 193 | 194 | if __name__ == "__main__": 195 | # args = parser.parse_args() 196 | # main(args) 197 | 198 | initializeInstruments() 199 | app.run(host=host, port=port, threaded=True, debug=True) 200 | 201 | # queryValue("dmm", "voltage") 202 | # queryValue("osc", "frequency") 203 | -------------------------------------------------------------------------------- /ardw-app/instrumentscripts/templates/instrument_panel.html: -------------------------------------------------------------------------------- 1 |
2 |

Instrument Panel Test

3 |

Click button to put DMM into a mode:

4 |
5 | 6 | 7 |
8 | 9 |

Measurement

10 |
11 | 12 | 13 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /ardw-app/refresh_python_modules.sh: -------------------------------------------------------------------------------- 1 | . venv/bin/activate && pip install -r requirements.txt 2 | -------------------------------------------------------------------------------- /ardw-app/requirements.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.17 2 | amqp==5.0.9 3 | autopep8==1.6.0 4 | bidict==0.21.2 5 | billiard==3.6.4.0 6 | cached-property==1.5.2 7 | celery==5.2.3 8 | click==8.0.3 9 | click-didyoumean==0.3.0 10 | click-plugins==1.1.1 11 | click-repl==0.2.0 12 | Deprecated==1.2.13 13 | dnspython==1.16.0 14 | eventlet==0.31.1 15 | Flask==2.0.1 16 | Flask-Celery==2.4.3 17 | Flask-Script==2.0.6 18 | Flask-SocketIO==5.1.0 19 | greenlet==1.1.0 20 | importlib-metadata==4.11.2 21 | itsdangerous==2.0.1 22 | Jinja2==3.0.1 23 | kombu==5.2.3 24 | macholib==1.14 25 | MarkupSafe==2.0.1 26 | numpy==1.21.5 27 | packaging==21.3 28 | prompt-toolkit==3.0.24 29 | pycodestyle==2.8.0 30 | pyinstaller==5.13.1 31 | pyinstaller-hooks-contrib==2021.2 32 | pyparsing==3.0.6 33 | python-engineio==4.2.0 34 | python-socketio==5.3.0 35 | pytz==2021.3 36 | PyVISA==1.11.3 37 | PyVISA-py==0.5.2 38 | redis==4.1.1 39 | Shapely==1.8.0 40 | six==1.16.0 41 | toml==0.10.2 42 | typing_extensions==4.1.1 43 | vine==5.0.0 44 | wcwidth==0.2.5 45 | Werkzeug==2.0.1 46 | wrapt==1.13.3 47 | zipp==3.7.0 48 | -------------------------------------------------------------------------------- /ardw-app/save_python_modules.sh: -------------------------------------------------------------------------------- 1 | pip freeze > requirements.txt 2 | -------------------------------------------------------------------------------- /ardw-app/static/capture.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-app/static/capture.wav -------------------------------------------------------------------------------- /ardw-app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-app/static/favicon.ico -------------------------------------------------------------------------------- /ardw-app/static/index.js: -------------------------------------------------------------------------------- 1 | // var socket = io() -------------------------------------------------------------------------------- /ardw-app/static/projector.js: -------------------------------------------------------------------------------- 1 | // This is the primary file for the projector webpage (the projector view) 2 | // It calls initialization functions from other files for the page. 3 | // It also contains some custom functions for the projector page, 4 | // mainly for handling socket selection events 5 | 6 | // Set to true so that functions in render.js ignore the resize transform (s/x/y) 7 | IS_PROJECTOR = true; 8 | 9 | /** Projector view transform from server/main page */ 10 | var transform = { 11 | "tx": 0, 12 | "ty": 0, 13 | "r": 0, 14 | "z": 1 15 | }; 16 | 17 | var active_session_is_recording = false; 18 | 19 | 20 | /** Sets various ibom settings to false to avoid displaying unwanted things */ 21 | function initSettings() { 22 | ibom_settings["renderDrawings"] = false; 23 | ibom_settings["renderEdgeCuts"] = true; 24 | ibom_settings["renderFabrication"] = false; 25 | ibom_settings["renderPads"] = false; 26 | ibom_settings["renderReferences"] = false; 27 | ibom_settings["renderSilkscreen"] = false; 28 | ibom_settings["renderTracks"] = false; 29 | ibom_settings["renderValues"] = false; 30 | ibom_settings["renderZones"] = false; 31 | } 32 | 33 | /** Highlights the selected component */ 34 | function projectorSelectComponent(refid) { 35 | var selected = parseInt(refid); 36 | if (compdict[selected] == undefined) { 37 | logerr(`selected refid ${selected} is not in compdict`); 38 | return; 39 | } 40 | 41 | projectorDeselectAll(); 42 | current_selection.type = "comp"; 43 | current_selection.val = selected; 44 | drawHighlights(); 45 | } 46 | 47 | /** Highlights the selected pin */ 48 | function projectorSelectPins(pin_hits) { 49 | // Permitting only single selection, but likely to change 50 | var selected = pin_hits[0]; 51 | if (pindict[selected] == undefined) { 52 | logerr(`selected pinidx ${selected} is not in pindict`); 53 | return; 54 | } 55 | 56 | projectorDeselectAll(); 57 | current_selection.type = "pin"; 58 | current_selection.val = selected; 59 | drawHighlights(); 60 | } 61 | 62 | /** Highlights the selected net */ 63 | function projectorSelectNet(netname) { 64 | if (!(netname in netdict)) { 65 | logerr(`selected net ${netname} is not in netdict`); 66 | return; 67 | } 68 | 69 | projectorDeselectAll(); 70 | current_selection.type = "net"; 71 | current_selection.val = netname; 72 | drawHighlights(); 73 | } 74 | 75 | /** Removes any highlights */ 76 | function projectorDeselectAll() { 77 | current_selection.type = null; 78 | current_selection.val = null; 79 | 80 | draw_crosshair = false; 81 | target_boxes["S"] = null; 82 | target_boxes["F"] = null; 83 | target_boxes["B"] = null; 84 | drawHighlights(); 85 | } 86 | 87 | var boardoff_counter = 0; 88 | var boardoff_n = 200; 89 | var boardoff_sums = { 90 | "x": 0, 91 | "y": 0, 92 | "r": 0, 93 | } 94 | function calcBoardOffset(boardpos) { 95 | if (boardoff_counter < boardoff_n) { 96 | boardoff_sums.x += boardpos.x; 97 | boardoff_sums.y += boardpos.y; 98 | boardoff_sums.r += boardpos.r; 99 | } else if (boardoff_counter == boardoff_n) { 100 | boardoff_sums.x /= boardoff_n; 101 | boardoff_sums.y /= boardoff_n; 102 | boardoff_sums.r /= boardoff_n; 103 | 104 | console.log(`transform is x=${transform.tx.toFixed(4)}, y=${transform.ty.toFixed(4)}, r=${transform.r.toFixed(4)}`); 105 | console.log(`boardpos was x=${boardoff_sums.x.toFixed(4)}, y=${boardoff_sums.y.toFixed(4)}, r=${boardoff_sums.r.toFixed(4)}`); 106 | 107 | var offx = -transform.tx * transform.z + boardoff_sums.x; 108 | var offy = transform.ty * transform.z + boardoff_sums.y; 109 | var offr = -transform.r - boardoff_sums.r; 110 | console.log(`theoretical offset is x=${offx.toFixed(4)}, y=${offy.toFixed(4)}, r=${offr.toFixed(4)}`); 111 | 112 | var avgx = (pcbdata.edges_bbox.minx + pcbdata.edges_bbox.maxx) / 2; 113 | var avgy = (pcbdata.edges_bbox.miny + pcbdata.edges_bbox.maxy) / 2; 114 | console.log(`edgecut center is (${avgx.toFixed(2)},${avgy.toFixed(2)})`); 115 | } 116 | boardoff_counter++; 117 | } 118 | 119 | /** Initializes all socket listeners for the projector */ 120 | function initSocket() { 121 | socket = io(); 122 | socket.on("connect", () => { 123 | console.log("connected") 124 | }); 125 | socket.on("selection", (selection) => { 126 | multimenu_active = null; 127 | switch (selection.type) { 128 | case "comp": 129 | projectorSelectComponent(selection.val); 130 | break; 131 | case "pin": 132 | projectorSelectPins([selection.val]); 133 | break; 134 | case "net": 135 | projectorSelectNet(selection.val); 136 | break; 137 | case "deselect": 138 | projectorDeselectAll(); 139 | break; 140 | case "multi": 141 | if (selection.from_optitrack) { 142 | multimenu_active = {"hits": selection.hits, "layer": selection.layer, "device": "probe"} 143 | drawHighlights(); 144 | } 145 | break; 146 | case "cancel-multi": 147 | multimenu_active = null; 148 | drawHighlights(); 149 | break; 150 | } 151 | }); 152 | socket.on("projector-mode", (mode) => { 153 | if (mode === "calibrate") { 154 | // ibom_settings["renderDrawings"] = true; 155 | ibom_settings["renderEdgeCuts"] = true; 156 | ibom_settings["renderPads"] = true; 157 | } else { 158 | // ibom_settings["renderDrawings"] = false; 159 | ibom_settings["renderEdgeCuts"] = false; 160 | ibom_settings["renderPads"] = false; 161 | } 162 | resizeAll(); 163 | }); 164 | socket.on("board-update", (update) => { 165 | transform = update; 166 | 167 | for (let layerdict of [allcanvas.front, allcanvas.back]) { 168 | layerdict.transform.panx = layerdict.layer == "F" ? transform.tx : -transform.tx; 169 | layerdict.transform.pany = transform.ty 170 | ibom_settings.boardRotation = transform.r; 171 | layerdict.transform.zoom = transform.z; 172 | } 173 | resizeAll(); 174 | }) 175 | socket.on("udp", (data) => { 176 | probes["pos"].location = data["tippos_layout"]; 177 | probes["neg"].location = data["greytip"]; 178 | probe_end_delta = data["endpos_delta"]; 179 | drawHighlights(); 180 | 181 | calcBoardOffset(data["boardpos"]); 182 | }) 183 | socket.on("tool-selection", (data) => { 184 | console.log("tool-selection") 185 | console.log(data) 186 | if (data.selection == "multi") { 187 | // TODO multi menu 188 | multimenu_active = {"hits": data.hits, "layer": data.layer, "device": data.device} 189 | drawHighlights(); 190 | } else { 191 | probes[data.device].selection = data.selection; 192 | drawHighlights(); 193 | } 194 | }) 195 | 196 | socket.on("debug-session", (data) => { 197 | // projector page just needs simplified debug session state for now 198 | console.log(data) 199 | switch (data.event) { 200 | case "record": 201 | active_session_is_recording = data.record; 202 | if (!active_session_is_recording) { 203 | probes.pos.selection = null; 204 | probes.neg.selection = null; 205 | probes.osc.selection = null; 206 | } 207 | drawHighlights(); 208 | break; 209 | case "next": 210 | // TODO support osc 211 | console.log(active_session_is_recording) 212 | if (active_session_is_recording) { 213 | if (data.id === -1) { 214 | // deselect 215 | probes.pos.selection = null; 216 | probes.neg.selection = null; 217 | probes.osc.selection = null; 218 | drawHighlights(); 219 | } else { 220 | // show user where to measure next 221 | console.log("highlighting next!") 222 | probes.pos.selection = data.card.pos; 223 | probes.neg.selection = data.card.neg; 224 | console.log(probes.pos.selection) 225 | console.log(probes.neg.selection) 226 | drawHighlights(); 227 | } 228 | } 229 | break; 230 | } 231 | }) 232 | 233 | socket.on("config", (data) => { 234 | for (let device in data.devices) { 235 | let colors = data.devices[device]; 236 | probes[device].color.loc = colors[0]; 237 | probes[device].color.sel = colors[1]; 238 | probes[device].color.zone = colors[1]; 239 | } 240 | var root = document.documentElement; 241 | console.log(data) 242 | root.style.setProperty("--pad-color-highlight", data.padcolor); 243 | root.style.setProperty("--track-color-highlight", data.trackcolor); 244 | drawHighlights(); 245 | }) 246 | 247 | socket.on("study-event", (data) => { 248 | switch (data.event) { 249 | case "task": 250 | projectorDeselectAll(); 251 | break; 252 | case "highlight": 253 | projectorDeselectAll(); 254 | if (data.task == "1A" && data.boardviz) { 255 | projectorSelectComponent(data.refid); 256 | } else if (data.task == "1B") { 257 | projectorSelectComponent(data.refid); 258 | } 259 | break; 260 | case "success": 261 | if (data.task == "1A") { 262 | projectorSelectComponent(data.refid); 263 | } 264 | break; 265 | default: 266 | console.log(data); 267 | break; 268 | } 269 | }) 270 | } 271 | 272 | window.addEventListener("keydown", (evt) => { 273 | if (evt.key == "c") { 274 | console.log("Recalculating board offset"); 275 | boardoff_counter = 0; 276 | boardoff_sums = { 277 | "x": 0, 278 | "y": 0, 279 | "r": 0 280 | } 281 | } 282 | }) 283 | 284 | 285 | window.onload = () => { 286 | let data_urls = ["schdata", "pcbdata", "datadicts"] 287 | data_urls = data_urls.map((name) => ("http://" + window.location.host + "/" + name)) 288 | 289 | Promise.all(data_urls.map((url) => fetch(url))).then((responses) => 290 | Promise.all(responses.map((res) => res.json())) 291 | ).then((datas) => { 292 | initData(datas); 293 | 294 | initUtils(); 295 | 296 | initLayout(); 297 | 298 | initSettings(); 299 | 300 | initSocket(); 301 | 302 | resizeAll(); 303 | }).catch((e) => console.log(e)) 304 | } 305 | window.onresize = resizeAll; -------------------------------------------------------------------------------- /ardw-app/static/split.min.js: -------------------------------------------------------------------------------- 1 | /*! Split.js - v1.6.4 */ 2 | /* 3 | Copyright (c) 2020 Nathan Cahill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | */ 23 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Split=t()}(this,(function(){"use strict";var e="undefined"!=typeof window?window:null,t=null===e,n=t?void 0:e.document,i=function(){return!1},r=t?"calc":["","-webkit-","-moz-","-o-"].filter((function(e){var t=n.createElement("div");return t.style.cssText="width:"+e+"calc(9px)",!!t.style.length})).shift()+"calc",s=function(e){return"string"==typeof e||e instanceof String},o=function(e){if(s(e)){var t=n.querySelector(e);if(!t)throw new Error("Selector "+e+" did not match a DOM element");return t}return e},a=function(e,t,n){var i=e[t];return void 0!==i?i:n},u=function(e,t,n,i){if(t){if("end"===i)return 0;if("center"===i)return e/2}else if(n){if("start"===i)return 0;if("center"===i)return e/2}return e},l=function(e,t){var i=n.createElement("div");return i.className="gutter gutter-"+t,i},c=function(e,t,n){var i={};return s(t)?i[e]=t:i[e]=r+"("+t+"% - "+n+"px)",i},h=function(e,t){var n;return(n={})[e]=t+"px",n};return function(r,s){if(void 0===s&&(s={}),t)return{};var d,f,v,m,g,p,y=r;Array.from&&(y=Array.from(y));var z=o(y[0]).parentNode,S=getComputedStyle?getComputedStyle(z):null,b=S?S.flexDirection:null,E=a(s,"sizes")||y.map((function(){return 100/y.length})),_=a(s,"minSize",100),L=Array.isArray(_)?_:y.map((function(){return _})),w=a(s,"maxSize",1/0),x=Array.isArray(w)?w:y.map((function(){return w})),k=a(s,"expandToMin",!1),C=a(s,"gutterSize",10),M=a(s,"gutterAlign","center"),U=a(s,"snapOffset",30),A=a(s,"dragInterval",1),O=a(s,"direction","horizontal"),D=a(s,"cursor","horizontal"===O?"col-resize":"row-resize"),B=a(s,"gutter",l),T=a(s,"elementStyle",c),j=a(s,"gutterStyle",h);function F(e,t,n,i){var r=T(d,t,n,i);Object.keys(r).forEach((function(t){e.style[t]=r[t]}))}function R(){return p.map((function(e){return e.size}))}function N(e){return"touches"in e?e.touches[0][f]:e[f]}function q(e){var t=p[this.a],n=p[this.b],i=t.size+n.size;t.size=e/this.size*i,n.size=i-e/this.size*i,F(t.element,t.size,this._b,t.i),F(n.element,n.size,this._c,n.i)}function H(e){var t,n=p[this.a],r=p[this.b];this.dragging&&(t=N(e)-this.start+(this._b-this.dragOffset),A>1&&(t=Math.round(t/A)*A),t<=n.minSize+U+this._b?t=n.minSize+this._b:t>=this.size-(r.minSize+U+this._c)&&(t=this.size-(r.minSize+this._c)),t>=n.maxSize-U+this._b?t=n.maxSize+this._b:t<=this.size-(r.maxSize-U+this._c)&&(t=this.size-(r.maxSize+this._c)),q.call(this,t),a(s,"onDrag",i)(R()))}function I(){var e=p[this.a].element,t=p[this.b].element,n=e.getBoundingClientRect(),i=t.getBoundingClientRect();this.size=n[d]+i[d]+this._b+this._c,this.start=n[v],this.end=n[m]}function W(e){var t=function(e){if(!getComputedStyle)return null;var t=getComputedStyle(e);if(!t)return null;var n=e[g];return 0===n?null:n-="horizontal"===O?parseFloat(t.paddingLeft)+parseFloat(t.paddingRight):parseFloat(t.paddingTop)+parseFloat(t.paddingBottom)}(z);if(null===t)return e;if(L.reduce((function(e,t){return e+t}),0)>t)return e;var n=0,i=[],r=e.map((function(r,s){var o=t*r/100,a=u(C,0===s,s===e.length-1,M),l=L[s]+a;return o0&&i[r]-n>0){var o=Math.min(n,i[r]-n);n-=o,s=e-o}return s/t*100}))}function X(){var t=p[this.a].element,r=p[this.b].element;this.dragging&&a(s,"onDragEnd",i)(R()),this.dragging=!1,e.removeEventListener("mouseup",this.stop),e.removeEventListener("touchend",this.stop),e.removeEventListener("touchcancel",this.stop),e.removeEventListener("mousemove",this.move),e.removeEventListener("touchmove",this.move),this.stop=null,this.move=null,t.removeEventListener("selectstart",i),t.removeEventListener("dragstart",i),r.removeEventListener("selectstart",i),r.removeEventListener("dragstart",i),t.style.userSelect="",t.style.webkitUserSelect="",t.style.MozUserSelect="",t.style.pointerEvents="",r.style.userSelect="",r.style.webkitUserSelect="",r.style.MozUserSelect="",r.style.pointerEvents="",this.gutter.style.cursor="",this.parent.style.cursor="",n.body.style.cursor=""}function Y(t){if(!("button"in t)||0===t.button){var r=p[this.a].element,o=p[this.b].element;this.dragging||a(s,"onDragStart",i)(R()),t.preventDefault(),this.dragging=!0,this.move=H.bind(this),this.stop=X.bind(this),e.addEventListener("mouseup",this.stop),e.addEventListener("touchend",this.stop),e.addEventListener("touchcancel",this.stop),e.addEventListener("mousemove",this.move),e.addEventListener("touchmove",this.move),r.addEventListener("selectstart",i),r.addEventListener("dragstart",i),o.addEventListener("selectstart",i),o.addEventListener("dragstart",i),r.style.userSelect="none",r.style.webkitUserSelect="none",r.style.MozUserSelect="none",r.style.pointerEvents="none",o.style.userSelect="none",o.style.webkitUserSelect="none",o.style.MozUserSelect="none",o.style.pointerEvents="none",this.gutter.style.cursor=D,this.parent.style.cursor=D,n.body.style.cursor=D,I.call(this),this.dragOffset=N(t)-this.end}}"horizontal"===O?(d="width",f="clientX",v="left",m="right",g="clientWidth"):"vertical"===O&&(d="height",f="clientY",v="top",m="bottom",g="clientHeight"),E=W(E);var G=[];function J(e){var t=e.i===G.length,n=t?G[e.i-1]:G[e.i];I.call(n);var i=t?n.size-e.minSize-n._c:e.minSize+n._b;q.call(n,i)}return(p=y.map((function(e,t){var n,i={element:o(e),size:E[t],minSize:L[t],maxSize:x[t],i:t};if(t>0&&((n={a:t-1,b:t,dragging:!1,direction:O,parent:z})._b=u(C,t-1==0,!1,M),n._c=u(C,!1,t===y.length-1,M),"row-reverse"===b||"column-reverse"===b)){var r=n.a;n.a=n.b,n.b=r}if(t>0){var s=B(t,O,i.element);!function(e,t,n){var i=j(d,t,n);Object.keys(i).forEach((function(t){e.style[t]=i[t]}))}(s,C,t),n._a=Y.bind(n),s.addEventListener("mousedown",n._a),s.addEventListener("touchstart",n._a),z.insertBefore(s,i.element),n.gutter=s}return F(i.element,i.size,u(C,0===t,t===y.length-1,M),t),t>0&&G.push(n),i}))).forEach((function(e){var t=e.element.getBoundingClientRect()[d];t0){var i=G[n-1],r=p[i.a],s=p[i.b];r.size=t[n-1],s.size=e,F(r.element,r.size,i._b,r.i),F(s.element,s.size,i._c,s.i)}}))},getSizes:R,collapse:function(e){J(p[e])},destroy:function(e,t){G.forEach((function(n){if(!0!==t?n.parent.removeChild(n.gutter):(n.gutter.removeEventListener("mousedown",n._a),n.gutter.removeEventListener("touchstart",n._a)),!0!==e){var i=T(d,n.a.size,n._b);Object.keys(i).forEach((function(e){p[n.a].element.style[e]="",p[n.b].element.style[e]=""}))}}))},parent:z,pairs:G}}})); 24 | //# sourceMappingURL=split.min.js.map 25 | -------------------------------------------------------------------------------- /ardw-app/static/study.js: -------------------------------------------------------------------------------- 1 | var socket = io(); 2 | 3 | var first_with = { 4 | "1A": { 5 | "with": document.getElementById("first-with-1a"), 6 | "without": document.getElementById("first-without-1a"), 7 | }, 8 | "1B": { 9 | "with": document.getElementById("first-with-1b"), 10 | "without": document.getElementById("first-without-1b"), 11 | } 12 | } 13 | 14 | for (let task in first_with) { 15 | first_with[task].with.addEventListener("click", () => { 16 | socket.emit("study-event", {"event": "settings", "task": task, "first_with": true}); 17 | }); 18 | first_with[task].without.addEventListener("click", () => { 19 | socket.emit("study-event", {"event": "settings", "task": task, "first_with": false}); 20 | }); 21 | } 22 | 23 | var task_buttons = { 24 | "off": document.getElementById("task-off"), 25 | "1A": document.getElementById("task-1a"), 26 | "1B": document.getElementById("task-1b"), 27 | "2": document.getElementById("task-2"), 28 | } 29 | 30 | task_buttons["off"].addEventListener("click", () => { 31 | socket.emit("study-event", {"event": "task", "task": "off"}) 32 | }) 33 | task_buttons["1A"].addEventListener("click", () => { 34 | socket.emit("study-event", {"event": "task", "task": "1A"}) 35 | }) 36 | task_buttons["1B"].addEventListener("click", () => { 37 | socket.emit("study-event", {"event": "task", "task": "1B"}) 38 | }) 39 | task_buttons["2"].addEventListener("click", () => { 40 | socket.emit("study-event", {"event": "task", "task": "2"}) 41 | }) 42 | 43 | var next_button = document.getElementById("next"); 44 | next_button.addEventListener("click", goNext) 45 | 46 | var step_text = document.getElementById("step"); 47 | var status_text = document.getElementById("status"); 48 | var boardviz_text = document.getElementById("boardviz"); 49 | var comp_text = document.getElementById("comp"); 50 | 51 | var custom_input = document.getElementById("custom-input"); 52 | custom_input.addEventListener("keydown", (evt) => { 53 | if (evt.key === "Enter") { 54 | submitCustom(); 55 | } 56 | }) 57 | var custom_submit = document.getElementById("custom-btn"); 58 | custom_submit.addEventListener("click", submitCustom); 59 | 60 | window.addEventListener("keydown", (evt) => { 61 | if (evt.key == "n" && document.activeElement !== custom_input) { 62 | goNext(); 63 | } 64 | }) 65 | 66 | 67 | var timer_text = document.getElementById("timer-text"); 68 | var timer_btn = document.getElementById("timer-btn"); 69 | 70 | const TIMER_HZ = 20; 71 | var timer_on = false; 72 | var timer_task = null; 73 | var timer_val = 0; 74 | timer_btn.addEventListener("click", () => { 75 | socket.emit("study-event", {"event": "timer", "turn_on": !timer_on}) 76 | }) 77 | 78 | /** each has HTML elements btn, xin, yin and values x, y where 79 | * x and y are in "mm", but technically in optitrack pixels */ 80 | var probe_elements = { 81 | "red": {}, 82 | "grey": {}, 83 | } 84 | /** null, "red", or "grey" */ 85 | var probe_listening = null; 86 | 87 | function probe_btn_click(color) { 88 | probe_elements.red.btn.classList.remove("selected"); 89 | probe_elements.grey.btn.classList.remove("selected"); 90 | probe_elements.red.btn.innerText = "Off"; 91 | probe_elements.grey.btn.innerText = "Off"; 92 | 93 | if (probe_listening === color) { 94 | probe_listening = null; 95 | } else { 96 | probe_listening = color; 97 | probe_elements[color].btn.classList.add("selected"); 98 | probe_elements[color].btn.innerText = "On"; 99 | } 100 | } 101 | 102 | function probe_input(evt, color) { 103 | if (evt.key === "Enter") { 104 | var xoff = parseFloat(probe_elements[color].xin.value); 105 | var yoff = parseFloat(probe_elements[color].yin.value); 106 | if (isNaN(xoff)) xoff = 0; 107 | if (isNaN(yoff)) yoff = 0; 108 | socket.emit("probe-adjust", {"probe": color, "x": xoff, "y": yoff}); 109 | } 110 | } 111 | 112 | for (let color in probe_elements) { 113 | probe_elements[color]["btn"] = document.getElementById(`${color}-btn`); 114 | probe_elements[color]["xin"] = document.getElementById(`${color}-x`); 115 | probe_elements[color]["yin"] = document.getElementById(`${color}-y`); 116 | probe_elements[color]["x"] = 0; 117 | probe_elements[color]["y"] = 0; 118 | 119 | probe_elements[color].btn.addEventListener("click", () => { 120 | probe_btn_click(color); 121 | }); 122 | probe_elements[color].xin.addEventListener("keydown", (evt) => { 123 | probe_input(evt, color); 124 | }); 125 | probe_elements[color].yin.addEventListener("keydown", (evt) => { 126 | probe_input(evt, color); 127 | }); 128 | } 129 | 130 | window.addEventListener("keydown", (evt) => { 131 | if (document.activeElement !== custom_input && probe_listening !== null) { 132 | var did_something = false; 133 | var x = probe_elements[probe_listening].x; 134 | var y = probe_elements[probe_listening].y; 135 | switch (evt.key) { 136 | case "w": 137 | y -= 0.1; 138 | did_something = true; 139 | break; 140 | case "s": 141 | y += 0.1; 142 | did_something = true; 143 | break; 144 | case "a": 145 | x -= 0.1; 146 | did_something = true; 147 | break; 148 | case "d": 149 | x += 0.1; 150 | did_something = true; 151 | break; 152 | } 153 | if (did_something) { 154 | socket.emit("probe-adjust", {"probe": probe_listening, "x": x, "y": y}); 155 | } 156 | } 157 | }) 158 | 159 | 160 | function goNext() { 161 | socket.emit("study-event", {"event": "step"}); 162 | } 163 | 164 | function submitCustom() { 165 | var custom_input = document.getElementById("custom-input"); 166 | socket.emit("study-event", {"event": "note", "note": custom_input.value}); 167 | custom_input.value = ""; 168 | } 169 | 170 | 171 | function displaySeconds(time, decimal=3) { 172 | var mins = Math.floor(time / 60); 173 | var secs = time % 60; 174 | if (mins < 10) mins = "0" + mins; 175 | if (secs < 10) secs = "0" + secs.toFixed(decimal); 176 | else secs = secs.toFixed(decimal); 177 | return `${mins}:${secs}`; 178 | } 179 | 180 | socket.on("study-event", (data) => { 181 | // console.log(data) 182 | switch (data.event) { 183 | case "task": 184 | for (task in task_buttons) { 185 | task_buttons[task].classList.remove("selected"); 186 | } 187 | task_buttons[data.task].classList.add("selected"); 188 | if (data.task == "off" || data.task == "2") { 189 | step_text.firstChild.textContent = "Step N/A"; 190 | status_text.innerText = ""; 191 | } else { 192 | step_text.firstChild.textContent = "Ready to Start"; 193 | next_button.innerText = "Start"; 194 | status_text.innerText = ""; 195 | } 196 | break; 197 | case "highlight": 198 | step_text.firstChild.textContent = `Step ${data.step + 1}`; 199 | next_button.innerText = "Skip"; 200 | status_text.innerText = "In-Progress"; 201 | boardviz_text.innerText = data.boardviz ? "On" : "Off"; 202 | comp_text.innerText = data.ref; 203 | break; 204 | case "success": 205 | status_text.innerText = "Complete"; 206 | next_button.innerText = "Next"; 207 | break; 208 | case "timer": 209 | timer_on = data.on; 210 | if (data.on) { 211 | timer_btn.innerText = "Stop"; 212 | timer_val = 0; 213 | if (timer_task === null) { 214 | timer_task = window.setInterval(() => { 215 | timer_val += 1 / TIMER_HZ; 216 | timer_text.innerText = displaySeconds(timer_val); 217 | }, 1000 / TIMER_HZ) 218 | } 219 | } else { 220 | timer_btn.innerText = "Start"; 221 | clearInterval(timer_task); 222 | timer_task = null; 223 | timer_text.innerText = displaySeconds(data.time); 224 | } 225 | break; 226 | case "settings": 227 | if (data.first_with) { 228 | first_with[data.task].with.classList.add("selected"); 229 | first_with[data.task].without.classList.remove("selected"); 230 | } else { 231 | first_with[data.task].with.classList.remove("selected"); 232 | first_with[data.task].without.classList.add("selected"); 233 | } 234 | break; 235 | default: 236 | console.log(data); 237 | break; 238 | } 239 | }) 240 | 241 | socket.on("probe-adjust", (data) => { 242 | probe_elements[data.probe].x = data.x; 243 | probe_elements[data.probe].y = data.y; 244 | probe_elements[data.probe].xin.value = data.x.toFixed(1); 245 | probe_elements[data.probe].yin.value = data.y.toFixed(1); 246 | }) 247 | -------------------------------------------------------------------------------- /ardw-app/static/tool-test.js: -------------------------------------------------------------------------------- 1 | 2 | var socket = io(); 3 | 4 | window.onload = () => { 5 | document.getElementById("log").addEventListener("click", () => { 6 | socket.emit("tool-debug", { "name": "log" }); 7 | }); 8 | document.getElementById("ptr-conn").addEventListener("click", () => { 9 | var data = { 10 | "name": "tool-connect", 11 | "type": "ptr", 12 | "val": "device", 13 | "status": "success" 14 | }; 15 | socket.emit("tool-debug", data); 16 | }); 17 | document.getElementById("ptr-conn-fail").addEventListener("click", () => { 18 | var data = { 19 | "name": "tool-connect", 20 | "type": "ptr", 21 | "val": "device", 22 | "status": "fail" 23 | }; 24 | socket.emit("tool-debug", data); 25 | }); 26 | document.getElementById("ptr-select").addEventListener("click", () => { 27 | var data = { 28 | "name": "tool-measure", 29 | "type": "ptr", 30 | "coords": { 31 | "x": 100, 32 | "y": 100, 33 | } 34 | }; 35 | socket.emit("tool-debug", data); 36 | }); 37 | document.getElementById("dmm-conn").addEventListener("click", () => { 38 | var data = { 39 | "name": "tool-connect", 40 | "type": "dmm", 41 | "val": "device", 42 | "status": "success" 43 | }; 44 | socket.emit("tool-debug", data); 45 | }); 46 | document.getElementById("dmm-red-conn").addEventListener("click", () => { 47 | var data = { 48 | "name": "tool-connect", 49 | "type": "dmm", 50 | "val": "pos", 51 | "status": "success" 52 | }; 53 | socket.emit("tool-debug", data); 54 | }); 55 | document.getElementById("dmm-black-conn").addEventListener("click", () => { 56 | var data = { 57 | "name": "tool-connect", 58 | "type": "dmm", 59 | "val": "neg", 60 | "status": "success" 61 | }; 62 | socket.emit("tool-debug", data); 63 | }); 64 | document.getElementById("dmm-conn-fail").addEventListener("click", () => { 65 | var data = { 66 | "name": "tool-connect", 67 | "type": "dmm", 68 | "val": "device", 69 | "status": "fail" 70 | }; 71 | socket.emit("tool-debug", data); 72 | }); 73 | document.getElementById("dmm-red-conn-fail").addEventListener("click", () => { 74 | var data = { 75 | "name": "tool-connect", 76 | "type": "dmm", 77 | "val": "pos", 78 | "status": "fail" 79 | }; 80 | socket.emit("tool-debug", data); 81 | }); 82 | document.getElementById("dmm-black-conn-fail").addEventListener("click", () => { 83 | var data = { 84 | "name": "tool-connect", 85 | "type": "dmm", 86 | "val": "neg", 87 | "status": "fail" 88 | }; 89 | socket.emit("tool-debug", data); 90 | }); 91 | document.getElementById("dmm-measure").addEventListener("click", () => { 92 | var data = { 93 | "name": "measurement", 94 | "measurement": { 95 | "device": "dmm", 96 | "pos": { 97 | "type": "pin", 98 | "val": 148 99 | }, 100 | "neg": { 101 | "type": "pin", 102 | "val": 124 103 | }, 104 | "unit": "V", 105 | "val": 5.2 106 | } 107 | } 108 | console.log(data); 109 | socket.emit("tool-debug", data); 110 | }); 111 | }; 112 | -------------------------------------------------------------------------------- /ardw-app/static/util.js: -------------------------------------------------------------------------------- 1 | // This file is included on all webpages. 2 | // It contains settings, data, and other general global variables, 3 | // as well as a handful of utility functions. 4 | 5 | 6 | /** raw schematic json file */ 7 | var schdata; 8 | /** raw layout json file */ 9 | var pcbdata; 10 | 11 | /** schid : index in schdata.schematics */ 12 | var schid_to_idx = {}; 13 | /** ref : refid */ 14 | var ref_to_id = {}; 15 | /** 'ref.pinnum' : pinidx */ 16 | var pinref_to_idx = {}; 17 | 18 | /** refid : ref, schids, units={unitnum : schid, bbox, pins=[pinidx]} */ 19 | var compdict = {}; 20 | /** netname : schids, pins=[pinidx] */ 21 | var netdict = {}; 22 | /** pinidx : pin = {ref, name, num, pos, schid, net} */ 23 | var pindict = []; 24 | 25 | var num_schematics; 26 | var current_schematic; // schid (starts at 1) 27 | 28 | /** A handful of application settings */ 29 | var settings = { 30 | "log-error": true, 31 | "log-warning": true, 32 | "find-activate": "key", // 'key', 'auto' 33 | "find-type": "xhair", // 'zoom', 'xhair' 34 | "tool-selection-display": "icon", // 'xhair', 'icon' 35 | }; 36 | 37 | 38 | function logerr(msg) { 39 | if (settings["log-error"]) { 40 | console.log("Error: " + msg); 41 | } 42 | } 43 | 44 | function logwarn(msg) { 45 | if (settings["log-warning"]) { 46 | console.log("Warning: " + msg); 47 | } 48 | } 49 | 50 | /** 51 | * Returns the display name for the element, based on type 52 | * @param {*} element Must be {type, val} 53 | */ 54 | function getElementName(element) { 55 | if (element === null) { 56 | return ""; 57 | } 58 | switch (element.type) { 59 | case "comp": 60 | return `Component ${compdict[element.val].ref}`; 61 | case "pin": 62 | return `Pin ${pindict[element.val].ref}.${pindict[element.val].num}`; 63 | case "net": 64 | return `Net ${element.val}`; 65 | case "deselect": 66 | return "Cancel" 67 | default: 68 | return "" 69 | } 70 | } 71 | 72 | /** 73 | * Forces the given input field to only accept numeric input 74 | * @param {*} input Expects input[type="text"] 75 | */ 76 | function forceNumericInput(input) { 77 | input.addEventListener("input", () => { 78 | if (/^-?\d*.?\d*$/.test(input.value)) { 79 | // all good 80 | input.oldValue = input.value; 81 | } else { 82 | if (input.hasOwnProperty("oldValue")) { 83 | input.value = input.oldValue; 84 | } else { 85 | input.value = ""; 86 | } 87 | } 88 | }); 89 | } 90 | 91 | // ----- From IBOM web/util.js ----- // 92 | var units = { 93 | prefixes: { 94 | giga: ["G", "g", "giga", "Giga", "GIGA"], 95 | mega: ["M", "mega", "Mega", "MEGA"], 96 | kilo: ["K", "k", "kilo", "Kilo", "KILO"], 97 | milli: ["m", "milli", "Milli", "MILLI"], 98 | micro: ["U", "u", "micro", "Micro", "MICRO", "μ", "µ"], // different utf8 μ 99 | nano: ["N", "n", "nano", "Nano", "NANO"], 100 | pico: ["P", "p", "pico", "Pico", "PICO"], 101 | }, 102 | unitsShort: ["R", "r", "Ω", "F", "f", "H", "h"], 103 | unitsLong: [ 104 | "OHM", "Ohm", "ohm", "ohms", 105 | "FARAD", "Farad", "farad", 106 | "HENRY", "Henry", "henry" 107 | ], 108 | getMultiplier: function (s) { 109 | if (this.prefixes.giga.includes(s)) return 1e9; 110 | if (this.prefixes.mega.includes(s)) return 1e6; 111 | if (this.prefixes.kilo.includes(s)) return 1e3; 112 | if (this.prefixes.milli.includes(s)) return 1e-3; 113 | if (this.prefixes.micro.includes(s)) return 1e-6; 114 | if (this.prefixes.nano.includes(s)) return 1e-9; 115 | if (this.prefixes.pico.includes(s)) return 1e-12; 116 | return 1; 117 | }, 118 | valueRegex: null, 119 | } 120 | 121 | function parseValue(val, ref) { 122 | var inferUnit = (unit, ref) => { 123 | if (unit) { 124 | unit = unit.toLowerCase(); 125 | if (unit == 'Ω' || unit == "ohm" || unit == "ohms") { 126 | unit = 'r'; 127 | } 128 | unit = unit[0]; 129 | } else { 130 | ref = /^([a-z]+)\d+$/i.exec(ref); 131 | if (ref) { 132 | ref = ref[1].toLowerCase(); 133 | if (ref == "c") unit = 'f'; 134 | else if (ref == "l") unit = 'h'; 135 | else if (ref == "r" || ref == "rv") unit = 'r'; 136 | else unit = null; 137 | } 138 | } 139 | return unit; 140 | }; 141 | val = val.replace(/,/g, ""); 142 | var match = units.valueRegex.exec(val); 143 | var unit; 144 | if (match) { 145 | val = parseFloat(match[1]); 146 | if (match[2]) { 147 | val = val * units.getMultiplier(match[2]); 148 | } 149 | unit = inferUnit(match[3], ref); 150 | if (!unit) return null; 151 | else return { 152 | val: val, 153 | unit: unit, 154 | extra: match[4], 155 | } 156 | } 157 | match = units.valueAltRegex.exec(val); 158 | if (match && (match[1] || match[4])) { 159 | val = parseFloat(match[1] + "." + match[4]); 160 | if (match[3]) { 161 | val = val * units.getMultiplier(match[3]); 162 | } 163 | unit = inferUnit(match[2], ref); 164 | if (!unit) return null; 165 | else return { 166 | val: val, 167 | unit: unit, 168 | extra: match[5], 169 | } 170 | } 171 | return null; 172 | } 173 | 174 | /** Initializes some IBOM unit parsing (TODO may be unnecessary now) */ 175 | function initUtils() { 176 | var allPrefixes = units.prefixes.giga 177 | .concat(units.prefixes.mega) 178 | .concat(units.prefixes.kilo) 179 | .concat(units.prefixes.milli) 180 | .concat(units.prefixes.micro) 181 | .concat(units.prefixes.nano) 182 | .concat(units.prefixes.pico); 183 | var allUnits = units.unitsShort.concat(units.unitsLong); 184 | units.valueRegex = new RegExp("^([0-9\.]+)" + 185 | "\\s*(" + allPrefixes.join("|") + ")?" + 186 | "(" + allUnits.join("|") + ")?" + 187 | "(\\b.*)?$", ""); 188 | units.valueAltRegex = new RegExp("^([0-9]*)" + 189 | "(" + units.unitsShort.join("|") + ")?" + 190 | "([GgMmKkUuNnPp])?" + 191 | "([0-9]*)" + 192 | "(\\b.*)?$", ""); 193 | for (var bom_type of ["both", "F", "B"]) { 194 | for (var row of pcbdata.bom[bom_type]) { 195 | row.push(parseValue(row[1], row[3][0][0])); 196 | } 197 | } 198 | } 199 | // -------------------- // 200 | 201 | /** Populates the appropriate variables with data from the server, 202 | * from array corresponding to urls ["schdata", "pcbdata", "datadicts"] */ 203 | function initData(data) { 204 | schdata = data[0]; 205 | pcbdata = data[1]; 206 | 207 | schid_to_idx = data[2]["schid_to_idx"] 208 | ref_to_id = data[2]["ref_to_id"] 209 | pinref_to_idx = data[2]["pinref_to_idx"] 210 | compdict = data[2]["compdict"] 211 | netdict = data[2]["netdict"] 212 | pindict = data[2]["pindict"] 213 | } 214 | 215 | // Functions to convert between different bbox formats 216 | // list: [x1, y1, x2, y2] 217 | // obj: {"minx", "miny", "maxx", "maxy"} 218 | // pcbnew: {"pos", "relpos", "angle", "size"} 219 | 220 | /** [x1, y1, x2, y2] => [minx, miny, maxx, maxy] */ 221 | function bboxListSort(bbox) { 222 | return [ 223 | Math.min(bbox[0], bbox[2]), 224 | Math.min(bbox[1], bbox[3]), 225 | Math.max(bbox[0], bbox[2]), 226 | Math.max(bbox[1], bbox[3]) 227 | ]; 228 | } 229 | /** [x1, y1, x2, y2] => {"minx", "miny", "maxx", "maxy"} */ 230 | function bboxListToObj(bbox) { 231 | return { 232 | "minx": Math.min(bbox[0], bbox[2]), 233 | "miny": Math.min(bbox[1], bbox[3]), 234 | "maxx": Math.max(bbox[0], bbox[2]), 235 | "maxy": Math.max(bbox[1], bbox[3]) 236 | }; 237 | } 238 | /** {"minx", "miny", "maxx", "maxy"} => [x1, y1, x2, y2] */ 239 | function bboxObjToList(bbox) { 240 | return [bbox.minx, bbox.miny, bbox.maxx, bbox.maxy]; 241 | } 242 | /** {"pos", "relpos", "angle", "size"} => [x1, y1, x2, y2] */ 243 | function bboxPcbnewToList(bbox) { 244 | var corner1; 245 | var corner2; 246 | if (bbox.relpos === undefined) { 247 | // footprint.pad 248 | corner1 = [-bbox.size[0] / 2, -bbox.size[1] / 2]; 249 | corner2 = [bbox.size[1] / 2, bbox.size[1] / 2]; 250 | } else { 251 | // footprint.bbox 252 | corner1 = [bbox.relpos[0], bbox.relpos[1]]; 253 | corner2 = [bbox.relpos[0] + bbox.size[0], bbox.relpos[1] + bbox.size[1]]; 254 | corner1 = rotateVector(corner1, bbox.angle); 255 | corner2 = rotateVector(corner2, bbox.angle); 256 | } 257 | 258 | return [ 259 | Math.min(corner1[0], corner2[0]) + bbox.pos[0], 260 | Math.min(corner1[1], corner2[1]) + bbox.pos[1], 261 | Math.max(corner1[0], corner2[0]) + bbox.pos[0], 262 | Math.max(corner1[1], corner2[1]) + bbox.pos[1] 263 | ]; 264 | } 265 | /** {"pos", "relpos", "angle", "size"} => {"minx", "miny", "maxx", "maxy"} */ 266 | function bboxPcbnewToObj(bbox) { 267 | return bboxListToObj(bboxPcbnewToList(bbox)); 268 | } 269 | 270 | 271 | /** Converts page offset coords (eg. from event.offsetX) to layout coords*/ 272 | function offsetToLayoutCoords(point, layerdict) { 273 | var t = layerdict.transform; 274 | if (layerdict.layer == "B") { 275 | point[0] = (devicePixelRatio * point[0] / t.zoom - t.panx + t.x) / -t.s; 276 | } else { 277 | point[0] = (devicePixelRatio * point[0] / t.zoom - t.panx - t.x) / t.s; 278 | } 279 | point[1] = (devicePixelRatio * point[1] / t.zoom - t.y - t.pany) / t.s; 280 | return rotateVector(point, -ibom_settings.boardRotation); 281 | } 282 | /** Converts layout coords to page client coords (eg. from event.clientX) */ 283 | function layoutToClientCoords(point, layer) { 284 | var layerdict = (layer == "F" ? allcanvas.front : allcanvas.back); 285 | var t = layerdict.transform; 286 | var v = rotateVector(point, ibom_settings.boardRotation); 287 | if (layer == "B") { 288 | v[0] = (v[0] * -t.s + t.panx - t.x) * t.zoom / devicePixelRatio; 289 | } else { 290 | v[0] = (v[0] * t.s + t.panx + t.x) * t.zoom / devicePixelRatio; 291 | } 292 | v[1] = (v[1] * t.s + t.pany + t.y) * t.zoom / devicePixelRatio; 293 | var offset_parent = layerdict.bg.offsetParent; 294 | // Last step is converting from offset coords to client coords 295 | return [v[0] + offset_parent.offsetLeft, v[1] + offset_parent.offsetTop] 296 | } 297 | -------------------------------------------------------------------------------- /ardw-app/static/win7fail.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-app/static/win7fail.wav -------------------------------------------------------------------------------- /ardw-app/static/win7success.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-app/static/win7success.wav -------------------------------------------------------------------------------- /ardw-app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AR Debug Workbench Index 6 | 7 | 8 | 9 | 10 | 11 |
12 | Just links to the other pages 13 |
14 |
15 | Main Page 16 |
17 | Projector Page 18 |
19 | Tool Debug Page 20 |
21 | Study Control Panel 22 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ardw-app/templates/instrument_panel.html: -------------------------------------------------------------------------------- 1 |
2 |

Instrument Panel Test

3 |

Click button to put DMM into a mode:

4 |
5 | 6 | 7 |
8 | 9 |

Measurement

10 |
11 | 12 | 13 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ardw-app/templates/projector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AR Debug Workbench Projector View 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | 31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /ardw-app/templates/study.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AR Debug Workbench Study Control Panel 6 | 7 | 8 | 9 | 10 | 11 |

Study Control Panel

12 | 13 |

Participant

14 |
15 |
Task 1A with BoardViz
16 |
With First
17 |
Without First
18 |
19 |
20 |
Task 1B with BoardViz
21 |
With First
22 |
Without First
23 |
24 | 25 |

Task

26 |
27 |
Off
28 |
Task 1A
29 |
Task 1B
30 |
Task 2
31 |
32 | 33 |

Task 1A/B State

34 |
35 |
Step N/A
36 | 37 |
38 |
39 |
BoardViz: N/A
40 |
41 |
42 |
Current Component: 
43 |
N/A
44 |
45 | 46 |

Custom

47 |
48 | 49 |
Log
50 |
51 |
52 |
Timer: 00:00.000
53 |
Start
54 |
55 | 56 |

Manual Probe Adjustment

57 |
58 |
When a probe is turned on, use WASD to adjust its x and y
59 |
60 |
61 |
Red Probe
62 |
63 | x: 64 | 65 | mm 66 |
67 |
68 | y: 69 | 70 | mm 71 |
72 |
Off
73 |
74 |
75 |
Grey Probe
76 |
77 | x: 78 | 79 | mm 80 |
81 |
82 | y: 83 | 84 | mm 85 |
86 |
Off
87 |
88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /ardw-app/templates/tool-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tool Debug Page 6 | 7 | 8 | 9 |
10 | Click links to send server tool events 11 |
12 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /ardw-app/tools.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | 5 | class DebugCard: 6 | def __init__(self, device, pos, neg, unit=None, val=None, lo=None, hi=None) -> None: 7 | self.device: str = device 8 | self.pos: dict = pos 9 | self.neg: dict = neg 10 | self.unit: str = unit 11 | self.val: float = val 12 | self.lo: float = lo 13 | self.hi: float = hi 14 | if unit is None: 15 | self.lo = None 16 | self.hi = None 17 | 18 | def __repr__(self) -> str: 19 | return str(self.__dict__) 20 | 21 | # returns true iff self is a valid measurement update for target 22 | def will_update(self, target) -> bool: 23 | if target.is_complete(): 24 | return False 25 | return ( 26 | self.device == target.device and 27 | self.pos == target.pos and 28 | self.neg == target.neg and 29 | (target.unit is None or self.unit == target.unit) 30 | ) 31 | 32 | def is_complete(self) -> bool: 33 | return self.val is not None 34 | 35 | # 0 is inbounds, -1 is too low, +1 is too high, None is no bounds/val 36 | def inbounds(self) -> int: 37 | if self.val is None or (self.lo is None and self.hi is None): 38 | return None 39 | elif self.lo is not None and self.val < self.lo: 40 | return -1 41 | elif self.hi is not None and self.val > self.hi: 42 | return 1 43 | else: 44 | return 0 45 | 46 | def to_dict(self) -> dict: 47 | return self.__dict__ 48 | 49 | 50 | class DebugSession: 51 | def __init__(self, name="", notes="") -> None: 52 | self.name: str = name 53 | self.notes: str = notes 54 | self.timestamp: str = time.strftime("%H:%M:%S", time.localtime()) 55 | self.cards: list[DebugCard] = [] 56 | self.next: int = -1 57 | 58 | def __repr__(self) -> str: 59 | return str(self.__dict__) 60 | 61 | def add_card(self, card: DebugCard) -> int: 62 | i = len(self.cards) 63 | self.cards.append(card) 64 | if not card.is_complete() and self.next == -1: 65 | self.next = i 66 | return i 67 | 68 | def remove_card(self, i) -> DebugCard: 69 | if self.next == i: 70 | self.__update_next(i + 1) 71 | return self.cards.pop(i) 72 | 73 | def get_next(self): 74 | if self.next == -1: 75 | return -1, None 76 | else: 77 | return self.next, self.cards[self.next] 78 | 79 | # returns the resulting card, the id of the card, and update flag 80 | def measure(self, device, pos, neg, unit, val): 81 | measure_card = DebugCard(device, pos, neg, unit, val) 82 | # check if we have a card for this measurement 83 | for i, card in enumerate(self.cards): 84 | if measure_card.will_update(card): 85 | card.unit = measure_card.unit 86 | card.val = measure_card.val 87 | if self.next == i: 88 | self.__update_next(i + 1) 89 | return card, i, True 90 | 91 | # didn't have a matching card, so just append to end of deck 92 | self.cards.append(measure_card) 93 | return measure_card, len(self.cards) - 1, False 94 | 95 | def export(self) -> None: 96 | logging.info("Export WIP") 97 | logging.info(str(self)) 98 | 99 | def to_dict(self) -> dict: 100 | output = self.__dict__.copy() 101 | output["cards"] = [] 102 | for card in self.cards: 103 | output["cards"].append(card.__dict__) 104 | return output 105 | 106 | def __update_next(self, start=0): 107 | self.next = -1 108 | for i in range(start, len(self.cards)): 109 | if not self.cards[i].is_complete(): 110 | self.next = i 111 | break 112 | -------------------------------------------------------------------------------- /ardw-plugin/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Registers the plugin for KiCad 3 | from .ardw_action import ARDebugWorkbenchPlugin 4 | ARDebugWorkbenchPlugin().register() 5 | -------------------------------------------------------------------------------- /ardw-plugin/__main__.py: -------------------------------------------------------------------------------- 1 | 2 | # TODO this is for the command line version of plugin 3 | -------------------------------------------------------------------------------- /ardw-plugin/ardw_action.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import logging 4 | import os 5 | import sys 6 | import time 7 | import traceback 8 | 9 | import pcbnew 10 | from .ibom.pcbparser import PcbnewParser, generate_bom, round_floats 11 | from .ibom.pcbparser import Config 12 | from .core import parseProject, fix_file 13 | 14 | class ARDebugWorkbenchPlugin(pcbnew.ActionPlugin, object): 15 | 16 | def defaults(self): 17 | self.name = "AR Debug Workbench Plugin" 18 | self.category = "Read PCB" 19 | self.pcbnew_icon_support = hasattr(self, "show_toolbar_button") 20 | self.show_toolbar_button = True 21 | self.icon_file_name = os.path.join(os.path.dirname(__file__), 'icon.png') 22 | self.description = "Extracts pcbnew data for AR Debug Workbench" 23 | 24 | def Run(self): 25 | 26 | plugin_dir = os.path.dirname(os.path.realpath(__file__)) 27 | log_file = os.path.join(plugin_dir, "ardw_plugin.log") 28 | 29 | logging.basicConfig(level=logging.DEBUG, 30 | filename=log_file, 31 | filemode='w', 32 | format='%(asctime)s %(name)s %(lineno)d:%(message)s', 33 | datefmt='%m-%d %H:%M:%S') 34 | logger = logging.getLogger("ARDW Plugin") 35 | 36 | logger.info("Plugin executed on: " + repr(sys.platform)) 37 | logger.info("Plugin executed with python version: " + repr(sys.version)) 38 | 39 | try: 40 | config = Config("test") 41 | 42 | board = pcbnew.GetBoard() 43 | pcb_file_path = board.GetFileName() 44 | parser = PcbnewParser(pcb_file_path, config, logger, board) 45 | 46 | project_dir = os.path.dirname(os.path.realpath(pcb_file_path)) 47 | output_dir = os.path.join(project_dir, "ardw") 48 | try: 49 | os.makedirs(output_dir) 50 | except OSError: 51 | if not os.path.isdir(output_dir): 52 | raise 53 | 54 | sch_json_path = os.path.join(output_dir, "schdata.json") 55 | parseProject(logger, pcb_file_path, sch_json_path) 56 | 57 | logger.info("Parsing pcbdata") 58 | pcbdata, components = parser.parse() 59 | if not pcbdata and not components: 60 | logger.error("Failed to parse PCB data") 61 | 62 | logger.info("Generating bom data") 63 | pcbdata["bom"] = generate_bom(components, config) 64 | pcbdata_str = json.dumps(round_floats(pcbdata, 6)) 65 | 66 | 67 | pcb_json_path = os.path.join(output_dir, "pcbdata.json") 68 | logger.info("Writing result to {}".format(pcb_json_path)) 69 | 70 | with open(pcb_json_path, "w") as json_file: 71 | json_file.write(pcbdata_str) 72 | 73 | logger.info("Checking for svgs and fixing") 74 | for filename in os.listdir(output_dir): 75 | if filename.endswith(".svg"): 76 | filepath = os.path.join(output_dir, filename) 77 | logger.info("Fixing {}".format(filepath)) 78 | fix_file(filepath) 79 | 80 | logger.info("Done") 81 | 82 | except Exception as _: 83 | logger.error(traceback.format_exc()) 84 | 85 | 86 | -------------------------------------------------------------------------------- /ardw-plugin/clear_pyc.sh: -------------------------------------------------------------------------------- 1 | find . -name "*.pyc" -type f -delete 2 | rm ardw_plugin.log 3 | -------------------------------------------------------------------------------- /ardw-plugin/core/__init__.py: -------------------------------------------------------------------------------- 1 | # Takes the board file path from pcbnew and figures out 2 | # where the .sch, .lib, and .net files are for sch_reader.py 3 | 4 | import os 5 | 6 | from .sch_reader import parseFiles, outputJSON 7 | from .svg_fix import fix_file 8 | 9 | # Takes the path to the project's .kicad_pcb file 10 | # from pcbnew.GetBoard().GetFileName() 11 | # libpaths holds real paths to any external lib files 12 | """ 13 | Given a board file, figures out where the important files are 14 | and runs parseFiles(), writing the result to output_path 15 | Inputs: logger -- logger to write errors, etc 16 | pcb_file_path -- real path to board, from pcbnew.py 17 | output_path -- real path to output file 18 | extlibpaths -- list of real paths to lib files not in the main directory 19 | Output: none 20 | """ 21 | def parseProject(logger, pcb_file_path, output_path, extlibpaths=[]): 22 | 23 | project_dir = os.path.dirname(os.path.realpath(pcb_file_path)) 24 | 25 | project_name = os.path.split(pcb_file_path)[1][:-10] 26 | 27 | schfiles = [] 28 | libfiles = [] 29 | for filename in os.listdir(project_dir): 30 | if filename.endswith(".sch"): 31 | schname = str(filename[:-4]) 32 | if schname != project_name: 33 | schfiles.append(schname) 34 | elif filename.endswith(".lib"): 35 | libname = str(filename[:-4]) 36 | if libname != (project_name + "-cache"): 37 | libfiles.append(libname) 38 | 39 | logger.info("Parsing '{}' at {}".format(project_name, project_dir)) 40 | schlist, netlist = parseFiles(project_dir, project_name, schfiles, libfiles) 41 | logger.info("Writing result to {}".format(output_path)) 42 | outputJSON(output_path, schlist, netlist) 43 | -------------------------------------------------------------------------------- /ardw-plugin/core/svg_fix.py: -------------------------------------------------------------------------------- 1 | # Changes svg width/height to be proportional to their viewbox 2 | # This is necessary for the scaling and zooming of the schematic to work 3 | 4 | import os 5 | import re 6 | import sys 7 | 8 | def fix_file(svgpath, outpath=None): 9 | if not os.path.isfile(svgpath): 10 | print("Error: file not found at {}".format(svgpath)) 11 | return 12 | 13 | with open(svgpath, "r") as file: 14 | data = file.readlines() 15 | 16 | for linenum in range(len(data)): 17 | line = data[linenum] 18 | matchpattern = 'width="(.*)" height="(.*)" viewBox=" *([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*) *"' 19 | match = re.search(matchpattern, line) 20 | if not match is None: 21 | print("Match found in line {}".format(linenum)) 22 | width = int(match.groups()[4]) // 10 23 | height = int(match.groups()[5]) // 10 24 | replacepattern = 'width="{}" height="{}" viewBox="\g<3> \g<4> \g<5> \g<6>"'.format(width, height) 25 | data[linenum] = re.sub(matchpattern, replacepattern, line) 26 | break 27 | 28 | if outpath is None: 29 | outpath = svgpath 30 | with open(outpath, "w") as file: 31 | file.writelines(data) 32 | 33 | 34 | if __name__ == "__main__": 35 | if len(sys.argv) < 2: 36 | sys.exit("Usage: python svg_fix.py ") 37 | 38 | svgpath = sys.argv[1] 39 | 40 | fix_file(svgpath) 41 | 42 | 43 | -------------------------------------------------------------------------------- /ardw-plugin/ibom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-plugin/ibom/__init__.py -------------------------------------------------------------------------------- /ardw-plugin/ibom/common.py: -------------------------------------------------------------------------------- 1 | # Taken as is from Interactive HTML BOM 2 | # https://github.com/openscopeproject/InteractiveHtmlBom 3 | 4 | import math 5 | 6 | from .svgpath import parse_path 7 | 8 | 9 | class EcadParser(object): 10 | 11 | def __init__(self, file_name, config, logger): 12 | """ 13 | :param file_name: path to file that should be parsed. 14 | :param config: Config instance 15 | :param logger: logging object. 16 | """ 17 | self.file_name = file_name 18 | self.config = config 19 | self.logger = logger 20 | self.extra_data_func = lambda f, b: ([], {}) 21 | 22 | def parse(self): 23 | """ 24 | Abstract method that should be overridden in implementations. 25 | Performs all the parsing and returns a tuple of 26 | (pcbdata, components) 27 | pcbdata is described in DATAFORMAT.md 28 | components is list of Component objects 29 | :return: 30 | """ 31 | pass 32 | 33 | def latest_extra_data(self, extra_dirs=None): 34 | """ 35 | Abstract method that may be overridden in implementations that support 36 | extra field data. 37 | :param extra_dirs: List of extra directories to search. 38 | :return: File name of most recent file with extra field data. 39 | """ 40 | return None 41 | 42 | def add_drawing_bounding_box(self, drawing, bbox): 43 | # type: (dict, BoundingBox) -> None 44 | 45 | def add_segment(): 46 | bbox.add_segment(drawing['start'][0], drawing['start'][1], 47 | drawing['end'][0], drawing['end'][1], 48 | drawing['width'] / 2) 49 | 50 | def add_circle(): 51 | bbox.add_circle(drawing['start'][0], drawing['start'][1], 52 | drawing['radius'] + drawing['width'] / 2) 53 | 54 | def add_svgpath(): 55 | width = drawing.get('width', 0) 56 | bbox.add_svgpath(drawing['svgpath'], width, self.logger) 57 | 58 | def add_polygon(): 59 | if 'polygons' not in drawing: 60 | add_svgpath() 61 | return 62 | polygon = drawing['polygons'][0] 63 | for point in polygon: 64 | bbox.add_point(point[0], point[1]) 65 | 66 | { 67 | 'segment': add_segment, 68 | 'circle': add_circle, 69 | 'arc': add_svgpath, 70 | 'polygon': add_polygon, 71 | 'text': lambda: None, # text is not really needed for bounding box 72 | }.get(drawing['type'])() 73 | 74 | 75 | class Component(object): 76 | """Simple data object to store component data needed for bom table.""" 77 | 78 | def __init__(self, ref, val, footprint, layer, attr=None, extra_fields={}): 79 | self.ref = ref 80 | self.val = val 81 | self.footprint = footprint 82 | self.layer = layer 83 | self.attr = attr 84 | self.extra_fields = extra_fields 85 | 86 | 87 | class BoundingBox(object): 88 | """Geometry util to calculate and compound bounding box of simple shapes.""" 89 | 90 | def __init__(self): 91 | self._x0 = None 92 | self._y0 = None 93 | self._x1 = None 94 | self._y1 = None 95 | 96 | def to_dict(self): 97 | # type: () -> dict 98 | return { 99 | "minx": self._x0, 100 | "miny": self._y0, 101 | "maxx": self._x1, 102 | "maxy": self._y1, 103 | } 104 | 105 | def to_component_dict(self): 106 | # type: () -> dict 107 | return { 108 | "pos": [self._x0, self._y0], 109 | "relpos": [0, 0], 110 | "size": [self._x1 - self._x0, self._y1 - self._y0], 111 | "angle": 0, 112 | } 113 | 114 | def add(self, other): 115 | """Add another bounding box. 116 | :type other: BoundingBox 117 | """ 118 | if other._x0 is not None: 119 | self.add_point(other._x0, other._y0) 120 | self.add_point(other._x1, other._y1) 121 | return self 122 | 123 | @staticmethod 124 | def _rotate(x, y, rx, ry, angle): 125 | sin = math.sin(math.radians(angle)) 126 | cos = math.cos(math.radians(angle)) 127 | new_x = rx + (x - rx) * cos - (y - ry) * sin 128 | new_y = ry + (x - rx) * sin + (y - ry) * cos 129 | return new_x, new_y 130 | 131 | def add_point(self, x, y, rx=0, ry=0, angle=0): 132 | x, y = self._rotate(x, y, rx, ry, angle) 133 | if self._x0 is None: 134 | self._x0 = x 135 | self._y0 = y 136 | self._x1 = x 137 | self._y1 = y 138 | else: 139 | self._x0 = min(self._x0, x) 140 | self._y0 = min(self._y0, y) 141 | self._x1 = max(self._x1, x) 142 | self._y1 = max(self._y1, y) 143 | return self 144 | 145 | def add_segment(self, x0, y0, x1, y1, r): 146 | self.add_circle(x0, y0, r) 147 | self.add_circle(x1, y1, r) 148 | return self 149 | 150 | def add_rectangle(self, x, y, w, h, angle=0): 151 | self.add_point(x - w / 2, y - h / 2, x, y, angle) 152 | self.add_point(x + w / 2, y - h / 2, x, y, angle) 153 | self.add_point(x - w / 2, y + h / 2, x, y, angle) 154 | self.add_point(x + w / 2, y + h / 2, x, y, angle) 155 | return self 156 | 157 | def add_circle(self, x, y, r): 158 | self.add_point(x - r, y) 159 | self.add_point(x, y - r) 160 | self.add_point(x + r, y) 161 | self.add_point(x, y + r) 162 | return self 163 | 164 | def add_svgpath(self, svgpath, width, logger): 165 | w = width / 2 166 | for segment in parse_path(svgpath, logger): 167 | x0, x1, y0, y1 = segment.bbox() 168 | self.add_point(x0 - w, y0 - w) 169 | self.add_point(x1 + w, y1 + w) 170 | 171 | def pad(self, amount): 172 | """Add small padding to the box.""" 173 | if self._x0 is not None: 174 | self._x0 -= amount 175 | self._y0 -= amount 176 | self._x1 += amount 177 | self._y1 += amount 178 | 179 | def initialized(self): 180 | return self._x0 is not None 181 | -------------------------------------------------------------------------------- /ardw-plugin/ibom/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-plugin/ibom/core/__init__.py -------------------------------------------------------------------------------- /ardw-plugin/ibom/core/fontparser.py: -------------------------------------------------------------------------------- 1 | # Taken as is from Interactive HTML BOM 2 | # https://github.com/openscopeproject/InteractiveHtmlBom 3 | 4 | from .newstroke_font import NEWSTROKE_FONT 5 | 6 | 7 | class FontParser: 8 | STROKE_FONT_SCALE = 1.0 / 21.0 9 | FONT_OFFSET = -10 10 | 11 | def __init__(self): 12 | self.parsed_font = {} 13 | 14 | def parse_font_char(self, chr): 15 | lines = [] 16 | line = [] 17 | glyph_x = 0 18 | index = ord(chr) - ord(' ') 19 | if index >= len(NEWSTROKE_FONT): 20 | index = ord('?') - ord(' ') 21 | glyph_str = NEWSTROKE_FONT[index] 22 | for i in range(0, len(glyph_str), 2): 23 | coord = glyph_str[i:i + 2] 24 | 25 | # The first two values contain the width of the char 26 | if i < 2: 27 | glyph_x = (ord(coord[0]) - ord('R')) * self.STROKE_FONT_SCALE 28 | glyph_width = (ord(coord[1]) - ord(coord[0])) * self.STROKE_FONT_SCALE 29 | elif coord[0] == ' ' and coord[1] == 'R': 30 | lines.append(line) 31 | line = [] 32 | else: 33 | line.append([ 34 | (ord(coord[0]) - ord('R')) * self.STROKE_FONT_SCALE - glyph_x, 35 | (ord(coord[1]) - ord('R') + self.FONT_OFFSET) * self.STROKE_FONT_SCALE 36 | ]) 37 | 38 | if len(line) > 0: 39 | lines.append(line) 40 | 41 | return { 42 | 'w': glyph_width, 43 | 'l': lines 44 | } 45 | 46 | def parse_font_for_string(self, s): 47 | for c in s: 48 | if c == '\t' and ' ' not in self.parsed_font: 49 | # tabs rely on space char to calculate offset 50 | self.parsed_font[' '] = self.parse_font_char(' ') 51 | if c not in self.parsed_font and ord(c) >= ord(' '): 52 | self.parsed_font[c] = self.parse_font_char(c) 53 | 54 | def get_parsed_font(self): 55 | return self.parsed_font 56 | -------------------------------------------------------------------------------- /ardw-plugin/ibom/core/units.py: -------------------------------------------------------------------------------- 1 | # _*_ coding:utf-8 _*_ 2 | 3 | # Stolen from https://github.com/SchrodingersGat/KiBoM/blob/master/KiBOM/units.py 4 | 5 | """ 6 | 7 | This file contains a set of functions for matching values which may be written 8 | in different formats e.g. 9 | 0.1uF = 100n (different suffix specified, one has missing unit) 10 | 0R1 = 0.1Ohm (Unit replaces decimal, different units) 11 | 12 | """ 13 | 14 | import re 15 | import locale 16 | 17 | current_locale = locale.setlocale(locale.LC_NUMERIC) 18 | try: 19 | locale.setlocale(locale.LC_NUMERIC, '') 20 | except Exception as ignore: 21 | # sometimes setlocale with empty string doesn't work on OSX 22 | pass 23 | decimal_separator = locale.localeconv()['decimal_point'] 24 | locale.setlocale(locale.LC_NUMERIC, current_locale) 25 | 26 | PREFIX_MICRO = [u"μ", "u", "micro"] 27 | PREFIX_MILLI = ["milli", "m"] 28 | PREFIX_NANO = ["nano", "n"] 29 | PREFIX_PICO = ["pico", "p"] 30 | PREFIX_KILO = ["kilo", "k"] 31 | PREFIX_MEGA = ["mega", "meg"] 32 | PREFIX_GIGA = ["giga", "g"] 33 | 34 | # All prefices 35 | PREFIX_ALL = PREFIX_PICO + PREFIX_NANO + PREFIX_MICRO + \ 36 | PREFIX_MILLI + PREFIX_KILO + PREFIX_MEGA + PREFIX_GIGA 37 | 38 | # Common methods of expressing component units 39 | UNIT_R = ["r", "ohms", "ohm", u"Ω"] 40 | UNIT_C = ["farad", "f"] 41 | UNIT_L = ["henry", "h"] 42 | 43 | UNIT_ALL = UNIT_R + UNIT_C + UNIT_L 44 | 45 | """ 46 | Return a simplified version of a units string, for comparison purposes 47 | """ 48 | 49 | 50 | def getUnit(unit): 51 | if not unit: 52 | return None 53 | 54 | unit = unit.lower() 55 | 56 | if unit in UNIT_R: 57 | return "R" 58 | if unit in UNIT_C: 59 | return "F" 60 | if unit in UNIT_L: 61 | return "H" 62 | 63 | return None 64 | 65 | 66 | """ 67 | Return the (numerical) value of a given prefix 68 | """ 69 | 70 | 71 | def getPrefix(prefix): 72 | if not prefix: 73 | return 1 74 | 75 | prefix = prefix.lower() 76 | 77 | if prefix in PREFIX_PICO: 78 | return 1.0e-12 79 | if prefix in PREFIX_NANO: 80 | return 1.0e-9 81 | if prefix in PREFIX_MICRO: 82 | return 1.0e-6 83 | if prefix in PREFIX_MILLI: 84 | return 1.0e-3 85 | if prefix in PREFIX_KILO: 86 | return 1.0e3 87 | if prefix in PREFIX_MEGA: 88 | return 1.0e6 89 | if prefix in PREFIX_GIGA: 90 | return 1.0e9 91 | 92 | return 1 93 | 94 | 95 | def groupString(group): # return a reg-ex string for a list of values 96 | return "|".join(group) 97 | 98 | 99 | def matchString(): 100 | return "^([0-9\.]+)(" + groupString(PREFIX_ALL) + ")*(" + groupString( 101 | UNIT_ALL) + ")*(\d*)$" 102 | 103 | 104 | """ 105 | Return a normalized value and units for a given component value string 106 | e.g. compMatch("10R2") returns (10, R) 107 | e.g. compMatch("3.3mOhm") returns (0.0033, R) 108 | """ 109 | 110 | 111 | def compMatch(component): 112 | component = component.strip().lower() 113 | if decimal_separator == ',': 114 | # replace separator with dot 115 | component = component.replace(",", ".") 116 | else: 117 | # remove thousands separator 118 | component = component.replace(",", "") 119 | match = matchString() 120 | result = re.search(match, component) 121 | 122 | if not result: 123 | return None 124 | 125 | if not len(result.groups()) == 4: 126 | return None 127 | 128 | value, prefix, units, post = result.groups() 129 | 130 | # special case where units is in the middle of the string 131 | # e.g. "0R05" for 0.05Ohm 132 | # in this case, we will NOT have a decimal 133 | # we will also have a trailing number 134 | 135 | if post and "." not in value: 136 | try: 137 | value = float(int(value)) 138 | postValue = float(int(post)) / (10 ** len(post)) 139 | value = value * 1.0 + postValue 140 | except: 141 | return None 142 | 143 | try: 144 | val = float(value) 145 | except: 146 | return None 147 | 148 | val = "{0:.15f}".format(val * 1.0 * getPrefix(prefix)) 149 | 150 | return (val, getUnit(units)) 151 | 152 | 153 | def componentValue(valString): 154 | result = compMatch(valString) 155 | 156 | if not result: 157 | return valString, None # return the same string back with `None` unit 158 | 159 | if not len(result) == 2: # result length is incorrect 160 | return valString, None # return the same string back with `None` unit 161 | 162 | return result # (val,unit) 163 | 164 | 165 | # compare two values 166 | 167 | 168 | def compareValues(c1, c2): 169 | r1 = compMatch(c1) 170 | r2 = compMatch(c2) 171 | 172 | if not r1 or not r2: 173 | return False 174 | 175 | (v1, u1) = r1 176 | (v2, u2) = r2 177 | 178 | if v1 == v2: 179 | # values match 180 | if u1 == u2: 181 | return True # units match 182 | if not u1: 183 | return True # no units for component 1 184 | if not u2: 185 | return True # no units for component 2 186 | 187 | return False 188 | -------------------------------------------------------------------------------- /ardw-plugin/ibom/kicad_extra/__init__.py: -------------------------------------------------------------------------------- 1 | # Taken as is from Interactive HTML BOM 2 | # https://github.com/openscopeproject/InteractiveHtmlBom 3 | 4 | import os 5 | 6 | from .xmlparser import XmlParser 7 | from .netlistparser import NetlistParser 8 | 9 | PARSERS = { 10 | '.xml': XmlParser, 11 | '.net': NetlistParser 12 | } 13 | 14 | 15 | def parse_schematic_data(file_name, normalize_case): 16 | if not os.path.isfile(file_name): 17 | return None 18 | extension = os.path.splitext(file_name)[1] 19 | if extension not in PARSERS: 20 | return None 21 | else: 22 | parser = PARSERS[extension](file_name) 23 | return parser.parse(normalize_case) 24 | 25 | 26 | def find_latest_schematic_data(base_name, directories): 27 | """ 28 | :param base_name: base name of pcb file 29 | :param directories: list of directories to search 30 | :return: last modified parsable file path or None if not found 31 | """ 32 | files = [] 33 | for d in directories: 34 | files.extend(_find_in_dir(d)) 35 | # sort by decreasing modification time 36 | files = sorted(files, reverse=True) 37 | if files: 38 | # try to find first (last modified) file that has name matching pcb file 39 | for _, f in files: 40 | if os.path.splitext(os.path.basename(f))[0] == base_name: 41 | return f 42 | # if no such file is found just return last modified 43 | return files[0][1] 44 | else: 45 | return None 46 | 47 | 48 | def _find_in_dir(dir): 49 | _, _, files = next(os.walk(dir), (None, None, [])) 50 | # filter out files that we can not parse 51 | files = [f for f in files if os.path.splitext(f)[1] in PARSERS.keys()] 52 | files = [os.path.join(dir, f) for f in files] 53 | # get their modification time and sort in descending order 54 | return [(os.path.getmtime(f), f) for f in files] 55 | -------------------------------------------------------------------------------- /ardw-plugin/ibom/kicad_extra/netlistparser.py: -------------------------------------------------------------------------------- 1 | # Taken as is from Interactive HTML BOM 2 | # https://github.com/openscopeproject/InteractiveHtmlBom 3 | 4 | import io 5 | 6 | from .parser_base import ParserBase 7 | from .sexpressions import parse_sexpression 8 | 9 | 10 | class NetlistParser(ParserBase): 11 | def get_extra_field_data(self): 12 | with io.open(self.file_name, 'r', encoding='utf-8') as f: 13 | sexpression = parse_sexpression(f.read()) 14 | components = None 15 | for s in sexpression: 16 | if s[0] == 'components': 17 | components = s[1:] 18 | if components is None: 19 | return None 20 | field_set = set() 21 | comp_dict = {} 22 | for c in components: 23 | ref = None 24 | fields = None 25 | datasheet = None 26 | libsource = None 27 | for f in c[1:]: 28 | if f[0] == 'ref': 29 | ref = f[1] 30 | if f[0] == 'fields': 31 | fields = f[1:] 32 | if f[0] == 'datasheet': 33 | datasheet = f[1] 34 | if f[0] == 'libsource': 35 | libsource = f[1:] 36 | if ref is None: 37 | return None 38 | ref_fields = comp_dict.setdefault(ref, {}) 39 | if datasheet and datasheet != '~': 40 | field_set.add('Datasheet') 41 | ref_fields['Datasheet'] = datasheet 42 | if libsource is not None: 43 | for lib_field in libsource: 44 | if lib_field[0] == 'description': 45 | field_set.add('Description') 46 | ref_fields['Description'] = lib_field[1] 47 | if fields is None: 48 | continue 49 | for f in fields: 50 | if len(f) > 1: 51 | field_set.add(f[1][1]) 52 | if len(f) > 2: 53 | ref_fields[f[1][1]] = f[2] 54 | 55 | return list(field_set), comp_dict 56 | -------------------------------------------------------------------------------- /ardw-plugin/ibom/kicad_extra/parser_base.py: -------------------------------------------------------------------------------- 1 | # Taken as is from Interactive HTML BOM 2 | # https://github.com/openscopeproject/InteractiveHtmlBom 3 | 4 | class ParserBase: 5 | 6 | def __init__(self, file_name): 7 | """ 8 | :param file_name: path to file that should be parsed. 9 | """ 10 | self.file_name = file_name 11 | 12 | @staticmethod 13 | def normalize_field_names(data): 14 | field_map = {f.lower(): f for f in reversed(data[0])} 15 | 16 | def remap(ref_fields): 17 | return {field_map[f.lower()]: v for (f, v) in 18 | sorted(ref_fields.items(), reverse=True)} 19 | 20 | field_data = {r: remap(d) for (r, d) in data[1].items()} 21 | return field_map.values(), field_data 22 | 23 | def parse(self, normalize_case): 24 | data = self.get_extra_field_data() 25 | if data is None: 26 | return None 27 | if normalize_case: 28 | data = self.normalize_field_names(data) 29 | return sorted(data[0]), data[1] 30 | 31 | def get_extra_field_data(self): 32 | # type: () -> tuple 33 | """ 34 | Parses the file and returns extra field data. 35 | :return: tuple of the format 36 | ( 37 | [field_name1, field_name2,... ], 38 | { 39 | ref1: { 40 | field_name1: field_value1, 41 | field_name2: field_value2, 42 | ... 43 | ], 44 | ref2: ... 45 | } 46 | ) 47 | """ 48 | pass 49 | -------------------------------------------------------------------------------- /ardw-plugin/ibom/kicad_extra/sexpressions.py: -------------------------------------------------------------------------------- 1 | # Taken as is from Interactive HTML BOM 2 | # https://github.com/openscopeproject/InteractiveHtmlBom 3 | 4 | import re 5 | 6 | term_regex = r'''(?mx) 7 | \s*(?: 8 | (?P\()| 9 | (?P\))| 10 | (?P"(?:\\\\|\\"|[^"])*")| 11 | (?P[^(^)\s]+) 12 | )''' 13 | pattern = re.compile(term_regex) 14 | 15 | 16 | def parse_sexpression(sexpression): 17 | stack = [] 18 | out = [] 19 | for terms in pattern.finditer(sexpression): 20 | term, value = [(t, v) for t, v in terms.groupdict().items() if v][0] 21 | if term == 'open': 22 | stack.append(out) 23 | out = [] 24 | elif term == 'close': 25 | assert stack, "Trouble with nesting of brackets" 26 | tmp, out = out, stack.pop(-1) 27 | out.append(tmp) 28 | elif term == 'sq': 29 | out.append(value[1:-1].replace('\\\\', '\\').replace('\\"', '"')) 30 | elif term == 's': 31 | out.append(value) 32 | else: 33 | raise NotImplementedError("Error: %s, %s" % (term, value)) 34 | assert not stack, "Trouble with nesting of brackets" 35 | return out[0] 36 | -------------------------------------------------------------------------------- /ardw-plugin/ibom/kicad_extra/xmlparser.py: -------------------------------------------------------------------------------- 1 | # Taken as is from Interactive HTML BOM 2 | # https://github.com/openscopeproject/InteractiveHtmlBom 3 | 4 | from xml.dom import minidom 5 | 6 | from .parser_base import ParserBase 7 | 8 | 9 | class XmlParser(ParserBase): 10 | @staticmethod 11 | def get_text(nodelist): 12 | rc = [] 13 | for node in nodelist: 14 | if node.nodeType == node.TEXT_NODE: 15 | rc.append(node.data) 16 | return ''.join(rc) 17 | 18 | def get_extra_field_data(self): 19 | xml = minidom.parse(self.file_name) 20 | components = xml.getElementsByTagName('comp') 21 | field_set = set() 22 | comp_dict = {} 23 | for c in components: 24 | ref_fields = comp_dict.setdefault(c.attributes['ref'].value, {}) 25 | datasheet = c.getElementsByTagName('datasheet') 26 | if datasheet: 27 | datasheet = self.get_text(datasheet[0].childNodes) 28 | if datasheet != '~': 29 | field_set.add('Datasheet') 30 | ref_fields['Datasheet'] = datasheet 31 | libsource = c.getElementsByTagName('libsource') 32 | if libsource and libsource[0].hasAttribute('description'): 33 | field_set.add('Description') 34 | attr = libsource[0].attributes['description'] 35 | ref_fields['Description'] = attr.value 36 | for f in c.getElementsByTagName('field'): 37 | name = f.attributes['name'].value 38 | field_set.add(name) 39 | ref_fields[name] = self.get_text(f.childNodes) 40 | 41 | return list(field_set), comp_dict 42 | -------------------------------------------------------------------------------- /ardw-plugin/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/ardw-plugin/icon.png -------------------------------------------------------------------------------- /cv/projector_calibration.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 as cv 3 | import math 4 | 5 | #size of projector frame 6 | frameSize = [1920, 1080] 7 | # size of checkerboard squares to project 8 | gridSize = [4, 3] 9 | numCaptures = (gridSize[0]+1) * (gridSize[1]+1) 10 | 11 | # world points in homogeneous coordinates 12 | """ 13 | [X_1 X_2 X_3 ... X_n] 14 | [Y_1 Y_2 Y_3 ... Y_n] 15 | [Z_1 Z_2 Z_3 ... Z_n] 16 | [ 1 1 1 ... 1 ] 17 | """ 18 | capturedWorldPoints = np.ones((4, numCaptures)) 19 | 20 | #image points in homogeneous coordinates 21 | """ 22 | [u_1 u_2 u_3 ... u_n] 23 | [v_1 v_2 v_3 ... v_n] 24 | [ 1 1 1 ... 1 ] 25 | """ 26 | imagePoints = np.ones((3, numCaptures)) 27 | 28 | #Projection matrix 29 | projectionMatrix = np.empty((3, 4)) 30 | 31 | 32 | 33 | checkerBoard = np.zeros((frameSize[1], frameSize[0])) 34 | 35 | def makeCheckerboard(gridSize, frameSize): 36 | if frameSize[0] % gridSize[0] != 0: 37 | print("Frame width %i is not a multiple of checkerboard width %i. Use exact divsor for maximum precision." % (frameSize[0], gridSize[0])) 38 | 39 | if frameSize[1] % gridSize[1] != 0: 40 | print("Frame height %i is not a multiple of checkerboard height %i. Use exact divsor for maximum precision." % (frameSize[1], gridSize[1])) 41 | 42 | for row in range(0, np.shape(checkerBoard)[0]): 43 | for col in range(0, np.shape(checkerBoard)[1]): 44 | if ((math.floor(row / (frameSize[1] / gridSize[1]))) + (math.floor(col / (frameSize[0] / gridSize[0])))) % 2 == 0: 45 | checkerBoard[row, col] = 1 46 | 47 | cv.imshow("image", checkerBoard) 48 | cv.waitKey(0) 49 | cv.destroyAllWindows() 50 | 51 | def pointGuidance(gridSize, checkerBoard, imagePoints, capturedWorldPoints): 52 | 53 | thickeness = 2 54 | radius = 20 55 | circleColor = (1,0,0) 56 | 57 | text = "Please place probe tip accurately on corner circled in blue. Press any key to capture position" 58 | textColor = (0.5,0.5,0.5) 59 | 60 | for row in range(0, gridSize[0]+1): 61 | for col in range(0, gridSize[1]+1): 62 | center = ( row*(frameSize[0] // gridSize[0]), col*(frameSize[1] // gridSize[1])) 63 | bleh = row*4+col 64 | imagePoints[0:2, bleh] = center[0:2] 65 | checkerBoardWithCircle = cv.circle(np.repeat(checkerBoard[:, :, np.newaxis], 3, axis=2), center, radius, circleColor, thickeness) 66 | 67 | checkerBoardWithCircle = cv.putText(checkerBoardWithCircle, text, (50, frameSize[1] - 50), cv.FONT_HERSHEY_PLAIN, 2, textColor) 68 | cv.imshow("image2", checkerBoardWithCircle) 69 | cv.waitKey(5) 70 | 71 | #TODO capture the probe position 72 | #capturedWorldPoints[0:2, row+col] = probePosition[0:2] 73 | 74 | cv.destroyAllWindows() 75 | 76 | capturedWorldPoints[0:3, :] = np.array(\ 77 | [[0.29800502, 0.0061638 , 0.19329104],\ 78 | [0.29790819, 0.00609048, 0.12417287],\ 79 | [0.29282362, 0.00589446, 0.0649954 ],\ 80 | [0.29282882, 0.00585762, -0.00738237],\ 81 | #---------------------------------------- 82 | [0.23772776, 0.00590986, 0.19536735],\ 83 | [0.23420618, 0.0054013 , 0.12486595],\ 84 | [0.2346742 , 0.0055742 , 0.06042208],\ 85 | [0.22915092, 0.00530273, -0.00476075],\ 86 | #----------------------------------------- 87 | [0.17455278, 0.00515153, 0.19494631],\ 88 | [0.17370129, 0.00505166, 0.12570471],\ 89 | [0.1713025 , 0.00490197, 0.0634917 ],\ 90 | [0.17024334, 0.00482908, -0.0078378 ],\ 91 | #---------------------------------------- 92 | [0.10822128, 0.004411 , 0.1962533 ],\ 93 | [0.10675856, 0.00411283, 0.12804918],\ 94 | [0.10582848, 0.00404256, 0.06549262],\ 95 | [0.10449569, 0.00389199, -0.00398948],\ 96 | #---------------------------------------- 97 | [0.04599235, 0.0034341 , 0.1978229 ],\ 98 | [0.04655383, 0.00365754, 0.12727729],\ 99 | [0.04237613, 0.00342598, 0.06747411],\ 100 | [0.04168911, 0.00318181, -0.00282326]]).T 101 | 102 | def getProjectionMatrix(imagePoints, capturedWorldPoints, projectionMatrix): 103 | projectionMatrix = np.linalg.lstsq(capturedWorldPoints.T, imagePoints.T)[0].T 104 | #projectionMatrix2 = np.linalg.solve(capturedWorldPoints.T, imagePoints.T).T 105 | 106 | print(projectionMatrix) 107 | 108 | def projectPointOnProbeTip(projectionMatix): 109 | k = -1 110 | 111 | while (k == -1): 112 | #TODO 113 | #currentWorldPoint = 114 | 115 | currentPixelPoint = np.matmul(projectionMatix, currentWorldPoint) 116 | 117 | img = np.zeros(frameSize) 118 | img = cv.line(img, (currentPixelPoint[0], 0), (currentPixelPoint[0], frameSize[0]), 1) 119 | img = cv.line(img, (0, currentPixelPoint[1]), (frameSize[1], currentPixelPoint[1]), 1) 120 | 121 | cv.imshow(img) 122 | k = cv.waitKey(1) 123 | 124 | cv.destroyAllWindows() 125 | 126 | 127 | 128 | 129 | if __name__ == "__main__": 130 | makeCheckerboard(gridSize, frameSize) 131 | pointGuidance(gridSize, checkerBoard, imagePoints, capturedWorldPoints) 132 | getProjectionMatrix(imagePoints, capturedWorldPoints, projectionMatrix) 133 | -------------------------------------------------------------------------------- /learning/calibration/calibrate.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from scipy.sparse.linalg import lsqr 3 | import numpy as np 4 | from scipy.spatial.transform import Rotation as R 5 | from utils import load_opti_data, progress, GREY_OPTI_POS, RED_OPTI_ROT, GREY_OPTI_ROT, RED_OPTI_POS 6 | 7 | CALIBRATION_KEY = "red_calib" 8 | 9 | 10 | def main(): 11 | tip = calibrate(CALIBRATION_KEY) 12 | pickle.dump(tip, open(CALIBRATION_KEY+".pkl", 'wb')) 13 | 14 | 15 | def fix_frame_opti(opti_data, start_frame, use_palm, use_markers): 16 | start = opti_data.index[start_frame] 17 | end = opti_data.index[start_frame + 1] 18 | # assert (opti_data.head_frame.iloc[start_frame] == opti_data.hand_frame.iloc[start_frame]) 19 | # assert (opti_data.head_frame.iloc[start_frame + 1] == opti_data.hand_frame.iloc[start_frame + 1]) 20 | missing_frames = np.linspace(start + 1, end - 1, end - start - 1).astype('int') 21 | for missing_frame in missing_frames: 22 | opti_data.loc[missing_frame] = None 23 | if use_markers: 24 | opti_data.at[missing_frame, 'marker_finger'] = opti_data.at[start, 'marker_finger'] 25 | opti_data.at[missing_frame, 'marker_wrist'] = opti_data.at[start, 'marker_wrist'] 26 | if use_palm: 27 | opti_data.at[missing_frame, 'marker_palm'] = opti_data.at[start, 'marker_palm'] 28 | return opti_data 29 | 30 | 31 | def handle_missing_frames_opti(opti_data, use_palm, use_markers): 32 | print("Interpolating missing opti frames") 33 | 34 | # first let's deal with frame drops (probably from dropped UDP packets) 35 | opti_data = opti_data.set_index('frame_wrist') 36 | duplicate_frames = np.where(np.diff(opti_data.index.values) == 0)[0] 37 | # print(len(opti_data)) 38 | opti_data.drop(opti_data.index[duplicate_frames], inplace=True) 39 | # print(len(opti_data)) 40 | 41 | assert (np.sum(np.diff(opti_data.index.values) == 0) == 0) # this won't work if we have duplicated frame numbers 42 | frames_to_fix, = np.where(np.diff(opti_data.index.values) != 1) 43 | # print(frames_to_fix) 44 | for frame in frames_to_fix: 45 | opti_data = fix_frame_opti(opti_data, frame, use_palm, use_markers) 46 | progress(1) 47 | opti_data = opti_data.sort_index() 48 | opti_data.interpolate(inplace=True) 49 | print(f"Interpolated {len(frames_to_fix)} missing frames") 50 | return opti_data 51 | 52 | 53 | def find_tip_pose(pos, rot): 54 | pos = pos[::30] 55 | rot = rot[::30] 56 | size_of_eq = int(len(pos) * (len(pos) - 1) / 2) 57 | rotation = [] 58 | position = [] 59 | for index in range(len(pos)): 60 | for index2 in range(index+1, len(pos)): 61 | # rotation += R.from_dcm(rot[index].as_dcm() - rot[index2].as_dcm()) 62 | rotation.append(rot[index].as_dcm() - rot[index2].as_dcm()) 63 | position += [pos[index2] - pos[index]] 64 | position = np.reshape(position, (size_of_eq * 3, 1)) 65 | rotation = np.reshape(rotation, (size_of_eq * 3, 3)) 66 | 67 | # tip, res, rank, s = np.linalg.lstsq(rotation, position) 68 | tip, istop, itn, r1norm = lsqr(rotation, position)[:4] 69 | return tip 70 | 71 | 72 | def calibrate(key): 73 | print("processing opti file") 74 | opti_data, use_palm, use_markers = load_opti_data(key) 75 | print("done loading opti file") 76 | 77 | opti_data = handle_missing_frames_opti(opti_data, use_palm, use_markers) 78 | grey_pos = opti_data[GREY_OPTI_POS].values 79 | red_pos = opti_data[RED_OPTI_POS].values 80 | grey_rot = R.from_quat(opti_data[GREY_OPTI_ROT].values) 81 | red_rot = R.from_quat(opti_data[RED_OPTI_ROT].values) 82 | 83 | if CALIBRATION_KEY == "red_calib": 84 | tip = find_tip_pose(red_pos, red_rot) 85 | else: 86 | tip = find_tip_pose(grey_pos, grey_rot) 87 | 88 | print(tip) 89 | return tip 90 | 91 | 92 | if __name__ == "__main__": 93 | main() 94 | -------------------------------------------------------------------------------- /learning/calibration/grey_calib.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/learning/calibration/grey_calib.pkl -------------------------------------------------------------------------------- /learning/calibration/red_calib.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/learning/calibration/red_calib.pkl -------------------------------------------------------------------------------- /learning/preprocessing/step1_extract_coordinates.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | # import quaternion 4 | import matplotlib.pyplot as plt 5 | from mpl_toolkits.mplot3d import Axes3D 6 | import numpy as np 7 | import pandas as pd 8 | from scipy.spatial.transform import Rotation as R 9 | # from preprocessing.optiTrackMarkers import get_opti_marker 10 | from settings import DATA_ROOT 11 | from utils import load_opti_data, \ 12 | save_extracted_opti_data, progress, GREY_OPTI_POS, RED_OPTI_ROT, GREY_OPTI_ROT, \ 13 | RED_OPTI_POS, BOARD_OPTI_ROT, BOARD_OPTI_POS 14 | 15 | TRIAL = "red_calib" 16 | 17 | 18 | def main(): 19 | df_opti = extract_coordinates_opti(TRIAL) 20 | save_extracted_opti_data(df_opti, TRIAL) 21 | 22 | 23 | def fix_frame_opti(opti_data, start_frame, use_tray, use_markers): 24 | start = opti_data.index[start_frame] 25 | end = opti_data.index[start_frame + 1] 26 | # assert (opti_data.head_frame.iloc[start_frame] == opti_data.hand_frame.iloc[start_frame]) 27 | # assert (opti_data.head_frame.iloc[start_frame + 1] == opti_data.hand_frame.iloc[start_frame + 1]) 28 | missing_frames = np.linspace(start + 1, end - 1, end - start - 1).astype('int') 29 | for missing_frame in missing_frames: 30 | opti_data.loc[missing_frame] = None 31 | if use_markers: 32 | opti_data.at[missing_frame, 'marker_finger'] = opti_data.at[start, 'marker_finger'] 33 | opti_data.at[missing_frame, 'marker_wrist'] = opti_data.at[start, 'marker_wrist'] 34 | if use_tray: 35 | opti_data.at[missing_frame, 'marker_palm'] = opti_data.at[start, 'marker_palm'] 36 | return opti_data 37 | 38 | 39 | def handle_missing_frames_opti(opti_data, use_tray, use_markers): 40 | print("Interpolating missing opti frames") 41 | 42 | # first let's deal with frame drops (probably from dropped UDP packets) 43 | opti_data = opti_data.set_index('frame_wrist') 44 | duplicate_frames = np.where(np.diff(opti_data.index.values) == 0)[0] 45 | # print(len(opti_data)) 46 | opti_data.drop(opti_data.index[duplicate_frames], inplace=True) 47 | # print(len(opti_data)) 48 | 49 | assert (np.sum(np.diff(opti_data.index.values) == 0) == 0) # this won't work if we have duplicated frame numbers 50 | frames_to_fix, = np.where(np.diff(opti_data.index.values) != 1) 51 | # print(frames_to_fix) 52 | for frame in frames_to_fix: 53 | opti_data = fix_frame_opti(opti_data, frame, use_tray, use_markers) 54 | progress(1) 55 | opti_data = opti_data.sort_index() 56 | opti_data.interpolate(inplace=True) 57 | print(f"Interpolated {len(frames_to_fix)} missing frames") 58 | return opti_data 59 | 60 | 61 | def extract_coordinates_opti(key): 62 | print("processing opti file") 63 | opti_data, use_board, use_markers = load_opti_data(key) 64 | print("done loading opti file") 65 | 66 | opti_data = handle_missing_frames_opti(opti_data, use_board, use_markers) 67 | markers = 0 68 | grey_pos = opti_data[GREY_OPTI_POS].values 69 | red_pos = opti_data[RED_OPTI_POS].values 70 | grey_rot = R.from_quat(opti_data[GREY_OPTI_ROT].values).inv() 71 | red_rot = R.from_quat(opti_data[RED_OPTI_ROT].values).inv() 72 | if use_board: 73 | board_pos = opti_data[BOARD_OPTI_POS].values 74 | board_rot = R.from_quat(opti_data[BOARD_OPTI_ROT].values).inv() 75 | 76 | red_tip = pickle.load(open(os.path.join(DATA_ROOT, 'calibration', 'red_calib.pkl'), 'rb')) 77 | grey_tip = pickle.load(open(os.path.join(DATA_ROOT, 'calibration', 'grey_calib.pkl'), 'rb')) 78 | 79 | grey_markers_adj = grey_pos + grey_rot.inv().apply(grey_tip) 80 | red_markers_adj = red_pos + red_rot.inv().apply(red_tip) 81 | 82 | # grey_markers_adj = grey_rot.apply(grey_pos - grey_tip) 83 | # red_markers_adj = red_rot.apply(red_pos - red_tip) 84 | 85 | two_tip_dist = grey_markers_adj - red_markers_adj 86 | plt.figure() 87 | plt.plot(np.linalg.norm(two_tip_dist, axis=1)) 88 | 89 | plt.figure() 90 | plt.plot(grey_markers_adj) 91 | plt.figure() 92 | plt.plot(red_markers_adj) 93 | # plt.figure() 94 | # plt.plot(board_pos) 95 | # plt.figure() 96 | # plt.plot(board_pos) 97 | plt.show() 98 | 99 | return(package_data_opti(opti_data, grey_markers_adj, red_markers_adj, board_pos, board_rot.as_quat(), markers, 100 | use_board, use_markers)) 101 | 102 | 103 | def package_data_opti(opti_data, grey_markers_adj, red_markers_adj, board_pos, board_rot, markers, use_board, 104 | use_markers): 105 | if use_board: 106 | data = np.concatenate((grey_markers_adj, red_markers_adj, board_pos, board_rot), axis=1) 107 | labels = ['g_x', 'g_y', 'g_z', 'r_x', 'r_y', 'r_z', 'board_x', 'board_y', 'board_z', 'board_qw', 'board_qx', 108 | 'board_qy', 'board_qz'] 109 | else: 110 | data = np.concatenate((grey_markers_adj, red_markers_adj), axis=1) 111 | labels = ['g_x', 'g_y', 'g_z', 'g_qw', 'g_qx', 'g_qy', 'g_qz', 'r_x', 'r_y', 'r_z', 'r_qw', 'r_qx', 'r_qy', 112 | 'r_qz'] 113 | 114 | if use_markers: 115 | data = np.concatenate((data, markers), axis=1) 116 | labels += [f"m{i}" for i in range(markers.shape[1])] 117 | 118 | concat = pd.DataFrame(data=data, columns=labels) 119 | return concat 120 | 121 | 122 | if __name__ == "__main__": 123 | main() 124 | -------------------------------------------------------------------------------- /learning/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pyquaternion 3 | seaborn 4 | numpy-quaternion -------------------------------------------------------------------------------- /learning/settings.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | try: 4 | from local_settings import * 5 | except: 6 | DATA_ROOT = r'D:\board_viz_data' 7 | 8 | RECORDINGS_DIR = os.path.join(DATA_ROOT, "recordings") 9 | RECORDINGS_NATIVE_DIR = os.path.join(DATA_ROOT, "recordings_native") 10 | # RECORDINGS_NATIVE_DIR = os.path.join(DATA_ROOT, "recordings_interference") 11 | PROCESSED_RECORDINGS_DIR = os.path.join(DATA_ROOT, "processed") 12 | PREDICTIONS_DIR = os.path.join(DATA_ROOT, "predictions") 13 | CALIBRATION_DIR = os.path.join(DATA_ROOT, "rigid_body_calibration\hand\markers_18_12_05_19_23_00.txt") 14 | PREDICTION_MATLAB_DIR = os.path.join(DATA_ROOT, "MATLAB") 15 | TABFINDER_DIR = os.path.join(DATA_ROOT, "tap_finder") 16 | 17 | 18 | -------------------------------------------------------------------------------- /pyboard/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /pyboard/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyboard/.idea/pyboard.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /pyboard/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyboard/capturedWorldPoints.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/capturedWorldPoints.pkl -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle00.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle01.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle01.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle02.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle02.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle03.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle03.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle10.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle10.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle11.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle11.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle12.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle12.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle13.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle13.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle20.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle20.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle21.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle21.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle22.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle22.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle23.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle23.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle30.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle30.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle31.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle31.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle31.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle32.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle32.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle33.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle33.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle33.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle40.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle40.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle40.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle41.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle41.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle41.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle42.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle42.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle42.png -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle43.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle43.jpg -------------------------------------------------------------------------------- /pyboard/checkerBoard_images/checkerBoardWithCircle43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/checkerBoard_images/checkerBoardWithCircle43.png -------------------------------------------------------------------------------- /pyboard/collect_data.py: -------------------------------------------------------------------------------- 1 | import pyrealtime as prt 2 | import struct 3 | 4 | from extract_opti_cordinate import extract_cord_layer 5 | from opti_lib import get_opti_source 6 | from projector_calib import projection_draw, get_pixel_point, get_board_position, get_red_pixel_point, \ 7 | ProjectionCalibrate, get_board_rot 8 | 9 | CALIBRATE_PROJECTOR = False 10 | CALIBRATE_TIP_POS = False 11 | DEBUG = False 12 | FILTERED = True 13 | SEND_OVER_UDP = True 14 | USE_BOARD = True 15 | RECORD = False 16 | ALPHA = 0.3 17 | ALPHA_BOARD = 0.1 18 | 19 | 20 | def encode_udp(data): 21 | if USE_BOARD: 22 | # print(data['tip_pos'][2]) 23 | return struct.pack("f" * 14, 24 | data['red_tip_pixel'][0], data['red_tip_pixel'][1], data['tip_pos'][2], 25 | data['red_top_pixel'][0], data['red_top_pixel'][1], 26 | data['board'][0], data['board'][1], data['board_pos'][2], 27 | data['grey_tip_pixel'][0], data['grey_tip_pixel'][1], data['tip_pos'][5], 28 | data['grey_top_pixel'][0], data['grey_top_pixel'][1], data['board_rot'][0]) 29 | 30 | return struct.pack("f" * 8, data['red_tip_pixel'][0], data['red_tip_pixel'][1], 31 | data['tip_pos_opti'][0], data['tip_pos_opti'][1], data['tip_pos_opti'][2], 32 | data['opti']['red']['pos'][0], data['opti']['red']['pos'][1], data['opti']['red']['pos'][2]) 33 | 34 | 35 | def main(): 36 | opti = get_opti_source(show_plot=False, use_board=USE_BOARD, use_tray=False) 37 | if USE_BOARD: 38 | board_rot = get_board_rot(opti) 39 | # prt.PrintLayer(board_rot) 40 | # fps = prt.FigureManager(fps=10000) 41 | # prt.TimePlotLayer(board_rot, ylim=(-200, 200), n_channels=3, fig_manager=fps) 42 | if RECORD: 43 | prt.RecordLayer(opti, file_prefix="opti") 44 | if not CALIBRATE_TIP_POS: 45 | tip_pos = extract_cord_layer(opti, use_board=USE_BOARD) 46 | if CALIBRATE_PROJECTOR: 47 | ProjectionCalibrate(tip_pos, win_width=1920, win_height=1080) 48 | else: 49 | red_pixel_point = get_pixel_point(tip_pos, marker="RED_TIP") 50 | # prt.PrintLayer(red_pixel_point) 51 | if DEBUG: 52 | draw = projection_draw(red_pixel_point, win_width=1920, win_height=1080) 53 | else: 54 | red_top_pixel = get_pixel_point(opti, marker="RED_TOP") 55 | grey_pixel_point = get_pixel_point(tip_pos, marker="GREY_TIP") 56 | grey_top_pixel = get_pixel_point(opti, marker="GREY_TOP") 57 | if USE_BOARD: 58 | board_pixel_point = get_pixel_point(tip_pos, marker="BOARD") 59 | board_pos = get_board_position(tip_pos) 60 | if FILTERED: 61 | red_pixel_point = prt.ExponentialFilter(red_pixel_point, alpha=ALPHA) 62 | red_top_pixel = prt.ExponentialFilter(red_top_pixel, alpha=ALPHA) 63 | grey_pixel_point = prt.ExponentialFilter(grey_pixel_point, alpha=ALPHA) 64 | grey_top_pixel = prt.ExponentialFilter(grey_top_pixel, alpha=ALPHA) 65 | # prt.PrintLayer(grey_pixel_point) 66 | if SEND_OVER_UDP: 67 | data = prt.MergeLayer(None) 68 | data.set_input(red_pixel_point, "red_tip_pixel") 69 | data.set_input(red_top_pixel, "red_top_pixel") 70 | data.set_input(grey_pixel_point, "grey_tip_pixel") 71 | data.set_input(grey_top_pixel, "grey_top_pixel") 72 | data.set_input(tip_pos, "tip_pos") 73 | if USE_BOARD: 74 | if FILTERED: 75 | board_pixel_point = prt.ExponentialFilter(board_pixel_point, alpha=ALPHA_BOARD) 76 | # board_pos = prt.ExponentialFilter(board_pos, alpha=ALPHA_BOARD) 77 | #board_rot = prt.ExponentialFilter(board_rot, alpha=ALPHA_BOARD) 78 | data.set_input(board_pixel_point, "board") 79 | data.set_input(board_pos, "board_pos") 80 | data.set_input(board_rot, "board_rot") 81 | 82 | prt.UDPWriteLayer(data, port=8052, encoder=encode_udp) 83 | prt.LayerManager.session().run(show_monitor=False) 84 | 85 | 86 | if __name__ == "__main__": 87 | main() -------------------------------------------------------------------------------- /pyboard/controller_lib.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from settings import PORT, BAUD, DATA_ROOT 4 | import serial 5 | import pyrealtime as prt 6 | import struct 7 | import numpy as np 8 | 9 | OSR = 256 10 | BYTES_PER_SAMPLE = 2 11 | RX_CHANNELS_ADC = 3 12 | BUFFER_SIZE = 8 13 | AGGREGATE = 1 14 | RX_CHANNELS = 9 # was 6 15 | 16 | NUM_SIGN_BYTES = 1 17 | 18 | NUM_BYTES = BYTES_PER_SAMPLE * RX_CHANNELS_ADC * AGGREGATE + NUM_SIGN_BYTES 19 | PACKET_FORMAT = '<' + 'B' * RX_CHANNELS_ADC * AGGREGATE * BYTES_PER_SAMPLE + ("B" * NUM_SIGN_BYTES) 20 | 21 | USE_NATIVE = True 22 | RECORD = False 23 | 24 | VICON_PORT = 9987 25 | 26 | FRAME_FORMAT = "IHhhhB" 27 | FRAME_SIZE = 13 28 | FRAMES_PER_PACKET = 40 29 | 30 | 31 | def convert_to_volts(data): 32 | center = 0x800000 >> int(((256 / OSR) - 1) * 3) 33 | if BYTES_PER_SAMPLE == 2: 34 | center = center >> 8 35 | return np.sign(data) * (np.abs(data) - center) / (center - 1) * -1.2 36 | 37 | 38 | def fix_signs(data, counts, buffer_size): 39 | signs = [(1 if x > buffer_size / 2 else -1) for x in counts] 40 | return data * signs 41 | 42 | 43 | def fix_signs_single_byte(data, sign_byte): 44 | values = [(sign_byte >> 0) & 0x03, (sign_byte >> 2) & 0x03, (sign_byte >> 4) & 0x03] 45 | # print(values) 46 | signs = [(1 if x >= 2 else -1) for x in values] 47 | return data * signs 48 | 49 | 50 | BYTES_PER_GROUP = 3 51 | AGGREGATE = 1 52 | DEMO = False 53 | 54 | 55 | def process(data): 56 | try: 57 | data = struct.unpack('>' + 'B' * 17, data) 58 | data = np.array([int(x) for x in data]) 59 | 60 | except (ValueError, struct.error): 61 | print("Parse error") 62 | return None 63 | 64 | def convert_adc_bottom(data): 65 | all_data = np.zeros((3, 1)) 66 | for i in range(3): 67 | adc_b = data[1 + i * 2] + ((data[0 + i * 2] & 0x0f) << 8) 68 | adc_b = adc_b - 2 ** 12 if adc_b > 2 ** 11 - 1 else adc_b 69 | all_data[i] = -adc_b 70 | result = all_data.T / 2048 * 1.2 71 | return result 72 | 73 | def convert_adc_top(data): 74 | 75 | all_data = np.zeros((6, 1)) 76 | for i in range(3): 77 | adc_a = (data[0 + i * BYTES_PER_GROUP] << 4) + ((data[1 + i * BYTES_PER_GROUP] & 0xf0) >> 4) 78 | adc_b = data[2 + i * BYTES_PER_GROUP] + ((data[1 + i * BYTES_PER_GROUP] & 0x0f) << 8) 79 | adc_a = adc_a - 2 ** 12 if adc_a > 2 ** 11 - 1 else adc_a 80 | adc_b = adc_b - 2 ** 12 if adc_b > 2 ** 11 - 1 else adc_b 81 | all_data[i * 2 + 0] = -adc_a 82 | all_data[i * 2 + 1] = -adc_b 83 | 84 | result = all_data.T / 2048 * 1.2 85 | return result 86 | 87 | # top = convert_adc(data[1:10], 'top') 88 | # bottom = convert_adc(data[11:17], 'bottom') 89 | top = convert_adc_top(data[1:10]) 90 | bottom = convert_adc_bottom(data[11:17]) 91 | # bottom = bottom[0,[1,3,5]] 92 | all_data = np.hstack((np.atleast_2d(data[[0, 10]]), top, bottom)) 93 | if np.any(all_data < -.02): 94 | print("bad packet") 95 | return None 96 | return all_data 97 | 98 | 99 | @prt.transformer 100 | def decimate(layer): 101 | return layer[0, :] 102 | 103 | 104 | @prt.transformer(multi_output=True) 105 | def split(data): 106 | return {"frame_num": data[:, 0:2], "adc": data[:, 2:]} 107 | 108 | last_data = [0,0] 109 | @prt.transformer 110 | def diff(data): 111 | global last_data 112 | d1 = data[0,:] - last_data 113 | d = np.diff(data, axis=0) 114 | last_data = data[-1,:] 115 | # print(data) 116 | return np.vstack((d1, d)) 117 | 118 | 119 | @prt.transformer 120 | def get_channel(data): 121 | return data[:,0] 122 | 123 | 124 | @prt.transformer 125 | def get_energy(x): 126 | return np.atleast_2d(np.sum(x ** 2, axis=1)).T 127 | 128 | 129 | @prt.transformer 130 | def get_taps(energy_thresholds): 131 | energy = energy_thresholds['energy'] 132 | if "thresholds" in energy_thresholds: 133 | thresholds = energy_thresholds['thresholds'] 134 | else: 135 | thresholds = (0.000008, 0.000015) 136 | if np.max(energy) > thresholds[1]: # 0.000015: 137 | # print(2) 138 | return np.array([2]) 139 | # else: 140 | # return False 141 | # return np.array([1]) 142 | elif np.max(energy) > thresholds[0]: # 0.000008: 143 | # print(1) 144 | return np.array([1]) 145 | else: 146 | return np.array([0]) 147 | 148 | 149 | def decode_mag_file(line): 150 | data = np.array(eval(line.decode('utf-8'))) 151 | return data 152 | 153 | 154 | def playback_device_data(key, show_plot=True, strip_frame_num=True): 155 | filename = os.path.join(DATA_ROOT, "recordings", f"mag_{key}.txt") 156 | if not os.path.exists(filename): 157 | filename = f"mag_{key}.txt" 158 | data = prt.PlaybackLayer(filename, rate=59, decoder=decode_mag_file, print_fps=True, loop=True) 159 | if strip_frame_num: 160 | split_data = split(data) 161 | adc = split_data.get_port("adc") 162 | else: 163 | adc = data 164 | 165 | if show_plot: 166 | fm = prt.FigureManager(fps=10000) 167 | prt.TimePlotLayer(split_data.get_port("adc"), window_size=5000, n_channels=RX_CHANNELS, ylim=(-0.1, 1), lw=1, 168 | fig_manager=fm) 169 | return data, adc 170 | 171 | 172 | def get_device_data(show_plot=True, buffer_size=BUFFER_SIZE): 173 | serial_port = serial.Serial(prt.find_serial_port(PORT), BAUD, timeout=5) 174 | serial_buffer = prt.FixedBuffer(buffer_size, use_np=True, shape=(RX_CHANNELS + 2,), axis=0) 175 | raw_data = prt.ByteSerialReadLayer.from_port(serial=serial_port, decoder=process, print_fps=True, preamble=b'UW', 176 | num_bytes=17, buffer=serial_buffer, multi_output=False) 177 | split_data = split(raw_data) 178 | # prt.PrintLayer(raw_data) 179 | if show_plot: 180 | fm = prt.FigureManager(fps=10000) 181 | if DEMO: 182 | filtered = prt.ExponentialFilter(split_data.get_port("adc"), alpha=.1, batch=True) 183 | prt.TimePlotLayer(filtered, window_size=5000, n_channels=RX_CHANNELS, ylim=(0, 0.2), lw=3, fig_manager=fm) 184 | else: 185 | # import scipy.signal 186 | # # filtered = prt.ExponentialFilter(split_data.get_port("adc"), alpha=1, batch=True) 187 | # prt.TimePlotLayer(split_data.get_port("adc"), window_size=5000, n_channels=RX_CHANNELS, ylim=(0, 1), lw=1, fig_manager=fm) 188 | # sos = scipy.signal.butter(5, [25,55], fs=472, btype="bandpass", output='sos') 189 | # filtered = prt.SOSFilter(split_data.get_port("adc"), sos, axis=0, shape=(9,)) 190 | # energy = get_energy(filtered) 191 | # 192 | # prt.TimePlotLayer(energy, window_size=1000, n_channels=1, ylim=(0, .00005), lw=1)#, fig_manager=fm) 193 | # filtered2 = prt.ExponentialFilter(energy, alpha=.3, batch=True) 194 | # prt.TimePlotLayer(filtered2, window_size=1000, n_channels=1, ylim=(0, .00005), lw=1)#, fig_manager=fm) 195 | # # prt.Spectrogram(get_channel(filtered)) 196 | # taps = get_taps(filtered2) 197 | # prt.TextPlotLayer(taps) 198 | 199 | prt.TimePlotLayer(split_data.get_port("adc"), window_size=5000, n_channels=RX_CHANNELS, ylim=(-0.1, 1), lw=1, fig_manager=fm) 200 | # prt.TimePlotLayer(diff(split_data.get_port("frame_num")), window_size=5000, n_channels=2, ylim=(0, 5), lw=1) 201 | 202 | return raw_data, split_data.get_port("adc") 203 | 204 | 205 | def encode_touch(x): 206 | return "Touch" if x else "" 207 | 208 | 209 | class ThresholdPlot(prt.TimePlotLayer): 210 | def __init__(self, port_in, thresholds, *args, **kwargs): 211 | super().__init__(port_in, *args, **kwargs) 212 | self.thresholds = thresholds 213 | self.threshold_series = [] 214 | 215 | def transform(self, data): 216 | super().transform(data) 217 | return self.thresholds 218 | 219 | def post_init(self, data): 220 | 221 | for i, threshold in enumerate(self.thresholds): 222 | handle, = self.ax.plot([], [], '-', lw=self.lw, label=f"threshold_{i}") 223 | self.threshold_series.append(handle) 224 | 225 | self.fig_manager.fig.canvas.mpl_connect('button_press_event', self.on_click) 226 | super().post_init(data) 227 | 228 | def on_click(self, event): 229 | if event.xdata > self.window_size / 2: 230 | self.thresholds[1] = event.ydata 231 | else: 232 | self.thresholds[0] = event.ydata 233 | 234 | def update_fig(self, data): 235 | for (i, series) in enumerate(self.threshold_series): 236 | series.set_data([self.x_data[0], self.x_data[-1]], [self.thresholds[i], self.thresholds[i]]) 237 | 238 | return super().update_fig(data) + self.threshold_series 239 | 240 | 241 | def detect_touch(data, show_plot=True): 242 | 243 | import scipy.signal 244 | # filtered = prt.ExponentialFilter(split_data.get_port("adc"), alpha=1, batch=True) 245 | sos = scipy.signal.butter(5, [25,80], fs=472, btype="bandpass", output='sos') 246 | filtered = prt.SOSFilter(data, sos, axis=0, shape=(9,)) 247 | energy = get_energy(filtered) 248 | 249 | smoothed_energy = prt.ExponentialFilter(energy, alpha=.2, batch=True) 250 | # prt.TimePlotLayer(filtered2, window_size=1000, n_channels=1, ylim=(0, .000005), lw=1)#, fig_manager=fm) 251 | # prt.Spectrogram(get_channel(filtered), fs=472) 252 | energy_thresholds = prt.MergeLayer(None) 253 | 254 | energy_thresholds.set_input(smoothed_energy, "energy") 255 | if show_plot: 256 | thresholds = ThresholdPlot(smoothed_energy, thresholds=[.000002, .00001], window_size=1000, n_channels=1, ylim=(0, .00008), lw=1) 257 | thresholds.fig_manager.fps = 10 258 | energy_thresholds.set_input(thresholds, key="thresholds") 259 | taps = get_taps(energy_thresholds) 260 | # prt.TextPlotLayer(taps, encoder=encode_touch) 261 | return taps, filtered, smoothed_energy 262 | -------------------------------------------------------------------------------- /pyboard/extract_opti_cordinate.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import numpy as np 3 | import math 4 | from scipy.spatial.transform import Rotation as R 5 | import pyrealtime as prt 6 | import pickle 7 | import os 8 | from settings import DATA_ROOT 9 | 10 | USE_BOARD = False 11 | calib = 1 12 | 13 | 14 | @prt.transformer 15 | def extract_cord(data): 16 | 17 | global red_tip 18 | global calib 19 | global grey_tip 20 | 21 | if calib: 22 | red_tip = pickle.load(open(os.path.join(DATA_ROOT, 'calibration', 'red_calib.pkl'), 'rb')) 23 | grey_tip = pickle.load(open(os.path.join(DATA_ROOT, 'calibration', 'grey_calib.pkl'), 'rb')) 24 | calib = 0 25 | 26 | grey_pos = data['grey']['pos'] 27 | red_pos = data['red']['pos'] 28 | grey_rot = R.from_quat(data['grey']['rot']) 29 | red_rot = R.from_quat(data['red']['rot']) 30 | 31 | grey_markers_adj = grey_pos + grey_rot.apply(grey_tip) 32 | red_markers_adj = red_pos + red_rot.apply(red_tip) 33 | 34 | if USE_BOARD: 35 | board_pos = data['board']['pos'] 36 | return [red_markers_adj[0], red_markers_adj[1], red_markers_adj[2], grey_markers_adj[0], grey_markers_adj[1], grey_markers_adj[2], board_pos[0], board_pos[1], board_pos[2]] 37 | return [red_markers_adj[0], red_markers_adj[1], red_markers_adj[2], grey_markers_adj[0], grey_markers_adj[1], grey_markers_adj[2]] 38 | 39 | 40 | def extract_cord_layer(data, use_board=False): 41 | global USE_BOARD 42 | USE_BOARD = use_board 43 | pos = extract_cord(data) 44 | return pos 45 | -------------------------------------------------------------------------------- /pyboard/imagePoints.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/imagePoints.pkl -------------------------------------------------------------------------------- /pyboard/opti_lib.py: -------------------------------------------------------------------------------- 1 | from prt_natnet import NatNetLayer 2 | import pyrealtime as prt 3 | import numpy as np 4 | 5 | GREY_PROBE_RIGID_BODY_NAME = b"grey" 6 | RED_PROBE_RIGID_BODY_NAME = b"red" 7 | BOARD_RIGID_BODY_NAME = b"board" 8 | TRAY_RIGID_BODY_NAME = b"tray" 9 | 10 | 11 | @prt.transformer(multi_output=True) 12 | def parse(data): 13 | return {'pos': np.array(data[0]), 'rot': np.array(data[1])} 14 | 15 | 16 | def setup_fig(fig): 17 | ax1 = fig.add_subplot(421) 18 | ax2 = fig.add_subplot(422) 19 | ax3 = fig.add_subplot(423) 20 | ax4 = fig.add_subplot(424) 21 | ax5 = fig.add_subplot(425) 22 | ax6 = fig.add_subplot(426) 23 | ax7 = fig.add_subplot(427) 24 | ax8 = fig.add_subplot(428) 25 | return {f'{GREY_PROBE_RIGID_BODY_NAME}_pos': ax1, f'{GREY_PROBE_RIGID_BODY_NAME}_rot': ax2, f'{RED_PROBE_RIGID_BODY_NAME}_pos': ax3, f'{RED_PROBE_RIGID_BODY_NAME}_rot': ax4, 26 | f'{TRAY_RIGID_BODY_NAME}_pos': ax5, f'{TRAY_RIGID_BODY_NAME}_rot': ax6, f'{BOARD_RIGID_BODY_NAME}_pos': ax7, f'{BOARD_RIGID_BODY_NAME}_rot': ax8} 27 | 28 | 29 | def get_opti_source(show_plot=True, use_board=False, use_tray=False): 30 | bodies = [GREY_PROBE_RIGID_BODY_NAME, RED_PROBE_RIGID_BODY_NAME] 31 | if use_tray: 32 | bodies += [TRAY_RIGID_BODY_NAME] 33 | if use_board: 34 | bodies += [BOARD_RIGID_BODY_NAME] 35 | 36 | natnet = NatNetLayer(bodies_to_track=bodies, multi_output=True, print_fps=True, track_markers=True) 37 | # prt.PrintLayer(parse(natnet.get_port(RIGID_BODY_NAME))) 38 | frame_num = natnet.get_port("frame_num") 39 | parsed_grey = parse(natnet.get_port(GREY_PROBE_RIGID_BODY_NAME)) 40 | parsed_red = parse(natnet.get_port(RED_PROBE_RIGID_BODY_NAME)) 41 | if use_tray: 42 | parsed_tray = parse(natnet.get_port(TRAY_RIGID_BODY_NAME)) 43 | if use_board: 44 | parsed_board = parse(natnet.get_port(BOARD_RIGID_BODY_NAME)) 45 | markers = natnet.get_port('markers') 46 | # unlabled = natnet.get_port('unlabeled') 47 | if show_plot: 48 | fm = prt.FigureManager(setup_fig) 49 | prt.TimePlotLayer(parsed_grey.get_port('pos'), ylim=(-2, 2), n_channels=3, plot_key=f'{GREY_PROBE_RIGID_BODY_NAME}_pos', 50 | fig_manager=fm) 51 | prt.TimePlotLayer(parsed_grey.get_port('rot'), ylim=(-2,2), n_channels=4, plot_key=f'{GREY_PROBE_RIGID_BODY_NAME}_rot', fig_manager=fm) 52 | prt.TimePlotLayer(parsed_red.get_port('pos'), ylim=(-2, 2), n_channels=3, plot_key=f'{RED_PROBE_RIGID_BODY_NAME}_pos', 53 | fig_manager=fm) 54 | prt.TimePlotLayer(parsed_red.get_port('rot'), ylim=(-2,2), n_channels=4, plot_key=f'{RED_PROBE_RIGID_BODY_NAME}_rot', fig_manager=fm) 55 | if use_tray: 56 | prt.TimePlotLayer(parsed_tray.get_port('rot'), ylim=(-2,2), n_channels=4, plot_key=f'{TRAY_RIGID_BODY_NAME}_rot', fig_manager=fm) 57 | if use_board: 58 | prt.TimePlotLayer(parsed_board.get_port('rot'), ylim=(-2,2), n_channels=4, plot_key=f'{BOARD_RIGID_BODY_NAME}_rot', fig_manager=fm) 59 | 60 | data = prt.MergeLayer(None) 61 | data.set_input(frame_num, "frame_num") 62 | data.set_input(parsed_grey, "grey") 63 | data.set_input(parsed_red, "red") 64 | if use_tray: 65 | data.set_input(parsed_tray, "tray") 66 | if use_board: 67 | data.set_input(parsed_board, "board") 68 | data.set_input(markers, "markers") 69 | # data.set_input(unlabled, "unlabeled") 70 | # prt.PrintLayer(data) 71 | return data 72 | -------------------------------------------------------------------------------- /pyboard/projectionMatrix.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubicomplab/ar-debug-workbench/b8aee106f1626990e4f8bbd64547b76c5c1d3890/pyboard/projectionMatrix.pkl -------------------------------------------------------------------------------- /pyboard/projector_calibration.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 as cv 3 | import math 4 | import pyrealtime as prt 5 | #size of projector frame 6 | frameSize = [1920, 1080] 7 | # size of checkerboard squares to project 8 | gridSize = [4, 3] 9 | numCaptures = (gridSize[0]+1) * (gridSize[1]+1) 10 | 11 | # world points in homogeneous coordinates 12 | """ 13 | [X_1 X_2 X_3 ... X_n] 14 | [Y_1 Y_2 Y_3 ... Y_n] 15 | [Z_1 Z_2 Z_3 ... Z_n] 16 | [ 1 1 1 ... 1 ] 17 | """ 18 | capturedWorldPoints = np.ones((4, numCaptures)) 19 | 20 | #image points in homogeneous coordinates 21 | """ 22 | [u_1 u_2 u_3 ... u_n] 23 | [v_1 v_2 v_3 ... v_n] 24 | [ 1 1 1 ... 1 ] 25 | """ 26 | imagePoints = np.ones((3, numCaptures)) 27 | 28 | #Projection matrix 29 | projectionMatrix = np.empty((3, 4)) 30 | 31 | 32 | 33 | checkerBoard = np.zeros((frameSize[1], frameSize[0])) 34 | 35 | 36 | def makeCheckerboard(): 37 | if frameSize[0] % gridSize[0] != 0: 38 | print("Frame width %i is not a multiple of checkerboard width %i. Use exact divsor for maximum precision." % (frameSize[0], gridSize[0])) 39 | 40 | if frameSize[1] % gridSize[1] != 0: 41 | print("Frame height %i is not a multiple of checkerboard height %i. Use exact divsor for maximum precision." % (frameSize[1], gridSize[1])) 42 | 43 | for row in range(0, np.shape(checkerBoard)[0]): 44 | for col in range(0, np.shape(checkerBoard)[1]): 45 | if ((math.floor(row / (frameSize[1] / gridSize[1]))) + (math.floor(col / (frameSize[0] / gridSize[0])))) % 2 == 0: 46 | checkerBoard[row, col] = 1 47 | 48 | cv.imshow("image", checkerBoard) 49 | cv.waitKey(0) 50 | cv.destroyAllWindows() 51 | 52 | 53 | @prt.transformer 54 | def pointGuidance(probePosition): 55 | 56 | global checkerBoard 57 | global imagePoints 58 | global capturedWorldPoints 59 | global projectionMatrix 60 | 61 | thickeness = 2 62 | radius = 20 63 | circleColor = (1,0,0) 64 | 65 | text = "Please place probe tip accurately on corner circled in blue. Press any key to capture position" 66 | textColor = (0.5,0.5,0.5) 67 | 68 | for row in range(0, gridSize[0]+1): 69 | for col in range(0, gridSize[1]+1): 70 | center = ( row*(frameSize[0] // gridSize[0]), col*(frameSize[1] // gridSize[1])) 71 | bleh = row*4+col 72 | imagePoints[0:2, bleh] = center[0:2] 73 | checkerBoardWithCircle = cv.circle(np.repeat(checkerBoard[:, :, np.newaxis], 3, axis=2), center, radius, circleColor, thickeness) 74 | 75 | checkerBoardWithCircle = cv.putText(checkerBoardWithCircle, text, (50, frameSize[1] - 50), cv.FONT_HERSHEY_PLAIN, 2, textColor) 76 | cv.imshow("image2", checkerBoardWithCircle) 77 | cv.waitKey(0) 78 | 79 | #TODO capture the probe position 80 | capturedWorldPoints[0:3, row*4+col] = probePosition 81 | 82 | cv.destroyAllWindows() 83 | projectionMatrix = np.linalg.lstsq(capturedWorldPoints.T, imagePoints.T)[0].T 84 | 85 | # capturedWorldPoints[0:3, :] = np.array(\ 86 | # [[0.29800502, 0.0061638 , 0.19329104],\ 87 | # [0.29790819, 0.00609048, 0.12417287],\ 88 | # [0.29282362, 0.00589446, 0.0649954 ],\ 89 | # [0.29282882, 0.00585762, -0.00738237],\ 90 | # #---------------------------------------- 91 | # [0.23772776, 0.00590986, 0.19536735],\ 92 | # [0.23420618, 0.0054013 , 0.12486595],\ 93 | # [0.2346742 , 0.0055742 , 0.06042208],\ 94 | # [0.22915092, 0.00530273, -0.00476075],\ 95 | # #----------------------------------------- 96 | # [0.17455278, 0.00515153, 0.19494631],\ 97 | # [0.17370129, 0.00505166, 0.12570471],\ 98 | # [0.1713025 , 0.00490197, 0.0634917 ],\ 99 | # [0.17024334, 0.00482908, -0.0078378 ],\ 100 | # #---------------------------------------- 101 | # [0.10822128, 0.004411 , 0.1962533 ],\ 102 | # [0.10675856, 0.00411283, 0.12804918],\ 103 | # [0.10582848, 0.00404256, 0.06549262],\ 104 | # [0.10449569, 0.00389199, -0.00398948],\ 105 | # #---------------------------------------- 106 | # [0.04599235, 0.0034341 , 0.1978229 ],\ 107 | # [0.04655383, 0.00365754, 0.12727729],\ 108 | # [0.04237613, 0.00342598, 0.06747411],\ 109 | # [0.04168911, 0.00318181, -0.00282326]]).T 110 | 111 | 112 | @prt.transformer 113 | def projectPointOnProbeTip(probePosition): 114 | k = -1 115 | global projectPointOnProbeTip 116 | global projectionMatrix 117 | global currentWorldPoint 118 | 119 | 120 | while (k == -1): 121 | #TODO 122 | currentWorldPoint = np.array(probePosition) 123 | 124 | currentPixelPoint = np.matmul(projectionMatrix.T, currentWorldPoint.T) 125 | 126 | img = np.zeros(frameSize) 127 | img = cv.line(img, (currentPixelPoint[0], 0), (currentPixelPoint[0], frameSize[0]), 1) 128 | img = cv.line(img, (0, currentPixelPoint[1]), (frameSize[1], currentPixelPoint[1]), 1) 129 | 130 | cv.imshow(img) 131 | k = cv.waitKey(1) 132 | 133 | cv.destroyAllWindows() 134 | 135 | 136 | def projector(tip_pos): 137 | makeCheckerboard() 138 | pointGuidance(tip_pos) 139 | 140 | projectPointOnProbeTip(tip_pos) 141 | 142 | # getProjectionMatrix(imagePoints, capturedWorldPoints, projectionMatrix) 143 | 144 | if __name__ == "__main__": 145 | projector(0) 146 | # makeCheckerboard(gridSize, frameSize) 147 | # pointGuidance(gridSize, checkerBoard, imagePoints, capturedWorldPoints) 148 | # getProjectionMatrix(imagePoints, capturedWorldPoints, projectionMatrix) 149 | -------------------------------------------------------------------------------- /pyboard/prt_natnet.py: -------------------------------------------------------------------------------- 1 | import pyrealtime as prt 2 | 3 | from NatNetClient import NatNetClient 4 | 5 | 6 | class NatNetLayer(prt.ProducerMixin, prt.ThreadLayer): 7 | 8 | def __init__(self, bodies_to_track, *args, track_markers=False, **kwargs): 9 | self.bodies_to_track = bodies_to_track 10 | self.id_to_name = {} 11 | self.track_markers = track_markers 12 | super().__init__(*args, **kwargs) 13 | 14 | # def on_data(self, id, frame_num, pos, rot): 15 | # if id in self.id_to_name: 16 | # self.supply_input({self.id_to_name[id]: (frame_num, pos, rot)}) 17 | def on_data(self, frame_num, bodies, markers): 18 | ids_in_frame = list(bodies.keys()) 19 | for id in ids_in_frame: 20 | bodies[self.id_to_name[id]] = bodies.pop(id) 21 | markers.pop(b'all') 22 | self.supply_input({'frame_num': frame_num, 'markers': markers, **bodies}) 23 | 24 | def initialize(self): 25 | streamingClient = NatNetClient() 26 | streamingClient.run() 27 | 28 | bodies = streamingClient.get_rigid_bodies() 29 | 30 | for body_name in self.bodies_to_track: 31 | if body_name in bodies: 32 | # streamingClient.register_rigid_body_listener(bodies[body_name].id, self.on_data) 33 | self.id_to_name[bodies[body_name].id] = body_name 34 | print(body_name) 35 | else: 36 | print(body_name, " not found") 37 | print(bodies) 38 | streamingClient.register_callback(self.on_data) -------------------------------------------------------------------------------- /pyboard/settings.py: -------------------------------------------------------------------------------- 1 | from local_settings import * 2 | 3 | # Port should be defined in local_settings 4 | # PORT = "COM25" 5 | DATA_ROOT = r'C:\board_viz_data' 6 | BAUD = 460800 -------------------------------------------------------------------------------- /pyboard/test.py: -------------------------------------------------------------------------------- 1 | import msvcrt 2 | import cv2 as cv 3 | 4 | 5 | 6 | image = cv.imread("checkerBoardWithCircle00.jpg") 7 | cv.imshow("image", image) 8 | k = cv.waitKey(0) 9 | 10 | --------------------------------------------------------------------------------