/*
Project Name: SPIKE Prime Web Interface
File name: ServiceDock_SPIKE.js
Author: Jeremy Jung
Last update: 11/5/2020
Description: HTML Element definition for <service-spike> to be used in ServiceDocks
Credits/inspirations:
History:
    Created by Jeremy on 7/16/20
    Fixed baudRate by Teddy on 10/11/20
LICENSE: MIT
(C) Tufts Center for Engineering Education and Outreach (CEEO)
TODO:
include bluetooth_button and main_button in PrimeHub() SPIKE APP functions
Remove all instances of getPortsInfo in example codes
implement get_color
*/

// import { Service_SPIKE } from "./Service_SPIKE.js";

class servicespike extends HTMLElement {   

    constructor () {
        super();

        var active = false; // whether the service was activated
        this.service = new Service_SPIKE(); // instantiate a service object ( one object per button )

        this.service.executeAfterDisconnect(function () {
            active = false;
            status.style.backgroundColor = "red";
        })

        // Create a shadow root
        var shadow = this.attachShadow({ mode: 'open' });

        /* wrapper definition and CSS */

        var wrapper = document.createElement('div');
        wrapper.setAttribute('class', 'wrapper');
        wrapper.setAttribute("style", "width: 50px; height: 50px; position: relative; margin-top: 10px;")

        /* ServiceDock button definition and CSS */ 

        var button = document.createElement("button");
        button.setAttribute("id", "sl_button");
        button.setAttribute("class", "SD_button");

        //var imageRelPath = "./modules/views/SPIKE_button.png" // relative to the document in which a servicespike is created ( NOT this file )
        var length = 50; // for width and height of button
        var buttonBackgroundColor = "#A2E1EF" // background color of the button
        var buttonStyle = "width:" + length + "px; height:" + length + "px; background:" + "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAANJySURBVHgB7P1t7DVLch+GVZ//uc+9d3e5e/m2dxlR3DXlcGkDWi6BgJSciFzHJhkBtqgANmDEIk3kQ4B8CGSYML8YMCkEARI7yjqWgHxIAhKykcTSB9mOGEl8sSjSliIZtkhKFN+lJZcvu8u7e/f9vj7/8plzzpzprq6qruruOW9P/+79PzPTXV3d09PTv6rqmTkBLohX/+lHX9q+Cz78uHn4cAD4lhDgJQT88JQXED5wEsTTPwagIcmoCy1yWEi26wjHKkNUKoCip+a8vOeEBpna9jS2xdc3Fjk0NaP9vNA4BHW5sMtEOPaD9TphjUzltS7V1esaMeNi6pnTfYTxWBEKVbXVIoNKnkOmNKaK+Yd/2LlFm6tK17nqnC3nqwjUtldQt7+PcN6H5Z46lbPMC6zMx3Y89rFd8c/u9O22b//ttx4fP/au7/5//TxcCQKcGV/4g49+JGwevvdpCB8JeCD7eWCq0AYEL1xIcpCPdXJtrYsMwng/SHrOTLpqxpoGSSTWPJmLydU3OpEzGAAWMea8keTPfTEbjqwOZldMrCLlFWUK+bPRfNqHxTCa0yx66gwA531STYgWwpQzlz5CgeDEg5UMgGNm5fnYDQC5raf75pgYkOmXY5FQ0xZ9vEwGwc+/jY//RXjz4T9/8U/+2MfgQjiLATCRPuxIf3f6P7A7fGmuOL1Jkb0AJyBAYURR4UKSkXhUUYVUktm6XFdshS5phfrc5A/pjKkK5ruijJhsaI/xOsTREUGReKgk2vuwqwFwlClNkNGGw+zpAmo3ckXfXFSmzUiIvf9EJOujzgZAScZyH5XGQy1hwkxwkcEInBFQcU87rk2WgXKWWticTw6M1ydzutR+z3aYPL0du8j3zz/Fx/8rvPnWz7z4J/8/H4MzYlUDYCJ+3G5/GB7xI4z7EgGTAYyoXVh9oEeChmQHEYriso4Qja3QpS4145jduR7mxpgzbIMn18Pmq0kVpFvbf6Kop9/RoLPULzxiz8Ua+jcbl5gVdcJCDhbCQ7GIhtSpIA5Fdi1DqtxDvq7+MerfbyzjpiCnlD8ZjMBcf3ZuFdpuvT89fZplS/NCxVyQ5DnagkJBKZ0kz3186uugFzvMqfhjT99868+dyxBYxQD4/Ct/4U/DJnx0d0IfYDvveHEf8TCJPeI8KA0D2zz40ZDkMCaKoqVB5yRMsivKiNmOfjKJpkJ5yMw4eRUrc7bHMrGqScjapim5dhqXFcsdqhyzS2XoOvhCkD492qFvwi1dk1JdaSJdAqjVY5Ip9U9sV9TqEfKWpR7LcqnlXrRcsx7XimQQmXjZBrI5haqoaLPlmrF5Qe5HlwGwGKJxFGq/DcyCw1w2nMcQ6GoAnDx+hI8sqYczmgYuPmLm3VOrXYU2COQC6WF2gxonZVV0NmBSkdNFdy0HWCYKITM7P2NdxS5grhmmoTIu7JoXt/a1cvNlsmCbeMSklExOLcBiQZJlHZdou67Wa2+UC0yxYNZlmVR9fa0l1Rg3E/xLZhYZZ/80ycBp3PPG2rFw8bYujdVkpzKfSXCSbXxeRcMGrW2yXPNjRlUeOPtIvgdjBCE6sLYh0MUA2D/N/9LDDyOGfzvNORD+TP62EEg8OWXZBwTDTVAzgEsFrBNkhJQsVwybs9mWc7OQbX5T5dfJMukU5E4iBpmOxCVOttZ+RIPMLGgdt13kIjIht5943XqQtkeuQma+l5JjTPN71FMl4yRBSUaOSlnuH4A+BF9xTQv58dSdOBDdDAAmodZowYr2MHWdzjMaAmy0an/dA0d+H3t8xD/34nf/Jz8GndFsALz6qY9++GH73F+Ffbh/xmHN8fHx8XR82nAjOypXPQh4YUOyceIuiuoEGEoyki7rZMtmdSYlspsHsFA33Lq1xyLru/4cOaYPjml97riuJlEfWVhkLvKQoCXaZhmvyjU7Xa9scr0AufeSOW6okX3YryQmq4zLQCCJxvOKsURx0vuQV2FpU2mMldt0yqjt52SOTJcAAArP30QIZIlg/3zAW32jAU0GwBdf/Qt/dufY/0dx2mTJLcSf5LC7koxuCBQGuFaJZcKRClomWwLqfdnqLA3OQqbnHONYvk1hQijihEuL0nhtqU26AGSNtkzwbBKSpQzuwbEebT7qqTS2/DKyXDDIiHpEuZVk2CReTzDIWPSocqsaEodMyUteRAz3tKye7gj5QkKnfG65tNzuyjlPa1ONAeDqw1QmoK+ukxGwDIiPPb715r/YywjYQCW+8Opf/Ogjhv/oNP3vLt7Tp08F8pcRPxQRDgGQMh/VIruTrPYPVjdoX2XOKOtBuxE4TI1zNmsuknan0Jc1FzQ4BOaGNAALxzYFlrEUXENO1eNAYPaxJGhriENGQOlUnKrRXA9Co//DKy81wGBkIElVDRtWjXJevU6ZUygOKtbnJRoaxpB3zjOhT0cFYd+KeQk9SvjAZvvcP3j7p77/34IOcLdpWu9/eGkX8o8e9EN8NBC/bnUtExNmabkq60VusCg5PWiQYRBcdVrqUgTcNwO66iqGstTi83SGje1JlJomVTkpl8mfA7BMusaxhJ2vv2FMziHymEyW/Yp+NPShnFyhqyCTR9kYuR5tsfRP6Xo48/OolGGclcYjZjv2fK0fS/2D8rMNxe9ZaOdUmvNq21yYprxjNZBqg7muecINSf9sIPzIc9/1l/4cNMAVAdg/7Pee5/7WQv5Orx+BnjmTFXQvZUKw2i2BrwSF/DWBx0FeREg2bnjLWZqUDlWSYlRhlrZ6yZ1dR6IVk3HiKakB7den49Dkrtlp0u0G4/l7wc4X4fRvSFJoQSfYeoyRnSKM937gSy1TVq+6el77OiTzvOPWkMEMliryB7sBriC735DP4xOYTEy/5/EI+CNv/eT3/zA0wGwAzOS/q//D+PTd8Pj6P7sjf88gCsZwcOS1SNm1EYAA9nuaQ+MItRVHbwGxuBnFevJ+jCcmT8S+DDSeQ6mflCUJISEemsHT5pYxxSrrARSPZMMaKlG8qevBqsbTv/FfUU8pUSMLUUdPoFodRv/qCGoVqoz7MvraQ8dhfOuUAgjlezooeUyidj1Nk5ouFH90SZ4nDcYI6bV4SaDVCDAbAA/v2f7oRP77JuDb8PTxtSjXcWcoJxoKx3Kis6LaucpQLqhF15ncTQ3oJU9asczPlv7u5U1J+ttlaWTD7CVj7aCS9JUE/P1EIwFhfUZbHfR6+cA4CGoNBj0rdamJKAHKEqEg09J+1WvjxWbjLZjIGA3qO9+HKsrRTJ38AWrnxMQIQPyR1//G9/3bUAHT5f7Cq//xRw/v+AfhKX+HR66Mu5lIbA5pZZgrZS0w62kJCZ3CW842m61CLrn9mmRyzC5v28tFTZV5PwKUXE9BTjicx1usJp9verUZoPvbAtYxctwthspLXvC5ZdikXI/qXQmHtXWphUrjsHRdCz/qY/3Vv2qZ0phv6R8m73TrtrwGmGWVrofx3tHyHW9aJPMLKvdgRX0heoj78Sn+iy/+yf/0Z8CBYgRgetVv/sAP/7BfaUQTBP5w7iCTlRug3lJVyUIp1MWy7+UBW06gpyuSe0otl8BVV3URBHZyCnLxxJaoaEYRHsMmGGRUuXxCrSP/a0RIV/MCd7Ur7pGLnz+yU43rPjMLh8b8PsDCcQrLsoXjImr3jkrGamGxAqw1RpR5Y/+BvSM2D+GvvvbX/40PgAOqAfDq73/0A/N7/ovnH99qzjtGMLi5MJ46ryHYPSRjcpUuBbEXGb8GWNbUaxbqcSPI4vklCHoBl/YecsJ6oKFb9mJrWDgendbLZ3H2IDesc/MAzjXndwCm04DFZhL09BHv2HEK95imPbC2B/1Fi9ELMCNEW26/uQILaq+n43LH3j9/fu1OYbQc8NLDw/avggOqAfDw/HN/a66g2fM/WrfczRpriudotoYg5qwPx4U/TbwB9Kc/ezckGGQovPNgdA3RomTNy9XUoQjxWyeHlEitw2E3VtdfpwHh9E862faLAAix3ZIMi4oLGsy2XV6wi/i681E8Jm0Ppzbci1p+a9AgpNXEjkQI7UTYFbVVkWgEMlnu0WJoy2wE7P798Js/+W9+FIwQDYAvvPoXfhiOv+bn/biPBZT0D2mpX8Ked8vDVvOoSxIshcAwectXKZCBr6NxkK9tGx0jGrFl6yrcsyFFhELS4UOdLgPUUIUsa5zkgkHGgf3QRe68KiMAoZjAdF5l6MMYsYm3dvSKAHRE4fLbPiPRi0w7E65waQOfXVZQjV7nZdNzmisDN2diWVXxVwkPBycjYLdk/9pf/zMfAQNYA2AK/e90/cik9OnTHuQfTTnMnLMMhDNYeEhrLyEYRfmA+H7ixSW1XOM5ZhkP0iEbf5kytuC7otswwGJSfEWoUWNuhrkDHAZnMOoR5dLzCmAoUqqrSYZBJw5C8tcXgd1dByFbAohDx3aDe6VrVmqAU2V8Xodo3Llguce61yY8ABhSIRbBmLwYAQ8P8MNgAGsAPLyw/dG9ul53U+R6oGIFUm9M1GOC4P21RABKMlyKu75ecNQVSplpoJ/+fIDdYV3hjmvsUhqiC1zGWT0Gy4QQAY0ZlqpRaY+KYEpaC/FtnZo9PdDjnq2/mEhzuvWrhVSYMVTVHXxd1GCr7uk1xhpaBOwtjpdv1lgCYOIKH3n9b/ybxVcDMwPg86/8xz+wK/0R+Ud9PDh2kmHg0PB/O1Bsjg+WCEBQm+H6qEz9TF0Pr+UeHOTv1o8+eU/fopyYeiOmghXo7JkZvYbZaDvtgwdoSqqSYQuFPEmA65qV2oNaLUYZNb80UGWvFI0aTOAt3LSy7tCV7q+hRhPV165Z2K8iiAenZbiMqnPRNBuVPOZgjgKETfhh/Ks/8BIo2DApPzwp60P+0a4yeuMwV1FdC+NU3UH1s128DNC3PgVrWMMxovHqdwiM0mudg26nJd4WJnN6MOhdu+OleiUgu5veZ5YowYoGKXJ1odqEYNIDqg5fvuP+Dy0yQTmKx2ew2n11uMAw3ler+Vnd22QwxkJDcUEsaFJoVGLMw4O18dLr73hbjQIkBsDnX/kLf3q3+cDh3ULN9NBAyiHZRvA7xtjGj43c2lads/LatnrLeeWJQzQv3fRD6HSdfOE5KS1YdJmrutTsWph8elSwZjHelsmOY/MBFR3Xi8AGTGOjbZrYrXZfVb4bddc+LhVHqLojcAmlygrnhIqMMlbl+3AFHCvfEfyf1aIAG3L0ZyEL/XuMgTJBB7YUzqVV1c3Q1re0QhW2UCD7Z5l/UGqBUqBiRGLmOMZTVCM8H8shuymCWowmyxE4Q3tm47SIHq6qH/P4q9NcOXJXi7bl0oGJEJSrt3okna8H+kTctV/IxvQiM3AKjnCXik4JhgiAhto+LhgHpoKe6g5z6UtvvPjWD0gyJwNgevJ/V+Ijj/OXhdTFGBT+mGxBw4zQ/ZJjVZZaqIYkITcC+kA5iZrZosGwSdH91gVTKxo6lnohc9q8Jnk4NlRgPXWTXK9+5AP9fu2VHXzG4ZA/WFxpaKlz3jmQzzX5fFlWoaJlIurcP6eoxumfc9gvwSajnRbqRS3F1jHzeWw24XvFvHnn4cnDD+9vHWSIPAHyhwLhS83kPGR5Gc6oXIPP5EoLNYQmgiuAUDlxxcVDfNCmjq0CQV+vywrACugT5+SMNM6rvD54L9qyCVzG1SCYkjQczvHaLp6jnwWn6XTtSgOz0bk9F7I5v9slc8zrrWoKqmOuk/nNwjEV5zRHVA4/IfwR6bsAm2jn5P0HTnfMYsjkG9oYdwRC8aU/UrIR7EkZC1VUP3uR89OtNhWNnm3MYCb4w9bhWEc6JHrNKlY3IGRF8nzbuaFQwvWL09ZrZZKzjDnfdWszZvxj5FCpQabkXBRkpAgOi6r7vxfa56/Frl+53ZXerVchpRK08GAvcreIFtWU6zEtbVuJ0wta6QY+wontDYBXP/XRD+9W/T8A6JjOEdyxRS2cFdRCHbz/KhVoKBfEI//Ei+yuKNMDjvObSTIPZfW6MUOFEdMOzkKPf7/BVLgbTDNhQQazo0ByBFO/EtZweqmYMTxLqpi/TClWXn3/nxM9G9jjujYNwAjy4n7sIXP3YF5XRR/VGn6dbo34HPmuKLRnf9oo52U1MWK78hsI/xaXtzcANmHzp9H8c6Y2saCkTZaRvX89F93iQYiJvIxzgo8nWLTweYLgEc4rXmGS47ysOX3pGjQoMF5xs/UJ3c53iUpxKyihXLgwdZ0feVsyA+eUSoRKsN5PK3ZHEFqAngJrNLBpPB7vqJCnzqpNEdPTeGyFxahz9GFh5VIVwYKCTJOW1GEeitncqCa+LLLLKNXlqUnM/wD+9P/q/TT1YAA8hO+c2Kp4IaChfSd59HKqEShVWAlrQVRLzhOtb7hUIFToMc0nOUmk169AfisYJeWlEmul+a04G27p+VlUFG/AlfrCAATfcxtuVGitu0TA0SBmE2ugRRoaUEKkp7lzUUw5mAcrhYtXhTw/LjNHr2txNKI84iwMXnldiKsSvsgYh8nBf+Ot8L+k6ZtD5uNH5vdLKTkHYf+kGIQKs3Lpmr+pa1ZZ77LqbKzb7VwRidVvZKy6V/LfcljHnNNRF50R9QCZaAPYl2/M9Ts6u5ocBbFgvJUsnrLV4CrVV92/ecG6ZxwsRoJTT4fbgIbFKaWpkQ4AOPutWAHOJEAxz6vZYIgzu0yTCrBFJOi142Sq4XT6dmT/LUCw+cIffPQjB49nDuamilDY50CNBWrd0S7rN1axKstUqKL83rK1zJslJWvKO5WeblAs+vwpzB5yT/h6W+c0S4jOUJ/3+wYWGYN4EKtmLFSrXC9gMYGNAGQpWZdYjGmLjCzOZjQZ7ceWk3Oh1VKDIEOpDWGpy9KevKwXQUzNlt1Aq+LMHnfROLDdCJwz7bqFtGgDZo3SVYXNR2ja5hHgw4BzWB4zq5OzQik44k+/7c+H/dUmr/20q4pjyxqaML8u1/dNh559UkGSIhcIukRSEeC95pZJuSgbGTfzn8fCMV/e9jBeDeLIhlqXWPWK7Q7FBLV/g5gW/HWVjJ+m29NGXpK3KO1XVYWHuizt4cv2QTw1FJ+WtxrZvYwE/mf7usFg0yrCEkxG3QfoVwE3uwo+cPrxgETZcomQDEEanlqk5jA/nog/RtECSmbgDrB4GH0rOGF+Xc7+TnKLtQEVQEO5dERAFBrHPFcpbmyg96bzyAebyLwEsHRPpbdUBVwa0kMP9Jg/ep2/hVDs/Rh7j8W3AISk64LBVUDDs0SmC37OMS2rC9FeAKXp6NPbjPjXzjrVGbScPhe1iInnv/TkzQ/EaZudBZCsCyDZUv9oJvaZ6FPC55vq8vzdvWsJjyhyUmEs5DOI+wBdBFJUrSNbC+plvjZOpCfZXu1JlJZcBrHYnCCFHtHDw10tdL9oURXprvy+PkdD0ppPSY0aT87HauTeMaQ8wdFOOjYPBjjnVhGYwldKxK4kUwvhllSJP5O26dXzDRehxQEN+W5TT3Ye4A+bXcQ/wuYxQBISoKF/qU3cyVEPXw4/MgqrvvbnjA+26kyUYyGFT6uv01rcWGuNU84SI7p4uB2VoUAmBCyNT9dDZV1fn60MzWYSqr/hQOUFRIOeystIs+fRnt+NJT1nMMwbwM0l6r12ErTPOr4WNOoxXEtu36q+Op9Dy5ezEoM7COPT0xboiF2LQiBLAIgvlYoFYQuF4/WIfy6s6TXKOlVr4IyhftfP2qi+nh0KaTR8pxfuSZKVFwflBGqw+qowrimWhYwq2ybm5ml9FaNugsVoy0cb/6XNzuR1qLhdj6ojMHupgWP6qigW5sV0x9Qeg8LmLg/CfilVRXWb+o2fsmHTqy4jHvED8eHOAIAPQCW6TSarTSpeWG543mCRUx11nm0shKo+dxvG13JdU4slQzYMe7fb/ACgzVsqycSRDfrMhu/U0JRkQ2VYOqtevsvOPJV2BmaXn5IijbC60TrAg5LQ4Z4Rr2NgU8uobtM6E5do2GinhnJJImSSCRt4T5y6gQZUdRNCatLOaW4l/iwzormYzyycOR4mKoRzT0qOKEFFw1BYAtCq4UqoyouosVzA1MzYQeoejOg2p/g8s5hMuoxFqy2zCvQogVjtNRmiGgI/u9Awsh5x840PF0pE5ejnkBTFU3GnGl0YvQXUghV6Fkn5/kNdlSnPsuTFK9rA4xkoKgvHt1Cjt5yDFM1FeAFqqBpMBfAMpHJTVpzp2O6p6FsJ5qbXGo0+zGuuZfRc3gD72DOEkzESr0MQq1fBLrfUWJygtidAfo9h16iFA53qiHsq3sbRHBF0AuKUL4JCeQ9QPdTagkm1QSf/Na7fWeaP6HywxQjHQjLylRuwwdndif/WQOzx1ysoi+gJzvJlIamv7degJ6F1JCwg5xCYQV2q5iJLBo5wGKRh1f0Nusr473tdyuLL+cUpzd2LxsQV7dBSC/r9FDDpMbG/Q7JRZSxVaq0oDZHWsdt97JfPG6N/qyPKXQv0sdRj420OzFQZOKbmWCIAvKLNkhn/NSAuHv8ebv/RJddbW1eFiRaH5ZbvR/Q+15X7TqgzPjcahiy2qMbc7XPvmeXj8zkZN+5ZyLK8Af3hqJa7fjZUehYFz52X8RsSSPbNY9NUFxnAetzdVl8Fw1EjLt7Ww9FmK5rsrnVmTUOVfEYtIWvBF42Ue1vmrD5pCYDVh6nHHv86CrfFWAYX4o/1NQH1LHSWkRQUi8gC/i//gbHenreF/zxPxJGNsc7tMqmz3DD+uymZ6k9d5Bk/pQocC+fVJCtnUENgVVjuxcquZdfGkS4FWH54i/GYSgZJD8exJMMMAY70V7+GZ6rlFH276FdfGdSuySdI59A4gqqI9oFD36aoJX7ya35lj27ZYXoGIDC96m1DECYAQVZInYMc/b/8F+Ti7qiH76amTnxKIFb3zHienqYh+O4ma1OTIh0nQM8E1+kWiq9V8UysBjRKNTmVW7s242kkx1D8aNu66MH6kQwTcIi31oB6H/QwgHztOY9h0wjzKYWkSCjItCGohxrsbwFcxEpTmJm1ObxtPOo3L2rz3kVqAp2pnwJtgQX2ts3kH6Ji5tIqQQu1ebqNWiZJxfZkHRbPHjqiF5ks4VR0lJL0lFFw043UZVsmyKXiF4kuZwho8A8S7lwQ8UrPT4N+7vM0EbQI2Von7YigJXBEB7iRv87pWIw1vuam1wDXg+F2Psu6SS4r9e2Bk9DoOXqGgUJqp6ocnWGsuszhiqIAzlPsdVvUDQorRSUwM865vf9USWBTmbFfxdGVhqfHeBOk4qakSwAeLYwyLtFud8l1OTxojjQmknQvbZQq6gnnDRSPy9m4qQowdTXCj8DKPEaQigdG5pK4QgPAwVA15bgi5sl8QTIBhaKJIJdWxQu6vGF2IyhpxBOsryaDtPmXtyo9ZIMdOV/++FGX60NFOBUjLwsKTo+FoKougcGNd8I/DjkNEUoGSROvMgv7nEywGdxokLkIakkT0WDAoUF35fyQIbi8fE49a3SzduAFDDai7koMAATTDMOKWWcmRt60OIpizsnz32/RdDmj0svs3IT+HmYsdnoe9HjcjashUugd4033BCZV0+0aDoUZlVF3i4g61Coj9DYitYT2681KdrKVhPVEgl4jwUhMmPojcwuW6xb/K6GhzSgeWAro0IaA6RGSc96VWG/MCCLxFMdGAIJRkTXPoe+CBgAd7gbx6hBkQa+Iwu0WwO7ASup9M1cDrPp4ggyCjEVPH4ROapczcRs2ccF0xyDr0cshsLuagmDtsmqSJDLWTrQYE4W6Akmal+DcONv9pyGIJHlKLi2T4emfcl2OZDm/3XstGtye+6wbgtExLGkJ5LhCXS2xOPRdwACIp13nhe01DszV5wJBkLM3DdldF9zlUDCgGLkjUu/DUaXZtPcispaDkl9IoueYqDot5RTOtjhzrQyxeUFMWWzNykFnGT9WQ6Ikh5wMvwaORQmtBDSGRXiVfhlMhjffnEJ7zNem4fq7oVugeJRRh3PX+8w4PoretS8OU8l2jgLGTuIjAPPd1otdSy2ouKLWicWqaJkNy7IMTuurkdVtOysr+fu99SJM55x6x7mHBboSdxjRihI5CAh6FqfKPDpNbbCGhxwM0nALLcUqYv6W8VPl2VuLpB+mQlZ0xTGn5XckqdSg6XhGVcZba4FyBKBRhbNcbw9SzsWIF2abwT1Muo0r2brcsILdbyT06UZSlNVX044IVTGZpfYQGgxUBxH7y7aUS4XiELKpeFdrfQ2Q84MowpGcZIUnKYn0nsEdy0b0x5uQU2DUxcsFgwwWi3lRMM0tgtU1pHUFm3hFO7LIjWkJoCF/Ffgu9Co2SG/s67Sf14nKqinLsmRjtMz5CABbq7ClMxrd5/4qIE5yjezXoWmJntWgKK+ZPJ1tnae2+Kn4YFFUPcgtLQK/fhT0QGkYdDrPUHOxWitdxKmnbHfQrXVV3gTKdbEisOo8Rj5fZCnoKBxq88vazTapOb8i+rPiXDd7ymmEai1UnDsLLJRIvf/4OTHUVTFNQUsTyukCCs8AUCMgTkdmvwGqmhavv71tAaQpYcU7QwJmO6tUMd+Q8Xn3/cohwmU+LsXDx9U9243JRpVR5XLy4011i2cPBple5C8msgjCvr0uC5zzRtNwyAsHdhsMaiyRw4oLXs3MvAHEkaFvVd0ArYj1XjMCT//klVMG9U0zFWaRU/yyrwFKQQWkQp3qcgrGAzW/fzztijR0Oh0Xahbb0D0NOgefZ1HM0gpbOJoacrOF7ruuxoav8YGjUJbhHIdQVmCqvlkuk2H6COWiGG0hOq5G9dq4Y5ygQSY6iqfFnEq09qC1mr6wNI053N+LaGlav/ufEy2X81V/8vzn41DZ/SHW0gHlZwDOiKIV5qYfRgeko60oj5yG5JgSiA2WiUCqNUquNT4qu9F9rrkprAu39EcmY5uFYskTobgvqPEcPXp7Blcilam3xfRTdZi8Qc4NjP5Nh9lyftZxUk6ywWKUGQgnEot3A81q7duu18bR15EonU/6ftzYYYWsNk5n9ZgYbytXZ0B+LWZsmnlWqS/ZR7K/KqITjk1qFXKgjYbk/G3pMINWVe4j2ZAeMkWxoMJxYS9wV3BLGJy3rMJsMVj63jop+ciNkxZH96rXYYUIiFCLbXlqJTfYwT1spnBZ6pwNvoqm/AyOvg5pNd2GG4oHILensTcx1qXVFE7iNBrAKDPU1yokt3eT6OltkHGEj6UCrV4/YS9zJ6ZeRhBFeneUAVXV+Y2O04ANuhyLVebWuUXQocsD63HUde15iM0vdwRKJVYct/7hdoQvBHEyxsNyJ9p7p+L8V7IZEhAved4u3n+v6ybocUQohAQzXDSMVkUrhM+kOg16kMyp9dVWkDv69GwyXUj+Yh1aerzvvsIrTKidjL3TGjEAVIesTDYDGrN7e9l5R8W/se6rq/dsydvOTMXFJEmPu8VrzDWNYyMw+9yt6lTLVyAV9PF4NU4zBjtGS42oGJ9YyOhgmFIvOTYC5rRV4famHUoxTzXXUArPoTPdXGeFXtSV1M2MFTdV8OnZJAqQKSddMc4QkPRkhajV0AOkQWa1uWCAdHKhW1ebTAZRtfvUQTT1rurfV012CvIrnG+MoGfRsByx1/s1qbc9JCAxViFvQnqFSaanglZYI0uGdoVMi+X+70xuXEMUUVEAU1XcNSypONtgA2ddQUnGTgYqq7xGGZajDgY9gYgj1J5KRammCAAV7nW/9FdG9GK6a6pKF4w9jQOHN5BRt1Nvv/FSIHtUP5X0dAMbOg2LCUlKYPa6wDH5m3QZg0T0I1Uhk+APlcQyjPOrSSaTk/1835htC5eyekriasNQObLqKJY+r21AgUpyKDQtnP4p6HVes2CQEessd+bhZ+GPvJE/CuHA+hfuaABwXjmXFoPmS3mGmcsNotetHou5qTfsqcDbNuOsbpGN6zc2OSaKqQzvhaBeXVxvN6wz+GPCSA16B8Oaa+kB4h5aSlRXj6akaoRaGTIaiQOePnPdil5GrJOUFD3F69kahXB3XScnES35glAQDzRl2a4o48iSbpvcAL8U1CUAC6hhwOUj9CcBBzpOzjP5J+RoRsfLXWWAWMXx9G/sOVYqWxde+0tPSKI7LqQWQ0mwACexYzkznnCK4qxAMCXZGKcXSaa18t5/8Ywd6ERuc0sdOoIjNUErCTvJzTxoC0GJppmyts21fRV7SWIe3wyUSxaArmQZcoHK7wBcghQEA8NF/Lyw/owTOgcqlqrLZT16u4jlHlMa8Vjk+l5pdLQPDX1oJ2NuwmlxfHQ5x7Wy6nQYCsUJJyiFrZWoxdaL3uxHblVY1dPZPeA3gJDZr/6+QZWMFSbXXV3/t12NyvGooXloOkcdiValmVpB6NUEFZf9EqAZQozFRf567sl7isauj/68E4wn3NR77X+p5BTyR16NYwmzP4qV2z1tjErU+4vGUt1+A8DXysUjTrdpLvgsA7uNpcNyexhk1n0+ZQWUFrkL910AyxcOqtdWSHLnG1pxYKue2WgCdhBBU3J8zeIfVPPBww/16m7EAIjg5VmHMCWG6uFXO5DYhjhOGNuGWWAbokALi/VAaz8qJdJr3WNyiASt18EzwAxDJliLOaIJpkQ0yDRwFNW8vJarCPVAjyWxUgSLRDOQ7Nt+BbCBLJJ8u0GtI5hyVEO8dF4uWIxf0E8fZwE0FI9mFpTvTR3ncSKu0ACICY+Qn3s86AWSNdNk3kbwVVaakfyqFjhma318ipDWVpsmll5lRLT7EP4ojwEmdZbxUm/cFI1Yj+qqeagyksBqSCNV+2FeiqVaTrpk3FiiJJ0tEHaJqjWa5Cre+V5QasD+d14jDBZ2IXuONcbRRgBOs+XMbdGGMvSGX4kBgJCTLubZbp1GiePOgQgbh2UPr9VtiFgNEEZfYvjEA9faTmt/9ZQT+oXxRCkZntYfT9e8l6U9V9JLn89NR1JKjXBYngFAvqinTW4ZlHNP4/J4Yss4tam2IaS7mp7KU5RkUCpWigCUhglmO42wGEAGRHNuqRqprB2B3c1kGufuw/wSsihHHSxLNhZcfQRAsXQQKok/L8RZ1yE6qL9QFZGC7rC2PhRTTZou7v379dNVlWAteBJpMEzOgP14Zg25BoUZLEYXU5htjL91sRrX9SMlupB3MoEoMqUKgkHUoOZ8iCqssMCWtfFg6J6KXkEuAZX8OR2dehfEn6WeRfWWh+q6epfbNJU2gbNtGXZHRryprjyHvWcTD9hbuWFg2QWo6Q92oFE8FZJDVL1xhlmKu7DkKH4wx9UijzPeYxxQOcvQwTQKoBIkFuprBhNxwIJMoU2nc0JjN2d6LMRl1ZXvpnA8+RVFADAtDaZnSZrOpYCqa2ZROX3bAKNjXdqi8YRaQ7MhcpdGUMvOp8lb0Rxj/qAKm9x2if8sQCFt/guQ18GIgixiawNfiF3bnkkf48lyTfL3qXMpbPihELmo9cYz1l3TxIbLwUGOALTrTirpBguJpKQW32kMdRr0gV+uly6jU5Qb8j28xGKGU4f9hNXWW0gJrfdqBVqiNqgrDl3vFWu9WrmeA16jsl6TCfqLMNiUJ5jSHwhpc5bFkmnBQVEcGmRJP2pOyMqvSf4G3bXkPzOZqUhueFXdhCc1HrfYrbxRJsXcRXlXWa+NsV1d55CjMqsDAyk5phGA1FAwK7Ql9gHamuH2pixKzehgyRBdmvvE+5OZik5AU5IZQT7cj1VcxutVoNMzB4FsZYlSfaUbIgjlwJ4OkwHAuw0Mc5aVnfLjvwC63VCFvMFUJTUE6MQYlBuwXDdR7C3TEw2q+VdUDK50NljEGlY4df+UkYyNtS6FaTbzGI4lOWLKoHFJB42JyCUYr3mNDLOEEwS6V6eP6iY6B0Yn5pIIY44P9KlG0LLKvYDJRmtJ9bnVFqw9X6Nx4LIhsLW+CquEwYYleiT7nAwny6H7IEOTxKlpuFicMR/N9O8bS8hX1AIUDzwFRbDLG0fvv+o+wkWrGR4jqdinVrLiq06jHsa4sxVFeUtYvx5YLRCcipxylbriV8W4CJ9tFFY0spedY9DBOS7LscUY1/WTHXv56vMLyWbeTSmjoBwLebXkqSntbBzsm4nesVpAqV8q8rZQ2wBuf6WJzas4von2D3xh7vn7ib9iHdU10NeZqKSbjXqMrKGQK/MhGhdh8wI8PP9+CNuXYBP9Temw+9tvBwaeYeDTLwO8/Ro8vvmZ/f34+OXf2e1/GnC3xdd2f7u8tEBBYQAjMWAuzJa1RLCQFY3V2X67QTFaXGRXOicD8PRPsTlMT1bUZzn3hj6M4DMAzgo/6Sd2CAJ5FiD6iUbIKN3enl7k3wSrxZoL0X5aPKq+xspE7g8vfhAennsZti98YE/8AwMDMsLDOwB2fw/Pf/X+ePMV/+MkH9/4NDy+9rvw9LO/sDcKHr/0O7Iy9/Sz5nxFtTf8fqNzHkpmPDTIWPSIuWk+dT4LjVgB5fO6YgOgjIz0YTkIUWYQQok2WKxepYwp2xlzbRhD84BMrdbCUojxpnvYEf32xW+C51785kH4AwOdEXaGwWQcPLz0of3x3iD4wq/D26/83f02FQanp3xGrFU3Kokr1cl9lvpk3CDHM4ULY75mfU7oSgyAmJ51r1VLy65ClFC3BlMdL6rO1hGg6XUVXKzSo7ZkW9WizfPw5Cv+2I74PwibJ++DgYGB8+BkEHzNH9sbA2/93o/D4+d/HXC3bODUBNnEVO2k6HMUF4Hshuw0onivyrto0Gs7p/lYrQdLdYEBN7sEQLsKhTyd+E+XdiYz5iniNpRuCEOZYrazjWbyV24t5B80CiVVxKCayP65d35o9/fhsX4/MHBhTMbAk3/m+/f7T1/5/8Nbv/vje6PANsc4Q+klPYLoHCZP5x9FjxfI18jnRYUaeRSFtPYlDoKkX419VBDb+tY+rKvnHqIq50pGwH7/aHDGX/NrtypXIP9moO0ejOWPkPrD1E+kzs32PfDCV33vPtw/MDBwfZgiAtPf3hD4nb/GRwRccwn4hJGvanY1ZiNAnn/cjWtAQ0ieEYx9pZwpG8+r5noVqjREAOLToMGbeNsPtDaupnmNZWldZ49fSKoSavX8jUVmCzsrytoOy4C11DuF+p9/z3fCc1/x7TAwMHD9WAyBv7szBH48NQTcU1D9PJ9G5Xs4aFU116EQ/pdqDNoadctyhBflCEAOiSx4zetYahzh74mMWAehS/215N+prk7yKJ0HWQY7dKHB8j329ZN3f/vu7ztGqH9g4Abx8DV/HDZf8U37ZYHJGKhD+4QYT93qkmO3ybfDOjna9ATG/RIjAM2n12vJ5mgA6CvxdVXU2IvSYsM8Jmav/7DhvxTWjN5hMXoSK+FQRdrrejgKiwr34f6vnML974eBgYHbxf4ZgW/8fnj82j8Gb/7mXzo+HyCgFLF0TmPpEoAh6iixB4oHAtabb6l6WlP++h8nVYsOhs0RG6rWA82S0wwDi759Bx5HTSCduTxI0iPsj3wDTGU9oi7loI4uVpoXCsK2hCe7UP87Xv7fDPIfGLgjTJGA5//ovwfbr/ufH1OYeSNwCfXPfcXLtDbHsILcVuV6vb743Obj7q4pVuYVsI3LS963dNE8EYDA7GeEf3ygby8TCS3evnHd2gTBqjV1pomR68pR+YaIBL2e876GKcz//Luntf5vg4GBgftDeHgRnvuGfx3Cc18Fb/32X8kFtLlLnI9kNohzvG4Qr4ipSyMjNCn1yZBlVYBO52iChRRsptZmPonUw9ar48g8FMrMvnby4QRqRcVtwUWoj7eftCJNcutYuwxmzSzKc0kIglXKK55C/u/42u8b5D8w8Axg+3X/Erzw4f/DfnlAB52obaJ50uFngNFR1pyv5akPHFgmWSzkoK06XVXlOdQoW5AtAcR/KESHtIgBRFtKQhB7+bMhEO8fCyBH1M1gPH7ks+TyNZ5/xTnEnWkSPoAacnTtjcrHOJD/94+P+QwMPEOYyP/5f+7fMRgBTr2QT2Ezp+hTW885v6Qy1BNyomV5rLqoojY0UBPdMLR/E2JBhhQDEqNg3sfFU49J/GQ4YL6eHxsVB32xd4+ZIdEPqB66y5vFa8gfrdEbttrUoDqg1J+bJy/v1/vH53sHBp497I2AP/rvweYdX9/mhRMxOv8gouCUGBWuUaATIUvcFQrlmhHcGQn2EYC9z40kLB6W3fmKzUS+//DOKTMN21MLbyb6uZ6QefeO8IkLtB5Yf+RhU0WMHrsg5/3PWwS5TXvy33n+4xW/gYFnF9MPET355/8d2Lzz66EOpTB5VBe0EqPFobMwCXYh5DlmfZprq0P5lsZYzv2YYeiCzawh9sRnJo/Je5ZZvuLEEcqB4JF49hiV46ykvhDIt4r4K8zdaniU8IH95DsJEBsBQth/kP/AwMAReyPgn2swAphpJgj7ZgWryfRHYgSA0Aq1aRZmNBo20UbDRlcZnwoldkjyqDcqeafrkb9C/FXkb2wpFhPsSsxFg6rK0vJ5zX+Q/8DAwIywPRgByzMBDsINYg4sjqBaO/RBRMNahcVTK587fcgamTyrrn4y9n7cgAOa2svYXIWaqxql2m/Gsk55l5HCCB+T8jcxBM9/kP/AwICAyQh4/p/v82Agdf7O/gBgM9CcO+8Hi3B1niMCYIDLANCwblifQ0yEhEhdhMrpqxGvHbzY3nnR8xqpBUpt0gP25D8e+BsYGBCwfzDwg//bvTFgLsOkzXNSuDpuR0N2UIsGJS/d1Qp4UEVsKroZAOsDhX05qU63V7zyosxPUpqLp0LJkgqmb2osJXLFL3zl9wzyHxgYKCK84+th+4f+FYvk/NiYiuopusqZO6L2KXkDWVOPX34DIKQFKuuzxFC8nXXFBgAKaaiLuOto8fob6q2yBjHfQ5qzZNAqps/7Pveu8ZGfgYEBG6aPBW3f9y8dDti5T58ULZTV2ak16rVYK7Y1+bgHghYW6LKGXuI/g7ER4cojAIKLjIxIld4W1OrArtXvw2sAQrB/wbTuP/2i38DAwIAH26//V5TnAYKYenr/H5bX5GRUTojIJYSlEWgtR2B00OgP0mHtOr+pDNLKm3EFBgAKf1E2QN55VeOlJlQPwiA7B5b+SCxpofr0Vc4UL3zV946H/gYGBtzYvxnwR37AU0TlLaEWgwzjCJYW4mthVIHC0iwYUqmmMoKhSNUSwJqExrnrDNFLRQNTvKoNkiVRKHbaN7ZZ1YdNhu4pKhWkdSde+XPv/BZ4eP79MDAwMFCDzbu/ab8ckEOf0Og6uRuldXPkEg1lO8D1dkMxMqC+QyklKPps2KTEyLnZlDipHBZkA7iJE0FvlllJvHUW1QZWjUKsOom85mN3UttIwhT6f36E/gcGBhqxXwrg3gqIprbAJKs/AHQqpIY1/VAJ1+B4FsDZHtX+nXT+Vc8MBFefbXjNGslTOSjIIl+FVGU9T4LcloqiWYa3YdH5u89pqS/5vCQur9MEQZ7iybu/czz1PzAw0IyJ/CcjwFVm+ieamviotTJBVs2doJNgoy+3bNLfNgihUrXpHImQ9rEBVwRAIt4aIkZDvsE+qEcHpbVhlwyhwiRML0RiZVaMrMn7n8L/AwMDAz0wLQNYPhBEHwCk6SdI4e/VgHWEyyAcP7J+WgKoph00JDv6qDoCgMpW+qMVW2UBViL/yqIYH2SJFQrbkTRJCLNpdU3e/8DAwEBPPPlnfwCkeYdSB/d9km7AmrxeD+QBoEmufbmBFURFxhUBEHSIxxOkB/O6EnoJJevCUTxJBGg7kfkieHTknv+81R400Qbg8P4HBgbWwPRA4Oad3wAlb9ruFVfO37bECr1lIyH76FqVMQJGywiN5SzKHpOjutcAV/fmNcUtnrlUvFHnrCP+ul+bpsP2qDIIUtrlHt7/wMDAWnj42j9+2IkmoQBZEghfxrHBvXzKNGaFig+BbkzcRfUDQGpVJQvKA0u0IaX8G/gQ0Ly9VuI/6mlc/+HWxkIiEWfplQ3vf2BgYE1MBgB9I2Cm4GW7+w87za90zssmzOiJKXVab+QQmM8vpMsbrNqQbGrrc+U5T++KDABK8p3COfMf//RJB6Cg31AuakOiAheLkgv9l6p6eP4DMDAwMLAW9m8ETJ8IZpamtWXLTNiUZ3lHXhdfA6UmJhZDT+U1yx+n7B5LAN0gkX27lcand4gkxArjp/zd4SomNfb62bFja/t4739gYGBtbL7qw2y6b3YVpINBxo02Pemjb3jcnhL6wm3E1D3cuElJ0XoWaDhGZZ+mdUZ2Kr3rOOrr+dLnTPiYP/ynliOYvvg33vsfGBhYG5t3/uH9A4EUS/hfYZXSdOaesi0FgiHqIM/PmGs7nCtKpXqFIjrw16kpxWcA0PBH5UHIl/ZXQHGkdbYiPfaSscBpQLFlbRhr/wMDA+fCw1d/a3K8OC7hdCzToOIMVX0Fr5TfhwPic8IoLa8LDQZHRZ5UoKhvamW3JQA3A64H5BJ6t+8Y8j/uustGoNEteY3fcQ5H0e0uAjAwMDBwDjyQZYB4xlrxQXwGhtpq6QDzQxr6r1ONruR2sEsANwAaSEBmnxXuVT+mdfoKJ0exhx+v9/Oev6+KzZOXR/h/YGDgbJi+CrgRvgxYPZ+1LK2uQZ6BP9wHKkIeDWALrYr6k74+A4BbUeB6GGMhut+rLZHXX6dATOFeHbF+4U8Se3jufTAwMDBwTuQPAx4mJe1z9SrW8oA7cfL0fAMXta2avw21lVF/Ylu4NDiClx4zSAqgJNChQX31ZN49MlGA03F9/dsXvwkGBgYGzonNu76BPCG//KujKe7pR+f1eJ12z3luWF3VZSIArnA6Jfq1OhbT3aZ4PGYpE6jXH4AGOZyVYlrnw5MRARgYGDgvpjcB4oCtbf60TrJnNBAkKJ8hkAPEhXbXRpalYsUgAF8w/TVA+lfSU7KaNL3IyLKFQSnYG9ioHk9Pv85ISJ+Qf17WVAXpr2O4bfPCWP8fGBg4O6bnAOKvAobQa5ZGU5Jt/b/B2CDzdvIMAFS0BwFq39t311XI2xR1S+QdO+NW4yFTTvfNhTsB4bTO38nrR8X75wbMYUBhIuur94DpAcCBgYGBSyBE3wOQf7/kJFGVVSxQ+9CBq86jwwUNK+/mgpYvALY92LDhmZ0jZwbmME/JkjAp64RjnTPpr/yuSvJ5X7YE5rKm6lJNmxH+HxgYuBCkNwF4WL1f5+SMSn1d6GVx1CT31a6mISLhQaGuLZEE/jh29zka0/KuEEb7xq9sQfyFKO7VP62so5oTNg8j/D8wMHAZbF74muw5JpnCLfMewyWVYe6mSV5owjy/819tLUCjyTPTp+MtAM3OcdtAZwKmu23RkkSndg3jdX8p9O/uq1OFfLnxAODAwMClEJgIgPb7OGVYBXs5nX6jpJpOuvxC4kmZnr1vstxHl38NcBUgH5Do2O+oJGrxEaV0oZJuFszAwMBAX8wGQMsc27L+r8q0Gwnz7xvM+6ZqC9pc8BBOJiIL3MaXAIsQWL5rQGJZ+aEPgMwef4A8JIRZ+VryzxITbB7eAwMDAwOXwPQWAAKuFokXajXKlJ5jKzcMmQR+Nu91kg49oV7PjRoAhPDniyj/LFNDPfllnlNCdMA93X+QcViNcQVJTQZsXoCBgYGBSyA8vJg5RrkQGIGmJJMHDA2cQJywluaneegvV1tfodW3ZwCcfpAHIVlLEYIAlZVAyWqMHwKREAyee6wzF7WbDWEYAAMDA5fCFAEoEV9X5yxCKNTbCZRiigaPF25lwWBs6LhSA4B29fGPjrBez3/QehjEr/PtNwz5o3JkboKeMDAwMHB7oBOoTViB4Px5VKFFqKyqKwV1L3ATSwCEfDFK43jZ4Vi3IB6ze4sPl/SgyTc/CbOmuTwwMDDQHw1L0StNd2gwDoS5NprrSVLm/HWZ92s9+WKnF5cAJHal+z0Quc8njx5JEyIjYFUORCiRLTIHgZTm9TmaEG8H6Q8MDDzTYObQ6mnR+pAgqK0JviIFtHvtPlFdYJuQDxumiffpB3+CrcK9aODz17Q3WMyV6OsH8xo/LcmfeQ/Lr0XHMBwGBgauHehKbq8rlEVKHLD/w+S42ls/O8pr5Ol3AFDRs8+f3WBceoLWIS2MYy+rrgYxdcdpPOIH/DjS70L+Xc6fhCUGBgYG7glYSBTn0WBwjLCpajdMjppDptH7n2D7EBAqx1iQvTiyOPse2doOHoMUmJsLhgUCe1MyM7LxScar6++BgYEBCutE1TgfWuvDcl304z+nQDZqOivymFqbZYz13eGXAG1nnq18BMi+7TxjWbxwvAtarFhMsCkZxD8wMHDtcDnhV2IkMA423Q9VzbK02xIpbzj3x/TwDr4EiOTvAImoTwSPJArAXPTUGGgk/6qBzukZ5D8wMHAjcE2aRFic57CQz+iywsDP/O+6FOblmXQEnV2RRJgJNurhrUAPzot2FkL67SCB9JErWIPMLkmNlCplY71/YGDgVmCe7qxzo1Wh09Oeocyvy0OBwoJyzQeJEOrm9E5Gw5UvAdCzLK/MF1UAJA/4xZrT/V7r8p1CVb2txIGBgYFLYs05rai7HI6X3nXjIwBQUIeGrE7hf+3UyBLAFRoAticpAklhn9hXIi6ZLCgFPOg6qLHeQhwYGBi4CqzA9LVke8out4m6mmGNAGxC1p2ebXC8Fh4tAZzLxaSPUyCzryMOxSSGQKSKhvdR0eWpW0SmooPOWNXAwMDAPSGIBw1zXod5nByUPxUEVr/VVN9qYNRvqVeNBapMfW6pliDsS/XYT1x7i457uC8ux+03h/ozo63XRcRB/AMDA7eLkqetzZvVznDh/f8Koo5ZLBazRwM6T+S1xgaDbVrWo9kqi0yur5Ux6Wfr9ig/GcC90rfexRhr/QMDAwMpKplcW/9uCf87GpDwCsrPBPSqr4+MD6u+BWDtKO4hi8ybx/RLfNywwigvX9fvuL7Phvt7KR8YGBi4F6AhuSf5lVjHPs/Gnj4lf7MWBBDb1NGTr0USAeBIlT5cZwFdoxfLHQWkb+7v95FPB9DC+lxF8dYJMaDRz9oc/D8wMHBfUCa16hB/ob6iTnvFUmR5zrNFJEwCfaAtuQjJ2/1JxuchxDkwMoeSLkRYfucH8hA8JfcQET73pIAEugbDyXf5Ul8RvS9mC/kPq+GseOMnAb705/N0ywCuKRPdc8n+V/0NuAt85n+h59PJJE4Pgqz1Oszy7/pBgOe/CwY6w+zdoq9sCxr8v3pe8RFyrbo9KoyqDSXjOdQeji78/Iu98Y/57ddDMC0Tl41/7TcO2yeyx13628r0fOJwvn6PdyT/6HwSS6fryDx20CD/24HU5XHIi5NFZqs9QxtvY4N8fev2uuCZALz98yz251nRwLZaYsu0N68rF6qYD5AU6TrjrjH2KhqYPQNA77U5jSVqzEP13Pf0MyMgOqZGSz5XYiQ/z7KUoTtcGk51ktkLCH1+vnfMXmcHd3PQPOk4Jv1Yj2ZUAJN/j3ZfyViawXk4Lf1zj315DTiFknvBqAsb8gOfxN22Zp1yKZ1fSkaQCP+A3nJh+HmfbmdI0TdOLtYxp3HRPCS60y1m9YVedy82C9RV1KS2k0U84Aclq5KHjqCHuLg0SZ+m61YhTQCgHGuzMjehcfUB3F9fXgtOYWAG7vkqumDNcx26k4N4bIjednH0SAM6l9se8qfw+XJqHvLn0rh7ebkvD3XFP7MIxxQruof684POwP6DdxD/+VGyVjlo5Cbp545p3fcAbXLxTkLI5HPlB+mvi9L4dI9hawE0ZFt0xRHngswa9+MKJK9hA4BkzomJeT5HZCQO+8s27bjZc08sptMxRjKQ1bAakPwliWtWtgLGRHZ+pDeFfnnjvHSwy/J8+Ou+SD+GFtLnwpBIZDh9gdkHpty99ulNoaNTs8L13A+5SG+ge1gobK4FuEpymeY5P2/UlmYGRnA2Apb7VbqTMCkBjC4Jq/EZmhP7VdhNPZqSBi6A0JjPebWU9Ky6bhlSCJ8aW/F+KQyp9S3AuIcuCVffd7pQmO0wecwwwYZbT4sOeNNLeWbkjTJ/CEgzrHNc+A47O+n3rsa3VjVwRqAxrQSNxCiZPQugXj7N4/rGClT0DfSDRrRZsmVgWy9ShyUCRmL/yjorZaivZv1f8/7LFbqSZ6z6JcCzgVuJOMVvYvchFl6rAT10RZZaT9UD/RBfj3hocQExulxQuqaS7vj43jAPe81z0srG4KIKpecGBtrREhUrXYvmaxUMuUt8u3i7FdtjuVE7GUG1yw1wlT8HXACdHEVvf203ihB0cx3I79dMiAPrg4am4y0I6aikS/e5lndPiOOvXN/OqO2PZ6UfL4nuJN5r4rN5/4EscjctAdQAzYk2GUPR644AIOTeE81P9jk3qzeiOla0K9TzFgsNnA0xWc3H89Z83Y7gbNWYAO+VvOj9qz33QA0CFGSl61B6bmCgEyxk5JyvsJCJpbK2C42A4rByAZUJwD1VrztIryMCQKLeotfbzduuwbGRXatmwv1uj3+Q/0VAr1NpjTq+tqGgd5bn0u6JtErnZemnuXxJn0VmoBHWCcw9ydWhxBdKE0JT83qeH3btKjrkN5mljHn9xX1QytB0FI4xKoia8LkJ71jnHBNaRT/I/S+WuURfDGSGahwFKJWLt9Il1IzgeycsSxSFS6cPCXLGwZyn9e9AB6BfBhWZkjotv+J+0Z5FNdWJioA7CoJdRCYEQXzLKrMaAdwxtbClm1hEqYJzANndVfTPcE1IY/a6Ssxj3gLpjpQ8VO1+unUEYavJx/0j7VOdUt7AunATn1Vvz5sBT/+Ggkw1kvuX3My97+vMxsJTrTE2YglLDcjWRPLjRMmjR7iamS3+gZ7uTSLn6T791Ro2YAUq+5z7gIYyFljJ8ZaAjjTNSLCS+iD88+OSU1XRU88PgqucUyDhQ0vHBENV9g7mtG3l2UlSQc1tgNw94dybayUvwQrr1sxeigb5XwVCIZ166ZwXT/MkeVrOYzDcCiyePHfupWMw5g2si1LUqnY6M3v/Zbn50/ShUU/9ubgzqirgtDkfAtRIaBX2XBF4hmaielilY+DyQOY4JinNSODIKBhl75nESn3IHaOQx+kduAzWmvNKhoUBi62ZEiQ/tCwVWrx/RherGplyRmTRjaUCZQngWcAx9IJrk/+xnviwWo+vqoEzQVpnniBdDysZoVLXPV1rztvnwPkd1FAIigwweQMXAPpkWjxjQ9ThMHUefpzuNIyOebkN2hiyD4quapK3tgnJ3oL7NwBO72TiGciSqYCbvEw60CY2cBmkhrUexrd4qlQvlb/X6y1FTGh/SsSu3V9SFGHcN9eB6usQDGWtymXv2AWNkGvvXazOTOS087q9LwGqIDODm3xr6ywnVetqEBvojPlG9oblPTNLHAan27kNzwJodEAifO05gZLugTOi58A1OEcGOesjJIlQMyzev8+zt0A6txuOAMRm1TGsv7cMLxDiP5e+3tUO+DATEXcdfPfjAs1zpeQWp90rguE4GGWxcDywLjQiLhGfIYQvwrKcBIfwf5yATUFiT8TBWaSyGbHNLJ3XjRgAcwifCecnr+2d4w7HfJ/YIvX6ajFmtrOBugpS6J8OE/oHZItCOar73i41CltNdkKNpz88//OhE0mvUrciGtv4i0Svm66TLnOgOI82XPlDgDHRx8e5yHmh2VBEzKXTUS1qAgNnAxdyliIC8X78J6UjyMsL9xgBiM+X9ieVA9CfFaDXAAvbgXVRIvja64ANc/AxL32yf8mTbr24bFWbaqCpDIIAUjH2TBNsLkci0R2JxKvfJ9NjODOUiovE3KAbylkDV4CYrCixxzIeaCHse/RepSf3LXLxMb0G1LCItwProzQvBm+BWM7aALlezp4MTeMjGJtifAoYyyIWxBGA+ZcNadHtad08zgrHf+JSgVmIVH8vMeT58aR5Ssh3LwthNqpuXzwbGb1+h0CAK+q6Zwnc+nycTvdnIJTnAcmosJS9RZT6LM6jhhdAbggMXBHKnmrfWUzWE5BSUIf5HU//GAQtN36v+viiNOS/ZcMXcyXxdUFCjEVOQz7/6tgqNmqEvqieVJBsW4GGlIGzIB42HiKyePkcweWLk/cDavBkjgLISwXcQ4FGR2sYCyugldyxMs+Qn87G/FwauBLd7rmSwWHpq4ooCKpLADYdrrybAfL7NLy/9rnS+oyZnvluoCMk8qi5EJynD5CT4T0iNqJiUHLX8rlb2OJ03KsxdVVg5i1xjjsnFhPAGnxqqMaP0KoPzanP2JcAJwiE2mUMIqnD2BQVQUwd89eVgF72APy1RWU/CHkU92QMcJETmhdDiwjQMlI0ZtUZf8APh0cbDDKmrHD6N46ou20TVARQPCgplMXD6R8XptV7yY+4QwOAm3UF0gfIPf1qVuVmMYO4KpA3iM6ZYw67ALiQs8WdoBcPSLnS+LtHi48jato/CLLxBMA/ZzTCYxdG58Fqujf0ix2OgucPrpFaau9xs8FBslAWuzMDgJs57Nn+upykT4sBNwDlhnHLomf59MEAj/hSWY2AOZ0jqDjkTfXeM7T1f8sav9PBGjgHmMFf6n90Z7jkDrnyL/+x6/8GjX3QS5fPrLlRA8DpInQl/lkJ564UiggBCLo3Iyi1Bl7JwLnBhaBLdqFlDErEf2/XmkY/OEMKIO0PKYJC9QHZd96yA05gvGMl0EDKrgDicCVzqBZ1r/XUa4W7eP9k4kDdJ7khAwCZrdIhCJ3IflYYK47TDEWxJMDtHyBaqt3ObaAJMWnR4UFD05xXq4Fb574n0HAWZ91mIS+SV3pWINb3LEVVrg1sfxsvhClyIEdNly0CFuS6AMUDHcGs1CWnMdYV/RgQNf8N7o/nJq6+4Z2EDxZxFI+po0Ojo/ynF8ZsdlbERAJkXyKlmhmGuyW6zlQXhnYu3OCnx/GWixho9d1TP94s1p+3MPHwDzfR6TZC6ZbCem98qUYpa0q0KUVFAvkAZYwt76rE6kozj2aGB+G4ZKJ3sAqroRkghqIVKNWG5CKnZQf5nw3PfQjgnT8IA51wDX05XdOBjkBTEg1Ty+os81u9dyw6U7VRotrpuMM0LqlIWTiV2spFUdh6qka/HOXfUCjaBdZ2CkVN3gTv5cc51NTSLDdHxQO9sHkZ4PnvgoFOGH15h6hlzgqYlgYOcNNI7Skkp2+JWFucXetcj0Vu2RA9G0aHpR6bbMlmiP+kcggrjSepckOxuDjdVwstR1I0Mol4omQAWy/AwMDAwI2i4ytOGC0CGIRrK6koa3XibIpjwuceqaHYis4/XXvj5AJTTsqj+Z687mgM7xuLLytOyzHNz6qZI1Ah1jFC/QMDA9cOa/jfmB8a8wXRef2/Ctr6v9v7P2Y0tYVvwkzbpS7ampRjIV8rfzHeosEPuu9UUVE3rRWV1mTWGh6Mh7D/b5D/wMDAPQDZXVbO5DBiId8Ka4HKufhMzxTE3RIbAYzEHnfyIaCZXgHK6wlGddqx2gZkcyZw5J882EyKhupRMzAwMHBhsFOX1Zvq83zTYX7FdK7V2tUy3SKjb07HYgGLUiU5l8vZIzeqbtgA4Ai3IeyAwtbdBh6ct7/XEKkIrO6BgYGBe4HVCetDjlkUFpXSs5XgrC7PI3N3rS2DlYWRDzhwzb9SA0Ai9QaCt1ZhKnj449b0tQcvaDgmoPQayiD+gYGBe4PGvl41NqfrsJAaRVsD74iJrNkDmvdfrK92DSBNkj5/fAUfAkJDOhrkC1U0v1qYmodWCytIqkBr4LMHfO1vwqURnnwY4OFlWeDpJwHf/Hm4NMKL36MLvPkLgE8/ARfFw/t2/fktqsi45nYUr/m1QQyzN3j2Tl0S7wZXAZdAP2hGDuoJ8fJyksp8RW7Ls2NcnEunLZAe+edeJTgT0cXVx1u3g80bH1wPiL2iWYCgCpWRX+mbxNPP/4dwaTy8+4d2E+13i/kTEVxDO7cFMnjcEevj6z8Bl0R44XvgoWAAjGtux/bWDAAN2JCPBSESebfNqthFxCm4Gni64dvFfAhIiodL6VKaVW8nSM2qtjf0QtScQZJXVtPREBo/CTgwMHAtMDk8UtmGuQzlw6KP1OL9l4ySCpUHWDgCM2l2WVlQdUW/BeCE5G67HWs8Fp+fFfVVT2MbSWaxdCVqB9zAwMDANcPkF6GxLC7JWDACurymR5SgqVCDDCmBuWN62grqrtsA8PSBIstf24Wy6Qd75DL8YslJU4vlawGKBwMDAwNXCutchYZsiye+HASSUrX+Xxv+X321Gw0pB+dWashlDQDpMQGKxo7E6N9UGbLNkJoTGA2zdSnV2gW6hTEwMDBwedij1XymaXrzhcXnqTNocmtFbLGyzn2ecJ5axAHy88Rjv0qGzzZjOO6YW1zgjvW28UBhHwzpovDS4Ny75y1DLGhK0vHwOsmcwa65dAX2VzkwMDDQE02OWqGwQy9dzqVUFUvaog4WOBpYJH+sLJciGIptVSUobCX5uOazEBbnu0N0+dHbZ6ytMzvfiTWJFm2NGA/3DQwM3ArWnK7kUCtbd+K7orGQF15yMcMX5ZBTUYl+HLDOEkD3gcCF79O8PLC/lJjTuI4opcUPjwRWT+dQP38wMDAwcIMwzmMlZ8dBtvSX/0KtXnM5q5Kecgvj0UB9kltQdRUPAaa0jka5FNIKRNwhs5xO6JFOzMuF01FnDPIfGBi4K3QiPFdYvCPJWhhULFeRV1lJbDulketyJGGbi1AbQlr41/LSBi3WCTL5IbHY9L6z9x5CfgZcC7V39tPOXJGUMdsZGBgYuGEQn/TMU1sc+g9wDWg0OAwOInV2LXVu528EZ6EDNqBOj+N1d/49+lhLYBpKH9bgQNunyVPSDyRvXw6Xh/hkM2Ztwh9kPzAwcM9Am0wnjznmF1MxrBQwkLFHXQ2kEH/Cf4b6tjz5+8G9R39Il/XStXpJliN/MKQlRke8li8+wY/rWYunk+vR21T3MCYGBgauFOjOqJLDxCHVJQ3KjCDzOLbUZzc4KHeCcqxhw/nzXhIU19CZ7cyDsYeuPcBHPXmE1MKj1l6yHhIJp6F+etZUS0dgVB9zITsoh4GBgYGbRCfvP3beaKQZiRyTyOqz1lvGuosQORfaeWH/ECD1RTWvXfLQtffqubV4Wp7KccYDAB/GBxrO7xP/6QQU9jvoHdw/MDBwdSh5xMeMlvkrC30fSCAOjXPLwKbIKxrl2HJCRq3BYTCQuGVuK7bSg3KUlEmdWV5czhqW546zNfzjAf2Wc0z08cMeh2caSjGJlYDxjtZzjZUM4h8YGLh2nHGeoq/+HdI4Lmr07LGQXwPk+MIGytWLTjBhexIOqRJ2HZ1USo2BeMuVVQ2DmcQDJD9csC9XOJmQrP1IcYYVwQ6K4e0PDAw8a+g87znkQnTI02nv8L9RT1GdjfwTbiWqa8h/wgbmUPqxIOKifJ+HaQNiz3veT55ti/ZDZCUEmkb+9sQf6Y7rRGAyktgDTT+Tp3/qrLXqJJ0/MDAwcLVAdneNavjw/pImOZzFdpm/NhiUPCLTae5GQJbwY4ccHV9LnLClGYETjqyNQBzs2AiAeJ+UL3ryKCfIYZyViZ6r8nS8dt1kfWNgYGDgFtBCskYZygDZ0rFYogRrKN4hU+S90nLDgtIzeGqrJqHHNGkb+9chkeTLJ8QPTEsiXYtOui4fSwD07fTOSJp4pvrjH3YeGBgYuBtYyN+XlzyLZuAnXbeFjDtxVaVDLHMwJq+4g6JixulTwEGVpFp1dlqeyKSPCUp1XBHTsTYJnqGJR6OIfWVwYGBg4B7Q5tmEEPthWpS4BpVRAin8jw2GAuUfa7ucTqrxtwCwcCzJ3xCLze8VnhZUzhDiz4yNwfoDAwM3jNbwf2EeTN/4ioqAZAQY51Tz1IvGcoUorpVCleSjyxgl+PljA88SShcjeahv5YbQKzkwMDBw18A2Vz1xuhkiljzxVtTOz9gskCEw+xj9663qKn4N8CxIHjtg4/zrV77a2v6wIAYGBi4MLGRWer1SHn2KrLreWa4b6shYy6eB6fgZu/R5uxLSpwDvxwBICF5I1B6sWAX0EkHnKgfxDwwM3BP86+bNPr45dG4N/0PHqTn177nX/6bv4NjesMuFbncJIF4AOvE8CgJnxryMMEcbEFdoBi7nPuyAgYGBq0Wj9x8hRP+evF5RfYeJUSHPQgEZQZFDVviwh7lY0fgpNOf6IwA0Wh+H8Tm+vygbMiOxsyVIdwcGBgbuHcvHbg5vl60xrbry1qpTEkZCf4xIDS4TAZBIMiH0yIOej2dv+toYEBkvf61Q/8nsHRgYGLgnyF7xkpNSoOwJYyogVudfcpCSTLq0OoVk7gs6fgrglW+XBoW0RkS5es9nAbIPGIdEVUr+yKzhA1wd4VsGV686uMNr646BgYEBFmgM/+ukuDzxjjxFcIVM86RAXmiQkdA5PEGD4HFuayxkm5LssQq2h6OXDjAo8pA2V4tX3BKRjU/yDgwMDPjQQv4TEhrJ4wDc1kTYriVjh0yXiMNBjj7dL+0rKuhOhq1QQtMGNs/8xglz/jDQ2Ygf1cOBgYGB2wK6nWet+GHtfzEFAqZEaA6Lu4hYKr8+cm+/ZglAb+yz8x2AIkhEI3urYM165cOBgYGB2wMavX89Lya+mfzjqTnU1F0ySlzRAa6clIf+spqRgxX6CJ5hA4CMsP3+OZcnMNkMDAwMPHsQJkDy6ZRApEXij4WcVTaX1eD6LDCyInRJoAeeIQMAHRdgpfoNSQMDAwM3D+ybf3j9b/H+eRI8UmRx2cE68VrXL7D9fIkg9+Q/Tbfq0nBnBsAcL9EeTrwE1ogs3Jf1sH35p+DaEV78Htju/q4dm/f80P7v2jGu+b3C/qCbVY6j4twbtj6IZ6uzr6+N/mT1PCwnUjZg7sAAYMj1ok/st6/L2OpoebpmYGBgYCV08v6zt8QhTXeTvxsWx81QIfrma/7Nhni/oKdoPSy4YgNA6gK4Ut7D/LArR2PheGBgYODSsJCTRxOyhF/lm2vEWHL4az/Bpz1QKOhDpkm0eWJzS9EPUqiDAVDqOYssCd3vfzlvlo+3R1yF8ytcUMlsXaOugYGBgasBOkPs5WS69s2v/1sdQ0XA9ZDeWlhedAxaxAH7LU5s8x6dv9SHyeHC3TFJ45G0Z4E4H/KOo09wZIRpZM5rIz9qqnVVXE4aGBgYuD9gFvpvUOXIdzqcmuEglTV4/3H1wdKYCm7Y5gWZUHayjwuRz4laeS4LbeLXAeYqmi5oS33+rIGBgYGLwkyyZQJLPwCkF+sLVA9L4u78I+KPHIHge+/fhDDpszuOdUsAN0XgHkgmG658npqpCIP4BwYGbhsl8mcKJEFVpJ4wxLkGD73SsRKnZWuU2na+yMhRAyissPZ9mV8DvBpwlgyy/L9O3fMfwCD/gYGBZxaF5flQWbYoUIpeFw2LinpRSUDlYUfT222+c3hGvwRYCGGs+pAh86xEnLXKswQ3gC/9ebg4nv+u3R3xITn/rV8EePMn4eJ45w/q+W/s2vj2L8JFMfXj1J8axjW3o3TNrx3JsjGTlaRgMhXGPhn76t9aQHdGGQXydxR0ZUu4UwOA9kaB0VcnW274Ii9S1ZY7sBbeMEyyyaxQyJNkqf0Vy0xEoN0Rj5+0t1Oqg1vtsZ7TjBIZTOQ/tZNrB6dT61colNV0lAyAUl+W6gPQrzGXX3vNLefOTTtxvlSO00Plb9kAKITgqV3AdVtQdZfqrixbrbdScTSGXD6ottxQUHJnSwCop6PwtxrmCqIhTaP+TW1Y/QQuC3p6gUmnJEdlqT41luhsj5SvRXHiCZ6LbZbOwQJKjtxM6hk2Ul8ik9YCiUC5fK3v4r4t6bSC9lu8DUy97FNskPYb9wIVlX/mkK6GY7MXXimHkoxFX+UFxGQTacHVpvkbigD0vJBrwWCATHCZd3odTaquHaFDOiUFIFuPUUBJh3p+gZGnspwcLSMRRKksJ6O1QTOeLHVy9Xj6k2uX5i3XlG9plwbNwIwNPMu5abpuHQXvP81CceiWy1bk16Kb3kURDdItl99K/n7vf8KFDAB6p9D0eV8pHsNQZF0Y7lYU9itxD3ODCC8pe/I5gvDUxw1drg6ujBWhcOwFJaRYZ1DqlMiUk5/rmbeWMR63S9IZy8X6aVuhoIPTZwGCHkHRCB9AHhvSuKR9csuGgIOgw/Hf+Cd/YU3vH41yCQIUw/uawYOsxmQ/Hx4BXFzoxDbVoo00yd2RXAerWRvJ77ORFy2pPTtQP+7apuWmyGq9yLmvCO+EZ/UetfvIO8GW2hgMbSsd9wAlEwAfWVpvYSpvPZeScRbLIZTrKZGqJAeFNpbON64rMHVz8rSdAPz4vFXyP8E2QWH0b/mUC6Togsdztlq1lmRMk1EYarge+U/Y+lzTkqySf7oJmdE+E3/JvuCOVwdn+Fgvcmu9suMzG5o3Pz/M4MjKIk+PpUmfjqtazLql+ud9ENoR52uepVTeOsY8tzKtp2Cns8dA9HlgufZc2ywGlXRd4uMacHMVzefka/rr7HNeB5zaLMyXWEjAgnLUs6vnaFQSi3VakQrHUwpNr9EnJXGJW5PVyU2uAPoNVrIPaF0XJ3oOIvWKh73qpIMi6Xbk57ebRq23STuh5PFxJKDYdCKCchwUOVo3JWKJxDR9UvuQ0UVlaJ6FpDT5mgGpyXNzDs0D4McBTdeumRVxOzhjUBt7pT6m+jkdN4XyTXXoQuYHfwIw3/23Dq6VyEObJ7Q8VISRv+SHLDScbZtHsBFl4z8AnQsRVI6saNcFgMCfeEG0U72H+QST1BkhFRXtsbtD6eQkIzMocpR8vZOrZw7i7pn4OCapEulKMho4Yz3OK+3HQKOMZ0CiIU3rI+0YSZviNLrvQRD0xfmg1GcZPzdL+Efg6R+DKGa22nzvBEm5Oid4Bp9zEBTnorqBb7n9XWCbwWt7Rr8ESC0Xw4WTJvPm+uc9zB0YXLbxuJacnWcCpUmXTs6SPPXgrJAmeCmtpLt08WqMFE4/R85mjwVk0uP63hOl4NKKE62QjoW2BEc9Jcz9yY0jalzSm5WOTck46DLXXApBTiXnlZw+6QOke936hGlfzbgr5TmFk/Ptev2Pyh7T1GfEAEDIyR5lUWcRCwKpIPb22ahmVCcycqnITc8UKWhfa15eKSQ3byU5OjF7Uao//tPKoaEO7yXmytA6g1AmFPRQfcjI17R3Ri0J0uiOpIM7Twml8RUfB2ZfM6bu0ZLHbCfNRv10Z0c6v/0NnYMFORQP1oPBcIiHa7CUO+WjkK5URHCHXwKsvLDWCblSaWkunhNo1Dbe0jkrHFPvYd7Yo2YypBNtUPRKHduzA4NSt0YKXr1eeYls+EHF66LnInnZVnLl6ojroem0LQDl85HKWK+Bdj6l85TmFK6/uOObvbF154pzXOY5UL4l0XDNsCpLz0cTibeSRuCVKnV6LWNe/sYiAGj4KxSV1K0COQyWDHTkHQg6f3Dkf1fo6ZGhcowF2VpI3h4l/wA8YXFtLJGMp02B6KVpwVC31He92hu3A5i64vtVMlZom+J2BaVsqT1YSOfyPYasNgHcAirbezrt0KDYRNJCgqusQ0ioV7IlY1uiXK11YOkEd0URAG6WnPcB3LMKN8EBrHRT2ZQmTcE0fU7KST7fzxTcGyTi0SZGSqq0jAbrZCvVK8kG5bjUjhqi4urnPH8E3pOP20H1SO1vJXtaj5aneeFxv3HXU7rJPG2j/VJTjoNlArgZ1JFhEIuiUD4XEVFtTDXMsSgnUAee8/6rbCH0lxE+BCTNwAD6rMaROL1DY3AzNjD7wJfXVCvFLgK0O19BUvCsgCMc68RYIiyOMBBsk61nIqf1aTpKba6BZAzFeVYdwOiwHHug1WUxsugNFF9jyzxRahs3tUlyALnxZTFwbh42v5U+s7Q//ebpDY1ZDu+/tV6lCDc8fbqsFqgO5UNAVlblKIzmVZgmlqq541WhzySShx/nq2XgGSZ/jSg9k6fk6XJ6vYTI1cdBsmu5NnkNGy+CM08jNy5N8gss4Mpq9cXlAuhtKhmP3lsqQG5ccGNL0u+9drXX+8pBu023jyze/8SmwXg9nQRrpUAOEhmQU0qHkMwtqRrreYSi7CZXXkIDmSOz1fal47MBgWtMMBSh3EIHPcdJ6eUynvRF+uXMaJ0IObKghOW5pygkuzkAXzcKaVJ7pH0NKGxLcpbyHLlxA9oCyXDg2qsZDHO+1u5g0CHppWXim5YSP4J87TkEpX33cH9jfpicctP5Baj+XG5zv1rrDdG/kOwvfXG80KX7VBJALgGL57hNCs+toRMUTefNl7LBIVnO2o1/tsFvrwiFA9otnKULoM0Jjif6b31SoODGm7WcJK+NYUmuBkHY52SobEwWADrZ0H1v2xDymScwbeEGq9RuCs+1A6EtUnskQ4rL584VoL5tkj7LvnTMtaf32Lw0kE+gtGEoaK2gUe4o2zK/CgRBh0eKgiFzIhOGZBvaulEtfyTH8Zbuc8eg6L4IeLeiNL9mc8+xX+afL4jLSfOlfOEPykzv8tNrck+Iz4ubKLVxF8DWNyjU1xOopHF1nmvCp4MwKG3R2lAq42m/ZkiAoNNjUSPos66lrdINbd2nXkF8HF+De763CU5DMXJSxdPuRsSWPM8FQFfynBcPyZRT0Fido0MMos/Ah4BKFg5A/BOU3HyUzCPIzyeSgU8RMq00ncGzMDFkd0RBdgYq6dyEG8gx1aFBm6y5a0Qn/8DoWQscuYDQHis40qV94IV206Aiz13ruAw3nqQyrbDoYmd9kA2IewDyCfRy5d2CQvmSfinPeaGL9VrJP02QAlh2oCnJc743+SGg3HnhXBo6A1L5dB6MSZ67voFoDUpNIZOv+FhPr8npVsF3ZO7FcoQAwJNtydMsQSO/oMiD0oYexCyBa6/UXxy0gS7dCAD9xm5Q2lIji0qZFnATADdWufZoEZSeY+HcKJBh7EixcrUkLOmzosXoyBANDJSLIhqeKzPDd77uCMA5x6M0V2GSzpE/gKUjTvfskf1n8g8F+WBqK57+zH02F5EqUQveCej5SySrkbmlOziv0QsriVJjJS6vWZZz+Rpo59aiUzK6avrSQ8jadaXjghoqNM8L67lRQueu5yxn6ac7JP/4Eqw2a7WQeNc+5887TgOAjuTvL+c2ANajGsz28XinLPcRJn9YM+ukKk7vnwaiJpA/AOvYMK7nH0TlffMprXdFLgLvDYhOHZyBYTUaSjqlerhoBJD6tfI10AiIq0c6pjo5OUp83ogKCO2SDAR0lpPqtl5zywSgMRtNrzVEbhBcEGp2ssQuaCHwElDJcM27topiXslvR0+FaEpS0xls+BnAMitY7kLuD8gWmeNUfzCSaqBNiFRKBM/NkVxrdKTnYbq3/ZUwdXUbrZeHZAyVUJrIg+GYGwQSuHYGZ5rUZi3CUXOpNcNi3g+GNpTq5trqndfoVmKIfHqwTUWcbC8S5vpU6ltk5O4Uh9MjJ4jSadcMGl9WcyFU2lhIpsPuxBS11x/NiWk2EdlyBdO5awphB0C3EaC1It4udXKG89xmzmFL5idkWoORHpR10jqjosK97Dzvppv8GZgpJgRHOpI8SrahIBPLcXkaNJIu1RMfB9DJQtNRC25wW+optZ+T97RX6weaJtUnGV7S2Kgx+kr9F7clMOW5Y629UtqNQLsF1/f+O8/RTiftNFSI0xnfPvbvF1gr9/PEhhZfVCxed/yUvBWlOYDmW5p9un9x2e73kdeZng+/L7Uh1hUS06liYDXdwP6LepPgJkUUttrgyu4ypS5k0kqgZTzDgrNkAfJLzJ2vB6XzCg31aGQbb62wTBQUklsVhLIx4VMDxgqNvSzHgexz1yA2TiRd1w5CePSU6OnV6u6Xb2gNFuSQT+LsvuWy957TjzU61W6T8iEdh0D2uTStOWzisTAyk8f+i46Y1hEYfTP5A0kjVQDXZuk+PuSVztA6WPzF8gLGGar3OLoUrOQTQyPUOA2hrN860QbDPhjSZ6CyH5hjy/WWyKUkL0G6cZA59hJrXE4yLgB44y8ouqSJzHLNKIKQJo0/rx5ue4tAergkhGN+sBT0VMImo7EYKnlGKGUDlKvWdVrO1YN8kG4DpvWxc+xMzOHgbdP6Y+LWEGbyh1TPyUoUSD2W4fIA8nmDOld68w7LHFZpQUUjsHDsy74LeCZFjizpIAiCLE3rDY6EPIM7TvO2MxTaoYFrKwDflzXESok/3qeQ0mNdsxzdVt7SKsreRLqNZaS+54y+mwRPwLRbLMXSvMoOEYuVBlV9ZdotLOUnmZjsUPVa1S5s4jppG07bo2EQ/2JTcn+RdE4XlaPGM1ceGT2Sw1Qqt+SjoMU5SyD5q8bN3uXnA5LtjFBIj8trE66XVOPyaMjX9Mc3UktboNCOuC4r0YRCeqxPuq00SOTcawJAktezb6W6qHHEXX+p76nONcfESqBDYwaKeUYvXOuD6ikUjSJWTzxN2C9RA73cBsLAbAeMBeRsQWRvAGh2EDf3csYu59RQHZzTU3LKtLlBnicoqUszlHPkYF0xXhG3NdbdXP+NgZtIkckvldfSvX0aD/w4jeq3EC0drq2gN1xg2mTtP61/YgNHmwC0drZMAMDsc3V40kuQ2iPJSUYAKOml8XzF4Hgi/mQ6K205V+zdIda6pYtUKHXUa7n9s+pq8jQozd9o9cT3NGd8l4xTevKWe4KW02XwuC8RfAVQ+atWSJXTdEOxe0epjyUvCoV8rrxHfwmcB2hpg0ZqpRvKOg7885VcT8sEUALnJtJ0D3Fay3uAxrQ5nU5upXHRNLdcH2hQiO5XwfVrf1jI99Sb7TB5fIJkzwa2rFDQBIMhg7LA1lNvrYHP3QOcrlDUYLkbnVd8lZuvU5uaTcYbAp0wtcEDRJZL4ybheAiVdIOxTmt5rZ0t7SlBIiPthpTQk1hL+idIYyAUtpI+qremTZpHohlI0hSmGTI3B8xtR5S620Bc+3xPZ1jIHwv5RQVFJB+Xg/jc0UDU7IEOs84cm1wej+PUd/KclRcyDz2WlP6A2Qdovis4Tu56o9FzqCgeb12F7gBz15UmXBTyUNFLiVaTL4FzcSxA0p7A6JPKSX1jQQBfW2l/Ach2NyrHHpQiATVREVSOW8ZAqT56fWmd8bFmTKxhYJ0RZ2m+RpjitQ2F/CNq52GUbhc0q2DPpWTM2JUn2NLMObQeCgXpLBGyNO4uszS4E6lZJ7LmCub9iuIAaRtNarqexPXghe+Ci+PhZT1/s8t//graWcL2Q3BxWNpwDX15L9f8SpAQ/5rev0aYc0NQLWTQHfgyBTWUFQ72XCj/6I+m0GroWvOOaPg1QCyknYGoYpLnOmkVwu9wjm4V0gneEd75g3D1eO5Dh79rx0RYt0Ba45rfHTDbKQlW5q+uG43JKIqkM7aF/Ns9eq/sdf4csETsGsmvwo20AR0Iv7rwHZP/wMDAzYOL/+ZAo+ev0GXJ+1d1N+YrCJjHhYNFrxZx6MIdMraZvBTFD4ycVKc1zsG1j8ZPzhZUqImxKKJVDvsg+YGBgduF/uCfR4t1LiSyYrEOXrPg/e9bgDJ9ruPZQ4GybLq2athcI1+LV07zLNf1LBxIL1UnldqxW0Ef0YGBgYFzQyR/0/zfsPZfLqSIdTASvKLamgl6lfnRvgRgOjmD7CrQrI+GxnQ5D82CMhQbGBgYuCaonr+HrOsevmvK3+cpc7Fmk6h6V1o27sQD1/kMQBVW6jG6qNMENKb5RQYGBgauE54JzOoQOdfKTU3whf5BaUb5LTqomNetXGFXfCMGwJkZkBqh1dV3aPcg/4GBgRuA7P1De+i/hJYVXVc5VJtwhga0nSdp5CbNXRNc+B2jP+14JVD1tOpuip1F3SqGlTAwMHBZ8IFSTDM5IJTZM1kr96yXY0fvP5eRvlWFiIYlBw8w2RTljMlbnphnlN7B42D9agEqedxxI7TTaPLwOzxMWNWeQfrnBr72N+Hp5/9DuDS2L/+Umv/4uf8AHl//CbgkwgvfAw/v+XdVmbc/+S/DpfHw7h+C8OJ3w0A/sFyOhQLmj/50flXOFXmwv3Fgjwb0PBd/4U1Bo7BvLXMBcB59zWm4Kjkn6AkNDAwMXCOMXqvLU3bwkGmq9Myl5bX/+ZO/aFRjSrf2Y6E4h9t9CJCGjVblZDSmVahZp9DAwMDABeEh/wbnshhZ8KmT85BVSwPL83cQbBEAUok1eF6Tf8JjcnR9BgCNqp/tU7+dFTerGMQ/MDBwizDOXSXyL6y/N3nMrumVPNUgEHxQ8iqq0RIZEd/a/4z0Q0D0y38I/NlI+TS9pFsC58135UNLIxrVuwus2J6BgYGBc6Nr+N1ZtNtUelAk0RqlwFBWBevM83X8kX8KmO6XQhLoTOfyzorOZNt5oK1dZGBgYGA9GLzvfT6a1PjyJI/VqShK3v+KHyw/5ovk9Ba/Fve/BdAdpVNyef+53B1+CEh6MAAM6Y6qmm2IFa3fgYGBgVuFgbgchRkRuz6M9QlL9gEMZkfJ+5fC/91+CpgXvkEDoMS+Z/LszdU0WgtdjI2BgYGBtWH0/i9Zd0M+952D0xf/TOcsTOYS+ZvQ1tkXNgBirz2OdXRj3zo0q6dDxKkQC8cDAwMD1whT6L/hp35F/St8j4U+/Afph3/wmBpKdZfOSSuARjlDMoetLM2dlETUrZ1feuCgM6gh1s3DRmNaAeb2DMtgYGDgSmD22yzEVRH2X9NvjKqIjYAkUylXRda1eQ7hra+QRNRXSEScoYmGfZfyDuddVfeMsTYwMDDwjKCFELEgpHn/TMQ+qGVbYHmGARwg50HK3v5DgDQgoX03oLqCGNZlCqO6poKD/AcGBp4BoDujW2UcpSzhf4uazt4/691y5cp9cz0GgHJO6ioFzcdCmWIjYuWdSbe5TQMDAwM3CLMH7ln37xD6N5Tl/MlgrXu10L+ln6JWCzo3GWG2dJZVPq6LW4dH8ufVbwbXEJrXSbVroDRZMQMDAwPXgxL5ZwikbI+6LYp4Ofq6XwDDKWFc2gJrG62w6dueZLmtpJeD12FGw353cLZchwot51hUQMMZWC4CMB4DGBgYuF6YPf/4wErWsO7ch9L06vnojyeiYcnrG1HotwRwVSRU28POKqzGHVvYkmaod5D/wMDA1WKNCcoxV5qNj7KIecrXwvQiLuPJ3fhDgGfssAbOXoQbL7LHQB4YGBi4JNCT7/SULfku8sfsMEgFTfOv53wM0YwVvP8JV2AAUGK8cEy7A0/LihxKB8kPDAzcKkof/EmFTUlJRgPpWeZW7g1yu/fvmbwN5+OGXdmWJ16NAenHgAD4NWyLDk7uwuQfb6sKz6i0IuaudBUf1sLAwMC1weKBe+cu68RYQcLRIWW4IIjWVtFNgRbJMNS55aUt5hUK6R4dJblOiKtofI0/V9zpdcGqLhnEPzAwcGOo8tC1udakQMhC9pB97Y8xDPKylU7fOfMi3M+HgCyBB4AVQvudL7izcPVziAMDAwNrAD0EbM0/zrnV5aFq3X+Z6Q97fedbKxH0lltwGwYAR/IAPAev4hR7ohsONR0KrnK6AwMDAzUwE3BDiL4bUnqnNSXf+u9qeJQKxCLrzvDrGgBxvISSNyjplPABViZ5a/igQqUThxZwHTcwMDBwxUDPXGX9WVxPfr1RgSis+e/3Pb/250SxrFB3KZphxLaqnOVHAen3+YHIUnRysm2wNsKoqgtXHxRhciyDexRzYGBg4PphIbSafIWolejDXEr0V82GhcWgwUJ+nGc5F0ceI1AXAeBC7/RY2l8dXPgAoKt3H1cz73dQFo6+Pwf2MQccRsDAwMANYIXlT7NcgTA5qgqmwlB/Xl3LGZUxZHFjDwFy4QQaauj0VH6pGfG2WkGaZh7q2PwY4sDAwMB5UO3dYyEfukx+UjR1mpFD7TMNzcsCldFop84zGQB89x4QBHlNl3TciQpXYVQ+7CQhCS4gLzs8/4GBgduC06M3iXsm7EWWW7GeJQI0fu9fk3OG6Zt1KksYWz5UPmuk+xINIVM2awUoLYSLQDstJ1I1ZYXqQgUJ7XOPUCApMzAwMHB10LxkIamW0K3ZpzV/5OfOIJTj9TrJv0UMPcI2bPVapH2LvBVnJH+OdTut3y/7C/KPSaRNiMdQCBHxRwU5HXnNFzKgBgYGBiSUSNJMshX5imExR1Wl59RrjAo9H21la/PUtugFb/9DQBPoFdQCEl0IP0/DgqT45ALn0hNl7NoUDM9/YGDgSpHMYZ5X/oxhcrQIxcKBlGVznLBGNIwfMZIVyHJFcT3EfTsGQEzqJbbV0poq15tg0iAYg7aBiKf6BwYGBu4LVlKzqeESw/EfxHQ5NcR73Xgj1V4U85J/B9ltowlkb4OlDs1zByWtO+RKzOR/POd5TZ/qiLc2ZQMDAwM3AM7bYfNb9NcpoUXp21Sh9M6/dm7N3n+pTnJQjJKUK92qioIxT3qaLWkM2HE2ktfpvPTwHYD8fv68pk/1lFvV+3vTAwMDA2fCpUP/KB6oS6vzvKv6qiXDRirUck5ZstEFNX68SF8CQGPexTx1CzQrJQ+pa6eVacBowFDCj8azLbR/0O4m/6vp5/tHePIt8PDufxeuHZsXv3vf1ovi4X1lkSvoy/DkQzDQCVhIMBHhivkRUsdtmayty7C25DUm5746b/ghwIU0JavI+mpekehhCeXPnn1mNKD8xL5sBJSsKEY82MUHOmNHauHFMrFdHE8+fBNRpPDi98DAM4KS528qb/X8beqWe2RnAmDB+XJ56VFi0YnuE81I8+z9tIGrQ9rC5eEM+gfMVtOUg3uSnob0T1Yhpq/nIZELpHzcsuUc4pIVI5ZWPDAwMHBV0Ofk87VhqT+OyHLPX9nIn5aieeDLc4N/iyGv02ckbZcc6lpy/mtqN5lqiHQt/ImCTD54LN65BtGbh/RsZg8fgHGy51A/8i0NjK45N+2tSjN1EP7AwMDNwOqtokHGIIC2kjRefJjzreSfHVgL1YmUllNE+Ajj9CXA+IdoDiqk8DRWVI2FkuVcKqU9g8iaEscrnhD9UXBew6eg3jzNy487MnZuCw0MDAzcFrCDjGsOlI2PnCPWIH8PKnTWLikIydsSyXk88BobJQu5G/SUKHZeq4+PZ8Wxpx7/qE5APrahDhA2ctL5op5HwcDAwEAj1pr76r1/isX7R0dZNCWJ1oZVZ0lvV50HbLOQCOhUJunivHCLDvSUZ4RDdJyGdvLQPAh1SCR/sBCl0lpaAU18HV+pgYGBgVuEkYDRM89hOjNGRaVlX0VVrsTejLZ8v6CznxZs52qoEQCQd5Cn3RhyHQl/xyF5IpA8cXAUkDx39hiEhz2g5NGnDQk9iFaycJqUDfIfGBi4YqA7gxFFRx2YJ5HcIJZ1osVLd4XpjaRhPBeO+7Zxu7nQfOJhB/6axDJxo7h18sQDFyImc73c05uo6YMU1LAJbM6hJL8e1DBKsHDsVjAwYMSbvwD49BNwUUyvTF76WwQDl4GF/E1EWZlfLGs1LDrPwRr5ywWy3QzGdfk8oh09AxDrouUD43hmK9/ILyVQo4Lz1K2RBiT1cmUXWTy9e5ASP79viwwIxbvDYyXDwECCx9f+Jjy+/hNwSYQXvgcehgEwwKHFS2az0gTKQ643sarmU6P3X6OT7PKilopz8p+wXwKQyI8zLIJwLG05PYFtXjk8n+tEte2L5k7Qwg9VyhqUDOIfGBi4NrTOS24vWVBBjk98YVZtDdMDmOZxVBiu5XQd7/2zEQAoQAqrS/WVCbmcHzJtmKSUvP+uMIRW6pU6lQzSHxgYuFZgIdM0f3k8dFTyqItliPKaIhNCYnFJwuPsGb1/Zx7/DEASoI9Lp+Q7y+ThlCBUwOvIjYTZNoNTyD7VkUoCdCR+BP60uxEtV0GFioGBgYF7R4kojXNhkMRrvf/SMw1FvXYv3U7+dp10ST7GVizF1m5Nk/OkB+1WCdnzVenp1dXHhlSFQsnmGhgYGLhpGLx/d+ifX/ePcxeHsSGqYCtUEBOMmtrlBBcw0QyPae4N/xiQAK8X3yWU38HL733dBwYGBi4ONIr4SDrA8u3a02vlprK9EL2frqLSqKltMyouKKPz9gwA7CRTVbHE0o4KJTVVbR4Ww8DAwLXCGiK3qUmTePLP3gBwfkjIklTIiEQUzmBD/0ajonBO6aI9kxhhm5XqtsBOUNKPcKUh8MaGNdoLdqUDAwMD1wCHJ1vy/pFPOM3IyL9ajnNmCZp73LL2r+ktF1JEbH01P0hPF6bzZwCouWBtL/duX6t3viqvcadP8wG6uOfdjalB+AMDAzcGE/nXq6bfhHGt+88F0x1fA2oFUEmoaAr91g5CTP1xn+TYSu0oopEjz48eFkqhSOXzf7kyGsgqiA8MDAxcBQzeMZU1Z6ekRp1hycu16bbmN064LcUF738Ookjf7ZmjJBzu4CFAeslXYkQLH1dXTZXTLSM+MDAwcHUI4Au9W/PTH/o5/aQ75OF/+wRZ6fEWOWANveWIAht4Rt0gugIDgAvmlPY5HSs1TTs+l3JjMGBgYGDgcjBOUO51/zRZ+mLNifzNxoXn9TwrzhP6l+lg+aYOYjkasuUU8MSrydE0+nAAJ8/FyrUY+orsx8ZNoBMq2JuuAgCsevoDAwMDXWAmX0/+IXEmNdbT3QOTyAALzHaEfCGx6P13zhPyudPI3oCAMrZlj9rbcqzUZcmvgGSLWMs0VailFYrT7cDAwMA1wzSvVobHI9Bp/ER2aCM9H/kb8915ffpBZBuj+vv7ENAMKUCxaoXnKzYwMDBw30gpDokjbiN7SAu5QRbYFZF1HFjrkkL+yp8Ft/8hIO1sVyHXBqUVqwE2pQMDAwNXjJJbioUEtajhV/6qQv+GvJJQ05KC3l9puH/5TR29vem3gLezsuKzd6U8SR4gX5ywYMW39mygJ1tZXFsRacIg/oGBgRuAmZx10PfdT+kt5I+mggURK7nZjRrTmxScInTxf/QhIGmp3rukr6VVntN5QC9ihau+GtmvrnRgYGCgP0qet1AokKLpk/5RxtpAq4DTuGhpOrK7ewRjuRn3+wzACRKhl9xz4xXyRjZcSud9g/jAwMDA1QEd2ZjvMU7zQnKeD6Wt4H2isQ2LsFGvLJuzgmINGHTeoQFgjb17LkhjvgsFi8JpGwwMDAxcBO55k8x7ZDcA/cgNEoNA0o+FfGdeSTdbNjq3CqKWsulvInhxQwaAJ0TfwIwF/l0HRuOkZillYGBg4Nw4rY27C+3BvdO+kP2y0F0mf2/+CpECq5fuUEMVWb7cw2HrLwLOqmLC1vKtddaUI8VKDyWuQrAN7R0YGBi4J6CegJGznJK/V/daof/OOvei6FZD+yV4dMIpAlBaG+/BlGdiMtojWrNXb5KzgkH2AwMDtw70TmTEmyUOc62LaqjKn18SrA1KG+pdWBiX40bOED4FbEm7IErr4BdprmVpQigyMDAwcOuwMLXi+UuERtf97Y2xwjtfW8k/SijSQtn7R0ZODf0b+mqb8JYFteaYVo4LOHDpWpmzQyL88oUcGBgYuEtg/fyHmIrND/0la//F+o1yYtlSntPzd+nW8w59MX3yJy9QQ/4TtpFmHbEMtzBjqQ8FvRa5i8Jx9bx9Uo34IgwMDAxcGh7yz+dN8UM/s0DLVIeFDBP/ecgfC/lxni9acaKYDlP/8iXAcr38cWsjroa/OhDq6pwsXIRhAwwMDNwJaFgbEctBZ4xLdIZ7fjWSvyJA7Y35Q7/BotfR3mfgQ0AzakMUTPZKbyDyyqyvPg4MDAxcGUrecXRIg6ez5x/M054g2OL9t4b+NShhesy6KCd/1txxPhV45wZAJ7LEwvEq4EIseIYow8DAwMBakCcu7it3xQ/coHggJiUZpnnUSv5oKnbIsy2XzJ5/nMV/FnlODK6lihswAFg7B7ozITU/zwp6jqiLDPIfGBi4djiDrvMUl6//94ZxAnWtz3ci/wgx+YckDZK0Gt0zGj4ElNTK6JDSZmQrPUIeV1ZLM+As4fvGyjue7sDAwMBloXvngcsszXfV3r8hvym0XxJQHFdcclmix0riF7LJh4BKn8iz1uB9j68UY3e25WqIEm0imi00MDAwcGvQyLlIzMZ5kz8Qk5KMrqF/BwznZmm666uIivA2l463tViBwaRAwdWRpXfwQjnQMTAwMHArKMxhdJk6DWujY15vcVJLYmus+4Oh3uUgiQRgYe3fpDe3BrYJsQZRrmx2cMvYXCCAplmi/yCkXQVZOgbV8O4HzoDNe35o/zcwcHYYCFKaxoPVOz/le5769+j2TNLBrlsTEE6j/PpjG6Gk3wGoIeWkMYVji8xVE2SFd+8s7sewKAYGBq4ATu+Y+p28Dkml1UNXZO0KhCw0FTvkl/Vy3/qfi7Y+pZdVdsTWIPOMcIwWsmgk/q7gLLOBgYGBawJP/izpzwJrk79ZvxOd5n42SKyRf4fzeYY+BDSD7eYojyZ51qTWAn/LNP8U1MDAwEB3zF+sT+cn+jX5Q5o37C/M361TYeVT9HuU+MHh/cev/amev7u9vPwdGwCSR88dK8UvRv5Mm5mxPx4rGBgYuDYg43nT6cv0WVtBu0uu1VNueqagTP7LbvrgX3V8F8WD7PBGDIB46AQhXSqnJBctNzgTBGOFqT8QO2CQ/8DAwPUAi4chOjKtb2O2I+QzNZk8Ze8sapQ3cAv7ofeWSd3JZ1vGHjPU4LVNaB2cDq7l3HcJHN68wKuWouuCM1x0i5ISPpK8gYGBgctCn1Bzf4v/aVtepZf8lTJWYCGxqN5ef/L1w5LdUrP0KxTZyhIc+c7pikZzKyzljRfyKrx4rQFcqEEn/BlcSc2MGhgYGLgk4qfXYyA9qgq9985fiSBcof9lySQcE6vX/qWKBGzLhQXi4sRrgghaMOAmF7gV75455EJANN4haRvr/wMDA9cG+pGffRqQxVv0OC9oSnLla0JokK91OlFP0B/8A4deW962mUFq2Unhx2L6VSAb0stxfBUdwYtYo/YNpTndY3MNDAwMrAkpzjk/S31KKZF/9bxvLOj2/FE9TPPq9YY19BaKbaAVaNi/aZSsGDxcnP0uZlkWcMResokCDPIfGBi4HkgOCcZ7Ll4QZsIu3n8lGnRTgg/CvhsoHhTxDH4HgIO0DkGpmRGxpDHg34nla6blksruxtAaGBi4aWC6ZBmYfIsOVZhN9oTmHSFZMMhbRRm9cR81vfNvrjPHM2AAJN0M+fCkNBwf4umpjP3rd43v4Mc1opBOPftkSWA/SgyvtgwMDAxcEJmjYkEV+TvyzfUzCcW6saiXzvmnZdwW8m885zs1ACjRy676/MUlVVW0z3nuFJphoLVsMQSOX9JCZKxoZPUNDAwMXBLCE1F9PH+xULAVc+ev6WQt3n/T634d2tz+DMBZQdiYPab0TMLkp/X6Y5bQZ9LDeCjIlSCtjeXGwiHaABz5Q339AwMDA2siniNDlurRYM0yxmBPof+VIgtGBzIcE2jYv8rUKEUrjEorIwCS3ymll2Tmb0fHMjMKr9TR+ErgxQLy5CnVRLfS2XLV0jPkjIfFOkYXiQ/CH+iBNx4BfuuLAJ94DeCzbwF8ard9/enu7/GwnfHCw+5v5ya858lh/+UXAN734m67+3vPczAwoAAd3r+X/KOMKgbVdGMhPxatrDxqevDqrfX85yXkCNtyYT2Ebk/XZVDq9MAQvNHiKiVTldQjL72PD5Ec3ecu7GwB9v15x4EBG37rSwC/+nmAX/vcgfQt2BsFTxf5qfyM9+2MgffuDIFv+UqA978TBgb2MM9tdVQBPoK2CGUFsl1e1BOiR5ZfyuVKQPUwRV6j/h0AukihxaTJmWXrGwx7cs+zZWKCw68h2EUzSFGCIKRz5cIpH0/7szEcYJD/wPnwuR1x/8JnAP7+p1PPvgc+8frh7xdfPUQIJiPgO18ekYGBCWicgEUfuG4C9ygphdFVtbzAiRcwTztti+dlbbOjg06NyiIAeqGE6KyeN/J9m5Hm8YALvbdAInFun5PjlgZ4XUuHnp7S5wSF+gYG1sLk7f/sJw/bc+Bzb+4MgTcPxsCHvnIYAs8iYqfH5pmzB/aCVj4y5TvIf48AfCSb181Lg6FdUh7DVOhXfDIAJPKTQt5YSONIVypjvfyWEHzJg+cCCpJXzhkK+yfzk9CFvOZwNsLvYTUN3AUmj/+//Pj5iJ/DZAQMQ+DZQx35e/KxkE/LYyE/hpmiJZaP8tKEhGNUgvdEKzzkL2NLy2sEGsvQtW6JWGOC1whca7+kx2KsIIAYxs91HR5GnNOz9+4D4+lfCkkHDAtg4ODxrxHqr8VsCHzHe3d/L8PAncNH/h5yXhteIrU2EsXUjIvcD/0FJd+oF44GgOa9z8c0vUTGoBxbQ/Ilo4CWoUYJiPoOewd+D4uuiExPOrDdymoGPSE0WsIDzwQmr/8v/xbAJ1+Dq8TPfgrgFz4L8P3fOKIBd43VyN8xB5ccIjaMzuVJ5XXdEnfGiX0iw30m/+wZAC8JS4aDxTCIQyPUwweyr3nxQPRNFwlDVP5I9NyP9ODxgnLPA1wM3Imi1JsDzzL+3isAP/cpv9c/vdr38vF1vvlp/uk1v5cIQU9P/0/r+5P+aVlhMjJqlhcmHf/3Xz9EA77ta2DgWcM5pi0X+U9wzqeGyC9ye1jgr5ZX/hq8/wlbLowek7I1OiAdcyF4C5EvdZGwfCY8k/qB4Bfdhy/pnXQJXvNFiX7CqYPmZYZjb6EkPDBwwBTyn7xrKybSf/+7DgQ8vc8/EX4Jk0EwGwUffPdhOxsDv/b5nWf/KpgxlfuJ3z9sx5LAswgP0TEZKtlBRdVG3ft8W9sXzlqcWi5azZV15RXL2riCfQgwTgOQ187jdqTHB9Lmw/OY5hzX2eMn6k8diEc+BDyF6w8P4UFutQD5oI7zAztnBQ1DJecySH6gjP/vxw9hdQsm4v+2r939fbWN9Iv6Hg7GwPT3J3Ye/W/vjIG//cnDUoQFk9EyRRb+1NfDwLOAaiLDQj4tb11acM6xniBBJDxHn3WH19MWh8FixDYmylMjWVc9nMjp4HAvQsl7jROZHwlZXgegYQxM0pP2YLxvfMr02iCuOw2yH/DDSv69iZ/DS08Of9MT//u1/s/YDIFfPEYOhhFw56j2zIOSb1bUXsz4sR9t+VsM/9eG/qsNqh0e08NtUYFkXSGWTwrzomJn3BOKFucg/oE6WMl/8s7/1a9fj/g5TOv7H3rp8EyCZWlgGAF3DrdnTjJbPX82i/VIq5A+FE/C/rHjCh3JvwRU5Jnkpl8DrCXxuyL/U4df0Ksf9sQzgWnNv0T+k9f/He87eP2XwBQNmAyP9z6/MwT+oPxw4mQETM8YjGcC7hXeyckhHz875dJtWFrAgpuKucffz7QAtV49z1f7nf4ccCdIMZ1MaPXLnuNC1Q5cBtPT/qUH/l56DuFfe3/Y/1jPpfHtu6WHD74H4D/9J+XfHZjOa4pUjLcD7gQtIWqXDlt43p23hz65ppRAnz8DpV5vm2sMHFvyhBv7OeCVgeRv3kGyTYVgdRamSzJZtcMKuGdMa+o/VyD/6f36P/ON10H+M6ZowJ/Zv/tfHp+TEWB9iHDgVoCmpCXD4Jlb8ksypulSD9HH/lcgkYLqB/5K6/5ScUSofXj82TEAOBI9/dEO1Hr7TIhJPo4xZZmD/O8df+mf6KH0iWC/7xsPhHttmNr0fTvDpGQETOf3Vz4GA7cM6jitXpkny2FcGNfncSlg88XQs/jdqf8yNelTgPdnACQ8jumgPF1c+hcXvgLipx4+apkD94xp3X/6iI6EA/mHqyT/GbMR8FLhK4DTLwtO5ztwy/ASM8koee7ufE9koVDBMStECVzoH0sKilloLtb62vj1GwAcoc/pkhd/IvpYkCq7ErC2CGqZA88IppC4tu7/wub6yX/G1MZ/7f0Iz28eVbnptwzGUsAdokT+5ilOEJwjpVmiEc7X/eaP5c2IX4Vnn/h3PfVvzWNP2qVsk/Ej5Rup4ZzzTNNR+0NDOi5tiAn92r14DSLhZx0PA882/rbiDSM+wp94L94E+c9434sBvuO94fT5bQ7TUsD0a4YDtwiBmEvkX6k6QfFbM9AV8Qd/hI/MWhWVErwKTFkzNqcrxJEqNQJO5ExkECL5SBa0PzCkR/puBZwtwhI+0MSBgRMmL/gXpXfpd/fW9L79t3/t7a3gffvXBvimr4gN+hzTZ4Yv+XPGA+cAsru6qNEZFctL+Wisf97F7MM/8dalW6qIOXQhM4Z4ZRuzFpQImSPrZ4jUSvbLvRg2A2eF7P0jvHv7CN/5vtv9msb3/KEAT4K+FDCeBXhG0Er+Z6s/Tyh+lbYU9s9IWj7My/YxhsZrgBZoXr2YgUzBzmDbMXDrEL3/6eubu79v+cppTf12DYCp7d/+NcelAGEimyIA41mAG4Y4L0UZbvK11EESi3MjmrJClBAMRZtQNCoqowpMsWEAxNA4XBxsZ8Yg/buH5P1Pl/zdDzvv/+vO+H3flfDt793AE3hblZl+V2DgnuAh5lmocrIresKy8XkqnxxmCXoVjU/nV6GiymfLAJBIvWitatbAimzMtXPg7iGtf+PjFPq/j1t2+vLf9AzDI8pLAdMbAQM3iNZ5SgtxxzK2xLr6I8Rr/DTsX7Xuz0aOuTynXqkihTvu1wCQuDvOZ4UBdLY9M9m3hLAGbg770Df33v/eY3mED7zrfn5J44+99wGeD4/iWwHTGwHjYcAbgu4Ss7u8WCP5l6ZEJ5HO3n9sl0ybPuTPpffQazvHyxoAWJEuee8a2WeKzZbBujCTPQqFzFbCwI3gF4S1/8ed9z/95O5Lz9+PATBFAd7/zt30+ih/5vBXPwcDN4+yJxqLlX6ER00sEqk3H/MIAFT+oF1r28zlgpKXYnMSsnCilIeKbImgpbxSGVZBqeKs8Pkh9bFaALyFBm4Un3w9Tzt4Hk/hm1+6H/KfMUUB5ocbOfzqF2DgZtGN0ZRkrxeNxmrxRPgY5ev+pV23v90WvSTR0P0bUWiOd3C8qRE5lZWUI4BMbhqpc8dxuQuBawr3V1TiKjBwR5ieev/kayTxGPrHp0934f/bf/iP4n0vTg8DPu7PkZvopuWQN57CwM3CSEYnPliJ/Esg5cPpd/6Onj/O6RWh/5a2oSfPzxmbMlNZyRcATN53vA9CXVSndnxhWLtGLTzIfgDgE68xiQH2D/99wzt3IfPt/UUApnN67wuPuyUOmeXHcwC3CI+He/qHRzO5o5mgwykJl2MsNGGtj/2U+kVS5KCTjUmhmOYhsBsnOM0mMhfuRPiaDTVws/gkYwBMofHHnfc/fUb3XrE/t6fyw4CsYTRwpSBz2yrkH9XROJVqle5VH8lfvPvW+thPSaDTOW9h4ID4KqMiY1aENYVt6rntwM3jE9z6/+Pj/rv//8y7n4N7xfveEeARn+7CrMhOtJ8dHwS6PaBVpob8tXyLDjl/8vzNpvZqnj+6VJn7hLFknj0DoNRBbmVnIPuBZwLZWvcxdDkZAc/f8Rc7vu6dW8CnbwJup28CTCeazlKfHBGA24I7MurLMqOCSONf+4tD/+74W237nQaLG+SzG7dvAFAODiDzclMlNXkdqxm4e+Tv/x+ejp/Wx7/yhfu1AKbXAacPAj0c3wYIIZ1uX3+EgZuAcQJr9fwLxUvYUwNZ94/f9Y/Jvzr0LyW0hP1L8hU0dXkDICbrIKRbdNB9Vz9egHkH2Q8QUKLD4yLk9AbAV75wf28AzNif23Gpg5t1x1sANwTzMml3paSIvQz9zO9ctP1DP2yCo6wxr1hWItnJALC0j/Owufx4Db20np40UJDtQpKSJdE1RCBX3bU67SIM3DpeZ4ju8VF+OO6eMJ2n9J7268MAuA20kJQo4/T8jdGFZQbF0/Hq6Or9e6IKMl/YIgAo7NO0klwXMC5ClmexJFZqoET6VdVJhD/I/1kAHgnxWTAA9t86eHwcQ/tegdmOImOQbWgDRgnT+/4Ix/f9CacGWthl4DjbXvsdgUaD6AqeAaCkLS3ic2EGSd/KQGNz3E2RFI5Z8dlD5A3j/S+C7yMdcDB6zuKNDZwZjWF/U/TA4/mTDEwZKAjlNd2qcO0U3mIUGRwH8iEggPSY/kFFHjB5wOwD2Ze8eISzEqJ2qgCNzalUiIXjgZsEu8x/XAJ49bW34V7xmencTh9rycf/HT/+cP+wzI8lGVN5WYg+WhbvzS6X+o3/1ghcsf1oLFdhFBWw8WmsuUrc1b0SxrLaPF0rlIwjhwqqauAuED/oH//q2ITX3r7fKMBnXnvrGKlEdjg/PwyA2wRmO4qMkGgiTyyLRAjxX1ScXSlvfejP1P7OcPDCfb1bhIY0yZNfBQ3WxFkMkoFrwvPRghx9Fe53P/cG3Ctee+to3ISQvaI14aX7/QbSMwAv+fevPvXul4mUqz5b9y/obsuv7BsTF9g697YMgFKnzCYcR/KrEWin8EFlQGDgfvC+F+KjwyAIR1L83S+8CfeK3/zM68nES2yfsQRws/BOZGTeVOd7VPPnd/mjDfi9c+/TKJ62N0RFik2wLinMDwHGGWs+gTOTtJRO8y0evTWvGR2VozPdovLtL0PYvgMGbhfviT3d/VgIBzbc/f3e5+/XAPjdz++iG2Fm+ZDNAy+/AAPXjreOv9hkmcNaw/4GQZpzeNbfQW+u0L/TsDDrrdHtq3yjhshLf2BMK+WBkH8WUNdbamxjFWuf19PxvdRbx8svRgdHz3+/u9nsSfK1t+7vhfjpnH7z068dIh2bwD6C/b4XYeDKgW99maYIgoVEkwFhEGJETmv8x3lYfIHLve5vNSsQmtb8Vee3JqrQugSAxrSrgGSBxOsGnLyzCsnA6QbhPIYBcPN4/ztJwtH7D2EDrz9F+J07fA7g11/58t7ACZspAhCOzz6kE+p7RwTg+vHlT0VznWfSc06QFnI+isxr/rP3j8AHmF1Bb1QqZPPBlifqlvIogkPvght9CBDJVsrjPHpJvrL61Yi+VDHJef0VGLhtTGvd9IG3PSFO/+8I8m//5mfg3vAPP/GFvQGw2f8djIAYU/j/pScwcDNQPH83AdJ8+yS7rCiH0zFtg/uVPywklDx0DSXdWjlU5AtqNrLUWozGsadVVpKxEL0DuH4V5QZwf7LI4xufhoHbxze9e9nfh8WPEYCHhwf4jU9/+a6WAT79pTfh7//253bEv93NRJtjxCOVGeH/2wB+7mPgIyGH52zJB6ounzdLj5ddjPxLlRWLe/o9fZ14kzMagk7SWPgr6QIoM6wkuwIDa9UyvNsfKGwZMaVNjyMCcBf44Huigz0h7jzjhy2EnQEw/VjQ3/r1P4B7wa+/8sX9uW125zZ5/7PBE+NDXwkDNwCclgCqC5fyCxPxMTt5v59akqR4YOu4AEocUzQs0FEuT3R8CMjKhhJxnxGSLRLnnYXcKUoGEeai1na+eX/h4WcR03MAp9fedjf4Zr8+vtl7yQ87svyZ3TLAl+8kCvDXf/lTe/KfjJuwjwBskrE+vRWRPRcxcJXAz/2WkKEkdpx/0zfA00kTSyRa8vw1779I4J6TdERF9nlByeMT6Zr/bT4DwAUFOLJU+HV9aJVjLtpomDx+7ldh4D7wbV993JkfApwMgO3OU94+B29MUYBf/STcOn78H/0efHYX0pjOaR8F2Bs6IfkIwAfeBQM3gsfP/dM0QSROQv4m71/Kg0Td8tDfcozofMivCCNBl/JbPH+fUAL6PdGNSJqlulrJlPPIQdjHgtzFwVkkACB59rGY8xzEwfz2lwHHcwB3gTTsfXgGYCLJhyNZ/sxvvLJbP7/dNwKmtv+9j392T/4Pzz13XOKYPkmSju4/8V4YuAHgl3fLUtlrgK1KAXxr8rPnHxkDrcZF3iAl36RESfYYFline7fZEAtgk8hoxAuFY+9f1C7aTjbvYuDIXOscUpQRDY5zC0JrmNelRxTgTjA99T6Hvg/vxz8c1sn3hPkE3oAH+Cv/4LfhVvHj/3Dy/nFP/g8Pu7/tgfzj9f9v+crx9P/NgPP+M3g9Z9skGYTtaa4FqTkG/S0c5CrrEMbKPAH39VsA1bCQe7w1qjhCI3EPSnoev/RxGLgPfMfLy/7m+J78w94AeH6//Ye//0X46V/9BNwa/qtf+cTe+5/If/vkBdjstmGzPTwDEGF4/7eDp7//3y4Ha3rOTPbs7U/e/8kIIMXdSwBYqLDFQy/VWyR4r+ePqt5nyADgvHWO5JXiALrjb7w2s/c+D14uDyC1XgORWbaHBkw/Gfv46X8AA/eBKQJwegAuwP4p+cn73/89eX4fDfj/7dbRf+fVzqHXFTGF/n/8l37/eB7Pn5Y0puhGjOH93ximVwBNxAhlON+Xn8mfy8eaOrCYoKPlff9QKudpSyorqb7xDwFpeRzJl8qCmeBNa0yRLEfqVEX8JGtsIIRYGg9kP2/nn7Pcy7/x6fEcwB3hX/36+Y2Aw2dyw8NmT57bHXk+9/wL8PrjBv5vP/ur8OkvXv/zAFMb/y8/9cu75YvNzvN/fv+32Z3L9HAjkNf/hvd/O5jW/x/33wBgc8GnrJ78Q1Q8nkPddWgVmjx/qzomMoKlwtan/peEbJmYHN+AAYAAoseu5SnqAMp2glGNJbxEWyZ5/bHOmOSXPz16MKU9HVGAu8HkBS9vBOyWAcLDIXS+I//Jg562r+7W0v/8T/7SVRsBn/7i6zvy/6VdW5/uvP4n+9D/w5MnhwjA7pzicTwtfQzv/3aAr/wSQ2zxFgzkBi7yXL7hn5J/7GSx7/m7X/cj+S3IyjsWJlDhtVKbI6NoqlF5DVDylDlitbAmGtK0PwC/BQl6FXF6tLVeilA45vK4LWc8HC7Q0tj4o2jxBy60uqe0x8/8PAzcDyZCnH8Nb34WYPPw3C4C8OI+CjB50q++8Qj/55/4R1dpBEzkPxkon3nt6Z74t7t2T0sYUxRj7/3PXwCEw3v/3zG8/5vC42/9DEkhMU7LFF40DujhMqHPkdigqcKZ/mz6We9cbZ9iXGCjfnceZhuNpzYyawJAkcRpGclQkNIYNad9LNsIkkqmij2BGqqVQEP3cbg+TqM6kSm7COHpb0/+eGjj/AyJWlbA9CYAfvl3YOB+8K+//7gUsCPKw7MA2xOZPvfCi3sj4LNv7CIBV2YEfPwzX9q36TOvPz2E/XcGy3MvHIyW6RmG/Y8AHcn/hd1M9H3fCAM3hH34f4oApKnsrqwEdcEoK5AEyQniUysnf8/kawKqh3metd1yP2pVbEykKuVz9Uvt0OrQyLyEbIDIYr2vJfXMEfL1/nj9PsxkH1mM8WsrCCnp0+iBFU9f+e9h4H4whcRnz3gizCkCMHnPB0J9x8EIeP55+MwuEvC//2s/Dz/1j38XLo2pDQfyf9x7+9u5nVP4f1r733/6dwlAfsf7Ruj/1vD0V/5ydFQxuzqLsHNikcAbCMWiGyvO26LfRf56vsYf26ICVPKLJ9CQ3wCP10zJWyqHRDdGpRKiFwYEt16vLSl4ST/G27//0/DwP/qXIWzfAQP3gW/7Gtj/FsDPfvIw3iYD4AEOExDi49GARHjjzTfhL/93vwUff/VL8Ke+5Rvgq9913t/S/fKbb8OP/te/Br/wu6/uvfzUSHlx/+zC4cM/Efm/HD3rMHAzwD+o9P73C/aFWfqYFTtDCJiWMJG/lq8ktPITFhLPQP7ScnMM1gDwhp3XRqk9NBwfkyu3XyJ5Tnf2hD7icekSTwU9XntQ2lBF/nEj3/oyPP3U34HtzggYuB9MUYDPvQnwC68elgKyu3c3IN86jp6/+09fgV/7xOfhj/+Rr4U/9eH3w9qYiH/y+n/6l38PXn8bT28rbF948WAAvLgzALbHV/+in/2dXvkb6/63h8ff/pnDFwDBEdI+5QcoPZBHHa1YOpz0aPV4Gcwhv/qv+9nUlDISPx7zJZQJW7s6m2GQPc1O9tc2LvIH6/KARmwQIFM+G25zuP64i7EyUpYDF9aXYCZ/LTKz23n85H8DMAyAu8P0auCEyQiY1tAnQj0gwPLbAbvw+hsBXn3jbfhrv/g78Hd+45N7I+CD73tP94jATPw/tSf+x8MHi46v+R28/4MBsA/7H9f954f+JvKfz2fgtvD0l6fwfw2x2bxyJAkJf2BhnnR7/sY8i+5m/RYhRiHDRZnRxGDLCVNw+VKZUsgBjXpLD3h4jYyF2EOmDwnBp23ASK7OO7dGBVSw4aogZe6/Cjg9ELh5zwdh4L6wGAGb/WO80+d04bh+Pr0qGI4/rPP0jTfg6W5Qf+b1t+HH/s6vw+PjI/wLf+Rl+Nb3f/XOGHgJ3vFkCzWYSP/jn/ki/NQv/S786ic/txD/9skx7P/8fq1/elNhe3xTYR/23zycvvg3yP92sXj/EdYmTpPM5J0FR3nCFiZy9so4lhVaQv+gPDuGB+7jemZ7zBcVIeSkGgxlKDji5/Rx3jLNo3pp24DIheOaU5glUTAykC8/yzYRuBdaZyc9IuOtX/9ReP5/8n+EgfvDRJ7veTI9EzBZAGH6fz++p1cFN9MHg3aE+/bOMHjrzddh89Zb8PTt3eLA41P4u//kD3Z/nwLcGQOTEfCHv+qd8MGvm4yB5+Br3vV8FiGYXuObCP+3d4T/8U9/CT7+6rT9Irw2kX44/FTxYV3/4fB1vydPjm8oHIh/u/P8w/TQYuT5T2v+I+x/u8i8fwsxVoGPMARVHM3qlMQ6oHhgKOttd5SBOYfOoE4sxZYjdouhIq2rc4YBkDyMSoak7kPawVpJ6T8c0096jxVj1ACpXac1e+DJvBS616ISXcBaTphsZNNKUfv6K/D27/3UeBbgTjGR6PT63M9+KsDrTx+OPxy0Gyeb+QeEDj8h/PStN+Dtt96Ex7ff3v29tSP/p/towK/9wRfgVz/1efjpX/n904en4p/jhZPhfMQUWYB56eHhUMf0M8UP03f9n+wJf/5A0X6t/7jeP4f9p7ZOT/uPB/5uFwfv/1OuMn7vP57/F17vH0HV8qm4l6C9kQXv/I6sDUb5r6RyO4e/D9IhIuKoUXNkZT8fBIhD6ZTAw8nmQCKRXlQ4Sc16IbHg4q88pW2aS8LSv8zgCIWtBRYjoQnxByqQmmGZMNTg7d/+L+Hhvf/CeCPgTjG9HfBN7wb4T/5JgM+9tSNkOP588NEAmL4c+PabT+BhZwA8nf52BsDj07cBd3+TEbCzBvbRgPlrkxOSSWQ2CMIhujDlTqQ/GxgPx18o3D/092RaAniy/3W/ySg4vee/+5s+ZjR9z2C86ne7mML+B+9/TigVMLpNkZ7FCY3GYgs5W8qvAiP5a0KOiAVljeWbN3oDtqd1bySdDguJI8z5iycNQFfUl3BD3KCQNADZhsdZ0nChlg0nR73+q4AavgmMUOfR+vaX4enOCNh+478BA/eJiVT/d988RQKOSwJhWhI4fDXw8eFAxo87gp7If28AHI2AfURgT/5HQ+D0MfXpi+GL6R02hx/rmcL8p58mnvROxL83Mp6cIgH7vEl+czBEJq//2752hPzvAU9/5T9bvH/TNKV7tYfIbJzCPO0PvedyNCUteV7jwjl/u8SR3aWH0hP/HLZxaMXiRSPTpMDsUxmAnO4kUtcIPAnrA++lX4T80SJAe/AMpumuird/96dg89XfOh4IvHNMJPuhlwB+brckMD0guDl+OXAyAh53kYCHp0+PpL/72++/tff+H3dLAvsIwD4igNEMEg7/7yMK0wOHM/kfDYDT38MpIrAvtTnczdOvGU7PKgyv//YxffM//+yvVqBMnBzZx/nzJtTWYwq999SNBhma5/H++bzM+4fFeS9hq9XBKdDC4iUDgqZL3nyp4RchejVCT3sLC0pWQEH1W7/2o/DkW//9sRRw55jIdiLdP/HecDQEEB6mUPzjZlrwA9wevf7939O9IbD88NRjFOE7YrN/tPBgCGweTmv6099+/+HwTv/m+HT/FH2YiH960O/0c8YDNw1868vw9n/3F+3Tl2Oa41yiwOTnBkLNXCp70NCs30H+PiG1CO+UC+TPVCcaAOcIpedPBFzIe+cql0geqScfpALrgQsmFArg67v1u9/aLQX8kbEU8CwgNQQAPvbFB/jcm7txMC0NHD+PisfwP0a/STFheS32sH6/jxIeQ/qH7wwcHgbcPxR4HIcvPIR9qH+KQAyP/77w+Mv/Wf7aH4eKV/2QSaTPdAVaAg0sgcUEpWw5eqEmtHj+Fv1HZCyEaXooqwD1ReBzk/FFPHo2jWZSkkeuUH/Q9RWaVyyc4u3f+8ndTP3VsP1D3wUDzwZmQ2DCb30pwC++GuATryF88vWJwB8Pz/U84szzZPI7PgOwJ/tDrG5eGZj+eek5gG96D8AH3z28/XvF42/8NXj6mz/eHk6PZNIpLSdP1flEpgxTh5jQ6vn36AdX2J+PWmSRk4i7sqi60qa6L4FcK6Qw/emYG1YWd3pFki9VZyJ6ezhg/1bAS98M4Z1/GAaeLUwkfSDqAJ99E3ZGwAY++dq0BXj9KcLn3gr7z/i+/ri8mTJ59tMvET6/+3v5RdiT/rSd9LzwAAN3jMnrf/sf/lg7ae5luN1jpCk6FJd38Qxz8NrLCiUhbRoX7Jj0QUp/+7eJpljPud1/re001mH6fedIBjVXWko7E1xVx2Qfd4pDydtfhjf/8V+EJ3/0hyC8MF7IflYxRQamv8l7P4Cbes89CQxcC/bk/3P/fiePNwf18otfWbX4OE2heYTTp2Br9WtARd7h+WdSWHjiv9DETSKIkHJK6Q8K6UU9mJZJCkbHGAkhVVxqRAVJrgmuaaogFW48n6k7X3sF3vzF/wBwZwwMDAwMxNg/9Lcjf/ySYd2/NA+ReW4OTycPqmGB32eeMNaxJMqHORrJv6jfP1+H6E/KU3UbqtzwJAqgE+wxLQs9IHvI6wddtygLhrQrgGT0AJQtvsTDbzw/rv5p9/WdEfALwwgYGBhYYCb/2YEryUBKZBhNRNQp7hNvYubMkqOFBaFW8ncaFnFUZLZ9sjcjTqdZT/4TNrw0Cvs0jWM1jfmAKXfDKNlIZiWSwsr2GNuBX/r4MAIGBgb2OJH/Zz9mkTZnH6YiBKTEj2VuLpJn5mQyMqU61iL/SppLPnl/SluO6x+HyAtuYEAHKn+xTJOS6ivarOJgBPyfhhEwMPAMw0z+JVKeZYAPYcfHUnj7oKPAnhZiN5G/J99J/lojUNHP5C0RlLjf7O/7S+nDANC8eDepSso6o4v9sCjYGwH//Y/slwUGBgaeLewf+PuvftBA/uX5jD5LzqVPGRd/vHRNz78B3Dtq8XaxERTDQsiYzIVHknofBoD1YjR78d2U2dFslMRK6D6c9vfPBEwPBg4jYGDgmQF+7mPw9s9a1vwNEw/C6an02UONQ//F8PUcXaj6EI/sPRuVFPTbii75Xv3HeZjx/E9b5N/VkfVGGciXvE0DQIqia568mzglYq9mYb0qqr6pGqpAewVyEZ3eDnjj7/0QvP07PwkDAwP3jekjP2//3A+Xv/JnIWRMDyEi/ZMKNLzqV0KJnNWyWDYwTOReqKMwz+aJC6QIQOz5J89S8GqIkrDnfwx5B29PhQOzZZUJeXE6laF5ALb3Omvg0sk1hmtYZ4/ek25WKl04xXoVrsHbv/n/BnjjFXh4//eO3w4YGLgzTOv90+d9n/7GjxuEwYnDlyPpNGN7j99LzGiQccBSh1q+zbCgPjpH/vR5CkubJuJffsM39fm3iffJbYXGFtNLHNqNUyXSK5E6lef2G5u0hoFzqkBK8wxYOWuKAjx95R/Acx/8X8PmpW+GgYGB28f0q37TD/uY3vGv+Lb/IQmzNf/mephatMN2OPQ3RWrhRP4xW0Fhn6rg1c9XYnlkkOpY4VPAHCF7ZbRwwpwGkD4bCYIMwIpMvKgvON79KjKIdDI+5m8FPLzvfwrbKRrwwtfAwMDA7cHn9RsnD6RTDWsNZIcu4yCTqSB+NAg21aHwWSmqgLnYrC0k+cwT/6i3Z/H64WQI0IcAt7ndkQVvQPakOS9bS6PlW73ylYmdVhXO2ZQGT75zW55+4r+Bx8/+ys4I+NN7Y2BgYOB2sP9Bn1/5K4Bvfgm6ISMuzKdHZh5avv6HcPopSUM91XBHMZwV7sUrPS5SJNYy2yziQ38l8j/yVRoBmECXAFSNnjg+GtKs5H9BWG2SLhXRKMd5PXxbZce9116Bt37l/wFvf+w/h+0HvndnCPzPYGBg4HrhCvefCtURZub2oRbjndnJ65UHJb8SmQ4yuRb5AAv55cTAHWuefwEL2x7IP44AUNzXrwGWcFby5CqPYYyGWOyybigP/GlZ4K1f+X/uDIH/YhgCAwNXhinUj7/1t+Dxt3d/n/sYuOB+/Y6XYaia0eMlf5JYakvV8wuO0H8j+ceuH42Rx2Xt6/6zURWOm5z8uetyOwYAbT23UkHTJT2rQ1v2iGUKRVdF1KEVD+HEhsD0kOBkDIxnBAYGLoPpff7H3/v78PibP74zAqJQv4m0a8gyTTxF9CGl69BcFxbyK4A98h0OGyPD0VmwKNKuQ/bEPwBk+2ktW6UFOjj5zJQxlovzSnXWlOuOJNhVkKkYKKueD4k6NNY1GQJPP/Ff7/8mQ2B6RmDaDmNgYGBd4Jc/Bfh7/y08/v7u75V/RDKtSmpkeE92TmbXrTXSzNXW5e9laiY0RxnLefCFkl3uaToqqqnh8jB58G/x+uG0HzIVW8oHLkjyJQ/27IRdA4srbjwRqsq7qOOGEIGwnFIDHl/9lf3fhPCuPwybd70fNl/zrbD5im8YBsHAQCP2hP/KP96H9ifin455QXDATsrSI+ABS35fe4TBhNboQpfIgD1ffrCPaVcoqQ6nEAwl/9gwUB4C7ISbIHeKTqEFa8T/HH003wwBz1M3Gbz4xd+Gp9PfJ37u0IztO3ZGwTfs/zY7YyDsjIJ9+mQYTHnjg0MDzzim9Xt464u7vy8fvs63I/jH/fYPdsT/jwDf5H6wi7CD2fPXBeMH+TCrb1FBH2Pm64I29JqzMj3EOVLL2g0lNrFgaxym6cVfTzNBoahD2P/wxD8ADf/HXv/+OwBE+R0+BBgPxc5sx3nyyMicDQbPfo32VFjr068N4md30YHd31Pw609PD4V0fl0NBQ9FnbS4RrT0peZ5uMqqiW4RVrgpEugNpxoylPEQpLJmIhQPCrKKkKf/rCFldQx4z1kWnO+XWPMym6L4ZFOQ6gmGcCcqCZZzKvVh7f2TQGFhB/lrD/X5DKijo0W8/ngfo8WYOUJAVV2hAZDZRpAyLidT0lHRhGvx5Gllp64I528LZjsr6c8XMJAK5EWWNEwfd6FbEI4XjSFPqkVLn6lFvBOSsZKqc9VuGEHcl7FHYCQTgkLnK1OaR1iUFTLOQv6VYxSZcqWqYCH+uHeo9w/ArfkHaP68bwmta/7mfiursuRnV0CbHwr3/3IPLCF/Sv6nKxfiGXHBFpJmpfZeXFkuR/NoGamcpF862w4sh0xT5n2pCZVV9UVE+ln7ztBArt5eHcOoSSaY5BhFufl4FitPcYXGWCflkrrZlWrR4cswZbPC7HW2Fo+vmEU+x6FqfgKUmhV7pbGsGcnE28mYMpGJo4BKkKjIuJWK2WzPYDqbh5ISQz1QY9RUv7ZoHOxVty+yu5xEaDC251OXXvcDoIbBsc78GQB6A6NQKQj5WGgsOPVXoDSBWci9sQl9cDwR5CfDc1WfGxo17MBD40WMZOIUKew4i8RTR2B0momh9RSzfmso78msIX1jsqym34BEcp2PiQm4OOA8xekfnWErJDs1fVKe6I2NcGSjQYYrZrwL2D7HKMKS5/N1FVBr3Hrr0fSbJwbPfVceEyHL98z1EfkHSvAx+Yf0+OT9s58CPgOsnZ3f4fLM7/HY+81VjYhOpsdN0NoUMd1j5Pnq0zTNQStuuCRGAMbyOnjvpHQNnPB4dSYdc4LB8DJXF/Vq7XmzY8NSZoEU1aHquYAdd73ryN8wIa1xG1r7rzv5FwpEfY40kaoBZ38LdamJpXOreto/FGS4vHKflcDRVNDIv6QtIX84EX68T8k/fyZgwTYq2RfxHRxvDXOaiX/WaHN3zFeLDgGw9cMaSK6D56I49BfUcSRAPbz4lp33OW9PuLWFhkX7PU73VHkv0qeK2yegVLjyvGsnLNG5yTPia4uQjoW5RKCasDa6UxvyrzQem4ifybDU7SDJuM+p168ZX+46sZjQTv6WPuxeh133TP6+H/eZM2NvH0B61/+kKhBjgKliWzUhWIDO9JuDQp4oHhSTuyOas7J0/qCuDuV4nlSmvXiCoZN8XPxkBDC6tONck3xYjdOEHqAuFAmGybB13FRMrqoaR2GUEnnijd9UjXNZ8vf2t2ecFyfgopBQrIX8K+WCPjbjK5GaxuTeLJIl1aDJNcq4x+8KhpOjDXTpMpA8u96I/A3v+i9P/BOZ/ZhIa362fgugCvTKhNRVoe/ZXwry/Aq91/GlumgSF37XplHapcHQBDlXPqwCSyStpLgWOp1/l+cYUnqh6/xS5Gfp4cp1flp1k2xlf1rIUR3Ids9ykbMJonCdXOQflfOJVBinTc8WQLsBgoUM4ZTyOc17T2H0b0z+kJA/sOQPKflHcjOGAZCBuaB01orzayfXHlCdRiwIV9QluQ1ZEkZFAtCHvLJJH+H0y6Dza8N15F8xYZaA2U6DjgoBc7W9SD/bcZSxidJwP/WM4jS6NVXgybBemypjwkj+qCpxXkuDoY+pdNyU+UCKykl6LHWJhYrkD231mPTX3IP62Ij7UyV/Tf/0f1jIPA//k7zAfA44LLL0Gt65AcDcYaeRTDz5kppLooVAetbNVJOS+kz0C+nTQvGEHj/Md/LwDKcStAYZdZhQmhhM5asyhRnXqKfOyeAOHOXKmfN1ju/KQKRTHwX9xMsZ65p8b1jHDH97QLUhV+Ehz/cs9Qu5YRe89VWRtrMOUYeD/Kuhk/8pq8m4yF/po2H/OY9+6Icj/335zU1GAGgvMb4kGsoms035Ap4d3IzICpyJ8A2C3DxWWuOHmPgBsgnIVndID3uhx4ORpUnJWtZj+LX0welUK9poEDgZe4GvNr/+6D+fZCD2IH7n/NDF6Ki8npVEmX3ZL7ove9dVyKhDD/J3E3Q0kIVilJ1Sh0jTnWbgUVm6vn9IzD70A+nDgHA0CIDk0yq36TStMRBDuuxQYe50Vh55vfvdwA+0U9Oiu90zT3cef90gTvq0vzOB/vVHSGuWx0YgEklrcQm4xHNqIGXLDWT6oSvxxwcV5N9j0nNVWUkWmYre42nRR2/XJOIDAvG3VQm+ybwgeDbybznvOkKOKcl2L9bebx7ii2Vq+rLmXqsZL+V65mhmTMs2TP0cTs/q8Z/4hfz4NMnGDwSm8txnmbd5R6DcsHgbm4ynY8h1SOmSXgAofimmlHbtEAcvRzydT1CqRkBqsiGbfsgjx0hkGPL3AZNNEzJbpqHPz0b8WaPr+qLJWNAy80EVj4HcKxLcCE/7rMRfo7A3+ZcVqEkecAEd2t/JtTDVZ5g0kEsg5c5B/qU6qsmfryMby3iYK4NL95IxXzv5K3/RE/6BHM/EnwQp4ocAU9QvAVjnzB735S2AMltxwvSkO1Foi/biwkIxyE4YszpkysXru5Zmyeg/Keb6GhVWXd86scxI7sI1vdqImeA8FiTKSD3OynPynIvnWrkMkGTaN6kvZnYgSEr+8+HhesyUcICJ/DmFrEwhw0TMXlKuQV8Dgx/jDi8r0nIK7VvIPzuey5L05CHAdKyOtwBqIA0Q9F70zqAsrYgBLBMCXaNaJotcVebFYTr9aQQfii2yJVfBYvWbdbgzzSJqoZb+8J6/mfgP4LxOOo6CpMM/T3IHipxXr0WW3hVeVBgeJYJM5OTE/XWxNrnHg3iijLOeUh1CUnt+Ok6pb8UFuf0f+ok+8Vsk/6OqwJF/GgnIHgLMfwtgoIjixG9g3d5AZ3qExRNY2j0PYkrwVC2NHljJ39zQXl0YWyhua5zRVZfpFmMLnJP4VVFZBwrNpeMokagm6Z6DBOzqqg2VQsaZyJ+/FpqutvrkfCpraBUWEkx9iPV1CNVRw5d7q4LXTTLxELanD/vF+/Info9aCuQ/HdMueDYNABdnx5daKtBrQjI0o1FB7O3TJ1MpTSbv50Of+r1ZVdWcLhVGiZV6KsiwRkwsUNM3Vi+5WJYmMh3CyNM1ZlsdljbVkoUi0Hp9XCKV5F9dL7LXgC7PsD3a4yE807mFNmJm80vlrXUce0kh/3gLDU4Nnsg/9vYPW9nzj4yDkud/9x8CqppUPJZAzUzciMoqFw+fU7kMq8NxnJeuDSIViPSXIfTtWt1ILZcuerhMQzShqvqayZPIJ+6IoZ20fDEjbyMle1prMjk2GzPKOZl0V45DNBYqeXkmOVrEb2guS3u8LZgTF9XVYXybZc5B/rV1yOXoMuhCxUu6FfxrfvN2IW31E7+QPwgYk/8pLV0BiH4MyNNi21nZe4OT5WaTGUloV1MKUPZHamalTtBmTgWL6NIHMelLXSa2QZChl8IGehFXhnVydunSMrCivKFAS/NP85WxnXO200ZIypLiifGYNAlPUaSaOmzuk1PgkuTvQt2503kgCHK8rkZCtqJqeYEMWhP5N8qQ/IyecKFiKlMeE/E6P4C23l/6xG9C+kEg/8BFAOI72AvO3OfmIM+FiIkdOYaMQ0YuxXAxRKeUNYM0T54rkdk7HHFlaK9lk0GA/FU9AboMuSnntDW6m9p0rZV4iaNRTCzQTP4VCsSBhrK4YizGpROfxUv+KB4Y5DvJetrgJf5eRByJpF2MueFu7qMO5G86v5br6iH/lmsHWf/y/Vpz8x7mxyXsL5F/tAxg+MrfTP4nhNxQoNhm5CqeEOOaI2G15JeGajqGK4vC9gqBlXmM2OzRL7+ftyAQOZp+skxDuk3qMJJ/uaVkv/flyYYlttdhmrQNvdNK/EKSXVU8LXnK8dBsAjSWCZZCErqTf8u47DV/yUlyUR/5z6Af4F481MJIwR59bZWpMTLIKFuV/PE0N1JmBEjJ3/+u/zK3zK/qHVJz8k/IXvm+PyTkD4ueU5lNIvtIGrxNG2ZlMEm+5aa5QTSc7jKkD0NqIfxDSmzR03JcE0K8JZfH86qejI7kZakq9vKxOIXZdLqEsNAuqz5iJJNdN7K3GpyWpZKZGp9yGW5yTDTUnB9mOwZZo5CLfI0F0Jjo6YsKIp77PRwnfu7aqXfOuV7zs9SFhoxW8jfcB/GeRP6iomI/zK/76a/5peH8WTVD+nOVLPmH9JsA7BLAQA7ujmmZtIFOlrLhRCdWTse8fzRWxfq4un1IppQ0eS2cRntGR1BdsTiBGdm8YBeYCvTosy4/zytnnoxPTI/pPqcjAPjPsclQMGSuQfwOse5ySuHYG6Xv+Iv3/1le8zPWY0GR/PsZGJrnPyMoatg6ECKCBsgf/Eu9/IX8qaefGgoQuLz0AcLZQBjfAWCvKoDCyScEOYupALNUbhuYUpkGhOVncpV2ca3QZHjQSbTTzVusku+3vqRPM2snDEehlu5D6aB24jYIYDoOJwRRA9YR//9Q3tXs2HUc5+o7QyYW7JiKZDi2nISKEMgLO6ICx1vLG1FSEMCMlXWiTYDAC0VPYGmTtbTLUlknAKm8gOg34COMgFg2IlmcRLTImbn3ls+556+6urq7qrvvzFAu4vKc011//ftV9/mZQKZFOxhBKSVvZkMln8CjqcBp/GNsViFthEw1z5/5Suq8Yd9W6TG0aSX4T7dLpNwZRjGhLDvfOMU9f5LuCMCP+fQDQTNvBPy9wMCtFh7xLYDHnSQ0BZLGojrNXOqv2CnrMrSm1fxwDDlT+jjYc95ZBnVzBKR0QC4AEAa2hMUtyWsIXWil02nKqGIVBZtPikaF2YnIp7nmMeyfVMrvTxidMO3+taiwQvC31LG2b1mLowQuzsZHTOw2nxn4qdJqnlJbxjqtzJf6cjAzjfNVeh6NG9+NL0dBHgj4A0l3Hh/f9p+9cmxXgPHOwYKTZBY6FP21l7KeJLu0FXgjaheLyrmBT3gT2E+P4S15juRSvZjETD6JTke63ZrzqyTf50kgu6Yuayg58Rcas64Mm9Ie9JaCflYknhmb+GKB6fzpaCyYDDF6oeBPMBQHIkrBFoFlIKqXDcNjvhRpb5MJJi+LbTUDf8zkJzJIvku6FOnvCv92IOz82d8HbXq9BAbgAToIcpFtf5cICIJbALg8QLIz61jb0VpJIVyCYn/dNyA0pGOJS3TI+GAY00GfrNWCrcS/a1gMV1jSJFtPfC1HoyAsbkezCwByeN1CbzvGwroglVhbPG8mbwn++YlRH0CO01JqMkxRbqIOeKsYFGI1AYgigI6K6hjpsFm6hx8CmLrMpV/5F9RpLfiDPxWK87xhnIQ2nPBdfx/svWth238B//hX/sRtf3o+3QKQ/hww/54OplaCpRMdk9Pij2bh78cmvAnjkQMdUHHw1xfZ3yoar0cFvo+tQH5njXkg1YwAUqXtqKXYh+GL9eUynYbRqDdmBwtkI+qo3nN6wG86lzhdSlejsV/HyycrKKDaIKSwb1vaF8ORDewaMzxm2zlAVgc458RTBP5hmWJ1OH8GPaZMMSdNLmrf95/SlnTIg78TVvmJtPVm43l6yMeURBYglCgeUywb7DJUo+jNIrGszzFiYZCIwwSCvFZ27IiCgIvgzqxPiB7bgL/UaBj4dy6EezaanIid3m6Va4lgqgm1ClwwLcLcp30cU/pKyksNqvk1DIU+oWKiy+otBH8tCfPIkMxmN1QsJowBRxM6j9f8iu34NlKYMOVH+7AG/HeH2IN/QjAg3dMvAX/gOwAr8G4FMG8Pt7iBg+7f4BKFVpihdbh2gCRnuW2wqAx5IAvh9GM3ACEII/AJiv/hWgjk+DUG9uUggObTzLEtvQfykOYJutsAPXFCSgsrJ87ekjA4gaZGrWCQYzU1RnRaqC+iWG9WWWOmkMXHxjQ9BEJYWH1NeZkHZv2GPqoBqCSfxocUXzxRWow2mWMwm5B3v9X9fg1lfcnnuwS7/zBlyYCnq3kJ/CG8Fv+sL0DNyl++FeBgs9563vY7AEfd8foYtwSYEkahS1gQgjUvNs0L+aZ0fr2kU4thfuwIgm0+WBwRwAR/bM527Cjl2UjZ2c4b7AMbsZpupb8Bc3EdsR7XqogYvSiQN2QysBDHAU9nz4hk+3LpxK4G//ForTYt8GbZCsEfDeNEW4esWxav/o320roalU9lK8OYC+CE+vP7PkByftMUdSfudio02/4y+MtgjwzcxTyexoKDs+32/6i/hx34HocAHS+3BLoAcqdEIY8DvBP4AOJ+SBMZ/WaMA4iCNZeLTXJOcS75padUbQtsF0XRAdfAMaxmaCJSKZhRawABT66SifTr2JiiqxzH5PZej5bylfiSA4qAV5Fp8cPyt0rEOQg9aTqnJeeaVg/6afku9SeEWWIEz3nfdyXjdVSwPJEvgb8QDBSCP6aAHpZAAMaH/+aAoTuenJwdU69Xm83mCECeLKRzeu2BP+mgux+GvDhmIksjLvtgjVRheD3/YBFCrp/5nWvakqZ3WY3shzQvwXreNDXcbL+hE+qyGQtfVF8RZ1rUPx8ATcFf4RwZTyhIAtAxhuXl9WSUCtR20DuY/EFDYTCqBIo7AOrrwYldD+dzRzNGcimdGru5DG3Ri3giYy6no9WOBqMQDxKFT9Zd12a4ALt923/MbwH+7Mn/Wcb1DwBuuz63uke9P8TN9iPcdiwrN3e46atTdLU9VxapNRzKEa4gYAHwuSOTY/AQC5cTZCg5xivlaa/BmC8TMg0os8S2Oy6CkgDfGPjLMiv0xgSEtmnZDsk61cimGZZnbAZyxCTfXXOMx0n6SsvuAb8UbsT4VUoNMpI/Wl6rbg2fzX44a+B8PjFk5yPtw34akGxVVo2tKJ+RUrZYHh8juzRN30n6SQF9uB5EKLgnHuyrWPlP6SH4M5nxuF5vYOPA2wE47JD/aPdRD/qX/FIRJwV/xkvTUxNQDtwRdECsBWutvryWySoq+AS2Fh2+lDB6sUc7l0W/EHXV+tmiPhWgT/ub96wM+uKY0eAnYjwv5YqYgEY5q52GQpZg1BSAtOtMfDGVbqMau6V9tqS9C8G/QaDB11zBYhRKd1WGAZh/z78c/BeQB9Cv/IWAYXe9gtP1GtZ/CP4OALjt3f7dwKurFeSIg7hUeS4jpyFN41gmrzLwj7R+y4/a7Is0UW1zW42ZaQNXFaMh6Ad6ChSqRSIAi34oCjIXy0eRSTUuMJugkIkxsWDaBLpgE9oH+Fe8akfDOprGeV2NXS0YJ5JNdjX2WgB/ztZ4kPBq7nUIHkzr9PuZ/jv8GfAXr0G58hd4k+/8r8DfIRiu1+vtvRf/+X22A3B6drS90m0LXLlyLVXcHLDHeGrIZfNiVlHgRlGDqqO5PNuFEl6Ak5jLqPAngn06IcFuy2rRTE5J2XImHnTHWn3JH6KFNrtfyvYsaTNrdWrboKS+zwH8h6QQ+FXv9xfbjGjNFgPzPNm5IMfHeRoEGiyVAv/Sm7GgngfpnV63n5X/7JWb/F9lH/hbVv4rIc3BWbf63263H/HSrJ68dee4M3Kvy4QYaSqpDfhrehoyq8h+Gb0zG4L3wFvsB2CfoPZNO99w+c2J+7IFumr2MowTZFZ3SphdV+mLqJ/q3Kow2zyys3Q1OD386r+jvJw78htExikIC8dl4I4C/NVVMxWGXGqJjt1iinQOrf/83ovClN8+oSEkfACZNqv6ql9JeRU88jZF8lJkz40vMZskJsQn8M8yKua3BeAV4C9eg3LlTwMI8MAfpF0At/LkaUBwcnLaG73DS7QavfnFerOG/GzPUZF3LKn2JF0YkYeEDzEeCCdpTKgCweRlJKlMM+Dv0flYs0SZVYx5W1XCKLdxreoJbErrPFs9cZ0odO+YJJKz3apm6itW8ooZGzgROZViYbxqyAMHhZCluUy8SseZzkVqggPyQ2WQpmnTaFkKl2daeyhlMJ5aMuhwEREeKDurDRxAGaPgz0EdIHzVzyXAf/TKAeTu+Yuf/SUy033/4WuEDk7Xm/4bAPd4kcYb/9u728129xe+IiWH2JQDKRDHyNTFJ1iazlSofty8dH2ZySsPARxMFbCh7dy5KBA7ZmxFJw0tCcItqyYAwEIdmMsMwUCS5av8iSXcCRj1lbgc+GtsyyQTO7X6hwYhVb1reLkoGnhluOXfL1XHlNqgXwRhoZEsdk32hAxDnZXaclM+43HsN+VjTJmiH8c/6+sgvxOQ3vafvQ7An73LL638wX/tj678cVz9b7bboxd/9r4cADz5d/91t2vw400XJZiANwbEsWup88X4H3dCdh6tj1TlNKacT1EBqZEUtpoULTORtaqyoC1KdaQy4yJ0GzgF9uBdE39L3C4tKloYxnPFXQRPxOpbtv8y3Say3QLBSIZ51a8lS71meaaAw6pHGJ8af3JjLjOeUCHib/1baRiBg4cU5JdWXXpZbOU/5jt5p4Cu8KVdgSkQiK78g3f+Vx5PHwB0px9IpZsf/cctvrdhfynITIXz5peKApClk3QzVLQTnQtNZg3MaBdJK0M5qYV+irw1t1WyPmHkfLh0CJEPwuRAAuvqA6WEjDKTLVSrjcoG5xFWC/ib3KjsG0RuDiHUYD3ZVpsgCajga0mYvJRFavraBL0+j5PEUZAx2VFu91PwB8W2P13J70SkXYGJZwWoWPkvPEuw0T/8d7JdwcMr33xfKuHy7t+hu9PfAqgOAr7sFAA8QPD1vNqVZClxkFe7gSVCoTjYxNJ+QFjXtRQA/r5AHyBXKU7gcuyH5Dj8iD4zmHGDQUJaVmWPMVp8DOpU6ZdFv8mXtIB0y8YxJY7xOqFbh7YR1MAvgr9T8BXYxZTN+CVk2G20OLAbFxgCOq3z5a+xYlxd0pbmQT9IgzxAAvwhCf6YAn8XA38//bcnG/j8qRv3fvjGv92TSjkHAE/+7X/2DHfXXcSAWNVKXw6SwH2eMyUQUc+S7UjCa7UL0RFdZreIBEX7qNYA9Gr1FDKRstEhNg15Tstkhst0gmh/vi/oG4bOoirveKxuM4MgKnVYfJnGu0IgxkYhY4JjFNq7PQltail3SxeyfJWBHanPiZUGysvOQKSRkk08ZCIawX+WzoC/oyt/lwD/Hpr9+/r+A3958D/dAnz69Rvw+Vf+9L1YaVd+0fGdL+0ugASS86t0EAK+iELFM1s9IRS4gYlfhe0iEuqz0i2VuRa7MVlx34aLZ82UBwESKTAdagAR/VbUg7nKMBxbBtFlvFn4FRkt+xMhJx6nr/gvpp22zYJXelO8oCuXCYytgMw6iManeSzmeBS2CDl2Pr8yW9T4JJxwBvB3sDx5n1v5w/IWQRr8B/26e/7TrgBJX12FX159Hh498czRq6+++n6sxF4AsHsYsNsF6B8GfCx2ARieeOfexEk60TTZeOVDQckFUVAOULgTq4hGPjRRxNJ2MyS0Ja++GoE+xhnIJp5XFG6ervD59r4DPZ6pKTmRWuViTGQ8FYE+2top0xZ6XkkcQbvydxMr0N7t9wG6/Zzt5lUfFBI6qbbsUxto+GryZx5MCyT0OIGH3kKbz4V8ix0KzpaVf/AwH0B05S+/58+3/Yet/9TX/qSV/7yjsLoC97/+ffjtE9/pn/5/J1XiVVg/3S5A9//Z2Rk0JRR+NM/jQ18GALwomYK698K0BOQoOHKJSKoTlYspIWMZi32QFNEj+KOyiQ3BJO0PYptbdYVZTmRHT7T/b9l+DFXTbcq4Dwj2PX5mSJdoZqlusLnwhg4QrbBIY1ld1PJP08146WgiCLs+sLR5UqfBdlbYoi/FnBxGKJ7GdSnGI6YzqLsc+EPwj9jDnB0ksMNX7ADiSt658D185xKv+rm5EGnwT638V8mVP7grcPLNH8Anq2/05Umu/gGEAGDYBcC7/ZcBN+s1BCtpDsLJH+EJBj3hAQB/RQAQPj0PrMFAkXZJCBO/YiUVvnB1VYTy0WtbaNc8ns4GSrP1ME1C6fp3pNh0YqKrfSBpjuh3QMZMCQVuKfuKqQ9goRwXVwolAcg6wcfUKIUYy9CmGIA7V5cHfzTZTSTqy47WiooY0ahARb6mjQn4U+CnXYpsrMvqUk4gjIAKAuhPR0e8GsF/PPdW/oTfA3Kg2/7+a3rL+WqXh4Urf3CHsP7OD+HjzVPdAn736d93IEPiXwDClXurPy4PBEotVdIDUEjHSPpjRLyKlPOvrCiluJAk/6qUUaU0DZq4GzU7jX6UKrtQZy3jaD7Xo1ESpMF1CYmySkBTgz5hLgJZ8GdqrZzVhpofwQL8y5Pk/hGAFUsNwAAmEM4FAVpVlmcMxERDH5h5MuOGkYvw8VW/N75wCMai4VbS1yET5yfy4+C/eBgDf2f4vC/kH/iDMDhIPfCH7gDWz/wNfHLwbfji0aMet7Orf4BIANC/EYCwfa8v5Onp6ePxPMC+SQvu5qpKRQ8FhJD3sVgpvYZmLidNcgCp3R5X+YrsOJAj2fz9fQd8ZQ9s2pjOK8tAfFhODA1gAn6rnMRv7BwI+/HJsMNCb+MsoIPzEb3pOdztwaT90nqomROU/SIXbOgNghX8l+Qw0JjaYKK5vpHWvyedtDNn4gj6KIH/8psB2aXAH/xriIF/ZKsf2La/04M/dOC/+fYP4OG178JvPru/865b/f8YFBT/G8DrzdudxaMe/PudgN8LQuGIQrokI2ZofhXUWJ2snCaFg7OpTQnwvYxKvUrjdELpaV6BjCx0og+2I4M0Uij6Cl/NvFojq2IskaP8BaCfZG/gk5KmDSa+0ne7SXoBfvoIktT2vv3aesA4b1Yn6mzmMjT2tDw5YYRgx2Uaa5P4EHz7b15QftCCvwfCQxqCtPUvpUfAnwD4Av4AMuCnPvIjgz8w8F9/66/h5Knvwce/+vVQOsR3utX/ESjIpTLv3/6HG+4AP+xOrx0eHkL/e2yIj8rUdRNjtKs2prnXw17UZ5XuxSbXz4d4C51lTLMXuFxTbmlS4mncBgcVE2H0wiBnZLQ2Q4mPuUnaxC+pKKurcCSjl87583oVzGjIUNsuHdeo5KP5jcrIePj4UgdHSTvSNj8JCOa0cSngpHSyKzCmc4D3V/7gvd5Hv9VvAf9g5f+tF+HR0y/A//zy492D+/3W/82bN58FJa1SmU/e6m8FwDv9eb8LcOE7AShcx345OZPR3A9ANlxAKJjledXKYwaErAZFyuunhS7Ux/WqhRbyVu6KOZCvUobz0QH2jYki8C+p/8qqNNssbUPMKkwm5fWXgf9ibulMHPhVqvlDzQb71ItcUtz2eYJ/T07JF6fs30hAepKYu0DjihL8gQE6+Cv70XNIgz8B7h2ol4E/X/lv/uRGAP7arX8gnmfp/gc/fdc592Z/nt0JoOEaP6dWY9d7W+Vyh1L5e3JAMr+3siqMnIttEKq1oWF1k9kn4nAbWKGgtmioTjTIK5lKfNeCnMdvyDwH4A+nn0VH2aq/pj4q2qYq4GAZansK5pw9YdwhP87BQeKRv1zfmr7u5z30lwB/D9BBBvZJJgn+hLfVyr8H/2/04P+r+ZX9zWbz427r/y4YSL0wOf7v1z/sDi/157sg4OAy3Q7g0YOECOcWZfikBqjWRi/Qj6itRsZrQC7DmotXl/OhkC4yiRWRN6EaGktdH6R0Nf1BO/GLMoZMM+DW1ZkjGR7IWOqoGPxr60DJrA0wWwQcGlsZ8O//c5rFQybI8EFcAv3pSME6shOQ2fbPvttfDf4vdOB/wwP/buX/1iuvvPIuGGmlZcTN+lbn1b2+5OuzNZye7ePtAIR4S6Jw5PycR5KP5ReS5A53rXU1ZQ0LWQB78gOYvdEgJvwq1g0KNQYAGH/TEKRZPI1eD+fdv1av8Xk6jI1lsi30DyuVPuCnzazqq6jzZWSj7dkn4gwWY5qlbS3fcQj0WkFNsqssu5ih4SvgyeTz8TWJ0SBgbgcIH/jT2sLdt3pT4M8BHJLgv3gsr/ylvHbgfwMeXPueB/79Q38l4D+VQk33b//kmlsdftj5cmO3JdOdXL16dakTca2UWpXHXIrpuCAqKcZeCVVJ50IecDWsHKOaZRK36aPNKosNuV5+beCLUoKx7koCoRq3cfqvxc5EBeAFMrV1tiQ6lsyHfXv7mfrMgmhNOxT2D1QwagMNRnzmp21gr6NBEkcFuXf846/zkWv+MKBi5e/L2cAfvI/8DOD/6Veeg08+/WwpZQf+N2/efBsKyRQA9LQLAg4Ob3eevTSlDc8FHMBjQ9LIph3pwkGeTzmYZj1P8oBgTmisvyEjhs2ZunYxJRNXLYh6xo3KVOwNgV+zSs3KqTJsPmpAKOCXE6VpwGmB0Bl8QXViOyDW2G1Z74msoaowCvyT+I4PaRrKYzLpN47/jyt/x1fyABz8py8BiuBPwZqlpe75e3zJ2wL5lf+jp78PH7tn4ItHJ0spK8F/KEkh3b/903fdyr05jYFV5+iVq1d2uwKXgngHuXBQ5yRBUMTJi/KbouI+VvpFKuJCdD6WwieX0ORidlrUvWXyr2Jt1HfOC/itvjUG/unKMX4vLaq7jR95vpZ2K/qHpu6VwYYUcHtTDZau+pdM3YrfB3/6pH90lb9LbrXyz/9J3x78f9Ot+v/3yp/BZin38WazeUvzpb8cVaH1/duvv935+nOadnhwAAfdboA6ENCMtiBEF9IuFfFCKQbOZQhQ9r269+xYmOOV43maWYFIWv2U8LSaSgMncx0BVO9SaACjgKVdYFIGfkut99OsA/rX++Z+YFoJl7ZhZSCEij4UZdljwKEI/Ciwc/CfOXHZKXBmW2MmDtv4Q1WFK/6l5cnrf/yhvik4EMGfvMu/Z/A/XSN88gfPwvETf7F8QwDxqMPWWy+//PI9aEBVAUBP3S2B62518GF3en1W2jnbBwKrgxW4mIlLC94lpAR8HrxcCsAXL/Zop46ZV5vDAP4CXgn8pzMXJpZRzAmrYrMfDQKXoD+WAa2KweojgkloAhpBSQA+8hf/GviiAMQ8L+XBASCqvuWv4YvxoIInkSis/CcKd+2stmgmAfXgGNkFcDw9Bv5uzHJELgP+0rb/dF9fAP8p72y97rb6T+Gzrz4PX/zR8/MtjA747zx48OCNW7duHUMjqg4AeuqfCwA46HcD3uT4dtAFAv0rg5fm1oCaJFBXRi0G1nOj8wL7IvV+hcVio5kDIcmDkeOkv2jVlyPtxFvBtjALpS8pSzDBG6LS7GRs4Y+pKQue/LaXwV+qPtfCl5YAbLGtaQ+tzbnyFPNcKpGcxoLwMBBLLJ4SGdOK3w7+bvYk/iDgBP6rQU8p+Et/6Y+s/Pu/4Pfw0SM4WW/hwZN/1YH/X07gf7xer9947bXX7kBjaorKfDfAm3y7gvSBwGq1usBgIDvMwTQyI3PxhVPNiq7G3nSuFtBPqsEELlwD0+6nDdu/5slQ4VsmwShvFKgC/uDCKKvIOEfgz2YkAKnaF0sAqAbiUtsVQSIqGBsEG/XAv2Ta3vEPwd8Hc8pLwZvL+ec58F++BEhX/qvdK/Un6zM4OTmFLfY7D1fg//+4A/+vPbfj22637z18+PDtlqt+SntB4vu3//6fOpD/eXd6fazPgcZ+2QcA/W2CPhjYT0Agrf14T1IgVsHC6EKoFoSa2W3GHLBLAQBldVElbve9iuYdvbTOiwC/YeCimeCTssoMqwnN/e2MjUUagx2gXX6RTzYfshkqEDbUhSbwaAX8Hm8iMaHGMZ6q+/0zuCtBPwB4gOgWPwhP/zfY9l9vNrDuVvjr7RZOT89gS3zYukP4/Kkb8MVXn+313e0S32p1rz9GewkAJhoDgX90sLwy2JMHz7t6d0MQ4NwcDFyKWwYFc5JdcWpta1AVFUvrihUv5pXnNQL7SlfexeGarsbrJn2boAuTGjWyVx8QrxveXC6S1gzwqeESZdlJuKdIQUtIAF1eR5TNZX3TJWt9sZoL6sgchCwnbu4ZMmjyZxpkZU4WNPmSYGDtQkUcMec8f8oCSITJdT8QCIMAGD4G1B/Jg4E73gmEgD0k6BzTtcjxlf/As2Lg33+dD3YAv+2W9j3w97Z2q3xP/wT+3cr/6RvHD7/653c2G/wP6yd9S+lcULb/q4Jd0f915fBHncnr8PtEFBV4Oqfdgz67TJoI7VAgQgyBRDcmb4TF6JDkT1AxchH7SPWT9KaEmcIV6STnQlPRVamU7oFbgHJQ4Vdics3KAuTjM/pEvc80l0UQDQMdwS691iIBNaAp8tgXhsAtHyRTsV1aYCcRcLg0iyijEPQANdFOM3ezWysCAwH+VCzk+ArCYCJIHucN5CAP4AP8DPgAdOWPuMjSoAAAAvBf8uTzcKfBDf448IB+ee2Q7CYg3H3wted+8esnvvvuvrb6Y3QuAQCl+7dff6kr8U+68v+oM34DvkwUPK079nbtE7znAfaUMoATYoA8KfFXqxbOKTRIdDTfQAMaFdKZuiXwC3UWa16+68H1UDknqK7ysaacJlHW7iXlCupUFYHIejKUA3yTvyhl5Hgy5LVdPgBI74Rg8jLtAzD/Mc0Lsn8Tiwt4MwETpt2byr2dgJeDOfrBAYAUEKTAP8yTnjGQ0x2g+AriyItwjG51r9sZ+GDrrt558V/+/QguiM49AKA0vD1w2O8O3Oh3Bro6e6FL7tLwWgce1+HcSTEFa1b0rVeZrUjG74DE1SkMILas8kOAc4KZ3TUSXfuOcTwEamAs1iUwz7iEPxCkU5H8dnbGPygETFFXnsRyCWVQB37iaponRsql8pkBKgsAXMiRVqPNKGmGBEhKQVZsrPo6HJi2+aNdaMjk7U8DES+N5Ml+RSiVNQM7AWwUVvheQCAFBzLog6dHC/5sV2DXOK4D+f4Hx935UReofNQx3Dvbwr0Xf/b+Xu/rW+h3mmpbrGQmFkMAAAAASUVORK5CYII=')" + " no-repeat; background-size: 50px 50px; background-color:" + buttonBackgroundColor
            + "; border: none; background-position: center; cursor: pointer; border-radius: 10px; position: relative; margin: 4px 0px; "
        button.setAttribute("style", buttonStyle);

        /* status circle definition and CSS */

        var status = document.createElement("div");
        status.setAttribute("class", "status");
        var length = 20; // for width and height of circle
        var statusBackgroundColor = "red" // default background color of service (inactive color)
        var posLeft = 30;
        var posTop = 20;
        var statusStyle = "border-radius: 50%; height:" + length + "px; width:" + length + "px; background-color:" + statusBackgroundColor +
            "; position: relative; left:" + posLeft + "px; top:" + posTop + "px;";
        status.setAttribute("style", statusStyle);

        /* event listeners */
        
        button.addEventListener("mouseleave", function (event) {
            button.style.backgroundColor = "#A2E1EF";
            button.style.color = "#000000";
        });

        button.addEventListener("mouseenter", function (event) {
            button.style.backgroundColor = "#FFFFFF";
            button.style.color = "#000000";
        })

        // when ServiceDock button is double clicked
        this.addEventListener("click", async function () {
            // check active flag so once activated, the service doesnt reinit
            if (!active) {
                if ('serial' in navigator) {
                    console.log("%cTuftsCEEO ", "color: #3ba336;", "Activating SPIKE Service");
                    var initSuccessful = await this.service.init();
                    if (initSuccessful) {
                        active = true;
                        status.style.backgroundColor = "green";
                    }
                }
                else {
                    var bodyTags = document.getElementsByTagName("body"); 
                    if (bodyTags != undefined) {
                        var bodyTag = document.getElementsByTagName("body")[0];
                        bodyTag.innerHTML = `
                    <div>
                    <h1>
                    To use the ServiceDock's LEGO SPIKE Prime Service, you must enable the <em><b>WebSerial API</b></em> in your
                    browser. To do so, please
                    make sure:
                    </h1>
                    <h3>
                    <ol style = "font-size: 20px">
                        <li>You are using the
                            <a id = "googlechromelink" href="https://www.google.com/chrome/" target="_blank">
                                Google Chrome browser</a>.</li>
                                <br/>
                        <li>The following chrome flags are enabled on chrome://flags.</li>
                    </ol>
                    <ul>
                    <li>Mac OSX user? #enable-experimental-web-platform-features</li>
                    <li>Windows user? #enable-experimental-web-platform-features AND #new-usb-backend</li>
                    </ul>
                    </h3>
                    <h2>
                    To enable these flags:
                    </h2>
                    <h3>
                    <ol style="font-size: 20px;">
                        <li>In your Browser URL, visit
                            <em>chrome://flags</em></li>
                        <br/>
                        <li> Set the your required flags to "Enabled" via dropdown</li>
                        <br/>
                        <li> Relaunch the browser to have changes take effect </li>
                        <br/>
                        <li> Revisit your Coding Rooms classroom (this website) </li>
                        <br/>
                    </ol>
                    </h3>
                    `;
                    }
                    else {
                        alert("Error: Please make sure you are using GOOGLE CHROME with the #enable-experimental-web-platform-features flag ENABLED")
                    }
                }
            } else {
                this.service.rebootHub();
            }
        });


        shadow.appendChild(wrapper);
        button.appendChild(status);
        wrapper.appendChild(button);

    }

    /* get the Service_SPIKE object */
    getService() {
        return this.service;
    }

    /* get whether the ServiceDock button was clicked */
    getClicked() {
        return this.active;
    }

}

// when defining custom element, the name must have at least one - dash 
window.customElements.define('service-spike', servicespike);

/*
Project Name: SPIKE Prime Web Interface
File name: Service_SPIKE.js
Author: Jeremy Jung
Last update: 7/22/20
Description: SPIKE Service Library (OOP)
Credits/inspirations:
    Based on code wrriten by Ethan Danahy, Chris Rogers
History:
    Created by Jeremy on 7/15/20
LICENSE: MIT
(C) Tufts Center for Engineering Education and Outreach (CEEO)
*/


/**
 * @class Service_SPIKE 
 * @classdesc
 * ServiceDock library for interfacing with LEGO® SPIKE™ Prime
 * @example
 * // assuming you declared <service-spike> with the id, "service_spike"
 * var serviceSPIKE = document.getElemenyById("service_spike").getService();
 * serviceSPIKE.executeAfterInit(async function() {
 *     // write code here
 * })
 * 
 * serviceSPIKE.init();
 */
function Service_SPIKE() {

    //////////////////////////////////////////
    //                                      //
    //          Global Variables            //
    //                                      //
    //////////////////////////////////////////

    /* private members */

    const VENDOR_ID = 0x0694; // LEGO SPIKE Prime Hub

    // common characters to send (for REPL/uPython on the Hub)
    const CONTROL_C = '\x03'; // CTRL-C character (ETX character)
    const CONTROL_D = '\x04'; // CTRL-D character (EOT character)
    const RETURN = '\x0D';	// RETURN key (enter, new line)

    /* using this filter in webserial setup will only take serial ports*/
    const filter = {
        usbVendorId: VENDOR_ID

    };

    // define for communication
    let port;
    let reader;
    let writer;
    let value;
    let done;
    let writableStreamClosed;

    //define for json concatenation
    let jsonline = "";

    // contains latest full json object from SPIKE readings
    let lastUJSONRPC;

    // object containing real-time info on devices connected to each port of SPIKE Prime 
    let ports =
    {
        "A": { "device": "none", "data": {} },
        "B": { "device": "none", "data": {} },
        "C": { "device": "none", "data": {} },
        "D": { "device": "none", "data": {} },
        "E": { "device": "none", "data": {} },
        "F": { "device": "none", "data": {} }
    };

    // object containing real-time info on hub sensor values
    /*
        !say the usb wire is the nose of the spike prime

        ( looks at which side of the hub is facing up)
        gyro[0] - up/down detector ( down: 1000, up: -1000, neutral: 0)
        gyro[1] - rightside/leftside detector ( leftside : 1000 , rightside: -1000, neutal: 0 )
        gyro[2] - front/back detector ( front: 1000, back: -1000, neutral: 0 )

        ( assume the usb wire port is the nose of the spike prime )
        accel[0] - roll acceleration (roll to right: -, roll to left: +)
        accel[1] - pitch acceleration (up: +, down: -)
        accel[2] - yaw acceleration (counterclockwise: +. clockwise: -)

        ()
        pos[0] - yaw angle
        pos[1] - pitch angle
        pos[2] - roll angle

    */
    let hub =
    {
        "gyro": [0, 0, 0],
        "accel": [0, 0, 0],
        "pos": [0, 0, 0]
    }

    let batteryAmount = 0; // battery [0-100]

    // string containing real-time info on hub events
    let hubFrontEvent;

    /*
        up: hub is upright/standing, with the display looking horizontally
        down: hub is upsidedown with the display, with the display looking horizontally
        front: hub's display facing towards the sky
        back: hub's display facing towards the earth
        leftside: hub rotated so that the side to the left of the display is facing the earth
        rightside: hub rotated so that the side to the right of the display is facing the earth
    */
    let lastHubOrientation; //PrimeHub orientation read from caught UJSONRPC 

    /*
        shake
        freefall
    */
    let hubGesture;

    // 
    let hubMainButton = { "pressed": false, "duration": 0 };

    let hubBluetoothButton = { "pressed": false, "duration": 0 };

    let hubLeftButton = { "pressed": false, "duration": 0 };

    let hubRightButton = { "pressed": false, "duration": 0 };

    /* PrimeHub data storage arrays for was_***() functions */
    let hubGestures = []; // array of hubGestures run since program started or since was_gesture() ran
    let hubButtonPresses = [];
    let hubName = undefined;
    let lastDetectedColor = undefined;

    /* SPIKE Prime Projects */

    let hubProjects = {
        "0": "None",
        "1": "None",
        "2": "None",
        "3": "None",
        "4": "None",
        "5": "None",
        "6": "None",
        "7": "None",
        "8": "None",
        "9": "None",
        "10": "None",
        "11": "None",
        "12": "None",
        "13": "None",
        "14": "None",
        "15": "None",
        "16": "None",
        "17": "None",
        "18": "None",
        "19": "None"
    };

    var colorDictionary = {
        0: "BLACK",
        1: "VIOLET",
        3: "BLUE",
        4: "AZURE",
        5: "GREEN",
        7: "YELLOW",
        9: "RED",
        1: "WHITE",
    };

    // true after Force Sensor is pressed, turned to false after reading it for the first time that it is released
    let ForceSensorWasPressed = false;

    var micropython_interpreter = false; // whether micropython was reached or not

    let serviceActive = false; //serviceActive flag

    var waitForNewOriFirst = true; //whether the wait_for_new_orientation method would be the first time called

    /* stored callback functions from wait_until functions and etc. */

    var funcAtInit = undefined; // function to call after init of SPIKE Service

    var funcAfterNewGesture = undefined;
    var funcAfterNewOrientation = undefined;

    var funcAfterLeftButtonPress = undefined;
    var funcAfterLeftButtonRelease = undefined;
    var funcAfterRightButtonPress = undefined;
    var funcAfterRightButtonRelease = undefined;

    var funcAfterNewColor = undefined;

    var waitUntilColorCallback = undefined; // [colorToDetect, function to execute]
    var waitForDistanceFartherThanCallback = undefined; // [distance, function to execute]
    var waitForDistanceCloserThanCallback = undefined; // [distance, function to execute]

    var funcAfterForceSensorPress = undefined;
    var funcAfterForceSensorRelease = undefined;

    /* array that holds the pointers to callback functions to be executed after a UJSONRPC response */
    var responseCallbacks = [];

    // array of information needed for writing program
    var startWriteProgramCallback = undefined; // [message_id, function to execute ]
    var writePackageInformation = undefined; // [ message_id, remaining_data, transfer_id, blocksize]
    var writeProgramCallback = undefined; // callback function to run after a program was successfully written
    var writeProgramSetTimeout = undefined; // setTimeout object for looking for response to start_write_program

    /* callback functions added for Coding Rooms */
    
    var getFirmwareInfoCallback = undefined;

    var funcAfterPrint = undefined; // function to call for SPIKE python program print statements or errors
    var funcAfterError = undefined; // function to call for errors in ServiceDock

    var funcAfterDisconnect = undefined; // function to call after SPIKE Prime is disconnected

    var funcWithStream = undefined; // function to call after every parsed UJSONRPC package

    var triggerCurrentStateCallback = undefined;

    //////////////////////////////////////////
    //                                      //
    //          Public Functions            //
    //                                      //
    //////////////////////////////////////////

    /**  initialize SPIKE_service
     * <p> Makes prompt in Google Chrome ( Google Chrome Browser needs "Experimental Web Interface" enabled) </p>
     * <p> Starts streaming UJSONRPC </p>
     * <p> <em> this function needs to be executed after executeAfterInit but before all other public functions </em> </p>
     * @public
     * @returns {boolean} True if service was successsfully initialized, false otherwise
     */
    async function init() {

        // reinit variables in the case of hardware disconnection and Service reactivation
        reader = undefined;
        writer = undefined;

        // initialize web serial connection
        var webSerialConnected = await initWebSerial();

        if (webSerialConnected) {

            // start streaming UJSONRPC
            streamUJSONRPC();

            await sleep(1000);

            triggerCurrentState();
            getFirmwareInfo( function (version) {
                console.log("%cTuftsCEEO ", "color: #3ba336;", "This SPIKE Prime is using Hub OS ", version);
            });
            serviceActive = true;

            await sleep(2000); // wait for service to init

            // call funcAtInit if defined
            if (funcAtInit !== undefined) {
                funcAtInit();
            }
            return true;
        }
        else {
            return false;
        }
    }

    /**  Get the callback function to execute after service is initialized.
     * <p> <em> This function needs to be executed before calling init() </em> </p>
     * @public
     * @param {function} callback Function to execute after initialization ( during init() )
     * @example
     * serviceSPIKE.executeAfterInit( function () {
     *     var motor = serviceSPIKE.Motor("A");
     *     var speed = motor.get_speed();
     *     // do something with speed
     * })
     */
    function executeAfterInit(callback) {
        // Assigns global variable funcAtInit a pointer to callback function
        funcAtInit = callback;
    }

    /**  Get the callback function to execute after a print or error from SPIKE python program
     * @ignore
     * @param {function} callback 
     */
    function executeAfterPrint(callback) {
        funcAfterPrint = callback;
    }

    /**  Get the callback function to execute after Service Dock encounters an error
     * @ignore
     * @param {any} callback 
     */
    function executeAfterError(callback) {
        funcAfterError = callback;
    }


    /**  Execute a stack of functions continuously with SPIKE sensor feed
     * 
     * @public
     * @param {any} callback 
     * @example
     * var motor = new serviceSPIKE.Motor('A')
     * serviceSPIKE.executeWithStream( async function() {
     *      var speed = await motor.get_speed();
     *      // do something with motor speed
     * })
     */
    function executeWithStream(callback) {
        funcWithStream = callback;
    }

    /**  Get the callback function to execute after service is disconnected
     * @ignore
     * @param {any} callback 
     */
    function executeAfterDisconnect(callback) {
        funcAfterDisconnect = callback;
    }

    /**  Send command to the SPIKE Prime (UJSON RPC or Micropy depending on current interpreter)
     * <p> May make the SPIKE Prime do something </p>
     * @ignore
     * @param {string} command Command to send (or sequence of commands, separated by new lines)
     */
    async function sendDATA(command) {
        // look up the command to send
        var commands = command.split("\n"); // split on new line
        
        // ignore console logging trigger_current_state (to avoid it spamming)
        if (command.indexOf("trigger_current_state") == -1)
            console.log("%cTuftsCEEO ", "color: #3ba336;", "sendDATA: " + commands);

        // make sure ready to write to device
        setupWriter();

        // send it in micropy if micropy reached
        if (micropython_interpreter) {

            for (var i = 0; i < commands.length; i++) {
                // console.log("%cTuftsCEEO ", "color: #3ba336;", "commands.length", commands.length)

                // trim trailing, leading whitespaces
                var current = commands[i].trim();

                writer.write(current);
                writer.write(RETURN); // extra return at the end
            }
        }
        // expect json scripts if micropy not reached
        else {
            // go through each line of the command
            // trim it, send it, and send a return...
            for (var i = 0; i < commands.length; i++) {

                //console.log("%cTuftsCEEO ", "color: #3ba336;", "commands.length", commands.length)

                current = commands[i].trim();
                //console.log("%cTuftsCEEO ", "color: #3ba336;", "current", current);
                // turn string into JSON

                //string_current = (JSON.stringify(current));
                //myobj = JSON.parse(string_current);
                var myobj = await JSON.parse(current);

                // turn JSON back into string and write it out
                writer.write(JSON.stringify(myobj));
                writer.write(RETURN); // extra return at the end
            }
        }
    }


    /**  Send character sequences to reboot SPIKE Prime
     * <p> <em> Run this function to exit micropython interpreter </em> </p>
     * @public
     * @example
     * serviceSPIKE.rebootHub();
     */
    function rebootHub() {
        console.log("%cTuftsCEEO ", "color: #3ba336;", "rebooting")
        // make sure ready to write to device
        setupWriter();
        writer.write(CONTROL_C);
        writer.write(CONTROL_D);

        //toggle micropython_interpreter flag if its was active
        if (micropython_interpreter) {
            micropython_interpreter = false;
        }
    }

    /**  Get the information of all the ports and devices connected to them
     * @ignore
     * @returns {object} <p> An object with keys as port letters and values as objects of device type and info </p>
     * @example
     * // USAGE 
     * 
     * var portsInfo = await serviceSPIKE.getPortsInfo();
     * // ports.{yourPortLetter}.device --returns--> device type (ex. "smallMotor" or "ultrasonic") </p>
     * // ports.{yourPortLetter}.data --returns--> device info (ex. {"speed": 0, "angle":0, "uAngle": 0, "power":0} ) </p>
     * 
     * // Motor on port A
     * var motorSpeed = portsInfo["A"]["speed"]; // motor speed
     * var motorDegreesCounted = portsInfo["A"]["angle"]; // motor angle
     * var motorPosition = portsInfo["A"]["uAngle"]; // motor angle in unit circle ( -180 ~ 180 )
     * var motorPower = portsInfo["A"]["power"]; // motor power
     * 
     * // Ultrasonic Sensor on port A
     * var distance = portsInfo["A"]["distance"] // distance value from ultrasonic sensor
     * 
     * // Color Sensor on port A
     * var reflectedLight = portsInfo["A"]["reflected"]; // reflected light
     * var color = portsInfo["A"]["color"]; // name of detected color
     * var RGB = portsInfo["A"]["RGB"]; // [R, G, B]
     * 
     * // Force Sensor on port A
     * var forceNewtons = portsInfo["A"]["force"]; // Force in Newtons ( 1 ~ 10 ) 
     * var pressedBool = portsInfo["A"]["pressed"] // whether pressed or not ( true or false )
     * var forceSensitive = portsInfo["A"]["forceSensitive"] // More sensitive force output( 0 ~ 900 )
     */
    function getPortsInfo() {
        return ports;
    }

    /**  get the info of a single port
     * @ignore
     * @param {string} letter Port on the SPIKE hub
     * @returns {object} Keys as device and info as value
     */
    function getPortInfo(letter) {
        return ports[letter];
    }

    /**  Get battery status
     * @ignore
     * @returns {integer} battery percentage
     */
    function getBatteryStatus() {
        return batteryAmount;
    }

    /**  Get info of the hub
     * @ignore
     * @returns {object} Info of the hub
     * @example
     * var hubInfo = await serviceSPIKE.getHubInfo();
     * 
     * var upDownDetector = hubInfo["gyro"][0];
     * var rightSideLeftSideDetector = hubInfo["gyro"][1];
     * var frontBackDetector = hubInfo["gyro"][2];
     * 
     * var rollAcceleration = hubInfo["pos"][0];  
     * var pitchAcceleration = hubInfo["pos"][1]; 
     * var yawAcceleration = hubInfo["pos"][2];   
     * 
     * var yawAngle = hubInfo["pos"][0];
     * var pitchAngle = hubInfo["pos"][1];
     * var rollAngle = hubInfo["pos"][2];
     * 
     * 
     */
    function getHubInfo() {
        return hub;
    }

    /**  Get the name of the hub
     * 
     * @public
     * @returns name of hub
     */
    function getHubName() {
        return hubName;
    }

    /**
     * @ignore
     * @param {any} callback 
     */
    async function getFirmwareInfo(callback) {

        UJSONRPC.getFirmwareInfo(callback);

    }


    /**  get projects in all the slots of SPIKE Prime hub
     * 
     * @ignore
     * @returns {object}
     */
    async function getProjects() {

        UJSONRPC.getStorageStatus();

        await sleep(2000);

        return hubProjects
    }

    /**  Reach the micropython interpreter beneath UJSON RPC
     * <p> Note: Stops UJSON RPC stream </p>
     * <p> hub needs to be rebooted to return to UJSONRPC stream</p>
     * @ignore
     * @example
     * serviceSPIKE.reachMicroPy();
     * serviceSPIKE.sendDATA("from spike import PrimeHub");
     * serviceSPIKE.sendDATA("hub = PrimeHub()");
     * serviceSPIKE.sendDATA("hub.light_matrix.show_image('HAPPY')");
     */
    function reachMicroPy() {
        console.log("%cTuftsCEEO ", "color: #3ba336;", "starting micropy interpreter");
        setupWriter();
        writer.write(CONTROL_C);
        micropython_interpreter = true;
    }

    /**  Get the latest complete line of UJSON RPC from stream
     * @ignore
     * @returns {string} Represents a JSON object from UJSON RPC
     */
    async function getLatestUJSON() {

        try {
            var parsedUJSON = await JSON.parse(lastUJSONRPC)
        }
        catch (error) {
            //console.log("%cTuftsCEEO ", "color: #3ba336;", '[retrieveData] ERROR', error);
        }

        return lastUJSONRPC
    }

    /** Get whether the Service was initialized or not
     * @public
     * @returns {boolean} True if service initialized, false otherwise
     * @example
     * if (serviceSPIKE.isActive()) {
     *      // do something
     * }
     */
    function isActive() {
        return serviceActive;
    }

    /**  Get the most recently detected event on the display of the hub
     * @public
     * @returns {string} ['tapped','doubletapped']
     * var event = await serviceSPIKE.getHubEvent();
     * if (event == "tapped" ) {
     *      console.log("SPIKE is tapped");
     * }
     */
    function getHubEvent() {
        return hubFrontEvent;
    }

    /**  Get the most recently detected gesture of the hub ( Gesture names differ from SPIKE app )
     * @public
     * @returns {string} ['shaken', 'freefall', 'tapped', 'doubletapped']
     * @example
     * var gesture = await serviceSPIKE.getHubGesture();
     * if (gesture == "shaken") {
     *      console.log("SPIKE is being shaked");
     * }
     */
    function getHubGesture() {
        return hubGesture;
    }

    /**  Get the most recently detected orientation of the hub
     * @public
     * @returns {string} ['up','down','front','back','leftside','rightside']
     * @example
     * var orientation = await serviceSPIKE.getHubOrientation();
     * if (orientation == "front") {
     *      console.log("SPIKE is facing up");
     * }
     */
    function getHubOrientation() {
        return lastHubOrientation;
    }


    /**  Get the latest press event information on the "connect" button
     * @ignore
     * @returns {object} { "pressed": BOOLEAN, "duration": NUMBER } 
     * @example
     * var bluetoothButtonInfo = await serviceSPIKE.getBluetoothButton();
     * var pressedBool = bluetoothButtonInfo["pressed"];
     * var pressedDuration = bluetoothButtonInfo["duration"]; // duration is miliseconds the button was pressed until release
     */
    function getBluetoothButton() {
        return hubBluetoothButton;
    }

    /**  Get the latest press event information on the "center" button
     * @ignore
     * @returns {object} { "pressed": BOOLEAN, "duration": NUMBER }
     * @example
     * var mainButtonInfo = await serviceSPIKE.getMainButton();
     * var pressedBool = mainButtonInfo["pressed"];
     * var pressedDuration = mainButtonInfo["duration"]; // duration is miliseconds the button was pressed until release
     * 
     */
    function getMainButton() {
        return hubMainButton;
    }

    /**  Get the latest press event information on the "left" button
     * @ignore
     * @returns {object} { "pressed": BOOLEAN, "duration": NUMBER } 
     * @example
     * var leftButtonInfo = await serviceSPIKE.getLeftButton();
     * var pressedBool = leftButtonInfo["pressed"];
     * var pressedDuration = leftButtonInfo["duration"]; // duration is miliseconds the button was pressed until release
     * 
     */
    function getLeftButton() {
        return hubLeftButton;
    }

    /**  Get the latest press event information on the "right" button
     * @ignore
     * @returns {object} { "pressed": BOOLEAN, "duration": NUMBER } 
     * @example
     * var rightButtonInfo = await serviceSPIKE.getRightButton();
     * var pressedBool = rightButtonInfo["pressed"];
     * var pressedDuration = rightButtonInfo["duration"]; // duration is miliseconds the button was pressed until release
     */
    function getRightButton() {
        return hubRightButton;
    }

    /**  Get the letters of ports connected to any kind of Motors
     * @public
     * @returns {(string|Array)} Ports that are connected to Motors
     * @example
     * var motorPorts = serviceSPIKE.getMotorPorts();
     *
     * // get the alphabetically earliest port connected to a motor
     * var randomPort = motorPorts[0];
     *
     * // get Motor object connected to the port
     * var mySensor = new Motor(randomPort);
     */
    function getMotorPorts() {

        var portsInfo = ports;
        var motorPorts = [];
        for (var key in portsInfo) {
            if (portsInfo[key].device == "smallMotor" || portsInfo[key].device == "bigMotor") {
                motorPorts.push(key);
            }
        }
        return motorPorts;

    }

    /**  Get the letters of ports connected to Small Motors
     * @public
     * @returns {(string|Array)} Ports that are connected to Small Motors
     * @example
     * var smallMotorPorts = serviceSPIKE.getSmallMotorPorts();
     *
     * // get the alphabetically earliest port connected to a small motor
     * var randomPort = smallMotorPorts[0];
     *
     * // get Motor object connected to the port
     * var mySensor = new Motor(randomPort);
     */
    function getSmallMotorPorts() {

        var portsInfo = ports;
        var motorPorts = [];
        for (var key in portsInfo) {
            if (portsInfo[key].device == "smallMotor") {
                motorPorts.push(key);
            }
        }
        return motorPorts;

    }

    /**  Get the letters of ports connected to Big Motors
     * @public
     * @returns {(string|Array)} Ports that are connected to Big Motors
     * @example
     * var bigMotorPorts = serviceSPIKE.getBigMotorPorts();
     *
     * // get the alphabetically earliest port connected to a big motor
     * var randomPort = bigMotorPorts[0];
     *
     * // get Motor object connected to the port
     * var mySensor = new Motor(randomPort);
     */
    function getBigMotorPorts() {
        var portsInfo = ports;
        var motorPorts = [];
        for (var key in portsInfo) {
            if (portsInfo[key].device == "bigMotor") {
                motorPorts.push(key);
            }
        }
        return motorPorts;
    }

    /**  Get the letters of ports connected to Distance Sensors
     * @public
     * @returns {(string|Array)} Ports that are connected to Distance Sensors
     * @example
     * var distanceSensorPorts = serviceSPIKE.getDistancePorts();
     *
     * // get the alphabetically earliest port connected to a DistanceSensor
     * var randomPort = distanceSensorPorts[0];
     *
     * // get DistanceSensor object connected to the port
     * var mySensor = new DistanceSensor(randomPort);
     */
    function getUltrasonicPorts() {

        var portsInfo = this.getPortsInfo();
        var ultrasonicPorts = [];

        for (var key in portsInfo) {
            if (portsInfo[key].device == "ultrasonic") {
                ultrasonicPorts.push(key);
            }
        }

        return ultrasonicPorts;

    }

    /**  Get the letters of ports connected to Color Sensors
     * @public
     * @returns {(string|Array)} Ports that are connected to Color Sensors
     * @example
     * var colorSensorPorts = serviceSPIKE.getColorPorts();
     *
     * // get the alphabetically earliest port connected to a ColorSensor
     * var randomPort = colorSensorPorts[0];
     *
     * // get ColorSensor object connected to the port
     * var mySensor = new ColorSensor(randomPort);
     */
    function getColorPorts() {

        var portsInfo = this.getPortsInfo();
        var colorPorts = [];

        for (var key in portsInfo) {
            if (portsInfo[key].device == "color") {
                colorPorts.push(key);
            }
        }

        return colorPorts;

    }

    /**  Get the letters of ports connected to Force Sensors
     * @public
     * @returns {(string|Array)} Ports that are connected to Force Sensors
     * @example
     * var forceSensorPorts = serviceSPIKE.getForcePorts();
     * 
     * // get the alphabetically earliest port connected to a ForceSensor
     * var randomPort = forceSensorPorts[0];
     * 
     * // get ForceSensor object connected to the port
     * var mySensor = new ForceSensor(randomPort);
     */
    function getForcePorts() {

        var portsInfo = this.getPortsInfo();
        var forcePorts = [];

        for (var key in portsInfo) {
            if (portsInfo[key].device == "force") {
                forcePorts.push(key);
            }
        }

        return forcePorts;

    }

    /**  Get all motor objects currently connected to SPIKE
     * 
     * @public
     * @returns {array} All connected Motor objects
     * @example
     * var motors = serviceSPIKE.getMotors();
     * 
     * if (motors.length > 0) {
     * 
     *      var myMotor = motors[0]; // get motor connected to most alphabetically early port
     * 
     *      // run motor for 10 seconds at 100 speed
     *      myMotor.run_for_seconds(10,100);
     * }
     */
    function getMotors() {
        var portsInfo = ports;
        var motors = [];
        for (var key in portsInfo) {
            if (portsInfo[key].device == "smallMotor" || portsInfo[key].device == "bigMotor") {
                motors.push(new Motor(key));
            }
        }
        return motors;
    }

    /** Get all distance sensor objects currently connected to SPIKE
     * 
     * @public
     * @returns {array} All connected DistanceSensor objects
     * @example
     * var distanceSensors = serviceSPIKE.getDistanceSensors();
     * 
     * if (distanceSensors.length > 0) {
     *
     *      var myDistanceSensor = distanceSensors[0]; // get DistanceSensor connected to most alphabetically early port
     * 
     *      // get distance in centimeters
     *      var distance = myDistanceSensor.get_distance_cm();
     *      console.log("distance in CM: ", distance);
     * }
     */
    function getDistanceSensors() {
        var portsInfo = ports;
        var distanceSensors = [];
        for (var key in portsInfo) {
            if (portsInfo[key].device == "ultrasonic") {
                distanceSensors.push(new DistanceSensor(key));
            }
        }
        return distanceSensors;
    }

    /**  Get all color sensor objects currently connected to SPIKE
     * 
     * @public
     * @returns {object} All connected ColorSensor objects
     * @example
     * var colorSensors = serviceSPIKE.getColorSensors();
     *
     * if (colorSensors.length > 0) {
     *
     *      var color_sensor = colorSensors[0]; // get ColorSensor connected to most alphabetically early port
     *
     *      // get detected color
     *      var color = color_sensor.get_color();
     *      console.log("detected color: ", color);
     * }
     */
    function getColorSensors() {
        var portsInfo = ports;
        var colorSensors = [];
        for (var key in portsInfo) {
            if (portsInfo[key].device == "color") {
                colorSensors.push( new ColorSensor(key));
            }
        }
        return colorSensors;
    }

    /**  Get all force sensor objects currently connected to SPIKE
     * 
     * @public
     * @returns {object} All connected ForceSensor objects
     * @example
     * var forceSensors = serviceSPIKE.getForceSensors();
     *
     * if (forceSensors.length > 0) {
     *
     *      var force_sensor = forceSensors[0]; // get ForceSensor connected to most alphabetically early port
     *
     *      // when ForceSensor is pressed, indicate button state on console
     *      force_sensor.wait_until_pressed( function() {
     *           console.log("ForceSensor at port A was pressed");
     *      })
     * }
     * 
     */
    function getForceSensors() {
        var portsInfo = ports;
        var forceSensors = [];
        for (var key in portsInfo) {
            if (portsInfo[key].device == "force") {
                forceSensors.push( new ForceSensor(key));
            }
        }
        return forceSensors;
    }


    /**  Terminate currently running micropy program
     */
    function stopCurrentProgram() {
        UJSONRPC.programTerminate();
    }

    /** Push micropython code that retrieves all JS global variables and local variables at the scope in which
     * this function was called
     * @public
     * @param {integer} slotid 
     * @param {string} program program to write must be in TEMPLATE LITERAL
     * @ignore
     * @example
     * serviceSPIKE.micropython(10, `
     *from spike import PrimeHub, LightMatrix, Motor, MotorPair
     *from spike.control import wait_for_seconds, wait_until, Timer
     *
     *hub = PrimeHub()
     *
     *hub.light_matrix.write(run_for_seconds(2))
     *
     *run_for_seconds(3)
     * `)
     */
    function micropython(slotid, program) {
        // initialize microPyUtils
        micropyUtils.init();
        /* add local variables of the caller of this function */
        // get the function definition of caller
        /* parse and add all local variable declarations to micropyUtils.storedVariables
    
        var aString = "hi" or var aString = 'hi' > {aString: "hi"}
    
        */
        var thisFunction = arguments.callee.caller.toString();

        // split function scope by newlines
        var newLineRule = /\n/g
        var arrayLines = thisFunction.split(newLineRule);

        // filter lines that dont contain var, or contains function
        var arrayVarLines = [];
        for (var index in arrayLines) {
            if (arrayLines[index].indexOf("var") > -1) {
                // filter out functions and objects
                if (arrayLines[index].indexOf("function") == -1 && arrayLines[index].indexOf("{") == -1 && arrayLines[index].indexOf("}") == -1) {
                    arrayVarLines.push(arrayLines[index]);
                }
            }
        }

        var parseRule = /[[ ]/g
        for (var index in arrayVarLines) {
            // process line
            var processedLine = micropyUtils.processString(arrayVarLines[index]);

            // get [datatype] object = value format
            var listParsedLine = processedLine.split(parseRule);
            //listParsedLine = listParsedLine.split(/[=]/g)

            var keyValue = micropyUtils.checkString(listParsedLine);

            // insert into variables 
            for (var name in keyValue) {
                micropyUtils.storedVariables[name] = keyValue[name];
            }
        }

        /* generate lines of micropy variable declarations */
        var lines = [];
        for (var name in micropyUtils.storedVariables) {
            var variableName = name;
            if (typeof micropyUtils.storedVariables[name] !== "function" && typeof micropyUtils.storedVariables[name] !== "object") {
                var variableValue = micropyUtils.convertToString(micropyUtils.storedVariables[name]);

                lines.push("" + variableName + " = " + variableValue);

            }
        }

        // do add new lines to every line
        var linesChunk = ""
        for (var index in lines ) {
            var linePiece = lines[index];
            linesChunk = linesChunk + linePiece + "\n";
        }

        var programToWrite = linesChunk + program;
        writeProgram("micropython", programToWrite, slotid, function() {
            console.log("%cTuftsCEEO ", "color: #3ba336;", "micropy program write complete");
        })
    }

    /** Write a micropy program into a slot of the SPIKE Prime
     * 
     * @param {string} projectName name of the program
     * @param {string} data the micropython source code (expecting an input tag's value). All characters must be ASCII
     * @param {integer} slotid slot number to assign the program
     * @param {function} callback function to run after program is written
     */
    async function writeProgram(projectName, data, slotid, callback) {

        // check for non-ascii characters 
        let ascii = /[^\x00-\x7F]/;
        if (ascii.test(data)) {
            throw new Error(
                "non-ASCII characters detected in micropy program. Only ASCII characters are supported. Please check your micropy input."
                )
        }
        else {
            // reinit witeProgramTimeout
            if (writeProgramSetTimeout != undefined) {
                clearTimeout(writeProgramSetTimeout);
                writeProgramSetTimeout = undefined;
            }

            // template of python file that needs to be concatenated
            var firstPart = "from runtime import VirtualMachine\n\n# Stack for execution:\nasync def stack_1(vm, stack):\n"
            var secondPart = "# Setup for execution:\ndef setup(rpc, system, stop):\n\n    # Initialize VM:\n    vm = VirtualMachine(rpc, system, stop, \"Target__1\")\n\n    # Register stack on VM:\n    vm.register_on_start(\"stack_1\", stack_1)\n\n    return vm"

            // stringify data and strip trailing and leading quotation marks
            var stringifiedData = JSON.stringify(data);
            stringifiedData = stringifiedData.substring(1, stringifiedData.length - 1);

            var result = ""; // string to which the final code will be appended

            var splitData = stringifiedData.split(/\\n/); // split the code by every newline

            // add a tab before every newline (this is syntactically needed for concatenating with the template)
            for (var index in splitData) {

                var addedTab = "    " + splitData[index] + "\n";

                result = result + addedTab;
            }

            // replace tab characters
            result = result.replace(/\\t/g, "    ");

            stringifiedData = firstPart + result + secondPart;

            writeProgramCallback = callback;

            // begin the write program process
            UJSONRPC.startWriteProgram(projectName, "python", stringifiedData, slotid);
        }
    }

    /** Execute a program in SPIKE Prime
     * 
     * @param {integer} slotid slot of which program to execute
     * @example
     * // execute program in slot 1 of SPIKE Prime hub
     * serviceSPIKE.executeProgram(1);
     */
    function executeProgram(slotid) {
        UJSONRPC.programExecute(slotid)
    }

    //////////////////////////////////////////
    //                                      //
    //         SPIKE APP Functions          //
    //                                      //
    //////////////////////////////////////////

    /** The PrimeHub object includes controllable interfaces ("constants") for your SPIKE Prime, such as left_button, right_button, motion_sensor, and light_matrix.
    * @namespace
    * @memberof Service_SPIKE
    * @example
    * // Initialize the Hub
    * var hub = new serviceSPIKE.PrimeHub()
    */
    PrimeHub = function () {
        var newOrigin = 0;

        /** The left button on the hub
        * @namespace
        * @memberof! PrimeHub
        * @returns {functions} - functions from PrimeHub.left_button
        * @example
        * var hub = new serviceSPIKE.PrimeHub();
        * var left_button = hub.left_button;
        * // do something with left_button
        */
        var left_button = {};

        /** execute callback after this button is pressed
        * @param {function} callback function to run when button is pressed
        * @example
        * var hub = new serviceSPIKE.PrimeHub();
        * var left_button = hub.left_button;
        * left_button.wait_until_pressed ( function () {
        *     console.log("left_button was pressed");
        * })
        * 
        */
        left_button.wait_until_pressed = function wait_until_pressed(callback) {
            funcAfterLeftButtonPress = callback;
        }
        /** execute callback after this button is released
         *
         * @param {function} callback function to run when button is released
         * @example
         * var hub = new serviceSPIKE.PrimeHub();
         * var left_button = hub.left_button;
         * left_button.wait_until_released ( function () {
         *     console.log("left_button was released");
         * })
         */
        left_button.wait_until_released = function wait_until_released(callback) {
            funcAfterLeftButtonRelease = callback;
        }
        /** Tests to see whether the button has been pressed since the last time this method called.
         *
         * @returns {boolean} - True if was pressed, false otherwise
         * @example
         * if (left_button.was_pressed()) {
         *      console.log("left_button was pressed")
         * }
         */
        left_button.was_pressed = function was_pressed() {
            if (hubLeftButton.duration > 0) {
                hubLeftButton.duration = 0;
                return true;
            } else {
                return false;
            }
        }

        /** Tests to see whether the button is pressed
        *
        * @returns {boolean} True if pressed, false otherwise
        * @example
        * if (left_button.is_pressed()) {
        *      console.log("left_button is pressed")
        * }
        */
        left_button.is_pressed = function is_pressed() {
            if (hubLeftButton.pressed) {
                return true;
            }
            else {
                return false;
            }
        }

        /** The right button on the hub
         * @namespace
         * @memberof! PrimeHub
         * @returns {functions} functions from PrimeHub.right_button
         * @example
         * var hub = serviceSPIKE.PrimeHub();
         * var right_button = hub.right_button;
         * // do something with right_button
         */
        var right_button = {};

        /** execute callback after this button is pressed
        *
        * @param {function} callback function to run when button is pressed
        * @example
        * var hub = new serviceSPIKE.PrimeHub();
        * var right_button = hub.right_button;
        * right_button.wait_until_pressed ( function () {
        *     console.log("right_button was pressed");
        * })
        */
        right_button.wait_until_pressed = function wait_until_pressed(callback) {

            funcAfterRightButtonPress = callback;
        }

        /** execute callback after this button is released
         * 
         * @param {function} callback function to run when button is released
         * @example
         * var hub = new serviceSPIKE.PrimeHub();
         * var right_button = hub.right_button;
         * right_button.wait_until_released ( function () {
         *     console.log("right_button was released");
         * })
         */
        right_button.wait_until_released = function wait_until_released(callback) {

            funcAfterRightButtonRelease = callback;
        }

        /** Tests to see whether the button has been pressed since the last time this method called.
         * 
         * @returns {boolean} - True if was pressed, false otherwise
         * @example
         * var hub = new serviceSPIKE.PrimeHub();
         * if ( hub.right_button.was_pressed() ) {
         *     console.log("right_button was pressed");
         * }
         */
        right_button.was_pressed = function was_pressed() {
            if (hubRightButton.duration > 0) {
                hubRightButton.duration = 0;
                return true;
            } else {
                return false;
            }
        }

        /** Tests to see whether the button is pressed
         * 
         * @returns {boolean} True if pressed, false otherwise
         * @example
         * if (right_button.is_pressed()) {
         *      console.log("right_button is pressed")
         * }
         */
        right_button.is_pressed = function is_pressed() {
            if (hubRightButton.pressed) {
                return true;
            }
            else {
                return false;
            }
        }
        /** Following are all of the functions that are linked to the Hub’s programmable Brick Status Light.
         * @namespace
         * @memberof! PrimeHub
         * @returns {functions} - functions from PrimeHub.light_matrix
         * @example
         * var hub = serviceSPIKE.PrimeHub();
         * var status_light = hub.status_light;
         * // do something with status_light
        */
        var status_light = {};

        /** Sets the color of the light.
         * @param {string} color ["azure","black","blue","cyan","green","orange","pink","red","violet","yellow","white"]
         * @example
         * var hub = new Primehub()
         * hub.status_light.on("blue")
         * 
         */
        status_light.on = function on (color) {
            let dictColor = {
                "azure": 4,
                "black": 12, 
                "blue": 3, 
                "cyan": 5, 
                "green": 6, 
                "orange": 8,
                "pink": 1,
                "red": 9, 
                "violet": 2, 
                "yellow": 7,
                "white": 10
            }

            let intColor = dictColor[color];
            UJSONRPC.centerButtonLightUp(intColor);
        }

        /** Turns off the light.
         * @example
         * var hub = new Primehub()
         * hub.status_light.off()
         */
        status_light.off = function off () {
            UJSONRPC.centerButtonLightUp(0);
        }

        /** Hub's light matrix
         * @namespace
         * @memberof! PrimeHub
         * @returns {functions} - functions from PrimeHub.light_matrix
         * @example
         * var hub = serviceSPIKE.PrimeHub();
         * var light_matrix = hub.light_matrix;
         * // do something with light_matrix
         */
        var light_matrix = {};

        /**
         * @todo Implement this function
         * @ignore
         * @param {string}
         */
        light_matrix.show_image = function show_image(image) {

        }
        /** Sets the brightness of one pixel (one of the 25 LED) on the Light Matrix.
         * 
         * @param {integer} x [0 to 4]
         * @param {integer} y [0 to 4]
         * @param {integer} brightness [0 to 100]
         */
        light_matrix.set_pixel = function set_pixel(x, y, brightness = 100) {
            UJSONRPC.displaySetPixel(x, y, brightness);

        }
        /** Writes text on the Light Matrix, one letter at a time, scrolling from right to left.
         * 
         * @param {string} message 
         */
        light_matrix.write = function write(message) {
            UJSONRPC.displayText(message);
        }
        /** Turns off all the pixels on the Light Matrix.
         * 
         */
        light_matrix.off = function off() {
            UJSONRPC.displayClear();
        }

        /** Hub's speaker
         * @namespace
         * @memberof! PrimeHub
         * @returns {functions} functions from Primehub.speaker
         * @example
         * var hub = serviceSPIKE.PrimeHub();
         * var speaker = hub.speaker;
         * // do something with speaker
         */
        var speaker = {};

        speaker.volume = 100;

        /** Plays a beep on the Hub.
         * 
         * @param {integer} note The MIDI note number [44 to 123 (60 is middle C note)]
         * @param {number} seconds The duration of the beep in seconds
         */
        speaker.beep = function beep(note, seconds) {
            UJSONRPC.soundBeep(speaker.volume, note);
            setTimeout(function () { UJSONRPC.soundStop() }, seconds * 1000);
        }

        /** Starts playing a beep.
         * 
         * @param {integer} note The MIDI note number [44 to 123 (60 is middle C note)]
         */
        speaker.start_beep = function start_beep(note) {
            UJSONRPC.soundBeep(speaker.volume, note)
        }

        /** Stops any sound that is playing.
         * 
         */
        speaker.stop = function stop() {
            UJSONRPC.soundStop();
        }

        /** Retrieves the value of the speaker volume.
         * @returns {number} The current volume [0 to 100]
         */
        speaker.get_volume = function get_volume() {
            return speaker.volume;
        }

        /** Sets the speaker volume.
         * 
         * @param {integer} newVolume 
         */
        speaker.set_volume = function set_volume(newVolume) {
            speaker.volume = newVolume
        }

        /** Hub's motion sensor
         * @namespace
         * @memberof! PrimeHub
         * @returns {functions} functions from PrimeHub.motion_sensor
         * @example
         * var hub = serviceSPIKE.PrimeHub();
         * var motion_sensor = hub.motion_sensor;
         * // do something with motion_sensor
         */
        var motion_sensor = {};

        /** Sees whether a gesture has occurred since the last time was_gesture() 
         * was used or since the beginning of the program (for the first use).
         * 
         * @param  {string} gesture
         * @returns {boolean} true if the gesture was made, false otherwise
         */
        motion_sensor.was_gesture = function was_gesture(gesture) {

            var gestureWasMade = false;

            // iterate over the hubGestures array
            for (index in hubGestures) {

                // pick a gesture from the array
                var oneGesture = hubGestures[index];

                // switch the flag that gesture existed
                if (oneGesture == gesture) {
                    gestureWasMade = true;
                    break;
                }
            }
            // reinitialize hubGestures so it only holds gestures that occurred after this was_gesture() execution
            hubGestures = [];

            return gestureWasMade;

        }

        /** Executes callback when a new gesture happens
         * 
         * @param  {function(string)} callback - A callback of which argument is name of the gesture
         * @example
         * motion_sensor.wait_for_new_gesture( function ( newGesture ) {
         *      if ( newGesture == 'tapped') {
         *             console.log("SPIKE was tapped")
         *      }
         *      else if ( newGesture == 'doubletapped') {
         *             console.log("SPIKE was doubletapped")
         *      }
         *      else if ( newGesture == 'shaken') {
         *             console.log("SPIKE was shaken")
         *      }
         *      else if ( newGesture == 'freefall') {
         *             console.log("SPIKE was freefall")
         *      }
         * })
         */
        motion_sensor.wait_for_new_gesture = function wait_for_new_gesture(callback) {

            funcAfterNewGesture = callback;

        }

        /** Executes callback when the orientation of the Hub changes or when function was first called
         * 
         * @param  {function(string)} callback - A callback whose signature is name of the orientation
         * @example
         * motion_sensor.wait_for_new_orientation( function ( newOrientation ) {
         *        if (newOrientation == "up") {
         *              console.log("orientation is up");
         *        }
         *        else if (newOrientation == "down") {
         *              console.log("orientation is down");
         *        }
         *        else if (newOrientation == "front") {
         *              console.log("orientation is front");
         *        }
         *        else if (newOrientation == "back") {
         *              console.log("orientation is back");
         *        }
         *        else if (newOrientation == "leftSide") {
         *              console.log("orientation is leftSide");
         *        }
         *        else if (newOrientation == "rightSide") {
         *              console.log("orientation is rightSide");
         *        }
         * })
         */
        motion_sensor.wait_for_new_orientation = function wait_for_new_orientation(callback) {
            // immediately return current orientation if the method was called for the first time
            if (waitForNewOriFirst) {
                waitForNewOriFirst = false;
                callback(lastHubOrientation);
            }
            // for future executions, wait until new orientation
            else {
                funcAfterNewOrientation = callback;
            }

        }

        /** “Yaw” is the rotation around the front-back (vertical) axis.
         * 
         * @returns {integer} yaw angle
         */
        motion_sensor.get_yaw_angle = function get_yaw_angle() {
            var currPos = hub.pos[0];

            return currPos;
        }

        /** “Pitch” the is rotation around the left-right (transverse) axis.
         * 
         * @returns {integer} pitch angle
         */
        motion_sensor.get_pitch_angle = function get_pitch_angle() {
            return hub.pos[1];
        }

        /** “Roll” the is rotation around the front-back (longitudinal) axis.
         * 
         * @returns {integer} roll angle
         */
        motion_sensor.get_roll_angle = function get_roll_angle() {
            return hub.pos[2];
        }

        /** Gets the acceleration of the SPIKE's yaw axis
         * 
         * @returns {integer} acceleration
         */
        motion_sensor.get_yaw_acceleration = function get_yaw_acceleration() {
            return hub.pos[2];
        }

        /**  Gets the acceleration of the SPIKE's pitch axis
         * 
         * @returns {integer} acceleration
         */
        motion_sensor.get_pitch_acceleration = function get_pitch_acceleration() {
            return hub.pos[1];
        }

        /** Gets the acceleration of the SPIKE's roll axis
         * 
         * @returns {integer} acceleration
         */
        motion_sensor.get_roll_acceleration = function get_roll_acceleration() {
            return hub.pos[0];
        }

        /** Retrieves the most recently detected gesture.
         * 
         * @returns {string} the name of gesture
         */
        motion_sensor.get_gesture = function get_gesture() {
            return hubGesture;
        }

        /** Retrieves the most recently detected orientation
         * Note: Hub does not detect orientation of when it was connected
         * 
         * @returns {string} the name of orientation
         */
        motion_sensor.get_orientation = function get_orientation() {
            return lastHubOrientation;
        }

        return {
            motion_sensor: motion_sensor,
            light_matrix: light_matrix,
            left_button: left_button,
            right_button: right_button,
            speaker: speaker
        }
    }

    /** Motor
     * @namespace
     * @memberof! Service_SPIKE
     * @param {string} Port
     * @returns {functions}
     * @example
     * // Initialize the Motor
     * var motor = new serviceSPIKE.Motor("A")
     */
    Motor = function (port) {

        var motor = ports[port]; // get the motor info by port

        // default settings
        var defaultSpeed = 100;
        var stopMethod = 1; // stop method doesnt seem to work in this current ujsonrpc config
        var stallSetting = true;

        var direction = {
            COUNTERCLOCKWISE: 'counterClockwise',
            CLOCKWISE: 'clockwise'
        }

        // check if device is a motor
        if (motor.device != "smallMotor" && motor.device != "bigMotor") {
            throw new Error("No motor detected at port " + port);
        }

        /** Get current speed of the motor
         *  
         * @returns {number} speed of motor [-100 to 100]
         */
        function get_speed() {
            var motor = ports[port]; // get the motor info by port
            var motorInfo = motor.data;
            return motorInfo.speed;

        }

        /** Get current position of the motor. The position may differ by a little margin from 
         * the position to which a motor ran with run_to_position()
         * @returns {number} position of motor [0 to 359]
         */
        function get_position() {
            var motor = ports[port]; // get the motor info by port
            var motorInfo = motor.data;
            let position = motorInfo.uAngle;
            if (position < 0)
                position = 360 + position;
            return position;
        }

        /** Get current degrees counted of the motor
         * 
         * @returns {number} counted degrees of the motor [any number]
         */
        function get_degrees_counted() {
            var motor = ports[port]; // get the motor info by port
            var motorInfo = motor.data;
            return motorInfo.angle;
        }

        /** Get the power of the motor
         * 
         * @returns {number} motor power
         */
        function get_power() {
            var motor = ports[port]; // get the motor info by port
            var motorInfo = motor.data;
            return motorInfo.power;
        }

        /** Get the default speed of this motor
         * 
         * @returns {number} motor default speed [-100 to 100]
         */
        function get_default_speed() {
            return defaultSpeed;
        }

        /** Set the default speed for this motor
         * 
         * @param {number} speed [-100 to 100]
         */
        function set_default_speed(speed) {
            if (typeof speed == "number") {
                defaultSpeed = speed;
            }
        }

        /** Turns stall detection on or off.
         * Stall detection senses when a motor has been blocked and can’t move.
         * If stall detection has been enabled and a motor is blocked, the motor will be powered off
         * after two seconds and the current motor command will be interrupted. If stall detection has been
         * disabled, the motor will keep trying to run and programs will “get stuck” until the motor is no
         * longer blocked.
         * @param {boolean} boolean - true if to detect stall, false otherwise
         */
        function set_stall_detection(boolean) {
            if (typeof boolean == "boolean") {
                stallSetting = boolean;
            }
        }


        /** Runs the motor to an absolute position.
         * The sign of the speed will be ignored (i.e., absolute value), and the motor will always travel in the direction that’s been specified by the "direction" parameter. 
         * If the speed is greater than "100," it will be limited to "100."

         * @param {integer} degrees [0 to 359]
         * @param {string} direction "Clockwise" or "Counterclockwise"
         * @param {integer} speed [-100 to 100]
         * @param {function} callback Params: "stalled" or "done"
         * @ignore
         * @example
         * motor.run_to_position(180, 100, function() {
         *      console.log("motor finished moving");
         * })
         */
        function run_to_position(degrees, direction, speed, callback = undefined) {
            if (speed !== undefined && typeof speed == "number")
                UJSONRPC.motorGoRelPos(port, degrees, speed, stallSetting, stopMethod, callback);
            else
                UJSONRPC.motorGoRelPos(port, degrees, defaultSpeed, stallSetting, stopMethod, callback);
        }

        /** Runs the motor until the number of degrees counted is equal to the value that has been specified by the "degrees" parameter.
         * 
         * @param {integer} degrees any number
         * @param {integer} speed [0 to 100]
         * @param {any} [callback] (optional callback) callback param: "stalled" or "done"
         */
        function run_to_degrees_counted(degrees, speed, callback = undefined) {
            if (speed !== undefined && typeof speed == "number")
                UJSONRPC.motorGoRelPos(port, degrees, speed, stallSetting, stopMethod, callback);
            else
                UJSONRPC.motorGoRelPos(port, degrees, defaultSpeed, stallSetting, stopMethod, callback);


        }

        /** Start the motor at some power
         * 
         * @param {integer} power [-100 to 100]
         */
        function start_at_power(power) {
            UJSONRPC.motorPwm(port, power, stallSetting);
        }


        /** Start the motor at some speed
         * 
         * @param {integer} speed [-100 to 100]
         */
        function start(speed = defaultSpeed) {
            // if (speed !== undefined && typeof speed == "number") {
            // UJSONRPC.motorStart (port, speed, stallSetting);
            // }
            // else {
            // UJSONRPC.motorStart(port, defaultSpeed, stallSetting);
            // }

            UJSONRPC.motorStart(port, speed, stallSetting);
        }

        /** Run the motor for some seconds
         * 
         * @param {integer} seconds 
         * @param {integer} speed [-100 to 100]
         * @param {function} [callback==undefined] Parameters:"stalled" or "done"
         * @example
         * motor.run_for_seconds(10, 100, function() {
         *      console.log("motor just ran for 10 seconds");
         * })
         */
        function run_for_seconds(seconds, speed, callback = undefined) {
            if (speed !== undefined && typeof speed == "number") {
                UJSONRPC.motorRunTimed(port, seconds, speed, stallSetting, stopMethod, callback)
            }
            else {
                UJSONRPC.motorRunTimed(port, seconds, defaultSpeed, stallSetting, stopMethod, callback)
            }
        }

        /** Run the motor for some degrees
         * 
         * @param {integer} degrees 
         * @param {integer} speed [-100 to 100]
         * @param {function} [callback==undefined] Parameters:"stalled" or "done"
         * motor.run_for_degrees(720, 100, function () {
         *      console.log("motor just ran for 720 degrees");
         * })
         */
        function run_for_degrees(degrees, speed, callback = undefined) {
            if (speed !== undefined && typeof speed == "number") {
                UJSONRPC.motorRunDegrees(port, degrees, speed, stallSetting, stopMethod, callback);
            }
            else {
                UJSONRPC.motorRunDegrees(port, degrees, defaultSpeed, stallSetting, stopMethod, callback);
            }
        }

        /** Stop the motor
         * 
         */
        function stop() {
            UJSONRPC.motorPwm(port, 0, stallSetting);
        }

        return {
            run_to_position: run_to_position,
            run_to_degrees_counted: run_to_degrees_counted,
            start_at_power: start_at_power,
            start: start,
            stop: stop,
            run_for_degrees: run_for_degrees,
            run_for_seconds: run_for_seconds,
            set_default_speed: set_default_speed,
            set_stall_detection: set_stall_detection,
            get_power: get_power,
            get_degrees_counted: get_degrees_counted,
            get_position: get_position,
            get_speed: get_speed,
            get_default_speed: get_default_speed
        }
    }

    /** ColorSensor
     * @namespace
     * @param {string} Port
     * @memberof Service_SPIKE
     * @example
     * // Initialize the Color Sensor
     * var color = new serviceSPIKE.ColorSensor("E")
     */
    ColorSensor = function (port) {
        var waitForNewColorFirst = false;

        var colorsensor = ports[port]; // get the color sensor info by port
        var colorsensorData = colorsensor.data;

        // check if device is a color sensor
        if (colorsensor.device != "color") {
            throw new Error("No Color Sensor detected at port " + port);
        }

        /** Get the name of the detected color
         * @returns {string} 'black','violet','blue','cyan','green','yellow','red','white'
         */
        function get_color() {
            var colorsensor = ports[port]; // get the color sensor info by port
            var colorsensorData = colorsensor.data;

            var color = colorsensorData.color;

            return color;
        }

        /** Retrieves the intensity of the ambient light.
         * @ignore
         * @returns {number} The ambient light intensity. [0 to 100]
         */
        function get_ambient_light() {
            var colorsensor = ports[port]; // get the color sensor info by port
            var colorsensorData = colorsensor.data;

            return colorsensorData.Cambient;
        }

        /** Retrieves the intensity of the reflected light.
         * 
         * @returns {number} The reflected light intensity. [0 to 100]
         */
        function get_reflected_light() {
            var colorsensor = ports[port]; // get the color sensor info by port
            var colorsensorData = colorsensor.data;

            return colorsensorData.Creflected;
        }

        /** Retrieves the red, green, blue, and overall color intensity.
         * @todo Implement overall intensity
         * @ignore
         * @returns {(number|Array)} Red, green, blue, and overall intensity (0-1024)
         */
        function get_rgb_intensity() {
            var colorsensor = ports[port]; // get the color sensor info by port
            var colorsensorData = colorsensor.data;

            var toReturn = [];
            toReturn.push(colorsensorData.Cr);
            toReturn.push(colorsensorData.Cg);
            toReturn.push(colorsensorData.Cb)
            toReturn.push("TODO: unimplemented");;
        }

        /** Retrieves the red color intensity.
         * 
        * @returns {number} [0 to 1024]
         */
        function get_red() {
            var colorsensor = ports[port]; // get the color sensor info by port
            var colorsensorData = colorsensor.data;

            return colorsensorData.RGB[0];
        }

        /** Retrieves the green color intensity.
         * 
         * @returns {number} [0 to 1024]
         */
        function get_green() {
            var colorsensor = ports[port]; // get the color sensor info by port
            var colorsensorData = colorsensor.data;

            return colorsensorData.RGB[1];
        }

        /** Retrieves the blue color intensity.
         * 
         * @returns {number} [0 to 1024]
         */
        function get_blue() {
            var colorsensor = ports[port]; // get the color sensor info by port
            var colorsensorData = colorsensor.data;

            return colorsensorData.RGB[2];
        }

        /** Waits until the Color Sensor detects the specified color.
         * 
         * @param {string} colorInput 'black','violet','blue','cyan','green','yellow','red','white'
         * @param {function} callback callback function
         */
        function wait_until_color(colorInput, callback) {
            waitUntilColorCallback = [colorInput, callback];
        }


        /** Execute callback when Color Sensor detects a new color.
         * The first time this method is called, it returns immediately the detected color. 
         * After that, it waits until the Color Sensor detects a color that is different from the color that
         * was detected the last time this method was used.
         * @param {function(string)} callback params: detected new color
         */
        function wait_for_new_color(callback) {

            // check if this method has been executed after start of program
            if (waitForNewColorFirst) {
                waitForNewColorFirst = false;

                var currentColor = get_color();
                callback(currentColor)
            }
            funcAfterNewColor = callback;
        }

        return {
            get_color: get_color,
            wait_until_color: wait_until_color,
            wait_for_new_color: wait_for_new_color,
            get_ambient_light: get_ambient_light,
            get_reflected_light: get_reflected_light,
            get_rgb_intensity: get_rgb_intensity,
            get_red: get_red,
            get_green: get_green,
            get_blue: get_blue
        }

    }

    /** DistanceSensor
     * @namespace
     * @param {string} Port
     * @memberof Service_SPIKE
     * @example
     * // Initialize the DistanceSensor
     * var distance_sensor = new serviceSPIKE.DistanceSensor("A");
     */
    var  DistanceSensor = function (port) {
        var distanceSensor = ports[port]; // get the distance sensor info by port

        // check if device is a distance sensor
        if (distanceSensor.device != "ultrasonic") {
            console.error("Ports Info: ", ports);
            throw new Error("No DistanceSensor detected at port " + port);
        }

        /** Retrieves the measured distance in centimeters.
         * @returns {number} [0 to 200]
         * @todo find the short_range handling ujsonrpc script
         * @example
         * var distance_cm = distance_sensor.get_distance_cm();
         */
        function get_distance_cm() {
            var distanceSensor = ports[port] // get the distance sensor info by port
            var distanceSensorData = distanceSensor.data;

            return distanceSensorData.distance;
        }

        /** Retrieves the measured distance in inches.
         * 
         * @returns {number} [0 to 79]
         * @todo find the short_range handling ujsonrpc script
         * @example
         * var distance_inches = distance_sensor.get_distance_inches();
         */
        function get_distance_inches() {
            var distanceSensor = ports[port] // get the distance sensor info by port
            var distanceSensorData = distanceSensor.data;

            var inches = distanceSensorData.distance * 0.393701; // convert to inches

            if (inches % 1 < 0.5)
                inches = Math.floor(inches);
            else
                inches = Math.ceil(inches);

            return inches;
        }

        /** Retrieves the measured distance in percent.
         * 
         * @returns {number/string} [0 to 100] or 'none' if no distance is read
         * var distance_percentage = distance_sensor.get_distance_percentage();
         */
        function get_distance_percentage() {
            var distanceSensor = ports[port] // get the distance sensor info by port
            var distanceSensorData = distanceSensor.data;

            if (distanceSensorData.distance == null) {
                return "none"
            }
            var percentage = distanceSensorData.distance / 200;
            return percentage;
        }

        /** Waits until the measured distance is greater than distance.
         * @param {integer} threshold 
         * @param {string} unit 'cm','in','%'
         * @param {function} callback function to execute when distance is farther than threshold
         * @example
         * distance_sensor.wait_for_distance_farther_than(10, 'cm', function () {
         *      console.log("distance is farther than 10 CM");
         * })
         */
        function wait_for_distance_farther_than(threshold, unit, callback) {

            // set callbacks to be executed in updateHubPortsInfo()
            if (unit == 'cm') {
                waitForDistanceFartherThanCallback = [threshold, callback];
            }
            else if (unit == 'in') {
                waitForDistanceFartherThanCallback = [threshold / 0.393701, callback];
            }
            else if (unit == '%') {
                waitForDistanceFartherThanCallback = [(threshold * 0.01) * 200, callback];
            }
            else {
                throw new Error("The 'unit' argument in wait_for_distance_farther_than(threshold, unit, callback) must be either 'cm', 'in', or '%'.")
            }
        }

        /** Waits until the measured distance is less than distance.
         * @param {integer} threshold 
         * @param {string} unit 'cm','in','%'
         * @param {function} callback function to execute when distance is closer than threshold
         * @example
         * distance_sensor.wait_for_distance_closer_than(10, 'cm', function () {
         *      console.log("distance is closer than 10 CM");
         * })   
         */
        function wait_for_distance_closer_than(threshold, unit, callback) {
            // set callbacks to be executed in updateHubPortsInfo()
            if (unit == 'cm') {
                waitForDistanceCloserThanCallback = [threshold, callback];
            }
            else if (unit == 'in') {
                waitForDistanceCloserThanCallback = [threshold / 0.393701, callback];
            }
            else if (unit == '%') {

                /* floor or ceil thresholds larger or smaller than what's possible */
                if (threshold > 100) {
                    threshold = 100;
                }
                else if (threshold < 0) {
                    threshold = 0;
                }

                waitForDistanceCloserThanCallback = [(threshold * 0.01) * 200, callback];
            }
            else {
                throw new Error("The 'unit' argument in wait_for_distance_closer_than(threshold, unit, callback) must be either 'cm', 'in', or '%'.")
            }
        }

        /** Sets the brightness of the individual lights on the Distance Sensor.
         * 
         * @param {integer} right_top Brightness [1-100]
         * @param {integer} left_top Brightness [1-100]
         * @param {integer} right_bottom Brightness [1-100]
         * @param {integer} left_bottom Brightness [1-100]
         * @example
         * distance_sensor.light_up(100,100,100,100);
         */
        function light_up (right_top, left_top, right_bottom, left_bottom) {
            let lightArray = [0,0,0,0];
            lightArray[0] = right_top;
            lightArray[1] = left_top;
            lightArray[2] = right_bottom;
            lightArray[3] = left_bottom;

            UJSONRPC.ultrasonicLightUp(port, lightArray);
        }

        /** Lights up all of the lights on the Distance Sensor at the specified brightness.
         * 
         * @param {number} [brightness=100] The specified brightness of all of the lights
         * @example
         * distance_sensor.light_up_all(50)
         */
        function light_up_all(brightness = 100) {

            let lightArray = [brightness, brightness, brightness, brightness];

            UJSONRPC.ultrasonicLightUp(port, lightArray);
        }

        return {
            get_distance_cm: get_distance_cm,
            get_distance_inches: get_distance_inches,
            get_distance_percentage: get_distance_percentage,
            light_up: light_up,
            light_up_all: light_up_all,
            wait_for_distance_closer_than: wait_for_distance_closer_than,
            wait_for_distance_farther_than:wait_for_distance_farther_than
        }

    }

    /** ForceSensor
     * @namespace
     * @param {string} Port
     * @memberof Service_SPIKE
     * @example
     * // Initialize the ForceSensor
     * var force_sensor = new serviceSPIKE.ForceSensor("E")
     */
    ForceSensor = function (port) {

        var sensor = ports[port]; // get the force sensor info by port

        if (sensor.device != "force") {
            throw new Error("No Force Sensor detected at port " + port);
        }

        /** Tests whether the button on the sensor is pressed.
         * 
         * @returns {boolean} true if force sensor is pressed, false otherwise
         * @example
         * if (force_sensor.is_pressed() === true) {
         *      console.log("force sensor is pressed");
         * }
         */
        function is_pressed() {
            var sensor = ports[port]; // get the force sensor info by port
            var ForceSensorData = sensor.data;

            return ForceSensorData.pressed;
        }

        /** Retrieves the measured force, in newtons.
         * 
         * @returns {number}  Force in newtons [0 to 10]
         * @example
         * var newtons = force_sensor.get_force_newtons();
         */
        function get_force_newton() {
            var sensor = ports[port]; // get the force sensor info by port
            var ForceSensorData = sensor.data;

            return ForceSensorData.force;
        }

        /** Retrieves the measured force as a percentage of the maximum force.
         * 
         * @returns {number} percentage [0 to 100]
         * var percentage = force_sensor.get_force_percentage();
         */
        function get_force_percentage() {
            var sensor = ports[port]; // get the force sensor info by port
            var ForceSensorData = sensor.data;

            var denominator = 704 - 384 // highest detected - lowest detected forceSensitive values
            var numerator = ForceSensorData.forceSensitive - 384 // 384 is the forceSensitive value when not pressed
            var percentage = Math.round((numerator / denominator) * 100);
            return percentage;
        }

        /** Executes callback when Force Sensor is pressed
         * The function is executed in updateHubPortsInfo()'s Force Sensor part
         * @param {function} callback 
         * @example
         * force_sensor.wait_until_pressed( function () {
         *      console.log("force sensor is pressed!");
         * })
         */
        function wait_until_pressed(callback) {
            funcAfterForceSensorPress = callback;
        }

        /** Executes callback when Force Sensor is released
         * The function is executed in updateHubPortsInfo()'s Force Sensor part
         * @param {function} callback 
         * @example
         * force_sensor.wait_until_released ( function () {
         *      console.log("force sensor is released!");
         * })
         */
        function wait_until_released(callback) {
            funcAfterForceSensorRelease = callback;
        }

        return {
            is_pressed: is_pressed,
            get_force_newton: get_force_newton,
            get_force_percentage: get_force_percentage,
            wait_until_pressed: wait_until_pressed,
            wait_until_released: wait_until_released
        }

    }

    /** MotorPair
     * @namespace
     * @param {string} leftPort
     * @param {string} rightPort
     * @memberof Service_SPIKE
     * @example
     * var pair = new serviceSPIKE.MotorPair("A", "B")
     */
    MotorPair = function (leftPort, rightPort) {
        // settings 
        var defaultSpeed = 100;

        var leftMotor = ports[leftPort];
        var rightMotor = ports[rightPort];

        var DistanceTravelToRevolutionRatio = 17.6;

        // check if device is a motor
        if (leftMotor.device != "smallMotor" && leftMotor.device != "bigMotor") {
            throw new Error("No motor detected at port " + port);
        }
        if (rightMotor.device != "smallMotor" && rightMotor.device != "bigMotor") {
            throw new Error("No motor detected at port " + port);
        }

        /** Sets the ratio of one motor rotation to the distance traveled.
         * 
         * If there are no gears used between the motors and the wheels of the Driving Base, 
         * then amount is the circumference of one wheel.
         * 
         * Calling this method does not affect the Driving Base if it is already currently running. 
         * It will only have an effect the next time one of the move or start methods is used.
         * 
         * @param {number} amount 
         * @param {string} unit 'cm','in'
         */
        function set_motor_rotation(amount, unit) {

            // assume unit is 'cm' when undefined
            if (unit == "cm" || unit !== undefined) {
                DistanceTravelToRevolutionRatio = amount;
            }
            else if (unit == "in") {
                // convert to cm
                DistanceTravelToRevolutionRatio = amount * 2.54;
            }
        }

        /** Starts moving the Driving Base
         * 
         * @param {integer} left_speed [-100 to 100]
         * @param {integer} right_speed [-100 to 100]
         * @example
         * pair.start_tank(100,100);
         */
        function start_tank(left_speed, right_speed) {
            UJSONRPC.moveTankSpeeds(left_speed, right_speed, leftPort, rightPort);
        }

        /** Starts moving the Driving Base
         * 
         * @param {integer} leftPower [-100 to 100]
         * @param {integer} rightPower [-100 to 100]
         * @example
         * pair.start_tank_at_power(10, 10);
         */
        function start_tank_at_power(leftPower, rightPower) {
            UJSONRPC.moveTankPowers(leftPower, rightPower, leftPort, rightPort);
        }

        /** Stops the 2 motors simultaneously, which will stop a Driving Base.
         * @example
         * pair.stop();
         */
        function stop() {
            UJSONRPC.moveTankPowers(0, 0, leftPort, rightPort);
        }

        return {
            stop: stop,
            set_motor_rotation: set_motor_rotation,
            start_tank: start_tank,
            start_tank_at_power: start_tank_at_power
        }

    }

    //////////////////////////////////////////
    //                                      //
    //          UJSONRPC Functions          //
    //                                      //
    //////////////////////////////////////////

    /** Low Level UJSONRPC Commands
     * @ignore
     * @namespace UJSONRPC
     */
    var UJSONRPC = {};

    /**
     * 
     * @memberof! UJSONRPC
     * @param {string} text 
     */
    UJSONRPC.displayText = async function displayText(text) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.display_text", "p": {"text":' + '"' + text + '"' + '} }'
        sendDATA(command);
    }

    /**
     * @memberof! UJSONRPC
     * @param {integer} x [0 to 4]
     * @param {integer} y [0 to 4]
     * @param {integer} brightness [1 to 100]
     */
    UJSONRPC.displaySetPixel = async function displaySetPixel(x, y, brightness) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.display_set_pixel", "p": {"x":' + x +
            ', "y":' + y + ', "brightness":' + brightness + '} }';
        sendDATA(command);
    }

    /**
     * @memberof! UJSONRPC
     */
    UJSONRPC.displayClear = async function displayClear() {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.display_clear" }';
        sendDATA(command);
    }
    
    /**
     * @memberof! UJSONRPC
     * @param {string} port 
     * @param {array} array [1-100,1-100,1-100,1-100] array of size 4
     */
    UJSONRPC.ultrasonicLightUp = async function ultrasonicLightUp(port, array) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.ultrasonic_light_up", "p": {' +
            '"port": ' + '"' + port + '"' +
            ', "lights": ' + '[' + array + ']' +
        '} }';
        sendDATA(command);
    }

    /**
     * @memberof! UJSONRPC
     * @param {string} port 
     * @param {integer} speed 
     * @param {integer} stall 
     */
    UJSONRPC.motorStart = async function motorStart(port, speed, stall) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.motor_start", "p": {"port":'
            + '"' + port + '"' +
            ', "speed":' + speed +
            ', "stall":' + stall +
            '} }';
        sendDATA(command);
    }

    /** moves motor to a position
     * 
     * @memberof! UJSONRPC
     * @param {string} port 
     * @param {integer} position 
     * @param {integer} speed 
     * @param {boolean} stall 
     * @param {boolean} stop 
     * @param {function} callback
     */
    UJSONRPC.motorGoRelPos = async function motorGoRelPos(port, position, speed, stall, stop, callback) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.motor_go_to_relative_position"' +
            ', "p": {' +
            '"port":' + '"' + port + '"' +
            ', "position":' + position +
            ', "speed":' + speed +
            ', "stall":' + stall +
            ', "stop":' + stop +
            '} }';

        if (callback != undefined) {
            pushResponseCallback(randomId, callback);
        }
        sendDATA(command);
    }

    UJSONRPC.motorGoDirToPosition = async function motorGoDirToPosition(port, position, direction, speed, stall, stop, callback) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.motor_go_direction_to_position"' +
            ', "p": {' +
            '"port":' + '"' + port + '"' +
            ', "position":' + position +
            ', "direction":' + direction +
            ', "speed":' + speed +
            ', "stall":' + stall +
            ', "stop":' + stop +
            '} }';

        if (callback != undefined) {
            pushResponseCallback(randomId, callback);
        }
        sendDATA(command);

    }

    /**
     * 
     * @memberof! UJSONRPC
     * @param {string} port 
     * @param {integer} time 
     * @param {integer} speed 
     * @param {integer} stall 
     * @param {boolean} stop
     * @param {function} callback
     */
    UJSONRPC.motorRunTimed = async function motorRunTimed(port, time, speed, stall, stop, callback) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.motor_run_timed"' +
            ', "p": {' +
            '"port":' + '"' + port + '"' +
            ', "time":' + time +
            ', "speed":' + speed +
            ', "stall":' + stall +
            ', "stop":' + stop +
            '} }';
        if (callback != undefined) {
            pushResponseCallback(randomId, callback);
        }
        sendDATA(command);
    }

    /**
     * 
     * @memberof! UJSONRPC
     * @param {string} port 
     * @param {integer} degrees 
     * @param {integer} speed 
     * @param {integer} stall 
     * @param {boolean} stop
     * @param {function} callback
     */
    UJSONRPC.motorRunDegrees = async function motorRunDegrees(port, degrees, speed, stall, stop, callback) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.motor_run_for_degrees"' +
            ', "p": {' +
            '"port":' + '"' + port + '"' +
            ', "degrees":' + degrees +
            ', "speed":' + speed +
            ', "stall":' + stall +
            ', "stop":' + stop +
            '} }';
        if ( callback != undefined ) {
            pushResponseCallback(randomId, callback);
        }
        sendDATA(command);
    }

    /**
     * @memberof! UJSONRPC
     * @param {integer} time 
     * @param {integer} lspeed 
     * @param {integer} rspeed 
     * @param {string} lmotor 
     * @param {string} rmotor 
     * @param {boolean} stop
     * @param {function} callback
     */
    UJSONRPC.moveTankTime = async function moveTankTime(time, lspeed, rspeed, lmotor, rmotor, stop, callback) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.move_tank_time"' +
            ', "p": {' +
            '"time":' + time +
            ', "lspeed":' + lspeed +
            ', "rspeed":' + rspeed +
            ', "lmotor":' + '"' + lmotor + '"' +
            ', "rmotor":' + '"' + rmotor + '"' +
            ', "stop":' + stop +
            '} }';
        if (callback != undefined) {
            pushResponseCallback(randomId, callback);
        }
        sendDATA(command);
    }

    /**
     * 
     * @memberof! UJSONRPC
     * @param {integer} degrees 
     * @param {integer} lspeed 
     * @param {integer} rspeed 
     * @param {string} lmotor 
     * @param {string} rmotor 
     * @param {boolean} stop
     * @param {function} callback
     */
    UJSONRPC.moveTankDegrees = async function moveTankDegrees(degrees, lspeed, rspeed, lmotor, rmotor, stop, callback) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.move_tank_degrees"' +
            ', "p": {' +
            '"degrees":' + degrees +
            ', "lspeed":' + lspeed +
            ', "rspeed":' + rspeed +
            ', "lmotor":' + '"' + lmotor + '"' +
            ', "rmotor":' + '"' + rmotor + '"' +
            ', "stop":' + stop +
            '} }';
        if (callback != undefined) {
            pushResponseCallback(randomId, callback);
        }
        sendDATA(command);
    }

    /**
     * 
     * @memberof! UJSONRPC
     * @param {integer} lspeed 
     * @param {integer} rspeed 
     * @param {string} lmotor 
     * @param {string} rmotor 
     * @param {function} callback
     */
    UJSONRPC.moveTankSpeeds = async function moveTankSpeeds(lspeed, rspeed, lmotor, rmotor, callback) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.move_start_speeds"' +
            ', "p": {' +
            '"lspeed":' + lspeed +
            ', "rspeed":' + rspeed +
            ', "lmotor":' + '"' + lmotor + '"' +
            ', "rmotor":' + '"' + rmotor + '"' +
            '} }';
        if (callback != undefined) {
            pushResponseCallback(randomId, callback);
        }
        sendDATA(command);
    }

    /**
     * 
     * @memberof! UJSONRPC
     * @param {integer} lpower 
     * @param {integer} rpower 
     * @param {string} lmotor 
     * @param {string} rmotor 
     * @param {function} callback
     */
    UJSONRPC.moveTankPowers = async function moveTankPowers(lpower, rpower, lmotor, rmotor, callback) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.move_start_powers"' +
            ', "p": {' +
            '"lpower":' + lpower +
            ', "rpower":' + rpower +
            ', "lmotor":' + '"' + lmotor + '"' +
            ', "rmotor":' + '"' + rmotor + '"' +
            '} }';
        if (callback != undefined) {
            pushResponseCallback(randomId, callback);
        }
        sendDATA(command);
    }

    /**
     * 
     * @memberof! UJSONRPC
     * @param {integer} volume 
     * @param {integer} note 
     */
    UJSONRPC.soundBeep = async function soundBeep(volume, note) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.sound_beep"' +
            ', "p": {' +
            '"volume":' + volume +
            ', "note":' + note +
            '} }';
        sendDATA(command);
    }

    /**
     * @memberof! UJSONRPC
     */
    UJSONRPC.soundStop = async function soundStop() {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.sound_off"' +
            '}';
        sendDATA(command);
    }

    /**
     * 
     * @memberof! UJSONRPC
     * @param {string} port 
     * @param {integer} power 
     * @param {integer} stall 
     */
    UJSONRPC.motorPwm = async function motorPwm(port, power, stall) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.motor_pwm", "p": {"port":' + '"' + port + '"' +
            ', "power":' + power + ', "stall":' + stall + '} }';
        sendDATA(command);
    }

    /**
     * 
     * @memberof! UJSONRPC
     * @param {function} callback
     */
    UJSONRPC.getFirmwareInfo = async function getFirmwareInfo(callback) {
        var randomId = generateId();

        var command = '{"i":' + '"' + randomId + '"' + ', "m": "get_hub_info" ' + '}';
        sendDATA(command);
        if (callback != undefined) {
            getFirmwareInfoCallback = [randomId, callback];
        }
    }

    /**
     * @memberof! UJSONRPC
     * @param {function} callback 
     */
    UJSONRPC.triggerCurrentState = async function triggerCurrentState(callback) {
        var randomId = generateId();

        var command = '{"i":' + '"' + randomId + '"' + ', "m": "trigger_current_state" ' + '}';
        sendDATA(command);
        if (callback != undefined) {
            triggerCurrentStateCallback = callback;
        }
    }

    /** 
     * 
     * @memberof! UJSONRPC
     * @param {integer} slotid 
     */
    UJSONRPC.programExecute = async function programExecute(slotid) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' + ', "m": "program_execute", "p": {"slotid":' + slotid + '} }';
        sendDATA(command);
    }

    /**
     * @memberof! UJSONRPC
     */
    UJSONRPC.programTerminate = function programTerminate() {

        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "program_terminate"' +
            '}';

        sendDATA(command);
    }

    /**
     * @memberof! UJSONRPC
     * @param {string} projectName name of the project
     * @param {integer} type type of data (micropy or scratch)
     * @param {string} data entire data to send in ASCII
     * @param {integer} slotid slot to which to assign the program
     */
    UJSONRPC.startWriteProgram = async function startWriteProgram (projectName, type, data, slotid) {

        console.log("%cTuftsCEEO ", "color: #3ba336;", "in startWriteProgram...");
        console.log("%cTuftsCEEO ", "color: #3ba336;", "constructing start_write_program script...");

        if (type == "python") {
            var typeInt = 0;
        }

        // construct the UJSONRPC packet to start writing program

        var dataSize = (new TextEncoder().encode(data)).length;

        var randomId = generateId();

        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "start_write_program", "p": {' +
            '"meta": {' +
            '"created": ' + parseInt(Date.now()) +
            ', "modified": ' + parseInt(Date.now()) +
            ', "name": ' + '"' + btoa(projectName) + '"' +
            ', "type": ' + typeInt +
            ', "project_id":' + Math.floor(Math.random() * 1000) +
            '}' +
            ', "fname": ' + '"' + projectName + '"' +
            ', "size": ' + dataSize +
            ', "slotid": ' + slotid +
            '} }';

        console.log("%cTuftsCEEO ", "color: #3ba336;", "constructed start_write_program script...");

        // assign function to start sending packets after confirming blocksize and transferid
        startWriteProgramCallback = [randomId, writePackageFunc];

        console.log("%cTuftsCEEO ", "color: #3ba336;", "sending start_write_program script");

        sendDATA(command);

        // check if start_write_program received a response after 5 seconds
        writeProgramSetTimeout = setTimeout(function () {
            if (startWriteProgramCallback != undefined) {
                if (funcAfterError != undefined) {
                    funcAfterError("5 seconds have passed without response... Please reboot the hub and try again.")
                }
                console.error("%cTuftsCEEO ", "color: #3ba336;", "5 seconds have passed without response... Please reboot the hub and try again.");
            }
        }, 5000)

        // function to write the first packet of data
        function writePackageFunc(blocksize, transferid) {

            console.log("%cTuftsCEEO ", "color: #3ba336;", "in writePackageFunc...");

            console.log("%cTuftsCEEO ", "color: #3ba336;", "stringified the entire data to send: ", data);

            // when data's length is less than the blocksize limit of sending data
            if (data.length <= blocksize) {
                console.log("%cTuftsCEEO ", "color: #3ba336;", "data's length is less than the blocksize of ", blocksize);

                // if the data's length is not zero (not empty)
                if (data.length != 0) {

                    var dataToSend = data.substring(0, data.length); // get the entirety of data

                    console.log("%cTuftsCEEO ", "color: #3ba336;", "data's length is not zero, sending the entire data: ", dataToSend);

                    var base64data = btoa(dataToSend); // encode the packet to base64

                    UJSONRPC.writePackage(base64data, transferid); // send the packet

                    // writeProgram's callback defined by the user
                    if (writeProgramCallback != undefined) {
                        writeProgramCallback();
                    }

                }
                // the package to send is empty, so throw error
                else {
                    throw new Error("package to send is initially empty");
                }

            }
            // if the length of data to send is larger than the blocksize, send only a blocksize amount
            // and save the remaining data to send packet by packet
            else if (data.length > blocksize) {

                console.log("%cTuftsCEEO ", "color: #3ba336;", "data's length is more than the blocksize of ", blocksize);

                var dataToSend = data.substring(0, blocksize); // get the first block of packet

                console.log("%cTuftsCEEO ", "color: #3ba336;", "sending the blocksize amount of data: ", dataToSend);

                var base64data = btoa(dataToSend); // encode the packet to base64

                var msgID = UJSONRPC.writePackage(base64data, transferid); // send the packet

                var remainingData = data.substring(blocksize, data.length); // remove the portion just sent from data

                console.log("%cTuftsCEEO ", "color: #3ba336;", "reassigning writePackageInformation with message ID: ", msgID);
                console.log("%cTuftsCEEO ", "color: #3ba336;", "reassigning writePackageInformation with remainingData: ", remainingData);

                // update package information to be used for sending remaining packets
                writePackageInformation = [msgID, remainingData, transferid, blocksize];

            }

        }

    }



    /**
     * 
     * @memberof! UJSONRPC
     * @param {string} base64data base64 encoded data to send
     * @param {string} transferid transferid of this program write process
     * @returns {string} the randomly generated message id used to send this UJSONRPC script
     */
    UJSONRPC.writePackage = function writePackage(base64data, transferid) {

        var randomId = generateId();
        var writePackageCommand = '{"i":' + '"' + randomId + '"' +
            ', "m": "write_package", "p": {' +
            '"data": ' + '"' + base64data + '"' +
            ', "transferid": ' + '"' + transferid + '"' +
            '} }';

        sendDATA(writePackageCommand);

        return randomId;

    }

    /**
     * @memberof! UJSONRPC
     */
    UJSONRPC.getStorageStatus = function getStorageStatus() {

        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "get_storage_status"' +
            '}';

        sendDATA(command);

    }

    /**
     * @memberof! UJSONRPC
     * @param {string} slotid 
     */
    UJSONRPC.removeProject = function removeProject(slotid) {

        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "remove_project", "p": {' +
            '"slotid": ' + slotid +
            '} }';

        sendDATA(command);
    }

    /**
     * 
     * @memberof! UJSONRPC
     * @param {string} oldslotid 
     * @param {string} newslotid 
     */
    UJSONRPC.moveProject = function moveProject(oldslotid, newslotid) {

        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "move_project", "p": {' +
            '"old_slotid": ' + oldslotid +
            ', "new_slotid: ' + newslotid +
            '} }';

        sendDATA(command);

    }

    UJSONRPC.centerButtonLightUp = function centerButtonLightUp(color) {
        var randomId = generateId();
        var command = '{"i":' + '"' + randomId + '"' +
            ', "m": "scratch.center_button_lights", "p": {' +
            '"color": '  + color + 
            '} }';

        sendDATA(command);
    }
    //////////////////////////////////////////
    //                                      //
    //          Private Functions           //
    //                                      //
    //////////////////////////////////////////
    
    /**
    * @private
    * @param {function} callback 
    */
    async function triggerCurrentState(callback) {

        UJSONRPC.triggerCurrentState(callback);
    }

    /** 
     * 
     * @private
     * @param {string} id 
     * @param {string} funcName 
     */
    function pushResponseCallback(id, funcName) {
        var toPush = []; // [ ujson string id, function pointer ]

        toPush.push(id);
        toPush.push(funcName);

        // responseCallbacks has elements in it
        if (responseCallbacks.length > 0) {

            var emptyFound = false; // empty index was found flag

            // insert the pointer to the function where index is empty
            for (var index in responseCallbacks) {
                if (responseCallbacks[index] == undefined) {
                    responseCallbacks[index] = toPush;
                    emptyFound = true;
                }
            }

            // if all indices were full, push to the back
            if (!emptyFound) {
                responseCallbacks.push(toPush);
            }

        }
        // responseCallbacks current has no elements in it
        else {
            responseCallbacks.push(toPush);
        }

    }

    /**  Sleep function
     * @private
     * @param {number} ms Miliseconds to sleep
     * @returns {Promise} 
     */
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**  generate random id for UJSONRPC messages
     * @private
     * @returns {string}
     */
    function generateId() {
        var generatedID = ""
        var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

        for (var i = 0; i < 4; i++) {
            var randomIndex = Math.floor(Math.random() * characters.length);
            generatedID = generatedID + characters[randomIndex];
        }

        return generatedID;
    }

    /**  Prompt user to select web serial port and make connection to SPIKE Prime
     * <p> Effect Makes prompt in Google Chrome ( Google Chrome Browser needs "Experimental Web Interface" enabled) </p>
     * <p> Note: </p>
     * <p> This function is to be executed before reading in JSON RPC streams from the hub </p>
     * <p> This function needs to be called when system is handling a user gesture (like button click) </p>
     * @private
     * @returns {boolean} True if web serial initialization is successful, false otherwise
     */
    async function initWebSerial() {
        try {
            var success = false;

            port = await navigator.serial.getPorts();
            console.log("%cTuftsCEEO ", "color: #3ba336;", "ports:", port);
            // select device
            port = await navigator.serial.requestPort({
                // filters:[filter]
            });
            // wait for the port to open.
            try {
                await port.open({ baudRate: 115200 });
            }
            catch (er) {
                console.error("%cTuftsCEEO ", "color: #3ba336;", er);

                // check if system requires baudRate syntax
                if (er.message.indexOf("baudrate") > -1) {
                    console.log("%cTuftsCEEO ", "color: #3ba336;", "baudRate needs to be baudrate");
                    await port.open({ baudrate: 115200 });
                }

                // check if error is due to unsuccessful closing of previous port
                else if (er.message.indexOf("close") > -1) {
                    if (funcAfterError != undefined) {
                        funcAfterError(er + "\nPlease try again. If error persists, refresh this environment.");
                    }
                    console.error("%cTuftsCEEO ", "color: #3ba336;", "Please check if you have any other window or app currently connected to your SPIKE Prime.");

                    await port.close();
                }

                // check if error in port.open was because it was already open
                /* "failed to open serial port" */
                else if (er.message.indexOf("open") > -1) {
                    try {
                        await port.close();
                    }
                    catch (err) {
                        console.error("%cTuftsCEEO ", "color: #3ba336;", err);
                        console.error("%cTuftsCEEO ", "color: #3ba336;", "Please check if you have any other window or app currently connected to your SPIKE Prime.");
                    }
                }

                else {
                    if (funcAfterError != undefined) {
                        funcAfterError(er + "\nPlease try again. If error persists, refresh this environment.");
                    }
                    console.error("%cTuftsCEEO ", "color: #3ba336;", "If error persists, refresh this environment");
                }
                await port.close();
            }

            if (port.readable) {
                success = true;
            }
            else {
                success = false;
            }

            return success;


        } catch (e) {
            if (e.message.indexOf("close") > -1) {
                await port.close;
            }
            else {
                console.log("%cTuftsCEEO ", "color: #3ba336;", "Cannot read port:", e);
                if (funcAfterError != undefined) {
                    funcAfterError(e);
                }
                return false;
            }
        }
    }

    /**  Initialize writer object before sending commands
     * @private
     * 
     */
    function setupWriter() {
        // if writer not yet defined:
        if (typeof writer === 'undefined') {
            // set up writer for the first time
            const encoder = new TextEncoderStream();
            writableStreamClosed = encoder.readable.pipeTo(port.writable);
            writer = encoder.writable.getWriter();
        }
    }

    /** clean the json_string for concatenation into jsonline
     * @private
     * 
     * @param {any} json_string 
     * @returns {string}
     */
    function cleanJsonString(json_string) {
        var cleanedJsonString = "";
        json_string = json_string.trim();

        let findEscapedQuotes = /\\"/g;

        cleanedJsonString = json_string.replace(findEscapedQuotes, '"');
        cleanedJsonString = cleanedJsonString.substring(1, cleanedJsonString.length - 1);
        // cleanedJsonString = cleanedJsonString.replace(findNewLines,'');

        return cleanedJsonString;
    }

    /** Process the UJSON RPC script
     * 
     * @private
     * @param {any} lastUJSONRPC 
     * @param {string} [json_string="undefined"] 
     * @param {boolean} [testing=false] 
     * @param {any} callback 
     */
    async function processFullUJSONRPC(lastUJSONRPC, cleanedJsonString = "undefined", json_string = "undefined", testing = false, callback) {
        try {

            var parseTest = await JSON.parse(lastUJSONRPC)
            
            if (testing) {
                console.log("%cTuftsCEEO ", "color: #3ba336;", "processing FullUJSONRPC line: ", lastUJSONRPC);
            }

            // update hub information using lastUJSONRPC
            if (parseTest["m"] == 0) {
                await updateHubPortsInfo();
            }
            PrimeHubEventHandler();

            if (funcWithStream) {
                await funcWithStream();
            }

        }
        catch (e) {
            // don't throw error when failure of processing UJSONRPC is due to micropython
            if (lastUJSONRPC.indexOf("Traceback") == -1 && lastUJSONRPC.indexOf(">>>") == -1 && json_string.indexOf("Traceback") == -1 && json_string.indexOf(">>>") == -1) {
                if (funcAfterError != undefined) {
                    funcAfterError("Fatal Error: Please close any other window or program that is connected to your SPIKE Prime");
                }
            }
            console.log(e);
            console.log("%cTuftsCEEO ", "color: #3ba336;", "error parsing lastUJSONRPC: ", lastUJSONRPC);
            console.log("%cTuftsCEEO ", "color: #3ba336;", "current jsonline: ", jsonline);
            console.log("%cTuftsCEEO ", "color: #3ba336;", "current cleaned json_string: ", cleanedJsonString)
            console.log("%cTuftsCEEO ", "color: #3ba336;", "current json_string: ", json_string);
            console.log("%cTuftsCEEO ", "color: #3ba336;", "current value: ", value);

            if (callback != undefined) {
                callback();
            }

        }
    }

    /**  Process a packet in UJSONRPC
    * @private
    * 
    */
    async function parsePacket(value, testing = false, callback) {

        // console.log("%cTuftsCEEO ", "color: #3ba336;", value);

        // stringify the packet to look for carriage return
        var json_string = await JSON.stringify(value);
        
        // remove quotation marks from json_string
        var cleanedJsonString = cleanJsonString(json_string);
        // cleanedJsonString = cleanedJsonString.replace(findNewLines,'');

        // console.log(cleanedJsonString);

        jsonline = jsonline + cleanedJsonString; // concatenate packet to data
        jsonline = jsonline.trim();

        // regex search for carriage return
        let pattern = /\\r/g;
        var carriageReIndex = jsonline.search(pattern);

        // there is at least one carriage return in this packet
        if (carriageReIndex > -1) {
            //////////////////////////////// NEW parsePacket implementation ongoing since (29/12/20)

            let jsonlineSplitByCR = jsonline.split(/\\r/); // array of jsonline split by \r
            
            jsonline = ""; //reset jsonline
            /*
                each element in this array will be assessed for processing, 
                and the last element, if unable to be processed, will be concatenated to jsonline
            */

            for (let i = 0; i < jsonlineSplitByCR.length; i++) {

                // set lastUJSONRPC to an element in split array
                lastUJSONRPC = jsonlineSplitByCR[i]; 
                // remove any newline character in the beginning of lastUJSONRPC
                if (lastUJSONRPC.search(/\\n/g) == 0)
                    lastUJSONRPC = lastUJSONRPC.substring(2, lastUJSONRPC.length);

                /* Case 1: lastUJSONRPC is a valid, complete, and standard UJSONRPC packet */
                if (lastUJSONRPC[0] == "{" && lastUJSONRPC[lastUJSONRPC.length - 1] == "}") {

                    let arrayLeftCurly = lastUJSONRPC.match(/{/g);
                    let arrayRightCurly = lastUJSONRPC.match(/}/g);
                    if (arrayLeftCurly.length === arrayRightCurly.length) {
                        /* Case 1A: complete packet*/
                        
                        await processFullUJSONRPC(lastUJSONRPC, cleanedJsonString, json_string, testing, callback);   
                    }
                    else {
                        /* Case 1B: {"i": 1234, "r": {} */
                        jsonline = lastUJSONRPC;    
                    }
                }
                /* Case 3: lastUJSONRPC is a micropy print result */
                else if (lastUJSONRPC != "" && lastUJSONRPC.indexOf('"p":') == -1 && lastUJSONRPC.indexOf('],') == -1 && lastUJSONRPC.indexOf('"m":') == -1 && 
                    lastUJSONRPC.indexOf('}') == -1 && lastUJSONRPC.indexOf('{"i":') == -1 && lastUJSONRPC.indexOf('{') == -1 ) {
                        /* filter reboot message */
                        var rebootMessage = 
                        'Traceback (most recent call last): File "main.py", line 8, in <module> File "hub_runtime.py", line 1, in start File "event_loop/event_loop.py", line 1, in run_forever File "event_loop/event_loop.py", line 1, in step KeyboardInterrupt: MicroPython v1.12-1033-g97d7f7dd4 on 2020-09-18; LEGO Technic Large Hub with STM32F413xx Type "help()" for more in formation. >>> HUB: sync filesystems HUB: soft reboot'
                        let rebootMessageRemovedWS = rebootMessage.replace(/[' ']/g, "");
                        let lastUJSONRPCRemovedWS = lastUJSONRPC.replace(/[' ']/g, "");
                        if (rebootMessageRemovedWS.indexOf(lastUJSONRPCRemovedWS) == -1) {
                            console.log("%cTuftsCEEO ", "color: #3ba336;", "micropy print: ", lastUJSONRPC);
                            if (funcAfterPrint != undefined)
                                funcAfterPrint(lastUJSONRPC);
                        }
                }
                /* Case 3: lastUJSONRPC is only a portion of a standard UJSONRPC packet 
                    Then lastUJSONRPC must be EITHER THE FIRST OR THE LAST ELEMENT in jsonlineSplitByCR
                    because
                    an incomplete UJSONRPC can either be 
                    Case 3A: the beginning portion of a UJSONRPC packet with no \r in the end (LAST)
                    Case 3B: the last portion of a UJSONRPC packet with \r in the end (FIRST)
                */
                else {
                    /* Case 3A: */
                    if (lastUJSONRPC[0] == "{") {
                        jsonline = lastUJSONRPC;
                        // console.log("TEST (last elemnt in split array): ", i == jsonlineSplitByCR.length-1);
                        // console.log("%cTuftsCEEO ", "color: #3ba336;", "jsonline was reset to:" + jsonline);
                    }
                    /* Case 3B: */
                    else {
                        /* the last portion of UJSONRPC cannot be concatenated to form a full packet
                            -> need to purge lastUJSONRPC
                        */
                    }
                }
            }
        }

    }


    /**  Continuously take UJSON RPC input from SPIKE Prime
     * @private
     */
    async function streamUJSONRPC() {
        try {
            // COMMENTED BY JEREMY JUNG (DECEMBER/10/2020)
            // var triggerCurrentStateInterval = setInterval(function() {
                // UJSONRPC.triggerCurrentState();
            // }, 500);

            var firstReading = true;
            // read when port is set up
            while (port.readable) {

                // initialize readers
                const decoder = new TextDecoderStream();
                const readableStreamClosed = port.readable.pipeTo(decoder.writable);
                reader = decoder.readable.getReader();

                // continuously get
                while (true) {
                    try {

                        if (firstReading) {
                            console.log("%cTuftsCEEO ", "color: #3ba336;", "##### READING FIRST UJSONRPC LINE ##### CHECKING VARIABLES");
                            console.log("%cTuftsCEEO ", "color: #3ba336;", "jsonline: ", jsonline);
                            console.log("%cTuftsCEEO ", "color: #3ba336;", "lastUJSONRPC: ", lastUJSONRPC);
                            firstReading = false;
                        }
                        // read UJSON RPC stream ( actual data in {value} )
                        ({ value, done } = await reader.read());

                        // log value
                        if (micropython_interpreter) {
                            console.log("%cTuftsCEEO ", "color: #3ba336;", value);
                        }

                        // console.log("%cTuftsCEEO ", "color: #3ba336;", value);

                        //concatenate UJSONRPC packets into complete JSON objects
                        if (value) {
                            await parsePacket(value);
                        }
                        if (done) {
                            serviceActive = false;
                            // reader has been canceled.
                            console.log("%cTuftsCEEO ", "color: #3ba336;", "[readLoop] DONE", done);
                        }
                    }
                    // error handler
                    catch (error) {
                        console.log("%cTuftsCEEO ", "color: #3ba336;", '[readLoop] ERROR', error);

                        serviceActive = false;

                        if (funcAfterDisconnect != undefined) {
                            funcAfterDisconnect();
                        }

                        if (funcAfterError != undefined) {
                            funcAfterError("SPIKE Prime hub has been disconnected");
                        }

                        writer.close();
                        //await writer.releaseLock();
                        await writableStreamClosed;

                        reader.cancel();
                        //await reader.releaseLock();
                        await readableStreamClosed.catch(reason => { });

                        await port.close();

                        writer = undefined;
                        reader = undefined;
                        jsonline = "";
                        lastUJSONRPC = undefined;
                        json_string = undefined;
                        cleanedJsonString = undefined;

                        break; // stop trying to read
                    }
                } // end of: while (true) [reader loop]

                // release the lock
                reader.releaseLock();

            } // end of: while (port.readable) [checking if readable loop]
            console.log("%cTuftsCEEO ", "color: #3ba336;", "- port.readable is FALSE")
        } // end of: trying to open port
        catch (e) {
            serviceActive = false;
            // Permission to access a device was denied implicitly or explicitly by the user.
            console.log("%cTuftsCEEO ", "color: #3ba336;", 'ERROR trying to open:', e);
        }
    }

    /** Get the devices that are connected to each port on the SPIKE Prime
     * <p> Effect: </p>
     * <p> Modifies {ports} global variable </p>
     * <p> Modifies {hub} global variable </p>
     * @private
     */
    async function updateHubPortsInfo() {

        // if a complete ujson rpc line was read
        if (lastUJSONRPC) {
            var data_stream; //UJSON RPC info to be parsed

            //get a line from the latest JSON RPC stream and parse to devices info
            try {
                data_stream = await JSON.parse(lastUJSONRPC);
                data_stream = data_stream.p;
            }
            catch (e) {
                console.log("%cTuftsCEEO ", "color: #3ba336;", "error parsing lastUJSONRPC at updateHubPortsInfo", lastUJSONRPC);
                console.log("%cTuftsCEEO ", "color: #3ba336;", typeof lastUJSONRPC);
                console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC.p);

                if (funcAfterError != undefined) {
                    funcAfterError("Fatal Error: Please reboot the Hub and refresh this environment");
                }

            }

            var index_to_port = ["A", "B", "C", "D", "E", "F"]

            // iterate through each port and assign a device_type to {ports}
            for (var key = 0; key < 6; key++) {

                let device_value = { "device": "none", "data": {} }; // value to go in ports associated with the port letter keys

                try {
                    var letter = index_to_port[key];

                    // get SMALL MOTOR information
                    if (data_stream[key][0] == 48) {

                        // parse motor information
                        var Mspeed = await data_stream[key][1][0];
                        var Mangle = await data_stream[key][1][1];
                        var Muangle = await data_stream[key][1][2];
                        var Mpower = await data_stream[key][1][3];

                        // populate value object
                        device_value.device = "smallMotor";
                        device_value.data = { "speed": Mspeed, "angle": Mangle, "uAngle": Muangle, "power": Mpower };
                        ports[letter] = device_value;

                    }
                    // get BIG MOTOR information
                    else if (data_stream[key][0] == 49) {

                        // parse motor information
                        var Mspeed = await data_stream[key][1][0];
                        var Mangle = await data_stream[key][1][1];
                        var Muangle = await data_stream[key][1][2];
                        var Mpower = await data_stream[key][1][3];

                        // populate value object
                        device_value.device = "bigMotor";
                        device_value.data = { "speed": Mspeed, "angle": Mangle, "uAngle": Muangle, "power": Mpower };
                        ports[letter] = device_value;

                    }
                    // get ULTRASONIC sensor information
                    else if (data_stream[key][0] == 62) {

                        // parse ultrasonic sensor information
                        var Udist = await data_stream[key][1][0];

                        // populate value object
                        device_value.device = "ultrasonic";
                        device_value.data = { "distance": Udist };
                        ports[letter] = device_value;

                        /* check if callback from wait_for_distance_farther_than() can be executed */
                        if (waitForDistanceFartherThanCallback != undefined) {
                            let thresholdDistance = waitForDistanceFartherThanCallback[0];

                            if (Udist > thresholdDistance) {
                                
                                // current distance is farther than threshold, so execute callback
                                waitForDistanceFartherThanCallback[1]();
                                waitForDistanceFartherThanCallback = undefined; // reset callback
                            }
                        }
                    
                        /* check if callback from wait_for_distance_closer_than() can be executed */
                        if (waitForDistanceCloserThanCallback != undefined) {
                            let thresholdDistance = waitForDistanceCloserThanCallback[0];
                            
                            if (Udist < thresholdDistance) {

                                // current distance is closer than threshold, so execute callback
                                waitForDistanceCloserThanCallback[1]();
                                waitForDistanceCloserThanCallback = undefined; // reset callback
                            }
                        }


                    }
                    // get FORCE sensor information
                    else if (data_stream[key][0] == 63) {

                        // parse force sensor information
                        var Famount = await data_stream[key][1][0];
                        var Fbinary = await data_stream[key][1][1];
                        var Fbigamount = await data_stream[key][1][2];

                        // convert the binary output to boolean for "pressed" key
                        if (Fbinary == 1) {
                            var Fboolean = true;
                        } else {
                            var Fboolean = false;
                        }
                        // execute callback from ForceSensor.wait_until_pressed() 
                        if (Fboolean) {
                            // execute call back from wait_until_pressed() if it is defined
                            funcAfterForceSensorPress !== undefined && funcAfterForceSensorPress();

                            // destruct callback function
                            funcAfterForceSensorPress = undefined;

                            // indicate that the ForceSensor was pressed
                            ForceSensorWasPressed = true;
                        }
                        // execute callback from ForceSensor.wait_until_released()
                        else {
                            // check if the Force Sensor was just released
                            if (ForceSensorWasPressed) {
                                ForceSensorWasPressed = false;
                                funcAfterForceSensorRelease !== undefined && funcAfterForceSensorRelease();
                                funcAfterForceSensorRelease = undefined;
                            }
                        }

                        // populate value object
                        device_value.device = "force";
                        device_value.data = { "force": Famount, "pressed": Fboolean, "forceSensitive": Fbigamount }
                        ports[letter] = device_value;
                    }
                    // get COLOR sensor information
                    else if (data_stream[key][0] == 61) {

                        // parse color sensor information
                        var Creflected = await data_stream[key][1][0];
                        var CcolorID = await data_stream[key][1][1];
                        var Ccolor = colorDictionary[CcolorID];
                        var Cr = await data_stream[key][1][2];
                        var Cg = await data_stream[key][1][3];
                        var Cb = await data_stream[key][1][4];
                        var rgb_array = [Cr, Cg, Cb];

                        // populate value object
                        device_value.device = "color";

                        // convert Ccolor to lower case because in the SPIKE APP the color is lower case
                        if (Ccolor !== undefined)
                            Ccolor = Ccolor.toLowerCase();
                        else
                            Ccolor = "null";
                        device_value.data = { "reflected": Creflected, "color": Ccolor, "RGB": rgb_array };

                        // execute wait_until_color callback when color matches its argument
                        if (waitUntilColorCallback != undefined)
                            if (Ccolor == waitUntilColorCallback[0]) {
                                waitUntilColorCallback[1]();

                                waitUntilColorCallback = undefined;
                            }
                        
                        if (lastDetectedColor != Ccolor) {
                            
                            if (funcAfterNewColor != undefined) {
                                funcAfterNewColor(Ccolor);
                                funcAfterNewColor = undefined;
                            }

                            lastDetectedColor = Ccolor;
                        }
                        ports[letter] = device_value;
                    }
                    /// NOTHING is connected
                    else if (data_stream[key][0] == 0) {
                        // populate value object
                        device_value.device = "none";
                        device_value.data = {};
                        ports[letter] = device_value;
                    }

                    ports.time = Date.now();

                    //parse hub information
                    var gyro_x = data_stream[6][0];
                    var gyro_y = data_stream[6][1];
                    var gyro_z = data_stream[6][2];
                    var gyro = [gyro_x, gyro_y, gyro_z];
                    hub["gyro"] = gyro;

                    var newOri = setHubOrientation(gyro);
                    // see if currently detected orientation is different from the last detected orientation
                    if (newOri !== lastHubOrientation) {
                        lastHubOrientation = newOri;

                        typeof funcAfterNewOrientation == "function" && funcAfterNewOrientation(newOri);
                        funcAfterNewOrientation = undefined;
                    }

                    var accel_x = data_stream[7][0];
                    var accel_y = data_stream[7][1];
                    var accel_z = data_stream[7][2];
                    var accel = [accel_x, accel_y, accel_z];
                    hub["accel"] = accel;

                    var posi_x = data_stream[8][0];
                    var posi_y = data_stream[8][1];
                    var posi_z = data_stream[8][2];
                    var pos = [posi_x, posi_y, posi_z];
                    hub["pos"] = pos;

                } catch (e) {
                    console.log(e);
                 } //ignore errors
            }
        }
    }

    /**  Catch hub events in UJSONRPC
     * <p> Effect: </p>
     * <p> Logs in the console when some particular messages are caught </p>
     * <p> Assigns the hub events global variables </p>
     * @private
     */
    async function PrimeHubEventHandler() {

        var parsedUJSON = await JSON.parse(lastUJSONRPC);

        var messageType = parsedUJSON["m"];

        //catch runtime_error made at ujsonrpc level
        if (messageType == "runtime_error") {
            var decodedResponse = atob(parsedUJSON["p"][3]);

            decodedResponse = JSON.stringify(decodedResponse);

            console.log("%cTuftsCEEO ", "color: #3ba336;", decodedResponse);

            var splitData = decodedResponse.split(/\\n/); // split the code by every newline

            // execute function after print if defined (only print the last line of error message)
            if (funcAfterError != undefined) {
                var errorType = splitData[splitData.length - 2];

                // error is a syntax error
                if (errorType.indexOf("SyntaxError") > -1) {
                    /* get the error line number*/
                    var lineNumberLine = splitData[splitData.length - 3];
                    console.log("%cTuftsCEEO ", "color: #3ba336;", "lineNumberLine: ", lineNumberLine);
                    var indexLine = lineNumberLine.indexOf("line");
                    var lineNumberSubstring = lineNumberLine.substring(indexLine, lineNumberLine.length);
                    var numberPattern = /\d+/g;
                    var lineNumber = lineNumberSubstring.match(numberPattern)[0];
                    console.log("%cTuftsCEEO ", "color: #3ba336;", lineNumberSubstring.match(numberPattern));
                    console.log("%cTuftsCEEO ", "color: #3ba336;", "lineNumber:", lineNumber);
                    console.log("%cTuftsCEEO ", "color: #3ba336;", "typeof lineNumber:", typeof lineNumber);
                    var lineNumberInNumber = parseInt(lineNumber) - 5;
                    console.log("%cTuftsCEEO ", "color: #3ba336;", "typeof lineNumberInNumber:", typeof lineNumberInNumber);

                    funcAfterError("line " + lineNumberInNumber + ": " + errorType);
                }
                else {
                    funcAfterError(errorType);
                }
            }
        }
        else if (messageType == 0) {
            /*
                DEV NOTE (26/12/2020):
                    messageType = 0 is regular UJSONRPC stream.
                    Pixel matrix SOMETIMES shows in this message, but exactly when is not clear.
            */
            // console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);
        }
        // storage information
        else if (messageType == 1) {

            var storageInfo = parsedUJSON["p"]["slots"]; // get info of all the slots

            for (var slotid in storageInfo) {
                hubProjects[slotid] = storageInfo[slotid]; // reassign hubProjects global variable
            }

        }
        // battery status
        else if (messageType == 2) {
            batteryAmount = parsedUJSON["p"][1];
        }
        // give center button click, left, right (?)
        else if (messageType == 3) {
            console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);
            if (parsedUJSON.p[0] == "center") {
                hubMainButton.pressed = true;

                if (parsedUJSON.p[1] > 0) {
                    hubMainButton.pressed = false;
                    hubMainButton.duration = parsedUJSON.p[1];
                }
            }
            else if (parsedUJSON.p[0] == "connect") {
                hubBluetoothButton.pressed = true;

                if (parsedUJSON.p[1] > 0) {
                    hubBluetoothButton.pressed = false;
                    hubBluetoothButton.duration = parsedUJSON.p[1];
                }
            }
            else if (parsedUJSON.p[0] == "left") {
                hubLeftButton.pressed = true;

                // execute callback for wait_until_pressed() if defined
                if (funcAfterLeftButtonPress != undefined ) {
                    funcAfterLeftButtonPress();
                }
                funcAfterLeftButtonPress = undefined;

                if (parsedUJSON.p[1] > 0) {
                    hubLeftButton.pressed = false;
                    hubLeftButton.duration = parsedUJSON.p[1];

                    // execute callback for wait_until_released() if defined
                    if (funcAfterLeftButtonRelease != undefined ) {
                        funcAfterLeftButtonRelease();
                    }

                    funcAfterLeftButtonRelease = undefined;
                }

            }
            else if (parsedUJSON.p[0] == "right") {
                hubRightButton.pressed = true;

                // execute callback for wait_until_pressed() if defined
                if (funcAfterRightButtonPress != undefined) {
                    funcAfterRightButtonPress();
                }

                funcAfterRightButtonPress = undefined;

                if (parsedUJSON.p[1] > 0) {
                    hubRightButton.pressed = false;
                    hubRightButton.duration = parsedUJSON.p[1];

                    // execute callback for wait_until_released() if defined
                    if (funcAfterRightButtonRelease != undefined) {
                        funcAfterRightButtonRelease();
                    }

                    funcAfterRightButtonRelease = undefined;
                }
            }

        }
        // gives orientation of the hub (leftside, up,..)
        else if (messageType == 14) {
            /* this data stream is about hub orientation */

            var newOrientation = parsedUJSON.p;
            // console.log(newOrientation);
            if (newOrientation == "1") {
                lastHubOrientation = "up";
            }
            else if (newOrientation == "4") {
                lastHubOrientation = "down";
            }
            else if (newOrientation == "0") {
                lastHubOrientation = "front";
            }
            else if (newOrientation == "3") {
                lastHubOrientation = "back";
            }
            else if (newOrientation == "2") {
                lastHubOrientation = "rightside";
            }
            else if (newOrientation == "5") {
                lastHubOrientation = "leftside";
            }

            console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);
        }
        else if (messageType == 7) {
            if (funcAfterPrint != undefined) {
                funcAfterPrint(">>> Program started!");
            }
        }
        else if (messageType == 8) {
            if (funcAfterPrint != undefined) {
                funcAfterPrint(">>> Program finished!");
            }
        }
        else if (messageType == 9) {
            var encodedName = parsedUJSON["p"];
            var decodedName = atob(encodedName);
            hubName = decodedName;

            if (triggerCurrentStateCallback != undefined) {
                triggerCurrentStateCallback();
            }
        }
        else if (messageType == 11) {
            console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);
        }
        else if (messageType == 12) {
            // this is usually the response from trigger_current_state, don't console log to avoid spam
        }
        else if (messageType == 4) {
            var newGesture = parsedUJSON.p;

            if (newGesture == "3") {
                hubGesture = "freefall";
                hubGestures.push(hubGesture);
            }
            else if (newGesture == "2") {
                hubGesture = "shaken";
                hubGestures.push("shaken"); // the string is different at higher level
            }
            else if (newGesture == "1") {
                hubFrontEvent = "doubletapped";
                hubGesture = "doubletapped";
                hubGestures.push(hubGesture);
            }
            else if (newGesture == "0") {
                hubFrontEvent = "tapped";
                hubGesture = "tapped";
                hubGestures.push(hubGesture);
            }

            // execute funcAfterNewGesture callback that was taken at wait_for_new_gesture()
            if (typeof funcAfterNewGesture === "function") {
                funcAfterNewGesture(hubGesture);
                funcAfterNewGesture = undefined;
            }

            console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);

        }
        else {

            // general parameters check
            if (parsedUJSON["r"]) {
                if (parsedUJSON["r"]["slots"]) {

                    var storageInfo = parsedUJSON["r"]["slots"]; // get info of all the slots

                    for (var slotid in storageInfo) {
                        hubProjects[slotid] = storageInfo[slotid]; // reassign hubProjects global variable
                    }

                }
            }

            // getFirmwareInfo callback check
            if (getFirmwareInfoCallback != undefined) {
                if (getFirmwareInfoCallback[0] == parsedUJSON["i"]) {
                    var version = parsedUJSON["r"]["runtime"]["version"];
                    var stringVersion = ""
                    for (var index in version) {
                        if (index < version.length - 1) {
                            stringVersion = stringVersion + version[index] + ".";
                        }
                        else {
                            stringVersion = stringVersion + version[index];
                        }
                    }
                    // console.log("%cTuftsCEEO ", "color: #3ba336;", "firmware version: ", stringVersion);
                    getFirmwareInfoCallback[1](stringVersion);
                }
            }
            // COMMENTED BY JEREMY JUNG ON DECEMBER 10TH AFTER REMOVING TRIGGER_CURRENT_STATE INTERVAL
            // if (parsedUJSON.r !== undefined && parsedUJSON.r !== null) {
                // if (Object.keys(parsedUJSON.r).length !== 0 && parsedUJSON.r.constructor === Object) {
                    // console.log("%cTuftsCEEO ", "color: #3ba336;", "received response: ", lastUJSONRPC);
                // }
            // }
            // else {
                // console.log("%cTuftsCEEO ", "color: #3ba336;", "received response: ", lastUJSONRPC);
            // }

            console.log("%cTuftsCEEO ", "color: #3ba336;", "received response: ", lastUJSONRPC);
            /* See if any of the stored responseCallbacks need to be executed due to this UJSONRPC response */
            for (var index = 0; index < responseCallbacks.length; index++) {

                var currCallbackInfo = responseCallbacks[index];

                if (currCallbackInfo != undefined) {

                    if (currCallbackInfo[0] == parsedUJSON["i"]) {
                        /* the message id of UJSONRPC corresponds to that of a response callback */
                        
                        var response = "null";

                        // parse motor stoppage reason responses 
                        if (parsedUJSON["r"] == 0) {
                            response = "done";
                        }
                        else if (parsedUJSON["r"] == 2) {
                            response = "stalled";
                        }

                        // execute callback with the response
                        currCallbackInfo[1](response);

                        // empty the index of which callback that was just executed
                        responseCallbacks[index] = undefined;
                    }   
                }
            }

            // execute the callback function after sending start_write_program UJSONRPC
            if (startWriteProgramCallback != undefined) {

                console.log("%cTuftsCEEO ", "color: #3ba336;", "startWriteProgramCallback is defined. Looking for matching mesasage id: ", startWriteProgramCallback[0]);

                // check if the message id of UJSONRPC corresponds to that of a response callback
                if (startWriteProgramCallback[0] == parsedUJSON["i"]) {

                    console.log("%cTuftsCEEO ", "color: #3ba336;", "matching message id detected with startWriteProgramCallback[0]: ", startWriteProgramCallback[0])

                    // get the information for the packet sending
                    var blocksize = parsedUJSON["r"]["blocksize"]; // maximum size of each packet to be sent in bytes
                    var transferid = parsedUJSON["r"]["transferid"]; // id to use for transferring this program

                    console.log("%cTuftsCEEO ", "color: #3ba336;", "executing writePackageFunc expecting transferID of ", transferid);

                    // execute callback
                    await startWriteProgramCallback[1](blocksize, transferid);

                    console.log("%cTuftsCEEO ", "color: #3ba336;", "deallocating startWriteProgramCallback");

                    // deallocate callback
                    startWriteProgramCallback = undefined;
                }

            }

            // check if the program should write packages for a program
            if (writePackageInformation != undefined) {

                console.log("%cTuftsCEEO ", "color: #3ba336;", "writePackageInformation is defined. Looking for matching mesasage id: ", writePackageInformation[0]);

                // check if the message id of UJSONRPC corresponds to that of the first write_package script that was sent
                if (writePackageInformation[0] == parsedUJSON["i"]) {

                    console.log("%cTuftsCEEO ", "color: #3ba336;", "matching message id detected with writePackageInformation[0]: ", writePackageInformation[0]);

                    // get the information for the package sending process
                    var remainingData = writePackageInformation[1];
                    var transferID = writePackageInformation[2];
                    var blocksize = writePackageInformation[3];

                    // the size of the remaining data to send is less than or equal to blocksize
                    if (remainingData.length <= blocksize) {
                        console.log("%cTuftsCEEO ", "color: #3ba336;", "remaining data's length is less than or equal to blocksize");

                        // the size of remaining data is not zero
                        if (remainingData.length != 0) {

                            var dataToSend = remainingData.substring(0, remainingData.length);

                            console.log("%cTuftsCEEO ", "color: #3ba336;", "reminaing data's length is not zero, sending entire remaining data: ", dataToSend);

                            var base64data = btoa(dataToSend);

                            UJSONRPC.writePackage(base64data, transferID);

                            console.log("%cTuftsCEEO ", "color: #3ba336;", "deallocating writePackageInforamtion")

                            if (writeProgramCallback != undefined) {

                                writeProgramCallback();
                            }


                            writePackageInformation = undefined;
                        }
                    }
                    // the size of remaining data is more than the blocksize
                    else if (remainingData.length > blocksize) {

                        console.log("%cTuftsCEEO ", "color: #3ba336;", "remaining data's length is more than blocksize");

                        var dataToSend = remainingData.substring(0, blocksize);

                        console.log("%cTuftsCEEO ", "color: #3ba336;", "sending blocksize amount of data: ", dataToSend)

                        var base64data = btoa(dataToSend);

                        var messageid = UJSONRPC.writePackage(base64data, transferID);

                        console.log("%cTuftsCEEO ", "color: #3ba336;", "expected response with message id of ", messageid);

                        var remainingData = remainingData.substring(blocksize, remainingData.length);

                        writePackageInformation = [messageid, remainingData, transferID, blocksize];
                    }
                }
            }
        }
    }

    /** Get the orientation of the hub based on gyroscope values
     * 
     * @private
     * @param {(number|Array)} gyro 
     */
    function setHubOrientation(gyro) {
        var newOrientation;
        if (gyro[0] < 500 && gyro[0] > -500) {
            if (gyro[1] < 500 && gyro[1] > -500) {

                if (gyro[2] > 500) {
                    newOrientation = "front";
                }
                else if (gyro[2] < -500) {
                    newOrientation = "back";
                }
            }
            else if (gyro[1] > 500) {
                newOrientation = "up";
            }
            else if (gyro[1] < -500) {
                newOrientation = "down";
            }
        } else if (gyro[0] > 500) {
            newOrientation = "rightside";
        }
        else if (gyro[0] < -500) {
            newOrientation = "leftside";
        }

        return newOrientation;
    }


    /**
     * 
     * @private
     * @param {any} rawContent 
     * @returns {string}
     */
    async function parseWaitForSeconds(rawContent) {
        let index_waitForSeconds = await rawContent.indexOf("wait_for_seconds(");
        if (index_waitForSeconds > -1) {
            //find the index of rawContent at which the waitForSeconds function ends
            let index_lastparen = await rawContent.indexOf(")", index_waitForSeconds);

            //divide the rawContent into parts before the waitForSeconds and after
            let first_rawContent_part = await rawContent.substring(0, index_waitForSeconds);
            let second_rawContent_part = await rawContent.substring(index_lastparen + 1, rawContent.length);

            //find the argument of the waitForSeconds
            let waitForSeconds_string = await rawContent.substring(index_waitForSeconds, index_lastparen + 1);
            let index_first_paren = await waitForSeconds_string.indexOf("(");
            let index_last_paren = await waitForSeconds_string.indexOf(")");
            let tagName = await waitForSeconds_string.substring(index_first_paren + 1, index_last_paren);

            // get the tag's value from the cloud
            var yieldCommand = "yield(" + tagName + "000)";

            //send the final UJSONRPC script to the hub.
            let final_RPC_command;

            final_RPC_command = await first_rawContent_part + yieldCommand + second_rawContent_part;

            return parseWaitForSeconds(final_RPC_command);
        }
        else {
            return rawContent;
        }
    }

    // public members
    return {
        init: init,
        sendDATA: sendDATA,
        rebootHub: rebootHub,
        reachMicroPy: reachMicroPy,
        executeAfterInit: executeAfterInit,
        executeAfterPrint: executeAfterPrint,
        executeAfterError: executeAfterError,
        executeAfterDisconnect: executeAfterDisconnect,
        executeWithStream: executeWithStream,
        getPortsInfo: getPortsInfo,
        getPortInfo: getPortInfo,
        getBatteryStatus: getBatteryStatus,
        getFirmwareInfo: getFirmwareInfo,
        getHubInfo: getHubInfo,
        getHubName: getHubName,
        getProjects: getProjects,
        isActive: isActive,
        getBigMotorPorts: getBigMotorPorts,
        getSmallMotorPorts: getSmallMotorPorts,
        getUltrasonicPorts: getUltrasonicPorts,
        getColorPorts: getColorPorts,
        getForcePorts: getForcePorts,
        getMotorPorts: getMotorPorts,
        getMotors: getMotors,
        getDistanceSensors: getDistanceSensors,
        getColorSensors: getColorSensors,
        getForceSensors: getForceSensors,
        getLatestUJSON: getLatestUJSON,
        getBluetoothButton: getBluetoothButton,
        getMainButton: getMainButton,
        getLeftButton: getLeftButton,
        getRightButton: getRightButton,
        getHubGesture: getHubGesture,
        getHubEvent: getHubEvent,
        getHubOrientation: getHubOrientation,
        Motor: Motor,
        PrimeHub: PrimeHub,
        ForceSensor: ForceSensor,
        DistanceSensor: DistanceSensor,
        ColorSensor: ColorSensor,
        MotorPair: MotorPair,
        writeProgram: writeProgram,
        stopCurrentProgram: stopCurrentProgram,
        executeProgram: executeProgram,
        micropython: micropython // for final projects
    };
}



/*
Project Name: SPIKE Prime Web Interface
File name: micropyUtils.js
Author: Jeremy Jung
Last update: 10/22/20
Description: utility class to convert javascript variables to python variablse 
            for EN1 Simple Robotics final projects
Credits/inspirations:
History:
    Created by Jeremy on 10/18/20
(C) Tufts Center for Engineering Education and Outreach (CEEO)
NOTE:
strings need to be in single quotes
*/

var micropyUtils = {};

micropyUtils.storedVariables = {}; // all variables declared in window
micropyUtils.beginVariables = {}; // all variables declared in window before code

// automatically initialize microPyUtils to exclude predeclared variables when window loads
// this initializes after global variable declarations but before hoisted functions in <script> are executed
window.onload = function () {
    console.log("onload")
    //micropyUtils.init();
}

// this initializes after global variable declarations but before hoisted functions in <script> are executed
// this runs earlier than onload
document.addEventListener("DOMContentLoaded", function () {
    console.log("DOMCONtent")
    //micropyUtils.init();
})
//////////////////////////////////////////
//                                      //
//           Public Functions           //
//                                      //
//////////////////////////////////////////

// remember global variables declared BEFORE user code
micropyUtils.remember = function () {
    for (var name in window) {
        micropyUtils.beginVariables[name] = window[name];
    }
    console.log("remembered predeclared variables ", micropyUtils.beginVariables)
}

/* parse and add all local variable declarations to micropyUtils.storedVariables

var aString = "hi" or var aString = 'hi' > {aString: "hi"}


*/
// micropyUtils.addLocalVariables = function() {
//     // get the function definition of caller
//     var thisFunction = arguments.callee.caller.toString();

//     console.log(thisFunction);

//     // split function scope by newlines
//     var newLineRule = /\n/g
//     var arrayLines = thisFunction.split(newLineRule);

//     // filter lines that dont contain var, or contains function
//     var arrayVarLines = [];
//     for ( var index in arrayLines ) {
//         if ( arrayLines[index].indexOf("var") > -1 ) {
//             // filter out functions and objects
//             if (arrayLines[index].indexOf("function") == -1 && arrayLines[index].indexOf("{") == -1 && arrayLines[index].indexOf("}") == -1) {
//                 arrayVarLines.push(arrayLines[index]);
//             }
//         }
//     }

//     var parseRule = /[[ ]/g
//     for ( var index in arrayVarLines ) {
//         // process line
//         var processedLine = micropyUtils.processString(arrayVarLines[index]);

//         // get [datatype] object = value format
//         var listParsedLine = processedLine.split(parseRule);
//         //listParsedLine = listParsedLine.split(/[=]/g)

//         var keyValue = micropyUtils.checkString(listParsedLine);

//         // insert into variables 
//         for ( var name in keyValue ) {
//             micropyUtils.storedVariables[name] = keyValue[name];
//         }
//     }

// }

// initialize utility object (find window variables to exclude from conversion)
micropyUtils.init = function () {
    var excludeVariables = {};

    // get variables to exclude
    for (var compare in micropyUtils.beginVariables) {
        // if variables found on remember() are defined, these are not user-generated variables, so flag them predeclared
        if (typeof micropyUtils.beginVariables[compare] !== "undefined") {
            excludeVariables[compare] = "predeclared"
        }
    }

    // append window variables to micropyUtils.storedVariables, but exclude those predeclared
    for (var name in window) {
        if (excludeVariables[name] != "predeclared") {
            micropyUtils.storedVariables[name] = window[name];
        }
    }
    console.log("stored Variabls in init: ", micropyUtils.storedVariables);
}

micropyUtils.makeMicroPyDeclarations = function () {
    // initialize microPyUtils
    micropyUtils.init();

    /* add local variables of the caller of this function */
    // get the function definition of caller
    /* parse and add all local variable declarations to micropyUtils.storedVariables

    var aString = "hi" or var aString = 'hi' > {aString: "hi"}


    */
    var thisFunction = arguments.callee.caller.toString();

    console.log(thisFunction);

    // split function scope by newlines
    var newLineRule = /\n/g
    var arrayLines = thisFunction.split(newLineRule);

    // filter lines that dont contain var, or contains function
    var arrayVarLines = [];
    for (var index in arrayLines) {
        if (arrayLines[index].indexOf("var") > -1) {
            // filter out functions and objects
            if (arrayLines[index].indexOf("function") == -1 && arrayLines[index].indexOf("{") == -1 && arrayLines[index].indexOf("}") == -1) {
                arrayVarLines.push(arrayLines[index]);
            }
        }
    }

    var parseRule = /[[ ]/g
    for (var index in arrayVarLines) {
        // process line
        var processedLine = micropyUtils.processString(arrayVarLines[index]);

        // get [datatype] object = value format
        var listParsedLine = processedLine.split(parseRule);
        //listParsedLine = listParsedLine.split(/[=]/g)

        var keyValue = micropyUtils.checkString(listParsedLine);

        // insert into variables 
        for (var name in keyValue) {
            micropyUtils.storedVariables[name] = keyValue[name];
        }
    }

    /* generate lines of micropy variable declarations */
    var lines = [];
    for (var name in micropyUtils.storedVariables) {
        var variableName = name;
        if (typeof micropyUtils.storedVariables[name] !== "function" && typeof micropyUtils.storedVariables[name] !== "object") {
            var variableValue = micropyUtils.convertToString(micropyUtils.storedVariables[name]);
            lines.push("" + variableName + " = " + variableValue);

        }
    }

    return lines
}

//////////////////////////////////////////
//                                      //
//          Private Functions           //
//                                      //
//////////////////////////////////////////

// add local variables in which scope the utility tool is being used
micropyUtils.addVariables = function (object) {
    for (var name in object) {
        micropyUtils.storedVariables[name] = object[name];
    }
}


// filter out unparsable variable declarations and process valid ones
micropyUtils.processString = function (input) {
    var result = input.trim();
    var removeRule = /[;]/g
    result = result.replace(removeRule, "");
    var doubleQuotes = /[",']/g
    result = result.replace(doubleQuotes, "");
    return result;
}

// return key value pair of variable declaration
micropyUtils.checkString = function (list) {
    var result = {}; // {variable name: variable value}
    // check if list starts with var
    if (list[0] == "var") {
        var variableName = list[1];
        // check assignment operator
        if (list[2] == "=") {
            // assume the right hand side of assignment operator is only one term
            var value = list[3];

            result[variableName] = micropyUtils.convertFromString(value);

            return result;
        }
        else {
            return undefined;
        }
    }
    else {
        return undefined;
    }
}

// convert string value to correct data type value
micropyUtils.convertFromString = function (value) {
    // value is not a number
    if (isNaN(parseInt(value))) {
        // value is a bool
        if (value.indexOf("true") > -1) {
            return true;
        }
        else if (value.indexOf("false") > -1) {
            return false;
        }
        // value is a string
        else {
            return value;
        }
    }
    else {
        // value is a number
        var number = Number(value);
        return number;
    }
}

// convert datatype value to string value
micropyUtils.convertToString = function (value) {
    console.log(value)
    // value is a string, enclose with single quots and return
    if (typeof value == "string") {
        return "'" + value + "'";
    }
    else {
        // value is a number
        if (Number(value)) {
            return "" + value;
        }
        // value is boolean
        else {
            if (value) {
                return "True";
            }
            else {
                return "False";
            }
        }
    }
}

//////////////////////////////////////////
//                                      //
//                  Main                //
//                                      //
//////////////////////////////////////////

// remember predeclared variables when this file is loaded
micropyUtils.remember();