{"title":"Stanley Solutions Blog","link":[{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/","rel":"alternate"}},{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/feeds\/all.atom.xml","rel":"self"}}],"id":"https:\/\/blog.stanleysolutionsnw.com\/","updated":"2026-04-15T21:44:00-07:00","subtitle":"engineering and creativity - all under one hat","entry":[{"title":"Triggering REAPER Recording from Python","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/triggering-reaper-recording-from-python.html","rel":"alternate"}},"published":"2026-04-15T21:44:00-07:00","updated":"2026-04-15T21:44:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2026-04-15:\/triggering-reaper-recording-from-python.html","summary":"<p>Python, REAPER, and a Stream Deck? What do those things have in common? Is this one of those weird games? Not really. This is how I hooked up a Stream-Deck controller to control my REAPER audio software.<\/p>","content":"<p>Did you think you needed new ways of controlling your <a href=\"https:\/\/www.reaper.fm\/\">REAPER audio software<\/a>. I knew it! I knew it. Well, today is your lucky day.<\/p>\n<p>I did this a while back, and it was great... until I updated my Ubuntu installation, and started using a newer version of Python. Best I can tell, REAPER doesn't\nactually support Python 3.11 or newer. <em>Somebody want to prove me wrong? Leave a comment! I'd love to hear a better way to do this.<\/em> This left me flailing; looking\nfor a better solution so I could still have a big \"Record\" button on my Stream-Deck.<\/p>\n<p>This is all because REAPER has this cool system that allows you to interact with the software through a scripting interface, and even drive some actions directly\nthrough Python. Very cool for geeks like me!<\/p>\n<p>Now... this would've taken me quite a while, so I had a friend help me with some of the research. As this whole thing breaks down, you ultimately need to do the\nfollowing:<\/p>\n<ol>\n<li>Install <code>pyenv<\/code><\/li>\n<li>Install <strong>Python 3.10<\/strong> with shared-library support<\/li>\n<li>Configure REAPER to use that Python version<\/li>\n<li>Install <code>reapy-next<\/code><\/li>\n<li>Create a working <strong>toggle recording<\/strong> script<\/li>\n<\/ol>\n<hr>\n<h2>1. Install <code>pyenv<\/code><\/h2>\n<h3>Install dependencies<\/h3>\n<p>Here's a list of packages that will likely be needed to help get your <code>pyenv<\/code> system up and running.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>sudo<span class=\"w\"> <\/span>apt<span class=\"w\"> <\/span>update\nsudo<span class=\"w\"> <\/span>apt<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>-y<span class=\"w\"> <\/span>build-essential<span class=\"w\"> <\/span>libssl-dev<span class=\"w\"> <\/span>zlib1g-dev<span class=\"w\"> <\/span>\n<span class=\"w\">  <\/span>libbz2-dev<span class=\"w\"> <\/span>libreadline-dev<span class=\"w\"> <\/span>libsqlite3-dev<span class=\"w\"> <\/span>curl<span class=\"w\"> <\/span>\n<span class=\"w\">  <\/span>libncursesw5-dev<span class=\"w\"> <\/span>xz-utils<span class=\"w\"> <\/span>tk-dev<span class=\"w\"> <\/span>libxml2-dev<span class=\"w\"> <\/span>\n<span class=\"w\">  <\/span>libxmlsec1-dev<span class=\"w\"> <\/span>libffi-dev<span class=\"w\"> <\/span>liblzma-dev\n<\/code><\/pre><\/div>\n\n<h3>Install <code>pyenv<\/code><\/h3>\n<div class=\"highlight\"><pre><span><\/span><code>curl<span class=\"w\"> <\/span>https:\/\/pyenv.run<span class=\"w\"> <\/span><span class=\"p\">|<\/span><span class=\"w\"> <\/span>bash\n<\/code><\/pre><\/div>\n\n<h3>Add <code>pyenv<\/code> to your shell<\/h3>\n<p>Add these lines to <code>~\/.bashrc<\/code>:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"nb\">export<\/span><span class=\"w\"> <\/span><span class=\"nv\">PATH<\/span><span class=\"o\">=<\/span><span class=\"s2\">&quot;<\/span><span class=\"nv\">$HOME<\/span><span class=\"s2\">\/.pyenv\/bin:<\/span><span class=\"nv\">$PATH<\/span><span class=\"s2\">&quot;<\/span>\n<span class=\"nb\">eval<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;<\/span><span class=\"k\">$(<\/span>pyenv<span class=\"w\"> <\/span>init<span class=\"w\"> <\/span>-<span class=\"k\">)<\/span><span class=\"s2\">&quot;<\/span>\n<span class=\"nb\">eval<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;<\/span><span class=\"k\">$(<\/span>pyenv<span class=\"w\"> <\/span>virtualenv-init<span class=\"w\"> <\/span>-<span class=\"k\">)<\/span><span class=\"s2\">&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>Reload your shell:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"nb\">source<\/span><span class=\"w\"> <\/span>~\/.bashrc\n<\/code><\/pre><\/div>\n\n<hr>\n<h2>2. Install Python 3.10 with shared-library support<\/h2>\n<p>REAPER <strong>requires<\/strong> the Python shared library (<code>libpython3.10.so<\/code>).<\/p>\n<p>Install Python 3.10:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>env<span class=\"w\"> <\/span><span class=\"nv\">PYTHON_CONFIGURE_OPTS<\/span><span class=\"o\">=<\/span><span class=\"s2\">&quot;--enable-shared&quot;<\/span><span class=\"w\"> <\/span>pyenv<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span><span class=\"m\">3<\/span>.10.13\n<\/code><\/pre><\/div>\n\n<p>Set it as your working version:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>pyenv<span class=\"w\"> <\/span>shell<span class=\"w\"> <\/span><span class=\"m\">3<\/span>.10.13\n<\/code><\/pre><\/div>\n\n<p>Verify the <code>.so<\/code> exists:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>find<span class=\"w\"> <\/span>~\/.pyenv\/versions\/3.10.13<span class=\"w\"> <\/span>-name<span class=\"w\"> <\/span><span class=\"s2\">&quot;libpython3.10*.so&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>Expected output:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>~\/.pyenv\/versions\/3.10.13\/lib\/libpython3.10.so\n<\/code><\/pre><\/div>\n\n<hr>\n<h2>3. Install <code>reapy-next<\/code><\/h2>\n<p><code>reapy-next<\/code> is the Python package that provides a whole slew of functionality\nfor interacting with REAPER through a Python lens. And hey, let's be honest... I'm basically blind to anything <em>other<\/em>\nthan Python.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>pip<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>reapy-next\n<\/code><\/pre><\/div>\n\n<hr>\n<h2>4. Configure REAPER to use <code>pyenv<\/code> Python 3.10<\/h2>\n<p>REAPER must be pointed at:<\/p>\n<ul>\n<li>The Python <strong>interpreter<\/strong><\/li>\n<li>The Python <strong>shared library (.so)<\/strong><\/li>\n<\/ul>\n<p>Open:<\/p>\n<blockquote>\n<p><strong>Options \u2192 Preferences \u2192 Plug-ins \u2192 ReaScript<\/strong><\/p>\n<\/blockquote>\n<p>Set:<\/p>\n<p><strong>Python interpreter:<\/strong><\/p>\n<div class=\"highlight\"><pre><span><\/span><code>\/home\/joestan\/.pyenv\/versions\/3.10.13\/lib\n<\/code><\/pre><\/div>\n\n<p><strong>Python library (.so):<\/strong><\/p>\n<div class=\"highlight\"><pre><span><\/span><code>libpython3.10.so\n<\/code><\/pre><\/div>\n\n<p>Restart REAPER.<\/p>\n<hr>\n<h2>5. Ensure REAPER actually uses Python 3.10<\/h2>\n<p>With REAPER open, run the following terminal command:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>~\/.pyenv\/versions\/3.10.13\/bin\/python3<span class=\"w\"> <\/span>-c<span class=\"w\"> <\/span><span class=\"s2\">&quot;import reapy; reapy.configure_reaper()&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>That command should immediately return without any error. If it hangs, and a message appears in REAPER regarding some\nPython 3.12 path that cannot be found, it's time to get to <code>grep<\/code>in'.<\/p>\n<h3>5.1 Fix REAPER config files<\/h3>\n<p>If you saw Python 3.12 above, it's time to fix your <code>reaper.ini<\/code>. I had to do some searching to actually find that.<\/p>\n<p>Run the following command to find any locations that are still referencing Python 3.12<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>grep<span class=\"w\"> <\/span>-Rni<span class=\"w\"> <\/span><span class=\"s2\">&quot;python3.12&quot;<\/span><span class=\"w\"> <\/span>~\/.config\/REAPER\n<\/code><\/pre><\/div>\n\n<p>Then go pop those files open and change those full Python paths to the <code>pyenv<\/code> equivalents shown above.<\/p>\n<p>Restart REAPER again.<\/p>\n<hr>\n<h2>6. Install the <code>ReaCmd<\/code> Package<\/h2>\n<p>That's right. You guessed it.<\/p>\n<blockquote>\n<p>I wrote another Python package.<\/p>\n<\/blockquote>\n<p>But this one's small! You can snag it from <a href=\"https:\/\/github.com\/engineerjoe440\/ReaCmd\">GitHub<\/a>. To make it easy, just\ninstall that package in your <code>pyenv<\/code> environment:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>~\/.pyenv\/versions\/3.10.13\/bin\/python3<span class=\"w\"> <\/span>-m<span class=\"w\"> <\/span>pip<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>git+https:\/\/github.com\/engineerjoe440\/ReaCmd.git\n<\/code><\/pre><\/div>\n\n<p>Now you should have a script accessible to actually operate the record-toggle operation.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>~\/.pyenv\/versions\/3.10.13\/bin\/rea-record\n<\/code><\/pre><\/div>\n\n<p><em>Voila<\/em><\/p>\n<hr>\n<h1>Done.<\/h1>\n<p>You now have:<\/p>\n<ul>\n<li>A working pyenv Python 3.10 installation<\/li>\n<li>REAPER correctly embedding Python 3.10<\/li>\n<li>A working <code>reapy-next<\/code> client script that toggles recording<\/li>\n<\/ul>\n<p>I hope you find this helpful. I'm sure I'll be coming back to that to fix my system again in the future!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"daw"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"reaper"}},{"@attributes":{"term":"scripting"}},{"@attributes":{"term":"stream-deck"}},{"@attributes":{"term":"ubuntu"}},{"@attributes":{"term":"linux"}}]},{"title":"More Python Nonsense","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/more-python-nonsense.html","rel":"alternate"}},"published":"2026-04-14T12:07:00-07:00","updated":"2026-04-14T12:07:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2026-04-14:\/more-python-nonsense.html","summary":"<p>You asked... so here it is! A whole bunch of random Python goodies. Things to help you get started, to learn about the language, to go deeper... and more!<\/p>","content":"<p>Python is my favorite programming language. There's no doubt about that! I've built a lot of random things with it;\neverything from helper scripts, to web-scrapers, to websites, and beyond.<\/p>\n<p>I've had some questions, recently about where I get my Python resources, so I thought I'd share some of that goodness...<\/p>\n<h3>Podcasts<\/h3>\n<blockquote>\n<p>Everybody's got a podcast.<\/p>\n<\/blockquote>\n<p>That's true, but sometimes, that's because there are so many interesting things to discuss. That's especially true in the\nPython\/coding world. There are other Python podcasts, like the <a href=\"https:\/\/realpython.com\/podcasts\/rpp\/\"><em>Real Python Podcast<\/em><\/a>,\nbut the ones below are some of my favorites. I've listed them before in <a href=\"\/tech-podcasts-galore.html\">another blog post<\/a>,\nbut it's always nice to remind myself about them.<\/p>\n<h4><em>\"Python Bytes\"<\/em><\/h4>\n<p><img src=\"https:\/\/pythonbytes.fm\/static\/img\/logo.png?cache_id=391cb49247369a67c4be78b27f2b3cd5\"\n    width=\"150\" alt=\"Python Bytes\" align=\"left\"><\/p>\n<p>By far, my favorite of the programming-pods, <a href=\"https:\/\/pythonbytes.fm\/\"><em>Python Bytes<\/em><\/a> covers the latest news\nin the Python programming language and the areas of the tech world that Python supports. Everything from web\nservers to data-science, from embedded Python to the data-center!<\/p>\n<p><em>Python Bytes<\/em> is a little on the shorter side; at least when compared with the other podcasts I've listed so\nfar. Michael Kennedy and Brian Okken cover all the latest-and-greatest Python modules and techniques with at\nleast one guest host every week.<\/p>\n<h4><em>\"Talk Python to Me\"<\/em><\/h4>\n<p>Another podcast from Michael Kennedy (one of the hosts of the aforementioned <em>Python Bytes<\/em>) is\n<a href=\"https:\/\/talkpython.fm\"><em>Talk Python to Me<\/em><\/a>. It's a little longer than its sister podcast, and goes into\ngreater detail in the technology or topic of interest. Michael brings on a variety of fantastic guests, all\nof which help discuss the latest news.<\/p>\n<h4><em>\"Test and Code\"<\/em><\/h4>\n<p>Brian Okken's other podcast, much like <em>Talk Python to Me<\/em> is a companion podcast to <em>Python Bytes<\/em>. Brian,\nthe author of <em>\"Python Testing with pytest: Simple, Rapid, Effective, and Scalable\"<\/em> covers the intricacies\nof testing code effectively. <a href=\"https:\/\/testandcode.com\/\"><em>Test and Code<\/em><\/a> is a great podcast, and is a little\non the shorter side, so it makes for quick listening!<\/p>\n<hr>\n<h3>Websites<\/h3>\n<p>There are a <em>lot<\/em> of great resources when it comes to Python on the web, but here's just a few...<\/p>\n<ul>\n<li><a href=\"https:\/\/www.geeksforgeeks.org\/python\/python-programming-language-tutorial\/\"><em>Geeks for Geeks<\/em><\/a><\/li>\n<li><a href=\"https:\/\/pythongeeks.org\/\"><em>Python Geeks<\/em><\/a><\/li>\n<li><a href=\"https:\/\/www.w3schools.com\/python\/python_intro.asp\"><em>W3 Schools<\/em><\/a><\/li>\n<li><a href=\"https:\/\/codecombat.com\/\"><em>Code Combat<\/em><\/a><\/li>\n<li><a href=\"https:\/\/www.boot.dev\/courses\/learn-code-python\"><em>boot.dev<\/em><\/a><\/li>\n<\/ul>\n<p>There's also plenty of cool projects. I've got to plug my own, but there's some other greats in here, too!<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/ElectricPy\"><em>ElectricPy<\/em><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/phasors\"><em>Phasors<\/em><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/selprotopy\"><em>SELProtoPy<\/em><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/pycev\"><em>pyCEV<\/em><\/a><\/li>\n<li><a href=\"https:\/\/fastapi.tiangolo.com\/\"><em>FastAPI<\/em><\/a><\/li>\n<li><a href=\"https:\/\/numpy.org\/\"><em>NumPy<\/em><\/a><\/li>\n<li><a href=\"https:\/\/pydantic.dev\/docs\/validation\/latest\/get-started\/\"><em>Pydantic<\/em><\/a><\/li>\n<li><a href=\"https:\/\/www.djangoproject.com\/\"><em>django<\/em><\/a><\/li>\n<\/ul>\n<hr>\n<h3>Email Lists<\/h3>\n<p>Don't like searching for your information? Want it brought right to you in some form of e-news\/letter format. Have\nyou considered email?<\/p>\n<h4><em>\"Python Weekly\"<\/em><\/h4>\n<p>This is a great newsletter, one of my favorites. They often have good \"how-to\" articles, links to online discussions,\nand they often include some great projects just to show them off! <a href=\"https:\/\/www.pythonweekly.com\/\"><em>Python Weekly<\/em><\/a> is\none of the few email subscriptions I actually read.<\/p>\n<h4><em>\"PyCoders Weekly\"<\/em><\/h4>\n<p>Speaking of newsletters I actually read, <a href=\"https:\/\/pycoders.com\/\"><em>PyCoders Weekly<\/em><\/a> is another one of my favorites, and\nI look forward to its weekly delivery. I know I'll see neat projects and see articles that will give me pause to think, or share with a friend or colleague. This is another one of the great news resources for Python.<\/p>\n<h4>The Official <em>\"PSF Newsletter\"<\/em><\/h4>\n<p>Of course, I'd be remiss if I didn't at least mention the official <a href=\"https:\/\/www.python.org\/psf\/newsletter\/\"><em>PSF Newsletter<\/em><\/a>.\nPut together by the Python Software Foundation, or PSF, this newsletter comes with interesting information about what's\nhappening in the Python ecosystem. New PEPs, new projects, news, news, news!<\/p>\n<hr>\n<h3>Github and Other Resources<\/h3>\n<p>What would this list be without an \"awesome list\"? They've become popular on Github, so I better list the one for Pyton:\n<a href=\"https:\/\/github.com\/vinta\/awesome-python\">https:\/\/github.com\/vinta\/awesome-python<\/a> There's plenty of good stuff in there, so go take a look!<\/p>\n<hr>\n<p>I could go on for days, here, but I've got other things to go do. So that'll do for now.<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"education"}},{"@attributes":{"term":"electrical-engineering"}},{"@attributes":{"term":"learning"}},{"@attributes":{"term":"online"}},{"@attributes":{"term":"programming"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"resources"}},{"@attributes":{"term":"teaching"}}]},{"title":"Smello World? For HTTP Requests","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/smello-world-with-http.html","rel":"alternate"}},"published":"2026-03-31T13:47:00-07:00","updated":"2026-03-31T13:47:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2026-03-31:\/smello-world-with-http.html","summary":"<p>A short article, but hopefully getting a jump on getting back in the habbit of documenting what I'm up to. Check out this cool tool!<\/p>","content":"<p>OK... It's been a while.<\/p>\n<p>Hi, my name is Joe.<\/p>\n<p>I'm here today to tell you about a neat little tool that I came across in one of the email subscriptions I have\nfor Python. Yes... I'm one of <em>those<\/em> guys. I like to see all the cool new stuff happening in the Python sphere.<\/p>\n<p>All I really have to say is check this out...<\/p>\n<p><img alt=\"Hello, Smello!\" src=\".\/images\/smello.png\" width=\"100%\"><\/p>\n<p>Let me introduce you to <a href=\"https:\/\/github.com\/smelloscope\/smello\">Smello<\/a>, an MIT-licensed tool to help you capture\nHTTP requests from your Python app and log them for analysis. Go read up on their\n<a href=\"https:\/\/roman.pt\/posts\/smello\/\">blog post<\/a> for some more information, but let me say... this thing is neat! I'll'\ndefinitely need to try it out sometime soon.<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"http"}},{"@attributes":{"term":"web"}},{"@attributes":{"term":"requests"}},{"@attributes":{"term":"httpx"}},{"@attributes":{"term":"rest"}},{"@attributes":{"term":"api"}}]},{"title":"Digital Media with Idaho 4-H Youth","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/digital-media-with-idaho-4h-youth.html","rel":"alternate"}},"published":"2026-02-17T10:09:00-08:00","updated":"2026-02-17T10:09:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2026-02-17:\/digital-media-with-idaho-4h-youth.html","summary":"<p>Idaho 4-H youth are astounding, aren't they? The things they'll do, the accomplishments they'll achieve. The distances they'll go. It's just awesome. These future leaders can do some amazing things. This last weekend at KYG is a perfect example of it. Read on to see more about how the Video Editor did an incredible job producing some great content.<\/p>","content":"<p><img alt=\"All Conference Photo\" src=\".\/images\/kyg_all_conference_2026.jpg\" width=\"100%\"><\/p>\n<p>The <a href=\"https:\/\/www.uidaho.edu\/extension\/4h\/events\/kyg\">Know Your Government<\/a> conference is a tremendous event, one of the\npremiere 4-H teen programs held across the state. It's open to 8th, 9th, and 10th grade students across the state. Located\nin the state capitol city, Boise, the conference helps youth experience Legislative and Judicial branches of state government.<\/p>\n<p>Each year, a group of students who have already participated in the conference for two years may apply for a third year at the\nconference, where they will actually participate in organizing and creating the conference for the next group of 4-H'ers. These\nyouth are members of the steering committee, and they are all strong leaders. In addition to the few dozen third-year members,\nthere are four 4th-year members. These remaining four members are:<\/p>\n<ul>\n<li>Speaker of the House<\/li>\n<li>Chief Justice<\/li>\n<li>News Editor<\/li>\n<li>Video Editor<\/li>\n<\/ul>\n<p>This year, the video editor -a terrific young leader- produced some wonderful content with several of the other reporter-team\nmembers. Among these was an interview with the Keynote (special guest) speaker, a full recording of the special presentation by\nthe special guest presenter, and ultimately a marvelous end-of-conference slideshow showcasing the efforts of all the youth\npresent at the conference.<\/p>\n<p>It's wonderful getting to watch these youth take charge of their activities, and really find their spark in their activities\nat the conference. These youth are <em>hard workers<\/em> and they demonstrate responsibility, creativity, and vision. I hope you\ncan enjoy the little sampling of these great pieces of work that I've collected here. Enjoy!<\/p>\n<h3>Interview with the Nels Anderson; the 2026 KYG Special Guest Presenter<\/h3>\n<iframe width=\"100%\" height=\"500\" src=\"https:\/\/www.youtube.com\/embed\/CNa7d3AI4gk?si=JUl3ZEcii_8jTafd\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe>\n\n<h3>Recap of the 2026 Idaho 4-H Know Your Government (KYG) Conference<\/h3>\n<iframe width=\"100%\" height=\"500\" src=\"https:\/\/www.youtube.com\/embed\/UHiCnTu6z1o?si=ERLaHSl9qpKVzIuV\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe>\n\n<h3>Stream of the <em>Entire<\/em> Special Performance Event<\/h3>\n<iframe width=\"100%\" height=\"500\" src=\"https:\/\/www.youtube.com\/embed\/pSDshEusczQ?si=VquKLXk531P691FT\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"4h"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"digital-media"}},{"@attributes":{"term":"idaho"}},{"@attributes":{"term":"media"}},{"@attributes":{"term":"recording"}},{"@attributes":{"term":"video"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"youtube"}}]},{"title":"Complex, Calculated Coffee!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/complex-calculated-coffee.html","rel":"alternate"}},"published":"2025-05-09T07:36:00-07:00","updated":"2025-05-09T07:36:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2025-05-09:\/complex-calculated-coffee.html","summary":"<p>Admit it, you've always wanted an overly-complex coffee-making machine, haven't you? Something that's innately simple to use, but a complex masterpiece to marvel at; especially on those early Saturday mornings when the caffeine hasn't kicked in yet. Well, I've done it. I've built just that. I call it... the 5282. Let me explain.<\/p>","content":"<p>This little endeavor started as a joke. No kidding. I'm not going to tell the whole story here, but if you ask nicely enough, I might tell you in person.\nIf you know, you know...<\/p>\n<p>I wanted to make a single-cup coffee maker that runs off of an <a href=\"https:\/\/selinc.com\/products\/2411\">SEL-2411<\/a>. The SEL-2411 can use digital or analog inputs to\ninterpret the various sensors, and it has a variety of output mechanisms which may be used to do all sorts of fun things with controlling the coffee-maker.<\/p>\n<p>In one of my last days of teaching ECE101 at the University of Idaho, I brought in my 2411 to class so that I could collaborate with the students to design\nthe digital logic circuit required to operate the controller. We started with a simple control for the LED and pump motor, but after some further setup, I\nrealized it was going to be a bit more complex. Not unwieldy, but certainly more than we'd accomplished in class. What follows is the complete logic.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/SEL-5282.drawio.svg\" style=\"width: 100%;\" alt=\"digital logic for the complete controller\"><\/p>\n<p>Of course... this is very way over-complicated. But hey... We do this stuff because it's fun and interesting. And we learn something new with each new project.<\/p>\n<p>This particular system was fun for me to learn about the particular design that was used in the coffee machine I disassembled to accomplish my goal. There was\na lot more sensing involved than I originally expected. In total, the I\/O list was as follows:<\/p>\n<ul>\n<li><em>2x<\/em> Thermistors (likely around 50k\u03a9 nominal according to my research -- not tested, yet)<\/li>\n<li><em>1x<\/em> capacitive water sensor<\/li>\n<li><em>1x<\/em> rotary encoder<\/li>\n<li><em>1x<\/em> 12V solenoid valve<\/li>\n<li><em>1x<\/em> 12V pump motor<\/li>\n<li><em>1x<\/em> 120V heater element<\/li>\n<\/ul>\n<p>That's all quite a bit of sensing and closed-loop control design to put together. So I just decided I wasn't going to do any of that. I opted to simply use an\nopen-loop control mode based purely on timing. I suspect that the original system really just used the sensing to increase or decrease the motor speed in\norder to manipulate the residence time. The residence time is, of course, the time which the water would spend in the heating apparatus (a metal tube with the\nheating element running through it). The longer the water remains in the heater; the hotter it gets! Simple, isn't it?<\/p>\n<p>Well, my design took a <em>different<\/em> approach. That's right. I took the approach of just turning the pump on \"full blast.\" Who needs temperature adjustments, anyway?\nThat did result in my inability to achieve <em>quite<\/em> the same temperatures that the original coffee machine controls achieved. But I got close enough. In fact, if\nyou look closely at the output controls (enlarged below to highlight some of the important details), you'll notice that there's actually a timing system for\npulsing one of the outputs in a 4-seconds-on, 1-second-off pattern; that's an 80% duty cycle for a 5-second periodic signal. I used this to control the solenoid\nvalve which is used to switch between pumping water into the single-use-pod to a recirculating system. Using that recirculation on a cycle allows me to heat up\nthe water just a little more. Just what we needed!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/SEL-5282_timing.drawio.svg\" style=\"width: 100%;\" alt=\"output control timing\"><\/p>\n<p>Now, that's all well-and-good, but a big part of this is also working out the LED and user-interfacing bit. This coffee maker won't be very good if I can't use\nit to actually inform a user whether it's set for small, medium, or large (8, 10, or 12oz). This whole user-interfacing bit was where I actually had some\nstudents work with me in class (I quite enjoyed this bit, actually). In class, we established what the goal for the LEDs and push-buttons were going to be, then,\nusing the digital logic principles we'd worked on through the semester, we designed the circuit.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/SEL-5282_ui.drawio.svg\" style=\"width: 100%;\" alt=\"user interface control\"><\/p>\n<p>Worth noting is that this logic design is slightly modified from what we designed in the class, but that's really because I changed my mind when the LED should\nblink versus being solid. I wanted blinking to indicate to the user they may change the setting (for small\/medium\/large); and I wanted the solid LED to indicate\nwhich selection had been made.<\/p>\n<hr>\n<p>Well, that's probably enough for now. If I'm feeling ambitious later, I might just post the settings here, too. But for now, have a great summer!<\/p>\n<iframe src='https:\/\/immich.stanleysolutionsnw.com\/share\/AQfx1LH7oW4xQEWuB2iHjRdmJsV2s4TZjJoTXKlKnk6Y5qIl0ovMlgZZ4W6HFF21sIc'\nwidth='100%' height='600px' frameborder='0'>\n<\/iframe>","category":[{"@attributes":{"term":"education"}},{"@attributes":{"term":"coffee"}},{"@attributes":{"term":"coffee-maker"}},{"@attributes":{"term":"control"}},{"@attributes":{"term":"controller"}},{"@attributes":{"term":"digital-logic"}},{"@attributes":{"term":"logic"}},{"@attributes":{"term":"plc"}},{"@attributes":{"term":"programming"}},{"@attributes":{"term":"relay"}},{"@attributes":{"term":"iot"}},{"@attributes":{"term":"selogic"}}]},{"title":"Jupyter Lab Setup for Electrical Engineers","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/jupyter-lab-setup-for-electrical-engineers.html","rel":"alternate"}},"published":"2025-03-25T06:49:00-07:00","updated":"2025-03-25T06:49:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2025-03-25:\/jupyter-lab-setup-for-electrical-engineers.html","summary":"<p>Over the years, I've tried a few installation methods for Jupyter Notebooks and Jupyter lab. I've finally found one that I think is a little simpler than some of the others. Let me share that now for the benefit of Electrical Engineering students.<\/p>","content":"<p><a href=\"https:\/\/jupyter.org\/\">Jupyter<\/a> is one of the fastest-growing exploratory tools in Python, one of the fastest-growing\nlanguages in the early 2020's. I've used Python throughout my educational and professional career for simple calculations\nall the way up to complex operational control systems. While I won't claim Python is the best-suited tool for every\napplication. It's my first go-to. If it won't work for something, I will often find that quicker in Python, and I'll be\ndirected to the \"right tool\" for that particular job. Just like in spoken and written languages, there's often a language\nparticularly well-suited for each application.<\/p>\n<p>Jupyter is a framework which allows scientists, engineers, researchers, and others to quickly dive into the Python space\nwith great power and efficiency. It provides a clean web-interface for individuals and teams to work together on projects,\nand is now recommended by a number of resources for engineers.<\/p>\n<ul>\n<li><a href=\"https:\/\/artofdataengineering.com\/why-every-data-engineer-needs-jupyter\/\"><em>Why Every Data Engineer Needs Jupyter<\/em> -Tim Webster (<strong>Art of Data Engineering<\/strong>)<\/a><\/li>\n<li><a href=\"https:\/\/www.packtpub.com\/en-us\/learning\/how-to-tutorials\/10-reasons-data-scientists-love-jupyter-notebooks\/\"><em>10 Reasons Why Data Scientists Love Jupyter Notebooks<\/em> -Aarthi Kumaraswamy (<strong>Packt<\/strong>)<\/a><\/li>\n<li><a href=\"https:\/\/cache.org\/sites\/default\/files\/S19-Jupyter-Notebooks.pdf\"><em>Jupyter Notebooks for Chemical Engineering Education<\/em> -Jeffrey C. Kantor (<strong>University of Notre Dame<\/strong>)<\/a><\/li>\n<\/ul>\n<p>I've come to enjoy Jupyter for use with electrical engineering work for many of the same reasons that scientists and\nresearchers enjoy it. Code cells right next to documentation cells makes for a truly wonderful and simple pairing.<\/p>\n<p>I'm not really here to sell you on Jupyter as a tool, however. I'm really here to describe what I feel is becoming the\nbest way to install Jupyter, to date.<\/p>\n<hr>\n<p>With Python's evolving landscape of package managers, <a href=\"https:\/\/pipx.pypa.io\/latest\/\"><code>pipx<\/code><\/a> has come to be my \"go-to-tool\"\nfor command-line applications both big and small. I won't go into great length about what it is and isn't, but I'll say\nthat it makes it possible to install great command-line applications in Python without dependency conflicts or disrupting\nany system-level packages.<\/p>\n<blockquote>\n<p>Now, to cut to the chase...<\/p>\n<\/blockquote>\n<h4>0. Install Python<\/h4>\n<p>The rest of these instructions really rely on Python already being installed. If you haven't done that, yet, now's the\ntime to remedy the situation!<\/p>\n<h4>1. Install <code>pipx<\/code><\/h4>\n<p>Pipx is not a default package that's included with Python installations. So that's the first thing we'll need to install.\nI could give specific directions, but instead, I'll refer you directly to <a href=\"https:\/\/pipx.pypa.io\/latest\/installation\/\">their thorough documentation<\/a>.\nFollow their guide to getting pipx installed.<\/p>\n<p>I anticipate that many electrical engineers who might be reading this will be working with Windows. That does make things\never so slightly trickier. I'd recommend that if you're running Windows, just use Python's <code>pip<\/code> (the regular one) to\ndo the installation for you.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>python<span class=\"w\"> <\/span>-m<span class=\"w\"> <\/span>pip<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>--user<span class=\"w\"> <\/span>pipx\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p>\u2139\ufe0f <strong>NOTE:<\/strong><\/p>\n<p>This assumes something important...<\/p>\n<p>It assumes that Python is already part of your Windows path. If it's not, or you run the command above and see an error\nabout \"python could not be found\" (or something similar), then you'll need to add Python to your path. For the sake of\nbrevity here, I'll point you towards <a href=\"https:\/\/realpython.com\/add-python-to-path\/\">another great article<\/a> showing\ninformation on that.<\/p>\n<\/blockquote>\n<h4>2. Installing Jupyter Lab with <code>pipx<\/code><\/h4>\n<p>This is, perhaps, the easiest part. Just run the following command and let pipx do all of the work!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>pipx<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>jupyterlab<span class=\"w\"> <\/span>--include-deps\n<\/code><\/pre><\/div>\n\n<h4>3. Install Jupyter Notebook with Jupyter Lab (so you have both)<\/h4>\n<p>I like to have both <code>jupyter notebook<\/code> and <code>jupyter lab<\/code> installed because for different use-cases, I use the different\napplications. The following command will install the notebook server right alongside the lab server. This makes it really\neasy to start either.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>pipx<span class=\"w\"> <\/span>inject<span class=\"w\"> <\/span>jupyterlab<span class=\"w\"> <\/span>notebook\n<\/code><\/pre><\/div>\n\n<h4>4. Add other Numerical Libraries<\/h4>\n<p>While we're at it, we might as well install some of the common Python numerical libraries. Things like <code>numpy<\/code>, or <code>scipy<\/code>\nare, perhaps, most common. At the risk of becoming a shameless, self-promoter. Why don't we install\n<a href=\"https:\/\/electricpy.readthedocs.io\/en\/latest\/\"><code>electricpy<\/code><\/a>; the Python package I began maintaining in 2018 which\ncontains all manner of electrical-engineering formulas. Luckily for us, it <em>depends<\/em> on those other numerical libraries,\nso when we install <code>electricpy<\/code>, they'll come along for the ride!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>pipx<span class=\"w\"> <\/span>inject<span class=\"w\"> <\/span>jupyterlab<span class=\"w\"> <\/span>electricpy\n<\/code><\/pre><\/div>\n\n<h4>5. Profit!<\/h4>\n<p>As the \"cool kids\" like to say, it's time to profit off the efforts, now. Go ahead! Open a terminal and issue the\nfollowing command, you should see Jupyter Notebook spring to life in your browser and get right into it!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>jupyter<span class=\"w\"> <\/span>notebook\n<\/code><\/pre><\/div>\n\n<p>Notice that when you ran that command, the session in your browser started up in the same folder... This means that\nwherever your terminal is running when you start <code>jupyter notebook<\/code> or <code>jupyter lab<\/code>, that's where the browser session\nwill also start. By extension, you can see how you'd be able to open or work with any file just by navigating to that\ndirectory in your terminal. For guides on that, take a look at the following articles.<\/p>\n<ul>\n<li><a href=\"https:\/\/www.digitalcitizen.life\/command-prompt-how-use-basic-commands\/\"><em>CMD: 11 basic commands you should know (cd, dir, mkdir, etc.)<\/em> -Codrut Neagu (<strong>Digital Citizen<\/strong>)<\/a><\/li>\n<li><a href=\"https:\/\/phoenixnap.com\/kb\/bash-commands\"><em>30 Bash Commands Cheat Sheet<\/em> -Bosko Marijan (<strong>Phoenix NAP<\/strong>)<\/a><\/li>\n<\/ul>\n<hr>\n<p>Now get out there, and engineer something awesome!<\/p>","category":[{"@attributes":{"term":"education"}},{"@attributes":{"term":"calculations"}},{"@attributes":{"term":"jupyter"}},{"@attributes":{"term":"lab"}},{"@attributes":{"term":"notebook"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"research"}},{"@attributes":{"term":"study"}},{"@attributes":{"term":"university"}}]},{"title":"A Collaborative Word Cloud for Teaching","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/a-collaborative-wordcloud-for-teaching.html","rel":"alternate"}},"published":"2024-11-26T10:35:00-08:00","updated":"2024-11-26T10:35:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-11-26:\/a-collaborative-wordcloud-for-teaching.html","summary":"<p>A few months ago I wrapped up some work on a fun little project I developed for some teaching exercises. Let me tell you about it!<\/p>","content":"<p>It started back in June when I was frantically preparing some exercises to teach and prepare a group of the College Staff to support Idaho's\n<a href=\"https:\/\/www.uidaho.edu\/extension\/4h\/events\/stac\">State Teen Association Convention<\/a>. I was trying to prepare some training to align the team.\nThat's not something I've done before, and I can't make any claims that it's perfect, but I think it was well received.<\/p>\n<p>Let me explain what that exercise really was...<\/p>\n<p>I used a handful of different materials to try making it both fun and engaging. Some of those materials were from National 4-H's\n<a href=\"https:\/\/www.4-h.org\/wp-content\/uploads\/2024\/04\/09105552\/4H-Harmony-Guide-2024.pdf\">\"Meet Up Buddy\"<\/a> card activity. Some were of my own\ncreation to encourage the College Staff to think about how to prioritize things when time constraints pressed them. My alignment training\nwas where we started, it was basically a challenge to the participants. Here's how I set it up, though, maybe not in so many well-placed words.<\/p>\n<blockquote>\n<p>4-H is full of projects. So many things for youth to engage with and to find their spark. While this is wonderful, it does pose a bit of a\nchallenge. Before I explain, I want you to think about all the words that come to mind when you think of 4-H.<\/p>\n<\/blockquote>\n<p>At this point, I'd share a QR code with them that they could use to navigate to the game page. The game page provides participants with a\ntext box where they can enter each word that they think about. When they enter a word, a new text box appears so they can enter another word.\nAs they'd enter words, my system keeps track of them (hidden, of course) and begins to create a word cloud with all of their entries.<\/p>\n<p>When I decide that they've had enough time (normally it looks like they've run out of ideas for words), then I shut down the entry system and\nreveal the word cloud generated from all of their entries. At the end of it all, we're left with something like this:<\/p>\n<p><img src=\"https:\/\/immich.stanleysolutionsnw.com\/api\/assets\/5692b76b-9049-4ebc-b0b8-4812c751716a\/thumbnail?size=preview&key=klMHZEmZIUXG5UF9NeiFfKmLCZOcCm99k9mdI6Rw_U9TbJhC6zepXq-tEantGNrj748\" style=\"width: 100%;\" alt=\"college staff word cloud\"><\/p>\n<p>That gives us something to talk about.<\/p>\n<blockquote>\n<p>Look at this. These are all the things that our little group thinks about when we think about 4-H. But what do we share?<\/p>\n<p>Often, we're working in our own little silos, not terribly focussed on the same things that other leaders are focussed on. That can make\nworking with other leaders difficult. We're each moving in our own direction, and sometimes, that feels like pulling a ball of yarn from\ndifferent ends; just making the knot perpetually tighter.<\/p>\n<p>But this shows us something. Even when we are moving in different directions, it's possible to find some common ground. These are the first\nthings that <em>you<\/em> thought of when I asked you to think about 4-H. These are the inately tied to your path in 4-H, and see how we have\nsomething common shared between us?<\/p>\n<\/blockquote>\n<p>At that point, I'd talk about why I think it's good to get a common understanding of what is shared between the group, and how I like to think\nabout using this commonality with people to find shared ground on certain topics. To move forward together on what we see as the shared pathway.<\/p>\n<p>It worked pretty well for our little team, I was able to refer back to it a number of times, and it made working with the team all the more\npleasant and constructive.<\/p>\n<hr>\n<p>The exercise wasn't purely without its challenges, however. In fact, we discovered that the web-service that I'd chosen to use only allowed\nsomething like 10 players, and each player could only enter 15 words. Not exactly the sort of restrictions you want to run into during an\nexercise. Add to that, the fact that you've got to have a solid internet connection, which is not always easy in 4-H programming in the state\nof Idaho.<\/p>\n<p>That's why I returned to my recent build, the <a href=\"\/making-portable-digital-learning.html\">PortaServer<\/a>. My little (not so little, actually) ammo\ncan with a stack of mini computers. I could serve my own system there, and since I run the network, I could also make the address very easy\nto access.<\/p>\n<blockquote>\n<p>Enter: <a href=\"https:\/\/github.com\/engineerjoe440\/wordwall\">WordWall<\/a><\/p>\n<\/blockquote>\n<p>It's pretty simple in concept. A little more effort in practice, but that's alright. I made a Python FastAPI server with a tiny, ephemeral\nSQLite database backing it to run all the management of adding, removing, and sharing words. Tie that with a Material-UI based React frontend,\nand I've got a relatively simple little service that I can run on my own system.<\/p>\n<p>Now, I've got something I can take with me, wherever I travel to align 4-H leaders, youth, or others.<\/p>","category":[{"@attributes":{"term":"development"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"react"}},{"@attributes":{"term":"teaching"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"software"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"fastapi"}},{"@attributes":{"term":"material-ui"}}]},{"title":"Basic Auth without the Hubub in NGINX","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/basic-auth-without-the-hubub-in-nginx.html","rel":"alternate"}},"published":"2024-11-08T20:49:00-08:00","updated":"2024-11-08T20:49:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-11-08:\/basic-auth-without-the-hubub-in-nginx.html","summary":"<p>Today I learned that it IS possible to make a relatively simple static site that has some automatic authentication built in to NGINX that will use a custom HTML file for the login page as opposed to the basic-auth prompt that gets so annoying.<\/p>","content":"<p>Okay... As I write this, I'm falling asleep. Hah! So I'll make this quick.<\/p>\n<p>I wanted a website that:<\/p>\n<ul>\n<li>was a static site (no ORM, database, etc.)<\/li>\n<li>had some basic authentication (yeah, BasicAuth is fine)<\/li>\n<li>had a custom login page with my own styled HTML\/CSS<\/li>\n<\/ul>\n<p>Now, I've known that NGINX allows users to configure basic auth for some time.\nI've done that, before. But I didn't know how to get it to do the custom HTML in\nconjunction with that. Here's what I did.<\/p>\n<p>I set up a route that would allow any login, or static file to be served without\nauthentication.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"w\">    <\/span><span class=\"k\">location<\/span><span class=\"w\"> <\/span><span class=\"p\">~<\/span><span class=\"sr\">*<\/span><span class=\"w\"> <\/span><span class=\"s\">^\/(login|css|fonts|img|js|404.html)<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">root<\/span><span class=\"w\"> <\/span><span class=\"s\">\/home\/...\/public\/<\/span><span class=\"p\">;<\/span><span class=\"w\">         <\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n<\/code><\/pre><\/div>\n\n<p>And I had the main part of the website set up to use basic auth. Even had the\n<code>401<\/code> redirect in there to manage spitting users out in the right spot. But that\nused the annoying \"basic auth popup\" that I was trying to avoid.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"w\">    <\/span><span class=\"k\">location<\/span><span class=\"w\"> <\/span><span class=\"s\">\/<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">root<\/span><span class=\"w\"> <\/span><span class=\"s\">\/home\/...\/public\/<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">auth_basic<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;Restricted&quot;<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">auth_basic_user_file<\/span><span class=\"w\"> <\/span><span class=\"s\">\/home\/...\/.htpasswd<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">proxy_intercept_errors<\/span><span class=\"w\"> <\/span><span class=\"no\">on<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">error_page<\/span><span class=\"w\"> <\/span><span class=\"mi\">401<\/span><span class=\"w\">  <\/span><span class=\"s\">\/login\/<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p>What to do, what to do...<\/p>\n<\/blockquote>\n<p>After I don't even know how much searching, it dawned on me that I could use an\n<code>if<\/code> statement to do a redirect, and after a little more tweaking, and learning\nhow to \"hack\" together a logical \"AND\" between three <code>if<\/code> statements, I was able\nto come up with this:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"w\">    <\/span><span class=\"k\">if<\/span><span class=\"w\"> <\/span><span class=\"s\">(<\/span><span class=\"nv\">$http_authorization<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;&quot;)<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">set<\/span><span class=\"w\"> <\/span><span class=\"nv\">$temp_cache<\/span><span class=\"w\"> <\/span><span class=\"mi\">1<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n<span class=\"w\">    <\/span><span class=\"k\">if<\/span><span class=\"w\"> <\/span><span class=\"s\">(<\/span><span class=\"nv\">$request_uri<\/span><span class=\"w\"> <\/span><span class=\"s\">!~*<\/span><span class=\"w\"> <\/span><span class=\"s\">^\/(login|css|fonts|img|js|404.html))<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">set<\/span><span class=\"w\"> <\/span><span class=\"nv\">$temp_cache<\/span><span class=\"w\"> <\/span><span class=\"mi\">1<\/span><span class=\"nv\">$temp_cache<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n<span class=\"w\">    <\/span><span class=\"k\">if<\/span><span class=\"w\"> <\/span><span class=\"s\">(<\/span><span class=\"nv\">$temp_cache<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"mi\">11<\/span><span class=\"s\">)<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">return<\/span><span class=\"w\"> <\/span><span class=\"mi\">302<\/span><span class=\"w\"> <\/span><span class=\"s\">https:\/\/<\/span><span class=\"nv\">$host\/login\/<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n<\/code><\/pre><\/div>\n\n<p>If there's no authorization header and the request URI isn't the login or static\nfile, then set a redirect to the <code>\/login<\/code> page. TADA!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">server<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"kn\">listen<\/span><span class=\"w\"> <\/span><span class=\"mi\">443<\/span><span class=\"w\"> <\/span><span class=\"s\">ssl<\/span><span class=\"p\">;<\/span><span class=\"w\"> <\/span><span class=\"c1\"># managed by Certbot<\/span>\n<span class=\"w\">    <\/span><span class=\"kn\">server_name<\/span><span class=\"w\"> <\/span><span class=\"s\">k3b4h.idaho4h.org<\/span><span class=\"p\">;<\/span>\n\n<span class=\"w\">    <\/span><span class=\"kn\">client_max_body_size<\/span><span class=\"w\"> <\/span><span class=\"s\">512M<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">    <\/span><span class=\"kn\">client_body_timeout<\/span><span class=\"w\"> <\/span><span class=\"s\">300s<\/span><span class=\"p\">;<\/span>\n\n<span class=\"w\">    <\/span><span class=\"kn\">if<\/span><span class=\"w\"> <\/span><span class=\"s\">(<\/span><span class=\"nv\">$http_authorization<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;&quot;)<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">set<\/span><span class=\"w\"> <\/span><span class=\"nv\">$temp_cache<\/span><span class=\"w\"> <\/span><span class=\"mi\">1<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n<span class=\"w\">    <\/span><span class=\"kn\">if<\/span><span class=\"w\"> <\/span><span class=\"s\">(<\/span><span class=\"nv\">$request_uri<\/span><span class=\"w\"> <\/span><span class=\"s\">!~*<\/span><span class=\"w\"> <\/span><span class=\"s\">^\/(login|css|fonts|img|js|404.html))<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">set<\/span><span class=\"w\"> <\/span><span class=\"nv\">$temp_cache<\/span><span class=\"w\"> <\/span><span class=\"mi\">1<\/span><span class=\"nv\">$temp_cache<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n<span class=\"w\">    <\/span><span class=\"kn\">if<\/span><span class=\"w\"> <\/span><span class=\"s\">(<\/span><span class=\"nv\">$temp_cache<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"mi\">11<\/span><span class=\"s\">)<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">return<\/span><span class=\"w\"> <\/span><span class=\"mi\">302<\/span><span class=\"w\"> <\/span><span class=\"s\">https:\/\/<\/span><span class=\"nv\">$host\/login\/<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n\n<span class=\"w\">    <\/span><span class=\"kn\">location<\/span><span class=\"w\"> <\/span><span class=\"s\">\/<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">root<\/span><span class=\"w\"> <\/span><span class=\"s\">\/home\/...\/public\/<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">auth_basic<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;Restricted&quot;<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">auth_basic_user_file<\/span><span class=\"w\"> <\/span><span class=\"s\">\/home\/...\/.htpasswd<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">proxy_intercept_errors<\/span><span class=\"w\"> <\/span><span class=\"no\">on<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">error_page<\/span><span class=\"w\"> <\/span><span class=\"mi\">401<\/span><span class=\"w\">  <\/span><span class=\"s\">\/login\/<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n\n<span class=\"w\">    <\/span><span class=\"kn\">location<\/span><span class=\"w\"> <\/span><span class=\"p\">~<\/span><span class=\"sr\">*<\/span><span class=\"w\"> <\/span><span class=\"s\">^\/(login|css|fonts|img|js|404.html)<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">root<\/span><span class=\"w\"> <\/span><span class=\"s\">\/home\/...\/public\/<\/span><span class=\"p\">;<\/span><span class=\"w\">         <\/span>\n<span class=\"w\">    <\/span><span class=\"p\">}<\/span>\n\n\n<span class=\"w\">    <\/span><span class=\"kn\">ssl_certificate<\/span><span class=\"w\"> <\/span><span class=\"s\">\/etc\/...fullchain.pem<\/span><span class=\"p\">;<\/span><span class=\"w\"> <\/span><span class=\"c1\"># managed by Certbot<\/span>\n<span class=\"w\">    <\/span><span class=\"kn\">ssl_certificate_key<\/span><span class=\"w\"> <\/span><span class=\"s\">\/etc\/...privkey.pem<\/span><span class=\"p\">;<\/span><span class=\"w\"> <\/span><span class=\"c1\"># managed by Certbot<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre><\/div>\n\n<p>Now... I've yet to get some javascript cobbled together to actually set the\nauthorization header. That's next, but I <em>think<\/em> that should be manageable.<\/p>\n<blockquote>\n<p>God, I hope so.<\/p>\n<\/blockquote>\n<p>Good night!<\/p>\n<h2>Update:<\/h2>\n<p>Dangit. That won't work at all. Seems that while it does everything that I want\nin terms of the visual component, it won't let me actually set the authorization\nheader for the browser to retain it for subsequent navigation.<\/p>\n<p>Maybe there's a workaround for it, but I haven't found one yet. I'll have to\nkeep thinking\/looking.<\/p>","category":[{"@attributes":{"term":"development"}},{"@attributes":{"term":"auth"}},{"@attributes":{"term":"html"}},{"@attributes":{"term":"http"}},{"@attributes":{"term":"nginx"}},{"@attributes":{"term":"proxy"}}]},{"title":"Making Portable Digital Learning","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/making-portable-digital-learning.html","rel":"alternate"}},"published":"2024-09-03T16:41:00-07:00","updated":"2024-09-26T15:42:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-09-03:\/making-portable-digital-learning.html","summary":"<p>With all of these 4-H activities that I've been helping with, I've been in my car. A lot. That means that I don't always have access to great internet, great resources, and I can't always connect home to my servers, there. I've decided to combat that by building a single, portable network. A network in a box, if you will. Here's how I did it.<\/p>","content":"<p>That's right! I've been build a portable network and server system. It's all built into an ammo can. It's set up so that\nI can drag around all kinds of services with me, and I can easily set up my own WiFi network for 4-H users to access.<\/p>\n<p>Many of the locations I visit to support 4-H's hands-on-learning don't offer great networks, and in some cases, the\nlesson(s) warrant use of some technology or service that requires accounts and special access. That puts all sorts of\nundue challenges on me and others to put things together beforehand and waste some valuable time getting things working.<\/p>\n<p>A few years ago (yes, it's been that long), I had an idea to put a few Raspberry Pi's and a little network switch in an\nammo can so that I could pack the whole thing around and use it for different programming\/hacking exercises with 4-H\nyouth. That quickly evolved into what I now call the Port-A-Server. My portable network and server infrastructure in a\nbox!<\/p>\n<p>This thing comes packed. Here's the BOM (discounting fasteners and cabling, of course).<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: right;\"><strong>Quantity<\/strong><\/th>\n<th><strong>Device<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: right;\">2<\/td>\n<td>Raspberry Pi 3B+<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">2<\/td>\n<td>Raspberry Pi 4B+<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">1<\/td>\n<td>Raspberry Pi CM4 in a Dual NIC Enclosure<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">1<\/td>\n<td>ZimaBoard x86 Single Board Computer<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">1<\/td>\n<td>gl.iNet Travel Router<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\">3<\/td>\n<td>Small 1Gbps Network Switches<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The whole thing runs off a single power supply which means ONE CABLE, and it's even got a couple power ports on the side.\nJust a few months ago, I even ported through some ports to access the HDMI outputs from a couple of the crucial devices,\nand USB for a bunch of things. With that gl.iNet router, I'm able to use its USB port to connect to my cellphone to use\ncellular tethering. That means that in spots where WiFi isn't available, but cellular is, I'm set!<\/p>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgaGVpZ2h0PSI1NDNweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjk3M3B4O2hlaWdodDo1NDNweDtiYWNrZ3JvdW5kOiMwMDAwMDA7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA5NzMgNTQzIiB3aWR0aD0iOTczcHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjw\/cGxhbnR1bWwgMS4yMDI2LjNiZXRhNj8+PGRlZnMvPjxnPjx0ZXh0IGZpbGw9IiMzM0ZGMDIiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmb250LXN0eWxlPSJpdGFsaWMiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNDcyLjY0MDYiIHg9IjUiIHk9IjE3Ij5QbGFudFVNTCB2ZXJzaW9uIDEuMjAyNi4zYmV0YTYgLyA5N2YwZjQwIFsyMDI2LTA0LTAyIDE3OjQ2OjQwIFVUQ108L3RleHQ+PHJlY3QgZmlsbD0iIzMzRkYwMiIgaGVpZ2h0PSIyMS4yOTY5IiBzdHlsZT0ic3Ryb2tlOiMzM0ZGMDI7c3Ryb2tlLXdpZHRoOjE7IiB3aWR0aD0iMjU5Ljc1ODgiIHg9IjUiIHk9IjI2Ljk2ODgiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxNzEuMjgxMyIgeD0iNiIgeT0iNDEuOTY4OCI+W0Zyb20gc3RyaW5nIChsaW5lIDMpIF08L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNC44NzQiIHg9IjUiIHk9IjYyLjI2NTYiPiYjMTYwOzwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjAyIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI4MS40MjI5IiB4PSI1IiB5PSI3OC41NjI1Ij5Ac3RhcnR1bWw8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTM0LjE2MjEiIHg9IjUiIHk9Ijk0Ljg1OTQiPiF0aGVtZSBibHVlcHJpbnQ8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNC44NzQiIHg9IjUiIHk9IjExMS4xNTYzIj4mIzE2MDs8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTc5LjE4MzYiIHg9IjUiIHk9IjEyNy40NTMxIj4hJFRIRU1FID0gImJsdWVwcmludCI8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNC44NzQiIHg9IjUiIHk9IjE0My43NSI+JiMxNjA7PC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMDIiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE1Ljk1NTEiIHg9IjUiIHk9IjE2MC4wNDY5Ij4uLi48L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTg3LjAxMDciIHg9IjUiIHk9IjE3Ni4zNDM4Ij4uLi4gKCBza2lwcGluZyAxMzEgbGluZXMgKTwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjAyIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxNS45NTUxIiB4PSI1IiB5PSIxOTIuNjQwNiI+Li4uPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMDIiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjIyNS45OTYxIiB4PSIxNC43NDgiIHk9IjIwOC45Mzc1Ij5CYWNrZ3JvdW5kQ29sb3IgJEJHQ09MT1I8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTg0LjI3NjQiIHg9IjE0Ljc0OCIgeT0iMjI1LjIzNDQiPkJvcmRlckNvbG9yICRGR0NPTE9SPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMDIiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjkuOTY2OCIgeD0iNSIgeT0iMjQxLjUzMTMiPn08L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTk5LjcxODgiIHg9IjUiIHk9IjI1Ny44MjgxIj5za2lucGFyYW0gU3RlcmVvdHlwZUUgezwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjAyIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyMjUuOTk2MSIgeD0iMTQuNzQ4IiB5PSIyNzQuMTI1Ij5CYWNrZ3JvdW5kQ29sb3IgJEJHQ09MT1I8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTg0LjI3NjQiIHg9IjE0Ljc0OCIgeT0iMjkwLjQyMTkiPkJvcmRlckNvbG9yICRGR0NPTE9SPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMDIiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjkuOTY2OCIgeD0iNSIgeT0iMzA2LjcxODgiPn08L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTk1LjM2NDMiIHg9IjUiIHk9IjMyMy4wMTU2Ij5za2lucGFyYW0gU3RlcmVvdHlwZUkgezwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjAyIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyMjUuOTk2MSIgeD0iMTQuNzQ4IiB5PSIzMzkuMzEyNSI+QmFja2dyb3VuZENvbG9yICRCR0NPTE9SPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMDIiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE4NC4yNzY0IiB4PSIxNC43NDgiIHk9IjM1NS42MDk0Ij5Cb3JkZXJDb2xvciAkRkdDT0xPUjwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjAyIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI5Ljk2NjgiIHg9IjUiIHk9IjM3MS45MDYzIj59PC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMDIiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjIwMS44NzIxIiB4PSI1IiB5PSIzODguMjAzMSI+c2tpbnBhcmFtIFN0ZXJlb3R5cGVOIHs8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjI1Ljk5NjEiIHg9IjE0Ljc0OCIgeT0iNDA0LjUiPkJhY2tncm91bmRDb2xvciAkQkdDT0xPUjwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjAyIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxODQuMjc2NCIgeD0iMTQuNzQ4IiB5PSI0MjAuNzk2OSI+Qm9yZGVyQ29sb3IgJEZHQ09MT1I8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iOS45NjY4IiB4PSI1IiB5PSI0MzcuMDkzOCI+fTwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjAyIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyNTkuNzU4OCIgeD0iNSIgeT0iNDUzLjM5MDYiPnNraW5wYXJhbSBVc2VDYXNlU3RlcmVvVHlwZSB7PC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMDIiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE2Ni41MDI5IiB4PSIxNC43NDgiIHk9IjQ2OS42ODc1Ij5Gb250Q29sb3IgJEZHQ09MT1I8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTkxLjQ4MTQiIHg9IjE0Ljc0OCIgeT0iNDg1Ljk4NDQiPkZvbnROYW1lICRGT05UX05BTUU8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYwMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iOS45NjY4IiB4PSI1IiB5PSI1MDIuMjgxMyI+fTwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjAyIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0LWRlY29yYXRpb249IndhdnkgdW5kZXJsaW5lIiB0ZXh0TGVuZ3RoPSI5NjIuNTk1NyIgeD0iNSIgeT0iNTE4LjU3ODEiPiFpbmNsdWRlIGh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9wbGFudHVtbC1zdGRsaWIvZ2lsYmFyYmFyYS1wbGFudHVtbC1zcHJpdGVzL21hc3Rlci9zcHJpdGVzL3Jhc3BiZXJyeS1waS5wdW1sPC90ZXh0Pjx0ZXh0IGZpbGw9IiNGRjAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEzNS44ODQ4IiB4PSI5Ljg3NCIgeT0iNTM0Ljg3NSI+Q2Fubm90IG9wZW4gVVJMPC90ZXh0Pjw\/cGxhbnR1bWwtc3JjIGhMUGpKemltNEZ4VU4tNWZkLWNoRzY5MmFzV3gxMThtakIzanl5TS05aFN3VGRQc1c2eGdWdi1KNGpoR1RiMmVyNUpyLVJhZFR5LVRwcHJuVmdSblNxaThqalFCbC02NzQ0RUdxdUhqT1RKOFFpbFlPeFN4dzFxRWdocXBRX29aVE5teTdlb3hKelBobzUzeUc1UFdlM0FVcHRCYUlwQkpUTm9tdWNSaFJLZm9RajB2cVFqaUx5MDk2TjRPNWxGbUEya0IzNHFUZ09KU2FwTUZnLVNDMnpQdnpKcTl3STZfMGZXWWc1UzFGTmNESnhYSVdPWi1lRVp0Tl8waTdzREx4TmU1M3ZHTHNLeklGWE0zUmhUMmQ3R1V3ejlvS3A4dkM2SlliRGRVTnJnakFSVHZLeG1iUTVsZmJlV25KZzNHVmdyWVF1V2hRd3BCR1E5d21ldmltT0l5V2hzYTJVZ0dRRUdQRjJRcGE5Y3BjakdBdjBST2F3R1NaQnkxZlNXYWVoU1FTZUlfMnBHSUl3ZklpdzdiSzgxajJLT2ZBN2pWYWFuMEVycFAtVXZtWTF3WUtaWlhrUzI2VURCSjdWYlA5VmhzX1h1MkZzX0JKVTVpRldQaGFpdk4yckUtM2RXSG1CZnQ0X2ZHZU1ncjNYa3g4c0FmS090SWk5OXJJX3l0ZEJmZ0dkbTNIT0tKbnFyMjNRNF9OejFiRm85S2hwaHJkWWFrRkJ1Uy1MUUVBMklFcmVTMXlwUUlpLUtxc0RkdFYzYVdUbFNqSHM4dUJ6RkFWTmlCTjRYaFJhOHI4QzREb0VzTE1rRFdrV0ZfTmV3V0xVV3F6SndNOGpPYXRxbm5SVF9RQnJfWk5vN3dITnpsd0U5dk9yOGo1VHpKbnFVLWJkLXRtek5hZzlselQ2ekNleHFuVlRhUnF5TlVjQXhWb0hJa2FYNHVOMTNVeFpYYkZmMkhrV1p0VS1menhadnJFV3B0eE9IeU1pSTVHbXZkbU9TX0M2cmZpcXdrbXdMT1BGaDBVUUtmeFlJYU8tM21XT0ZiTGl4YXFOS29TMjZTSmhyYVM3Y0NwQkUzZERmdjBCUFFsTlFfc3B6ZzMtOW0ybnh6MG0wMD8+PC9nPjwvc3ZnPg==\" style=\"max-width:100%\" width=\"100%\" class=\"uml\" alt=\"Portable 'Network in a Box'\" title=\"Porta-Network\" \/><\/p>\n<iframe src='https:\/\/immich.stanleysolutionsnw.com\/share\/9yb9_5eOa4VWItFhjnzxuNnbU_7B63HkToT5a_Q5li4Oyy77o91bW0Gx6_hUbGITCYs'\nwidth='100%' height='600px' frameborder='0'>\n<\/iframe>\n\n<p>Now, this is all pretty slick, but what's a metal box full of computers without some kind of applications that they can\nbe used for? Good question! So how about we talk about the applications that are running on this system.<\/p>\n<p>For starters, I'm running the venerable <a href=\"https:\/\/pi-hole.net\/\">PiHole<\/a> for network-wide advertisement blocking, and some\nsmall allowance of security. In addition to that, I'm using NGINX alongside the gl.iNet router to provide convenient\nhostname access to the various applications. The CorePi device provides PiHole and NGINX services.<\/p>\n<p>Beyond the networking applications, I also use a new Python-backed app that I've been building;\n<a href=\"https:\/\/github.com\/engineerjoe440\/wordwall\">WordWall<\/a>. WordWall is a pretty simple little application which drives a\ncollaborative word cloud application that I can use for some of the trainings I provide. As a matter of fact, I need to\nshare more about that training in a separate post.<\/p>\n<blockquote>\n<p>Someday... Someday...<\/p>\n<\/blockquote>\n<p>Now, for a portable network, I really need some simple little file-sharing tool. So I've got that too! I'm using\n<a href=\"https:\/\/github.com\/svenstaro\/miniserve\">miniserve<\/a> a simple little CLI application that I wrapped in a small <code>systemd<\/code>\nunit file. That unit file helps make sure that the file system gets cleared before the application gets started, and\nafterwards too. That makes it so any shared files are temporary only.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">[Unit]<\/span>\n<span class=\"na\">Description<\/span><span class=\"o\">=<\/span><span class=\"s\">todo<\/span>\n<\/code><\/pre><\/div>\n\n<p>I'm also going to be setting up a couple typing-games as little applications that some of the 4-H kids can play. I keep\nhearing that kids aren't building great typing skills on their own. So... I aim to do what little I can to help work\nthat out.<\/p>\n<p>Lastly, I'm also planning to set up a little self-hosted Kahoot alternative to use with quiz games for internet-deprived\nclassrooms. That application is <a href=\"https:\/\/classquiz.de\/\">ClassQuiz<\/a>, it's a pretty neat Python-backed application in its\nown right. I really can't wait to start using it for some of my activities. I haven't fully wrapped up deploying this\napplication. It's going to require some SSL certificate management, but I need to figure some of that out. Hopefully\nI'll be back to write more about that... later.<\/p>\n<p>But this leaves me to question how I'm going to keep track of all my configuration files. Hmm... Well, I think that\ncalls for <code>git<\/code>, more specifically... <a href=\"https:\/\/about.gitea.com\/\">Gitea<\/a> That ought to make my version control for these\nlittle applications pretty slick-n-easy.<\/p>","category":[{"@attributes":{"term":"Self-Hosted"}},{"@attributes":{"term":"server"}},{"@attributes":{"term":"portable"}},{"@attributes":{"term":"learning"}},{"@attributes":{"term":"education"}},{"@attributes":{"term":"rpi"}},{"@attributes":{"term":"raspberry-pi"}},{"@attributes":{"term":"linux"}},{"@attributes":{"term":"open-source"}},{"@attributes":{"term":"gl.inet"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"nginx"}}]},{"title":"I'm Presenting at LinuxFest Northwest 2024!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/presenting-at-linux-fest-northwest-2024.html","rel":"alternate"}},"published":"2024-04-25T08:00:00-07:00","updated":"2024-04-29T15:00:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-04-25:\/presenting-at-linux-fest-northwest-2024.html","summary":"<p>This is it... My first non-work-related technical presentation at a conference. It's pretty exciting! It's right in my own back yard, even! I'm presenting at LinuxFest Northwest, 2024!<\/p>","content":"<p>That's right... this is the big-times! Hah!<\/p>\n<p>I'm very excited to be presenting, this year, at LinuxFest Northwest 2024. This will be the first conference where I present as an individual, and not for work. I'm pretty\nexcited about that. I think it will be a good experience. Unfortunately, I won't have the option to attend in person, since I will simultaneously be helping facilitate for\na teen camp-counselor training in Rathdrum, this weekend. Oh, and did I mention I'm also releasing a podcast episode, too?<\/p>\n<p>Yeah... busy weekend.<\/p>\n<p><a href=\"https:\/\/lfnw2024.sessionize.com\/session\/599025\">\n<img src=\"https:\/\/2024.lfnw.org\/logo.svg\" style=\"width: 100%; margin: 10px;\" alt=\"Worth a Thousand Words\">\n<\/a><\/p>\n<p>You can check the full schedule <a href=\"https:\/\/lfnw2024.sessionize.com\/\">here<\/a> to get more details about all of the other amazing presentations. Mine will be held at<\/p>\n<blockquote>\n<p>11:00 AM, Pacific on Saturday<\/p>\n<\/blockquote>\n<p>Should be about 30 minutes of presentation, and then I'd love to answer questions about the work. Look forward to seeing you, there (it's free, after all)!<\/p>\n<hr>\n<h2>The Presentation!<\/h2>\n<div class=\"videowrapper youtube\">\n<iframe frameborder=\"0\" src=\"https:\/\/www.youtube-nocookie.com\/embed\/ZQ2HagO_7jo\"><\/iframe>\n<\/div>\n<iframe src='https:\/\/view.officeapps.live.com\/op\/embed.aspx?src=https:\/\/idaho4h.us-east-1.linodeobjects.com\/WorthAThousandWords-LFNWPresentation-JoeStanley.pptx'\nwidth='100%' height='600px' frameborder='0'>\n<\/iframe>","category":[{"@attributes":{"term":"Conferences"}},{"@attributes":{"term":"lecture"}},{"@attributes":{"term":"education"}},{"@attributes":{"term":"presentation"}},{"@attributes":{"term":"4h"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"pyd"}},{"@attributes":{"term":"students"}},{"@attributes":{"term":"programming"}},{"@attributes":{"term":"conferences"}}]},{"title":"First Time as a Guest Lecturer?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/first-time-as-a-guest-lecturer.html","rel":"alternate"}},"published":"2024-04-20T08:00:00-07:00","updated":"2024-04-20T08:00:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-04-20:\/first-time-as-a-guest-lecturer.html","summary":"<p>Recently, I was asked to provide a guest lecture. That was a first, for me, and was very fun getting prepared for the lecture. Don't get too excited, it wasn't anything wild, but it was a lot of fun!<\/p>","content":"<p>I was asked to come and provide a guest lecture regarding 4-H and technology. After all, 4-H is not just \"Cows and Cookies.\" Or, as some of the folks\nat the recent National Collegiate 4-H conference in Bozeman, Montana put it: \"not just sows, cows, and plows\" which I happen to like better with the\nreference to pigs.<\/p>\n<p>Either way, I wanted to share my presentation here on the blog, too. So here ya go!<\/p>\n<iframe src='https:\/\/view.officeapps.live.com\/op\/embed.aspx?src=https:\/\/idaho4h.us-east-1.linodeobjects.com\/Lesson Materials\/Collegiate\/Technology\/4-H_and_Technology_WebContent.pptx'\nwidth='100%' height='600px' frameborder='0'>\n<\/iframe>\n\n<p>I presented this slideshow along with a discussion-style lecture at the University of Idaho for their Ag Education\/Communications course\nall about extension. With 4-H being administered by Extension through the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Land-grant_university\">Land Grant University<\/a>\nsystem across the United States, it was a great junction!<\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"lecture"}},{"@attributes":{"term":"education"}},{"@attributes":{"term":"college"}},{"@attributes":{"term":"4h"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"pyd"}},{"@attributes":{"term":"students"}}]},{"title":"Electronics Dissection - Amping Up","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/electronics-dissection-amping-up.html","rel":"alternate"}},"published":"2024-04-18T16:45:00-07:00","updated":"2024-04-18T16:45:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-04-18:\/electronics-dissection-amping-up.html","summary":"<p>Stand back... electronics are flying EVERYWHERE! Here's some info on those electronics dissection activities that I've been hosting around Idaho.<\/p>","content":"<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ElectronicsDissection.png\" style=\"width: 50%; margin: 10px;\" align=\"left\" alt=\"Latah County\"><\/p>\n<p>This seems to have become my M.O... Gathering all sorts of electronics <em>junk<\/em> and ripping it apart with 4-H'ers.<\/p>\n<p>Not such a bad thing, come to think of it.<\/p>\n<p>I've been doing a lot of electronics dissecting in Idaho. Last year, we had three dissection days, in total. One in Benewah County, one in Kootenai County, and the\nlast in Custer County. Each were pretty successful, and had kids tearing apart all kinds of machines. Now, admittedly, some of them could have been saved. But\nwhat's the fun in that? Hah!<\/p>\n<p>Truly, I think this workshop could actually be broken into two activities. The first being a \"computer resurrection\" activity to attempt to <em>save<\/em> computers that\nmight otherwise be thought of as dead. The second part, then, being the dissection (read: <em>destruction<\/em>). That way, we're more effectively utilizing resources, and\nturning computers back into <em>slightly<\/em> more useful machines for those who wish to use them. Why would we want to do that? Well, there's no good sense in throwing out\nthings that are still useful. While sluggish, those old computers could be used for a number of other things, like...<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/2024-04-02_20-34-57_Electronic+Dissection+Day.png\" style=\"width: 40%; margin: 10px;\" align=\"right\" alt=\"Southern District\"><\/p>\n<ol>\n<li>Portable internet access machines.<ul>\n<li>Use them for 4-H meetings to access <a href=\"https:\/\/www.zsuite.org\/\">ZSuite<\/a>.<\/li>\n<li>Use them for <a href=\"https:\/\/connectednation.org\/programs\/teens-teach-tech\">Teens Teach Tech<\/a> activities.<\/li>\n<li>Use them for <a href=\"https:\/\/4-h.org\/programs\/tech-changemakers\/\">Tech Changemakers<\/a> activities.<\/li>\n<\/ul>\n<\/li>\n<li>\"Lightweight\" hacking machines.<ul>\n<li>Use them to teach programming skills, remotely.<\/li>\n<li>Hand them out to youth who need to access a computer for a short time.<\/li>\n<\/ul>\n<\/li>\n<li>Can you think of other cool things to do with them? Leave me an idea in the comments...<\/li>\n<\/ol>\n<p>But now... the dissection? That's a blast!!!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/tearinitup.png\" style=\"width: 100%; margin: 10px;\" alt=\"Tearin' it Up...\"><\/p>\n<p>One thing, in particular, that's especially impactful for these opportunities is that they provide youth a chance to explore, get curious, and work with their hands.\nIs it always the most in-depth, or deeply-educational? Maybe... Maybe not. But it does give kids a chance to use a screwdriver, a hammer, and some pliers. Building\nthose dexterity skills. Always fun!<\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"4h"}},{"@attributes":{"term":"electronics"}},{"@attributes":{"term":"electricity"}},{"@attributes":{"term":"hacking"}},{"@attributes":{"term":"exploration"}},{"@attributes":{"term":"dissection"}},{"@attributes":{"term":"investigation"}}]},{"title":"TX\/RX Swapping Made EZ","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/tx-rx-swapping-made-ez.html","rel":"alternate"}},"published":"2024-04-17T10:29:00-07:00","updated":"2024-04-17T10:29:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-04-17:\/tx-rx-swapping-made-ez.html","summary":"<p>This little prototyping \"hack\" was just too good not to share.<\/p>","content":"<p>This is just a short article to revel in the pure genius that is the \"UART RX\/TX Poka-Yoke\" as featured on\n<a href=\"https:\/\/www.reddit.com\/r\/electronics\/comments\/1anceqy\/rx_tx_routing_woes_be_gone\/\">Reddit<\/a><\/p>\n<p><img alt=\"Rx\/Tx Poka-Yoke\" src=\"https:\/\/i.redd.it\/reic232k6qhc1.jpeg\"><\/p>\n<p>I heard of this in the\n<a href=\"https:\/\/www.macrofab.com\/podcasts\/prototype-top-features-to-add\/\">Circuit Break - A MacroFab Podcast: EP#426: Top Features to Add to Your Next Prototype<\/a>.<\/p>\n<p>It was just too good not to share.<\/p>","category":[{"@attributes":{"term":"EngrTips"}},{"@attributes":{"term":"electronics"}},{"@attributes":{"term":"pcb"}},{"@attributes":{"term":"design"}},{"@attributes":{"term":"kicad"}},{"@attributes":{"term":"circuits"}},{"@attributes":{"term":"circuitry"}},{"@attributes":{"term":"electricity"}},{"@attributes":{"term":"prototyping"}}]},{"title":"The 4-H Lineup (for Spring, 2024)","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/the-4-h-lineup-for-spring-2024.html","rel":"alternate"}},"published":"2024-01-24T21:52:00-08:00","updated":"2024-01-24T21:52:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-01-24:\/the-4-h-lineup-for-spring-2024.html","summary":"<p>Holy cow... did somebody order a whole bunch of 4-H activities? Oh... I think that might've been me.<\/p>","content":"<p>Maybe I should back up a bit, here...<\/p>\n<p>I can start by explaining all of the fun and interesting things that I've done with 4-H over the past few months. I think I'll rewind to about June of last year...<\/p>\n<blockquote>\n<p>(more than just a <em>few<\/em> months? oh well...)<\/p>\n<\/blockquote>\n<h3>Back in June, we had a bunch of things goin' on...<\/h3>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/stac-prep-2023.jpg\" style=\"width: 30%; margin: 10px;\" align=\"right\" alt=\"Preparing for STAC, 2023\"><\/p>\n<ul>\n<li>I helped with a fun video for our teen association district representative's campaign video: <a href=\"https:\/\/youtu.be\/ekmfGRu4-rY?si=lGsrvIvp0gkvf4qx\">Watch Kale's Video on Youtube<\/a><\/li>\n<li>I helped plan and organize STAC, the State Teen Association Convention (see image at right)<\/li>\n<\/ul>\n<h3>In July...<\/h3>\n<ul>\n<li>I helped chaperone not just one, but <em>TWO<\/em> 4-H camps, the CL2NI 4-H camp held at Tensed, and the Kootenai County 4-H STEM camp<blockquote>\n<p>side note, that Kootenai county 4-H camp had some super cool underwater drones that the kids (and some adults) played around with... AWESOME! <a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/cLxmInmj3lQZgnbGLg-hw8sP3wJE-m2Bp59NQNYmv3-B1ApO77oD_V5eiC224uyDgQA\">[go look, yourself!]<\/a><\/p>\n<\/blockquote>\n<\/li>\n<\/ul>\n<h3>In August...<\/h3>\n<ul>\n<li>I joined the fine folks in Gem-Boise county to help judge for their herdsman award (animal care, clenliness, upkeep, etc.)<\/li>\n<li>I visited some youth and represented the Idaho 4-H Volunteer Association at the Bonneville county fair (namely, some of the youth who are helping with <a href=\"https:\/\/gitlab.stanleysolutionsnw.com\/idaho4h\/4HPhotoUploader\">BetterPix<\/a>)<\/li>\n<li>I helped set up and run the camera system for the Kootenai County 4-H youth livestock auction <a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/9gqF0E8WNQwSDywzREshncvK6cd1cNr85u87DdJGco5VqTAZEBzPlO_yJ6N9amBgYbk\">[go look, yourself!]<\/a><\/li>\n<li>I made it over to DJ for Idaho County 4-H at their fair! <a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/xB1tnwepB-Ptdoc6vMcAtalrGQBAa-JdmfuggCQ9Otx-4RFBRbu6FiPQ6huag7R5Z74\">[go look, yourself!]<\/a><\/li>\n<\/ul>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/idaho-co-fair-2023.png\" style=\"width: 30%; margin: 10px;\" alt=\"Idaho Co. Fair Dance, 2023\"><\/p>\n<h3>In September...<\/h3>\n<ul>\n<li>The Latah County Fair was a blast! <a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/w7XmzFmO1k-OVr6GAOGYnP3C5hvV4X0s3y1C6Y-oggy6wsD7-0MhbdJqLmxTT78OIhs\">[go look, yourself!]<\/a><\/li>\n<li>I also made it over to the Lewis County fair to visit with the staff, and help out (where I could)<\/li>\n<\/ul>\n<h3>In October...<\/h3>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/volunteer-recognition-2023.png\" style=\"width: 30%; margin: 10px;\" align=\"right\" alt=\"Recognizing Outstanding Volunteers, 2023\"><\/p>\n<ul>\n<li>It was time to recognize some volunteers in some cool places around the state!<\/li>\n<li>I attended and chaperoned the Idaho LEAD (Learn, Engage, Act, Develop) Summit <a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/u9kxfOkr9cxm7egusmRn0XYbARkvFkX5mUxw22g_JknyUFJr7LujqjCUXDJCpHsxRao\">[go look, yourself]<\/a><\/li>\n<\/ul>\n<h3>In November...<\/h3>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/joe-and-computers.jpg\" style=\"width: 30%; margin: 10px;\" alt=\"Tearing Apart Computers with Kids!\"><\/p>\n<ul>\n<li>We had our first Latah-county-wide swine project meeting, which was pig-tastic! <a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/h1Iz35ZbUzJA0YRe2Ub-5x-cBiDEwBVNotztHDNq4bJ7qLZuQOR7A5VDecD6JZTmROM\">[go look, yourself!]<\/a><\/li>\n<li>I traveled to Mackay, Idaho to help with a 4-H Senior's senior project, and we had a blast tearing apart all sorts of computers and electronic <em>stuff<\/em> <a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/8q-jn90QeH8SqvbCSfj_7YD_zcePS75wwv3YVdaFfiLHcX5XuNXnzdkGLgcsLuon9ak\">[go look, yourself!]<\/a><\/li>\n<\/ul>\n<h3>At the end of December...<\/h3>\n<ul>\n<li>The FMBE club (and I) participated in a volunteering activity in Moscow to help provide gifts for the \"Sharing Tree\" a program to give gifts to children in need. <a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/fiJyJTbSlw6aw3WF6T2C0rQp2UJJ3Belo_k0thsIlDB3bHSeQsZAh0v8Yvh6z-vs3SM\">[go look, yourself]<\/a><\/li>\n<li>We resurrected our Northern District teen retreat to great success! Small turnout, sure, but it was greatly enjoyed by all involved! <a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/K1SUyxhnQaJAaUfhhMWK0UEGnuIEEyVh0n9XRvoEHnsBmmBaKN8R72zxv5w2zRkQMHI\">[go look, yourself!]<\/a><\/li>\n<\/ul>\n<hr>\n<p>Now we're all caught up, let me share with you the plans for the new year...<\/p>\n<hr>\n<p>We just got back from a teen planning meeting for youth to organize two of the major 4-H conferences in the state, STAC and I-LEADS. (You can go listen to some interviews on\nthe Idaho 4-H Roundup podcast: https:\/\/idaho4hroundup.com\/episodes\/recorded-live-at-the-2024-steering-committee-meeting\/)<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/youth-video-production-2024.png\" style=\"width: 30%; margin: 10px;\" align=\"right\" alt=\"Youth Producing Videos\"><\/p>\n<p>I'm still working with a few youth to finish up recordings for a statewide presentation contest organized by the Idaho Pork Producers Association, and that's going quite well.\nIt's been great to get some of the youth involved in recording videos and editing their work.<\/p>\n<p>I'm now gearing up to help educate youth at the multi-county Swine Field Day coming up in March. There, I'll be educating youth about ear-notching, and I've got a pretty fun\nactivity planned. I'll be sure to take pictures and share what I'm doing.<\/p>\n<p>I'm also working on some more electronics dissection workshops with youth in Latah County. Hope to have that program going in mid March! (YAY!) Plus... I've got youth engaged\nwith me for planning it.<\/p>\n<p>Lastly, I'm also working on what I think will be a fun program for the swine project in my club... \"Pig Papers Please\" where youth will learn and better understand the\nimportance of having the correct documentation for crossing state lines with their livestock.<\/p>\n<p>And so many more leadership opportunities with youth...<\/p>\n<blockquote>\n<p>This is shaping up to be a fun year in 4-H!<\/p>\n<\/blockquote>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"4h"}},{"@attributes":{"term":"swine"}},{"@attributes":{"term":"teen-leadership"}},{"@attributes":{"term":"leadership"}},{"@attributes":{"term":"engagement"}},{"@attributes":{"term":"education"}},{"@attributes":{"term":"learning"}},{"@attributes":{"term":"higher-education"}},{"@attributes":{"term":"fair"}},{"@attributes":{"term":"travel"}}]},{"title":"Quick-n-Simple Photo Converter (for HEIC format)","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/quick-n-simple-photo-converter.html","rel":"alternate"}},"published":"2024-01-24T20:52:00-08:00","updated":"2024-01-24T20:52:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-01-24:\/quick-n-simple-photo-converter.html","summary":"<p>Those techy, geeks in the audience that use an iPhone probably recognize that the photos they take are in a format that's generally a little... Tricky to use with other resources. Seems I most frequently run into this issue when working on updating my blog with pictures that I took on my phone. I really would much rather use a <code>.png<\/code> format in place of the somewhat irksome <code>.heic<\/code> format that my iPhone captures. There's plenty of things online that I could upload my photos too, but who really wants to do that? I'd rather use something a little more \"close to home.\"<\/p>","content":"<p>I end up taking lots of pictures. Maybe not as many as some folks, but I do take a lot... Pretty much all of those pictures are taken with my iPhone. As such, they're all in\nthe <a href=\"https:\/\/en.wikipedia.org\/wiki\/High_Efficiency_Image_File_Format\"><code>.heic<\/code> (high efficiency image) format<\/a>. Now, that's neat and all, but those images are harder to embed\nin all the places I want to put them. Thus, I'm stuck back-converting them to <a href=\"https:\/\/en.wikipedia.org\/wiki\/PNG\">PNG format<\/a>.<\/p>\n<p>Like any good programmer, I went right to Google to do a little \"lookin' around\" to find a way to do this with locally on a Linux machine.<\/p>\n<p>Didn't take long to find plenty of articles recommending <code>heif-convert<\/code>, which can be installed with the following command:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>sudo<span class=\"w\"> <\/span>apt<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>libheif-examples\n<\/code><\/pre><\/div>\n\n<p><strong>GREAT!<\/strong><\/p>\n<p>... but ...<\/p>\n<p>It's a touch too long for my impatient fingers, and I don't want to run that same command over, and over, and over, and over, and over again...<\/p>\n<blockquote>\n<p>Time for a script!<\/p>\n<\/blockquote>\n<p>I decided to make a simple little script to find all of the <code>*.heic<\/code> photos in the specified directory, and run the conversion required...<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"ch\">#!\/usr\/bin\/bash<\/span>\n<span class=\"c1\"># Simple HEIC Conversion Utility (so Joe doesn&#39;t need to remember)<\/span>\n<span class=\"k\">for<\/span><span class=\"w\"> <\/span>file<span class=\"w\"> <\/span><span class=\"k\">in<\/span><span class=\"w\"> <\/span><span class=\"nv\">$1<\/span>\/*.HEIC\n<span class=\"k\">do<\/span>\n<span class=\"w\">    <\/span><span class=\"nb\">echo<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;Converting: <\/span><span class=\"nv\">$file<\/span><span class=\"s2\">&quot;<\/span>\n<span class=\"w\">    <\/span>heif-convert<span class=\"w\"> <\/span><span class=\"s2\">&quot;<\/span><span class=\"nv\">$file<\/span><span class=\"s2\">&quot;<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;<\/span><span class=\"si\">${<\/span><span class=\"nv\">file<\/span><span class=\"p\">%.*<\/span><span class=\"si\">}<\/span><span class=\"s2\">.png&quot;<\/span>\n<span class=\"w\">    <\/span>rm<span class=\"w\"> <\/span><span class=\"nv\">$file<\/span>\n<span class=\"k\">done<\/span>\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p><a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/tree\/master\/photo-convert\"><code>photo-convert<\/code> script<\/a><\/p>\n<\/blockquote>\n<p>This really made for a nice, handy little script for my photo conversion needs. Maybe you'll find it helpful, too!<\/p>","category":[{"@attributes":{"term":"Scripting"}},{"@attributes":{"term":"blog"}},{"@attributes":{"term":"website"}},{"@attributes":{"term":"shell"}},{"@attributes":{"term":"script"}},{"@attributes":{"term":"heic"}},{"@attributes":{"term":"image"}},{"@attributes":{"term":"photo"}},{"@attributes":{"term":"converter"}},{"@attributes":{"term":"conversion"}},{"@attributes":{"term":"file"}},{"@attributes":{"term":"iphone"}}]},{"title":"Adding a Subscription System to my Blog","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/adding-a-subscription-system-to-my-blog.html","rel":"alternate"}},"published":"2024-01-04T20:52:00-08:00","updated":"2024-01-04T20:52:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2024-01-04:\/adding-a-subscription-system-to-my-blog.html","summary":"<p>I've finally gotten around to getting some basic configurations with my Listmonk mailing list system going. I got started with configurations for the Idaho 4-H Roundup podcast (did I mention I've started a podcast?), but now I'm also getting one started for my blog site. Here's how I added the HTML to get it working here.<\/p>","content":"<p>Okay, so really there isn't much to this. I ended up needing to do some troubleshooting, but most of that was because of my own misunderstanding.\nUltimately, with my <a href=\"https:\/\/github.com\/nairobilug\/pelican-alchemy\">Pelican-Alchemy<\/a> theme, there's an option to add a list of the direct templates which\nshould be rendered as HTML pages for the Pelican site. After enough monkeying around, I found this option, and added my new <code>subscribe<\/code> option.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># Default value is [&#39;index&#39;, &#39;tags&#39;, &#39;categories&#39;, &#39;authors&#39;, &#39;archives&#39;]<\/span>\n<span class=\"n\">DIRECT_TEMPLATES<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"s1\">&#39;index&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;tags&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;categories&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;authors&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;archives&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;subscribe&#39;<\/span><span class=\"p\">]<\/span>\n<\/code><\/pre><\/div>\n\n<p>That <code>subscribe<\/code> corresponds to the new <code>subscribe.html<\/code> file that I stored in my\n<a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/tree\/master\/content\/templates\"><code>content\/templates\/<\/code><\/a> folder. That file is actually pretty simple:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>{% extends &quot;base.html&quot; %}\n\n{% block title %}\n  Subscribe {{ super() }}\n{% endblock %}\n\n{% block page_header %}\n  Subscribe\n{% endblock %}\n\n{% block content %}\n  <span class=\"p\">&lt;<\/span><span class=\"nt\">form<\/span> <span class=\"na\">method<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;post&quot;<\/span> <span class=\"na\">action<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;{{ LISTMONK_URL }}&quot;<\/span> <span class=\"na\">class<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;listmonk-form&quot;<\/span><span class=\"p\">&gt;<\/span>\n    <span class=\"p\">&lt;<\/span><span class=\"nt\">div<\/span><span class=\"p\">&gt;<\/span>\n        <span class=\"p\">&lt;<\/span><span class=\"nt\">h3<\/span><span class=\"p\">&gt;<\/span>{{ SITENAME }} Newsletter<span class=\"p\">&lt;\/<\/span><span class=\"nt\">h3<\/span><span class=\"p\">&gt;<\/span>\n        <span class=\"p\">&lt;<\/span><span class=\"nt\">input<\/span> <span class=\"na\">type<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;hidden&quot;<\/span> <span class=\"na\">name<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;nonce&quot;<\/span> <span class=\"p\">\/&gt;<\/span>\n        <span class=\"p\">&lt;<\/span><span class=\"nt\">p<\/span><span class=\"p\">&gt;&lt;<\/span><span class=\"nt\">input<\/span> <span class=\"na\">type<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;email&quot;<\/span> <span class=\"na\">class<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;stork-input&quot;<\/span> <span class=\"na\">name<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;email&quot;<\/span> <span class=\"na\">required<\/span> <span class=\"na\">placeholder<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;E-mail&quot;<\/span> <span class=\"p\">\/&gt;&lt;\/<\/span><span class=\"nt\">p<\/span><span class=\"p\">&gt;<\/span>\n        <span class=\"p\">&lt;<\/span><span class=\"nt\">p<\/span><span class=\"p\">&gt;&lt;<\/span><span class=\"nt\">input<\/span> <span class=\"na\">type<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;text&quot;<\/span> <span class=\"na\">class<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;stork-input&quot;<\/span> <span class=\"na\">name<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;name&quot;<\/span> <span class=\"na\">placeholder<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;Name (optional)&quot;<\/span> <span class=\"p\">\/&gt;&lt;\/<\/span><span class=\"nt\">p<\/span><span class=\"p\">&gt;<\/span>\n\n        <span class=\"p\">&lt;<\/span><span class=\"nt\">p<\/span><span class=\"p\">&gt;<\/span>\n          <span class=\"p\">&lt;<\/span><span class=\"nt\">input<\/span> <span class=\"na\">hidden<\/span> <span class=\"na\">id<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;8a08b&quot;<\/span> <span class=\"na\">type<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;checkbox&quot;<\/span> <span class=\"na\">name<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;l&quot;<\/span> <span class=\"na\">checked<\/span> <span class=\"na\">value<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;{{ LISTMONK_LIST_ID }}&quot;<\/span> <span class=\"p\">\/&gt;<\/span>\n        <span class=\"p\">&lt;\/<\/span><span class=\"nt\">p<\/span><span class=\"p\">&gt;<\/span>\n\n        <span class=\"p\">&lt;<\/span><span class=\"nt\">p<\/span><span class=\"p\">&gt;&lt;<\/span><span class=\"nt\">input<\/span> <span class=\"na\">type<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;submit&quot;<\/span> <span class=\"na\">value<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;Subscribe&quot;<\/span> <span class=\"na\">class<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;btn btn-success btn-lg&quot;<\/span> <span class=\"p\">\/&gt;&lt;\/<\/span><span class=\"nt\">p<\/span><span class=\"p\">&gt;<\/span>\n    <span class=\"p\">&lt;\/<\/span><span class=\"nt\">div<\/span><span class=\"p\">&gt;<\/span>\n  <span class=\"p\">&lt;\/<\/span><span class=\"nt\">form<\/span><span class=\"p\">&gt;<\/span>\n{% endblock %}\n<\/code><\/pre><\/div>\n\n<p>You can see that I made some customizing tweaks to allow me to use variables for the Listmonk URL and List ID. Those end up landing in the\n<code>pelicanconf.py<\/code> file:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"n\">LISTMONK_URL<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;https:\/\/listmonk.stanleysolutionsnw.com&quot;<\/span>\n<span class=\"n\">LISTMONK_LIST_ID<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;8a08bea9-66e2-4b36-9140-17f303bda981&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>With the final addition to my customized <code>footer.html<\/code> as shown below, I'm up and running with a new subscribe page on the ol' website!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"w\"> <\/span>     &lt;li class=&quot;list-inline-item&quot;&gt;&lt;a href=&quot;{{ SITEURL }}\/{{ TAGS_URL or TAGS_SAVE_AS or &#39;tags.html&#39; }}&quot;&gt;Tags&lt;\/a&gt;&lt;\/li&gt;\n<span class=\"w\"> <\/span>   {% endif %}\n<span class=\"gi\">+    &lt;li class=&quot;list-inline-item&quot;&gt;&lt;a href=&quot;{{ SITEURL }}\/{{ SUBSCRIBE_URL or SUBSCRIBE_SAVE_AS or &#39;subscribe.html&#39; }}&quot;&gt;Subscribe&lt;\/a&gt;&lt;\/li&gt;<\/span>\n<span class=\"w\"> <\/span> {% else %}\n<\/code><\/pre><\/div>\n\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/add-subscription-to-blog.png\" style=\"width: 100%\" alt=\"Making Subscribing WAY Easier\"><\/p>","category":[{"@attributes":{"term":"Blogging"}},{"@attributes":{"term":"blog"}},{"@attributes":{"term":"website"}},{"@attributes":{"term":"html"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"pelican"}},{"@attributes":{"term":"jinja2"}},{"@attributes":{"term":"static-site"}},{"@attributes":{"term":"mailing-list"}},{"@attributes":{"term":"listmonk"}},{"@attributes":{"term":"email"}},{"@attributes":{"term":"newsletter"}}]},{"title":"Adding Search to my Pelican Blog Site","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/adding-search-to-my-pelican-blog-site.html","rel":"alternate"}},"published":"2023-12-26T11:45:00-08:00","updated":"2023-12-26T11:45:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-12-26:\/adding-search-to-my-pelican-blog-site.html","summary":"<p>I'm quickly approaching the mark of 50 blog posts, which is wonderful! But it also means that finding that \"thing\" I wrote some time ago is becoming an increasing challenge. So... I need to add search to my blog site. But how? It's a static site, after all, there's not really a \"backend\" to do the search functionality for me... Luckily, there's some wonderful folks who've written tooling to add just this functionality. Here's how I added it to my blog site!<\/p>","content":"<p>Search is never easy, right? Especially when we're adding it to a static website, which by its very definition does not have any server-side tools to\npermit the construction of some form of server index, and live searching capability.<\/p>\n<p>So how do we do it?<\/p>\n<p><em>With Javascript, of course!<\/em><\/p>\n<p>... wait ... that sounds kinda awful for somebody who's a Python fan through-and-through.<\/p>\n<p>Thank goodness there's some tools out there, already that make this all possible!<\/p>\n<hr>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/stanley-solutions-blog-search.png\" style=\"width: 60%; margin: 10px;\" alt=\"Behold! Search...\" align=\"right\"><\/p>\n<h2>Introducing: <a href=\"https:\/\/pypi.org\/project\/pelican-search\/\"><code>pelican-search<\/code><\/a><\/h2>\n<p>As the name implies, this Pelican plugin incorporates search capabilities into a Python <a href=\"https:\/\/getpelican.com\/\"><em>Pelican<\/em><\/a>-based system. Simply put,\nat the time of generation, the plugin indexes all of the content in the blog's many pages and puts together one big \"cheat-sheet\" from which a few Javascript\nutilities may pull references to support a search functionality. The end product looks like what I've shown here, on the right.<\/p>\n<p>Pelican Search relies heavily on <a href=\"https:\/\/stork-search.net\/\">Stork Search<\/a>, a Rust-powered\n(<em>insert audio effect from <a href=\"https:\/\/www.jupiterbroadcasting.com\/\">Jupiter Broadcasting<\/a> here...<\/em>) search tool that markets itself as:<\/p>\n<blockquote>\n<p>Impossibly fast web search, made for static sites.<\/p>\n<\/blockquote>\n<p>I started working on this a few months ago, in a little \"free time\" (basically non-existent for me these days), and didn't have much success.\nMost notably, my current system was not using a <code>&lt;main&gt;...&lt;\/main&gt;<\/code> tag to contain the body of the page as is\n<a href=\"https:\/\/github.com\/pelican-plugins\/search?tab=readme-ov-file#stork-html-selector\">documented in their repository<\/a>, and although I'd tried a few of their\nother incantations, nothing was quite working as I needed.<\/p>\n<p>To further complicate things, my primary Pelican theme (<a href=\"https:\/\/github.com\/nairobilug\/pelican-alchemy\/tree\/master\"><code>Pelican-Alchemy<\/code><\/a>), was being loaded\ninto my project simply by installing the tarball, directly from GitHub. This meant that although I was pulling the most-up-to-date source, it wasn't\navailable directly in my blog's repository.<\/p>\n<p>So... There's my first task. Remove the tarball from my <code>requirements.txt<\/code> file and make <code>Pelican-Alchemy<\/code> a proper git submodule in my project.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>pelican\n<span class=\"gd\">- https:\/\/github.com\/nairobilug\/pelican-alchemy\/tarball\/master<\/span>\nMarkdown\nplantuml-markdown\nschemdraw-markdown\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p><a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/commit\/69ec5d9c17c009db95154cde95855c096c469232#diff-4d7c51b1efe9043e44439a949dfd92e5827321b34082903477fd04876edb7552L2\"><em>source<\/em><\/a><\/p>\n<\/blockquote>\n<p>And in case you're wondering, I just needed to run the following command to add the package as a git submodule:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>git<span class=\"w\"> <\/span>submodule<span class=\"w\"> <\/span>add<span class=\"w\"> <\/span>https:\/\/github.com\/nairobilug\/pelican-alchemy<span class=\"w\"> <\/span>themes\/pelican-alchemy\n<\/code><\/pre><\/div>\n\n<p>A little editing in my <code>pelicanconf.py<\/code> file, later, and I was now ready to use the submodule-style theme. Oh! While I'm at it,\nI'll also call out the reference for the new CSS stylings I'll need to get the theme for Stork working, nicely.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"gd\">- import alchemy<\/span>\n<span class=\"gd\">- THEME = alchemy.path()<\/span>\n<span class=\"gi\">+ THEME = &#39;themes\/pelican-alchemy\/alchemy&#39;<\/span>\nTHEME_TEMPLATES_OVERRIDES = [&#39;content\/templates&#39;]\nBOOTSTRAP_CSS = &#39;https:\/\/bootswatch.com\/4\/darkly\/bootstrap.css&#39;\nTHEME_CSS_OVERRIDES = [\n<span class=\"w\"> <\/span>   &#39;\/custom.css&#39;,\n<span class=\"gi\">+    &#39;https:\/\/files.stork-search.net\/dark.css&#39;<\/span>\n]\nSITESUBTITLE = &#39;engineering and creativity - all under one hat&#39;\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p><a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/commit\/69ec5d9c17c009db95154cde95855c096c469232#diff-d65cf0288f1d9a86915f40ad4e588bbbb59fd7b1f932ea9beaa39927e3bcf18c\"><em>source<\/em><\/a><\/p>\n<\/blockquote>\n<p>Oh! and don't forget that we need to use submodules when we do the checkout in GitHub actions!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"w\"> <\/span>   name: Build\n<span class=\"w\"> <\/span>   runs-on: ubuntu-latest\n<span class=\"w\"> <\/span>   steps:\n<span class=\"gd\">-      - uses: actions\/checkout@v2<\/span>\n<span class=\"gi\">+      - name: Checkout<\/span>\n<span class=\"gi\">+        uses: actions\/checkout@v4<\/span>\n<span class=\"gi\">+        with:<\/span>\n<span class=\"gi\">+          submodules: true<\/span>\n<span class=\"w\"> <\/span>     - name: Install Python\n<span class=\"w\"> <\/span>       uses: actions\/setup-python@v1\n<span class=\"w\"> <\/span>       with:\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p><a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/commit\/69ec5d9c17c009db95154cde95855c096c469232#diff-38c69d4be1b4265f1a6d512ddf513406b8ab04ce80c69d55c88bb945f5e0aa49\"><em>source<\/em><\/a><\/p>\n<\/blockquote>\n<p>Now... with the full source of <code>Python-Alchemy<\/code> in my repo, I got to some spelunking...<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/commit-doge-such-wow.png\" style=\"width: 50%; margin: 10px;\" alt=\"Such beauty, such grace... it's commit doge!\" align=\"left\"><\/p>\n<blockquote>\n<p>I've just got to stop and show you this. While I was poking around in there, I saw this commit message... Just look at this thing.<\/p>\n<p>Glorious.<\/p>\n<\/blockquote>\n<p>Anyway...<\/p>\n<p>I started poking around, and found that if I over-rode the <code>article.html<\/code> Jinja template, I could add the <code>&lt;main&gt;...&lt;\/main&gt;<\/code> that I so\ndesperately wanted. So, I did! I added a new file called <code>article.html<\/code> to my <code>content\/templates\/<\/code> directory. This file was almost an\nexact copy of the original Alchemy template, with the addition of the <code>main<\/code> tag as shown here.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>...\n{% endblock %}\n\n{% block content %}\n<span class=\"gi\">+  &lt;main&gt;<\/span>\n<span class=\"w\"> <\/span>   &lt;article class=&quot;article&quot;&gt;\n<span class=\"w\"> <\/span>     &lt;header&gt;\n...\n<span class=\"w\"> <\/span>     &lt;\/div&gt;\n<span class=\"w\"> <\/span>   &lt;\/article&gt;\n<span class=\"gi\">+  &lt;\/main&gt;<\/span>\n<span class=\"w\"> <\/span> {% include &#39;include\/comments.html&#39; %}\n{% endblock %}\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p><a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/commit\/69ec5d9c17c009db95154cde95855c096c469232#diff-6b66599ff6ea4101dd39c64fe73d90e190fea2325620fc0ea1dbb477f12a6b4b\"><em>source<\/em><\/a><\/p>\n<\/blockquote>\n<p>With those additions now in place, the changes I'd made <em>ages ago<\/em> to support using Stork in the GitHub Actions workflow would now work!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"gd\">-      - name: Install dependencies<\/span>\n\n<span class=\"gi\">+      - name: Collect Toolchain Requirments<\/span>\n<span class=\"gi\">+        run: |<\/span>\n<span class=\"gi\">+          wget https:\/\/files.stork-search.net\/releases\/v1.6.0\/stork-ubuntu-20-04<\/span>\n<span class=\"gi\">+          chmod +x stork-ubuntu-20-04<\/span>\n<span class=\"gi\">+      - name: Install Dependencies<\/span>\n<span class=\"w\"> <\/span>       run: |\n<span class=\"w\"> <\/span>         pip install -r requirements.txt\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p><a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/commit\/63aa1174389e90212a8fad25407fbdc769129a95#diff-38c69d4be1b4265f1a6d512ddf513406b8ab04ce80c69d55c88bb945f5e0aa49\"><em>source<\/em><\/a><\/p>\n<\/blockquote>\n<p>I decided to continue with my dark theme, opting to use the Dark theme <a href=\"https:\/\/stork-search.net\/themes\">provided for Stork<\/a>, which\ndefinitely looks clean on the site. This just took me adding the following to a customized <code>content\/templates\/include\/header.html<\/code> file:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"w\"> <\/span>         &lt;li class=&quot;list-inline-item&quot;&gt;&lt;a class=&quot;{{ fa(icon) }}&quot; href=&quot;{{ url(link) }}&quot; target=&quot;_blank&quot;&gt;&lt;\/a&gt;&lt;\/li&gt;\n<span class=\"w\"> <\/span>       {% endfor %}\n<span class=\"w\"> <\/span>     &lt;\/ul&gt;\n<span class=\"w\"> <\/span>   {% endif %}\n<span class=\"gi\">+    &lt;div class=&quot;stork-wrapper-dark&quot;&gt;<\/span>\n<span class=\"gi\">+      &lt;input data-stork=&quot;sitesearch&quot; class=&quot;stork-input&quot; placeholder=&quot;search&quot;\/&gt;<\/span>\n<span class=\"gi\">+      &lt;div data-stork=&quot;sitesearch-output&quot; class=&quot;stork-output&quot;&gt;&lt;\/div&gt;<\/span>\n<span class=\"gi\">+    &lt;\/div&gt;<\/span>\n<span class=\"gi\">+    &lt;script&gt;<\/span>\n<span class=\"gi\">+      stork.register(&quot;sitesearch&quot;, &quot;{{ SITEURL }}\/search-index.st&quot;);<\/span>\n<span class=\"gi\">+    &lt;\/script&gt;<\/span>\n<span class=\"w\"> <\/span> &lt;\/div&gt;\n&lt;\/div&gt;\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p><a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/commit\/f718252d6c776d60a4206df595836bf3908424d2#diff-5d5d17acf95c9b8def158afca8c40d2c3c99f8df6d471c6605d81d4376ad8c5f\"><em>source<\/em><\/a><\/p>\n<\/blockquote>\n<p>And a little bit to a customized <code>content\/templates\/base.html<\/code><\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"w\"> <\/span> {% for stylesheet in THEME_CSS_OVERRIDES or () %}\n<span class=\"w\"> <\/span> &lt;link rel=&quot;stylesheet&quot; href=&quot;{{ url(stylesheet) }}&quot;&gt;\n<span class=\"w\"> <\/span> {% endfor %}\n<span class=\"gi\">+  &lt;script src=&quot;https:\/\/files.stork-search.net\/releases\/v1.6.0\/stork.js&quot;&gt;&lt;\/script&gt;<\/span>\n\n<span class=\"w\"> <\/span> {% include &#39;include\/xml_feeds.html&#39; %}\n<span class=\"w\"> <\/span> {% block head %}{% endblock %}\n<span class=\"w\"> <\/span> {% include &#39;include\/analytics.html&#39; %}\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p><a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/commit\/f718252d6c776d60a4206df595836bf3908424d2#diff-f52c71daaaacd19772a9f70cb8921269f9a68114b6a2c3d4f92f3a8ee320c473\"><em>source<\/em><\/a><\/p>\n<\/blockquote>\n<hr>\n<p>Certainly a good number of changes for this, but really, it's not all that bad. And now, I've got search!<\/p>","category":[{"@attributes":{"term":"Blogging"}},{"@attributes":{"term":"blog"}},{"@attributes":{"term":"search"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"stork"}},{"@attributes":{"term":"static-site"}},{"@attributes":{"term":"html"}},{"@attributes":{"term":"jinja2"}},{"@attributes":{"term":"website"}}]},{"title":"How Many Protocols Does it Take to Open a Door?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/how-many-protocols-does-it-take-to-open-a-door.html","rel":"alternate"}},"published":"2023-12-25T14:45:00-08:00","updated":"2023-12-25T14:45:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-12-25:\/how-many-protocols-does-it-take-to-open-a-door.html","summary":"<p>It's just a garage door, right? It's just a switch, right? How hard can it be to automate it? In this article, I explore the connections between a number of projects which I've been working on for quite some time, now.<\/p>","content":"<p>Holy smokes. Really? How many protocols <em>does<\/em> it take?<\/p>\n<p>I'm about to find out.<\/p>\n<hr>\n<p>A few years ago, I endeavored to start creating an SEL protocol parser in Python. Why? Well, because I want to use some second-hand protection relays for\na variety of my home-automation needs. They're ultra-robust, well-understood, and pretty much un-breakable. With all of that, they've got great (and simple)\ninput\/output capabilities for both analog and digital needs. For me, that's excellent!<\/p>\n<p>But why the protocol? Well, I've got to be able to talk to them, of course!<\/p>\n<p>So way back in 2020 (yeah, Covid times) I started on <a href=\"\/sel-protocol-coming-to-python.html\"><code>selprotopy<\/code><\/a>. It's a relatively simple system. At the time it\njust used Telnet, and it was super rickety... I've recently updated the <a href=\"https:\/\/selprotopy.readthedocs.io\/en\/latest\/\">documentation website<\/a> to use a\nmore modern theme, and to run on ReadTheDocs. But so far, I've only talked about one protocol. What others are there?<\/p>\n<p>Well, a few years ago, I \"hung\" an older SEL-351S relay in my garage so that I could interface with the garage door openers. Realistically, it's only a\nfew hundred feet of cable (at most) from the relay's mounting point to the control-side in my house. Even with this short run, I decided that I'd prefer to\ngo with an RS-485 serial connection over the RS-232 form. After all, RS-485 can span much greater differences due to the nature of its differential signal.\nThis lead me down the rabbit hole of getting re-familiarized with RS-485. I've monkeyed with it, before, in a more professional setting, but only to test a\nknown-good system with a \"questionable\" one, so that was relatively straight-forward.<\/p>\n<p>Here, at home, I wanted to interface my SEL-351S relay with a Raspberry Pi 4. To do this, I needed a 485 adapter. I started out with the\n<a href=\"https:\/\/www.amazon.com\/RS485-CAN-HAT-Long-Distance-Communication\/dp\/B07VMB1ZKH\/\">Waveshare RS485 CAN HAT for Raspberry Pi<\/a>, but as I came to learn, this\nwasn't the best option for me, as it only supports a half-duplex connection (true RS-485). As I re-learned, the SEL relay that I'm interfacing with (as\nall SEL relays) is a full-duplex device. That means that it can simultaneously receive and transmit data. That's part of what makes SEL protocol so neat.<\/p>\n<p>So I sat with PuTTy and banged away, trying to get some form of communications, and only ever got gibberish.<\/p>\n<blockquote>\n<p>Not ideal.<\/p>\n<\/blockquote>\n<p>I ended up picking up one of these <a href=\"https:\/\/www.amazon.com\/dp\/B07B416CPK\">nifty little USB RS-485\/RS-422 converters from Amazon<\/a> to do a little more testing.\nSure enough, with a properly configured, full duplex system my relay became quite the chatty little box.<\/p>\n<p>... or ... at least as chatty as an SEL relay can get. Which really isn't very chatty at all, actually.<\/p>\n<p>So, after proving out my little theory. I decided that I should get something that would be a little easier to mount. That lead me to this <em>other<\/em> USB to\nRS-485\/RS-422 converter: <a href=\"https:\/\/www.amazon.com\/dp\/B083XSG1RG\">DSD TECH SH-U11F Isolated USB to RS485 RS422 Converter<\/a>. That really did the trick, plus,\nI could nicely mount it in my \"lab control panel.\"<\/p>\n<hr>\n<p>Wait a second. How many protocols are we up to?<\/p>\n<p>Oh, right... only 2. (because I consider RS-485 its own protocol in this context)<\/p>\n<p>That's not so bad, right? Well, I still haven't tied this system into the rest of my home automation!<\/p>\n<hr>\n<p>To connect my system to the automation center of my home (my Home-Assistant instance), I'd like to use my trusty friend, MQTT!<\/p>\n<p>So, I needed a little service to subscribe to the specific topics where my controls would be published. Not so bad. This little application would also need\nto manage the SEL protocol control, and be easily modified (with a configuration file). So I pulled out my handy little Python cookbook and started one of my\nfamous \"feverish coding sessions.\" That lead me to produce this little application:\n<a href=\"https:\/\/gitlab.stanleysolutionsnw.com\/stanleysolutionsinfra\/lab-automation\/-\/tree\/master\">Lab-Automation<\/a> Go have a look for yourself!<\/p>\n<p>With this application, I'm able to read from a configuration file just exactly what topics I should be using, and how to interface them to the operations with the\nrelay. Really, it's a pretty simple pattern that I've used in a few places around the house, now. That's been very nice to re-use the pattern.<\/p>\n<p>Now, to teach Home-Assistant to \"talk relay,\" I really just needed to configure a few buttons (in the <code>yaml<\/code>):<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"nt\">garage_door_1<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">Garage Door 1<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">icon<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">mdi:garage-variant<\/span>\n\n<span class=\"nt\">garage_door_2<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">Garage Door 2<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">icon<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">mdi:garage-variant<\/span>\n<\/code><\/pre><\/div>\n\n<p>And some automation scripts to tie it all together:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\">####################################################<\/span>\n<span class=\"c1\"># Garage Door Triggers<\/span>\n<span class=\"c1\">####################################################<\/span>\n<span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"nt\">id<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">garage_door_1_trigger<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">alias<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">Trigger Garage Door 1<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">description<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s\">&#39;&#39;<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">trigger<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"nt\">platform<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">state<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">entity_id<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">input_button.garage_door_1<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">action<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"nt\">service<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">mqtt.publish<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">data<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"nt\">topic<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;ctrl\/stanley\/lab\/garage\/door&quot;<\/span>\n<span class=\"w\">      <\/span><span class=\"nt\">payload<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;1&quot;<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">mode<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">queued<\/span>\n<span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"nt\">id<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">garage_door_2_trigger<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">alias<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">Trigger Garage Door 2<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">description<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s\">&#39;&#39;<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">trigger<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"nt\">platform<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">state<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">entity_id<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">input_button.garage_door_2<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">action<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"nt\">service<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">mqtt.publish<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">data<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"nt\">topic<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;ctrl\/stanley\/lab\/garage\/door&quot;<\/span>\n<span class=\"w\">      <\/span><span class=\"nt\">payload<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;2&quot;<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">mode<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">queued<\/span>\n<\/code><\/pre><\/div>\n\n<hr>\n<p>So that's it, all 3 protocols to make some garage doors work. So, I guess that's not really <em>too bad<\/em>, but it was more than I'd originally planned. Hah!<\/p>\n<p>Still fun, and <strong>WONDERFUL<\/strong> to have the garage doors tied into my home automation system. Having that control really has been terrific. Have a gander at the gallery\nof pictures from the project. There aren't too many, but maybe it'll give you a sense of the project, and inspire you to go do something cool!<\/p>\n<p><a href=\"https:\/\/immich.stanleysolutionsnw.com\/share\/YgHn0kfhbethMOSJ3ke8QODAm0hkEevMiA7NHYcWOz1S46gh95DBbQLWo8JC7mRBuI0\">Garage Automation with SEL Protocol Album<\/a><\/p>","category":[{"@attributes":{"term":"Home-Automation"}},{"@attributes":{"term":"home-automation"}},{"@attributes":{"term":"home-assistant"}},{"@attributes":{"term":"mqtt"}},{"@attributes":{"term":"sel"}},{"@attributes":{"term":"sel-protocol"}},{"@attributes":{"term":"open-source"}},{"@attributes":{"term":"relay"}},{"@attributes":{"term":"automation"}}]},{"title":"Custom Lightning on Demand","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/custom-lightning-on-demand.html","rel":"alternate"}},"published":"2023-11-01T10:00:00-07:00","updated":"2023-11-01T10:00:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-11-01:\/custom-lightning-on-demand.html","summary":"<p>Clearly I haven't written in a while. I've been busy with all manner of projects, of late. However, since this will be mostly video and image posting, I thought it couldn't hurt to get a quick article up.<\/p>","content":"<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/IMG_2605.png\" style=\"width: 40%; margin: 10px;\" alt=\"Stanley on Spruce\" align=\"right\"><\/p>\n<p>And yet another halloween has come...<\/p>\n<blockquote>\n<p><em>...and gone<\/em><\/p>\n<\/blockquote>\n<p>Ah, but 'twas spooky!<\/p>\n<hr>\n<p>This year, I added a new Shelly control to be able to support Home-Assistant based control of my front porch lights. Notably, it's really only the ability to turn them on or off, not changing any of the special effects settings. If you want to see what that's all about, go <a href=\"https:\/\/blog.stanleysolutionsnw.com\/spooky-scary-porch-projects.html\">check out my article from last year<\/a>.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/IMG_2596.png\" style=\"width: 20%; margin: 10px;\" alt=\"Shelly Control\" align=\"left\"><\/p>\n<p>Always fun, and now I think there's a few new tricks that I want to add to my bag.... Hmm... gonna have to start working on those soon.<\/p>\n<video id=\"halloween-effects-2023\" class=\"video-js vjs-default-skin\" controls\npreload=\"auto\" width=\"683\" height=\"384\"\ndata-setup=\"{}\">\n<source src=\"https:\/\/immich.stanleysolutionsnw.com\/api\/assets\/fd65dbb4-9305-417e-81cd-a93c7f05dbfd\/video\/playback?key=xOGoWucj4ULglfDzCsxYS0jv3wEnt9eu6u20WGUMygZWhxxbxK_FtA2DPNyCc3dezWU&c=U1vdkCqm8Ts3ORRMmkoiG1opKYE%3D\" type='video\/mp4'>\n<\/video>\n\n<video id=\"halloween-effects-2023\" class=\"video-js vjs-default-skin\" controls\npreload=\"auto\" width=\"683\" height=\"384\"\ndata-setup=\"{}\">\n<source src=\"https:\/\/immich.stanleysolutionsnw.com\/api\/assets\/977604b5-4294-44d6-a2c9-5a41e6eae7e5\/video\/playback?key=xOGoWucj4ULglfDzCsxYS0jv3wEnt9eu6u20WGUMygZWhxxbxK_FtA2DPNyCc3dezWU&c=apiFBwRcWfIGlF9EWPsJ5sGy1Yo%3D\" type='video\/mp4'>\n<\/video>\n\n<hr>\n<iframe src='https:\/\/immich.stanleysolutionsnw.com\/share\/xOGoWucj4ULglfDzCsxYS0jv3wEnt9eu6u20WGUMygZWhxxbxK_FtA2DPNyCc3dezWU?at=977604b5-4294-44d6-a2c9-5a41e6eae7e5'\nwidth='100%' height='600px' frameborder='0'>\n<\/iframe>","category":[{"@attributes":{"term":"Home-Improvement"}},{"@attributes":{"term":"effects"}},{"@attributes":{"term":"lighting"}},{"@attributes":{"term":"lightning"}},{"@attributes":{"term":"lights-alive"}},{"@attributes":{"term":"halloween"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"home-automation"}},{"@attributes":{"term":"shelly"}},{"@attributes":{"term":"home-assistant"}},{"@attributes":{"term":"hassio"}}]},{"title":"Dissecting Computers for the First Time?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/dissecting-computers-for-the-first-time.html","rel":"alternate"}},"published":"2023-03-19T12:13:00-07:00","updated":"2023-03-19T12:13:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-03-19:\/dissecting-computers-for-the-first-time.html","summary":"<p>Computers are so intricate, complex, and finely tuned that it can be scary for young folks to spend any significant time getting their hands on the hardware, and really exploring. What's more, it's not common for people to really understand what parts make up a computer, and how they can be used.<\/p>","content":"<p>I'm very happy that I've been able to participate in a new style of workshop with 4-H; dissecting computers -- tearing them apart and learning about\nwhat parts they consist of, how they're put together, and how they might be refurbished.<\/p>\n<p>Admittedly, there's a few things that I'll need to improve for the next cycle of the workshop, but there was a lot of learning, enjoyment, and fun\nhad by most of the participants. That's right, I said <em>most<\/em>, not <em>all<\/em>. That's because there was some feedback that the exercise was boring, or that\nit wasn't really what the youth were interested in doing. Fair is fair! There's always room to grow.<\/p>\n<p><img src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/apps\/files_sharing\/publicpreview\/otMmsgzgyGHAr9j?file=\/336518685_235097258964719_2275477676002838990_n.jpg&fileId=13324&x=1920&y=1080&a=true\" width=\"100%\" alt=\"Our Group\"><\/p>\n<h2>What Worked Well<\/h2>\n<p>Everybody's got old, half-dead computers. Right? Unless you're a hoarder like me... then you've got LOTS of old, half-dead computers in varying states\nof repair (or <em>disrepair<\/em>). We were able to secure PLENTY of computers that the youth could tear apart. Several wonderfully gracious community members were able\nto provide a collection of great computers for us to tear down. Everything from laptops, to towers, to an all-in-one! I'm an avid believer that this is a win-win\nfor us and the community. We were able to acquire old computers, ready for destruction; and the community was able to do a little spring-cleaning, getting rid of\nsome of their clutter. Beyond that, we're able to take those old machines and recycle their waste parts. Very nice!<\/p>\n<p>I was able to amass a small arsenal of inexpensive SSDs for the activity, which meant when some computer's needed their drives replaced, it was an easy matter.\nThat also meant that systems could see a nice little upgrade.<\/p>\n<p>We were also able to use a very cool little software tool called <a href=\"https:\/\/classquiz.de\/\">ClassQuiz<\/a> to evaluate some of the results. Unfortunately due to some\nnetworking issues on my side, this was a bit \"clunky.\" Still, it was successful and fun! I've really enjoyed working with ClassQuiz. It's a tool unto my own desires.\nAs it mentions on the website, it's self-hostable, doesn't have tracking of any kind, and the developer has been very pleasant to work with. I had some trouble\ngetting the software running on a Raspberry Pi, but we got to the bottom of it, and the developer was able to encourage me to use an x86 platform for my weekend\nactivities.<\/p>\n<p><img src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/apps\/files_sharing\/publicpreview\/otMmsgzgyGHAr9j?file=\/336596127_1273019789978195_4674619552819249092_n.jpg&fileId=13334&x=1920&y=1080&a=true\" width=\"100%\" alt=\"Oh Dear...\"><\/p>\n<h1>Where the Exercise Needs Some Improvement<\/h1>\n<p>Like I mentioned, there were some youth who did not enjoy the activity as much as others. I don't want this to overshadow how much some of the youth enjoyed\nthe activity -- it was still very well received, but clearly there's some work that can be done. The crux of this, I feel was a lack of opportunity for the\nolder youth to take on more of a leadership role. I think that this is something that I can improve with more time to prepare, and more time working through\nthe exercise. With every \"run\" of this event, it'll become more polished, and hopefully more fun! Also, I think that with more time to prepare with the youth\nbefore the event, I'll be able to give \"training\" to the older members to prepare them to help with the activities. Hopefully, that will empower them to take\ncharge in the event, and help their younger peers; and after all, there's no better way to learn than to teach!<\/p>\n<p>Another key point I think that could use some refining is the distinction of parts in the event. We tried to do two disparate things in one exercise. Perhaps\nnot the best choice on my part. It wasn't a complete disaster, but it was a bit chaotic, and could use some refining. We tried to both dismantle the computers\nand then install new operating systems on them.<\/p>\n<p>Yeah, in retrospect that <em>does<\/em> seem like a bad order-of-operations, and a bit prone to disaster. Luckily, pretty much everyone had a good time with it, still.\nI think what we can do to improve this is just breaking out two separate workshop activities; the first of which can be attempting to upgrade computers and\nload a new operating system on those machines (effectively refurbishing those old computers), and the second activity can be tearing apart the computers that\nwere <em>really<\/em> dead and couldn't take a new operating system. Hindsight is 20\/20, right? Well, I think I'll be able to improve it vastly for the second go-around.<\/p>\n<p>Lastly, the networking was a bit tricky. That's my fault -- full stop. It's not that it can't, or shouldn't be done; it's that I didn't prepare well enough in\nadvance. Don't get me wrong, I'm not trying to bash myself too much, I'm just trying to point out that more could have been done in this area to make this part\nsmooth and useful. We weren't able to connect any of the computers to a network, and that's mostly because I didn't have the systems well-enough-prepared in\nadvance. And that's ok! It's something to learn from, and honestly, I don't think any of the youth really noticed. So it wasn't really a \"problem.\"<\/p>\n<h2>Closing Thoughts<\/h2>\n<p>Altogether, I think it was a very successful event. 5\/7 youth were very appreciative of the exercise, and said that it was lots of fun and they would attend\nagain if given the chance. We were able to identify some computer hardware that could still be used, and we were able to prepare a good chunk of e-waste for\nrecycling.<\/p>\n<p>I'm hopeful that I'll be able to take the activity to other counties soon, and continue to bring the event to others!<\/p>\n<h2>See All the Pictures!<\/h2>\n<p>Check out more on my Nextcloud instance:\n<strong><em><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/otMmsgzgyGHAr9j?\">https:\/\/nextcloud.stanleysolutionsnw.com\/s\/otMmsgzgyGHAr9j<\/a><\/em><\/strong><\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"4-h"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"computers"}},{"@attributes":{"term":"workshop"}},{"@attributes":{"term":"teen"}},{"@attributes":{"term":"learning"}},{"@attributes":{"term":"education"}},{"@attributes":{"term":"hands-on"}},{"@attributes":{"term":"recycling"}},{"@attributes":{"term":"technology"}},{"@attributes":{"term":"linux"}}]},{"title":"KYG 2023","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/kyg-2023.html","rel":"alternate"}},"published":"2023-02-21T21:19:00-08:00","updated":"2023-02-21T21:19:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-02-21:\/kyg-2023.html","summary":"<p>I was very happy to participate in the Idaho 4-H \"Know Your Government\" (KYG) Conference in Boise this past weekend. Let me tell you, it was an absolute blast! I loved it! And what's more, I think the youth loved it, too.<\/p>","content":"<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/82D6A57A-6C3C-4C7A-860B-2C937815E372.jpeg\" width=\"40%\" alt=\"some of the delegates\" align=\"right\" ><\/p>\n<p>I just wanted to leave a few of the pictures I had taken of me during STAC this year. We had an absolute blast in Boise this President's Day weekend. Idaho 4-H delegates\nfrom around the state in 8th and 9th grades toured the capitol, the Idaho supreme court, and other parts of Boise. They learned about the legislative and judicial branches\nof Idaho state government, and they even met with the Governor of Idaho!<\/p>\n<p>I always come away from these events realizing just how fortunate I am to get to work with some truly wonderful people, supporting such a fun and exciting program.\nParticipating in 4-H is a true privilege, and it's just so much fun to hang out with all those brilliant young leaders. Seeing them grow and mature is so much fun!<\/p>\n<p>Something else that really made me happy is that this year I was able to see some young 4-H'ers from Clearwater county (where I grew up) get involved in the leadership\nteams organizing the event. How cool is that?! It's come a long way since when I grew up in Clearwater county, and it's exciting to see some new faces getting involved.\nIn fact, I think it's been nearly a decade since someone from Clearwater county was involved at this level! How cool is that?<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/AB2433E4-A34B-4846-9962-8CB3E3AE689F.jpeg\" width=\"40%\" alt=\"some cool chaperones, too!\"><\/p>\n<p>Not only are there really cool youth participants, the chaperones and adults make these events so much fun. I'm reminded just how much fun it is to get to hang around\nother adults that like indulging in some of the more fun parts of youth events. Being silly with the delegates, having a good time, and still carrying ourselves with\ndignity. Clearly I'm still \"riding the high\" of the event!<\/p>\n<p>If you ever get the chance to send your youth to KYG, or go yourself... <strong><em>DO IT!<\/em><\/strong><\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"4-h"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"conference"}},{"@attributes":{"term":"government"}},{"@attributes":{"term":"learning"}},{"@attributes":{"term":"legislature"}},{"@attributes":{"term":"leadership"}},{"@attributes":{"term":"capitol"}}]},{"title":"Updated Wiki Page for Capstone Projects","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/updated-wiki-page-for-capstone-projects.html","rel":"alternate"}},"published":"2023-01-22T11:42:00-08:00","updated":"2023-01-22T11:42:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-22:\/updated-wiki-page-for-capstone-projects.html","summary":"<p>I've often written about the various bits of project work pertaining to my Engineering Capstones. I'm proud to say that they've made some really impressive steps, and for that I'm both thankful and very proud. But until now, it's been a bit of a mess trying to put all that information in one place. Now, I'm getting better about that!<\/p>","content":"<p>Yes, I've written a bunch of blogposts about the Engineering Capstone project work that students have been conducting at the University of Idaho. If you don't\nbelieve me, or you've forgotten, go take a look at the <a href=\"https:\/\/blog.stanleysolutionsnw.com\/category\/capstone.html\">summary of posts<\/a>.<\/p>\n<p>There's a lot of really cool things that they've done in the past, but I've not done the best keeping up with it all and putting it in one <em>central<\/em> place so that\nfolks can just find the stuff they're interested in. Well, I've finally gotten around to updating my <a href=\"https:\/\/js.wiki\/\">Wiki.js<\/a> instance to have some nice pages\nabout all that information.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/stanleywiki-capstone-pages.png\" width=\"100%\" alt=\"new wiki, who dis?\"><\/p>\n<p>More than anything, it's a \"landing place\" for most of the other information that's already been culminated. Regardless, it's a nice face-lift!<\/p>\n<p><a href=\"https:\/\/wikijs.stanleysolutionsnw.com\/\">Go Take a Look!<\/a> Then, let me know what you think in the new GitHub comments section, below!<\/p>","category":[{"@attributes":{"term":"Capstone"}},{"@attributes":{"term":"wiki"}},{"@attributes":{"term":"wikijs"}},{"@attributes":{"term":"markdown"}},{"@attributes":{"term":"capstone"}},{"@attributes":{"term":"self-hosted"}},{"@attributes":{"term":"container"}},{"@attributes":{"term":"docker"}}]},{"title":"Making Configuration for a Python Application Simple!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/making-configuration-for-a-python-application-simple.html","rel":"alternate"}},"published":"2023-01-17T14:23:00-08:00","updated":"2023-01-17T14:23:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-17:\/making-configuration-for-a-python-application-simple.html","summary":"<p>If you're a nerd like me, you can probably think of your favorite self-hosted application right now. Better yet, you can probably think of all the reasons you love it. You know, one of the staples of a great self-hosted application is its ability to make the configuration <em>your own<\/em> and do so easily! I've been working on a number of little applications lately and they all need configuration, so I've started setting this up with the help of Python and TOML!<\/p>","content":"<p>If you know me, you'll know that I often have <em>way too many projects<\/em> all in process <em>at the same dang time<\/em>.<\/p>\n<blockquote>\n<p>So proud. So proud...<\/p>\n<\/blockquote>\n<p>Well, as a result of this, lately, I've been able to capitalize on some common work. Primarily surrounding the configuration management for these apps.\nConfiguration is a bit of a tricky subject, sometimes. Because often-times, it depends greatly on how the application will be hosted, how the configuration\nshould be set-up. But also, different devops folks will like different mechanisms to apply their config. After all, some folks like using nothing more than\nenvironment variables for EVERYTHING. This makes configuring an app with tools like <a href=\"https:\/\/docs.docker.com\/get-started\/08_using_compose\/\"><code>docker-compose<\/code><\/a>\na cinch. However, there are others who would rather set up their configuration with the file, itself. Thus, marrying the options can be a bit challenging at times.<\/p>\n<p>I've recently come into the awareness of the Python package <a href=\"https:\/\/pypi.org\/project\/toml-config\/\"><code>toml-config<\/code><\/a>. This simple little package wraps other\nPython libraries to support using TOML as the basis of configuration files.<\/p>\n<h2>What is TOML, anyway?<\/h2>\n<p>Well, if you'd like to go read for yourself, you can visit <a href=\"https:\/\/toml.io\/en\/\">the TOML website<\/a>. But here's a simple example:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># This is a TOML document<\/span>\n\n<span class=\"n\">title<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;TOML Example&quot;<\/span>\n\n<span class=\"k\">[owner]<\/span>\n<span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;Tom Preston-Werner&quot;<\/span>\n<span class=\"n\">dob<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"ld\">1979-05-27T07:32:00-08:00<\/span>\n\n<span class=\"k\">[database]<\/span>\n<span class=\"n\">enabled<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"kc\">true<\/span>\n<span class=\"n\">ports<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"w\"> <\/span><span class=\"mi\">8000<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"mi\">8001<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"mi\">8002<\/span><span class=\"w\"> <\/span><span class=\"p\">]<\/span>\n<span class=\"n\">data<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"s2\">&quot;delta&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;phi&quot;<\/span><span class=\"p\">],<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"mf\">3.14<\/span><span class=\"p\">]<\/span><span class=\"w\"> <\/span><span class=\"p\">]<\/span>\n<span class=\"n\">temp_targets<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span><span class=\"w\"> <\/span><span class=\"n\">cpu<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"mf\">79.5<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"n\">case<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"mf\">72.0<\/span><span class=\"w\"> <\/span><span class=\"p\">}<\/span>\n\n<span class=\"k\">[servers]<\/span>\n\n<span class=\"k\">[servers.alpha]<\/span>\n<span class=\"n\">ip<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;10.0.0.1&quot;<\/span>\n<span class=\"n\">role<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;frontend&quot;<\/span>\n\n<span class=\"k\">[servers.beta]<\/span>\n<span class=\"n\">ip<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;10.0.0.2&quot;<\/span>\n<span class=\"n\">role<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;backend&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>TOML is the basis for the modern Python packaging standard providing <code>pyproject.toml<\/code> files in place of the executable <code>setup.py<\/code>. That's another\nconversation, for a different day, perhaps I'll dive into that sometime soon.<\/p>\n<p>Anyway, TOML is a nice, concise way of describing settings and configuration options in an easily readable format. I'm a big fan of JSON as a general\nrule, but TOML makes configuration pretty easy to get started with.<\/p>\n<h2>How do I connect TOML and Environment Variables Easily?<\/h2>\n<p>Well, with that slick little tool, <code>toml-config<\/code>, I've been able to create a really nice little framework.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kn\">from<\/span> <span class=\"nn\">typing<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">List<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">os<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">pathlib<\/span>\n<span class=\"kn\">from<\/span> <span class=\"nn\">toml_config.core<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">Config<\/span>\n\n\n<span class=\"c1\"># Inject helper method to simplify modifying values on the fly.<\/span>\n<span class=\"k\">def<\/span> <span class=\"nf\">update<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">:<\/span> <span class=\"n\">Config<\/span><span class=\"p\">,<\/span> <span class=\"n\">key_name<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Update the Specified Key Name - Section Independent.&quot;&quot;&quot;<\/span>\n    <span class=\"k\">for<\/span> <span class=\"n\">section<\/span><span class=\"p\">,<\/span> <span class=\"n\">data<\/span> <span class=\"ow\">in<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n        <span class=\"k\">if<\/span> <span class=\"n\">key_name<\/span> <span class=\"ow\">in<\/span> <span class=\"nb\">list<\/span><span class=\"p\">(<\/span><span class=\"n\">data<\/span><span class=\"o\">.<\/span><span class=\"n\">keys<\/span><span class=\"p\">()):<\/span>\n            <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">get_section<\/span><span class=\"p\">(<\/span><span class=\"n\">section<\/span><span class=\"p\">)<\/span>\n            <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span><span class=\"o\">**<\/span><span class=\"p\">{<\/span><span class=\"n\">key_name<\/span><span class=\"p\">:<\/span> <span class=\"n\">value<\/span><span class=\"p\">})<\/span>\n<span class=\"n\">Config<\/span><span class=\"o\">.<\/span><span class=\"n\">update<\/span> <span class=\"o\">=<\/span> <span class=\"n\">update<\/span>\n\n\n\n<span class=\"k\">class<\/span> <span class=\"nc\">BaseConfig<\/span><span class=\"p\">(<\/span><span class=\"nb\">object<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Base Configuration Object: Used for Inheritance for Additional Config.&quot;&quot;&quot;<\/span>\n    <span class=\"n\">_config<\/span><span class=\"p\">:<\/span> <span class=\"n\">Config<\/span>\n\n    <span class=\"nd\">@property<\/span>\n    <span class=\"k\">def<\/span> <span class=\"nf\">config<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Return the Full Configuration.&quot;&quot;&quot;<\/span>\n        <span class=\"k\">return<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"fm\">__setattr__<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">,<\/span> <span class=\"n\">name<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">:<\/span> <span class=\"n\">Any<\/span><span class=\"p\">)<\/span> <span class=\"o\">-&gt;<\/span> <span class=\"kc\">None<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Magic Attribute Setter: Update the Config Object at the Same Time.&quot;&quot;&quot;<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"vm\">__dict__<\/span><span class=\"p\">[<\/span><span class=\"n\">name<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">value<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">update<\/span><span class=\"p\">(<\/span><span class=\"n\">name<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"nf\">_do_load<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n        <span class=\"c1\"># Load Class Attributes<\/span>\n        <span class=\"k\">for<\/span> <span class=\"n\">_<\/span><span class=\"p\">,<\/span> <span class=\"n\">data<\/span> <span class=\"ow\">in<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n            <span class=\"k\">for<\/span> <span class=\"n\">key<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">data<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n                <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"vm\">__dict__<\/span><span class=\"p\">[<\/span><span class=\"n\">key<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">value<\/span>\n\n\n<span class=\"k\">class<\/span> <span class=\"nc\">ExampleConfiguration<\/span><span class=\"p\">(<\/span><span class=\"n\">BaseConfig<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"sd\">    An Example Configuration to Demonstrate the TOML Config Paradigm.<\/span>\n<span class=\"sd\">    &quot;&quot;&quot;<\/span>\n    <span class=\"c1\"># Generic Web-Server Parameters<\/span>\n    <span class=\"n\">host<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span>\n    <span class=\"n\">port<\/span><span class=\"p\">:<\/span> <span class=\"nb\">int<\/span>\n    <span class=\"c1\"># Another Section<\/span>\n    <span class=\"n\">clients<\/span><span class=\"p\">:<\/span> <span class=\"n\">List<\/span><span class=\"p\">[<\/span><span class=\"nb\">str<\/span><span class=\"p\">]<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"fm\">__init__<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">,<\/span> <span class=\"n\">config_path<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Construct the Demonstration Configuration.&quot;&quot;&quot;<\/span>\n        <span class=\"n\">pathlib<\/span><span class=\"o\">.<\/span><span class=\"n\">Path<\/span><span class=\"p\">(<\/span><span class=\"n\">config_path<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">parent<\/span><span class=\"o\">.<\/span><span class=\"n\">mkdir<\/span><span class=\"p\">(<\/span><span class=\"n\">parents<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">,<\/span> <span class=\"n\">exist_ok<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">)<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span> <span class=\"o\">=<\/span> <span class=\"n\">Config<\/span><span class=\"p\">(<\/span><span class=\"n\">config_path<\/span><span class=\"p\">)<\/span>\n        <span class=\"c1\"># Generic Web-Server Settings<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">add_section<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;WebApp&#39;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span>\n            <span class=\"n\">host<\/span><span class=\"o\">=<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;WEB_HOST&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;127.0.0.1&quot;<\/span><span class=\"p\">),<\/span>\n            <span class=\"n\">port<\/span><span class=\"o\">=<\/span><span class=\"nb\">int<\/span><span class=\"p\">(<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;WEB_PORT&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;8080&quot;<\/span><span class=\"p\">)),<\/span>\n        <span class=\"p\">)<\/span>\n        <span class=\"c1\"># Another Section of Settings<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">add_section<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;Clients&#39;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span>\n            <span class=\"n\">clients<\/span><span class=\"o\">=<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;CLIENTS&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;&quot;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">split<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;,&#39;<\/span><span class=\"p\">)<\/span>\n        <span class=\"p\">)<\/span>\n        <span class=\"c1\"># Populate the Class Variables<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_do_load<\/span><span class=\"p\">()<\/span>\n<\/code><\/pre><\/div>\n\n<p>There's a lot to that sample of code, so let me break it down a bit.<\/p>\n<h3>Monkey-Patch an Update Method into the <code>Config<\/code> Class<\/h3>\n<p>Alright, so this isn't <em>entirely<\/em> necessary, but I find it to be extremely useful. Furthermore, it isn't entirely necessary to add the monkey-patch\nbecause I've <a href=\"https:\/\/github.com\/SemenovAV\/toml_config\/pull\/1\">successfully merged a pull-request<\/a> into the <code>toml_config<\/code> project that provides this\nsame functionality, directly. That means that it's not entirely necessary to use this monkey-patch, yourself.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># Inject helper method to simplify modifying values on the fly.<\/span>\n<span class=\"k\">def<\/span> <span class=\"nf\">update<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">:<\/span> <span class=\"n\">Config<\/span><span class=\"p\">,<\/span> <span class=\"n\">key_name<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Update the Specified Key Name - Section Independent.&quot;&quot;&quot;<\/span>\n    <span class=\"k\">for<\/span> <span class=\"n\">section<\/span><span class=\"p\">,<\/span> <span class=\"n\">data<\/span> <span class=\"ow\">in<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n        <span class=\"k\">if<\/span> <span class=\"n\">key_name<\/span> <span class=\"ow\">in<\/span> <span class=\"nb\">list<\/span><span class=\"p\">(<\/span><span class=\"n\">data<\/span><span class=\"o\">.<\/span><span class=\"n\">keys<\/span><span class=\"p\">()):<\/span>\n            <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">get_section<\/span><span class=\"p\">(<\/span><span class=\"n\">section<\/span><span class=\"p\">)<\/span>\n            <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span><span class=\"o\">**<\/span><span class=\"p\">{<\/span><span class=\"n\">key_name<\/span><span class=\"p\">:<\/span> <span class=\"n\">value<\/span><span class=\"p\">})<\/span>\n<span class=\"n\">Config<\/span><span class=\"o\">.<\/span><span class=\"n\">update<\/span> <span class=\"o\">=<\/span> <span class=\"n\">update<\/span>\n<\/code><\/pre><\/div>\n\n<p>What this really does for us, is it provides a convenient mechanism to update values in the config on-the-fly and with relative ease. What's more,\nis that it allows us to do a little magic of our own to make attributes a bit more <em>magic<\/em>.<\/p>\n<h3>Making Configuration Attributes <em>MAGIC<\/em><\/h3>\n<p>I'm using this pattern with some high-school students, so I really wanted to impress upon them just how \"magic\" and easy some things can be in a\nsolid, modern language like Python. So, I spent some time figuring out how I could make it such that the configuration class would support some\nintelligent attribute updates, and save the configuration file when the attributes are applied. To make that happen, and to make it possible to\nbuild upon the framework extensibly, I built a base class.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">class<\/span> <span class=\"nc\">BaseConfig<\/span><span class=\"p\">(<\/span><span class=\"nb\">object<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Base Configuration Object: Used for Inheritance for Additional Config.&quot;&quot;&quot;<\/span>\n    <span class=\"n\">_config<\/span><span class=\"p\">:<\/span> <span class=\"n\">Config<\/span>\n\n    <span class=\"nd\">@property<\/span>\n    <span class=\"k\">def<\/span> <span class=\"nf\">config<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Return the Full Configuration.&quot;&quot;&quot;<\/span>\n        <span class=\"k\">return<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span>\n\n    <span class=\"c1\"># THIS IS THE IMPORTANT PART, RIGHT HERE!!!<\/span>\n    <span class=\"k\">def<\/span> <span class=\"fm\">__setattr__<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">,<\/span> <span class=\"n\">name<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">:<\/span> <span class=\"n\">Any<\/span><span class=\"p\">)<\/span> <span class=\"o\">-&gt;<\/span> <span class=\"kc\">None<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Magic Attribute Setter: Update the Config Object at the Same Time.&quot;&quot;&quot;<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"vm\">__dict__<\/span><span class=\"p\">[<\/span><span class=\"n\">name<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">value<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">update<\/span><span class=\"p\">(<\/span><span class=\"n\">name<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"nf\">_do_load<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n        <span class=\"c1\"># Load Class Attributes<\/span>\n        <span class=\"k\">for<\/span> <span class=\"n\">_<\/span><span class=\"p\">,<\/span> <span class=\"n\">data<\/span> <span class=\"ow\">in<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">config<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n            <span class=\"k\">for<\/span> <span class=\"n\">key<\/span><span class=\"p\">,<\/span> <span class=\"n\">value<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">data<\/span><span class=\"o\">.<\/span><span class=\"n\">items<\/span><span class=\"p\">():<\/span>\n                <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"vm\">__dict__<\/span><span class=\"p\">[<\/span><span class=\"n\">key<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">value<\/span>\n<\/code><\/pre><\/div>\n\n<p>The real magic here comes from the use of the Python <em>magic-method<\/em>: <code>__setattr__<\/code>. This method is called when an attribute is modified, and\nallows me to do some fun things. Namely when I update a configuration value such as:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"o\">&gt;&gt;&gt;<\/span> <span class=\"n\">my_config<\/span> <span class=\"o\">=<\/span> <span class=\"n\">ExampleConfiguration<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;path\/to\/config.toml&quot;<\/span><span class=\"p\">)<\/span>\n<span class=\"o\">&gt;&gt;&gt;<\/span> <span class=\"n\">my_config<\/span><span class=\"o\">.<\/span><span class=\"n\">port<\/span>\n<span class=\"mi\">8080<\/span>\n<span class=\"o\">&gt;&gt;&gt;<\/span> <span class=\"n\">my_config<\/span><span class=\"o\">.<\/span><span class=\"n\">port<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">5050<\/span> <span class=\"c1\"># This will change the value, and modify the config file<\/span>\n<span class=\"o\">&gt;&gt;&gt;<\/span> <span class=\"n\">my_config<\/span><span class=\"o\">.<\/span><span class=\"n\">port<\/span>\n<span class=\"mi\">5050<\/span>\n<\/code><\/pre><\/div>\n\n<p>The configuration will magically apply the change <em>and<\/em> update the configuration file, just to make sure everything's set!<\/p>\n<blockquote>\n<p>Marvelous!<\/p>\n<\/blockquote>\n<h3>Pre-Loading the Data<\/h3>\n<p>Like I mentioned earlier, I want this thing to be somewhat intelligent, allowing me to set environment variables that can pre-load data for me so\nthat I don't have to deal with constructing the original TOML file, if I don't want to. And let's be honest. I'm lazy, I don't want to.\nBut setting this up is easy. I just use <code>os.getenv<\/code> to retrieve the necessary values, and use those as defaults for the config file!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">class<\/span> <span class=\"nc\">ExampleConfiguration<\/span><span class=\"p\">(<\/span><span class=\"n\">BaseConfig<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"sd\">    An Example Configuration to Demonstrate the TOML Config Paradigm.<\/span>\n<span class=\"sd\">    &quot;&quot;&quot;<\/span>\n    <span class=\"c1\"># Generic Web-Server Parameters<\/span>\n    <span class=\"n\">host<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span>\n    <span class=\"n\">port<\/span><span class=\"p\">:<\/span> <span class=\"nb\">int<\/span>\n    <span class=\"c1\"># Another Section<\/span>\n    <span class=\"n\">clients<\/span><span class=\"p\">:<\/span> <span class=\"n\">List<\/span><span class=\"p\">[<\/span><span class=\"nb\">str<\/span><span class=\"p\">]<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"fm\">__init__<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">,<\/span> <span class=\"n\">config_path<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">        <\/span><span class=\"sd\">&quot;&quot;&quot;Construct the Demonstration Configuration.&quot;&quot;&quot;<\/span>\n        <span class=\"n\">pathlib<\/span><span class=\"o\">.<\/span><span class=\"n\">Path<\/span><span class=\"p\">(<\/span><span class=\"n\">config_path<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">parent<\/span><span class=\"o\">.<\/span><span class=\"n\">mkdir<\/span><span class=\"p\">(<\/span><span class=\"n\">parents<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">,<\/span> <span class=\"n\">exist_ok<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">)<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span> <span class=\"o\">=<\/span> <span class=\"n\">Config<\/span><span class=\"p\">(<\/span><span class=\"n\">config_path<\/span><span class=\"p\">)<\/span>\n        <span class=\"c1\"># Generic Web-Server Settings<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">add_section<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;WebApp&#39;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span>\n            <span class=\"n\">host<\/span><span class=\"o\">=<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;WEB_HOST&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;127.0.0.1&quot;<\/span><span class=\"p\">),<\/span>\n            <span class=\"n\">port<\/span><span class=\"o\">=<\/span><span class=\"nb\">int<\/span><span class=\"p\">(<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;WEB_PORT&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;8080&quot;<\/span><span class=\"p\">)),<\/span>\n        <span class=\"p\">)<\/span>\n        <span class=\"c1\"># Another Section of Settings<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_config<\/span><span class=\"o\">.<\/span><span class=\"n\">add_section<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;Clients&#39;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">set<\/span><span class=\"p\">(<\/span>\n            <span class=\"n\">clients<\/span><span class=\"o\">=<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;CLIENTS&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;&quot;<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">split<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;,&#39;<\/span><span class=\"p\">)<\/span>\n        <span class=\"p\">)<\/span>\n        <span class=\"c1\"># Populate the Class Variables<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">_do_load<\/span><span class=\"p\">()<\/span>\n<\/code><\/pre><\/div>\n\n<p>See in that little snippet, I use <code>Config<\/code>'s system of adding sections with their respective names, then I set the data for each of the fields\ncontained within each section. Namely, here there's two sections: <code>WebApp<\/code> and <code>Clients<\/code>. For each value in those sections, I use <code>os.getenv<\/code>\nto pull in the appropriate initialization value, or fall back to a default if no such environment variable exists.<\/p>\n<h2>Closing Thoughts<\/h2>\n<p>I think this is a pretty simple, and convenient code-pattern to support configuration from environment variables and from TOML, while at the\nsame time, providing a convenient update mechanism. This isn't as secure, or as robust as something with a database might be. After all, it's\nentirely possible for on-disk-data to be corrupted because of improper shutdown during the data write; however unlikely that may be.<\/p>\n<p>Either way, it's simple, convenient, and I enjoy it!<\/p>\n<p>Happy coding!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"toml"}},{"@attributes":{"term":"configuration"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"environment-variables"}},{"@attributes":{"term":"dot-files"}}]},{"title":"Packaging Single File Python Projects","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/packaging-single-file-python-projects.html","rel":"alternate"}},"published":"2023-01-16T09:50:00-08:00","updated":"2023-01-16T09:50:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-16:\/packaging-single-file-python-projects.html","summary":"<p>Somehow, I've managed to build quite a few random Python package, and contribute to others. I've recently been working towards converting all of the Python projects I manage to the new pyproject.toml standards for packaging, and I've recently had to work through an interesting little challenge for some of the projects which only contain a sigle Python file. No module folder, no <code>__init__.py<\/code>. Just a single file.<\/p>","content":"<p>So... I somehow seem to have become a maintainer for a number of projects. I don't claim to be a <em>good<\/em> maintainer. Just that I am a maintainer. After all,\nI'm involved, in one way, or another with each of the following projects.<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/ElectricPy\"><code>ElectricPy<\/code><\/a> -- I'm the Primary maintainer for this one, after all, I was the original author.<\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/pycev\"><code>PyCEV<\/code><\/a> -- Again... Primary maintainer.<\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/python-comtrade\"><code>Python-COMTRADE<\/code><\/a> -- Ok... so this is my fork of the original project. I'm not <em>quite<\/em> that clever to put this one together by myself.<\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/selprotopy\"><code>SELProtoPy<\/code><\/a> -- Sole Maintainer.<\/li>\n<li><a href=\"https:\/\/github.com\/engineerjoe440\/schemdraw-markdown\"><code>Schemdraw-Markdown<\/code><\/a> -- Sole Maintainer.<\/li>\n<\/ul>\n<p>Still, even <em>questionable<\/em> maintainers, such as myself, can exercise some good practices when it comes to package management.<\/p>\n<h2>What is <code>pyproject.toml<\/code> and why do we care?<\/h2>\n<p>Good question.<\/p>\n<p>It's one that lots of people have asked. So... I've put together a list of quick \"finds\" when I searched for <em>\"why to move from setup.py to pyproject.toml\"<\/em> on Google.<\/p>\n<ul>\n<li>http:\/\/ivory.idyll.org\/blog\/2021-transition-to-pyproject.toml-example.html<\/li>\n<li>https:\/\/stackoverflow.com\/questions\/72352801\/migration-from-setup-py-to-pyproject-toml-how-to-specify-package-name<\/li>\n<li>https:\/\/ianhopkinson.org.uk\/2022\/02\/understanding-setup-py-setup-cfg-and-pyproject-toml-in-python\/<\/li>\n<li>https:\/\/setuptools.pypa.io\/en\/latest\/userguide\/pyproject_config.html<\/li>\n<\/ul>\n<p>There's plenty of other well-informed articles out there; I just picked the first couple. I'll rehash, though for what its worth.<\/p>\n<p>The <code>setup.py<\/code> file is much what it sounds like. It's an executable Python file which is largely responsible for demarking the particular packaging parameters needed\nfor a Python project. Nothing too crazy about it, but as the industry has grown, it's become increasingly clear that having a package's installation managed in an\nexecutable script is less than ideal.<\/p>\n<p>Along came <code>pyproject.toml<\/code>. Offering all the same great flavors that <code>setup.py<\/code> brought to the table, with half the fat and fewer calories... I mean, without the\nneed for any executable scripts being run during installation. Bingo!<\/p>\n<h2>What's so special about packaging single-file Python projects?<\/h2>\n<p>Well, let's look at a common Python package layout:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">my_package<\/span><span class=\"o\">\/<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">  <\/span><span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">__init__<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">  <\/span><span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">some_other_file<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<span class=\"o\">|<\/span>\n<span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">pyproject<\/span><span class=\"p\">.<\/span><span class=\"nx\">toml<\/span>\n<span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">setup<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<\/code><\/pre><\/div>\n\n<p>See that in this case, the \"package\" is all contained under the <code>my_package\/<\/code> folder, which contains the appropriate <code>__init__.py<\/code> necessary to make the folder work\nas a true Python package.<\/p>\n<p>I want to do something a little <em>different<\/em> though. I mean, let's be honest; is it really all that surprising that I, <em>Joe Stanley<\/em> want to do things <em>differently<\/em>?<\/p>\n<blockquote>\n<p>Nope.<\/p>\n<\/blockquote>\n<hr>\n<p>I want a flat package like the one shown below. A package that only contains a single Python file, because that's all that it needs. No extra bloatware!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">my_package<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">pyproject<\/span><span class=\"p\">.<\/span><span class=\"nx\">toml<\/span>\n<span class=\"o\">|-<\/span><span class=\"w\"> <\/span><span class=\"nx\">setup<\/span><span class=\"p\">.<\/span><span class=\"nx\">py<\/span>\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p>But... How do I do that?<\/p>\n<\/blockquote>\n<h2>Making <code>pyproject.toml<\/code> do my bidding...<\/h2>\n<p>So... after a bit of research on <a href=\"https:\/\/flit.pypa.io\/en\/stable\/pyproject_toml.html?highlight=tool.flit.module#module-section\"><code>flit<\/code>'s documentation<\/a>, and\nfound a nice, concise way of declaring the particular module that's available in the package. See the example below from\n<a href=\"https:\/\/github.com\/engineerjoe440\/python-comtrade\/blob\/master\/pyproject.toml\">my fork of <code>python-comtrade<\/code><\/a><\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">[build-system]<\/span>\n<span class=\"n\">requires<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"s2\">&quot;flit_core &gt;=3.2,&lt;4&quot;<\/span><span class=\"p\">]<\/span>\n<span class=\"n\">build-backend<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;flit_core.buildapi&quot;<\/span>\n\n<span class=\"k\">[project]<\/span>\n<span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;python-comtrade&quot;<\/span>\n<span class=\"n\">authors<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">{<\/span><span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;David Parrini&quot;<\/span><span class=\"p\">},<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">{<\/span><span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;Joe Stanley&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"n\">email<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;engineerjoe440@yahoo.com&quot;<\/span><span class=\"p\">}<\/span>\n<span class=\"p\">]<\/span>\n<span class=\"n\">maintainers<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span>\n<span class=\"w\">    <\/span><span class=\"p\">{<\/span><span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;Joe Stanley&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"n\">email<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;engineerjoe440@yahoo.com&quot;<\/span><span class=\"p\">}<\/span>\n<span class=\"p\">]<\/span>\n<span class=\"n\">description<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;A Python 3 module designed to read Common Format for Transient Data Exchange (COMTRADE) files.&quot;<\/span>\n<span class=\"n\">readme<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;README.md&quot;<\/span>\n<span class=\"n\">license<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span><span class=\"n\">file<\/span><span class=\"w\"> <\/span><span class=\"p\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;LICENSE&quot;<\/span><span class=\"p\">}<\/span>\n<span class=\"n\">classifiers<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span>\n<span class=\"w\">    <\/span><span class=\"s2\">&quot;License :: OSI Approved :: MIT License&quot;<\/span>\n<span class=\"p\">]<\/span>\n<span class=\"n\">dynamic<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"s2\">&quot;version&quot;<\/span><span class=\"p\">]<\/span>\n\n<span class=\"k\">[project.urls]<\/span>\n<span class=\"n\">Home<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;https:\/\/github.com\/engineerjoe440\/python-comtrade&quot;<\/span>\n\n<span class=\"c1\"># Here&#39;s where the magic happens....<\/span>\n<span class=\"k\">[tool.flit.module]<\/span>\n<span class=\"n\">name<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;comtrade&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>And just like that... This package is ready to publish just the one little-ol'-Python-file without any heartache.<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"packaging"}},{"@attributes":{"term":"pyproject.toml"}},{"@attributes":{"term":"pypi"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"build"}}]},{"title":"Python, Pianobar, and MQTT?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/python-pianobar-and-mqtt.html","rel":"alternate"}},"published":"2023-01-15T13:52:00-08:00","updated":"2023-01-15T13:52:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-15:\/python-pianobar-and-mqtt.html","summary":"<p>We know that I'm something of an audio buff. I love having music around me all the time. But isn't that becoming more of a staple in the American home, anyway? I certainly think so. There's lots of folks who are also listening to music all the time. I have my own personal preferences, though. (Shocker, I know.) The thing is, I want my audio system to tie nicely into my home. I want play\/pause buttons scattered around, and well... I've got more demands.<\/p>","content":"<blockquote>\n<p>Joe? Have Demands?<\/p>\n<\/blockquote>\n<p>Psh! I know... Right? It's surprising... <strong><em>NOT<\/em><\/strong><\/p>\n<p>Anyway... I want to have music streaming in my home, but I want it to meet some requirements:<\/p>\n<ul>\n<li>I want to play Pandora using <a href=\"https:\/\/github.com\/PromyLOPh\/pianobar\">Pianobar<\/a> -- The wonderful Linux-terminal, Pandora internet radio client.<\/li>\n<li>I want to control the application over MQTT<\/li>\n<li>I want to control the application over HTTP<\/li>\n<li>I want the application to be resilient and restart itself if things go... awry<\/li>\n<li>I want the application to give me information about what's playing<\/li>\n<li>I want the application to be built with a modern Python framework<\/li>\n<\/ul>\n<blockquote>\n<p>So... What exists, already?<\/p>\n<\/blockquote>\n<p>Well, there's actually some pretty neat tools already out there:<\/p>\n<ul>\n<li><a href=\"https:\/\/pithos.github.io\/\">Pithos<\/a><\/li>\n<li><a href=\"http:\/\/deviousfish.com\/pianod\/\">Pianod<\/a><\/li>\n<li><a href=\"https:\/\/hackaday.com\/2012\/09\/20\/how-to-build-your-own-dedicated-pandora-radio\/\">Standalone Pandora Player<\/a><\/li>\n<li><a href=\"https:\/\/www.instructables.com\/Pandoras-Box-An-Internet-Radio-player-made-with\/\">Pandora's Box<\/a><\/li>\n<li><a href=\"https:\/\/volumio.com\/en\/\">Volumio<\/a><\/li>\n<\/ul>\n<p>Unfortunately, for one reason or another, none of these really meet my objectives. In fact, I want to go on a rant some other time about why <em>Volumio<\/em> doesn't\nreally meet my needs as a tinkerer\/hacker\/general-nerd-extraordinaire.<\/p>\n<h2>My Solution?<\/h2>\n<p>So glad you asked...<\/p>\n<p>I decided to write my own tool. Because that's <em>just what I need<\/em>. Another project in my life. You know... because of all this free-time I have.<\/p>\n<p>So, this project of mine will leverage the fantastic ecosystem for Pianobar, allowing me to interact with their phenomenal system of control and monitoring,\nand I'll add to that an MQTT and HTTP interface. Both the MQTT and HTTP interfaces will allow simple control, real-time information, and more!<\/p>\n<p>I started this project back on December 26th, and I'm actually quite proud of how far it's come! I really am tailoring this just for me, but at the same\ntime, I'd like for this to be something that others could pick up and play with, so most of the configuration is customizable. Things like what the MQTT\ntopics are, what the MQTT broker uses for an IP address, etc. Those are all things that somebody could pick up and swap out in their own system.<\/p>\n<p>I haven't fully deployed this, quite yet, but that's because I'm still working on getting some of the other Pipewire plumbing in place so that I can\ndeploy it on my home audio server. We'll get there, soon! I'm excited about that!<\/p>\n<h2>Where is this thing stored?<\/h2>\n<p>Well, right now, I've put it all on my GitLab. I'm considering setting it up to push to GitHub automatically, that way I've got some move visible presence\nfor the project as a whole, but you can go visit it here:<\/p>\n<blockquote>\n<p><a href=\"https:\/\/gitlab.stanleysolutionsnw.com\/engineerjoe440\/pianobarplayer\">https:\/\/gitlab.stanleysolutionsnw.com\/engineerjoe440\/pianobarplayer<\/a><\/p>\n<\/blockquote>\n<p>I've picked up some interesting learnings from the project as a whole, but since I've been doing a lot with those same learnings elsewhere, I'll be\nwriting some of my thoughts on configuration with TOML in Python soon!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"pianobar"}},{"@attributes":{"term":"mqtt"}},{"@attributes":{"term":"http"}},{"@attributes":{"term":"configuration"}},{"@attributes":{"term":"pandora"}},{"@attributes":{"term":"music"}},{"@attributes":{"term":"streaming"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"home-automation"}},{"@attributes":{"term":"networking"}}]},{"title":"Using Pipewire Link to Bridge the Gaps","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/using-pipewire-link-to-bridge-the-gaps.html","rel":"alternate"}},"published":"2023-01-09T09:00:00-08:00","updated":"2023-01-09T09:00:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-09:\/using-pipewire-link-to-bridge-the-gaps.html","summary":"<p>I'm ready to manage my audio \"wiring\" a bit more virtually, these days, and I'm ready to do that with some of the cool new tools available in Linux. Luckily for me, Pipewire has some command-line applications that make doing that an absolute cinch! And what's better, I can do it from Python, and make it a little more automagic. Now THAT's what I'm talking about!<\/p>","content":"<p>Are you a nerd like me?<\/p>\n<p>Do you want to connect audio devices in a Linux system easily, programmatically, and with a little help from Python?<\/p>\n<p>Ok, so you're probably not <em>quite<\/em> a nerd like me, but if you'd probably still find some value from hearing about this\ncool sub-system in the Pipewire ecosystem.<\/p>\n<hr>\n<p>Pipewire composes ports on devices to provide a mechanism to make connections between outputs (sources) and inputs\n(sinks). This makes it possible to connect applications and interfaces into any arbitrary connection set. There are even\ngreat tools like <a href=\"https:\/\/gitlab.freedesktop.org\/rncbc\/qpwgraph\"><code>qpwgraph<\/code><\/a> which make it possible to connect these\ninterface graphically, and make it all smooth and beautiful.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/no-connections.png\" width=\"100%\" alt=\"qpwgraph on my system\"><\/p>\n<p>That graphical application is beautiful, but what if you want to make those connections a little more manually, or\n(if you're like <em>me<\/em>), programmatically. Well, good news! Pipewire offers a whole system to make those connections from\nthe command-line.<\/p>\n<h3>Enter <code>pw-link<\/code><\/h3>\n<div class=\"highlight\"><pre><span><\/span><code>$&gt;<span class=\"w\"> <\/span>pw-link<span class=\"w\"> <\/span>--help\npw-link<span class=\"w\"> <\/span>:<span class=\"w\"> <\/span>PipeWire<span class=\"w\"> <\/span>port<span class=\"w\"> <\/span>and<span class=\"w\"> <\/span>link<span class=\"w\"> <\/span>manager.\nGeneric:<span class=\"w\"> <\/span>pw-link<span class=\"w\"> <\/span><span class=\"o\">[<\/span>options<span class=\"o\">]<\/span>\n<span class=\"w\">  <\/span>-h,<span class=\"w\"> <\/span>--help<span class=\"w\">                            <\/span>Show<span class=\"w\"> <\/span>this<span class=\"w\"> <\/span><span class=\"nb\">help<\/span>\n<span class=\"w\">      <\/span>--version<span class=\"w\">                         <\/span>Show<span class=\"w\"> <\/span>version\n<span class=\"w\">  <\/span>-r,<span class=\"w\"> <\/span>--remote<span class=\"o\">=<\/span>NAME<span class=\"w\">                     <\/span>Remote<span class=\"w\"> <\/span>daemon<span class=\"w\"> <\/span>name\nList:<span class=\"w\"> <\/span>pw-link<span class=\"w\"> <\/span><span class=\"o\">[<\/span>options<span class=\"o\">]<\/span><span class=\"w\"> <\/span><span class=\"o\">[<\/span>out-pattern<span class=\"o\">]<\/span><span class=\"w\"> <\/span><span class=\"o\">[<\/span><span class=\"k\">in<\/span>-pattern<span class=\"o\">]<\/span>\n<span class=\"w\">  <\/span>-o,<span class=\"w\"> <\/span>--output<span class=\"w\">                          <\/span>List<span class=\"w\"> <\/span>output<span class=\"w\"> <\/span>ports\n<span class=\"w\">  <\/span>-i,<span class=\"w\"> <\/span>--input<span class=\"w\">                           <\/span>List<span class=\"w\"> <\/span>input<span class=\"w\"> <\/span>ports\n<span class=\"w\">  <\/span>-l,<span class=\"w\"> <\/span>--links<span class=\"w\">                           <\/span>List<span class=\"w\"> <\/span>links\n<span class=\"w\">  <\/span>-m,<span class=\"w\"> <\/span>--monitor<span class=\"w\">                         <\/span>Monitor<span class=\"w\"> <\/span>links<span class=\"w\"> <\/span>and<span class=\"w\"> <\/span>ports\n<span class=\"w\">  <\/span>-I,<span class=\"w\"> <\/span>--id<span class=\"w\">                              <\/span>List<span class=\"w\"> <\/span>IDs\n<span class=\"w\">  <\/span>-v,<span class=\"w\"> <\/span>--verbose<span class=\"w\">                         <\/span>Verbose<span class=\"w\"> <\/span>port<span class=\"w\"> <\/span>properties\nConnect:<span class=\"w\"> <\/span>pw-link<span class=\"w\"> <\/span><span class=\"o\">[<\/span>options<span class=\"o\">]<\/span><span class=\"w\"> <\/span>output<span class=\"w\"> <\/span>input\n<span class=\"w\">  <\/span>-L,<span class=\"w\"> <\/span>--linger<span class=\"w\">                          <\/span>Linger<span class=\"w\"> <\/span><span class=\"o\">(<\/span>default,<span class=\"w\"> <\/span>unless<span class=\"w\"> <\/span>-m<span class=\"w\"> <\/span>is<span class=\"w\"> <\/span>used<span class=\"o\">)<\/span>\n<span class=\"w\">  <\/span>-P,<span class=\"w\"> <\/span>--passive<span class=\"w\">                         <\/span>Passive<span class=\"w\"> <\/span>link\n<span class=\"w\">  <\/span>-p,<span class=\"w\"> <\/span>--props<span class=\"o\">=<\/span>PROPS<span class=\"w\">                     <\/span>Properties<span class=\"w\"> <\/span>as<span class=\"w\"> <\/span>JSON<span class=\"w\"> <\/span>object\nDisconnect:<span class=\"w\"> <\/span>pw-link<span class=\"w\"> <\/span>-d<span class=\"w\"> <\/span><span class=\"o\">[<\/span>options<span class=\"o\">]<\/span><span class=\"w\"> <\/span>output<span class=\"w\"> <\/span>input\n<span class=\"w\">            <\/span>pw-link<span class=\"w\"> <\/span>-d<span class=\"w\"> <\/span><span class=\"o\">[<\/span>options<span class=\"o\">]<\/span><span class=\"w\"> <\/span>link-id\n<span class=\"w\">  <\/span>-d,<span class=\"w\"> <\/span>--disconnect<span class=\"w\">                      <\/span>Disconnect<span class=\"w\"> <\/span>ports\n<\/code><\/pre><\/div>\n\n<p>That's what the interface of the PW-Link command-line tool looks like. Simple, powerful, and <em>GREAT<\/em>.<\/p>\n<p>Want to list the sources (outputs)?<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>$&gt;<span class=\"w\"> <\/span>pw-link<span class=\"w\"> <\/span>--output<span class=\"w\"> <\/span>--id\n<span class=\"w\">  <\/span><span class=\"m\">36<\/span><span class=\"w\"> <\/span>Midi-Bridge:Midi<span class=\"w\"> <\/span>Through:<span class=\"o\">(<\/span>capture_0<span class=\"o\">)<\/span><span class=\"w\"> <\/span>Midi<span class=\"w\"> <\/span>Through<span class=\"w\"> <\/span>Port-0\n<span class=\"w\">  <\/span><span class=\"m\">43<\/span><span class=\"w\"> <\/span>v4l2_input.pci-0000_03_00.4-usb-0_4_1.0:out_0\n<span class=\"w\">  <\/span><span class=\"m\">47<\/span><span class=\"w\"> <\/span>alsa_output.pci-0000_03_00.6.analog-stereo:monitor_FL\n<span class=\"w\">  <\/span><span class=\"m\">49<\/span><span class=\"w\"> <\/span>alsa_output.pci-0000_03_00.6.analog-stereo:monitor_FR\n<span class=\"w\">  <\/span><span class=\"m\">50<\/span><span class=\"w\"> <\/span>alsa_input.pci-0000_03_00.6.analog-stereo:capture_FL\n<span class=\"w\">  <\/span><span class=\"m\">51<\/span><span class=\"w\"> <\/span>alsa_input.pci-0000_03_00.6.analog-stereo:capture_FR\n<\/code><\/pre><\/div>\n\n<p>Or... how about the sinks (inputs)?<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>$&gt;<span class=\"w\"> <\/span>pw-link<span class=\"w\"> <\/span>--input<span class=\"w\"> <\/span>--id\n<span class=\"w\">  <\/span><span class=\"m\">35<\/span><span class=\"w\"> <\/span>Midi-Bridge:Midi<span class=\"w\"> <\/span>Through:<span class=\"o\">(<\/span>playback_0<span class=\"o\">)<\/span><span class=\"w\"> <\/span>Midi<span class=\"w\"> <\/span>Through<span class=\"w\"> <\/span>Port-0\n<span class=\"w\">  <\/span><span class=\"m\">46<\/span><span class=\"w\"> <\/span>alsa_output.pci-0000_03_00.6.analog-stereo:playback_FL\n<span class=\"w\">  <\/span><span class=\"m\">48<\/span><span class=\"w\"> <\/span>alsa_output.pci-0000_03_00.6.analog-stereo:playback_FR\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p>Marvelous, isn't it?<\/p>\n<\/blockquote>\n<p>But how about getting those links created? How's it done? Can it be done programmatically? <em>With <\/em><em>Python<\/em><em>?<\/em><\/p>\n<p>Heh.<\/p>\n<p>Well, now it can!<\/p>\n<h3>Enter <code>pipewire_python<\/code><\/h3>\n<p><a href=\"https:\/\/github.com\/pablodz\/pipewire_python\"><code>pipewire_python<\/code><\/a> is the core grounds of a Python Pipewire wrapper. It was\ncreated by <a href=\"https:\/\/github.com\/pablodz\">Pablo Diaz<\/a> and offers some great functionality with Pipewire wrapped up in\nPython. Unfortunately, it didn't yet have <code>pw-link<\/code> functionality to list, create, and modify links in Pipewire. That\nsaid, I've been able to add some of the functionality. It's in-progress in a\n<a href=\"https:\/\/github.com\/pablodz\/pipewire_python\/pull\/15\">pull-request<\/a>.<\/p>\n<p>The PR is only a draft at the time of this writing, however, I'm hopeful it will be something that I can open officially\nwithin a few days, and will be able to merge before the end of the week. When it <em>does<\/em> merge, <code>pipewire_python<\/code> will\nhave functional support to list inputs, outputs, and links, and it will be able to create links as mono (single\nchannel) or automatic-stereo (automated Left\/Right channel pairing) connections. It'll be simple!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kn\">from<\/span> <span class=\"nn\">pipewire_python<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">link<\/span>\n<span class=\"n\">inputs<\/span> <span class=\"o\">=<\/span> <span class=\"n\">link<\/span><span class=\"o\">.<\/span><span class=\"n\">list_inputs<\/span><span class=\"p\">()<\/span>\n<span class=\"n\">outputs<\/span> <span class=\"o\">=<\/span> <span class=\"n\">link<\/span><span class=\"o\">.<\/span><span class=\"n\">list_outputs<\/span><span class=\"p\">()<\/span>\n\n\n<span class=\"c1\"># Connect the last output to the last input -- during testing it was found that<\/span>\n<span class=\"c1\"># Midi channel is normally listed first, so this avoids that.<\/span>\n<span class=\"n\">source<\/span> <span class=\"o\">=<\/span> <span class=\"n\">outputs<\/span><span class=\"p\">[<\/span><span class=\"o\">-<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span>\n<span class=\"n\">sink<\/span> <span class=\"o\">=<\/span> <span class=\"n\">inputs<\/span><span class=\"p\">[<\/span><span class=\"o\">-<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span>\n<span class=\"n\">source<\/span><span class=\"o\">.<\/span><span class=\"n\">connect<\/span><span class=\"p\">(<\/span><span class=\"n\">sink<\/span><span class=\"p\">)<\/span>\n\n\n<span class=\"c1\"># Fun Fact! You can connect\/disconnect in either order!<\/span>\n<span class=\"n\">sink<\/span><span class=\"o\">.<\/span><span class=\"n\">disconnect<\/span><span class=\"p\">(<\/span><span class=\"n\">source<\/span><span class=\"p\">)<\/span> <span class=\"c1\"># Tada!<\/span>\n\n\n<span class=\"c1\"># Default Input\/Output links will be made with left-left and right-right<\/span>\n<span class=\"c1\"># connections; in other words, a straight stereo connection.<\/span>\n<span class=\"c1\"># It&#39;s possible to manually cross the lines, however!<\/span>\n<span class=\"n\">source<\/span><span class=\"o\">.<\/span><span class=\"n\">right<\/span><span class=\"o\">.<\/span><span class=\"n\">connect<\/span><span class=\"p\">(<\/span><span class=\"n\">sink<\/span><span class=\"o\">.<\/span><span class=\"n\">left<\/span><span class=\"p\">)<\/span>\n<span class=\"n\">source<\/span><span class=\"o\">.<\/span><span class=\"n\">left<\/span><span class=\"o\">.<\/span><span class=\"n\">connect<\/span><span class=\"p\">(<\/span><span class=\"n\">sink<\/span><span class=\"o\">.<\/span><span class=\"n\">right<\/span><span class=\"p\">)<\/span>\n<\/code><\/pre><\/div>\n\n<blockquote>\n<p>Won't that be slick?<\/p>\n<\/blockquote>\n<p>Needless to say, I'm very excited. I'm going to use this for an automated system designed to support a VBAN streaming\nnetwork built fully on Linux. It'll be a robust, low-latency audio streaming system for my home, and I won't have to use\nthe <em>\"dreaded Microsoft Windows\"<\/em>. Won't that be neat?<\/p>\n<p>Stay tuned... I'll be sharing more on this project soon!<\/p>","category":[{"@attributes":{"term":"Audio"}},{"@attributes":{"term":"linux"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"networking"}},{"@attributes":{"term":"pipewire"}},{"@attributes":{"term":"alsa"}},{"@attributes":{"term":"sound"}},{"@attributes":{"term":"terminal"}},{"@attributes":{"term":"command-line"}}]},{"title":"The Ranch (KRNC) Gets a Face Lift","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/the-ranch-gets-a-face-lift.html","rel":"alternate"}},"published":"2023-01-08T12:00:00-08:00","updated":"2023-01-08T12:00:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-08:\/the-ranch-gets-a-face-lift.html","summary":"<p>So, I've got an old (1992) Ford pickup, and it's got a newer-ish stereo. I love keeping my favorite tunes on a USB stick that I can play everything from, but I HATE it when the volume changes from one song to the next. I go from barely being able to hear the music to having my eardrums blown out in 0.5 seconds. So, I've come to the conclusion that I need to fix that. With PYTHON!<\/p>","content":"<p>Like I'd mentioned; I absolutely <em>HATE<\/em> it when the audio in my car goes from practically inaudible to raising the dead.<\/p>\n<blockquote>\n<p>Not fun.<\/p>\n<\/blockquote>\n<p>So the question becomes, how do I do something about this problem of mine? What can I do to equalize the volume levels?\nWell, of course I can use an application to compress and normalize the volume of tracks. It's something I was quite\naccustomed to from my old radio days. You see, that's pretty common to do for a radio-station's imaging\/branding tracks.\nYou know... the little quips you hear between songs like \"Your favorite station for the 90's through now!\" etc. Most of\nthose type of track go through some kind of compressor prior to be loaded into the on-air playback system, and what's\nmore, most stations use a real-time compressor that takes the fully-mixed audio stream from the on-air studio and\ncompresses that for going out on the air. That's twice the compression for those of you who are counting, but normally\nthe compression used for the imaging tracks is specifically customized such that it will \"play nicely\" with the in-line\ncompressor in the studio.<\/p>\n<p>Ok, so that was a diversion from the root of this conversation, but I hope it gives you some of the background to\nunderstand the next bit of this topic.<\/p>\n<hr>\n<p>Compression is a slick way of bringing the low-volume stuff up to make sure it's audible, and bringing the loud stuff\ndown a bit, without making the whole thing sound terrible. I found a GIF that does its best to show some of this. It's\nnot great, but I couldn't be bothered to make my own. Hah!<\/p>\n<p><img src=\"https:\/\/audiophilestyle.com\/uploads\/monthly_2019_08\/510421229_GrundmanvsDownloadWavforms.gif.2fb2cdf545abce8f61fdc881d4ac9db0.gif\" width=\"100%\" alt=\"compressing audio\"><\/p>\n<p>There's a number of compressors available, and they're all a bit touchy, but there's some really cool ways to do it with\nLinux command-line utilities. Namely <a href=\"https:\/\/ffmpeg.org\/\"><code>ffmpeg<\/code><\/a> -- the universal, quintessential audio utility.<\/p>\n<p>If there's an app on Linux or Windows that works with audio, it almost certainly has some compatibility or reliance on\nffmpeg.<\/p>\n<p>With ffmpeg, I can do lots of things with audio... basically whatever I wanted to... but to name a few things:<\/p>\n<ul>\n<li>compress<\/li>\n<li>normalize<\/li>\n<li>filter<\/li>\n<li>equalize<\/li>\n<li>and much, <em>much<\/em> more...<\/li>\n<\/ul>\n<p>So, I did some digital spelunking and found a handful of little sets of arguments I liked for some general filters:<\/p>\n<table>\n<thead>\n<tr>\n<th>What I call it<\/th>\n<th>What the command is<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\"default\"<\/td>\n<td><code>-filter_complex compand=attacks=0:points=-80\/-900\\|-45\/-15\\|-27\/-9\\|0\/-7\\|20\/-7:gain=5<\/code><\/td>\n<\/tr>\n<tr>\n<td>\"normalize\"<\/td>\n<td><code>-filter:a \"dynaudnorm\"<\/code><\/td>\n<\/tr>\n<tr>\n<td>\"light\"<\/td>\n<td><code>-filter:a compand=.3\\|.3:1\\|1:-90\/-60\\|-60\/-40\\|-40\/-30\\|-20\/-20:6:0:-90:0.2<\/code><\/td>\n<\/tr>\n<tr>\n<td>\"heavy\"<\/td>\n<td><code>-filter:a compand=0\\|0:1\\|1:-90\/-900\\|-70\/-70\\|-30\/-9\\|0\/-3:6:0:0:0<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>So clearly, I've got a good little set of compressors and filters. Still, would you remember any of those commands? I\ncertainly won't. So I'd like to have a little tool do that for me.<\/p>\n<p>At the same time, I've also been wanting to try out the new Python tool <a href=\"https:\/\/flet.dev\/\"><code>Flet<\/code><\/a> for building Flutter\nbased applications. No, I don't want to write Dart applications, but I wouldn't mind if there's a layer between me and\nthat mess.<\/p>\n<p>So... I got to work and rebuilt my\n<a href=\"https:\/\/gitlab.stanleysolutionsnw.com\/krnc\/usb-manager\">\"Universal Song Barn Manager\" Application<\/a>. Do you notice the\nplay-on-words in the name? <em>USB<\/em> Manager...<\/p>\n<p><img src=\"https:\/\/gitlab.stanleysolutionsnw.com\/krnc\/usb-manager\/-\/raw\/master\/images\/SongBarn.png\" width=\"100%\" alt=\"KRNC - Universal Song Barn Manager\"><\/p>\n<p>I want to toot my own horn here a little bit. I built this app in <em>one, single day<\/em>.<\/p>\n<p>Admittedly, I went back and have touched up things in subsequent days, but the original application was written in one\nday. I'm kinda proud of that.<\/p>\n<p>So, the application has a relatively simple workflow:<\/p>\n<ol>\n<li>Open app<\/li>\n<li>Add songs to your \"barn\" (record file) with the app's \"<code>+<\/code>\" button<\/li>\n<li>Change the filtering selection as desired, change the track's \"Title\" if needed<\/li>\n<li>Select the USB drive you want to store the files on<\/li>\n<li>Load the drive<\/li>\n<li>Listen and enjoy!<\/li>\n<\/ol>\n<hr>\n<p>Now... I've got some work left to do. I still want to get the app set up so that it can use a \"Saddle Bag\" system to\nsupport retrieving short audio clips as \"radio imaging\" because I'm just that kind of nerd. That'll have to come soon.<\/p>\n<p>Take a look at the GitLab information if you'd like. Let me know what you think, and feel free to use it for your own\nneeds if you'd like!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"linux"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"ffmpeg"}},{"@attributes":{"term":"sound"}},{"@attributes":{"term":"terminal"}},{"@attributes":{"term":"command-line"}},{"@attributes":{"term":"flet"}},{"@attributes":{"term":"flutter"}},{"@attributes":{"term":"application"}},{"@attributes":{"term":"krnc"}}]},{"title":"I'm Giving Up on Low-Level Audio in Linux","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/giving-up-on-low-level-linux-audio.html","rel":"alternate"}},"published":"2023-01-08T10:00:00-08:00","updated":"2023-01-08T10:00:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2023-01-08:\/giving-up-on-low-level-linux-audio.html","summary":"<p>I'm so fed up with low-level audio in Linux. It's a constant struggle, and I'm throwing in the towel. I'm done.<\/p>","content":"<p><strong>I <em>want<\/em> to love low-level audio in Linux.<\/strong><\/p>\n<p><em>I really do.<\/em><\/p>\n<p>But wow. It's atrocious! I'm so tired of fighting with <code>aplay<\/code> and <code>arecord<\/code> trying to figure out what the heck is\nactually being presented to me. It's not intuitive, it's not robust, and for pity sake, I don't think it actually works.<\/p>\n<p>So I'm giving up on <em>low-level<\/em> audio in Linux.<\/p>\n<blockquote>\n<p>Catch the drift?<\/p>\n<\/blockquote>\n<p>That's right, I'm not done with <em>audio<\/em> in Linux. I'm done with ALSA.<\/p>\n<p><img src=\"https:\/\/i.imgur.com\/ruxU5.png\" width=\"100%\" alt=\"seems about right...\"><\/p>\n<blockquote>\n<p>If you don't get the joke about their missing text, think about it... silence... there <em>should<\/em> be words, but there's\nnot... it's quiet when there clearly should be sound.<\/p>\n<\/blockquote>\n<p>I'm sick of trying to figure out which device I should use, and how to plumb it through the rest of my system. It's a\nconstant uphill battle with ALSA. I'm calling it quits.<\/p>\n<p>Luckily for me, there's Pipewire. <em>Beautiful, perfect, glorious, Pipewire.<\/em><\/p>\n<p>Pipewire bridges a gap that seems so perfect. It takes all of the lessons the Linux community has learned from ALSA,\nPulseAudio, and JACK and introduces something new. I know what you might be thinking: \"something else, <em>new<\/em>? Great...\"\nBut it's not just new <em>to be new<\/em>. It's new to fix all of the mistakes from those older systems. PulseAudio is great,\nbut it's often too simple. JACK is great for audio pros, but often is too much to just <em>dabble<\/em> in. And ALSA?<\/p>\n<blockquote>\n<p>Let's not talk about ALSA anymore, shall we?<\/p>\n<\/blockquote>\n<p>Ok... so it's true that ALSA is still being used underneath Pipewire, but <em>we<\/em> don't have to deal with that nonsense.\nThe Pipewire system takes care of all that garbage for us.<\/p>\n<p>I'm going to have a whole slew of articles to follow this one, introducing lots of neat things with Pipewire, but for\nnow, let me introduce you to a few cool things.<\/p>\n<h3>Pipewire \"Guide\"<\/h3>\n<p>I can't say whether this is the <em>definitive<\/em> guide to all things Pipewire, but it's a great resource, and it covers most\nof the great tools I like to utilize: https:\/\/github.com\/mikeroyal\/PipeWire-Guide<\/p>\n<h3>Pipewire Graph<\/h3>\n<p>Ok, one thing I picked up from JACK was its super-neat graph utility. It's something I really enjoy to visualize the\nconnections. I must be a bit old-school, huh? Well, there's something just as slick for Pipewire, actually, there's a\nfew resources, but my preferred choice is <a href=\"https:\/\/gitlab.freedesktop.org\/rncbc\/qpwgraph\">QPWGraph<\/a>. It's just stinkin'\nawesome, if you ask me.<\/p>\n<p>Check out how it looks on my system! Granted, nothing's happening here, but it's still a good reference to see the\ndefault view!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/no-connections.png\" width=\"100%\" alt=\"qpwgraph on my system\"><\/p>","category":[{"@attributes":{"term":"Audio"}},{"@attributes":{"term":"linux"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"networking"}},{"@attributes":{"term":"pipewire"}},{"@attributes":{"term":"alsa"}},{"@attributes":{"term":"sound"}}]},{"title":"Adding Radon Sensors to Home Assistant","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/adding-radon-sensors-to-homeassistant.html","rel":"alternate"}},"published":"2022-12-24T10:40:00-08:00","updated":"2022-12-24T10:40:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-12-24:\/adding-radon-sensors-to-homeassistant.html","summary":"<p>Radon - an colorless, oderless gas. Sounds familiar, right? But you've probably heard that phrase in relation to other gasses like the carbon-monoxide, or perhaps natural gas. Radon poses health threats, as well, but with the added benefit of radiation. Scared yet? Don't worry.<\/p>","content":"<h2>What is Radon, Anyway?<\/h2>\n<p>The <a href=\"https:\/\/www.cdc.gov\/nceh\/features\/protect-home-radon\/index.html\">CDC<\/a> explains that Radon is:<\/p>\n<blockquote>\n<p>\" ...the second leading cause of lung cancer after cigarette smoking. If you smoke and live in a home with high radon levels, you increase your risk of developing lung cancer. Having your home tested is the only effective way to determine whether you and your family are at risk of high radon exposure.<\/p>\n<p>Radon is a radioactive gas that forms naturally when uranium, thorium, or radium, which are radioactive metals break down in rocks, soil and groundwater. People can be exposed to radon primarily from breathing radon in air that comes through cracks and gaps in buildings and homes. Because radon comes naturally from the earth, people are always exposed to it. \"<\/p>\n<\/blockquote>\n<p>Luckily for me, I don't smoke. So some of that problem is a non-starter. Buuuut... Have you\nseen <a href=\"https:\/\/blog.stanleysolutionsnw.com\/more-servers-in-the-basement.html\">my basement<\/a>?\nYeah... that's where this all comes from. And the photos in that post don't even capture the\nwet season.<\/p>\n<p>Hah!<\/p>\n<h2>What can be done?<\/h2>\n<p>The <a href=\"https:\/\/www.cdc.gov\/nceh\/features\/protect-home-radon\/index.html\">CDC<\/a> goes into some detail about some options which exist to support annual tests for\nRadon, but there's also technology that can help; and, have you met me? Of course I want to use\nthe technology! That's the fun part.<\/p>\n<p>Airthings is a pretty neat company. They offer a digital air-quality and Radon sensing solution,\nand they even make their technology accessible <em>without<\/em> needing the cloud! For self-hosters like\nme, that's pretty exciting. You can read more about\n<a href=\"https:\/\/www.airthings.com\/resources\/radon-detection\">what thoughts Airthings share regarding Radon detection methods on their website<\/a>.<\/p>\n<h2>The Sensor<\/h2>\n<p>Airthings' Wave Plus sensor is just what I needed. I picked one up last year from Amazon, but\nyou can see from <a href=\"https:\/\/shop.airthings.com\/US_EN\/wave-plus.html\">Airthings' store<\/a> that these\nthings are a bit on the spendy side. Still not bad.<\/p>\n<p><img src=\"https:\/\/shop.airthings.com\/media\/catalog\/product\/cache\/2634002fa3d4cd41f308759bc7b7f687\/a\/i\/airthings-wave-plus-hero-image-for-e-commerce-front.jpg\" style=\"width: 40%; margin: 10px;\" alt=\"Airthings Wave Plus\" align=\"left\"><\/p>\n<p>This little gadget supports Bluetooth Low Energy (BLE), and therefore, doesn't really need\ninternet access. In fact, Airthings seems to know their audience, since they support some Python\nscripts in <a href=\"https:\/\/github.com\/Airthings\/waveplus-reader\">their GitHub repository<\/a>. Their\ndocumentation for using the scripts is nice and simple, too. Pretty straight-forward to get\nup-and-running if you've got Python 2.<\/p>\n<p>That's right. I said <em>Python 2<\/em>. Unfortunately, although there's a\n<a href=\"https:\/\/github.com\/Airthings\/waveplus-reader\/pull\/8\">pull request<\/a> open to add Python 3 support,\nit hasn't been resolved yet.<\/p>\n<p>No matter!<\/p>\n<p>I've taken the liberty of snagging the updated Python 3 content where that PR originates from, and\nit certainly \"checks out.\"<\/p>\n<h2>Incorporating Into Home Assistant<\/h2>\n<p>To go over the background, quickly:<\/p>\n<p>My Home Assistant is being served from a Docker container on an old Compaq computer.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-12-24_11-46.png\" style=\"width: 60%; margin: 10px;\" alt=\"My Old Compaq Computer\" align=\"right\"><\/p>\n<p>It's a 32-bit monster running Debian 11. Slow as heck, but it plugs along alright. I recently\nrebuilt the OS from the ground up to take advantage of a second hard-drive in the machine which,\nfrankly, I'd forgotten about until recently. That's meant that I can shove the whole config back\ninto a single repository in my GitLab server. Quite nice, if you ask me.<\/p>\n<p>I digress.<\/p>\n<p>The server is WAY too old to support Bluetooth in its existing hardware. But that's what USB is for, right? So I hooked up a little USB dongle which provides BLE support. Now, I just needed to pass it all through to Home Assistant.<\/p>\n<p>I'll go through the steps I took, next, but I want to tell you; this isn't <em>exactly<\/em> the order\nthat I used initially. Would you expect me to have been that smart? To have put it together\ncorrectly on the first pass?<\/p>\n<p><strong><em>As if!<\/em><\/strong><\/p>\n<h3>Installing the System Requirements<\/h3>\n<p>So, really, the only package that I <em>needed<\/em> was <code>bluez<\/code>, but for some of the Airthings scripts,\nthere were a few others, so I'll list them all here...<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>sudo<span class=\"w\"> <\/span>apt-get<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>bluez<span class=\"w\"> <\/span>libglib2.0-dev<span class=\"w\"> <\/span>-y\n<\/code><\/pre><\/div>\n\n<p>To get the Airthings scripts running, I also needed a few Python packages. They were installed\nwith:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>sudo<span class=\"w\"> <\/span>python3<span class=\"w\"> <\/span>-m<span class=\"w\"> <\/span>pip<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>bluepy<span class=\"w\"> <\/span>tableprint\n<\/code><\/pre><\/div>\n\n<p>Again... not entirely necessary, but could be useful if you're like me, and like debugging.<\/p>\n<h3>Preparing the System<\/h3>\n<p>With <code>bluez<\/code> installed, I could now turn on the Bluetooth system. I'm not entirely sure whether\nthis is necessary for Home Assistant's purposes, but it's useful from the Airthings script\npoint-of-view.<\/p>\n<h5>Turn on Bluetooth!<\/h5>\n<div class=\"highlight\"><pre><span><\/span><code>joestan@hasio:~$<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>bluetoothctl\n<span class=\"o\">[<\/span>bluetooth<span class=\"o\">]<\/span><span class=\"c1\"># power on<\/span>\n<span class=\"o\">[<\/span>bluetooth<span class=\"o\">]<\/span><span class=\"c1\"># quit<\/span>\njoestan@hasio:~$\n<\/code><\/pre><\/div>\n\n<h5>Set Bluetooth Dongle to Specific Device Alias<\/h5>\n<p>Again, not totally sure if this was necesssary, but even if it wasn't, I like the outcome of\nhaving a specifically named USB device show up. Makes things nice!<\/p>\n<p>Listing my USB devices with <code>lsusb<\/code>, I was able to determine the appropriate device:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>joestan@hasio:~$<span class=\"w\"> <\/span>lsusb\nBus<span class=\"w\"> <\/span><span class=\"m\">001<\/span><span class=\"w\"> <\/span>Device<span class=\"w\"> <\/span><span class=\"m\">002<\/span>:<span class=\"w\"> <\/span>ID<span class=\"w\"> <\/span>1a40:0101<span class=\"w\"> <\/span>Terminus<span class=\"w\"> <\/span>Technology<span class=\"w\"> <\/span>Inc.<span class=\"w\"> <\/span>Hub\nBus<span class=\"w\"> <\/span><span class=\"m\">001<\/span><span class=\"w\"> <\/span>Device<span class=\"w\"> <\/span><span class=\"m\">001<\/span>:<span class=\"w\"> <\/span>ID<span class=\"w\"> <\/span>1d6b:0002<span class=\"w\"> <\/span>Linux<span class=\"w\"> <\/span>Foundation<span class=\"w\"> <\/span><span class=\"m\">2<\/span>.0<span class=\"w\"> <\/span>root<span class=\"w\"> <\/span>hub\nBus<span class=\"w\"> <\/span><span class=\"m\">003<\/span><span class=\"w\"> <\/span>Device<span class=\"w\"> <\/span><span class=\"m\">002<\/span>:<span class=\"w\"> <\/span>ID<span class=\"w\"> <\/span>0a5c:21e8<span class=\"w\"> <\/span>Broadcom<span class=\"w\"> <\/span>Corp.<span class=\"w\"> <\/span>BCM20702A0<span class=\"w\"> <\/span>Bluetooth<span class=\"w\"> <\/span><span class=\"m\">4<\/span>.0\nBus<span class=\"w\"> <\/span><span class=\"m\">003<\/span><span class=\"w\"> <\/span>Device<span class=\"w\"> <\/span><span class=\"m\">001<\/span>:<span class=\"w\"> <\/span>ID<span class=\"w\"> <\/span>1d6b:0001<span class=\"w\"> <\/span>Linux<span class=\"w\"> <\/span>Foundation<span class=\"w\"> <\/span><span class=\"m\">1<\/span>.1<span class=\"w\"> <\/span>root<span class=\"w\"> <\/span>hub\nBus<span class=\"w\"> <\/span><span class=\"m\">002<\/span><span class=\"w\"> <\/span>Device<span class=\"w\"> <\/span><span class=\"m\">001<\/span>:<span class=\"w\"> <\/span>ID<span class=\"w\"> <\/span>1d6b:0001<span class=\"w\"> <\/span>Linux<span class=\"w\"> <\/span>Foundation<span class=\"w\"> <\/span><span class=\"m\">1<\/span>.1<span class=\"w\"> <\/span>root<span class=\"w\"> <\/span>hub\n<\/code><\/pre><\/div>\n\n<p>I needed to edit my udev-rules and add a custom alias.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>sudo<span class=\"w\"> <\/span>nano<span class=\"w\"> <\/span>\/etc\/udev\/rules.d\/99-my_rules.rules\n<\/code><\/pre><\/div>\n\n<p>And then, since it's pretty clear that Bus 3 Device 2 is the one I'm looking for, I could update\nmy udev file accordingly:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>ACTION==&quot;add&quot;, ATTRS{idVendor}==&quot;0a5c&quot;, ATTRS{idProduct}==&quot;21e8&quot;, SYMLINK+=&quot;btooth&quot;\n<\/code><\/pre><\/div>\n\n<p>Now, after a restart, I've got that device listed properly when I <code>ls \/dev<\/code>:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>joestan@hasio:~$<span class=\"w\"> <\/span>ls<span class=\"w\"> <\/span>\/dev\nagpgart<span class=\"w\">          <\/span>mqueue<span class=\"w\">    <\/span>stderr<span class=\"w\">  <\/span>tty29<span class=\"w\">  <\/span>tty52<span class=\"w\">    <\/span>vcs3\nautofs<span class=\"w\">           <\/span>net<span class=\"w\">       <\/span>stdin<span class=\"w\">   <\/span>tty3<span class=\"w\">   <\/span>tty53<span class=\"w\">    <\/span>vcs4\nblock<span class=\"w\">            <\/span>null<span class=\"w\">      <\/span>stdout<span class=\"w\">  <\/span>tty30<span class=\"w\">  <\/span>tty54<span class=\"w\">    <\/span>vcs5\nbsg<span class=\"w\">              <\/span>nvram<span class=\"w\">     <\/span>tty<span class=\"w\">     <\/span>tty31<span class=\"w\">  <\/span>tty55<span class=\"w\">    <\/span>vcs6\nbtooth<span class=\"w\">           <\/span>parport0<span class=\"w\">  <\/span>tty0<span class=\"w\">    <\/span>tty32<span class=\"w\">  <\/span>tty56<span class=\"w\">    <\/span>vcsa<span class=\"w\">        <\/span><span class=\"c1\">#&lt;--- Look!<\/span>\nbtrfs-control<span class=\"w\">    <\/span>port<span class=\"w\">      <\/span>tty1<span class=\"w\">    <\/span>tty33<span class=\"w\">  <\/span>tty57<span class=\"w\">    <\/span>vcsa1\nbus<span class=\"w\">              <\/span>ppp<span class=\"w\">       <\/span>tty10<span class=\"w\">   <\/span>tty34<span class=\"w\">  <\/span>tty58<span class=\"w\">    <\/span>vcsa2\nchar<span class=\"w\">             <\/span>psaux<span class=\"w\">     <\/span>tty11<span class=\"w\">   <\/span>tty35<span class=\"w\">  <\/span>tty59<span class=\"w\">    <\/span>vcsa3\nconsole<span class=\"w\">          <\/span>ptmx<span class=\"w\">      <\/span>tty12<span class=\"w\">   <\/span>tty36<span class=\"w\">  <\/span>tty6<span class=\"w\">     <\/span>vcsa4\ncore<span class=\"w\">             <\/span>pts<span class=\"w\">       <\/span>tty13<span class=\"w\">   <\/span>tty37<span class=\"w\">  <\/span>tty60<span class=\"w\">    <\/span>vcsa5\ncpu_dma_latency<span class=\"w\">  <\/span>random<span class=\"w\">    <\/span>tty14<span class=\"w\">   <\/span>tty38<span class=\"w\">  <\/span>tty61<span class=\"w\">    <\/span>vcsa6\ncuse<span class=\"w\">             <\/span>rfkill<span class=\"w\">    <\/span>tty15<span class=\"w\">   <\/span>tty39<span class=\"w\">  <\/span>tty62<span class=\"w\">    <\/span>vcsu\ndisk<span class=\"w\">             <\/span>rtc<span class=\"w\">       <\/span>tty16<span class=\"w\">   <\/span>tty4<span class=\"w\">   <\/span>tty63<span class=\"w\">    <\/span>vcsu1\nfd<span class=\"w\">               <\/span>rtc0<span class=\"w\">      <\/span>tty17<span class=\"w\">   <\/span>tty40<span class=\"w\">  <\/span>tty7<span class=\"w\">     <\/span>vcsu2\nfull<span class=\"w\">             <\/span>sda<span class=\"w\">       <\/span>tty18<span class=\"w\">   <\/span>tty41<span class=\"w\">  <\/span>tty8<span class=\"w\">     <\/span>vcsu3\nfuse<span class=\"w\">             <\/span>sda1<span class=\"w\">      <\/span>tty19<span class=\"w\">   <\/span>tty42<span class=\"w\">  <\/span>tty9<span class=\"w\">     <\/span>vcsu4\nhpet<span class=\"w\">             <\/span>sda2<span class=\"w\">      <\/span>tty2<span class=\"w\">    <\/span>tty43<span class=\"w\">  <\/span>ttyS0<span class=\"w\">    <\/span>vcsu5\nhugepages<span class=\"w\">        <\/span>sdb<span class=\"w\">       <\/span>tty20<span class=\"w\">   <\/span>tty44<span class=\"w\">  <\/span>ttyS1<span class=\"w\">    <\/span>vcsu6\nhwrng<span class=\"w\">            <\/span>sdb1<span class=\"w\">      <\/span>tty21<span class=\"w\">   <\/span>tty45<span class=\"w\">  <\/span>ttyS2<span class=\"w\">    <\/span>vfio\ninitctl<span class=\"w\">          <\/span>sdb2<span class=\"w\">      <\/span>tty22<span class=\"w\">   <\/span>tty46<span class=\"w\">  <\/span>ttyS3<span class=\"w\">    <\/span>vga_arbiter\ninput<span class=\"w\">            <\/span>sdb3<span class=\"w\">      <\/span>tty23<span class=\"w\">   <\/span>tty47<span class=\"w\">  <\/span>uhid<span class=\"w\">     <\/span>vhci\nkmsg<span class=\"w\">             <\/span>sg0<span class=\"w\">       <\/span>tty24<span class=\"w\">   <\/span>tty48<span class=\"w\">  <\/span>uinput<span class=\"w\">   <\/span>vhost-net\nlog<span class=\"w\">              <\/span>sg1<span class=\"w\">       <\/span>tty25<span class=\"w\">   <\/span>tty49<span class=\"w\">  <\/span>urandom<span class=\"w\">  <\/span>vhost-vsock\nloop-control<span class=\"w\">     <\/span>shm<span class=\"w\">       <\/span>tty26<span class=\"w\">   <\/span>tty5<span class=\"w\">   <\/span>vcs<span class=\"w\">      <\/span>watchdog\nmapper<span class=\"w\">           <\/span>snapshot<span class=\"w\">  <\/span>tty27<span class=\"w\">   <\/span>tty50<span class=\"w\">  <\/span>vcs1<span class=\"w\">     <\/span>watchdog0\nmem<span class=\"w\">              <\/span>snd<span class=\"w\">       <\/span>tty28<span class=\"w\">   <\/span>tty51<span class=\"w\">  <\/span>vcs2<span class=\"w\">     <\/span>zero\n<\/code><\/pre><\/div>\n\n<h5>Update Docker Container Command<\/h5>\n<p>I'm lazy.<\/p>\n<p>Although I haven't converted this machine to use <code>docker-compose<\/code>, I use a shell script to do\nthe heavy lifting for me, and get the latest container, update and redeploy it. So, when it came\ntime to update the command and make sure that I pass the Bluetooth device and <code>dbus<\/code> into the\ncontainer, it was pretty simple, just needed to add the arguments to the command...<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># Start HassIO<\/span>\n<span class=\"nb\">echo<\/span><span class=\"w\"> <\/span>Starting<span class=\"w\"> <\/span>Hasio\nsudo<span class=\"w\"> <\/span>docker<span class=\"w\"> <\/span>run<span class=\"w\"> <\/span>-d<span class=\"w\"> <\/span>--name<span class=\"w\"> <\/span>homeassistant<span class=\"w\"> <\/span>--restart<span class=\"o\">=<\/span>unless-stopped<span class=\"w\"> <\/span><span class=\"se\">\\<\/span>\n<span class=\"w\">  <\/span>-v<span class=\"w\"> <\/span>\/home\/joestan\/stanleyassistant\/homeassistant:\/config<span class=\"w\"> <\/span>-v<span class=\"w\"> <\/span>\/etc\/localtime:\/etc\/localtime:ro<span class=\"w\"> <\/span><span class=\"se\">\\<\/span>\n<span class=\"w\">  <\/span>-v<span class=\"w\"> <\/span>\/run\/dbus:\/run\/dbus:ro<span class=\"w\"> <\/span>--privileged<span class=\"w\"> <\/span>--network<span class=\"o\">=<\/span>host<span class=\"w\"> <\/span>--device<span class=\"w\"> <\/span>\/dev\/btooth<span class=\"w\"> <\/span><span class=\"se\">\\<\/span>\n<span class=\"w\">  <\/span>ghcr.io\/home-assistant\/home-assistant:stable\n<\/code><\/pre><\/div>\n\n<p>Notice on the second line from the bottom, I'm using the following commands:<\/p>\n<ul>\n<li><code>-v \/run\/dbus:\/run\/dbus:ro<\/code>: This maps the <code>dbus<\/code> volume into the container appropriately.<\/li>\n<li><code>--device \/dev\/btooth<\/code>: For good measure, this maps the Bluetooth device into the container.<\/li>\n<\/ul>\n<p>Those were really the only additions that were needed. Then it was just a matter of restarting\nthe container with the new arguments, and adding the config of the device in the HA Web-UI.<\/p>\n<h2>Closing Thoughts<\/h2>\n<p>Well, I think I learned a few things with this experience. Nothing terrible, or drastic. Just\nsome good points and things to remember. Kind of a nice accomplishment on a Christmas Eve, if you\nask me!<\/p>\n<p>I love the tight integrations that can be offered with Home Assistant, and I'm really pleased with\nmy Airthings Wave Plus sensor. I love the fact that I can use it in my local system and start\nmonitoring for \"bad gas.\"<\/p>\n<p>Let me know if you have any thoughts or other experiences!<\/p>","category":[{"@attributes":{"term":"Home-Improvement"}},{"@attributes":{"term":"homeassistant"}},{"@attributes":{"term":"home-assistant"}},{"@attributes":{"term":"hasio"}},{"@attributes":{"term":"airthings"}},{"@attributes":{"term":"ble"}},{"@attributes":{"term":"bluetooth"}},{"@attributes":{"term":"wireless"}},{"@attributes":{"term":"radon"}},{"@attributes":{"term":"air"}},{"@attributes":{"term":"quality"}}]},{"title":"Installing VBAN on Linux Systems with Pipewire","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/installing-vban-on-linux-with-pipewire.html","rel":"alternate"}},"published":"2022-12-18T10:00:00-08:00","updated":"2022-12-18T10:00:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-12-18:\/installing-vban-on-linux-with-pipewire.html","summary":"<p>I recently decided to drop Windows (finally), and move to Linux, full-time. But that means moving to Pipewire; which is both exciting and slightly daugnting. You see, to make the move, I need to get VBAN working on Linux, and talking to Pipewire. Hmm. Time to do some digging...<\/p>","content":"<h3>Reason for being here.<\/h3>\n<p>That's right, I'm finally moving away from Windows as my daily driver at home. Making the move\nto Kubuntu. I'm much happier with the change, but I do need to fix a reasonable set of things\nand get VBAN working to make sure that my audio is still uninterrupted!<\/p>\n<p>What's <code>VBAN<\/code>, you ask? Well, go check out my other articles:\n* <a href=\"https:\/\/blog.stanleysolutionsnw.com\/networked-audio-using-vban-and-rpi.html\">networked-audio-using-vban-and-rpi<\/a>\n* <a href=\"https:\/\/blog.stanleysolutionsnw.com\/spam-the-vban-for-non-stop-audio.html\">spam-the-vban-for-non-stop-audio<\/a>\n* <a href=\"https:\/\/blog.stanleysolutionsnw.com\/a-better-way-to-integrate-with-voicemeeter.html\">a-better-way-to-integrate-with-voicemeeter<\/a><\/p>\n<p>In a nutshell, however, I use VBAN as a low-latency audio networking solution for my home.\nIt's really helped to bring whole-home audio together quite nicely. Analog can still be \"easier\"\nin some regards, but it's more time and effor to run those wires, sometimes.<\/p>\n<h3>Installing VBAN<\/h3>\n<p>This hasn't really changed all that much since my article about\n<a href=\"https:\/\/blog.stanleysolutionsnw.com\/networked-audio-using-vban-and-rpi.html\">installing VBAN on the Raspberry Pi<\/a>. It starts with installing the basics.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>sudo<span class=\"w\"> <\/span>apt<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>libasound-dev<span class=\"w\"> <\/span>autoconf<span class=\"w\"> <\/span>automake\n<\/code><\/pre><\/div>\n\n<p>Cloning the repository...<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>git<span class=\"w\"> <\/span>clone<span class=\"w\"> <\/span>https:\/\/github.com\/quiniouben\/vban.git\n<\/code><\/pre><\/div>\n\n<p>Running the autogen script...<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>.\/autogen.sh\n<\/code><\/pre><\/div>\n\n<p>Setting up for only alsa...<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>.\/configure<span class=\"w\"> <\/span>--enable-alsa<span class=\"w\"> <\/span>--disable-pulseaudio<span class=\"w\"> <\/span>--disable-jack\n<\/code><\/pre><\/div>\n\n<p>Then install with <code>make<\/code> like the <a href=\"https:\/\/github.com\/quiniouben\/vban\">docs<\/a> show.<\/p>\n<hr>\n<p>Wanna just run one script? Yeah, me too...<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"ch\">#!\/usr\/bin\/bash<\/span>\nsudo<span class=\"w\"> <\/span>apt-get<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>libasound2-dev<span class=\"w\"> <\/span>autoconf<span class=\"w\"> <\/span>automake<span class=\"w\"> <\/span>-y\n\n<span class=\"nb\">cd<\/span><span class=\"w\"> <\/span>\/home\/<span class=\"k\">$(<\/span>whoami<span class=\"k\">)<\/span>\/Downloads\n\ngit<span class=\"w\"> <\/span>clone<span class=\"w\"> <\/span>https:\/\/github.com\/quiniouben\/vban.git\n\n<span class=\"nb\">cd<\/span><span class=\"w\"> <\/span>vban\n\n.\/autogen.sh\n\n.\/configure<span class=\"w\"> <\/span>--enable-alsa<span class=\"w\"> <\/span>--disable-pulseaudio<span class=\"w\"> <\/span>--disable-jack\n\nmake\n\n<span class=\"c1\"># I needed sudo, you might not? Too lazy to bother checking...<\/span>\nsudo<span class=\"w\"> <\/span>make<span class=\"w\"> <\/span>install\n<\/code><\/pre><\/div>\n\n<h2>Getting Alsa Working?<\/h2>\n<p>Well... that's the next step. I seem to be having \"fun\" with that, again. I'll have to\ndig around some more. I'm leaving this post incomplete, because I want to report back when I get\nthe darned thing working.<\/p>\n<blockquote>\n<p>... more soon ...<\/p>\n<\/blockquote>","category":[{"@attributes":{"term":"Audio"}},{"@attributes":{"term":"vban"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"networking"}},{"@attributes":{"term":"linux"}},{"@attributes":{"term":"pipewire"}},{"@attributes":{"term":"alsa"}},{"@attributes":{"term":"sound"}}]},{"title":"Reverse Proxying to two Git Servers","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/reverse-proxying-to-two-git-servers.html","rel":"alternate"}},"published":"2022-11-21T13:54:00-08:00","updated":"2022-11-21T13:54:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-11-21:\/reverse-proxying-to-two-git-servers.html","summary":"<p>I'm quite the self-hosting fiend. That's well-established, at this point, but I wanted to go into some of the details about my recent adventures, exposing SSH service to both my GitLab and Gitea servers.<\/p>","content":"<p>I host both a GitLab and Gitea server at home. I know, it's a bit wild that I've got two different Git servers at home, but it works to my advantage.\nYou see, I use GitLab for <em>most<\/em> of my development needs, and for the \"center\" of my infrastructure-as-code management. However, Gitea makes the introduction\nto Git and development easier for some 4-H youth activities, so it's advantageous to keep around.<\/p>\n<p>With these two servers running on separate machines, I need to have HTTPS and SSH tunneled through to both machines. With NGINX, that's not to terribly difficult for\nthe HTTPS services. NGINX does the HTTP reverse proxying, and Certbot comes in clutch to make the certificate stuff work nicely.<\/p>\n<blockquote>\n<p>Woah! Woah... Woah... What's a \"reverse proxy?\"<\/p>\n<\/blockquote>\n<p>Glad you asked!<\/p>\n<p>A reverse proxy is a way of <em>proxying<\/em> web requests through a single machine to multiple services on a LAN (Local Area Network). That is, if you're hosting lots of web-services\n(like I am) behind a router, you can open the specific web ports (80 for HTTP, and 443 for HTTPS) such that internet traffic can access those ports on your proxy. From there, the proxy can determine where the trafic is destined (i.e., which server it needs to go to) and make the appropriate requests, funnelling all responses back to the user.<\/p>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkRFU0NSSVBUSU9OIiBoZWlnaHQ9IjU5OC45NTgzcHgiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiIHN0eWxlPSJ3aWR0aDoyODVweDtoZWlnaHQ6NTk4cHg7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyODUgNTk4IiB3aWR0aD0iMjg1LjQxNjdweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PD9wbGFudHVtbCAxLjIwMjYuM2JldGE2Pz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImcxbnNlbGdxZGpycTNwMCIgeDE9IjUwJSIgeDI9IjUwJSIgeTE9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI0QzRjE5OCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI0I1RTg1MyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxnPjwhLS1lbnRpdHkgVXNlci0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IlVzZXIiIGRhdGEtc291cmNlLWxpbmU9IjMiIGlkPSJlbnQwMDAyIj48ZWxsaXBzZSBjeD0iNDYuODY0NiIgY3k9IjMzLjMzMzMiIGZpbGw9InVybCgjZzFuc2VsZ3FkanJxM3AwKSIgcng9IjE2LjY2NjciIHJ5PSIxNi42NjY3IiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48cGF0aCBkPSJNNDYuODY0Niw1NC4xNjY3IEM1MS4wMzEzLDU0LjE2NjcgNTQuMTU2Myw1NC4xNjY3IDU4LjMyMjksNTAgQzY2LjY1NjMsNTAgNzQuOTg5Niw1OC4zMzMzIDc0Ljk4OTYsNjYuNjY2NyBMNzQuOTg5Niw3MC44MzMzIEM3NC45ODk2LDc1IDcwLjgyMjksNzkuMTY2NyA2Ni42NTYzLDc5LjE2NjcgTDI3LjA3MjksNzkuMTY2NyBDMjIuOTA2Myw3OS4xNjY3IDE4LjczOTYsNzUgMTguNzM5Niw3MC44MzMzIEwxOC43Mzk2LDY2LjY2NjcgQzE4LjczOTYsNTguMzMzMyAyNy4wNzI5LDUwIDM1LjQwNjMsNTAgQzM5LjU3MjksNTQuMTY2NyA0Mi42OTc5LDU0LjE2NjcgNDYuODY0Niw1NC4xNjY3IiBmaWxsPSJ1cmwoI2cxbnNlbGdxZGpycTNwMCkiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjx0ZXh0IGZpbGw9IiNFQUVBRUEiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyOC40OTEyIiB4PSIzMi42MTkiIHk9Ijk3LjAxOTQiPlVzZXI8L3RleHQ+PC9nPjwhLS1lbnRpdHkgV0FOLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iV0FOIiBkYXRhLXNvdXJjZS1saW5lPSI0IiBpZD0iZW50MDAwMyI+PHBhdGggZD0iTTExNS41MTY2LDE3Ny43Njk1IEMxMTYuNzYxMiwxNzEuMjY0MSAxMjIuODc1NSwxNjguNzk3IDEyOC4xMjU4LDE3My4wOTM1IEMxMzIuMDk5NSwxNjUuNjg1NiAxMzcuMDk4LDE2Mi43NjM0IDE0My43NzksMTcwLjI0OCBDMTQ5LjI3ODEsMTY1LjM1NTIgMTU0LjgwNDMsMTY3LjEwMzkgMTU3LjQwNzgsMTczLjY4NjkgQzE2My4xNDAxLDE2OC42Mjc5IDE2OC40ODQ2LDE3MC41MDI4IDE2OS44NTUyLDE3OC4wMDc5IEMxNzkuODMxNSwxODIuMDg2MyAxODEuNzk0MiwxODkuNzEzNSAxNzQuMjY1NSwxOTcuNzEwMiBDMTgxLjIzMDMsMjA1LjY5NDIgMTgwLjc0ODEsMjEzLjc2NDggMTY5LjM3MTksMjE3LjEyNjMgQzE2OC4zOTE0LDIyNC45MTY3IDE2MS44MTkyLDIyNS43NzU2IDE1Ni44NjAzLDIyMS4xMjgyIEMxNTQuMjQzLDIyOC40MTExIDE0OC4yOTY3LDIzMS43MzQ1IDE0MS43ODc3LDIyNS40NTI0IEMxMzUuNDcyLDIzMS4wOTUxIDEzMC43MjE1LDIyOS4yMTg0IDEyOC44NTUxLDIyMS4yMzA4IEMxMjEuNzgyMSwyMjYuNTU5IDExNy41NjgzLDIyNS4zNTkzIDExNS42NDY3LDIxNi40NTEzIEMxMDQuMTk2OSwyMTUuMzA0MyA5OS43NDA5LDIwNi41OTg4IDEwNy40OTU4LDE5Ny4yODQ3IEMxMDEuNDI3NCwxODcuNzI5NCAxMDQuMjY5MywxODAuMjA4IDExNS41MTY2LDE3Ny43Njk1IiBmaWxsPSJub25lIiBzdHlsZT0ic3Ryb2tlOiNFQUVBRUE7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48dGV4dCBmaWxsPSIjRUFFQUVBIiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzAuMjYxMiIgeD0iMTI2LjUyMDgiIHk9IjIwMC4xMDI4Ij5XQU48L3RleHQ+PC9nPjwhLS1lbnRpdHkgcm91dGVyLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0icm91dGVyIiBkYXRhLXNvdXJjZS1saW5lPSI1IiBpZD0iZW50MDAwNCI+PHJlY3QgZmlsbD0idXJsKCNnMW5zZWxncWRqcnEzcDApIiBoZWlnaHQ9IjU2LjIxNzQiIHJ4PSI0LjE2NjciIHJ5PSI0LjE2NjciIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9Ijk0LjA2OTQiIHg9Ijk0LjYyNSIgeT0iMjg2LjM4NTQiLz48cmVjdCBmaWxsPSJ1cmwoI2cxbnNlbGdxZGpycTNwMCkiIGhlaWdodD0iMTAuNDE2NyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iMTUuNjI1IiB4PSIxNjcuODYxMSIgeT0iMjkxLjU5MzgiLz48cmVjdCBmaWxsPSJ1cmwoI2cxbnNlbGdxZGpycTNwMCkiIGhlaWdodD0iMi4wODMzIiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI0LjE2NjciIHg9IjE2NS43Nzc4IiB5PSIyOTMuNjc3MSIvPjxyZWN0IGZpbGw9InVybCgjZzFuc2VsZ3FkanJxM3AwKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iMTY1Ljc3NzgiIHk9IjI5Ny44NDM4Ii8+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQxLjk4NjEiIHg9IjExNS40NTgzIiB5PSIzMjQuMDI5OSI+Um91dGVyPC90ZXh0PjwvZz48IS0tZW50aXR5IHByb3h5LS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0icHJveHkiIGRhdGEtc291cmNlLWxpbmU9IjYiIGlkPSJlbnQwMDA1Ij48cmVjdCBmaWxsPSJ1cmwoI2cxbnNlbGdxZGpycTNwMCkiIGhlaWdodD0iNTYuMjE3NCIgcng9IjQuMTY2NyIgcnk9IjQuMTY2NyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iODcuMjAzIiB4PSI5OC4wNTIxIiB5PSI0MDUuMTA0MiIvPjxyZWN0IGZpbGw9InVybCgjZzFuc2VsZ3FkanJxM3AwKSIgaGVpZ2h0PSIxMC40MTY3IiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSIxNS42MjUiIHg9IjE2NC40MjE3IiB5PSI0MTAuMzEyNSIvPjxyZWN0IGZpbGw9InVybCgjZzFuc2VsZ3FkanJxM3AwKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iMTYyLjMzODQiIHk9IjQxMi4zOTU4Ii8+PHJlY3QgZmlsbD0idXJsKCNnMW5zZWxncWRqcnEzcDApIiBoZWlnaHQ9IjIuMDgzMyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iNC4xNjY3IiB4PSIxNjIuMzM4NCIgeT0iNDE2LjU2MjUiLz48dGV4dCBmaWxsPSIjMTUxNTE1IiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzUuMTE5NiIgeD0iMTE4Ljg4NTQiIHk9IjQ0Mi43NDg2Ij5Qcm94eTwvdGV4dD48L2c+PCEtLWVudGl0eSBzdmMxLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0ic3ZjMSIgZGF0YS1zb3VyY2UtbGluZT0iNyIgaWQ9ImVudDAwMDYiPjxyZWN0IGZpbGw9InVybCgjZzFuc2VsZ3FkanJxM3AwKSIgaGVpZ2h0PSI1Ni4yMTc0IiByeD0iNC4xNjY3IiByeT0iNC4xNjY3IiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSIxMDYuMjMzNyIgeD0iMTcuNzA4MyIgeT0iNTIzLjgyMjkiLz48cmVjdCBmaWxsPSJ1cmwoI2cxbnNlbGdxZGpycTNwMCkiIGhlaWdodD0iMTAuNDE2NyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iMTUuNjI1IiB4PSIxMDMuMTA4NyIgeT0iNTI5LjAzMTMiLz48cmVjdCBmaWxsPSJ1cmwoI2cxbnNlbGdxZGpycTNwMCkiIGhlaWdodD0iMi4wODMzIiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI0LjE2NjciIHg9IjEwMS4wMjU0IiB5PSI1MzEuMTE0NiIvPjxyZWN0IGZpbGw9InVybCgjZzFuc2VsZ3FkanJxM3AwKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iMTAxLjAyNTQiIHk9IjUzNS4yODEzIi8+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjU0LjE1MDQiIHg9IjM4LjU0MTciIHk9IjU2MS40Njc0Ij5TZXJ2aWNlMTwvdGV4dD48L2c+PCEtLWVudGl0eSBzdmMyLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0ic3ZjMiIgZGF0YS1zb3VyY2UtbGluZT0iOCIgaWQ9ImVudDAwMDciPjxyZWN0IGZpbGw9InVybCgjZzFuc2VsZ3FkanJxM3AwKSIgaGVpZ2h0PSI1Ni4yMTc0IiByeD0iNC4xNjY3IiByeT0iNC4xNjY3IiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSIxMDYuMjMzNyIgeD0iMTYwLjQxNjciIHk9IjUyMy44MjI5Ii8+PHJlY3QgZmlsbD0idXJsKCNnMW5zZWxncWRqcnEzcDApIiBoZWlnaHQ9IjEwLjQxNjciIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjE1LjYyNSIgeD0iMjQ1LjgxNzEiIHk9IjUyOS4wMzEzIi8+PHJlY3QgZmlsbD0idXJsKCNnMW5zZWxncWRqcnEzcDApIiBoZWlnaHQ9IjIuMDgzMyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iNC4xNjY3IiB4PSIyNDMuNzMzNyIgeT0iNTMxLjExNDYiLz48cmVjdCBmaWxsPSJ1cmwoI2cxbnNlbGdxZGpycTNwMCkiIGhlaWdodD0iMi4wODMzIiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI0LjE2NjciIHg9IjI0My43MzM3IiB5PSI1MzUuMjgxMyIvPjx0ZXh0IGZpbGw9IiMxNTE1MTUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI1NC4xNTA0IiB4PSIxODEuMjUiIHk9IjU2MS40Njc0Ij5TZXJ2aWNlMjwvdGV4dD48L2c+PCEtLWVudGl0eSBHaXRVc2VyLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iR2l0VXNlciIgZGF0YS1zb3VyY2UtbGluZT0iMTAiIGlkPSJlbnQwMDA4Ij48ZWxsaXBzZSBjeD0iMTQxLjY1NjMiIGN5PSIzMy4zMzMzIiBmaWxsPSJ1cmwoI2cxbnNlbGdxZGpycTNwMCkiIHJ4PSIxNi42NjY3IiByeT0iMTYuNjY2NyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PHBhdGggZD0iTTE0MS42NTYzLDU0LjE2NjcgQzE0NS44MjI5LDU0LjE2NjcgMTQ4Ljk0NzksNTQuMTY2NyAxNTMuMTE0Niw1MCBDMTYxLjQ0NzksNTAgMTY5Ljc4MTMsNTguMzMzMyAxNjkuNzgxMyw2Ni42NjY3IEwxNjkuNzgxMyw3MC44MzMzIEMxNjkuNzgxMyw3NSAxNjUuNjE0Niw3OS4xNjY3IDE2MS40NDc5LDc5LjE2NjcgTDEyMS44NjQ2LDc5LjE2NjcgQzExNy42OTc5LDc5LjE2NjcgMTEzLjUzMTMsNzUgMTEzLjUzMTMsNzAuODMzMyBMMTEzLjUzMTMsNjYuNjY2NyBDMTEzLjUzMTMsNTguMzMzMyAxMjEuODY0Niw1MCAxMzAuMTk3OSw1MCBDMTM0LjM2NDYsNTQuMTY2NyAxMzcuNDg5Niw1NC4xNjY3IDE0MS42NTYzLDU0LjE2NjciIGZpbGw9InVybCgjZzFuc2VsZ3FkanJxM3AwKSIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PHRleHQgZmlsbD0iI0VBRUFFQSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQ2LjU1MTUiIHg9IjExOC4zODA1IiB5PSI5Ny4wMTk0Ij5HaXRVc2VyPC90ZXh0PjwvZz48IS0tbGluayBHaXRVc2VyIHRvIFdBTi0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDA4IiBkYXRhLWVudGl0eS0yPSJlbnQwMDAzIiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iMTAiIGlkPSJsbms5Ij48cGF0aCBkPSJNMTQxLjY1NjMsMTA1LjQ1ODMgQzE0MS42NTYzLDEyNS45MjcxIDE0MS42NTYzLDE0My4zODU0IDE0MS42NTYzLDE2MS4yMjkyIiBmaWxsPSJub25lIiBpZD0iR2l0VXNlci10by1XQU4iIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSIxNDEuNjU2MywxNjcuNDc5MiwxNDUuODIyOSwxNTguMTA0MiwxNDEuNjU2MywxNjIuMjcwOCwxMzcuNDg5NiwxNTguMTA0MiwxNDEuNjU2MywxNjcuNDc5MiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgV0FOIHRvIHJvdXRlci0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDAzIiBkYXRhLWVudGl0eS0yPSJlbnQwMDA0IiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iMTEiIGlkPSJsbmsxMCI+PHBhdGggZD0iTTE0MS42NTYzLDIyNC4xNjY3IEMxNDEuNjU2MywyNDIuNzUgMTQxLjY1NjMsMjYxIDE0MS42NTYzLDI3OS42MjUiIGZpbGw9Im5vbmUiIGlkPSJXQU4tdG8tcm91dGVyIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iMTQxLjY1NjMsMjg1Ljg3NSwxNDUuODIyOSwyNzYuNSwxNDEuNjU2MywyODAuNjY2NywxMzcuNDg5NiwyNzYuNSwxNDEuNjU2MywyODUuODc1IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayByb3V0ZXIgdG8gcHJveHktLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwNCIgZGF0YS1lbnRpdHktMj0iZW50MDAwNSIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjEyIiBpZD0ibG5rMTEiPjxwYXRoIGQ9Ik0xNDEuNjU2MywzNDIuODg1NCBDMTQxLjY1NjMsMzYxLjQ2ODggMTQxLjY1NjMsMzc5LjcwODMgMTQxLjY1NjMsMzk4LjM0MzgiIGZpbGw9Im5vbmUiIGlkPSJyb3V0ZXItdG8tcHJveHkiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSIxNDEuNjU2Myw0MDQuNTkzOCwxNDUuODIyOSwzOTUuMjE4OCwxNDEuNjU2MywzOTkuMzg1NCwxMzcuNDg5NiwzOTUuMjE4OCwxNDEuNjU2Myw0MDQuNTkzOCIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgcHJveHkgdG8gc3ZjMS0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDA1IiBkYXRhLWVudGl0eS0yPSJlbnQwMDA2IiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iMTMiIGlkPSJsbmsxMiI+PHBhdGggZD0iTTEyNS4wNTIxLDQ2MS42MDQyIEMxMTMuNzYwNCw0ODAuMTg3NSAxMDIuMTI4Niw0OTkuMzM0NCA5MC44MTYxLDUxNy45Njk4IiBmaWxsPSJub25lIiBpZD0icHJveHktdG8tc3ZjMSIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9Ijg3LjU3MjksNTIzLjMxMjUsOTUuOTk5NSw1MTcuNDYwNyw5MC4yNzU2LDUxOC44NjAzLDg4Ljg3Niw1MTMuMTM2NCw4Ny41NzI5LDUyMy4zMTI1IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBwcm94eSB0byBzdmMyLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDUiIGRhdGEtZW50aXR5LTI9ImVudDAwMDciIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIxNCIgaWQ9ImxuazEzIj48cGF0aCBkPSJNMTU4LjUxMDQsNDYxLjYwNDIgQzE2OS45NTgzLDQ4MC4xODc1IDE4MS43NzQyLDQ5OS4zNTU2IDE5My4yNTMzLDUxNy45OTExIiBmaWxsPSJub25lIiBpZD0icHJveHktdG8tc3ZjMiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9IjE5Ni41MzEzLDUyMy4zMTI1LDE5NS4xNjIsNTEzLjE0NTEsMTkzLjc5OTYsNTE4Ljg3OCwxODguMDY2Nyw1MTcuNTE1NiwxOTYuNTMxMyw1MjMuMzEyNSIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PD9wbGFudHVtbC1zcmMgTE9uRDJpOTAzNFJ0RUtLeUcxVVQwbVlrdDhiT25DTlc4ZW8xM2txdTlKRnprdHRSTDBLTklSeGw0VHVYcm5tT1F0OEQyd0o2bHBpSTJhV2tId2RvdDNCSW8yYTYxYUx1Si1rVlFzRl9taVZmMGxPTS15cG9INGVlNmk1VTlLd3BhY3ZCTzJrTW1KaWtiUU4xYkp6YWxpVzBkN3RVdjVXS0hyTWxlX2NadE8tSU5RalJrb2hfZ2JhMD8+PC9nPjwvc3ZnPg==\" style=\"max-width:35%\" width=\"100%\" class=\"uml\" alt=\"Basics of a Reverse Web Proxy\" title=\"Basics of a Proxy\" \/><\/p>\n<p>There's a number of proxy services available out there... Just to name a few:<\/p>\n<ul>\n<li><a href=\"https:\/\/www.nginx.com\/\">NGINX<\/a> (my default selection)<\/li>\n<li><a href=\"http:\/\/www.haproxy.org\/\">HAProxy<\/a><\/li>\n<li><a href=\"https:\/\/traefik.io\/\">traefik<\/a><\/li>\n<\/ul>\n<p>There's others, too, I'm just most familiar with NGINX, and the others here, to a much lesser extent.<\/p>\n<hr>\n<h2>How's my HTTPS Proxy Configured?<\/h2>\n<p>Well, my specific system looks something like this...<\/p>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkRFU0NSSVBUSU9OIiBoZWlnaHQ9IjgzNy41cHgiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiIHN0eWxlPSJ3aWR0aDoxMTE1cHg7aGVpZ2h0OjgzN3B4OyIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTExNSA4MzciIHdpZHRoPSIxMTE1LjYyNXB4IiB6b29tQW5kUGFuPSJtYWduaWZ5Ij48P3BsYW50dW1sIDEuMjAyNi4zYmV0YTY\/PjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iZ3hxNXZmdmR6Z3R1bDAiIHgxPSI1MCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMxNTE1MTUiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMwMDAwMDAiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iZ3hxNXZmdmR6Z3R1bDEiIHgxPSI1MCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiNEM0YxOTgiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNCNUU4NTMiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48Zz48IS0tY2x1c3RlciBIb21lLU5ldHdvcmstLT48ZyBjbGFzcz0iY2x1c3RlciIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrIiBkYXRhLXNvdXJjZS1saW5lPSI4IiBpZD0iZW50MDAwNSI+PHBvbHlnb24gZmlsbD0iIzE1MTUxNSIgcG9pbnRzPSIyNy4wODMzLDMxNS41NTIxLDM3LjUsMzA1LjEzNTQsMTA4NC4zNzUsMzA1LjEzNTQsMTA4NC4zNzUsNzk2LjcwODMsMTA3My45NTgzLDgwNy4xMjUsMjcuMDgzMyw4MDcuMTI1LDI3LjA4MzMsMzE1LjU1MjEiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjEwNzMuOTU4MyIgeDI9IjEwODQuMzc1IiB5MT0iMzE1LjU1MjEiIHkyPSIzMDUuMTM1NCIvPjxsaW5lIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjI3LjA4MzMiIHgyPSIxMDczLjk1ODMiIHkxPSIzMTUuNTUyMSIgeTI9IjMxNS41NTIxIi8+PGxpbmUgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iMTA3My45NTgzIiB4Mj0iMTA3My45NTgzIiB5MT0iMzE1LjU1MjEiIHkyPSI4MDcuMTI1Ii8+PHRleHQgZmlsbD0iI0VBRUFFQSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEwNS4yNjczIiB4PSI0OTguOTI4OCIgeT0iMzM1LjQ4ODIiPkhvbWUtTmV0d29yazwvdGV4dD48L2c+PCEtLWNsdXN0ZXIgZ2l0ZWEtc3J2LS0+PGcgY2xhc3M9ImNsdXN0ZXIiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkhvbWUtTmV0d29yay5naXRlYS1zcnYiIGRhdGEtc291cmNlLWxpbmU9IjEyIiBpZD0iZW50MDAwOCI+PHBvbHlnb24gZmlsbD0iIzE1MTUxNSIgcG9pbnRzPSIxMzYuNDU4Myw1MTkuNzA4MywxNDYuODc1LDUwOS4yOTE3LDM2My41NDE3LDUwOS4yOTE3LDM2My41NDE3LDc4MC4wNDE3LDM1My4xMjUsNzkwLjQ1ODMsMTM2LjQ1ODMsNzkwLjQ1ODMsMTM2LjQ1ODMsNTE5LjcwODMiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjM1My4xMjUiIHgyPSIzNjMuNTQxNyIgeTE9IjUxOS43MDgzIiB5Mj0iNTA5LjI5MTciLz48bGluZSBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHgxPSIxMzYuNDU4MyIgeDI9IjM1My4xMjUiIHkxPSI1MTkuNzA4MyIgeTI9IjUxOS43MDgzIi8+PGxpbmUgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iMzUzLjEyNSIgeDI9IjM1My4xMjUiIHkxPSI1MTkuNzA4MyIgeTI9Ijc5MC40NTgzIi8+PHRleHQgZmlsbD0iI0VBRUFFQSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjM3LjQzMjkiIHg9IjIyNy4xMTY5IiB5PSI1MzkuNjQ0NCI+R2l0ZWE8L3RleHQ+PC9nPjwhLS1jbHVzdGVyIGdpdGxhYi1zcnYtLT48ZyBjbGFzcz0iY2x1c3RlciIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLmdpdGxhYi1zcnYiIGRhdGEtc291cmNlLWxpbmU9IjE4IiBpZD0iZW50MDAxMyI+PHBvbHlnb24gZmlsbD0iIzE1MTUxNSIgcG9pbnRzPSI0NzEuODc1LDUxOS43MDgzLDQ4Mi4yOTE3LDUwOS4yOTE3LDcwNC4xNjY3LDUwOS4yOTE3LDcwNC4xNjY3LDc4MC4wNDE3LDY5My43NSw3OTAuNDU4Myw0NzEuODc1LDc5MC40NTgzLDQ3MS44NzUsNTE5LjcwODMiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjY5My43NSIgeDI9IjcwNC4xNjY3IiB5MT0iNTE5LjcwODMiIHkyPSI1MDkuMjkxNyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjQ3MS44NzUiIHgyPSI2OTMuNzUiIHkxPSI1MTkuNzA4MyIgeTI9IjUxOS43MDgzIi8+PGxpbmUgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iNjkzLjc1IiB4Mj0iNjkzLjc1IiB5MT0iNTE5LjcwODMiIHkyPSI3OTAuNDU4MyIvPjx0ZXh0IGZpbGw9IiNFQUVBRUEiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI0NS44Njc5IiB4PSI1NjAuOTIwMiIgeT0iNTM5LjY0NDQiPkdpdExhYjwvdGV4dD48L2c+PCEtLWNsdXN0ZXIgcHJveHktLT48ZyBjbGFzcz0iY2x1c3RlciIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLnByb3h5IiBkYXRhLXNvdXJjZS1saW5lPSIyNCIgaWQ9ImVudDAwMTgiPjxwb2x5Z29uIGZpbGw9IiMxNTE1MTUiIHBvaW50cz0iODA2LjI1LDQxMi45NDc5LDgxNi42NjY3LDQwMi41MzEzLDEwNjcuNzA4Myw0MDIuNTMxMywxMDY3LjcwODMsNTQzLjY0NTgsMTA1Ny4yOTE3LDU1NC4wNjI1LDgwNi4yNSw1NTQuMDYyNSw4MDYuMjUsNDEyLjk0NzkiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjEwNTcuMjkxNyIgeDI9IjEwNjcuNzA4MyIgeTE9IjQxMi45NDc5IiB5Mj0iNDAyLjUzMTMiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHgxPSI4MDYuMjUiIHgyPSIxMDU3LjI5MTciIHkxPSI0MTIuOTQ3OSIgeTI9IjQxMi45NDc5Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iMTA1Ny4yOTE3IiB4Mj0iMTA1Ny4yOTE3IiB5MT0iNDEyLjk0NzkiIHkyPSI1NTQuMDYyNSIvPjx0ZXh0IGZpbGw9IiNFQUVBRUEiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxMDIuMTI0IiB4PSI4ODEuNzUwNSIgeT0iNDMyLjg4NCI+UmV2ZXJzZS1Qcm94eTwvdGV4dD48L2c+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLlJvdXRlcjgwIiBkYXRhLXNvdXJjZS1saW5lPSI5IiBpZD0iZW50MDAwNiI+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjU3Ljg5MTgiIHg9Ijg0NS4wMTI0IiB5PSIyNzguMjI5MSI+Um91dGVyODA8L3RleHQ+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIxMi41IiBzdHlsZT0ic3Ryb2tlOiM2RDhCMzI7c3Ryb2tlLXdpZHRoOjEuNTYyNTsiIHdpZHRoPSIxMi41IiB4PSI4NjcuNzA4MyIgeT0iMjk4Ljg4NTQiLz48L2c+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLlJvdXRlcjQ0MyIgZGF0YS1zb3VyY2UtbGluZT0iMTAiIGlkPSJlbnQwMDA3Ij48dGV4dCBmaWxsPSIjMTUxNTE1IiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNjUuODQ0NyIgeD0iNzQ5LjM2OTMiIHk9IjI3OC4yMjkxIj5Sb3V0ZXI0NDM8L3RleHQ+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIxMi41IiBzdHlsZT0ic3Ryb2tlOiM2RDhCMzI7c3Ryb2tlLXdpZHRoOjEuNTYyNTsiIHdpZHRoPSIxMi41IiB4PSI3NzYuMDQxNyIgeT0iMjk4Ljg4NTQiLz48L2c+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLmdpdGVhLXNydi5nZTgwIiBkYXRhLXNvdXJjZS1saW5lPSIxMyIgaWQ9ImVudDAwMDkiPjx0ZXh0IGZpbGw9IiMxNTE1MTUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxNS45MDU4IiB4PSIxNDcuMjU1NSIgeT0iNDgyLjM4NTMiPjgwPC90ZXh0PjxyZWN0IGZpbGw9Im5vbmUiIGhlaWdodD0iMTIuNSIgc3R5bGU9InN0cm9rZTojNkQ4QjMyO3N0cm9rZS13aWR0aDoxLjU2MjU7IiB3aWR0aD0iMTIuNSIgeD0iMTQ4Ljk1ODMiIHk9IjUwMy4wNDE3Ii8+PC9nPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkhvbWUtTmV0d29yay5naXRlYS1zcnYuZ2U0NDMiIGRhdGEtc291cmNlLWxpbmU9IjE0IiBpZD0iZW50MDAxMCI+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjIzLjg1ODYiIHg9IjIxNS4xNTQiIHk9IjQ4Mi4zODUzIj40NDM8L3RleHQ+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIxMi41IiBzdHlsZT0ic3Ryb2tlOiM2RDhCMzI7c3Ryb2tlLXdpZHRoOjEuNTYyNTsiIHdpZHRoPSIxMi41IiB4PSIyMjAuODMzMyIgeT0iNTAzLjA0MTciLz48L2c+PCEtLWVudGl0eSBHaXRlYS0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkhvbWUtTmV0d29yay5naXRlYS1zcnYuR2l0ZWEiIGRhdGEtc291cmNlLWxpbmU9IjE1IiBpZD0iZW50MDAxMSI+PHBhdGggZD0iTTE3OS4xMjUsNzI5LjAzMTMgQzE3OS4xMjUsNzE4LjYxNDYgMjExLjQ1NTMsNzE4LjYxNDYgMjExLjQ1NTMsNzE4LjYxNDYgQzIxMS40NTUzLDcxOC42MTQ2IDI0My43ODU2LDcxOC42MTQ2IDI0My43ODU2LDcyOS4wMzEzIEwyNDMuNzg1Niw3NjMuMzczNyBDMjQzLjc4NTYsNzczLjc5MDQgMjExLjQ1NTMsNzczLjc5MDQgMjExLjQ1NTMsNzczLjc5MDQgQzIxMS40NTUzLDc3My43OTA0IDE3OS4xMjUsNzczLjc5MDQgMTc5LjEyNSw3NjMuMzczNyBMMTc5LjEyNSw3MjkuMDMxMyIgZmlsbD0idXJsKCNneHE1dmZ2ZHpndHVsMCkiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjxwYXRoIGQ9Ik0xNzkuMTI1LDcyOS4wMzEzIEMxNzkuMTI1LDczOS40NDc5IDIxMS40NTUzLDczOS40NDc5IDIxMS40NTUzLDczOS40NDc5IEMyMTEuNDU1Myw3MzkuNDQ3OSAyNDMuNzg1Niw3MzkuNDQ3OSAyNDMuNzg1Niw3MjkuMDMxMyIgZmlsbD0ibm9uZSIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PHRleHQgZmlsbD0iI0VBRUFFQSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjMzLjQxMDYiIHg9IjE5NC43NSIgeT0iNzYwLjQyNTciPkdpdGVhPC90ZXh0PjwvZz48IS0tZW50aXR5IGdlbmdpbngtLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsuZ2l0ZWEtc3J2LmdlbmdpbngiIGRhdGEtc291cmNlLWxpbmU9IjE2IiBpZD0iZW50MDAxMiI+PHJlY3QgZmlsbD0idXJsKCNneHE1dmZ2ZHpndHVsMSkiIGhlaWdodD0iNTYuMjE3NCIgcng9IjQuMTY2NyIgcnk9IjQuMTY2NyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iOTIuNzIwNSIgeD0iMjUzLjYzNTQiIHk9IjU5OS44OTU4Ii8+PHJlY3QgZmlsbD0idXJsKCNneHE1dmZ2ZHpndHVsMSkiIGhlaWdodD0iMTAuNDE2NyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iMTUuNjI1IiB4PSIzMjUuNTIyNiIgeT0iNjA1LjEwNDIiLz48cmVjdCBmaWxsPSJ1cmwoI2d4cTV2ZnZkemd0dWwxKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iMzIzLjQzOTMiIHk9IjYwNy4xODc1Ii8+PHJlY3QgZmlsbD0idXJsKCNneHE1dmZ2ZHpndHVsMSkiIGhlaWdodD0iMi4wODMzIiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI0LjE2NjciIHg9IjMyMy40MzkzIiB5PSI2MTEuMzU0MiIvPjx0ZXh0IGZpbGw9IiMxNTE1MTUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI0MC42MzcyIiB4PSIyNzQuNDY4OCIgeT0iNjM3LjU0MDMiPk5HSU5YPC90ZXh0PjwvZz48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsuZ2l0bGFiLXNydi5nbDgwIiBkYXRhLXNvdXJjZS1saW5lPSIxOSIgaWQ9ImVudDAwMTQiPjx0ZXh0IGZpbGw9IiMxNTE1MTUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxNS45MDU4IiB4PSI0ODIuNjcyMSIgeT0iNDgyLjM4NTMiPjgwPC90ZXh0PjxyZWN0IGZpbGw9Im5vbmUiIGhlaWdodD0iMTIuNSIgc3R5bGU9InN0cm9rZTojNkQ4QjMyO3N0cm9rZS13aWR0aDoxLjU2MjU7IiB3aWR0aD0iMTIuNSIgeD0iNDg0LjM3NSIgeT0iNTAzLjA0MTciLz48L2c+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLmdpdGxhYi1zcnYuZ2w0NDMiIGRhdGEtc291cmNlLWxpbmU9IjIwIiBpZD0iZW50MDAxNSI+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjIzLjg1ODYiIHg9IjU1Mi42NTQiIHk9IjQ4Mi4zODUzIj40NDM8L3RleHQ+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIxMi41IiBzdHlsZT0ic3Ryb2tlOiM2RDhCMzI7c3Ryb2tlLXdpZHRoOjEuNTYyNTsiIHdpZHRoPSIxMi41IiB4PSI1NTguMzMzMyIgeT0iNTAzLjA0MTciLz48L2c+PCEtLWVudGl0eSBHaXRMYWItLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsuZ2l0bGFiLXNydi5HaXRMYWIiIGRhdGEtc291cmNlLWxpbmU9IjIxIiBpZD0iZW50MDAxNiI+PHBhdGggZD0iTTUxMy4wMjA4LDcyOS4wMzEzIEM1MTMuMDIwOCw3MTguNjE0NiA1NDguOTU1Myw3MTguNjE0NiA1NDguOTU1Myw3MTguNjE0NiBDNTQ4Ljk1NTMsNzE4LjYxNDYgNTg0Ljg4OTcsNzE4LjYxNDYgNTg0Ljg4OTcsNzI5LjAzMTMgTDU4NC44ODk3LDc2My4zNzM3IEM1ODQuODg5Nyw3NzMuNzkwNCA1NDguOTU1Myw3NzMuNzkwNCA1NDguOTU1Myw3NzMuNzkwNCBDNTQ4Ljk1NTMsNzczLjc5MDQgNTEzLjAyMDgsNzczLjc5MDQgNTEzLjAyMDgsNzYzLjM3MzcgTDUxMy4wMjA4LDcyOS4wMzEzIiBmaWxsPSJ1cmwoI2d4cTV2ZnZkemd0dWwwKSIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PHBhdGggZD0iTTUxMy4wMjA4LDcyOS4wMzEzIEM1MTMuMDIwOCw3MzkuNDQ3OSA1NDguOTU1Myw3MzkuNDQ3OSA1NDguOTU1Myw3MzkuNDQ3OSBDNTQ4Ljk1NTMsNzM5LjQ0NzkgNTg0Ljg4OTcsNzM5LjQ0NzkgNTg0Ljg4OTcsNzI5LjAzMTMiIGZpbGw9Im5vbmUiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjx0ZXh0IGZpbGw9IiNFQUVBRUEiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI0MC42MTg5IiB4PSI1MjguNjQ1OCIgeT0iNzYwLjQyNTciPkdpdExhYjwvdGV4dD48L2c+PCEtLWVudGl0eSBnbG5naW54LS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLmdpdGxhYi1zcnYuZ2xuZ2lueCIgZGF0YS1zb3VyY2UtbGluZT0iMjIiIGlkPSJlbnQwMDE3Ij48cmVjdCBmaWxsPSJ1cmwoI2d4cTV2ZnZkemd0dWwxKSIgaGVpZ2h0PSI1Ni4yMTc0IiByeD0iNC4xNjY3IiByeT0iNC4xNjY3IiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI5Mi43MjA1IiB4PSI1OTQuMjYwNCIgeT0iNTk5Ljg5NTgiLz48cmVjdCBmaWxsPSJ1cmwoI2d4cTV2ZnZkemd0dWwxKSIgaGVpZ2h0PSIxMC40MTY3IiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSIxNS42MjUiIHg9IjY2Ni4xNDc2IiB5PSI2MDUuMTA0MiIvPjxyZWN0IGZpbGw9InVybCgjZ3hxNXZmdmR6Z3R1bDEpIiBoZWlnaHQ9IjIuMDgzMyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iNC4xNjY3IiB4PSI2NjQuMDY0MyIgeT0iNjA3LjE4NzUiLz48cmVjdCBmaWxsPSJ1cmwoI2d4cTV2ZnZkemd0dWwxKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iNjY0LjA2NDMiIHk9IjYxMS4zNTQyIi8+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQwLjYzNzIiIHg9IjYxNS4wOTM4IiB5PSI2MzcuNTQwMyI+TkdJTlg8L3RleHQ+PC9nPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkhvbWUtTmV0d29yay5wcm94eS5weDgwIiBkYXRhLXNvdXJjZS1saW5lPSIyNSIgaWQ9ImVudDAwMTkiPjx0ZXh0IGZpbGw9IiMxNTE1MTUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxNS45MDU4IiB4PSI4NjYuMDA1NSIgeT0iMzc1LjYyNDkiPjgwPC90ZXh0PjxyZWN0IGZpbGw9Im5vbmUiIGhlaWdodD0iMTIuNSIgc3R5bGU9InN0cm9rZTojNkQ4QjMyO3N0cm9rZS13aWR0aDoxLjU2MjU7IiB3aWR0aD0iMTIuNSIgeD0iODY3LjcwODMiIHk9IjM5Ni4yODEzIi8+PC9nPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkhvbWUtTmV0d29yay5wcm94eS5weDQ0MyIgZGF0YS1zb3VyY2UtbGluZT0iMjYiIGlkPSJlbnQwMDIwIj48dGV4dCBmaWxsPSIjMTUxNTE1IiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjMuODU4NiIgeD0iODEzLjA3MDciIHk9IjM3NS42MjQ5Ij40NDM8L3RleHQ+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIxMi41IiBzdHlsZT0ic3Ryb2tlOiM2RDhCMzI7c3Ryb2tlLXdpZHRoOjEuNTYyNTsiIHdpZHRoPSIxMi41IiB4PSI4MTguNzUiIHk9IjM5Ni4yODEzIi8+PC9nPjwhLS1lbnRpdHkgcHJveHluZ2lueC0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkhvbWUtTmV0d29yay5wcm94eS5wcm94eW5naW54IiBkYXRhLXNvdXJjZS1saW5lPSIyNyIgaWQ9ImVudDAwMjEiPjxyZWN0IGZpbGw9InVybCgjZ3hxNXZmdmR6Z3R1bDEpIiBoZWlnaHQ9IjU2LjIxNzQiIHJ4PSI0LjE2NjciIHJ5PSI0LjE2NjciIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjkyLjcyMDUiIHg9Ijk1Ny44MDIxIiB5PSI0ODEuMTc3MSIvPjxyZWN0IGZpbGw9InVybCgjZ3hxNXZmdmR6Z3R1bDEpIiBoZWlnaHQ9IjEwLjQxNjciIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjE1LjYyNSIgeD0iMTAyOS42ODkzIiB5PSI0ODYuMzg1NCIvPjxyZWN0IGZpbGw9InVybCgjZ3hxNXZmdmR6Z3R1bDEpIiBoZWlnaHQ9IjIuMDgzMyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iNC4xNjY3IiB4PSIxMDI3LjYwNiIgeT0iNDg4LjQ2ODgiLz48cmVjdCBmaWxsPSJ1cmwoI2d4cTV2ZnZkemd0dWwxKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iMTAyNy42MDYiIHk9IjQ5Mi42MzU0Ii8+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQwLjYzNzIiIHg9Ijk3OC42MzU0IiB5PSI1MTguODIxNSI+TkdJTlg8L3RleHQ+PC9nPjwhLS1lbnRpdHkgR2l0VXNlci0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkdpdFVzZXIiIGRhdGEtc291cmNlLWxpbmU9IjMiIGlkPSJlbnQwMDAyIj48ZWxsaXBzZSBjeD0iODI4LjEyNSIgY3k9IjMzLjMzMzMiIGZpbGw9InVybCgjZ3hxNXZmdmR6Z3R1bDEpIiByeD0iMTYuNjY2NyIgcnk9IjE2LjY2NjciIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjxwYXRoIGQ9Ik04MjguMTI1LDU0LjE2NjcgQzgzMi4yOTE3LDU0LjE2NjcgODM1LjQxNjcsNTQuMTY2NyA4MzkuNTgzMyw1MCBDODQ3LjkxNjcsNTAgODU2LjI1LDU4LjMzMzMgODU2LjI1LDY2LjY2NjcgTDg1Ni4yNSw3MC44MzMzIEM4NTYuMjUsNzUgODUyLjA4MzMsNzkuMTY2NyA4NDcuOTE2Nyw3OS4xNjY3IEw4MDguMzMzMyw3OS4xNjY3IEM4MDQuMTY2Nyw3OS4xNjY3IDgwMCw3NSA4MDAsNzAuODMzMyBMODAwLDY2LjY2NjcgQzgwMCw1OC4zMzMzIDgwOC4zMzMzLDUwIDgxNi42NjY3LDUwIEM4MjAuODMzMyw1NC4xNjY3IDgyMy45NTgzLDU0LjE2NjcgODI4LjEyNSw1NC4xNjY3IiBmaWxsPSJ1cmwoI2d4cTV2ZnZkemd0dWwxKSIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PHRleHQgZmlsbD0iI0VBRUFFQSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQ2LjU1MTUiIHg9IjgwNC44NDkyIiB5PSI5Ny4wMTk0Ij5HaXRVc2VyPC90ZXh0PjwvZz48IS0tZW50aXR5IFdBTi0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IldBTiIgZGF0YS1zb3VyY2UtbGluZT0iNCIgaWQ9ImVudDAwMDMiPjxwYXRoIGQ9Ik04MDEuOTg1NCwxNzcuNzY5NSBDODAzLjIyOTksMTcxLjI2NDEgODA5LjM0NDMsMTY4Ljc5NyA4MTQuNTk0NiwxNzMuMDkzNSBDODE4LjU2ODMsMTY1LjY4NTYgODIzLjU2NjgsMTYyLjc2MzQgODMwLjI0NzcsMTcwLjI0OCBDODM1Ljc0NjksMTY1LjM1NTIgODQxLjI3MywxNjcuMTAzOSA4NDMuODc2NiwxNzMuNjg2OSBDODQ5LjYwODgsMTY4LjYyNzkgODU0Ljk1MzQsMTcwLjUwMjggODU2LjMyNCwxNzguMDA3OSBDODY2LjMwMDIsMTgyLjA4NjMgODY4LjI2MjksMTg5LjcxMzUgODYwLjczNDIsMTk3LjcxMDIgQzg2Ny42OTkxLDIwNS42OTQyIDg2Ny4yMTY4LDIxMy43NjQ4IDg1NS44NDA3LDIxNy4xMjYzIEM4NTQuODYwMSwyMjQuOTE2NyA4NDguMjg3OSwyMjUuNzc1NiA4NDMuMzI5LDIyMS4xMjgyIEM4NDAuNzExOCwyMjguNDExMSA4MzQuNzY1NCwyMzEuNzM0NSA4MjguMjU2NSwyMjUuNDUyNCBDODIxLjk0MDcsMjMxLjA5NTEgODE3LjE5MDMsMjI5LjIxODQgODE1LjMyMzksMjIxLjIzMDggQzgwOC4yNTA5LDIyNi41NTkgODA0LjAzNzEsMjI1LjM1OTMgODAyLjExNTQsMjE2LjQ1MTMgQzc5MC42NjU2LDIxNS4zMDQzIDc4Ni4yMDk2LDIwNi41OTg4IDc5My45NjQ2LDE5Ny4yODQ3IEM3ODcuODk2MiwxODcuNzI5NCA3OTAuNzM4MSwxODAuMjA4IDgwMS45ODU0LDE3Ny43Njk1IiBmaWxsPSJub25lIiBzdHlsZT0ic3Ryb2tlOiNFQUVBRUE7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48dGV4dCBmaWxsPSIjRUFFQUVBIiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzAuMjYxMiIgeD0iODEyLjk4OTYiIHk9IjIwMC4xMDI4Ij5XQU48L3RleHQ+PC9nPjwhLS1saW5rIEdpdFVzZXIgdG8gV0FOLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDIiIGRhdGEtZW50aXR5LTI9ImVudDAwMDMiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSI2IiBpZD0ibG5rNCI+PHBhdGggZD0iTTgyOC4xMjUsMTA1LjQ1ODMgQzgyOC4xMjUsMTI1LjkyNzEgODI4LjEyNSwxNDMuMzg1NCA4MjguMTI1LDE2MS4yMjkyIiBmaWxsPSJub25lIiBpZD0iR2l0VXNlci10by1XQU4iIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSI4MjguMTI1LDE2Ny40NzkyLDgzMi4yOTE3LDE1OC4xMDQyLDgyOC4xMjUsMTYyLjI3MDgsODIzLjk1ODMsMTU4LjEwNDIsODI4LjEyNSwxNjcuNDc5MiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgV0FOIHRvIFJvdXRlcjgwLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDMiIGRhdGEtZW50aXR5LTI9ImVudDAwMDYiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIzMCIgaWQ9ImxuazIyIj48cGF0aCBkPSJNODM5LjgxMjUsMjI0LjE2NjcgQzg1MC43Mzk2LDI0OS43NSA4NjMuNzMwNCwyODAuMTY5OSA4NjkuMzU1NCwyOTMuMzI2MSIgZmlsbD0ibm9uZSIgaWQ9IldBTi10by1Sb3V0ZXI4MCIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9Ijg3MS44MTI1LDI5OS4wNzI5LDg3MS45NTgxLDI4OC44MTQ3LDg2OS43NjUsMjk0LjI4MzksODY0LjI5NTcsMjkyLjA5MDgsODcxLjgxMjUsMjk5LjA3MjkiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PC9nPjwhLS1saW5rIFdBTiB0byBSb3V0ZXI0NDMtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwMyIgZGF0YS1lbnRpdHktMj0iZW50MDAwNyIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjMxIiBpZD0ibG5rMjMiPjxwYXRoIGQ9Ik04MTYuNDM3NSwyMjQuMTY2NyBDODA1LjUxMDQsMjQ5Ljc1IDc5Mi41MTk2LDI4MC4xNjk5IDc4Ni44OTQ2LDI5My4zMjYxIiBmaWxsPSJub25lIiBpZD0iV0FOLXRvLVJvdXRlcjQ0MyIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9Ijc4NC40Mzc1LDI5OS4wNzI5LDc5MS45NTQzLDI5Mi4wOTA4LDc4Ni40ODUsMjk0LjI4MzksNzg0LjI5MTksMjg4LjgxNDcsNzg0LjQzNzUsMjk5LjA3MjkiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PC9nPjwhLS1saW5rIFJvdXRlcjgwIHRvIHB4ODAtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwNiIgZGF0YS1lbnRpdHktMj0iZW50MDAxOSIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjMyIiBpZD0ibG5rMjQiPjxwYXRoIGQ9Ik04NzMuOTU4MywzMTEuMzAyMSBDODczLjk1ODMsMzI3LjY5NzkgODczLjk1ODMsMzcyLjU0MTcgODczLjk1ODMsMzg5LjYxNDYiIGZpbGw9Im5vbmUiIGlkPSJSb3V0ZXI4MC10by1weDgwIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iODczLjk1ODMsMzk1Ljg2NDYsODc4LjEyNSwzODYuNDg5Niw4NzMuOTU4MywzOTAuNjU2Myw4NjkuNzkxNywzODYuNDg5Niw4NzMuOTU4MywzOTUuODY0NiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgUm91dGVyNDQzIHRvIHB4NDQzLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDciIGRhdGEtZW50aXR5LTI9ImVudDAwMjAiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIzMyIgaWQ9ImxuazI1Ij48cGF0aCBkPSJNNzg0LjQ4OTYsMzExLjA2MjUgQzc4OS43MTg4LDMyMi42NjY3IDgwMy4zMTI1LDM1Mi44MjI5IDgxNC41ODMzLDM3OC4wNTIxIEM4MTcuMzQzOCwzODQuMjM5NiA4MTcuOTY4NiwzODUuNjI5MyA4MjAuMDcyOCwzOTAuMzc5MyIgZmlsbD0ibm9uZSIgaWQ9IlJvdXRlcjQ0My10by1weDQ0MyIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9IjgyMi42MDQyLDM5Ni4wOTM4LDgyMi42MTY3LDM4NS44MzQ1LDgyMC40OTQ3LDM5MS4zMzE3LDgxNC45OTc1LDM4OS4yMDk3LDgyMi42MDQyLDM5Ni4wOTM4IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBweDgwIHRvIHB4NDQzLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMTkiIGRhdGEtZW50aXR5LTI9ImVudDAwMjAiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIzNSIgaWQ9ImxuazI2Ij48cGF0aCBkPSJNODY3LjMyMjksNDAwLjM0MzggQzg1NS4zODU0LDM5NyA4NDkuNDY1MywzOTUuMzMxNiA4MzcuNTE3NCwzOTguNjg1NyIgZmlsbD0ibm9uZSIgaWQ9InB4ODAtdG8tcHg0NDMiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSI4MzEuNSw0MDAuMzc1LDg0MS42NTIzLDQwMS44NTI3LDgzNi41MTQ1LDM5OC45NjczLDgzOS4zOTk5LDM5My44Mjk1LDgzMS41LDQwMC4zNzUiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PC9nPjwhLS1saW5rIHB4NDQzIHRvIHByb3h5bmdpbngtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAyMCIgZGF0YS1lbnRpdHktMj0iZW50MDAyMSIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjM2IiBpZD0ibG5rMjciPjxwYXRoIGQ9Ik04MzEuNjY2Nyw0MDcuODc1IEM4MzYuNDQ3OSw0MTAuOTM3NSA4NDMuMDYyNSw0MTUuMTI1IDg0OC45NTgzLDQxOC42NzcxIEM4ODUuMDUyMSw0NDAuNDU4MyA5MjAuOTgzMyw0NjEuMTkyNCA5NTEuOTMxMiw0NzguNzg2MiIgZmlsbD0ibm9uZSIgaWQ9InB4NDQzLXRvLXByb3h5bmdpbngiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSI5NTcuMzY0Niw0ODEuODc1LDk1MS4yNzM4LDQ3My42MTk1LDk1Mi44MzY4LDQ3OS4zMDEsOTQ3LjE1NTMsNDgwLjg2NCw5NTcuMzY0Niw0ODEuODc1IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tcmV2ZXJzZSBsaW5rIHByb3h5bmdpbnggdG8gZ2w0NDMtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAyMSIgZGF0YS1lbnRpdHktMj0iZW50MDAxNSIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjM4IiBpZD0ibG5rMjkiPjxwYXRoIGQ9Ik05NzIuMDM3Nyw0NzYuNjI1NyBDOTU3LjEwMDIsNDYzLjIwOSA5NDIuMTE0Niw0NTIuODEyNSA5MjEuMzU0Miw0NDUuNzYwNCBDNzg0LjkyNzEsMzk5LjQ2ODggNjA3LjA5MzgsNDg2LjgzMzMgNTcxLjEwNDIsNTA1Ljc3MDgiIGZpbGw9Im5vbmUiIGlkPSJwcm94eW5naW54LWJhY2t0by1nbDQ0MyIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9Ijk3Ni42ODc1LDQ4MC44MDIxLDk3Mi40OTcxLDQ3MS40Mzc3LDk3Mi44MTI3LDQ3Ny4zMjE4LDk2Ni45Mjg2LDQ3Ny42Mzc0LDk3Ni42ODc1LDQ4MC44MDIxIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tcmV2ZXJzZSBsaW5rIGdlNDQzIHRvIHByb3h5bmdpbngtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAxMCIgZGF0YS1lbnRpdHktMj0iZW50MDAyMSIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjM5IiBpZD0ibG5rMzEiPjxwYXRoIGQ9Ik0yMzkuNDIxNiw1MDMuMTU5OCBDMjYwLjI3NTgsNDkyLjkwOTggMzIwLjM1NDIsNDY0Ljk2ODggMzc4LjgyMjksNDU0Ljc5MTcgQzQzOC44NzUsNDQ0LjMzMzMgODY5LjM1NDIsNDM2LjI3MDggOTI3LjQyNzEsNDU0Ljc5MTcgQzk0NC4wODMzLDQ2MC4xMDQyIDk2MC4yMjkyLDQ3MC41IDk3My4zNjQ2LDQ4MC43OTE3IiBmaWxsPSJub25lIiBpZD0iZ2U0NDMtYmFja3RvLXByb3h5bmdpbngiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSIyMzMuODEyNSw1MDUuOTE2NywyNDQuMDY0MSw1MDUuNTIwNywyMzguNDg2Nyw1MDMuNjE5MiwyNDAuMzg4Miw0OTguMDQxOSwyMzMuODEyNSw1MDUuOTE2NyIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgZ2w4MCB0byBnbDQ0My0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDE0IiBkYXRhLWVudGl0eS0yPSJlbnQwMDE1IiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iNDEiIGlkPSJsbmszMiI+PHBhdGggZD0iTTQ5Ny4yNzA4LDUwNy43NjA0IEM1MTcuNTIwOCw1MDMuNTQxNyA1MzEuNjQxOCw1MDIuMjc3NCA1NTEuODkxOCw1MDYuNDk2MSIgZmlsbD0ibm9uZSIgaWQ9ImdsODAtdG8tZ2w0NDMiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSI1NTguMDEwNCw1MDcuNzcwOCw1NDkuNjgyMyw1MDEuNzc5Nyw1NTIuOTExNiw1MDYuNzA4Niw1NDcuOTgyNyw1MDkuOTM3OCw1NTguMDEwNCw1MDcuNzcwOCIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgZ2w4MCB0byBHaXRMYWItLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAxNCIgZGF0YS1lbnRpdHktMj0iZW50MDAxNiIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjQyIiBpZD0ibG5rMzMiPjxwYXRoIGQ9Ik00ODMuOTQ3OSw1MTMuOTU4MyBDNDY2LjcyOTIsNTIzLjc1IDQyMS4wMTA0LDU1Mi42MjUgNDA0LjE2NjcsNTkxLjU2MjUgQzM5Mi43ODEzLDYxNy44OTU4IDM5MC4xNjY3LDYzMS4wNzI5IDQwNC4xNjY3LDY1Ni4xMTQ2IEM0MjcuMTM1NCw2OTcuMTk3OSA0NzEuODEsNzE5LjUzMDggNTA2LjY2NDIsNzMyLjEzNSIgZmlsbD0ibm9uZSIgaWQ9ImdsODAtdG8tR2l0TGFiIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iNTEyLjU0MTcsNzM0LjI2MDQsNTA1LjE0MjQsNzI3LjE1MzksNTA3LjY0MzgsNzMyLjQ4OTIsNTAyLjMwODUsNzM0Ljk5MDYsNTEyLjU0MTcsNzM0LjI2MDQiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PC9nPjwhLS1saW5rIGdsNDQzIHRvIGdsbmdpbngtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAxNSIgZGF0YS1lbnRpdHktMj0iZW50MDAxNyIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjQzIiBpZD0ibG5rMzQiPjxwYXRoIGQ9Ik01NjguMjUsNTE1Ljk1ODMgQzU3Ny45MjcxLDUzMC43OTE3IDYwMS4yMDA0LDU2Ni41MDQ1IDYxOS4zMjU0LDU5NC4yOTYyIiBmaWxsPSJub25lIiBpZD0iZ2w0NDMtdG8tZ2xuZ2lueCIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9IjYyMi43Mzk2LDU5OS41MzEzLDYyMS4xMDg0LDU4OS40MDI1LDYxOS44OTQ0LDU5NS4xNjg3LDYxNC4xMjgzLDU5My45NTQ4LDYyMi43Mzk2LDU5OS41MzEzIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBnbG5naW54IHRvIEdpdExhYi0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDE3IiBkYXRhLWVudGl0eS0yPSJlbnQwMDE2IiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iNDQiIGlkPSJsbmszNSI+PHBhdGggZD0iTTYxOC45MDYzLDY1Ni41NjI1IEM2MDQuMTg3NSw2NzUuMjA4MyA1ODguNjk1OSw2OTQuODIzOCA1NzQuMDgxMyw3MTMuMzM0MiIgZmlsbD0ibm9uZSIgaWQ9ImdsbmdpbngtdG8tR2l0TGFiIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iNTcwLjIwODMsNzE4LjIzOTYsNTc5LjI4OCw3MTMuNDYzNSw1NzMuNDM1OCw3MTQuMTUxOCw1NzIuNzQ3NSw3MDguMjk5NSw1NzAuMjA4Myw3MTguMjM5NiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgZ2U4MCB0byBnZTQ0My0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDA5IiBkYXRhLWVudGl0eS0yPSJlbnQwMDEwIiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iNDYiIGlkPSJsbmszNiI+PHBhdGggZD0iTTE2MS45NDc5LDUwNy42OTc5IEMxODEuNDU4Myw1MDMuNTYyNSAxOTQuODQ0OCw1MDIuMjczOCAyMTQuMzU1Miw1MDYuNDE5NyIgZmlsbD0ibm9uZSIgaWQ9ImdlODAtdG8tZ2U0NDMiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSIyMjAuNDY4OCw1MDcuNzE4OCwyMTIuMTY0Niw1MDEuNjk0NSwyMTUuMzc0Miw1MDYuNjM2MiwyMTAuNDMyNCw1MDkuODQ1OCwyMjAuNDY4OCw1MDcuNzE4OCIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgZ2U4MCB0byBHaXRlYS0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDA5IiBkYXRhLWVudGl0eS0yPSJlbnQwMDExIiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iNDciIGlkPSJsbmszNyI+PHBhdGggZD0iTTE0OC44NDM4LDUxNC4wNTIxIEMxMzIuNDM3NSw1MjQuMDYyNSA4OC44NjQ2LDU1My40MDYzIDcyLjkxNjcsNTkxLjU2MjUgQzYxLjg1NDIsNjE4LjAzMTMgNTkuMTY2Nyw2MzAuOTM3NSA3Mi45MTY3LDY1Ni4xMTQ2IEM5NS4yNSw2OTcuMDIwOCAxMzkuNDcwMSw3MTkuNzE3NSAxNzIuODg2Nyw3MzIuNDE1NCIgZmlsbD0ibm9uZSIgaWQ9ImdlODAtdG8tR2l0ZWEiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSIxNzguNzI5Miw3MzQuNjM1NCwxNzEuNDQ1Niw3MjcuNDEwNCwxNzMuODYwNSw3MzIuNzg1NCwxNjguNDg1NSw3MzUuMjAwMywxNzguNzI5Miw3MzQuNjM1NCIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgZ2U0NDMgdG8gZ2VuZ2lueC0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDEwIiBkYXRhLWVudGl0eS0yPSJlbnQwMDEyIiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iNDgiIGlkPSJsbmszOCI+PHBhdGggZD0iTTIzMC42MDQyLDUxNS45NTgzIEMyMzkuODc1LDUzMC43OTE3IDI2Mi4xNTQxLDU2Ni40NDA5IDI3OS41Mzk1LDU5NC4yMzI2IiBmaWxsPSJub25lIiBpZD0iZ2U0NDMtdG8tZ2VuZ2lueCIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9IjI4Mi44NTQyLDU5OS41MzEzLDI4MS40MTQ2LDU4OS4zNzM1LDI4MC4wOTIsNTk1LjExNTcsMjc0LjM0OTgsNTkzLjc5MywyODIuODU0Miw1OTkuNTMxMyIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgZ2VuZ2lueCB0byBHaXRlYS0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDEyIiBkYXRhLWVudGl0eS0yPSJlbnQwMDExIiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iNDkiIGlkPSJsbmszOSI+PHBhdGggZD0iTTI3OS4wMjA4LDY1Ni41NjI1IEMyNjQuODAyMSw2NzUuMjA4MyAyNDkuODkzOSw2OTQuNzU5MiAyMzUuNzc5Myw3MTMuMjY5NiIgZmlsbD0ibm9uZSIgaWQ9ImdlbmdpbngtdG8tR2l0ZWEiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSIyMzEuOTg5Niw3MTguMjM5NiwyNDAuOTg3NSw3MTMuMzExMSwyMzUuMTQ3Nyw3MTQuMDk3OSwyMzQuMzYwOCw3MDguMjU4MiwyMzEuOTg5Niw3MTguMjM5NiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PD9wbGFudHVtbC1zcmMgUlA5RlF5Q20zQ05sX1hIUXhlUjJVa1hmaTVDdDY2NXFaRXNnOUE4RFNNblprc3RBdzdUVl9BVEVpLU5JTUV6OXhtVEk3em8xVWU4M3JYcVBQaGpNUVpKTzB6UEVjR3p0NEdIdTlnandPYTYyUlVpLXhUWFEydExpdUVrdlAybjltRmJKNUFIZzI1eDY2THdJRW9sSm1HX0pvaFhNbWF1VDdQZERRYndscnV6ZlFpbVkxQkltem16a3BJY0Y1ZmpMNEhvUW5lam5DZWEtZXE2NzVUZUttc2hMUW9MOUVZWXRoLUx0eDlGeExXeGljZDVsTTJNVW02ZVBBOVEwdVl6bTM1ZWFZWGJuSG9Tb2JsWXhLU0Y1LXplX19aN3JpQzNLV3NTa0M0QjYzTkRidW43MkNWQW92dThGa0xiMTNoM2k5SWRTbjNqdFRWWUR6S0hhT0hLME5DcHJ3SGNqVTJCY2puYVl1V1BwY0pPT213OFZTekJ0bGVkdl9xMWkyZzZ2WjdTRk9oOUoxaXFvMjd5MT8+PC9nPjwvc3ZnPg==\" style=\"max-width:100%\" width=\"100%\" class=\"uml\" alt=\"Stanley Solutions HTTP\/HTTPS Proxy Configuration\" title=\"Stanley Solutions Proxies\" \/><\/p>\n<h2>But, Joe, You Said Something about SSH?<\/h2>\n<p>Yep! That's right!<\/p>\n<p>If you're not terribly familiar with Git, let me just say that you can clone either over HTTP(S) or SSH. In the following images, see how both GitLab and Gitea support\nHTTPS and SSH:<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/gitea-clone.png\" align=\"left\" style=\"width: 55%; margin: 2%;\" alt=\"Gitea Clone\">\n<img src=\"https:\/\/blog.stanleysolutionsnw.com\/gitlab-clone.png\" align=\"right\" style=\"width: 35%; margin: 2%;\" alt=\"GitLab Clone\"><\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/laughing-hysterically.gif\" align=\"right\" style=\"width: 20%\" alt=\"Laughing Hysterically\"><\/p>\n<p>SSH is often just a bit faster, and brings other perks, but it can't be proxied in quite the same way as HTTP traffic. That's because SSH doesn't use hostnames in headers\nin the way that HTTPS does. But, we can do some unique things to make some of this work to our liking.<\/p>\n<blockquote>\n<p>So, what DO we do, then?<\/p>\n<\/blockquote>\n<p>In this case, we can customize the SSH services for both endpoints! We just need to use non-standard ports, and inform both GitLab and Gitea that they're using those\nspecific ports. That way when users clone repositories, the non-standard ports will be in the URL, and used automagically!<\/p>\n<h2>Customizing GitLab<\/h2>\n<p>I want to highlight a few points, here. Notably, for my specific configuration.<\/p>\n<h4>HTTPS Relevant Configuration<\/h4>\n<p>I want to call out how I got the HTTPS routing for GitLab working:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\">## GitLab URL<\/span>\n<span class=\"c1\">##! URL on which GitLab will be reachable.<\/span>\n<span class=\"c1\">##! For more details on configuring external_url see:<\/span>\n<span class=\"c1\">##! https:\/\/docs.gitlab.com\/omnibus\/settings\/configuration.html#configuring-the-external-url-for-gitlab<\/span>\n<span class=\"c1\">##!<\/span>\n<span class=\"c1\">##! Note: During installation\/upgrades, the value of the environment variable<\/span>\n<span class=\"c1\">##! EXTERNAL_URL will be used to populate\/replace this value.<\/span>\n<span class=\"c1\">##! On AWS EC2 instances, we also attempt to fetch the public hostname\/IP<\/span>\n<span class=\"c1\">##! address from AWS. For more details, see:<\/span>\n<span class=\"c1\">##! https:\/\/docs.aws.amazon.com\/AWSEC2\/latest\/UserGuide\/instancedata-data-retrieval.html<\/span>\n<span class=\"n\">external_url<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;https:\/\/gitlab.stanleysolutionsnw.com&quot;<\/span>\n\n<span class=\"o\">...<\/span><span class=\"w\"> <\/span><span class=\"n\">more<\/span><span class=\"w\"> <\/span><span class=\"o\">...<\/span>\n\n<span class=\"c1\">##! **Override only if you use a reverse proxy**<\/span>\n<span class=\"c1\">##! Docs: https:\/\/docs.gitlab.com\/omnibus\/settings\/nginx.html#setting-the-nginx-listen-port<\/span>\n<span class=\"n\">nginx<\/span><span class=\"o\">[<\/span><span class=\"s1\">&#39;listen_port&#39;<\/span><span class=\"o\">]<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"mi\">80<\/span>\n\n<span class=\"c1\">##! **Override only if your reverse proxy internally communicates over HTTP**<\/span>\n<span class=\"c1\">##! Docs: https:\/\/docs.gitlab.com\/omnibus\/settings\/nginx.html#supporting-proxied-ssl<\/span>\n<span class=\"n\">nginx<\/span><span class=\"o\">[<\/span><span class=\"s1\">&#39;listen_https&#39;<\/span><span class=\"o\">]<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"kp\">false<\/span>\n<\/code><\/pre><\/div>\n\n<h4>SSH Relevant Config<\/h4>\n<p>I'll also highlight the config options I needed to get SSH working in the way I wanted:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\">### GitLab Shell settings for GitLab<\/span>\n<span class=\"n\">gitlab_rails<\/span><span class=\"o\">[<\/span><span class=\"s1\">&#39;gitlab_shell_ssh_port&#39;<\/span><span class=\"o\">]<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"mi\">8022<\/span>\n<span class=\"c1\"># gitlab_rails[&#39;gitlab_shell_git_timeout&#39;] = 800<\/span>\n<\/code><\/pre><\/div>\n\n<h2>Customizing Gitea<\/h2>\n<p>For Gitea, there were a few less things that I needed to tweak, but I did have to modify both the application's INI file, and the docker-compose configuration.<\/p>\n<h4>Application Config<\/h4>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">[server]<\/span>\n<span class=\"na\">APP_DATA_PATH<\/span><span class=\"w\">    <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">\/data\/gitea<\/span>\n<span class=\"na\">DOMAIN<\/span><span class=\"w\">           <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">gitea.stanleysolutionsnw.com<\/span>\n<span class=\"na\">SSH_DOMAIN<\/span><span class=\"w\">       <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">gitea.stanleysolutionsnw.com<\/span>\n<span class=\"na\">HTTP_PORT<\/span><span class=\"w\">        <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">3000<\/span>\n<span class=\"na\">ROOT_URL<\/span><span class=\"w\">         <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">https:\/\/gitea.stanleysolutionsnw.com\/<\/span>\n<span class=\"na\">DISABLE_SSH<\/span><span class=\"w\">      <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">false<\/span>\n<span class=\"na\">SSH_PORT<\/span><span class=\"w\">         <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">8023<\/span><span class=\"w\">     <\/span><span class=\"c1\">; This is the really important line, right here!<\/span>\n<span class=\"na\">SSH_LISTEN_PORT<\/span><span class=\"w\">  <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">22<\/span>\n<span class=\"na\">LFS_START_SERVER<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">true<\/span>\n<span class=\"na\">LFS_CONTENT_PATH<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">\/data\/git\/lfs<\/span>\n<span class=\"na\">LFS_JWT_SECRET<\/span><span class=\"w\">   <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">FFqnGuw9L0Zaj6tPeJpqEQgp4yZHpPpvRcul5G9Nv1o<\/span>\n<span class=\"na\">OFFLINE_MODE<\/span><span class=\"w\">     <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">false<\/span>\n<\/code><\/pre><\/div>\n\n<h4>Compose File<\/h4>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"nt\">version<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;3&quot;<\/span>\n\n<span class=\"nt\">networks<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">gitea<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">external<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">false<\/span>\n\n<span class=\"nt\">services<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">server<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">image<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">gitea\/gitea:latest<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">container_name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">gitea<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">environment<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">USER_UID=1000<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">USER_GID=1000<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">GITEA__database__DB_TYPE=postgres<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">GITEA__database__HOST=db:5432<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">env_file<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">.env<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">restart<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">always<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">networks<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">gitea<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">volumes<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">.\/gitea:\/data<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">\/etc\/timezone:\/etc\/timezone:ro<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">\/etc\/localtime:\/etc\/localtime:ro<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">ports<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;3000:3000&quot;<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;8023:22&quot;<\/span><span class=\"w\"> <\/span><span class=\"c1\"># This is the really important line, right here!<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">depends_on<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">db<\/span>\n<\/code><\/pre><\/div>\n\n<h2>Proxy-Server NGINX Streams<\/h2>\n<p>So, let's get back to the root of this whole thing, and I'll explain how I'm able to route these SSH channels, to begin with.<\/p>\n<p>As I had alluded to, earlier, we can't just have listening servers for SSH like we can for HTTP, where the hostname will help determine the \"upstream\" service which\nwill receive the routed trafic. Instead, we need to perform a port-based approach -- thus the whole usage of 8022 and 8023 in the previous configuration samples. Adding\nstreams is relatively straight-forward, and I just added them to my NGINX configuration file; you know, the one located at <code>\/etc\/nginx\/nginx.conf<\/code>.<\/p>\n<p>To do this, you can simply configure a <code>stream<\/code> block to have the respective \"upstreams\" and their listening services. Relatively simple!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\">################################################################################<\/span>\n<span class=\"c1\">#<\/span>\n<span class=\"c1\"># Stanley Solutions SSH Proxies Including:<\/span>\n<span class=\"c1\">#   - GitLab<\/span>\n<span class=\"c1\">#   - Gitea<\/span>\n<span class=\"c1\">#<\/span>\n<span class=\"c1\">################################################################################<\/span>\n\n<span class=\"k\">stream<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n\n<span class=\"w\">        <\/span><span class=\"kn\">upstream<\/span><span class=\"w\"> <\/span><span class=\"s\">gitlab-ssh<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"kn\">server<\/span><span class=\"w\"> <\/span><span class=\"n\">192.168.254.5<\/span><span class=\"p\">:<\/span><span class=\"mi\">22<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"p\">}<\/span>\n\n<span class=\"w\">        <\/span><span class=\"kn\">upstream<\/span><span class=\"w\"> <\/span><span class=\"s\">gitea-ssh<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"kn\">server<\/span><span class=\"w\"> <\/span><span class=\"n\">192.168.254.12<\/span><span class=\"p\">:<\/span><span class=\"mi\">8023<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"p\">}<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\"># Gitlab<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">server<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"kn\">listen<\/span><span class=\"w\"> <\/span><span class=\"mi\">8022<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">            <\/span><span class=\"kn\">proxy_pass<\/span><span class=\"w\"> <\/span><span class=\"s\">gitlab-ssh<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"p\">}<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\"># Gitea<\/span>\n<span class=\"w\">        <\/span><span class=\"kn\">server<\/span><span class=\"w\"> <\/span><span class=\"p\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"kn\">listen<\/span><span class=\"w\"> <\/span><span class=\"mi\">8023<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">            <\/span><span class=\"kn\">proxy_pass<\/span><span class=\"w\"> <\/span><span class=\"s\">gitea-ssh<\/span><span class=\"p\">;<\/span>\n<span class=\"w\">        <\/span><span class=\"p\">}<\/span>\n\n<span class=\"p\">}<\/span>\n<\/code><\/pre><\/div>\n\n<p>After this NGINX configuration is applied, it's just a matter of adding the port-forwardings to the router:<\/p>\n<ul>\n<li><code>8022 -&gt; 8022<\/code> Pointed at the Proxy Server<\/li>\n<li><code>8023 -&gt; 8023<\/code> Pointed at the Proxy Server<\/li>\n<\/ul>\n<p>So, now, the whole SSH topology configuration looks a little more like this:<\/p>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkRFU0NSSVBUSU9OIiBoZWlnaHQ9IjgzNy41cHgiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiIHN0eWxlPSJ3aWR0aDoxMDU3cHg7aGVpZ2h0OjgzN3B4OyIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTA1NyA4MzciIHdpZHRoPSIxMDU3LjI5MTdweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PD9wbGFudHVtbCAxLjIwMjYuM2JldGE2Pz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9Imcxcng4MHljZ3RxeDYxMCIgeDE9IjUwJSIgeDI9IjUwJSIgeTE9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzE1MTUxNSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJnMXJ4ODB5Y2d0cXg2MTEiIHgxPSI1MCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiNEM0YxOTgiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNCNUU4NTMiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48Zz48IS0tY2x1c3RlciBIb21lLU5ldHdvcmstLT48ZyBjbGFzcz0iY2x1c3RlciIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrIiBkYXRhLXNvdXJjZS1saW5lPSI4IiBpZD0iZW50MDAwNSI+PHBvbHlnb24gZmlsbD0iIzE1MTUxNSIgcG9pbnRzPSIyNy4wODMzLDMxNS41NTIxLDM3LjUsMzA1LjEzNTQsMTAyNi4wNDE3LDMwNS4xMzU0LDEwMjYuMDQxNyw3OTYuNzA4MywxMDE1LjYyNSw4MDcuMTI1LDI3LjA4MzMsODA3LjEyNSwyNy4wODMzLDMxNS41NTIxIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHgxPSIxMDE1LjYyNSIgeDI9IjEwMjYuMDQxNyIgeTE9IjMxNS41NTIxIiB5Mj0iMzA1LjEzNTQiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHgxPSIyNy4wODMzIiB4Mj0iMTAxNS42MjUiIHkxPSIzMTUuNTUyMSIgeTI9IjMxNS41NTIxIi8+PGxpbmUgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iMTAxNS42MjUiIHgyPSIxMDE1LjYyNSIgeTE9IjMxNS41NTIxIiB5Mj0iODA3LjEyNSIvPjx0ZXh0IGZpbGw9IiNFQUVBRUEiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxMDUuMjY3MyIgeD0iNDY5Ljc2MjIiIHk9IjMzNS40ODgyIj5Ib21lLU5ldHdvcms8L3RleHQ+PC9nPjwhLS1jbHVzdGVyIGdpdGVhLXNydi0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsuZ2l0ZWEtc3J2IiBkYXRhLXNvdXJjZS1saW5lPSIxMiIgaWQ9ImVudDAwMDgiPjxwb2x5Z29uIGZpbGw9IiMxNTE1MTUiIHBvaW50cz0iNDMuNzUsNTE5LjcwODMsNTQuMTY2Nyw1MDkuMjkxNywzMDUuMjA4Myw1MDkuMjkxNywzMDUuMjA4Myw2NjEuODQzOCwyOTQuNzkxNyw2NzIuMjYwNCw0My43NSw2NzIuMjYwNCw0My43NSw1MTkuNzA4MyIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iMjk0Ljc5MTciIHgyPSIzMDUuMjA4MyIgeTE9IjUxOS43MDgzIiB5Mj0iNTA5LjI5MTciLz48bGluZSBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHgxPSI0My43NSIgeDI9IjI5NC43OTE3IiB5MT0iNTE5LjcwODMiIHkyPSI1MTkuNzA4MyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjI5NC43OTE3IiB4Mj0iMjk0Ljc5MTciIHkxPSI1MTkuNzA4MyIgeTI9IjY3Mi4yNjA0Ii8+PHRleHQgZmlsbD0iI0VBRUFFQSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjM3LjQzMjkiIHg9IjE1MS41OTYxIiB5PSI1MzkuNjQ0NCI+R2l0ZWE8L3RleHQ+PC9nPjwhLS1jbHVzdGVyIGdpdGxhYi1zcnYtLT48ZyBjbGFzcz0iY2x1c3RlciIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLmdpdGxhYi1zcnYiIGRhdGEtc291cmNlLWxpbmU9IjE2IiBpZD0iZW50MDAxMSI+PHBvbHlnb24gZmlsbD0iIzE1MTUxNSIgcG9pbnRzPSIzMTYuNjY2Nyw1MTkuNzA4MywzMjcuMDgzMyw1MDkuMjkxNyw2NDUuODMzMyw1MDkuMjkxNyw2NDUuODMzMyw3ODAuMDQxNyw2MzUuNDE2Nyw3OTAuNDU4MywzMTYuNjY2Nyw3OTAuNDU4MywzMTYuNjY2Nyw1MTkuNzA4MyIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iNjM1LjQxNjciIHgyPSI2NDUuODMzMyIgeTE9IjUxOS43MDgzIiB5Mj0iNTA5LjI5MTciLz48bGluZSBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHgxPSIzMTYuNjY2NyIgeDI9IjYzNS40MTY3IiB5MT0iNTE5LjcwODMiIHkyPSI1MTkuNzA4MyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjYzNS40MTY3IiB4Mj0iNjM1LjQxNjciIHkxPSI1MTkuNzA4MyIgeTI9Ijc5MC40NTgzIi8+PHRleHQgZmlsbD0iI0VBRUFFQSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQ1Ljg2NzkiIHg9IjQ1NC4xNDk0IiB5PSI1MzkuNjQ0NCI+R2l0TGFiPC90ZXh0PjwvZz48IS0tY2x1c3RlciBwcm94eS0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsucHJveHkiIGRhdGEtc291cmNlLWxpbmU9IjIyIiBpZD0iZW50MDAxNiI+PHBvbHlnb24gZmlsbD0iIzE1MTUxNSIgcG9pbnRzPSI4MDEuMDQxNyw0MTIuOTQ3OSw4MTEuNDU4Myw0MDIuNTMxMywxMDA5LjM3NSw0MDIuNTMxMywxMDA5LjM3NSw1NDMuNjQ1OCw5OTguOTU4Myw1NTQuMDYyNSw4MDEuMDQxNyw1NTQuMDYyNSw4MDEuMDQxNyw0MTIuOTQ3OSIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iOTk4Ljk1ODMiIHgyPSIxMDA5LjM3NSIgeTE9IjQxMi45NDc5IiB5Mj0iNDAyLjUzMTMiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHgxPSI4MDEuMDQxNyIgeDI9Ijk5OC45NTgzIiB5MT0iNDEyLjk0NzkiIHkyPSI0MTIuOTQ3OSIvPjxsaW5lIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9Ijk5OC45NTgzIiB4Mj0iOTk4Ljk1ODMiIHkxPSI0MTIuOTQ3OSIgeTI9IjU1NC4wNjI1Ii8+PHRleHQgZmlsbD0iI0VBRUFFQSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEwMi4xMjQiIHg9Ijg0OS45Nzk3IiB5PSI0MzIuODg0Ij5SZXZlcnNlLVByb3h5PC90ZXh0PjwvZz48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsuUm91dGVyODAyMiIgZGF0YS1zb3VyY2UtbGluZT0iOSIgaWQ9ImVudDAwMDYiPjx0ZXh0IGZpbGw9IiMxNTE1MTUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3My43OTc2IiB4PSI4NTQuMjQ3IiB5PSIyNzguMjI5MSI+Um91dGVyODAyMjwvdGV4dD48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzZEOEIzMjtzdHJva2Utd2lkdGg6MS41NjI1OyIgd2lkdGg9IjEyLjUiIHg9Ijg4NC44OTU4IiB5PSIyOTguODg1NCIvPjwvZz48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsuUm91dGVyODAyMyIgZGF0YS1zb3VyY2UtbGluZT0iMTAiIGlkPSJlbnQwMDA3Ij48dGV4dCBmaWxsPSIjMTUxNTE1IiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNzMuNzk3NiIgeD0iNzU5LjQ1NTQiIHk9IjI3OC4yMjkxIj5Sb3V0ZXI4MDIzPC90ZXh0PjxyZWN0IGZpbGw9Im5vbmUiIGhlaWdodD0iMTIuNSIgc3R5bGU9InN0cm9rZTojNkQ4QjMyO3N0cm9rZS13aWR0aDoxLjU2MjU7IiB3aWR0aD0iMTIuNSIgeD0iNzkwLjEwNDIiIHk9IjI5OC44ODU0Ii8+PC9nPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkhvbWUtTmV0d29yay5naXRlYS1zcnYuZ2U4MDIzIiBkYXRhLXNvdXJjZS1saW5lPSIxMyIgaWQ9ImVudDAwMDkiPjx0ZXh0IGZpbGw9IiMxNTE1MTUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzMS44MTE1IiB4PSIyNDAuMzQ0MiIgeT0iNDgyLjM4NTMiPjgwMjM8L3RleHQ+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIxMi41IiBzdHlsZT0ic3Ryb2tlOiM2RDhCMzI7c3Ryb2tlLXdpZHRoOjEuNTYyNTsiIHdpZHRoPSIxMi41IiB4PSIyNTAiIHk9IjUwMy4wNDE3Ii8+PC9nPjwhLS1lbnRpdHkgR2l0ZWEtLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsuZ2l0ZWEtc3J2LkdpdGVhIiBkYXRhLXNvdXJjZS1saW5lPSIxNCIgaWQ9ImVudDAwMTAiPjxwYXRoIGQ9Ik0yMjMuOTE2Nyw2MTAuODMzMyBDMjIzLjkxNjcsNjAwLjQxNjcgMjU2LjI0Nyw2MDAuNDE2NyAyNTYuMjQ3LDYwMC40MTY3IEMyNTYuMjQ3LDYwMC40MTY3IDI4OC41NzczLDYwMC40MTY3IDI4OC41NzczLDYxMC44MzMzIEwyODguNTc3Myw2NDUuMTc1OCBDMjg4LjU3NzMsNjU1LjU5MjQgMjU2LjI0Nyw2NTUuNTkyNCAyNTYuMjQ3LDY1NS41OTI0IEMyNTYuMjQ3LDY1NS41OTI0IDIyMy45MTY3LDY1NS41OTI0IDIyMy45MTY3LDY0NS4xNzU4IEwyMjMuOTE2Nyw2MTAuODMzMyIgZmlsbD0idXJsKCNnMXJ4ODB5Y2d0cXg2MTApIiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48cGF0aCBkPSJNMjIzLjkxNjcsNjEwLjgzMzMgQzIyMy45MTY3LDYyMS4yNSAyNTYuMjQ3LDYyMS4yNSAyNTYuMjQ3LDYyMS4yNSBDMjU2LjI0Nyw2MjEuMjUgMjg4LjU3NzMsNjIxLjI1IDI4OC41NzczLDYxMC44MzMzIiBmaWxsPSJub25lIiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48dGV4dCBmaWxsPSIjRUFFQUVBIiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzMuNDEwNiIgeD0iMjM5LjU0MTciIHk9IjY0Mi4yMjc4Ij5HaXRlYTwvdGV4dD48L2c+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLmdpdGxhYi1zcnYuZ2wyMiIgZGF0YS1zb3VyY2UtbGluZT0iMTciIGlkPSJlbnQwMDEyIj48dGV4dCBmaWxsPSIjMTUxNTE1IiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTUuOTA1OCIgeD0iNDQ5LjMzODgiIHk9IjQ4Mi4zODUzIj4yMjwvdGV4dD48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzZEOEIzMjtzdHJva2Utd2lkdGg6MS41NjI1OyIgd2lkdGg9IjEyLjUiIHg9IjQ1MS4wNDE3IiB5PSI1MDMuMDQxNyIvPjwvZz48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsuZ2l0bGFiLXNydi5nbDgwMjIiIGRhdGEtc291cmNlLWxpbmU9IjE4IiBpZD0iZW50MDAxMyI+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjMxLjgxMTUiIHg9IjQ5MC4zNDQyIiB5PSI0ODIuMzg1MyI+ODAyMjwvdGV4dD48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzZEOEIzMjtzdHJva2Utd2lkdGg6MS41NjI1OyIgd2lkdGg9IjEyLjUiIHg9IjUwMCIgeT0iNTAzLjA0MTciLz48L2c+PCEtLWVudGl0eSBHaXRMYWItLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsuZ2l0bGFiLXNydi5HaXRMYWIiIGRhdGEtc291cmNlLWxpbmU9IjE5IiBpZD0iZW50MDAxNCI+PHBhdGggZD0iTTQ1NC42ODc1LDcyOS4wMzEzIEM0NTQuNjg3NSw3MTguNjE0NiA0OTAuNjIxOSw3MTguNjE0NiA0OTAuNjIxOSw3MTguNjE0NiBDNDkwLjYyMTksNzE4LjYxNDYgNTI2LjU1NjQsNzE4LjYxNDYgNTI2LjU1NjQsNzI5LjAzMTMgTDUyNi41NTY0LDc2My4zNzM3IEM1MjYuNTU2NCw3NzMuNzkwNCA0OTAuNjIxOSw3NzMuNzkwNCA0OTAuNjIxOSw3NzMuNzkwNCBDNDkwLjYyMTksNzczLjc5MDQgNDU0LjY4NzUsNzczLjc5MDQgNDU0LjY4NzUsNzYzLjM3MzcgTDQ1NC42ODc1LDcyOS4wMzEzIiBmaWxsPSJ1cmwoI2cxcng4MHljZ3RxeDYxMCkiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjxwYXRoIGQ9Ik00NTQuNjg3NSw3MjkuMDMxMyBDNDU0LjY4NzUsNzM5LjQ0NzkgNDkwLjYyMTksNzM5LjQ0NzkgNDkwLjYyMTksNzM5LjQ0NzkgQzQ5MC42MjE5LDczOS40NDc5IDUyNi41NTY0LDczOS40NDc5IDUyNi41NTY0LDcyOS4wMzEzIiBmaWxsPSJub25lIiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48dGV4dCBmaWxsPSIjRUFFQUVBIiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNDAuNjE4OSIgeD0iNDcwLjMxMjUiIHk9Ijc2MC40MjU3Ij5HaXRMYWI8L3RleHQ+PC9nPjwhLS1lbnRpdHkgZ2xuZ2lueC0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkhvbWUtTmV0d29yay5naXRsYWItc3J2LmdsbmdpbngiIGRhdGEtc291cmNlLWxpbmU9IjIwIiBpZD0iZW50MDAxNSI+PHJlY3QgZmlsbD0idXJsKCNnMXJ4ODB5Y2d0cXg2MTEpIiBoZWlnaHQ9IjU2LjIxNzQiIHJ4PSI0LjE2NjciIHJ5PSI0LjE2NjciIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjkyLjcyMDUiIHg9IjUzNS45MjcxIiB5PSI1OTkuODk1OCIvPjxyZWN0IGZpbGw9InVybCgjZzFyeDgweWNndHF4NjExKSIgaGVpZ2h0PSIxMC40MTY3IiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSIxNS42MjUiIHg9IjYwNy44MTQzIiB5PSI2MDUuMTA0MiIvPjxyZWN0IGZpbGw9InVybCgjZzFyeDgweWNndHF4NjExKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iNjA1LjczMSIgeT0iNjA3LjE4NzUiLz48cmVjdCBmaWxsPSJ1cmwoI2cxcng4MHljZ3RxeDYxMSkiIGhlaWdodD0iMi4wODMzIiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI0LjE2NjciIHg9IjYwNS43MzEiIHk9IjYxMS4zNTQyIi8+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQwLjYzNzIiIHg9IjU1Ni43NjA0IiB5PSI2MzcuNTQwMyI+TkdJTlg8L3RleHQ+PC9nPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IkhvbWUtTmV0d29yay5wcm94eS5weDgwMjIiIGRhdGEtc291cmNlLWxpbmU9IjIzIiBpZD0iZW50MDAxNyI+PHRleHQgZmlsbD0iIzE1MTUxNSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjMxLjgxMTUiIHg9Ijg3NS43NjA5IiB5PSIzNzUuNjI0OSI+ODAyMjwvdGV4dD48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzZEOEIzMjtzdHJva2Utd2lkdGg6MS41NjI1OyIgd2lkdGg9IjEyLjUiIHg9Ijg4NS40MTY3IiB5PSIzOTYuMjgxMyIvPjwvZz48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJIb21lLU5ldHdvcmsucHJveHkucHg4MDIzIiBkYXRhLXNvdXJjZS1saW5lPSIyNCIgaWQ9ImVudDAwMTgiPjx0ZXh0IGZpbGw9IiMxNTE1MTUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzMS44MTE1IiB4PSI4MDMuODg1OSIgeT0iMzc1LjYyNDkiPjgwMjM8L3RleHQ+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIxMi41IiBzdHlsZT0ic3Ryb2tlOiM2RDhCMzI7c3Ryb2tlLXdpZHRoOjEuNTYyNTsiIHdpZHRoPSIxMi41IiB4PSI4MTMuNTQxNyIgeT0iMzk2LjI4MTMiLz48L2c+PCEtLWVudGl0eSBwcm94eW5naW54LS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iSG9tZS1OZXR3b3JrLnByb3h5LnByb3h5bmdpbngiIGRhdGEtc291cmNlLWxpbmU9IjI1IiBpZD0iZW50MDAxOSI+PHJlY3QgZmlsbD0idXJsKCNnMXJ4ODB5Y2d0cXg2MTEpIiBoZWlnaHQ9IjU2LjIxNzQiIHJ4PSI0LjE2NjciIHJ5PSI0LjE2NjciIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjkyLjcyMDUiIHg9Ijg5OS40Njg4IiB5PSI0ODEuMTc3MSIvPjxyZWN0IGZpbGw9InVybCgjZzFyeDgweWNndHF4NjExKSIgaGVpZ2h0PSIxMC40MTY3IiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSIxNS42MjUiIHg9Ijk3MS4zNTYiIHk9IjQ4Ni4zODU0Ii8+PHJlY3QgZmlsbD0idXJsKCNnMXJ4ODB5Y2d0cXg2MTEpIiBoZWlnaHQ9IjIuMDgzMyIgc3R5bGU9InN0cm9rZTojRDNGMTk4O3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iNC4xNjY3IiB4PSI5NjkuMjcyNiIgeT0iNDg4LjQ2ODgiLz48cmVjdCBmaWxsPSJ1cmwoI2cxcng4MHljZ3RxeDYxMSkiIGhlaWdodD0iMi4wODMzIiBzdHlsZT0ic3Ryb2tlOiNEM0YxOTg7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI0LjE2NjciIHg9Ijk2OS4yNzI2IiB5PSI0OTIuNjM1NCIvPjx0ZXh0IGZpbGw9IiMxNTE1MTUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI0MC42MzcyIiB4PSI5MjAuMzAyMSIgeT0iNTE4LjgyMTUiPk5HSU5YPC90ZXh0PjwvZz48IS0tZW50aXR5IEdpdFNTSFVzZXItLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJHaXRTU0hVc2VyIiBkYXRhLXNvdXJjZS1saW5lPSIzIiBpZD0iZW50MDAwMiI+PGVsbGlwc2UgY3g9Ijg0My43NTM4IiBjeT0iMzMuMzMzMyIgZmlsbD0idXJsKCNnMXJ4ODB5Y2d0cXg2MTEpIiByeD0iMTYuNjY2NyIgcnk9IjE2LjY2NjciIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjxwYXRoIGQ9Ik04NDMuNzUzOCw1NC4xNjY3IEM4NDcuOTIwNSw1NC4xNjY3IDg1MS4wNDU1LDU0LjE2NjcgODU1LjIxMjEsNTAgQzg2My41NDU1LDUwIDg3MS44Nzg4LDU4LjMzMzMgODcxLjg3ODgsNjYuNjY2NyBMODcxLjg3ODgsNzAuODMzMyBDODcxLjg3ODgsNzUgODY3LjcxMjEsNzkuMTY2NyA4NjMuNTQ1NSw3OS4xNjY3IEw4MjMuOTYyMSw3OS4xNjY3IEM4MTkuNzk1NSw3OS4xNjY3IDgxNS42Mjg4LDc1IDgxNS42Mjg4LDcwLjgzMzMgTDgxNS42Mjg4LDY2LjY2NjcgQzgxNS42Mjg4LDU4LjMzMzMgODIzLjk2MjEsNTAgODMyLjI5NTUsNTAgQzgzNi40NjIxLDU0LjE2NjcgODM5LjU4NzEsNTQuMTY2NyA4NDMuNzUzOCw1NC4xNjY3IiBmaWxsPSJ1cmwoI2cxcng4MHljZ3RxeDYxMSkiIHN0eWxlPSJzdHJva2U6I0QzRjE5ODtzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjx0ZXh0IGZpbGw9IiNFQUVBRUEiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3MS44MjAxIiB4PSI4MDcuODQzOCIgeT0iOTcuMDE5NCI+R2l0U1NIVXNlcjwvdGV4dD48L2c+PCEtLWVudGl0eSBXQU4tLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJXQU4iIGRhdGEtc291cmNlLWxpbmU9IjQiIGlkPSJlbnQwMDAzIj48cGF0aCBkPSJNODE3LjYxMDQsMTc3Ljc2OTUgQzgxOC44NTQ5LDE3MS4yNjQxIDgyNC45NjkzLDE2OC43OTcgODMwLjIxOTYsMTczLjA5MzUgQzgzNC4xOTMzLDE2NS42ODU2IDgzOS4xOTE4LDE2Mi43NjM0IDg0NS44NzI3LDE3MC4yNDggQzg1MS4zNzE5LDE2NS4zNTUyIDg1Ni44OTgsMTY3LjEwMzkgODU5LjUwMTYsMTczLjY4NjkgQzg2NS4yMzM4LDE2OC42Mjc5IDg3MC41Nzg0LDE3MC41MDI4IDg3MS45NDksMTc4LjAwNzkgQzg4MS45MjUyLDE4Mi4wODYzIDg4My44ODc5LDE4OS43MTM1IDg3Ni4zNTkyLDE5Ny43MTAyIEM4ODMuMzI0MSwyMDUuNjk0MiA4ODIuODQxOCwyMTMuNzY0OCA4NzEuNDY1NywyMTcuMTI2MyBDODcwLjQ4NTEsMjI0LjkxNjcgODYzLjkxMjksMjI1Ljc3NTYgODU4Ljk1NCwyMjEuMTI4MiBDODU2LjMzNjgsMjI4LjQxMTEgODUwLjM5MDQsMjMxLjczNDUgODQzLjg4MTUsMjI1LjQ1MjQgQzgzNy41NjU3LDIzMS4wOTUxIDgzMi44MTUzLDIyOS4yMTg0IDgzMC45NDg5LDIyMS4yMzA4IEM4MjMuODc1OSwyMjYuNTU5IDgxOS42NjIxLDIyNS4zNTkzIDgxNy43NDA0LDIxNi40NTEzIEM4MDYuMjkwNiwyMTUuMzA0MyA4MDEuODM0NiwyMDYuNTk4OCA4MDkuNTg5NiwxOTcuMjg0NyBDODAzLjUyMTIsMTg3LjcyOTQgODA2LjM2MzEsMTgwLjIwOCA4MTcuNjEwNCwxNzcuNzY5NSIgZmlsbD0ibm9uZSIgc3R5bGU9InN0cm9rZTojRUFFQUVBO3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PHRleHQgZmlsbD0iI0VBRUFFQSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjMwLjI2MTIiIHg9IjgyOC42MTQ2IiB5PSIyMDAuMTAyOCI+V0FOPC90ZXh0PjwvZz48IS0tbGluayBHaXRTU0hVc2VyIHRvIFdBTi0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDAyIiBkYXRhLWVudGl0eS0yPSJlbnQwMDAzIiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iNiIgaWQ9ImxuazQiPjxwYXRoIGQ9Ik04NDMuNzUsMTA1LjQ1ODMgQzg0My43NSwxMjUuOTI3MSA4NDMuNzUsMTQzLjM4NTQgODQzLjc1LDE2MS4yMjkyIiBmaWxsPSJub25lIiBpZD0iR2l0U1NIVXNlci10by1XQU4iIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSI4NDMuNzUsMTY3LjQ3OTIsODQ3LjkxNjcsMTU4LjEwNDIsODQzLjc1LDE2Mi4yNzA4LDgzOS41ODMzLDE1OC4xMDQyLDg0My43NSwxNjcuNDc5MiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgV0FOIHRvIFJvdXRlcjgwMjItLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwMyIgZGF0YS1lbnRpdHktMj0iZW50MDAwNiIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjI4IiBpZD0ibG5rMjAiPjxwYXRoIGQ9Ik04NTUuOTY4OCwyMjQuMTY2NyBDODY3LjM4NTQsMjQ5Ljc1IDg4MC45OTMyLDI4MC4yMDk4IDg4Ni44NjgyLDI5My4zNjYxIiBmaWxsPSJub25lIiBpZD0iV0FOLXRvLVJvdXRlcjgwMjIiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSI4ODkuNDE2NywyOTkuMDcyOSw4ODkuMzk4NiwyODguODEzNyw4ODcuMjkzLDI5NC4zMTcyLDg4MS43ODk1LDI5Mi4yMTE2LDg4OS40MTY3LDI5OS4wNzI5IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBXQU4gdG8gUm91dGVyODAyMy0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDAzIiBkYXRhLWVudGl0eS0yPSJlbnQwMDA3IiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iMjkiIGlkPSJsbmsyMSI+PHBhdGggZD0iTTgzMS44MDIxLDIyNC4xNjY3IEM4MjAuNjI1LDI0OS43NSA4MDcuMzI1OSwyODAuMTg5NyA4MDEuNTc1OSwyOTMuMzQ2IiBmaWxsPSJub25lIiBpZD0iV0FOLXRvLVJvdXRlcjgwMjMiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iI0I1RTg1MyIgcG9pbnRzPSI3OTkuMDcyOSwyOTkuMDcyOSw4MDYuNjQ1MywyOTIuMTUxMiw4MDEuMTU4NywyOTQuMzAwNSw3OTkuMDA5NCwyODguODEzOSw3OTkuMDcyOSwyOTkuMDcyOSIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgUm91dGVyODAyMiB0byBweDgwMjItLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwNiIgZGF0YS1lbnRpdHktMj0iZW50MDAxNyIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjMwIiBpZD0ibG5rMjIiPjxwYXRoIGQ9Ik04OTEuNjY2NywzMTEuMzAyMSBDODkxLjY2NjcsMzI3LjY5NzkgODkxLjY2NjcsMzcyLjU0MTcgODkxLjY2NjcsMzg5LjYxNDYiIGZpbGw9Im5vbmUiIGlkPSJSb3V0ZXI4MDIyLXRvLXB4ODAyMiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9Ijg5MS42NjY3LDM5NS44NjQ2LDg5NS44MzMzLDM4Ni40ODk2LDg5MS42NjY3LDM5MC42NTYzLDg4Ny41LDM4Ni40ODk2LDg5MS42NjY3LDM5NS44NjQ2IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBSb3V0ZXI4MDIzIHRvIHB4ODAyMy0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDA3IiBkYXRhLWVudGl0eS0yPSJlbnQwMDE4IiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iMzEiIGlkPSJsbmsyMyI+PHBhdGggZD0iTTc5OC4xMDQyLDMxMS4zMDIxIEM4MDIuMDUyMSwzMjcuNjk3OSA4MTIuODcyNSwzNzIuNzE0OCA4MTYuOTc2NywzODkuNzg3NyIgZmlsbD0ibm9uZSIgaWQ9IlJvdXRlcjgwMjMtdG8tcHg4MDIzIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iODE4LjQzNzUsMzk1Ljg2NDYsODIwLjI5NzUsMzg1Ljc3NTQsODE3LjIyMDEsMzkwLjgwMDUsODEyLjE5NSwzODcuNzIzMSw4MTguNDM3NSwzOTUuODY0NiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgcHg4MDIyIHRvIHByb3h5bmdpbngtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAxNyIgZGF0YS1lbnRpdHktMj0iZW50MDAxOSIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjMzIiBpZD0ibG5rMjQiPjxwYXRoIGQ9Ik04OTQuNTgzMyw0MDkuMjA4MyBDOTAxLjQzNzUsNDIyLjQ0NzkgOTE2LjA5NzMsNDUwLjc4MiA5MjguNzQzMSw0NzUuMjI5OSIgZmlsbD0ibm9uZSIgaWQ9InB4ODAyMi10by1wcm94eW5naW54IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iOTMxLjYxNDYsNDgwLjc4MTMsOTMxLjAwODMsNDcwLjU0LDkyOS4yMjE3LDQ3Ni4xNTUxLDkyMy42MDY1LDQ3NC4zNjg2LDkzMS42MTQ2LDQ4MC43ODEzIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBweDgwMjMgdG8gcHJveHluZ2lueC0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDE4IiBkYXRhLWVudGl0eS0yPSJlbnQwMDE5IiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iMzQiIGlkPSJsbmsyNSI+PHBhdGggZD0iTTgyNi4yMTg4LDQwOC45MDYzIEM4NDEuOTE2Nyw0MjEuOTQ3OSA4NzguMzE2Niw0NTIuMTg0NCA5MDcuOTcyOCw0NzYuODA5NCIgZmlsbD0ibm9uZSIgaWQ9InB4ODAyMy10by1wcm94eW5naW54IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iOTEyLjc4MTMsNDgwLjgwMjEsOTA4LjIzMDQsNDcxLjYwNzQsOTA4Ljc3NDIsNDc3LjQ3NDksOTAyLjkwNjgsNDc4LjAxODcsOTEyLjc4MTMsNDgwLjgwMjEiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PC9nPjwhLS1saW5rIHByb3h5bmdpbnggdG8gZ2wyMi0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDE5IiBkYXRhLWVudGl0eS0yPSJlbnQwMDEyIiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iMzYiIGlkPSJsbmsyNyI+PHBhdGggZD0iTTkxOC4zNTQyLDQ4MC44MDIxIEM5MDMuNDE2Nyw0NjcuMzg1NCA4ODMuNzgxMyw0NTIuODEyNSA4NjMuMDIwOCw0NDUuNzYwNCBDNzg2LjUyMDgsNDE5LjgwMjEgNTcwLjc5MTcsNDA3LjgwMjEgNDk5LjQ3OTIsNDQ1Ljc2MDQgQzQ3Ni4zNDM4LDQ1OC4wODMzIDQ2NS43Mzk2LDQ4My40MTU0IDQ2MS4yMTg3LDQ5Ni45NTcxIiBmaWxsPSJub25lIiBpZD0icHJveHluZ2lueC10by1nbDIyIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iNDU5LjIzOTYsNTAyLjg4NTQsNDY2LjE2MDYsNDk1LjMxMjMsNDYwLjg4ODksNDk3Ljk0NTEsNDU4LjI1NjEsNDkyLjY3MzQsNDU5LjIzOTYsNTAyLjg4NTQiIHN0eWxlPSJzdHJva2U6I0I1RTg1MztzdHJva2Utd2lkdGg6My4xMjU7Ii8+PC9nPjwhLS1yZXZlcnNlIGxpbmsgZ2U4MDIzIHRvIHByb3h5bmdpbngtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwOSIgZGF0YS1lbnRpdHktMj0iZW50MDAxOSIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjM3IiBpZD0ibG5rMjkiPjxwYXRoIGQ9Ik0yNjQuNDM0NCw0OTcuNzI1MiBDMjc0LjQwMzIsNDg0LjAwNjQgMjk2LjE2NjcsNDU3LjQ1ODMgMzI2LjU2MjUsNDQ1Ljc2MDQgQzM4Mi4xODc1LDQyNC4zNTQyIDgwNi41NzI5LDQyNi42MDQyIDg2My4wMjA4LDQ0NS43NjA0IEM4ODMuNzgxMyw0NTIuODEyNSA5MDMuNDE2Nyw0NjcuMzg1NCA5MTguMzU0Miw0ODAuODAyMSIgZmlsbD0ibm9uZSIgaWQ9ImdlODAyMy1iYWNrdG8tcHJveHluZ2lueCIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9IjI2MC43NjA0LDUwMi43ODEzLDI2OS42NDIyLDQ5Ny42NDY1LDI2My44MjIxLDQ5OC41Njc4LDI2Mi45MDA3LDQ5Mi43NDc4LDI2MC43NjA0LDUwMi43ODEzIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBnbDgwMjIgdG8gZ2xuZ2lueC0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDEzIiBkYXRhLWVudGl0eS0yPSJlbnQwMDE1IiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iMzkiIGlkPSJsbmszMCI+PHBhdGggZD0iTTUwOS45MTY3LDUxNS45NTgzIEM1MTkuNTkzOCw1MzAuNzkxNyA1NDIuODY3MSw1NjYuNTA0NSA1NjAuOTkyMSw1OTQuMjk2MiIgZmlsbD0ibm9uZSIgaWQ9ImdsODAyMi10by1nbG5naW54IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iNTY0LjQwNjMsNTk5LjUzMTMsNTYyLjc3NSw1ODkuNDAyNSw1NjEuNTYxMSw1OTUuMTY4Nyw1NTUuNzk0OSw1OTMuOTU0OCw1NjQuNDA2Myw1OTkuNTMxMyIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgZ2xuZ2lueCB0byBHaXRMYWItLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAxNSIgZGF0YS1lbnRpdHktMj0iZW50MDAxNCIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjQwIiBpZD0ibG5rMzEiPjxwYXRoIGQ9Ik01NjAuNTcyOSw2NTYuNTYyNSBDNTQ1Ljg1NDIsNjc1LjIwODMgNTMwLjM2MjUsNjk0LjgyMzggNTE1Ljc0OCw3MTMuMzM0MiIgZmlsbD0ibm9uZSIgaWQ9ImdsbmdpbngtdG8tR2l0TGFiIiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiNCNUU4NTMiIHBvaW50cz0iNTExLjg3NSw3MTguMjM5Niw1MjAuOTU0Nyw3MTMuNDYzNSw1MTUuMTAyNSw3MTQuMTUxOCw1MTQuNDE0Miw3MDguMjk5NSw1MTEuODc1LDcxOC4yMzk2IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBnbDIyIHRvIEdpdExhYi0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDEyIiBkYXRhLWVudGl0eS0yPSJlbnQwMDE0IiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iNDEiIGlkPSJsbmszMiI+PHBhdGggZD0iTTQ1MC41ODMzLDUxMi42OTc5IEM0MzAuMTc3MSw1MjAuMzAyMSAzNjguODQzOCw1NDYuMzIyOSAzNDUuODMzMyw1OTEuNTYyNSBDMzI5LjU3MjksNjIzLjUzMTMgMzI3LjAyMDgsNjQxLjcyOTIgMzQ1LjgzMzMsNjcyLjI2MDQgQzM2OS4zNzUsNzEwLjQ3OTIgNDEzLjczNDQsNzI3LjgzNDcgNDQ4LjM5MDcsNzM2LjY0NzIiIGZpbGw9Im5vbmUiIGlkPSJnbDIyLXRvLUdpdExhYiIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9IjQ1NC40NDc5LDczOC4xODc1LDQ0Ni4zODg5LDczMS44MzksNDQ5LjQwMDIsNzM2LjkwNCw0NDQuMzM1Miw3MzkuOTE1Myw0NTQuNDQ3OSw3MzguMTg3NSIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48L2c+PCEtLWxpbmsgZ2U4MDIzIHRvIEdpdGVhLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDkiIGRhdGEtZW50aXR5LTI9ImVudDAwMTAiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSI0MyIgaWQ9ImxuazMzIj48cGF0aCBkPSJNMjU2LjI1LDUxNS45NTgzIEMyNTYuMjUsNTMwLjg4NTQgMjU2LjI1LDU2NS45ODk2IDI1Ni4yNSw1OTMuNzkxNyIgZmlsbD0ibm9uZSIgaWQ9ImdlODAyMy10by1HaXRlYSIgc3R5bGU9InN0cm9rZTojQjVFODUzO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjQjVFODUzIiBwb2ludHM9IjI1Ni4yNSw2MDAuMDQxNywyNjAuNDE2Nyw1OTAuNjY2NywyNTYuMjUsNTk0LjgzMzMsMjUyLjA4MzMsNTkwLjY2NjcsMjU2LjI1LDYwMC4wNDE3IiBzdHlsZT0ic3Ryb2tlOiNCNUU4NTM7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48P3BsYW50dW1sLXNyYyBSTDdCUmVDbTRCcHhBeFF2TXdoV3FhQ0xnUVVhS2VNZ2VBZ3pCaDFBNDYwWnNxYzhlbG54X0cxQ2Nmdk9QZFBjeEY2SkVMMTdTQzJvOFNMcUt1aVU1TlEwZlA0Z0R2VU0wQy1hUEtWQ0d4MmZKUHZsbHhKVEJianZoRTN4Qk1DaWVpM3ZFYzEyTFdIUmctR1BjUkRLM0xuUEJ2TWYxVXBhcVAxd1VLd0kxb0dUWFRRSDQ1MzN0V3JTZ3pFaU5oY3JiVVY4QW9lcU1BMFVIVW1zRXRuV0NMZ3FNRG54OTRibVFFQ1BkRS00LWhDalZRcUxBc05OSXE3MkdCUHZwdHgyZmpaTk9mV29UdEdZZk9iX0FaYlM3RC14dVZnT3EwX0JzeVNSVUpwekJ5YXhKTTR0UFlsc0xJLWdWODFJNWJkRlpBYm44UUFzenkwNkE4TzV6WV9BTjc3bW9iai1qcGtGTzlkcnozdWl6RWF6ZmlCNmh5VjZVaHRidWZ5NS1HR0hfVzgwPz48L2c+PC9zdmc+\" style=\"max-width:100%\" width=\"100%\" class=\"uml\" alt=\"Stanley Solutions SSH Proxy Configuration\" title=\"Stanley Solutions SSH Proxies\" \/><\/p>\n<p>Now, at this point, upon inspecting the diagram, you may wonder a few things.<\/p>\n<h4>1) Why is NGINX still involved in the GitLab route?<\/h4>\n<p>Well, this is due to the fact that although we're advertising Git-over-SSH service on port 8022, we're actually listening on port 22, still. That means that for <em>local<\/em>\naccess (<em>coughs<\/em> -- for me) I'll need to have some routing to support the appropriate port 8022.<\/p>\n<h4>2) Why didn't you just point your port-forwarding at the GitLab and Gitea servers, instead of routing through NGINX?<\/h4>\n<p>Well, I'd like to say that this was purely because I wanted some opportunity to have some education, but if I'm honest, it's mostly because this possibility didn't occur to me.<\/p>\n<blockquote>\n<p>Oops. :\/<\/p>\n<\/blockquote>\n<p>But I guess that means this really was a learning opportunity, then , doesn't it? For now, I'm happy with the topology, and I don't think I'll change unless I find a \"good-enoug\" reason to.<\/p>\n<h2>Conclusion<\/h2>\n<p>Now, I can successfully clone from both GitLab and Gitea over SSH. Whoop-whoop!!!!<\/p>","category":[{"@attributes":{"term":"Self-Hosting"}},{"@attributes":{"term":"git"}},{"@attributes":{"term":"self-hosting"}},{"@attributes":{"term":"nginx"}},{"@attributes":{"term":"gitlab"}},{"@attributes":{"term":"gitea"}},{"@attributes":{"term":"ssh"}},{"@attributes":{"term":"proxy"}},{"@attributes":{"term":"networking"}}]},{"title":"My Way of Intalling Python on Windows in 2022","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/my-way-of-installing-python-on-windows-in-2022.html","rel":"alternate"}},"published":"2022-11-02T18:41:00-07:00","updated":"2022-11-02T18:41:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-11-02:\/my-way-of-installing-python-on-windows-in-2022.html","summary":"<p>Finally! Python 3.11 is out! It's new, it's fast(er than previous Python versions), and it's got some dandy new features. And if you wanted to know how I go about putting it on a Windows machine, let me show you...<\/p>","content":"<p>Python is a bit of a tricky subject on Windows, and that's why I've developed my own \"best practice\" for installing it on my systems. Let me briefly walk you through the steps.<\/p>\n<h2>1) Download the latest version of Python<\/h2>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-36-36.png\" style=\"width: 100%\" alt=\"Step 1 - Download the Latest Python\"><\/p>\n<h2>2) Run the installer<\/h2>\n<blockquote>\n<p>but make sure you check \"Add Python to Path\" and use the \"Customize\" option for installation<\/p>\n<\/blockquote>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-37-12.png\" style=\"width: 100%\" alt=\"Step 2 - Run the Installer\"><\/p>\n<h2>3) Select Everything!<\/h2>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-37-41.png\" style=\"width: 100%\" alt=\"Step 3 - Select Everything\"><\/p>\n<h2>4) Select \"Install for All Users\" and Customize the Installation Path<\/h2>\n<p>This will make sure that Python is installed in a simple, and accessible place. I find it VERY helpful to have Python rooted at the <code>C:\\<\/code> drive level. You can argue with me;\nthat's fine. This is just the way <em>I<\/em> do it.\n<img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-40-03.png\" style=\"width: 100%\" alt=\"Step 4 - Change the Path\"><\/p>\n<h2>5) Install!<\/h2>\n<h2>6) Verify the Path<\/h2>\n<p>It's time to make sure that Python got installed and the Path variable was set correctly.<\/p>\n<p>Press your Windows key and search for \"path\". Then open the \"Edit the system environment variables\" dialog.\n<img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-37-41.png\" style=\"width: 100%\" alt=\"Step 6 - Verify the Path\"><\/p>\n<p>From the dialog, select \"Environment Variables\" in the bottom-right.<\/p>\n<p>Then, in the bottom window, make sure that you can see \"<code>C:\\Python311\\Scripts\\<\/code>\" and \"<code>C:\\Python311\\<\/code>\" listed in the \"Path\" variable.\nIf they're not there, double click on the \"Path\" variable, and add them!\n<img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-02_16-45-01.png\" style=\"width: 100%\" alt=\"Step 6 - Verify the Path\"><\/p>\n<hr>\n<p>That's about it! Have fun with the faster, newer, Python, everyone!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"windows"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"installing"}}]},{"title":"Spooky Scary Porch Projects","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/spooky-scary-porch-projects.html","rel":"alternate"}},"published":"2022-11-01T18:41:00-07:00","updated":"2022-11-01T18:41:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-11-01:\/spooky-scary-porch-projects.html","summary":"<p>It's been a crazy month, but isn't that how Halloween is supposed to be, anyway? Crazy? Well, maybe not like this. Let me explain...<\/p>","content":"<p>I've been slowly plugging away at some projects around the house.<\/p>\n<blockquote>\n<p>emphasis on <em>slowly<\/em><\/p>\n<\/blockquote>\n<p>So, earlier this fall, I picked up a whole bunch of cedar boards to replace the nasty old porch ceiling that I have. If you wanted to see more of what the old\nceiling looked like, go look at my <a href=\".\/mt-st-helens-adventures\">article discussing the St Helens aftermath...<\/a> All of that led up to a <strong><em>BUSY<\/em><\/strong> weekend putting\nthe front porch together...<\/p>\n<h5>See?<\/h5>\n<p><img src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/EAMNwBRgTm4zERa\/download?path=&files=22-10-29%2018-22-14%201717.jpg\" style=\"width: 100%\" alt=\"Cedar Going Up\"><\/p>\n<p>But since I was doing all of this work, I wanted to have some cool new lights put in, and I wanted to add some special effects, too; and that required some custom\nwiring.<\/p>\n<p><img src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/EAMNwBRgTm4zERa\/download?path=&files=22-10-29%2011-22-25%201711.jpg\" style=\"width: 100%\" alt=\"Control Box\"><\/p>\n<p>Lastly, I've got to call out the wonderful little box that does so much of the heavy lifting for me... the FireFly Lightning simulator by\n<a href=\"https:\/\/lightsalive.com\">Lights Alive<\/a>. I love this little thing, it's so much fun!<\/p>\n<p><img src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/EAMNwBRgTm4zERa\/download?path=&files=22-10-31%2020-32-13%201735.jpg\" style=\"width: 100%\" alt=\"FireFly by Lights Alive\"><\/p>\n<p>All of the controls are now accessible right next to the front door, which is <em>wonderful!<\/em> I'm not going to go into too much detail here. Feel free to leave a\ncomment if you'd like to know more, but I'll leave you with the video of all the special effects!<\/p>\n<video id=\"halloween-effects-2022\" class=\"video-js vjs-default-skin\" controls\npreload=\"auto\" width=\"683\" height=\"384\" poster=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/EAMNwBRgTm4zERa\/download?path=&files=22-10-31%2019-00-12%201733.jpg\"\ndata-setup=\"{}\">\n<source src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/EAMNwBRgTm4zERa\/download?path=&files=22-10-31%2018-57-40%201730.mov\" type='video\/mp4'>\n<\/video>\n\n<hr>\n<p><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/EAMNwBRgTm4zERa\">Here's a full set of pictures, go check them out!<\/a><\/p>\n<hr>\n<h2>UPDATE:<\/h2>\n<p>A colleague shared this, and I thought it was too funny not to share...<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/2022-11-01_16-03-09.png\" style=\"width: 100%\" alt=\"A Good Laugh about a Lingering Problem...\"><\/p>","category":[{"@attributes":{"term":"Home-Improvement"}},{"@attributes":{"term":"effects"}},{"@attributes":{"term":"lighting"}},{"@attributes":{"term":"lights-alive"}},{"@attributes":{"term":"halloween"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"home-automation"}}]},{"title":"Starting Jenkins Right Away in Winders","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/starting-jenkins-right-away-in-winders.html","rel":"alternate"}},"published":"2022-10-24T16:25:00-07:00","updated":"2022-10-24T16:25:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-10-24:\/starting-jenkins-right-away-in-winders.html","summary":"<p>Jenkins is a powerful, albeit confusing, tool. You can structure all manner of automated processes to build source code, distribute packages, and automate the boring stuff. It's great! But Jenkins and Windows (or... <em>Winders<\/em>) don't always play nicely. I learned this the hard way in my \"day-job\" and had to find a technique to resolve some of this bad-behavior. So hopefully you might find this helpful!<\/p>","content":"<blockquote>\n<p>Let me begin by describing the circumstances.<\/p>\n<\/blockquote>\n<p>As it turns out, in some applications, Jenkins must be run in a full UI-based environment to function properly with some tools. Notably, some of the tools I use in my day-to-day\nwork. A colleague and I were working on getting Jenkins running in build nodes to support automated package builds at work. Challenge is, we needed the full UI, and that's not\na \"standard\" configuration for Jenkins nodes on Windows. So we had to do some exploring. <\/p>\n<p>Most commonly, Jenkins agents will run as part of a \"scheduled task\" on a Windows host. However, this has some inherent limitations that we ran into. Specifically, some very\npeculiar (and specific) C# challenges. As it turns out, those issues were only really present if the Jenkins agent was running as a desktop-environment-less task. That said,\nthere's not a clean way (that I found, at least) to make a task run in a full desktop environment. This left us scratching our heads to find a way to make Jenkins run\nautomagically in a desktop.<\/p>\n<blockquote>\n<p>Hmmm...<\/p>\n<\/blockquote>\n<p>Eventually, I stumbled upon a potential solution! We could define a \"startup user\" whose account is opened by default when the Windows system starts. Almost like some kind of\n\"kiosk mode\" for windows, but specifically for our application. Now, this still took some exploration, and learning, but I finally found a way to make a default user\n\"automatically log in\" when the system starts, and then make a simple startup script to run the Jenkins command.<\/p>\n<h5>References<\/h5>\n<ul>\n<li><a href=\"https:\/\/stackoverflow.com\/questions\/18906753\/jenkins-windows-slave-service-does-not-interact-with-desktop\">getting Jenkins to interact with Windows desktop<\/a><\/li>\n<li><a href=\"https:\/\/serverfault.com\/questions\/269832\/windows-server-2008-automatic-user-logon-on-power-on\/606130#606130\">article on ServerFault<\/a><\/li>\n<\/ul>\n<h2>Instructions<\/h2>\n<h4>1) Create a batch file to start Jenkins agent<\/h4>\n<p>The Jenkins agent has to be run on the VM from the specified workspace directory (<code>C:\\_jenkins<\/code> in this case), and with the appropriate token information.\nSo create a batch file that you can store in the Windows startup folder for the specific user. Don't worry about it's location, now, we'll come back to that later.<\/p>\n<p>Create the file <code>jenkins_start.bat<\/code> with the following information (substituting information where needed from Jenkins' direction).<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">cd<\/span> C:\\_jenkins\n\njava -jar agent.jar -jnlpUrl https:\/\/<span class=\"p\">&lt;<\/span> HOST <span class=\"p\">&gt;<\/span>\/computer\/<span class=\"p\">&lt;<\/span> AGENT-NAME <span class=\"p\">&gt;<\/span>\/jenkins-agent.jnlp -secret <span class=\"p\">&lt;<\/span> SECRET <span class=\"p\">&gt;<\/span> -workDir <span class=\"s2\">&quot;c:\\_jenkins&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<h4>2) Move the startup script to the service-user's startup folder<\/h4>\n<p>To make the script run as the user would, we'll need to place it in the user's startup folder.<\/p>\n<ol>\n<li>As the service user, press Ctrl+r to open the \"Run\" launcher prompt. (or search for \"Run\" in the Windows start menu)<\/li>\n<li>Enter <code>shell:startup<\/code> in the prompt and press Enter\/click \"Run\"<\/li>\n<li>Copy\/Move the batch script you created previously into the startup folder which now appears on-screen<\/li>\n<\/ol>\n<p>If the above steps fail, you can manually browse to the start folder as follows:<\/p>\n<p><code>C:\\Users\\&lt; SERVICE-USERNAME &gt;\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup<\/code><\/p>\n<h4>OPTIONALLY:<\/h4>\n<p>It may be valuable to restart the machine and log in as the service-user to confirm that the startup script is working as expected and connects back to the Jenkins master\nwith a visible console window.<\/p>\n<h4>3) Modify the registry to make the service-user logon automatically after startup<\/h4>\n<p>As described in the <a href=\"https:\/\/serverfault.com\/questions\/269832\/windows-server-2008-automatic-user-logon-on-power-on\/606130#606130\">article on ServerFault<\/a>, generate a file called <code>AutoLogon.reg<\/code> somewhere on the virtual machine with the following content<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"na\">Windows Registry Editor Version 5.00<\/span>\n\n<span class=\"k\">[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon]<\/span>\n<span class=\"na\">&quot;DefaultDomainName&quot;<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;SERVICE_ACCOUNT_DOMAIN&quot;<\/span>\n<span class=\"na\">&quot;DefaultUserName&quot;<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;SERVICE_ACCOUNT_USER&quot;<\/span>\n<span class=\"na\">&quot;DefaultPassword&quot;<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;SERVICE_ACCOUNT_PASSWORD&quot;<\/span>\n<span class=\"na\">&quot;ForceAutoLogon&quot;<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;1&quot;<\/span>\n<span class=\"na\">&quot;AutoAdminLogon&quot;<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;1&quot;<\/span>\n<span class=\"na\">&quot;LegalNoticeCaption&quot;<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;&quot;<\/span>\n<span class=\"na\">&quot;LegalNoticeText&quot;<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;&quot;<\/span>\n\n<span class=\"k\">[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\AutoLogonChecked]<\/span>\n<span class=\"na\">@<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;1&quot;<\/span>\n\n<span class=\"k\">[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\policies\\system]<\/span>\n<span class=\"na\">&quot;LegalNoticeCaption&quot;<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;&quot;<\/span>\n<span class=\"na\">&quot;LegalNoticeText&quot;<\/span><span class=\"o\">=<\/span><span class=\"s\">&quot;&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p>After saving the file, right-click on it in a file explorer and select \"Merge,\" you'll be prompted to confirm the operation; do so.<\/p>\n<h4>Restart the machine and confirm that it automatically connects to Jenkins<\/h4>\n<p>Restarting the machine should make use of the new registry edits and auto-login the service account user. In-so-doing will launch the startup batch-file script which will\nconnect to the Jenkins master through the console in a graphical session.<\/p>\n<hr>\n<h2>Additional Improvement Notes<\/h2>\n<p>I think one thing I should call out is that this mechanism doesn't \"restart\" if a failure occurs. That is, if Jenkins crashes, it doesn't restart. But I think it could\neasily be added by using some kind of <code>FOR<\/code>\/<code>WHILE<\/code> loop in the batch-file to script some kind of automatic retry. Nothing crazy, just worth noting!<\/p>\n<h5>Closing Thoughts<\/h5>\n<p>I know there's bound to be other ways of doing this. Let me know what you've done! Tell me about your Jenkins \"woes;\" I'd love to hear them! Hopefully my solution will\ngive you some inspiration.<\/p>","category":[{"@attributes":{"term":"DevOps"}},{"@attributes":{"term":"devops"}},{"@attributes":{"term":"jenkins"}},{"@attributes":{"term":"ci\/cd"}},{"@attributes":{"term":"continuous-integration"}},{"@attributes":{"term":"continuous-deployment"}},{"@attributes":{"term":"build-systems"}},{"@attributes":{"term":"automation"}},{"@attributes":{"term":"windows"}}]},{"title":"Finding Broken Blog Links","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/finding-broken-blog-links.html","rel":"alternate"}},"published":"2022-10-24T16:00:00-07:00","updated":"2022-10-24T16:00:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-10-24:\/finding-broken-blog-links.html","summary":"<p>Well, I just recently converted to a new Nextcloud instance at home. Gosh, I sure hope I wasn't using any of those links externally on any important websites like a blog, or anything... Oh. I was? Awkward...<\/p>","content":"<p>It's something of a solved problem, but I thought I'd share the way that I solved it...<\/p>\n<p>That's right, finding dead links is a common practice with hosting websites of any form, or fashion. Just a matter of determining how they should be checked, right?\nSince I'm hosting my blog site from a GitHub-Pages GitHub-Action based deployment, I thought it would make the most sense to perform a little sanity check by way of\nanother GitHub Action. If you'd like to go see it's source, you can check it out\n<a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/blob\/master\/.github\/workflows\/broken-link-detector.yml\">here<\/a>.<\/p>\n<p>Or you could just look at the code... that's an option, too.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Broken<\/span><span class=\"w\"> <\/span><span class=\"n\">Link<\/span><span class=\"w\"> <\/span><span class=\"n\">Detector<\/span>\n<span class=\"n\">on<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"n\">schedule<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">    <\/span><span class=\"err\">#<\/span><span class=\"w\"> <\/span><span class=\"n\">Runs<\/span><span class=\"w\"> <\/span><span class=\"n\">every<\/span><span class=\"w\"> <\/span><span class=\"n\">day<\/span><span class=\"w\"> <\/span><span class=\"n\">at<\/span><span class=\"w\"> <\/span><span class=\"mi\">1<\/span><span class=\"o\">:<\/span><span class=\"mi\">00<\/span><span class=\"n\">AM<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">cron<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;0 1 * * *&#39;<\/span>\n<span class=\"w\">  <\/span><span class=\"n\">workflow_dispatch<\/span><span class=\"o\">:<\/span>\n\n<span class=\"n\">jobs<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"n\">find<\/span><span class=\"o\">-<\/span><span class=\"n\">broken<\/span><span class=\"o\">-<\/span><span class=\"n\">links<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Find<\/span><span class=\"w\"> <\/span><span class=\"n\">Broken<\/span><span class=\"w\"> <\/span><span class=\"n\">Links<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">runs<\/span><span class=\"o\">-<\/span><span class=\"n\">on<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">ubuntu<\/span><span class=\"o\">-<\/span><span class=\"n\">latest<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">steps<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">actions<\/span><span class=\"o\">\/<\/span><span class=\"n\">checkout<\/span><span class=\"err\">@<\/span><span class=\"n\">v2<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Set<\/span><span class=\"w\"> <\/span><span class=\"n\">up<\/span><span class=\"w\"> <\/span><span class=\"n\">Python<\/span><span class=\"w\"> <\/span><span class=\"mf\">3.10<\/span>\n<span class=\"w\">      <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">actions<\/span><span class=\"o\">\/<\/span><span class=\"n\">setup<\/span><span class=\"o\">-<\/span><span class=\"n\">python<\/span><span class=\"err\">@<\/span><span class=\"n\">v2<\/span>\n<span class=\"w\">      <\/span><span class=\"k\">with<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">python<\/span><span class=\"o\">-<\/span><span class=\"n\">version<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;3.10&quot;<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Install<\/span><span class=\"w\"> <\/span><span class=\"n\">dependencies<\/span>\n<span class=\"w\">      <\/span><span class=\"n\">run<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"o\">|<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">m<\/span><span class=\"w\"> <\/span><span class=\"n\">pip<\/span><span class=\"w\"> <\/span><span class=\"n\">install<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">upgrade<\/span><span class=\"w\"> <\/span><span class=\"n\">pip<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">m<\/span><span class=\"w\"> <\/span><span class=\"n\">pip<\/span><span class=\"w\"> <\/span><span class=\"n\">install<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">upgrade<\/span><span class=\"w\"> <\/span><span class=\"n\">lxml<\/span><span class=\"w\"> <\/span><span class=\"n\">beautifulsoup4<\/span><span class=\"w\"> <\/span><span class=\"n\">requests<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Find<\/span><span class=\"w\"> <\/span><span class=\"n\">Broken<\/span><span class=\"w\"> <\/span><span class=\"n\">Links<\/span>\n<span class=\"w\">      <\/span><span class=\"n\">run<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"o\">|<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"n\">broken<\/span><span class=\"o\">-<\/span><span class=\"n\">link<\/span><span class=\"o\">-<\/span><span class=\"n\">detector<\/span><span class=\"o\">.<\/span><span class=\"na\">py<\/span>\n<\/code><\/pre><\/div>\n\n<p>But that doesn't <em>actually<\/em> find the dead links, it just runs the script responsible for finding them. We leave that little bit up to Python!<\/p>\n<blockquote>\n<p>What else would you have expected me to use. I mean, c'mon... really?!<\/p>\n<\/blockquote>\n<p>I'll admit, I had some pretty helpful resources to lean on for ideas!<\/p>\n<ul>\n<li>https:\/\/brianli.com\/2021\/06\/how-to-find-broken-links-with-python\/<\/li>\n<li>https:\/\/www.webucator.com\/article\/checking-your-sitemap-for-broken-links-with-python\/<\/li>\n<\/ul>\n<p>I took those resources and made a couple of functions to help me out...<\/p>\n<h4>Checking for Dead Resources on a Page<\/h4>\n<p>I need to look at a few things to verify whether they point at valid resources:<\/p>\n<ul>\n<li>Links (<code>&lt;a&gt;<\/code> tags)<\/li>\n<li>Images (<code>&lt;img&gt;<\/code> tags)<\/li>\n<li>Videos (posters with <code>&lt;video&gt;<\/code> tags and video files with <code>&lt;source&gt;<\/code> tags)<\/li>\n<\/ul>\n<p>This, with a little help from one of those other resources I mentioned, helped me to make a handy-dandy little function to find broken links on a page.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">def<\/span> <span class=\"nf\">find_broken_resources<\/span><span class=\"p\">(<\/span><span class=\"n\">html_text<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"sd\">    Find broken resources on a single page:<\/span>\n<span class=\"sd\">    * images (&lt;img&gt; tag)<\/span>\n<span class=\"sd\">    * links (&lt;a&gt; tag)<\/span>\n<span class=\"sd\">    * video posters (&lt;video&gt; tag)<\/span>\n<span class=\"sd\">    * videos (&lt;source&gt; tag)<\/span>\n<span class=\"sd\">    &quot;&quot;&quot;<\/span>\n    <span class=\"n\">failures<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[]<\/span>\n\n    <span class=\"c1\"># Set root domain.<\/span>\n    <span class=\"n\">root_domain<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;stanleysolutionsnw.com&quot;<\/span>\n\n    <span class=\"c1\"># Internal function for validating HTTP status code.<\/span>\n    <span class=\"k\">def<\/span> <span class=\"nf\">_validate_url<\/span><span class=\"p\">(<\/span><span class=\"n\">url<\/span><span class=\"p\">):<\/span>\n        <span class=\"n\">r<\/span> <span class=\"o\">=<\/span> <span class=\"n\">requests<\/span><span class=\"o\">.<\/span><span class=\"n\">head<\/span><span class=\"p\">(<\/span><span class=\"n\">url<\/span><span class=\"p\">)<\/span>\n        <span class=\"k\">if<\/span> <span class=\"n\">r<\/span><span class=\"o\">.<\/span><span class=\"n\">status_code<\/span> <span class=\"o\">==<\/span> <span class=\"mi\">404<\/span><span class=\"p\">:<\/span>\n            <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;    Broken Link!<\/span><span class=\"se\">\\n<\/span><span class=\"s2\">    &quot;<\/span> <span class=\"o\">+<\/span> <span class=\"n\">url<\/span><span class=\"p\">)<\/span>\n            <span class=\"n\">failures<\/span><span class=\"o\">.<\/span><span class=\"n\">append<\/span><span class=\"p\">(<\/span><span class=\"n\">url<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"c1\"># Parse HTML from request.<\/span>\n    <span class=\"n\">soup<\/span> <span class=\"o\">=<\/span> <span class=\"n\">BeautifulSoup<\/span><span class=\"p\">(<\/span><span class=\"n\">html_text<\/span><span class=\"p\">,<\/span> <span class=\"n\">features<\/span><span class=\"o\">=<\/span><span class=\"s2\">&quot;html.parser&quot;<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"c1\"># Create a list containing all links with the root domain.<\/span>\n    <span class=\"n\">links<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"n\">l<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;href&quot;<\/span><span class=\"p\">)<\/span> <span class=\"k\">for<\/span> <span class=\"n\">l<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">soup<\/span><span class=\"o\">.<\/span><span class=\"n\">find_all<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;a&quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">href<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">)<\/span> <span class=\"k\">if<\/span> <span class=\"sa\">f<\/span><span class=\"s2\">&quot;<\/span><span class=\"si\">{<\/span><span class=\"n\">root_domain<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">l<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;href&quot;<\/span><span class=\"p\">)]<\/span>\n    <span class=\"n\">imgs<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"n\">i<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;src&quot;<\/span><span class=\"p\">)<\/span> <span class=\"k\">for<\/span> <span class=\"n\">i<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">soup<\/span><span class=\"o\">.<\/span><span class=\"n\">find_all<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;img&quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">src<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">)<\/span> <span class=\"k\">if<\/span> <span class=\"sa\">f<\/span><span class=\"s2\">&quot;<\/span><span class=\"si\">{<\/span><span class=\"n\">root_domain<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">i<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;src&quot;<\/span><span class=\"p\">)]<\/span>\n    <span class=\"n\">vid_posters<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"n\">v<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;poster&quot;<\/span><span class=\"p\">)<\/span> <span class=\"k\">for<\/span> <span class=\"n\">v<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">soup<\/span><span class=\"o\">.<\/span><span class=\"n\">find_all<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;video&quot;<\/span><span class=\"p\">)<\/span> <span class=\"k\">if<\/span> <span class=\"sa\">f<\/span><span class=\"s2\">&quot;<\/span><span class=\"si\">{<\/span><span class=\"n\">root_domain<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">v<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;poster&quot;<\/span><span class=\"p\">)]<\/span>\n    <span class=\"n\">vids<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"n\">v<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;src&quot;<\/span><span class=\"p\">)<\/span> <span class=\"k\">for<\/span> <span class=\"n\">v<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">soup<\/span><span class=\"o\">.<\/span><span class=\"n\">find_all<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;source&quot;<\/span><span class=\"p\">)<\/span> <span class=\"k\">if<\/span> <span class=\"sa\">f<\/span><span class=\"s2\">&quot;<\/span><span class=\"si\">{<\/span><span class=\"n\">root_domain<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">v<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;src&quot;<\/span><span class=\"p\">)]<\/span>\n\n    <span class=\"c1\"># Loop through links checking for 404 responses.<\/span>\n    <span class=\"k\">with<\/span> <span class=\"n\">ThreadPoolExecutor<\/span><span class=\"p\">(<\/span><span class=\"n\">max_workers<\/span><span class=\"o\">=<\/span><span class=\"mi\">8<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">executor<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">executor<\/span><span class=\"o\">.<\/span><span class=\"n\">map<\/span><span class=\"p\">(<\/span><span class=\"n\">_validate_url<\/span><span class=\"p\">,<\/span> <span class=\"n\">links<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"c1\"># Loop through images checking for 404 responses.<\/span>\n    <span class=\"k\">with<\/span> <span class=\"n\">ThreadPoolExecutor<\/span><span class=\"p\">(<\/span><span class=\"n\">max_workers<\/span><span class=\"o\">=<\/span><span class=\"mi\">8<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">executor<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">executor<\/span><span class=\"o\">.<\/span><span class=\"n\">map<\/span><span class=\"p\">(<\/span><span class=\"n\">_validate_url<\/span><span class=\"p\">,<\/span> <span class=\"n\">imgs<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"c1\"># Loop through videos checking for 404 responses.<\/span>\n    <span class=\"k\">with<\/span> <span class=\"n\">ThreadPoolExecutor<\/span><span class=\"p\">(<\/span><span class=\"n\">max_workers<\/span><span class=\"o\">=<\/span><span class=\"mi\">8<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">executor<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">executor<\/span><span class=\"o\">.<\/span><span class=\"n\">map<\/span><span class=\"p\">(<\/span><span class=\"n\">_validate_url<\/span><span class=\"p\">,<\/span> <span class=\"n\">vid_posters<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"c1\"># Loop through videos checking for 404 responses.<\/span>\n    <span class=\"k\">with<\/span> <span class=\"n\">ThreadPoolExecutor<\/span><span class=\"p\">(<\/span><span class=\"n\">max_workers<\/span><span class=\"o\">=<\/span><span class=\"mi\">8<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">executor<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">executor<\/span><span class=\"o\">.<\/span><span class=\"n\">map<\/span><span class=\"p\">(<\/span><span class=\"n\">_validate_url<\/span><span class=\"p\">,<\/span> <span class=\"n\">vids<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"k\">if<\/span> <span class=\"nb\">len<\/span><span class=\"p\">(<\/span><span class=\"n\">failures<\/span><span class=\"p\">)<\/span> <span class=\"o\">&gt;<\/span> <span class=\"mi\">0<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">for<\/span> <span class=\"n\">failed_url<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">failures<\/span><span class=\"p\">:<\/span>\n            <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"sa\">f<\/span><span class=\"s2\">&quot;  Failed to access resource: <\/span><span class=\"si\">{<\/span><span class=\"n\">failed_url<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span><span class=\"p\">)<\/span>\n        <span class=\"k\">return<\/span> <span class=\"kc\">True<\/span>\n<\/code><\/pre><\/div>\n\n<p>But that only finds broken resources on a single page. How am I going to do the whole website?<\/p>\n<h4>Checking Each URL from the Sitemap<\/h4>\n<p>So, there's a function for that, I got this one started up so that it could find all of the pages from the sitemap, and use that to parse each page.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">def<\/span> <span class=\"nf\">check_sitemap_urls<\/span><span class=\"p\">(<\/span><span class=\"n\">sitemap<\/span><span class=\"p\">,<\/span> <span class=\"n\">limit<\/span><span class=\"o\">=<\/span><span class=\"mi\">100<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Attempts to resolve all urls in a sitemap and returns the results<\/span>\n\n<span class=\"sd\">    Args:<\/span>\n<span class=\"sd\">        sitemap (str): A URL<\/span>\n<span class=\"sd\">        limit (int, optional): The maximum number of URLs to check. Defaults to 50.<\/span>\n<span class=\"sd\">            Pass None for no limit.<\/span>\n\n<span class=\"sd\">    Returns:<\/span>\n<span class=\"sd\">        list of tuples: [(status_code, history, url, msg)].<\/span>\n<span class=\"sd\">            The history contains a list of redirects.<\/span>\n<span class=\"sd\">    &quot;&quot;&quot;<\/span>\n    <span class=\"n\">success<\/span> <span class=\"o\">=<\/span> <span class=\"kc\">True<\/span>\n    <span class=\"n\">res<\/span> <span class=\"o\">=<\/span> <span class=\"n\">requests<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"n\">sitemap<\/span><span class=\"p\">)<\/span>\n    <span class=\"n\">doc<\/span> <span class=\"o\">=<\/span> <span class=\"n\">etree<\/span><span class=\"o\">.<\/span><span class=\"n\">XML<\/span><span class=\"p\">(<\/span><span class=\"n\">res<\/span><span class=\"o\">.<\/span><span class=\"n\">content<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"c1\"># xpath query for selecting all element nodes in namespace<\/span>\n    <span class=\"n\">query<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;descendant-or-self::*[namespace-uri()!=&#39;&#39;]&quot;<\/span>\n    <span class=\"c1\"># for each element returned by the above xpath query...<\/span>\n    <span class=\"k\">for<\/span> <span class=\"n\">element<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">doc<\/span><span class=\"o\">.<\/span><span class=\"n\">xpath<\/span><span class=\"p\">(<\/span><span class=\"n\">query<\/span><span class=\"p\">):<\/span>\n        <span class=\"c1\"># replace element name with its local name<\/span>\n        <span class=\"n\">element<\/span><span class=\"o\">.<\/span><span class=\"n\">tag<\/span> <span class=\"o\">=<\/span> <span class=\"n\">etree<\/span><span class=\"o\">.<\/span><span class=\"n\">QName<\/span><span class=\"p\">(<\/span><span class=\"n\">element<\/span><span class=\"p\">)<\/span><span class=\"o\">.<\/span><span class=\"n\">localname<\/span>\n\n    <span class=\"c1\"># get all the loc elements<\/span>\n    <span class=\"n\">links<\/span> <span class=\"o\">=<\/span> <span class=\"n\">doc<\/span><span class=\"o\">.<\/span><span class=\"n\">xpath<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;.\/\/loc&quot;<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">for<\/span> <span class=\"n\">i<\/span><span class=\"p\">,<\/span> <span class=\"n\">link<\/span> <span class=\"ow\">in<\/span> <span class=\"nb\">enumerate<\/span><span class=\"p\">(<\/span><span class=\"n\">links<\/span><span class=\"p\">,<\/span> <span class=\"mi\">1<\/span><span class=\"p\">):<\/span>\n        <span class=\"n\">url<\/span> <span class=\"o\">=<\/span> <span class=\"n\">link<\/span><span class=\"o\">.<\/span><span class=\"n\">text<\/span>\n        <span class=\"k\">if<\/span> <span class=\"s2\">&quot;\/tag\/&quot;<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">url<\/span><span class=\"p\">:<\/span>\n            <span class=\"k\">break<\/span> <span class=\"c1\"># Don&#39;t go over all the tags<\/span>\n        <span class=\"n\">load_fail<\/span> <span class=\"o\">=<\/span> <span class=\"kc\">False<\/span>\n        <span class=\"k\">for<\/span> <span class=\"n\">_<\/span> <span class=\"ow\">in<\/span> <span class=\"nb\">range<\/span><span class=\"p\">(<\/span><span class=\"mi\">10<\/span><span class=\"p\">):<\/span>\n            <span class=\"k\">try<\/span><span class=\"p\">:<\/span>\n                <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"sa\">f<\/span><span class=\"s2\">&quot;<\/span><span class=\"si\">{<\/span><span class=\"n\">i<\/span><span class=\"si\">}<\/span><span class=\"s2\">. Checking <\/span><span class=\"si\">{<\/span><span class=\"n\">url<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span><span class=\"p\">)<\/span>\n                <span class=\"n\">r<\/span> <span class=\"o\">=<\/span> <span class=\"n\">requests<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"n\">url<\/span><span class=\"p\">)<\/span>\n\n                <span class=\"c1\"># Locate any broken resources on the page<\/span>\n                <span class=\"n\">success<\/span> <span class=\"o\">&amp;=<\/span> <span class=\"n\">find_broken_resources<\/span><span class=\"p\">(<\/span><span class=\"n\">html_text<\/span><span class=\"o\">=<\/span><span class=\"n\">r<\/span><span class=\"o\">.<\/span><span class=\"n\">text<\/span><span class=\"p\">)<\/span>\n\n                <span class=\"n\">load_fail<\/span> <span class=\"o\">=<\/span> <span class=\"kc\">False<\/span>\n                <span class=\"k\">break<\/span>\n\n            <span class=\"k\">except<\/span> <span class=\"ne\">Exception<\/span> <span class=\"k\">as<\/span> <span class=\"n\">e<\/span><span class=\"p\">:<\/span>\n                <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"sa\">f<\/span><span class=\"s2\">&quot;Retry... caused by: <\/span><span class=\"si\">{<\/span><span class=\"n\">e<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span><span class=\"p\">)<\/span>\n                <span class=\"n\">load_fail<\/span> <span class=\"o\">=<\/span> <span class=\"kc\">True<\/span>\n                <span class=\"n\">time<\/span><span class=\"o\">.<\/span><span class=\"n\">sleep<\/span><span class=\"p\">(<\/span><span class=\"mi\">2<\/span><span class=\"p\">)<\/span>\n        <span class=\"k\">if<\/span> <span class=\"n\">load_fail<\/span><span class=\"p\">:<\/span>\n            <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;    Failed to load page:<\/span><span class=\"se\">\\n<\/span><span class=\"s2\">    &quot;<\/span> <span class=\"o\">+<\/span> <span class=\"n\">url<\/span><span class=\"p\">)<\/span>\n            <span class=\"n\">success<\/span> <span class=\"o\">=<\/span> <span class=\"kc\">False<\/span>\n\n        <span class=\"k\">if<\/span> <span class=\"n\">limit<\/span> <span class=\"ow\">and<\/span> <span class=\"n\">i<\/span> <span class=\"o\">&gt;=<\/span> <span class=\"n\">limit<\/span><span class=\"p\">:<\/span>\n            <span class=\"k\">break<\/span>\n    <span class=\"k\">return<\/span> <span class=\"n\">success<\/span>\n<\/code><\/pre><\/div>\n\n<h5>Closing Thoughts<\/h5>\n<p>So... Like I said. This isn't a new problem, or one that hasn't been solved before. But it's how <em>I<\/em> did it. Thanks for reading!<\/p>","category":[{"@attributes":{"term":"Blogging"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"blogging"}},{"@attributes":{"term":"automation"}},{"@attributes":{"term":"github"}},{"@attributes":{"term":"github-actions"}},{"@attributes":{"term":"ci\/cd"}},{"@attributes":{"term":"continuous-integration"}},{"@attributes":{"term":"continuous-deployment"}}]},{"title":"Mount St. Helens Adventures","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/mt-st-helens-adventures.html","rel":"alternate"}},"published":"2022-09-06T19:43:00-07:00","updated":"2022-09-06T19:43:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-09-06:\/mt-st-helens-adventures.html","summary":"<p>Ok, so I clearly wasn't alive when Mount St. Helens blew its lid in May of 1980; but that doesn't mean that I haven't had some adventures of my own with the eruption's aftermath! I spent this Labor Day with my mother, working on some improvements to the front porch celing; prepping it for some new tongue-and-groove Cedar boards, new lights, and even speakers. But before any of the new... we need to get rid of the old...<\/p>","content":"<h2>Before<\/h2>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_46ba6d7.jpeg\" style=\"width: 30%; margin: 10px;\" alt=\"ye olde celing\" align=\"right\"><\/p>\n<p>Well... before I could put up any new tongue-and-groove Cedar, I needed some help getting rid of the old lumber. I couldn't have done it without the\nhelp of my mother, either!!!<\/p>\n<p>As much as she might've liked to have just put the Cedar boards right over what was already there, there was just <em>too much junk<\/em> up there. In the\nphoto above, you can see the painted particle-board that was the celing previously, and you can see just how... <em>elegant<\/em> it was.<\/p>\n<blockquote>\n<p><strong>Lovely.<\/strong><\/p>\n<\/blockquote>\n<p>So, I recently was able to anchor a great deal on these Cedar boards (again, thank you to my mother). Trouble is, I didn't want to just put them over the\n<em>junk<\/em> that was already there. So... we got to pulling those old boards down...<\/p>\n<h2>During<\/h2>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_2c5d0b8.jpeg\" style=\"width: 30%; margin: 10px;\" alt=\"under the surface lies... more mess!\"><\/p>\n<blockquote>\n<p>What's that, under there?<\/p>\n<\/blockquote>\n<p>To quote the old Barnaked Ladies song: <em>\"I just made you say underwear.\"<\/em> Well under that gross particle board is some old (and quite rough) tongue-and-groove\nlumber of an unknown age. It's in pretty rough shape, rotten, dry, and falling apart in a number of places.<\/p>\n<p>Those old boards were quite \"fun\" to pull, and they led to quite the disaster... Let me introduce you to my history lesson for the weekend.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_2b4ed17.jpeg\" style=\"width: 30%; margin: 10px;\" alt=\"what's all that black stuff?\" align=\"right\"><\/p>\n<p>Oh yes... see all that black powder, covering everything you see? It was covering us, too. You do not want to picture the color of my bathwater... let me just\nsay that.<\/p>\n<p>My mother and I were both a bit stumped by what the powder was, and why it was black... Well, after a bit of brainstorming, and chatting with some other folks\nwho have also renovated some of these older homes in Potlatch, we came upon the most likely answer: Mount St. Helens!<\/p>\n<p>Yep, that's right. We think all that black stuff is ash, just not from the home's chimney, from the biggest chimney the western United States has seen in the last\nhalf-century. (Weird to think that 1980 is that far back, isn't it?) From our own homegrown-research, this seems to be the most likely origin of that ash-dust.<\/p>\n<blockquote>\n<p>Still... <strong>Woah.<\/strong><\/p>\n<\/blockquote>\n<h2>After<\/h2>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_b58184b.jpeg\" style=\"width: 30%; margin: 10px;\" alt=\"clean, but not too clean\" align=\"right\"><\/p>\n<p>Now with all that nastiness cleaned out, we were able to get the whole porch cleaned up, ready for me to run my new wiring for lights, speakers, and special\neffects (more on that later). Now... I just need to get all of that done.<\/p>\n<hr>\n<h2>Want to see more Potlatch history?<\/h2>\n<p>Go check out <a href=\"https:\/\/kitstokes.github.io\/potlatch-portal\/\">this great initiative!<\/a><\/p>","category":[{"@attributes":{"term":"Home-Improvement"}},{"@attributes":{"term":"home-improvement"}},{"@attributes":{"term":"home-projects"}},{"@attributes":{"term":"home-renovation"}},{"@attributes":{"term":"improvement"}},{"@attributes":{"term":"renovation"}},{"@attributes":{"term":"restoration"}}]},{"title":"Making Drawing Circuits in Markdown a Cinch!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/making-drawing-circuits-in-markdown-a-cinch.html","rel":"alternate"}},"published":"2022-08-30T13:01:00-07:00","updated":"2022-08-30T13:01:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-08-30:\/making-drawing-circuits-in-markdown-a-cinch.html","summary":"<p>I've talked about how someone can make PlantUML diagrams come to life, directly in markdown for blog-sites, and I've touched on other automation techniques I use to make blogs come together from plain text, but how about some circuit diagrams? Well, there wasn't a neat tool to help make this a possibility, until now!<\/p>","content":"<p>So, wouldn't it be nice to be able to draw circuits right in your markdown? The same way that PlantUML drawings can be carved out from plain-text.<\/p>\n<blockquote>\n<p><em>Sure, but that technology doesn't exist... right?<\/em><\/p>\n<\/blockquote>\n<p>Hah! That's what you thought...<\/p>\n<p>Enter: <a href=\"https:\/\/github.com\/engineerjoe440\/schemdraw-markdown\"><code>schemdraw-markdown<\/code><\/a>, a brand-new tool built by yours truly that can take\n<a href=\"https:\/\/schemdraw.readthedocs.io\/en\/latest\/index.html\"><code>schemdraw<\/code><\/a> logic embedded in special blocks of markdown and build appropriate SVG circuits to illustrate\nthe circuits, embedding them directly in the markdown!<\/p>\n<p>I can't take all of the credit, there's some good folks who built the <a href=\"https:\/\/github.com\/mikitex70\/plantuml-markdown\"><code>plantuml-markdown<\/code> extension<\/a> extension,\nwhich I've already built into my Pelican-blogsite generation, allowing me to make those awesome little drawings in some of my\n<a href=\"https:\/\/blog.stanleysolutionsnw.com\/making-feline-stink-a-distant-memory.html\">past<\/a>\n<a href=\"https:\/\/blog.stanleysolutionsnw.com\/using-python-to-provide-simple-photo-connections-for-youth.html\">articles<\/a>. That tool was the starting-point for my work.\nCall it inspiration; call it shameless, code-plundering; they built an excellent framework which I was able to reconfigure to support <code>schemdraw<\/code>. Either way,\nthose folks built an awesome tool, and made it really easy for me to build something similar.<\/p>\n<h2>C'mon, let's show this off!<\/h2>\n<p>Want to see it in action? Here's a few samples:<\/p>\n<h4>The Standard Schemdraw Intro Diagram:<\/h4>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6bGFuZz0iZW4iIGhlaWdodD0iMTM5LjE2NnB0IiB3aWR0aD0iMTY3Ljk4OHB0IiB2aWV3Qm94PSItNDYuMzg4MDAwMDAwMDAwMDEgLTI2LjU2NiAxNjcuOTg4IDEzOS4xNjYiPjxjaXJjbGUgY3g9Ii0xLjI2ODA2NjUxOTY5MDQ0MDJlLTE0IiBjeT0iNTMuOTk5OTk5OTk5OTk5OTg2IiByPSIxOC4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTsiIC8+PHBhdGggZD0iTSAwLjAsLTAuMCBMIDM2LjAsLTAuMCBMIDM5LjAsLTkuMCBMIDQ1LjAsOS4wIEwgNTAuOTk5OTk5OTk5OTk5OTksLTkuMCBMIDU3LjAsOS4wIEwgNjMuMCwtOS4wIEwgNjkuMCw5LjAgTCA3Mi4wLC0wLjAgTCAxMDguMCwtMC4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gMTA4LjAsLTAuMCBMIDEwNy45OTk5OTk5OTk5OTk5OSw1MC43NiBNIDExNi45OTk5OTk5OTk5OTk5OSw1MC43NiBMIDk4Ljk5OTk5OTk5OTk5OTk5LDUwLjc2IE0gMTE2Ljk5OTk5OTk5OTk5OTk5LDU3LjIzOTk5OTk5OTk5OTk5NSBMIDk4Ljk5OTk5OTk5OTk5OTk5LDU3LjIzOTk5OTk5OTk5OTk5NSBNIDEwNy45OTk5OTk5OTk5OTk5OSw1Ny4yMzk5OTk5OTk5OTk5OTUgTCAxMDcuOTk5OTk5OTk5OTk5OTksMTA4LjAiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PHBhdGggZD0iTSAxMDcuOTk5OTk5OTk5OTk5OTksMTA4LjAgTCA1My45OTk5OTk5OTk5OTk5ODYsMTA4LjAgTCAtMS41OTg3MjExNTU0NjAyMjU0ZS0xNCwxMDcuOTk5OTk5OTk5OTk5OTkiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PHBhdGggZD0iTSAtMS41OTg3MjExNTU0NjAyMjU0ZS0xNCwxMDcuOTk5OTk5OTk5OTk5OTkgTCAtMS4zNzgyODQ3MzE2MTM3MDE4ZS0xNCw3MS45OTk5OTk5OTk5OTk5OSBMIC0xLjM3ODI4NDczMTYxMzcwMThlLTE0LDcxLjk5OTk5OTk5OTk5OTk5IE0gLTEuMTU3ODQ4MzA3NzY3MTc4M2UtMTQsMzUuOTk5OTk5OTk5OTk5OTg2IEwgLTEuMTU3ODQ4MzA3NzY3MTc4M2UtMTQsMzUuOTk5OTk5OTk5OTk5OTg2IEwgLTkuMzc0MTE4ODM5MjA2NTQ3ZS0xNSwtMS41OTg3MjExNTU0NjAyMjU0ZS0xNCIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48cGF0aCBkPSJNIDguOTk5OTk5OTk5OTk5OTg4LDUzLjk5OTk5OTk5OTk5OTk4NiBMIDguMjQ5OTk5OTk5OTk5OTg2LDUyLjEzNjUwMjg3NTI2MTgyNiBMIDcuNDk5OTk5OTk5OTk5OTg4NSw1MC4zOTk5OTk5OTk5OTk5OCBMIDYuNzQ5OTk5OTk5OTk5OTg4LDQ4LjkwODgzMTE3NTQ1Njg0IEwgNS45OTk5OTk5OTk5OTk5ODg1LDQ3Ljc2NDYxNzA5Mjc1MjAyNiBMIDUuMjQ5OTk5OTk5OTk5OTg4NSw0Ny4wNDUzMzQwNTA3MTg2OTQgTCA0LjQ5OTk5OTk5OTk5OTk4OCw0Ni43OTk5OTk5OTk5OTk5OCBMIDMuNzQ5OTk5OTk5OTk5OTg4LDQ3LjA0NTMzNDA1MDcxODY5IEwgMi45OTk5OTk5OTk5OTk5ODgsNDcuNzY0NjE3MDkyNzUyMDI2IEwgMi4yNDk5OTk5OTk5OTk5ODc2LDQ4LjkwODgzMTE3NTQ1Njg1IEwgMS40OTk5OTk5OTk5OTk5ODgyLDUwLjM5OTk5OTk5OTk5OTk4IEwgMC43NDk5OTk5OTk5OTk5ODc3LDUyLjEzNjUwMjg3NTI2MTgzIEwgLTEuMjY4MDY2NTE5NjkwNDQwMmUtMTQsNTMuOTk5OTk5OTk5OTk5OTg2IEwgLTAuNzUwMDAwMDAwMDAwMDEyMSw1NS44NjM0OTcxMjQ3MzgxMyBMIC0xLjUwMDAwMDAwMDAwMDAxMTUsNTcuNTk5OTk5OTk5OTk5OTggTCAtMi4yNTAwMDAwMDAwMDAwMTMsNTkuMDkxMTY4ODI0NTQzMTMgTCAtMy4wMDAwMDAwMDAwMDAwMTI0LDYwLjIzNTM4MjkwNzI0Nzk0IEwgLTMuNzUwMDAwMDAwMDAwMDExNSw2MC45NTQ2NjU5NDkyODEyNyBMIC00LjUwMDAwMDAwMDAwMDAxMyw2MS4xOTk5OTk5OTk5OTk5OCBMIC01LjI1MDAwMDAwMDAwMDAxMiw2MC45NTQ2NjU5NDkyODEyOCBMIC02LjAwMDAwMDAwMDAwMDAxMTUsNjAuMjM1MzgyOTA3MjQ3OTQgTCAtNi43NTAwMDAwMDAwMDAwMTMsNTkuMDkxMTY4ODI0NTQzMTI1IEwgLTcuNTAwMDAwMDAwMDAwMDEyLDU3LjU5OTk5OTk5OTk5OTk5IEwgLTguMjUwMDAwMDAwMDAwMDEyLDU1Ljg2MzQ5NzEyNDczODEzIEwgLTkuMDAwMDAwMDAwMDAwMDEyLDUzLjk5OTk5OTk5OTk5OTk4NiIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48Zz48Zz48Zz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfNDI0NCIgdmlld0JveD0iMCAtMzEuNTI0IDMuNzU2IDUxLjIxNiI+PHBhdGggZD0iTSAzLjc1NiAwIEwgMy43NTYgMCBMIDIuNzYgMCBMIDIuNzYgLTYuMzQ4IEwgMS4yMTIgLTYuMzQ4IEwgMS4yMTIgLTYuOTk2IFEgMS44NDggLTcuMDkyIDIuMjU2IC03LjIzIFEgMi42NjQgLTcuMzY4IDMgLTcuNTYgTCAzLjc1NiAtNy41NiBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfNDI0NCIgeD0iNDUuNzEyIiB5PSItNDkuMzc4IiB3aWR0aD0iNC4zODIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfODY4IiB2aWV3Qm94PSIwIC0zMS41MjQgOC43NzIgNTEuMjE2Ij48cGF0aCBkPSJNIDguNDk2IDAgTCA4LjQ5NiAwIEwgNS4zNjQgMCBMIDUuMzY0IC0wLjg2NCBRIDUuOTI4IC0xLjMyIDYuMzE4IC0xLjk1IFEgNi43MDggLTIuNTggNi45MDYgLTMuMjgyIFEgNy4xMDQgLTMuOTg0IDcuMTA0IC00LjY0NCBRIDcuMTA0IC02IDYuNDU2IC02Ljc2MiBRIDUuODA4IC03LjUyNCA0LjcwNCAtNy41MjQgUSAzLjU2NCAtNy41MjQgMi45MDQgLTYuNzUgUSAyLjI0NCAtNS45NzYgMi4yNDQgLTQuNjQ0IFEgMi4yNDQgLTMuOTk2IDIuNDM2IC0zLjI4MiBRIDIuNjI4IC0yLjU2OCAzLjAxOCAtMS45MzIgUSAzLjQwOCAtMS4yOTYgMy45ODQgLTAuODUyIEwgMy45ODQgMCBMIDAuODE2IDAgTCAwLjU2NCAtMi4xMjQgTCAwLjkxMiAtMi4xMjQgUSAxLjAyIC0xLjcxNiAxLjEyMiAtMS40NjQgUSAxLjIyNCAtMS4yMTIgMS40MSAtMS4wOTIgUSAxLjU5NiAtMC45NzIgMS45MzIgLTAuOTcyIEwgMy4wMjQgLTAuOTcyIEwgMy4wMjQgLTEuMDQ0IFEgMi42ODggLTEuMjk2IDIuMzIyIC0xLjYyNiBRIDEuOTU2IC0xLjk1NiAxLjY0NCAtMi4zODggUSAxLjMzMiAtMi44MiAxLjE0IC0zLjM5IFEgMC45NDggLTMuOTYgMC45NDggLTQuNjkyIFEgMC45NDggLTUuNjg4IDEuNDI4IC02LjQzOCBRIDEuOTA4IC03LjE4OCAyLjc2IC03LjYwOCBRIDMuNjEyIC04LjAyOCA0LjcxNiAtOC4wMjggUSA1LjgzMiAtOC4wMjggNi42NiAtNy42MDggUSA3LjQ4OCAtNy4xODggNy45NDQgLTYuNDM4IFEgOC40IC01LjY4OCA4LjQgLTQuNzA0IFEgOC40IC0zLjk3MiA4LjE5NiAtMy40MDIgUSA3Ljk5MiAtMi44MzIgNy42NzQgLTIuMzk0IFEgNy4zNTYgLTEuOTU2IDcuMDAyIC0xLjYyNiBRIDYuNjQ4IC0xLjI5NiA2LjMzNiAtMS4wNDQgTCA2LjMzNiAtMC45NzIgTCA3LjM0NCAtMC45NzIgUSA3LjcwNCAtMC45NzIgNy44OSAtMS4wOTIgUSA4LjA3NiAtMS4yMTIgOC4xOSAtMS40NyBRIDguMzA0IC0xLjcyOCA4LjQyNCAtMi4xMjQgTCA4Ljc3MiAtMi4xMjQgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzg2OCIgeD0iNTIuODI0IiB5PSItNDkuMzc4IiB3aWR0aD0iMTAuMjM0IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjwvZz48L2c+PGc+PGc+PGc+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzQyNDQiIHZpZXdCb3g9IjAgLTMxLjUyNCAzLjc1NiA1MS4yMTYiPjxwYXRoIGQ9Ik0gMy43NTYgMCBMIDMuNzU2IDAgTCAyLjc2IDAgTCAyLjc2IC02LjM0OCBMIDEuMjEyIC02LjM0OCBMIDEuMjEyIC02Ljk5NiBRIDEuODQ4IC03LjA5MiAyLjI1NiAtNy4yMyBRIDIuNjY0IC03LjM2OCAzIC03LjU2IEwgMy43NTYgLTcuNTYgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzQyNDQiIHg9IjY3Ljc3OCIgeT0iMjAuMzAyIiB3aWR0aD0iNC4zODIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfNDI0MyIgdmlld0JveD0iMCAtMzEuNTI0IDUuNTU2IDUxLjIxNiI+PHBhdGggZD0iTSAzLjA0OCAwLjE0NCBMIDMuMDQ4IDAuMTQ0IFEgMS44ODQgMC4xNDQgMS4yMTggLTAuODgyIFEgMC41NTIgLTEuOTA4IDAuNTUyIC0zLjgwNCBRIDAuNTUyIC01LjcxMiAxLjIxOCAtNi43MDIgUSAxLjg4NCAtNy42OTIgMy4wNDggLTcuNjkyIFEgNC4yMjQgLTcuNjkyIDQuODkgLTYuNjk2IFEgNS41NTYgLTUuNyA1LjU1NiAtMy44MDQgUSA1LjU1NiAtMS45MDggNC44OSAtMC44ODIgUSA0LjIyNCAwLjE0NCAzLjA0OCAwLjE0NCBaIE0gMy4wNDggLTAuNjcyIEwgMy4wNDggLTAuNjcyIFEgMy43MzIgLTAuNjcyIDQuMTUyIC0xLjQxIFEgNC41NzIgLTIuMTQ4IDQuNTcyIC0zLjgwNCBRIDQuNTcyIC01LjQ2IDQuMTUyIC02LjE3NCBRIDMuNzMyIC02Ljg4OCAzLjA0OCAtNi44ODggUSAyLjM3NiAtNi44ODggMS45NSAtNi4xNzQgUSAxLjUyNCAtNS40NiAxLjUyNCAtMy44MDQgUSAxLjUyNCAtMi4xNDggMS45NSAtMS40MSBRIDIuMzc2IC0wLjY3MiAzLjA0OCAtMC42NzIgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzQyNDMiIHg9Ijc0Ljg5IiB5PSIyMC4zMDIiIHdpZHRoPSI2LjQ4MiIgaGVpZ2h0PSI1OS43NTIiIGZpbGw9IndoaXRlIiAvPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl84OTYiIHZpZXdCb3g9IjAgLTMxLjUyNCA2LjY4NCA1MS4yMTYiPjxwYXRoIGQ9Ik0gNS40NzIgLTUuNTY4IEwgNS40NzIgLTUuNTY4IEwgNC45NTYgLTIuNjA0IFEgNC45NTYgLTIuNTU2IDQuOTY4IC0yLjM3NiBRIDQuOTggLTIuMTk2IDQuOTkyIC0yLjA2NCBRIDUuMDQgLTEuMzggNS4yMiAtMS4wNzQgUSA1LjQgLTAuNzY4IDUuNzM2IC0wLjc2OCBRIDYuMDI0IC0wLjc2OCA2LjE4NiAtMC45MTIgUSA2LjM0OCAtMS4wNTYgNi40OCAtMS4yMzYgTCA2LjY4NCAtMS4xMDQgUSA2LjU4OCAtMC44MjggNi40MTQgLTAuNTQgUSA2LjI0IC0wLjI1MiA1Ljk3IC0wLjA2NiBRIDUuNyAwLjEyIDUuMzA0IDAuMTIgUSA0Ljg2IDAuMTIgNC42OTggLTAuMTYyIFEgNC41MzYgLTAuNDQ0IDQuNDY0IC0wLjg2NCBRIDQuNDQgLTEuMDQ0IDQuNDEgLTEuMjY2IFEgNC4zOCAtMS40ODggNC4zMzIgLTEuOTY4IEwgNC4yNiAtMS45NjggUSA0LjA2OCAtMS4wOTIgMy43OTggLTAuNjMgUSAzLjUyOCAtMC4xNjggMy4yMjIgLTAuMDA2IFEgMi45MTYgMC4xNTYgMi42MTYgMC4xNTYgUSAyLjI5MiAwLjE1NiAyLjA3IC0wLjAxOCBRIDEuODQ4IC0wLjE5MiAxLjcxIC0wLjQyNiBRIDEuNTcyIC0wLjY2IDEuNDg4IC0wLjgyOCBMIDEuNDI4IC0wLjgyOCBRIDEuNDQgLTAuMzQ4IDEuNDUyIDAuMTAyIFEgMS40NjQgMC41NTIgMS41MDYgMS4wNjggUSAxLjU0OCAxLjU4NCAxLjYyIDIuMjkyIEwgMC43MiAyLjYwNCBMIDAuNTI4IDIuNTY4IFEgMC41NCAxLjcyOCAwLjU3IDEuMDM4IFEgMC42IDAuMzQ4IDAuNjc4IC0wLjMyNCBRIDAuNzU2IC0wLjk5NiAwLjkgLTEuNzg4IEwgMC44ODggLTUuNjQgTCAxLjk2OCAtNS42NzYgTCAyLjAxNiAtNS41OCBMIDEuNTg0IC0yLjYwNCBRIDEuNTg0IC0yLjYwNCAxLjU3OCAtMi41MjYgUSAxLjU3MiAtMi40NDggMS41NzIgLTIuMzQgUSAxLjU3MiAtMS45OCAxLjcxIC0xLjYzOCBRIDEuODQ4IC0xLjI5NiAyLjA5NCAtMS4wNjggUSAyLjM0IC0wLjg0IDIuNjY0IC0wLjg0IFEgMi45ODggLTAuODQgMy4yNTIgLTEuMDY4IFEgMy41MTYgLTEuMjk2IDMuNzIgLTEuNjQ0IFEgMy45MjQgLTEuOTkyIDQuMDYyIC0yLjM1MiBRIDQuMiAtMi43MTIgNC4yNzIgLTMgUSA0LjM0NCAtMy4yODggNC4zNDQgLTMuMzcyIEwgNC4zOCAtNS42NCBMIDUuNDEyIC01LjY3NiBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfODk2IiB4PSI4Mi4wMDIiIHk9IjIwLjMwMiIgd2lkdGg9IjcuNzk4IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzM2NjQiIHZpZXdCb3g9IjAgLTMxLjUyNCA1Ljc0OCA1MS4yMTYiPjxwYXRoIGQ9Ik0gMS4xMDQgMCBMIDEuMTA0IDAgTCAxLjEwNCAtNy44ODQgTCA1Ljc0OCAtNy44ODQgTCA1Ljc0OCAtNy4wMiBMIDIuMTM2IC03LjAyIEwgMi4xMzYgLTQuMzggTCA1LjE5NiAtNC4zOCBMIDUuMTk2IC0zLjUxNiBMIDIuMTM2IC0zLjUxNiBMIDIuMTM2IDAgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzM2NjQiIHg9Ijg5LjczIiB5PSIyMC4zMDIiIHdpZHRoPSI2LjcwNiIgaGVpZ2h0PSI1OS43NTIiIGZpbGw9IndoaXRlIiAvPjwvZz48L2c+PC9nPjxnPjxnPjxnPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl80MjQ0IiB2aWV3Qm94PSIwIC0zMS41MjQgMy43NTYgNTEuMjE2Ij48cGF0aCBkPSJNIDMuNzU2IDAgTCAzLjc1NiAwIEwgMi43NiAwIEwgMi43NiAtNi4zNDggTCAxLjIxMiAtNi4zNDggTCAxLjIxMiAtNi45OTYgUSAxLjg0OCAtNy4wOTIgMi4yNTYgLTcuMjMgUSAyLjY2NCAtNy4zNjggMyAtNy41NiBMIDMuNzU2IC03LjU2IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl80MjQ0IiB4PSItNDEuNzg4IiB5PSIyMS43MzciIHdpZHRoPSI0LjM4MiIgaGVpZ2h0PSI1OS43NTIiIGZpbGw9IndoaXRlIiAvPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl80MjQzIiB2aWV3Qm94PSIwIC0zMS41MjQgNS41NTYgNTEuMjE2Ij48cGF0aCBkPSJNIDMuMDQ4IDAuMTQ0IEwgMy4wNDggMC4xNDQgUSAxLjg4NCAwLjE0NCAxLjIxOCAtMC44ODIgUSAwLjU1MiAtMS45MDggMC41NTIgLTMuODA0IFEgMC41NTIgLTUuNzEyIDEuMjE4IC02LjcwMiBRIDEuODg0IC03LjY5MiAzLjA0OCAtNy42OTIgUSA0LjIyNCAtNy42OTIgNC44OSAtNi42OTYgUSA1LjU1NiAtNS43IDUuNTU2IC0zLjgwNCBRIDUuNTU2IC0xLjkwOCA0Ljg5IC0wLjg4MiBRIDQuMjI0IDAuMTQ0IDMuMDQ4IDAuMTQ0IFogTSAzLjA0OCAtMC42NzIgTCAzLjA0OCAtMC42NzIgUSAzLjczMiAtMC42NzIgNC4xNTIgLTEuNDEgUSA0LjU3MiAtMi4xNDggNC41NzIgLTMuODA0IFEgNC41NzIgLTUuNDYgNC4xNTIgLTYuMTc0IFEgMy43MzIgLTYuODg4IDMuMDQ4IC02Ljg4OCBRIDIuMzc2IC02Ljg4OCAxLjk1IC02LjE3NCBRIDEuNTI0IC01LjQ2IDEuNTI0IC0zLjgwNCBRIDEuNTI0IC0yLjE0OCAxLjk1IC0xLjQxIFEgMi4zNzYgLTAuNjcyIDMuMDQ4IC0wLjY3MiBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfNDI0MyIgeD0iLTM0LjY3NiIgeT0iMjEuNzM3IiB3aWR0aD0iNi40ODIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMzY4MCIgdmlld0JveD0iMCAtMzEuNTI0IDYuMzI0IDUxLjIxNiI+PHBhdGggZD0iTSAyLjU4IDAgTCAyLjU4IDAgTCAwLjAxMiAtNy44ODQgTCAxLjEwNCAtNy44ODQgTCAyLjQgLTMuNjM2IFEgMi42MDQgLTIuOTQgMi43NzIgLTIuMzM0IFEgMi45NCAtMS43MjggMy4xNjggLTEuMDMyIEwgMy4yMjggLTEuMDMyIFEgMy40NDQgLTEuNzI4IDMuNjE4IC0yLjMzNCBRIDMuNzkyIC0yLjk0IDMuOTk2IC0zLjYzNiBMIDUuMjggLTcuODg0IEwgNi4zMjQgLTcuODg0IEwgMy43NjggMCBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfMzY4MCIgeD0iLTI3LjU2NCIgeT0iMjEuNzM3IiB3aWR0aD0iNy4zNzgiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48L2c+PC9nPjwvZz48L3N2Zz4=\" alt=\"Schemdraw Basic Diagram\" title=\"\" \/><\/p>\n<details>\n  <summary>Click to expand all the Schemdraw-Markdown Goodness!<\/summary>\n\n<div class=\"highlight\"><pre><span><\/span><code>::_schemdraw_:: alt=&quot;My super diagram&quot; color=&quot;white&quot;\n    += elm.Resistor().right().label(&#39;1\u03a9&#39;)\n    += elm.Capacitor().down().label(&#39;10\u03bcF&#39;)\n    += elm.Line().left()\n    += elm.SourceSin().up().label(&#39;10V&#39;)\n::end-schemdraw::\n<\/code><\/pre><\/div>\n\n\n<\/details>\n\n<h4>Something a bit Juicier:<\/h4>\n<p>Example <a href=\"https:\/\/schemdraw.readthedocs.io\/en\/latest\/gallery\/analog.html#discharging-capacitor\">from Schemdraw Docs<\/a><\/p>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6bGFuZz0iZW4iIGhlaWdodD0iMTM2Ljk2MjAwMDAwMDAwMDAycHQiIHdpZHRoPSIyNTguOTcwMnB0IiB2aWV3Qm94PSItNDAuMzM5OTk5OTk5OTk5OTkgLTEyOS42NjIgMjU4Ljk3MDIgMTM2Ljk2MjAwMDAwMDAwMDAyIj48Y2lyY2xlIGN4PSIzLjMwNjU0NjM1NzY5Nzg1MzNlLTE1IiBjeT0iLTU0LjAiIHI9IjE4LjAiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotOyIgLz48cGF0aCBkPSJNIDgzLjU1IC05MS40MyBhIDkuMCAxMy41IC05MCAwIDEgMTMuNDMwMDAwMDAwMDAwMDA3IC00LjYyOTk5OTk5OTk5OTk5NTUiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotOyIgLz48cGF0aCBkPSJNIDEwMy45MjgyMjIxNjQxNTU5MyAtOTUuMjQxMjIyNDcxMjIxMzMgTCA5Ni42NjA4NzI1ODM3NjMxOCAtOTMuMzc2OTE4MjkxMDA2NzYgTCA5Ny4yOTEzMDg4NzU2NDQ3NyAtOTguNzM5OTkxMTExODY5NjggWiIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOndoaXRlO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyOyIgLz48cGF0aCBkPSJNIDAuMCwtMC4wIEwgMi4yMDQzNjQyMzg0NjUyMzZlLTE1LC0zNi4wIEwgMi4yMDQzNjQyMzg0NjUyMzZlLTE1LC0zNi4wIE0gNC40MDg3Mjg0NzY5MzA0NzJlLTE1LC03Mi4wIEwgNC40MDg3Mjg0NzY5MzA0NzJlLTE1LC03Mi4wIEwgNi42MTMwOTI3MTUzOTU3MDdlLTE1LC0xMDguMCIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48cGF0aCBkPSJNIDMuNjAwMDAwMDAwMDAwMDAzLC00NS4wIEwgLTMuNTk5OTk5OTk5OTk5OTk3LC00NS4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gMy42MzcyMDA5OTM0Njc2MzllLTE1LC01OS40IEwgNC4wNzgwNzM4NDExNjA2ODZlLTE1LC02Ni42MDAwMDAwMDAwMDAwMSIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48cGF0aCBkPSJNIDMuNjAwMDAwMDAwMDAwMDA0LC02My4wIEwgLTMuNTk5OTk5OTk5OTk5OTk2LC02My4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gNi42MTMwOTI3MTUzOTU3MDdlLTE1LC0xMDguMCBMIDQwLjUwMDAwMDAwMDAwMDAxLC0xMDguMCBMIDgxLjAsLTEwOC4wIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gOTUuMzk5OTk5OTk5OTk5OTksLTc2LjMyMDAwMDAwMDAwMDAxIE0gOTEuOCwtODQuOTYgTCA4Ni4zOTk5OTk5OTk5OTk5OSwtMTAxLjUyIE0gODEuMCwtMTEyLjMyMDAwMDAwMDAwMDAxIiBzdHlsZT0ic3Ryb2tlOndoaXRlO2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiAvPjxwYXRoIGQ9Ik0gMTA5LjgsLTEwOC4wIEwgMTUwLjI5OTk5OTk5OTk5OTk4LC0xMDguMCBMIDE5MC43OTk5OTk5OTk5OTk5OCwtMTA4LjAiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PHBhdGggZD0iTSAxOTAuNzk5OTk5OTk5OTk5OTgsLTEwOC4wIEwgMTkwLjc5OTk5OTk5OTk5OTk4LC03Mi4wIEwgMTk5Ljc5OTk5OTk5OTk5OTk4LC02OS4wIEwgMTgxLjc5OTk5OTk5OTk5OTk4LC02My4wIEwgMTk5Ljc5OTk5OTk5OTk5OTk4LC01Ny4wMDAwMDAwMDAwMDAwMSBMIDE4MS43OTk5OTk5OTk5OTk5OCwtNTEuMCBMIDE5OS43OTk5OTk5OTk5OTk5OCwtNDUuMCBMIDE4MS43OTk5OTk5OTk5OTk5OCwtMzkuMDAwMDAwMDAwMDAwMDEgTCAxOTAuNzk5OTk5OTk5OTk5OTgsLTM2LjAgTCAxOTAuNzk5OTk5OTk5OTk5OTUsLTAuMCIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOm5vbmU7c3Ryb2tlLXdpZHRoOjIuMDtzdHJva2UtZGFzaGFycmF5Oi07c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kOyIgLz48cGF0aCBkPSJNIDE5MC43OTk5OTk5OTk5OTk5NSwtMC4wIEwgOTUuMzk5OTk5OTk5OTk5OTgsLTEuMTY4MzEzMDQ2Mzg2NTc0OGUtMTQgTCAwLjAsLTIuMzM2NjI2MDkyNzczMTQ5N2UtMTQiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PHBhdGggZD0iTSA5NS4zOTk5OTk5OTk5OTk5OSwtODAuNjQwMDAwMDAwMDAwMDEgTCA5NS4zOTk5OTk5OTk5OTk5OSwtNDMuNTYwMDAwMDAwMDAwMDEgTSAxMDQuMzk5OTk5OTk5OTk5OTksLTQzLjU2MDAwMDAwMDAwMDAxIEwgODYuMzk5OTk5OTk5OTk5OTksLTQzLjU2MDAwMDAwMDAwMDAxIE0gMTA0LjM5OTk5OTk5OTk5OTk5LC0zNy4wODAwMDAwMDAwMDAwMSBMIDg2LjM5OTk5OTk5OTk5OTk5LC0zNy4wODAwMDAwMDAwMDAwMSBNIDk1LjM5OTk5OTk5OTk5OTk5LC0zNy4wODAwMDAwMDAwMDAwMSBMIDk1LjM5OTk5OTk5OTk5OTk5LC0wLjAiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDpub25lO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDsiIC8+PGc+PGc+PGc+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzQyNDgiIHZpZXdCb3g9IjAgLTMxLjUyNCA1LjUwOCA1MS4yMTYiPjxwYXRoIGQ9Ik0gMi44NjggMC4xNDQgTCAyLjg2OCAwLjE0NCBRIDEuOTMyIDAuMTQ0IDEuMzIgLTAuMTggUSAwLjcwOCAtMC41MDQgMC4zIC0wLjg4OCBMIDAuODA0IC0xLjU0OCBRIDEuMTUyIC0xLjIxMiAxLjYxNCAtMC45NTQgUSAyLjA3NiAtMC42OTYgMi43NzIgLTAuNjk2IFEgMy40OCAtMC42OTYgMy45NzggLTEuMTU4IFEgNC40NzYgLTEuNjIgNC40NzYgLTIuNCBRIDQuNDc2IC0zLjE4IDQuMDI2IC0zLjYwNiBRIDMuNTc2IC00LjAzMiAyLjgzMiAtNC4wMzIgUSAyLjQyNCAtNC4wMzIgMi4xMyAtMy45MTggUSAxLjgzNiAtMy44MDQgMS40ODggLTMuNTc2IEwgMC45MzYgLTMuOTI0IEwgMS4yIC03LjU2IEwgNS4xMTIgLTcuNTYgTCA1LjExMiAtNi42ODQgTCAyLjA4OCAtNi42ODQgTCAxLjg4NCAtNC40ODggUSAyLjE2IC00LjYzMiAyLjQzNiAtNC43MTYgUSAyLjcxMiAtNC44IDMuMDg0IC00LjggUSAzLjc1NiAtNC44IDQuMzAyIC00LjU0OCBRIDQuODQ4IC00LjI5NiA1LjE3OCAtMy43NzQgUSA1LjUwOCAtMy4yNTIgNS41MDggLTIuNDI0IFEgNS41MDggLTEuNjA4IDUuMTM2IC0xLjAzMiBRIDQuNzY0IC0wLjQ1NiA0LjE1OCAtMC4xNTYgUSAzLjU1MiAwLjE0NCAyLjg2OCAwLjE0NCBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfNDI0OCIgeD0iLTM1Ljc0IiB5PSItODYuMjYzIiB3aWR0aD0iNi40MjYiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMzY4MCIgdmlld0JveD0iMCAtMzEuNTI0IDYuMzI0IDUxLjIxNiI+PHBhdGggZD0iTSAyLjU4IDAgTCAyLjU4IDAgTCAwLjAxMiAtNy44ODQgTCAxLjEwNCAtNy44ODQgTCAyLjQgLTMuNjM2IFEgMi42MDQgLTIuOTQgMi43NzIgLTIuMzM0IFEgMi45NCAtMS43MjggMy4xNjggLTEuMDMyIEwgMy4yMjggLTEuMDMyIFEgMy40NDQgLTEuNzI4IDMuNjE4IC0yLjMzNCBRIDMuNzkyIC0yLjk0IDMuOTk2IC0zLjYzNiBMIDUuMjggLTcuODg0IEwgNi4zMjQgLTcuODg0IEwgMy43NjggMCBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfMzY4MCIgeD0iLTI4LjYyOCIgeT0iLTg2LjI2MyIgd2lkdGg9IjcuMzc4IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjwvZz48L2c+PGNpcmNsZSBjeD0iOTUuMzk5OTk5OTk5OTk5OTkiIGN5PSItODAuNjQwMDAwMDAwMDAwMDEiIHI9IjQuMzIiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDp3aGl0ZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTsiIC8+PGNpcmNsZSBjeD0iMTA5LjgiIGN5PSItMTA4LjAiIHI9IjQuMzIiIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDp3aGl0ZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTsiIC8+PGNpcmNsZSBjeD0iODEuMCIgY3k9Ii0xMDguMCIgcj0iNC4zMiIgc3R5bGU9InN0cm9rZTp3aGl0ZTtmaWxsOndoaXRlO3N0cm9rZS13aWR0aDoyLjA7c3Ryb2tlLWRhc2hhcnJheTotOyIgLz48Zz48Zz48Zz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMzM0NyIgdmlld0JveD0iMCAtMzEuNTI0IDMuODA0IDUxLjIxNiI+PHBhdGggZD0iTSAzLjgwNCAtNS42MDQgTCAzLjgwNCAtNS42MDQgTCAzLjcwOCAtNS4xMzYgTCAyLjQ5NiAtNS4xMzYgTCAxLjc4OCAtMS41NiBRIDEuNzg4IC0xLjU2IDEuNzQ2IC0xLjMzOCBRIDEuNzA0IC0xLjExNiAxLjcwNCAtMC45NiBRIDEuNzA0IC0wLjg2NCAxLjc0NiAtMC43ODYgUSAxLjc4OCAtMC43MDggMS45MDggLTAuNzA4IFEgMi4xNzIgLTAuNzA4IDIuMzk0IC0wLjg2NCBRIDIuNjE2IC0xLjAyIDIuOTI4IC0xLjQyOCBMIDMuMTY4IC0xLjI0OCBRIDMgLTEuMDIgMi43NiAtMC42OTYgUSAyLjUyIC0wLjM3MiAyLjE4NCAtMC4xMzIgUSAxLjg0OCAwLjEwOCAxLjM2OCAwLjEwOCBRIDAuOTk2IDAuMTA4IDAuODE2IC0wLjA3MiBRIDAuNjM2IC0wLjI1MiAwLjYzNiAtMC41MjggUSAwLjYzNiAtMC42ODQgMC42NzIgLTAuOTA2IFEgMC43MDggLTEuMTI4IDAuNzIgLTEuMjEyIEwgMS41MzYgLTUuMTM2IEwgMC44NTIgLTUuMTM2IEwgMC45IC01LjM4OCBRIDEuMjg0IC01LjU5MiAxLjU0MiAtNS43ODQgUSAxLjggLTUuOTc2IDIuMDM0IC02LjI4OCBRIDIuMjY4IC02LjYgMi41NjggLTcuMTE2IEwgMi45MjggLTcuMTE2IEwgMi41OTIgLTUuNjA0IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl8zMzQ3IiB4PSI3OS43MTYiIHk9Ii0xNTIuODUyIiB3aWR0aD0iNC40MzgiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMTIwMiIgdmlld0JveD0iMCAtMzEuNTI0IDcuODk2IDUxLjIxNiI+PHBhdGggZD0iTSA3Ljg5NiAtMy45MjQgTCA3Ljg5NiAtMy45MjQgTCAwLjc0NCAtMy45MjQgTCAwLjc0NCAtNC43NCBMIDcuODk2IC00Ljc0IFogTSA3Ljg5NiAtMS40NzYgTCA3Ljg5NiAtMS40NzYgTCAwLjc0NCAtMS40NzYgTCAwLjc0NCAtMi4yOCBMIDcuODk2IC0yLjI4IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl8xMjAyIiB4PSI4OS42MzIiIHk9Ii0xNTIuODUyIiB3aWR0aD0iOS4yMTIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMTEzNyIgdmlld0JveD0iMCAtMzEuNTI0IDUuNjE2IDUxLjIxNiI+PHBhdGggZD0iTSA1LjYxNiAtMy44MDQgTCA1LjYxNiAtMy44MDQgUSA1LjYxNiAtMy4wMzYgNS40NjYgLTIuMzI4IFEgNS4zMTYgLTEuNjIgNC45OTggLTEuMDY4IFEgNC42OCAtMC41MTYgNC4xNzYgLTAuMTkyIFEgMy42NzIgMC4xMzIgMi45NjQgMC4xMzIgUSAyLjI0NCAwLjEzMiAxLjc0IC0wLjE4NiBRIDEuMjM2IC0wLjUwNCAwLjkyNCAtMS4wNTYgUSAwLjYxMiAtMS42MDggMC40NjggLTIuMzI4IFEgMC4zMjQgLTMuMDQ4IDAuMzI0IC0zLjg0IFEgMC4zMjQgLTQuOTggMC42MyAtNS44NSBRIDAuOTM2IC02LjcyIDEuNTMgLTcuMjEyIFEgMi4xMjQgLTcuNzA0IDIuOTg4IC03LjcwNCBRIDMuNzY4IC03LjcwNCA0LjM1NiAtNy4yNDIgUSA0Ljk0NCAtNi43OCA1LjI4IC01LjkxIFEgNS42MTYgLTUuMDQgNS42MTYgLTMuODA0IFogTSA0LjQ1MiAtMy43MzIgTCA0LjQ1MiAtMy43MzIgUSA0LjQ1MiAtNS41NTYgNC4wNzQgLTYuNDI2IFEgMy42OTYgLTcuMjk2IDIuOTUyIC03LjI5NiBRIDIuMjQ0IC03LjI5NiAxLjg2IC02LjQzMiBRIDEuNDc2IC01LjU2OCAxLjQ3NiAtMy43OCBRIDEuNDc2IC0yLjA0IDEuODYgLTEuMTcgUSAyLjI0NCAtMC4zIDIuOTY0IC0wLjMgUSAzLjY3MiAtMC4zIDQuMDYyIC0xLjE1MiBRIDQuNDUyIC0yLjAwNCA0LjQ1MiAtMy43MzIgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzExMzciIHg9IjEwNC44OTYiIHk9Ii0xNTIuODUyIiB3aWR0aD0iNi41NTIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48L2c+PC9nPjwvZz48Zz48Zz48Zz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMTEzOCIgdmlld0JveD0iMCAtMzEuNTI0IDUuMTEyIDUxLjIxNiI+PHBhdGggZD0iTSA1LjExMiAwIEwgNS4xMTIgMCBMIDAuOTYgMCBMIDAuOTYgLTAuMzM2IFEgMS44MTIgLTAuMzM2IDIuMTYgLTAuNTE2IFEgMi41MDggLTAuNjk2IDIuNTA4IC0xLjE0IEwgMi41MDggLTYuMjE2IFEgMi41MDggLTYuNTI4IDIuNDI0IC02LjY2NiBRIDIuMzQgLTYuODA0IDIuMTEyIC02LjgwNCBRIDEuOTMyIC02LjgwNCAxLjU0OCAtNi43NTYgUSAxLjE2NCAtNi43MDggMC44ODggLTYuNjEyIEwgMC44ODggLTcuMDA4IEwgMy4yMTYgLTcuNjY4IEwgMy41NjQgLTcuNjY4IEwgMy41NjQgLTEuMTQgUSAzLjU2NCAtMC42OTYgMy45MjQgLTAuNTE2IFEgNC4yODQgLTAuMzM2IDUuMTEyIC0wLjMzNiBaICIgLz48L3N5bWJvbD48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfMTEzOCIgeD0iMTQ3LjU2OCIgeT0iLTg2LjE3MiIgd2lkdGg9IjUuOTY0IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzExMzciIHZpZXdCb3g9IjAgLTMxLjUyNCA1LjYxNiA1MS4yMTYiPjxwYXRoIGQ9Ik0gNS42MTYgLTMuODA0IEwgNS42MTYgLTMuODA0IFEgNS42MTYgLTMuMDM2IDUuNDY2IC0yLjMyOCBRIDUuMzE2IC0xLjYyIDQuOTk4IC0xLjA2OCBRIDQuNjggLTAuNTE2IDQuMTc2IC0wLjE5MiBRIDMuNjcyIDAuMTMyIDIuOTY0IDAuMTMyIFEgMi4yNDQgMC4xMzIgMS43NCAtMC4xODYgUSAxLjIzNiAtMC41MDQgMC45MjQgLTEuMDU2IFEgMC42MTIgLTEuNjA4IDAuNDY4IC0yLjMyOCBRIDAuMzI0IC0zLjA0OCAwLjMyNCAtMy44NCBRIDAuMzI0IC00Ljk4IDAuNjMgLTUuODUgUSAwLjkzNiAtNi43MiAxLjUzIC03LjIxMiBRIDIuMTI0IC03LjcwNCAyLjk4OCAtNy43MDQgUSAzLjc2OCAtNy43MDQgNC4zNTYgLTcuMjQyIFEgNC45NDQgLTYuNzggNS4yOCAtNS45MSBRIDUuNjE2IC01LjA0IDUuNjE2IC0zLjgwNCBaIE0gNC40NTIgLTMuNzMyIEwgNC40NTIgLTMuNzMyIFEgNC40NTIgLTUuNTU2IDQuMDc0IC02LjQyNiBRIDMuNjk2IC03LjI5NiAyLjk1MiAtNy4yOTYgUSAyLjI0NCAtNy4yOTYgMS44NiAtNi40MzIgUSAxLjQ3NiAtNS41NjggMS40NzYgLTMuNzggUSAxLjQ3NiAtMi4wNCAxLjg2IC0xLjE3IFEgMi4yNDQgLTAuMyAyLjk2NCAtMC4zIFEgMy42NzIgLTAuMyA0LjA2MiAtMS4xNTIgUSA0LjQ1MiAtMi4wMDQgNC40NTIgLTMuNzMyIFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl8xMTM3IiB4PSIxNTQuNDk4IiB5PSItODYuMTcyIiB3aWR0aD0iNi41NTIiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48dXNlIGhyZWY9IiNTVElYVHdvTWF0aFJlZ3VsYXJfMTEzNyIgeD0iMTYxLjQyOCIgeT0iLTg2LjE3MiIgd2lkdGg9IjYuNTUyIiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzg2OCIgdmlld0JveD0iMCAtMzEuNTI0IDguNzcyIDUxLjIxNiI+PHBhdGggZD0iTSA4LjQ5NiAwIEwgOC40OTYgMCBMIDUuMzY0IDAgTCA1LjM2NCAtMC44NjQgUSA1LjkyOCAtMS4zMiA2LjMxOCAtMS45NSBRIDYuNzA4IC0yLjU4IDYuOTA2IC0zLjI4MiBRIDcuMTA0IC0zLjk4NCA3LjEwNCAtNC42NDQgUSA3LjEwNCAtNiA2LjQ1NiAtNi43NjIgUSA1LjgwOCAtNy41MjQgNC43MDQgLTcuNTI0IFEgMy41NjQgLTcuNTI0IDIuOTA0IC02Ljc1IFEgMi4yNDQgLTUuOTc2IDIuMjQ0IC00LjY0NCBRIDIuMjQ0IC0zLjk5NiAyLjQzNiAtMy4yODIgUSAyLjYyOCAtMi41NjggMy4wMTggLTEuOTMyIFEgMy40MDggLTEuMjk2IDMuOTg0IC0wLjg1MiBMIDMuOTg0IDAgTCAwLjgxNiAwIEwgMC41NjQgLTIuMTI0IEwgMC45MTIgLTIuMTI0IFEgMS4wMiAtMS43MTYgMS4xMjIgLTEuNDY0IFEgMS4yMjQgLTEuMjEyIDEuNDEgLTEuMDkyIFEgMS41OTYgLTAuOTcyIDEuOTMyIC0wLjk3MiBMIDMuMDI0IC0wLjk3MiBMIDMuMDI0IC0xLjA0NCBRIDIuNjg4IC0xLjI5NiAyLjMyMiAtMS42MjYgUSAxLjk1NiAtMS45NTYgMS42NDQgLTIuMzg4IFEgMS4zMzIgLTIuODIgMS4xNCAtMy4zOSBRIDAuOTQ4IC0zLjk2IDAuOTQ4IC00LjY5MiBRIDAuOTQ4IC01LjY4OCAxLjQyOCAtNi40MzggUSAxLjkwOCAtNy4xODggMi43NiAtNy42MDggUSAzLjYxMiAtOC4wMjggNC43MTYgLTguMDI4IFEgNS44MzIgLTguMDI4IDYuNjYgLTcuNjA4IFEgNy40ODggLTcuMTg4IDcuOTQ0IC02LjQzOCBRIDguNCAtNS42ODggOC40IC00LjcwNCBRIDguNCAtMy45NzIgOC4xOTYgLTMuNDAyIFEgNy45OTIgLTIuODMyIDcuNjc0IC0yLjM5NCBRIDcuMzU2IC0xLjk1NiA3LjAwMiAtMS42MjYgUSA2LjY0OCAtMS4yOTYgNi4zMzYgLTEuMDQ0IEwgNi4zMzYgLTAuOTcyIEwgNy4zNDQgLTAuOTcyIFEgNy43MDQgLTAuOTcyIDcuODkgLTEuMDkyIFEgOC4wNzYgLTEuMjEyIDguMTkgLTEuNDcgUSA4LjMwNCAtMS43MjggOC40MjQgLTIuMTI0IEwgOC43NzIgLTIuMTI0IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl84NjgiIHg9IjE2OC4zNTgiIHk9Ii04Ni4xNzIiIHdpZHRoPSIxMC4yMzQiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48L2c+PC9nPjwvZz48Zz48Zz48Zz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfMTE5NiIgdmlld0JveD0iMCAtMzEuNTI0IDcuODk2IDUxLjIxNiI+PHBhdGggZD0iTSA3Ljg5NiAtMi43IEwgNy44OTYgLTIuNyBMIDQuNzUyIC0yLjcgTCA0Ljc1MiAwLjQ4IEwgMy44ODggMC40OCBMIDMuODg4IC0yLjcgTCAwLjc0NCAtMi43IEwgMC43NDQgLTMuNTE2IEwgMy44ODggLTMuNTE2IEwgMy44ODggLTYuNjk2IEwgNC43NTIgLTYuNjk2IEwgNC43NTIgLTMuNTE2IEwgNy44OTYgLTMuNTE2IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl8xMTk2IiB4PSIyMDMuNCIgeT0iLTExNC4xNTIiIHdpZHRoPSI5LjIxMiIgaGVpZ2h0PSI1OS43NTIiIGZpbGw9IndoaXRlIiAvPjwvZz48L2c+PC9nPjxnPjxnPjxnPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl8zMzUwIiB2aWV3Qm94PSIwIC0zMS41MjQgNS43NDggNTEuMjE2Ij48cGF0aCBkPSJNIDIuNCAtNS43MTIgTCAyLjQgLTUuNzEyIEwgMi42NjQgLTUuNzEyIEwgMi4xIC0zLjQwOCBRIDIuMDE2IC0zLjA5NiAxLjkyNiAtMi42NTggUSAxLjgzNiAtMi4yMiAxLjgzNiAtMS44MTIgUSAxLjgzNiAtMS4zNDQgMi4wMjggLTEuMDAyIFEgMi4yMiAtMC42NiAyLjc3MiAtMC42NiBRIDMuMzg0IC0wLjY2IDMuODI4IC0wLjk1NCBRIDQuMjcyIC0xLjI0OCA0LjU1NCAtMS43MDQgUSA0LjgzNiAtMi4xNiA0Ljk3NCAtMi42NjQgUSA1LjExMiAtMy4xNjggNS4xMTIgLTMuNTg4IFEgNS4xMTIgLTMuOSA1LjAwNCAtNC4xMjggUSA0Ljg5NiAtNC4zNTYgNC43NDYgLTQuNTM2IFEgNC41OTYgLTQuNzE2IDQuNDg4IC00Ljg4NCBRIDQuMzggLTUuMDUyIDQuMzggLTUuMjQ0IFEgNC4zOCAtNS40ODQgNC41MTIgLTUuNjE2IFEgNC42NDQgLTUuNzQ4IDQuODM2IC01Ljc0OCBRIDUuMjQ0IC01Ljc0OCA1LjQ5NiAtNS4zNCBRIDUuNzQ4IC00LjkzMiA1Ljc0OCAtNC4yOTYgUSA1Ljc0OCAtMy43NDQgNS42MSAtMy4xMzggUSA1LjQ3MiAtMi41MzIgNS4xOTYgLTEuOTU2IFEgNC45MiAtMS4zOCA0LjUxMiAtMC45MTIgUSA0LjEwNCAtMC40NDQgMy41NzYgLTAuMTY4IFEgMy4wNDggMC4xMDggMi40IDAuMTA4IFEgMS44IDAuMTA4IDEuNDY0IC0wLjEzMiBRIDEuMTI4IC0wLjM3MiAwLjk5IC0wLjc2OCBRIDAuODUyIC0xLjE2NCAwLjg1MiAtMS42MiBRIDAuODUyIC0yLjA1MiAwLjkzNiAtMi41MDggUSAxLjAyIC0yLjk2NCAxLjExNiAtMy4zMzYgTCAxLjM1NiAtNC4yNDggUSAxLjM4IC00LjM0NCAxLjQyOCAtNC41MzYgUSAxLjQ3NiAtNC43MjggMS40NzYgLTQuOTA4IFEgMS40NzYgLTUuMDY0IDEuNDE2IC01LjE3MiBRIDEuMzU2IC01LjI4IDEuMTg4IC01LjI4IFEgMS4wNDQgLTUuMjggMC44NCAtNS4yNjIgUSAwLjYzNiAtNS4yNDQgMC42MzYgLTUuMjQ0IEwgMC42MzYgLTUuNTggWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzMzNTAiIHg9IjIwMy40IiB5PSItODguOTU0IiB3aWR0aD0iNi43MDYiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48c3ltYm9sIGlkPSJTVElYVHdvTWF0aFJlZ3VsYXJfNDQzOCIgdmlld0JveD0iMCAtMzEuNTI0IDYuMyA1MS4yMTYiPjxwYXRoIGQ9Ik0gNi4zIC0zLjc2OCBMIDYuMyAtMy43NjggUSA2LjMgLTIuNjc2IDUuODUgLTEuNzg4IFEgNS40IC0wLjkgNC41OTYgLTAuMzc4IFEgMy43OTIgMC4xNDQgMi43MjQgMC4xNDQgUSAxLjkwOCAwLjE0NCAxLjQwNCAtMC4yMDQgUSAwLjkgLTAuNTUyIDAuNjY2IC0xLjEwNCBRIDAuNDMyIC0xLjY1NiAwLjQzMiAtMi4yNTYgUSAwLjQzMiAtMy4wMjQgMC42ODQgLTMuNzMyIFEgMC45MzYgLTQuNDQgMS40MDQgLTQuOTk4IFEgMS44NzIgLTUuNTU2IDIuNTIgLTUuODggUSAzLjE2OCAtNi4yMDQgMy45NiAtNi4yMDQgUSA0LjU2IC02LjIwNCA1LjA5NCAtNS45MjIgUSA1LjYyOCAtNS42NCA1Ljk2NCAtNS4wOTQgUSA2LjMgLTQuNTQ4IDYuMyAtMy43NjggWiBNIDUuMDQgLTMuODQgTCA1LjA0IC0zLjg0IFEgNS4wNCAtNC4zNTYgNC45MiAtNC43ODIgUSA0LjggLTUuMjA4IDQuNTQyIC01LjQ2NiBRIDQuMjg0IC01LjcyNCAzLjg1MiAtNS43MjQgUSAzLjI2NCAtNS43MjQgMi43NzIgLTUuMzA0IFEgMi4yOCAtNC44ODQgMS45ODYgLTQuMDk4IFEgMS42OTIgLTMuMzEyIDEuNjkyIC0yLjE4NCBRIDEuNjkyIC0xLjcxNiAxLjc4OCAtMS4yOTYgUSAxLjg4NCAtMC44NzYgMi4xMzYgLTAuNjE4IFEgMi4zODggLTAuMzYgMi44NTYgLTAuMzYgUSAzLjg1MiAtMC4zNiA0LjQ0NiAtMS4yNjYgUSA1LjA0IC0yLjE3MiA1LjA0IC0zLjg0IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl80NDM4IiB4PSIyMTAuMTA2IiB5PSItNzQuOTgiIHdpZHRoPSI1LjE0NSIgaGVpZ2h0PSI0MS44MjYiIGZpbGw9IndoaXRlIiAvPjwvZz48L2c+PC9nPjxnPjxnPjxnPjxyZWN0IHg9IjIwMy40IiB5PSItMzEuMDg4IiB3aWR0aD0iMy43OCIgaGVpZ2h0PSIwLjk1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjwvZz48L2c+PGNpcmNsZSBjeD0iOTUuMzk5OTk5OTk5OTk5OTkiIGN5PSItMC4wIiByPSIyLjY5OTk5OTk5OTk5OTk5OTciIHN0eWxlPSJzdHJva2U6d2hpdGU7ZmlsbDp3aGl0ZTtzdHJva2Utd2lkdGg6Mi4wO3N0cm9rZS1kYXNoYXJyYXk6LTsiIC8+PGc+PGc+PGc+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzQyNDQiIHZpZXdCb3g9IjAgLTMxLjUyNCAzLjc1NiA1MS4yMTYiPjxwYXRoIGQ9Ik0gMy43NTYgMCBMIDMuNzU2IDAgTCAyLjc2IDAgTCAyLjc2IC02LjM0OCBMIDEuMjEyIC02LjM0OCBMIDEuMjEyIC02Ljk5NiBRIDEuODQ4IC03LjA5MiAyLjI1NiAtNy4yMyBRIDIuNjY0IC03LjM2OCAzIC03LjU2IEwgMy43NTYgLTcuNTYgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzQyNDQiIHg9IjYyLjQwMiIgeT0iLTczLjk1NiIgd2lkdGg9IjQuMzgyIiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjxnPjxzeW1ib2wgaWQ9IlNUSVhUd29NYXRoUmVndWxhcl80MDI2IiB2aWV3Qm94PSItMC4yODggLTMxLjUyNCA2Ljg4OCA1MS4yMTYiPjxwYXRoIGQ9Ik0gNi4zMzYgLTUuNTY4IEwgNi4zMzYgLTUuNTY4IEwgNS4xNiAtMi42MDQgUSA1LjE2IC0yLjU1NiA1LjEyNCAtMi4yOTggUSA1LjA4OCAtMi4wNCA1LjA4OCAtMS44MzYgUSA1LjA4OCAtMS4yNDggNS4yMjYgLTEuMDIgUSA1LjM2NCAtMC43OTIgNS42MjggLTAuNzkyIFEgNS45MTYgLTAuNzkyIDYuMDg0IC0wLjk0OCBRIDYuMjUyIC0xLjEwNCA2LjQwOCAtMS4yNzIgTCA2LjYgLTEuMDY4IFEgNi40NTYgLTAuNzkyIDYuMjUyIC0wLjUyMiBRIDYuMDQ4IC0wLjI1MiA1Ljc2NiAtMC4wNzggUSA1LjQ4NCAwLjA5NiA1LjA4OCAwLjA5NiBRIDQuNjQ0IDAuMDk2IDQuNDcgLTAuMTc0IFEgNC4yOTYgLTAuNDQ0IDQuMjk2IC0wLjg2NCBRIDQuMjk2IC0xLjAzMiA0LjMyIC0xLjI2IFEgNC4zNDQgLTEuNDg4IDQuMzkyIC0xLjk2OCBMIDQuMzIgLTEuOTY4IFEgMy45NiAtMS4wOTIgMy42IC0wLjYzIFEgMy4yNCAtMC4xNjggMi45MSAtMC4wMDYgUSAyLjU4IDAuMTU2IDIuMjggMC4xNTYgUSAxLjk1NiAwLjE1NiAxLjc2NCAtMC4wMTggUSAxLjU3MiAtMC4xOTIgMS40NzYgLTAuNDI2IFEgMS4zOCAtMC42NiAxLjMzMiAtMC44MjggTCAxLjI3MiAtMC44MjggUSAxLjEyOCAtMC4wOTYgMS4wOTIgMC41MjggUSAxLjA1NiAxLjE1MiAwLjg4OCAxLjgyNCBRIDAuNzY4IDIuMzE2IDAuNDc0IDIuNDYgUSAwLjE4IDIuNjA0IC0wLjEyIDIuNjA0IEwgLTAuMjg4IDIuNTY4IFEgLTAuMDk2IDEuNzUyIDAuMDcyIDEuMDYyIFEgMC4yNCAwLjM3MiAwLjQ1IC0wLjMxMiBRIDAuNjYgLTAuOTk2IDAuOTM2IC0xLjc4OCBRIDEuMDMyIC0yLjI2OCAxLjE0IC0yLjgyNiBRIDEuMjQ4IC0zLjM4NCAxLjM1IC0zLjkzIFEgMS40NTIgLTQuNDc2IDEuNTQyIC00LjkyNiBRIDEuNjMyIC01LjM3NiAxLjcwNCAtNS42NCBMIDIuNzg0IC01LjY3NiBMIDIuODQ0IC01LjU2OCBMIDEuNzUyIC0yLjYwNCBRIDEuNzUyIC0yLjYwNCAxLjcxNiAtMi40NDIgUSAxLjY4IC0yLjI4IDEuNjggLTIuMDQgUSAxLjY4IC0xLjU5NiAxLjg2NiAtMS4yMTggUSAyLjA1MiAtMC44NCAyLjQ5NiAtMC44NCBRIDIuODIgLTAuODQgMy4xMzIgLTEuMDY4IFEgMy40NDQgLTEuMjk2IDMuNzIgLTEuNjQ0IFEgMy45OTYgLTEuOTkyIDQuMjA2IC0yLjM1MiBRIDQuNDE2IC0yLjcxMiA0LjU0MiAtMyBRIDQuNjY4IC0zLjI4OCA0LjY5MiAtMy4zNzIgUSA0LjggLTMuODY0IDQuOTM4IC00LjUwNiBRIDUuMDc2IC01LjE0OCA1LjIyIC01LjY0IEwgNi4yNzYgLTUuNjc2IFogIiAvPjwvc3ltYm9sPjx1c2UgaHJlZj0iI1NUSVhUd29NYXRoUmVndWxhcl80MDI2IiB4PSI2OC4xIiB5PSItNzMuOTU2IiB3aWR0aD0iOC4wMzYiIGhlaWdodD0iNTkuNzUyIiBmaWxsPSJ3aGl0ZSIgLz48L2c+PGc+PHN5bWJvbCBpZD0iU1RJWFR3b01hdGhSZWd1bGFyXzM2NjQiIHZpZXdCb3g9IjAgLTMxLjUyNCA1Ljc0OCA1MS4yMTYiPjxwYXRoIGQ9Ik0gMS4xMDQgMCBMIDEuMTA0IDAgTCAxLjEwNCAtNy44ODQgTCA1Ljc0OCAtNy44ODQgTCA1Ljc0OCAtNy4wMiBMIDIuMTM2IC03LjAyIEwgMi4xMzYgLTQuMzggTCA1LjE5NiAtNC4zOCBMIDUuMTk2IC0zLjUxNiBMIDIuMTM2IC0zLjUxNiBMIDIuMTM2IDAgWiAiIC8+PC9zeW1ib2w+PHVzZSBocmVmPSIjU1RJWFR3b01hdGhSZWd1bGFyXzM2NjQiIHg9Ijc3LjAwNCIgeT0iLTczLjk1NiIgd2lkdGg9IjYuNzA2IiBoZWlnaHQ9IjU5Ljc1MiIgZmlsbD0id2hpdGUiIC8+PC9nPjwvZz48L2c+PC9zdmc+\" alt=\"Analog Circuit\" title=\"\" \/><\/p>\n<details>\n  <summary>Click to expand all the Schemdraw-Markdown Goodness!<\/summary>\n\n<div class=\"highlight\"><pre><span><\/span><code>::_schemdraw_:: alt=&quot;Analog Circuit&quot; color=&quot;white&quot;\n    (V1 := elm.SourceV().label(&#39;5V&#39;))\n    elm.Line().right(drawing.unit*.75)\n    (S1 := elm.SwitchSpdt2(action=&#39;close&#39;).up().anchor(&#39;b&#39;).label(&#39;$t=0$&#39;, loc=&#39;rgt&#39;))\n    elm.Line().right(drawing.unit*.75).at(S1.c)\n    elm.Resistor().down().label(&#39;$100\\\\Omega$&#39;).label([&#39;+&#39;,&#39;$v_o$&#39;,&#39;-&#39;], loc=&#39;bot&#39;)\n    elm.Line().to(V1.start)\n    elm.Capacitor().at(S1.a).toy(V1.start).label(&#39;1$\\\\mu$F&#39;).dot()\n::end-schemdraw::\n<\/code><\/pre><\/div>\n\n\n<\/details>\n\n<h2>How Does it Work?<\/h2>\n<p>In a nutshell, we use some regular expressions to slurp the specially formatted section out of the markdown, and then we do some conditioning for the \"instructions\"\nand punch them into a custom <code>exec<\/code> block. Yeah, that's right, the dreaded exec block. I'm not particularly pleased about it, either, but it could be worse...<\/p>\n<blockquote>\n<p><strong><em>right?<\/em><\/strong><\/p>\n<\/blockquote>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/techy-granny.jpg\" width=\"100%\" alt=\"Granny's got it...\"><\/p>\n<p>At any rate, there's no sense crying over that spilled milk, for the time-being, it's simple enough, and doesn't cause too much trouble. I clearly call out a\nwarning in the README docs for the new package. And speaking of the \"new package,\" it's already on <a href=\"https:\/\/pypi.org\/project\/schemdraw-markdown\/\">PyPI<\/a>.<\/p>\n<p>To get started with it in your documentation, just go ahead and...<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>$<span class=\"w\"> <\/span>pip3<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>schemdraw-markdown\n<\/code><\/pre><\/div>\n\n<p>And let me know what you think in the comments below!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"markdown"}},{"@attributes":{"term":"pelican"}},{"@attributes":{"term":"blogging"}},{"@attributes":{"term":"circuits"}},{"@attributes":{"term":"schematics"}},{"@attributes":{"term":"python"}}]},{"title":"Scraping the ISP Router to Support Self-Hosting","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/scraping-the-isp-router-to-support-selfhosting.html","rel":"alternate"}},"published":"2022-08-26T16:27:00-07:00","updated":"2022-08-26T16:27:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-08-26:\/scraping-the-isp-router-to-support-selfhosting.html","summary":"<p>To host my home services, I need to be able to have a dynamic-DNS, and to do that, I've been using DuckDNS (a great service, by the way), but it's time that I start getting more serious, and remove theCNAME from my domain, and get something a little more proper. But... to do that, I'll need my public IP. Good thing my ISP-provided-router displays that on the front page! Now, it's time for just a little Python...<\/p>","content":"<p>Routers have all kinds of \"good stuff.\" A literal wealth of information, all we need is to tap that resource, right? That's just exactly what I'm working on.<\/p>\n<p>See, I've started working on <a href=\"https:\/\/github.com\/engineerjoe440\/ZiplyFrontierRouterStats\">a project<\/a> to scrape all of those \"goodies\" from my Ziply (formerly\nFrontier) router. All the important, and relevant stuff is provided right there, in the front of the user-interface. My little project is pretty simple, I'll\nscrape all of the useful bits into a big dictionary, then getting any of that data is a cinch!<\/p>\n<p>Python's already got all the horsepower that I need. I can use <a href=\"https:\/\/pypi.org\/project\/requests\/\"><code>requests<\/code><\/a> to pull the web-page down, and then use a\nlittle <a href=\"https:\/\/pypi.org\/project\/beautifulsoup4\/\"><code>beautifulsoup<\/code><\/a> to parse everything into where I need it. Simple, right?<\/p>\n<h3><em>Exactly!<\/em><\/h3>\n<p>Now, I've got my little tool all spun up so I can use it directly on the command line, but I can also use it to host a little web-server with\n<a href=\"https:\/\/fastapi.tiangolo.com\/\"><code>FastAPI<\/code><\/a> and <a href=\"http:\/\/www.uvicorn.org\/\"><code>Uvicorn<\/code><\/a> to host a simple little app.<\/p>\n<ul>\n<li>Want to scrape the router statistics to your terminal? No Problem.<\/li>\n<li>Want to scrape the router statistics and serve them in an easily-consumed web API? You bet!!! Even easier!<\/li>\n<\/ul>\n<p>I don't have much else to say about the little project. Probably won't ever release it on PyPI, but if people ever asked, I suppose that I could. If you\nwant to see it for yourself, go check it out on <a href=\"https:\/\/github.com\/engineerjoe440\/ZiplyFrontierRouterStats\">my Github<\/a>.<\/p>\n<h2>What's Next?<\/h2>\n<p>Well, I'm going to use this little tool, in conjunction with some Porkbun scripts to automate my DNS refreshes for my home-servers. This'll let me\nbypass the reverse-DNS provided by DuckDNS (what I use right now), and let me get some better performance out of my system, as a whole! Namely,\nI'll be able to change the CNAME records out for something a little more reliable for my home-server needs!<\/p>\n<p>Leave a comment if you've got thoughts, questions, or just want to say hi!<\/p>","category":[{"@attributes":{"term":"Self-Hosting"}},{"@attributes":{"term":"self-hosting"}},{"@attributes":{"term":"isp"}},{"@attributes":{"term":"router"}},{"@attributes":{"term":"networks"}},{"@attributes":{"term":"ethernet"}},{"@attributes":{"term":"servers"}},{"@attributes":{"term":"networking"}}]},{"title":"RheoRailroad - A Digital Toy Train Set","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/rheo-railroad-a-digital-toy-train.html","rel":"alternate"}},"published":"2022-08-23T17:00:00-07:00","updated":"2022-08-23T17:00:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-08-23:\/rheo-railroad-a-digital-toy-train.html","summary":"<p>Model railroading has always played close to new technology. I mean, after all Lionel used an electric motor back when they were still relatively new to make the first electric toy train. I'm continuing in that tradition, but doing it at my own speed, making an electric toy train speed-control that I can control from the palm of my hand. That's right, from my phone!<\/p>","content":"<p>Well, I'm fairly certain that I've discussed this before. But on the off chance I haven't. I like trains.<\/p>\n<p>Yep, I'm one of those guys.<\/p>\n<p>When I was little, I remember spending countless hours with Thomas the Tank Engine playing so many wild adventures. But I also remember playing as many,\nif not more, countless hours with my Lionel Toy Trains. Those little electrical wonders. A teensy, tiny little motor, a handful of electric lights, and\na really slick looking little die-cast metal body, and those trains were basically real. it's sort of a Christmas tradition for lots of families to set up\na little Lionel layout around the Christmas tree. Now that I have my own house, it's no different for me! I've got my little loop of track, and my\ngreat-uncle's postwar Lionel Berkshire, all steamed up and ready to entertain!<\/p>\n<blockquote>\n<p>Me. Entertain me. That's about it.<\/p>\n<\/blockquote>\n<p>Anywho...<\/p>\n<p>Those Lionel trains are pretty slick with their bells, and whistles, but I thought I'd try something different. There's these new-fangled things called\nmicrocontrollers, and they work really well with this magic called WiFi. Have you heard of it? So I thought I'd give it a whack.<\/p>\n<hr>\n<p>You probably already know that I'm an avid home-automation enthusiast. I host a bunch of my own web-services, and I have my Home Assistant running on an\nancient x86 PC (yeah, you can hear the spinning rust any time I ask it to do something). So I thought it would be neat to take it a step further, and\nautomate my little toy trains. Mind you, Lionel's already gotten this perfected. They have a <em>way-cool<\/em> system called TMCC with their \"Legacy\" remote.\nThey've even got bluetooth! Wild, huh? Well, that would all be well-and-good, except for the fact that I need to control OLD trains. Remember what I said\nearlier?<\/p>\n<blockquote>\n<p>\"My great-uncle's postwar Lionel berkshire\"?<\/p>\n<\/blockquote>\n<p>Yeah. That.<\/p>\n<hr>\n<p>Well, that wasn't going to stop me. I thought I'd build my own control. I could get all slick-and-fancy and build some cool digitally-controlled, smart\npower-supply; but where's the fun in that? Actually, I just didn't feel like doing the math, required... but maybe another time.<\/p>\n<p>I decided to use one of Lionel's early control mechanisms, a manual rheostat! That's right, they made variable resistors to slow down, or speed up those\nold trains. What's advantageous of that old system <em>for me<\/em>, however, is that it's a linear device. I mean, it moves in a straight line from one extreme,\nto the other. That means that I could take it and add some jurry-rigged control systems. Namely, a piece of all-thread, and a stepper motor, controlled\nby a driver board and an ESP8266.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_d0da9c3.jpeg\" width=\"50%\" alt=\"Rheo-Railroad Control\"><\/p>\n<h3>Neat, huh?<\/h3>\n<p>So that's one of the things I've been working on, as of late. Working on my own Lionel Toy Train control. I've still got plenty of work to do (isn't that\nwhat I always say, anymore), but it's a start! Just need to do the programming, now, and I think I'll be able to nail that with PlatformIO and some\nhandy-dandy ESP8266 libraries. Gosh, I love libraries!<\/p>\n<p>Let me know what you think in the comments, below! Use those comments! That computer in my basement isn't just sitting there for nothin'!<\/p>","category":[{"@attributes":{"term":"Model-Railroading"}},{"@attributes":{"term":"arduino"}},{"@attributes":{"term":"electro-mechanical"}},{"@attributes":{"term":"smart-home"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"platformio"}},{"@attributes":{"term":"toy-trains"}},{"@attributes":{"term":"lionel"}},{"@attributes":{"term":"model-railroading"}},{"@attributes":{"term":"esp8266"}}]},{"title":"Making Feline Stink a Distant Memory","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/making-feline-stink-a-distant-memory.html","rel":"alternate"}},"published":"2022-08-04T15:52:00-07:00","updated":"2022-08-04T15:52:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-08-04:\/making-feline-stink-a-distant-memory.html","summary":"<p>We all love our pets, but nobody likes cleaning up after them when they \"do their business,\" right? Well, I'm in that same boat. It's the reason I have a \"Cat Closet,\" where my automatic litterbox is located. Trouble is, when he uses it, that thing gets to stinkin' - and pretty quick too! Thus, I've come to a resolution with a new home-automation system. I call it \"ScentAssist.\" Let me explain...<\/p>","content":"<p><img src=\"https:\/\/raw.githubusercontent.com\/engineerjoe440\/ScentAssist\/master\/logo\/ScentAssist.png\" style=\"width: 40%; margin: 10px;\" align=\"right\" alt=\"ScentAssist\"><\/p>\n<p>Like I said, we all love our pets, but sometimes the \"stink\" can be overwhelming. <em>Especially with cats...<\/em><\/p>\n<p>My little \"Cat Closet\" helps to keep things somewhat isolated, but it produces a new challenge: all of that stink gets trapped <strong><em>indefinitely<\/em><\/strong>. That's... well, less than\nideal in just about any situation. So I came up with a solution that I call <a href=\"https:\/\/github.com\/engineerjoe440\/ScentAssist\"><code>ScentAssist<\/code><\/a>. In a nutshell, it's an automatic\nfan. Nothing crazy, but it's SUPER helpful, and pretty simple!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_1a16017.jpeg\" style=\"width: 100%;\" align=\"right\" alt=\"ScentAssist\"><\/p>\n<p>So, with a system that's not really IoT, but is embedded, I wanted a very predictable routine configuration. I wanted the system to be predictable, and work without too much\nfuss.<\/p>\n<blockquote>\n<p><em>asside:<\/em> I'm still working on that last part, but I'll get to that later...<\/p>\n<\/blockquote>\n<p>With this desire in mind, I opted to base my system around a relatively simple state-machine.<\/p>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IlNUQVRFIiBoZWlnaHQ9IjUxMnB4IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiBzdHlsZT0id2lkdGg6NzQ2cHg7aGVpZ2h0OjUxMnB4O2JhY2tncm91bmQ6IzI4MjgyODsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDc0NiA1MTIiIHdpZHRoPSI3NDZweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PD9wbGFudHVtbCAxLjIwMjYuM2JldGE2Pz48ZGVmcy8+PGc+PHJlY3QgZmlsbD0iIzI4MjgyOCIgaGVpZ2h0PSI1MTIiIHN0eWxlPSJzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSI3NDYiIHg9IjAiIHk9IjAiLz48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJJRExFIiBpZD0iZW50MDAwMiI+PHJlY3QgZmlsbD0iIzI4MjgyOCIgaGVpZ2h0PSI2NC4yMzQ0IiByeD0iMTIuNSIgcnk9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzMzRkYzMztzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxNzIuNDg0NCIgeD0iMzY2LjQ5IiB5PSIxMDgiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiMzM0ZGMzM7c3Ryb2tlLXdpZHRoOjE7IiB4MT0iMzY2LjQ5IiB4Mj0iNTM4Ljk3NDQiIHkxPSIxMzQuMjk2OSIgeTI9IjEzNC4yOTY5Ii8+PHRleHQgZmlsbD0iIzMzRkYzMyIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzEuNTU0NyIgeD0iNDM2Ljk1NDgiIHk9IjEyNS45OTUxIj5JRExFPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEzMC42NzU4IiB4PSIzNzEuNDkiIHk9IjE1MC40MzU1Ij5HZW5lcmljIFN5c3RlbSBTdGF0ZTwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjMzIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxNTIuNDg0NCIgeD0iMzcxLjQ5IiB5PSIxNjQuNDA0MyI+KGV2ZXJ5dGhpbmcgcmV0dXJucyBoZXJlKTwvdGV4dD48L2c+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iQUNUSVZBVEUiIGlkPSJlbnQwMDAzIj48cmVjdCBmaWxsPSIjMjgyODI4IiBoZWlnaHQ9IjY0LjIzNDQiIHJ4PSIxMi41IiByeT0iMTIuNSIgc3R5bGU9InN0cm9rZTojMzNGRjMzO3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjE1My44NjkxIiB4PSIyNjkuOCIgeT0iNDM0LjQ0Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojMzNGRjMzO3N0cm9rZS13aWR0aDoxOyIgeDE9IjI2OS44IiB4Mj0iNDIzLjY2OTEiIHkxPSI0NjAuNzM2OSIgeTI9IjQ2MC43MzY5Ii8+PHRleHQgZmlsbD0iIzMzRkYzMyIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNjguNTg1IiB4PSIzMTIuNDQyMSIgeT0iNDUyLjQzNTEiPkFDVElWQVRFPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEzMy44NjkxIiB4PSIyNzQuOCIgeT0iNDc2Ljg3NTUiPlR1cm4gb24gTEVEIGFuZCByZWxheTwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjMzIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI5MC43OTY5IiB4PSIyNzQuOCIgeT0iNDkwLjg0NDMiPnRvIGVuZXJnaXplIGZhbjwvdGV4dD48L2c+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iREVURUNURUQiIGlkPSJlbnQwMDA0Ij48cmVjdCBmaWxsPSIjMjgyODI4IiBoZWlnaHQ9Ijc4LjIwMzEiIHJ4PSIxMi41IiByeT0iMTIuNSIgc3R5bGU9InN0cm9rZTojMzNGRjMzO3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjE0My4yNDYxIiB4PSIxNjkuMTEiIHk9IjI2NC4yMyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzMzRkYzMztzdHJva2Utd2lkdGg6MTsiIHgxPSIxNjkuMTEiIHgyPSIzMTIuMzU2MSIgeTE9IjI5MC41MjY5IiB5Mj0iMjkwLjUyNjkiLz48dGV4dCBmaWxsPSIjMzNGRjMzIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3NC45NzY2IiB4PSIyMDMuMjQ0OCIgeT0iMjgyLjIyNTEiPkRFVEVDVEVEPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEyMy4yNDYxIiB4PSIxNzQuMTEiIHk9IjMwNi42NjU1Ij5Db25kaXRpb24gdHVybmluZyBvbjwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjMzIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI5Ni44MjAzIiB4PSIxNzQuMTEiIHk9IjMyMC42MzQzIj5mYW4sIG9yIHJlc2V0dGluZzwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjMzIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI2OC42NDg0IiB4PSIxNzQuMTEiIHk9IjMzNC42MDMiPmRlbGF5IHRpbWVyPC90ZXh0PjwvZz48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSJSRVNFVCIgaWQ9ImVudDAwMDUiPjxyZWN0IGZpbGw9IiMyODI4MjgiIGhlaWdodD0iNzguMjAzMSIgcng9IjEyLjUiIHJ5PSIxMi41IiBzdHlsZT0ic3Ryb2tlOiMzM0ZGMzM7c3Ryb2tlLXdpZHRoOjE7IiB3aWR0aD0iMTM5Ljg5NDUiIHg9IjU5MS43OSIgeT0iMjY0LjIzIi8+PGxpbmUgc3R5bGU9InN0cm9rZTojMzNGRjMzO3N0cm9rZS13aWR0aDoxOyIgeDE9IjU5MS43OSIgeDI9IjczMS42ODQ1IiB5MT0iMjkwLjUyNjkiIHkyPSIyOTAuNTI2OSIvPjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQ0Ljg1NzQiIHg9IjYzOS4zMDg2IiB5PSIyODIuMjI1MSI+UkVTRVQ8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYzMyIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTIiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTAwLjI4MzIiIHg9IjU5Ni43OSIgeT0iMzA2LjY2NTUiPlR1cm4gZmFuIG9mZiwgYW5kPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjExOS44OTQ1IiB4PSI1OTYuNzkiIHk9IjMyMC42MzQzIj5yZXN0b3JlIGRlZmF1bHQgdGltZTwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjMzIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI4OS4wMTU2IiB4PSI1OTYuNzkiIHk9IjMzNC42MDMiPmNvdW50ZXIgdmFsdWVzPC90ZXh0PjwvZz48ZyBjbGFzcz0ic3RhcnRfZW50aXR5IiBkYXRhLXF1YWxpZmllZC1uYW1lPSIuc3RhcnQuIiBkYXRhLXNvdXJjZS1saW5lPSIxNCIgaWQ9ImVudDAwMDIiPjxlbGxpcHNlIGN4PSI0NTIuNzMiIGN5PSIyMSIgZmlsbD0iIzI4MjgyOCIgcng9IjEwIiByeT0iMTAiIHN0eWxlPSJzdHJva2U6IzMzRkYzMztzdHJva2Utd2lkdGg6MTsiLz48L2c+PCEtLWxpbmsgKnN0YXJ0KiB0byBJRExFLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDIiIGRhdGEtZW50aXR5LTI9ImVudDAwMDIiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIxNCIgaWQ9ImxuazMiPjxwYXRoIGQ9Ik00NTIuNzMsMzEuMTkgQzQ1Mi43Myw0Ny40MiA0NTIuNzMsNzUuODcgNDUyLjczLDEwMS41IiBmaWxsPSJub25lIiBpZD0iKnN0YXJ0Ki10by1JRExFIiBzdHlsZT0ic3Ryb2tlOiMzM0ZGMzM7c3Ryb2tlLXdpZHRoOjE7Ii8+PHBvbHlnb24gZmlsbD0iIzMzRkYzMyIgcG9pbnRzPSI0NTIuNzMsMTA3LjUsNDU2LjczLDk4LjUsNDUyLjczLDEwMi41LDQ0OC43Myw5OC41LDQ1Mi43MywxMDcuNSIgc3R5bGU9InN0cm9rZTojMzNGRjMzO3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQ1Ljc0NzYiIHg9IjQ1My43MyIgeT0iNzQuMDY2OSI+Ym9vdHVwPC90ZXh0PjwvZz48IS0tbGluayBJRExFIHRvIEFDVElWQVRFLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDIiIGRhdGEtZW50aXR5LTI9ImVudDAwMDMiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIxNSIgaWQ9ImxuazQiPjxwYXRoIGQ9Ik00NTguMDMsMTcyLjMxIEM0NjUuMTksMjI0LjIgNDcyLjY5LDMyOS44NyA0MzAuNzMsNDA0LjQ0IEM0MjQuMjgsNDE1LjkxIDQxOS4zNjcxLDQyMi4wNzY3IDQwOC45MzcxLDQzMC4zNjY3IiBmaWxsPSJub25lIiBpZD0iSURMRS10by1BQ1RJVkFURSIgc3R5bGU9InN0cm9rZTojMzNGRjMzO3N0cm9rZS13aWR0aDoxOyIvPjxwb2x5Z29uIGZpbGw9IiMzM0ZGMzMiIHBvaW50cz0iNDA0LjI0LDQzNC4xLDQxMy43NzQ1LDQzMS42MzE0LDQwOC4xNTQyLDQzMC45ODg5LDQwOC43OTY3LDQyNS4zNjg2LDQwNC4yNCw0MzQuMSIgc3R5bGU9InN0cm9rZTojMzNGRjMzO3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjkzLjc1NDkiIHg9IjQ2NC43MyIgeT0iMzAwLjM5NjkiPlVzZXIgbWFudWFsbHk8L3RleHQ+PHRleHQgZmlsbD0iIzMzRkYzMyIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iODQuMjY1MSIgeD0iNDcxLjU0MSIgeT0iMzE1LjUyOTciPmFjdGl2YXRlcyBmYW48L3RleHQ+PC9nPjwhLS1saW5rIElETEUgdG8gREVURUNURUQtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwMiIgZGF0YS1lbnRpdHktMj0iZW50MDAwNCIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjE2IiBpZD0ibG5rNSI+PHBhdGggZD0iTTM2Ni4zNywxNDEuNjEgQzI2MC42MiwxNDQuMjYgOTAuNTksMTU1Ljg2IDUwLjczLDIwMi4yMyBDMTEsMjQ4LjQ2IDkzLjY0MjgsMjc1LjI2MjQgMTYyLjgwMjgsMjg5LjQ3MjQiIGZpbGw9Im5vbmUiIGlkPSJJRExFLXRvLURFVEVDVEVEIiBzdHlsZT0ic3Ryb2tlOiMzM0ZGMzM7c3Ryb2tlLXdpZHRoOjE7Ii8+PHBvbHlnb24gZmlsbD0iIzMzRkYzMyIgcG9pbnRzPSIxNjguNjgsMjkwLjY4LDE2MC42NjkyLDI4NC45NTA1LDE2My43ODIzLDI4OS42NzM3LDE1OS4wNTkxLDI5Mi43ODY4LDE2OC42OCwyOTAuNjgiIHN0eWxlPSJzdHJva2U6IzMzRkYzMztzdHJva2Utd2lkdGg6MTsiLz48dGV4dCBmaWxsPSIjMzNGRjMzIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxNDEuMDcwMyIgeD0iNTEuNzMiIHk9IjIxNS4yOTY5Ij5GdWxseSBRdWFsaWZpZWQgTW90aW9uPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjExMS44NDU3IiB4PSI3MC40NzQ2IiB5PSIyMzAuNDI5NyI+U2Vuc29yIERldGVjdGlvbjwvdGV4dD48L2c+PCEtLWxpbmsgREVURUNURUQgdG8gSURMRS0tPjxnIGNsYXNzPSJsaW5rIiBkYXRhLWVudGl0eS0xPSJlbnQwMDA0IiBkYXRhLWVudGl0eS0yPSJlbnQwMDAyIiBkYXRhLWxpbmstdHlwZT0iZGVwZW5kZW5jeSIgZGF0YS1zb3VyY2UtbGluZT0iMTgiIGlkPSJsbms3Ij48cGF0aCBkPSJNMjI4Ljg5LDI2NC4xNSBDMjI1LjA0LDI0My44NSAyMjQuNjMsMjE5LjU3IDIzNy43MywyMDIuMjMgQzI1My42NywxODEuMTQgMzA3LjY1MjMsMTY2LjM0NTYgMzYwLjE4MjMsMTU2LjAzNTYiIGZpbGw9Im5vbmUiIGlkPSJERVRFQ1RFRC10by1JRExFIiBzdHlsZT0ic3Ryb2tlOiMzM0ZGMzM7c3Ryb2tlLXdpZHRoOjE7Ii8+PHBvbHlnb24gZmlsbD0iIzMzRkYzMyIgcG9pbnRzPSIzNjYuMDcsMTU0Ljg4LDM1Ni40NjgxLDE1Mi42ODgyLDM2MS4xNjM2LDE1NS44NDMsMzU4LjAwODksMTYwLjUzODUsMzY2LjA3LDE1NC44OCIgc3R5bGU9InN0cm9rZTojMzNGRjMzO3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE0Ni40OTEyIiB4PSIyNTcuMDA4MSIgeT0iMjE1LjI5NjkiPlN0YXJ0IGNvdW50ZG93biB0aW1lcjwvdGV4dD48dGV4dCBmaWxsPSIjMzNGRjMzIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxODMuMDQ3NCIgeD0iMjQyLjg2MjMiIHk9IjIzMC40Mjk3Ij4oZGVsYXkgYWZ0ZXIgZGV0ZWN0aW5nIGEgY2F0KTwvdGV4dD48L2c+PCEtLWxpbmsgREVURUNURUQgdG8gQUNUSVZBVEUtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwNCIgZGF0YS1lbnRpdHktMj0iZW50MDAwMyIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjE3IiBpZD0ibG5rNiI+PHBhdGggZD0iTTIzOS44LDM0Mi41MiBDMjQwLjk3LDM2Mi4zNSAyNDUuMTIsMzg2LjE3IDI1Ni43Myw0MDQuNDQgQzI2NC4wMyw0MTUuOTEgMjY5LjUyOCw0MjIuMTg2IDI4MC41NzgsNDMwLjQxNiIgZmlsbD0ibm9uZSIgaWQ9IkRFVEVDVEVELXRvLUFDVElWQVRFIiBzdHlsZT0ic3Ryb2tlOiMzM0ZGMzM7c3Ryb2tlLXdpZHRoOjE7Ii8+PHBvbHlnb24gZmlsbD0iIzMzRkYzMyIgcG9pbnRzPSIyODUuMzksNDM0LDI4MC41NjEzLDQyNS40MTYxLDI4MS4zOCw0MzEuMDEzNCwyNzUuNzgyNyw0MzEuODMyMSwyODUuMzksNDM0IiBzdHlsZT0ic3Ryb2tlOiMzM0ZGMzM7c3Ryb2tlLXdpZHRoOjE7Ii8+PHRleHQgZmlsbD0iIzMzRkYzMyIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTY0LjU4MiIgeD0iMjU3LjczIiB5PSIzODUuNTA2OSI+RmFuIHdhcyBhbHJlYWR5IHJ1bm5pbmcsPC90ZXh0Pjx0ZXh0IGZpbGw9IiMzM0ZGMzMiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE1MC44MzMiIHg9IjI2OC43MzY4IiB5PSI0MDAuNjM5NyI+bWF4aW1pemUgcnVubmluZyB0aW1lPC90ZXh0PjwvZz48IS0tbGluayBJRExFIHRvIFJFU0VULS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDIiIGRhdGEtZW50aXR5LTI9ImVudDAwMDUiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIxOSIgaWQ9ImxuazgiPjxwYXRoIGQ9Ik00NjUuMjksMTcyLjU3IEM0NzQuNTMsMTkyLjQyIDQ4OC43OSwyMTcuMzkgNTA3LjczLDIzNC4yMyBDNTIxLjIyLDI0Ni4yMyA1NTEuOTcxMywyNjAuMjY2MyA1ODUuOTUxMywyNzMuODk2MyIgZmlsbD0ibm9uZSIgaWQ9IklETEUtdG8tUkVTRVQiIHN0eWxlPSJzdHJva2U6IzMzRkYzMztzdHJva2Utd2lkdGg6MTsiLz48cG9seWdvbiBmaWxsPSIjMzNGRjMzIiBwb2ludHM9IjU5MS41MiwyNzYuMTMsNTg0LjY1NjEsMjY5LjA2Nyw1ODYuODc5NCwyNzQuMjY4Niw1ODEuNjc3OCwyNzYuNDkxOSw1OTEuNTIsMjc2LjEzIiBzdHlsZT0ic3Ryb2tlOiMzM0ZGMzM7c3Ryb2tlLXdpZHRoOjE7Ii8+PHRleHQgZmlsbD0iIzMzRkYzMyIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTMyLjUxMzciIHg9IjUwOC43MyIgeT0iMjIyLjc5NjkiPkZhbiBydW50aW1lIGVsYXBzZXM8L3RleHQ+PC9nPjwhLS1saW5rIFJFU0VUIHRvIElETEUtLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwNSIgZGF0YS1lbnRpdHktMj0iZW50MDAwMiIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjIwIiBpZD0ibG5rOSI+PHBhdGggZD0iTTY2NC43NSwyNjQuMTEgQzY2NC4zMSwyNDMuNTQgNjYwLjIxLDIxOS4wMyA2NDUuNzMsMjAyLjIzIEM2MTkuMjMsMTcxLjQ4IDU4My4yNDA3LDE1Ny4yMzA3IDU0NS4yNjA3LDE0OS41NDA3IiBmaWxsPSJub25lIiBpZD0iUkVTRVQtdG8tSURMRSIgc3R5bGU9InN0cm9rZTojMzNGRjMzO3N0cm9rZS13aWR0aDoxOyIvPjxwb2x5Z29uIGZpbGw9IiMzM0ZGMzMiIHBvaW50cz0iNTM5LjM4LDE0OC4zNSw1NDcuNDA3MiwxNTQuMDU2NSw1NDQuMjgwNiwxNDkuMzQyMiw1NDguOTk0OCwxNDYuMjE1Niw1MzkuMzgsMTQ4LjM1IiBzdHlsZT0ic3Ryb2tlOiMzM0ZGMzM7c3Ryb2tlLXdpZHRoOjE7Ii8+PC9nPjw\/cGxhbnR1bWwtc3JjIE5MNnpKaUNtNER4cDVDVk1lbE81RWJJZzZYVExBV0NxaTUwNjRyeVFJdWFUc1VVTXlGSVN1bUdBZ3hfeFZkcjdEUk84ZlFGYnNJNlFoRE9BMFRrRVViM2VJd1N4cWpQYS1fbVdPMEtGUUQzZjRld3o5c3BYSTk5bW1jUHVHVFRKaFNxUDc1OW1uYUVEM2tWUFBibGlOcFU1TTQ3MWhzMEQ3NEdFcVlZLVFzSF9XdkU1dzczTU5tWUxENWFrMmg0akhCdzJoSkxBbm9XR2JRRTdsT052VTA3TWlRMTc4ZVBsQzFMRFc3SUJCZGlNSHI2Q0VQVzNqZ2VNQ1NhNEM5LWlHLVBLQ1pHcWkyUWlqQzRHRWhaODlnM0ZpaFZ4VHJXa3JwM3NfeDJNR2ZWTVlDekpBdVBVRkREUVFPOWljWHZFSGZRYUJwb1Q3cWVjblhnY240cE9YTlp1bkZVd3FnWldxR3hMSm1RRVEzcHRwOTZtNzd4Y1hfTkZTeVZqaGpBM1IxbkFyT0NCOWl3c1kyQWpfREhqdDdYeUozTl9BOHNiLThTVGZVUkFOYXJRQ0toQ3FnZ29ZZWtlYjhRNTk5SUl2aHlaM0RFRFFUV2lxZTY5ZFVTMTRwUVBWR0MwPz48L2c+PC9zdmc+\" style=\"max-width:100%\" width=\"100%\" class=\"uml\" alt=\"ScentAssist State Machine\" title=\"ScentAssist State Machine\" \/><\/p>\n<p>So, that's all pretty simple, what <em>wasn't<\/em> simple (or should I say <em>isn't<\/em>, since I'm still working on it) was the motion-sensor qualification. I'm cheap, and a fan of\nreusing old parts, so I pulled out an old 12VDC power supply from my \"storage.\" It was originally from an RGB DJ light that I tore apart and hacked. Don't get me wrong,\nthe power supply is reasonable, enough, but it's not exactly... \"clean.\" The output is a bit noisy. In fact, I've had trouble with these things before. I've used its\nbrethren before (that's right, I had like 8 of these things, at one point) and they <em>worked<\/em>, but gave me trouble. So with all that noise, the motion sensor drives some\nfunny behavior on its output.<\/p>\n<p>I'm using little motion sensors like the one pictured below; they're simple enough, +\/- voltage rails, and an output. The output provides a little less than a solid\ndigital signal, so I hooked it up to one of the analog inputs on the Arduino. No biggie. However, with all that noise from my friend the 12V supply, the output signal\nbecomes a bit...<\/p>\n<p>messy.<\/p>\n<p><a href=\"https:\/\/www.amazon.com\/gp\/product\/B096NZ4P3K\/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&amp;psc=1\"><img alt=\"motion sensor\" src=\"https:\/\/m.media-amazon.com\/images\/I\/41AkdTj2yML._AC_SL1001_.jpg\"><\/a><\/p>\n<p>Still, it's not unreadable. I can pretty easily detect the step-change when visually inspecting the waveform, or watching the analog readouts from the Arduino, itself. Just\nneed to filter out all that garbage. Sure! Analog filtering is pretty easy. Right?<\/p>\n<p><strong>...<\/strong><\/p>\n<blockquote>\n<p><em>Right...?<\/em><\/p>\n<\/blockquote>\n<p>It was easy when I was in college, and had just learned the stuff. But I haven't touched algorithms like IIR (Infinite Impulse Response) in about three years! So,\nneedless to say, I was a bit rusty. Still am... in fact. The filtering I built still isn't <em>quite<\/em> where I want it, but I'm close. Should just be a few more tweaks, and\nthen I'll be able to call this project complete!<\/p>\n<p>When I do complete it, I'll be sure to bring some more thoughts to this article (below), but for now... enjoy the few project photos!<\/p>","category":[{"@attributes":{"term":"Home-Improvement"}},{"@attributes":{"term":"arduino"}},{"@attributes":{"term":"electro-mechanical"}},{"@attributes":{"term":"smart-home"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"platformio"}},{"@attributes":{"term":"pets"}},{"@attributes":{"term":"exhaust"}}]},{"title":"Photos in the Nick of Time at STAC","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/photos-in-the-nick-of-time-at-stac.html","rel":"alternate"}},"published":"2022-07-02T11:25:00-07:00","updated":"2022-07-02T11:25:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-07-02:\/photos-in-the-nick-of-time-at-stac.html","summary":"<p>Well. It was here, and now it's gone. The Idaho State Teen Association Convention, or STAC - as we fondly call it, has come and gone. It was a great week of little sleep, much excitement, and lots of smiles; but what's more, is that we had some technology wins and losses to talk about. Thank you, Python!<\/p>","content":"<p>I don't like making \"a big deal of myself\" or patting myself on the back, but I have to say, I'm very proud of my little team's accomplishments\nwith this project for our Idaho State 4-H youth conference. Sure; not everything worked perfectly. Not everything was exactly what we wanted.\nBut that's just how some of these things go, and overall, it was a phenominal success.<\/p>\n<p>If you're not sure what I'm talking about, let me point you at some of the other blogposts I've written on the subject...<\/p>\n<ul>\n<li><a href=\".\/react.js-python-pictures-and-4-h\">React.js, Python, Pictures, and 4-H!<\/a><\/li>\n<li><a href=\".\/using-python-to-provide-simple-photo-connections-for-youth\">Using Python to Provide Simple Photo Connections for Youth<\/a><\/li>\n<li><a href=\".\/automagic-test-websites\">Automagic Test Websites<\/a><\/li>\n<li><a href=\".\/error-pages-for-education\">Error Pages for Education<\/a><\/li>\n<\/ul>\n<h1>Our Wins<\/h1>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/FBAC19F1-F146-4038-BCFF-0D7CF2C3CCD5.jpeg\" style=\"width: 30%; margin: 10px;\" align=\"right\" alt=\"Go Team!\"><\/p>\n<p>Yeah! We did have some big wins this year. First and foremost was that we had a great conference. The youth all really seemed to enjoy themselves,\nand minor challenges and hiccups aside, all things were good.<\/p>\n<p>You know, we even had more delegates than usual! Idaho invited Washington State to bring a small delegation of youth who are going to be involved\nin planning their own STAC-equivalent in Washington, next year. Pretty cool, huh?<\/p>\n<p>Whats more, we had a successful first run of our Photo Uploader Site. Hmm... I think we still need a better name, don't we? Thoughts, anyone?\nLeave them in the comments. The site didn't crash, it didn't buckle, it didn't lose anything (that I didn't accidentally delete). And it didn't\ngo haywire in some other way.<\/p>\n<p>In total, we had more than 1200 photos uploaded to <a href=\"https:\/\/albums.idaho4h.com\/\"><code>albums.idaho4h.com<\/code><\/a>. Yeah, that's right. More than 1200 in\nless than 4 days. Whoop, whoop! Now, in reality, there's nothing terribly impressive about this. That's not really <em>that many<\/em> pictures in today's\nworld, but for an app built on weekends by two people who were both learning all the way through, it's a big win!<\/p>\n<blockquote>\n<p>Ok. Let me toot my own horn, one more time.<\/p>\n<\/blockquote>\n<p>Our biggest win, is that <em>we<\/em> built this thing. I worked with a youth member who's involved (and super-clever, might I add) in some of these state\nevents, and together we created this tool to help all of the adults, and all of his peers to engage in a pleasant and simpler way. If you ask me,\n<em>THAT<\/em> is the biggest win. We've definatively proven that the youth are more than capable of building their own infrastructure with guidance; and,\nafter all, that's what 4-H is all about. The whole ethos really is <em>\"learn by doing.\"<\/em><\/p>\n<h1>Where's the Proof?<\/h1>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/20220619_172613.jpg\" style=\"width: 50%; margin: 10px;\" align=\"left\" alt=\"Where is it all?\"><\/p>\n<p>Oh! You want to see the uploader? The album-site? The photos? OK!<\/p>\n<table>\n<thead>\n<tr>\n<th><strong>Description<\/strong> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<\/th>\n<th><strong>Link<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Photo Uploader<\/td>\n<td><a href=\"https:\/\/photos.idaho4h.com\/\"><code>photos.idaho4h.com<\/code><\/a><\/td>\n<\/tr>\n<tr>\n<td>Albums and Pics<\/td>\n<td><a href=\"https:\/\/albums.idaho4h.com\/\"><code>albums.idaho4h.com<\/code><\/a><\/td>\n<\/tr>\n<tr>\n<td>Source Code<\/td>\n<td><a href=\"https:\/\/gitlab.stanleysolutionsnw.com\/idaho4h\/4HPhotoUploader\">StanleySolutions Gitlab<\/a><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>You can go browse all of the source code that our team (one of the Steering Committee youth members and myself) created, or you can go look at the\nfinished product (the uploader), and even take a look at the album site. The album site has a single public album where all of the photos have\nbeen moved after they've been filtered and cleaned.<\/p>\n<p>Ah! But before you ask; yes, that filtering and cleaning <em>is<\/em> a manual process. Someone has to go through all of them and look for anything that\nshouldn't be made public before the photos become viewable. And that sorta takes us into the next topic...<\/p>\n<h1>Our Losses<\/h1>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/DSC_0630.JPG\" style=\"width: 40%; margin: 10px;\" align=\"right\" alt=\"This clown...\"><\/p>\n<p>As much as I would <em>LOVE<\/em> to say that we had zero issues this year with our new tool, that just isn't the truth. We had some great failures, too.\nI mean, after all, just look at that guy on the right. Would you trust him to make something that didn't have at least one or two hiccups? Me neither.<\/p>\n<p>So here are all the little things that went wrong...<\/p>\n<h4>1) Not all Districts Were Accounted For<\/h4>\n<p>Ok. Admittedly, this isn't exactly a failure, but I wanted to call it out, anyway, since there was some work to be done here, and it emphasized a\npoint of contention in our system. You see, we've got a hard-coded mechanism for managing district validation for the district selection. That selection\nis this little section here...<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/district-selection.png\" style=\"width: 55%; margin: 10px;\" align=\"left\" alt=\"District Selection\"><\/p>\n<p><br><br><\/p>\n<p>In the code snippet below, you can see that there's a hard-coded number of options that are validated against. This isn't the only place though, the\nReact.js frontend also has these values hard-coded. SO... We'll need to determine a way to make those selections more discrete, and customizable. Hmm...<\/p>\n<p><br><br><br><\/p>\n<details>\n  <summary>Click to examine the district validator function...<\/summary>\n\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"k\">def<\/span> <span class=\"nf\">validate_district<\/span><span class=\"p\">(<\/span><span class=\"n\">district_name<\/span><span class=\"p\">:<\/span> <span class=\"nb\">str<\/span><span class=\"p\">)<\/span> <span class=\"o\">-&gt;<\/span> <span class=\"n\">ValidatedDistrict<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"sd\">    District Validation Function<\/span>\n\n<span class=\"sd\">    This function validates a string providing the name of the district which<\/span>\n<span class=\"sd\">    photos should be uploaded in association with. If the name is valid, it will<\/span>\n<span class=\"sd\">    return `True` and the sanitized name, should the name NOT be valid, `False`<\/span>\n<span class=\"sd\">    will be returned with an empty string.<\/span>\n<span class=\"sd\">    &quot;&quot;&quot;<\/span>\n    <span class=\"n\">count<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">0<\/span>\n    <span class=\"n\">district<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;&quot;<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">district_name<\/span><span class=\"o\">.<\/span><span class=\"n\">lower<\/span><span class=\"p\">()<\/span><span class=\"o\">.<\/span><span class=\"n\">find<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;eastern&quot;<\/span><span class=\"p\">)<\/span> <span class=\"o\">!=<\/span> <span class=\"o\">-<\/span><span class=\"mi\">1<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">district<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;Eastern District&quot;<\/span>\n        <span class=\"n\">count<\/span> <span class=\"o\">+=<\/span> <span class=\"mi\">1<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">district_name<\/span><span class=\"o\">.<\/span><span class=\"n\">lower<\/span><span class=\"p\">()<\/span><span class=\"o\">.<\/span><span class=\"n\">find<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;northern&quot;<\/span><span class=\"p\">)<\/span> <span class=\"o\">!=<\/span> <span class=\"o\">-<\/span><span class=\"mi\">1<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">district<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;Northern District&quot;<\/span>\n        <span class=\"n\">count<\/span> <span class=\"o\">+=<\/span> <span class=\"mi\">1<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">district_name<\/span><span class=\"o\">.<\/span><span class=\"n\">lower<\/span><span class=\"p\">()<\/span><span class=\"o\">.<\/span><span class=\"n\">find<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;central&quot;<\/span><span class=\"p\">)<\/span> <span class=\"o\">!=<\/span> <span class=\"o\">-<\/span><span class=\"mi\">1<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">district<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;Central District&quot;<\/span>\n        <span class=\"n\">count<\/span> <span class=\"o\">+=<\/span> <span class=\"mi\">1<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">district_name<\/span><span class=\"o\">.<\/span><span class=\"n\">lower<\/span><span class=\"p\">()<\/span><span class=\"o\">.<\/span><span class=\"n\">find<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;southern&quot;<\/span><span class=\"p\">)<\/span> <span class=\"o\">!=<\/span> <span class=\"o\">-<\/span><span class=\"mi\">1<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">district<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;Southern District&quot;<\/span>\n        <span class=\"n\">count<\/span> <span class=\"o\">+=<\/span> <span class=\"mi\">1<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">district_name<\/span><span class=\"o\">.<\/span><span class=\"n\">lower<\/span><span class=\"p\">()<\/span><span class=\"o\">.<\/span><span class=\"n\">find<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;washington&quot;<\/span><span class=\"p\">)<\/span> <span class=\"o\">!=<\/span> <span class=\"o\">-<\/span><span class=\"mi\">1<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">district<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;Washington State&quot;<\/span>\n        <span class=\"n\">count<\/span> <span class=\"o\">+=<\/span> <span class=\"mi\">1<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">count<\/span> <span class=\"o\">==<\/span> <span class=\"mi\">0<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">return<\/span><span class=\"p\">(<\/span><span class=\"kc\">False<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;This isn&#39;t a district!&quot;<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">elif<\/span> <span class=\"n\">count<\/span> <span class=\"o\">&gt;<\/span> <span class=\"mi\">1<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">return<\/span><span class=\"p\">(<\/span><span class=\"kc\">False<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;This is a weirdly named district!&quot;<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">return<\/span> <span class=\"kc\">True<\/span><span class=\"p\">,<\/span> <span class=\"n\">district<\/span>\n<\/code><\/pre><\/div>\n\n\n<\/details>\n\n<p>Ultimately, this came up last minute (Sunday night before the delegation arrived) when we were discussing the Washington-State delegation's participation\nin the district competitions. Since they would be participating, we'd need them added to the list. Thankfully, due to our automated build system (thank\ngoodness I spent the time to get that thing working), I was able to make the code changes, verify that my changes had some bugs, fix the bugs, and deploy\n- all within the time of about a half-an-hour.<\/p>\n<p>So yeah, I'll call it a loss, but there was a hidden win, too!<\/p>\n<h4>2) Limited Number of Photos in an Upload Group<\/h4>\n<p>We've all heard that joke about assumptions, right?<\/p>\n<p>You know, the one that says something about making an a$$ out of you and me? That one... Well, is it not the case that in engineering, we have to make a\nLOT of assumptions? Oh yeah. We do.<\/p>\n<p>Well, we made an assumption about how many pictures people would want to upload at any given time. Guess we got those numbers a little wrong. We quickly\nlearned that people wanted to upload a lot more than 10 pictures at any given time. Dang. Good thing GitLab has bug-tracking! So I went ahead and logged\nit already: https:\/\/gitlab.stanleysolutionsnw.com\/idaho4h\/4HPhotoUploader\/-\/issues\/48  -- Love GitLab.<\/p>\n<h4>3) Limited Download Access<\/h4>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/20220619_172534.jpg\" style=\"width: 40%; margin: 10px;\" align=\"left\" alt=\"Not Enough Access.\"><\/p>\n<p>Unfortunately, another thing that we learned the hard way this year was that downloading an entire album as a zipped file isn't as easy when you're not\nthe web-admin (<em>coughs<\/em> that's me). The teen officers (who are in charge of putting together a slide-show with all of these pictures), had one heck of a\ntime trying to interact with the Lychee albums app to get the pictures that they need. Now, admittedly, this is partly a combination of lack of teaching\nfrom me, and partly general technical challenges, but in total, this application is supposed to make things easier for these youth, lowering the barrier\nto entry and making the process simpler, so they have more time to enjoy the conference.<\/p>\n<p>There are learning points to take away from it, as a whole. There's the sentiment of:<\/p>\n<blockquote>\n<p>There's no better time to learn something than the present.<\/p>\n<\/blockquote>\n<p>Still, if it's easier, overall, it could give the teen officers a greater opportunity to enjoy their conference. So we'll need to figure out a better\nway to make those images available for download in a zip.<\/p>\n<h4>4) Manually Screening Pictures<\/h4>\n<p>You know me... I hate doing something that I think the computer can. That's why I consider myself an \"automation engineer,\" I automate things; that's\nwhat I do.<\/p>\n<p>So when we still have to manually screen pictures and make sure that they're appropriate, that makes a small part of me cry inside. There HAS to be a\nbetter way. I still think there is, but I don't know what it is yet.<\/p>\n<p>I think I'd like to explore what those ever-present buzz-word technoligies might be able to provide. You know the ones; AI and ML. What can a little\ncomputer-learning bring to the table? Can it identify photos that would be considered explicit, or inappropriate? We're 4-H, so we <em>shouldn't<\/em> be seeing\nthose, anyway. But, we're working with youth. So there's always the possibility. Right? So we should have some screening mechanism in place. Even if it's\nfor nothing else than to remove photos that just aren't flattering.<\/p>\n<h1>Closing Thoughts and Special Thanks<\/h1>\n<blockquote>\n<p>Administrative note: We're diverging from the technical stuff, here, so if that's what you came for, you're free to take off at this point!\nHowever, if you like the 4-H memories, stick around and read on!<\/p>\n<\/blockquote>\n<p>I truly love participating in this event. It excites me each year to watch these young people grow and find themselves, and become true leaders. They're\ngoing places, and I love seeing that. I'm so happy to work alongside some really cool people who make the event possible.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/528FB398-BCDA-435E-A3E8-EC5AF00A1F4F.jpeg\" style=\"width: 50%\" align=\"left\" alt=\"These Folks Rock!\">\n<img src=\"https:\/\/blog.stanleysolutionsnw.com\/028EFD6B-950A-4995-B1E5-A98BF31BD10A.jpeg\" style=\"width: 50%\" align=\"right\" alt=\"What goofs...\"><\/p>\n<p>This isn't everyone, but it's a solid group! From left-to-right, Tara, Teresa, (yours truly), SheilAnne, and Kandee. Not pictured are Carrie and Mike\nwho help to \"round out\" the remaining adults on the STAC Steering Committe. They're all a great group of folks to work with, and spend time with. I\ncouldn't be happier to collaborate with them. Of course, I'd be remiss to ignore the fact that there's plenty of youth wha also make up the team. They\nall make it so much fun to work on.<\/p>\n<h3>Dr. Lindstrom<\/h3>\n<p>I'm a bit of a sap, so I want to end on this sappy note. Think of that what you will.<\/p>\n<p>It's hard to believe that nine years ago was <em>my<\/em> first time attending this conference as a youth, myself. That year was a wild and wonderful ride; I\nlearned so much. That was also Dr. Jim Lindstrom's first year as the Idaho State 4-H Director.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/DSC_0925.JPG\" style=\"width: 100%; margin: 10px;\" align=\"right\" alt=\"Dr. Lindstrom\"><\/p>\n<p>I don't want to spend too much time pounding on this point, but I want to leave a lasting \"Thank You,\" right here. There's a lot that has gone on\nbehind the scenes in the state. I'm glad to have been able to have seen some of these trials and tribulations, so that I might understand a small part\nof the process. It takes an army, not just a villiage, to keep a state 4-H program strong, and ours is. We have wonderful volunteers, leaders, parents\nyouth, educators, and 4-H professionals. Many times, there are folks who wear several of those hats all at the same time.<\/p>\n<p>Dr. Lindstrom has been behind this development for that entire time. He's worked diligently to keep the pipelines flowing and keep all of those folks\nengaged. I still remember sitting with Dr. Lindstrom at our formal banquet the first year at STAC and talking to him about what projects I was taking\nthat year, and what I enjoyed about 4-H in the state. This is Dr. Lindstrom's last year, and it's been wonderful to reflect on all of the great things\nthat the state has accomplished over these years, and I'm so thankful for Jim's leadership, care, and persistence. He's been a solid guide, and a true\nleader. The sort of person we all hope to \"grow up to be.\"<\/p>\n<p>Thank you, Dr. Lindstrom! May Idaho 4-H continue to reap the benefits of all the greatness you've sewn, and may you be able to look back on it with\nfond memories of all the youth who've seen tremendous growth.<\/p>\n<hr>\n<p>Last thing...<\/p>\n<p>Keep Making the Best Better.<\/p>\n<p>~Joe<\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"react.js"}},{"@attributes":{"term":"4-h"}},{"@attributes":{"term":"fastapi"}},{"@attributes":{"term":"materialui"}},{"@attributes":{"term":"linode"}}]},{"title":"Making Progress with Capstones","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/making-progress-with-capstones.html","rel":"alternate"}},"published":"2022-05-24T15:25:00-07:00","updated":"2022-05-24T15:25:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-05-24:\/making-progress-with-capstones.html","summary":"<p>Over the last two years, I've become more involved as a project sponsor for some University of Idaho Engineering Capstone projects, and I'm very excited about their growth. Progress may be slow, but it's incremental, and we're approaching goals, which is always exciting!<\/p>","content":"<h3>Quick! Recap what's being researched...<\/h3>\n<p>This year, I'm very thankful that I've been able to sponsor two different projects. A continuation project from\nlast year's <a href=\"https:\/\/engineerjoe440.github.io\/stanley-solutions-blog\/hearing-fires-while-seeing-smoke.html\">wildfire detection project<\/a>\nand a continuation of my own Capstone project, a <a href=\"http:\/\/mindworks.shoutwiki.com\/wiki\/Biochar_Production_System\">biochar reactor<\/a>.<\/p>\n<h1>Wildfire Detector (Team FireSense)<\/h1>\n<p>I've already talked a lot about this project, so I'm not going to do that here. If you'd like to read up on it more, feel free to check out some of the links\nabove to the articles I've written previously. Otherwise, check out this year's team poster!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/firesenseposter.png\" style=\"width: 100%\" alt=\"Team FireSense Poster\"><\/p>\n<script src=\"http:\/\/vjs.zencdn.net\/4.0\/video.js\"><\/script>\n\n<video id=\"wildfire-wing-drop\" class=\"video-js vjs-default-skin\" controls\npreload=\"auto\" width=\"683\" height=\"384\" poster=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/TtT5YzoM5NiC5xy\/download\/Image%20Final%20Assembly%20ISO.PNG\"\ndata-setup=\"{}\">\n<source src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/fTjxgg3MYJSZiFb\/download\/20220426_155354.mp4\" type='video\/mp4'>\n<\/video>\n\n<p><img src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/4EoeExXHspY5Mmt\/download?path=&files=PCB_v3_Layout.jpg\" style=\"width: 100%\" alt=\"FireSense PCB\"><\/p>\n<p><strong>Other Pictures and Videos<\/strong><\/p>\n<ul>\n<li><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/Td3bLsJ4qxj6qqy\">https:\/\/nextcloud.stanleysolutionsnw.com\/s\/Td3bLsJ4qxj6qqy<\/a><\/li>\n<li><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/5LpHagXFSzgqSon\">https:\/\/nextcloud.stanleysolutionsnw.com\/s\/5LpHagXFSzgqSon<\/a><\/li>\n<li><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/4EoeExXHspY5Mmt\">https:\/\/nextcloud.stanleysolutionsnw.com\/s\/4EoeExXHspY5Mmt<\/a><\/li>\n<\/ul>\n<h1>Biochar Reactor (Team Biochargers)<\/h1>\n<p>I've talked less about this project here in my blog... but there's still quite a bit of information that I've written about it. So...<\/p>\n<p>Yeah.. anyway, feel free to leave a comment if you have questions!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/biochargersposter.png\" style=\"width: 100%\" alt=\"Team Biochargers Poster\"><\/p>\n<script src=\"http:\/\/vjs.zencdn.net\/4.0\/video.js\"><\/script>\n\n<video id=\"wildfire-wing-drop\" class=\"video-js vjs-default-skin\" controls\npreload=\"auto\" width=\"683\" height=\"384\" poster=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/L29wCAw6xtXJpRz\/download?path=&files=Matt%20and%20Kaitlyn%20with%20Reactor%20(Extra%20Insulation).JPG\"\ndata-setup=\"{}\">\n<source src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/booBGRygP5zFEmb\/download?path=&files=00-00%20Full%20Assembly_Final.mp4\" type='video\/mp4'>\n<\/video>\n\n<p><img src=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/booBGRygP5zFEmb\/download?path=&files=System_Breakdown_GIF.gif\" style=\"width: 100%\" alt=\"System Breakdown\"><\/p>\n<p><strong>Other Pictures and Videos<\/strong><\/p>\n<ul>\n<li><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/KRNrAZidKwwiANC\">https:\/\/nextcloud.stanleysolutionsnw.com\/s\/KRNrAZidKwwiANC<\/a><\/li>\n<li><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/booBGRygP5zFEmb\">https:\/\/nextcloud.stanleysolutionsnw.com\/s\/booBGRygP5zFEmb<\/a><\/li>\n<li><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/pzeaXT55ffTioEE\">https:\/\/nextcloud.stanleysolutionsnw.com\/s\/pzeaXT55ffTioEE<\/a><\/li>\n<li><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/L29wCAw6xtXJpRz\">https:\/\/nextcloud.stanleysolutionsnw.com\/s\/L29wCAw6xtXJpRz<\/a><\/li>\n<li><a href=\"https:\/\/nextcloud.stanleysolutionsnw.com\/s\/KPN7QkCsZdSqYpq\">https:\/\/nextcloud.stanleysolutionsnw.com\/s\/KPN7QkCsZdSqYpq<\/a><\/li>\n<\/ul>","category":[{"@attributes":{"term":"Capstone"}},{"@attributes":{"term":"capstone"}},{"@attributes":{"term":"wildfire"}},{"@attributes":{"term":"university"}},{"@attributes":{"term":"research"}},{"@attributes":{"term":"students"}},{"@attributes":{"term":"biochar"}},{"@attributes":{"term":"agriculture"}}]},{"title":"Home Automation... Condensed!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/home-automation-condensed.html","rel":"alternate"}},"published":"2022-05-23T15:23:00-07:00","updated":"2022-05-23T15:23:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-05-23:\/home-automation-condensed.html","summary":"<p>Seems that, these days, there's an endless number of things that need to be automated for an \"all-out\" home-automation system. But how should we bring all of that stuff together? Well, I think I've got an idea...<\/p>","content":"<p>As you may already know, I've been doing lots to automate my home. It's come to the point that my mother and other family members complain that they\ncan't do anything in my house without me. Well, that's left me with a bit of a conundrum. How can I make it so that people can use the home-automation\nthings, but not need any of the nuianced nonsense?<\/p>\n<h2>All of the Audio<\/h2>\n<p>I've been working to make the audio \"network\" in my home quite extensive. Between use of <a href=\"https:\/\/vb-audio.com\/Voicemeeter\/vban.htm\">VBAN<\/a> for digital\naudio streaming between my study and living room (<a href=\"\/networked-audio-using-vban-and-rpi\">read more here<\/a>), and analog connections from my study to\nkitchen, lab, and (yes) even bathroom. Now, the challenge with all that is that it's still highly reliant on my desktop or cell phone as the main source\nof audio. Additionally, whenever I have virtual meetings in my study, I need to shut everything down around the house. Not very much fun.<\/p>\n<p>I decided that I need a central control to allow easy Pianobar playback control, in addition to a \"master mute\" button that'll disable all audio streaming\naround the house. Now... I'm still working on the actual \"control box\" which will have all of the buttons, but the operational pannel is already set up.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_3d778f5.jpeg\" style=\"width: 100%\" alt=\"Audio Output Blocks\"><\/p>\n<p>Since my analog stream is in stereo, I - of course - need to have four sets of terminals to make wiring easy. That's what's shown in the above picture;\nthere's one for +R, -R, +L, and -L. Everything we need! They all connect to an input block by way of an ESP-8266 relay-breakout-board (shown in the upper-right\nof the image below).<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_eec766f.jpeg\" style=\"width: 100%\" alt=\"Audio Connector Control\"><\/p>\n<p>When I get this set up (completely) the little ESP board will listen for <a href=\"https:\/\/mqtt.org\/\">MQTT<\/a> commands from my home-automation server that will tell it\nto open the normally-closed relay contacts and effectively shut down the whole analog audio network by disconnecting my study mixer from the rest of the\nhouse.<\/p>\n<h2>Detecting Doorbell Use<\/h2>\n<p>In my house, the doorbell is a bit difficult to hear from anywhere other than the livingroom. I mean, it's possible to hear it from around the house, but not\nalways. When I'm in the kitchen, if I'm cooking (which is often what I'm doing when I'm waiting for the doorbell), I can't often hear the bell over my skillet!\nSo, I want to integrate the doorbell detection into some more intelligent operations and be able to send push notifications or even play audio chimes through\nthe centralized audio system. Shouldn't be difficult, but I need to interface with one of my little <a href=\"https:\/\/www.embeddedts.com\/products\/TS-7500\">Technologic TS7500<\/a>\ncomputers. Now... this computer has a 5VDC input. Not exactly ideal when the doorbell is running at about 16VAC.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_c71dcbc.jpeg\" style=\"width: 100%\" alt=\"Doorbell Conditioning Circuit\"><\/p>\n<p>Thus, another part of my home-automation center was built up. I needed to consume the AC input, run it through a bridge rectifier (the grey square on the right),\napply a little filtering and smoothing with a \"reasonably big\" capacitor, and then shove that into a 12VDC relay.<\/p>\n<blockquote>\n<p>and why, Joe, didn't you just use an AC relay?<\/p>\n<\/blockquote>\n<p>Erm... I don't really have a good answer for that other than the fact that I had all of these components on-hand, and it seemed like fun. Call me cheap for not\npicking up a $4.00 relay. Go ahead! I won't argue. It is cheap. But, I already had a few things on-hand, and heck! Why not?!<\/p>\n<p>That relay is really only needed to use the doorbell signal to trigger the 5VDC signal to pass to the little Linux computer's input which then, in turn, can be\nmonitored and used for triggering all manner of things.<\/p>\n<h2>Leveraging Lights<\/h2>\n<p>Another feature that I want to add is making one of the lights in my entryway a three-way switched light. The purpose of which is reasonably self-explanatory,\nbut I want to bring intelligence to one of the light in my living room. The challenge is that the light is a traditional one, and it's already wired into the\nwall (properly), and I'd rather have a true three-way-switch than the dumb little \"hack\" that I used for my dining room light (which will surely change someday).<\/p>\n<p>I've gone ahead and connected a nice, big, wiring block (shown in the upper-right of the image below) to allow for connections into the computer and the light\nitself. This should make it nice and convenient, and make a true 3-way switch possible where the computer and the light switch will always be responsive to\ncontrol, and I won't have to wait for the dumb thing to boot up and initialize.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_a8b8778.jpeg\" style=\"width: 100%\" alt=\"Wiring Blocks for Lights\"><\/p>\n<p>The Technologic computer, of course, has its own relay-break-out-board which also sandwiches nicely into its fancy little aluminum case (shown below), and that\nis how I'm taking control of the light. Just like any other three-way-switch, I'm using the NO\/NC contacts to wire opposite those of the three way switch I'll\nmount in the wall, and then I'll be able to control the light, either from a physical switch, or from the computer. Either way, no problem!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_5caf4e9.jpeg\" style=\"width: 100%\" alt=\"Computer and all the Power Supplies\"><\/p>\n<p>You might also notice that there's a couple of power supplies there. I've got a nice little \"computer-style\" power supply which provides 5V, and I've also got\na little board that I salvaged from some old (crappy) DJ dance lights that provides 12V.<\/p>\n<blockquote>\n<p>and what, Joe, is the 12V supply for?<\/p>\n<\/blockquote>\n<p>I'm glad you asked!<\/p>\n<h2>Talking with the Telegraph<\/h2>\n<p>That's right... I'm nerdy enough that I want to connect to an old Telegraph key, and actually <em>do something<\/em> with it. I'm wiring it in by my front entryway\nfor no other purpose than to excentuate my \"nerdiness\". I'm not even quite sure what the system will do, yet, but I'll come up with something cool. Maybe\nlisten for special \"commands\" and \"do\" respond with secret messages if you send the right code. Maybe just repeating messages back to the sender. Maybe\nother things. Who knows! If you've got some fun ideas, drop a note in the comments below!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"sd\">Side Note:<\/span>\n<span class=\"sd\">----------<\/span>\n\n<span class=\"sd\">In case you weren&#39;t aware (and since I still haven&#39;t written a post about how I did it, you&#39;re probably not aware)<\/span>\n<span class=\"sd\">the comment system embedded here in my blog is all hosted on an old computer in my basement (one of many), and is<\/span>\n<span class=\"sd\">completely managed by me! So no worry about some third party service discontinuing their product. I get to keep<\/span>\n<span class=\"sd\">hosting it as long as I want... Go ahead! Try it out! It even supports markdown...<\/span>\n<span class=\"sd\">&quot;&quot;&quot;<\/span>\n<\/code><\/pre><\/div>\n\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_fe0af05.jpeg\" style=\"width: 100%\" alt=\"The Wiring Blocks and Power Supplies\"><\/p>\n<h2>Wrapping Up<\/h2>\n<p>It's fair to say that there's quite a bit more that I still need to do here. In fact, I've got a lot of wiring to do, still... But I'm excited that this\nproject which, notably, is recycling the \"box\" that I used for my first foray into some home autoation is coming along nicely. In fact, this box contained\nthe same computer and some of the same components to allow me to dabble in my first home-automation and Python web-serving application. There's still much\nto be learned.<\/p>\n<p>The Linux computer is OLD, and will only (realistically) run Debian 5, which will make updates near impossible. That means that Python is going to be\nconstrained to Python 2 (if I'm lucky, maybe I can load from the DeadSnakes PPA - but I'm doubtful), and that pretty much blocks the use of MQTT. That said,\nI still think I can pretty effectively set up the computer to host a little web-server to have REST-API endpoints, and I think I can even get it set up to\n<em>make<\/em> REST-API calls.<\/p>\n<p>I'm excited to see what fun will come of this! Stay tuned for more!<\/p>","category":[{"@attributes":{"term":"Home-Improvement"}},{"@attributes":{"term":"linux"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"self-hosting"}},{"@attributes":{"term":"home-automation"}},{"@attributes":{"term":"iot"}},{"@attributes":{"term":"esp8266"}}]},{"title":"More Servers in the Basement?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/more-servers-in-the-basement.html","rel":"alternate"}},"published":"2022-04-03T21:09:00-07:00","updated":"2022-04-03T21:09:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-04-03:\/more-servers-in-the-basement.html","summary":"<p>It's spring; time for spring cleaning, right? Well, that's even more true if you've got half a dozen servers all floating around, waiting to be used. So I think it's about time that I get some of my new servers mounted in the basement to improve the general usage of my resources!<\/p>","content":"<p>Alright, I really don't have much to say about this one, other than I've got some new servers mounted in the basement!<\/p>\n<p>I picked up some new server hardware including a really nice Synology NAS, and a 2-U 64-bit computer (yeah, I get excited about that since, remember,\nmost of my computing hardware is ancient 32-bit stuff). I'm really excited to start setting them up as the new home for my Nextcloud service, and a nice\nbackup file storage server to start sharing the load of all my git backups, etc. to some rugged machinery.<\/p>\n<p>Anyway... check it all out!!!<\/p>\n<h3>Update:<\/h3>\n<p>I've got even more computers now! Take a look!<\/p>\n<p>They're working to host Nextcloud, Gitea, Prometheus, Rainloop, Grafana, and more!<\/p>","category":[{"@attributes":{"term":"Self-Hosting"}},{"@attributes":{"term":"server"}},{"@attributes":{"term":"web"}},{"@attributes":{"term":"hosting"}},{"@attributes":{"term":"self-hosting"}},{"@attributes":{"term":"hardware"}},{"@attributes":{"term":"computing"}}]},{"title":"Error Pages for Education","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/error-pages-for-education.html","rel":"alternate"}},"published":"2022-03-29T17:04:00-07:00","updated":"2022-03-29T17:04:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-03-29:\/error-pages-for-education.html","summary":"<p>What better way to learn, then by doing. That, after all, is the ethos of 4-H. The very esence of how 4-H teaches youth effective life-skills every day. This \"photo upload app\" that I've been working on relies on some of the newest, prettiest, fanciest web framework technologies available to make the common user-interaction experience very smooth so that youth may engage in digital activities easily, without having to learn new hoops to jump through. Still, it's being built <em>for<\/em> youth, <em>by<\/em> youth, so wouldn't it be disappointing if there weren't aspects that were clearly touched by youth? Clearly inspired by them? Sharing their energy and creativity?<\/p>","content":"<p>Errors are a given when it comes to computers. Heck, they're a given when it comes to most technology. We're all familiar with the <em>\"dreaded 404\"<\/em> that's\npresented to users when a page or resource can't be found. That doesn't mean that they have to be boring! After all, the text \"404\" on screen is plain;\nuninteresting. Wouldn't it be disingenuous to use just plain text for a web-service that's exciting and interesting?<\/p>\n<p>I think it would be!<\/p>\n<hr>\n<h3>Enter Youth-Designed Error Pages!<\/h3>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/err404.png\" style=\"width: 100%\" alt=\"404 Error Page\"><\/p>\n<p>This 404 page was designed by the youth that I'm working with. It gives some flavor to the site when something goes wrong. Admittedly, it's never the goal\nfor things <em>to go wrong,<\/em> but when it (inevitably) happens, we're ready!<\/p>\n<h2>Intent<\/h2>\n<p>I wanted to introduce the 4-H member who's helping me to a little \"old-fasioned\" web design. Crafting plain <code>HTML<\/code> with <code>CSS<\/code> and putting it all together\nfor the specific purposes of showing errors, but that's not exactly <em>easy<\/em> when you're just starting out. I also wanted to capture some of the \"flare\" and\ncreativity that 4-H members always seem to have. After all, isn't part of the fun of any development project putting in the easter eggs? I think so!<\/p>\n<p>So, that leaves me with two clear goals:<\/p>\n<ul>\n<li>Youth-designed static HTML\/CSS<\/li>\n<li>Capturing creativity of youth<\/li>\n<\/ul>\n<h2>Tools<\/h2>\n<p>Like I said... It's not easy to just spin HTML off the cuff when you've never done any of it before in your life! Especially when we're talking about full-\nfledged pages that have pretty formatting and the like. What to do... what to do...<\/p>\n<p>Enter: Microsoft Word.<\/p>\n<p><img src=\"https:\/\/i.redd.it\/1s0rlfdtsvg61.jpg\" style=\"width: 100%\" alt=\"The Ultimate IDE?\"><\/p>\n<p>Yep, that's right. The ultimate IDE... Well, something like that.<\/p>\n<p>Anyway, Microsoft Word may not be a good IDE by most (sane) people's standards, but that doesn't mean that we can't use it for a little visual assistance.\nOne thing that Word does have going for it in this space is the ability to easily format pages with text, images, formatting, and more. All in a convenient\ninterface. After all, isn't that the reason that most of the world uses MS Word instead of LaTeX?<\/p>\n<p>With this convenience, it's pretty easy to put some images together, add some text, center it how you want, and publish! Did I mention two key features?<\/p>\n<ul>\n<li>Microsoft Word has an web-page viewing mode to present the page as how it might appear in a browser<\/li>\n<li>Microsoft Word natively supports saving a file as <code>*.htm<\/code>\/<code>*.html<\/code><\/li>\n<\/ul>\n<p>That means that I could get anyone started with creating the pages, and worrying less about the HTML and more about how they want the pages to look.<\/p>\n<p>Excellent.<\/p>\n<h2>Bumps in the Proverbial \"Information Superhighway\"<\/h2>\n<p>Well, that's all well and good; quite simple really, but here's where the rubber hits the <code>10101001010010101010010101010010100010101111<\/code>...<\/p>\n<p>When it comes time to publish these pages, it's pretty simple, the whole repository that we're working in is based around the separation of a frontend and\na backend. The backend houses all the Python goodness, and all of the static and template files that are rendered and loaded for web-serving. That means\nthat if we want to host specific files for error pages, we can split them up and distribute them accordingly into the backend directories like shown below.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"n\">backend<\/span>\n<span class=\"o\">|--<\/span><span class=\"w\"> <\/span><span class=\"k\">static<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">   <\/span><span class=\"o\">|--<\/span><span class=\"w\"> <\/span><span class=\"n\">someErrPage_files<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">   <\/span><span class=\"o\">|<\/span><span class=\"w\">   <\/span><span class=\"o\">|--<\/span><span class=\"w\"> <\/span><span class=\"p\">(<\/span><span class=\"n\">css<\/span><span class=\"o\">\/<\/span><span class=\"n\">js<\/span><span class=\"o\">\/<\/span><span class=\"n\">other<\/span><span class=\"w\"> <\/span><span class=\"n\">supporting<\/span><span class=\"w\"> <\/span><span class=\"n\">files<\/span><span class=\"w\"> <\/span><span class=\"k\">go<\/span><span class=\"w\"> <\/span><span class=\"n\">here<\/span><span class=\"p\">)<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">   <\/span><span class=\"o\">|<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">   <\/span><span class=\"o\">|--<\/span><span class=\"w\"> <\/span><span class=\"n\">someOtherErrPage_files<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">       <\/span><span class=\"o\">|--<\/span><span class=\"w\"> <\/span><span class=\"p\">(<\/span><span class=\"n\">css<\/span><span class=\"o\">\/<\/span><span class=\"n\">js<\/span><span class=\"o\">\/<\/span><span class=\"n\">other<\/span><span class=\"w\"> <\/span><span class=\"n\">supporting<\/span><span class=\"w\"> <\/span><span class=\"n\">files<\/span><span class=\"w\"> <\/span><span class=\"k\">go<\/span><span class=\"w\"> <\/span><span class=\"n\">here<\/span><span class=\"p\">)<\/span>\n<span class=\"o\">|<\/span>\n<span class=\"o\">|--<\/span><span class=\"w\"> <\/span><span class=\"n\">templates<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">   <\/span><span class=\"o\">|--<\/span><span class=\"w\"> <\/span><span class=\"n\">someErrPage<\/span><span class=\"p\">.<\/span><span class=\"n\">html<\/span>\n<span class=\"o\">|<\/span><span class=\"w\">   <\/span><span class=\"o\">|--<\/span><span class=\"w\"> <\/span><span class=\"n\">someOtherErrPage<\/span><span class=\"p\">.<\/span><span class=\"n\">html<\/span>\n<span class=\"o\">|<\/span>\n<span class=\"o\">|<\/span><span class=\"c1\">-- main.py<\/span>\n<\/code><\/pre><\/div>\n\n<p>When we \"export\" or save the rendered error page from Word, we end up generating a single HTML file, and a folder full of supporting files. Everything\nfrom CSS, to XML, to images, and more. Since that's all static stuff, we can stick it in a sub-folder under the <code>backend\/static<\/code> directory. The plain HTML\nfile, on the other hand, goes to the <code>backend\/templates<\/code> folder where it will be pulled from by Jinja in Python and rendered.<\/p>\n<p>Key to all of this, is getting paths right. After all, pirates didn't find their treasure if they meant \"paces\" when they said \"leagues\" right?!<\/p>\n<p>In much the same way, when Word generates those files, it assumes a certain path. It assumes that the folder containing all support files is colocated with\nthe HTML file, in the same directory. Clearly, here, they are not.<\/p>\n<p>This separation is for good reason though, all of the support files are static, they're not templates. We wouldn't ever want Jinja to attempt rendering\nany one of them, so they go in the <code>static<\/code> directory. Simple as that. Now, the Python server already knows to route accordingly for static files, after\nall, early on, it uses a static router for FastAPI to do the lifting here, and it also sets up the templates structure accordingly so as to make Jinja\nrendering easy:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># Application Base<\/span>\n<span class=\"n\">app<\/span> <span class=\"o\">=<\/span> <span class=\"n\">FastAPI<\/span><span class=\"p\">()<\/span>\n\n<span class=\"n\">TEMPLATES<\/span> <span class=\"o\">=<\/span> <span class=\"kc\">None<\/span>\n\n<span class=\"nd\">@app<\/span><span class=\"o\">.<\/span><span class=\"n\">on_event<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;startup&quot;<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">async<\/span> <span class=\"k\">def<\/span> <span class=\"nf\">startup_event<\/span><span class=\"p\">():<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Event that Only Runs When App is Starting&quot;&quot;&quot;<\/span>\n    <span class=\"k\">global<\/span> <span class=\"n\">TEMPLATES<\/span>\n    <span class=\"c1\"># Mount the Static File Path<\/span>\n    <span class=\"n\">app<\/span><span class=\"o\">.<\/span><span class=\"n\">mount<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;\/static&quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">StaticFiles<\/span><span class=\"p\">(<\/span><span class=\"n\">directory<\/span><span class=\"o\">=<\/span><span class=\"s2\">&quot;static&quot;<\/span><span class=\"p\">),<\/span> <span class=\"n\">name<\/span><span class=\"o\">=<\/span><span class=\"s2\">&quot;static&quot;<\/span><span class=\"p\">)<\/span>\n    <span class=\"n\">TEMPLATES<\/span> <span class=\"o\">=<\/span> <span class=\"n\">Jinja2Templates<\/span><span class=\"p\">(<\/span><span class=\"n\">directory<\/span><span class=\"o\">=<\/span><span class=\"s2\">&quot;templates&quot;<\/span><span class=\"p\">)<\/span>\n<\/code><\/pre><\/div>\n\n<p>Notice however, that for FastAPI to \"catch\" any such requests, it needs to see a URI with the prefix of <code>\/static<\/code>. Are you beginning to see the problem?<\/p>\n<p>That's right, because MS Word doesn't include that prefix, the web-client will unknowingly ask for files that don't exist, and FastAPI will throw up its\nhands in defeat. Now, that's not all bad, since they're all just plain files, we can edit the HTML quite easily, and with a tweak or two, we can add the\nprefix where it's needed. It took me a bit to figure out exactly where this needed to be done, but when I did, it was very straight forward!<\/p>\n<h2>Results<\/h2>\n<p>I'll let you be the judge here. You've already seen the 404, but here's the 501 and 503:<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/err501.png\" style=\"width: 100%\" alt=\"404 Error Page\"><\/p>\n<p>I'm very satisfied with the results; they pages are clever, cute, and most of all, they're genuine and showcase the very esence of learning and exploring\nwith 4-H. That all makes me very happy.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/err503.png\" style=\"width: 100%\" alt=\"404 Error Page\"><\/p>\n<h2>Making Them Stand Out<\/h2>\n<p>I really wanted to be able to show off these pages whenever I want. After all, it would kinda suck to only be able to see them when there's a legitimate\nerror. That would make it very difficult to show them to passers-by! So, we did a little function-crafting!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># HTTP Error Response<\/span>\n<span class=\"nd\">@app<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;\/404&quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">response_class<\/span><span class=\"o\">=<\/span><span class=\"n\">HTMLResponse<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">async<\/span> <span class=\"k\">def<\/span> <span class=\"nf\">page404<\/span><span class=\"p\">(<\/span><span class=\"n\">request<\/span><span class=\"p\">:<\/span> <span class=\"n\">Request<\/span><span class=\"p\">):<\/span>\n    <span class=\"k\">return<\/span> <span class=\"n\">TEMPLATES<\/span><span class=\"o\">.<\/span><span class=\"n\">TemplateResponse<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;404.htm&quot;<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span><span class=\"s2\">&quot;request&quot;<\/span><span class=\"p\">:<\/span> <span class=\"n\">request<\/span><span class=\"p\">})<\/span>\n\n<span class=\"c1\"># HTTP Error Response<\/span>\n<span class=\"nd\">@app<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;\/501&quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">response_class<\/span><span class=\"o\">=<\/span><span class=\"n\">HTMLResponse<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">async<\/span> <span class=\"k\">def<\/span> <span class=\"nf\">page501<\/span><span class=\"p\">(<\/span><span class=\"n\">request<\/span><span class=\"p\">:<\/span> <span class=\"n\">Request<\/span><span class=\"p\">):<\/span>\n    <span class=\"k\">return<\/span> <span class=\"n\">TEMPLATES<\/span><span class=\"o\">.<\/span><span class=\"n\">TemplateResponse<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;501.htm&quot;<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span><span class=\"s2\">&quot;request&quot;<\/span><span class=\"p\">:<\/span> <span class=\"n\">request<\/span><span class=\"p\">})<\/span>\n\n<span class=\"c1\"># HTTP Error Response<\/span>\n<span class=\"nd\">@app<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;\/503&quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">response_class<\/span><span class=\"o\">=<\/span><span class=\"n\">HTMLResponse<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">async<\/span> <span class=\"k\">def<\/span> <span class=\"nf\">page501<\/span><span class=\"p\">(<\/span><span class=\"n\">request<\/span><span class=\"p\">:<\/span> <span class=\"n\">Request<\/span><span class=\"p\">):<\/span>\n    <span class=\"k\">return<\/span> <span class=\"n\">TEMPLATES<\/span><span class=\"o\">.<\/span><span class=\"n\">TemplateResponse<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;503.htm&quot;<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span><span class=\"s2\">&quot;request&quot;<\/span><span class=\"p\">:<\/span> <span class=\"n\">request<\/span><span class=\"p\">})<\/span>\n\n<span class=\"c1\"># HTTP Exception Handlers<\/span>\n<span class=\"nd\">@app<\/span><span class=\"o\">.<\/span><span class=\"n\">exception_handler<\/span><span class=\"p\">(<\/span><span class=\"n\">StarletteHTTPException<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">async<\/span> <span class=\"k\">def<\/span> <span class=\"nf\">custom_http_exception_handler<\/span><span class=\"p\">(<\/span><span class=\"n\">request<\/span><span class=\"p\">:<\/span> <span class=\"n\">Request<\/span><span class=\"p\">,<\/span>\n                                        <span class=\"n\">exc<\/span><span class=\"p\">:<\/span> <span class=\"n\">StarletteHTTPException<\/span><span class=\"p\">):<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">exc<\/span><span class=\"o\">.<\/span><span class=\"n\">status_code<\/span> <span class=\"o\">==<\/span> <span class=\"mi\">404<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">return<\/span> <span class=\"k\">await<\/span> <span class=\"n\">page404<\/span><span class=\"p\">(<\/span><span class=\"n\">request<\/span><span class=\"o\">=<\/span><span class=\"n\">request<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">elif<\/span> <span class=\"n\">exc<\/span><span class=\"o\">.<\/span><span class=\"n\">status_code<\/span> <span class=\"o\">==<\/span> <span class=\"mi\">501<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">return<\/span> <span class=\"k\">await<\/span> <span class=\"n\">page501<\/span><span class=\"p\">(<\/span><span class=\"n\">request<\/span><span class=\"o\">=<\/span><span class=\"n\">request<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">elif<\/span> <span class=\"n\">exc<\/span><span class=\"o\">.<\/span><span class=\"n\">status_code<\/span> <span class=\"o\">==<\/span> <span class=\"mi\">503<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">return<\/span> <span class=\"k\">await<\/span> <span class=\"n\">page501<\/span><span class=\"p\">(<\/span><span class=\"n\">request<\/span><span class=\"o\">=<\/span><span class=\"n\">request<\/span><span class=\"p\">)<\/span>\n<\/code><\/pre><\/div>\n\n<p>See here, that we were able to create three unique web-endpoints with FastAPI for each of the error pages, and then we could wrap them in the single error\nhandler. This means that when a user goes to <code>https:\/\/develop.idaho4h.com\/404<\/code> they actually see the 404 page without generating a <em>real<\/em> 404. The same is\ntrue for the other two errors, making the whole thing pretty slick, indeed.<\/p>\n<p>Guess I'm tooting my own horn pretty loudly, at this point, aren't I?<\/p>\n<hr>\n<p>In conclusion, it's been lots of fun getting to work with youth to make these custom error pages and show them off! It really helps demonstrate the work\nthat this 4-H member has put in!<\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"4-h"}},{"@attributes":{"term":"education"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"http"}},{"@attributes":{"term":"html"}},{"@attributes":{"term":"status-codes"}}]},{"title":"What is Tasmota, Anyway?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/what-is-tasmota-anyway.html","rel":"alternate"}},"published":"2022-03-23T22:34:00-07:00","updated":"2022-03-23T22:34:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-03-23:\/what-is-tasmota-anyway.html","summary":"<p>I've had a handfull of friends and colleagues ask about Tasmota; what it is, what it does, how to use it, etc. So I thought I'd put together a little article to tell you about this AWESOME open-source IoT firmware, why I use it, and how you can get started with it too!<\/p>","content":"<p><img src=\"https:\/\/tasmota.github.io\/docs\/_media\/frontlogo.svg\" width=\"300\" alt=\"Tasmota\" align=\"right\"><\/p>\n<p>If I've talked to you at all in the past year or so, you probably already know that I've gone a little wild with the home automation thing. I've got\nall sorts of \"smart stuff\" scattered around the house. Mostly to turn lights on or off, but I do have some more intelligent things too. But, I'm a bit of\na tin-foil-hat-wearer, I guess you could say. After all, I run my own local Home Assistant, and to tie in nicely, I also use a free-and-open-source\nfirmware for most of my IoT devices, a little, something called <em>Tasmota<\/em>.<\/p>\n<h2>What is Tasmota?<\/h2>\n<p><a href=\"https:\/\/tasmota.github.io\/docs\/Getting-Started\/\">Tasmota<\/a> is an open-source firmware, developed for the ESP8266 and ESP32 microcontrollers manufactured by Espressif. It's a lightweight, multifunctional, and\nfully featured smart-home software for nearly every IoT device you can think of. It supports both MQTT and RESTful API communications, meaning that it can tie into nearly every home automation system available today.<\/p>\n<p>In particular, there's a few things that I really like about the project.<\/p>\n<h4>1) Its Openness<\/h4>\n<p>It's absolutely fantastic to have such a wonderful, well developed project all floating out in the open. Tasmota started as a simple project built for\nESP8266 devices, but quickly expanded to work for just about any IoT devices that run on the Espressif chipset. All sorts of common, economical brands of\nsmart plugs, smart bulbs, and more are supported by Tasmota, all because they're built on the Espressif chip, and the Tasmota firmware core is open,\nallowing for continued expanse and improvement.<\/p>\n<p>Tasmota is all available on GitHub, stored in the open, improved by the many. Plenty of wonderful developers who are all working on the project, but\nclearly, I'm caught up on the great utopian parts of it all, and overlooking the downsides and drawbacks of open source. But perhaps that's ok. Maybe\nthat's just what I need to keep the spirit, and maybe that's good for us all. I guess I'm digressing, aren't I?<\/p>\n<h4>2) Its Feature Set<\/h4>\n<p>Tasmota's got some awesome features, too! From it's simple, easy-to-get-started-with web interface and configuration to its integration with MQTT, and\nsimple HTTP REST interfaces. Even it's fully baked-in console for configuration. Tasmota has all the features that the other modern IoT devices have,\nand all in one place.<\/p>\n<h4>3) Its Lack of Cloud Dependence<\/h4>\n<p>Did I mention that I've got a case of \"tin-foil-hat-itis\"? Well, it's a problem. Perhaps not such a \"problem\" but it does mean that most of my smart\nthings don't talk to the rest of the world. That does mean that I can't exactly take direct control of all of my home automation when I'm not at home,\nbut I think I like it that way. A little security, of sorts...<\/p>\n<p>Having no reliance on the wild inter-webs, it means that when things go down (like my connection to the internet), I still have control of all my things.\nWell, until I break things (as per the usual).<\/p>\n<h2>Wrapping Up<\/h2>\n<p>I'm a big fan of open source (clearly) and if you're ever looking to make your home a little smarter, let me recommend you take a look at Tasmota!<\/p>","category":[{"@attributes":{"term":"IoT"}},{"@attributes":{"term":"tasmota"}},{"@attributes":{"term":"iot"}},{"@attributes":{"term":"smart-home"}},{"@attributes":{"term":"open-source"}},{"@attributes":{"term":"esp8266"}},{"@attributes":{"term":"esp32"}},{"@attributes":{"term":"home-automation"}},{"@attributes":{"term":"wifi"}}]},{"title":"Automagic Test Websites","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/automagic-test-websites.html","rel":"alternate"}},"published":"2022-03-22T18:32:00-07:00","updated":"2022-03-23T14:49:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-03-22:\/automagic-test-websites.html","summary":"<p>Over the last few months, I've been working with a 4-H member to develop and build a smart, simple, and elegant web-application for 4-H members to upload photos they take while participating in a youth conference. Since I'm working with a 4-H member to develop the website, everything needs to have a focus on education, and I need to spend as little time as possible fussing with the infrastructure side of things, since that's not what we're focusing on. I decided that we should make a system using my self-hosted GitLab and Jenkins instances to automagically deploy changes so that the youth doesn't have to learn how that's done, and fight with the server all the time!<\/p>","content":"<p>As you may already know, I've been working on some fun and interesting stuff with some 4-H members over the last few months. We're developing a\n<a href=\"\/reactjs-python-pictures-and-4h.html\">Python-based photo-upload-app<\/a> to allow 4-H youth (delegates) at a youth conference upload photos securely\nto participate in competitions during the conference, and to share photos for the end-of-conference-slideshow.<\/p>\n<p>Anyway, I'm not here to talk about the app; I'll be doing plenty more of that, I'm sure.<\/p>\n<p>What I am here to talk about is how I'm making it automatically deploy the application for\n<a href=\"https:\/\/gitlab.stanleysolutionsnw.com\/idaho4h\/4HPhotoUploader\">each development branch in my GitLab instance<\/a> automatically with a little Jenkins magic.\nYou see, since I have a personal Jenkins instance set up to work with my GitLab, it's able to authenticate and pull all of the source-code from any branch\nin whichever repository I configure. Better yet, I can make it automatically look for changes to any-and-all branches, and automatically run builds when\nthose branches change.<\/p>\n<p>What I've just described is nothing new to those who work in CI\/CD (Continuous Integration\/Continuous Deployment) systems, but it is valuable and, I think,\nnovel in this context. I'm using this automated build system to abstract the complexity of deploying a containerized application so that I can focus on\ndevelopment with a youth member, and focus on programming, not deploying servers. Using Jenkins, in this way, the 4-H'er that I'm working with can write\nhis own code in GitLab's built in editor, so he doesn't need to learn <code>git<\/code> and he can commit his work, and watch it automatically deploy for him!<\/p>\n<h2>How I'm Making the Magic Happen with Jenkins<\/h2>\n<p>So... Like I said, Jenkins is doing most of the heavy lifting here. It's looking for changes, then running builds when those changes appear; but Jenkins\nisn't the <em>hosting server<\/em>, itself, it's just the CI server. In fact, I'm using a Linode server which is configured and provisioned as a Jenkins agent (as\nfar as my Jenkins instance is concerned) that has NGINX, Python, Docker, and docker-compose installed to do the heavy lifting. This way, I can make it all\ncome together quite nicely.<\/p>\n<p>The pipeline, in a nutshell, looks something like this:<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/jenkins_4h_deployment.png\" style=\"width: 100%\" alt=\"Jenkins Pipeline\"><\/p>\n<p>So let's walk through those stages...<\/p>\n<h4>1) Prep<\/h4>\n<p>Pretty much what it sounds like. Pull down the repo source, do any other setup that's necessary.<\/p>\n<p>Notably though, this stage actually URL-ifies the branch name. In other words, it sanitizes the branch-name to make it appropriate for a unique\nsubdomain-name. This is kinda critical. Each branch will end up getting its own subdomain under the <code>idaho4h.com<\/code> domain that's filed for this server.\nThe process is pretty simple, really, just replace whitespace and slashes with dashes so that it's a \"legal\" subdomain.<\/p>\n<p>Here's the function I use to do it:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">urlifyBranchName<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"c1\">\/\/ &quot;Sanitize&quot; the Branch Name as a Sub-Domain Name<\/span>\n<span class=\"w\">    <\/span><span class=\"c1\">\/\/ i.e., https:\/\/&lt;branch-name&gt;.idaho4h.com\/<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">DOCKER_BRANCH_NAME<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"n\">BRANCH_NAME<\/span><span class=\"o\">.<\/span><span class=\"na\">replaceAll<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot; &quot;<\/span><span class=\"o\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;-&quot;<\/span><span class=\"o\">).<\/span><span class=\"na\">replaceAll<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;\/&quot;<\/span><span class=\"o\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;-&quot;<\/span><span class=\"o\">)<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">echo<\/span><span class=\"w\"> <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">DOCKER_BRANCH_NAME<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre><\/div>\n\n<h4>2) Test Python<\/h4>\n<p>This one's pretty simple. Since Python is holding up the backend, I want to run some tests. This is pretty much unit-test only (no functional or\nintegration tests, here - yet), but it allows us to check some of the simple sanitizer functions we're going to count on for the service.<\/p>\n<p>Adittedly, I had a bit of trouble with some of the virtual-environment stuff, and haven't gotten back around to fixing that yet, so for now just ignore\nthe title of the function. The execution is pretty simple. pip-install the requirements we need, then run <code>pytest<\/code>. Voila!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">runPythonTestsInVirtualEnv<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;Test Python&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Install Requirements<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;&quot;&quot;python3 -m pip install -r ${WORKSPACE}\/${requirementsFile}&quot;&quot;&quot;<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Run `pytest`<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;pytest&quot;<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre><\/div>\n\n<h4>3) Build the Frontend<\/h4>\n<p>This whole project relies on a Python backend, and a React.js frontend. I need to get around to documenting this whole intertie a little better, but for\nnow, just know that there are two root folders. One for the frontend, the other for the backend. When building the whole thing, we build and export the\nfrontend and shove it into the static folder of the backend. Python can then serve all of the javascript, html, and css files from there. Pretty simple,\nreally (took a while to figure out, though).<\/p>\n<p>What this stage does can be summarized as follows: install npm resources, fix what audit warnings can be handled, build the artifacts. Fairly simple!\nAgain though, I'll note that there is some prior setup required to make the whole npm ecosystem \"happy\" with the structure of the frontend folder.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">buildReactFrontend<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;React Frontend&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">dir<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;frontend&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"c1\">\/\/ Ensure all Packages are Installed<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;npm install&quot;<\/span>\n\n<span class=\"w\">            <\/span><span class=\"c1\">\/\/ Attempt to Resolve Reported Vulnerabilities<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;npm audit fix || true&quot;<\/span><span class=\"w\"> <\/span><span class=\"c1\">\/\/ Don&#39;t fail on normal warning<\/span>\n\n<span class=\"w\">            <\/span><span class=\"c1\">\/\/ Use the Build Tooling<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;npm run build&quot;<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">}<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre><\/div>\n\n<h4>4) Dockerize<\/h4>\n<p>This whole thing runs in a little docker container. Pretty simple!<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c\"># Dockerfile for Idaho 4-H Photo Upload Service<\/span>\n<span class=\"k\">FROM<\/span><span class=\"w\"> <\/span><span class=\"s\">python:3.9<\/span>\n\n<span class=\"k\">WORKDIR<\/span><span class=\"w\"> <\/span><span class=\"s\">\/server<\/span>\n\n<span class=\"k\">COPY<\/span><span class=\"w\"> <\/span>.\/backend<span class=\"w\"> <\/span>\/server\n\n<span class=\"k\">RUN<\/span><span class=\"w\"> <\/span>pip<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>--no-cache-dir<span class=\"w\"> <\/span>--upgrade<span class=\"w\"> <\/span>-r<span class=\"w\"> <\/span>\/server\/requirements.txt\n\n<span class=\"c\"># Run Server on localhost:8383 so Nginx can Hit it without Direct Extern. Access<\/span>\n<span class=\"k\">CMD<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"s2\">&quot;uvicorn&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;main:app&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;--host&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;0.0.0.0&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;--port&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;80&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;--log-config&quot;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;log_conf.yml&quot;<\/span><span class=\"p\">]<\/span>\n<\/code><\/pre><\/div>\n\n<p>Basically, we just dockerize the Python application after pulling all of its backend source contents into one spot. This whole container is then built and\ndeployed with a little docker-compose magic:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"nt\">version<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s\">&quot;3.9&quot;<\/span>\n\n<span class=\"c1\"># Web-UI is Defined by Local &quot;Dockerfile&quot;, Must Be Rebuilt Each Time<\/span>\n<span class=\"nt\">services<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"nt\">webui<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">build<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"nt\">context<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">.\/<\/span>\n<span class=\"w\">      <\/span><span class=\"nt\">dockerfile<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">.\/Dockerfile<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">ports<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">$NEXT_AVAIL_APP_PORT:80<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">restart<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">$RESTART_POLICY<\/span>\n<span class=\"w\">    <\/span><span class=\"nt\">environment<\/span><span class=\"p\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">LYCHEE_API_USER<\/span>\n<span class=\"w\">      <\/span><span class=\"p p-Indicator\">-<\/span><span class=\"w\"> <\/span><span class=\"l l-Scalar l-Scalar-Plain\">LYCHEE_API_TOKEN<\/span>\n<\/code><\/pre><\/div>\n\n<p>Notice the use of a few things, we've got several environment variables in that docker-compose file:<\/p>\n<ul>\n<li><code>NEXT_AVAIL_APP_PORT<\/code> Defines the port that should be used on the host system such that NGINX may pass it traffic.<\/li>\n<li><code>RESTART_POLICY<\/code> Defines whether or not the container should restart automatically, this is only set for the <code>main<\/code> and <code>develop<\/code> branches.<\/li>\n<li><code>LYCHEE_API_USER<\/code> Calls out that a local environment variable should be pulled from Jenkins to declare the username to use for Lychee access.<\/li>\n<li><code>LYCHEE_API_TOKEN<\/code> As for the username, calls out reference to what Jenkins provides as the password (token) for the API access to Lychee.<\/li>\n<\/ul>\n<p>This, of course, is all leveraged in Jenkins:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">buildDockerContainer<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;Docker Container&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Check for &quot;Default&quot; Conditions<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ MAIN and DEVELOP Branches Should Have Consistent Ports<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Their Containers Should Also Restart Automatically<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">if<\/span><span class=\"w\"> <\/span><span class=\"o\">(<\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">DOCKER_BRANCH_NAME<\/span><span class=\"w\"> <\/span><span class=\"o\">==<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;main&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">NEXT_AVAIL_APP_PORT<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;8000&quot;<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">RESTART_POLICY<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;always&quot;<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">}<\/span><span class=\"w\"> <\/span><span class=\"k\">else<\/span><span class=\"w\"> <\/span><span class=\"k\">if<\/span><span class=\"w\"> <\/span><span class=\"o\">(<\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">DOCKER_BRANCH_NAME<\/span><span class=\"w\"> <\/span><span class=\"o\">==<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;develop&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">NEXT_AVAIL_APP_PORT<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;8001&quot;<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">RESTART_POLICY<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;always&quot;<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">}<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">echo<\/span><span class=\"w\"> <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">NEXT_AVAIL_APP_PORT<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Provide credentials to the docker-compose script so that<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ container will reference the Lychee API credentials.<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">withCredentials<\/span><span class=\"o\">([<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">usernamePassword<\/span><span class=\"o\">(<\/span><span class=\"nl\">credentialsId:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;idaho4hapi&#39;<\/span><span class=\"o\">,<\/span>\n<span class=\"w\">            <\/span><span class=\"nl\">usernameVariable:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;LYCHEE_API_USER&#39;<\/span><span class=\"o\">,<\/span>\n<span class=\"w\">            <\/span><span class=\"nl\">passwordVariable:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;LYCHEE_API_TOKEN&#39;<\/span><span class=\"o\">)<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">])<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"c1\">\/\/ Build the Container - Using the Branch-Name as Env Var<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;docker-compose up -d --build&quot;<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">}<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre><\/div>\n\n<p>The Jenkins scripting is responsible for doing a few things. First and foremost, if the branch is one of the \"static\" branches (<code>main<\/code> or <code>develop<\/code>),\nit's given a static port (the same as the <em>last<\/em> time this branch was deployed). If it's not one of the key branches, then it's given the next available\nport, one that comes from a file on the host OS and is incremented each time there's a new deployment. Admittedly, this is a bit intense, and it makes\nthe port move a little more than it probably needs to, but it does ensure somewhat robust port management.<\/p>\n<p>At the same time the port number is being decided, the restart-policy may be updated. You see, the default restart policy is <code>\"no\"<\/code>, indicating that the\ncontainer should NOT restart if it were to fail, or the host OS were to restart; however, that's not the behavior we want for the \"static\" branches. They\nshould restart whenever possible! And that's exactly what the <code>RESTART_POLICY<\/code> environmental override does. It updates the restart policy fro <code>\"no\"<\/code> to\n<code>\"always\"<\/code> for the static branches.<\/p>\n<p>The next piece of the script is to run the docker-compose command, but notice how that's done in the context of a <code>withCredentials<\/code> block. This allows\nJenkins to provision the Lyche credentials to environmental variables that docker-compose can reference and inject into the container at build-time. This\ngives us the ability to launch the container with the appropriate credentials, while securing them in Jenkins and avoiding the need to have them stored as\na static file either in GitLab, or on the server, itself. Sweet!<\/p>\n<h4>5) Deploy in NGINX<\/h4>\n<p>Who likes typing in the specific port for the web-service they want to use... or better yet, besides the nerdier of us who know about it... who would even think of that?<\/p>\n<p>That's exactly where NGINX comes in in this case. NGINX does all of the proxying for me on this server. What's a proxy? you ask. Well, according to\nthe Merriam-Webster dictionary:<\/p>\n<blockquote>\n<p>proxy (noun)<\/p>\n<p>\\ \u02c8pr\u00e4k-s\u0113  \\<\/p>\n<p>the agency, function, or office of a deputy who acts as a substitute for another<\/p>\n<\/blockquote>\n<p>In my case, NGINX <em>proxies<\/em> or represents each of the various services and, in turn, passes web requests to them. So for each new branch, I need to create a new proxy for the associated, url-ified branch name to pass to the specific application port that the container is listening on.<\/p>\n<p>That's done pretty simply by running a little Python script to generate the NGINX configuration, then we restart NGINX to accept the new config.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">deployContainerInNginx<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;Deploy in NGINX&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Generate the NGINX Config File<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;python3 .\/writeNginxConfig.py&quot;<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Restart NGINX To Apply Config<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;sudo \/usr\/bin\/systemctl restart nginx&quot;<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre><\/div>\n\n<p>That little Python script helps us out too, it basically sucks in a few of the environmental variables to determine how the configuration should be written\n(or if it should be written at all - it's not written for <code>main<\/code> or <code>develop<\/code> branches). After slurping up those environmental variables, it writes out a\nnew file with the configuration into NGINX's sites-enabled directory.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\">################################################################################<\/span>\n<span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"sd\">deployNginx.py<\/span>\n\n<span class=\"sd\">A deployment helper script to automate the addition of new NGINX configuration<\/span>\n<span class=\"sd\">files in the \/etc\/nginx\/sites-enabled directory.<\/span>\n<span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"c1\">################################################################################<\/span>\n\n<span class=\"kn\">import<\/span> <span class=\"nn\">os<\/span>\n\n<span class=\"n\">PORT_VAR<\/span> <span class=\"o\">=<\/span> <span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;NEXT_AVAIL_APP_PORT&quot;<\/span><span class=\"p\">)<\/span>\n<span class=\"n\">BRANCH_VAR<\/span> <span class=\"o\">=<\/span> <span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;DOCKER_BRANCH_NAME&quot;<\/span><span class=\"p\">)<\/span>\n\n<span class=\"n\">NGINX_CONFIG<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;&quot;&quot;<\/span>\n<span class=\"s2\"># <\/span><span class=\"si\">{BRANCH_NAME}<\/span><span class=\"s2\"> Configuration<\/span>\n<span class=\"s2\">server {{<\/span>\n<span class=\"s2\">    listen 80;<\/span>\n<span class=\"s2\">    server_name <\/span><span class=\"si\">{BRANCH_NAME}<\/span><span class=\"s2\">.idaho4h.com www.<\/span><span class=\"si\">{BRANCH_NAME}<\/span><span class=\"s2\">.idaho4h.com;<\/span>\n\n<span class=\"s2\">    access_log  \/home\/jenkins\/nginx-logs\/<\/span><span class=\"si\">{BRANCH_NAME}<\/span><span class=\"s2\">.log;<\/span>\n\n<span class=\"s2\">    location \/ {{<\/span>\n<span class=\"s2\">        proxy_pass http:\/\/127.0.0.1:<\/span><span class=\"si\">{CONTAINER_PORT}<\/span><span class=\"s2\">;<\/span>\n<span class=\"s2\">        proxy_set_header Host $http_host;<\/span>\n<span class=\"s2\">        proxy_set_header Upgrade $http_upgrade;<\/span>\n<span class=\"s2\">        proxy_set_header Connection &quot;upgrade&quot;;<\/span>\n<span class=\"s2\">        proxy_set_header X-Real-IP $remote_addr;<\/span>\n<span class=\"s2\">        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;<\/span>\n<span class=\"s2\">        proxy_set_header X-Scheme $scheme;<\/span>\n\n<span class=\"s2\">        client_max_body_size 0;<\/span>\n<span class=\"s2\">    }}<\/span>\n\n<span class=\"s2\">}}<\/span>\n<span class=\"s2\">&quot;&quot;&quot;<\/span>\n\n<span class=\"k\">if<\/span> <span class=\"vm\">__name__<\/span> <span class=\"o\">==<\/span> <span class=\"s1\">&#39;__main__&#39;<\/span><span class=\"p\">:<\/span>\n    <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Branch: &quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">BRANCH_VAR<\/span><span class=\"p\">)<\/span>\n    <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Port: &quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">PORT_VAR<\/span><span class=\"p\">)<\/span>\n    <span class=\"c1\"># Validate that this is not one of the &quot;special&quot; cases<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">BRANCH_VAR<\/span><span class=\"o\">.<\/span><span class=\"n\">lower<\/span><span class=\"p\">()<\/span> <span class=\"ow\">in<\/span> <span class=\"p\">[<\/span><span class=\"s1\">&#39;main&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;develop&#39;<\/span><span class=\"p\">]:<\/span>\n        <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Skipping NGINX Redeploy&quot;<\/span><span class=\"p\">)<\/span>\n        <span class=\"n\">exit<\/span><span class=\"p\">(<\/span><span class=\"mi\">0<\/span><span class=\"p\">)<\/span> <span class=\"c1\"># Stop - Don&#39;t Change Port Number<\/span>\n    <span class=\"c1\"># Write the New Config File<\/span>\n    <span class=\"k\">with<\/span> <span class=\"nb\">open<\/span><span class=\"p\">(<\/span><span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">path<\/span><span class=\"o\">.<\/span><span class=\"n\">join<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;\/etc\/nginx\/sites-enabled&#39;<\/span><span class=\"p\">,<\/span> <span class=\"n\">BRANCH_VAR<\/span><span class=\"p\">),<\/span> <span class=\"s1\">&#39;w&#39;<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">f<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">f<\/span><span class=\"o\">.<\/span><span class=\"n\">write<\/span><span class=\"p\">(<\/span>\n            <span class=\"c1\"># Fill with the Cleaned Branch Variable and the Container&#39;s Port<\/span>\n            <span class=\"n\">NGINX_CONFIG<\/span><span class=\"o\">.<\/span><span class=\"n\">format<\/span><span class=\"p\">(<\/span>\n                <span class=\"n\">BRANCH_NAME<\/span><span class=\"o\">=<\/span><span class=\"n\">BRANCH_VAR<\/span><span class=\"p\">,<\/span>\n                <span class=\"n\">CONTAINER_PORT<\/span><span class=\"o\">=<\/span><span class=\"n\">PORT_VAR<\/span>\n            <span class=\"p\">)<\/span>\n        <span class=\"p\">)<\/span>\n\n<span class=\"c1\"># END<\/span>\n<\/code><\/pre><\/div>\n\n<h4>6) Wrap Up<\/h4>\n<p>Pretty much exactly what it sounds like, this final stage just puts things \"away\" after the fact. The primary purpose of this operation is just to store\nthe configuration for the TCP port to use for Docker the next time around. It's all simplified with the use of a Python file.<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\">################################################################################<\/span>\n<span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"sd\">updatePort.py<\/span>\n\n<span class=\"sd\">A deployment helper script to automate the &quot;bumping&quot; of the container port file<\/span>\n<span class=\"sd\">to coordinate cross-branch container deployments.<\/span>\n<span class=\"sd\">&quot;&quot;&quot;<\/span>\n<span class=\"c1\">################################################################################<\/span>\n\n<span class=\"kn\">import<\/span> <span class=\"nn\">os<\/span>\n\n<span class=\"n\">PORT_VAR<\/span> <span class=\"o\">=<\/span> <span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;NEXT_AVAIL_APP_PORT&quot;<\/span><span class=\"p\">)<\/span>\n<span class=\"n\">BRANCH_VAR<\/span> <span class=\"o\">=<\/span> <span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getenv<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;DOCKER_BRANCH_NAME&quot;<\/span><span class=\"p\">)<\/span>\n\n<span class=\"k\">if<\/span> <span class=\"vm\">__name__<\/span> <span class=\"o\">==<\/span> <span class=\"s1\">&#39;__main__&#39;<\/span><span class=\"p\">:<\/span>\n    <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Branch: &quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">BRANCH_VAR<\/span><span class=\"p\">)<\/span>\n    <span class=\"c1\"># Validate that this is not one of the &quot;special&quot; cases<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">BRANCH_VAR<\/span><span class=\"o\">.<\/span><span class=\"n\">lower<\/span><span class=\"p\">()<\/span> <span class=\"ow\">in<\/span> <span class=\"p\">[<\/span><span class=\"s1\">&#39;main&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;develop&#39;<\/span><span class=\"p\">]:<\/span>\n        <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Skipping Automatic Port Update&quot;<\/span><span class=\"p\">)<\/span>\n        <span class=\"n\">exit<\/span><span class=\"p\">(<\/span><span class=\"mi\">0<\/span><span class=\"p\">)<\/span> <span class=\"c1\"># Stop - Don&#39;t Change Port Number<\/span>\n\n    <span class=\"c1\"># Run Main Operation<\/span>\n    <span class=\"k\">with<\/span> <span class=\"nb\">open<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;\/home\/jenkins\/app-ports&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;r&#39;<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">f<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">content<\/span> <span class=\"o\">=<\/span> <span class=\"n\">f<\/span><span class=\"o\">.<\/span><span class=\"n\">read<\/span><span class=\"p\">()<\/span>\n\n    <span class=\"c1\"># Modify the Port Number<\/span>\n    <span class=\"n\">current_port<\/span> <span class=\"o\">=<\/span> <span class=\"nb\">int<\/span><span class=\"p\">(<\/span><span class=\"n\">PORT_VAR<\/span><span class=\"p\">)<\/span>\n    <span class=\"n\">next_port<\/span> <span class=\"o\">=<\/span> <span class=\"n\">current_port<\/span> <span class=\"o\">+<\/span> <span class=\"mi\">1<\/span>\n    <span class=\"n\">content<\/span> <span class=\"o\">=<\/span> <span class=\"n\">content<\/span><span class=\"o\">.<\/span><span class=\"n\">replace<\/span><span class=\"p\">(<\/span><span class=\"nb\">str<\/span><span class=\"p\">(<\/span><span class=\"n\">current_port<\/span><span class=\"p\">),<\/span> <span class=\"nb\">str<\/span><span class=\"p\">(<\/span><span class=\"n\">next_port<\/span><span class=\"p\">))<\/span>\n    <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Current Deployment Port: &quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">current_port<\/span><span class=\"p\">)<\/span>\n    <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Next Deployment Port: &quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">next_port<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"k\">with<\/span> <span class=\"nb\">open<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;\/home\/jenkins\/app-ports&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;w&#39;<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">f<\/span><span class=\"p\">:<\/span>\n        <span class=\"n\">f<\/span><span class=\"o\">.<\/span><span class=\"n\">write<\/span><span class=\"p\">(<\/span><span class=\"n\">content<\/span><span class=\"p\">)<\/span>\n\n<span class=\"c1\"># END<\/span>\n<\/code><\/pre><\/div>\n\n<h2>Recapping<\/h2>\n<p>This whole automated flow allows me to keep builds going auto-magically so that I can work with a 4-H youth member to develop this simple little\nphoto-upload app without having to go too deep into the weeds with all of the development nightmares, we can focus on the basics of the code, and then let\nthe automated system keep it all moving along for us.<\/p>\n<details>\n  <summary>Click to expand all the Jenkinsfile goodness!<\/summary>\n\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"cm\">\/*******************************************************************************<\/span>\n<span class=\"cm\"> *<\/span>\n<span class=\"cm\"> * Jenkinsfile for Idaho 4-H Delegate Photo Upload Interface<\/span>\n<span class=\"cm\"> *<\/span>\n<span class=\"cm\"> * 2021 - Stanley Solutions<\/span>\n<span class=\"cm\"> * Joe Stanley<\/span>\n<span class=\"cm\"> ******************************************************************************\/<\/span>\n\n<span class=\"c1\">\/\/ Global Variables<\/span>\n<span class=\"c1\">\/\/ In Groovy, global variables are not given a type or proceeded by def<\/span>\n<span class=\"n\">requirementsFile<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;backend\/requirements.txt&quot;<\/span>\n\n<span class=\"n\">properties<\/span><span class=\"o\">([[<\/span><span class=\"n\">$class<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;GitLabConnectionProperty&#39;<\/span><span class=\"o\">,<\/span><span class=\"w\"> <\/span><span class=\"nl\">gitLabConnection:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;StanleySolutions GitLab&#39;<\/span><span class=\"o\">]])<\/span>\n\n<span class=\"n\">node<\/span><span class=\"w\"> <\/span><span class=\"o\">(<\/span><span class=\"s1\">&#39;4happserver&#39;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n\n<span class=\"w\">    <\/span><span class=\"c1\">\/\/ Set Up Environment Variables<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">parameters<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">string<\/span><span class=\"o\">(<\/span><span class=\"nl\">name:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;DOCKER_BRANCH_NAME&#39;<\/span><span class=\"o\">,<\/span><span class=\"w\"> <\/span><span class=\"nl\">defaultValue:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;&#39;<\/span><span class=\"o\">)<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">string<\/span><span class=\"o\">(<\/span><span class=\"nl\">name:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;NEXT_AVAIL_APP_PORT&#39;<\/span><span class=\"o\">,<\/span><span class=\"w\"> <\/span><span class=\"nl\">defaultValue:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;&#39;<\/span><span class=\"o\">)<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">string<\/span><span class=\"o\">(<\/span><span class=\"nl\">name:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;RESTART_POLICY&#39;<\/span><span class=\"o\">,<\/span><span class=\"w\"> <\/span><span class=\"nl\">defaultValue:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;no&#39;<\/span><span class=\"o\">)<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"w\">    <\/span><span class=\"c1\">\/\/ Clean Workspace<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">cleanWs<\/span><span class=\"o\">()<\/span>\n\n<span class=\"w\">    <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s1\">&#39;Prep&#39;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Check Out Source Code<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">checkout<\/span><span class=\"w\"> <\/span><span class=\"n\">scm<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Load the Next Available Port as an Env Var<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">load<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;\/home\/jenkins\/app-ports&quot;<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Clean Branch Name<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">urlifyBranchName<\/span><span class=\"o\">()<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n\n<span class=\"w\">    <\/span><span class=\"c1\">\/\/ Publish Status of All Contained Stages<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">gitlabCommitStatus<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Test Backend<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">runPythonTestsInVirtualEnv<\/span><span class=\"o\">()<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Build Frontend<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">buildReactFrontend<\/span><span class=\"o\">()<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Stand Up Container<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">buildDockerContainer<\/span><span class=\"o\">()<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Deploy Container<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">deployContainerInNginx<\/span><span class=\"o\">()<\/span>\n\n<span class=\"w\">        <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;Wrap Up&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"c1\">\/\/ Update Port for Next Build<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;python3 .\/updatePort.py&quot;<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">}<\/span>\n\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n\n\n<span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">urlifyBranchName<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"c1\">\/\/ &quot;Sanitize&quot; the Branch Name as a Sub-Domain Name<\/span>\n<span class=\"w\">    <\/span><span class=\"c1\">\/\/ i.e., https:\/\/&lt;branch-name&gt;.idaho4h.com\/<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">DOCKER_BRANCH_NAME<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"n\">BRANCH_NAME<\/span><span class=\"o\">.<\/span><span class=\"na\">replaceAll<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot; &quot;<\/span><span class=\"o\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;-&quot;<\/span><span class=\"o\">).<\/span><span class=\"na\">replaceAll<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;\/&quot;<\/span><span class=\"o\">,<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;-&quot;<\/span><span class=\"o\">)<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">echo<\/span><span class=\"w\"> <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">DOCKER_BRANCH_NAME<\/span>\n<span class=\"o\">}<\/span>\n\n<span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">buildReactFrontend<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;React Frontend&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">dir<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;frontend&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"c1\">\/\/ Ensure all Packages are Installed<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;npm install&quot;<\/span>\n\n<span class=\"w\">            <\/span><span class=\"c1\">\/\/ Attempt to Resolve Reported Vulnerabilities<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;npm audit fix || true&quot;<\/span><span class=\"w\"> <\/span><span class=\"c1\">\/\/ Don&#39;t fail on normal warning<\/span>\n\n<span class=\"w\">            <\/span><span class=\"c1\">\/\/ Use the Build Tooling<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;npm run build&quot;<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">}<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n\n<span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">buildDockerContainer<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;Docker Container&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Check for &quot;Default&quot; Conditions<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ MAIN and DEVELOP Branches Should Have Consistent Ports<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Their Containers Should Also Restart Automatically<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">if<\/span><span class=\"w\"> <\/span><span class=\"o\">(<\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">DOCKER_BRANCH_NAME<\/span><span class=\"w\"> <\/span><span class=\"o\">==<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;main&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">NEXT_AVAIL_APP_PORT<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;8000&quot;<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">RESTART_POLICY<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;always&quot;<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">}<\/span><span class=\"w\"> <\/span><span class=\"k\">else<\/span><span class=\"w\"> <\/span><span class=\"k\">if<\/span><span class=\"w\"> <\/span><span class=\"o\">(<\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">DOCKER_BRANCH_NAME<\/span><span class=\"w\"> <\/span><span class=\"o\">==<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;develop&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">NEXT_AVAIL_APP_PORT<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;8001&quot;<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">RESTART_POLICY<\/span><span class=\"w\"> <\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;always&quot;<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">}<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">echo<\/span><span class=\"w\"> <\/span><span class=\"n\">env<\/span><span class=\"o\">.<\/span><span class=\"na\">NEXT_AVAIL_APP_PORT<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Provide credentials to the docker-compose script so that<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ container will reference the Lychee API credentials.<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">withCredentials<\/span><span class=\"o\">([<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">usernamePassword<\/span><span class=\"o\">(<\/span><span class=\"nl\">credentialsId:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;idaho4hapi&#39;<\/span><span class=\"o\">,<\/span>\n<span class=\"w\">            <\/span><span class=\"nl\">usernameVariable:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;LYCHEE_API_USER&#39;<\/span><span class=\"o\">,<\/span>\n<span class=\"w\">            <\/span><span class=\"nl\">passwordVariable:<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;LYCHEE_API_TOKEN&#39;<\/span><span class=\"o\">)<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">])<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">            <\/span><span class=\"c1\">\/\/ Build the Container - Using the Branch-Name as Env Var<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;docker-compose up -d --build&quot;<\/span>\n<span class=\"w\">        <\/span><span class=\"o\">}<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n\n<span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">deployContainerInNginx<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;Deploy in NGINX&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Generate the NGINX Config File<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;python3 .\/writeNginxConfig.py&quot;<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Restart NGINX To Apply Config<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;sudo \/usr\/bin\/systemctl restart nginx&quot;<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n\n<span class=\"kt\">def<\/span><span class=\"w\"> <\/span><span class=\"nf\">runPythonTestsInVirtualEnv<\/span><span class=\"o\">()<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">stage<\/span><span class=\"o\">(<\/span><span class=\"s2\">&quot;Test Python&quot;<\/span><span class=\"o\">)<\/span><span class=\"w\"> <\/span><span class=\"o\">{<\/span>\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Install Requirements<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;&quot;&quot;python3 -m pip install -r ${WORKSPACE}\/${requirementsFile}&quot;&quot;&quot;<\/span>\n\n<span class=\"w\">        <\/span><span class=\"c1\">\/\/ Run `pytest`<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">sh<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;pytest&quot;<\/span>\n<span class=\"w\">    <\/span><span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre><\/div>\n\n\n<\/details>\n\n<p>That means we spend MORE time on learning, and LESS time on fighting the system! In theory, at least...<\/p>\n<p><img src=\"https:\/\/imgs.xkcd.com\/comics\/automation.png\" style=\"width: 100%\" alt=\"Woes of Automation\"><\/p>\n<blockquote>\n<p><em>image credit: <a href=\"https:\/\/xkcd.com\/1319\/\">XKCD comics<\/a><\/em><\/p>\n<\/blockquote>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"4-h"}},{"@attributes":{"term":"education"}},{"@attributes":{"term":"jenkins"}},{"@attributes":{"term":"nginx"}},{"@attributes":{"term":"docker"}},{"@attributes":{"term":"docker-compose"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"ci"}},{"@attributes":{"term":"ci-cd"}},{"@attributes":{"term":"self-hosted"}},{"@attributes":{"term":"gitlab"}}]},{"title":"Ideas for Swine Education","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/ideas-for-swine-education.html","rel":"alternate"}},"published":"2022-03-22T16:54:00-07:00","updated":"2022-03-22T16:54:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-03-22:\/ideas-for-swine-education.html","summary":"<p>I recently attended the 2022 University of Idaho, Washington State University joint Youth Swine Field day in Asotin, WA. It's been a while since I've been very involved in swine projects in 4-H, but it's been good getting back into the swing of things. Admmittedly, I'm learning new things all the time, myself, but since the ethos of 4-H is \"Learn by Doing,\" I'm always one for good, tangible, hands-on exercises to help engage youth while teaching. I thought it about time that I share a few of my ideas!<\/p>","content":"<p>I'm just getting back into the swing of 4-H activities. Monthly community meetings, regular project meetings, etc. But I want to bring some exciting new\nlearning activities to the projects I'm supporting.<\/p>\n<p>I spent... gosh... 10(?) years in the swine project, about 8 of which were both in market and breeding projects, so quite a bit of time around those pigs!\nWell worth it, if you ask me.<\/p>\n<p>As I'm getting started with leading, I've been starting to amass some of the ideas I've had for learning exercise. Some are borrowed, and some are of my\nown creation. This article will chronicle some of those ideas; both the good, and the... interesting.<\/p>\n<h2>\u201cBad Water\u201d<\/h2>\n<p><img src=\"https:\/\/t4.ftcdn.net\/jpg\/01\/18\/85\/23\/360_F_118852383_kjyToVFqvQ9T1rNlfrYQuYiAlmtqZTU9.jpg\"\n    width=\"300\" alt=\"Dirty Water\" align=\"right\"><\/p>\n<blockquote>\n<p><em>image credit: <a href=\"https:\/\/stock.adobe.com\/hu\/search\/images?k=dirty+water+glass\">Adobe Stock Photos<\/a><\/em><\/p>\n<\/blockquote>\n<p>Ok, I can't claim that this was my idea (thanks, Roxanne!); but I'll shamelessly use it, because... <strong><em>Wow!<\/em><\/strong><\/p>\n<p>Clean water is essential! Most of us know this, and some of us have first-hand experience with the alternatives and their repercussions, but it's very\nimportant to instill this <em>need<\/em> in youth who are raising livestock (not just pigs). I think most of the reasons are pretty self-expanitory, but I'll\nlist some of them, anyway.<\/p>\n<p><strong>Hogs who don't have access to clean water...<\/strong><\/p>\n<ul>\n<li>...are often unhealthy, prone to disease; if not because of pathogens in the water, because of lack of hydration.<\/li>\n<li>...rarely gain weight optimally. Their bodies are too preoccupied with surviving to pack on muscle the way we want them to.<\/li>\n<li>...can become aggressive over clean water, or become lethargic, not getting the exercise they need.<\/li>\n<\/ul>\n<p>So, the exercise that Roxanne so graciously provided me brings together Sprite, and soda-water. One is pretty universally enjoyed by young people, the\nother almost the opposite. Fill so many cups that each 4-H'er can have a cup to themselves, splitting the pour right down the middle. Half the cups get\nSprite, the other half get soda-water. Be sure not to over-fill the cups so that the kiddos will need to pee too quickly. Tell all of the kids that you've\ngot some \"soda\" that they can drink, but don't tell them that some are better than the others. Let them all take a cup and have a drink. After you get the\nusual \"grumblings\" and complaints, offer the kids more if they'd like it.<\/p>\n<p>At this point, the kids who had Sprite will likely get more, the others may, or may not. It's probably a good time to ask the kids who <em>clearly<\/em> didn't\nlike their drinks what they didn't like about it. This all, of course, leads right in to a perfect opportunity for you to talk about the importance of\nfresh, clean clean water, and how having anything other than good clean water will likely make pigs less likely to drink anything.<\/p>\n<p>Pretty fun, huh?<\/p>\n<h2>\u201cWithdrawl Period\u201d<\/h2>\n<p>So, admittedly, this one's still in the incubator... gonna have to work on the scheme a little more still before it's where I want it. But here's the\ngeneral idea:<\/p>\n<p>Most, if not all, livestock medications have what's known as a \"withdrawl period;\" a length of time after which the animal should not be harvested for\nrisk of the medication still being present in their system and likely in their meat. In general, it's considered <em>unwise<\/em> to eat meat which still has\nany measurable level of medicine. This is important for 4-H youth to understand, because they're responsible for the care of their animal right up to its\nharvest, which means that if they were to administer medicine to the animal within the withdrawl period, they could be contaminating the animal's meat.<\/p>\n<p>The withdrawl period is brought up a few times throughout the year, and my idea is to show the kiddos how to experience it, first-hand.<\/p>\n<p>Like I said... still needs some work, but the idea basically surrounds Asparagus. Yep, that's right. You might already be two steps ahead of me here, but\nif you're not quite with me yet, let me remind you of that one-so-ever-fascinating-trait of Asparagus; after you eat enough of it, when you pee, you can smell it!<\/p>\n<p>Yep. I went there.<\/p>\n<p>But remember, we're dealing with kids, here! That stuff is funny, and it can have a big impact! Imagine being a kid and having somme kind of Asparagus-based treat one day when you hear about \"Withdral Periods,\" and then remembering it when you go to pee the next! Talk about a first-hand lesson!<\/p>\n<h2>\u201cWorms and Nutrition\u201d<\/h2>\n<p>Ok... so this one could be viewed as a bit... unorthodox. Anywho!<\/p>\n<p>Parasites are always a problem with livestock, and it's important to deworm animals somewhat regularly. Especially when you're trying to help those\nanimals acheive certain growth rates! A lot of people who raise hogs don't worm them for one reason or another. Some because it's introducing \"yet\nanother manmade thing into the pigs,\" some becuase it can be difficult, and others because it's either cost prohibitive or they merely don't want to\npay for it.<\/p>\n<p>Regardless, it's important.<\/p>\n<p>So, what if we showed the kids just how much those \"worms\" can actually eat up from the nutritional input that our hogs digest.<\/p>\n<p>The idea is essentially to have two glasses of colored water filled to the same level. Then, demonstrate the difference between a young worm and an adult\none by showing how much each would eat if the way they ate was to absorb water. Take a Q-tip (representing the young worm) and dip it in one glass and let\nit sit there for a bit. Take it out and demonstrate that it absorbed a little of the \"nutrients\" but not very much.<\/p>\n<p>After you've shown them the \"young\" worm, bring out a super-ultra-mega-absorbant tampon. You read that correctly. I think that some folks might find this\na bit... odd, but I think it would provide a good teaching opportunity. Anyway... Take out that <em>new<\/em> tampon, pull it out of its packaging and place it in\nthe second glass. Let it soak up the \"nutrients\" for a little bit, then pull it out.<\/p>\n<p>After both \"worms\" have been removed, have the kids get close and examine the difference in how much of the \"nutrients\" have been taken away from the pig.\nThe pig who received wormer, and only has young worms still has lots of nutrients for themselves. The pig who <em>didn't<\/em> receive wormer doesn't have quite\nthe same luxury, they've lost a substantial ammount of nutrition, because the worm \"ate\" it all!<\/p>\n<h2>\u201cMold Growth and Clean Feed\u201d<\/h2>\n<p><img src=\"https:\/\/media.wired.com\/photos\/5f4d18f6bb023d54b55e4e19\/master\/pass\/Science_penicillum_922527614.jpg\"\n    width=\"300\" alt=\"Dirty Water\" align=\"right\"><\/p>\n<blockquote>\n<p><em>image credit: <a href=\"https:\/\/www.wired.com\/story\/grim-reality-reopening-more-mold-offices-schools-houses\/\">Wired<\/a><\/em><\/p>\n<\/blockquote>\n<p>Many 4-H'ers will buy feed in bulk at the beginning of the season so that they can take it at lower cost. This often means one <em>GREAT BIG<\/em> sack of feed\nsitting around all summer, which may not be the best option... Why? you ask... Well...<\/p>\n<p>When feed is left to sit for long periods of time, it regularly absorbs moisture, which in itself isn't a bad thing, but with moisture often comes mold.\nThe longer that feed sits, the more moisture it will gradually absorb, and as it absorbs more moisture, it will begin to break down, creating additional\nheat, and making a terrific environment to support the growth of mold.<\/p>\n<p>Do you know anyone who <em>likes<\/em> mold? Me neither. And pigs don't either!<\/p>\n<p>Just like us, pigs can tolerate small ammounts of mold if they eat it accidentally; but they they don't <em>like<\/em> it! It doesn't <em>taste<\/em> good! It's not good\n<em>for<\/em> them!<\/p>\n<p>So this last \"experiment\" I want to demonstrate is the progression of a person's plate of food over a month. It's a hands-on activity where the kids help\nme fill my plate with food, and I can stick it in a refrigerator for the period of one month to bring it back and show them later. At the same time, I'll\nstore some pig food in a warm, moist place where it can naturally grow mold. I'll take photos of it every day so that I can make a time-lapse video of its\nmoldy progression, then with the kids, we can look at all the <em>gross mold!<\/em><\/p>\n<p>I know I'm weird. It's ok, you don't need to remind me.<\/p>\n<p>What'll be fun about this exercise, though is that the kids will see first hand how just keeping food for long periods of time can let it grow mold, and\nhow it really doesn't take much for it to happen!<\/p>\n<h2>What's Next?<\/h2>\n<p>Well... I think I had some other ideas too... but I've forgotten them. At least I wrote them down on a piece of paper at home! Will have to go look at\nthose later and report back!<\/p>\n<p>Talk soon!<\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"swine"}},{"@attributes":{"term":"4-h"}},{"@attributes":{"term":"livestock"}},{"@attributes":{"term":"education"}}]},{"title":"Bringing Light to the... Living Room?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/bringing-light-to-the-living-room.html","rel":"alternate"}},"published":"2022-02-27T09:52:00-08:00","updated":"2022-02-27T09:52:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-02-27:\/bringing-light-to-the-living-room.html","summary":"<p>Shadow boxes deserve a shadow... right? Well, if there's a shadow, there has to be some light! That's what I've been working on; adding a little intelligent lighting to my shadow boxes! But, I've also been going crazy in the kitchen with some lights around the cabinets.<\/p>","content":"<p>I've been working on adding all sorts of smart lights around my home since I bought it, and the progress doesn't slow down!\nI've pretty much covered all of the things that can be automated with little smart plugs. Everything from my scentsy's to my lamps, but now,\nI need to automate some of the more... challenging things.<\/p>\n<p>I started a couple months ago with my coffee nook. I added a couple lights under the cabinet and hooked them up to a custom board, using an ESP-01S\nto do my heavy lifting. I ran into a whole SLEW of issues with this. Drat! Everything from conflicting WiFi signals with my dual access points, to\nblown out ESP modules, to faulty board designs. That's right, I incorrectly designed the board the first time around. <em>give me a dope-slap for that<\/em><\/p>\n<p>Turns out I'd left two pins on the ESP module floating (i.e., not connected to any power source or other electrical element), and they very much\nneeded to be connected to the Vcc node with a pull-up resistor. Should've looked more closely at those reference circuits the first time through.\nAnyway, I've gotten that working now! Only took me two board-revs to get it done. Hah! We won't talk about the fact that the footprint of the board\nas a whole isn't quite right... Another day...<\/p>\n<p>I really do need to share my learning from that project, don't I?<\/p>\n<h4>I digress...<\/h4>\n<p>So, yesterday I finally wrapped up the project of adding smart control to some new lights for the shadow-boxes in my living room. I use some old\nwooden crates for shadow boxes. They go very well with the rest of my \"old stuff\" around the house, and now they have even more character!<\/p>\n<p>I picked up a set of <a href=\"https:\/\/www.amazon.com\/gp\/product\/B01LMSYUP4\/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&amp;psc=1\">under cabinet LED lights<\/a>\nfrom Amazon, and some <a href=\"https:\/\/www.amazon.com\/gp\/product\/B00FEOB4EI\/ref=ppx_yo_dt_b_asin_title_o04_s00?ie=UTF8&amp;psc=1\">12V DC supplies<\/a>, combining\nthose with my little generic board, I was able to make some nice little controls.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_906029b.jpeg\" style=\"width: 100%\" alt=\"Imagination in a Box!\"><\/p>\n<p>I did struggle a bit with picking the right N-MOS transistor for controlling the LEDs. My go-to is a BUZ11A, which is normally good enough. It's\nrugged, general-purpose, and packs a punch for current control ability. Trouble is, that with my boards, the voltages weren't quite right. I\ndiscovered this late yesterday afternoon when powering up the little board and flipping the WiFi-enabled \"on-switch\", and the LEDs barely\nilluminated enough to see a dim glow. Not ideal for a shadow box.<\/p>\n<p>I scratched my head for a bit, and realized that the transistor's voltage thresholds weren't where I wanted them. Luckily enough, I had some options!\nI dug around in my hoarder-size-stash of electronics and found a set of logic-level transistors which were better suited for the job. Still being\nN-MOS, they were exactly what I needed. A bit of solder-wick later, I had my new transistors replaced, and my lights were working like a charm!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_5049e25.jpeg\" style=\"width: 100%\" alt=\"Illuminating my Imagination\"><\/p>\n<p>Now mind you, this isn't a story of all up-and-up success. This is the... third whack at the design. I started with some 4.5V supplies. Those were\nample for the control circuit, but not for the LEDs themselves (since they're rated for closer to 12V). I then thought I'd get clever and use a\nboost converter to do my dirty work so I could leave the rest of the circuitry intact. Didn't I learn anywhere that it's tricky to use a boost\nconverter coming off a PWM-enabled circuit? Guess not.<\/p>\n<p>Needless to say, both of those \"adventures\" were a bit less than ideal; but we got there! Third time's the charm!<\/p>\n<h4>Don't forget the kitchen!<\/h4>\n<p>Ok... Ok... I've also been working on illuminating the kitchen. I <em>seriously<\/em> need to sit down and tell you about what I've been doing with that\ncoffee nook light. Those are some \"fun\" stories.<\/p>\n<p>Anyway, I had tried my hand at flashing Tasmota on some off-the-shelf WiFi-enabled smart LED controls. That failed pretty miserably. But those\ncontrols were cheap, and I wasn't impressed anyway. I spent some serious time Googling around, and stumbled upon <a href=\"https:\/\/www.athom.tech\/\">ATHOM<\/a>\nwhich produces pre-flashed Tasmota\/ESPHome\/WLED-enabled boards in enclosures designed for makers and tinkerers like me! From browsing their\nwebsite, it's clear that English is not their first language, but I'm solidly impressed by their hardware. Simple, effective, and in clean packaging.\nThey worked great, right out of the box, and I didn't have to fight those dumb boards that don't already come with Tasmota flashed. MARVELOUS!<\/p>\n<p>So... I went crazy with installing these things all over my kitchen for above and below each of the cabinets... Just take a look!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/ima_4ee1367.jpeg\" style=\"width: 100%\" alt=\"Illuminating my Imagination\"><\/p>\n<p>That's basically the \"orange\" shade that Home-Assistant sets them with. But wow... when you come visit, remind me, and I'll show you the vibrance\nof the blues, greens, and reds... it's stunning!<\/p>","category":[{"@attributes":{"term":"Home-Improvement"}},{"@attributes":{"term":"esp8266"}},{"@attributes":{"term":"tasmota"}},{"@attributes":{"term":"smart-home"}},{"@attributes":{"term":"home-assistant"}},{"@attributes":{"term":"mqtt"}},{"@attributes":{"term":"iot"}},{"@attributes":{"term":"lighting"}},{"@attributes":{"term":"lights"}},{"@attributes":{"term":"development"}}]},{"title":"Sensing Fire in a World Without Water","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/sensing-fire-in-a-world-without-water.html","rel":"alternate"}},"published":"2022-02-15T18:41:00-08:00","updated":"2022-02-15T18:41:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-02-15:\/sensing-fire-in-a-world-without-water.html","summary":"<p>The world we live in is congested with controversy; left-vs-right, masks-vs-maskless, us-vs-them. So many arguments, one thing that we don't need to argue about is that there are parts of the U.S. that don't have much access to water, and that there are parts of the U.S. that also face some real challenges with wildfire.<\/p>","content":"<p>Wildfire, water-shortages, weird-weather; all sounds like some dystopian sci-fi novel, right?<\/p>\n<p>Scary that some of that is appearing across the United States, isn't it?<\/p>\n<p>Yeah. Now, weather you're a \"climate change subscriber,\" or not, I think there's a lot to be said about the challenges the world faces.\nIf for nothing else, we've got to figure out where to get all that water from, and we need to figure out what we're going to do to help\nface the challenges of wildfire in our increasingly populous world.<\/p>\n<p>I've written in the past about some of the project work that I'm sponsoring at the University of Idaho, and I wanted to highlight that\nbriefly while also pointing out some interesting things popping up from around the world.<\/p>\n<h3>What's Happening with Water?<\/h3>\n<p>I really don't have much to say here, but I did want to highlight an article that shows some of the drama of the water shortages that\nare appearing across the American Southwest.<\/p>\n<p><a href=\"https:\/\/archives.stanleysolutionsnw.com\/archive\/1651029667.381326\/output.html\">Read the article (archived in my personal archive server)<\/a><\/p>\n<p>Makes me wish there were other ways I could get involved in researching additional technology in this area.<\/p>\n<h3>What's New<\/h3>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/firesense-wing.jpeg\" style=\"width: 100%\" alt=\"Wings for Dropping Fire Sensors\">\n<em>photo credit: Luke Woods - student research team<\/em><\/p>\n<p>This year is my second year acting as the sponsor for a wildfire detection system, and I have to say, I'm stoked. This is a very\nexciting project, and has a lot of potential. If you want to read more about the concept or tech, go check out my articles on\n<a href=\"\/finding-fire-and-making-it.html\">finding fire and making it<\/a>, <a href=\"\/hearing-fires-while-seeing-smoke\">hearing fires while seeing smoke<\/a>,\nor <a href=\"\/detectinf-fires-with-sound\">detecting fires with sound<\/a>.<\/p>\n<p>What's new and exciting right now, is the team's approach, using inspiration from Mother Nature. They're using some designs for their\ndrop-payload which were inspired by a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Samara_%28fruit%29\">Samara leaf<\/a> so they can make the payload\ndrop in a safe, and controlled fashion. Now, this isn't exactly a novel idea, there's a few others who have done similar things:<\/p>\n<ul>\n<li><a href=\"https:\/\/hackaday.com\/2021\/08\/07\/helicopter-seed-robot-can-also-drop-like-a-rock\/\">HELICOPTER SEED ROBOT CAN ALSO DROP LIKE A ROCK<\/a><\/li>\n<li><a href=\"https:\/\/www.newscientist.com\/article\/dn20045-spinning-seeds-inspire-single-bladed-helicopters\/\">SPINNING SEEDS INSPIRE SINGLE-BLADED HELICOPTERS<\/a><\/li>\n<\/ul>\n<p>Still, this is brand new to us, and so far as I'm aware, it's a new way to drop materials as a deployment technique. I'm very proud of\nthe team's work, and I'm so excited to see where they'll take this project!<\/p>\n<hr>\n<p>Hopefully, I'll be keeping more updates coming; but let's be honest, I'm in charge of the updates, so they might be a bit sparse, or even\na bit all-over-the-place.<\/p>","category":[{"@attributes":{"term":"Capstone"}},{"@attributes":{"term":"capstone"}},{"@attributes":{"term":"students"}},{"@attributes":{"term":"university"}},{"@attributes":{"term":"research"}},{"@attributes":{"term":"university of idaho"}},{"@attributes":{"term":"wildfire"}},{"@attributes":{"term":"infrasound"}}]},{"title":"Using Python to Provide Simple Photo Connections for Youth","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/using-python-to-provide-simple-photo-connections-for-youth.html","rel":"alternate"}},"published":"2022-02-13T20:44:00-08:00","updated":"2022-02-15T20:17:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-02-13:\/using-python-to-provide-simple-photo-connections-for-youth.html","summary":"<p>So if you're gonna use Python to help bring photos from youth together; how, exactly, do you do it? This article will look at how we're using Lychee, Python, FastAPI, and React.js to make an ultra-simple, highly effective photo upload service for a youth conference.<\/p>","content":"<p>If you've been keeping tabs on what I've been up to lately, you might know that I've been slowly getting back into 4-H\ninvolvement. Namely, I'm now a certified volunteer in Idaho, and I'm starting to dip my toes into leading projects.<\/p>\n<p>Recently, I shared <a href=\"\/reactjs-python-pictures-and-4h.html\">how I'm working with youth to create an app in Python and React.js<\/a>\nand how I'm excited to be working <em>with<\/em> some of the youth to develop the app. We're working to use some of the \"latest and\ngreatest\" technology to ensure that the service is not only forward-looking, but helps to teach forward-looking practices.<\/p>\n<p>In this article though, I want to talk a bit more about the technical details about what we're doing, and how it's working.<\/p>\n<hr>\n<h3>Let's talk program organization...<\/h3>\n<p>The whole program is structured around Lychee as the \"database\". Since Lychee provides a beautiful, functional API,\nand an easily maintained container-based database and storage, we can use it as the core of the service. Lychee\nprovides simple, but powerful album organization, and that's all available through the API (excellent!!!). Since\nthe entire goal is to provide a simple way to organize photos according to the \"district\" that submits them, we'll\nbe able to use the API to poke those photos into just the right spot.<\/p>\n<p>It'll be the responsibility of our Python back-end (using <a href=\"https:\/\/pypi.org\/project\/pychee\/\"><code>pychee<\/code><\/a>) to make the\nconnection between the React front-end and the Lychee service itself. This same Python back-end will serve the\nReact public files. Effectively, like so many other places I use Python, it acts as the all-important glue in this\nsystem.<\/p>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkRFU0NSSVBUSU9OIiBoZWlnaHQ9IjY0NS44MzMzcHgiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiIHN0eWxlPSJ3aWR0aDo1MDlweDtoZWlnaHQ6NjQ1cHg7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MDkgNjQ1IiB3aWR0aD0iNTA5LjM3NXB4IiB6b29tQW5kUGFuPSJtYWduaWZ5Ij48P3BsYW50dW1sIDEuMjAyNi4zYmV0YTY\/PjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iZzZvMjVuNmh5anN3ejAiIHgxPSI1MCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiNCOEI4QjgiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM2QjZCNkIiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iZzZvMjVuNmh5anN3ejEiIHgxPSI1MCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM3QzlBQjkiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMzMDRENkQiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48Zz48IS0tY2x1c3RlciBOR0lOWC0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLXF1YWxpZmllZC1uYW1lPSJOR0lOWCIgZGF0YS1zb3VyY2UtbGluZT0iNCIgaWQ9ImVudDAwMDMiPjxwb2x5Z29uIGZpbGw9InVybCgjZzZvMjVuNmh5anN3ejApIiBwb2ludHM9IjI3LjA4MzMsMTMyLjI1LDM3LjUsMTIxLjgzMzMsNDc4LjEyNSwxMjEuODMzMyw0NzguMTI1LDYwNS4wNzI5LDQ2Ny43MDgzLDYxNS40ODk2LDI3LjA4MzMsNjE1LjQ4OTYsMjcuMDgzMywxMzIuMjUiIHN0eWxlPSJzdHJva2U6IzQ0NkU5QjtzdHJva2Utd2lkdGg6MS4wNDE3OyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzQ0NkU5QjtzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjQ2Ny43MDgzIiB4Mj0iNDc4LjEyNSIgeTE9IjEzMi4yNSIgeTI9IjEyMS44MzMzIi8+PGxpbmUgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iMjcuMDgzMyIgeDI9IjQ2Ny43MDgzIiB5MT0iMTMyLjI1IiB5Mj0iMTMyLjI1Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iNDY3LjcwODMiIHgyPSI0NjcuNzA4MyIgeTE9IjEzMi4yNSIgeTI9IjYxNS40ODk2Ii8+PHRleHQgZmlsbD0iI0VFRUVFRSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQ1LjQ3MTIiIHg9IjIyNS43MDE5IiB5PSIxNTIuMTg2MSI+TkdJTlg8L3RleHQ+PC9nPjwhLS1jbHVzdGVyIHB5LS0+PGcgY2xhc3M9ImNsdXN0ZXIiIGRhdGEtcXVhbGlmaWVkLW5hbWU9Ik5HSU5YLnB5IiBkYXRhLXNvdXJjZS1saW5lPSI1IiBpZD0iZW50MDAwNCI+PHBvbHlnb24gZmlsbD0idXJsKCNnNm8yNW42aHlqc3d6MCkiIHBvaW50cz0iNjAuNDE2NywxOTcuODc1LDcwLjgzMzMsMTg3LjQ1ODMsMjMyLjI5MTcsMTg3LjQ1ODMsMjMyLjI5MTcsNDU0LjA2MjUsMjIxLjg3NSw0NjQuNDc5Miw2MC40MTY3LDQ2NC40NzkyLDYwLjQxNjcsMTk3Ljg3NSIgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iMjIxLjg3NSIgeDI9IjIzMi4yOTE3IiB5MT0iMTk3Ljg3NSIgeTI9IjE4Ny40NTgzIi8+PGxpbmUgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iNjAuNDE2NyIgeDI9IjIyMS44NzUiIHkxPSIxOTcuODc1IiB5Mj0iMTk3Ljg3NSIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzQ0NkU5QjtzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjIyMS44NzUiIHgyPSIyMjEuODc1IiB5MT0iMTk3Ljg3NSIgeTI9IjQ2NC40NzkyIi8+PHRleHQgZmlsbD0iI0VFRUVFRSIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBmb250LXdlaWdodD0iNzAwIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjgyLjQyOCIgeD0iMTAwLjk3MzUiIHk9IjIxNy44MTExIj5QeXRob24tQXBwPC90ZXh0PjwvZz48IS0tY2x1c3RlciBseWMtLT48ZyBjbGFzcz0iY2x1c3RlciIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iTkdJTlgubHljIiBkYXRhLXNvdXJjZS1saW5lPSI5IiBpZD0iZW50MDAwNyI+PHBvbHlnb24gZmlsbD0idXJsKCNnNm8yNW42aHlqc3d6MCkiIHBvaW50cz0iMjczLjk1ODMsMzM2LjM4NTQsMjg0LjM3NSwzMjUuOTY4OCw0NDQuNzkxNywzMjUuOTY4OCw0NDQuNzkxNyw1NzEuNzM5Niw0MzQuMzc1LDU4Mi4xNTYzLDI3My45NTgzLDU4Mi4xNTYzLDI3My45NTgzLDMzNi4zODU0IiBzdHlsZT0ic3Ryb2tlOiM0NDZFOUI7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiM0NDZFOUI7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHgxPSI0MzQuMzc1IiB4Mj0iNDQ0Ljc5MTciIHkxPSIzMzYuMzg1NCIgeTI9IjMyNS45Njg4Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB4MT0iMjczLjk1ODMiIHgyPSI0MzQuMzc1IiB5MT0iMzM2LjM4NTQiIHkyPSIzMzYuMzg1NCIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzQ0NkU5QjtzdHJva2Utd2lkdGg6MS4wNDE3OyIgeDE9IjQzNC4zNzUiIHgyPSI0MzQuMzc1IiB5MT0iMzM2LjM4NTQiIHkyPSI1ODIuMTU2MyIvPjx0ZXh0IGZpbGw9IiNFRUVFRUUiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgZm9udC13ZWlnaHQ9IjcwMCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI4Mi4xMzUiIHg9IjMxNC4xNDA4IiB5PSIzNTYuMzIxNSI+THljaGVlLUFwcDwvdGV4dD48L2c+PCEtLWVudGl0eSBmYXN0YXBpLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iTkdJTlgucHkuZmFzdGFwaSIgZGF0YS1zb3VyY2UtbGluZT0iNiIgaWQ9ImVudDAwMDUiPjxyZWN0IGZpbGw9InVybCgjZzZvMjVuNmh5anN3ejEpIiBoZWlnaHQ9IjU2LjIxNzQiIHJ4PSI0LjE2NjciIHJ5PSI0LjE2NjciIHN0eWxlPSJzdHJva2U6IzMwNEQ2RDtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9Ijk4LjEyMjIiIHg9Ijg4LjQzNzUiIHk9IjM4My4yNjA0Ii8+PHJlY3QgZmlsbD0idXJsKCNnNm8yNW42aHlqc3d6MSkiIGhlaWdodD0iMTAuNDE2NyIgc3R5bGU9InN0cm9rZTojMzA0RDZEO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iMTUuNjI1IiB4PSIxNjUuNzI2MyIgeT0iMzg4LjQ2ODgiLz48cmVjdCBmaWxsPSJ1cmwoI2c2bzI1bjZoeWpzd3oxKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6IzMwNEQ2RDtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iMTYzLjY0MyIgeT0iMzkwLjU1MjEiLz48cmVjdCBmaWxsPSJ1cmwoI2c2bzI1bjZoeWpzd3oxKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6IzMwNEQ2RDtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iMTYzLjY0MyIgeT0iMzk0LjcxODgiLz48dGV4dCBmaWxsPSIjRkZGRkZGIiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNDYuMDM4OCIgeD0iMTA5LjI3MDgiIHk9IjQyMC45MDQ5Ij5GYXN0QVBJPC90ZXh0PjwvZz48IS0tZW50aXR5IHJlYWN0LS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iTkdJTlgucHkucmVhY3QiIGRhdGEtc291cmNlLWxpbmU9IjciIGlkPSJlbnQwMDA2Ij48cmVjdCBmaWxsPSJ1cmwoI2c2bzI1bjZoeWpzd3oxKSIgaGVpZ2h0PSI1Ni4yMTc0IiByeD0iNC4xNjY3IiByeT0iNC4xNjY3IiBzdHlsZT0ic3Ryb2tlOiMzMDRENkQ7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSIxMDEuODUxNCIgeD0iODUuNTMxMyIgeT0iMjQ0Ljc1Ii8+PHJlY3QgZmlsbD0idXJsKCNnNm8yNW42aHlqc3d6MSkiIGhlaWdodD0iMTAuNDE2NyIgc3R5bGU9InN0cm9rZTojMzA0RDZEO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iMTUuNjI1IiB4PSIxNjYuNTQ5MyIgeT0iMjQ5Ljk1ODMiLz48cmVjdCBmaWxsPSJ1cmwoI2c2bzI1bjZoeWpzd3oxKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6IzMwNEQ2RDtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iMTY0LjQ2NiIgeT0iMjUyLjA0MTciLz48cmVjdCBmaWxsPSJ1cmwoI2c2bzI1bjZoeWpzd3oxKSIgaGVpZ2h0PSIyLjA4MzMiIHN0eWxlPSJzdHJva2U6IzMwNEQ2RDtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjQuMTY2NyIgeD0iMTY0LjQ2NiIgeT0iMjU2LjIwODMiLz48dGV4dCBmaWxsPSIjRkZGRkZGIiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNDkuNzY4MSIgeD0iMTA2LjM2NDYiIHk9IjI4Mi4zOTQ0Ij5SZWFjdC5qczwvdGV4dD48L2c+PCEtLWVudGl0eSBEQi0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9Ik5HSU5YLmx5Yy5EQiIgZGF0YS1zb3VyY2UtbGluZT0iMTAiIGlkPSJlbnQwMDA4Ij48cGF0aCBkPSJNMzA5LjY0NTgsNTEyLjM5NTggQzMwOS42NDU4LDUwMS45NzkyIDMzNC4zNzEyLDUwMS45NzkyIDMzNC4zNzEyLDUwMS45NzkyIEMzMzQuMzcxMiw1MDEuOTc5MiAzNTkuMDk2NSw1MDEuOTc5MiAzNTkuMDk2NSw1MTIuMzk1OCBMMzU5LjA5NjUsNTQ2LjczODMgQzM1OS4wOTY1LDU1Ny4xNTQ5IDMzNC4zNzEyLDU1Ny4xNTQ5IDMzNC4zNzEyLDU1Ny4xNTQ5IEMzMzQuMzcxMiw1NTcuMTU0OSAzMDkuNjQ1OCw1NTcuMTU0OSAzMDkuNjQ1OCw1NDYuNzM4MyBMMzA5LjY0NTgsNTEyLjM5NTgiIGZpbGw9InVybCgjZzZvMjVuNmh5anN3ejEpIiBzdHlsZT0ic3Ryb2tlOiMzMDRENkQ7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48cGF0aCBkPSJNMzA5LjY0NTgsNTEyLjM5NTggQzMwOS42NDU4LDUyMi44MTI1IDMzNC4zNzEyLDUyMi44MTI1IDMzNC4zNzEyLDUyMi44MTI1IEMzMzQuMzcxMiw1MjIuODEyNSAzNTkuMDk2NSw1MjIuODEyNSAzNTkuMDk2NSw1MTIuMzk1OCIgZmlsbD0ibm9uZSIgc3R5bGU9InN0cm9rZTojMzA0RDZEO3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PHRleHQgZmlsbD0iI0ZGRkZGRiIgZm9udC1mYW1pbHk9IidWZXJkYW5hJyIgZm9udC1zaXplPSIxMi41IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE4LjIwMDciIHg9IjMyNS4yNzA4IiB5PSI1NDMuNzkwMyI+REI8L3RleHQ+PC9nPjwhLS1lbnRpdHkgYXBpLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iTkdJTlgubHljLmFwaSIgZGF0YS1zb3VyY2UtbGluZT0iMTEiIGlkPSJlbnQwMDA5Ij48cmVjdCBmaWxsPSJ1cmwoI2c2bzI1bjZoeWpzd3oxKSIgaGVpZ2h0PSI1Ni4yMTc0IiByeD0iNC4xNjY3IiByeT0iNC4xNjY3IiBzdHlsZT0ic3Ryb2tlOiMzMDRENkQ7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI3MS44NTg3IiB4PSIyOTguNDQ3OSIgeT0iMzgzLjI2MDQiLz48cmVjdCBmaWxsPSJ1cmwoI2c2bzI1bjZoeWpzd3oxKSIgaGVpZ2h0PSIxMC40MTY3IiBzdHlsZT0ic3Ryb2tlOiMzMDRENkQ7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSIxNS42MjUiIHg9IjM0OS40NzMzIiB5PSIzODguNDY4OCIvPjxyZWN0IGZpbGw9InVybCgjZzZvMjVuNmh5anN3ejEpIiBoZWlnaHQ9IjIuMDgzMyIgc3R5bGU9InN0cm9rZTojMzA0RDZEO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iNC4xNjY3IiB4PSIzNDcuMzkiIHk9IjM5MC41NTIxIi8+PHJlY3QgZmlsbD0idXJsKCNnNm8yNW42aHlqc3d6MSkiIGhlaWdodD0iMi4wODMzIiBzdHlsZT0ic3Ryb2tlOiMzMDRENkQ7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI0LjE2NjciIHg9IjM0Ny4zOSIgeT0iMzk0LjcxODgiLz48dGV4dCBmaWxsPSIjRkZGRkZGIiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTkuNzc1NCIgeD0iMzE5LjI4MTMiIHk9IjQyMC45MDQ5Ij5BUEk8L3RleHQ+PC9nPjwhLS1lbnRpdHkgVXNlci0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtcXVhbGlmaWVkLW5hbWU9IlVzZXIiIGRhdGEtc291cmNlLWxpbmU9IjMiIGlkPSJlbnQwMDAyIj48ZWxsaXBzZSBjeD0iMjgzLjMzMzMiIGN5PSIzMy4zMzMzIiBmaWxsPSJ1cmwoI2c2bzI1bjZoeWpzd3oxKSIgcng9IjE2LjY2NjciIHJ5PSIxNi42NjY3IiBzdHlsZT0ic3Ryb2tlOiMzMDRENkQ7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48cGF0aCBkPSJNMjgzLjMzMzMsNTQuMTY2NyBDMjg3LjUsNTQuMTY2NyAyOTAuNjI1LDU0LjE2NjcgMjk0Ljc5MTcsNTAgQzMwMy4xMjUsNTAgMzExLjQ1ODMsNTguMzMzMyAzMTEuNDU4Myw2Ni42NjY3IEwzMTEuNDU4Myw3MC44MzMzIEMzMTEuNDU4Myw3NSAzMDcuMjkxNyw3OS4xNjY3IDMwMy4xMjUsNzkuMTY2NyBMMjYzLjU0MTcsNzkuMTY2NyBDMjU5LjM3NSw3OS4xNjY3IDI1NS4yMDgzLDc1IDI1NS4yMDgzLDcwLjgzMzMgTDI1NS4yMDgzLDY2LjY2NjcgQzI1NS4yMDgzLDU4LjMzMzMgMjYzLjU0MTcsNTAgMjcxLjg3NSw1MCBDMjc2LjA0MTcsNTQuMTY2NyAyNzkuMTY2Nyw1NC4xNjY3IDI4My4zMzMzLDU0LjE2NjciIGZpbGw9InVybCgjZzZvMjVuNmh5anN3ejEpIiBzdHlsZT0ic3Ryb2tlOiMzMDRENkQ7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiLz48dGV4dCBmaWxsPSIjRkZGRkZGIiBmb250LWZhbWlseT0iJ1ZlcmRhbmEnIiBmb250LXNpemU9IjEyLjUiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjguNDkxMiIgeD0iMjY5LjA4NzciIHk9Ijk3LjAxOTQiPlVzZXI8L3RleHQ+PC9nPjwhLS1saW5rIFVzZXIgdG8gbHljLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDIiIGRhdGEtZW50aXR5LTI9ImVudDAwMDciIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIxNCIgaWQ9ImxuazEwIj48cGF0aCBkPSJNMzA0LjU4MzMsMTA1LjI3MDggQzMyNy41MjA4LDE1NC4wODMzIDM2My44MjI5LDIzNS44MTI1IDM4Ni40NTgzLDMwOS4zMDIxIEMzODguMDkyNCwzMTQuNjA4MSAzODguMDYxMiwzMTQuMDYyMiAzODkuNTEyNiwzMTkuNjM1NyIgZmlsbD0ibm9uZSIgaWQ9IlVzZXItdG8tbHljIiBzdHlsZT0ic3Ryb2tlOiM0NDZFOUI7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiM0NDZFOUIiIHBvaW50cz0iMzkxLjA4NzYsMzI1LjY4NCwzOTIuNzU3MywzMTUuNTYxNiwzODkuNzc1MSwzMjAuNjQzOCwzODQuNjkyOSwzMTcuNjYxNiwzOTEuMDg3NiwzMjUuNjg0IiBzdHlsZT0ic3Ryb2tlOiM0NDZFOUI7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBVc2VyIHRvIHB5LS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDIiIGRhdGEtZW50aXR5LTI9ImVudDAwMDQiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIxNSIgaWQ9ImxuazExIj48cGF0aCBkPSJNMjcwLjg4NTQsMTA1LjU0MTcgQzI2Mi4zMTc3LDEzNS45MDYzIDI1MC44MTUxLDE3Ni42NjE1IDI0MS4zNDI0LDIxMC4yMjQgQzIzOC45NzQzLDIxOC42MTQ2IDIzNi43MzMsMjI2LjU1NTcgMjM0LjY5NjEsMjMzLjc3MjUgQzIzNC4xODY5LDIzNS41NzY3IDIzMy42OTA1LDIzNy4zMzU2IDIzMy4yMDgxLDIzOS4wNDUgQzIzMi45NjY4LDIzOS44OTk2IDIzMi43MjkxLDI0MC43NDE5IDIzMi40OTUsMjQxLjU3MTMgQzIzMi40MzY1LDI0MS43Nzg3IDIzNC4wNzU5LDIzNS45NzAyIDIzNC4wMTc4LDIzNi4xNzU5IiBmaWxsPSJub25lIiBpZD0iVXNlci10by1weSIgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjNDQ2RTlCIiBwb2ludHM9IjIzMi4zMjAyLDI0Mi4xOTA5LDIzOC44NzY2LDIzNC4zMDAxLDIzMy43MzQ5LDIzNy4xNzg0LDIzMC44NTY2LDIzMi4wMzY2LDIzMi4zMjAyLDI0Mi4xOTA5IiBzdHlsZT0ic3Ryb2tlOiM0NDZFOUI7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayByZWFjdCB0byBmYXN0YXBpLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDYiIGRhdGEtZW50aXR5LTI9ImVudDAwMDUiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIxNiIgaWQ9ImxuazEyIj48cGF0aCBkPSJNMTM2LjcxMzIsMzA3LjQzNzMgQzEzNi44OTAzLDMzMS4yMjkgMTM3LjA2OCwzNTMuMDYyNyAxMzcuMjQ1MSwzNzYuODQzOSIgZmlsbD0ibm9uZSIgaWQ9InJlYWN0LWZhc3RhcGkiIHN0eWxlPSJzdHJva2U6IzQ0NkU5QjtzdHJva2Utd2lkdGg6My4xMjU7Ii8+PHBvbHlnb24gZmlsbD0iIzQ0NkU5QiIgcG9pbnRzPSIxMzYuNjY2NywzMDEuMTg3NSwxMzIuNTY5OSwzMTAuNTkzMywxMzYuNzA1NCwzMDYuMzk1NywxNDAuOTAzLDMxMC41MzEyLDEzNi42NjY3LDMwMS4xODc1IiBzdHlsZT0ic3Ryb2tlOiM0NDZFOUI7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiM0NDZFOUIiIHBvaW50cz0iMTM3LjI5MTcsMzgzLjA5MzgsMTQxLjM4ODQsMzczLjY4OCwxMzcuMjUyOSwzNzcuODg1NiwxMzMuMDU1MywzNzMuNzUsMTM3LjI5MTcsMzgzLjA5MzgiIHN0eWxlPSJzdHJva2U6IzQ0NkU5QjtzdHJva2Utd2lkdGg6My4xMjU7Ii8+PC9nPjwhLS1saW5rIGZhc3RhcGkgdG8gYXBpLS0+PGcgY2xhc3M9ImxpbmsiIGRhdGEtZW50aXR5LTE9ImVudDAwMDUiIGRhdGEtZW50aXR5LTI9ImVudDAwMDkiIGRhdGEtbGluay10eXBlPSJkZXBlbmRlbmN5IiBkYXRhLXNvdXJjZS1saW5lPSIxNyIgaWQ9ImxuazEzIj48cGF0aCBkPSJNMTg2LjcxODgsNDExLjM3NSBDMjIzLjgyMjksNDExLjM3NSAyNTQuNjc3MSw0MTEuMzc1IDI5MS43OTE3LDQxMS4zNzUiIGZpbGw9Im5vbmUiIGlkPSJmYXN0YXBpLXRvLWFwaSIgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjNDQ2RTlCIiBwb2ludHM9IjI5OC4wNDE3LDQxMS4zNzUsMjg4LjY2NjcsNDA3LjIwODMsMjkyLjgzMzMsNDExLjM3NSwyODguNjY2Nyw0MTUuNTQxNywyOTguMDQxNyw0MTEuMzc1IiBzdHlsZT0ic3Ryb2tlOiM0NDZFOUI7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjwvZz48IS0tbGluayBhcGkgdG8gREItLT48ZyBjbGFzcz0ibGluayIgZGF0YS1lbnRpdHktMT0iZW50MDAwOSIgZGF0YS1lbnRpdHktMj0iZW50MDAwOCIgZGF0YS1saW5rLXR5cGU9ImRlcGVuZGVuY3kiIGRhdGEtc291cmNlLWxpbmU9IjE4IiBpZD0ibG5rMTQiPjxwYXRoIGQ9Ik0zMzQuMzc1LDQ0Ni4xNzcxIEMzMzQuMzc1LDQ2NC44MjI5IDMzNC4zNzUsNDc2Ljg0MzggMzM0LjM3NSw0OTUuMzU0MiIgZmlsbD0ibm9uZSIgaWQ9ImFwaS1EQiIgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDozLjEyNTsiLz48cG9seWdvbiBmaWxsPSIjNDQ2RTlCIiBwb2ludHM9IjMzNC4zNzUsNDM5LjkyNzEsMzMwLjIwODMsNDQ5LjMwMjEsMzM0LjM3NSw0NDUuMTM1NCwzMzguNTQxNyw0NDkuMzAyMSwzMzQuMzc1LDQzOS45MjcxIiBzdHlsZT0ic3Ryb2tlOiM0NDZFOUI7c3Ryb2tlLXdpZHRoOjMuMTI1OyIvPjxwb2x5Z29uIGZpbGw9IiM0NDZFOUIiIHBvaW50cz0iMzM0LjM3NSw1MDEuNjA0MiwzMzguNTQxNyw0OTIuMjI5MiwzMzQuMzc1LDQ5Ni4zOTU4LDMzMC4yMDgzLDQ5Mi4yMjkyLDMzNC4zNzUsNTAxLjYwNDIiIHN0eWxlPSJzdHJva2U6IzQ0NkU5QjtzdHJva2Utd2lkdGg6My4xMjU7Ii8+PC9nPjw\/cGxhbnR1bWwtc3JjIEhPeEgyZThtNThSbHByRVM3aTJVODA5NTJZNTJZWTJHQmV4cFg5UXdpR3JZWUVfVURmQ3U1X2tfVnVUbHN6WU0xcVFaSTcxRkRQWHREb2hJRDIwOUFfTkxrZndIRmNwYW0xMGhsMWRNQ0NnNnlObWlwZFVTNVlZVFJVTU9mNGVYNkxIa1drZjBuZ1BieUdaNXFzVEk3TEdOemZoam95SE0xdTF2Q1BvU1E5Yk5HLV80MTByUGdpYW12WGJLZ29mZVBaX1g4dldhLXgzdzEtS1dNZDROUTd0c1R3QWwwZU9PeF9GaTJtMDA\/PjwvZz48L3N2Zz4=\" style=\"max-width:100%\" width=\"100%\" class=\"uml\" alt=\"Idaho 4-H Photos Site\" title=\"Idaho 4-H Photos Site\" \/><\/p>\n<p>The React front end is basically passive in this configuration, by that, I mean there's no Javascript\/Node.js\nserver configuration on the box. It's all built and exported to static files that Python serves as needed to\n\"pull up\" the site. Once the site is loaded, the React.js front-end just hits the appropriate API endpoints on\nthe Python side which, in turn, causes the Python service to access the appropriate API endpoints on the Lychee\nservice using a specific API credential-set.<\/p>\n<hr>\n<p>There's a lot more going on in the automated build system that helps me work with youth to deploy the site in\nthe various stages of development so that the youth don't have to learn all of the intricacies of Node.js'\nawful dependency stack. That said, I'll have to write more (hopefully soon) on how we tied my personal Jenkins\nservice with the Linode that this system is running on.<\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"react.js"}},{"@attributes":{"term":"microservice"}},{"@attributes":{"term":"docker"}},{"@attributes":{"term":"lychee"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"fastapi"}}]},{"title":"Adding PlantUML to Pelican Without Installing Java?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/adding-plantuml-to-pelican-without-installing-java.html","rel":"alternate"}},"published":"2022-02-13T19:54:00-08:00","updated":"2022-02-13T20:32:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-02-13:\/adding-plantuml-to-pelican-without-installing-java.html","summary":"<p>We know that I don't like to do anything that computers can do for me. We also know that I like Python, and am not a huge fan of Java. So how am I going to get PlantUML integrated into my blog without actually needing to install the Java packages? Let me show you...<\/p>","content":"<p>I've recently wanted to start adding <a href=\"https:\/\/plantuml.com\/\">PlantUML<\/a> drawings to my blog-posts for a variety of reasons.\nTrouble is, PlantUML requires Java. My blog-site is built in GitHub actions, and adding PlantUML as some kind of CLI\nutility to a GitHub action sounds... well... gross!<\/p>\n<p>Thankfully, I've found a way around that with the help of <a href=\"https:\/\/pypi.org\/project\/plantuml-markdown\/\"><code>PlantUML-Markdown<\/code><\/a>,\na nice little package built to do pretty much EXACTLY what I need. It's able to shoot off the UML content to a server,\nand the server hands back an image to suit my request. To demonstrate, when I use the following code-block in my markdown\narticle(s):<\/p>\n<div class=\"highlight\"><pre><span><\/span><code>::_uml_:: format=&quot;svg&quot; classes=&quot;uml myDiagram&quot; alt=&quot;My super diagram placeholder&quot; title=&quot;My super diagram&quot; width=&quot;300px&quot; height=&quot;300px&quot;\n   !theme spacelab\n   Bob-&gt;Alice : Hello!\n::end-uml::\n<\/code><\/pre><\/div>\n\n<p><em>Note:<\/em> Now the auto-rendering is SO good, that I had to add underscores around the keyword <code>uml<\/code> in the example above. I'll do that\nthroughout this article in places where I <em>don't<\/em> want the PlantUML to actually render to an image.<\/p>\n<p>I, in turn, render an image such as the following:<\/p>\n<p><img src=\"data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IlNFUVVFTkNFIiBoZWlnaHQ9IjE2Mi41cHgiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiIHN0eWxlPSJ3aWR0aDozMTNweDtoZWlnaHQ6MTYycHg7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMTMgMTYyIiB3aWR0aD0iMzEzLjU0MTdweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PD9wbGFudHVtbCAxLjIwMjYuM2JldGE2Pz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImdsOGx4Ync2N2FrNWQwIiB4MT0iNTAlIiB4Mj0iNTAlIiB5MT0iMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjN0M5QUI5Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMzA0RDZEIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PGc+PGcgY2xhc3M9InBhcnRpY2lwYW50LWxpZmVsaW5lIiBkYXRhLWVudGl0eS11aWQ9InBhcnQxIiBkYXRhLXF1YWxpZmllZC1uYW1lPSJCb2IiIGRhdGEtc291cmNlLWxpbmU9IjIiIGlkPSJwYXJ0MS1saWZlbGluZSI+PGc+PHRpdGxlPkJvYjwvdGl0bGU+PHJlY3QgZmlsbD0iIzAwMDAwMCIgZmlsbC1vcGFjaXR5PSIwLjAwMDAwIiBoZWlnaHQ9IjYwLjM4NDEiIHdpZHRoPSI4LjMzMzMiIHg9Ijc3LjcwMzkiIHk9IjUxLjAwOTEiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiM5OTk5OTk7c3Ryb2tlLXdpZHRoOjEuMDQxNztzdHJva2UtZGFzaGFycmF5OjUuMjA4Myw1LjIwODM7IiB4MT0iODEuMjUiIHgyPSI4MS4yNSIgeTE9IjUxLjAwOTEiIHkyPSIxMTEuMzkzMiIvPjwvZz48L2c+PGcgY2xhc3M9InBhcnRpY2lwYW50LWxpZmVsaW5lIiBkYXRhLWVudGl0eS11aWQ9InBhcnQyIiBkYXRhLXF1YWxpZmllZC1uYW1lPSJBbGljZSIgZGF0YS1zb3VyY2UtbGluZT0iMiIgaWQ9InBhcnQyLWxpZmVsaW5lIj48Zz48dGl0bGU+QWxpY2U8L3RpdGxlPjxyZWN0IGZpbGw9IiMwMDAwMDAiIGZpbGwtb3BhY2l0eT0iMC4wMDAwMCIgaGVpZ2h0PSI2MC4zODQxIiB3aWR0aD0iOC4zMzMzIiB4PSIyMjMuNTYyNiIgeT0iNTEuMDA5MSIvPjxsaW5lIHN0eWxlPSJzdHJva2U6Izk5OTk5OTtzdHJva2Utd2lkdGg6MS4wNDE3O3N0cm9rZS1kYXNoYXJyYXk6NS4yMDgzLDUuMjA4MzsiIHgxPSIyMjcuMjgyNyIgeDI9IjIyNy4yODI3IiB5MT0iNTEuMDA5MSIgeTI9IjExMS4zOTMyIi8+PC9nPjwvZz48ZyBjbGFzcz0icGFydGljaXBhbnQgcGFydGljaXBhbnQtaGVhZCIgZGF0YS1lbnRpdHktdWlkPSJwYXJ0MSIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iQm9iIiBkYXRhLXNvdXJjZS1saW5lPSIyIiBpZD0icGFydDEtaGVhZCI+PHJlY3QgZmlsbD0idXJsKCNnbDhseGJ3NjdhazVkMCkiIGhlaWdodD0iMzkuNTUwOCIgcng9IjQuMTY2NyIgcnk9IjQuMTY2NyIgc3R5bGU9InN0cm9rZTojMzA0RDZEO3N0cm9rZS13aWR0aDoxLjA0MTc7IiB3aWR0aD0iNDkuMTU3NyIgeD0iNTcuMjkxNyIgeT0iMTAuNDE2NyIvPjx0ZXh0IGZpbGw9IiNGRkZGRkYiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyNC4xNTc3IiB4PSI2OS43OTE3IiB5PSIzNC41MTk0Ij5Cb2I8L3RleHQ+PC9nPjxnIGNsYXNzPSJwYXJ0aWNpcGFudCBwYXJ0aWNpcGFudC10YWlsIiBkYXRhLWVudGl0eS11aWQ9InBhcnQxIiBkYXRhLXF1YWxpZmllZC1uYW1lPSJCb2IiIGRhdGEtc291cmNlLWxpbmU9IjIiIGlkPSJwYXJ0MS10YWlsIj48cmVjdCBmaWxsPSJ1cmwoI2dsOGx4Ync2N2FrNWQwKSIgaGVpZ2h0PSIzOS41NTA4IiByeD0iNC4xNjY3IiByeT0iNC4xNjY3IiBzdHlsZT0ic3Ryb2tlOiMzMDRENkQ7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI0OS4xNTc3IiB4PSI1Ny4yOTE3IiB5PSIxMTAuMzUxNiIvPjx0ZXh0IGZpbGw9IiNGRkZGRkYiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyNC4xNTc3IiB4PSI2OS43OTE3IiB5PSIxMzQuNDU0MyI+Qm9iPC90ZXh0PjwvZz48ZyBjbGFzcz0icGFydGljaXBhbnQgcGFydGljaXBhbnQtaGVhZCIgZGF0YS1lbnRpdHktdWlkPSJwYXJ0MiIgZGF0YS1xdWFsaWZpZWQtbmFtZT0iQWxpY2UiIGRhdGEtc291cmNlLWxpbmU9IjIiIGlkPSJwYXJ0Mi1oZWFkIj48cmVjdCBmaWxsPSJ1cmwoI2dsOGx4Ync2N2FrNWQwKSIgaGVpZ2h0PSIzOS41NTA4IiByeD0iNC4xNjY3IiByeT0iNC4xNjY3IiBzdHlsZT0ic3Ryb2tlOiMzMDRENkQ7c3Ryb2tlLXdpZHRoOjEuMDQxNzsiIHdpZHRoPSI1NS4wNTk4IiB4PSIyMDAuMTk5NCIgeT0iMTAuNDE2NyIvPjx0ZXh0IGZpbGw9IiNGRkZGRkYiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzMC4wNTk4IiB4PSIyMTIuNjk5NCIgeT0iMzQuNTE5NCI+QWxpY2U8L3RleHQ+PC9nPjxnIGNsYXNzPSJwYXJ0aWNpcGFudCBwYXJ0aWNpcGFudC10YWlsIiBkYXRhLWVudGl0eS11aWQ9InBhcnQyIiBkYXRhLXF1YWxpZmllZC1uYW1lPSJBbGljZSIgZGF0YS1zb3VyY2UtbGluZT0iMiIgaWQ9InBhcnQyLXRhaWwiPjxyZWN0IGZpbGw9InVybCgjZ2w4bHhidzY3YWs1ZDApIiBoZWlnaHQ9IjM5LjU1MDgiIHJ4PSI0LjE2NjciIHJ5PSI0LjE2NjciIHN0eWxlPSJzdHJva2U6IzMwNEQ2RDtzdHJva2Utd2lkdGg6MS4wNDE3OyIgd2lkdGg9IjU1LjA1OTgiIHg9IjIwMC4xOTk0IiB5PSIxMTAuMzUxNiIvPjx0ZXh0IGZpbGw9IiNGRkZGRkYiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzMC4wNTk4IiB4PSIyMTIuNjk5NCIgeT0iMTM0LjQ1NDMiPkFsaWNlPC90ZXh0PjwvZz48ZyBjbGFzcz0ibWVzc2FnZSIgZGF0YS1lbnRpdHktMT0icGFydDEiIGRhdGEtZW50aXR5LTI9InBhcnQyIiBkYXRhLXNvdXJjZS1saW5lPSIyIiBpZD0ibXNnMSI+PHBvbHlnb24gZmlsbD0iIzQ0NkU5QiIgcG9pbnRzPSIyMTUuMjI5Myw4OC40NzY2LDIyNS42NDYsOTIuNjQzMiwyMTUuMjI5Myw5Ni44MDk5LDIxOS4zOTYsOTIuNjQzMiIgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDoxLjA0MTc7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojNDQ2RTlCO3N0cm9rZS13aWR0aDozLjEyNTsiIHgxPSI4MS44NzA1IiB4Mj0iMjIxLjQ3OTMiIHkxPSI5Mi42NDMyIiB5Mj0iOTIuNjQzMiIvPjx0ZXh0IGZpbGw9IiNGRkZGRkYiIGZvbnQtZmFtaWx5PSInVmVyZGFuYSciIGZvbnQtc2l6ZT0iMTIuNSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzNi42OTQzIiB4PSI5NC4zNzA1IiB5PSI4Mi40MDM2Ij5IZWxsbyE8L3RleHQ+PC9nPjw\/cGxhbnR1bWwtc3JjIEtvcDlJQ3JETElXa0k0bkVwS2JDdWRCQUp6QXJTeXA5SjR2TGk1Qm1JQ3Q5b0xTNDAwMDA\/PjwvZz48L3N2Zz4=\" style=\"max-width:300px;max-height:300px\" width=\"100%\" class=\"uml myDiagram\" alt=\"My super diagram placeholder\" title=\"My super diagram\" \/><\/p>\n<p>Now, to make all this work, I did need to make a few changes, and I couldn't find a clean, comprehensive set of documentation on this,\nso I'm putting it together here.<\/p>\n<hr>\n<p>I tried a number of things that didn't work, so let me just list those quickly to put them behind us:<\/p>\n<ul>\n<li>I'd tried using a tag directly in the uml-header to specify the server; later, I found that this option isn't even supported. So I\ndon't know what I was thinking!<\/li>\n<\/ul>\n<div class=\"highlight\"><pre><span><\/span><code>::_uml_:: format=&quot;svg&quot; ... server=&quot;http:\/\/www.plantuml.com\/plantuml&quot;\n<\/code><\/pre><\/div>\n\n<ul>\n<li>I then tried integrating with Pelican's PlantUML extension (which requires the Java tool be installed). I thought that I <em>must<\/em>\nneed some kind of cooperation between the two extensions... Nope. Fail.<\/li>\n<\/ul>\n<p>Alright... so after trying a number of packages, and combining configurations, I finally found this little note in the PlantUML-Markdown\nPyPI page:<\/p>\n<blockquote>\n<p>Then you need to specify the configuration file on the command line:<\/p>\n<p><code>markdown_py -x plantuml_markdown -c myconfig.yml mydoc.md &gt; out.html<\/code><\/p>\n<\/blockquote>\n<p>That, in turn, lead me to start thinking a bit differently.<\/p>\n<p>If PlantUML can be generated with specific \"global options\" (i.e., the <code>-c myconfig.yml<\/code> portion of that command), and I can specify\nwhich extension the <code>Python-Markdown<\/code> generator will run with, then I <em>should<\/em> be able to specify some of those configuration options\nin the <code>pelicanconf.py<\/code> file for the Pelican site generation, right?<\/p>\n<p><strong>Bingo.<\/strong><\/p>\n<p>So... I started browsing the inter-webs, and found\n<a href=\"https:\/\/jackdewinter.github.io\/2019\/10\/16\/fine-tuning-pelican-markdown-configuration\/\">this article about fine-tuning markdown config for Pelican<\/a>.\nThat was helpful, but left me with a few questions... So, off to the <a href=\"https:\/\/docs.getpelican.com\/en\/latest\/settings.html\">Pelican docs<\/a> I went!<\/p>\n<p>Lo and behold, I found that it's basically just a dictionary specifying the exact Python modules which should see certain configuration\nvalues modified. So I did a bit of poking around to find that the <code>PlantUML-Markdown<\/code> module's primary file is aptly named <code>plantuml_markdown.py<\/code>\nTaking the default <code>MARKDOWN<\/code> configuration dictionary provided in the Pelican documentation, I did a little modification to add the\nserver option I needed to let <code>PlantUML-Markdown<\/code> do its magic in\n<a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/blob\/master\/pelicanconf.py\">my <code>pelicanconf.py<\/code> file<\/a>:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"n\">MARKDOWN<\/span> <span class=\"o\">=<\/span> <span class=\"p\">{<\/span>\n    <span class=\"s1\">&#39;extension_configs&#39;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n        <span class=\"s1\">&#39;markdown.extensions.codehilite&#39;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span><span class=\"s1\">&#39;css_class&#39;<\/span><span class=\"p\">:<\/span> <span class=\"s1\">&#39;highlight&#39;<\/span><span class=\"p\">},<\/span>\n        <span class=\"s1\">&#39;markdown.extensions.extra&#39;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{},<\/span>\n        <span class=\"s1\">&#39;markdown.extensions.meta&#39;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{},<\/span>\n        <span class=\"s1\">&#39;plantuml_markdown&#39;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>                              <span class=\"c1\"># This line,<\/span>\n            <span class=\"s1\">&#39;server&#39;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&quot;http:\/\/www.plantuml.com\/plantuml&quot;<\/span><span class=\"p\">,<\/span>   <span class=\"c1\"># and this one,<\/span>\n        <span class=\"p\">},<\/span>                                                  <span class=\"c1\"># and this one, were what I changed from the default.<\/span>\n    <span class=\"p\">},<\/span>\n    <span class=\"s1\">&#39;output_format&#39;<\/span><span class=\"p\">:<\/span> <span class=\"s1\">&#39;html5&#39;<\/span><span class=\"p\">,<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre><\/div>\n\n<p>And of course, I added <code>plantuml-markdown<\/code> to <a href=\"https:\/\/github.com\/engineerjoe440\/stanley-solutions-blog\/blob\/master\/requires.txt\">my <code>requires.txt<\/code> file<\/a>\nso that <code>pip<\/code> will pull it in for the rendering.<\/p>\n<h6>Voila!<\/h6>\n<p>Magic, don't you think? So, hopefully this means I'll start doing a little more \"drawing\" in my articles.<\/p>","category":[{"@attributes":{"term":"Blogging"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"pelican"}},{"@attributes":{"term":"blogging"}},{"@attributes":{"term":"static-sites"}},{"@attributes":{"term":"html"}},{"@attributes":{"term":"plantuml"}},{"@attributes":{"term":"diagrams"}},{"@attributes":{"term":"github-actions"}}]},{"title":"Think you Know Everything?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/think-you-know-everything.html","rel":"alternate"}},"published":"2022-02-08T19:50:00-08:00","updated":"2022-02-09T07:54:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-02-08:\/think-you-know-everything.html","summary":"<p>A step back down memory-lane, I pulled out the ol' 'Quiz Board' for a presentation!<\/p>","content":"<p>This is gonna be a quick one, but I just wanted to share some photos of my old high-school\nengineering project, the <strong><em>\"Quiz Board\"<\/em><\/strong>. It was a project of mine to make a <em>cool<\/em>\nand engaging public education system. I used it at the county fair to help teach folks about\nswine, since that was my 4-H project.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/22-02-08 17-02-31 1227.jpg\" style=\"width: 100%\" alt=\"The Quiz\"><\/p>\n<h2>The Inspiration<\/h2>\n<p>Long ago, in a town far away, I was given an old \"quiz board\" to fix. It was literally a board,\nand it had screws protruding from both sides. The system was simple. There were questions printed\non the left side, answers printed on the right. If you connected the provided wires using connected\nalligator clips the board would tell you if you were \"right\" by illuminating an little bulb.\nPretty neat little idea for kids, you connect a wire on the left to select the question, then you\ncan connect the wire on the right to select an answer. If you were right, the bulb lights up!<\/p>\n<h4>But it was boring!<\/h4>\n<p>Or at least, I thought it was. There was no <em>real<\/em> feedback from the system if you got the answer\nwrong, and it was kinda sad, for that reason. Needless to say, I wasn't satisfied. It wasn't long\nafter I repaired that board before I was starting to mull over ideas of how I could make my own\nand improve upon the design.<\/p>\n<p>Around the same time, I was learning about how Walt Disney had used rotating \"drums\" with little\ndepressions to make practical control systems before precise timing in microcontrollers was easy\nand practical. I distinctly remember an old clip from some archive of Walt giving a tour of some\nof the Haunted Mansion Audio-Animatronic control systems, and showing how these big rotating drums\nwould depress and release valve controlls for the pneumatics. These valves would then, in turn,\noperate the cylinders responsible for actually moving the animatronics around, and by coordinating\nall of these depressions at just the right speed and timing, the Disney Imagineers could craft\nsome spectacular repeating animations. Wonderful!<\/p>\n<p>I liked that idea, and I had NO idea how microcontrollers worked. I didn't even have an Arduino\nyet, and I had never even seen real software code (needless to say, my parents were not the\nbiggest tech-geeks in the area). So I started mulling this over. After what must've been a few\nmonths (really, I can't remember), an idea came to me. I realized that those little snap-action\nswitches have two positions, one defaulting to ON, the other defaulting to OFF; more commonly\nreferred to as \"Normally Closed\" and \"Normally Open,\" respectively. That meant that I could use\ntwo switches pretty easily to \"route\" electricity for a couple of buttons labeled \"TRUE\" and\n\"FALSE\". The switch associated with the <em>right<\/em> answer would be pressed, the other would not.\nThat meant that I could easily \"line-up\" the switches so that if the user pressed the <em>right<\/em>\nbutton, it would turn on a green light, and if they pressed the wrong button, it would turn on\nthe red light.<\/p>\n<p>Thus, \"The Quiz\" was born.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/22-02-08 16-59-31 1225.jpg\" style=\"width: 100%\" alt=\"The Guts\"><\/p>\n<h4>Processing with Electro-Mechanical Logic<\/h4>\n<p>I love touting that this machine had a 3-bit processor; which, if you stretch your imagination\na little bit, I guess that <em>is<\/em> true since it had three switches, and their positions all\nmattered for the result of the question\/answer. I've already explained what two of the switches\nwere for, but that third switch was responsible for keeping the whole thing moving in just the\nright way. That's right, that last switch was responsible for controlling the motor.<\/p>\n<p>The whole system revolved -pun intended- around this rotating drum idea. Questions were printed\non a sheet of paper taped to a 4\" diameter piece of PVC pipe, and the pipe turned to line the\nquestions up with a \"viewing window\" in the face of this gargantuan machine. When the user\npressed \"start\" that would deactivate the \"constant\" source of power for the motor, so as soon\nas the next switch press happens, the motor would stop. This would, in turn, line up the\nquestion with the viewing window, and then the user could read and respond to the question.<\/p>\n<p>This whole project took my father and I much of the year to build. In all, probably between one-\nand two-hundred hours, total. When it was finally completed, I got to take the machine with me\nto the Clearwater County fair, and load it up with questions about pigs to help entertain and\neducate the public.<\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"analog"}},{"@attributes":{"term":"logic"}},{"@attributes":{"term":"4-h"}},{"@attributes":{"term":"knowledge"}},{"@attributes":{"term":"throwback"}}]},{"title":"React.js, Python, Pictures, and 4-H!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/reactjs-python-pictures-and-4h.html","rel":"alternate"}},"published":"2022-01-26T17:35:00-08:00","updated":"2022-01-26T17:35:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-01-26:\/reactjs-python-pictures-and-4h.html","summary":"<p>How do you get nearly one-hundred high-school age youth to collect nearly as many photos for a competition at a youth conference? Facebook? Google-Drive? How about Python...?<\/p>","content":"<p>Do you remember going to those youth conferences when you were younger?<\/p>\n<p>I do!<\/p>\n<p>I loved them!<\/p>\n<p>In Idaho 4-H, there's an annual conference known as the\n<a href=\"https:\/\/www.uidaho.edu\/extension\/4h\/events\/stac\">\"State Teen Association Convention,\"<\/a> though, admittedly,\nwhen I attended, it was known as \"Teen Conference.\" Essentially, it's a conference focused on giving youth from\naround the state the opportunity to explore what opportunities exist after high school. Youth get to spend about\nfour days on campus at the University of Idaho, and explore various career- and education-focused \"tracks.\"<\/p>\n<p>But what would a youth-conference be, without some social activities and competition?<\/p>\n<p>Idaho \"STAC\" has a number of social activities and competitions in addition to the educational portions to help\nbring youth across the state together and help them build new connections. One of the competitions that happens\nthroughout the conference is what's known as \"district competitions.\"<\/p>\n<h3>What are the districts, and what are the district competitions?<\/h3>\n<p>Well, the state of Idaho is broken into four regional districts: Northern, Southern, Central, and Eastern. The\ndistricts help keep some organization across the state, and at STAC, they are the dividing line(s) for youth\nmeetings.<\/p>\n<p>Throughout the conference the districts compete against one another for points in district competitions. After\nall, who doesn't like a little good-humored, friendly competition? Now, to be honest, I don't think that there's\nany specific \"prize\" to come out of winning, but it's a lot of fun.<\/p>\n<p>There are competitions focusing in a few key areas including creativity (photo contest, skit performances) and\nhealthy living (skit\/dance performances).<\/p>\n<h3>The challenge...<\/h3>\n<p>The photo competition is always an interesting challenge to host. It ends up becoming something of a technical\nchallenge to administer since there's no \"effective\" way to collect the photos into one place. In years past\nall forms of systems have been used; everything from Facebook with hashtags, to public Google Drives, email, and\nmore... It's been a pain!<\/p>\n<p>Last year something dawned on me, we could create something a little more interesting!<\/p>\n<p>What if we created a simple little web-app to allow the youth to upload the photos they take for the competition,\nand made it a requirement for the youth to agree to a photo-consent-release. So that's what we got started on!<\/p>\n<h3>The Solution:<\/h3>\n<p>I spent some time a few months back doing some research on what we could use for an effective photo storage system.\nI wanted to use an open-source project that would allow API-based access to upload photos, and whose management\nis simple, intuitive, and without too many frills. I did some poking around, and playing with different services\non a $5\/month Linode (love those things for testing, by-the-way), and I finally landed on\n<a href=\"https:\/\/lycheeorg.github.io\/\">Lychee<\/a> for a couple of reasons:<\/p>\n<ul>\n<li>There's a terrific, and simple <code>docker-compose<\/code> configuration provided by <a href=\"https:\/\/docs.linuxserver.io\/images\/docker-lychee\">LinuxServer.io<\/a><\/li>\n<li>The API is VERY WELL documented, and exceedingly simple<\/li>\n<li>There's already a <a href=\"https:\/\/pypi.org\/project\/pychee\/\">Python Lychee client<\/a><\/li>\n<li>The user interface is very simple and focused without too many frills<\/li>\n<\/ul>\n<p>Using Lychee as the \"back-end storage\" for this project, I can create a nice \"wrapper\" on the front to make an easy\nweb-app to allow youth delegates to upload their photos. To do that, I can use my slick little Python\/React.js\nintegrated web-server pairing.<\/p>\n<p><em>Want to see what it's looking like?<\/em><\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/Screenshot_20220126_174318.png\" style=\"width: 100%\" alt=\"Idaho 4-H Photo Upload\"><\/p>\n<h3>Other Perks:<\/h3>\n<p>Pretty early on in this project's investigation, I met a 4-H'er who is actually learning about some programming, and\nso we've been working together on developing the web-front-end interface for youth.<\/p>\n<p>I've got quite a bit more fun stories to tell about how we're using my GitLab and Jenkins instances to help us keep\nthis project moving, but that will be coming a little later. That said, take a look at the consent form that we've built,\nand the dark-mode that we were able to build!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/Screenshot_20220126_174254.png\" style=\"width: 100%\" alt=\"Idaho 4-H Photo Upload\"><\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/Screenshot_20220126_174342.png\" style=\"width: 100%\" alt=\"Idaho 4-H Photo Upload\"><\/p>","category":[{"@attributes":{"term":"Youth"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"react.js"}},{"@attributes":{"term":"4-h"}},{"@attributes":{"term":"fastapi"}},{"@attributes":{"term":"materialui"}},{"@attributes":{"term":"linode"}}]},{"title":"Telnetlib, Python, and SEL Protocol","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/telnetlib-python-and-sel-protocol.html","rel":"alternate"}},"published":"2022-01-26T16:32:00-08:00","updated":"2022-01-26T16:32:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2022-01-26:\/telnetlib-python-and-sel-protocol.html","summary":"<p>Python's telnetlib library doesn't like null-characters, SEL protocol does, that makes for some interesting challenges.<\/p>","content":"<h3>Zeros don't matter, right?<\/h3>\n<p>Well, that depends on who, (<em>or what<\/em>) you're talking to. If it happens to be an SEL relay, those zeros are pretty important.<\/p>\n<p>How important? Well, every byte tells a story... so that means that every zero is important.<\/p>\n<h3>Where did this start?<\/h3>\n<p>We can take a few steps back. Go take a look at <a href=\"\/reading-data-with-selprotopy.html\">my article introducing SELProtoPy<\/a> to see\nwhat the buzz is all about. In short, I want to write a full protocol client in Python for SEL Fast-Meter, and potentially branch\nout from there. We'll see how far I can take it.<\/p>\n<p>This is definitely an article that I've been wanting to write for some time, because it's very fascinating. Last year, I started\nworking on the project and began writing the protocol parser from specifications provided by an\n<a href=\"https:\/\/selinc.com\/api\/download\/5026\/\">SEL application guide<\/a> which describes the intricacies of the binary SEL Fast-Meter\nprotocol.<\/p>\n<p>Before I describe the challenges in too much detail, however, perhaps I should summarize SEL protocol...<\/p>\n<hr>\n<blockquote>\n<p>When we talk about SEL protocol, we're really discussing a <em>suite<\/em> of protocols which includes:<\/p>\n<ul>\n<li>Fast Meter<\/li>\n<li>Fast Message<\/li>\n<li>Fast Operate<\/li>\n<\/ul>\n<p>Those \"protocols\" are all very closely linked, and are all intended to be \"described\" protocols. In other words, SEL protocol is\nself-describing. It essentially defines one \"main\" command\/response sequence which then provides the definition for each of the\nvarious sub-protocols. SEL protocol commands all start with the hexadecimal-encoded byte <code>A5<\/code>. Each command is two bytes in length,\nand the \"device definition\" command will return a definition of all the other available commands. That is, a device wishing to\nquery an SEL protocol enabled device would issue the hexadecimal string: <code>A5 C0<\/code> and interpret the response to determine what other\ncommands are available for the device. The response from the device definition command not only provides a listing of what commands\nare supported, but what hexadecimal string is required to query for those commands.<\/p>\n<\/blockquote>\n<hr>\n<p>So with the basics recounted, where this gets interesting comes into play when we account for the fact that SEL protocol was originally\na serial-based protocol, and made extensive use of null-padding, which is the practice of using zeros to separate content to account\nfor reasonable byte-alignment. That means that in almost every command response, there's a significant number of null characters (zeros)\nin not only the definitions, but the data regions provided.<\/p>\n<p>The application-guide I mentioned earlier has become something of the \"de facto standard\" for the protocol suite, and it defines how\nSEL protocol provides the numerical quantities used to describe the power system. In many cases, it's possible for those quantities\nto be zero for analog measurements. Whats more, SEL protocol provides an extensive set of word-bits (boolean points) in a bit-packed\nformat (8 word-bits packed into a single byte).<\/p>\n<p>Let's pause and think about that for a moment.<\/p>\n<p>Eight boolean statuses packed into a byte; many dozens, if not hundreds, of bits packed into bytes for a single response. At least a\n50\/50 chance that each bit will be a 0 (false\/deasserted). This all means that it's highly likely that one or more of the bytes will\nbe all 0's... a null-character.<\/p>\n<p>When I was working on this project, pretty early on, I found something interesting; when I would issue commands to request fast-meter\ndata, I'd see that the total data length was significantly shorter than what the response message indicated it should be. After\ndigging in, and looking at a little Wireshark, I found that my usage of Python's <code>telnetlib<\/code> was effectively cutting the null\ncharacters out.<\/p>\n<p>A little googling, and it was further confirmed from an <a href=\"https:\/\/stackoverflow.com\/a\/32616342\/10406011\">answer on StackOverflow<\/a><\/p>\n<h4><em>\"<\/em><\/h4>\n<blockquote>\n<p>I stumbled in this same problem when trying to get data from an RS232-TCP\/IP Converter using telnet - the telnetlib would suppress every 0x00 from the message. As Fredrik Johansson well answered, it is the way telnetlib was implemented.<\/p>\n<\/blockquote>\n<div style=\"text-align: right\">\n<h4><i>\"<\/i><\/h4>\n<\/div>\n\n<p>Luckily enough, there's a fantastic way to resolve this problem, you can actually play a few games with <code>telnetlib<\/code> to monkey-patch\nfunctionality to retain null characters. Just check out this code snippet from that StackOverflow answer:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"kn\">import<\/span> <span class=\"nn\">telnetlib<\/span>\n<span class=\"kn\">from<\/span> <span class=\"nn\">telnetlib<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">IAC<\/span><span class=\"p\">,<\/span> <span class=\"n\">DO<\/span><span class=\"p\">,<\/span> <span class=\"n\">DONT<\/span><span class=\"p\">,<\/span> <span class=\"n\">WILL<\/span><span class=\"p\">,<\/span> <span class=\"n\">WONT<\/span><span class=\"p\">,<\/span> <span class=\"n\">SE<\/span><span class=\"p\">,<\/span> <span class=\"n\">NOOPT<\/span>\n\n<span class=\"k\">def<\/span> <span class=\"nf\">_process_rawq<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"p\">):<\/span>\n<span class=\"w\">    <\/span><span class=\"sd\">&quot;&quot;&quot;Altera\u00e7\u00e3o da implementa\u00e7\u00e3o desta fun\u00e7\u00e3o necess\u00e1ria pois telnetlib suprime 0x00 e \\021 dos dados lidos<\/span>\n<span class=\"sd\">    &quot;&quot;&quot;<\/span>\n    <span class=\"n\">buf<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"s1\">&#39;&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;&#39;<\/span><span class=\"p\">]<\/span>\n    <span class=\"k\">try<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">while<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">rawq<\/span><span class=\"p\">:<\/span>\n            <span class=\"n\">c<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">rawq_getchar<\/span><span class=\"p\">()<\/span>\n            <span class=\"k\">if<\/span> <span class=\"ow\">not<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span><span class=\"p\">:<\/span>\n<span class=\"c1\">#                if c == theNULL:<\/span>\n<span class=\"c1\">#                    continue<\/span>\n<span class=\"c1\">#                if c == &quot;\\021&quot;:<\/span>\n<span class=\"c1\">#                    continue<\/span>\n                <span class=\"k\">if<\/span> <span class=\"n\">c<\/span> <span class=\"o\">!=<\/span> <span class=\"n\">IAC<\/span><span class=\"p\">:<\/span>\n                    <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span><span class=\"p\">]<\/span> <span class=\"o\">+<\/span> <span class=\"n\">c<\/span>\n                    <span class=\"k\">continue<\/span>\n                <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">+=<\/span> <span class=\"n\">c<\/span>\n            <span class=\"k\">elif<\/span> <span class=\"nb\">len<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span><span class=\"p\">)<\/span> <span class=\"o\">==<\/span> <span class=\"mi\">1<\/span><span class=\"p\">:<\/span>\n                <span class=\"c1\"># &#39;IAC: IAC CMD [OPTION only for WILL\/WONT\/DO\/DONT]&#39;<\/span>\n                <span class=\"k\">if<\/span> <span class=\"n\">c<\/span> <span class=\"ow\">in<\/span> <span class=\"p\">(<\/span><span class=\"n\">DO<\/span><span class=\"p\">,<\/span> <span class=\"n\">DONT<\/span><span class=\"p\">,<\/span> <span class=\"n\">WILL<\/span><span class=\"p\">,<\/span> <span class=\"n\">WONT<\/span><span class=\"p\">):<\/span>\n                    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">+=<\/span> <span class=\"n\">c<\/span>\n                    <span class=\"k\">continue<\/span>\n\n                <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span>\n                <span class=\"k\">if<\/span> <span class=\"n\">c<\/span> <span class=\"o\">==<\/span> <span class=\"n\">IAC<\/span><span class=\"p\">:<\/span>\n                    <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span><span class=\"p\">]<\/span> <span class=\"o\">+<\/span> <span class=\"n\">c<\/span>\n                <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"n\">c<\/span> <span class=\"o\">==<\/span> <span class=\"n\">SB<\/span><span class=\"p\">:<\/span> <span class=\"c1\"># SB ... SE start.<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">1<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span>\n                    <span class=\"k\">elif<\/span> <span class=\"n\">c<\/span> <span class=\"o\">==<\/span> <span class=\"n\">SE<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">0<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">+<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span>\n                        <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">:<\/span>\n                        <span class=\"c1\"># Callback is supposed to look into<\/span>\n                        <span class=\"c1\"># the sbdataq<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"p\">,<\/span> <span class=\"n\">c<\/span><span class=\"p\">,<\/span> <span class=\"n\">NOOPT<\/span><span class=\"p\">)<\/span>\n                    <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                        <span class=\"c1\"># We can&#39;t offer automatic processing of<\/span>\n                        <span class=\"c1\"># suboptions. Alas, we should not get any<\/span>\n                        <span class=\"c1\"># unless we did a WILL\/DO before.<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">msg<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;IAC <\/span><span class=\"si\">%d<\/span><span class=\"s1\"> not recognized&#39;<\/span> <span class=\"o\">%<\/span> <span class=\"nb\">ord<\/span><span class=\"p\">(<\/span><span class=\"n\">c<\/span><span class=\"p\">))<\/span>\n            <span class=\"k\">elif<\/span> <span class=\"nb\">len<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span><span class=\"p\">)<\/span> <span class=\"o\">==<\/span> <span class=\"mi\">2<\/span><span class=\"p\">:<\/span>\n                <span class=\"n\">cmd<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span><span class=\"p\">[<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span>\n                <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span>\n                <span class=\"n\">opt<\/span> <span class=\"o\">=<\/span> <span class=\"n\">c<\/span>\n                <span class=\"k\">if<\/span> <span class=\"n\">cmd<\/span> <span class=\"ow\">in<\/span> <span class=\"p\">(<\/span><span class=\"n\">DO<\/span><span class=\"p\">,<\/span> <span class=\"n\">DONT<\/span><span class=\"p\">):<\/span>\n                    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">msg<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;IAC <\/span><span class=\"si\">%s<\/span><span class=\"s1\"> <\/span><span class=\"si\">%d<\/span><span class=\"s1\">&#39;<\/span><span class=\"p\">,<\/span>\n                        <span class=\"n\">cmd<\/span> <span class=\"o\">==<\/span> <span class=\"n\">DO<\/span> <span class=\"ow\">and<\/span> <span class=\"s1\">&#39;DO&#39;<\/span> <span class=\"ow\">or<\/span> <span class=\"s1\">&#39;DONT&#39;<\/span><span class=\"p\">,<\/span> <span class=\"nb\">ord<\/span><span class=\"p\">(<\/span><span class=\"n\">opt<\/span><span class=\"p\">))<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"p\">,<\/span> <span class=\"n\">cmd<\/span><span class=\"p\">,<\/span> <span class=\"n\">opt<\/span><span class=\"p\">)<\/span>\n                    <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"o\">.<\/span><span class=\"n\">sendall<\/span><span class=\"p\">(<\/span><span class=\"n\">IAC<\/span> <span class=\"o\">+<\/span> <span class=\"n\">WONT<\/span> <span class=\"o\">+<\/span> <span class=\"n\">opt<\/span><span class=\"p\">)<\/span>\n                <span class=\"k\">elif<\/span> <span class=\"n\">cmd<\/span> <span class=\"ow\">in<\/span> <span class=\"p\">(<\/span><span class=\"n\">WILL<\/span><span class=\"p\">,<\/span> <span class=\"n\">WONT<\/span><span class=\"p\">):<\/span>\n                    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">msg<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;IAC <\/span><span class=\"si\">%s<\/span><span class=\"s1\"> <\/span><span class=\"si\">%d<\/span><span class=\"s1\">&#39;<\/span><span class=\"p\">,<\/span>\n                        <span class=\"n\">cmd<\/span> <span class=\"o\">==<\/span> <span class=\"n\">WILL<\/span> <span class=\"ow\">and<\/span> <span class=\"s1\">&#39;WILL&#39;<\/span> <span class=\"ow\">or<\/span> <span class=\"s1\">&#39;WONT&#39;<\/span><span class=\"p\">,<\/span> <span class=\"nb\">ord<\/span><span class=\"p\">(<\/span><span class=\"n\">opt<\/span><span class=\"p\">))<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">option_callback<\/span><span class=\"p\">(<\/span><span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"p\">,<\/span> <span class=\"n\">cmd<\/span><span class=\"p\">,<\/span> <span class=\"n\">opt<\/span><span class=\"p\">)<\/span>\n                    <span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n                        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sock<\/span><span class=\"o\">.<\/span><span class=\"n\">sendall<\/span><span class=\"p\">(<\/span><span class=\"n\">IAC<\/span> <span class=\"o\">+<\/span> <span class=\"n\">DONT<\/span> <span class=\"o\">+<\/span> <span class=\"n\">opt<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">except<\/span> <span class=\"ne\">EOFError<\/span><span class=\"p\">:<\/span> <span class=\"c1\"># raised by self.rawq_getchar()<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">iacseq<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;&#39;<\/span> <span class=\"c1\"># Reset on EOF<\/span>\n        <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sb<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">0<\/span>\n        <span class=\"k\">pass<\/span>\n    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">cookedq<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">cookedq<\/span> <span class=\"o\">+<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"mi\">0<\/span><span class=\"p\">]<\/span>\n    <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">=<\/span> <span class=\"bp\">self<\/span><span class=\"o\">.<\/span><span class=\"n\">sbdataq<\/span> <span class=\"o\">+<\/span> <span class=\"n\">buf<\/span><span class=\"p\">[<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span>\n<span class=\"n\">telnetlib<\/span><span class=\"o\">.<\/span><span class=\"n\">Telnet<\/span><span class=\"o\">.<\/span><span class=\"n\">process_rawq<\/span> <span class=\"o\">=<\/span> <span class=\"n\">_process_rawq<\/span>\n<\/code><\/pre><\/div>\n\n<p><em>Very interesting........<\/em><\/p>\n<h3>Parting Thoughts<\/h3>\n<p>This is one problem solved, but I've come across an interesting issue where sending the commands over Telnet is not\nworking, but sending the same commands over a plain TCP socket works without failure. Hmm... Weird. That one's\ngoing to take some more research. I'll be sure to post what I find, when I find it!<\/p>\n<p>If you have questions, thoughts, or just want to say \"hi\", feel free to drop me a note in my new comments system\nbelow!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"sel"}},{"@attributes":{"term":"communications"}},{"@attributes":{"term":"telnet"}},{"@attributes":{"term":"libraries"}},{"@attributes":{"term":"monkey-patch"}}]},{"title":"A \"Different\" Way to Wrap Gifts","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/a-different-way-to-wrap-gifts.html","rel":"alternate"}},"published":"2021-12-07T22:08:00-08:00","updated":"2021-12-07T22:08:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-12-07:\/a-different-way-to-wrap-gifts.html","summary":"<p>When it comes to wrapping Christmas gifts, I've used wrapping paper, tissue paper, even newspaper. This year, I'm going a little further up the chain though. I'm using wood!<\/p>","content":"<h3>What could possibly be more fun than ripping all that Christmas paper off on December 25th?<\/h3>\n<p>Unlocking a box, of course!!!<\/p>\n<p>I've always wanted to make one of those <a href=\"https:\/\/www.youtube.com\/watch?v=apVR5Htz0K4\">useless box<\/a>, but until I get off my laurels,\nI'll have to settle for something a little more practical. You see, I created a new \"lock-box\" as a Christmas gift.<\/p>\n<p>Well, that might be a little misleading.<\/p>\n<p>The box itself isn't the gift; but rather, what's inside! The box is just to make it a bit more fun, and so I guess that does end up\nbeing part of the gift, anyway.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/263718619_489805155698943_4659655016610554471_n.jpg\" style=\"width: 75%\" alt=\"Stanley Lock Box\"><\/p>\n<p>A bit wild looking, don't you think? What's the point, anyway? Well, the box has two electronic locks that have to be bypassed before\nit will reveal its contents. Yep, you have to crack the code if you want in.<\/p>\n<p>Right now, the two locks are just a combination of switches and a keypad. Set the switches in the right position, then punch in the\ncorrect code (which is stamped on the side of the box) and the box will magically unlock and allow you to open it!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/263745955_409049780964805_407698870180682018_n.jpg\" style=\"width: 50%\" alt=\"Do Not Open Until December 25\"><\/p>\n<p>With only two locks, it won't be <em>that hard<\/em> to unlock, but I have plans to expand it. You see, I intend to give these boxes out on the\ncondition that they return to me. The boxes will remain \"mine\" for the purpose of being built up over the years. Next year, I'll add\none or two more locks, and the year after I'll do the same; so on, and so on.<\/p>\n<p>This ought to be a fun little tradition, and I'm hopeful that my victims... errm... recipients... will have a good time cracking the\n\"code\".<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/263380603_595182361758282_4319520950240861117_n.jpg\" style=\"width: 75%\"><\/p>\n<p>You know, it occurs to me that I haven't provided any of the fun details! This project is all powered off a 12V SLA (sealed-lead-acid)\nbattery, and is run by the brains of an Arduino. The Arduino takes input from the little touch-pad, and signals to a BUZ11A transistor\nthat switches the solenoid responsible for <em>actually locking<\/em> the box.<\/p>\n<p>There's not too much to the code, but if you'd like, it's all available in one of\n<a href=\"https:\/\/github.com\/engineerjoe440\/stanley-lock-box\/blob\/main\/StanleyLockBox\/src\/main.cpp\">my GitHub repositories<\/a><\/p>","category":[{"@attributes":{"term":"Arduino"}},{"@attributes":{"term":"arduino"}},{"@attributes":{"term":"atmega328p"}},{"@attributes":{"term":"christmas"}},{"@attributes":{"term":"diy"}},{"@attributes":{"term":"c++"}},{"@attributes":{"term":"platformio"}}]},{"title":"Creepy, Killer Vibes?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/creepy-killer-vibes.html","rel":"alternate"}},"published":"2021-12-07T21:36:00-08:00","updated":"2021-12-07T21:36:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-12-07:\/creepy-killer-vibes.html","summary":"<p>My 115-year old house is getting a bit of a face lift. Well... Something like that.<\/p>","content":"<p>Ok... I'm way behind on articles, so I'm gonna try to get this one out fast.<\/p>\n<p>This is, or at least should be, one of many articles on the subject of my home-improvement endeavors. I know, it's bit of a tangent\nfrom my normal tech-focused stuff, but it's still very much \"me.\" After all, it's taking what's old, and giving it new life!<\/p>\n<p>I bought my home in Potlatch in 2020, and I'm very pleased to be working on renovating so many parts of the home. It's daunting, sure;\nbut it's exciting to see the ol' place come back together!<\/p>\n<p>Right now, the big project is renovating upstairs. Downstairs was largely renovated when I bought, so upstairs is next. I'm working on\nremoving the old lath-and-plaster and replacing it with proper drywall. Admittedly, it's a bit spooky looking, and that's largely what\nthis post is intended to be about... Showing the creepy \"behind the scenes\" looks...<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/20211115_004428524_iOS.png\" style=\"width: 50%;\"><\/p>\n<h5>Hey! Take a look there!<\/h5>\n<p>It's the stairwell and the old chimney brick for the old kitchen stove. I'm going to work on lighting that brick up and framing it out\nso it'll still be visible after the remodel. After all, isn't it cool? From a bygone era!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/20211115_004436243_iOS.jpg\" style=\"width: 50%;\"><\/p>\n<h5>I've even got a helper!<\/h5>\n<p>Look very closely, you'll see a companion who <em>thinks<\/em> he's helping. Let's not tell him, hmm?<\/p>\n<p>This is a look around the master bedroom, and a look through what will become the bedroom wall. As I write, there's now some Sheetrock\nhung here, but there's more to be done!<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/20211115_004449997_iOS.jpg\" style=\"width: 100%;\"><\/p>\n<p>Lastly, here's a look at the closet, and back out into the hallway upstairs. This helps tie a few of the previous pictures together.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/20211115_004454965_iOS.jpg\" style=\"width: 50%;\"><\/p>\n<p>Hopefully I'll keep on it, and provide some more updates as more transpires!<\/p>","category":[{"@attributes":{"term":"Home-Improvement"}},{"@attributes":{"term":"home-renovation"}},{"@attributes":{"term":"improvement"}},{"@attributes":{"term":"renovation"}},{"@attributes":{"term":"restoration"}}]},{"title":"Finding Fire and Making It","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/finding-fire-and-making-it.html","rel":"alternate"}},"published":"2021-11-25T15:25:00-08:00","updated":"2021-11-25T15:25:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-11-25:\/finding-fire-and-making-it.html","summary":"<p>There are just too many cool things to research, and thank goodness for the students at the University of Idaho.<\/p>","content":"<p>If you know me, you probably know that it's far too easy to captivate me with what I think is a good idea.<\/p>\n<p>Well, any idea, really.<\/p>\n<p>I'm something of a sucker for interesting research topics, and I'm incredibly thankful that there are some\nwonderful students and faculty at the University of Idaho who are willing to investigate some of these exciting\nideas.<\/p>\n<h3>What Ideas are Being Researched?<\/h3>\n<p>This year, I'm very thankful that I've been able to sponsor two different projects. A continuation project from\nlast year's <a href=\"https:\/\/engineerjoe440.github.io\/stanley-solutions-blog\/hearing-fires-while-seeing-smoke.html\">wildfire detection project<\/a>\nand a continuation of my own Capstone project, a <a href=\"http:\/\/mindworks.shoutwiki.com\/wiki\/Biochar_Production_System\">biochar reactor<\/a>.<\/p>\n<p>So... What are these wild ideas, anyway?<\/p>\n<h3>Wildfire Detection System<\/h3>\n<p>Wildfires are threatening more and more of the western United States each year, and currently, our only way to\ndetect fire is by sight. Namely by seeing the smoke of a growing inferno. That's often too late.<\/p>\n<p>Wildfire has a fascinating trait where it creates a very low rumble. It's the same audible rumble that wildlife\nand livestock respond to and run away from in advance of the flames becoming wild. This sound is typically in\nthe range between 0~8Hz; a very low-frequency sound which is below our (human) range of hearing. But that\ndoesn't mean a little technology can't provide value.<\/p>\n<p>In fact, that's exactly where we're researching. The idea of the project is to create very small, economical devices\nwhich can pick up this low-frequency sound and make quick determination whether sound is wild<em>fire<\/em> or wild<em>life<\/em>; a\nclear distinction to be made! If we can make small devices that cost around <span class=\"math\">\\(10-\\)<\/span>15 per unit that can do all this,\nwe can make hundreds, if not thousands, and distribute them from an aircraft. Then let those little buggers create\ntheir own wireless network and report back to a central base where all of the information can triangulate regions\nwhere the fire has appeared.<\/p>\n<h6>What are the students working on this year?<\/h6>\n<p>This year's student group is working on minimizing the hardware design. Last year, the group worked on utilizing a\nlarge development board; a unit that had more peripheral devices than were needed. Things like a dedicated power\nmanagement unit, multiple GPS units, and several radio and serial communication ports. That's not to say that last\nyear's team was bad, only that there's room to improve the design and reduce the circuit's footprint.<\/p>\n<p>This year's team is focusing on design from the microprocessor up. They're starting with the processor, and working\nout what additional peripherals are needed. But they're also working on the enclosure that's needed. They've taken\ninspiration from both last year's team, and a Samara leaf for the enclosure. The plan is that the leaf's natural\nspinning nature will provide the basis necessary to make the design fall smoothly and without a damaging the\ncrucial components inside.<\/p>\n<h3>Biochar Production System<\/h3>\n<p>Biochar - What is it?<\/p>\n<p>Well, it's \"the poor man's activated carbon.\" Essentially, it's charcoal, but with some more useful properties.\nThere's a lot of interest in the academic and agricultural world for using biochar both in agricultural markets\nand wastewater realms. There are many who are interested in the ability of biochar to act as a wastewater filter\nin large scales, and there are others who are very interested in use of biochar to help act as a topsoil balancer.<\/p>\n<p>Let me further explain the benefits here.<\/p>\n<p>Biochar is a very porous material, since it's what's left from wood after all the other oils and gasses have been\nseparated from the wood. Because of this porous nature, biochar can both retain water, and allow it to move more\neasily through soil. This is really interesting for farmers in regions like the Palouse where the topology flows\nacross rolling hills and generates a large number of both micro valleys and hills. These regions are prone to low\ncrop yield in both the valleys and on the tops of hills. It's all to do with water, actually. Too much water in\nthe valleys, and not enough on the tops of hills.<\/p>\n<p>So, biochar is very interesting because it can provide the necessary balance to help water pass through the valleys\nmore easily and retain the water near the tops of those hills. It's very exciting because this material might help\nsupport our farmers to increase their yields to help feed our growing world.<\/p>\n<h6>What are the students working on this year?<\/h6>\n<p>My team didn't complete a fully-functional biochar reactor, so this year's team is picking up where we'd left off.\nThey're working on getting the continuous reactor working efficiently so that we can start experimenting more\neffectively. I'm very excited about some of the realizations they've made, too! They've made some excellent\nobservations about efficiently heating the reactor. The team introduced me to the idea of using electric heat\nrather than using either a diesel or propane heater.<\/p>\n<p>At first, I wasn't convinced, but they pretty clearly demonstrated the ins and outs of why it's such a good idea.\nConsidering the fact that a diesel heater consumed as much energy as 73 kitchen toasters, whereas the electric\nheaters only uses as much energy as 3 toasters, it makes a lot of sense! The students did a terrific job in\ndemonstrating their findings and selling me on their proposal. In short, I'm very proud of them for their hard\nwork!<\/p>\n<h2>Recapping<\/h2>\n<p>I'm so very excited to have these students working on these exciting projects, and I'm thrilled to have the\npleasure of sponsoring the projects. In earnest, I often feel a tinge of imposter syndrome about supporting the\nprojects, because I'm not ever certain that I'm qualified to support them, and I feel that I'd pull resources\naway from other groups. So, I'm very thankful to be able to support these projects. I really hope that I'm able\nto continue with these projects!<\/p>\n<script type=\"text\/javascript\">if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {\n    var align = \"center\",\n        indent = \"0em\",\n        linebreak = \"false\";\n\n    if (false) {\n        align = (screen.width < 768) ? \"left\" : align;\n        indent = (screen.width < 768) ? \"0em\" : indent;\n        linebreak = (screen.width < 768) ? 'true' : linebreak;\n    }\n\n    var mathjaxscript = document.createElement('script');\n    mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';\n    mathjaxscript.type = 'text\/javascript';\n    mathjaxscript.src = 'https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/mathjax\/2.7.3\/latest.js?config=TeX-AMS-MML_HTMLorMML';\n\n    var configscript = document.createElement('script');\n    configscript.type = 'text\/x-mathjax-config';\n    configscript[(window.opera ? \"innerHTML\" : \"text\")] =\n        \"MathJax.Hub.Config({\" +\n        \"    config: ['MMLorHTML.js'],\" +\n        \"    TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'none' } },\" +\n        \"    jax: ['input\/TeX','input\/MathML','output\/HTML-CSS'],\" +\n        \"    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js'],\" +\n        \"    displayAlign: '\"+ align +\"',\" +\n        \"    displayIndent: '\"+ indent +\"',\" +\n        \"    showMathMenu: true,\" +\n        \"    messageStyle: 'normal',\" +\n        \"    tex2jax: { \" +\n        \"        inlineMath: [ ['\\\\\\\\(','\\\\\\\\)'] ], \" +\n        \"        displayMath: [ ['$$','$$'] ],\" +\n        \"        processEscapes: true,\" +\n        \"        preview: 'TeX',\" +\n        \"    }, \" +\n        \"    'HTML-CSS': { \" +\n        \"        availableFonts: ['STIX', 'TeX'],\" +\n        \"        preferredFont: 'STIX',\" +\n        \"        styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} },\" +\n        \"        linebreaks: { automatic: \"+ linebreak +\", width: '90% container' },\" +\n        \"    }, \" +\n        \"}); \" +\n        \"if ('default' !== 'default') {\" +\n            \"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {\" +\n                \"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;\" +\n                \"VARIANT['normal'].fonts.unshift('MathJax_default');\" +\n                \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" +\n                \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" +\n                \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" +\n            \"});\" +\n            \"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {\" +\n                \"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;\" +\n                \"VARIANT['normal'].fonts.unshift('MathJax_default');\" +\n                \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" +\n                \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" +\n                \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" +\n            \"});\" +\n        \"}\";\n\n    (document.body || document.getElementsByTagName('head')[0]).appendChild(configscript);\n    (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);\n}\n<\/script>","category":[{"@attributes":{"term":"Capstone"}},{"@attributes":{"term":"capstone"}},{"@attributes":{"term":"wildfire"}},{"@attributes":{"term":"university"}},{"@attributes":{"term":"research"}},{"@attributes":{"term":"students"}},{"@attributes":{"term":"biochar"}},{"@attributes":{"term":"agriculture"}}]},{"title":"Automating Python Releases with GitHub Actions","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/automating-python-releases-with-github-actions.html","rel":"alternate"}},"published":"2021-11-25T14:01:00-08:00","updated":"2021-11-25T14:01:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-11-25:\/automating-python-releases-with-github-actions.html","summary":"<p>I'm pretty lazy... We've covered that already, but wouldn't it be exceptionally nice if I could make GitHub automate Python package releases for me? Lets do that...<\/p>","content":"<p>Oh yes. I'm lazy.<\/p>\n<p>Haven't we established that, yet? Well, we're going to hit that nail home with this topic...<\/p>\n<p>We've established previously that I manage a number of random Python packages, including <em>ElectricPy<\/em>, <em>SELProtoPy<\/em>, and <em>PyCEV<\/em>.\nI've come to the realization that I need all the help I can get with releasing updates on a regular basis. So... How shall we do\nthat?<\/p>\n<h3>Where to Start?<\/h3>\n<p>I decided that I needed this for <em>ElectricPy<\/em> first. So let's start with figuring out what we want to do:<\/p>\n<ul>\n<li>Identify the Current ElectricPy Version from the Source Code (bail out if the version is the same or older than what's previously been released)<\/li>\n<li>Create a Tag that Matches the ElectricPy Version, then Push that to GitHub<\/li>\n<li>Build the Python Package as a Source-Code Bundle, and as a <a href=\"https:\/\/pythonwheels.com\">Python Wheel<\/a><\/li>\n<li>Push the Packages to the Python Package Index (PyPI)<\/li>\n<\/ul>\n<p>So those are the primary requirements. Now, let's work out how we're going to do it. I know that I want to use GitHub actions to\ndo this whole thing. So let's start there.<\/p>\n<h3>What are GitHub Actions, Anyway?<\/h3>\n<p>Well, GitHub actions are GitHub's way of providing CI\/CD systems. Essentially, providing Linux-container based workflows that are\ndefined through YAML description files. The YAML (which stands for Yet Another Mark-up Language) files define what container base\nshould be used, and what the steps need to be completed.<\/p>\n<h3>Are there any Read-to-Go GitHub Actions?<\/h3>\n<p>Well, yes... But, actually no.<\/p>\n<p>There's quite a few pretty good actions available in the community, but getting everything <em>just<\/em> right is a bit more tricky. Why, you\nask? Well, they all completed one or two of those actions I'd outlined above, but they didn't cover the whole list. So, I decided to\nglue them all together with a bit of Python!<\/p>\n<h3>Start by Identifying the Version<\/h3>\n<p>We need to pick the version out of the ElectricPy package, and then we need to double-check that it's not already used, or older than\nthe most-up-to-date version. So I built a simple little script:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># Release Versioning Support Script<\/span>\n<span class=\"c1\"># Joe Stanley | 2021<\/span>\n\n<span class=\"kn\">import<\/span> <span class=\"nn\">requests<\/span>\n\n<span class=\"n\">USERNAME<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;engineerjoe440&#39;<\/span>\n<span class=\"n\">REPO<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;electricpy&#39;<\/span>\n\n<span class=\"k\">try<\/span><span class=\"p\">:<\/span>\n    <span class=\"kn\">import<\/span> <span class=\"nn\">electricpy<\/span> <span class=\"k\">as<\/span> <span class=\"nn\">ep<\/span>\n<span class=\"k\">except<\/span> <span class=\"ne\">ImportError<\/span><span class=\"p\">:<\/span>\n    <span class=\"kn\">import<\/span> <span class=\"nn\">os<\/span><span class=\"o\">,<\/span> <span class=\"nn\">sys<\/span>\n    <span class=\"n\">sys<\/span><span class=\"o\">.<\/span><span class=\"n\">path<\/span><span class=\"o\">.<\/span><span class=\"n\">insert<\/span><span class=\"p\">(<\/span><span class=\"mi\">0<\/span><span class=\"p\">,<\/span> <span class=\"n\">os<\/span><span class=\"o\">.<\/span><span class=\"n\">getcwd<\/span><span class=\"p\">())<\/span>\n    <span class=\"kn\">import<\/span> <span class=\"nn\">electricpy<\/span> <span class=\"k\">as<\/span> <span class=\"nn\">ep<\/span>\n\n<span class=\"kn\">import<\/span> <span class=\"nn\">requests<\/span>\n\n<span class=\"n\">response<\/span> <span class=\"o\">=<\/span> <span class=\"n\">requests<\/span><span class=\"o\">.<\/span><span class=\"n\">get<\/span><span class=\"p\">(<\/span><span class=\"sa\">f<\/span><span class=\"s2\">&quot;https:\/\/api.github.com\/repos\/<\/span><span class=\"si\">{<\/span><span class=\"n\">USERNAME<\/span><span class=\"si\">}<\/span><span class=\"s2\">\/<\/span><span class=\"si\">{<\/span><span class=\"n\">REPO<\/span><span class=\"si\">}<\/span><span class=\"s2\">\/releases\/latest&quot;<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">try<\/span><span class=\"p\">:<\/span>\n    <span class=\"n\">latest<\/span> <span class=\"o\">=<\/span> <span class=\"n\">response<\/span><span class=\"o\">.<\/span><span class=\"n\">json<\/span><span class=\"p\">()[<\/span><span class=\"s2\">&quot;name&quot;<\/span><span class=\"p\">]<\/span>\n<span class=\"k\">except<\/span> <span class=\"ne\">Exception<\/span><span class=\"p\">:<\/span>\n    <span class=\"n\">latest<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;0.0.0&#39;<\/span>\n\n<span class=\"c1\"># Verify Version is Newer<\/span>\n<span class=\"n\">version<\/span> <span class=\"o\">=<\/span> <span class=\"sa\">f<\/span><span class=\"s2\">&quot;v<\/span><span class=\"si\">{<\/span><span class=\"n\">ep<\/span><span class=\"o\">.<\/span><span class=\"n\">_version_<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span>\n<span class=\"k\">if<\/span> <span class=\"n\">version<\/span> <span class=\"o\">&lt;=<\/span> <span class=\"n\">latest<\/span><span class=\"p\">:<\/span>\n    <span class=\"k\">raise<\/span> <span class=\"ne\">ValueError<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Module version is not newer than previous release!&quot;<\/span><span class=\"p\">)<\/span>\n<span class=\"k\">else<\/span><span class=\"p\">:<\/span>\n    <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"n\">version<\/span><span class=\"p\">)<\/span>\n<\/code><\/pre><\/div>\n\n<p>So, that script does a couple things for us. It polls GitHub for the latest release marked in the repo under my\nusername and project name. It then verifies that the version is valid and <em>new<\/em>.<\/p>\n<h3>How About that Action Definition?<\/h3>\n<p>So, we've covered one of the four pieces we need to accomplish. What's left? Well, we still need to build the\nPython package (but that's easy) and then create the release and push it to PyPI. Lucky for us, both of those\nremaining \"questions\" there's ready-made GitHub actions! So what does this whole thing look like?<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Release<\/span>\n<span class=\"n\">on<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"n\">push<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">   <\/span><span class=\"n\">branches<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">     <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">master<\/span>\n\n<span class=\"n\">jobs<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">  <\/span><span class=\"n\">release<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">runs<\/span><span class=\"o\">-<\/span><span class=\"n\">on<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">ubuntu<\/span><span class=\"o\">-<\/span><span class=\"n\">latest<\/span>\n<span class=\"w\">    <\/span><span class=\"n\">steps<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">actions<\/span><span class=\"o\">\/<\/span><span class=\"n\">checkout<\/span><span class=\"err\">@<\/span><span class=\"n\">v2<\/span>\n<span class=\"w\">      <\/span><span class=\"err\">#<\/span><span class=\"w\"> <\/span><span class=\"n\">https<\/span><span class=\"o\">:\/\/<\/span><span class=\"n\">github<\/span><span class=\"o\">.<\/span><span class=\"na\">com<\/span><span class=\"sr\">\/marketplace\/actions\/s<\/span><span class=\"n\">etup<\/span><span class=\"o\">-<\/span><span class=\"n\">python<\/span>\n<span class=\"w\">      <\/span><span class=\"err\">#<\/span><span class=\"w\"> <\/span><span class=\"o\">^--<\/span><span class=\"w\"> <\/span><span class=\"n\">This<\/span><span class=\"w\"> <\/span><span class=\"n\">gives<\/span><span class=\"w\"> <\/span><span class=\"n\">info<\/span><span class=\"w\"> <\/span><span class=\"n\">on<\/span><span class=\"w\"> <\/span><span class=\"n\">matrix<\/span><span class=\"w\"> <\/span><span class=\"n\">testing<\/span><span class=\"o\">.<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Install<\/span><span class=\"w\"> <\/span><span class=\"n\">Python<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">actions<\/span><span class=\"o\">\/<\/span><span class=\"n\">setup<\/span><span class=\"o\">-<\/span><span class=\"n\">python<\/span><span class=\"err\">@<\/span><span class=\"n\">v1<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">with<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">python<\/span><span class=\"o\">-<\/span><span class=\"n\">version<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;3.10&quot;<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Identify<\/span><span class=\"w\"> <\/span><span class=\"n\">Version<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">id<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">version<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">run<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"o\">|<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">m<\/span><span class=\"w\"> <\/span><span class=\"n\">pip<\/span><span class=\"w\"> <\/span><span class=\"n\">install<\/span><span class=\"w\"> <\/span><span class=\"n\">requests<\/span><span class=\"w\"> <\/span><span class=\"n\">build<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">user<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">m<\/span><span class=\"w\"> <\/span><span class=\"n\">pip<\/span><span class=\"w\"> <\/span><span class=\"n\">install<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">r<\/span><span class=\"w\"> <\/span><span class=\"n\">requirements<\/span><span class=\"o\">.<\/span><span class=\"na\">txt<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">user<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">output<\/span><span class=\"o\">=<\/span><span class=\"n\">$<\/span><span class=\"o\">(<\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"n\">release<\/span><span class=\"o\">-<\/span><span class=\"n\">version<\/span><span class=\"o\">.<\/span><span class=\"na\">py<\/span><span class=\"o\">)<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">echo<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;::set-output name=version::$output&quot;<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Build<\/span><span class=\"w\"> <\/span><span class=\"n\">Artifacts<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">if<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">success<\/span><span class=\"o\">()<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">id<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">build<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">run<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"o\">|<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">python<\/span><span class=\"w\"> <\/span><span class=\"o\">-<\/span><span class=\"n\">m<\/span><span class=\"w\"> <\/span><span class=\"n\">build<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">sdist<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">wheel<\/span><span class=\"w\"> <\/span><span class=\"o\">--<\/span><span class=\"n\">outdir<\/span><span class=\"w\"> <\/span><span class=\"n\">dist<\/span><span class=\"o\">\/<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Create<\/span><span class=\"w\"> <\/span><span class=\"n\">Release<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">ncipollo<\/span><span class=\"o\">\/<\/span><span class=\"n\">release<\/span><span class=\"o\">-<\/span><span class=\"n\">action<\/span><span class=\"err\">@<\/span><span class=\"n\">v1<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">with<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">tag<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">$<\/span><span class=\"o\">{{<\/span><span class=\"w\"> <\/span><span class=\"n\">steps<\/span><span class=\"o\">.<\/span><span class=\"na\">version<\/span><span class=\"o\">.<\/span><span class=\"na\">outputs<\/span><span class=\"o\">.<\/span><span class=\"na\">version<\/span><span class=\"w\"> <\/span><span class=\"o\">}}<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Release<\/span><span class=\"w\"> <\/span><span class=\"n\">$<\/span><span class=\"o\">{{<\/span><span class=\"w\"> <\/span><span class=\"n\">steps<\/span><span class=\"o\">.<\/span><span class=\"na\">version<\/span><span class=\"o\">.<\/span><span class=\"na\">outputs<\/span><span class=\"o\">.<\/span><span class=\"na\">version<\/span><span class=\"w\"> <\/span><span class=\"o\">}}<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">body<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">$<\/span><span class=\"o\">{{<\/span><span class=\"w\"> <\/span><span class=\"n\">steps<\/span><span class=\"o\">.<\/span><span class=\"na\">tag_version<\/span><span class=\"o\">.<\/span><span class=\"na\">outputs<\/span><span class=\"o\">.<\/span><span class=\"na\">changelog<\/span><span class=\"w\"> <\/span><span class=\"o\">}}<\/span>\n<span class=\"w\">          <\/span><span class=\"n\">artifacts<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">&quot;dist\/*&quot;<\/span>\n<span class=\"w\">      <\/span><span class=\"o\">-<\/span><span class=\"w\"> <\/span><span class=\"n\">name<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">Publish<\/span><span class=\"w\"> <\/span><span class=\"n\">distribution<\/span><span class=\"w\"> <\/span><span class=\"err\">\ud83d\udce6<\/span><span class=\"w\"> <\/span><span class=\"n\">to<\/span><span class=\"w\"> <\/span><span class=\"n\">PyPI<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">if<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">success<\/span><span class=\"o\">()<\/span>\n<span class=\"w\">        <\/span><span class=\"n\">uses<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">pypa<\/span><span class=\"o\">\/<\/span><span class=\"n\">gh<\/span><span class=\"o\">-<\/span><span class=\"n\">action<\/span><span class=\"o\">-<\/span><span class=\"n\">pypi<\/span><span class=\"o\">-<\/span><span class=\"n\">publish<\/span><span class=\"err\">@<\/span><span class=\"n\">master<\/span>\n<span class=\"w\">        <\/span><span class=\"k\">with<\/span><span class=\"o\">:<\/span>\n<span class=\"w\">            <\/span><span class=\"n\">password<\/span><span class=\"o\">:<\/span><span class=\"w\"> <\/span><span class=\"n\">$<\/span><span class=\"o\">{{<\/span><span class=\"w\"> <\/span><span class=\"n\">secrets<\/span><span class=\"o\">.<\/span><span class=\"na\">PYPI_API_TOKEN<\/span><span class=\"w\"> <\/span><span class=\"o\">}}<\/span>\n<\/code><\/pre><\/div>\n\n<p>That big definition basically does a bunch of stuff for us; I'll break it out by each of the steps:<\/p>\n<ol>\n<li>Check out the source code.<\/li>\n<li>Install Python 3.10 - Because we kinda need that. Notice here that <code>3.10<\/code> is in double quotes as: \"3.10\".\nThat's because otherwise, the GitHub system might mistake it as 3.1... You know, because 3.10 is really just\na decimal number with an extra 0 at the end.<\/li>\n<li>Use the Python Script (from above) to figure out the version. But not before installing the required\npackages; both for the script, and for <em>ElectricPy<\/em>.<\/li>\n<li>Build the Artifacts - The things we want to keep. Namely the source-code distribution (<code>--sdist<\/code>) and the\nwheel file.<\/li>\n<li>Create the GitHub release. This will place the package on the GitHub repo's \"Releases\" page and add a new\ntag to the repository so it's easy to back-track the code.<\/li>\n<li>Finally, push those artifacts to PyPI so they're available for download and install with <code>pip<\/code>.<\/li>\n<\/ol>\n<h3>Wrapping Up<\/h3>\n<p>This might not be the biggest accomplishment, but it's a huge relief because it makes automating releases\nand pushing out updates MUCH easier. So, let's bring on the new features and updates!!!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"automation"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"github actions"}},{"@attributes":{"term":"pypi"}}]},{"title":"DJ Joe Playlister","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/dj-joe-playlister.html","rel":"alternate"}},"published":"2021-10-16T12:03:00-07:00","updated":"2021-10-16T12:03:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-10-16:\/dj-joe-playlister.html","summary":"<p>I've been going a bit crazy with the web-app craze lately. Let me show you what I've been up to...<\/p>","content":"<p>I've been working up a fever on web-apps recently. Ones that I'm developing, ones that I'm deploying, and ones that\nI'm reviewing. That goes for both work and home. Gosh... I think I need a vacation. Maybe next lifetime.<\/p>\n<p>Recently, at home, my focus has been on several \"DJ Joe Services,\" things that I can utilize for my mobile DJ work and\nthat will help me make those processes easier. Remember, I'm lazy! I want to find the easiest way to do things. Right\nnow, I've got two apps deployed, and I'll be working on a third here pretty soon.<\/p>\n<p>The first app was an availability calendar. I'll have to write about it soon, since it was a fun project tying APIs,\nPython, and React.js all into one solution. However, this is about my second app. What I call a \"playlister,\" i.e.,\nsomething that can slurp the playlist information out of another file\/service\/etc., and provide it in a more consumable\nmanner. Right now, it's focus is on Spotify and Apple-Music, since those are the two prominent sources that are\nconsistent enough for me to work with.<\/p>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/Screenshot_20211016_120952.png\" style=\"width: 100%;\" alt=\"DJ Joe Playlister\"><\/p>\n<h3>Inspiration<\/h3>\n<p>As a mobile DJ, I often am provided \"playlists\" in various forms: Word documents, text\nfiles, quickly-scribbled hand-written notes, Spotify playlists, and Apple Music playlists.<\/p>\n<p>It quickly became apparent for me, that I spent <em>way<\/em> more time working through these\nSpotify playlists and Apple Music playlists to get them into a form that was actually\nhelpful for me. In most cases, I could not simply copy\/paste the Spotify list(s) out so\nthat I could search for the songs of interest in my own library and then determine whether\nI'd need to aqcuire additional music. Thus... I came to the conclusion, I'd want a little\nassistance from my computer.<\/p>\n<h3>Stages of Development<\/h3>\n<p>I originally started with a simple Tkinter-app that used the <a href=\"https:\/\/spotipy.readthedocs.io\/en\/latest\/\"><code>spotipy<\/code><\/a>\npackage to pull playlist information into a simple plain-text file. It was helpful, but\nended up incurring a few additional challenges of its own. The largest of which being the\nfact I had to securly pass the API secrets around with the script itself. This became a\nreal burden, so I decided to enhance the system into a full-service mini web-app that\ncould be utilized for exactly this purpose. The web-app could run persistently on a server\nthat could hang on to those secrets and allow me to access the tool from anywhere.<\/p>\n<p>Thus, the <code>djjoeplaylister<\/code> was born.<\/p>\n<h3>Technical Details<\/h3>\n<p>This app is built on the shoulders of giants, so let me give credit to those where it's due!<\/p>\n<p><strong>Technology Specs<\/strong><\/p>\n<ul>\n<li>Language: Python 3<\/li>\n<li>Web Framework: <a href=\"https:\/\/fastapi.tiangolo.com\/\">FastAPI<\/a><\/li>\n<li>Web Listener\/ASGI Server: <a href=\"https:\/\/uvicorn.org\/\">Uvicorn<\/a><\/li>\n<li>Reverse Proxy: <a href=\"https:\/\/nginx.com\/\">Nginx<\/a><\/li>\n<li>Hosting Provider: <a href=\"https:\/\/linode.com\/\">Linode virtual hosting<\/a><\/li>\n<li>Operating System: Ubuntu server<\/li>\n<li>App Deployment Enviromnent: Dockerized Container<\/li>\n<\/ul>\n<p><strong>Python Packages Leveraged<\/strong><\/p>\n<ul>\n<li>Spotify Client: <a href=\"https:\/\/spotipy.readthedocs.io\/en\/latest\/\"><code>spotipy<\/code><\/a><\/li>\n<li>Apple Music Client: <a href=\"https:\/\/docs.python-requests.org\/en\/latest\/\"><code>requests<\/code><\/a><\/li>\n<li>HTML Table Generation: <a href=\"https:\/\/pandas.pydata.org\/\"><code>pandas<\/code><\/a><\/li>\n<\/ul>\n<p>Additionally, I'd like to provide a special thanks and shout-out to this gist that\nhelped me get up and running with consuming the Apple Music playlist without dealing\nwith Apple's crummy developer program ($99 dolars a year, just to access an API? No\nthank you!)\n<a href=\"https:\/\/gist.github.com\/aleclol\/ef9e87d0964f00975f82d5373a814447\">https:\/\/gist.github.com\/aleclol\/ef9e87d0964f00975f82d5373a814447<\/a><\/p>\n<hr>\n<p>That's it! My little DJ Playlister! Want to go see it? <a href=\"https:\/\/playlists.djjoeidaho.com\/\">Go Check it Out!<\/a> It's not\nanything too terribly special, and it's got plenty of room to grow, but it's a helpful little tool, and I think it\nshowcases the utility of the Python programming language.<\/p>\n<h5>I mean... just think about it.<\/h5>\n<p>I started with a simple little Tkinter script for which I had to lug secrets around all the time, and it was great! But\nit had some significant shortcomings. Python to the rescue though, a little refactoring, and throw in some HTML, CSS,\nand some more packages and I've got a full web-application. Still full Python, and it's fully-deployed! You can't do that\nwith a lot of other tools. Imagine if I had started with some Excel macro, or some bash script. It would've been very\ndifficult to scale those apps out to something that's actually useful in the context that I need.<\/p>\n<p><strong><em>Not with Python!<\/em><\/strong><\/p>\n<p>Preaching session over. Chat again soon, goodbye!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"spotify"}},{"@attributes":{"term":"apple-music"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"web-apps"}},{"@attributes":{"term":"dj"}},{"@attributes":{"term":"docker"}}]},{"title":"Why do I Self-Host?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/why-do-i-selfhost.html","rel":"alternate"}},"published":"2021-09-28T21:53:00-07:00","updated":"2021-09-28T21:53:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-09-28:\/why-do-i-selfhost.html","summary":"<p>I was recently asked \"why do you run your own servers, when there's perfectly good, cheap, cloud providers?\" Well... Here's why!<\/p>","content":"<p>I was recently having a conversation with a few techie friends about the ridiculous and nerdy things that I'm doing at home. And, yes,\nI realize I'm very nerdy. If that isn't clear already, go read some of my other articles.<\/p>\n<p>One of my friends was curious why, exactly, I've decided to self-host so many of my services when I could easily lob those things up on\nsome cloud-service like <a href=\"https:\/\/www.linode.com\/unplugged\">Linode<\/a>. It's a fair question! To my friend's point, those services are fast,\neasy, and require very little maintenance (comparatively, of course). To be clear, I mean that these services manage much of the storage,\nhardware constraints, and networking limitations that I'm working through regularly. There's still the regular work of updating operating\nsystems that needs to be managed whether the server is running locally or in the cloud.<\/p>\n<p>I won't discredit these features! They're quite attractive, but there's still reason for me to keep my servers plugged in.<\/p>\n<p><em>Let me explain...<\/em><\/p>\n<h3>Reduce, Reuse, Recycle<\/h3>\n<p>This point is perhaps the most important to me. I believe I've mentioned before that a lot... Let me emphasize that point...<\/p>\n<p><strong><em>A LOT<\/em><\/strong><\/p>\n<p>of the hardware I'm using is old. Many of the computers are what could be considered \"ancient\" desktop towers. They're old computers doing\nmodern things. In fact, at this point I think I have more 32-bit computers than I do 64-bit ones. Before you ask, yes, it's a bit painful;\nbut it's worthwhile. I mean, just think about it... I'm currently running seven computers that other people were ready to just throw in\nthe trash. That's 7 computers that didn't need to be recycled, 7 computers whose lifetime just got extended courtesy of yours truly.<\/p>\n<p>Admittedly, they're power-hungry, and they're not as powerful as more modern equivalents, but they're doing great things for me, and that's\nsomething that they wouldn't be able to do otherwise. Others would more than likely just chuck them, and I think it's far more valuable to\nmilk those old machines for everything I can. Perhaps it's just the naive part of my personality, but I'd like to think that these little\nchoices have an impact in some small way.<\/p>\n<h3>My Disks, My Data<\/h3>\n<p>As just about any self-hosting fiend would likely tell you: \"my data lives on <em>my<\/em> disks\". Point being, since I own the disks, I'm the\nproprietor of the data, too! That means I don't ever have to concern myself with whether or not I can revoke control over the data. Now,\nI'll grant, this certainly makes me sound a bit more like a conspiracy theorist or an \"old codger\". I guess I don't have any rebuttal\nagainst that. I'll just have to take it.<\/p>\n<h4>Data that's Important to Me, For My Eyes Only<\/h4>\n<p>To tack on to the previous point, since I own the data, I can use what I consider \"private\" or \"privileged\" data on my servers. Why?\nBecause I own them! I can <em>see<\/em> where the disks that store the data reside. Now, I know this may not seem like the greatest argument, but\nwhen it comes to the argument of security and sensitivity, isn't it really all about perception and comfort anyway?<\/p>\n<p>To put it another way, why do millions of homeowners install a home-security system? Is it because they think that the system will\nimmediately stop thieves in the act? Wouldn't better deadbolts and barred windows provide the same level of protection? Perhaps,\nbut then, maybe not. I'd expect that in more cases than not, homeowners want a balance. They want features, and they want function.\nThey want to <em>feel<\/em> secure, and they want to enjoy their home without barred windows and dozens of deadbolts.<\/p>\n<p>That's why having my \"private\" data on my local servers is important to me. I've been graciously granted numerous data-set samples to\ntest some of my other Python projects against, but that data is important to the people who gave it to me. It's the sort of data that I'm\nhonored to have been granted access to, and I don't really think it's appropriate to share with the rest of the world. So, I host it on\nservers that live in my basement. Somewhere that lives behind <em>my<\/em> firewalls.<\/p>\n<p>I don't think that my solution is right for everyone, and I don't think it's the \"be-all-end-all\" solution that I wish it was, but it\nworks for me, and makes me happy! After all, if it helps me sleep well at night, isn't that worth something?<\/p>\n<h3>Diversified Service Structure<\/h3>\n<p>You've heard of diversified investments, haven't you?<\/p>\n<p>Well, that's kinda what I'm doing with my diversified infrastructure. You see, I'm not <em>only<\/em> self-hosting. <gasp!> I'm also using cloud\nservices (namely Linode - thanks, Linode Team!) to help me with some services, and I'm planning to spin up some others in the (relatively)\nnear future. Sometimes speed is important, sometimes it's not. When speed <em>is<\/em> a concern, I try to host on Linode, since they're so\n<strong>SUPER-FAST<\/strong> and available, it makes sense for me. But in other cases, it doesn't make sense.<\/p>\n<p>That's all part of the whole \"multi-cloud\" paradigm anyway, though. Different providers for different applications, diversified to be more\nrobust. In fact, I <em>do<\/em> use Linode for some off-site backups. I'm still working out my backup strategy. It's not the greatest, at the\nmoment, but it's coming along, and some of that is thanks to Linode's services, and the peace-of-mind they offer. Mind you, the data I'm\nbacking up to Linode still goes through secure tunnels, and it's not the \"private\" data that I was mentioning earlier.<\/p>\n<h3>Hands-On Learning Opportunities<\/h3>\n<p>My last point for keeping these machines kicking around in my closets, basement, and elsewhere is because they all offer me some great\nopportunities to learn! After all, if I'm going to keep these things up and running, I've got to constantly be improving, adding, reworking\nand modifying to (stealing a bit from my 4-H background here) make the best better. Having these servers in my house affords me the ability\nto simply plug in a USB stick, or connect a monitor if I blow up SSH so badly I can't reconnect. It means that when I botch the install, I\njust start over, and when I need to copy the whole darn disk, I just pull it out and stick it in my external hard-drive bay.<\/p>\n<p>Believe me, I've learned a lot in the past year playing with these things and keeping them up. And there's still a lot more I want to learn.<\/p>\n<hr>\n<p>So that's my story, and I'm sticking to it.<\/p>\n<p>Like I mentioned earlier, I wouldn't recommend this to everyone; it's a solution that fits my needs, but that may be different than what\nothers are interested in. Still, I'm proud of the fact that I'm running so much out of my own home, and I'm excited to keep growing with\nthese old computers. I'm happy that I'm able to reuse machines that would otherwise litter some landfill, and keep things running for\nmyself and some of the university students that I support.<\/p>","category":[{"@attributes":{"term":"DevOps"}},{"@attributes":{"term":"self-hosting"}},{"@attributes":{"term":"servers"}},{"@attributes":{"term":"computing"}},{"@attributes":{"term":"hosting"}},{"@attributes":{"term":"web"}},{"@attributes":{"term":"services"}}]},{"title":"Just Some Thoghts on a Song","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/just-some-thoghts-on-a-song.html","rel":"alternate"}},"published":"2021-09-23T13:58:00-07:00","updated":"2021-10-16T12:35:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-09-23:\/just-some-thoghts-on-a-song.html","summary":"<p>Some songs have a hidden meaning, and one song that I really enjoy has something hidden so deeply, I just love it!<\/p>","content":"<p><img src=\"https:\/\/images.genius.com\/526afafd58e01d4b67deca68f8840a80.1000x1000x1.jpg\"\n    width=\"300\" alt=\"LANCO\" align=\"right\"><\/p>\n<p>I'm a fan of many music genres, but I most certainly grew up on country. I mean... Have you met me? I wear a cowboy hat,\nfor pete-sake! I really lots of music, but I'll always have a soft spot for a good country love song.<\/p>\n<p>This definitely diverges from the other content that I often write about, but I think it's relevant. You see, one of the\nmodern country groups who I really enjoy is <a href=\"https:\/\/www.lancomusic.com\/\">\"LANCO\"<\/a>; they're more on the soulful side of\ncountry, and their bluesy style is one that I really enjoy. Specifically, I wanted to write about one of their songs\ncalled \"Born to Love You\", it's a great love song, and I want to highlight one specific piece...<\/p>\n<p>In the second stanza, they sing of:<\/p>\n<blockquote>\n<p>Born again in a church where the steeple's white\nPreacher preach Book of John and my momma cried\nMeanin' of life was in verse 2\nDidn't make sense 'til I found you<\/p>\n<\/blockquote>\n<p>At least... According to Google...<\/p>\n<p>Notice that in the third line of that stanza, the words \"in verse 2\" are separated. But if you listen to the song, I think\nthat they're <em>not<\/em> actually separate, at all! Instead, I think the line is a little closer to \"inverse-two\".<\/p>\n<p>So what the H-E-double-hockey-sticks is an \"inverse-two\" and why is it in a love song?<\/p>\n<p>Remember back in high-school math class, how we learned about number's inverses? An <em>inverse<\/em> of a number is 1-over-that\nnumber. In other words, a number's inverse is its fraction if you simply placed it as the denominator with 1 as the\nnumerator. To give a more concrete example, inverse-three is: <span class=\"math\">\\(\\frac{1}{3}\\)<\/span>. Likewise, inverse-two is <span class=\"math\">\\(\\frac{1}{2}\\)<\/span>.<\/p>\n<p>That's right, inverse-two is \"one-half\"... Read that stanza again.<\/p>\n<p>Suddenly, it's a little more touching, isn't it? Now whether that's truly LANCO's intention, or not, I think it adds a\nwhole new meaning and level of sincerity to that song. <a href=\"https:\/\/www.youtube.com\/watch?v=gjkn7orWpeA\"><strong>Go have a listen for yourself!<\/strong><\/a><\/p>\n<script type=\"text\/javascript\">if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {\n    var align = \"center\",\n        indent = \"0em\",\n        linebreak = \"false\";\n\n    if (false) {\n        align = (screen.width < 768) ? \"left\" : align;\n        indent = (screen.width < 768) ? \"0em\" : indent;\n        linebreak = (screen.width < 768) ? 'true' : linebreak;\n    }\n\n    var mathjaxscript = document.createElement('script');\n    mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';\n    mathjaxscript.type = 'text\/javascript';\n    mathjaxscript.src = 'https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/mathjax\/2.7.3\/latest.js?config=TeX-AMS-MML_HTMLorMML';\n\n    var configscript = document.createElement('script');\n    configscript.type = 'text\/x-mathjax-config';\n    configscript[(window.opera ? \"innerHTML\" : \"text\")] =\n        \"MathJax.Hub.Config({\" +\n        \"    config: ['MMLorHTML.js'],\" +\n        \"    TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'none' } },\" +\n        \"    jax: ['input\/TeX','input\/MathML','output\/HTML-CSS'],\" +\n        \"    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js'],\" +\n        \"    displayAlign: '\"+ align +\"',\" +\n        \"    displayIndent: '\"+ indent +\"',\" +\n        \"    showMathMenu: true,\" +\n        \"    messageStyle: 'normal',\" +\n        \"    tex2jax: { \" +\n        \"        inlineMath: [ ['\\\\\\\\(','\\\\\\\\)'] ], \" +\n        \"        displayMath: [ ['$$','$$'] ],\" +\n        \"        processEscapes: true,\" +\n        \"        preview: 'TeX',\" +\n        \"    }, \" +\n        \"    'HTML-CSS': { \" +\n        \"        availableFonts: ['STIX', 'TeX'],\" +\n        \"        preferredFont: 'STIX',\" +\n        \"        styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} },\" +\n        \"        linebreaks: { automatic: \"+ linebreak +\", width: '90% container' },\" +\n        \"    }, \" +\n        \"}); \" +\n        \"if ('default' !== 'default') {\" +\n            \"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {\" +\n                \"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;\" +\n                \"VARIANT['normal'].fonts.unshift('MathJax_default');\" +\n                \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" +\n                \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" +\n                \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" +\n            \"});\" +\n            \"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {\" +\n                \"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;\" +\n                \"VARIANT['normal'].fonts.unshift('MathJax_default');\" +\n                \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" +\n                \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" +\n                \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" +\n            \"});\" +\n        \"}\";\n\n    (document.body || document.getElementsByTagName('head')[0]).appendChild(configscript);\n    (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);\n}\n<\/script>","category":[{"@attributes":{"term":"Audio"}},{"@attributes":{"term":"music"}},{"@attributes":{"term":"songs"}},{"@attributes":{"term":"audio"}}]},{"title":"A Better Way to Integrate with VoiceMeeter?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/a-better-way-to-integrate-with-voicemeeter.html","rel":"alternate"}},"published":"2021-09-12T17:07:00-07:00","updated":"2021-09-15T17:38:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-09-12:\/a-better-way-to-integrate-with-voicemeeter.html","summary":"<p>Wait... What? There's an API for VoiceMeeter? And there's already a Python API for it? Sign me up!<\/p>","content":"<p>So, I finally sucked it up and bought an Intel NUC to run as my own mini audio-server. For what purpose,\nyou ask? Well, so that I can have an \"always on\" <a href=\"https:\/\/vb-audio.com\/Voicemeeter\/vban.htm\">VBAN<\/a> server\nwhere I can route audio through-out my house. From my desktop, my laptop, my mixer, my stereo.... All\nover!<\/p>\n<p>Now, I did run into a little trouble in the process. My little NUC is mounted nicely out of the way at my\ndesk in my study. It looks great, and runs great... buuuuut... There's one little problem.<\/p>\n<p>When I connect or disconnect over a Remote Desktop (RDP) connection it gets, shall we say, a little mixed\nup. In fact, the VoiceMeeter audio engine falls all over itself and gets tangled up. Now, to get around\nthis problem, I can restart the audio engine, or the software itself. So I started looking into how to\nautomate the kick-in-the-pants the software needed. Mind you, I'd done this before by using Python to find\nthe process ID that VoiceMeeter was associated with, and kill it, restarting a moment later. But that's\nboring and slow. So I did some Googling...<\/p>\n<p>Turns out, VoiceMeeter has an <a href=\"https:\/\/forum.vb-audio.com\/viewtopic.php?f=8&amp;t=346\">API<\/a>! FANTASTIC!<\/p>\n<p>Now, I realize that that's a C-API, and I'd much rather do my programming in Python. I don't really want\nto fuss with installing GCC on my little NUC. SO... I started investigating how to wrap the C-API with\nPython. It's something I'd never done before, but I figured it must be possible! Once more, I turned to\nthe internet wizards, and found a very <a href=\"https:\/\/stackoverflow.com\/a\/252473\/10406011\">nice little article on StackOverflow<\/a>\non how to wrap a C-level DLL with Python.<\/p>\n<p>I used that article and proved to myself that, YES, I can write Python code to hit the DLL. But, it\noccurred to me, that maybe somebody else had already done that work.<\/p>\n<p>Back to Google...<\/p>\n<p>EUREKA! Turns out that someone (<a href=\"https:\/\/github.com\/chvolkmann\">Christian Volkmann<\/a>, to be specific) had\nalready written a full API against the DLL. It's all in Python, and it's glorious! Here...\n<a href=\"https:\/\/github.com\/chvolkmann\/voicemeeter-remote-python\">Go take a look!<\/a><\/p>\n<p>So all that left for me was whacking out a little script to run in the background, monitor for new RDP\nconnections, and restart the audio engine when the connection state changed. Here's what that looked\nlike:<\/p>\n<div class=\"highlight\"><pre><span><\/span><code><span class=\"c1\"># vmeetermanager - an automated tool to keep VoiceMeeter running correctly.<\/span>\n<span class=\"c1\"># (c) 2021 - Stanley Solutions | Joe Stanley<\/span>\n\n<span class=\"c1\"># Imports<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">voicemeeter<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">subprocess<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">time<\/span>\n\n\n<span class=\"c1\"># Define function to determine rdp connection<\/span>\n<span class=\"k\">def<\/span> <span class=\"nf\">is_rdp_connected<\/span><span class=\"p\">():<\/span>\n    <span class=\"n\">args<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"s2\">&quot;netstat&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;-n&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;|&#39;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;find&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;&quot;:3389 &quot;&#39;<\/span><span class=\"p\">]<\/span>\n    <span class=\"n\">resp<\/span> <span class=\"o\">=<\/span> <span class=\"n\">subprocess<\/span><span class=\"o\">.<\/span><span class=\"n\">run<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39; &#39;<\/span><span class=\"o\">.<\/span><span class=\"n\">join<\/span><span class=\"p\">(<\/span><span class=\"n\">args<\/span><span class=\"p\">),<\/span> <span class=\"n\">shell<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">,<\/span> <span class=\"n\">capture_output<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">if<\/span> <span class=\"s2\">&quot;ESTABLISHED&quot;<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">resp<\/span><span class=\"o\">.<\/span><span class=\"n\">stdout<\/span><span class=\"o\">.<\/span><span class=\"n\">decode<\/span><span class=\"p\">(<\/span><span class=\"s1\">&#39;utf-8&#39;<\/span><span class=\"p\">):<\/span>\n        <span class=\"k\">return<\/span> <span class=\"kc\">True<\/span>\n    <span class=\"k\">return<\/span> <span class=\"kc\">False<\/span> <span class=\"c1\"># Default<\/span>\n\n\n<span class=\"c1\"># Main Body<\/span>\n<span class=\"k\">if<\/span> <span class=\"vm\">__name__<\/span> <span class=\"o\">==<\/span> <span class=\"s2\">&quot;__main__&quot;<\/span><span class=\"p\">:<\/span>\n    <span class=\"n\">last_state<\/span> <span class=\"o\">=<\/span> <span class=\"kc\">False<\/span>\n    <span class=\"c1\"># Establish VoiceMeeter Connection<\/span>\n    <span class=\"k\">while<\/span> <span class=\"kc\">True<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">try<\/span><span class=\"p\">:<\/span>\n            <span class=\"k\">with<\/span> <span class=\"n\">voicemeeter<\/span><span class=\"o\">.<\/span><span class=\"n\">remote<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;banana&quot;<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">vmr<\/span><span class=\"p\">:<\/span>\n                <span class=\"c1\"># Run Loop<\/span>\n                <span class=\"k\">while<\/span> <span class=\"kc\">True<\/span><span class=\"p\">:<\/span>\n                    <span class=\"c1\"># Determine Connection State<\/span>\n                    <span class=\"n\">connected<\/span> <span class=\"o\">=<\/span> <span class=\"n\">is_rdp_connected<\/span><span class=\"p\">()<\/span>\n                    <span class=\"n\">changed_state<\/span> <span class=\"o\">=<\/span> <span class=\"n\">connected<\/span> <span class=\"o\">!=<\/span> <span class=\"n\">last_state<\/span>\n                    <span class=\"n\">last_state<\/span> <span class=\"o\">=<\/span> <span class=\"n\">connected<\/span>\n                    <span class=\"c1\"># If the state has changed, restart audio engine<\/span>\n                    <span class=\"k\">if<\/span> <span class=\"n\">changed_state<\/span><span class=\"p\">:<\/span>\n                        <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"sa\">f<\/span><span class=\"s2\">&quot;RDP Connection State Changed to: CONNECTED=<\/span><span class=\"si\">{<\/span><span class=\"n\">connected<\/span><span class=\"si\">}<\/span><span class=\"s2\">&quot;<\/span><span class=\"p\">)<\/span>\n                        <span class=\"n\">time<\/span><span class=\"o\">.<\/span><span class=\"n\">sleep<\/span><span class=\"p\">(<\/span><span class=\"mf\">0.25<\/span><span class=\"p\">)<\/span>\n                        <span class=\"n\">vmr<\/span><span class=\"o\">.<\/span><span class=\"n\">restart<\/span><span class=\"p\">()<\/span>\n                    <span class=\"c1\"># Don&#39;t overburden the systems<\/span>\n                    <span class=\"n\">time<\/span><span class=\"o\">.<\/span><span class=\"n\">sleep<\/span><span class=\"p\">(<\/span><span class=\"mi\">1<\/span><span class=\"p\">)<\/span>\n        <span class=\"k\">except<\/span> <span class=\"ne\">Exception<\/span><span class=\"p\">:<\/span>\n            <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;VoiceMeeter Hasn&#39;t Started Yet...&quot;<\/span><span class=\"p\">)<\/span>\n            <span class=\"n\">time<\/span><span class=\"o\">.<\/span><span class=\"n\">sleep<\/span><span class=\"p\">(<\/span><span class=\"mi\">3<\/span><span class=\"p\">)<\/span>\n<\/code><\/pre><\/div>\n\n<h3>What Else Will Come?<\/h3>\n<p>Goodness, there's so many other things that I can do with this now. Imagine having a full web-based front\nend that I could use to control it! That would be pretty awesome, wouldn't it? I'll really have to do\nsome more exploring with this!<\/p>","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"voicemeeter"}},{"@attributes":{"term":"api"}},{"@attributes":{"term":"sdk"}},{"@attributes":{"term":"mixer"}},{"@attributes":{"term":"python"}}]},{"title":"Tech Podcasts Galore!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/tech-podcasts-galore.html","rel":"alternate"}},"published":"2021-09-12T15:34:00-07:00","updated":"2021-09-12T15:34:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-09-12:\/tech-podcasts-galore.html","summary":"<p>Everyone's got their new favorite podcast these days. So here, let me list all of my favorites!<\/p>","content":"<p>I've been asked a lot lately what tech podcasts I listen to, so I thought I'd briefly summarize them all\nright here!<\/p>\n<p>Where should I even begin, though? Perhaps I'll start with the security side of computers...<\/p>\n<h4><em>\"Darknet Diaries\"<\/em><\/h4>\n<p><img src=\"https:\/\/upload.wikimedia.org\/wikipedia\/en\/6\/6a\/Darknet_Diaries_podcast_artwork.jpg\"\n    width=\"150\" alt=\"Darknet Diaries\" align=\"right\"><\/p>\n<p>One of my favorites was recommended to me by a colleague. <a href=\"https:\/\/darknetdiaries.com\/\"><em>Darknet Diaries<\/em><\/a>\nis an excellent podcast covering everything in the darker side of the internet. No, don't worry... You\ndon't need to download a TOR browser to listen. This podcast covers everything from hackers to penetration\ntesters; everything in the cybersecurity space.<\/p>\n<p>Jack Rhysider, the host of <em>Darknet Diaries<\/em> covers a wide variety of stories in the tech and security\nworld, and yet, somehow he manages to make each story captivating for all audiences. I really do mean <strong>all<\/strong>\naudiences, too. I've even had my mother and other non-techy folks listen to episodes and find some value.<\/p>\n<h4><em>\"Security Now\"<\/em><\/h4>\n<p>For those interested in the deep-tech-intricacies of cybersecurity, Steve Gibson and\n<a href=\"https:\/\/twit.tv\/shows\/security-now\"><em>Security Now<\/em><\/a> digs deep into the meat-and-potatoes; or should I say,\nthey bytes-and-bits of the stuff. Covering everything from <em>Boot-Hole<\/em> and <em>Print-Nightmare<\/em> to the latest\nin <a href=\"https:\/\/www.wireguard.com\/\">Wireguard<\/a> and new security practices for IoT devices. <em>Security Now<\/em> is a\ngreat podcast for the tech-minded and security-interested.<\/p>\n<p>Now... Perhaps I should switch gears to the more programming-focused side of the table. After all, I've got\na number of podcasts that I listen to in this arena too!<\/p>\n<h4><em>\"Python Bytes\"<\/em><\/h4>\n<p><img src=\"https:\/\/pythonbytes.fm\/static\/img\/logo.png?cache_id=391cb49247369a67c4be78b27f2b3cd5\"\n    width=\"150\" alt=\"Python Bytes\" align=\"left\"><\/p>\n<p>By far, my favorite of the programming-pods, <a href=\"https:\/\/pythonbytes.fm\/\"><em>Python Bytes<\/em><\/a> covers the latest news\nin the Python programming language and the areas of the tech world that Python supports. Everything from web\nservers to data-science, from embedded Python to the data-center!<\/p>\n<p><em>Python Bytes<\/em> is a little on the shorter side; at least when compared with the other podcasts I've listed so\nfar. Michael Kennedy and Brian Okken cover all the latest-and-greatest Python modules and techniques with at\nleast one guest host every week.<\/p>\n<h4><em>\"Talk Python to Me\"<\/em><\/h4>\n<p>Another podcast from Michael Kennedy (one of the hosts of the aforementioned <em>Python Bytes<\/em>) is\n<a href=\"https:\/\/talkpython.fm\"><em>Talk Python to Me<\/em><\/a>. It's a little longer than its sister podcast, and goes into\ngreater detail in the technology or topic of interest. Michael brings on a variety of fantastic guests, all\nof which help discuss the latest news.<\/p>\n<h4><em>\"Test and Code\"<\/em><\/h4>\n<p>Brian Okken's other podcast, much like <em>Talk Python to Me<\/em> is a companion podcast to <em>Python Bytes<\/em>. Brian,\nthe author of <em>\"Python Testing with pytest: Simple, Rapid, Effective, and Scalable\"<\/em> covers the intricacies\nof testing code effectively. <a href=\"https:\/\/testandcode.com\/\"><em>Test and Code<\/em><\/a> is a great podcast, and is a little\non the shorter side, so it makes for quick listening!<\/p>\n<h4><em>\"Coder Radio\"<\/em><\/h4>\n<p><a href=\"https:\/\/coder.show\/\"><em>Coder Radio<\/em><\/a> is a discussion-based show talking all about \"the art and business of\nprogramming\". It's often very opinionated, but let's be honest... Have you ever met a programmer who isn't?\nA Jupiter Broadcasting family podcast, <em>Coder Radio<\/em> talks everything from Python to Objective-C, Rust to\nRuby, GO to JS.<\/p>\n<p>Now, getting into the rest of the tech-stack, we simply must talk about Linux! And here, I've got quite the\nassortment of Linux podcasts... Let's get started.<\/p>\n<h4><em>\"Linux Unplugged\"<\/em><\/h4>\n<p><img src=\"https:\/\/assets.fireside.fm\/file\/fireside-images\/podcasts\/images\/f\/f31a453c-fa15-491f-8618-3f71f1d565e5\/cover_small.jpg?v=3\"\n    width=\"150\" alt=\"Linux Unplugged\" align=\"right\"><\/p>\n<p>By far, my favorite Linux pod, <a href=\"https:\/\/linuxunplugged.com\/\"><em>Linux Unplugged<\/em><\/a> covers everything in the\ncommunity of Linux; from new technologies to community happenings. Chris Fisher and Wes Payne bring the best\nof the community together and make a fantastic weekly show as part of the Jupiter Broadcasting family. Always\nkeeping high-energy, enthusiastic goals, and a hopeful spirit; Chris and Wes open up with the community every\nTuesday.<\/p>\n<h4><em>\"Self-Hosted\"<\/em><\/h4>\n<p>Admittedly, it's not <em>directly<\/em> Linux focused, but it <em>is<\/em> \"Linux adjacent\". <a href=\"https:\/\/selfhosted.show\/\"><em>Self-Hosted<\/em><\/a>\nis another Jupiter Broadcasting show, but it caters towards (namely) the self-hosting side of the open-source\ncommunity. Everything from Nextcloud to Plex, Wireguard to Home Assistant. This is another one of my favorite\npodcasts, and thus its placement in my lineup.<\/p>\n<h4><em>\"Linux Action News\"<\/em><\/h4>\n<p><a href=\"https:\/\/linuxactionnews.com\/\">LAN<\/a>, A.K.A. Linux Action News, is a short-and-sweet weekly podcast covering\nall the latest news in the Linux world. Whether you're a die-hard Linux fan-boy, or a part-time tech\nenthusiast, LAN is a great place to get the latest news in the Linux landscape. Being part of the Jupiter\nBroadcasting family, it sees the same level of wonderful production care.<\/p>\n<h2>More?<\/h2>\n<p>Hmm... this isn't an all encompassing list, but there just isn't time.... I'll have to write more after a while.<\/p>","category":[{"@attributes":{"term":"Audio"}},{"@attributes":{"term":"tech"}},{"@attributes":{"term":"podcasts"}},{"@attributes":{"term":"media"}}]},{"title":"Hearing Fires While Seeing Smoke","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/hearing-fires-while-seeing-smoke.html","rel":"alternate"}},"published":"2021-07-12T20:18:00-07:00","updated":"2024-04-30T12:32:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-07-12:\/hearing-fires-while-seeing-smoke.html","summary":"<p>While most of North Idaho is seeing (and smelling) plenty of smoke, I'm looking back over a successful Capstone project and more to come...<\/p>","content":"<p>If you've talked to me at all in the past nine months or so, you probably already know that I had the great privilege of sponsoring an Engineering\nCapstone project at the University of Idaho. Now, admittedly, it's something of a selfish endeavor as I have far too many \"fun things\" that I'd like\nto explore, and not enough time or energy to do them all. Thankfully, some good faculty members are just as excited about some of these intriguing\nprojects as I am.<\/p>\n<p>What's more, though, as I sit here in North Idaho, surrounded by worsening smoke from many terrible wildfires, I'm reflecting on the project I was\nso lucky to sponsor, and how excited I am for its future, and what future students will bring to it.<\/p>\n<h3>The Project<\/h3>\n<p><img src=\"https:\/\/blog.stanleysolutionsnw.com\/team-firewatch.jpg\" width=\"300\" alt=\"Meet Team Firewatch!\" align=\"right\"><\/p>\n<p>So what was it? Well, the project in a nutshell was to lay the groundwork for a design of a device which will detect wildfire using infrasonic sound;\nthat's the sound that's lower than what we can hear - think that death rumble from that old beater car you drove in high-school. No? Just me? Oh...<\/p>\n<p>The infrasonic sound would be detected by an ultra-economical (yeah, cheap) condenser microphone and amplified before being measured by a small\nmicrocontroller responsible for wrapping that data up into a message sent over-the-air back to some central base. But what's to make it more interesting,\nthis wireless message is to be sent over a mesh network of these devices. Think hundreds... no, thousands of these little devices scattered around the\nforest, all taking readings at specific intervals and relaying that information back to us so we can <em>hear<\/em> if there's a wildfire starting anywhere.<\/p>\n<p>If this all sounds familiar, it's probably because I've already <a href=\".\/wildfire-prevention-with-sound\">told you all about it<\/a>. This project is one that\nwas started a few years back at the <a href=\"https:\/\/uidaho.edu\/\">University of Idaho<\/a> and was discontinued due to a lack of funding and support. Luckily\nthe sponsoring professor was still just as interested as I was, so we were able to pick it back up and continue down the road to developing some really\nneat tech.<\/p>\n<h3>This Year's Team<\/h3>\n<p>So, this year, we had a team of some really outstanding students from a variety of backgrounds. Meridian, a mechanical engineer; Carlos, a computer\nscientist; and two electrical engineers, Cory and Drew. We had the luxury of giving this fantastic team the flexibility of choosing their own path\nin terms of the areas they were really interested in researching, which made for a lot of fun.<\/p>\n<p>I was really impressed by these students and their willingness to work so hard on this project. They produced some really fantastic work, and I get\nthe pleasure of keeping it! At least... until next year, when we get some more students to work on it.<\/p>\n<h3>The Product<\/h3>\n<p>The team was able to design a really wonderful and unique enclosure to encapsulate their microcontroller, sensor, antenna, and amplifier circuit. They\nwere also able to use some clear acrylic tubing to really show off how sharp this thing looks!<\/p>\n<p><img src=\"http:\/\/images.shoutwiki.com\/mindworks\/thumb\/5\/5b\/2021_infrasonic_wildfire_detector_finished_enclosure.png\/800px-2021_infrasonic_wildfire_detector_finished_enclosure.png\" width=\"500\" alt=\"The Sensor...\" align=\"left\" style=\"padding:10px;\"><\/p>\n<p>Now, I'm not going to go too-deep into the technical specs of this thing, partly because I don't want to type it all out, and partly because the team\nhas already done such a fantastic job documenting it both in their <a href=\"http:\/\/mindworks.shoutwiki.com\/wiki\/Infrasonic_Wildfire_Detector\">Wiki page<\/a>\nand in their <a href=\"https:\/\/gitlab.stanleysolutionsnw.com\/infrasound-detector\/portfolio-2020-2021\">GitLab repo<\/a> (which is one of the services I host!).<\/p>\n<h3>What's Next<\/h3>\n<p>The project is nowhere near complete (even as cool as that enclosure looks). So I'm already lined up to sponsor a continued Capstone program again this\nyear (so long as the University will let me!), and I'm very excited to do so. There's certainly a few key things that we still need to work out:<\/p>\n<ul>\n<li>Power - An obvious concern for any IoT device, and certainly one responsible for such a challenging environment.<\/li>\n<li>Sensor Validation - This years team did a great job bootstrapping their way up, but with luck, next year's team will be able to refine that work into\n  something that can prove the validity of the sensor.<\/li>\n<li>Wireless Mesh Networking - This was something that was really exciting since this year's team was able to find an open-source library to help with\n  this challenge, but there's more to be done; especially if the network is going to be anything larger than a few nodes.<\/li>\n<li>Enclosure Testing - Anything exposed to the elements needs to be hardened, and this enclosure still needs a little refinement there.<\/li>\n<\/ul>\n<p>All that said, I'm really excited to see where this project continues to grow, and what this next year's group of students will be able to accomplish!<\/p>\n<h3>Team Presentation<\/h3>\n<div class=\"videowrapper youtube\">\n<iframe frameborder=\"0\" src=\"https:\/\/www.youtube-nocookie.com\/embed\/wcGvln6jVRM\"><\/iframe>\n<\/div>\n<h4>University of Idaho Mention<\/h4>\n<blockquote>\n<p>In 2020, 10.3 million acres were burned by wildfire, up from 4.7 million in 2019. These disasters cause major destruction, yet our primary tool for detecting wildfire is simply smelling smoke.<\/p>\n<p>The scientific world only recently discovered large wildfires generate infrasonic waves, or sound below the frequency of what the average human can hear, under 20 hertz.<\/p>\n<p>Our interdisciplinary team of engineering and computer science seniors, Meridian Haas, Cory Holt, Andrew Malinowski and Carlos Santos, are developing a device that uses infrasonic detection and signal processing to detect wildfires.<\/p>\n<p>These devices, deployed in batches, can be used to create a communications network to relay detection information to responders along with GPS coordinates and timestamps at regular intervals.<\/p>\n<p>\u201cThe ability to signal to firefighters to pinpoint a wildfire location when it\u2019s still small could help save thousands of acres and homes,\u201d Haas said. \u201cWe want to make this device as cheap as possible and have as many as we can.\u201d<\/p>\n<p>Using a long-range and low-power data transmit network, Holt said the team has verified device communication up to 12 miles so far, well past their \u00bd mile goal.<\/p>\n<p>The team has developed a compact, low-cost, prototype that could eventually be constructed from decomposable materials to reduce environmental impact.<\/p>\n<p>The long-term goal would be to drop these devices on a location by air. Materials testing is ongoing.<\/p>\n<p>Learn more about this project and all the ways U of I students are pushing the boundaries of science and technology at the VIRTUAL Engineering Design EXPO!<\/p>\n<\/blockquote>","category":[{"@attributes":{"term":"Capstone"}},{"@attributes":{"term":"wildfire"}},{"@attributes":{"term":"iot"}},{"@attributes":{"term":"mcu"}},{"@attributes":{"term":"mesh"}},{"@attributes":{"term":"radio"}},{"@attributes":{"term":"smart-sensor"}},{"@attributes":{"term":"capstone"}},{"@attributes":{"term":"university"}}]},{"title":"Demonstrating Electricity","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/demonstrating-electricity.html","rel":"alternate"}},"published":"2021-06-25T19:03:00-07:00","updated":"2021-06-25T19:03:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-06-25:\/demonstrating-electricity.html","summary":"<p class=\"first last\">I recently had the pleasure of chaperoning a 4-H conference at the University of Idaho, but I also was able to teach!<\/p>\n","content":"<p>If you've known me for any period of time, you probably already know that I grew up participating in my local 4-H program. I was a Cloverbud member\nbefore I could be a regular member, meaning I spent about twelve years in the program. Ten of those years were spent raising hogs, and a few were\nalso spent participating in various leadership roles.<\/p>\n<p>I spent a year as my community-club's secretary, two years as president; and I even served as vice-president and even president of my county's teen\nleadership club. It was a great experience, and I learned so much about myself during all of those adventures. In my last two years as a member, I\nwas able to attend the Idaho state &quot;Teen Conference&quot; as it was called at the time. It was (and is) a statewide event held on the University of Idaho's\ncampus to give high-school students the opportunity to explore a college campus, learn new leadership skills, and connect with peers from around the\nstate.<\/p>\n<p>In the past few years, I've had the extreme pleasure of supporting the conference (now called the\n<a class=\"reference external\" href=\"https:\/\/www.uidaho.edu\/extension\/4h\/events\/stac\">State Teen Association Convention<\/a> - or STAC for short) as a college staff member, but this year I\nwas able to participate as a chaperone, and what's more, I was able to co-lead a workshop during the conference. What did I lead? Oh! A workshop covering\nelectricity and the electrical power grid; of course!<\/p>\n<p>I worked with a few colleagues from Schweitzer Engineering Laboratories to create a workshop to cover a few interesting topics about the grid. But, if\nyou know me, you know I much prefer hands-on exercises than any presentation. So I spent a little too much time working on creating a couple of demo\nboards; seven in total.<\/p>\n<div class=\"section\" id=\"the-demos\">\n<h2>The Demos:<\/h2>\n<p>I made two sets of three boards, and a single stand-alone board. They were all to basically show:<\/p>\n<ul class=\"simple\">\n<li>Basic circuit: generation\/source, transmission, and load<\/li>\n<li>Overloading circuit: multiple incandescent lights and a USB charger<\/li>\n<li>&quot;Breaker&quot; demonstration<\/li>\n<\/ul>\n<p>I made three of the basic circuit, ultimately to make a point about what an electrical fault might appear as, and how repairing it might be possible. It\nwas basically constructed with a AA battery pack, a few spring terminals, and a single incandescent T-10 socket\/bulb. With this model, we're able to\nexplain the basics of an energy source, energy transmission, and an energy sink (load). What made this fun, was that we could go around once the students\nhad their little power system running and create a &quot;fault&quot; by cutting the wire between the spring terminals. Then we could instruct the students to fix\ntheir system and leave them to their own devices (and a few odd assorted materials).<\/p>\n<img alt=\"Basic circuit demonstration board (one of three identical models).\" src=\"https:\/\/blog.stanleysolutionsnw.com\/203019151_490948755513699_8766751111523803463_n.jpg\" style=\"width: 600px;\" \/>\n<p>I also made three of the overloading circuit. This one was, by far, my favorite. You see, these boards are set up with 5 separately controlled T-10\nsockets, each with their own ON-OFF latching pushbutton. There's also the matter of a USB phone charger connected at the &quot;far end&quot;. The whole thing is\npowered by a single 9V battery, connected to the rest of the circuit by a single 1-ohm power resistor. This all means that as each of the incandescent\nlights are turned on (being as they're all connected in parallel) the voltage across the power resistor will continue to drop. The USB charger circuit\nis rated for some 6-24VDC (approximately). Meaning that when the voltage input drops below the rated minimum (6V) the supply will cease to function.\nThat means that the system will quite easily overload to the point of failure. This was a great example for the students, because it allowed them to\ntruly play with the system until they can get a sense about how the system can be <em>&quot;loaded down&quot;<\/em>. The point here was just to share with students just\nhow the power system can theoretically be overloaded, and how that's not good for any of the consumers. Pretty cool to see in action!<\/p>\n<img alt=\"Overloading circuit demonstration board (one of three identical models).\" src=\"https:\/\/blog.stanleysolutionsnw.com\/203656236_2868616930066285_3189504440222786790_n.jpg\" style=\"width: 600px;\" \/>\n<p>Finally, I built a single board (on a whim, I might add) to demonstrate how a breaker acts (effectively) as a latching switch. I used a simple ice-cube\nrelay to act in this capacity, and to be controlled by a few simple push-buttons. One button (red, of course, to match the electrical industry standard)\nwas used to <em>close<\/em> the breaker. One button (green -just as the red- matching the industry standard) to <em>&quot;open&quot;<\/em> the circuit, and finally a simple button\noffset from the others, and labeled to indicate a fault. It's quite simple, really; both the fault and open switches effectively create a simple dead-short\nso as to reduce the voltage across the relay coil to the point of loosing the magnetic field, and <em>falling<\/em> open. However, from the student's perspective,\nit all looks and feels real. It seems that the &quot;breaker&quot; is responding to commands to open or close, and that it responds to a fault by opening to protect\nitself and the system. Lastly, I should note, there is a single blue LED in this circuit, too, just to indicate whether the circuit is energized or not.<\/p>\n<img alt=\"Breaker circuit demo board.\" src=\"https:\/\/blog.stanleysolutionsnw.com\/203681209_804202393601180_447891412997591781_n.jpg\" style=\"width: 600px;\" \/>\n<\/div>\n<div class=\"section\" id=\"the-results\">\n<h2>The Results:<\/h2>\n<p>The demo boards were a pretty good success. The 4-H delegates really seemed to enjoy them, and got engaged right away with them, seemingly having great\nfun playing with them. I'm very excited to say that they'll be even more useful in the hands of the SEL K-12 outreach program. My colleague will be taking\nthem to play even more, and hopefully continue using them for a variety of great, hands-on exercises.<\/p>\n<p>I've been thinking more and more about some other activities that I might be able to &quot;construct&quot; in hardware to simplify the educational experience and\nmake them more accessible for others moving forward. One that I'm really looking towards is a simple power-system protection system. This where students\ncan actually make decisions to open breakers, shed load, and increase generation, all to respond to various electrical phenomena, and all actions being\naccounted for by an automated system capable of making real changes to the system to show what happens after each scenario.<\/p>\n<p>A little vague description, I know... But that's because I hope to actually <em>build<\/em> this thing, and show it off in the relatively (relatively) near\nfuture!<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"Teaching"}},{"@attributes":{"term":"demo"}},{"@attributes":{"term":"teaching"}},{"@attributes":{"term":"electricity"}},{"@attributes":{"term":"basic-circuits"}},{"@attributes":{"term":"youth"}},{"@attributes":{"term":"4-h"}}]},{"title":"Powering My Own Relaxation","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/powering-my-own-relaxation.html","rel":"alternate"}},"published":"2021-06-19T11:40:00-07:00","updated":"2021-06-19T11:40:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-06-19:\/powering-my-own-relaxation.html","summary":"<p class=\"first last\">I spend enough time in my arm-chair with my laptop that I thought it about time to power all my digital toys in the same spot!<\/p>\n","content":"<p>You know, I never thought I'd spend so much time in my arm-chair. But here we are.<\/p>\n<p>It's become pretty comfortable, recently, with all the new audio networking I've been working on (namely my audio network using VBAN - you can read\nmore about that <a class=\"reference external\" href=\"https:\/\/blog.stanleysolutionsnw.com\/spam-the-vban-for-non-stop-audio.html\">here<\/a>). Still, I'm not about to just sit back and listen\nwithout doing <em>something<\/em>. I mean, c'mon! I'm an insatiable tinkerer, after all. So, I spend a lot of time working away on my laptop (really starting\nto love <a class=\"reference external\" href=\"https:\/\/kde.org\/plasma-desktop\/\">KDE Plasma!<\/a>) and poking around at random bits and bytes.<\/p>\n<p>So I need a charger, but I get bored dragging around that plain ole' wall-wart! So I thought I could do one better. I've seen plenty of those neat\nlooking amo-can stereos, online, but that's not quite my style. If you know me, you know I prefer cabinet stereos. No, instead I thought I could put\nall of my power supply components in the can; and throw in a few of my own touches along the way. So, I decided I'd get to work.<\/p>\n<p>A little too much drooling and visitation of our favorite, least-favorite A-to-Z online marketplace, I had an assortment of nerdy-looking switch\nassemblies, voltage meters, and a beefy little multi-output DC power supply.<\/p>\n<img alt=\"The Controls of my &quot;Unique&quot; Power Can\" src=\"https:\/\/blog.stanleysolutionsnw.com\/203727287_161197975998707_4701277717982762523_n.jpg\" style=\"width: 600px;\" \/>\n<p>Now with all these neat little switches, I've got to do <strong>*something*<\/strong> with them, right?<\/p>\n<p>You bet!<\/p>\n<p>How about adding a little Linux computer, to the mix? Sure! I gave yet another old machine new life by taking an old\n<a class=\"reference external\" href=\"https:\/\/www.embeddedarm.com\/\">Technologic computer<\/a> and lobbing it into the case; mounted with Ethernet, serial terminal, and dual USB ports exposed\nfor easy access. My own sort of computer tower, you might say.<\/p>\n<img alt=\"Mounted a Linux computer in there too!\" src=\"https:\/\/blog.stanleysolutionsnw.com\/203630587_497812148003314_6021780960392169872_n.jpg\" style=\"width: 600px;\" \/>\n<p>But this thing's ultimately a power supply, right? So it's got to have <em>power<\/em>! And that, it does! Inside, I've mounted a little multi-output power supply\nwith 3 discrete taps; 24V, 12V, and 5V. I don't quite know what I'm going to do with 24V, yet, but the 5V and 12V are already serving my purposes to power\nthe little computer, a variety of lights, and perhaps a few more odds and ends as time progresses.<\/p>\n<img alt=\"The power!\" src=\"https:\/\/blog.stanleysolutionsnw.com\/203150896_2002088606597054_7741904286201403555_n.jpg\" style=\"width: 600px;\" \/>\n<p>What's more, however, is that I've also inserted several USB power hubs, and mounted a computer charger inside the can so that I can power my laptop, too!\nIn fact, as I sit her writing, I'm using the power supply right now!<\/p>\n<p>Now, I'll grant that like so many of my little tinkering projects, this one is nowhere near being complete, but I will say that <em>one<\/em> of the switches\ndoes function to control the two voltage meters mounted inside (one analog, the other digital).<\/p>\n<img alt=\"The final (but not finished) product!\" src=\"https:\/\/blog.stanleysolutionsnw.com\/202866327_175178427900304_1831963921642931273_n.jpg\" style=\"width: 600px;\" \/>\n<div class=\"section\" id=\"specs\">\n<h2>Specs:<\/h2>\n<p>So let's talk about what all's in this thing!<\/p>\n<div class=\"section\" id=\"power-supplies\">\n<h3>Power Supplies:<\/h3>\n<ul class=\"simple\">\n<li>2x 5V, 2.1A USB Chargers<\/li>\n<li>2x 5V, 2.4A USB Fast Chargers<\/li>\n<li>1x 19V, ~2A Computer Charger<\/li>\n<li>2x 120VAC Outlets (one internal, the other external)<\/li>\n<li>1x 24V DC Rail<\/li>\n<li>1x 12V DC Rail (powers the USB chargers, and a few other bits-and-bobs)<\/li>\n<li>1x 5V DC Rail (powers the Technologic Linux computer)<\/li>\n<\/ul>\n<\/div>\n<div class=\"section\" id=\"switches\">\n<h3>Switches:<\/h3>\n<ul class=\"simple\">\n<li>3x Safety Toggle Switches<\/li>\n<li>5x Illuminated Rocker Switches<\/li>\n<\/ul>\n<\/div>\n<div class=\"section\" id=\"meters\">\n<h3>Meters:<\/h3>\n<ul class=\"simple\">\n<li>1x Backlit AC Panel Meter, 0-150VAC<\/li>\n<li>1x Blue LED Digital Meter, 0-200VAC<\/li>\n<\/ul>\n<\/div>\n<div class=\"section\" id=\"computing-specs\">\n<h3>Computing Specs:<\/h3>\n<ul class=\"simple\">\n<li>1x Technologic 7800 ARM Computer; Runs Pre-built Debian 5.0, &quot;Lenny&quot; (yeah, it's old, okay?!)<\/li>\n<\/ul>\n<\/div>\n<\/div>\n<div class=\"section\" id=\"final-thoughts\">\n<h2>Final Thoughts<\/h2>\n<p>Another quick note about the project, I finally got to use my brand-new drill press for it! Woo-hoo!!! That was a lot of fun!<\/p>\n<p>Hopefully, when I get the little computer tied into some more important things, I'll have more to say on the matter, but first, I've\ngot to get it booting Debian... Then see if I can possibly get it to run newer images... Like, I dunno, something <em>NEW<\/em>?!?!<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"Home-Projects"}},{"@attributes":{"term":"ammo-can"}},{"@attributes":{"term":"power-supply"}},{"@attributes":{"term":"arm-chair"}},{"@attributes":{"term":"linux"}},{"@attributes":{"term":"charger"}}]},{"title":"Spam the VBAN for Non-Stop Audio","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/spam-the-vban-for-non-stop-audio.html","rel":"alternate"}},"published":"2021-02-15T20:22:00-08:00","updated":"2021-02-15T20:43:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-02-15:\/spam-the-vban-for-non-stop-audio.html","summary":"<p class=\"first last\">When things get sticky, leave it to Python to keep the wheels greased!<\/p>\n","content":"<p>In a <a class=\"reference external\" href=\"https:\/\/blog.stanleysolutionsnw.com\/networked-audio-using-vban-and-rpi.html\">recent article<\/a> I wrote about how I'd started to integrate more of my house's\naudio system with a networked audio protocol known as &quot;VBAN&quot;. I'd gotten some great\nuse out of the system, but I'd started running into some problems more recently...<\/p>\n<p>You see, for some reason, if I were streaming some audio to my Raspberry Pi, and the\nstream dropped into a lull (i.e. between songs, say) I'd often see some pretty nasty\nbuffer errors from Alsa. Now, I could've dug into it much deeper and tried to get to\nthe root of the problem in <cite>C<\/cite>, but I didn't really feel like it. Instead, I thought\nI'd just throw some Python at it! So after spending an intermittent afternoon\nreminding myself how the <cite>subprocess<\/cite> module works, and debugging my own madness, I\ngot a working script that I use as a <em>systemd<\/em> service.<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"c1\"># VBAN Receiver in Python<\/span>\n\n<span class=\"c1\"># Imports<\/span>\n<span class=\"kn\">import<\/span> <span class=\"nn\">subprocess<\/span>\n\n<span class=\"c1\"># Executor Function<\/span>\n<span class=\"k\">def<\/span> <span class=\"nf\">execute<\/span><span class=\"p\">(<\/span><span class=\"n\">cmd<\/span><span class=\"p\">):<\/span>\n    <span class=\"n\">popen<\/span> <span class=\"o\">=<\/span> <span class=\"n\">subprocess<\/span><span class=\"o\">.<\/span><span class=\"n\">Popen<\/span><span class=\"p\">(<\/span><span class=\"n\">cmd<\/span><span class=\"p\">,<\/span> <span class=\"n\">stdout<\/span><span class=\"o\">=<\/span><span class=\"n\">subprocess<\/span><span class=\"o\">.<\/span><span class=\"n\">PIPE<\/span><span class=\"p\">,<\/span>\n                             <span class=\"n\">stderr<\/span><span class=\"o\">=<\/span><span class=\"n\">subprocess<\/span><span class=\"o\">.<\/span><span class=\"n\">STDOUT<\/span><span class=\"p\">,<\/span> <span class=\"n\">universal_newlines<\/span><span class=\"o\">=<\/span><span class=\"kc\">True<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">while<\/span> <span class=\"kc\">True<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">for<\/span> <span class=\"n\">stdout_line<\/span> <span class=\"ow\">in<\/span> <span class=\"nb\">iter<\/span><span class=\"p\">(<\/span><span class=\"n\">popen<\/span><span class=\"o\">.<\/span><span class=\"n\">stdout<\/span><span class=\"o\">.<\/span><span class=\"n\">readline<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;&quot;<\/span><span class=\"p\">):<\/span>\n            <span class=\"k\">yield<\/span> <span class=\"n\">stdout_line<\/span><span class=\"p\">,<\/span> <span class=\"n\">popen<\/span>\n    <span class=\"n\">popen<\/span><span class=\"o\">.<\/span><span class=\"n\">stdout<\/span><span class=\"o\">.<\/span><span class=\"n\">close<\/span><span class=\"p\">()<\/span>\n    <span class=\"n\">return_code<\/span> <span class=\"o\">=<\/span> <span class=\"n\">popen<\/span><span class=\"o\">.<\/span><span class=\"n\">wait<\/span><span class=\"p\">()<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">return_code<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">raise<\/span> <span class=\"n\">subprocess<\/span><span class=\"o\">.<\/span><span class=\"n\">CalledProcessError<\/span><span class=\"p\">(<\/span><span class=\"n\">return_code<\/span><span class=\"p\">,<\/span> <span class=\"n\">cmd<\/span><span class=\"p\">)<\/span>\n\n<span class=\"c1\"># Define Attributes<\/span>\n<span class=\"n\">MASTER_PC_IP<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;&lt;your-ip-here&gt;&quot;<\/span>\n<span class=\"n\">LOG_FILE<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">&#39;\/var\/vban_log.log&#39;<\/span>\n\n<span class=\"c1\"># Define Command and Args<\/span>\n<span class=\"n\">EXECUTABLE<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">&quot;\/usr\/local\/bin\/vban_receptor&quot;<\/span>\n<span class=\"n\">ARGS<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"s2\">&quot;-i&quot;<\/span><span class=\"p\">,<\/span> <span class=\"n\">MASTER_PC_IP<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;-p&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;6980&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;-s&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;StereoPi&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;-d&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;front&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;-q&quot;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&quot;0&quot;<\/span><span class=\"p\">]<\/span>\n\n<span class=\"n\">CALL<\/span> <span class=\"o\">=<\/span> <span class=\"p\">[<\/span><span class=\"n\">EXECUTABLE<\/span><span class=\"p\">]<\/span>\n<span class=\"n\">CALL<\/span><span class=\"o\">.<\/span><span class=\"n\">extend<\/span><span class=\"p\">(<\/span><span class=\"n\">ARGS<\/span><span class=\"p\">)<\/span>\n\n<span class=\"c1\"># Call the System<\/span>\n<span class=\"k\">while<\/span> <span class=\"kc\">True<\/span><span class=\"p\">:<\/span>\n    <span class=\"k\">with<\/span> <span class=\"nb\">open<\/span><span class=\"p\">(<\/span><span class=\"n\">LOG_FILE<\/span><span class=\"p\">,<\/span> <span class=\"s1\">&#39;a&#39;<\/span><span class=\"p\">)<\/span> <span class=\"k\">as<\/span> <span class=\"n\">logFile<\/span><span class=\"p\">:<\/span>\n        <span class=\"k\">for<\/span> <span class=\"n\">output<\/span><span class=\"p\">,<\/span> <span class=\"n\">proc_handle<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">execute<\/span><span class=\"p\">(<\/span><span class=\"n\">CALL<\/span><span class=\"p\">):<\/span>\n            <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"n\">output<\/span><span class=\"p\">,<\/span> <span class=\"n\">end<\/span><span class=\"o\">=<\/span><span class=\"s1\">&#39;&#39;<\/span><span class=\"p\">)<\/span>\n            <span class=\"n\">logFile<\/span><span class=\"o\">.<\/span><span class=\"n\">write<\/span><span class=\"p\">(<\/span><span class=\"n\">output<\/span><span class=\"p\">)<\/span>\n            <span class=\"c1\"># Catch Error<\/span>\n            <span class=\"k\">if<\/span> <span class=\"s2\">&quot;Error: alsa_write:&quot;<\/span> <span class=\"ow\">in<\/span> <span class=\"n\">output<\/span><span class=\"p\">:<\/span>\n                <span class=\"nb\">print<\/span><span class=\"p\">(<\/span><span class=\"s2\">&quot;Failure... Python intervening!&quot;<\/span><span class=\"p\">)<\/span>\n                <span class=\"n\">proc_handle<\/span><span class=\"o\">.<\/span><span class=\"n\">kill<\/span><span class=\"p\">()<\/span>\n                <span class=\"n\">proc_handle<\/span><span class=\"o\">.<\/span><span class=\"n\">wait<\/span><span class=\"p\">()<\/span>\n                <span class=\"k\">break<\/span> <span class=\"c1\"># Continue While Loop - Call Again<\/span>\n<span class=\"c1\"># END<\/span>\n<\/pre><\/div>\n<p>With that magic little Python script, I basically kick VBAN in the butt every time\nthat Alsa decides to be unfriendly (which happens quite regularly) by killing the\nprocess, and then starting it right back up. With the magic of computers, this\nhappens very fast, and as I'd briefly mentioned earlier, it only seems to really\nplay into the &quot;mix&quot; in-between songs anyway. So after building the script, giving\nit a nice little test drive, and scrutinizing my Raspberry Pi; I thought it was\ntime to build it back into my simple little service.<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"c1\"># \/etc\/systemd\/system\/vbanstereorx.service<\/span>\n<span class=\"c1\"># vbanstereorx.service<\/span>\n<span class=\"c1\"># VBAN Receptor Stereo Service<\/span>\n\n<span class=\"k\">[Unit]<\/span>\n<span class=\"na\">Description<\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">VBAN Stereo Receptor<\/span>\n\n<span class=\"k\">[Service]<\/span>\n<span class=\"na\">Type<\/span><span class=\"o\">=<\/span><span class=\"s\">simple<\/span>\n<span class=\"na\">ExecStart<\/span><span class=\"o\">=<\/span><span class=\"s\">\/usr\/bin\/python3 \/home\/vbanner.py<\/span>\n<span class=\"na\">Restart<\/span><span class=\"o\">=<\/span><span class=\"s\">always<\/span>\n\n<span class=\"k\">[Install]<\/span>\n<span class=\"na\">WantedBy<\/span><span class=\"o\">=<\/span><span class=\"s\">multi-user.target<\/span>\n<\/pre><\/div>\n<p>I'm sure I'll be back to crack the hood back open on this one at some point, but\nfor now, I'm happy to stream my music back to my cabinet stereo with the power of\nLinux.<\/p>\n","category":[{"@attributes":{"term":"Raspberry Pi"}},{"@attributes":{"term":"vban"}},{"@attributes":{"term":"audio network"}},{"@attributes":{"term":"raspberry pi"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"linux"}}]},{"title":"Servers in the Basement...","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/jenkins-servers-in-the-basement.html","rel":"alternate"}},"published":"2021-02-14T19:23:00-08:00","updated":"2021-02-14T20:40:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-02-14:\/jenkins-servers-in-the-basement.html","summary":"<p class=\"first last\">Some people keep their creepy Christmas decorations in their basement. Others keep their continuous integration servers down there too...<\/p>\n","content":"<img alt=\"SEL Rugged Computers mounted and ready for work!\" src=\"https:\/\/blog.stanleysolutionsnw.com\/IMG_0851.jpg\" style=\"width: 600px;\" \/>\n<p>Whelp, I've gone and done it. I've mounted and installed one of my SEL computers\nand set it up for running Jenkins!<\/p>\n<p>This isn't going to be a very in-depth article, but I wanted to say that it's\ndone. The server is mounted with a brand new switch and surge protector (no UPS\nfor the moment, but perhaps to come in the <em>relatively<\/em> near future). They're\nnetworked back upstairs to my little IT closet, and Jenkins is waiting idly for\nme to push new code.<\/p>\n<p>I spent Saturday mounting the server, re-routing all the networking, and setting\nup my modem to provide access to the servers by way of a reverse proxy. Perhaps\nI'll document what that is and how it works, but that might be another article.<\/p>\n<p>Today I got to work standing up a few <cite>pytest<\/cite> projects for both <a class=\"reference external\" href=\"https:\/\/github.com\/engineerjoe440\/selprotopy\">selprotopy<\/a>\nand <a class=\"reference external\" href=\"https:\/\/github.com\/engineerjoe440\/pycev\">pycev<\/a>, what's exciting about this though, is the fact that they're set up\nnow so that they can access the private resources they need for testing, but\nthey can be kicked off by my commits and pushes to their repositories on GitHub.<\/p>\n<p>So... now I can really start cranking on that code, and Jenkins can do some of\nmy dirty work to start running the tests for me!<\/p>\n<img alt=\"Gotta love that SEL blue!\" src=\"https:\/\/blog.stanleysolutionsnw.com\/IMG_0852.jpg\" style=\"width: 600px;\" \/>\n<p>I want to restate that I'm very excited to be using some old SEL hardware and\ngiving it a second lease on life. These computers are rugged, industrial\nmachines; and I'm getting to put them to work making these projects solid. Not\nto mention that these projects are actually tailored to supporting SEL tech.<\/p>\n<p>Yep. It's nerdy.<\/p>\n<p>Yep. I'm still excited.<\/p>\n<p>Yep. You guessed it; I'll surely be giving more updates moving forward!<\/p>\n","category":[{"@attributes":{"term":"DevOps"}},{"@attributes":{"term":"sel"}},{"@attributes":{"term":"ci\/cd"}},{"@attributes":{"term":"jenkins"}},{"@attributes":{"term":"devops"}}]},{"title":"CI\/CD On Industrial Grade Hardware","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/jenkins-on-sel-industrial-hardware.html","rel":"alternate"}},"published":"2021-02-07T16:19:00-08:00","updated":"2021-02-07T16:19:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2021-02-07:\/jenkins-on-sel-industrial-hardware.html","summary":"<p class=\"first last\">Run DevOps CI\/CD pipelines on industrial equipment with no moving parts? Ok! Sign me up!!!<\/p>\n","content":"<p>Yes... I already have too many computers. But with that said, what's a few more?<\/p>\n<p>I know it was only about a month-and-a-half ago that I was writing about Jenkins\nrunning on a Raspberry Pi, but I outgrew that pretty quickly. In reality, I\nreally just started with it, and basically gave up; but hey! I learned a lot in\nthat time. So now, I'm upgrading!<\/p>\n<div class=\"section\" id=\"the-new-hardware\">\n<h2>The new hardware<\/h2>\n<p>I am something of a hardware graveyard. Old machines come to me to live out the\nend of their lives and, eventually, give up the ghost. I managed to get my hands\non some second-hand industrial computers, to do some bidding for me. Namely, I\npicked up some old SEL (Schweitzer Engineering Laboratories) SEL-1102 rugged\ncomputers. They're based on an old Intel x686 processor, and don't have anything\nspecial in the memory arena... but they're super solid machines.<\/p>\n<img alt=\"SEL Rugged Computers to Run my DevOps Pipeline\" src=\"https:\/\/blog.stanleysolutionsnw.com\/IMG_0849.jpg\" style=\"width: 600px;\" \/>\n<p>My comment about being a &quot;computer graveyard&quot; might still apply to these\ncomputers too, but well, they've got a lot more life left in them. You see,\nthese are ruggedized computers designed for installation into some of the most\nextreme environments around the world. Rated for harsh operating conditions,\nbuilt with no moving parts (that's right, a computer without fans), and a whole\nslew of serial ports (16 DB9 ports alone). SEL maintains a 10 worldwide warranty\ntoo; but I'll grant that this warranty is void because these devices were sold\nto me secondhand. I bring up this point, however because it really exemplifies\nthe commitment to quality that SEL brings to the table.<\/p>\n<p>Now, for those of you who know me well, you'll also know that I <em>work<\/em> for SEL.\nSo yeah, I do have some bias there; but I've also gotten to see (first-hand) the\nquality that we at SEL put into our products, so I'm very proud to have a few of\nthese machines running at home, and I'm very excited to put them into production.<\/p>\n<\/div>\n<div class=\"section\" id=\"the-new-work\">\n<h2>The new work<\/h2>\n<p>With these new servers, I'm excited to set them up running Debian (because, yeah,\nthey will do that - and very well, I might add) to support a Jenkins server. I\nplan to use that, and expose it as my primary integration system. With Jenkins\nrunning on these new machines, I'm going to set up a Pi Cluster to offload the\nactual pipeline work.<\/p>\n<p>But why?<\/p>\n<p>Well, I want the main Jenkins server to be just that... the main server. I want\nother machines to do all the &quot;dirty work&quot; for me.<\/p>\n<p>So, before you get too carried away with your thoughts; yes, that does mean more\ncomputers. I've already put in an order for a Raspberry Pi cluster, which I'm\nvery excited about; but that's another article for the near future.<\/p>\n<p>Part of this excitement also stems from my need to integrate with some SEL relays\nserially for testing with my SELProtoPy project. With all of those serial ports\non these computers, I'll be able to tie in to those relays quite nicely to allow\nsome really solid automated testing. Better yet, with all of that integration,\nI'll be able to do some really nice pipelined builds for testing SELProtoPy and\nPyCEV.<\/p>\n<\/div>\n<div class=\"section\" id=\"what-s-next\">\n<h2>What's next?<\/h2>\n<p>Well, next on my plate is to get these machines up and running on my network so\nthat I can access them remotely and start integrating with GitHub actions to\nfire off the builds and testing.<\/p>\n<p>I'm very excited to be putting some SEL equipment to work in my own personal\ndevelopment practices, so I'm sure I'll have some more updates as I go along!<\/p>\n<img alt=\"Putting some SEL Hardware to Work\" src=\"https:\/\/blog.stanleysolutionsnw.com\/IMG_0850.jpg\" style=\"width: 600px;\" \/>\n<\/div>\n","category":[{"@attributes":{"term":"DevOps"}},{"@attributes":{"term":"sel"}},{"@attributes":{"term":"industrial"}},{"@attributes":{"term":"rugged computer"}},{"@attributes":{"term":"ci\/cd"}},{"@attributes":{"term":"development"}},{"@attributes":{"term":"server"}}]},{"title":"GitLab, Jenkins, Python, and the Raspberry Pi!","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/gitlab-jenkins-and-the-rpi.html","rel":"alternate"}},"published":"2020-12-21T19:07:00-08:00","updated":"2021-01-04T21:07:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-12-21:\/gitlab-jenkins-and-the-rpi.html","summary":"<p class=\"first\">I'm finally getting around to setting up some CI\/CD systems for my self-hosted GitLab server... About Time!<\/p>\n<p class=\"last\">CI\/CD, Dev Ops, Pipelines, Workflows, Automated Deployment<\/p>\n","content":"<p>Think that's enough buzz words to catch the Google SEO engine's eye?<\/p>\n<p>Probably not, I know, but I'm not going to spend anymore time on it at the moment. See,\nI've got bigger items to tackle! Namely, getting Jenkins set up on a Raspberry Pi, as\nthe name of this article so implies.<\/p>\n<p>Good news for you; I've cut out the &quot;dirty-work&quot; through the magic of &quot;blog-posting.&quot;<\/p>\n<p>As part of the work I've been tackling for some of the other open source projects I'm\ndeveloping, I need to develop a local (on-premises) continuous integration solution to\neffectively slam my code with testing and verification. After all, what's great code\nwithout equally great tests? I need to have the system on-premises for a couple reasons;\nthe largest of which being the fact that I need access to custom hardware.<\/p>\n<blockquote>\nSo, why a Pi? A Pi 3-B no less?!<\/blockquote>\n<p>Well, that's quite simple; actually. It's the only spare computer I have at the moment.<\/p>\n<p>So now that I've thoroughly introduced you, to my reasoning, and the topic at hand;\nlet's get into it!<\/p>\n<div class=\"section\" id=\"installing-jenkins-on-the-pi\">\n<h2>Installing Jenkins on the Pi<\/h2>\n<p>I already have GitLab set up on an old x86 laptop running Ubuntu Server 20.04, so\nfor this article, I'm going to focus on setting up Jenkins on a Raspberry Pi, and\ngetting the basics of the workflow between Jenkins and GitLab running.<\/p>\n<ol class=\"arabic\">\n<li><p class=\"first\">Start with a fresh Pi (latest build of the RaspberryPiOS). I had a Pi sitting\naround with an older build of Raspbian, but that's several years old, and I\nreally just wanted to start fresh.<\/p>\n<\/li>\n<li><p class=\"first\">Update the Raspberry Pi. Well, in the spirit of starting fresh, might as well\nupdate the system!<\/p>\n<\/li>\n<li><p class=\"first\">Install Java with:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>apt-get<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>openjdk-11-jre\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">Verify Java Version with:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>java<span class=\"w\"> <\/span>--version\n<span class=\"go\">openjdk 11.0.9.1 2020-11-04<\/span>\n<span class=\"go\">OpenJDK Runtime Environment (build 11.0.9.1+1-post-Raspbian-1deb10u2)<\/span>\n<span class=\"go\">OpenJDK Server VM (build 11.0.9.1+1-post-Raspbian-1deb10u2, mixed mode)<\/span>\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">At this point, I took some time to get the Python system up to a state that\nwould be a bit more useful for me. So I installed <cite>pip3<\/cite>, and a number of\nPython packages. I suppose this could really be done at any point during this\nwhole process, but I felt like this was the most sensible time.<\/p>\n<\/li>\n<li><p class=\"first\">Download and add the Jenkins Key with:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>wget<span class=\"w\"> <\/span>-q<span class=\"w\"> <\/span>-O<span class=\"w\"> <\/span>-<span class=\"w\"> <\/span>https:\/\/pkg.jenkins.io\/debian\/jenkins.io.key<span class=\"w\"> <\/span><span class=\"p\">|<\/span><span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>apt-key<span class=\"w\"> <\/span>add<span class=\"w\"> <\/span>-\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">Open a new file:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>nano<span class=\"w\"> <\/span>\/etc\/apt\/sources.list.d\/jenkins.list\n<\/pre><\/div>\n<p>Then add the following line and save the file to add the Jenkins repository as\na source:<\/p>\n<div class=\"highlight\"><pre><span><\/span>deb https:\/\/pkg.jenkins.io\/debian binary\/\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">Next, another good <cite>sudo apt-get update<\/cite> is in order, followed by\n<cite>sudo apt-get install jenkins<\/cite><\/p>\n<\/li>\n<li><p class=\"first\">Using the command listed below, you can grab the initial admin password to get\nstarted:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>cat<span class=\"w\"> <\/span>\/var\/lib\/jenkins\/secrets\/initialAdminPassword\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">Now it's time to navigate to <cite>&lt;raspberry-pi-ip-address:8080<\/cite> and use that fancy\npassword to log in for the first time and start the setup wizard; or should I say\nbutler?<\/p>\n<\/li>\n<\/ol>\n<p>After the &quot;butler&quot; has completed, it's time to get started with setting up some CI\njobs.<\/p>\n<\/div>\n<div class=\"section\" id=\"preparing-a-simple-pytest-job-with-jenkins\">\n<h2>Preparing a Simple <cite>pytest<\/cite> Job with Jenkins<\/h2>\n<p>Now, I'll caution that I this portion doesn't cover any of the GitLab\/Jenkins\ninterfacing, maybe I'll get to writing that in another article... As part of the\nmaterial I'm skipping, I'm going to breeze right over the GitLab connection and\nrepository information. I'm going to focus, instead, on the build operations.<\/p>\n<ol class=\"arabic\">\n<li><p class=\"first\">With the new Jenkins server up and running, create a &quot;New Item,&quot; give it a\ndescriptive, memorable name, and set it as a &quot;Freestyle Project&quot;<\/p>\n<img alt=\"Create a new project in Jenkins for CI.\" src=\"https:\/\/blog.stanleysolutionsnw.com\/jenkins-new-config.png\" style=\"width: 800px;\" \/>\n<\/li>\n<li><p class=\"first\">After configuring the various other settings relevant to the project (repository,\nbuild-triggers, etc.) find the <em>&quot;Build&quot;<\/em> section and from the <em>&quot;Add build step&quot;<\/em>\nselect <em>&quot;Execute shell&quot;<\/em>.<\/p>\n<\/li>\n<li><p class=\"first\">In the new &quot;Command&quot; field of the &quot;Execute shell&quot; section, insert the commands\nnecessary to navigate to the appropriate subdirectory and run <cite>pytest<\/cite>. In my case,\nmy pytest &quot;test folder&quot; is located in the root directory, so I don't really need\nto change the working directory; I just go and run <cite>pytest<\/cite>. I do run a few other\ngeneric commands just to make sure that I've got a fair report of the build\nenvironment in case I need to go back and debug some things. So, here's a sample\nof what my configuration might look like.<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"go\">echo &quot;Current Directory&quot;<\/span>\n<span class=\"go\">pwd<\/span>\n<span class=\"go\">echo &quot;List Folder Structure&quot;<\/span>\n<span class=\"go\">ls -a .\/&lt;name-of-my-python-package-folder&gt;<\/span>\n<span class=\"go\">echo &quot;Run pytest&quot;<\/span>\n<span class=\"go\">pytest -v<\/span>\n<\/pre><\/div>\n<\/li>\n<\/ol>\n<\/div>\n<div class=\"section\" id=\"summary\">\n<h2>Summary<\/h2>\n<p>Well, that's a pretty rough intro into what I've been doing in Jenkins and GitLab.\nKinda rough, but I hope I'll be looking to add more in the near future.<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"jenkins"}},{"@attributes":{"term":"gitlab"}},{"@attributes":{"term":"raspberry pi"}},{"@attributes":{"term":"dev ops"}},{"@attributes":{"term":"git"}},{"@attributes":{"term":"ci\/cd"}}]},{"title":"Synchronizing Home Audio with the Raspberry Pi and VBAN","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/networked-audio-using-vban-and-rpi.html","rel":"alternate"}},"published":"2020-12-19T19:44:00-08:00","updated":"2020-12-19T19:44:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-12-19:\/networked-audio-using-vban-and-rpi.html","summary":"<p class=\"first last\">Ever wish your music was synchronized across your home, but you didn't have to sell your soul to Google to make it happen? Well, I did it with a Raspberry Pi and a nifty little open-source project!<\/p>\n","content":"<p>I'm something of an audiophile.<\/p>\n<p>Not completely, mind you, just partly.<\/p>\n<p>Why partly? Well, I love audio! Anymore, I don't really watch television, or movies; but I listen\nto audiobooks, podcasts, music, and more all the time! I like to listen everywhere. Home, work,\nwhile driving. EVERYWHERE. And I like to have my audio follow me around all the time. In other\nwords, I like having the same music in my study as what's in my living room, kitchen, bathroom,\nbedroom... EVERYWHERE.<\/p>\n<p>I'd say that I enjoy quality HiFi audio, but I'm not the pickiest out there. My kitchen, for\nexample has some pretty crude audio since I bought a pretty cheap stereo reciever and just\nsorta threw it together. Still, I enjoy it. It gives me audio when I'm cooking, cleaning, or just\nhanging out; and for me, that's the most important part.<\/p>\n<p>When I bought my home, I decided that I was going to install an &quot;Audio Bus&quot; to allow bi-directional\naudio transportation. I've since started on the project, but I've run out of speaker cable, and\nconsidering the crazy holiday spending, I've put on the brakes for the moment on my &quot;personal&quot;\nspending. That means that I've only been able to hard-wire a connection between the kitchen and\nmy study. But I <em>really<\/em> want more.<\/p>\n<div class=\"section\" id=\"my-solution\">\n<h2>My Solution<\/h2>\n<p>Enter <a class=\"reference external\" href=\"https:\/\/vb-audio.com\/Voicemeeter\/vban.htm\">VB Audio Network<\/a>.<\/p>\n<p>VBAN is a UDP-based network audio streaming protocol that was developed by the same individual\nwho created the <a class=\"reference external\" href=\"https:\/\/vb-audio.com\/Voicemeeter\/banana.htm\">Banana<\/a> audio mixer which I've come to love for my desktop audio mixing desires.\nIt's essentially a networked audio system, allowing streaming audio transmission and reception,\nand it's officially supported on Windows, Android, and iOS; and community-supported on Linux!<\/p>\n<p>That's right, it's available on Linux, and it's fully open-source as <a class=\"reference external\" href=\"https:\/\/github.com\/quiniouben\/vban\">vban<\/a> where it's\naccessible as a command-line-based reciever or transmitter.<\/p>\n<\/div>\n<div class=\"section\" id=\"my-process\">\n<h2>My Process<\/h2>\n<p>So... Late last night, after a long day of work, I decided I wasn't done programming, so I\nstarted in on this project. I took an old Raspberry Pi that I'd already mounted in my\nvintage Zenith cabinet stereo (that's a story for another day, and I'll have to tell you about\nit!) and I began preparing it. Somehow, I'd ruined the SD card, so I chucked the original SD and\nflashed a new one with the latest build of RaspberryOS. After I had the card flashed, I dropped\nan empty file titled 'ssh' in the <cite>\/boot<\/cite> directory so that it would enable SSH on initial boot.<\/p>\n<p>Now, with SSH set up, and my new SD card plugged into the Pi, I powered it up and connected\nremotely; updating the system with <cite>sudo apt-get update<\/cite> and <cite>sudo apt-get upgrade -y<\/cite> I had a\nfresh install ready for my experimentation!<\/p>\n<p>Now, for the benefit of new tinkerers interested in VBAN setup on a Pi (and a way to jog my own\nterrible memory sometime in the future), I'll try to illustrate the remaining steps as best I can\nremember. I had to play with some trial and error, so I'm reordering some steps to make it more\nclear and straight-forward, but I'll comment on some of the &quot;hardships&quot; I had after we get through\nall of the steps. My process was centered around the installation instructions on the <a class=\"reference external\" href=\"https:\/\/github.com\/quiniouben\/vban\">vban<\/a>\nproject README, but I did deviate a little to get it working.<\/p>\n<ol class=\"arabic\">\n<li><p class=\"first\">After preparing my &quot;fresh install,&quot; I went ahead and installed the Raspberry Pi and Alsa\nheaders so that they'd show up for the installation.<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>apt-get<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>raspberrypi-kernel-headers\n<span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>apt-get<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>libasound-dev\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">With my new headers installed, I was ready to clone the git repo and build it. I guess that at\nthis point, I <em>should<\/em> mention that I'd previously installed git, but I suppose you might have\nbeen able to infer that on your own. Anyway, I cloned the repo to my home folder:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>git<span class=\"w\"> <\/span>clone<span class=\"w\"> <\/span>https:\/\/github.com\/quiniouben\/vban.git\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">Now, I moved myself into that new directory with: <cite>cd vban<\/cite><\/p>\n<\/li>\n<li><p class=\"first\">Before I could build the <cite>vban<\/cite> system by following the instructions on the GitHub repo, I\nneeded to install the <cite>autoconf<\/cite> tools so that the autoconfiguration scripts would function.<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>apt-get<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>autotools-dev\n<span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>sudo<span class=\"w\"> <\/span>apt-get<span class=\"w\"> <\/span>install<span class=\"w\"> <\/span>autoconf\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">I then went ahead and followed the installation instructions from the <cite>vban<\/cite> project README,\nsubstituting a few extra arguments to satisfy the system requirements (Alsa only, no Pulse\/Jack).<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>.\/autogen.sh\n<span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>.\/configure<span class=\"w\"> <\/span>--enable-alsa<span class=\"w\"> <\/span>--disable-pulseaudio<span class=\"w\"> <\/span>--disable-jack<span class=\"w\">      <\/span><span class=\"c1\"># Only using Alsa<\/span>\n<span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>make\n<span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>make<span class=\"w\"> <\/span>install\n<\/pre><\/div>\n<\/li>\n<li><p class=\"first\">After I got all of that working, I was able to set up my Windows desktop running Banana Mixer\nto stream to my Raspberry Pi using VBAN. I configured VBAN on my desktop to stream as such:<\/p>\n<img alt=\"Windows VBAN server configuration.\" src=\"https:\/\/blog.stanleysolutionsnw.com\/vban_desktop.png\" style=\"width: 800px;\" \/>\n<\/li>\n<li><p class=\"first\">Then, I could simply issue the following command in my Raspberry Pi to start listening!<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>vban_receptor<span class=\"w\"> <\/span>-i<span class=\"w\"> <\/span>&lt;my-desktop-ip&gt;<span class=\"w\"> <\/span>-p<span class=\"w\"> <\/span><span class=\"m\">6980<\/span><span class=\"w\"> <\/span>-s<span class=\"w\"> <\/span>StereoPi<span class=\"w\"> <\/span>-d<span class=\"w\"> <\/span>front<span class=\"w\"> <\/span>-q<span class=\"w\"> <\/span><span class=\"m\">0<\/span>\n<\/pre><\/div>\n<p>This meant that I'd listen for a stream of name &quot;StereoPi&quot; from my desktop with it's specific\nIP address on port 6980. I'd then stream that audio to the &quot;front&quot; output in my Alsa config,\nand (since this is a hard-wired Ethernet connection) I set the highest quality to reduce delay.<\/p>\n<\/li>\n<\/ol>\n<p>Viola! I've now begun sharing audio between my desktop and the Raspberry Pi!<\/p>\n<\/div>\n<div class=\"section\" id=\"another-dilemma\">\n<h2>Another Dilemma<\/h2>\n<p>Ah, but we weren't done yet! See, that command is blocking, meaning that if I close my SSH\nconnection, say good bye to audio! Drat!<\/p>\n<p>I decided that to fix this, I'd write a little systemd service, and keep it disabled, so that\nI could start and stop it easily enough (so if I want to use other audio services, they won't\nclash too terribly).<\/p>\n<p>So I wrote this:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"c1\"># vbanstereorx.service<\/span>\n<span class=\"c1\"># VBAN Receptor Stereo Service<\/span>\n\n<span class=\"k\">[Unit]<\/span>\n<span class=\"na\">Description<\/span><span class=\"o\">=<\/span><span class=\"w\"> <\/span><span class=\"s\">VBAN Stereo Receptor<\/span>\n\n<span class=\"k\">[Service]<\/span>\n<span class=\"na\">Type<\/span><span class=\"o\">=<\/span><span class=\"s\">simple<\/span>\n<span class=\"na\">ExecStart<\/span><span class=\"o\">=<\/span><span class=\"s\">\/usr\/local\/bin\/vban_receptor  -i &lt;my-desktop-ip&gt; -p 6980 -s StereoPi -d front -q 0<\/span>\n\n<span class=\"k\">[Install]<\/span>\n<span class=\"na\">WantedBy<\/span><span class=\"o\">=<\/span><span class=\"s\">multi-user.target<\/span>\n<\/pre><\/div>\n<p>Then simply &quot;installed&quot; it with the following command:<\/p>\n<div class=\"highlight\"><pre><span><\/span><span class=\"gp\">$<\/span>&gt;<span class=\"w\"> <\/span>cp<span class=\"w\"> <\/span>vbanstereorx.service<span class=\"w\"> <\/span>\/etc\/systemd\/system\/vbanstereorx.service\n<\/pre><\/div>\n<p>Now, I can just start or stop the reciever by issuing <cite>sudo systemctl start vbanstereorx<\/cite> or\n<cite>sudo systemctl stop vbanstereeorx<\/cite>, respectively!<\/p>\n<div class=\"section\" id=\"the-other-challenges\">\n<h3>The Other Challenges<\/h3>\n<p>I'm afraid it all wasn't easy-peasy, and setup smooth; there were still a few hiccups.<\/p>\n<p>Right now, the biggest thing is that I believe Alsa is on its way out for the Raspberry\nPi, so I have a feeling that I'm gonna need to reform this at some point, but perhaps\nthat's just for the Pi4 for the time being? I'm not really sure... Anyone who might know,\n<a class=\"reference external\" href=\"mailto:engineerjoe440&#64;yahoo.com\">hit me up<\/a>.<\/p>\n<p>The other issue that I ran into during installation was the use of autoconfig scripts and\nRPi\/Alsa headers. Since they weren't <em>explicitly<\/em> called out as installation requisites,\nI bumped into them, and had to take to Googling my way out of a corner. Wasn't bad, just\nslowed me down.<\/p>\n<\/div>\n<\/div>\n<div class=\"section\" id=\"what-s-next\">\n<h2>What's next?<\/h2>\n<p>I'll have to save that for another post; another day. Just keep your eyes peeled!<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"Raspberry Pi"}},{"@attributes":{"term":"raspberrypi"}},{"@attributes":{"term":"vban"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"networking"}},{"@attributes":{"term":"music"}},{"@attributes":{"term":"home-automation"}}]},{"title":"pycev - A Python CEV Reader","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/pycev-a-python-cev-reader.html","rel":"alternate"}},"published":"2020-12-12T10:59:00-08:00","updated":"2020-12-12T10:59:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-12-12:\/pycev-a-python-cev-reader.html","summary":"<p class=\"first last\">Another new project? Well, why not? This time, we'll be tackling reading CEV files from SEL in Python.<\/p>\n","content":"<img alt=\"Introducing: pycev!\" src=\"https:\/\/raw.githubusercontent.com\/engineerjoe440\/pycev\/main\/logo\/pycev.png\" style=\"width: 600px;\" \/>\n<p>Wait...<\/p>\n<p>Another new project?<\/p>\n<p>Yes. That's right. I'm starting another new project. But hey! There's a lot of framework\nthat needs to be introduced before I can start doing all the cool high-level stuff that we\nall want to see and use. I mean, by now you should understand that I'm all about getting\nthe framework right. If you're still not sure, go read my rant about getting the framework\nright... <a class=\"reference external\" href=\"https:\/\/blog.stanleysolutionsnw.com\/write-framework-once.html\">This is my rant on getting framework right.<\/a><\/p>\n<div class=\"section\" id=\"goal\">\n<h2>Goal:<\/h2>\n<p>So what's the plan here, anyway?<\/p>\n<p>Well, this project, <cite>pycev<\/cite> (I've almost considered that it should be pronounced &quot;pie-safe&quot;, but\nthat's not how it looks, so we'll let that stew a while longer) will be a package for reading and\ninterpreting SEL Compressed EVent records. They're a proprietary (but open) format in which SEL\nprotective relays collect event information and &quot;compress&quot; it into a format that's easily read\nby machines (computers).<\/p>\n<p>There's already a handful of projects out in the wild for reading COMTRADE records; which, if\nyou're unfamiliar are &quot;<em>Common Format for Transient Data Exchange<\/em>&quot; files, and are supported by\nmany SEL relays in addition to a much broader number of other vendor devices. Trouble is, not\neveryone uses COMTRADE, and comparatively, CEV files are a little simpler, and (in my opinion)\nmore straight-forward and robust. Perhaps the best Python project for reading COMTRADE files\nis <a class=\"reference external\" href=\"https:\/\/github.com\/dparrini\/python-comtrade\">Python Comtrade<\/a>. That project shows great maturity and value. It also sees regular updates\nand bugfixes as needed.<\/p>\n<p>Since it's such a well respected and mature project, I'd like to take it as inspiration for\n<cite>pycev<\/cite> and use it to help me realize the best API for the package so that the two libraries\ncould (potentially) be used interchangeably for various projects.<\/p>\n<\/div>\n<div class=\"section\" id=\"what-s-first\">\n<h2>What's First?<\/h2>\n<p>Well, I guess starting the package development is first!<\/p>\n<p>I've already carved out a repository, and I've got something of a skeleton package put together.\nI think the first step will be getting enough working that I can upload it to PyPI to reserve\nthe namespace. Then full development will need to come. There's a good handful of things that\nneed to be tackled:<\/p>\n<ul class=\"simple\">\n<li>Upload Project to PyPI<\/li>\n<li>Develop Core Functionality and Match API to that of &quot;Python-Comtrade&quot;<\/li>\n<li>Develop Automated Test Suite with Local Server and Various Existing CEV Files<\/li>\n<\/ul>\n<p>So that's the sort of roadmap I see before me. Now, the time-frame is still way up in the air;\nso who knows whe this all will <em>actually<\/em> happen. But here's hoping!<\/p>\n<p>If you're interested in checking in on the project, and would like to jump in and contribute,\nhave a little look at the <a class=\"reference external\" href=\"https:\/\/github.com\/engineerjoe440\/pycev\">repository<\/a>, and feel free to open an <a class=\"reference external\" href=\"https:\/\/github.com\/engineerjoe440\/pycev\/issues\">issue<\/a> to start a conversation!<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"sel"}},{"@attributes":{"term":"cev"}},{"@attributes":{"term":"event"}},{"@attributes":{"term":"record"}},{"@attributes":{"term":"files"}},{"@attributes":{"term":"power system"}},{"@attributes":{"term":"analysis"}}]},{"title":"Sustainability in the News...","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/sustainability-in-the-news-december-2020.html","rel":"alternate"}},"published":"2020-12-02T19:45:00-08:00","updated":"2020-12-02T19:45:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-12-02:\/sustainability-in-the-news-december-2020.html","summary":"<p class=\"first last\">Recycled concrete and herbicide detecting transistors? Bring on the new sustainability-focused tech!<\/p>\n","content":"<p>I want to get in the habit of writing up some brief articles more regularly to talk about\nsome of the more interesting articles and news that I've heard recently in the realm of\nsustainability. Now, I'll grant, that I typically do my own sort of exploration(s) into\ndifferent sustainability projects and research, but I want to keep these articles focused\non what I hear is happening outside of my little pocket on Earth.<\/p>\n<p>Anyway... Here's what I've heard about new sustainability topics!<\/p>\n<div class=\"section\" id=\"recycled-concrete\">\n<h2>Recycled Concrete<\/h2>\n<p>Long has old concrete been a challenge for construction and demolition workers. Long has\nthe question arisen as: &quot;So, now that we've torn it down; what do we do with it?&quot; Well,\nit turns out there might just be a solution! I was introduced to this brief article which\ncame from a scientific paper describing some research findings. You can look at the\n<a class=\"reference external\" href=\"https:\/\/www.sciencedaily.com\/releases\/2020\/11\/201130150358.htm\">recycled concrete article<\/a> and see for yourself. For me, this is most exciting since it\nmeans that <em>enormous<\/em> amounts of waste concrete can be kept out of the landfill! Not to\nmention that fewer resources may need to be used to maintain high-wear concrete surfaces\nsuch as sidewalks for longer periods of time.<\/p>\n<\/div>\n<div class=\"section\" id=\"transistors-detecting-herbicides\">\n<h2>Transistors Detecting Herbicides<\/h2>\n<p>Okay, now this one is really interesting; not that concrete <em>isn't<\/em> interesting, it's just\nthat this one seems to fit into my area of expertise a little more (you know, electricity).<\/p>\n<p>This article on <a class=\"reference external\" href=\"https:\/\/www.sciencedaily.com\/releases\/2020\/12\/201201124142.htm\">herbicide sensing transistors<\/a> describes a scientific article that details\na new technology that's still being researched that would allow specific transistors to be\nimmersed in water or wastewater to detect certain levels of herbicides. Why is this\nimportant? Well, research is continuing to show that significant levels of herbicides can\nbe dangerous to marine life including everything from flora to fauna. Both plants and\nanimals can suffer from high levels of herbicides. With this emerging tech, we could see\nimproved water and wastewater management such that industrial facilities and municipalities\nmay be able to monitor their output, and make more informed decisions to adapt their systems.<\/p>\n<p>Well, that's it for this go-around, but hopefully I can keep talking about some new\nand emerging tech!<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"Sustainability"}},{"@attributes":{"term":"concrete"}},{"@attributes":{"term":"transistor"}},{"@attributes":{"term":"recycle"}},{"@attributes":{"term":"herbicide"}},{"@attributes":{"term":"sustainability"}}]},{"title":"Smart Christmas Trains for a Smart Home?","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/esp32-controlling-lionel-trains.html","rel":"alternate"}},"published":"2020-11-22T19:32:00-08:00","updated":"2020-11-22T19:32:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-11-22:\/esp32-controlling-lionel-trains.html","summary":"<p class=\"first last\">Finally, with my own home, I think I can return to my goal of having a Lionel train surround my Christmas tree, but perhaps I need to consider how I'm going to automate it...<\/p>\n","content":"<p>For any railfans, a train surrounding a Christmas tree is something of a given. But I recently\ncame up with a conundrum...<\/p>\n<p>See, my house is becoming more and more of a &quot;smart home,&quot; and I'd like the Christmas holiday\nto be much of the same. Now, that's pretty easy when it comes to the lights. Throw a smart-plug\nrunning <a class=\"reference external\" href=\"https:\/\/tasmota.github.io\/docs\/\">Tasmota<\/a> firmware (a free and open source alternative to the proprietary solutions) at\nthose lights, and let the magic commence. But for toy trains around the tree? That's a bit of a\ndifferent story.<\/p>\n<div class=\"section\" id=\"the-problem\">\n<h2>The problem:<\/h2>\n<p>So, why couldn't I just throw another smart plug at my toy trains?<\/p>\n<p>Well, I guess I <em>could<\/em>, but I don't really think that's the greatest idea. Reason being that\nturning the trains on and off in a binary fashion (all or nothing) might not be great on them,\nand wouldn't be the most pleasant sound either. I guess I could look at some of the solutions\nthat Lionel markets like their <a class=\"reference external\" href=\"http:\/\/www.lionel.com\/brands\/legacy\/\">Legacy<\/a> or TMCC options, but that would mean a lot of new\npurchases, research, and maybe some other annoyances that I'm just not too excited to deal with.\nSo, needless to say, that option's off the list too. Perhaps one of Lionel's new bluetooth\noptions? I guess, but that's kind of boring, don't you think? And to boot, if I were to follow\nthat route, I'd certainly need to buy a new locomotive, and I'd likely need to hack the remote.\nAnother option that I'm just not thrilled about.<\/p>\n<p>Hmm... well, what's left? My pickyness hasn't left me with many options, but I think there's\nstill <em>got to be something<\/em>.<\/p>\n<\/div>\n<div class=\"section\" id=\"think-think-think\">\n<h2>Think. Think. Think.<\/h2>\n<p>I spent some time thinking about my alternatives today, and well, let me just walk you through\nthat.<\/p>\n<p>I started out thinking about the toy train transformer itself. That's all it really is, a\ntransformer; well, <em>variac<\/em> specifically. A variac is just a variable transformer, essentially\na graphine brush that moves across the windings on the secondary side of a transformer. This\nvarying motion provides the variable &quot;tap&quot; on the transformer, and allows it to acheive a\nvoltage that can be varied and controlled to change the speed of the connected trains.<\/p>\n<p>It's pretty simple really, Lionel and others pioneered this practice for the model and toy scene\nnearly a century ago. Trouble is, the transformers (in my case, a Lionel ZW transformer) aren't\nexactly built for modification, and like I mentioned earlier, I don't exactly idolize the thought\nof tearing apart my toy train controls just to make them &quot;smarter.&quot;<\/p>\n<img alt=\"Here's what a variac looks like...\" src=\"https:\/\/images-na.ssl-images-amazon.com\/images\/I\/91SL6j6kkNL._AC_SX425_.jpg\" style=\"width: 350px;\" \/>\n<p>I spent some time looking around for a cheap (I'd like to stay under $100 since I need to buy gifts,\ntoo!) variac that is digitally controlled. No dice.<\/p>\n<p>I guess I could buy a big variac and hook up a motor and...<\/p>\n<p>No.<\/p>\n<p>Not gonna fly.<\/p>\n<p>Hmm... well, what else?<\/p>\n<p>Another option is a rheostat. For those of us who either forgot or skipped electrical machines in\ncollege; a rheostat is basically a variable resistor. Similar to a variac, rheostats are typically\nmade from some form of wound wire that has a varying resistance as the user employs a sweeper to\nmove from one end to the other. That's really the big difference, and the reason it's not just\ncalled a variable resistor, or even a potentiometer. Rheostats are basically &quot;variable resistors\non steroids,&quot; built for high-energy systems.<\/p>\n<img alt=\"And this is a rheostat...\" src=\"https:\/\/cdn.images.fecom-media.com\/A49116.jpg\" style=\"width: 350px;\" \/>\n<p>So... yours truly started poking around online looking (once again) for a cheap electrical\ndevice beefy enough to run toy trains, but not so big that it would break the bank. I bummed around\nthrough Amazon with no luck, but after a quick search on eBay, I found something pretty enticing...<\/p>\n<p>Guess which toy train company just happened to make rheostats way back in the day?<\/p>\n<p>That's right! Lionel rheostats!<\/p>\n<img alt=\"A Lionel rheostat!\" src=\"https:\/\/image.invaluable.com\/housePhotos\/SeymourAuctions\/95\/563895\/H4246-L72135272.jpg\" style=\"width: 450px;\" \/>\n<\/div>\n<div class=\"section\" id=\"a-solution\">\n<h2>A solution?<\/h2>\n<p>So, is that it? Just like that? Problem solved?<\/p>\n<p>Well, yes; but also, no.<\/p>\n<p>Lionel rheostats seem to be running about $10 and about as much in shipping on eBay at the time of\nwriting, so that's really good news. Not to mention the fact that they were built <em>specifically\nfor toy trains<\/em> (score!).<\/p>\n<p>But what about the fact that it's mechanical, not digital?<\/p>\n<p>I knew you'd ask that...<\/p>\n<p>Well, here, the big difference is the form factor. These rheostats are significantly easier to\ninterface with. Since they're linear, a single piece of all-thread-rod and a little stepper motor\ncould quite easily do just what I need. I could connect a little stepper motor to an ESP32\/ESP8266\nand hook that up to a Lionel rheostat via all-thread and a moving nut with a sweeper attached.<\/p>\n<p>Easy-peasy! Well, sort-of.<\/p>\n<\/div>\n<div class=\"section\" id=\"what-s-next\">\n<h2>What's next?<\/h2>\n<p>Well, there's still quite a bit left on this one, and a good chance I won't actually do anything\nabout it this Christmas. Still, it's an exciting idea, and I'm definitely going to pursue it!<\/p>\n<p>That's all for now, but stay tuned!<\/p>\n<p>Who knows, maybe I'll even throw a whistle control on there!!!<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"ESP32"}},{"@attributes":{"term":"esp32"}},{"@attributes":{"term":"iot"}},{"@attributes":{"term":"smart-home"}},{"@attributes":{"term":"automation"}},{"@attributes":{"term":"wifi"}},{"@attributes":{"term":"lionel"}},{"@attributes":{"term":"vintage"}},{"@attributes":{"term":"variac"}}]},{"title":"Reading Data with selprotopy","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/reading-data-with-selprotopy.html","rel":"alternate"}},"published":"2020-11-22T19:02:00-08:00","updated":"2020-11-22T19:02:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-11-22:\/reading-data-with-selprotopy.html","summary":"<p class=\"first last\">Finally reading some data from SEL relays using Python! Now to get the controls working...<\/p>\n","content":"<p>Still deep in the process of getting a fully functional SEL protocol binding suite in Python, but,\nhey! At least I can write a little update on what's been going on!<\/p>\n<p>(if you haven't read my article on what <cite>selprotopy<\/cite> is, take a look <a class=\"reference external\" href=\"https:\/\/blog.stanleysolutionsnw.com\/sel-protocol-coming-to-python.html\">here<\/a>)<\/p>\n<p>In the past month of so, I've been able to really &quot;whack out&quot; some reasonable functionality. In\nfact, I've been able to poll an SEL-351 for both digital and analog data. For those of you who\nare a little familiar with SEL protocol, that means that I've been able to create a parser for\nthe relay definition block, and the various fast-meter blocks in addition to the DNA definition.\nTo boot, I've even tested (albeit breifly) on an SEL-751 and saw pretty promissing results.<\/p>\n<p>That's all pretty good, but the eventual goal (well, <em>my<\/em> eventual goal) is to be able to poll\nregularly and send commands\/controls as needed. I'd also like to be able to read CEV reports\n(more on that in the future) and perhaps the relay's SER (Sequential Event Recorder). So, is any\nof the control functionality working yet? Not really...<\/p>\n<p>I've gotten to the point where the commands <em>should<\/em> be configured and sent correctly to the\nrelay, but no dice.<\/p>\n<p>Somewhere along the lines, I've clearly &quot;bugged&quot; something up. So now, it's really just a matter\nof doing some additional debugging. Hmm... will need to get started on that. Trouble is, I've got\nlots of other fun projects to work on too!!!<\/p>\n<div class=\"section\" id=\"c-mon-joe-wrap-this-thing-up\">\n<h2>C'mon, Joe; wrap this thing up...<\/h2>\n<p>Okay, so I'm rambling; at this point, I'm pretty excited to say that I've got some polling working\nwith <cite>selprotopy<\/cite>, but there's clearly some more to work on. I'm hoping that I can get commands\nworking here pretty soon, and then I've got a handful of options as the next step.<\/p>\n<div class=\"section\" id=\"i-could\">\n<h3>I could:<\/h3>\n<ul class=\"simple\">\n<li>create a system to read the SER<\/li>\n<li>get the CEV reading figured out<\/li>\n<li>start testing on a variety of SEL relays (but this would require testing a FOSS project at SEL,\nso I'm still not sure about this one)<\/li>\n<li>get an automated test suite built on my local GitLab instance<\/li>\n<\/ul>\n<p>Clearly some more work coming, so stay tuned!<\/p>\n<\/div>\n<\/div>\n","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"protocols"}},{"@attributes":{"term":"sel"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"communications"}},{"@attributes":{"term":"metering"}}]},{"title":"Introducing selprotopy","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/sel-protocol-coming-to-python.html","rel":"alternate"}},"published":"2020-09-20T11:07:00-07:00","updated":"2021-02-14T16:00:00-08:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-09-20:\/sel-protocol-coming-to-python.html","summary":"<p class=\"first last\">The SEL Protocol binding suite for Python is finally coming...<\/p>\n","content":"<p>SEL Protocol is finally coming for Python. That's right, you've heard correctly; it's coming.<\/p>\n<img alt=\"The new... `selprotopy`!\" src=\"https:\/\/raw.githubusercontent.com\/engineerjoe440\/sel-proto-py\/master\/logo\/selprotopy.png\" style=\"width: 350px;\" \/>\n<p>But, what is SEL Protocol anyway?<\/p>\n<p>Well, it's a communications protocol, or standard, that was developed by <a class=\"reference external\" href=\"https:\/\/selinc.com\/\">SEL<\/a> to support\nfast data communications between protective electric relays (the devices that monitor the\npower grid for faults) and communications processors. It was developed in the early '90s to\nhelp improve communication support of devices to allow users to monitor protective relays\nfrom a distance, and to perform control operations without being present.<\/p>\n<p>Anyway...<\/p>\n<p>I've begun writing <a class=\"reference external\" href=\"https:\/\/github.com\/engineerjoe440\/selprotopy\">selprotopy<\/a> which will be a protocol driver supporting SEL protocol in\nPython. This means that users will be able to integrate solutions with SEL relays using\nPython!<\/p>\n<p>Obviously, there's a lot to come, so this is not much more than an early announcement.\nIf you're interested in contributing, feel free to drop me a message! I'd love to interact!<\/p>\n","category":[{"@attributes":{"term":"Python"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"protocols"}},{"@attributes":{"term":"sel"}}]},{"title":"Write a Good Framework - ONCE.","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/write-framework-once.html","rel":"alternate"}},"published":"2020-09-20T10:26:00-07:00","updated":"2020-09-20T10:26:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-09-20:\/write-framework-once.html","summary":"<p class=\"first last\">We all want that next great application; NOW. And we KNOW that we can just hash out this great new thing. But where does that leave us the next time we want to do the same sort of thing?<\/p>\n","content":"<p>C'mon, every developer has had that epiphany moment:<\/p>\n<blockquote>\n&quot;I know, I can just write this thing now, and I'll have exactly what I need, and I'll\nnever need to touch it again; I'll never need to add more to it, and it will never\nchange.&quot;<\/blockquote>\n<p>Yeah. Right.<\/p>\n<p>We wish it worked that way, but let's be honest. Projects develop, add scope, change, and\nsometimes, they morph into something new entirely. Actually, that's often where great code\ncomes from. The best projects are the butterflies of the development world. They start as\na simple little caterpillar, but at some point along the line, they change into something\nfar more elegant and beautiful.<\/p>\n<p>That's why I'm writing this plea to developers out there in the &quot;real world.&quot; We know that\nyou could just write this little thing, but isn't it nice to get the <em>framework<\/em> right the\nfirst time so that when you need to revisit that code, it'll be so much easier. Or, better\nyet, isn't it better to prepare the project for the &quot;<em>next guy<\/em>&quot; who comes along to make\nyour framework into something incredible.<\/p>\n<p>I guess I've devolved into rambling, as usual, but my point is this: when you spend the time\nto develop the framework <em>the right way<\/em> the first time, you and all of your colleagues will\nthank you. It will make things just <strong>that much easier<\/strong>.<\/p>\n<p>I've had a handful of experiences with this myself. I started on a couple projects at work\nwhere I developed something that I <em>thought<\/em> was a great system, and didn't really require\na superior framework. This inevitably saved time in the short-term, but it bit me later.<\/p>\n<p>As it turned out, I recently realized that I was re-writing the same code over, and over,\nand over again every time I had to touch the code. In the end, as it seems, I didn't spend\nenough time writing the framework to begin, and that hurt me.<\/p>\n<div class=\"section\" id=\"here-s-the-takeaway\">\n<h2>Here's the Takeaway:<\/h2>\n<p>Long story short, we all love just &quot;pulling in&quot; a library that does the &quot;dirty work&quot; for us.\nI think it's time that we focus on getting those libraries right from the onset, so we don't\nneed to keep re-doing our own work, or the work of others.<\/p>\n<p>What do you think?<\/p>\n<p>I'd say drop me a note in the comments, but I haven't quite gotten that set up yet. So, for\nnow, just keep fighting the good fight, and <em>write those libraries!<\/em><\/p>\n<\/div>\n","category":[{"@attributes":{"term":"Development"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"iec-61131"}},{"@attributes":{"term":"development"}}]},{"title":"Wildfire Prevention with Sound","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/detecting-fires-with-sound.html","rel":"alternate"}},"published":"2020-09-10T20:39:00-07:00","updated":"2020-09-10T20:39:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-09-10:\/detecting-fires-with-sound.html","summary":"<p class=\"first last\">&quot;Where there's smoke, there's fire...&quot; Right? What about if sound were a part of it too?<\/p>\n","content":"<p>I'm very pleased to say that I'm helping to support some really exciting research at\nthe <a class=\"reference external\" href=\"https:\/\/uidaho.edu\">University of Idaho<\/a> this year.<\/p>\n<p>A few years ago, a couple of my classmates worked on\na truly exciting project that utilized some relatively new sensory technology in conjunction\nwith some wireless network technology (Zigbee radios) to communicate infrasound (below the\naudible range for humans) signatures back for scientists to analyze. These signatures will\ninform scientists and firefighters, alike, when (and where) a wildfire has started!<\/p>\n<p>Now, this was all well and good, but the <a class=\"reference external\" href=\"http:\/\/mindworks.shoutwiki.com\/wiki\/Infrasound_in_wildfire\">project<\/a> lost funding. Yep. Just like that.<\/p>\n<p>That's why I'm so excited to be getting involved. We've already got a team of four fantastic\nengineers. A mechanical engineer, two electrical engineers, and a computer scientist. With\nluck the group will be able to start some fantastic research that could eventually lead to a\ntruly exciting product that may help wildland firefighters around the country (and around the\nworld) find fires <em>faster<\/em> and with greater accuracy.<\/p>\n<p>The group is just getting started, but I'm very excited to see where things go.<\/p>\n<div class=\"section\" id=\"the-techy-side\">\n<h2>The Techy Side<\/h2>\n<p>So how is this supposed to work anyway?<\/p>\n<p>Well, we've all heard stories about how animals can sense an impending natural disaster, and\nin fact, that's quite true with wildfire. Most wild game (and many domesticated species too)\nhave much more astute hearing sense. In fact, they can hear the low-frequency sounds that\nwe're unable to hear.<\/p>\n<p>The sensors we're hoping to utilize will allow our digital system to detect the low-frequency\nrumble of wild fire, then with digital signal processing, we'll aim to identify audio\nsignatures unique to wildfire. With this, we'll be able to detect fire!<\/p>\n<p>Further, the goal will be to create a mesh-network of devices covering the forest floor in\nareas such that devices will be able to communicate amongst themselves to precisely locate\nthe fire, and to report it to the authorities quickly!<\/p>\n<\/div>\n","category":[{"@attributes":{"term":"capstone"}},{"@attributes":{"term":"sound"}},{"@attributes":{"term":"audio"}},{"@attributes":{"term":"wildfire"}},{"@attributes":{"term":"protection"}}]},{"title":"IEC 61131-3 Syntax Highlighting with highlight.js","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/iec-61131-syntax-highlighting.html","rel":"alternate"}},"published":"2020-09-01T23:00:00-07:00","updated":"2020-09-01T23:38:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-09-01:\/iec-61131-syntax-highlighting.html","summary":"<p class=\"first last\">Adding context to 61131 code snippets with <cite>highlight.js<\/cite>.<\/p>\n","content":"<p>I've been investigating some new resources for high-level documentation, including\nautomated presentation builders, and along the way, a question arose.<\/p>\n<p>If I want to demonstrate code examples for IEC 61131-3, is there a way that I can provide\nsyntax highlighting so that readers will be able to understand the material more clearly?<\/p>\n<p>So what is syntax highlighting anyway? Well, for code snippets, syntax highlighting uses\nvarious colors and fonts to isolate the unique keywords, operators, and other items that\nare standard in that particular programming language. I'll show an example here momentarily.\nThis is very useful because it allows readers to quickly interpret what the code is\nintentionally doing.<\/p>\n<p>I was fortunate enough that I was able to find a project that was already providing syntax\nhighlighting for IEC 61131-3 in the <cite>highlight.js<\/cite> project framework (though not natively)\nand thus, I could leverage existing work! Trouble is, since it's not already a native\n&quot;language&quot; it comes with its own set of challenges. The source of this highlighter comes\nfrom the <a class=\"reference external\" href=\"https:\/\/github.com\/highlightjs\/highlightjs-structured-text\">GitHub highlightjs Project<\/a><\/p>\n<p>My challenge was identifying an effective way of declaring the language so that I could\nuse it with <a class=\"reference external\" href=\"https:\/\/github.com\/marp-team\/marpit\">Marpit<\/a> which is part of the Marp project; a system built to convert\nmarkdown files to HTML or PPTX presentations. (Be on the lookout for an upcoming article\non this topic.)<\/p>\n<div class=\"section\" id=\"solution\">\n<h2>Solution<\/h2>\n<p>After much trial and tribulation, I finally realized a solution. Since I was using\n<cite>highlight.js<\/cite> as a required module in the marp framework, I could simply add the language\ndefinition and register it accordingly. Here are the steps I took to modify my installation\nto make it work as I wished.<\/p>\n<ol class=\"arabic\">\n<li><p class=\"first\">Locate existing highlight.js installation location by finding dependent module (in my\ncase marp). Then open directory (since I'm using Windows, I can use\n<cite>Explorer &lt;path\/to\/marp\/directory&gt;<\/cite><\/p>\n<img alt=\"Identify the install location.\" src=\"https:\/\/blog.stanleysolutionsnw.com\/cmd-view.png\" \/>\n<\/li>\n<li><p class=\"first\">Navigate to the directory containing the module of interest, then navigate to the\n<cite>node_modules\/highlight.js<\/cite> folder underneath the desired module. In my case, since\nI'm using marp-cli, I navigated to\n<cite>node_modules\/&#64;marp-team\/marp-cli\/node_modules\/highlight.js\/lib<\/cite><\/p>\n<img alt=\"Locate the `index.js` file for modification.\" src=\"https:\/\/blog.stanleysolutionsnw.com\/explorer-view.png\" \/>\n<\/li>\n<li><p class=\"first\">Open the <cite>index.js<\/cite> file in a text editor and add a new line to register the <cite>iecst<\/cite>\nlanguage.<\/p>\n<img alt=\"Registering the new language.\" src=\"https:\/\/blog.stanleysolutionsnw.com\/register-language.png\" \/>\n<\/li>\n<li><p class=\"first\">Finally, navigate to the languages folder and add the <cite>iecst.js<\/cite> file. Here, for my\napplication, I had to make some modifications (which I documented fully in an <a class=\"reference external\" href=\"https:\/\/github.com\/highlightjs\/highlightjs-structured-text\/issues\/9#issuecomment-685266264\">issue<\/a>\nand <a class=\"reference external\" href=\"https:\/\/github.com\/highlightjs\/highlightjs-structured-text\/pull\/10\">pull request<\/a> on the source repository).<\/p>\n<\/li>\n<\/ol>\n<\/div>\n<div class=\"section\" id=\"summary\">\n<h2>Summary<\/h2>\n<p>To make this long story longer, I'll be writing more later to document how my Marp\nintegration comes along. For the meantime, here's the takeaway:<\/p>\n<p>Syntax highlighting <em>does<\/em> exist for IEC 61131-3, and it'll become easier to implement\ngoing forward!<\/p>\n<p>Oh, and how about what that syntax-highlighted code? What does it look like anyway?<\/p>\n<p>Have a look for yourself!<\/p>\n<img alt=\"An example of (nonsense) syntax-highlighted 61131 code.\" src=\"https:\/\/blog.stanleysolutionsnw.com\/61131example.png\" \/>\n<\/div>\n","category":[{"@attributes":{"term":"iec-61131"}},{"@attributes":{"term":"iec-61131"}},{"@attributes":{"term":"documentation"}}]},{"title":"A Picture is Worth a Thousand Words","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/making-bitmap-images-from-bytes.html","rel":"alternate"}},"published":"2020-08-21T08:49:00-07:00","updated":"2020-09-01T11:42:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-08-21:\/making-bitmap-images-from-bytes.html","summary":"<p class=\"first last\">Making images and plotting might just be possible in real-time-controllers.<\/p>\n","content":"<p>I was introduced to a very nice gentleman about a year ago who was demonstrating some\nlogic that created a .PNG file to demonstrate fault currents easily, and could be\nattached in-line as part of an email body. What was so interesting to me is that this\nall part of some logic that originated on an <a class=\"reference external\" href=\"https:\/\/selinc.com\/products\/3530\/\">SEL RTAC<\/a> using some of the functionality\nin one of the many IEC 61131-3 libraries that are maintained by myself and the other\ndevelopers in the Automation Controllers Group.<\/p>\n<p>Now, the trouble with what this gentleman shared with me, is the reliance on an external\nserver to process the .CEV and <a class=\"reference external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Comtrade#:~:text=COMTRADE%20(Common%20format%20for%20Transient,to%20transient%20power%20system%20disturbances.\">COMTRADE<\/a> files generated by monitored protective relays.\nThat was all well and good, but I still wasn't quite satisfied since there was a certain\namount of reliance on some other device. I wanted something better.<\/p>\n<p>I suppose at this point, I should take a step back. For those who don't eat, sleep, and\nbreath power systems engineering and electrical power system protection, an &quot;event record&quot;\nis something of a novel idea. So what the h-e-double-hockey-sticks is an &quot;event record?&quot;<\/p>\n<p>I'm glad you've asked.<\/p>\n<p>Well, a fault on a power system (like when you run into a power pole and it knocks down\none of the wires - those are bad days) causes all sorts of &quot;bad behavior&quot; on the electrical\nsystem, and that bad behavior can be monitored and measured. It's what we use in the\ntraditional sense to detect faults in the first place and make a decision to open an\nelectrical breaker to shut off the power. Remember when you plugged in too many Christmas\nlights and the breaker popped? It's the same idea, just at a much higher degree of accuracy.<\/p>\n<p>So &quot;events&quot; have certain behavior, and engineers are often interested in characterizing\nthat behavior so that they can better understand not only what happened, but how it could\nbe prevented going forward. Back when Dr. Ed Schweitzer first invented the SEL-21 protective\nrelay (a conversation for another day), he added a very nifty little feature called &quot;event\nrecording.&quot; It added the ability to record information about the event when it occurred\nso that someone could go back and analyze it at a later time. These event records contain\na lot of information, and they require special software to open them, to interpret them,\netcetera. Here's an example of what an event record might look like. This was taken from\na technical paper written titled &quot;Numerical model framework of power quality events&quot; by\nRodney Tan and Vigna Ramachandaramurthy; the full paper's linked below, so take a look if\nyou're so inclined!<\/p>\n<img alt=\"An example power system fault &quot;Event Record&quot;.\" src=\"https:\/\/blog.stanleysolutionsnw.com\/power-system-fault.png\" \/>\n<p><a class=\"reference external\" href=\"https:\/\/www.researchgate.net\/publication\/290451701_Numerical_model_framework_of_power_quality_events\">Tan, Rodney &amp; Ramachandaramurthy, Vigna K.. (2010). Numerical model framework of power\nquality events. 43. 30-47.<\/a><\/p>\n<p>Soooo, the idea that this gentleman and his team had crafted was to extract the\nmost interesting portion of the information and plug that into an image that could be made\npart of an email. This way, a technician could easily look at the image and quickly make\ndecisions without having to load the full event record on a computer. This full analysis\ncould still be done later, but having the image available in the email might give an\nengineer the ability to make quick decisions to help restore power to homes and families\nmore quickly!<\/p>\n<p>I've recently completed some research where I've been able to effectively create a simple\nRGB image in the Bitmap format (.BMP file) so that it can be used and interpreted by most\nany computer available today. And should certainly allow for direct inclusion in emails.<\/p>\n<p>Ultimately, this means that there's a good chance that technicians could effectively see\nthe report of an event without any advanced software, simply by opening their email client\non their smart phone. Talk about convenience. I've been doing some development and testing\nand I found great value in this article from technical-recipes.com:<\/p>\n<p><a class=\"reference external\" href=\"https:\/\/www.technical-recipes.com\/2011\/creating-bitmap-files-from-raw-pixel-data-in-c\/\">https:\/\/www.technical-recipes.com\/2011\/creating-bitmap-files-from-raw-pixel-data-in-c\/<\/a><\/p>\n<p>I hope that I'll be sharing more as I fully flesh out some design where I can effectively\ncreate plots in a byte-array that can be stored as an image.<\/p>\n","category":[{"@attributes":{"term":"iec-61131"}},{"@attributes":{"term":"iec-61131"}},{"@attributes":{"term":"automation"}}]},{"title":"We're going live.","link":{"@attributes":{"href":"https:\/\/blog.stanleysolutionsnw.com\/we're-going-live.html","rel":"alternate"}},"published":"2020-08-21T08:35:00-07:00","updated":"2020-08-21T08:35:00-07:00","author":{"name":"Joe Stanley"},"id":"tag:blog.stanleysolutionsnw.com,2020-08-21:\/we're-going-live.html","summary":"<p class=\"first last\">Let the automation begin... Just stand back.<\/p>\n","content":"<p>I'm imagining the sound of an old-school electric motor grinding its way up to nominal\nspeed. Can you hear it? That low groan trying ever-so-hard to come up to a comfortable\nrange.<\/p>\n<p>I suppose I should step back and introduce myself. I'm Joe Stanley, at the time of writing,\nI'm twenty-three years old, and a graduate of the University of Idaho with both a Bachelors\nand Masters in Electrical Engineering. I work with Schweitzer Engineering Laboratories (SEL)\nin Pullman, Washington. I spend far too much time trying to automate things that would have\nbeen faster to do by hand; but I guess that's engineering. I'm an avid Pythonista, and I'm\nalways trying to &quot;convert&quot; others into the language. I also spend a great deal of time\ndeveloping and maintaining IEC 61131-3 libraries for SEL.<\/p>\n<p>I have far too many ideas and thoughts, so I think it's time to start sharing them. If it's\nsomewhere on the scale between sustainability and automation, I'm probably interested in it.<\/p>\n<p>My hope is that I might start sharing thoughts, ideas, projects, and guides here on this\nblog. Now, I'm sure you can hear that motor grinding up to speed!<\/p>\n","category":[{"@attributes":{"term":"automation"}},{"@attributes":{"term":"automation"}},{"@attributes":{"term":"python"}},{"@attributes":{"term":"iec-61131"}}]}]}