├── setup.cfg ├── octoprint_siocontrol ├── templates │ ├── README.txt │ ├── siocontrol_navbar.jinja2 │ ├── siocontrol_sidebar.jinja2 │ └── siocontrol_settings.jinja2 ├── static │ ├── less │ │ └── SIOControl.less │ ├── css │ │ ├── SIOControl.css │ │ └── fontawesome-iconpicker.min.css │ └── js │ │ ├── siocontrol.js │ │ └── fontawesome-iconpicker.min.js ├── Connection.py └── __init__.py ├── .gitignore ├── Assets ├── img │ ├── SideBarExample.PNG │ ├── SettingsExampleConn.PNG │ ├── GCodeScriptDialogFR1.PNG │ ├── SettingsExampleIOConfig.PNG │ └── SettingsExampleIntegrations.PNG └── UpdateNotes_0.6.5.txt ├── MANIFEST.in ├── babel.cfg ├── requirements.txt ├── .editorconfig ├── translations └── README.txt ├── setup.py └── README.md /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /octoprint_siocontrol/templates/README.txt: -------------------------------------------------------------------------------- 1 | Put your plugin's Jinja2 templates here. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .idea 4 | *.iml 5 | build 6 | dist 7 | *.egg* 8 | .DS_Store 9 | *.zip 10 | -------------------------------------------------------------------------------- /Assets/img/SideBarExample.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcassel/OctoPrint-Siocontrol/HEAD/Assets/img/SideBarExample.PNG -------------------------------------------------------------------------------- /octoprint_siocontrol/static/less/SIOControl.less: -------------------------------------------------------------------------------- 1 | // TODO: Put your plugin's LESS here, have it generated to ../css. 2 | -------------------------------------------------------------------------------- /Assets/img/SettingsExampleConn.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcassel/OctoPrint-Siocontrol/HEAD/Assets/img/SettingsExampleConn.PNG -------------------------------------------------------------------------------- /Assets/img/GCodeScriptDialogFR1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcassel/OctoPrint-Siocontrol/HEAD/Assets/img/GCodeScriptDialogFR1.PNG -------------------------------------------------------------------------------- /Assets/img/SettingsExampleIOConfig.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcassel/OctoPrint-Siocontrol/HEAD/Assets/img/SettingsExampleIOConfig.PNG -------------------------------------------------------------------------------- /Assets/img/SettingsExampleIntegrations.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcassel/OctoPrint-Siocontrol/HEAD/Assets/img/SettingsExampleIntegrations.PNG -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include octoprint_SIOControl/templates * 3 | recursive-include octoprint_SIOControl/translations * 4 | recursive-include octoprint_SIOControl/static * 5 | -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: */**.py] 2 | 3 | [jinja2: */**.jinja2] 4 | silent=false 5 | extensions=jinja2.ext.do, octoprint.util.jinja.trycatch 6 | 7 | [javascript: */**.js] 8 | extract_messages = gettext, ngettext 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ### 2 | # This file is only here to make sure that something like 3 | # 4 | # pip install -e . 5 | # 6 | # works as expected. Requirements can be found in setup.py. 7 | ### 8 | 9 | . 10 | -------------------------------------------------------------------------------- /Assets/UpdateNotes_0.6.5.txt: -------------------------------------------------------------------------------- 1 | SIO Control PlugIn Update notes 2 | 3 | -Fixed small bug in allignment for message at bottom of side navigation 4 | -Added ability to display IO control button on top Navigation bar 5 | -Updated UI for settings into TAbs to make it easier to manage additional settings. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [**.py] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [**.js] 17 | indent_style = space 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /octoprint_siocontrol/templates/siocontrol_navbar.jinja2: -------------------------------------------------------------------------------- 1 |
2 |
4 | -------------------------------------------------------------------------------- /octoprint_siocontrol/static/css/SIOControl.css: -------------------------------------------------------------------------------- 1 | /* TODO: Have your plugin's CSS files generated to here. */ 2 | .siocontrol-button-row .siocontrol-button-label { 3 | padding-top: 4px; 4 | padding-bottom: 4px; 5 | } 6 | 7 | .siocontrol-button-row .siocontrol-button-label i { 8 | width: 20px; 9 | } 10 | 11 | .siocontrol-button-row .siocontrol-button-controls .btn { 12 | width: 50%; 13 | } 14 | 15 | .siocontrol-settings-row { 16 | margin-bottom: 5px 17 | } 18 | 19 | .siofntBlk { 20 | color: black; 21 | } 22 | 23 | .siofntRed { 24 | color: red; 25 | } 26 | 27 | .siofntGrn { 28 | color: green; 29 | } 30 | 31 | .siofntYel { 32 | color: yellow; 33 | } 34 | 35 | .siocontrol_btOn i { 36 | color: #0F0 37 | } 38 | 39 | .siocontrol_btOff i { 40 | color: grey 41 | } 42 | 43 | .siocontrol_outliner { 44 | outline: 1px solid orange; 45 | } 46 | 47 | .siocontrol_InNumber { 48 | width: 100%; 49 | 50 | } 51 | 52 | .siocontrol_scriptList { 53 | width: 120px; 54 | } 55 | 56 | .siocontrol_label { 57 | margin-top: 5px; 58 | } 59 | 60 | .siocontrol_textName { 61 | width: 100%; 62 | 63 | } -------------------------------------------------------------------------------- /translations/README.txt: -------------------------------------------------------------------------------- 1 | Your plugin's translations will reside here. The provided setup.py supports a 2 | couple of additional commands to make managing your translations easier: 3 | 4 | babel_extract 5 | Extracts any translateable messages (marked with Jinja's `_("...")` or 6 | JavaScript's `gettext("...")`) and creates the initial `messages.pot` file. 7 | babel_refresh 8 | Reruns extraction and updates the `messages.pot` file. 9 | babel_new --locale= 10 | Creates a new translation folder for locale ``. 11 | babel_compile 12 | Compiles the translations into `mo` files, ready to be used within 13 | OctoPrint. 14 | babel_pack --locale= [ --author= ] 15 | Packs the translation for locale `` up as an installable 16 | language pack that can be manually installed by your plugin's users. This is 17 | interesting for languages you can not guarantee to keep up to date yourself 18 | with each new release of your plugin and have to depend on contributors for. 19 | 20 | If you want to bundle translations with your plugin, create a new folder 21 | `octoprint_SIOControl/translations`. When that folder exists, 22 | an additional command becomes available: 23 | 24 | babel_bundle --locale= 25 | Moves the translation for locale `` to octoprint_SIOControl/translations, 26 | effectively bundling it with your plugin. This is interesting for languages 27 | you can guarantee to keep up to date yourself with each new release of your 28 | plugin. 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from setuptools import setup 3 | 4 | ######################################################################################################################## 5 | plugin_identifier = "siocontrol" 6 | plugin_package = "octoprint_siocontrol" 7 | plugin_name = "SIO Control" 8 | plugin_version = "1.0.3" 9 | plugin_description = "Serial IO Control. Integrates a micro controller to give native IO to your OctoPrint device" 10 | plugin_author = "jcassel" 11 | plugin_author_email = "jcassel@softwaresedge.com" 12 | plugin_url = "https://github.com/jcassel/OctoPrint-Siocontrol" 13 | plugin_license = "AGPLv3" 14 | plugin_requires = [] 15 | plugin_additional_data = [] 16 | plugin_additional_packages = [] 17 | plugin_ignored_packages = [] 18 | additional_setup_parameters = {"python_requires": ">=3,<4"} 19 | ######################################################################################################################## 20 | 21 | 22 | try: 23 | import octoprint_setuptools 24 | except Exception: 25 | print( 26 | "Could not import OctoPrint's setuptools, are you sure you are running that under " 27 | "the same python installation that OctoPrint is installed under?" 28 | ) 29 | import sys 30 | 31 | sys.exit(-1) 32 | 33 | setup_parameters = octoprint_setuptools.create_plugin_setup_parameters( 34 | identifier=plugin_identifier, 35 | package=plugin_package, 36 | name=plugin_name, 37 | version=plugin_version, 38 | description=plugin_description, 39 | author=plugin_author, 40 | mail=plugin_author_email, 41 | url=plugin_url, 42 | license=plugin_license, 43 | requires=plugin_requires, 44 | additional_packages=plugin_additional_packages, 45 | ignored_packages=plugin_ignored_packages, 46 | additional_data=plugin_additional_data, 47 | ) 48 | 49 | if len(additional_setup_parameters): 50 | from octoprint.util import dict_merge 51 | 52 | setup_parameters = dict_merge(setup_parameters, additional_setup_parameters) 53 | 54 | setup(**setup_parameters) 55 | -------------------------------------------------------------------------------- /octoprint_siocontrol/templates/siocontrol_sidebar.jinja2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
-------------------------------------------------------------------------------- /octoprint_siocontrol/static/css/fontawesome-iconpicker.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Icon Picker 3 | * https://farbelous.github.io/fontawesome-iconpicker/ 4 | * 5 | * Originally written by (c) 2016 Javi Aguilar 6 | * Licensed under the MIT License 7 | * https://github.com/farbelous/fontawesome-iconpicker/blob/master/LICENSE 8 | * 9 | */.iconpicker-popover.popover{position:absolute;top:0;left:0;display:none;max-width:none;padding:1px;text-align:left;width:234px;background:#f7f7f7;z-index:9}.iconpicker-popover.popover.top,.iconpicker-popover.popover.topLeftCorner,.iconpicker-popover.popover.topLeft,.iconpicker-popover.popover.topRight,.iconpicker-popover.popover.topRightCorner{margin-top:-10px}.iconpicker-popover.popover.right,.iconpicker-popover.popover.rightTop,.iconpicker-popover.popover.rightBottom{margin-left:10px}.iconpicker-popover.popover.bottom,.iconpicker-popover.popover.bottomRightCorner,.iconpicker-popover.popover.bottomRight,.iconpicker-popover.popover.bottomLeft,.iconpicker-popover.popover.bottomLeftCorner{margin-top:10px}.iconpicker-popover.popover.left,.iconpicker-popover.popover.leftBottom,.iconpicker-popover.popover.leftTop{margin-left:-10px}.iconpicker-popover.popover.inline{margin:0 0 12px 0;position:relative;display:inline-block;opacity:1;top:auto;left:auto;bottom:auto;right:auto;max-width:100%;box-shadow:none;z-index:auto;vertical-align:top}.iconpicker-popover.popover.inline>.arrow{display:none}.dropdown-menu .iconpicker-popover.inline{margin:0;border:none}.dropdown-menu.iconpicker-container{padding:0}.iconpicker-popover.popover .popover-title{padding:12px;font-size:13px;line-height:15px;border-bottom:1px solid #ebebeb;background-color:#f7f7f7}.iconpicker-popover.popover .popover-title input[type=search].iconpicker-search{margin:0 0 2px 0}.iconpicker-popover.popover .popover-title-text~input[type=search].iconpicker-search{margin-top:12px}.iconpicker-popover.popover .popover-content{padding:0px;text-align:center}.iconpicker-popover .popover-footer{float:none;clear:both;padding:12px;text-align:right;margin:0;border-top:1px solid #ebebeb;background-color:#f7f7f7}.iconpicker-popover .popover-footer:before,.iconpicker-popover .popover-footer:after{content:" ";display:table}.iconpicker-popover .popover-footer:after{clear:both}.iconpicker-popover .popover-footer .iconpicker-btn{margin-left:10px}.iconpicker-popover .popover-footer input[type=search].iconpicker-search{margin-bottom:12px}.iconpicker-popover.popover>.arrow,.iconpicker-popover.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.iconpicker-popover.popover>.arrow{border-width:11px}.iconpicker-popover.popover>.arrow:after{border-width:10px;content:""}.iconpicker-popover.popover.top>.arrow,.iconpicker-popover.popover.topLeft>.arrow,.iconpicker-popover.popover.topRight>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);bottom:-11px}.iconpicker-popover.popover.top>.arrow:after,.iconpicker-popover.popover.topLeft>.arrow:after,.iconpicker-popover.popover.topRight>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.iconpicker-popover.popover.topLeft>.arrow{left:8px;margin-left:0}.iconpicker-popover.popover.topRight>.arrow{left:auto;right:8px;margin-left:0}.iconpicker-popover.popover.right>.arrow,.iconpicker-popover.popover.rightTop>.arrow,.iconpicker-popover.popover.rightBottom>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,0.25)}.iconpicker-popover.popover.right>.arrow:after,.iconpicker-popover.popover.rightTop>.arrow:after,.iconpicker-popover.popover.rightBottom>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.iconpicker-popover.popover.rightTop>.arrow{top:auto;bottom:8px;margin-top:0}.iconpicker-popover.popover.rightBottom>.arrow{top:8px;margin-top:0}.iconpicker-popover.popover.bottom>.arrow,.iconpicker-popover.popover.bottomRight>.arrow,.iconpicker-popover.popover.bottomLeft>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);top:-11px}.iconpicker-popover.popover.bottom>.arrow:after,.iconpicker-popover.popover.bottomRight>.arrow:after,.iconpicker-popover.popover.bottomLeft>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.iconpicker-popover.popover.bottomLeft>.arrow{left:8px;margin-left:0}.iconpicker-popover.popover.bottomRight>.arrow{left:auto;right:8px;margin-left:0}.iconpicker-popover.popover.left>.arrow,.iconpicker-popover.popover.leftBottom>.arrow,.iconpicker-popover.popover.leftTop>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,0.25)}.iconpicker-popover.popover.left>.arrow:after,.iconpicker-popover.popover.leftBottom>.arrow:after,.iconpicker-popover.popover.leftTop>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.iconpicker-popover.popover.leftBottom>.arrow{top:8px;margin-top:0}.iconpicker-popover.popover.leftTop>.arrow{top:auto;bottom:8px;margin-top:0}.iconpicker{position:relative;text-align:left;text-shadow:none;line-height:0;display:block;margin:0;overflow:hidden}.iconpicker *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;position:relative}.iconpicker:before,.iconpicker:after{content:" ";display:table}.iconpicker:after{clear:both}.iconpicker .iconpicker-items{position:relative;clear:both;float:none;padding:12px 0 0 12px;background:#fff;margin:0;overflow:hidden;overflow-y:auto;min-height:49px;max-height:246px}.iconpicker .iconpicker-items:before,.iconpicker .iconpicker-items:after{content:" ";display:table}.iconpicker .iconpicker-items:after{clear:both}.iconpicker .iconpicker-item{float:left;width:14px;height:14px;padding:12px;margin:0 12px 12px 0;text-align:center;cursor:pointer;border-radius:3px;font-size:14px;box-shadow:0 0 0 1px #ddd;color:inherit}.iconpicker .iconpicker-item:hover:not(.iconpicker-selected){background-color:#eee}.iconpicker .iconpicker-item.iconpicker-selected{box-shadow:none;color:#fff;background:#000}.iconpicker-component{cursor:pointer} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OctoPrint-SIOControl 2 | The Serial IO Control OctoPrint plugin, Adds a sidebar with on/off buttons for controlling of Outputs and monitoring of Inputs. It is also a SubPlugin for integration with PSU control, incorporates a physical EStop and simple Filament runout sensor. Serves as an alterative IO control for users that are not using a Raspberry Pi or other device that can take advantage local IO. Requires a Microcontroller as the IO. 3 | 4 | ![sidebar view](Assets/img/SideBarExample.PNG) 5 | 6 | With the Serial IO Control and an inexpensive Micro controller you can add Serial IO 7 | to any OctoPrint instance. Use a micro controller like the 8 | Esp8266/ESP32, Arduino Mega, Nano or some other MCU capable of Serial communications. 9 | 10 | Use the micro controllers Digital IO from within the Octoprint interface. An alternative to using GPIO/local IO on a Raspberry Pi like device. Great for Windows users as well as users of other linux devices that do not have native IO like the Raspberry Pi. 11 | 12 | Some hardware suggestions are listed at the end of this document. 13 | 14 | ## Setup 15 | 16 | Install via the bundled [Plugin Manager](https://docs.octoprint.org/en/master/bundledplugins/pluginmanager.html) 17 | or manually using this URL: 18 | 19 | https://github.com/jcassel/OctoPrint-Siocontrol/archive/main.zip 20 | 21 | ## Getting Started 22 | First you should choose the IO controller and ensure it is working as expected. Ensure you know what IO numbers are setup as Inputs and Outputs. Once ready continue on. If you are not sure on this topic, there are some hardware options listed at the bottom that require little to no knowlege of programing an MCU. 23 | 24 | Before you plug in your IO device, you should ensure that you can connect to your printer. Make note of the port that your printer is on. The SIOControl can sometimes be mistken by OctoPrint as a printer. 25 | 26 | ## Configuration 27 | 28 | ### Serial Connection 29 | ![Connection and integrations](Assets/img/SettingsExampleConn.PNG) 30 | 31 | 32 | Configure the Serial Port Details. (Enter the settings dialog for the SIOControl PlugIn) 33 | - Port Path(Linux) or Port Name(Windows). You may need to hit refresh to see the new port for your IO. 34 | - Baud rate (default firmware is 115200) 35 | - Sensing/reporting interval (default is 3000ms (3 seconds)). There should be no problem setting this higher or lower. Higher interval is good if you have a slower PC. Keep in mind that the IO Device will report on any changes in the state of the IO so even when the interval is high, you should not miss any transitions. 36 | - Push the connect button at the right of the Port selection. This will use the settings to try to connect. If it fails to connect, check your USB connection and ensure that your selecting the correct port. 37 | Save these settings check in the lower left navigation pain. It should now say "Connected". If not you may not have put in the correct connection details. You can now reopen the SIO setting and assign the rest of the details as needed. 38 | 39 | ### Integrations 40 | ![Integrations](Assets/img/SettingsExampleIntegrations.PNG) 41 | 42 | Simple selections for integrations (Optional) 43 | - Enable and select IO point for PSU Control Sub Plugin. 44 | - Enable and select IO point for physical EStop. 45 | - Enable and select IO point for Filament runout sensor. 46 | 47 | 48 | ### IO Configuration 49 | ![IO Configuration](Assets/img/SettingsExampleIOConfig.PNG) 50 | 51 | 52 | As you add SIO configurations they will appear on the side bar for interaction and monitoring: 53 | - select icon using icon picker (or typing manually) for easy identification 54 | - Type in a short name for the device connected to the IO Pin 55 | - Select the IO number for your IO point. Note that the available numbers just a list of numbers, if you select a IO number that does not exist, you will get an error message. 56 | - Select if device an Input or an Output and what is its active state. 57 | - Out_HIGH means that this Pin is an output and its resting state is LOW. When turned on, it will go HIGH (To V+). 58 | - In_LOW means that this is an input and its resting state is HiGH. When it is turned on it will go LOW(To ground). 59 | - select if IO point should be on or off by default after/on startup.(Only applies to Outputs) 60 | - Off would mean that no command will be sent to the IO after start up. 61 | - On would cause a command to set the Pin to its active level after start up of OctoPrint. 62 | 63 | 64 | ### Note: 65 | Configuration of a IO Point makes it accessible in the sideNav. It is ok if the IO Point is used by the other integrations like PSUCorol. Inputs and outputs not configured at the device level from within OctoPrint. Misconfiguring a Pin in the interface will not change the pins type in the controller. To set the pins IO type see the directions for that device. The official example firmware repositories all have some general instructions on how to set IO point types either in the device code or as part of the readme. 66 | 67 | IO Point numbers and configurations must match the IO device firmware setup. The [OctoPrint-Serial IO Board](https://www.tindie.com/products/softwaresedge/octoprint-serial-io-kit/) offered on Tindie has 2 relays, 1 Status LED, 4 designated inputs and 6 other IO points that can be setup as either inputs or outputs. 68 | 69 | ## Setting filament runout sensor 70 | This feature sends a pause command to the printer when the IO point is activated. This will only happen if OctoPrint has the status of “printing”. Essentially, it does the same thing as one was to push the pause button on the OctoPrint console. To make this really useful and allow for changing of the filament, you have to add some scripting to OctoPrint so it can take appropriate actions when it is asked to pause a running print. 71 | 72 | When OctoPrint pauses a print job, the printer will stops and the head remains in the same position it was in when the pause was issued. This is not a good place to change the filament. What is needed is a way to move the head to a good spot to change the filament. This can be done with a script that is evoked when the printer is paused. Let’s add a script to OctoPrint that will run when a print is paused. 73 | Navigate to the OctoPrint Settings GCODE Scripts dialog, as seen in this image. You will have to scroll down a little to see the 2 scripts that we are interested in. 74 | 75 | "After print job is paused" and "Before print job is resumed" 76 | 77 | ![GCodeScript](Assets/img/GCodeScriptDialogFR1.PNG) 78 | 79 | __In the After print job is paused script section, enter this script.__ 80 | 81 | ``` 82 | {% if pause_position.x is not none %} 83 | ;set to relative movement XYZ then E 84 | G91 85 | M83 86 | ; move Z by 20mm, Retract filament of 0.8mm 87 | G1 Z+20 E-0.8 F4500 88 | ; set to absolute movement XYZ then E 89 | M82 90 | G90 91 | ; move to a safe XY position, ***Change this if needed*** 92 | G1 X0 Y0 93 | {% endif %} 94 | ``` 95 | 96 | Now when every OctoPrint pauses a print, it will move as described in the script you entered. The example here sends the Z up by 20mm above the current layer. And then moves the head to X0 and Y0 (Likely front left of your printer) to make it easy for you to do a change of the filament. 97 | 98 | Now we need to ensure that when we resume the printer it starts back at the correct spot without hitting the print in progress. 99 | 100 | In the Before print job is resumed script section, enter this script. 101 | 102 | ``` 103 | {% if pause_position.x is not none %} 104 | ;set to relative movement for E 105 | M83 106 | ; re-prime nozzle so when we start to print it is good to go. 107 | G1 E-0.8 F4500 108 | G1 E1.6 F4500 109 | ; set to absolute movement for E 110 | M82 111 | ; set to absolute movement XYZ 112 | G90 113 | ; reset E’s position to the original position it was in at pause 114 | G92 E{{ pause_position.e }} 115 | ;**** use M83 or M82(extruder absolute mode) according what your slicer generates**** 116 | M82 ; set extruder to relative movement 117 | ; move back to pause position XY first and then Z (avoid hitting anything on the xy move) 118 | G1 X{{ pause_position.x }} Y{{ pause_position.y }} F4500 119 | G1 Z{{ pause_position.z }} F4500 120 | ; reset to feed rate if needed. 121 | {% if pause_position.f is not none %} 122 | G1 F{{ pause_position.f }}{% endif %} 123 | {% endif %} 124 | ``` 125 | 126 | 127 | ## 128 | ## Hardware options 129 | The number of IO and use case is configurable in the firmware of the micro controller. The serial protocol used is simple and can be ported to just about any micro controller with ease. There are several examples of firmware that can be used as is or adjusted to your needs. There are also several off the shelf IO board kits that can be purchased if you do not want to design and build one yourself. 130 | 131 | ### There are a few options over on Tindie if you want something more or less ready to go. 132 | - [2 Channel Relay board](https://www.tindie.com/products/softwaresedge/octoprint-siocontrol-2-relay-module/) 133 | - [Plug and play 2 Channel Relay board Kit with up to 11 other IO points](https://www.tindie.com/products/softwaresedge/octoprint-serial-io-kit/) 134 | - [4 Channel Relay board](https://www.tindie.com/products/softwaresedge/octoprint-siocontrol-4-relay-module/) 135 | 136 | 137 | ### Or you can also do it more DIY with options like these. 138 | - [CANADUINO PLC MEGA328](https://www.amazon.com/dp/B085F3YRK4) with 6 relay outputs and 4 digital inputs. This board can be a great option having both inputs and outputs,although pricy for what you get and it also does not come assembled. Meaning it requires a lot of soldering. 139 | - One could aslo adapt things to work with the standard Arduino Mega2560 plus PKA05Shield Take a look at the example firmware. [Octoprint_SIOControl_Firmware repository](https://github.com/jcassel/OctoPrint_SIOControl_Firmware) to just about any arduino device. 140 | 141 | 142 | ### Support 143 | I will be working on the wiki but in the mean time, if you are looking for some support, I lurk a lot on the [OctoPrint community site](https://community.octoprint.org/). Post there in the plugins section and I will likely see it and respond. 144 | 145 | 146 | 147 | ## Recognition of reference works 148 | Thank you to the other plugin developers doing great work in this space. 149 | 150 | - [GpoiControl(@catgiggle)](https://github.com/catgiggle/OctoPrint-GpioControl) A lot of the initial code for this was directly pulled from GPIO Control as a starting point. 151 | - [PSUControl(kantlivelong)](https://github.com/kantlivelong/OctoPrint-PSUControl) A well known and well used bit of code that I knew I could rely on as a good example of what to do. 152 | - [jneilliii](https://github.com/jneilliii) so many great Plugins. The BedLevelVisualizer specifically was very helpful in working out how to deal with core ViewModels. 153 | 154 | -------------------------------------------------------------------------------- /octoprint_siocontrol/static/js/siocontrol.js: -------------------------------------------------------------------------------- 1 | /* 2 | * View model for OctoPrint-SioControl 3 | * 4 | * Author: JCsGotThis 5 | * License: AGPLv3 6 | */ 7 | $(function () { 8 | function SioControlViewModel(parameters) { 9 | var self = this; 10 | self.settingsViewModel = parameters[0]; 11 | self.controlViewModel = parameters[1]; 12 | self.loginStateViewModel = parameters[2]; 13 | self.accessViewModel = parameters[3]; 14 | 15 | self.sioButtons = ko.observableArray(); 16 | self.sioConfigurations = ko.observableArray(); 17 | self.SIO_IOCounts = ko.observableArray(); 18 | self.SIO_BaudRate = ""; 19 | self.SIO_BaudRates = []; 20 | self.SIO_Port = ko.observable(''); 21 | self.SIO_Ports = ko.observableArray(); 22 | self.IOStatusMessage = ko.observable('Ready'); 23 | self.IOSWarnings = ko.observable(''); 24 | 25 | self.SIO_SI = 3001; 26 | 27 | SIO_EnablePSUIOPoint = 0; 28 | SIO_PSUIOPoint = "-1"; 29 | SIO_InvertPSUIOPoint = ""; 30 | 31 | SIO_EnableESTIOPoint = 0; 32 | SIO_ESTIOPoint = "-1" 33 | SIO_InvertESTIOPoint = ""; 34 | 35 | SIO_EnableFRSIOPoint = 0; 36 | SIO_FRSIOPoint = "-1"; 37 | SIO_InvertFRSIOPoint = ""; 38 | 39 | SioButtonStatusUpdateInterval = 1000; 40 | btnStates = []; 41 | 42 | 43 | var SioBtnInterval = -1; //setup for later access to clearInterval(SioBtnInterval); 44 | 45 | 46 | 47 | self.onBeforeBinding = function () { 48 | self.sioConfigurations(self.settingsViewModel.settings.plugins.siocontrol.sio_configurations.slice(0)); 49 | 50 | 51 | if (self.SIO_Port != null) { 52 | if (self.SIO_IOCounts().length == 0) { 53 | self.getIOCounts(); 54 | console.log(self.SIO_IOCounts().length); 55 | } 56 | 57 | SioBtnInterval = setInterval(function () { 58 | self.updateSioButtons(); 59 | }, self.SIO_SI); 60 | } 61 | 62 | 63 | self.IOStatusMessage(self.settingsViewModel.settings.plugins.siocontrol.IOStatusMessage()); 64 | self.IOSWarnings(self.settingsViewModel.settings.plugins.siocontrol.IOSWarnings()); 65 | 66 | self.SIO_Port(self.settingsViewModel.settings.plugins.siocontrol.IOPort()); 67 | self.SIO_Ports(self.settingsViewModel.settings.plugins.siocontrol.IOPorts()); 68 | self.SIO_BaudRate = self.settingsViewModel.settings.plugins.siocontrol.IOBaudRate(); 69 | self.SIO_BaudRates = self.settingsViewModel.settings.plugins.siocontrol.IOBaudRates(); 70 | self.SIO_SI = self.settingsViewModel.settings.plugins.siocontrol.IOSI(); 71 | self.SIO_EnablePSUIOPoint = self.settingsViewModel.settings.plugins.siocontrol.EnablePSUIOPoint(); 72 | self.SIO_PSUIOPoint = self.settingsViewModel.settings.plugins.siocontrol.PSUIOPoint(); 73 | self.SIO_InvertPSUIOPoint = self.settingsViewModel.settings.plugins.siocontrol.InvertPSUIOPoint(); 74 | self.SIO_EnableESTIOPoint = self.settingsViewModel.settings.plugins.siocontrol.EnableESTIOPoint(); 75 | self.SIO_ESTIOPoint = self.settingsViewModel.settings.plugins.siocontrol.ESTIOPoint(); 76 | self.SIO_InvertESTIOPoint = self.settingsViewModel.settings.plugins.siocontrol.InvertESTIOPoint(); 77 | self.SIO_EnableFRSIOPoint = self.settingsViewModel.settings.plugins.siocontrol.EnableFRSIOPoint(); 78 | self.SIO_FRSIOPoint = self.settingsViewModel.settings.plugins.siocontrol.FRSIOPoint(); 79 | self.SIO_InvertFRSIOPoint = self.settingsViewModel.settings.plugins.siocontrol.InvertFRSIOPoint(); 80 | 81 | console.log(self.SIO_Port()); //here for debuging. Easy to get to binding packed js 82 | 83 | setInterval(function () { 84 | self.getStatusMessage(); 85 | }, 5000); 86 | 87 | console.log(self.SIO_SI); 88 | 89 | }; 90 | 91 | 92 | self.hasControlPermission = function () { 93 | return self.loginStateViewModel.hasPermission(self.accessViewModel.permissions.CONTROL); 94 | }; 95 | 96 | 97 | self.onSettingsBeforeSave = function () { 98 | self.settingsViewModel.settings.plugins.siocontrol.sio_configurations(self.sioConfigurations.slice(0)); 99 | self.settingsViewModel.settings.plugins.siocontrol.IOPort(self.SIO_Port()); 100 | self.settingsViewModel.settings.plugins.siocontrol.IOBaudRate(self.SIO_BaudRate); 101 | if (self.SIO_SI != self.settingsViewModel.settings.plugins.siocontrol.IOSI()) { 102 | clearInterval(SioBtnInterval); //stop existing polling. 103 | SioBtnInterval = setInterval(function () { //start the updated one. 104 | self.updateSioButtons(); 105 | }, self.SIO_SI); 106 | } 107 | self.settingsViewModel.settings.plugins.siocontrol.IOSI(self.SIO_SI); //alwasy update 108 | self.settingsViewModel.settings.plugins.siocontrol.EnablePSUIOPoint(self.SIO_EnablePSUIOPoint); 109 | self.settingsViewModel.settings.plugins.siocontrol.PSUIOPoint(self.SIO_PSUIOPoint); 110 | self.settingsViewModel.settings.plugins.siocontrol.InvertPSUIOPoint(self.SIO_InvertPSUIOPoint); 111 | self.settingsViewModel.settings.plugins.siocontrol.EnableESTIOPoint(self.SIO_EnableESTIOPoint); 112 | self.settingsViewModel.settings.plugins.siocontrol.ESTIOPoint(self.SIO_ESTIOPoint); 113 | self.settingsViewModel.settings.plugins.siocontrol.InvertESTIOPoint(self.SIO_InvertESTIOPoint); 114 | self.settingsViewModel.settings.plugins.siocontrol.EnableFRSIOPoint(self.SIO_EnableFRSIOPoint); 115 | self.settingsViewModel.settings.plugins.siocontrol.FRSIOPoint(self.SIO_FRSIOPoint); 116 | self.settingsViewModel.settings.plugins.siocontrol.InvertFRSIOPoint(self.SIO_InvertFRSIOPoint); 117 | self.updateSioButtons(); 118 | self.getIOCounts(); 119 | }; 120 | 121 | self.onSettingsShown = function () { 122 | self.sioConfigurations(self.settingsViewModel.settings.plugins.siocontrol.sio_configurations.slice(0)); 123 | self.updateIconPicker(); 124 | }; 125 | 126 | self.onSettingsHidden = function () { 127 | self.sioConfigurations(self.settingsViewModel.settings.plugins.siocontrol.sio_configurations.slice(0)); 128 | }; 129 | 130 | self.addSioConfiguration = function () { 131 | self.sioConfigurations.push({ pin: 0, icon: "fas fa-plug", name: "", active_mode: "active_out_high", default_state: "default_off", on_nav: 0,on_side:0 }); 132 | self.updateIconPicker(); 133 | }; 134 | 135 | 136 | 137 | self.removeSioConfiguration = function (configuration) { 138 | self.sioConfigurations.remove(configuration); 139 | }; 140 | 141 | 142 | self.updateSioButtons = function () { 143 | if (self.SIO_IOCounts().length == 0) { 144 | self.requestIOCounts(); 145 | } 146 | 147 | OctoPrint.simpleApiGet("siocontrol").then(function (states) { 148 | updateBtns = false; 149 | 150 | if (self.btnStates === undefined) { 151 | self.btnStates = states; 152 | updateBtns = true; //first time through 153 | } 154 | 155 | for (i = 0; i < states.length; i++) { 156 | if (states[i] != self.btnStates[i]) { 157 | self.btnStates = states; 158 | updateBtns = true; 159 | continue; 160 | } 161 | } 162 | 163 | //if (updateBtns) { 164 | 165 | self.sioButtons(ko.toJS(self.sioConfigurations).map(function (item) { 166 | return { 167 | icon: item.icon, 168 | name: item.name, 169 | current_state: "unknown", 170 | active_mode: item.active_mode, 171 | pin: item.pin, 172 | on_nav: item.on_nav, 173 | on_side: item.on_side, 174 | } 175 | })); 176 | 177 | self.sioButtons().forEach(function (item, index) { 178 | self.sioButtons.replace(item, { 179 | icon: item.icon, 180 | name: item.name, 181 | current_state: states[index], 182 | active_mode: item.active_mode, 183 | pin: item.pin, 184 | on_nav: item.on_nav, 185 | on_side: item.on_side, 186 | }); 187 | 188 | //removeClass("off").addClass("on"); 189 | }); 190 | //} 191 | 192 | }); 193 | }; 194 | 195 | 196 | self.addSioScriptAlignment = function () { 197 | self.sioScriptAlignments.push({ name: "", pin: -1, trigger_type: "" }); 198 | }; 199 | 200 | self.removeSioScriptAlignment = function (alignments) { 201 | self.sioScriptAlignments.remove(alignments); 202 | }; 203 | 204 | self.getStatusMessage = function () { 205 | OctoPrint.simpleApiCommand("siocontrol", "getStatusMessage", {}).then(function (status) { 206 | self.IOStatusMessage(status[0]); 207 | self.IOSWarnings(status[1]); 208 | }); 209 | 210 | 211 | }; 212 | 213 | self.connectIO = function () { 214 | OctoPrint.simpleApiCommand("siocontrol", "connectIO", { port: self.SIO_Port(), baudRate: self.SIO_BaudRate, si: self.SIO_SI }).then(function (connected) { 215 | //no need to do anything the status will update. 216 | }); 217 | }; 218 | 219 | self.getIOCounts = function () { 220 | //self.requestIOCounts(); 221 | 222 | setTimeout(function () { 223 | self.requestIOCounts(); 224 | }, self.SIO_SI * 2); //when you change ports.. you got to give it enough time to connect and calculate the count. 225 | } 226 | 227 | self.requestIOCounts = function () { 228 | OctoPrint.simpleApiCommand("siocontrol", "getIOCounts", {}).then(function (counts) { 229 | self.SIO_IOCounts(counts); 230 | }); 231 | } 232 | 233 | 234 | self.getPorts = function () { 235 | OctoPrint.simpleApiCommand("siocontrol", "getPorts", {}).then(function (ports) { 236 | self.SIO_Ports(ports); 237 | }); 238 | } 239 | 240 | self.toggleSioByIONumber = function (sioPin) { 241 | OctoPrint.simpleApiCommand("siocontrol", "toggelSio", { pin: sioPin }).then(function (state) { 242 | //self.sioButtons.indexOf(this)["current_state"] = state; 243 | }); 244 | self.updateSioButtons(); 245 | } 246 | 247 | self.turnSioOnByIONumber = function (sioId) { 248 | OctoPrint.simpleApiCommand("siocontrol", "turnSioOn", { id: sioId }).then(function (state) { 249 | //self.sioButtons.indexOf(this)["current_state"] = state; 250 | }); 251 | self.updateSioButtons(); 252 | } 253 | 254 | self.turnSioOffByIONumber = function (sioId) { 255 | OctoPrint.simpleApiCommand("siocontrol", "turnSioOff", { id: sioId }).then(function (state) { 256 | //self.sioButtons.indexOf(this)["current_state"] = state; 257 | }); 258 | self.updateSioButtons(); 259 | } 260 | 261 | self.turnSioOn = function () { 262 | self.turnSioOnByIONumber(self.sioButtons.indexOf(this)); 263 | } 264 | 265 | self.turnSioOff = function () { 266 | self.turnSioOffByIONumber(self.sioButtons.indexOf(this)); 267 | } 268 | 269 | self.getBtnCls = function () { 270 | pin = self.sioButtons.indexOf(this); 271 | mode = self.sioConfigurations(pin)["active_mode"]; 272 | console.log(mode); 273 | return "btn"; 274 | 275 | } 276 | 277 | 278 | 279 | 280 | 281 | self.updateIconPicker = function () { 282 | $('.iconpicker').each(function (index, item) { 283 | $(item).iconpicker({ 284 | placement: "bottomLeft", 285 | hideOnSelect: true, 286 | }); 287 | }); 288 | }; 289 | 290 | 291 | 292 | 293 | } 294 | 295 | OCTOPRINT_VIEWMODELS.push({ 296 | construct: SioControlViewModel, 297 | dependencies: ["settingsViewModel", "controlViewModel", "loginStateViewModel", "accessViewModel"], 298 | elements: ["#settings_plugin_siocontrol", "#sidebar_plugin_siocontrol", "#navbar_plugin_siocontrol"] 299 | }); 300 | }); 301 | -------------------------------------------------------------------------------- /octoprint_siocontrol/templates/siocontrol_settings.jinja2: -------------------------------------------------------------------------------- 1 |
2 |
3 |

SIO Control Settings

4 | 10 |
11 |
12 |
13 |

IO Connection Settings

14 |
15 | You must save basic connection settings before you can connect. 16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 |
50 |
51 | 57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 |

Integrations

66 |
67 |
68 |
Type
69 |
Enable?
70 |
IO#
71 |
72 |
Active LOW?
73 |
74 |
75 |
76 |
77 | 78 |
79 |
80 | 81 |
82 |
83 | 84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 | 93 |
94 |
95 | 96 |
97 |
98 | 99 |
100 |
101 |
102 | 103 |
104 |
105 |
106 |
107 | 108 |
109 |
110 | 111 |
112 |
113 | 114 |
115 |
116 |
117 | 118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |

IO Configuration

126 |
127 | Add a row below for each IO point you wish to monitor or control. 128 | Each row represents a digital IO point. 129 | To add an Icon button to the navigation bar, check column [Nav]. 130 | To include the control on the side Nav, check column [Side]. 131 |

132 |
133 |
134 |

Icon

135 |

Label

136 |

IO#

137 |

Active As

138 |

Default To

139 |

Nav

140 |

Side

141 |

142 |
143 | 144 |
145 |
146 |
147 |
148 | 149 | 150 |
151 |
152 |
153 | 154 |
155 |
156 | 157 |
158 |
159 | 165 |
166 |
167 | 171 |
172 |
173 | 174 |
175 |
176 | 177 |
178 |
179 | 180 | 181 | 182 |
183 |
184 |
185 | 186 |
187 |
188 | 189 |
190 |
191 |
192 |
193 | 233 |
234 |
235 |
236 | -------------------------------------------------------------------------------- /octoprint_siocontrol/Connection.py: -------------------------------------------------------------------------------- 1 | # import fnmatch 2 | import glob 3 | import os 4 | import re 5 | 6 | # import sys 7 | import threading 8 | import time 9 | 10 | import serial 11 | import serial.tools.list_ports 12 | 13 | import octoprint.plugin 14 | 15 | # from octoprint.filemanager import valid_file_type 16 | # from octoprint.filemanager.destinations import FileDestinations 17 | from octoprint.settings import settings 18 | 19 | try: 20 | import winreg 21 | except ImportError: 22 | try: 23 | import _winreg as winreg # type: ignore 24 | except ImportError: 25 | pass 26 | 27 | regex_serial_devices = re.compile(r"^(?:ttyUSB|ttyACM|tty\.usb|cu\.|cuaU|ttyS|rfcomm).*") 28 | """Regex used to filter out valid tty devices""" 29 | 30 | 31 | class Connection: 32 | def __init__(self, plugin): 33 | self._logger = plugin._logger 34 | self._printer = plugin._printer 35 | self._printer_profile_manager = plugin._printer_profile_manager 36 | self._plugin_manager = plugin._plugin_manager 37 | self._identifier = plugin._identifier 38 | self._settings = plugin._settings 39 | self.plugin = plugin 40 | self.readThread = None 41 | self.pauseReadThread = False 42 | self.readThreadStop = False 43 | self.writeThread = None 44 | self.pauseWriteThread = False 45 | self.writeThreadStop = False 46 | self._connected = False 47 | self.serialConn = None 48 | self.gCodeExtrusion = 0 49 | self.boxExtrusion = 0 50 | self.boxExtrusionOffset = 0 51 | self.IOCount = 0 52 | self.commandQueue = [] 53 | self.enableCommandQueue = False 54 | self.deviceIsCompatible = False 55 | 56 | # this value should eventually be exposed. 57 | # It could be different depending on the controler 58 | self.iReadTimeoutCounterMax = 15 59 | 60 | def getVersionCompatibilty(self): 61 | self.send("VC") 62 | 63 | def IODeviceInitialize(self): 64 | self.serialConn.reset_input_buffer() 65 | self.serialConn.reset_output_buffer() 66 | self.serialConn.write("VC\n".encode()) # request version and compatibility info 67 | checkingCompatibility = True 68 | iReadTimeoutCounter = 0 69 | while checkingCompatibility is True: 70 | line = self.serialConn.readline() 71 | 72 | if line: 73 | try: 74 | line = line.strip().decode() 75 | except Exception: 76 | pass 77 | if line[:2] == "VI": 78 | self._logger.debug("IO Reported Version as:{}".format(line[3:])) 79 | 80 | elif line[:2] == "CP": 81 | self._logger.debug("IO Reported Compatibility as:{}".format(line)) 82 | if line[3:] != self.plugin.DeviceCompatibleVersion: 83 | self._logger.info("IO Reported Compatibility as:{}".format(line)) 84 | self._logger.info("Required Compatibility is:{}".format(self.plugin.DeviceCompatibleVersion)) 85 | self.disconnect() 86 | self._connected = False 87 | self._logger.error("IO Not compatible with this version of SIOPlugin") 88 | self._logger.error("Stopping communications to SIO") 89 | self.stopCommThreads() 90 | self.plugin.IOSWarnings = ("Conneced to encompatible device. Check Comm port setting") 91 | self.deviceIsCompatible = False 92 | else: # all good 93 | self.deviceIsCompatible = True 94 | self.send("IC") # que up request for IO Count 95 | self.plugin.IOSWarnings = "" 96 | # no matter what the result is we are done here. 97 | checkingCompatibility = False 98 | elif line[:2] == "OK": 99 | self._logger.debug("IO Responded with Ack:{}".format(line)) 100 | elif (line[:2] == "IO" or line[:2] == "RR" or line[:2] == "IT"): 101 | # ignore 102 | self._logger.debug("Unexpected(valid) Comm during compatibility check:{}".format(line)) 103 | # Attempt to reset comm and resend VC request 104 | # this can be needed for controllers that reset on connect. 105 | self._logger.debug("resetting comms resending VC request") 106 | self.serialConn.reset_input_buffer() 107 | self.serialConn.reset_output_buffer() 108 | self.serialConn.write("VC\n".encode()) 109 | elif line[:2] == "DG": 110 | # truly ignore 111 | self._logger.debug("Unexpected(valid) debug Comm during compatibility check:{}".format(line)) 112 | else: 113 | self._logger.debug("Unexpected Comm during compatibility check:{}".format(line)) 114 | iReadTimeoutCounter = iReadTimeoutCounter + 1 115 | self._logger.error("IO readtimeout Count:{}".format(iReadTimeoutCounter)) 116 | if iReadTimeoutCounter == (self.iReadTimeoutCounterMax / 2): # Attempt to reset comm and resend VC request 117 | self._logger.debug("resetting comms resending VC request") 118 | self.serialConn.reset_input_buffer() 119 | self.serialConn.reset_output_buffer() 120 | self.serialConn.write("VC\n".encode()) # request version and compatibility info 121 | 122 | if iReadTimeoutCounter > self.iReadTimeoutCounterMax: 123 | self.disconnect() 124 | self._connected = False 125 | self.stopCommThreads() 126 | self.plugin.IOSWarnings = ("Conneced to encompatible device. Check Comm port setting") 127 | self.deviceIsCompatible = False # Seems that the device is not responding. 128 | checkingCompatibility = False 129 | else: 130 | iReadTimeoutCounter = iReadTimeoutCounter + 1 131 | self._logger.error("IO readtimeout Count:{}".format(iReadTimeoutCounter)) 132 | 133 | if iReadTimeoutCounter > self.iReadTimeoutCounterMax: 134 | self.deviceIsCompatible = False 135 | # Seems that the device is not responding. 136 | checkingCompatibility = False 137 | 138 | def disconnect(self): 139 | self.commandQueue = [] # empty command queue 140 | self._logger.info("cleared Command queue") 141 | while self.serialConn.is_open: 142 | self.serialConn.close() 143 | time.sleep(1) 144 | 145 | self.IOCount = 0 146 | self._connected = False 147 | self.plugin.IOStatus = "Disconnected" 148 | return 149 | 150 | def connect(self): 151 | try: 152 | if (str(self._settings.get(["IOPort"])) != "None" and str(self._settings.get(["IOBaudRate"])) != "None"): 153 | self._logger.info("Connecting...") 154 | self._logger.info("Port:" + self._settings.get(["IOPort"])) 155 | self._logger.info("IOBaudRate:" + self._settings.get(["IOBaudRate"])) 156 | 157 | self.serialConn = serial.Serial( 158 | self._settings.get(["IOPort"]), 159 | int(self._settings.get(["IOBaudRate"])), 160 | timeout=1, 161 | ) 162 | 163 | if self.serialConn.is_open: 164 | self.commandQueue = [] 165 | self._logger.debug("cleared Command queue") 166 | self._connected = True 167 | self.IODeviceInitialize() 168 | if self.deviceIsCompatible is True: 169 | self.plugin.IOStatus = "Connected" 170 | self.plugin.IOWarnings = " " 171 | self._logger.debug("Starting read thread...") 172 | self.startCommThreads() 173 | else: 174 | self.plugin.IOStatus = "SIO device incompatible" 175 | self._logger.info("SIO device incompatible") 176 | self._connected = False 177 | else: 178 | self.plugin.IOStatus = "Could not open port" 179 | self._logger.info("Could not open port") 180 | self._connected = False 181 | else: 182 | self._logger.info("Connection Information not set. Conneciton to SIO not attempted.") 183 | self.plugin.IOStatus = "Conn settings error" 184 | self._connected = False 185 | 186 | except serial.SerialException as err: 187 | self.commandQueue = [] 188 | self._logger.debug("cleared Command queue") 189 | self._logger.debug("Connection failed!") 190 | self._logger.exception("Serial Exception: {}, {}".format(err,type(err))) 191 | 192 | except Exception as err: 193 | self._logger.exception("Unexpected {}, {}".format(err,type(err))) 194 | 195 | def Update_IOSI(self, value): 196 | self.send("SI " + value) 197 | 198 | def checkActionIO(self): 199 | self.checkEStop() 200 | self.checkFilamentRunOut() 201 | 202 | def checkFilamentRunOut(self): 203 | if not self._settings.get(["EnableFRSIOPoint"]): 204 | return 205 | 206 | if ( 207 | int(self._settings.get(["FRSIOPoint"])) >= len(self.plugin.IOCurrent) or int(self._settings.get(["FRSIOPoint"])) < 0 208 | ): 209 | self._logger.info("Filament RunOut IO point is out of range.") 210 | return 211 | 212 | if self._settings.get(["InvertFRSIOPoint"]): 213 | filamentOut = (self.plugin.IOCurrent[int(self._settings.get(["FRSIOPoint"]))] == "0") 214 | else: 215 | filamentOut = (self.plugin.IOCurrent[int(self._settings.get(["FRSIOPoint"]))] == "1") 216 | 217 | if filamentOut: 218 | if self._printer.is_printing(): 219 | self._logger.info("Detected Filament RunOut") 220 | self._printer.toggle_pause_print() 221 | 222 | def checkEStop(self): 223 | estopPushed = None 224 | if not self._settings.get(["EnableESTIOPoint"]): 225 | return 226 | 227 | if ( 228 | int(self._settings.get(["ESTIOPoint"])) >= len(self.plugin.IOCurrent) or int(self._settings.get(["ESTIOPoint"])) < 0 229 | ): 230 | self._logger.info("E-Stop IO point is out of range.") 231 | return 232 | 233 | if self._settings.get(["InvertESTIOPoint"]): 234 | estopPushed = ( 235 | self.plugin.IOCurrent[int(self._settings.get(["ESTIOPoint"]))] == "0" 236 | ) 237 | else: 238 | estopPushed = ( 239 | self.plugin.IOCurrent[int(self._settings.get(["ESTIOPoint"]))] == "1" 240 | ) 241 | 242 | if estopPushed: # estop 243 | self._printer.commands(["M112"]) 244 | 245 | def send(self, data): 246 | self.commandQueue.append("{}\n".format(data).encode()) # f"{data}\n".encode() 247 | self._logger.debug("Queueing Command: %s" % data) 248 | 249 | def write_thread(self, serialConnection): 250 | pauseWasSent = False 251 | while self.is_connected and self.readThreadStop is False: 252 | try: 253 | time.sleep(0.125) 254 | if self.enableCommandQueue is True and len(self.commandQueue) > 0: 255 | self.pauseReadThread = True 256 | if len(self.commandQueue) > 1 and pauseWasSent is False: 257 | serialConnection.reset_input_buffer() 258 | serialConnection.write("EIO\n".encode()) 259 | command = "EIO-NoPop" 260 | pauseWasSent = True 261 | else: 262 | command = self.commandQueue[0] 263 | serialConnection.write(command) 264 | self._logger.debug("SOI Sent:{}".format(command)) 265 | 266 | time.sleep(0.1) 267 | line = serialConnection.readline() 268 | if line: 269 | try: 270 | line = line.strip().decode() 271 | except Exception: 272 | pass 273 | self._logger.debug(">IO Responded with:{}".format(line)) 274 | if line[:2] == "OK" and command != "EIO-NoPop": 275 | pcommand = self.commandQueue.pop(0) 276 | self._logger.debug("Poped Command: %s" % pcommand) 277 | # errorCount = 0 278 | 279 | else: 280 | if pauseWasSent is True: 281 | pauseWasSent = False 282 | serialConnection.write("BIO\n".encode()) 283 | self._logger.debug("SOI Sent:BIO") 284 | 285 | self.pauseReadThread = False 286 | 287 | except serial.SerialException: 288 | self.disconnect() 289 | self._connected = False 290 | self._logger.error("error reading from USB") 291 | self.stopCommThreads() 292 | 293 | self._logger.debug("Write Thread: Thread stopped.") 294 | 295 | def read_thread(self, serialConnection): 296 | # need a short delay here for devices that reboot on connect like the nanno 297 | time.sleep(1) # time for write thread to do work. 298 | errorCount = 0 299 | self._logger.debug("Read Thread: Starting thread") 300 | self.enableCommandQueue = False 301 | while self.readThreadStop is False: 302 | try: 303 | if ( 304 | len(self.commandQueue) == 0 or self.enableCommandQueue is False 305 | ) and self.pauseReadThread is False: 306 | line = serialConnection.readline() 307 | if line: 308 | try: 309 | line = line.strip().decode() 310 | except Exception: 311 | pass 312 | # send line to down streem sub plugins before it is processed here. Note that sub PlugIns alter the line. 313 | # this is important because a subplugins must get the line for review. If adding something to the firmware / sub plugin, that will respond to 314 | # a subplugin, it should have a prefix with an "XT" as the lead 2 characters. If it does not have a known prefix, it will cause 315 | # the error recieved count to raise and might casue a disconnect. 316 | self.plugin.serialRecievequeue(line) 317 | if line[:2] == "VI": 318 | self._logger.debug("IO Reported Version as: {}".format(line)) 319 | errorCount = 0 320 | 321 | elif line[:2] == "CP": 322 | self._logger.debug("IO Reported Compatibility as: {}".format(line)) 323 | if line[3:] != self.plugin.DeviceCompatibleVersion: 324 | self._logger.info("IO Reported Compatibility as: {}".format(line)) 325 | self._logger.info("Required Compatibility is: {}".format(self.plugin.DeviceCompatibleVersion)) 326 | self.disconnect() 327 | self._connected = False 328 | self._logger.error("IO Not compatible with this version of SIOPlugin") 329 | self._logger.error("Stopping communications to SIO") 330 | self.stopCommThreads() 331 | errorCount = 0 332 | 333 | elif line[:2] == "IO": 334 | self._logger.debug("IO Reported State as: {}".format(line)) 335 | if (self.plugin.IOCurrent != line[3:]): # only react to changes. Maybe future have a timeout somewhere for no reports 336 | self._logger.info("IO Reported State change as:{}".format(line)) 337 | self.plugin.IOCurrent = line[3:] 338 | self.IOCount = len(self.plugin.IOCurrent) 339 | self.checkActionIO() 340 | self.plugin.broadCastStateToSubPlugins() # calls methods in Sub plugins for changed IO state. 341 | 342 | errorCount = 0 343 | 344 | elif line[:2] == "OK": 345 | self._logger.debug("IO Responded with Ack: {}".format(line)) 346 | errorCount = 0 347 | 348 | elif line[:2] == "IC": # explicit report IO count. 349 | self.IOCount = int(line[3:]) 350 | errorCount = 0 351 | 352 | elif line[:2] == "RR": # IO ready for commands 353 | self._logger.debug("IO claimed ready for commands: {}".format(line)) 354 | self.enableCommandQueue = True 355 | errorCount = 0 356 | 357 | elif line[:2] == "IT": # IO type List 358 | self._logger.debug("IO Type list recieved: {}".format(line)) 359 | errorCount = 0 360 | 361 | elif line[:2] == "DG": # Debug Message 362 | self._logger.debug("IO sent debug message: {}".format(line)) 363 | errorCount = 0 364 | elif line[:2] == "FS": # 4MB with spiffs(1.2MB APP/1.5 SPIFFS) This is expected firmware format 365 | self._logger.debug("IO Responded with Firmware information: {}".format(line)) 366 | errorCount = 0 367 | elif line[:2] == "XT": # this is an extended message set. Liklely from a custom change in the firmware or maybe to support a sub PlugIn 368 | self._logger.debug("IO Responded with Extened message response: {}".format(line)) 369 | errorCount = 0 370 | elif line[:2] == "TC": # IO type descriptors 371 | self._logger.debug("IO Responded with IO Desciptors information: {}".format(line)) 372 | errorCount = 0 373 | else: 374 | self._logger.debug("IO reported an unexpected data line: {}".format(line)) # error? 375 | errorCount = errorCount + 1 376 | if errorCount > self.iReadTimeoutCounterMax: 377 | self.plugin.IOCurrent = "" 378 | self.IOCount = 0 379 | self.disconnect() 380 | self._connected = False 381 | self._logger.error("Too many Comm Errors disconnecting IO") 382 | self.stopCommThreads() 383 | self.plugin.IOStatus = "COMM ERROR" 384 | else: 385 | time.sleep(0.25) # time for write thread to do work. 386 | except serial.SerialException: 387 | self.disconnect() 388 | self._connected = False 389 | self._logger.error("Error reading from USB Comm threads stop called.") 390 | self.stopCommThreads() 391 | 392 | self._logger.debug("Read Thread: Thread stopped.") 393 | 394 | # serialList was copied from util/comm.py in the core of OctoPrint (that unreachabe section is there too. Not sure what its deal is maybe a false pos) 395 | def serialList(self): 396 | if os.name == "nt": 397 | candidates = [] 398 | try: 399 | key = winreg.OpenKey( 400 | winreg.HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM" 401 | ) 402 | i = 0 403 | while True: 404 | portName = winreg.EnumValue(key, i)[1] 405 | if not self.isPrinterPort(portName): 406 | candidates += [portName] 407 | i += 1 408 | 409 | except Exception as err: 410 | # [22] happens on windows machines. This is normal. 411 | if err.errno == 22: 412 | self._logger.debug("Unexpected(windows OK) {}, {}".format(err,type(err))) 413 | else: 414 | self._logger.exception("Unexpected {}, {}".format(err,type(err))) 415 | 416 | pass 417 | else: 418 | candidates = [] 419 | try: 420 | with os.scandir("/dev") as it: 421 | for entry in it: 422 | if regex_serial_devices.match(entry.name): 423 | if not self.isPrinterPort(entry.path): 424 | candidates.append(entry.path) 425 | except Exception: 426 | self._logger.exception( 427 | "Could not scan /dev for serial ports on the system" 428 | ) 429 | 430 | # additional ports 431 | additionalPorts = settings().get(["serial", "additionalPorts"]) 432 | if additionalPorts: 433 | for additional in additionalPorts: 434 | if not additional == "VIRTUAL": 435 | candidates += glob.glob(additional) 436 | 437 | hooks = octoprint.plugin.plugin_manager().get_hooks( 438 | "octoprint.comm.transport.serial.additional_port_names" 439 | ) 440 | for name, hook in hooks.items(): 441 | try: 442 | if not hook(candidates)[0] == "VIRTUAL": 443 | candidates += hook(candidates) 444 | 445 | except Exception: 446 | self._logger.info( 447 | "Error while retrieving additional " 448 | "serial port names from hook {}".format(name) 449 | ) 450 | 451 | # blacklisted ports 452 | #blacklistedPorts = settings().get(["serial", "blacklistedPorts"]) 453 | #if blacklistedPorts: 454 | # for pattern in settings().get(["serial", "blacklistedPorts"]): 455 | # candidates = list( 456 | # filter(lambda x: not fnmatch.fnmatch(x, pattern), candidates) 457 | # ) 458 | 459 | # last used port = first to try, move to start 460 | prev = settings().get(["serial", "port"]) 461 | if prev in candidates: 462 | candidates.remove(prev) 463 | candidates.insert(0, prev) 464 | 465 | return candidates 466 | 467 | def getRealPaths(self, ports): 468 | self._logger.info("Paths: %s" % ports) 469 | for index, port in enumerate(ports): 470 | port = os.path.realpath(port) 471 | ports[index] = port 472 | return ports 473 | 474 | def isPrinterPort(self, selected_port): 475 | if os.name != "nt": 476 | selected_port = os.path.realpath(selected_port) 477 | 478 | printer_port = self._printer.get_current_connection()[1] 479 | self._logger.info( 480 | "Checking is this port: %s the printers connected port?" % selected_port 481 | ) 482 | self._logger.info("Printer port: %s" % printer_port) 483 | # because ports usually have a second available one (.tty or .cu) 484 | printer_port_alt = "" 485 | if printer_port is None: 486 | return False 487 | else: 488 | if "tty." in printer_port: 489 | printer_port_alt = printer_port.replace("tty.", "cu.", 1) 490 | elif "cu." in printer_port: 491 | printer_port_alt = printer_port.replace("cu.", "tty.", 1) 492 | self._logger.info("Printer port alt: %s" % printer_port_alt) 493 | if selected_port == printer_port or selected_port == printer_port_alt: 494 | return True 495 | else: 496 | return False 497 | 498 | def startCommThreads(self): 499 | if self.readThread is None: 500 | self.readThreadStop = False 501 | self.readThread = threading.Thread( 502 | target=self.read_thread, args=(self.serialConn,) 503 | ) 504 | self.readThread.daemon = True 505 | self.readThread.start() 506 | 507 | if self.writeThread is None: 508 | self.writeThreadStop = False 509 | self.writeThread = threading.Thread( 510 | target=self.write_thread, args=(self.serialConn,) 511 | ) 512 | self.writeThread.daemon = True 513 | self.writeThread.start() 514 | 515 | def stopCommThreads(self): 516 | self.readThreadStop = True 517 | if self.readThread and threading.current_thread() != self.readThread: 518 | try: 519 | self.readThread.join() 520 | except Exception: 521 | pass 522 | 523 | self.readThread = None 524 | 525 | self.writeThreadStop = True 526 | if self.writeThread and threading.current_thread() != self.writeThread: 527 | try: 528 | self.writeThread.join() 529 | except Exception: 530 | pass 531 | 532 | self.writeThread = None 533 | 534 | def is_connected(self): 535 | return self._connected 536 | -------------------------------------------------------------------------------- /octoprint_siocontrol/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, print_function 3 | 4 | import flask 5 | 6 | import octoprint.plugin 7 | from octoprint.access.permissions import Permissions 8 | from octoprint.util import fqfn 9 | 10 | from . import Connection 11 | 12 | #import sys 13 | #import threading 14 | #import time 15 | #import traceback 16 | # from time import sleep, time 17 | 18 | 19 | class SiocontrolPlugin( 20 | octoprint.plugin.SettingsPlugin, 21 | octoprint.plugin.AssetPlugin, 22 | octoprint.plugin.TemplatePlugin, 23 | octoprint.plugin.StartupPlugin, 24 | octoprint.plugin.SimpleApiPlugin, 25 | octoprint.plugin.RestartNeedingPlugin, 26 | ): 27 | def __init__(self): 28 | self.config = dict() 29 | self._sub_plugins = dict() 30 | self.DeviceCompatibleVersion = "SIOPlugin 0.1.1" 31 | self.IOCurrent = None 32 | self._lastIOCurent = None 33 | self.IOStatus = "Ready" 34 | self.IOSWarnings = "" 35 | self.conn = None # Connection.Connection(self) 36 | return 37 | 38 | def get_template_vars(self): 39 | avalIOSI = [ 40 | "500", 41 | "1000", 42 | "1500", 43 | "2000", 44 | "3000", 45 | "4000", 46 | "5000", 47 | "6000", 48 | "7000", 49 | "8000", 50 | "9000", 51 | "10000", 52 | ] 53 | # self._settings.set(["IOPorts"], self.conn.serialList()) 54 | avalPorts = self.get_AvaliblePorts() 55 | self._settings.set(["IOPorts"], avalPorts) 56 | self._settings.set(["IOCounts"], self.getCounts()) 57 | 58 | self.has_SIOC = False 59 | available_plugins = [] 60 | for k in list(self._sub_plugins.keys()): 61 | available_plugins.append(dict(pluginIdentifier=k, displayName=self._plugin_manager.plugins[k].name)) 62 | if k == "": # I think this should be != but leaving for now. also maybe the bool has_SIOC should be has_SubPlugIn 63 | self.has_SIOC = True 64 | return { 65 | "availablePlugins": available_plugins, 66 | "hasSIOC": self.has_SIOC, 67 | "PSUIOPoint": self._settings.get(["PSUIOPoint"]), 68 | "ESTIOPoint": self._settings.get(["ESTIOPoint"]), 69 | "EnableESTIOPoint": self._settings.get(["EnableESTIOPoint"]), 70 | "EnablePSUIOPoint": self._settings.get(["EnablePSUIOPoint"]), 71 | "IOSI": self._settings.get(["IOSI"]), 72 | "IOPort": self._settings.get(["IOPort"]), 73 | "IOPorts": self._settings.get(["IOPorts"]), 74 | "IOBaudRate": self._settings.get(["IOBaudRate"]), 75 | "IOBaudRates": self._settings.get(["IOBaudRates"]), 76 | "IOSIs": avalIOSI, 77 | "FRSIOPoint": self._settings.get(["FRSIOPoint"]), 78 | "EnableFRSIOPoint": self._settings.get(["EnableFRSIOPoint"]), 79 | "IOCounts": self._settings.get(["IOCounts"]), 80 | "sio_configurations": self._settings.get(["sio_configurations"]), 81 | "IOStatusMessage": self.IOStatus, 82 | "IOSWarnings": self.IOSWarnings, 83 | } 84 | 85 | def get_template_configs(self): 86 | return [ 87 | dict( 88 | type="settings", 89 | custom_bindings=True, 90 | template="siocontrol_settings.jinja2", 91 | ), 92 | dict( 93 | type="sidebar", 94 | custom_bindings=True, 95 | template="siocontrol_sidebar.jinja2", 96 | icon="map-signs", 97 | ), 98 | ] 99 | 100 | def get_settings_defaults(self): 101 | return dict( 102 | sio_configurations=[], 103 | PSUIOPoint="0", 104 | EnablePSUIOPoint=False, 105 | InvertPSUIOPoint=False, 106 | ESTIOPoint="0", 107 | EnableESTIOPoint=False, 108 | InvertESTIOPoint=False, 109 | FRSIOPoint="0", 110 | InvertFRSIOPoint=False, 111 | EnableFRSIOPoint=False, 112 | IOSI=3000, 113 | IOPort="", 114 | IOBaudRate="115200", 115 | IOBaudRates=["74880", "115200", "230400", "38400", "19200", "9600"], 116 | IOPorts=[], 117 | IOCounts=[], 118 | IOStatusMessage="Unknown Status", 119 | IOSWarnings="", 120 | ) 121 | 122 | def on_settings_initialized(self): 123 | self.reload_settings() 124 | self.clean_Settings() 125 | return super().on_settings_initialized() 126 | 127 | def clean_Settings(self): 128 | # do what is needed to make sure old settings will stay working on an update. 129 | upDatedConfigs = [] 130 | # add Nav Attribute to IO configs 131 | 132 | for configuration in self._settings.get(["sio_configurations"]): 133 | try: 134 | #needsnav = 'on_nav' in configuration 135 | 136 | if 'on_side' not in configuration: 137 | configuration['on_side'] = True # defalt to show all on the side 138 | 139 | if 'on_nav' not in configuration: 140 | configuration['on_nav'] = False 141 | 142 | #"active_mode": configuration["active_mode"], 143 | #"default_state": configuration["default_state"], 144 | #"icon": configuration["icon"], 145 | #"name": configuration["name"], 146 | #"on_nav": False, 147 | #"on_side": True, 148 | #"pin": configuration["pin"], 149 | upDatedConfigs.append(configuration) 150 | except Exception: 151 | self._logger.exception( 152 | "Error Configuration update. Item {} caused an error while checking. You settins maybe lost or you may have to resave them to use new features.".format( 153 | configuration['name'] 154 | )) 155 | pass 156 | # update the settings to have the new values into the future. 157 | self._settings.set(["sio_configurations"], upDatedConfigs) 158 | 159 | def reload_settings(self): 160 | for k, v in self.get_settings_defaults().items(): 161 | if type(v) is str: 162 | v = self._settings.get([k]) 163 | elif type(v) is int: 164 | v = self._settings.get_int([k]) 165 | elif type(v) is float: 166 | v = self._settings.get_float([k]) 167 | elif type(v) is bool: 168 | v = self._settings.get_boolean([k]) 169 | 170 | def setStartUpIO(self): 171 | # Need to workout Non blocking thread with delay or other way to 172 | # handle the comms to the MC better at startup. 173 | # Right now this will just not work for some Microcontrollers. 174 | # Due to the fact that the act of connecting causes it to reset. 175 | # So it is rebooting while these instructions are sent. 176 | # 2023-5-19 I this is is mostly worked out now. With the way the resend of VC works. 177 | 178 | self._logger.info("Setting initial State for Outputs") 179 | 180 | for configuration in self._settings.get(["sio_configurations"]): 181 | 182 | pin = int(configuration["pin"]) 183 | 184 | if pin != -1: 185 | 186 | if configuration["active_mode"] == "active_out_low": 187 | if configuration["default_state"] == "default_on": 188 | self.conn.send(f"IO {pin} 0") 189 | elif configuration["default_state"] == "default_off": 190 | self.conn.send(f"IO {pin} 1") 191 | elif configuration["active_mode"] == "active_out_high": 192 | if configuration["default_state"] == "default_on": 193 | self.conn.send(f"IO {pin} 1") 194 | elif configuration["default_state"] == "default_off": 195 | self.conn.send(f"IO {pin} 0") 196 | 197 | self._logger.info( 198 | "Configured SIO{}: {},{} ({}),{},{}".format( 199 | configuration["pin"], 200 | configuration["active_mode"], 201 | configuration["default_state"], 202 | configuration["name"], 203 | configuration["on_nav"], 204 | configuration["on_side"], 205 | ) 206 | ) 207 | 208 | return 209 | 210 | def on_after_startup(self, *args, **kwargs): 211 | 212 | # connect to IO 213 | self.conn = Connection.Connection(self) 214 | self.conn.connect() 215 | 216 | if self.conn.is_connected(): 217 | self._logger.info("Connected to Serial IO") 218 | self.IOStatus = "Connected" 219 | self.IOSWarnings = " " 220 | self.conn.send("SI " + self._settings.get(["IOSI"])) 221 | self.setStartUpIO() 222 | else: 223 | self.IOStatus = "Could not connect SIO" 224 | self._logger.error("Could not connect SIO") 225 | self._logger.info("IOSI:" + str(self._settings.get(["IOSI"]))) 226 | self._logger.info("IOPort:" + str(self._settings.get(["IOPort"]))) 227 | self._logger.info("IOPorts:" + str(self._settings.get(["IOPorts"]))) 228 | self._logger.info("IOBaudRate:" + str(self._settings.get(["IOBaudRate"]))) 229 | self._logger.info("IOBaudRates" + str(self._settings.get(["IOBaudRates"]))) 230 | 231 | psucontrol_helpers = self._plugin_manager.get_helpers("psucontrol") 232 | if not psucontrol_helpers: 233 | self._logger.warning("PSUControl Plugin not found.") 234 | return 235 | 236 | elif "register_plugin" not in psucontrol_helpers.keys(): 237 | self._logger.warning( 238 | "The version of PSUControl that is installed does not support plugin registration." 239 | ) 240 | 241 | if self._settings.get(["EnablePSUIOPoint"]): 242 | self.IOSWarnings = "PSUControl version mismatch" 243 | return 244 | else: 245 | psucontrol_helpers["register_plugin"](self) 246 | self._logger.info("Regester as Sub Plugin to PSUControl") 247 | 248 | def get_AvaliblePorts(self): 249 | avalPorts = self.conn.serialList() 250 | try: 251 | if str(self._settings.get(["IOPort"])) != "None": 252 | commPort = str(self._settings.get(["IOPort"])) 253 | if avalPorts.index(commPort) < 0: 254 | avalPorts.append(str(self._settings.get(["IOPort"]))) 255 | except Exception: 256 | self._logger.warning( 257 | "Looks like No Comm port was selected yet. List of avalible ports may need to be refreshed." 258 | ) 259 | 260 | return avalPorts 261 | 262 | def is_api_protected(self): 263 | """Require authentication for API access.""" 264 | return True # for now 265 | 266 | 267 | def get_api_commands(self): 268 | return dict( 269 | turnSioOn=["id"], 270 | turnSioOff=["id"], 271 | toggelSio=["pin"], 272 | getSioState=["id"], 273 | getPorts="", 274 | getIOCounts="", 275 | getStatusMessage="", 276 | connectIO=["port", "baudRate", "si"], 277 | ) 278 | 279 | def on_api_command(self, command, data): 280 | 281 | if command == "connectIO": 282 | self._settings.set(["IOSI"], data["si"]) 283 | self._settings.set(["IOBaudRate"], data["baudRate"]) 284 | self._settings.set(["IOPort"], data["port"]) 285 | 286 | if self.conn.is_connected(): 287 | self.conn.stopCommThreads() 288 | self.conn.disconnect() 289 | 290 | self.conn.connect() 291 | if self.conn.is_connected(): 292 | self._logger.info("Connected") 293 | self.conn.send("SI " + self._settings.get(["IOSI"])) 294 | else: 295 | self._logger.error("Could not connect to SIO.") 296 | 297 | return flask.jsonify(self.IOStatus) 298 | 299 | if command == "getStatusMessage": 300 | return flask.jsonify(self.IOStatus, self.IOSWarnings) 301 | 302 | if command == "getPorts": 303 | avalPorts = self.get_AvaliblePorts() 304 | self._settings.set(["IOPorts"], avalPorts) 305 | return flask.jsonify(avalPorts) 306 | 307 | if command == "getIOCounts": 308 | return flask.jsonify(self.getCounts()) 309 | 310 | if command == "toggelSio": 311 | pin = int(data["pin"]) 312 | 313 | if pin >= len(self.IOCurrent) or pin < 0: 314 | self._logger.info( 315 | "Toggle command ignored, Pin assignment out of range: {}".format(pin) 316 | ) 317 | return 318 | 319 | if Permissions.CONTROL.can() and pin >= 0: 320 | if self.conn.is_connected(): 321 | self._logger.debug("Toggle SIO{}".format(pin)) 322 | newState = "0" if self.IOCurrent[pin] == "1" else "1" 323 | self.conn.send(f"IO {pin} {newState}") 324 | return flask.jsonify(f"{pin}: {newState}") 325 | 326 | else: 327 | self._logger.info( 328 | "Not connected ignored IO command on Pin{}".format(pin) 329 | ) 330 | 331 | else: 332 | configuration = self._settings.get(["sio_configurations"])[int(data["id"])] 333 | pin = int(configuration["pin"]) 334 | 335 | if pin < len(self.IOCurrent): 336 | if command == "getSioState": 337 | if pin <= 0: 338 | return flask.jsonify("") 339 | elif configuration["active_mode"] == "active_out_low": 340 | rtnJ = flask.jsonify( 341 | "on" if self.IOCurrent[pin] == "1" else "off" 342 | ) 343 | return rtnJ 344 | elif configuration["active_mode"] == "active_out_high": 345 | rtnJ = flask.jsonify( 346 | "off" if self.IOCurrent[pin] == "1" else "on" 347 | ) 348 | return rtnJ 349 | 350 | elif command == "turnSioOn": 351 | if Permissions.CONTROL.can() and pin >= 0: 352 | if self.conn.is_connected(): 353 | self._logger.debug( 354 | "Turned on SIO{}".format(configuration["pin"]) 355 | ) 356 | 357 | if configuration["active_mode"] == "active_out_low": 358 | self.conn.send(f"IO {pin} 0") 359 | 360 | if self.IOCurrent[pin] == "0": 361 | return flask.jsonify(f"{pin}: on") 362 | else: 363 | return flask.jsonify(f"{pin}: off") 364 | 365 | elif configuration["active_mode"] == "active_out_high": 366 | self.conn.send(f"IO {pin} 1") 367 | 368 | if self.IOCurrent[pin] == "1": 369 | return flask.jsonify(f"{pin}: on") 370 | else: 371 | return flask.jsonify(f"{pin}: off") 372 | 373 | else: 374 | self._logger.info( 375 | "Not connected ignored IO command on Pin{}".format(pin) 376 | ) 377 | 378 | elif command == "turnSioOff": 379 | if Permissions.CONTROL.can() and pin >= 0: 380 | if self.conn.is_connected(): 381 | self._logger.debug( 382 | "Turned off SIO{}".format(configuration["pin"]) 383 | ) 384 | if configuration["active_mode"] == "active_out_low": 385 | self.conn.send(f"IO {pin} 1") 386 | if self.IOCurrent[pin] == "1": 387 | return flask.jsonify(f"{pin}: off") 388 | else: 389 | return flask.jsonify(f"{pin}: on") 390 | 391 | elif configuration["active_mode"] == "active_out_high": 392 | self.conn.send(f"IO {pin} 0") 393 | if self.IOCurrent[pin] == "0": 394 | return flask.jsonify(f"{pin}: off") 395 | else: 396 | return flask.jsonify(f"{pin}: on") 397 | 398 | else: 399 | self._logger.info( 400 | "Not connected ignored IO command on Pin{}".format(pin) 401 | ) 402 | else: 403 | self.IOSWarnings = "Pin [{}] out of range.".format(pin) 404 | self._logger.info("Pin [{}] outof range.".format(pin)) 405 | self._logger.info( 406 | "Max Pin assignment is [{}]".format(len(self.IOCurrent) - 1) 407 | ) 408 | 409 | def on_api_get(self, request): 410 | return flask.jsonify(self.get_sio_Configuration_status()) 411 | 412 | def getCounts(self): 413 | if self.conn.IOCount != 0: 414 | counts = [str(k) for k in range(0, self.conn.IOCount)] 415 | else: 416 | counts = [str(k) for k in range(0, 99)] 417 | return counts 418 | 419 | def on_settings_save(self, data): 420 | 421 | octoprint.plugin.SettingsPlugin.on_settings_save(self, data) 422 | 423 | # should do direct IO configuration sends here. 424 | 425 | comChanged = True 426 | if "IOPort" in data: 427 | self._settings.set(["IOPort"], data["IOPort"]) 428 | comChanged = True 429 | 430 | if "IOBaudRate" in data: 431 | self._settings.set(["IOBaudRate"], data["IOBaudRate"]) 432 | comChanged = True 433 | 434 | if comChanged: 435 | if self.conn.is_connected(): 436 | self.conn.stopCommThreads() 437 | self.conn.disconnect() 438 | 439 | self.conn.connect() 440 | if self.conn.is_connected(): 441 | self._logger.info("Connected") 442 | else: 443 | self._logger.error("Could not connect to SIO.") 444 | 445 | if "IOSI" in data: 446 | self._settings.set(["IOSI"], data["IOSI"]) 447 | 448 | if "PSUIOPoint" in data: 449 | self._settings.set(["PSUIOPoint"], data["PSUIOPoint"]) 450 | 451 | if self.conn.is_connected(): 452 | self.conn.Update_IOSI(self._settings.get(["IOSI"])) 453 | 454 | self.reload_settings() 455 | 456 | return 457 | 458 | def SetDIOPoint(self,pin,action): 459 | #actions ["on","off"] 460 | if pin != -1: 461 | for configuration in self._settings.get(["sio_configurations"]): 462 | cpin = int(configuration["pin"]) 463 | if pin == cpin: 464 | if configuration["active_mode"] == "active_out_low": 465 | if action == "on": 466 | self.conn.send(f"IO {pin} 0") 467 | elif action == "off": 468 | self.conn.send(f"IO {pin} 1") 469 | else: 470 | self._logger.info("Can't set Digital IO pin {} State to {}. Action is invalid".format(configuration["pin"],action)) 471 | 472 | elif configuration["active_mode"] == "active_out_high": 473 | if action == "on": 474 | self.conn.send(f"IO {pin} 1") 475 | elif action == "off": 476 | self.conn.send(f"IO {pin} 0") 477 | else: 478 | self._logger.info("Can't set Digital IO pin {} State to {}. Action is invalid".format(configuration["pin"],action)) 479 | else: 480 | self._logger.info("Can't set Digital IO pin {} State to {}. Pin number is out of range".format(configuration["pin"],action,)) 481 | return 482 | 483 | def turn_psu_on(self): 484 | if self._settings.get(["EnablePSUIOPoint"]): 485 | psupoint = self._settings.get(["PSUIOPoint"]) 486 | if self._settings.get(["InvertPSUIOPoint"]): 487 | self.conn.send(f"IO {psupoint} 0") 488 | else: 489 | self.conn.send(f"IO {psupoint} 1") 490 | self._logger.debug("******Switching PSU On: sending command to IO******") 491 | else: 492 | self._logger.debug( 493 | "******Turn On PSU requested: PSU Integration is not Endabled.******" 494 | ) 495 | 496 | def turn_psu_off(self): 497 | if self._settings.get(["EnablePSUIOPoint"]): 498 | psupoint = self._settings.get(["PSUIOPoint"]) 499 | if self._settings.get(["InvertPSUIOPoint"]): 500 | self.conn.send(f"IO {psupoint} 1") 501 | else: 502 | self.conn.send(f"IO {psupoint} 0") 503 | self._logger.debug("******Switching PSU Off: sending command to IO******") 504 | else: 505 | self._logger.debug( 506 | "******Turn Off PSU requested: PSU Integration is not Endabled.******" 507 | ) 508 | 509 | def get_psu_state(self): 510 | rtn = None 511 | 512 | if self.IOCurrent is None: 513 | return False 514 | if len(self.IOCurrent) >= int(self._settings.get(["PSUIOPoint"])): 515 | 516 | psuRelayState = self.IOCurrent[int(self._settings.get(["PSUIOPoint"]))] 517 | self._logger.debug("******Reporting PSU Current State:" + psuRelayState) 518 | 519 | if self._settings.get(["InvertPSUIOPoint"]): 520 | rtn = self.IOCurrent[int(self._settings.get(["PSUIOPoint"]))] == "0" 521 | else: 522 | rtn = self.IOCurrent[int(self._settings.get(["PSUIOPoint"]))] == "1" 523 | 524 | return rtn 525 | else: 526 | self._logger.debug( 527 | "Cant get PSU State due to lack of reporting from SIO Control" 528 | ) 529 | 530 | ##~~ AssetPlugin mixin 531 | 532 | def get_assets(self): 533 | self._logger.debug("SIOC Running get_assets") 534 | return dict( 535 | css=["css/SIOControl.css", "css/fontawesome-iconpicker.min.css"], 536 | js=["js/siocontrol.js", "js/fontawesome-iconpicker.min.js"], 537 | ) 538 | 539 | ##~~ Sub Plugin Hooks 540 | def _get_plugin_key(self, implementation): 541 | for k, v in self._plugin_manager.plugin_implementations.items(): 542 | if v == implementation: 543 | return k 544 | 545 | def register_plugin(self, implementation): 546 | k = self._get_plugin_key(implementation) 547 | 548 | self._logger.debug("Registering plugin - {} as SIO Control Sub plugin".format(k)) 549 | 550 | if k not in self._sub_plugins: 551 | self._logger.info("Registered plugin - {} as SIO Control Sub plugin".format(k)) 552 | self._sub_plugins[k] = implementation 553 | 554 | # Send serial data recieved for use in subPlugins. 555 | def serialRecievequeue(self,line): 556 | for k, v in self._sub_plugins.items(): 557 | if hasattr(v,'hook_sio_serial_stream'): 558 | callback = self._sub_plugins[k].hook_sio_serial_stream 559 | try: 560 | callback(line) 561 | except Exception: 562 | self._logger.exception("Error while executing sub Plugin callback method {}".format(callback),extra={"callback":fqfn(callback)},) 563 | 564 | return line 565 | 566 | # Can be used to send arbatrary command to the SIO Control module. Commands are entered into the send Queue. 567 | def send_sio_command(self,command): 568 | self.conn.send(command) 569 | 570 | def broadCastStateToSubPlugins(self): 571 | #update all sub plugins with state array for digital IO points when the state changes. 572 | if self.IOCurrent == self._lastIOCurent: 573 | return 574 | 575 | self._lastIOCurrent = self.IOCurrent 576 | for k, v in self._sub_plugins.items(): 577 | if hasattr(v,'sioStateChanged'): 578 | callback = self._sub_plugins[k].sioStateChanged 579 | try: 580 | IOStatus = self.get_sio_digital_status() 581 | callback(self.IOCurrent,IOStatus) 582 | except Exception: 583 | self._logger.exception("Error while executing sub Plugin callback method {}".format(callback),extra={"callback":fqfn(callback)},) 584 | #return dont drop out.. this might be just one of many sub plugins looking for info. 585 | 586 | def set_sio_digital_state(self,point,action): 587 | #actions ["on,off"] 588 | self.SetDIOPoint(point,action) 589 | return self.IOCurrent 590 | 591 | def get_sio_digital_state(self): 592 | return self.IOCurrent 593 | 594 | def get_sio_digital_status(self): 595 | conStatus = self.get_sio_Configuration_status() 596 | status = [] 597 | 598 | for _idx, _x in enumerate(self.IOCurrent): 599 | status.append("na") 600 | try: 601 | 602 | for idx,configuration in enumerate(self._settings.get(["sio_configurations"])): 603 | pin = int(configuration["pin"]) 604 | status[pin] = conStatus[idx] 605 | self._logger.debug("Pin#{} set to \"{}\"".format(idx,status[pin])) 606 | 607 | except Exception: 608 | self._logger.exception("Error while getting digital status ConStatus{} IOCurrent{}".format(conStatus,self.IOCurrent)) 609 | 610 | return status 611 | 612 | # Important to note that this is indexed in the order of the Configurations for the UI. Not in the pin IO Order. 613 | # It will also only return items that corospond to the sio_configurations. So if you only configured 2 points, you will 614 | # only get 2 items in the array. 615 | # to get the status for the configured pins, use get_sio_digital_status 616 | def get_sio_Configuration_status(self): 617 | status = [] 618 | for configuration in self._settings.get(["sio_configurations"]): 619 | if configuration["pin"] is not None: 620 | pin = int(configuration["pin"]) 621 | 622 | if self.IOCurrent is not None and pin < len(self.IOCurrent): 623 | if pin < 0: 624 | status.append("") 625 | elif configuration["active_mode"] == "active_in_low": 626 | pstatus = "off" if self.IOCurrent[pin] == "1" else "on" 627 | status.append(pstatus) 628 | elif configuration["active_mode"] == "active_in_high": 629 | pstatus = "on" if self.IOCurrent[pin] == "1" else "off" 630 | status.append(pstatus) 631 | elif configuration["active_mode"] == "active_out_low": 632 | pstatus = "off" if self.IOCurrent[pin] == "1" else "on" 633 | status.append(pstatus) 634 | elif configuration["active_mode"] == "active_out_high": 635 | pstatus = "on" if self.IOCurrent[pin] == "1" else "off" 636 | status.append(pstatus) 637 | else: 638 | if self.conn is not None and self.conn.is_connected() is True: 639 | self._logger.debug("Pin number assigned to IO control {} maybe out of range.".format(pin)) 640 | 641 | status.append("off") 642 | else: 643 | status.append("off") 644 | 645 | return status 646 | ##~~ Softwareupdate hook 647 | 648 | def get_update_information(self): 649 | # Define the configuration for your plugin to use with the Software Update 650 | # Plugin here. See https://docs.octoprint.org/en/master/bundledplugins/softwareupdate.html 651 | # for details. 652 | return { 653 | "siocontrol": { 654 | "displayName": "SIO Control", 655 | "displayVersion": self._plugin_version, 656 | # version check: github repository 657 | "type": "github_release", 658 | "user": "jcassel", 659 | "repo": "OctoPrint-Siocontrol", 660 | "current": self._plugin_version, 661 | # update method: pip 662 | "pip": "https://github.com/jcassel/OctoPrint-Siocontrol/archive/{target_version}.zip", 663 | } 664 | } 665 | 666 | 667 | # If you want your plugin to be registered within OctoPrint under a different 668 | # name than what you defined in setup.py 669 | # ("OctoPrint-PluginSkeleton"), you may define that here. Same goes for the 670 | # other metadata derived from setup.py that can be overwritten 671 | # via __plugin_xyz__ control properties. See the documentation for that. 672 | __plugin_name__ = "SIO Control" 673 | 674 | 675 | # Set the Python version your plugin is compatible with below. Recommended 676 | # is Python 3 only for all new plugins. 677 | # OctoPrint 1.4.0 - 1.7.x run under both Python 3 and the end-of-life Python 2. 678 | # OctoPrint 1.8.0 onwards only supports Python 3. 679 | __plugin_pythoncompat__ = ">=3,<4" # Only Python 3 680 | 681 | 682 | def __plugin_load__(): 683 | global __plugin_implementation__ 684 | __plugin_implementation__ = SiocontrolPlugin() 685 | 686 | global __plugin_hooks__ 687 | __plugin_hooks__ = { 688 | "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information 689 | } 690 | 691 | global __plugin_helpers__ 692 | __plugin_helpers__ = dict( 693 | set_sio_digital_state=__plugin_implementation__.set_sio_digital_state, 694 | get_sio_digital_state=__plugin_implementation__.get_sio_digital_state, 695 | send_sio_command=__plugin_implementation__.send_sio_command, 696 | register_plugin=__plugin_implementation__.register_plugin, 697 | 698 | ) 699 | -------------------------------------------------------------------------------- /octoprint_siocontrol/static/js/fontawesome-iconpicker.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Icon Picker 3 | * https://farbelous.github.io/fontawesome-iconpicker/ 4 | * 5 | * Originally written by (c) 2016 Javi Aguilar 6 | * Licensed under the MIT License 7 | * https://github.com/farbelous/fontawesome-iconpicker/blob/master/LICENSE 8 | * 9 | */ 10 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a(jQuery)}(function(a){a.ui=a.ui||{};a.ui.version="1.12.1";/*! 11 | * jQuery UI Position 1.12.1 12 | * http://jqueryui.com 13 | * 14 | * Copyright jQuery Foundation and other contributors 15 | * Released under the MIT license. 16 | * http://jquery.org/license 17 | * 18 | * http://api.jqueryui.com/position/ 19 | */ 20 | !function(){function b(a,b,c){return[parseFloat(a[0])*(l.test(a[0])?b/100:1),parseFloat(a[1])*(l.test(a[1])?c/100:1)]}function c(b,c){return parseInt(a.css(b,c),10)||0}function d(b){var c=b[0];return 9===c.nodeType?{width:b.width(),height:b.height(),offset:{top:0,left:0}}:a.isWindow(c)?{width:b.width(),height:b.height(),offset:{top:b.scrollTop(),left:b.scrollLeft()}}:c.preventDefault?{width:0,height:0,offset:{top:c.pageY,left:c.pageX}}:{width:b.outerWidth(),height:b.outerHeight(),offset:b.offset()}}var e,f=Math.max,g=Math.abs,h=/left|center|right/,i=/top|center|bottom/,j=/[\+\-]\d+(\.[\d]+)?%?/,k=/^\w+/,l=/%$/,m=a.fn.pos;a.pos={scrollbarWidth:function(){if(void 0!==e)return e;var b,c,d=a("
"),f=d.children()[0];return a("body").append(d),b=f.offsetWidth,d.css("overflow","scroll"),c=f.offsetWidth,b===c&&(c=d[0].clientWidth),d.remove(),e=b-c},getScrollInfo:function(b){var c=b.isWindow||b.isDocument?"":b.element.css("overflow-x"),d=b.isWindow||b.isDocument?"":b.element.css("overflow-y"),e="scroll"===c||"auto"===c&&b.width0?"right":"center",vertical:h<0?"top":d>0?"bottom":"middle"};nf(g(d),g(h))?l.important="horizontal":l.important="vertical",e.using.call(this,a,l)}),i.offset(a.extend(z,{using:h}))})},a.ui.pos={_trigger:function(a,b,c,d){b.elem&&b.elem.trigger({type:c,position:a,positionData:b,triggered:d})},fit:{left:function(b,c){a.ui.pos._trigger(b,c,"posCollide","fitLeft");var d,e=c.within,g=e.isWindow?e.scrollLeft:e.offset.left,h=e.width,i=b.left-c.collisionPosition.marginLeft,j=g-i,k=i+c.collisionWidth-h-g;c.collisionWidth>h?j>0&&k<=0?(d=b.left+j+c.collisionWidth-h-g,b.left+=j-d):b.left=k>0&&j<=0?g:j>k?g+h-c.collisionWidth:g:j>0?b.left+=j:k>0?b.left-=k:b.left=f(b.left-i,b.left),a.ui.pos._trigger(b,c,"posCollided","fitLeft")},top:function(b,c){a.ui.pos._trigger(b,c,"posCollide","fitTop");var d,e=c.within,g=e.isWindow?e.scrollTop:e.offset.top,h=c.within.height,i=b.top-c.collisionPosition.marginTop,j=g-i,k=i+c.collisionHeight-h-g;c.collisionHeight>h?j>0&&k<=0?(d=b.top+j+c.collisionHeight-h-g,b.top+=j-d):b.top=k>0&&j<=0?g:j>k?g+h-c.collisionHeight:g:j>0?b.top+=j:k>0?b.top-=k:b.top=f(b.top-i,b.top),a.ui.pos._trigger(b,c,"posCollided","fitTop")}},flip:{left:function(b,c){a.ui.pos._trigger(b,c,"posCollide","flipLeft");var d,e,f=c.within,h=f.offset.left+f.scrollLeft,i=f.width,j=f.isWindow?f.scrollLeft:f.offset.left,k=b.left-c.collisionPosition.marginLeft,l=k-j,m=k+c.collisionWidth-i-j,n="left"===c.my[0]?-c.elemWidth:"right"===c.my[0]?c.elemWidth:0,o="left"===c.at[0]?c.targetWidth:"right"===c.at[0]?-c.targetWidth:0,p=-2*c.offset[0];l<0?((d=b.left+n+o+p+c.collisionWidth-i-h)<0||d0&&((e=b.left-c.collisionPosition.marginLeft+n+o+p-j)>0||g(e)0&&((d=b.top-c.collisionPosition.marginTop+o+p+q-j)>0||g(d)10&&e<11,b.innerHTML="",c.removeChild(b)}()}();a.ui.position}),function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):window.jQuery&&!window.jQuery.fn.iconpicker&&a(window.jQuery)}(function(a){"use strict";var b={isEmpty:function(a){return!1===a||""===a||null===a||void 0===a},isEmptyObject:function(a){return!0===this.isEmpty(a)||0===a.length},isElement:function(b){return a(b).length>0},isString:function(a){return"string"==typeof a||a instanceof String},isArray:function(b){return a.isArray(b)},inArray:function(b,c){return-1!==a.inArray(b,c)},throwError:function(a){throw"Font Awesome Icon Picker Exception: "+a}},c=function(d,e){this._id=c._idCounter++,this.element=a(d).addClass("iconpicker-element"),this._trigger("iconpickerCreate",{iconpickerValue:this.iconpickerValue}),this.options=a.extend({},c.defaultOptions,this.element.data(),e),this.options.templates=a.extend({},c.defaultOptions.templates,this.options.templates),this.options.originalPlacement=this.options.placement,this.container=!!b.isElement(this.options.container)&&a(this.options.container),!1===this.container&&(this.element.is(".dropdown-toggle")?this.container=a("~ .dropdown-menu:first",this.element):this.container=this.element.is("input,textarea,button,.btn")?this.element.parent():this.element),this.container.addClass("iconpicker-container"),this.isDropdownMenu()&&(this.options.placement="inline"),this.input=!!this.element.is("input,textarea")&&this.element.addClass("iconpicker-input"),!1===this.input&&(this.input=this.container.find(this.options.input),this.input.is("input,textarea")||(this.input=!1)),this.component=this.isDropdownMenu()?this.container.parent().find(this.options.component):this.container.find(this.options.component),0===this.component.length?this.component=!1:this.component.find("i").addClass("iconpicker-component"),this._createPopover(),this._createIconpicker(),0===this.getAcceptButton().length&&(this.options.mustAccept=!1),this.isInputGroup()?this.container.parent().append(this.popover):this.container.append(this.popover),this._bindElementEvents(),this._bindWindowEvents(),this.update(this.options.selected),this.isInline()&&this.show(),this._trigger("iconpickerCreated",{iconpickerValue:this.iconpickerValue})};c._idCounter=0,c.defaultOptions={title:!1,selected:!1,defaultValue:!1,placement:"bottom",collision:"none",animation:!0,hideOnSelect:!1,showFooter:!1,searchInFooter:!1,mustAccept:!1,selectedCustomClass:"bg-primary",icons:[],fullClassFormatter:function(a){return a},input:"input,.iconpicker-input",inputSearch:!1,container:!1,component:".input-group-addon,.iconpicker-component",templates:{popover:'
',footer:'',buttons:' ',search:'',iconpicker:'
',iconpickerItem:''}},c.batch=function(b,c){var d=Array.prototype.slice.call(arguments,2);return a(b).each(function(){var b=a(this).data("iconpicker");b&&b[c].apply(b,d)})},c.prototype={constructor:c,options:{},_id:0,_trigger:function(b,c){c=c||{},this.element.trigger(a.extend({type:b,iconpickerInstance:this},c))},_createPopover:function(){this.popover=a(this.options.templates.popover);var c=this.popover.find(".popover-title");if(this.options.title&&c.append(a('
'+this.options.title+"
")),this.hasSeparatedSearchInput()&&!this.options.searchInFooter?c.append(this.options.templates.search):this.options.title||c.remove(),this.options.showFooter&&!b.isEmpty(this.options.templates.footer)){var d=a(this.options.templates.footer);this.hasSeparatedSearchInput()&&this.options.searchInFooter&&d.append(a(this.options.templates.search)),b.isEmpty(this.options.templates.buttons)||d.append(a(this.options.templates.buttons)),this.popover.append(d)}return!0===this.options.animation&&this.popover.addClass("fade"),this.popover},_createIconpicker:function(){var b=this;this.iconpicker=a(this.options.templates.iconpicker);var c=function(c){var d=a(this);d.is("i")&&(d=d.parent()),b._trigger("iconpickerSelect",{iconpickerItem:d,iconpickerValue:b.iconpickerValue}),!1===b.options.mustAccept?(b.update(d.data("iconpickerValue")),b._trigger("iconpickerSelected",{iconpickerItem:this,iconpickerValue:b.iconpickerValue})):b.update(d.data("iconpickerValue"),!0),b.options.hideOnSelect&&!1===b.options.mustAccept&&b.hide()};for(var d in this.options.icons)if("string"==typeof this.options.icons[d].title){var e=a(this.options.templates.iconpickerItem);if(e.find("i").addClass(this.options.fullClassFormatter(this.options.icons[d].title)),e.data("iconpickerValue",this.options.icons[d].title).on("click.iconpicker",c),this.iconpicker.find(".iconpicker-items").append(e.attr("title","."+this.options.icons[d].title)),this.options.icons[d].searchTerms.length>0){for(var f="",g=0;g0?a.attr("class",this.options.fullClassFormatter(this.iconpickerValue)):this.component.html(this.getHtml())}},_updateFormGroupStatus:function(a){return!!this.hasInput()&&(!1!==a?this.input.parents(".form-group:first").removeClass("has-error"):this.input.parents(".form-group:first").addClass("has-error"),!0)},getValid:function(c){b.isString(c)||(c="");var d=""===c;c=a.trim(c);for(var e=!1,f=0;f'},setSourceValue:function(a){return a=this.setValue(a),!1!==a&&""!==a&&(this.hasInput()?this.input.val(this.iconpickerValue):this.element.data("iconpickerValue",this.iconpickerValue),this._trigger("iconpickerSetSourceValue",{iconpickerValue:a})),a},getSourceValue:function(a){a=a||this.options.defaultValue;var b=a;return b=this.hasInput()?this.input.val():this.element.data("iconpickerValue"),void 0!==b&&""!==b&&null!==b&&!1!==b||(b=a),b},hasInput:function(){return!1!==this.input},isInputSearch:function(){return this.hasInput()&&!0===this.options.inputSearch},isInputGroup:function(){return this.container.is(".input-group")},isDropdownMenu:function(){return this.container.is(".dropdown-menu")},hasSeparatedSearchInput:function(){return!1!==this.options.templates.search&&!this.isInputSearch()},hasComponent:function(){return!1!==this.component},hasContainer:function(){return!1!==this.container},getAcceptButton:function(){return this.popover.find(".iconpicker-btn-accept")},getCancelButton:function(){return this.popover.find(".iconpicker-btn-cancel")},getSearchInput:function(){return this.popover.find(".iconpicker-search")},filter:function(c){if(b.isEmpty(c))return this.iconpicker.find(".iconpicker-item").show(),a(!1);var d=[];return this.iconpicker.find(".iconpicker-item").each(function(){var b=a(this),e=b.attr("title").toLowerCase();e=e+" "+(b.attr("data-search-terms")?b.attr("data-search-terms").toLowerCase():"");var f=!1;try{f=new RegExp("(^|\\W)"+c,"g")}catch(a){f=!1}!1!==f&&e.match(f)?(d.push(b),b.show()):b.hide()}),d},show:function(){if(this.popover.hasClass("in"))return!1;a.iconpicker.batch(a(".iconpicker-popover.in:not(.inline)").not(this.popover),"hide"),this._trigger("iconpickerShow",{iconpickerValue:this.iconpickerValue}),this.updatePlacement(),this.popover.addClass("in"),setTimeout(a.proxy(function(){this.popover.css("display",this.isInline()?"":"block"),this._trigger("iconpickerShown",{iconpickerValue:this.iconpickerValue})},this),this.options.animation?300:1)},hide:function(){if(!this.popover.hasClass("in"))return!1;this._trigger("iconpickerHide",{iconpickerValue:this.iconpickerValue}),this.popover.removeClass("in"),setTimeout(a.proxy(function(){this.popover.css("display","none"),this.getSearchInput().val(""),this.filter(""),this._trigger("iconpickerHidden",{iconpickerValue:this.iconpickerValue})},this),this.options.animation?300:1)},toggle:function(){this.popover.is(":visible")?this.hide():this.show(!0)},update:function(a,b){return a=a||this.getSourceValue(this.iconpickerValue),this._trigger("iconpickerUpdate",{iconpickerValue:this.iconpickerValue}),!0===b?a=this.setValue(a):(a=this.setSourceValue(a),this._updateFormGroupStatus(!1!==a)),!1!==a&&this._updateComponents(),this._trigger("iconpickerUpdated",{iconpickerValue:this.iconpickerValue}),a},destroy:function(){this._trigger("iconpickerDestroy",{iconpickerValue:this.iconpickerValue}),this.element.removeData("iconpicker").removeData("iconpickerValue").removeClass("iconpicker-element"),this._unbindElementEvents(),this._unbindWindowEvents(),a(this.popover).remove(),this._trigger("iconpickerDestroyed",{iconpickerValue:this.iconpickerValue})},disable:function(){return!!this.hasInput()&&(this.input.prop("disabled",!0),!0)},enable:function(){return!!this.hasInput()&&(this.input.prop("disabled",!1),!0)},isDisabled:function(){return!!this.hasInput()&&!0===this.input.prop("disabled")},isInline:function(){return"inline"===this.options.placement||this.popover.hasClass("inline")}},a.iconpicker=c,a.fn.iconpicker=function(b){return this.each(function(){var d=a(this);d.data("iconpicker")||d.data("iconpicker",new c(this,"object"==typeof b?b:{}))})},c.defaultOptions=a.extend(c.defaultOptions,{icons:[{title:"fab fa-500px",searchTerms:[]},{title:"fab fa-accessible-icon",searchTerms:["accessibility","wheelchair","handicap","person","wheelchair-alt"]},{title:"fab fa-accusoft",searchTerms:[]},{title:"fas fa-address-book",searchTerms:[]},{title:"far fa-address-book",searchTerms:[]},{title:"fas fa-address-card",searchTerms:[]},{title:"far fa-address-card",searchTerms:[]},{title:"fas fa-adjust",searchTerms:["contrast"]},{title:"fab fa-adn",searchTerms:[]},{title:"fab fa-adversal",searchTerms:[]},{title:"fab fa-affiliatetheme",searchTerms:[]},{title:"fab fa-algolia",searchTerms:[]},{title:"fas fa-align-center",searchTerms:["middle","text"]},{title:"fas fa-align-justify",searchTerms:["text"]},{title:"fas fa-align-left",searchTerms:["text"]},{title:"fas fa-align-right",searchTerms:["text"]},{title:"fab fa-amazon",searchTerms:[]},{title:"fab fa-amazon-pay",searchTerms:[]},{title:"fas fa-ambulance",searchTerms:["vehicle","support","help"]},{title:"fas fa-american-sign-language-interpreting",searchTerms:[]},{title:"fab fa-amilia",searchTerms:[]},{title:"fas fa-anchor",searchTerms:["link"]},{title:"fab fa-android",searchTerms:["robot"]},{title:"fab fa-angellist",searchTerms:[]},{title:"fas fa-angle-double-down",searchTerms:["arrows"]},{title:"fas fa-angle-double-left",searchTerms:["laquo","quote","previous","back","arrows"]},{title:"fas fa-angle-double-right",searchTerms:["raquo","quote","next","forward","arrows"]},{title:"fas fa-angle-double-up",searchTerms:["arrows"]},{title:"fas fa-angle-down",searchTerms:["arrow"]},{title:"fas fa-angle-left",searchTerms:["previous","back","arrow"]},{title:"fas fa-angle-right",searchTerms:["next","forward","arrow"]},{title:"fas fa-angle-up",searchTerms:["arrow"]},{title:"fab fa-angrycreative",searchTerms:[]},{title:"fab fa-angular",searchTerms:[]},{title:"fab fa-app-store",searchTerms:[]},{title:"fab fa-app-store-ios",searchTerms:[]},{title:"fab fa-apper",searchTerms:[]},{title:"fab fa-apple",searchTerms:["osx","food"]},{title:"fab fa-apple-pay",searchTerms:[]},{title:"fas fa-archive",searchTerms:["box","storage","package"]},{title:"fas fa-arrow-alt-circle-down",searchTerms:["download","arrow-circle-o-down"]},{title:"far fa-arrow-alt-circle-down",searchTerms:["download","arrow-circle-o-down"]},{title:"fas fa-arrow-alt-circle-left",searchTerms:["previous","back","arrow-circle-o-left"]},{title:"far fa-arrow-alt-circle-left",searchTerms:["previous","back","arrow-circle-o-left"]},{title:"fas fa-arrow-alt-circle-right",searchTerms:["next","forward","arrow-circle-o-right"]},{title:"far fa-arrow-alt-circle-right",searchTerms:["next","forward","arrow-circle-o-right"]},{title:"fas fa-arrow-alt-circle-up",searchTerms:["arrow-circle-o-up"]},{title:"far fa-arrow-alt-circle-up",searchTerms:["arrow-circle-o-up"]},{title:"fas fa-arrow-circle-down",searchTerms:["download"]},{title:"fas fa-arrow-circle-left",searchTerms:["previous","back"]},{title:"fas fa-arrow-circle-right",searchTerms:["next","forward"]},{title:"fas fa-arrow-circle-up",searchTerms:[]},{title:"fas fa-arrow-down",searchTerms:["download"]},{title:"fas fa-arrow-left",searchTerms:["previous","back"]},{title:"fas fa-arrow-right",searchTerms:["next","forward"]},{title:"fas fa-arrow-up",searchTerms:[]},{title:"fas fa-arrows-alt",searchTerms:["expand","enlarge","fullscreen","bigger","move","reorder","resize","arrow","arrows"]},{title:"fas fa-arrows-alt-h",searchTerms:["resize","arrows-h"]},{title:"fas fa-arrows-alt-v",searchTerms:["resize","arrows-v"]},{title:"fas fa-assistive-listening-systems",searchTerms:[]},{title:"fas fa-asterisk",searchTerms:["details"]},{title:"fab fa-asymmetrik",searchTerms:[]},{title:"fas fa-at",searchTerms:["email","e-mail"]},{title:"fab fa-audible",searchTerms:[]},{title:"fas fa-audio-description",searchTerms:[]},{title:"fab fa-autoprefixer",searchTerms:[]},{title:"fab fa-avianex",searchTerms:[]},{title:"fab fa-aviato",searchTerms:[]},{title:"fab fa-aws",searchTerms:[]},{title:"fas fa-backward",searchTerms:["rewind","previous"]},{title:"fas fa-balance-scale",searchTerms:[]},{title:"fas fa-ban",searchTerms:["delete","remove","trash","hide","block","stop","abort","cancel","ban","prohibit"]},{title:"fas fa-band-aid",searchTerms:["bandage","ouch","boo boo"]},{title:"fab fa-bandcamp",searchTerms:[]},{title:"fas fa-barcode",searchTerms:["scan"]},{title:"fas fa-bars",searchTerms:["menu","drag","reorder","settings","list","ul","ol","checklist","todo","list","hamburger"]},{title:"fas fa-baseball-ball",searchTerms:[]},{title:"fas fa-basketball-ball",searchTerms:[]},{title:"fas fa-bath",searchTerms:[]},{title:"fas fa-battery-empty",searchTerms:["power","status"]},{title:"fas fa-battery-full",searchTerms:["power","status"]},{title:"fas fa-battery-half",searchTerms:["power","status"]},{title:"fas fa-battery-quarter",searchTerms:["power","status"]},{title:"fas fa-battery-three-quarters",searchTerms:["power","status"]},{title:"fas fa-bed",searchTerms:["travel"]},{title:"fas fa-beer",searchTerms:["alcohol","stein","drink","mug","bar","liquor"]},{title:"fab fa-behance",searchTerms:[]},{title:"fab fa-behance-square",searchTerms:[]},{title:"fas fa-bell",searchTerms:["alert","reminder","notification"]},{title:"far fa-bell",searchTerms:["alert","reminder","notification"]},{title:"fas fa-bell-slash",searchTerms:[]},{title:"far fa-bell-slash",searchTerms:[]},{title:"fas fa-bicycle",searchTerms:["vehicle","bike","gears"]},{title:"fab fa-bimobject",searchTerms:[]},{title:"fas fa-binoculars",searchTerms:[]},{title:"fas fa-birthday-cake",searchTerms:[]},{title:"fab fa-bitbucket",searchTerms:["git","bitbucket-square"]},{title:"fab fa-bitcoin",searchTerms:[]},{title:"fab fa-bity",searchTerms:[]},{title:"fab fa-black-tie",searchTerms:[]},{title:"fab fa-blackberry",searchTerms:[]},{title:"fas fa-blind",searchTerms:[]},{title:"fab fa-blogger",searchTerms:[]},{title:"fab fa-blogger-b",searchTerms:[]},{title:"fab fa-bluetooth",searchTerms:[]},{title:"fab fa-bluetooth-b",searchTerms:[]},{title:"fas fa-bold",searchTerms:[]},{title:"fas fa-bolt",searchTerms:["lightning","weather"]},{title:"fas fa-bomb",searchTerms:[]},{title:"fas fa-book",searchTerms:["read","documentation"]},{title:"fas fa-bookmark",searchTerms:["save"]},{title:"far fa-bookmark",searchTerms:["save"]},{title:"fas fa-bowling-ball",searchTerms:[]},{title:"fas fa-box",searchTerms:[]},{title:"fas fa-boxes",searchTerms:[]},{title:"fas fa-braille",searchTerms:[]},{title:"fas fa-briefcase",searchTerms:["work","business","office","luggage","bag"]},{title:"fab fa-btc",searchTerms:[]},{title:"fas fa-bug",searchTerms:["report","insect"]},{title:"fas fa-building",searchTerms:["work","business","apartment","office","company"]},{title:"far fa-building",searchTerms:["work","business","apartment","office","company"]},{title:"fas fa-bullhorn",searchTerms:["announcement","share","broadcast","louder","megaphone"]},{title:"fas fa-bullseye",searchTerms:["target"]},{title:"fab fa-buromobelexperte",searchTerms:[]},{title:"fas fa-bus",searchTerms:["vehicle"]},{title:"fab fa-buysellads",searchTerms:[]},{title:"fas fa-calculator",searchTerms:[]},{title:"fas fa-calendar",searchTerms:["date","time","when","event","calendar-o"]},{title:"far fa-calendar",searchTerms:["date","time","when","event","calendar-o"]},{title:"fas fa-calendar-alt",searchTerms:["date","time","when","event","calendar"]},{title:"far fa-calendar-alt",searchTerms:["date","time","when","event","calendar"]},{title:"fas fa-calendar-check",searchTerms:["ok"]},{title:"far fa-calendar-check",searchTerms:["ok"]},{title:"fas fa-calendar-minus",searchTerms:[]},{title:"far fa-calendar-minus",searchTerms:[]},{title:"fas fa-calendar-plus",searchTerms:[]},{title:"far fa-calendar-plus",searchTerms:[]},{title:"fas fa-calendar-times",searchTerms:[]},{title:"far fa-calendar-times",searchTerms:[]},{title:"fas fa-camera",searchTerms:["photo","picture","record"]},{title:"fas fa-camera-retro",searchTerms:["photo","picture","record"]},{title:"fas fa-car",searchTerms:["vehicle"]},{title:"fas fa-caret-down",searchTerms:["more","dropdown","menu","triangle down","arrow"]},{title:"fas fa-caret-left",searchTerms:["previous","back","triangle left","arrow"]},{title:"fas fa-caret-right",searchTerms:["next","forward","triangle right","arrow"]},{title:"fas fa-caret-square-down",searchTerms:["more","dropdown","menu","caret-square-o-down"]},{title:"far fa-caret-square-down",searchTerms:["more","dropdown","menu","caret-square-o-down"]},{title:"fas fa-caret-square-left",searchTerms:["previous","back","caret-square-o-left"]},{title:"far fa-caret-square-left",searchTerms:["previous","back","caret-square-o-left"]},{title:"fas fa-caret-square-right",searchTerms:["next","forward","caret-square-o-right"]},{title:"far fa-caret-square-right",searchTerms:["next","forward","caret-square-o-right"]},{title:"fas fa-caret-square-up",searchTerms:["caret-square-o-up"]},{title:"far fa-caret-square-up",searchTerms:["caret-square-o-up"]},{title:"fas fa-caret-up",searchTerms:["triangle up","arrow"]},{title:"fas fa-cart-arrow-down",searchTerms:["shopping"]},{title:"fas fa-cart-plus",searchTerms:["add","shopping"]},{title:"fab fa-cc-amazon-pay",searchTerms:[]},{title:"fab fa-cc-amex",searchTerms:["amex"]},{title:"fab fa-cc-apple-pay",searchTerms:[]},{title:"fab fa-cc-diners-club",searchTerms:[]},{title:"fab fa-cc-discover",searchTerms:[]},{title:"fab fa-cc-jcb",searchTerms:[]},{title:"fab fa-cc-mastercard",searchTerms:[]},{title:"fab fa-cc-paypal",searchTerms:[]},{title:"fab fa-cc-stripe",searchTerms:[]},{title:"fab fa-cc-visa",searchTerms:[]},{title:"fab fa-centercode",searchTerms:[]},{title:"fas fa-certificate",searchTerms:["badge","star"]},{title:"fas fa-chart-area",searchTerms:["graph","analytics","area-chart"]},{title:"fas fa-chart-bar",searchTerms:["graph","analytics","bar-chart"]},{title:"far fa-chart-bar",searchTerms:["graph","analytics","bar-chart"]},{title:"fas fa-chart-line",searchTerms:["graph","analytics","line-chart","dashboard"]},{title:"fas fa-chart-pie",searchTerms:["graph","analytics","pie-chart"]},{title:"fas fa-check",searchTerms:["checkmark","done","todo","agree","accept","confirm","tick","ok","select"]},{title:"fas fa-check-circle",searchTerms:["todo","done","agree","accept","confirm","ok","select"]},{title:"far fa-check-circle",searchTerms:["todo","done","agree","accept","confirm","ok","select"]},{title:"fas fa-check-square",searchTerms:["checkmark","done","todo","agree","accept","confirm","ok","select"]},{title:"far fa-check-square",searchTerms:["checkmark","done","todo","agree","accept","confirm","ok","select"]},{title:"fas fa-chess",searchTerms:[]},{title:"fas fa-chess-bishop",searchTerms:[]},{title:"fas fa-chess-board",searchTerms:[]},{title:"fas fa-chess-king",searchTerms:[]},{title:"fas fa-chess-knight",searchTerms:[]},{title:"fas fa-chess-pawn",searchTerms:[]},{title:"fas fa-chess-queen",searchTerms:[]},{title:"fas fa-chess-rook",searchTerms:[]},{title:"fas fa-chevron-circle-down",searchTerms:["more","dropdown","menu","arrow"]},{title:"fas fa-chevron-circle-left",searchTerms:["previous","back","arrow"]},{title:"fas fa-chevron-circle-right",searchTerms:["next","forward","arrow"]},{title:"fas fa-chevron-circle-up",searchTerms:["arrow"]},{title:"fas fa-chevron-down",searchTerms:[]},{title:"fas fa-chevron-left",searchTerms:["bracket","previous","back"]},{title:"fas fa-chevron-right",searchTerms:["bracket","next","forward"]},{title:"fas fa-chevron-up",searchTerms:[]},{title:"fas fa-child",searchTerms:[]},{title:"fab fa-chrome",searchTerms:["browser"]},{title:"fas fa-circle",searchTerms:["dot","notification","circle-thin"]},{title:"far fa-circle",searchTerms:["dot","notification","circle-thin"]},{title:"fas fa-circle-notch",searchTerms:["circle-o-notch"]},{title:"fas fa-clipboard",searchTerms:["paste"]},{title:"far fa-clipboard",searchTerms:["paste"]},{title:"fas fa-clipboard-check",searchTerms:[]},{title:"fas fa-clipboard-list",searchTerms:[]},{title:"fas fa-clock",searchTerms:["watch","timer","late","timestamp","date"]},{title:"far fa-clock",searchTerms:["watch","timer","late","timestamp","date"]},{title:"fas fa-clone",searchTerms:["copy"]},{title:"far fa-clone",searchTerms:["copy"]},{title:"fas fa-closed-captioning",searchTerms:["cc"]},{title:"far fa-closed-captioning",searchTerms:["cc"]},{title:"fas fa-cloud",searchTerms:["save"]},{title:"fas fa-cloud-download-alt",searchTerms:["cloud-download"]},{title:"fas fa-cloud-upload-alt",searchTerms:["cloud-upload"]},{title:"fab fa-cloudscale",searchTerms:[]},{title:"fab fa-cloudsmith",searchTerms:[]},{title:"fab fa-cloudversify",searchTerms:[]},{title:"fas fa-code",searchTerms:["html","brackets"]},{title:"fas fa-code-branch",searchTerms:["git","fork","vcs","svn","github","rebase","version","branch","code-fork"]},{title:"fab fa-codepen",searchTerms:[]},{title:"fab fa-codiepie",searchTerms:[]},{title:"fas fa-coffee",searchTerms:["morning","mug","breakfast","tea","drink","cafe"]},{title:"fas fa-cog",searchTerms:["settings"]},{title:"fas fa-cogs",searchTerms:["settings","gears"]},{title:"fas fa-columns",searchTerms:["split","panes","dashboard"]},{title:"fas fa-comment",searchTerms:["speech","notification","note","chat","bubble","feedback","message","texting","sms","conversation"]},{title:"far fa-comment",searchTerms:["speech","notification","note","chat","bubble","feedback","message","texting","sms","conversation"]},{title:"fas fa-comment-alt",searchTerms:["speech","notification","note","chat","bubble","feedback","message","texting","sms","conversation","commenting","commenting"]},{title:"far fa-comment-alt",searchTerms:["speech","notification","note","chat","bubble","feedback","message","texting","sms","conversation","commenting","commenting"]},{title:"fas fa-comments",searchTerms:["speech","notification","note","chat","bubble","feedback","message","texting","sms","conversation"]},{title:"far fa-comments",searchTerms:["speech","notification","note","chat","bubble","feedback","message","texting","sms","conversation"]},{title:"fas fa-compass",searchTerms:["safari","directory","menu","location"]},{title:"far fa-compass",searchTerms:["safari","directory","menu","location"]},{title:"fas fa-compress",searchTerms:["collapse","combine","contract","merge","smaller"]},{title:"fab fa-connectdevelop",searchTerms:[]},{title:"fab fa-contao",searchTerms:[]},{title:"fas fa-copy",searchTerms:["duplicate","clone","file","files-o"]},{title:"far fa-copy",searchTerms:["duplicate","clone","file","files-o"]},{title:"fas fa-copyright",searchTerms:[]},{title:"far fa-copyright",searchTerms:[]},{title:"fab fa-cpanel",searchTerms:[]},{title:"fab fa-creative-commons",searchTerms:[]},{title:"fas fa-credit-card",searchTerms:["money","buy","debit","checkout","purchase","payment","credit-card-alt"]},{title:"far fa-credit-card",searchTerms:["money","buy","debit","checkout","purchase","payment","credit-card-alt"]},{title:"fas fa-crop",searchTerms:["design"]},{title:"fas fa-crosshairs",searchTerms:["picker","gpd"]},{title:"fab fa-css3",searchTerms:["code"]},{title:"fab fa-css3-alt",searchTerms:[]},{title:"fas fa-cube",searchTerms:["package"]},{title:"fas fa-cubes",searchTerms:["packages"]},{title:"fas fa-cut",searchTerms:["scissors","scissors"]},{title:"fab fa-cuttlefish",searchTerms:[]},{title:"fab fa-d-and-d",searchTerms:[]},{title:"fab fa-dashcube",searchTerms:[]},{title:"fas fa-database",searchTerms:[]},{title:"fas fa-deaf",searchTerms:[]},{title:"fab fa-delicious",searchTerms:[]},{title:"fab fa-deploydog",searchTerms:[]},{title:"fab fa-deskpro",searchTerms:[]},{title:"fas fa-desktop",searchTerms:["monitor","screen","desktop","computer","demo","device","pc"]},{title:"fab fa-deviantart",searchTerms:[]},{title:"fab fa-digg",searchTerms:[]},{title:"fab fa-digital-ocean",searchTerms:[]},{title:"fab fa-discord",searchTerms:[]},{title:"fab fa-discourse",searchTerms:[]},{title:"fas fa-dna",searchTerms:["double helix","helix"]},{title:"fab fa-dochub",searchTerms:[]},{title:"fab fa-docker",searchTerms:[]},{title:"fas fa-dollar-sign",searchTerms:["usd","price"]},{title:"fas fa-dolly",searchTerms:[]},{title:"fas fa-dolly-flatbed",searchTerms:[]},{title:"fas fa-dot-circle",searchTerms:["target","bullseye","notification"]},{title:"far fa-dot-circle",searchTerms:["target","bullseye","notification"]},{title:"fas fa-download",searchTerms:["import"]},{title:"fab fa-draft2digital",searchTerms:[]},{title:"fab fa-dribbble",searchTerms:[]},{title:"fab fa-dribbble-square",searchTerms:[]},{title:"fab fa-dropbox",searchTerms:[]},{title:"fab fa-drupal",searchTerms:[]},{title:"fab fa-dyalog",searchTerms:[]},{title:"fab fa-earlybirds",searchTerms:[]},{title:"fab fa-edge",searchTerms:["browser","ie"]},{title:"fas fa-edit",searchTerms:["write","edit","update","pencil","pen"]},{title:"far fa-edit",searchTerms:["write","edit","update","pencil","pen"]},{title:"fas fa-eject",searchTerms:[]},{title:"fab fa-elementor",searchTerms:[]},{title:"fas fa-ellipsis-h",searchTerms:["dots"]},{title:"fas fa-ellipsis-v",searchTerms:["dots"]},{title:"fab fa-ember",searchTerms:[]},{title:"fab fa-empire",searchTerms:[]},{title:"fas fa-envelope",searchTerms:["email","e-mail","letter","support","mail","message","notification"]},{title:"far fa-envelope",searchTerms:["email","e-mail","letter","support","mail","message","notification"]},{title:"fas fa-envelope-open",searchTerms:["email","e-mail","letter","support","mail","message","notification"]},{title:"far fa-envelope-open",searchTerms:["email","e-mail","letter","support","mail","message","notification"]},{title:"fas fa-envelope-square",searchTerms:["email","e-mail","letter","support","mail","message","notification"]},{title:"fab fa-envira",searchTerms:["leaf"]},{title:"fas fa-eraser",searchTerms:["remove","delete"]},{title:"fab fa-erlang",searchTerms:[]},{title:"fab fa-ethereum",searchTerms:[]},{title:"fab fa-etsy",searchTerms:[]},{title:"fas fa-euro-sign",searchTerms:["eur","eur"]},{title:"fas fa-exchange-alt",searchTerms:["transfer","arrows","arrow","exchange","swap"]},{title:"fas fa-exclamation",searchTerms:["warning","error","problem","notification","notify","alert","danger"]},{title:"fas fa-exclamation-circle",searchTerms:["warning","error","problem","notification","notify","alert","danger"]},{title:"fas fa-exclamation-triangle",searchTerms:["warning","error","problem","notification","notify","alert","danger"]},{title:"fas fa-expand",searchTerms:["enlarge","bigger","resize"]},{title:"fas fa-expand-arrows-alt",searchTerms:["enlarge","bigger","resize","move","arrows-alt"]},{title:"fab fa-expeditedssl",searchTerms:[]},{title:"fas fa-external-link-alt",searchTerms:["open","new","external-link"]},{title:"fas fa-external-link-square-alt",searchTerms:["open","new","external-link-square"]},{title:"fas fa-eye",searchTerms:["show","visible","views"]},{title:"fas fa-eye-dropper",searchTerms:["eyedropper"]},{title:"fas fa-eye-slash",searchTerms:["toggle","show","hide","visible","visiblity","views"]},{title:"far fa-eye-slash",searchTerms:["toggle","show","hide","visible","visiblity","views"]},{title:"fab fa-facebook",searchTerms:["social network","facebook-official"]},{title:"fab fa-facebook-f",searchTerms:["facebook"]},{title:"fab fa-facebook-messenger",searchTerms:[]},{title:"fab fa-facebook-square",searchTerms:["social network"]},{title:"fas fa-fast-backward",searchTerms:["rewind","previous","beginning","start","first"]},{title:"fas fa-fast-forward",searchTerms:["next","end","last"]},{title:"fas fa-fax",searchTerms:[]},{title:"fas fa-female",searchTerms:["woman","human","user","person","profile"]},{title:"fas fa-fighter-jet",searchTerms:["fly","plane","airplane","quick","fast","travel"]},{title:"fas fa-file",searchTerms:["new","page","pdf","document"]},{title:"far fa-file",searchTerms:["new","page","pdf","document"]},{title:"fas fa-file-alt",searchTerms:["new","page","pdf","document","file-text"]},{title:"far fa-file-alt",searchTerms:["new","page","pdf","document","file-text"]},{title:"fas fa-file-archive",searchTerms:[]},{title:"far fa-file-archive",searchTerms:[]},{title:"fas fa-file-audio",searchTerms:[]},{title:"far fa-file-audio",searchTerms:[]},{title:"fas fa-file-code",searchTerms:[]},{title:"far fa-file-code",searchTerms:[]},{title:"fas fa-file-excel",searchTerms:[]},{title:"far fa-file-excel",searchTerms:[]},{title:"fas fa-file-image",searchTerms:[]},{title:"far fa-file-image",searchTerms:[]},{title:"fas fa-file-pdf",searchTerms:[]},{title:"far fa-file-pdf",searchTerms:[]},{title:"fas fa-file-powerpoint",searchTerms:[]},{title:"far fa-file-powerpoint",searchTerms:[]},{title:"fas fa-file-video",searchTerms:[]},{title:"far fa-file-video",searchTerms:[]},{title:"fas fa-file-word",searchTerms:[]},{title:"far fa-file-word",searchTerms:[]},{title:"fas fa-film",searchTerms:["movie"]},{title:"fas fa-filter",searchTerms:["funnel","options"]},{title:"fas fa-fire",searchTerms:["flame","hot","popular"]},{title:"fas fa-fire-extinguisher",searchTerms:[]},{title:"fab fa-firefox",searchTerms:["browser"]},{title:"fas fa-first-aid",searchTerms:[]},{title:"fab fa-first-order",searchTerms:[]},{title:"fab fa-firstdraft",searchTerms:[]},{title:"fas fa-flag",searchTerms:["report","notification","notify"]},{title:"far fa-flag",searchTerms:["report","notification","notify"]},{title:"fas fa-flag-checkered",searchTerms:["report","notification","notify"]},{title:"fas fa-flask",searchTerms:["science","beaker","experimental","labs"]},{title:"fab fa-flickr",searchTerms:[]},{title:"fab fa-flipboard",searchTerms:[]},{title:"fab fa-fly",searchTerms:[]},{title:"fas fa-folder",searchTerms:[]},{title:"far fa-folder",searchTerms:[]},{title:"fas fa-folder-open",searchTerms:[]},{title:"far fa-folder-open",searchTerms:[]},{title:"fas fa-font",searchTerms:["text"]},{title:"fab fa-font-awesome",searchTerms:["meanpath"]},{title:"fab fa-font-awesome-alt",searchTerms:[]},{title:"fab fa-font-awesome-flag",searchTerms:[]},{title:"fab fa-fonticons",searchTerms:[]},{title:"fab fa-fonticons-fi",searchTerms:[]},{title:"fas fa-football-ball",searchTerms:[]},{title:"fab fa-fort-awesome",searchTerms:["castle"]},{title:"fab fa-fort-awesome-alt",searchTerms:["castle"]},{title:"fab fa-forumbee",searchTerms:[]},{title:"fas fa-forward",searchTerms:["forward","next"]},{title:"fab fa-foursquare",searchTerms:[]},{title:"fab fa-free-code-camp",searchTerms:[]},{title:"fab fa-freebsd",searchTerms:[]},{title:"fas fa-frown",searchTerms:["face","emoticon","sad","disapprove","rating"]},{title:"far fa-frown",searchTerms:["face","emoticon","sad","disapprove","rating"]},{title:"fas fa-futbol",searchTerms:[]},{title:"far fa-futbol",searchTerms:[]},{title:"fas fa-gamepad",searchTerms:["controller"]},{title:"fas fa-gavel",searchTerms:["judge","lawyer","opinion","hammer"]},{title:"fas fa-gem",searchTerms:["diamond"]},{title:"far fa-gem",searchTerms:["diamond"]},{title:"fas fa-genderless",searchTerms:[]},{title:"fab fa-get-pocket",searchTerms:[]},{title:"fab fa-gg",searchTerms:[]},{title:"fab fa-gg-circle",searchTerms:[]},{title:"fas fa-gift",searchTerms:["present"]},{title:"fab fa-git",searchTerms:[]},{title:"fab fa-git-square",searchTerms:[]},{title:"fab fa-github",searchTerms:["octocat"]},{title:"fab fa-github-alt",searchTerms:["octocat"]},{title:"fab fa-github-square",searchTerms:["octocat"]},{title:"fab fa-gitkraken",searchTerms:[]},{title:"fab fa-gitlab",searchTerms:["Axosoft"]},{title:"fab fa-gitter",searchTerms:[]},{title:"fas fa-glass-martini",searchTerms:["martini","drink","bar","alcohol","liquor","glass"]},{title:"fab fa-glide",searchTerms:[]},{title:"fab fa-glide-g",searchTerms:[]},{title:"fas fa-globe",searchTerms:["world","planet","map","place","travel","earth","global","translate","all","language","localize","location","coordinates","country","gps"]},{title:"fab fa-gofore",searchTerms:[]},{title:"fas fa-golf-ball",searchTerms:[]},{title:"fab fa-goodreads",searchTerms:[]},{title:"fab fa-goodreads-g",searchTerms:[]},{title:"fab fa-google",searchTerms:[]},{title:"fab fa-google-drive",searchTerms:[]},{title:"fab fa-google-play",searchTerms:[]},{title:"fab fa-google-plus",searchTerms:["google-plus-circle","google-plus-official"]},{title:"fab fa-google-plus-g",searchTerms:["social network","google-plus"]},{title:"fab fa-google-plus-square",searchTerms:["social network"]},{title:"fab fa-google-wallet",searchTerms:[]},{title:"fas fa-graduation-cap",searchTerms:["learning","school","student"]},{title:"fab fa-gratipay",searchTerms:["heart","like","favorite","love"]},{title:"fab fa-grav",searchTerms:[]},{title:"fab fa-gripfire",searchTerms:[]},{title:"fab fa-grunt",searchTerms:[]},{title:"fab fa-gulp",searchTerms:[]},{title:"fas fa-h-square",searchTerms:["hospital","hotel"]},{title:"fab fa-hacker-news",searchTerms:[]},{title:"fab fa-hacker-news-square",searchTerms:[]},{title:"fas fa-hand-lizard",searchTerms:[]},{title:"far fa-hand-lizard",searchTerms:[]},{title:"fas fa-hand-paper",searchTerms:["stop"]},{title:"far fa-hand-paper",searchTerms:["stop"]},{title:"fas fa-hand-peace",searchTerms:[]},{title:"far fa-hand-peace",searchTerms:[]},{title:"fas fa-hand-point-down",searchTerms:["point","finger","hand-o-down"]},{title:"far fa-hand-point-down",searchTerms:["point","finger","hand-o-down"]},{title:"fas fa-hand-point-left",searchTerms:["point","left","previous","back","finger","hand-o-left"]},{title:"far fa-hand-point-left",searchTerms:["point","left","previous","back","finger","hand-o-left"]},{title:"fas fa-hand-point-right",searchTerms:["point","right","next","forward","finger","hand-o-right"]},{title:"far fa-hand-point-right",searchTerms:["point","right","next","forward","finger","hand-o-right"]},{title:"fas fa-hand-point-up",searchTerms:["point","finger","hand-o-up"]},{title:"far fa-hand-point-up",searchTerms:["point","finger","hand-o-up"]},{title:"fas fa-hand-pointer",searchTerms:["select"]},{title:"far fa-hand-pointer",searchTerms:["select"]},{title:"fas fa-hand-rock",searchTerms:[]},{title:"far fa-hand-rock",searchTerms:[]},{title:"fas fa-hand-scissors",searchTerms:[]},{title:"far fa-hand-scissors",searchTerms:[]},{title:"fas fa-hand-spock",searchTerms:[]},{title:"far fa-hand-spock",searchTerms:[]},{title:"fas fa-handshake",searchTerms:[]},{title:"far fa-handshake",searchTerms:[]},{title:"fas fa-hashtag",searchTerms:[]},{title:"fas fa-hdd",searchTerms:["harddrive","hard drive","storage","save"]},{title:"far fa-hdd",searchTerms:["harddrive","hard drive","storage","save"]},{title:"fas fa-heading",searchTerms:["header","header"]},{title:"fas fa-headphones",searchTerms:["sound","listen","music","audio"]},{title:"fas fa-heart",searchTerms:["love","like","favorite"]},{title:"far fa-heart",searchTerms:["love","like","favorite"]},{title:"fas fa-heartbeat",searchTerms:["ekg","vital signs"]},{title:"fab fa-hips",searchTerms:[]},{title:"fab fa-hire-a-helper",searchTerms:[]},{title:"fas fa-history",searchTerms:[]},{title:"fas fa-hockey-puck",searchTerms:[]},{title:"fas fa-home",searchTerms:["main","house"]},{title:"fab fa-hooli",searchTerms:[]},{title:"fas fa-hospital",searchTerms:["building","medical center","emergency room"]},{title:"far fa-hospital",searchTerms:["building","medical center","emergency room"]},{title:"fas fa-hospital-symbol",searchTerms:[]},{title:"fab fa-hotjar",searchTerms:[]},{title:"fas fa-hourglass",searchTerms:[]},{title:"far fa-hourglass",searchTerms:[]},{title:"fas fa-hourglass-end",searchTerms:[]},{title:"fas fa-hourglass-half",searchTerms:[]},{title:"fas fa-hourglass-start",searchTerms:[]},{title:"fab fa-houzz",searchTerms:[]},{title:"fab fa-html5",searchTerms:[]},{title:"fab fa-hubspot",searchTerms:[]},{title:"fas fa-i-cursor",searchTerms:[]},{title:"fas fa-id-badge",searchTerms:[]},{title:"far fa-id-badge",searchTerms:[]},{title:"fas fa-id-card",searchTerms:[]},{title:"far fa-id-card",searchTerms:[]},{title:"fas fa-image",searchTerms:["photo","album","picture","picture"]},{title:"far fa-image",searchTerms:["photo","album","picture","picture"]},{title:"fas fa-images",searchTerms:["photo","album","picture"]},{title:"far fa-images",searchTerms:["photo","album","picture"]},{title:"fab fa-imdb",searchTerms:[]},{title:"fas fa-inbox",searchTerms:[]},{title:"fas fa-indent",searchTerms:[]},{title:"fas fa-industry",searchTerms:["factory"]},{title:"fas fa-info",searchTerms:["help","information","more","details"]},{title:"fas fa-info-circle",searchTerms:["help","information","more","details"]},{title:"fab fa-instagram",searchTerms:[]},{title:"fab fa-internet-explorer",searchTerms:["browser","ie"]},{title:"fab fa-ioxhost",searchTerms:[]},{title:"fas fa-italic",searchTerms:["italics"]},{title:"fab fa-itunes",searchTerms:[]},{title:"fab fa-itunes-note",searchTerms:[]},{title:"fab fa-jenkins",searchTerms:[]},{title:"fab fa-joget",searchTerms:[]},{title:"fab fa-joomla",searchTerms:[]},{title:"fab fa-js",searchTerms:[]},{title:"fab fa-js-square",searchTerms:[]},{title:"fab fa-jsfiddle",searchTerms:[]},{title:"fas fa-key",searchTerms:["unlock","password"]},{title:"fas fa-keyboard",searchTerms:["type","input"]},{title:"far fa-keyboard",searchTerms:["type","input"]},{title:"fab fa-keycdn",searchTerms:[]},{title:"fab fa-kickstarter",searchTerms:[]},{title:"fab fa-kickstarter-k",searchTerms:[]},{title:"fab fa-korvue",searchTerms:[]},{title:"fas fa-language",searchTerms:[]},{title:"fas fa-laptop",searchTerms:["demo","computer","device","pc"]},{title:"fab fa-laravel",searchTerms:[]},{title:"fab fa-lastfm",searchTerms:[]},{title:"fab fa-lastfm-square",searchTerms:[]},{title:"fas fa-leaf",searchTerms:["eco","nature","plant"]},{title:"fab fa-leanpub",searchTerms:[]},{title:"fas fa-lemon",searchTerms:["food"]},{title:"far fa-lemon",searchTerms:["food"]},{title:"fab fa-less",searchTerms:[]},{title:"fas fa-level-down-alt",searchTerms:["level-down"]},{title:"fas fa-level-up-alt",searchTerms:["level-up"]},{title:"fas fa-life-ring",searchTerms:["support"]},{title:"far fa-life-ring",searchTerms:["support"]},{title:"fas fa-lightbulb",searchTerms:["idea","inspiration"]},{title:"far fa-lightbulb",searchTerms:["idea","inspiration"]},{title:"fab fa-line",searchTerms:[]},{title:"fas fa-link",searchTerms:["chain"]},{title:"fab fa-linkedin",searchTerms:["linkedin-square"]},{title:"fab fa-linkedin-in",searchTerms:["linkedin"]},{title:"fab fa-linode",searchTerms:[]},{title:"fab fa-linux",searchTerms:["tux"]},{title:"fas fa-lira-sign",searchTerms:["try","turkish","try"]},{title:"fas fa-list",searchTerms:["ul","ol","checklist","finished","completed","done","todo"]},{title:"fas fa-list-alt",searchTerms:["ul","ol","checklist","finished","completed","done","todo"]},{title:"far fa-list-alt",searchTerms:["ul","ol","checklist","finished","completed","done","todo"]},{title:"fas fa-list-ol",searchTerms:["ul","ol","checklist","list","todo","list","numbers"]},{title:"fas fa-list-ul",searchTerms:["ul","ol","checklist","todo","list"]},{title:"fas fa-location-arrow",searchTerms:["map","coordinates","location","address","place","where","gps"]},{title:"fas fa-lock",searchTerms:["protect","admin","security"]},{title:"fas fa-lock-open",searchTerms:["protect","admin","password","lock","open"]},{title:"fas fa-long-arrow-alt-down",searchTerms:["long-arrow-down"]},{title:"fas fa-long-arrow-alt-left",searchTerms:["previous","back","long-arrow-left"]},{title:"fas fa-long-arrow-alt-right",searchTerms:["long-arrow-right"]},{title:"fas fa-long-arrow-alt-up",searchTerms:["long-arrow-up"]},{title:"fas fa-low-vision",searchTerms:[]},{title:"fab fa-lyft",searchTerms:[]},{title:"fab fa-magento",searchTerms:[]},{title:"fas fa-magic",searchTerms:["wizard","automatic","autocomplete"]},{title:"fas fa-magnet",searchTerms:[]},{title:"fas fa-male",searchTerms:["man","human","user","person","profile"]},{title:"fas fa-map",searchTerms:[]},{title:"far fa-map",searchTerms:[]},{title:"fas fa-map-marker",searchTerms:["map","pin","location","coordinates","localize","address","travel","where","place","gps"]},{title:"fas fa-map-marker-alt",searchTerms:["map-marker","gps"]},{title:"fas fa-map-pin",searchTerms:[]},{title:"fas fa-map-signs",searchTerms:[]},{title:"fas fa-mars",searchTerms:["male"]},{title:"fas fa-mars-double",searchTerms:[]},{title:"fas fa-mars-stroke",searchTerms:[]},{title:"fas fa-mars-stroke-h",searchTerms:[]},{title:"fas fa-mars-stroke-v",searchTerms:[]},{title:"fab fa-maxcdn",searchTerms:[]},{title:"fab fa-medapps",searchTerms:[]},{title:"fab fa-medium",searchTerms:[]},{title:"fab fa-medium-m",searchTerms:[]},{title:"fas fa-medkit",searchTerms:["first aid","firstaid","help","support","health"]},{title:"fab fa-medrt",searchTerms:[]},{title:"fab fa-meetup",searchTerms:[]},{title:"fas fa-meh",searchTerms:["face","emoticon","rating","neutral"]},{title:"far fa-meh",searchTerms:["face","emoticon","rating","neutral"]},{title:"fas fa-mercury",searchTerms:["transgender"]},{title:"fas fa-microchip",searchTerms:[]},{title:"fas fa-microphone",searchTerms:["record","voice","sound"]},{title:"fas fa-microphone-slash",searchTerms:["record","voice","sound","mute"]},{title:"fab fa-microsoft",searchTerms:[]},{title:"fas fa-minus",searchTerms:["hide","minify","delete","remove","trash","hide","collapse"]},{title:"fas fa-minus-circle",searchTerms:["delete","remove","trash","hide"]},{title:"fas fa-minus-square",searchTerms:["hide","minify","delete","remove","trash","hide","collapse"]},{title:"far fa-minus-square",searchTerms:["hide","minify","delete","remove","trash","hide","collapse"]},{title:"fab fa-mix",searchTerms:[]},{title:"fab fa-mixcloud",searchTerms:[]},{title:"fab fa-mizuni",searchTerms:[]},{title:"fas fa-mobile",searchTerms:["cell phone","cellphone","text","call","iphone","number","telephone"]},{title:"fas fa-mobile-alt",searchTerms:["mobile"]},{title:"fab fa-modx",searchTerms:[]},{title:"fab fa-monero",searchTerms:[]},{title:"fas fa-money-bill-alt",searchTerms:["cash","money","buy","checkout","purchase","payment","price"]},{title:"far fa-money-bill-alt",searchTerms:["cash","money","buy","checkout","purchase","payment","price"]},{title:"fas fa-moon",searchTerms:["night","darker","contrast"]},{title:"far fa-moon",searchTerms:["night","darker","contrast"]},{title:"fas fa-motorcycle",searchTerms:["vehicle","bike"]},{title:"fas fa-mouse-pointer",searchTerms:["select"]},{title:"fas fa-music",searchTerms:["note","sound"]},{title:"fab fa-napster",searchTerms:[]},{title:"fas fa-neuter",searchTerms:[]},{title:"fas fa-newspaper",searchTerms:["press","article"]},{title:"far fa-newspaper",searchTerms:["press","article"]},{title:"fab fa-nintendo-switch",searchTerms:[]},{title:"fab fa-node",searchTerms:[]},{title:"fab fa-node-js",searchTerms:[]},{title:"fab fa-npm",searchTerms:[]},{title:"fab fa-ns8",searchTerms:[]},{title:"fab fa-nutritionix",searchTerms:[]},{title:"fas fa-object-group",searchTerms:["design"]},{title:"far fa-object-group",searchTerms:["design"]},{title:"fas fa-object-ungroup",searchTerms:["design"]},{title:"far fa-object-ungroup",searchTerms:["design"]},{title:"fab fa-odnoklassniki",searchTerms:[]},{title:"fab fa-odnoklassniki-square",searchTerms:[]},{title:"fab fa-opencart",searchTerms:[]},{title:"fab fa-openid",searchTerms:[]},{title:"fab fa-opera",searchTerms:[]},{title:"fab fa-optin-monster",searchTerms:[]},{title:"fab fa-osi",searchTerms:[]},{title:"fas fa-outdent",searchTerms:[]},{title:"fab fa-page4",searchTerms:[]},{title:"fab fa-pagelines",searchTerms:["leaf","leaves","tree","plant","eco","nature"]},{title:"fas fa-paint-brush",searchTerms:[]},{title:"fab fa-palfed",searchTerms:[]},{title:"fas fa-pallet",searchTerms:[]},{title:"fas fa-paper-plane",searchTerms:[]},{title:"far fa-paper-plane",searchTerms:[]},{title:"fas fa-paperclip",searchTerms:["attachment"]},{title:"fas fa-paragraph",searchTerms:[]},{title:"fas fa-paste",searchTerms:["copy","clipboard"]},{title:"fab fa-patreon",searchTerms:[]},{title:"fas fa-pause",searchTerms:["wait"]},{title:"fas fa-pause-circle",searchTerms:[]},{title:"far fa-pause-circle",searchTerms:[]},{title:"fas fa-paw",searchTerms:["pet"]},{title:"fab fa-paypal",searchTerms:[]},{title:"fas fa-pen-square",searchTerms:["write","edit","update","pencil-square"]},{title:"fas fa-pencil-alt",searchTerms:["write","edit","update","pencil","design"]},{title:"fas fa-percent",searchTerms:[]},{title:"fab fa-periscope",searchTerms:[]},{title:"fab fa-phabricator",searchTerms:[]},{title:"fab fa-phoenix-framework",searchTerms:[]},{title:"fas fa-phone",searchTerms:["call","voice","number","support","earphone","telephone"]},{title:"fas fa-phone-square",searchTerms:["call","voice","number","support","telephone"]},{title:"fas fa-phone-volume",searchTerms:["telephone","volume-control-phone"]},{title:"fab fa-php",searchTerms:[]},{title:"fab fa-pied-piper",searchTerms:[]},{title:"fab fa-pied-piper-alt",searchTerms:[]},{title:"fab fa-pied-piper-pp",searchTerms:[]},{title:"fas fa-pills",searchTerms:["medicine","drugs"]},{title:"fab fa-pinterest",searchTerms:[]},{title:"fab fa-pinterest-p",searchTerms:[]},{title:"fab fa-pinterest-square",searchTerms:[]},{title:"fas fa-plane",searchTerms:["travel","trip","location","destination","airplane","fly","mode"]},{title:"fas fa-play",searchTerms:["start","playing","music","sound"]},{title:"fas fa-play-circle",searchTerms:["start","playing"]},{title:"far fa-play-circle",searchTerms:["start","playing"]},{title:"fab fa-playstation",searchTerms:[]},{title:"fas fa-plug",searchTerms:["power","connect"]},{title:"fas fa-plus",searchTerms:["add","new","create","expand"]},{title:"fas fa-plus-circle",searchTerms:["add","new","create","expand"]},{title:"fas fa-plus-square",searchTerms:["add","new","create","expand"]},{title:"far fa-plus-square",searchTerms:["add","new","create","expand"]},{title:"fas fa-podcast",searchTerms:[]},{title:"fas fa-pound-sign",searchTerms:["gbp","gbp"]},{title:"fas fa-power-off",searchTerms:["on"]},{title:"fas fa-print",searchTerms:[]},{title:"fab fa-product-hunt",searchTerms:[]},{title:"fab fa-pushed",searchTerms:[]},{title:"fas fa-puzzle-piece",searchTerms:["addon","add-on","section"]},{title:"fab fa-python",searchTerms:[]},{title:"fab fa-qq",searchTerms:[]},{title:"fas fa-qrcode",searchTerms:["scan"]},{title:"fas fa-question",searchTerms:["help","information","unknown","support"]},{title:"fas fa-question-circle",searchTerms:["help","information","unknown","support"]},{title:"far fa-question-circle",searchTerms:["help","information","unknown","support"]},{title:"fas fa-quidditch",searchTerms:[]},{title:"fab fa-quinscape",searchTerms:[]},{title:"fab fa-quora",searchTerms:[]},{title:"fas fa-quote-left",searchTerms:[]},{title:"fas fa-quote-right",searchTerms:[]},{title:"fas fa-random",searchTerms:["sort","shuffle"]},{title:"fab fa-ravelry",searchTerms:[]},{title:"fab fa-react",searchTerms:[]},{title:"fab fa-rebel",searchTerms:[]},{title:"fas fa-recycle",searchTerms:[]},{title:"fab fa-red-river",searchTerms:[]},{title:"fab fa-reddit",searchTerms:[]},{title:"fab fa-reddit-alien",searchTerms:[]},{title:"fab fa-reddit-square",searchTerms:[]},{title:"fas fa-redo",searchTerms:["forward","repeat","repeat"]},{title:"fas fa-redo-alt",searchTerms:["forward","repeat"]},{title:"fas fa-registered",searchTerms:[]},{title:"far fa-registered",searchTerms:[]},{title:"fab fa-rendact",searchTerms:[]},{title:"fab fa-renren",searchTerms:[]},{title:"fas fa-reply",searchTerms:[]},{title:"fas fa-reply-all",searchTerms:[]},{title:"fab fa-replyd",searchTerms:[]},{title:"fab fa-resolving",searchTerms:[]},{title:"fas fa-retweet",searchTerms:["refresh","reload","share","swap"]},{title:"fas fa-road",searchTerms:["street"]},{title:"fas fa-rocket",searchTerms:["app"]},{title:"fab fa-rocketchat",searchTerms:[]},{title:"fab fa-rockrms",searchTerms:[]},{title:"fas fa-rss",searchTerms:["blog"]},{title:"fas fa-rss-square",searchTerms:["feed","blog"]},{title:"fas fa-ruble-sign",searchTerms:["rub","rub"]},{title:"fas fa-rupee-sign",searchTerms:["indian","inr"]},{title:"fab fa-safari",searchTerms:["browser"]},{title:"fab fa-sass",searchTerms:[]},{title:"fas fa-save",searchTerms:["floppy","floppy-o"]},{title:"far fa-save",searchTerms:["floppy","floppy-o"]},{title:"fab fa-schlix",searchTerms:[]},{title:"fab fa-scribd",searchTerms:[]},{title:"fas fa-search",searchTerms:["magnify","zoom","enlarge","bigger"]},{title:"fas fa-search-minus",searchTerms:["magnify","minify","zoom","smaller"]},{title:"fas fa-search-plus",searchTerms:["magnify","zoom","enlarge","bigger"]},{title:"fab fa-searchengin",searchTerms:[]},{title:"fab fa-sellcast",searchTerms:["eercast"]},{title:"fab fa-sellsy",searchTerms:[]},{title:"fas fa-server",searchTerms:[]},{title:"fab fa-servicestack",searchTerms:[]},{title:"fas fa-share",searchTerms:[]},{title:"fas fa-share-alt",searchTerms:[]},{title:"fas fa-share-alt-square",searchTerms:[]},{title:"fas fa-share-square",searchTerms:["social","send"]},{title:"far fa-share-square",searchTerms:["social","send"]},{title:"fas fa-shekel-sign",searchTerms:["ils","ils"]},{title:"fas fa-shield-alt",searchTerms:["shield"]},{title:"fas fa-ship",searchTerms:["boat","sea"]},{title:"fas fa-shipping-fast",searchTerms:[]},{title:"fab fa-shirtsinbulk",searchTerms:[]},{title:"fas fa-shopping-bag",searchTerms:[]},{title:"fas fa-shopping-basket",searchTerms:[]},{title:"fas fa-shopping-cart",searchTerms:["checkout","buy","purchase","payment"]},{title:"fas fa-shower",searchTerms:[]},{title:"fas fa-sign-in-alt",searchTerms:["enter","join","log in","login","sign up","sign in","signin","signup","arrow","sign-in"]},{title:"fas fa-sign-language",searchTerms:[]},{title:"fas fa-sign-out-alt",searchTerms:["log out","logout","leave","exit","arrow","sign-out"]},{title:"fas fa-signal",searchTerms:["graph","bars","status"]},{title:"fab fa-simplybuilt",searchTerms:[]},{title:"fab fa-sistrix",searchTerms:[]},{title:"fas fa-sitemap",searchTerms:["directory","hierarchy","organization"]},{title:"fab fa-skyatlas",searchTerms:[]},{title:"fab fa-skype",searchTerms:[]},{title:"fab fa-slack",searchTerms:["hashtag","anchor","hash"]},{title:"fab fa-slack-hash",searchTerms:["hashtag","anchor","hash"]},{title:"fas fa-sliders-h",searchTerms:["settings","sliders"]},{title:"fab fa-slideshare",searchTerms:[]},{title:"fas fa-smile",searchTerms:["face","emoticon","happy","approve","satisfied","rating"]},{title:"far fa-smile",searchTerms:["face","emoticon","happy","approve","satisfied","rating"]},{title:"fab fa-snapchat",searchTerms:[]},{title:"fab fa-snapchat-ghost",searchTerms:[]},{title:"fab fa-snapchat-square",searchTerms:[]},{title:"fas fa-snowflake",searchTerms:[]},{title:"far fa-snowflake",searchTerms:[]},{title:"fas fa-sort",searchTerms:["order"]},{title:"fas fa-sort-alpha-down",searchTerms:["sort-alpha-asc"]},{title:"fas fa-sort-alpha-up",searchTerms:["sort-alpha-desc"]},{title:"fas fa-sort-amount-down",searchTerms:["sort-amount-asc"]},{title:"fas fa-sort-amount-up",searchTerms:["sort-amount-desc"]},{title:"fas fa-sort-down",searchTerms:["arrow","descending","sort-desc"]},{title:"fas fa-sort-numeric-down",searchTerms:["numbers","sort-numeric-asc"]},{title:"fas fa-sort-numeric-up",searchTerms:["numbers","sort-numeric-desc"]},{title:"fas fa-sort-up",searchTerms:["arrow","ascending","sort-asc"]},{title:"fab fa-soundcloud",searchTerms:[]},{title:"fas fa-space-shuttle",searchTerms:[]},{title:"fab fa-speakap",searchTerms:[]},{title:"fas fa-spinner",searchTerms:["loading","progress"]},{title:"fab fa-spotify",searchTerms:[]},{title:"fas fa-square",searchTerms:["block","box"]},{title:"far fa-square",searchTerms:["block","box"]},{title:"fas fa-square-full",searchTerms:[]},{title:"fab fa-stack-exchange",searchTerms:[]},{title:"fab fa-stack-overflow",searchTerms:[]},{title:"fas fa-star",searchTerms:["award","achievement","night","rating","score","favorite"]},{title:"far fa-star",searchTerms:["award","achievement","night","rating","score","favorite"]},{title:"fas fa-star-half",searchTerms:["award","achievement","rating","score","star-half-empty","star-half-full"]},{title:"far fa-star-half",searchTerms:["award","achievement","rating","score","star-half-empty","star-half-full"]},{title:"fab fa-staylinked",searchTerms:[]},{title:"fab fa-steam",searchTerms:[]},{title:"fab fa-steam-square",searchTerms:[]},{title:"fab fa-steam-symbol",searchTerms:[]},{title:"fas fa-step-backward",searchTerms:["rewind","previous","beginning","start","first"]},{title:"fas fa-step-forward",searchTerms:["next","end","last"]},{title:"fas fa-stethoscope",searchTerms:[]},{title:"fab fa-sticker-mule",searchTerms:[]},{title:"fas fa-sticky-note",searchTerms:[]},{title:"far fa-sticky-note",searchTerms:[]},{title:"fas fa-stop",searchTerms:["block","box","square"]},{title:"fas fa-stop-circle",searchTerms:[]},{title:"far fa-stop-circle",searchTerms:[]},{title:"fas fa-stopwatch",searchTerms:["time"]},{title:"fab fa-strava",searchTerms:[]},{title:"fas fa-street-view",searchTerms:["map"]},{title:"fas fa-strikethrough",searchTerms:[]},{title:"fab fa-stripe",searchTerms:[]},{title:"fab fa-stripe-s",searchTerms:[]},{title:"fab fa-studiovinari",searchTerms:[]},{title:"fab fa-stumbleupon",searchTerms:[]},{title:"fab fa-stumbleupon-circle",searchTerms:[]},{title:"fas fa-subscript",searchTerms:[]},{title:"fas fa-subway",searchTerms:[]},{title:"fas fa-suitcase",searchTerms:["trip","luggage","travel","move","baggage"]},{title:"fas fa-sun",searchTerms:["weather","contrast","lighter","brighten","day"]},{title:"far fa-sun",searchTerms:["weather","contrast","lighter","brighten","day"]},{title:"fab fa-superpowers",searchTerms:[]},{title:"fas fa-superscript",searchTerms:["exponential"]},{title:"fab fa-supple",searchTerms:[]},{title:"fas fa-sync",searchTerms:["reload","refresh","refresh"]},{title:"fas fa-sync-alt",searchTerms:["reload","refresh"]},{title:"fas fa-syringe",searchTerms:["immunizations","needle"]},{title:"fas fa-table",searchTerms:["data","excel","spreadsheet"]},{title:"fas fa-table-tennis",searchTerms:[]},{title:"fas fa-tablet",searchTerms:["ipad","device"]},{title:"fas fa-tablet-alt",searchTerms:["tablet"]},{title:"fas fa-tachometer-alt",searchTerms:["tachometer","dashboard"]},{title:"fas fa-tag",searchTerms:["label"]},{title:"fas fa-tags",searchTerms:["labels"]},{title:"fas fa-tasks",searchTerms:["progress","loading","downloading","downloads","settings"]},{title:"fas fa-taxi",searchTerms:["vehicle"]},{title:"fab fa-telegram",searchTerms:[]},{title:"fab fa-telegram-plane",searchTerms:[]},{title:"fab fa-tencent-weibo",searchTerms:[]},{title:"fas fa-terminal",searchTerms:["command","prompt","code"]},{title:"fas fa-text-height",searchTerms:[]},{title:"fas fa-text-width",searchTerms:[]},{title:"fas fa-th",searchTerms:["blocks","squares","boxes","grid"]},{title:"fas fa-th-large",searchTerms:["blocks","squares","boxes","grid"]},{title:"fas fa-th-list",searchTerms:["ul","ol","checklist","finished","completed","done","todo"]},{title:"fab fa-themeisle",searchTerms:[]},{title:"fas fa-thermometer",searchTerms:["temperature","fever"]},{title:"fas fa-thermometer-empty",searchTerms:["status"]},{title:"fas fa-thermometer-full",searchTerms:["status"]},{title:"fas fa-thermometer-half",searchTerms:["status"]},{title:"fas fa-thermometer-quarter",searchTerms:["status"]},{title:"fas fa-thermometer-three-quarters",searchTerms:["status"]},{title:"fas fa-thumbs-down",searchTerms:["dislike","disapprove","disagree","hand","thumbs-o-down"]},{title:"far fa-thumbs-down",searchTerms:["dislike","disapprove","disagree","hand","thumbs-o-down"]},{title:"fas fa-thumbs-up",searchTerms:["like","favorite","approve","agree","hand","thumbs-o-up"]},{title:"far fa-thumbs-up",searchTerms:["like","favorite","approve","agree","hand","thumbs-o-up"]},{title:"fas fa-thumbtack",searchTerms:["marker","pin","location","coordinates","thumb-tack"]},{title:"fas fa-ticket-alt",searchTerms:["ticket"]},{title:"fas fa-times",searchTerms:["close","exit","x","cross"]},{title:"fas fa-times-circle",searchTerms:["close","exit","x"]},{title:"far fa-times-circle",searchTerms:["close","exit","x"]},{title:"fas fa-tint",searchTerms:["raindrop","waterdrop","drop","droplet"]},{title:"fas fa-toggle-off",searchTerms:["switch"]},{title:"fas fa-toggle-on",searchTerms:["switch"]},{title:"fas fa-trademark",searchTerms:[]},{title:"fas fa-train",searchTerms:[]},{title:"fas fa-transgender",searchTerms:["intersex"]},{title:"fas fa-transgender-alt",searchTerms:[]},{title:"fas fa-trash",searchTerms:["garbage","delete","remove","hide"]},{title:"fas fa-trash-alt",searchTerms:["garbage","delete","remove","hide","trash","trash-o"]},{title:"far fa-trash-alt",searchTerms:["garbage","delete","remove","hide","trash","trash-o"]},{title:"fas fa-tree",searchTerms:[]},{title:"fab fa-trello",searchTerms:[]},{title:"fab fa-tripadvisor",searchTerms:[]},{title:"fas fa-trophy",searchTerms:["award","achievement","cup","winner","game"]},{title:"fas fa-truck",searchTerms:["shipping"]},{title:"fas fa-tty",searchTerms:[]},{title:"fab fa-tumblr",searchTerms:[]},{title:"fab fa-tumblr-square",searchTerms:[]},{title:"fas fa-tv",searchTerms:["display","computer","monitor","television"]},{title:"fab fa-twitch",searchTerms:[]},{title:"fab fa-twitter",searchTerms:["tweet","social network"]},{title:"fab fa-twitter-square",searchTerms:["tweet","social network"]},{title:"fab fa-typo3",searchTerms:[]},{title:"fab fa-uber",searchTerms:[]},{title:"fab fa-uikit",searchTerms:[]},{title:"fas fa-umbrella",searchTerms:[]},{title:"fas fa-underline",searchTerms:[]},{title:"fas fa-undo",searchTerms:["back"]},{title:"fas fa-undo-alt",searchTerms:["back"]},{title:"fab fa-uniregistry",searchTerms:[]},{title:"fas fa-universal-access",searchTerms:[]},{title:"fas fa-university",searchTerms:["bank","institution"]},{title:"fas fa-unlink",searchTerms:["remove","chain","chain-broken"]},{title:"fas fa-unlock",searchTerms:["protect","admin","password","lock"]},{title:"fas fa-unlock-alt",searchTerms:["protect","admin","password","lock"]},{title:"fab fa-untappd",searchTerms:[]},{title:"fas fa-upload",searchTerms:["import"]},{title:"fab fa-usb",searchTerms:[]},{title:"fas fa-user",searchTerms:["person","man","head","profile","account"]},{title:"far fa-user",searchTerms:["person","man","head","profile","account"]},{title:"fas fa-user-circle",searchTerms:["person","man","head","profile","account"]},{title:"far fa-user-circle",searchTerms:["person","man","head","profile","account"]},{title:"fas fa-user-md",searchTerms:["doctor","profile","medical","nurse","job","occupation"]},{title:"fas fa-user-plus",searchTerms:["sign up","signup"]},{title:"fas fa-user-secret",searchTerms:["whisper","spy","incognito","privacy"]},{title:"fas fa-user-times",searchTerms:[]},{title:"fas fa-users",searchTerms:["people","profiles","persons"]},{title:"fab fa-ussunnah",searchTerms:[]},{title:"fas fa-utensil-spoon",searchTerms:["spoon"]},{title:"fas fa-utensils",searchTerms:["food","restaurant","spoon","knife","dinner","eat","cutlery"]},{title:"fab fa-vaadin",searchTerms:[]},{title:"fas fa-venus",searchTerms:["female"]},{title:"fas fa-venus-double",searchTerms:[]},{title:"fas fa-venus-mars",searchTerms:[]},{title:"fab fa-viacoin",searchTerms:[]},{title:"fab fa-viadeo",searchTerms:[]},{title:"fab fa-viadeo-square",searchTerms:[]},{title:"fab fa-viber",searchTerms:[]},{title:"fas fa-video",searchTerms:["film","movie","record","camera","video-camera"]},{title:"fab fa-vimeo",searchTerms:[]},{title:"fab fa-vimeo-square",searchTerms:[]},{title:"fab fa-vimeo-v",searchTerms:["vimeo"]},{title:"fab fa-vine",searchTerms:[]},{title:"fab fa-vk",searchTerms:[]},{title:"fab fa-vnv",searchTerms:[]},{title:"fas fa-volleyball-ball",searchTerms:[]},{title:"fas fa-volume-down",searchTerms:["audio","lower","quieter","sound","music"]},{title:"fas fa-volume-off",searchTerms:["audio","mute","sound","music"]},{title:"fas fa-volume-up",searchTerms:["audio","higher","louder","sound","music"]},{title:"fab fa-vuejs",searchTerms:[]},{title:"fas fa-warehouse",searchTerms:[]},{title:"fab fa-weibo",searchTerms:[]},{title:"fas fa-weight",searchTerms:["scale"]},{title:"fab fa-weixin",searchTerms:[]},{title:"fab fa-whatsapp",searchTerms:[]},{title:"fab fa-whatsapp-square",searchTerms:[]},{title:"fas fa-wheelchair",searchTerms:["handicap","person"]},{title:"fab fa-whmcs",searchTerms:[]},{title:"fas fa-wifi",searchTerms:[]},{title:"fab fa-wikipedia-w",searchTerms:[]},{title:"fas fa-window-close",searchTerms:[]},{title:"far fa-window-close",searchTerms:[]},{title:"fas fa-window-maximize",searchTerms:[]},{title:"far fa-window-maximize",searchTerms:[]},{title:"fas fa-window-minimize",searchTerms:[]},{title:"far fa-window-minimize",searchTerms:[]},{title:"fas fa-window-restore",searchTerms:[]},{title:"far fa-window-restore",searchTerms:[]},{title:"fab fa-windows",searchTerms:["microsoft"]},{title:"fas fa-won-sign",searchTerms:["krw","krw"]},{title:"fab fa-wordpress",searchTerms:[]},{title:"fab fa-wordpress-simple",searchTerms:[]},{title:"fab fa-wpbeginner",searchTerms:[]},{title:"fab fa-wpexplorer",searchTerms:[]},{title:"fab fa-wpforms",searchTerms:[]},{title:"fas fa-wrench",searchTerms:["settings","fix","update","spanner","tool"]},{title:"fab fa-xbox",searchTerms:[]},{title:"fab fa-xing",searchTerms:[]},{title:"fab fa-xing-square",searchTerms:[]},{title:"fab fa-y-combinator",searchTerms:[]},{title:"fab fa-yahoo",searchTerms:[]},{title:"fab fa-yandex",searchTerms:[]},{title:"fab fa-yandex-international",searchTerms:[]},{title:"fab fa-yelp",searchTerms:[]},{title:"fas fa-yen-sign",searchTerms:["jpy","jpy"]},{title:"fab fa-yoast",searchTerms:[]},{title:"fab fa-youtube",searchTerms:["video","film","youtube-play","youtube-square"]},{title:"fab fa-youtube-square",searchTerms:[]}]})}); --------------------------------------------------------------------------------